akkoma/lib/pleroma/web/common_api/activity_draft.ex

292 lines
8.8 KiB
Elixir
Raw Normal View History

2019-09-24 09:10:54 +00:00
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
2019-09-24 09:10:54 +00:00
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Builder
2019-09-24 09:10:54 +00:00
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
import Pleroma.Web.Gettext
defstruct valid?: true,
errors: [],
user: nil,
params: %{},
status: nil,
summary: nil,
full_payload: nil,
attachments: [],
in_reply_to: nil,
in_reply_to_conversation: nil,
language: nil,
content_map: %{},
quote_id: nil,
quote: nil,
2019-09-24 09:10:54 +00:00
visibility: nil,
expires_at: nil,
2020-10-02 17:00:50 +00:00
extra: nil,
2019-09-24 09:10:54 +00:00
emoji: %{},
content_html: nil,
mentions: [],
tags: [],
to: [],
cc: [],
context: nil,
sensitive: false,
object: nil,
preview?: false,
changes: %{}
2024-04-23 21:12:39 +00:00
defp new(user, params) do
2019-09-24 09:10:54 +00:00
%__MODULE__{user: user}
|> put_params(params)
2020-10-02 17:00:50 +00:00
end
def create(user, params) do
user
|> new(params)
2019-09-24 09:10:54 +00:00
|> status()
|> summary()
|> with_valid(&attachments/1)
2019-09-24 09:10:54 +00:00
|> full_payload()
|> expires_at()
|> poll()
2019-09-23 11:52:41 +00:00
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
|> with_valid(&visibility/1)
|> with_valid(&quote_id/1)
2019-09-24 09:10:54 +00:00
|> content()
|> with_valid(&language/1)
2019-09-23 11:52:41 +00:00
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
2019-09-24 09:10:54 +00:00
|> sensitive()
2019-09-23 11:52:41 +00:00
|> with_valid(&object/1)
2019-09-24 09:10:54 +00:00
|> preview?()
2019-09-23 11:52:41 +00:00
|> with_valid(&changes/1)
2019-09-24 09:10:54 +00:00
|> validate()
end
defp put_params(draft, params) do
2020-05-12 19:59:26 +00:00
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | params: params}
end
2020-05-12 19:59:26 +00:00
defp status(%{params: %{status: status}} = draft) do
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | status: String.trim(status)}
end
defp summary(%{params: params} = draft) do
2020-05-12 19:59:26 +00:00
%__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
2019-09-24 09:10:54 +00:00
end
defp full_payload(%{status: status, summary: summary} = draft) do
full_payload = String.trim(status <> summary)
case Utils.validate_character_limit(full_payload, draft.attachments) do
:ok -> %__MODULE__{draft | full_payload: full_payload}
{:error, message} -> add_error(draft, message)
end
end
Restrict media usage to owners In Mastodon media can only be used by owners and only be associated with a single post. We currently allow media to be associated with several posts and until now did not limit their usage in posts to media owners. However, media update and GET lookup was already limited to owners. (In accordance with allowing media reuse, we also still allow GET lookups of media already used in a post unlike Mastodon) Allowing reuse isn’t problematic per se, but allowing use by non-owners can be problematic if media ids of private-scoped posts can be guessed since creating a new post with this media id will reveal the uploaded file content and alt text. Given media ids are currently just part of a sequentieal series shared with some other objects, guessing media ids is with some persistence indeed feasible. E.g. sampline some public media ids from a real-world instance with 112 total and 61 monthly-active users: 17.465.096 at t0 17.472.673 at t1 = t0 + 4h 17.473.248 at t2 = t1 + 20min This gives about 30 new ids per minute of which most won't be local media but remote and local posts, poll answers etc. Assuming the default ratelimit of 15 post actions per 10s, scraping all media for the 4h interval takes about 84 minutes and scraping the 20min range mere 6.3 minutes. (Until the preceding commit, post updates were not rate limited at all, allowing even faster scraping.) If an attacker can infer (e.g. via reply to a follower-only post not accessbile to the attacker) some sensitive information was uploaded during a specific time interval and has some pointers regarding the nature of the information, identifying the specific upload out of all scraped media for this timerange is not impossible. Thus restrict media usage to owners. Checking ownership just in ActivitDraft would already be sufficient, since when a scheduled status actually gets posted it goes through ActivityDraft again, but would erroneously return a success status when scheduling an illegal post. Independently discovered and fixed by mint in Pleroma https://git.pleroma.social/pleroma/pleroma/-/commit/1afde067b12ad0062c1820091ea9b0a680819281
2024-04-24 15:46:18 +00:00
defp attachments(%{params: params, user: user} = draft) do
case Utils.attachments_from_ids(user, params) do
attachments when is_list(attachments) ->
%__MODULE__{draft | attachments: attachments}
{:error, reason} ->
add_error(draft, reason)
end
2019-09-24 09:10:54 +00:00
end
2020-05-12 19:59:26 +00:00
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
2019-12-04 06:49:17 +00:00
2020-05-12 19:59:26 +00:00
defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
2019-12-04 06:49:17 +00:00
%__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
2019-09-24 09:10:54 +00:00
end
2020-05-12 19:59:26 +00:00
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
2019-12-04 06:49:17 +00:00
%__MODULE__{draft | in_reply_to: in_reply_to}
end
defp in_reply_to(draft), do: draft
2019-09-24 09:10:54 +00:00
defp in_reply_to_conversation(draft) do
2020-05-12 19:59:26 +00:00
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
end
defp quote_id(%{params: %{quote_id: ""}} = draft), do: draft
defp quote_id(%{params: %{quote_id: id}} = draft) when is_binary(id) do
with {:activity, %Activity{} = quote} <- {:activity, Activity.get_by_id(id)},
visibility <- CommonAPI.get_quoted_visibility(quote),
{:visibility, true} <- {:visibility, visibility in ["public", "unlisted"]} do
%__MODULE__{draft | quote: Activity.get_by_id(id)}
else
{:activity, _} ->
add_error(draft, dgettext("errors", "You can't quote a status that doesn't exist"))
{:visibility, false} ->
add_error(draft, dgettext("errors", "You can only quote public or unlisted statuses"))
end
end
defp quote_id(%{params: %{quote_id: %Activity{} = quote}} = draft) do
%__MODULE__{draft | quote: quote}
end
defp quote_id(draft), do: draft
defp language(%{params: %{language: language}, content_html: content} = draft)
when is_binary(language) do
if Pleroma.ISO639.valid_alpha2?(language) do
%__MODULE__{draft | content_map: %{language => content}}
else
add_error(draft, dgettext("errors", "Invalid language"))
end
end
2023-01-11 15:25:34 +00:00
defp language(%{content_html: content} = draft) do
# Use a default language if no language is specified
%__MODULE__{draft | content_map: %{"en" => content}}
end
2019-09-24 09:10:54 +00:00
defp visibility(%{params: params} = draft) do
case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
{visibility, "direct"} when visibility != "direct" ->
add_error(draft, dgettext("errors", "The message visibility must be direct"))
{visibility, _} ->
%__MODULE__{draft | visibility: visibility}
end
end
defp expires_at(draft) do
2020-05-12 19:59:26 +00:00
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
2019-09-24 09:10:54 +00:00
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message)
end
end
defp poll(draft) do
case Utils.make_poll_data(draft.params) do
{:ok, {poll, poll_emoji}} ->
2020-10-02 17:00:50 +00:00
%__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
2019-09-24 09:10:54 +00:00
{:error, message} ->
add_error(draft, message)
end
end
defp content(draft) do
2020-10-02 17:00:50 +00:00
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
2019-09-24 09:10:54 +00:00
2020-10-02 17:00:50 +00:00
mentions =
mentioned_users
2019-09-24 09:10:54 +00:00
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
2020-05-12 19:59:26 +00:00
|> Utils.get_addressed_users(draft.params[:to])
2019-09-24 09:10:54 +00:00
2020-10-02 17:00:50 +00:00
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
end
2019-09-24 09:10:54 +00:00
2020-10-02 17:00:50 +00:00
defp to_and_cc(draft) do
{to, cc} = Utils.get_to_and_cc(draft)
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | to: to, cc: cc}
end
defp context(draft) do
context = Utils.make_context(draft)
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | context: context}
end
defp sensitive(draft) do
sensitive = draft.params[:sensitive]
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | sensitive: sensitive}
end
defp object(draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
# Sometimes people create posts with subject containing emoji,
# since subjects are usually copied this will result in a broken
# subject when someone replies from an instance that does not have
# the emoji or has it under different shortcode. This is an attempt
# to mitigate this by copying emoji from inReplyTo if they are present
# in the subject.
summary_emoji =
with %Activity{} <- draft.in_reply_to,
%Object{data: %{"tag" => [_ | _] = tag}} <- Object.normalize(draft.in_reply_to) do
Enum.reduce(tag, %{}, fn
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}, acc ->
if String.contains?(draft.summary, name) do
Map.put(acc, name, url)
else
acc
end
_, acc ->
acc
end)
else
_ -> %{}
end
emoji = Map.merge(emoji, summary_emoji)
{:ok, note_data, _meta} = Builder.note(draft)
2022-06-14 15:25:28 +00:00
2019-09-24 09:10:54 +00:00
object =
note_data
2019-09-24 09:10:54 +00:00
|> Map.put("emoji", emoji)
2022-06-14 14:56:12 +00:00
|> Map.put("source", %{
"content" => draft.status,
"mediaType" => Utils.get_content_type(draft.params[:content_type])
2022-06-14 14:56:12 +00:00
})
|> Map.put("generator", draft.params[:generator])
|> Map.put("contentMap", draft.content_map)
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | object: object}
end
defp preview?(draft) do
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
2019-09-24 09:10:54 +00:00
%__MODULE__{draft | preview?: preview?}
end
defp changes(draft) do
direct? = draft.visibility == "direct"
additional = %{"cc" => draft.cc, "directMessage" => direct?}
additional =
case draft.expires_at do
2020-08-22 17:46:01 +00:00
%DateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
_ -> additional
end
2019-09-24 09:10:54 +00:00
changes =
%{
to: draft.to,
actor: draft.user,
context: draft.context,
object: draft.object,
additional: additional
2019-09-24 09:10:54 +00:00
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
%__MODULE__{draft | changes: changes}
end
2019-09-23 11:52:41 +00:00
defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
defp with_valid(draft, _func), do: draft
2019-09-24 09:10:54 +00:00
defp add_error(draft, message) do
%__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
end
defp validate(%{valid?: true} = draft), do: {:ok, draft}
defp validate(%{errors: [message | _]}), do: {:error, message}
end