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
19 alias Pleroma.Elasticsearch
22 import Pleroma.Web.Gettext
23 import Pleroma.Web.CommonAPI.Utils
25 require Pleroma.Constants
28 def block(blocker, blocked) do
29 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
30 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
35 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
36 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
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_content_length(_, true), do: :ok
76 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
78 defp validate_chat_content_length(content, _) do
79 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
82 {:error, :content_too_long}
86 def unblock(blocker, blocked) do
87 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
88 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
89 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
92 {:fetch_block, nil} ->
93 if User.blocks?(blocker, blocked) do
94 User.unblock(blocker, blocked)
97 {:error, :not_blocking}
105 def follow(follower, followed) do
106 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
108 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
109 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
110 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
111 if activity.data["state"] == "reject" do
114 {:ok, follower, followed, activity}
119 def unfollow(follower, unfollowed) do
120 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
121 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
122 {:ok, _subscription} <- User.unsubscribe(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.superuser?(user) || 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
153 {:find_activity, _} ->
156 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
157 # We have the create activity, but not the object, it was probably pruned.
158 # Insert a tombstone and try again
159 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
160 {:ok, _tombstone} <- Object.create(tombstone_data) do
161 delete(activity_id, user)
165 "Could not insert tombstone for missing object on deletion. Object is #{object}."
168 {:error, dgettext("errors", "Could not delete")}
172 {:error, dgettext("errors", "Could not delete")}
176 def repeat(id, user, params \\ %{}) do
177 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
178 object = %Object{} <- Object.normalize(activity, fetch: false),
179 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
180 public = public_announce?(object, params),
181 {:ok, announce, _} <- Builder.announce(user, object, public: public),
182 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
185 {:existing_announce, %Activity{} = announce} ->
193 def unrepeat(id, user) do
194 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
195 {:find_activity, Activity.get_by_id(id)},
196 %Object{} = note <- Object.normalize(activity, fetch: false),
197 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
198 {:ok, undo, _} <- Builder.undo(user, announce),
199 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
202 {:find_activity, _} -> {:error, :not_found}
203 _ -> {:error, dgettext("errors", "Could not unrepeat")}
207 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
208 def favorite(%User{} = user, id) do
209 case favorite_helper(user, id) do
213 {:error, :not_found} = res ->
217 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
218 {:error, dgettext("errors", "Could not favorite")}
222 def favorite_helper(user, id) do
223 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
224 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
225 {_, {:ok, %Activity{} = activity, _meta}} <-
227 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
233 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
234 if {:object, {"already liked by this actor", []}} in changeset.errors do
235 {:ok, :already_liked}
245 def unfavorite(id, user) do
246 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
247 {:find_activity, Activity.get_by_id(id)},
248 %Object{} = note <- Object.normalize(activity, fetch: false),
249 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
250 {:ok, undo, _} <- Builder.undo(user, like),
251 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
254 {:find_activity, _} -> {:error, :not_found}
255 _ -> {:error, dgettext("errors", "Could not unfavorite")}
259 def react_with_emoji(id, user, emoji) do
260 with %Activity{} = activity <- Activity.get_by_id(id),
261 object <- Object.normalize(activity, fetch: false),
262 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
263 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
267 {:error, dgettext("errors", "Could not add reaction emoji")}
271 def unreact_with_emoji(id, user, emoji) do
272 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
273 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
274 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
278 {:error, dgettext("errors", "Could not remove reaction emoji")}
282 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
283 with :ok <- validate_not_author(object, user),
284 :ok <- validate_existing_votes(user, object),
285 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
287 Enum.map(choices, fn index ->
288 {:ok, answer_object, _meta} =
289 Builder.answer(user, object, Enum.at(options, index)["name"])
291 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
293 {:ok, activity, _meta} =
295 |> Map.put("cc", answer_object["cc"])
296 |> Map.put("context", answer_object["context"])
297 |> Pipeline.common_pipeline(local: true)
299 # TODO: Do preload of Pleroma.Object in Pipeline
300 Activity.normalize(activity.data)
303 object = Object.get_cached_by_ap_id(object.data["id"])
304 {:ok, answer_activities, object}
308 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
309 do: {:error, dgettext("errors", "Poll's author can't vote")}
311 defp validate_not_author(_, _), do: :ok
313 defp validate_existing_votes(%{ap_id: ap_id}, object) do
314 if Utils.get_existing_votes(ap_id, object) == [] do
317 {:error, dgettext("errors", "Already voted")}
321 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
322 when is_list(any_of) and any_of != [],
323 do: {any_of, Enum.count(any_of)}
325 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
326 when is_list(one_of) and one_of != [],
329 defp normalize_and_validate_choices(choices, object) do
330 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
331 {options, max_count} = get_options_and_max_count(object)
332 count = Enum.count(options)
334 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
335 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
336 {:ok, options, choices}
338 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
339 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
343 def public_announce?(_, %{visibility: visibility})
344 when visibility in ~w{public unlisted private direct},
345 do: visibility in ~w(public unlisted)
347 def public_announce?(object, _) do
348 Visibility.is_public?(object)
351 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
353 def get_visibility(%{visibility: visibility}, in_reply_to, _)
354 when visibility in ~w{public local unlisted private direct},
355 do: {visibility, get_replied_to_visibility(in_reply_to)}
357 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
358 visibility = {:list, String.to_integer(list_id)}
359 {visibility, get_replied_to_visibility(in_reply_to)}
362 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
363 visibility = get_replied_to_visibility(in_reply_to)
364 {visibility, visibility}
367 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
369 def get_replied_to_visibility(nil), do: nil
371 def get_replied_to_visibility(activity) do
372 with %Object{} = object <- Object.normalize(activity, fetch: false) do
373 Visibility.get_visibility(object)
377 def check_expiry_date({:ok, nil} = res), do: res
379 def check_expiry_date({:ok, in_seconds}) do
380 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
382 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
385 {:error, "Expiry date is too soon"}
389 def check_expiry_date(expiry_str) do
390 Ecto.Type.cast(:integer, expiry_str)
391 |> check_expiry_date()
394 def listen(user, data) do
395 with {:ok, draft} <- ActivityDraft.listen(user, data) do
396 ActivityPub.listen(draft.changes)
400 def maybe_put_into_elasticsearch({:ok, activity}) do
401 if Config.get([:search, :provider]) == Pleroma.Search.Elasticsearch do
402 actor = Pleroma.Activity.user_actor(activity)
405 |> Map.put(:user_actor, actor)
406 |> Elasticsearch.put()
410 def maybe_put_into_elasticsearch(_) do
414 def post(user, %{status: _} = data) do
415 with {:ok, draft} <- ActivityDraft.create(user, data) do
416 activity = ActivityPub.create(draft.changes, draft.preview?)
417 maybe_put_into_elasticsearch(activity)
422 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
423 def pin(id, %User{} = user) do
424 with %Activity{} = activity <- create_activity_by_id(id),
425 true <- activity_belongs_to_actor(activity, user.ap_id),
426 true <- object_type_is_allowed_for_pin(activity.object),
427 true <- activity_is_public(activity),
428 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
430 Pipeline.common_pipeline(pin_data,
436 {:error, {:side_effects, error}} -> error
441 defp create_activity_by_id(id) do
442 with nil <- Activity.create_by_id_with_object(id) do
447 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
448 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
450 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
451 with false <- type in ["Note", "Article", "Question"] do
452 {:error, :not_allowed}
456 defp activity_is_public(activity) do
457 with false <- Visibility.is_public?(activity) do
458 {:error, :visibility_error}
462 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
463 def unpin(id, user) do
464 with %Activity{} = activity <- create_activity_by_id(id),
465 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
467 Pipeline.common_pipeline(unpin_data,
469 activity_id: activity.id,
470 expires_at: activity.data["expires_at"],
471 featured_address: user.featured_address
477 def add_mute(user, activity, params \\ %{}) do
478 expires_in = Map.get(params, :expires_in, 0)
480 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
481 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
483 Pleroma.Workers.MuteExpireWorker.enqueue(
484 "unmute_conversation",
485 %{"user_id" => user.id, "activity_id" => activity.id},
486 schedule_in: expires_in
492 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
496 def remove_mute(%User{} = user, %Activity{} = activity) do
497 ThreadMute.remove_mute(user.id, activity.data["context"])
501 def remove_mute(user_id, activity_id) do
502 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
503 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
504 remove_mute(user, activity)
506 {what, result} = error ->
508 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
515 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
516 when is_binary(context) do
517 ThreadMute.exists?(user_id, context)
520 def thread_muted?(_, _), do: false
522 def report(user, data) do
523 with {:ok, account} <- get_reported_account(data.account_id),
524 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
525 {:ok, statuses} <- get_report_statuses(account, data) do
527 context: Utils.generate_context_id(),
531 content: content_html,
532 forward: Map.get(data, :forward, false)
537 defp get_reported_account(account_id) do
538 case User.get_cached_by_id(account_id) do
539 %User{} = account -> {:ok, account}
540 _ -> {:error, dgettext("errors", "Account not found")}
544 def update_report_state(activity_ids, state) when is_list(activity_ids) do
545 case Utils.update_report_state(activity_ids, state) do
546 :ok -> {:ok, activity_ids}
547 _ -> {:error, dgettext("errors", "Could not update state")}
551 def update_report_state(activity_id, state) do
552 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
553 Utils.update_report_state(activity, state)
555 nil -> {:error, :not_found}
556 _ -> {:error, dgettext("errors", "Could not update state")}
560 def update_activity_scope(activity_id, opts \\ %{}) do
561 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
562 {:ok, activity} <- toggle_sensitive(activity, opts) do
563 set_visibility(activity, opts)
565 nil -> {:error, :not_found}
566 {:error, reason} -> {:error, reason}
570 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
571 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
574 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
575 when is_boolean(sensitive) do
576 new_data = Map.put(object.data, "sensitive", sensitive)
580 |> Object.change(%{data: new_data})
581 |> Object.update_and_set_cache()
583 {:ok, Map.put(activity, :object, object)}
586 defp toggle_sensitive(activity, _), do: {:ok, activity}
588 defp set_visibility(activity, %{visibility: visibility}) do
589 Utils.update_activity_visibility(activity, visibility)
592 defp set_visibility(activity, _), do: {:ok, activity}
594 def hide_reblogs(%User{} = user, %User{} = target) do
595 UserRelationship.create_reblog_mute(user, target)
598 def show_reblogs(%User{} = user, %User{} = target) do
599 UserRelationship.delete_reblog_mute(user, target)
602 def get_user(ap_id, fake_record_fallback \\ true) do
604 user = User.get_cached_by_ap_id(ap_id) ->
607 user = User.get_by_guessed_nickname(ap_id) ->
610 fake_record_fallback ->
611 # TODO: refactor (fake records is never a good idea)
612 User.error_user(ap_id)