@@ -104,28 +104,61 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||||
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) | def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) | ||||
when is_list(options) and is_integer(expires_in) do | when is_list(options) and is_integer(expires_in) do | ||||
{poll, emoji} = | |||||
Enum.map_reduce(options, %{}, fn option, emoji -> | |||||
{%{ | |||||
"name" => option, | |||||
"type" => "Note", | |||||
"replies" => %{"type" => "Collection", "totalItems" => 0} | |||||
}, Map.merge(emoji, Formatter.get_emoji_map(option))} | |||||
end) | |||||
%{max_expiration: max_expiration, min_expiration: min_expiration} = | |||||
limits = Pleroma.Config.get([:instance, :poll_limits]) | |||||
end_time = | |||||
NaiveDateTime.utc_now() | |||||
|> NaiveDateTime.add(expires_in) | |||||
|> NaiveDateTime.to_iso8601() | |||||
# XXX: There is probably a cleaner way of doing this | |||||
try do | |||||
if Enum.count(options) > limits.max_options do | |||||
raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" | |||||
end | |||||
poll = | |||||
if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do | |||||
%{"type" => "Question", "anyOf" => poll, "closed" => end_time} | |||||
else | |||||
%{"type" => "Question", "oneOf" => poll, "closed" => end_time} | |||||
{poll, emoji} = | |||||
Enum.map_reduce(options, %{}, fn option, emoji -> | |||||
if String.length(option) > limits.max_option_chars do | |||||
raise ArgumentError, | |||||
message: | |||||
"Poll options cannot be longer than #{limits.max_option_chars} characters each" | |||||
end | |||||
{%{ | |||||
"name" => option, | |||||
"type" => "Note", | |||||
"replies" => %{"type" => "Collection", "totalItems" => 0} | |||||
}, Map.merge(emoji, Formatter.get_emoji_map(option))} | |||||
end) | |||||
case expires_in do | |||||
expires_in when expires_in > max_expiration -> | |||||
raise ArgumentError, message: "Expiration date is too far in the future" | |||||
expires_in when expires_in < min_expiration -> | |||||
raise ArgumentError, message: "Expiration date is too soon" | |||||
_ -> | |||||
:noop | |||||
end | end | ||||
{poll, emoji} | |||||
end_time = | |||||
NaiveDateTime.utc_now() | |||||
|> NaiveDateTime.add(expires_in) | |||||
|> NaiveDateTime.to_iso8601() | |||||
poll = | |||||
if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do | |||||
%{"type" => "Question", "anyOf" => poll, "closed" => end_time} | |||||
else | |||||
%{"type" => "Question", "oneOf" => poll, "closed" => end_time} | |||||
end | |||||
{poll, emoji} | |||||
rescue | |||||
e in ArgumentError -> e.message | |||||
end | |||||
end | |||||
def make_poll_data(%{"poll" => _}) do | |||||
"Invalid poll" | |||||
end | end | ||||
def make_poll_data(_data) do | def make_poll_data(_data) do | ||||
@@ -473,12 +473,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||||
params | params | ||||
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) | |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) | ||||
idempotency_key = | |||||
case get_req_header(conn, "idempotency-key") do | |||||
[key] -> key | |||||
_ -> Ecto.UUID.generate() | |||||
end | |||||
scheduled_at = params["scheduled_at"] | scheduled_at = params["scheduled_at"] | ||||
if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do | if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do | ||||
@@ -491,17 +485,40 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||||
else | else | ||||
params = Map.drop(params, ["scheduled_at"]) | params = Map.drop(params, ["scheduled_at"]) | ||||
{:ok, activity} = | |||||
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> | |||||
CommonAPI.post(user, params) | |||||
end) | |||||
conn | |||||
|> put_view(StatusView) | |||||
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) | |||||
case get_cached_status_or_post(conn, params) do | |||||
{:ignore, message} -> | |||||
conn | |||||
|> put_status(401) | |||||
|> json(%{error: message}) | |||||
{:error, message} -> | |||||
conn | |||||
|> put_status(401) | |||||
|> json(%{error: message}) | |||||
{_, activity} -> | |||||
conn | |||||
|> put_view(StatusView) | |||||
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) | |||||
end | |||||
end | end | ||||
end | end | ||||
defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do | |||||
idempotency_key = | |||||
case get_req_header(conn, "idempotency-key") do | |||||
[key] -> key | |||||
_ -> Ecto.UUID.generate() | |||||
end | |||||
Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> | |||||
case CommonAPI.post(user, params) do | |||||
{:ok, activity} -> activity | |||||
{:error, message} -> {:ignore, message} | |||||
end | |||||
end) | |||||
end | |||||
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do | def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do | ||||
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do | with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do | ||||
json(conn, %{}) | json(conn, %{}) | ||||
@@ -146,26 +146,101 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||||
refute id == third_id | refute id == third_id | ||||
end | end | ||||
test "posting a poll", %{conn: conn} do | |||||
user = insert(:user) | |||||
time = NaiveDateTime.utc_now() | |||||
describe "posting polls" do | |||||
test "posting a poll", %{conn: conn} do | |||||
user = insert(:user) | |||||
time = NaiveDateTime.utc_now() | |||||
conn = | |||||
conn | |||||
|> assign(:user, user) | |||||
|> post("/api/v1/statuses", %{ | |||||
"status" => "Who is the best girl?", | |||||
"poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} | |||||
}) | |||||
conn = | |||||
conn | |||||
|> assign(:user, user) | |||||
|> post("/api/v1/statuses", %{ | |||||
"status" => "Who is the #bestgrill?", | |||||
"poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} | |||||
}) | |||||
response = json_response(conn, 200) | |||||
response = json_response(conn, 200) | |||||
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> | |||||
title in ["Rei", "Asuka", "Misato"] | |||||
end) | |||||
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> | |||||
title in ["Rei", "Asuka", "Misato"] | |||||
end) | |||||
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 | |||||
refute response["poll"]["expred"] | |||||
end | |||||
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 | |||||
refute response["poll"]["expred"] | |||||
test "option limit is enforced", %{conn: conn} do | |||||
user = insert(:user) | |||||
limit = Pleroma.Config.get([:instance, :poll_limits, :max_options]) | |||||
conn = | |||||
conn | |||||
|> assign(:user, user) | |||||
|> post("/api/v1/statuses", %{ | |||||
"status" => "desu~", | |||||
"poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} | |||||
}) | |||||
%{"error" => error} = json_response(conn, 401) | |||||
assert error == "Poll can't contain more than #{limit} options" | |||||
end | |||||
test "option character limit is enforced", %{conn: conn} do | |||||
user = insert(:user) | |||||
limit = Pleroma.Config.get([:instance, :poll_limits, :max_option_chars]) | |||||
conn = | |||||
conn | |||||
|> assign(:user, user) | |||||
|> post("/api/v1/statuses", %{ | |||||
"status" => "...", | |||||
"poll" => %{ | |||||
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], | |||||
"expires_in" => 1 | |||||
} | |||||
}) | |||||
%{"error" => error} = json_response(conn, 401) | |||||
assert error == "Poll options cannot be longer than #{limit} characters each" | |||||
end | |||||
test "minimal date limit is enforced", %{conn: conn} do | |||||
user = insert(:user) | |||||
limit = Pleroma.Config.get([:instance, :poll_limits, :min_expiration]) | |||||
conn = | |||||
conn | |||||
|> assign(:user, user) | |||||
|> post("/api/v1/statuses", %{ | |||||
"status" => "imagine arbitrary limits", | |||||
"poll" => %{ | |||||
"options" => ["this post was made by pleroma gang"], | |||||
"expires_in" => limit - 1 | |||||
} | |||||
}) | |||||
%{"error" => error} = json_response(conn, 401) | |||||
assert error == "Expiration date is too soon" | |||||
end | |||||
test "maximum date limit is enforced", %{conn: conn} do | |||||
user = insert(:user) | |||||
limit = Pleroma.Config.get([:instance, :poll_limits, :max_expiration]) | |||||
conn = | |||||
conn | |||||
|> assign(:user, user) | |||||
|> post("/api/v1/statuses", %{ | |||||
"status" => "imagine arbitrary limits", | |||||
"poll" => %{ | |||||
"options" => ["this post was made by pleroma gang"], | |||||
"expires_in" => limit + 1 | |||||
} | |||||
}) | |||||
%{"error" => error} = json_response(conn, 401) | |||||
assert error == "Expiration date is too far in the future" | |||||
end | |||||
end | end | ||||
test "posting a sensitive status", %{conn: conn} do | test "posting a sensitive status", %{conn: conn} do | ||||