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
231 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
232 if {:object, {"already liked by this actor", []}} in changeset.errors do
233 {:ok, :already_liked}
243 def unfavorite(id, user) do
244 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
245 {:find_activity, Activity.get_by_id(id)},
246 %Object{} = note <- Object.normalize(activity, fetch: false),
247 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
248 {:ok, undo, _} <- Builder.undo(user, like),
249 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
252 {:find_activity, _} -> {:error, :not_found}
253 _ -> {:error, dgettext("errors", "Could not unfavorite")}
257 def react_with_emoji(id, user, emoji) do
258 with %Activity{} = activity <- Activity.get_by_id(id),
259 object <- Object.normalize(activity, fetch: false),
260 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
261 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
264 _ -> {:error, dgettext("errors", "Could not add reaction emoji")}
268 def unreact_with_emoji(id, user, emoji) do
269 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
270 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
271 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
275 {:error, dgettext("errors", "Could not remove reaction emoji")}
279 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
280 with :ok <- validate_not_author(object, user),
281 :ok <- validate_existing_votes(user, object),
282 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
284 Enum.map(choices, fn index ->
285 {:ok, answer_object, _meta} =
286 Builder.answer(user, object, Enum.at(options, index)["name"])
288 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
290 {:ok, activity, _meta} =
292 |> Map.put("cc", answer_object["cc"])
293 |> Map.put("context", answer_object["context"])
294 |> Pipeline.common_pipeline(local: true)
296 # TODO: Do preload of Pleroma.Object in Pipeline
297 Activity.normalize(activity.data)
300 object = Object.get_cached_by_ap_id(object.data["id"])
301 {:ok, answer_activities, object}
305 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
306 do: {:error, dgettext("errors", "Poll's author can't vote")}
308 defp validate_not_author(_, _), do: :ok
310 defp validate_existing_votes(%{ap_id: ap_id}, object) do
311 if Utils.get_existing_votes(ap_id, object) == [] do
314 {:error, dgettext("errors", "Already voted")}
318 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
319 when is_list(any_of) and any_of != [],
320 do: {any_of, Enum.count(any_of)}
322 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
323 when is_list(one_of) and one_of != [],
326 defp normalize_and_validate_choices(choices, object) do
327 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
328 {options, max_count} = get_options_and_max_count(object)
329 count = Enum.count(options)
331 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
332 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
333 {:ok, options, choices}
335 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
336 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
340 def public_announce?(_, %{visibility: visibility})
341 when visibility in ~w{public unlisted private direct},
342 do: visibility in ~w(public unlisted)
344 def public_announce?(object, _) do
345 Visibility.is_public?(object)
348 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
350 def get_visibility(%{visibility: visibility}, in_reply_to, _)
351 when visibility in ~w{public local unlisted private direct},
352 do: {visibility, get_replied_to_visibility(in_reply_to)}
354 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
355 visibility = {:list, String.to_integer(list_id)}
356 {visibility, get_replied_to_visibility(in_reply_to)}
359 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
360 visibility = get_replied_to_visibility(in_reply_to)
361 {visibility, visibility}
364 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
366 def get_replied_to_visibility(nil), do: nil
368 def get_replied_to_visibility(activity) do
369 with %Object{} = object <- Object.normalize(activity, fetch: false) do
370 Visibility.get_visibility(object)
374 def check_expiry_date({:ok, nil} = res), do: res
376 def check_expiry_date({:ok, in_seconds}) do
377 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
379 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
382 {:error, "Expiry date is too soon"}
386 def check_expiry_date(expiry_str) do
387 Ecto.Type.cast(:integer, expiry_str)
388 |> check_expiry_date()
391 def listen(user, data) do
392 with {:ok, draft} <- ActivityDraft.listen(user, data) do
393 ActivityPub.listen(draft.changes)
397 def post(user, %{status: _} = data) do
398 with {:ok, draft} <- ActivityDraft.create(user, data) do
399 activity = ActivityPub.create(draft.changes, draft.preview?)
401 unless draft.preview? do
402 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
409 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
410 def pin(id, %User{} = user) do
411 with %Activity{} = activity <- create_activity_by_id(id),
412 true <- activity_belongs_to_actor(activity, user.ap_id),
413 true <- object_type_is_allowed_for_pin(activity.object),
414 true <- activity_is_public(activity),
415 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
417 Pipeline.common_pipeline(pin_data,
423 {:error, {:side_effects, error}} -> error
428 defp create_activity_by_id(id) do
429 with nil <- Activity.create_by_id_with_object(id) do
434 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
435 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
437 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
438 with false <- type in ["Note", "Article", "Question"] do
439 {:error, :not_allowed}
443 defp activity_is_public(activity) do
444 with false <- Visibility.is_public?(activity) do
445 {:error, :visibility_error}
449 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
450 def unpin(id, user) do
451 with %Activity{} = activity <- create_activity_by_id(id),
452 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
454 Pipeline.common_pipeline(unpin_data,
456 activity_id: activity.id,
457 expires_at: activity.data["expires_at"],
458 featured_address: user.featured_address
464 def add_mute(user, activity, params \\ %{}) do
465 expires_in = Map.get(params, :expires_in, 0)
467 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
468 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
470 Pleroma.Workers.MuteExpireWorker.enqueue(
471 "unmute_conversation",
472 %{"user_id" => user.id, "activity_id" => activity.id},
473 schedule_in: expires_in
479 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
483 def remove_mute(%User{} = user, %Activity{} = activity) do
484 ThreadMute.remove_mute(user.id, activity.data["context"])
488 def remove_mute(user_id, activity_id) do
489 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
490 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
491 remove_mute(user, activity)
493 {what, result} = error ->
495 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
502 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
503 when is_binary(context) do
504 ThreadMute.exists?(user_id, context)
507 def thread_muted?(_, _), do: false
509 def report(user, data) do
510 with {:ok, account} <- get_reported_account(data.account_id),
511 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
512 {:ok, statuses} <- get_report_statuses(account, data) do
514 context: Utils.generate_context_id(),
518 content: content_html,
519 forward: Map.get(data, :forward, false)
524 defp get_reported_account(account_id) do
525 case User.get_cached_by_id(account_id) do
526 %User{} = account -> {:ok, account}
527 _ -> {:error, dgettext("errors", "Account not found")}
531 def update_report_state(activity_ids, state) when is_list(activity_ids) do
532 case Utils.update_report_state(activity_ids, state) do
533 :ok -> {:ok, activity_ids}
534 _ -> {:error, dgettext("errors", "Could not update state")}
538 def update_report_state(activity_id, state) do
539 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
540 Utils.update_report_state(activity, state)
542 nil -> {:error, :not_found}
543 _ -> {:error, dgettext("errors", "Could not update state")}
547 def update_activity_scope(activity_id, opts \\ %{}) do
548 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
549 {:ok, activity} <- toggle_sensitive(activity, opts) do
550 set_visibility(activity, opts)
552 nil -> {:error, :not_found}
553 {:error, reason} -> {:error, reason}
557 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
558 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
561 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
562 when is_boolean(sensitive) do
563 new_data = Map.put(object.data, "sensitive", sensitive)
567 |> Object.change(%{data: new_data})
568 |> Object.update_and_set_cache()
570 {:ok, Map.put(activity, :object, object)}
573 defp toggle_sensitive(activity, _), do: {:ok, activity}
575 defp set_visibility(activity, %{visibility: visibility}) do
576 Utils.update_activity_visibility(activity, visibility)
579 defp set_visibility(activity, _), do: {:ok, activity}
581 def hide_reblogs(%User{} = user, %User{} = target) do
582 UserRelationship.create_reblog_mute(user, target)
585 def show_reblogs(%User{} = user, %User{} = target) do
586 UserRelationship.delete_reblog_mute(user, target)
589 def get_user(ap_id, fake_record_fallback \\ true) do
591 user = User.get_cached_by_ap_id(ap_id) ->
594 user = User.get_by_guessed_nickname(ap_id) ->
597 fake_record_fallback ->
598 # TODO: refactor (fake records is never a good idea)
599 User.error_user(ap_id)