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
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
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, %{})
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