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 # Remove from search index for local posts
150 Pleroma.Search.remove_from_index(object)
154 {:find_activity, _} ->
157 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
158 # We have the create activity, but not the object, it was probably pruned.
159 # Insert a tombstone and try again
160 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
161 {:ok, _tombstone} <- Object.create(tombstone_data) do
162 delete(activity_id, user)
166 "Could not insert tombstone for missing object on deletion. Object is #{object}."
169 {:error, dgettext("errors", "Could not delete")}
173 {:error, dgettext("errors", "Could not delete")}
177 def repeat(id, user, params \\ %{}) do
178 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
179 object = %Object{} <- Object.normalize(activity, fetch: false),
180 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
181 public = public_announce?(object, params),
182 {:ok, announce, _} <- Builder.announce(user, object, public: public),
183 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
186 {:existing_announce, %Activity{} = announce} ->
194 def unrepeat(id, user) do
195 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
196 {:find_activity, Activity.get_by_id(id)},
197 %Object{} = note <- Object.normalize(activity, fetch: false),
198 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
199 {:ok, undo, _} <- Builder.undo(user, announce),
200 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
203 {:find_activity, _} -> {:error, :not_found}
204 _ -> {:error, dgettext("errors", "Could not unrepeat")}
208 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
209 def favorite(%User{} = user, id) do
210 case favorite_helper(user, id) do
214 {:error, :not_found} = res ->
218 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
219 {:error, dgettext("errors", "Could not favorite")}
223 def favorite_helper(user, id) do
224 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
225 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
226 {_, {:ok, %Activity{} = activity, _meta}} <-
228 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
234 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
235 if {:object, {"already liked by this actor", []}} in changeset.errors do
236 {:ok, :already_liked}
246 def unfavorite(id, user) do
247 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
248 {:find_activity, Activity.get_by_id(id)},
249 %Object{} = note <- Object.normalize(activity, fetch: false),
250 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
251 {:ok, undo, _} <- Builder.undo(user, like),
252 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
255 {:find_activity, _} -> {:error, :not_found}
256 _ -> {:error, dgettext("errors", "Could not unfavorite")}
260 def react_with_emoji(id, user, emoji) do
261 with %Activity{} = activity <- Activity.get_by_id(id),
262 object <- Object.normalize(activity, fetch: false),
263 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
264 {: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 post(user, %{status: _} = data) do
401 with {:ok, draft} <- ActivityDraft.create(user, data) do
402 activity = ActivityPub.create(draft.changes, draft.preview?)
404 unless draft.preview? do
405 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
412 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
413 def pin(id, %User{} = user) do
414 with %Activity{} = activity <- create_activity_by_id(id),
415 true <- activity_belongs_to_actor(activity, user.ap_id),
416 true <- object_type_is_allowed_for_pin(activity.object),
417 true <- activity_is_public(activity),
418 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
420 Pipeline.common_pipeline(pin_data,
426 {:error, {:side_effects, error}} -> error
431 defp create_activity_by_id(id) do
432 with nil <- Activity.create_by_id_with_object(id) do
437 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
438 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
440 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
441 with false <- type in ["Note", "Article", "Question"] do
442 {:error, :not_allowed}
446 defp activity_is_public(activity) do
447 with false <- Visibility.is_public?(activity) do
448 {:error, :visibility_error}
452 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
453 def unpin(id, user) do
454 with %Activity{} = activity <- create_activity_by_id(id),
455 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
457 Pipeline.common_pipeline(unpin_data,
459 activity_id: activity.id,
460 expires_at: activity.data["expires_at"],
461 featured_address: user.featured_address
467 def add_mute(user, activity, params \\ %{}) do
468 expires_in = Map.get(params, :expires_in, 0)
470 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
471 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
473 Pleroma.Workers.MuteExpireWorker.enqueue(
474 "unmute_conversation",
475 %{"user_id" => user.id, "activity_id" => activity.id},
476 schedule_in: expires_in
482 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
486 def remove_mute(%User{} = user, %Activity{} = activity) do
487 ThreadMute.remove_mute(user.id, activity.data["context"])
491 def remove_mute(user_id, activity_id) do
492 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
493 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
494 remove_mute(user, activity)
496 {what, result} = error ->
498 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
505 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
506 when is_binary(context) do
507 ThreadMute.exists?(user_id, context)
510 def thread_muted?(_, _), do: false
512 def report(user, data) do
513 with {:ok, account} <- get_reported_account(data.account_id),
514 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
515 {:ok, statuses} <- get_report_statuses(account, data) do
517 context: Utils.generate_context_id(),
521 content: content_html,
522 forward: Map.get(data, :forward, false)
527 defp get_reported_account(account_id) do
528 case User.get_cached_by_id(account_id) do
529 %User{} = account -> {:ok, account}
530 _ -> {:error, dgettext("errors", "Account not found")}
534 def update_report_state(activity_ids, state) when is_list(activity_ids) do
535 case Utils.update_report_state(activity_ids, state) do
536 :ok -> {:ok, activity_ids}
537 _ -> {:error, dgettext("errors", "Could not update state")}
541 def update_report_state(activity_id, state) do
542 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
543 Utils.update_report_state(activity, state)
545 nil -> {:error, :not_found}
546 _ -> {:error, dgettext("errors", "Could not update state")}
550 def update_activity_scope(activity_id, opts \\ %{}) do
551 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
552 {:ok, activity} <- toggle_sensitive(activity, opts) do
553 set_visibility(activity, opts)
555 nil -> {:error, :not_found}
556 {:error, reason} -> {:error, reason}
560 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
561 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
564 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
565 when is_boolean(sensitive) do
566 new_data = Map.put(object.data, "sensitive", sensitive)
570 |> Object.change(%{data: new_data})
571 |> Object.update_and_set_cache()
573 {:ok, Map.put(activity, :object, object)}
576 defp toggle_sensitive(activity, _), do: {:ok, activity}
578 defp set_visibility(activity, %{visibility: visibility}) do
579 Utils.update_activity_visibility(activity, visibility)
582 defp set_visibility(activity, _), do: {:ok, activity}
584 def hide_reblogs(%User{} = user, %User{} = target) do
585 UserRelationship.create_reblog_mute(user, target)
588 def show_reblogs(%User{} = user, %User{} = target) do
589 UserRelationship.delete_reblog_mute(user, target)
592 def get_user(ap_id, fake_record_fallback \\ true) do
594 user = User.get_cached_by_ap_id(ap_id) ->
597 user = User.get_by_guessed_nickname(ap_id) ->
600 fake_record_fallback ->
601 # TODO: refactor (fake records is never a good idea)
602 User.error_user(ap_id)