1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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
10 alias Pleroma.ThreadMute
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
20 import Pleroma.Web.Gettext
21 import Pleroma.Web.CommonAPI.Utils
23 require Pleroma.Constants
26 def block(blocker, blocked) do
27 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
28 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
33 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
34 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
35 :ok <- validate_chat_content_length(content, !!maybe_attachment),
36 {_, {:ok, chat_message_data, _meta}} <-
41 content |> format_chat_content,
42 attachment: maybe_attachment
44 {_, {:ok, create_activity_data, _meta}} <-
45 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
46 {_, {:ok, %Activity{} = activity, _meta}} <-
48 Pipeline.common_pipeline(create_activity_data,
50 idempotency_key: opts[:idempotency_key]
54 {:common_pipeline, {:reject, _} = e} -> e
59 defp format_chat_content(nil), do: nil
61 defp format_chat_content(content) do
64 |> Formatter.html_escape("text/plain")
65 |> Formatter.linkify()
66 |> (fn {text, mentions, tags} ->
67 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
73 defp validate_chat_content_length(_, true), do: :ok
74 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
76 defp validate_chat_content_length(content, _) do
77 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
80 {:error, :content_too_long}
84 def unblock(blocker, blocked) do
85 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
86 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
87 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
90 {:fetch_block, nil} ->
91 if User.blocks?(blocker, blocked) do
92 User.unblock(blocker, blocked)
95 {:error, :not_blocking}
103 def follow(follower, followed) do
104 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
106 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
107 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
108 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
109 if activity.data["state"] == "reject" do
112 {:ok, follower, followed, activity}
117 def unfollow(follower, unfollowed) do
118 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
119 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
120 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
125 def accept_follow_request(follower, followed) do
126 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
127 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
128 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
133 def reject_follow_request(follower, followed) do
134 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
135 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
136 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
141 def delete(activity_id, user) do
142 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
143 {:find_activity, Activity.get_by_id(activity_id)},
144 {_, %Object{} = object, _} <-
145 {:find_object, Object.normalize(activity, fetch: false), activity},
146 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
147 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
148 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
151 {:find_activity, _} ->
154 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
155 # We have the create activity, but not the object, it was probably pruned.
156 # Insert a tombstone and try again
157 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
158 {:ok, _tombstone} <- Object.create(tombstone_data) do
159 delete(activity_id, user)
163 "Could not insert tombstone for missing object on deletion. Object is #{object}."
166 {:error, dgettext("errors", "Could not delete")}
170 {:error, dgettext("errors", "Could not delete")}
174 def repeat(id, user, params \\ %{}) do
175 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
176 object = %Object{} <- Object.normalize(activity, fetch: false),
177 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
178 public = public_announce?(object, params),
179 {:ok, announce, _} <- Builder.announce(user, object, public: public),
180 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
183 {:existing_announce, %Activity{} = announce} ->
191 def unrepeat(id, user) do
192 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
193 {:find_activity, Activity.get_by_id(id)},
194 %Object{} = note <- Object.normalize(activity, fetch: false),
195 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
196 {:ok, undo, _} <- Builder.undo(user, announce),
197 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
200 {:find_activity, _} -> {:error, :not_found}
201 _ -> {:error, dgettext("errors", "Could not unrepeat")}
205 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
206 def favorite(%User{} = user, id) do
207 case favorite_helper(user, id) do
211 {:error, :not_found} = res ->
215 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
216 {:error, dgettext("errors", "Could not favorite")}
220 def favorite_helper(user, id) do
221 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
222 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
223 {_, {:ok, %Activity{} = activity, _meta}} <-
225 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
242 if {:object, {"already liked by this actor", []}} in changeset.errors do
243 {:ok, :already_liked}
253 def unfavorite(id, user) do
254 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
255 {:find_activity, Activity.get_by_id(id)},
256 %Object{} = note <- Object.normalize(activity, fetch: false),
257 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
258 {:ok, undo, _} <- Builder.undo(user, like),
259 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
262 {:find_activity, _} -> {:error, :not_found}
263 _ -> {:error, dgettext("errors", "Could not unfavorite")}
267 def react_with_emoji(id, user, emoji) do
268 with %Activity{} = activity <- Activity.get_by_id(id),
269 object <- Object.normalize(activity, fetch: false),
270 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
271 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
275 {:error, dgettext("errors", "Could not add reaction emoji")}
279 def unreact_with_emoji(id, user, emoji) do
280 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
281 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
282 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
286 {:error, dgettext("errors", "Could not remove reaction emoji")}
290 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
291 with :ok <- validate_not_author(object, user),
292 :ok <- validate_existing_votes(user, object),
293 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
295 Enum.map(choices, fn index ->
296 {:ok, answer_object, _meta} =
297 Builder.answer(user, object, Enum.at(options, index)["name"])
299 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
301 {:ok, activity, _meta} =
303 |> Map.put("cc", answer_object["cc"])
304 |> Map.put("context", answer_object["context"])
305 |> Pipeline.common_pipeline(local: true)
307 # TODO: Do preload of Pleroma.Object in Pipeline
308 Activity.normalize(activity.data)
311 object = Object.get_cached_by_ap_id(object.data["id"])
312 {:ok, answer_activities, object}
316 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
317 do: {:error, dgettext("errors", "Poll's author can't vote")}
319 defp validate_not_author(_, _), do: :ok
321 defp validate_existing_votes(%{ap_id: ap_id}, object) do
322 if Utils.get_existing_votes(ap_id, object) == [] do
325 {:error, dgettext("errors", "Already voted")}
329 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
330 when is_list(any_of) and any_of != [],
331 do: {any_of, Enum.count(any_of)}
333 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
334 when is_list(one_of) and one_of != [],
337 defp normalize_and_validate_choices(choices, object) do
338 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
339 {options, max_count} = get_options_and_max_count(object)
340 count = Enum.count(options)
342 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
343 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
344 {:ok, options, choices}
346 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
347 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
351 def public_announce?(_, %{visibility: visibility})
352 when visibility in ~w{public unlisted private direct},
353 do: visibility in ~w(public unlisted)
355 def public_announce?(object, _) do
356 Visibility.is_public?(object)
359 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
361 def get_visibility(%{visibility: visibility}, in_reply_to, _)
362 when visibility in ~w{public local unlisted private direct},
363 do: {visibility, get_replied_to_visibility(in_reply_to)}
365 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
366 visibility = {:list, String.to_integer(list_id)}
367 {visibility, get_replied_to_visibility(in_reply_to)}
370 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
371 visibility = get_replied_to_visibility(in_reply_to)
372 {visibility, visibility}
375 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
377 def get_replied_to_visibility(nil), do: nil
379 def get_replied_to_visibility(activity) do
380 with %Object{} = object <- Object.normalize(activity, fetch: false) do
381 Visibility.get_visibility(object)
385 def check_expiry_date({:ok, nil} = res), do: res
387 def check_expiry_date({:ok, in_seconds}) do
388 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
390 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
393 {:error, "Expiry date is too soon"}
397 def check_expiry_date(expiry_str) do
398 Ecto.Type.cast(:integer, expiry_str)
399 |> check_expiry_date()
402 def listen(user, data) do
403 with {:ok, draft} <- ActivityDraft.listen(user, data) do
404 ActivityPub.listen(draft.changes)
408 def post(user, %{status: _} = data) do
409 with {:ok, draft} <- ActivityDraft.create(user, data) do
410 ActivityPub.create(draft.changes, draft.preview?)
414 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
415 def pin(id, %User{} = user) do
416 with %Activity{} = activity <- create_activity_by_id(id),
417 true <- activity_belongs_to_actor(activity, user.ap_id),
418 true <- object_type_is_allowed_for_pin(activity.object),
419 true <- activity_is_public(activity),
420 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
422 Pipeline.common_pipeline(pin_data,
425 featured_address: user.featured_address
429 {:error, {:execute_side_effects, error}} -> error
434 defp create_activity_by_id(id) do
435 with nil <- Activity.create_by_id_with_object(id) do
440 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
441 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
443 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
444 with false <- type in ["Note", "Article", "Question"] do
445 {:error, :not_allowed}
449 defp activity_is_public(activity) do
450 with false <- Visibility.is_public?(activity) do
451 {:error, :visibility_error}
455 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
456 def unpin(id, user) do
457 with %Activity{} = activity <- create_activity_by_id(id),
458 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
460 Pipeline.common_pipeline(unpin_data,
462 activity_id: activity.id,
463 expires_at: activity.data["expires_at"],
464 featured_address: user.featured_address
470 def add_mute(user, activity, params \\ %{}) do
471 expires_in = Map.get(params, :expires_in, 0)
473 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
474 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
476 Pleroma.Workers.MuteExpireWorker.enqueue(
477 "unmute_conversation",
478 %{"user_id" => user.id, "activity_id" => activity.id},
479 schedule_in: expires_in
485 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
489 def remove_mute(%User{} = user, %Activity{} = activity) do
490 ThreadMute.remove_mute(user.id, activity.data["context"])
494 def remove_mute(user_id, activity_id) do
495 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
496 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
497 remove_mute(user, activity)
499 {what, result} = error ->
501 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
510 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
511 when is_binary(context) do
512 ThreadMute.exists?(user_id, context)
515 def thread_muted?(_, _), do: false
517 def report(user, data) do
518 with {:ok, account} <- get_reported_account(data.account_id),
519 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
520 {:ok, statuses} <- get_report_statuses(account, data) do
522 context: Utils.generate_context_id(),
526 content: content_html,
527 forward: Map.get(data, :forward, false)
532 defp get_reported_account(account_id) do
533 case User.get_cached_by_id(account_id) do
534 %User{} = account -> {:ok, account}
535 _ -> {:error, dgettext("errors", "Account not found")}
539 def update_report_state(activity_ids, state) when is_list(activity_ids) do
540 case Utils.update_report_state(activity_ids, state) do
541 :ok -> {:ok, activity_ids}
542 _ -> {:error, dgettext("errors", "Could not update state")}
546 def update_report_state(activity_id, state) do
547 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
548 Utils.update_report_state(activity, state)
550 nil -> {:error, :not_found}
551 _ -> {:error, dgettext("errors", "Could not update state")}
555 def update_activity_scope(activity_id, opts \\ %{}) do
556 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
557 {:ok, activity} <- toggle_sensitive(activity, opts) do
558 set_visibility(activity, opts)
560 nil -> {:error, :not_found}
561 {:error, reason} -> {:error, reason}
565 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
566 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
569 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
570 when is_boolean(sensitive) do
571 new_data = Map.put(object.data, "sensitive", sensitive)
575 |> Object.change(%{data: new_data})
576 |> Object.update_and_set_cache()
578 {:ok, Map.put(activity, :object, object)}
581 defp toggle_sensitive(activity, _), do: {:ok, activity}
583 defp set_visibility(activity, %{visibility: visibility}) do
584 Utils.update_activity_visibility(activity, visibility)
587 defp set_visibility(activity, _), do: {:ok, activity}
589 def hide_reblogs(%User{} = user, %User{} = target) do
590 UserRelationship.create_reblog_mute(user, target)
593 def show_reblogs(%User{} = user, %User{} = target) do
594 UserRelationship.delete_reblog_mute(user, target)
597 def get_user(ap_id, fake_record_fallback \\ true) do
599 user = User.get_cached_by_ap_id(ap_id) ->
602 user = User.get_by_guessed_nickname(ap_id) ->
605 fake_record_fallback ->
606 # TODO: refactor (fake records is never a good idea)
607 User.error_user(ap_id)