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