forked from AkkomaGang/akkoma
Enforce poll limits and add error handling for MastodonAPI's post endpoint
This commit is contained in:
parent
76a7429bef
commit
3f96b3e4b8
3 changed files with 173 additions and 48 deletions
|
@ -104,8 +104,23 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") 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
|
||||||
|
%{max_expiration: max_expiration, min_expiration: min_expiration} =
|
||||||
|
limits = Pleroma.Config.get([:instance, :poll_limits])
|
||||||
|
|
||||||
|
# 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, emoji} =
|
{poll, emoji} =
|
||||||
Enum.map_reduce(options, %{}, fn option, 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,
|
"name" => option,
|
||||||
"type" => "Note",
|
"type" => "Note",
|
||||||
|
@ -113,6 +128,17 @@ def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_i
|
||||||
}, Map.merge(emoji, Formatter.get_emoji_map(option))}
|
}, Map.merge(emoji, Formatter.get_emoji_map(option))}
|
||||||
end)
|
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_time =
|
end_time =
|
||||||
NaiveDateTime.utc_now()
|
NaiveDateTime.utc_now()
|
||||||
|> NaiveDateTime.add(expires_in)
|
|> NaiveDateTime.add(expires_in)
|
||||||
|
@ -126,6 +152,13 @@ def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_i
|
||||||
end
|
end
|
||||||
|
|
||||||
{poll, emoji}
|
{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 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) 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,16 +485,39 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
|
||||||
else
|
else
|
||||||
params = Map.drop(params, ["scheduled_at"])
|
params = Map.drop(params, ["scheduled_at"])
|
||||||
|
|
||||||
{:ok, activity} =
|
case get_cached_status_or_post(conn, params) do
|
||||||
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
|
{:ignore, message} ->
|
||||||
CommonAPI.post(user, params)
|
conn
|
||||||
end)
|
|> put_status(401)
|
||||||
|
|> json(%{error: message})
|
||||||
|
|
||||||
|
{:error, message} ->
|
||||||
|
conn
|
||||||
|
|> put_status(401)
|
||||||
|
|> json(%{error: message})
|
||||||
|
|
||||||
|
{_, activity} ->
|
||||||
conn
|
conn
|
||||||
|> put_view(StatusView)
|
|> put_view(StatusView)
|
||||||
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
|
|> 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
|
||||||
|
|
|
@ -146,6 +146,7 @@ test "posting a status", %{conn: conn} do
|
||||||
refute id == third_id
|
refute id == third_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "posting polls" do
|
||||||
test "posting a poll", %{conn: conn} do
|
test "posting a poll", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
time = NaiveDateTime.utc_now()
|
time = NaiveDateTime.utc_now()
|
||||||
|
@ -154,7 +155,7 @@ test "posting a poll", %{conn: conn} do
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user)
|
|> assign(:user, user)
|
||||||
|> post("/api/v1/statuses", %{
|
|> post("/api/v1/statuses", %{
|
||||||
"status" => "Who is the best girl?",
|
"status" => "Who is the #bestgrill?",
|
||||||
"poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
|
"poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -168,6 +169,80 @@ test "posting a poll", %{conn: conn} do
|
||||||
refute response["poll"]["expred"]
|
refute response["poll"]["expred"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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
|
test "posting a sensitive status", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue