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) | |||
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 | |||
def create_notifications(_), do: {:ok, []} | |||
@@ -878,7 +878,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
|> maybe_preload_objects(opts) | |||
|> maybe_preload_bookmarks(opts) | |||
|> maybe_order(opts) | |||
|> exclude_poll_votes(opts) | |||
|> restrict_recipients(recipients, opts["user"]) | |||
|> restrict_tag(opts) | |||
|> restrict_tag_reject(opts) | |||
@@ -899,6 +898,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
|> restrict_pinned(opts) | |||
|> restrict_muted_reblogs(opts) | |||
|> Activity.restrict_deactivated_users() | |||
|> exclude_poll_votes(opts) | |||
end | |||
def fetch_activities(recipients, opts \\ %{}) do | |||
@@ -789,4 +789,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do | |||
[to, cc, recipients] | |||
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 |
@@ -119,6 +119,52 @@ defmodule Pleroma.Web.CommonAPI do | |||
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) | |||
when visibility in ~w{public unlisted private direct}, | |||
do: {visibility, get_replied_to_visibility(in_reply_to)} | |||
@@ -491,4 +491,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||
{:error, "No such conversation"} | |||
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 |
@@ -430,6 +430,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
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 | |||
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do | |||
conn | |||
@@ -335,6 +335,8 @@ defmodule Pleroma.Web.Router do | |||
put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) | |||
delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) | |||
post("/polls/:id/votes", MastodonAPIController, :poll_vote) | |||
post("/media", MastodonAPIController, :upload) | |||
put("/media/:id", MastodonAPIController, :update_media) | |||
@@ -3497,4 +3497,80 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
assert json_response(conn, 404) | |||
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 |