Add poll votes

Also in this commit by accident:
- Fix query ordering causing exclude_poll_votes to not work
- Do not create notifications for Answer objects
This commit is contained in:
rinpatch 2019-06-01 16:07:01 +03:00
parent 8b2d39c1ec
commit 300d94c628
8 changed files with 188 additions and 4 deletions

View file

@ -127,10 +127,15 @@ def dismiss(%{id: user_id} = _user, id) do
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do when type in ["Create", "Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity) object = Object.normalize(activity)
unless object && object.data["type"] == "Answer" do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end) notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications} {:ok, notifications}
else
{:ok, []}
end
end end
def create_notifications(_), do: {:ok, []} def create_notifications(_), do: {:ok, []}

View file

@ -878,7 +878,6 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> maybe_preload_objects(opts) |> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts) |> maybe_preload_bookmarks(opts)
|> maybe_order(opts) |> maybe_order(opts)
|> exclude_poll_votes(opts)
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts) |> restrict_tag(opts)
|> restrict_tag_reject(opts) |> restrict_tag_reject(opts)
@ -899,6 +898,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(opts) |> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users() |> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
end end
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}) do

View file

@ -789,4 +789,21 @@ defp get_updated_targets(
[to, cc, recipients] [to, cc, recipients]
end end
end end
def get_existing_votes(actor, %{data: %{"id" => id}}) do
query =
from(
[activity, object: object] in Activity.with_preloaded_object(Activity),
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
where:
fragment(
"(?)->'inReplyTo' = ?",
object.data,
^to_string(id)
),
where: fragment("(?)->>'type' = 'Answer'", object.data)
)
Repo.all(query)
end
end end

View file

@ -119,6 +119,52 @@ def unfavorite(id_or_ap_id, user) do
end end
end end
def vote(user, object, choices) do
with "Question" <- object.data["type"],
{:author, false} <- {:author, object.data["actor"] == user.ap_id},
{:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
{options, max_count} <- get_options_and_max_count(object),
option_count <- Enum.count(options),
{:choice_check, {choices, true}} <-
{:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
{:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
end)
{:ok, answer_activities, object}
else
{:author, _} -> {:error, "Already voted"}
{:existing_votes, _} -> {:error, "Already voted"}
{:choice_check, {_, false}} -> {:error, "Invalid indices"}
{:count_check, false} -> {:error, "Too many choices"}
end
end
defp get_options_and_max_count(object) do
if Map.has_key?(object.data, "anyOf") do
{object.data["anyOf"], Enum.count(object.data["anyOf"])}
else
{object.data["oneOf"], 1}
end
end
defp normalize_and_validate_choice_indices(choices, count) do
Enum.map_reduce(choices, true, fn index, valid ->
index = if is_binary(index), do: String.to_integer(index), else: index
{index, if(valid, do: index < count, else: valid)}
end)
end
def get_visibility(%{"visibility" => visibility}, in_reply_to) def get_visibility(%{"visibility" => visibility}, in_reply_to)
when visibility in ~w{public unlisted private direct}, when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)} do: {visibility, get_replied_to_visibility(in_reply_to)}

View file

@ -491,4 +491,15 @@ def conversation_id_to_context(id) do
{:error, "No such conversation"} {:error, "No such conversation"}
end end
end end
def make_answer_data(%User{ap_id: ap_id}, object, name) do
%{
"type" => "Answer",
"actor" => ap_id,
"cc" => [object.data["actor"]],
"to" => [],
"name" => name,
"inReplyTo" => object.data["id"]
}
end
end end

View file

@ -430,6 +430,33 @@ def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
end end
def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
with %Object{} = object <- Object.get_by_id(id),
true <- object.data["type"] == "Question",
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
conn
|> put_view(StatusView)
|> try_render("poll.json", %{object: object, for: user})
else
nil ->
conn
|> put_status(404)
|> json(%{error: "Record not found"})
false ->
conn
|> put_status(404)
|> json(%{error: "Record not found"})
{:error, message} ->
conn
|> put_status(422)
|> json(%{error: message})
end
end
def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn conn

View file

@ -335,6 +335,8 @@ defmodule Pleroma.Web.Router do
put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
post("/polls/:id/votes", MastodonAPIController, :poll_vote)
post("/media", MastodonAPIController, :upload) post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media) put("/media/:id", MastodonAPIController, :update_media)

View file

@ -3497,4 +3497,80 @@ test "does not expose polls for private statuses", %{conn: conn} do
assert json_response(conn, 404) assert json_response(conn, 404)
end end
end end
describe "POST /api/v1/polls/:id/votes" do
test "votes are added to the poll", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "A very delicious sandwich",
"poll" => %{
"options" => ["Lettuce", "Grilled Bacon", "Tomato"],
"expires_in" => 20,
"multiple" => true
}
})
object = Object.normalize(activity)
conn =
conn
|> assign(:user, other_user)
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
assert json_response(conn, 200)
object = Object.get_by_id(object.id)
assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => totalItems}} ->
totalItems == 1
end)
end
test "author can't vote", %{conn: conn} do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Am I cute?",
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
})
object = Object.normalize(activity)
assert conn
|> assign(:user, user)
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
|> json_response(422) == %{"error" => "Already voted"}
object = Object.get_by_id(object.id)
refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
end
test "does not allow multiple choices on a single-choice question", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "The glass is",
"poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
})
object = Object.normalize(activity)
assert conn
|> assign(:user, other_user)
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
|> json_response(422) == %{"error" => "Too many choices"}
object = Object.get_by_id(object.id)
refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => totalItems}} ->
totalItems == 1
end)
end
end
end end