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