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
149 # Also delete from search index
150 search_module = Pleroma.Config.get([Pleroma.Search, :module])
152 ConcurrentLimiter.limit(Pleroma.Search, fn ->
153 Task.start(fn -> search_module.remove_from_index(object) end)
158 {:find_activity, _} ->
161 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
162 # We have the create activity, but not the object, it was probably pruned.
163 # Insert a tombstone and try again
164 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
165 {:ok, _tombstone} <- Object.create(tombstone_data) do
166 delete(activity_id, user)
170 "Could not insert tombstone for missing object on deletion. Object is #{object}."
173 {:error, dgettext("errors", "Could not delete")}
177 {:error, dgettext("errors", "Could not delete")}
181 def repeat(id, user, params \\ %{}) do
182 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
183 object = %Object{} <- Object.normalize(activity, fetch: false),
184 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
185 public = public_announce?(object, params),
186 {:ok, announce, _} <- Builder.announce(user, object, public: public),
187 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
190 {:existing_announce, %Activity{} = announce} ->
198 def unrepeat(id, user) do
199 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
200 {:find_activity, Activity.get_by_id(id)},
201 %Object{} = note <- Object.normalize(activity, fetch: false),
202 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
203 {:ok, undo, _} <- Builder.undo(user, announce),
204 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
207 {:find_activity, _} -> {:error, :not_found}
208 _ -> {:error, dgettext("errors", "Could not unrepeat")}
212 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
213 def favorite(%User{} = user, id) do
214 case favorite_helper(user, id) do
218 {:error, :not_found} = res ->
222 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
223 {:error, dgettext("errors", "Could not favorite")}
227 def favorite_helper(user, id) do
228 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
229 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
230 {_, {:ok, %Activity{} = activity, _meta}} <-
232 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
238 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
239 if {:object, {"already liked by this actor", []}} in changeset.errors do
240 {:ok, :already_liked}
250 def unfavorite(id, user) do
251 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
252 {:find_activity, Activity.get_by_id(id)},
253 %Object{} = note <- Object.normalize(activity, fetch: false),
254 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
255 {:ok, undo, _} <- Builder.undo(user, like),
256 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
259 {:find_activity, _} -> {:error, :not_found}
260 _ -> {:error, dgettext("errors", "Could not unfavorite")}
264 def react_with_emoji(id, user, emoji) do
265 with %Activity{} = activity <- Activity.get_by_id(id),
266 object <- Object.normalize(activity, fetch: false),
267 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
268 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
271 _ -> {:error, dgettext("errors", "Could not add reaction emoji")}
275 def unreact_with_emoji(id, user, emoji) do
276 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
277 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
278 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
282 {:error, dgettext("errors", "Could not remove reaction emoji")}
286 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
287 with :ok <- validate_not_author(object, user),
288 :ok <- validate_existing_votes(user, object),
289 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
291 Enum.map(choices, fn index ->
292 {:ok, answer_object, _meta} =
293 Builder.answer(user, object, Enum.at(options, index)["name"])
295 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
297 {:ok, activity, _meta} =
299 |> Map.put("cc", answer_object["cc"])
300 |> Map.put("context", answer_object["context"])
301 |> Pipeline.common_pipeline(local: true)
303 # TODO: Do preload of Pleroma.Object in Pipeline
304 Activity.normalize(activity.data)
307 object = Object.get_cached_by_ap_id(object.data["id"])
308 {:ok, answer_activities, object}
312 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
313 do: {:error, dgettext("errors", "Poll's author can't vote")}
315 defp validate_not_author(_, _), do: :ok
317 defp validate_existing_votes(%{ap_id: ap_id}, object) do
318 if Utils.get_existing_votes(ap_id, object) == [] do
321 {:error, dgettext("errors", "Already voted")}
325 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
326 when is_list(any_of) and any_of != [],
327 do: {any_of, Enum.count(any_of)}
329 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
330 when is_list(one_of) and one_of != [],
333 defp normalize_and_validate_choices(choices, object) do
334 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
335 {options, max_count} = get_options_and_max_count(object)
336 count = Enum.count(options)
338 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
339 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
340 {:ok, options, choices}
342 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
343 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
347 def public_announce?(_, %{visibility: visibility})
348 when visibility in ~w{public unlisted private direct},
349 do: visibility in ~w(public unlisted)
351 def public_announce?(object, _) do
352 Visibility.is_public?(object)
355 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
357 def get_visibility(%{visibility: visibility}, in_reply_to, _)
358 when visibility in ~w{public local unlisted private direct},
359 do: {visibility, get_replied_to_visibility(in_reply_to)}
361 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
362 visibility = {:list, String.to_integer(list_id)}
363 {visibility, get_replied_to_visibility(in_reply_to)}
366 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
367 visibility = get_replied_to_visibility(in_reply_to)
368 {visibility, visibility}
371 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
373 def get_replied_to_visibility(nil), do: nil
375 def get_replied_to_visibility(activity) do
376 with %Object{} = object <- Object.normalize(activity, fetch: false) do
377 Visibility.get_visibility(object)
381 def check_expiry_date({:ok, nil} = res), do: res
383 def check_expiry_date({:ok, in_seconds}) do
384 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
386 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
389 {:error, "Expiry date is too soon"}
393 def check_expiry_date(expiry_str) do
394 Ecto.Type.cast(:integer, expiry_str)
395 |> check_expiry_date()
398 def listen(user, data) do
399 with {:ok, draft} <- ActivityDraft.listen(user, data) do
400 ActivityPub.listen(draft.changes)
404 def post(user, %{status: _} = data) do
405 with {:ok, draft} <- ActivityDraft.create(user, data) do
406 activity = ActivityPub.create(draft.changes, draft.preview?)
408 unless draft.preview? do
409 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
416 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
417 def pin(id, %User{} = user) do
418 with %Activity{} = activity <- create_activity_by_id(id),
419 true <- activity_belongs_to_actor(activity, user.ap_id),
420 true <- object_type_is_allowed_for_pin(activity.object),
421 true <- activity_is_public(activity),
422 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
424 Pipeline.common_pipeline(pin_data,
430 {:error, {:side_effects, error}} -> error
435 defp create_activity_by_id(id) do
436 with nil <- Activity.create_by_id_with_object(id) do
441 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
442 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
444 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
445 with false <- type in ["Note", "Article", "Question"] do
446 {:error, :not_allowed}
450 defp activity_is_public(activity) do
451 with false <- Visibility.is_public?(activity) do
452 {:error, :visibility_error}
456 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
457 def unpin(id, user) do
458 with %Activity{} = activity <- create_activity_by_id(id),
459 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
461 Pipeline.common_pipeline(unpin_data,
463 activity_id: activity.id,
464 expires_at: activity.data["expires_at"],
465 featured_address: user.featured_address
471 def add_mute(user, activity, params \\ %{}) do
472 expires_in = Map.get(params, :expires_in, 0)
474 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
475 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
477 Pleroma.Workers.MuteExpireWorker.enqueue(
478 "unmute_conversation",
479 %{"user_id" => user.id, "activity_id" => activity.id},
480 schedule_in: expires_in
486 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
490 def remove_mute(%User{} = user, %Activity{} = activity) do
491 ThreadMute.remove_mute(user.id, activity.data["context"])
495 def remove_mute(user_id, activity_id) do
496 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
497 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
498 remove_mute(user, activity)
500 {what, result} = error ->
502 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
509 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
510 when is_binary(context) do
511 ThreadMute.exists?(user_id, context)
514 def thread_muted?(_, _), do: false
516 def report(user, data) do
517 with {:ok, account} <- get_reported_account(data.account_id),
518 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
519 {:ok, statuses} <- get_report_statuses(account, data) do
521 context: Utils.generate_context_id(),
525 content: content_html,
526 forward: Map.get(data, :forward, false)
531 defp get_reported_account(account_id) do
532 case User.get_cached_by_id(account_id) do
533 %User{} = account -> {:ok, account}
534 _ -> {:error, dgettext("errors", "Account not found")}
538 def update_report_state(activity_ids, state) when is_list(activity_ids) do
539 case Utils.update_report_state(activity_ids, state) do
540 :ok -> {:ok, activity_ids}
541 _ -> {:error, dgettext("errors", "Could not update state")}
545 def update_report_state(activity_id, state) do
546 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
547 Utils.update_report_state(activity, state)
549 nil -> {:error, :not_found}
550 _ -> {:error, dgettext("errors", "Could not update state")}
554 def update_activity_scope(activity_id, opts \\ %{}) do
555 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
556 {:ok, activity} <- toggle_sensitive(activity, opts) do
557 set_visibility(activity, opts)
559 nil -> {:error, :not_found}
560 {:error, reason} -> {:error, reason}
564 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
565 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
568 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
569 when is_boolean(sensitive) do
570 new_data = Map.put(object.data, "sensitive", sensitive)
574 |> Object.change(%{data: new_data})
575 |> Object.update_and_set_cache()
577 {:ok, Map.put(activity, :object, object)}
580 defp toggle_sensitive(activity, _), do: {:ok, activity}
582 defp set_visibility(activity, %{visibility: visibility}) do
583 Utils.update_activity_visibility(activity, visibility)
586 defp set_visibility(activity, _), do: {:ok, activity}
588 def hide_reblogs(%User{} = user, %User{} = target) do
589 UserRelationship.create_reblog_mute(user, target)
592 def show_reblogs(%User{} = user, %User{} = target) do
593 UserRelationship.delete_reblog_mute(user, target)
596 def get_user(ap_id, fake_record_fallback \\ true) do
598 user = User.get_cached_by_ap_id(ap_id) ->
601 user = User.get_by_guessed_nickname(ap_id) ->
604 fake_record_fallback ->
605 # TODO: refactor (fake records is never a good idea)
606 User.error_user(ap_id)