Also in this commit by accident: - Fix query ordering causing exclude_poll_votes to not work - Do not create notifications for Answer objectstags/v1.1.4
@@ -127,10 +127,15 @@ defmodule Pleroma.Notification 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) | |||||
notifications = Enum.map(users, fn user -> create_notification(activity, user) end) | |||||
{:ok, notifications} | |||||
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) | |||||
{:ok, notifications} | |||||
else | |||||
{:ok, []} | |||||
end | |||||
end | end | ||||
def create_notifications(_), do: {:ok, []} | def create_notifications(_), do: {:ok, []} | ||||
@@ -878,7 +878,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||||
[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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest 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 |