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:
parent
8b2d39c1ec
commit
300d94c628
8 changed files with 188 additions and 4 deletions
|
@ -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, []}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue