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_content_length(content, !!maybe_attachment),
37 {_, {:ok, chat_message_data, _meta}} <-
42 content |> format_chat_content,
43 attachment: maybe_attachment
45 {_, {:ok, create_activity_data, _meta}} <-
46 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
47 {_, {:ok, %Activity{} = activity, _meta}} <-
49 Pipeline.common_pipeline(create_activity_data,
51 idempotency_key: opts[:idempotency_key]
55 {:common_pipeline, {:reject, _} = e} -> e
60 defp format_chat_content(nil), do: nil
62 defp format_chat_content(content) do
65 |> Formatter.html_escape("text/plain")
66 |> Formatter.linkify()
67 |> (fn {text, mentions, tags} ->
68 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
74 defp validate_chat_content_length(_, true), do: :ok
75 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
77 defp validate_chat_content_length(content, _) do
78 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
81 {:error, :content_too_long}
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
91 {:fetch_block, nil} ->
92 if User.blocks?(blocker, blocked) do
93 User.unblock(blocker, blocked)
96 {:error, :not_blocking}
104 def follow(follower, followed) do
105 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
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
113 {:ok, follower, followed, activity}
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
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
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
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
153 if object.data["type"] == "ChatMessage" do
154 "chat_message_delete"
159 ModerationLog.insert_log(%{
162 subject_id: activity_id
168 {:find_activity, _} ->
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)
180 "Could not insert tombstone for missing object on deletion. Object is #{object}."
183 {:error, dgettext("errors", "Could not delete")}
187 {:error, dgettext("errors", "Could not delete")}
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
200 {:existing_announce, %Activity{} = announce} ->
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
217 {:find_activity, _} -> {:error, :not_found}
218 _ -> {:error, dgettext("errors", "Could not unrepeat")}
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
228 {:error, :not_found} = res ->
232 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
233 {:error, dgettext("errors", "Could not favorite")}
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}} <-
242 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
248 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
249 if {:object, {"already liked by this actor", []}} in changeset.errors do
250 {:ok, :already_liked}
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
269 {:find_activity, _} -> {:error, :not_found}
270 _ -> {:error, dgettext("errors", "Could not unfavorite")}
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
282 {:error, dgettext("errors", "Could not add reaction emoji")}
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
293 {:error, dgettext("errors", "Could not remove reaction emoji")}
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
302 Enum.map(choices, fn index ->
303 {:ok, answer_object, _meta} =
304 Builder.answer(user, object, Enum.at(options, index)["name"])
306 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
308 {:ok, activity, _meta} =
310 |> Map.put("cc", answer_object["cc"])
311 |> Map.put("context", answer_object["context"])
312 |> Pipeline.common_pipeline(local: true)
314 # TODO: Do preload of Pleroma.Object in Pipeline
315 Activity.normalize(activity.data)
318 object = Object.get_cached_by_ap_id(object.data["id"])
319 {:ok, answer_activities, object}
323 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
324 do: {:error, dgettext("errors", "Poll's author can't vote")}
326 defp validate_not_author(_, _), do: :ok
328 defp validate_existing_votes(%{ap_id: ap_id}, object) do
329 if Utils.get_existing_votes(ap_id, object) == [] do
332 {:error, dgettext("errors", "Already voted")}
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)}
340 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
341 when is_list(one_of) and one_of != [],
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)
349 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
350 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
351 {:ok, options, choices}
353 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
354 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
358 def public_announce?(_, %{visibility: visibility})
359 when visibility in ~w{public unlisted private direct},
360 do: visibility in ~w(public unlisted)
362 def public_announce?(object, _) do
363 Visibility.is_public?(object)
366 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
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)}
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)}
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}
382 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
384 def get_replied_to_visibility(nil), do: nil
386 def get_replied_to_visibility(activity) do
387 with %Object{} = object <- Object.normalize(activity, fetch: false) do
388 Visibility.get_visibility(object)
392 def check_expiry_date({:ok, nil} = res), do: res
394 def check_expiry_date({:ok, in_seconds}) do
395 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
397 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
400 {:error, "Expiry date is too soon"}
404 def check_expiry_date(expiry_str) do
405 Ecto.Type.cast(:integer, expiry_str)
406 |> check_expiry_date()
409 def listen(user, data) do
410 with {:ok, draft} <- ActivityDraft.listen(user, data) do
411 ActivityPub.listen(draft.changes)
415 def post(user, %{status: _} = data) do
416 with {:ok, draft} <- ActivityDraft.create(user, data) do
417 ActivityPub.create(draft.changes, draft.preview?)
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
432 defp make_update_data(user, orig_object, changes) do
434 visibility: Visibility.get_visibility(orig_object),
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
444 params = Map.merge(changes, kept_params)
446 with {:ok, draft} <- ActivityDraft.create(user, params) do
448 Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
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),
464 Pipeline.common_pipeline(pin_data,
470 {:error, {:side_effects, error}} -> error
475 defp create_activity_by_id(id) do
476 with nil <- Activity.create_by_id_with_object(id) do
481 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
482 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
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}
490 defp activity_is_public(activity) do
491 with false <- Visibility.is_public?(activity) do
492 {:error, :visibility_error}
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),
501 Pipeline.common_pipeline(unpin_data,
503 activity_id: activity.id,
504 expires_at: activity.data["expires_at"],
505 featured_address: user.featured_address
511 def add_mute(user, activity, params \\ %{}) do
512 expires_in = Map.get(params, :expires_in, 0)
514 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
515 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
517 Pleroma.Workers.MuteExpireWorker.enqueue(
518 "unmute_conversation",
519 %{"user_id" => user.id, "activity_id" => activity.id},
520 schedule_in: expires_in
526 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
530 def remove_mute(%User{} = user, %Activity{} = activity) do
531 ThreadMute.remove_mute(user.id, activity.data["context"])
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)
540 {what, result} = error ->
542 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
549 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
550 when is_binary(context) do
551 ThreadMute.exists?(user_id, context)
554 def thread_muted?(_, _), do: false
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
561 context: Utils.generate_context_id(),
565 content: content_html,
566 forward: Map.get(data, :forward, false)
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")}
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")}
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)
589 nil -> {:error, :not_found}
590 _ -> {:error, dgettext("errors", "Could not update state")}
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)
599 nil -> {:error, :not_found}
600 {:error, reason} -> {:error, reason}
604 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
605 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
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)
614 |> Object.change(%{data: new_data})
615 |> Object.update_and_set_cache()
617 {:ok, Map.put(activity, :object, object)}
620 defp toggle_sensitive(activity, _), do: {:ok, activity}
622 defp set_visibility(activity, %{visibility: visibility}) do
623 Utils.update_activity_visibility(activity, visibility)
626 defp set_visibility(activity, _), do: {:ok, activity}
628 def hide_reblogs(%User{} = user, %User{} = target) do
629 UserRelationship.create_reblog_mute(user, target)
632 def show_reblogs(%User{} = user, %User{} = target) do
633 UserRelationship.delete_reblog_mute(user, target)
636 def get_user(ap_id, fake_record_fallback \\ true) do
638 user = User.get_cached_by_ap_id(ap_id) ->
641 user = User.get_by_guessed_nickname(ap_id) ->
644 fake_record_fallback ->
645 # TODO: refactor (fake records is never a good idea)
646 User.error_user(ap_id)