ChatMessages: Add attachments.
[akkoma] / lib / pleroma / web / common_api / common_api.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.CommonAPI do
6 alias Pleroma.Activity
7 alias Pleroma.ActivityExpiration
8 alias Pleroma.Conversation.Participation
9 alias Pleroma.FollowingRelationship
10 alias Pleroma.Formatter
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.ThreadMute
14 alias Pleroma.User
15 alias Pleroma.UserRelationship
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Builder
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
21
22 import Pleroma.Web.Gettext
23 import Pleroma.Web.CommonAPI.Utils
24
25 require Pleroma.Constants
26 require Logger
27
28 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
29 with :ok <- validate_chat_content_length(content),
30 maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
31 {_, {:ok, chat_message_data, _meta}} <-
32 {:build_object,
33 Builder.chat_message(
34 user,
35 recipient.ap_id,
36 content |> Formatter.html_escape("text/plain"),
37 attachment: maybe_attachment
38 )},
39 {_, {:ok, create_activity_data, _meta}} <-
40 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
41 {_, {:ok, %Activity{} = activity, _meta}} <-
42 {:common_pipeline,
43 Pipeline.common_pipeline(create_activity_data,
44 local: true
45 )} do
46 {:ok, activity}
47 end
48 end
49
50 defp validate_chat_content_length(content) do
51 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
52 :ok
53 else
54 {:error, :content_too_long}
55 end
56 end
57
58 def follow(follower, followed) do
59 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
60
61 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
62 {:ok, activity} <- ActivityPub.follow(follower, followed),
63 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
64 {:ok, follower, followed, activity}
65 end
66 end
67
68 def unfollow(follower, unfollowed) do
69 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
70 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
71 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
72 {:ok, follower}
73 end
74 end
75
76 def accept_follow_request(follower, followed) do
77 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
78 {:ok, follower} <- User.follow(follower, followed),
79 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
80 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
81 {:ok, _activity} <-
82 ActivityPub.accept(%{
83 to: [follower.ap_id],
84 actor: followed,
85 object: follow_activity.data["id"],
86 type: "Accept"
87 }) do
88 {:ok, follower}
89 end
90 end
91
92 def reject_follow_request(follower, followed) do
93 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
94 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
95 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
96 {:ok, _notifications} <- Notification.dismiss(follow_activity),
97 {:ok, _activity} <-
98 ActivityPub.reject(%{
99 to: [follower.ap_id],
100 actor: followed,
101 object: follow_activity.data["id"],
102 type: "Reject"
103 }) do
104 {:ok, follower}
105 end
106 end
107
108 def delete(activity_id, user) do
109 with {_, %Activity{data: %{"object" => _}} = activity} <-
110 {:find_activity, Activity.get_by_id_with_object(activity_id)},
111 %Object{} = object <- Object.normalize(activity),
112 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
113 {:ok, _} <- unpin(activity_id, user),
114 {:ok, delete} <- ActivityPub.delete(object) do
115 {:ok, delete}
116 else
117 {:find_activity, _} -> {:error, :not_found}
118 _ -> {:error, dgettext("errors", "Could not delete")}
119 end
120 end
121
122 def repeat(id, user, params \\ %{}) do
123 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
124 {:find_activity, Activity.get_by_id(id)},
125 object <- Object.normalize(activity),
126 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
127 public <- public_announce?(object, params) do
128 if announce_activity do
129 {:ok, announce_activity, object}
130 else
131 ActivityPub.announce(user, object, nil, true, public)
132 end
133 else
134 {:find_activity, _} -> {:error, :not_found}
135 _ -> {:error, dgettext("errors", "Could not repeat")}
136 end
137 end
138
139 def unrepeat(id, user) do
140 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
141 {:find_activity, Activity.get_by_id(id)} do
142 object = Object.normalize(activity)
143 ActivityPub.unannounce(user, object)
144 else
145 {:find_activity, _} -> {:error, :not_found}
146 _ -> {:error, dgettext("errors", "Could not unrepeat")}
147 end
148 end
149
150 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
151 def favorite(%User{} = user, id) do
152 case favorite_helper(user, id) do
153 {:ok, _} = res ->
154 res
155
156 {:error, :not_found} = res ->
157 res
158
159 {:error, e} ->
160 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
161 {:error, dgettext("errors", "Could not favorite")}
162 end
163 end
164
165 def favorite_helper(user, id) do
166 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
167 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
168 {_, {:ok, %Activity{} = activity, _meta}} <-
169 {:common_pipeline,
170 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
171 {:ok, activity}
172 else
173 {:find_object, _} ->
174 {:error, :not_found}
175
176 {:common_pipeline,
177 {
178 :error,
179 {
180 :validate_object,
181 {
182 :error,
183 changeset
184 }
185 }
186 }} = e ->
187 if {:object, {"already liked by this actor", []}} in changeset.errors do
188 {:ok, :already_liked}
189 else
190 {:error, e}
191 end
192
193 e ->
194 {:error, e}
195 end
196 end
197
198 def unfavorite(id, user) do
199 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
200 {:find_activity, Activity.get_by_id(id)} do
201 object = Object.normalize(activity)
202 ActivityPub.unlike(user, object)
203 else
204 {:find_activity, _} -> {:error, :not_found}
205 _ -> {:error, dgettext("errors", "Could not unfavorite")}
206 end
207 end
208
209 def react_with_emoji(id, user, emoji) do
210 with %Activity{} = activity <- Activity.get_by_id(id),
211 object <- Object.normalize(activity) do
212 ActivityPub.react_with_emoji(user, object, emoji)
213 else
214 _ ->
215 {:error, dgettext("errors", "Could not add reaction emoji")}
216 end
217 end
218
219 def unreact_with_emoji(id, user, emoji) do
220 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
221 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
222 else
223 _ ->
224 {:error, dgettext("errors", "Could not remove reaction emoji")}
225 end
226 end
227
228 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
229 with :ok <- validate_not_author(object, user),
230 :ok <- validate_existing_votes(user, object),
231 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
232 answer_activities =
233 Enum.map(choices, fn index ->
234 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
235
236 {:ok, activity} =
237 ActivityPub.create(%{
238 to: answer_data["to"],
239 actor: user,
240 context: object.data["context"],
241 object: answer_data,
242 additional: %{"cc" => answer_data["cc"]}
243 })
244
245 activity
246 end)
247
248 object = Object.get_cached_by_ap_id(object.data["id"])
249 {:ok, answer_activities, object}
250 end
251 end
252
253 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
254 do: {:error, dgettext("errors", "Poll's author can't vote")}
255
256 defp validate_not_author(_, _), do: :ok
257
258 defp validate_existing_votes(%{ap_id: ap_id}, object) do
259 if Utils.get_existing_votes(ap_id, object) == [] do
260 :ok
261 else
262 {:error, dgettext("errors", "Already voted")}
263 end
264 end
265
266 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
267 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
268
269 defp normalize_and_validate_choices(choices, object) do
270 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
271 {options, max_count} = get_options_and_max_count(object)
272 count = Enum.count(options)
273
274 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
275 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
276 {:ok, options, choices}
277 else
278 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
279 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
280 end
281 end
282
283 def public_announce?(_, %{"visibility" => visibility})
284 when visibility in ~w{public unlisted private direct},
285 do: visibility in ~w(public unlisted)
286
287 def public_announce?(object, _) do
288 Visibility.is_public?(object)
289 end
290
291 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
292
293 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
294 when visibility in ~w{public unlisted private direct},
295 do: {visibility, get_replied_to_visibility(in_reply_to)}
296
297 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
298 visibility = {:list, String.to_integer(list_id)}
299 {visibility, get_replied_to_visibility(in_reply_to)}
300 end
301
302 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
303 visibility = get_replied_to_visibility(in_reply_to)
304 {visibility, visibility}
305 end
306
307 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
308
309 def get_replied_to_visibility(nil), do: nil
310
311 def get_replied_to_visibility(activity) do
312 with %Object{} = object <- Object.normalize(activity) do
313 Visibility.get_visibility(object)
314 end
315 end
316
317 def check_expiry_date({:ok, nil} = res), do: res
318
319 def check_expiry_date({:ok, in_seconds}) do
320 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
321
322 if ActivityExpiration.expires_late_enough?(expiry) do
323 {:ok, expiry}
324 else
325 {:error, "Expiry date is too soon"}
326 end
327 end
328
329 def check_expiry_date(expiry_str) do
330 Ecto.Type.cast(:integer, expiry_str)
331 |> check_expiry_date()
332 end
333
334 def listen(user, %{"title" => _} = data) do
335 with visibility <- data["visibility"] || "public",
336 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
337 listen_data <-
338 Map.take(data, ["album", "artist", "title", "length"])
339 |> Map.put("type", "Audio")
340 |> Map.put("to", to)
341 |> Map.put("cc", cc)
342 |> Map.put("actor", user.ap_id),
343 {:ok, activity} <-
344 ActivityPub.listen(%{
345 actor: user,
346 to: to,
347 object: listen_data,
348 context: Utils.generate_context_id(),
349 additional: %{"cc" => cc}
350 }) do
351 {:ok, activity}
352 end
353 end
354
355 def post(user, %{"status" => _} = data) do
356 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
357 draft.changes
358 |> ActivityPub.create(draft.preview?)
359 |> maybe_create_activity_expiration(draft.expires_at)
360 end
361 end
362
363 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
364 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
365 {:ok, activity}
366 end
367 end
368
369 defp maybe_create_activity_expiration(result, _), do: result
370
371 def pin(id, %{ap_id: user_ap_id} = user) do
372 with %Activity{
373 actor: ^user_ap_id,
374 data: %{"type" => "Create"},
375 object: %Object{data: %{"type" => object_type}}
376 } = activity <- Activity.get_by_id_with_object(id),
377 true <- object_type in ["Note", "Article", "Question"],
378 true <- Visibility.is_public?(activity),
379 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
380 {:ok, activity}
381 else
382 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
383 _ -> {:error, dgettext("errors", "Could not pin")}
384 end
385 end
386
387 def unpin(id, user) do
388 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
389 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
390 {:ok, activity}
391 else
392 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
393 _ -> {:error, dgettext("errors", "Could not unpin")}
394 end
395 end
396
397 def add_mute(user, activity) do
398 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
399 {:ok, activity}
400 else
401 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
402 end
403 end
404
405 def remove_mute(user, activity) do
406 ThreadMute.remove_mute(user.id, activity.data["context"])
407 {:ok, activity}
408 end
409
410 def thread_muted?(%{id: nil} = _user, _activity), do: false
411
412 def thread_muted?(user, activity) do
413 ThreadMute.exists?(user.id, activity.data["context"])
414 end
415
416 def report(user, data) do
417 with {:ok, account} <- get_reported_account(data.account_id),
418 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
419 {:ok, statuses} <- get_report_statuses(account, data) do
420 ActivityPub.flag(%{
421 context: Utils.generate_context_id(),
422 actor: user,
423 account: account,
424 statuses: statuses,
425 content: content_html,
426 forward: Map.get(data, :forward, false)
427 })
428 end
429 end
430
431 defp get_reported_account(account_id) do
432 case User.get_cached_by_id(account_id) do
433 %User{} = account -> {:ok, account}
434 _ -> {:error, dgettext("errors", "Account not found")}
435 end
436 end
437
438 def update_report_state(activity_ids, state) when is_list(activity_ids) do
439 case Utils.update_report_state(activity_ids, state) do
440 :ok -> {:ok, activity_ids}
441 _ -> {:error, dgettext("errors", "Could not update state")}
442 end
443 end
444
445 def update_report_state(activity_id, state) do
446 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
447 Utils.update_report_state(activity, state)
448 else
449 nil -> {:error, :not_found}
450 _ -> {:error, dgettext("errors", "Could not update state")}
451 end
452 end
453
454 def update_activity_scope(activity_id, opts \\ %{}) do
455 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
456 {:ok, activity} <- toggle_sensitive(activity, opts) do
457 set_visibility(activity, opts)
458 else
459 nil -> {:error, :not_found}
460 {:error, reason} -> {:error, reason}
461 end
462 end
463
464 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
465 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
466 end
467
468 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
469 when is_boolean(sensitive) do
470 new_data = Map.put(object.data, "sensitive", sensitive)
471
472 {:ok, object} =
473 object
474 |> Object.change(%{data: new_data})
475 |> Object.update_and_set_cache()
476
477 {:ok, Map.put(activity, :object, object)}
478 end
479
480 defp toggle_sensitive(activity, _), do: {:ok, activity}
481
482 defp set_visibility(activity, %{"visibility" => visibility}) do
483 Utils.update_activity_visibility(activity, visibility)
484 end
485
486 defp set_visibility(activity, _), do: {:ok, activity}
487
488 def hide_reblogs(%User{} = user, %User{} = target) do
489 UserRelationship.create_reblog_mute(user, target)
490 end
491
492 def show_reblogs(%User{} = user, %User{} = target) do
493 UserRelationship.delete_reblog_mute(user, target)
494 end
495 end