95ac7b71a8a5394a0cf42b5d70f94b3d071b76c3
[akkoma] / lib / pleroma / web / common_api.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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.Conversation.Participation
8 alias Pleroma.Formatter
9 alias Pleroma.Object
10 alias Pleroma.ThreadMute
11 alias Pleroma.User
12 alias Pleroma.UserRelationship
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.Builder
15 alias Pleroma.Web.ActivityPub.Pipeline
16 alias Pleroma.Web.ActivityPub.Utils
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.CommonAPI.ActivityDraft
19 alias Pleroma.Elasticsearch
20 alias Pleroma.Config
21
22 import Pleroma.Web.Gettext
23 import Pleroma.Web.CommonAPI.Utils
24
25 require Pleroma.Constants
26 require Logger
27
28 def block(blocker, blocked) do
29 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
30 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
31 {:ok, block}
32 end
33 end
34
35 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
36 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
37 :ok <- validate_chat_content_length(content, !!maybe_attachment),
38 {_, {:ok, chat_message_data, _meta}} <-
39 {:build_object,
40 Builder.chat_message(
41 user,
42 recipient.ap_id,
43 content |> format_chat_content,
44 attachment: maybe_attachment
45 )},
46 {_, {:ok, create_activity_data, _meta}} <-
47 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
48 {_, {:ok, %Activity{} = activity, _meta}} <-
49 {:common_pipeline,
50 Pipeline.common_pipeline(create_activity_data,
51 local: true,
52 idempotency_key: opts[:idempotency_key]
53 )} do
54 {:ok, activity}
55 else
56 {:common_pipeline, {:reject, _} = e} -> e
57 e -> e
58 end
59 end
60
61 defp format_chat_content(nil), do: nil
62
63 defp format_chat_content(content) do
64 {text, _, _} =
65 content
66 |> Formatter.html_escape("text/plain")
67 |> Formatter.linkify()
68 |> (fn {text, mentions, tags} ->
69 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
70 end).()
71
72 text
73 end
74
75 defp validate_chat_content_length(_, true), do: :ok
76 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
77
78 defp validate_chat_content_length(content, _) do
79 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
80 :ok
81 else
82 {:error, :content_too_long}
83 end
84 end
85
86 def unblock(blocker, blocked) do
87 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
88 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
89 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
90 {:ok, unblock}
91 else
92 {:fetch_block, nil} ->
93 if User.blocks?(blocker, blocked) do
94 User.unblock(blocker, blocked)
95 {:ok, :no_activity}
96 else
97 {:error, :not_blocking}
98 end
99
100 e ->
101 e
102 end
103 end
104
105 def follow(follower, followed) do
106 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
107
108 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
109 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
110 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
111 if activity.data["state"] == "reject" do
112 {:error, :rejected}
113 else
114 {:ok, follower, followed, activity}
115 end
116 end
117 end
118
119 def unfollow(follower, unfollowed) do
120 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
121 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
122 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
123 {:ok, follower}
124 end
125 end
126
127 def accept_follow_request(follower, followed) do
128 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
129 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
130 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
131 {:ok, follower}
132 end
133 end
134
135 def reject_follow_request(follower, followed) do
136 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
137 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
138 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
139 {:ok, follower}
140 end
141 end
142
143 def delete(activity_id, user) do
144 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
145 {:find_activity, Activity.get_by_id(activity_id)},
146 {_, %Object{} = object, _} <-
147 {:find_object, Object.normalize(activity, fetch: false), activity},
148 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
149 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
150 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
151 {:ok, delete}
152 else
153 {:find_activity, _} ->
154 {:error, :not_found}
155
156 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
157 # We have the create activity, but not the object, it was probably pruned.
158 # Insert a tombstone and try again
159 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
160 {:ok, _tombstone} <- Object.create(tombstone_data) do
161 delete(activity_id, user)
162 else
163 _ ->
164 Logger.error(
165 "Could not insert tombstone for missing object on deletion. Object is #{object}."
166 )
167
168 {:error, dgettext("errors", "Could not delete")}
169 end
170
171 _ ->
172 {:error, dgettext("errors", "Could not delete")}
173 end
174 end
175
176 def repeat(id, user, params \\ %{}) do
177 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
178 object = %Object{} <- Object.normalize(activity, fetch: false),
179 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
180 public = public_announce?(object, params),
181 {:ok, announce, _} <- Builder.announce(user, object, public: public),
182 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
183 {:ok, activity}
184 else
185 {:existing_announce, %Activity{} = announce} ->
186 {:ok, announce}
187
188 _ ->
189 {:error, :not_found}
190 end
191 end
192
193 def unrepeat(id, user) do
194 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
195 {:find_activity, Activity.get_by_id(id)},
196 %Object{} = note <- Object.normalize(activity, fetch: false),
197 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
198 {:ok, undo, _} <- Builder.undo(user, announce),
199 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
200 {:ok, activity}
201 else
202 {:find_activity, _} -> {:error, :not_found}
203 _ -> {:error, dgettext("errors", "Could not unrepeat")}
204 end
205 end
206
207 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
208 def favorite(%User{} = user, id) do
209 case favorite_helper(user, id) do
210 {:ok, _} = res ->
211 res
212
213 {:error, :not_found} = res ->
214 res
215
216 {:error, e} ->
217 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
218 {:error, dgettext("errors", "Could not favorite")}
219 end
220 end
221
222 def favorite_helper(user, id) do
223 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
224 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
225 {_, {:ok, %Activity{} = activity, _meta}} <-
226 {:common_pipeline,
227 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
228 {:ok, activity}
229 else
230 {:find_object, _} ->
231 {:error, :not_found}
232
233 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
234 if {:object, {"already liked by this actor", []}} in changeset.errors do
235 {:ok, :already_liked}
236 else
237 {:error, e}
238 end
239
240 e ->
241 {:error, e}
242 end
243 end
244
245 def unfavorite(id, user) do
246 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
247 {:find_activity, Activity.get_by_id(id)},
248 %Object{} = note <- Object.normalize(activity, fetch: false),
249 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
250 {:ok, undo, _} <- Builder.undo(user, like),
251 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
252 {:ok, activity}
253 else
254 {:find_activity, _} -> {:error, :not_found}
255 _ -> {:error, dgettext("errors", "Could not unfavorite")}
256 end
257 end
258
259 def react_with_emoji(id, user, emoji) do
260 with %Activity{} = activity <- Activity.get_by_id(id),
261 object <- Object.normalize(activity, fetch: false),
262 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
263 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
264 {:ok, activity}
265 else
266 _ ->
267 {:error, dgettext("errors", "Could not add reaction emoji")}
268 end
269 end
270
271 def unreact_with_emoji(id, user, emoji) do
272 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
273 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
274 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
275 {:ok, activity}
276 else
277 _ ->
278 {:error, dgettext("errors", "Could not remove reaction emoji")}
279 end
280 end
281
282 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
283 with :ok <- validate_not_author(object, user),
284 :ok <- validate_existing_votes(user, object),
285 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
286 answer_activities =
287 Enum.map(choices, fn index ->
288 {:ok, answer_object, _meta} =
289 Builder.answer(user, object, Enum.at(options, index)["name"])
290
291 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
292
293 {:ok, activity, _meta} =
294 activity_data
295 |> Map.put("cc", answer_object["cc"])
296 |> Map.put("context", answer_object["context"])
297 |> Pipeline.common_pipeline(local: true)
298
299 # TODO: Do preload of Pleroma.Object in Pipeline
300 Activity.normalize(activity.data)
301 end)
302
303 object = Object.get_cached_by_ap_id(object.data["id"])
304 {:ok, answer_activities, object}
305 end
306 end
307
308 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
309 do: {:error, dgettext("errors", "Poll's author can't vote")}
310
311 defp validate_not_author(_, _), do: :ok
312
313 defp validate_existing_votes(%{ap_id: ap_id}, object) do
314 if Utils.get_existing_votes(ap_id, object) == [] do
315 :ok
316 else
317 {:error, dgettext("errors", "Already voted")}
318 end
319 end
320
321 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
322 when is_list(any_of) and any_of != [],
323 do: {any_of, Enum.count(any_of)}
324
325 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
326 when is_list(one_of) and one_of != [],
327 do: {one_of, 1}
328
329 defp normalize_and_validate_choices(choices, object) do
330 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
331 {options, max_count} = get_options_and_max_count(object)
332 count = Enum.count(options)
333
334 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
335 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
336 {:ok, options, choices}
337 else
338 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
339 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
340 end
341 end
342
343 def public_announce?(_, %{visibility: visibility})
344 when visibility in ~w{public unlisted private direct},
345 do: visibility in ~w(public unlisted)
346
347 def public_announce?(object, _) do
348 Visibility.is_public?(object)
349 end
350
351 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
352
353 def get_visibility(%{visibility: visibility}, in_reply_to, _)
354 when visibility in ~w{public local unlisted private direct},
355 do: {visibility, get_replied_to_visibility(in_reply_to)}
356
357 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
358 visibility = {:list, String.to_integer(list_id)}
359 {visibility, get_replied_to_visibility(in_reply_to)}
360 end
361
362 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
363 visibility = get_replied_to_visibility(in_reply_to)
364 {visibility, visibility}
365 end
366
367 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
368
369 def get_replied_to_visibility(nil), do: nil
370
371 def get_replied_to_visibility(activity) do
372 with %Object{} = object <- Object.normalize(activity, fetch: false) do
373 Visibility.get_visibility(object)
374 end
375 end
376
377 def check_expiry_date({:ok, nil} = res), do: res
378
379 def check_expiry_date({:ok, in_seconds}) do
380 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
381
382 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
383 {:ok, expiry}
384 else
385 {:error, "Expiry date is too soon"}
386 end
387 end
388
389 def check_expiry_date(expiry_str) do
390 Ecto.Type.cast(:integer, expiry_str)
391 |> check_expiry_date()
392 end
393
394 def listen(user, data) do
395 with {:ok, draft} <- ActivityDraft.listen(user, data) do
396 ActivityPub.listen(draft.changes)
397 end
398 end
399
400 def maybe_put_into_elasticsearch({:ok, activity}) do
401 if Config.get([:search, :provider]) == :elasticsearch do
402 actor = Pleroma.Activity.user_actor(activity)
403 activity
404 |> Map.put(:user_actor, actor)
405 |> Elasticsearch.put()
406 end
407 end
408
409 def maybe_put_into_elasticsearch(_) do
410 {:ok, :skipped}
411 end
412
413 def post(user, %{status: _} = data) do
414 with {:ok, draft} <- ActivityDraft.create(user, data) do
415 activity = ActivityPub.create(draft.changes, draft.preview?)
416 maybe_put_into_elasticsearch(activity)
417 activity
418 end
419 end
420
421 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
422 def pin(id, %User{} = user) do
423 with %Activity{} = activity <- create_activity_by_id(id),
424 true <- activity_belongs_to_actor(activity, user.ap_id),
425 true <- object_type_is_allowed_for_pin(activity.object),
426 true <- activity_is_public(activity),
427 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
428 {:ok, _pin, _} <-
429 Pipeline.common_pipeline(pin_data,
430 local: true,
431 activity_id: id
432 ) do
433 {:ok, activity}
434 else
435 {:error, {:side_effects, error}} -> error
436 error -> error
437 end
438 end
439
440 defp create_activity_by_id(id) do
441 with nil <- Activity.create_by_id_with_object(id) do
442 {:error, :not_found}
443 end
444 end
445
446 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
447 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
448
449 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
450 with false <- type in ["Note", "Article", "Question"] do
451 {:error, :not_allowed}
452 end
453 end
454
455 defp activity_is_public(activity) do
456 with false <- Visibility.is_public?(activity) do
457 {:error, :visibility_error}
458 end
459 end
460
461 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
462 def unpin(id, user) do
463 with %Activity{} = activity <- create_activity_by_id(id),
464 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
465 {:ok, _unpin, _} <-
466 Pipeline.common_pipeline(unpin_data,
467 local: true,
468 activity_id: activity.id,
469 expires_at: activity.data["expires_at"],
470 featured_address: user.featured_address
471 ) do
472 {:ok, activity}
473 end
474 end
475
476 def add_mute(user, activity, params \\ %{}) do
477 expires_in = Map.get(params, :expires_in, 0)
478
479 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
480 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
481 if expires_in > 0 do
482 Pleroma.Workers.MuteExpireWorker.enqueue(
483 "unmute_conversation",
484 %{"user_id" => user.id, "activity_id" => activity.id},
485 schedule_in: expires_in
486 )
487 end
488
489 {:ok, activity}
490 else
491 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
492 end
493 end
494
495 def remove_mute(%User{} = user, %Activity{} = activity) do
496 ThreadMute.remove_mute(user.id, activity.data["context"])
497 {:ok, activity}
498 end
499
500 def remove_mute(user_id, activity_id) do
501 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
502 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
503 remove_mute(user, activity)
504 else
505 {what, result} = error ->
506 Logger.warn(
507 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
508 )
509
510 {:error, error}
511 end
512 end
513
514 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
515 when is_binary(context) do
516 ThreadMute.exists?(user_id, context)
517 end
518
519 def thread_muted?(_, _), do: false
520
521 def report(user, data) do
522 with {:ok, account} <- get_reported_account(data.account_id),
523 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
524 {:ok, statuses} <- get_report_statuses(account, data) do
525 ActivityPub.flag(%{
526 context: Utils.generate_context_id(),
527 actor: user,
528 account: account,
529 statuses: statuses,
530 content: content_html,
531 forward: Map.get(data, :forward, false)
532 })
533 end
534 end
535
536 defp get_reported_account(account_id) do
537 case User.get_cached_by_id(account_id) do
538 %User{} = account -> {:ok, account}
539 _ -> {:error, dgettext("errors", "Account not found")}
540 end
541 end
542
543 def update_report_state(activity_ids, state) when is_list(activity_ids) do
544 case Utils.update_report_state(activity_ids, state) do
545 :ok -> {:ok, activity_ids}
546 _ -> {:error, dgettext("errors", "Could not update state")}
547 end
548 end
549
550 def update_report_state(activity_id, state) do
551 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
552 Utils.update_report_state(activity, state)
553 else
554 nil -> {:error, :not_found}
555 _ -> {:error, dgettext("errors", "Could not update state")}
556 end
557 end
558
559 def update_activity_scope(activity_id, opts \\ %{}) do
560 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
561 {:ok, activity} <- toggle_sensitive(activity, opts) do
562 set_visibility(activity, opts)
563 else
564 nil -> {:error, :not_found}
565 {:error, reason} -> {:error, reason}
566 end
567 end
568
569 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
570 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
571 end
572
573 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
574 when is_boolean(sensitive) do
575 new_data = Map.put(object.data, "sensitive", sensitive)
576
577 {:ok, object} =
578 object
579 |> Object.change(%{data: new_data})
580 |> Object.update_and_set_cache()
581
582 {:ok, Map.put(activity, :object, object)}
583 end
584
585 defp toggle_sensitive(activity, _), do: {:ok, activity}
586
587 defp set_visibility(activity, %{visibility: visibility}) do
588 Utils.update_activity_visibility(activity, visibility)
589 end
590
591 defp set_visibility(activity, _), do: {:ok, activity}
592
593 def hide_reblogs(%User{} = user, %User{} = target) do
594 UserRelationship.create_reblog_mute(user, target)
595 end
596
597 def show_reblogs(%User{} = user, %User{} = target) do
598 UserRelationship.delete_reblog_mute(user, target)
599 end
600
601 def get_user(ap_id, fake_record_fallback \\ true) do
602 cond do
603 user = User.get_cached_by_ap_id(ap_id) ->
604 user
605
606 user = User.get_by_guessed_nickname(ap_id) ->
607 user
608
609 fake_record_fallback ->
610 # TODO: refactor (fake records is never a good idea)
611 User.error_user(ap_id)
612
613 true ->
614 nil
615 end
616 end
617 end