First
[anni] / lib / pleroma / web / common_api / activity_draft.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.CommonAPI.ActivityDraft do
6   alias Pleroma.Activity
7   alias Pleroma.Conversation.Participation
8   alias Pleroma.Object
9   alias Pleroma.Web.ActivityPub.Builder
10   alias Pleroma.Web.CommonAPI
11   alias Pleroma.Web.CommonAPI.Utils
12
13   import Pleroma.Web.Gettext
14
15   defstruct valid?: true,
16             errors: [],
17             user: nil,
18             params: %{},
19             status: nil,
20             summary: nil,
21             full_payload: nil,
22             attachments: [],
23             in_reply_to: nil,
24             in_reply_to_conversation: nil,
25             visibility: nil,
26             expires_at: nil,
27             extra: nil,
28             emoji: %{},
29             content_html: nil,
30             mentions: [],
31             tags: [],
32             to: [],
33             cc: [],
34             context: nil,
35             sensitive: false,
36             object: nil,
37             preview?: false,
38             changes: %{}
39
40   def new(user, params) do
41     %__MODULE__{user: user}
42     |> put_params(params)
43   end
44
45   def create(user, params) do
46     user
47     |> new(params)
48     |> status()
49     |> summary()
50     |> with_valid(&attachments/1)
51     |> full_payload()
52     |> expires_at()
53     |> poll()
54     |> with_valid(&in_reply_to/1)
55     |> with_valid(&in_reply_to_conversation/1)
56     |> with_valid(&visibility/1)
57     |> content()
58     |> with_valid(&to_and_cc/1)
59     |> with_valid(&context/1)
60     |> sensitive()
61     |> with_valid(&object/1)
62     |> preview?()
63     |> with_valid(&changes/1)
64     |> validate()
65   end
66
67   def listen(user, params) do
68     user
69     |> new(params)
70     |> visibility()
71     |> to_and_cc()
72     |> context()
73     |> listen_object()
74     |> with_valid(&changes/1)
75     |> validate()
76   end
77
78   defp listen_object(draft) do
79     object =
80       draft.params
81       |> Map.take([:album, :artist, :title, :length])
82       |> Map.new(fn {key, value} -> {to_string(key), value} end)
83       |> Map.put("type", "Audio")
84       |> Map.put("to", draft.to)
85       |> Map.put("cc", draft.cc)
86       |> Map.put("actor", draft.user.ap_id)
87
88     %__MODULE__{draft | object: object}
89   end
90
91   defp put_params(draft, params) do
92     params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
93     %__MODULE__{draft | params: params}
94   end
95
96   defp status(%{params: %{status: status}} = draft) do
97     %__MODULE__{draft | status: String.trim(status)}
98   end
99
100   defp summary(%{params: params} = draft) do
101     %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
102   end
103
104   defp full_payload(%{status: status, summary: summary} = draft) do
105     full_payload = String.trim(status <> summary)
106
107     case Utils.validate_character_limit(full_payload, draft.attachments) do
108       :ok -> %__MODULE__{draft | full_payload: full_payload}
109       {:error, message} -> add_error(draft, message)
110     end
111   end
112
113   defp attachments(%{params: params} = draft) do
114     attachments = Utils.attachments_from_ids(params)
115     draft = %__MODULE__{draft | attachments: attachments}
116
117     case Utils.validate_attachments_count(attachments) do
118       :ok -> draft
119       {:error, message} -> add_error(draft, message)
120     end
121   end
122
123   defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
124
125   defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
126     %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
127   end
128
129   defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
130     %__MODULE__{draft | in_reply_to: in_reply_to}
131   end
132
133   defp in_reply_to(draft), do: draft
134
135   defp in_reply_to_conversation(draft) do
136     in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
137     %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
138   end
139
140   defp visibility(%{params: params} = draft) do
141     case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
142       {visibility, "direct"} when visibility != "direct" ->
143         add_error(draft, dgettext("errors", "The message visibility must be direct"))
144
145       {visibility, _} ->
146         %__MODULE__{draft | visibility: visibility}
147     end
148   end
149
150   defp expires_at(draft) do
151     case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
152       {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
153       {:error, message} -> add_error(draft, message)
154     end
155   end
156
157   defp poll(draft) do
158     case Utils.make_poll_data(draft.params) do
159       {:ok, {poll, poll_emoji}} ->
160         %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
161
162       {:error, message} ->
163         add_error(draft, message)
164     end
165   end
166
167   defp content(draft) do
168     {content_html, mentioned_users, tags} = Utils.make_content_html(draft)
169
170     mentions =
171       mentioned_users
172       |> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
173       |> Utils.get_addressed_users(draft.params[:to])
174
175     %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
176   end
177
178   defp to_and_cc(draft) do
179     {to, cc} = Utils.get_to_and_cc(draft)
180     %__MODULE__{draft | to: to, cc: cc}
181   end
182
183   defp context(draft) do
184     context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
185     %__MODULE__{draft | context: context}
186   end
187
188   defp sensitive(draft) do
189     sensitive = draft.params[:sensitive]
190     %__MODULE__{draft | sensitive: sensitive}
191   end
192
193   defp object(draft) do
194     emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
195
196     # Sometimes people create posts with subject containing emoji,
197     # since subjects are usually copied this will result in a broken
198     # subject when someone replies from an instance that does not have
199     # the emoji or has it under different shortcode. This is an attempt
200     # to mitigate this by copying emoji from inReplyTo if they are present
201     # in the subject.
202     summary_emoji =
203       with %Activity{} <- draft.in_reply_to,
204            %Object{data: %{"tag" => [_ | _] = tag}} <- Object.normalize(draft.in_reply_to) do
205         Enum.reduce(tag, %{}, fn
206           %{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}, acc ->
207             if String.contains?(draft.summary, name) do
208               Map.put(acc, name, url)
209             else
210               acc
211             end
212
213           _, acc ->
214             acc
215         end)
216       else
217         _ -> %{}
218       end
219
220     emoji = Map.merge(emoji, summary_emoji)
221
222     {:ok, note_data, _meta} = Builder.note(draft)
223
224     object =
225       note_data
226       |> Map.put("emoji", emoji)
227       |> Map.put("source", %{
228         "content" => draft.status,
229         "mediaType" => Utils.get_content_type(draft.params[:content_type])
230       })
231       |> Map.put("generator", draft.params[:generator])
232
233     %__MODULE__{draft | object: object}
234   end
235
236   defp preview?(draft) do
237     preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
238     %__MODULE__{draft | preview?: preview?}
239   end
240
241   defp changes(draft) do
242     direct? = draft.visibility == "direct"
243     additional = %{"cc" => draft.cc, "directMessage" => direct?}
244
245     additional =
246       case draft.expires_at do
247         %DateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
248         _ -> additional
249       end
250
251     changes =
252       %{
253         to: draft.to,
254         actor: draft.user,
255         context: draft.context,
256         object: draft.object,
257         additional: additional
258       }
259       |> Utils.maybe_add_list_data(draft.user, draft.visibility)
260
261     %__MODULE__{draft | changes: changes}
262   end
263
264   defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
265   defp with_valid(draft, _func), do: draft
266
267   defp add_error(draft, message) do
268     %__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
269   end
270
271   defp validate(%{valid?: true} = draft), do: {:ok, draft}
272   defp validate(%{errors: [message | _]}), do: {:error, message}
273 end