@@ -104,28 +104,61 @@ defmodule Pleroma.Web.CommonAPI.Utils do | |||
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) | |||
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 | |||
{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 | |||
def make_poll_data(_data) do | |||
@@ -473,12 +473,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
params | |||
|> 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"] | |||
if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do | |||
@@ -491,17 +485,40 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
else | |||
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 | |||
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 | |||
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do | |||
json(conn, %{}) | |||
@@ -146,26 +146,101 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
refute id == third_id | |||
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 | |||
test "posting a sensitive status", %{conn: conn} do | |||