1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.CommonAPI do
7 alias Pleroma.Conversation.Participation
8 alias Pleroma.Formatter
9 alias Pleroma.ModerationLog
11 alias Pleroma.ThreadMute
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
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
24 require Pleroma.Constants
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
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}} <-
43 content |> format_chat_content,
44 attachment: maybe_attachment
46 {_, {:ok, create_activity_data, _meta}} <-
47 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
48 {_, {:ok, %Activity{} = activity, _meta}} <-
50 Pipeline.common_pipeline(create_activity_data,
52 idempotency_key: opts[:idempotency_key]
56 {:common_pipeline, {:reject, _} = e} -> e
61 defp format_chat_content(nil), do: nil
63 defp format_chat_content(content) do
66 |> Formatter.html_escape("text/plain")
67 |> Formatter.linkify()
68 |> (fn {text, mentions, tags} ->
69 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
75 defp validate_chat_attachment_attribution(nil, _), do: :ok
77 defp validate_chat_attachment_attribution(attachment, user) do
78 with :ok <- Object.authorize_access(attachment, user) do
86 defp validate_chat_content_length(_, true), do: :ok
87 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
89 defp validate_chat_content_length(content, _) do
90 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
93 {:error, :content_too_long}
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
103 {:fetch_block, nil} ->
104 if User.blocks?(blocker, blocked) do
105 User.unblock(blocker, blocked)
108 {:error, :not_blocking}
116 def follow(follower, followed) do
117 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
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
125 {:ok, follower, followed, activity}
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
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
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
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
165 if object.data["type"] == "ChatMessage" do
166 "chat_message_delete"
171 ModerationLog.insert_log(%{
174 subject_id: activity_id
180 {:find_activity, _} ->
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)
192 "Could not insert tombstone for missing object on deletion. Object is #{object}."
195 {:error, dgettext("errors", "Could not delete")}
199 {:error, dgettext("errors", "Could not delete")}
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
212 {:existing_announce, %Activity{} = announce} ->
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
229 {:find_activity, _} -> {:error, :not_found}
230 _ -> {:error, dgettext("errors", "Could not unrepeat")}
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
240 {:error, :not_found} = res ->
244 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
245 {:error, dgettext("errors", "Could not favorite")}
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}} <-
254 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
260 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
261 if {:object, {"already liked by this actor", []}} in changeset.errors do
262 {:ok, :already_liked}
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
281 {:find_activity, _} -> {:error, :not_found}
282 _ -> {:error, dgettext("errors", "Could not unfavorite")}
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
294 {:error, dgettext("errors", "Could not add reaction emoji")}
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
305 {:error, dgettext("errors", "Could not remove reaction emoji")}
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
314 Enum.map(choices, fn index ->
315 {:ok, answer_object, _meta} =
316 Builder.answer(user, object, Enum.at(options, index)["name"])
318 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
320 {:ok, activity, _meta} =
322 |> Map.put("cc", answer_object["cc"])
323 |> Map.put("context", answer_object["context"])
324 |> Pipeline.common_pipeline(local: true)
326 # TODO: Do preload of Pleroma.Object in Pipeline
327 Activity.normalize(activity.data)
330 object = Object.get_cached_by_ap_id(object.data["id"])
331 {:ok, answer_activities, object}
335 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
336 do: {:error, dgettext("errors", "Poll's author can't vote")}
338 defp validate_not_author(_, _), do: :ok
340 defp validate_existing_votes(%{ap_id: ap_id}, object) do
341 if Utils.get_existing_votes(ap_id, object) == [] do
344 {:error, dgettext("errors", "Already voted")}
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)}
352 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
353 when is_list(one_of) and one_of != [],
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)
361 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
362 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
363 {:ok, options, choices}
365 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
366 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
370 def public_announce?(_, %{visibility: visibility})
371 when visibility in ~w{public unlisted private direct},
372 do: visibility in ~w(public unlisted)
374 def public_announce?(object, _) do
375 Visibility.is_public?(object)
378 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
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)}
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)}
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}
394 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
396 def get_replied_to_visibility(nil), do: nil
398 def get_replied_to_visibility(activity) do
399 with %Object{} = object <- Object.normalize(activity, fetch: false) do
400 Visibility.get_visibility(object)
404 def check_expiry_date({:ok, nil} = res), do: res
406 def check_expiry_date({:ok, in_seconds}) do
407 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
409 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
412 {:error, "Expiry date is too soon"}
416 def check_expiry_date(expiry_str) do
417 Ecto.Type.cast(:integer, expiry_str)
418 |> check_expiry_date()
421 def listen(user, data) do
422 with {:ok, draft} <- ActivityDraft.listen(user, data) do
423 ActivityPub.listen(draft.changes)
427 def post(user, %{status: _} = data) do
428 with {:ok, draft} <- ActivityDraft.create(user, data) do
429 ActivityPub.create(draft.changes, draft.preview?)
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
444 defp make_update_data(user, orig_object, changes) do
446 visibility: Visibility.get_visibility(orig_object),
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
456 params = Map.merge(changes, kept_params)
458 with {:ok, draft} <- ActivityDraft.create(user, params) do
460 Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
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),
476 Pipeline.common_pipeline(pin_data,
482 {:error, {:side_effects, error}} -> error
487 defp create_activity_by_id(id) do
488 with nil <- Activity.create_by_id_with_object(id) do
493 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
494 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
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}
502 defp activity_is_public(activity) do
503 with false <- Visibility.is_public?(activity) do
504 {:error, :visibility_error}
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),
513 Pipeline.common_pipeline(unpin_data,
515 activity_id: activity.id,
516 expires_at: activity.data["expires_at"],
517 featured_address: user.featured_address
523 def add_mute(user, activity, params \\ %{}) do
524 expires_in = Map.get(params, :expires_in, 0)
526 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
527 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
529 Pleroma.Workers.MuteExpireWorker.enqueue(
530 "unmute_conversation",
531 %{"user_id" => user.id, "activity_id" => activity.id},
532 schedule_in: expires_in
538 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
542 def remove_mute(%User{} = user, %Activity{} = activity) do
543 ThreadMute.remove_mute(user.id, activity.data["context"])
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)
552 {what, result} = error ->
554 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
561 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
562 when is_binary(context) do
563 ThreadMute.exists?(user_id, context)
566 def thread_muted?(_, _), do: false
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
573 context: Utils.generate_context_id(),
577 content: content_html,
578 forward: Map.get(data, :forward, false)
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")}
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")}
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)
601 nil -> {:error, :not_found}
602 _ -> {:error, dgettext("errors", "Could not update state")}
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)
611 nil -> {:error, :not_found}
612 {:error, reason} -> {:error, reason}
616 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
617 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
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)
626 |> Object.change(%{data: new_data})
627 |> Object.update_and_set_cache()
629 {:ok, Map.put(activity, :object, object)}
632 defp toggle_sensitive(activity, _), do: {:ok, activity}
634 defp set_visibility(activity, %{visibility: visibility}) do
635 Utils.update_activity_visibility(activity, visibility)
638 defp set_visibility(activity, _), do: {:ok, activity}
640 def hide_reblogs(%User{} = user, %User{} = target) do
641 UserRelationship.create_reblog_mute(user, target)
644 def show_reblogs(%User{} = user, %User{} = target) do
645 UserRelationship.delete_reblog_mute(user, target)
648 def get_user(ap_id, fake_record_fallback \\ true) do
650 user = User.get_cached_by_ap_id(ap_id) ->
653 user = User.get_by_guessed_nickname(ap_id) ->
656 fake_record_fallback ->
657 # TODO: refactor (fake records is never a good idea)
658 User.error_user(ap_id)