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
265 {:error, dgettext("errors", "Could not add reaction emoji")}
269 def unreact_with_emoji(id, user, emoji) do
270 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
271 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
272 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
276 {:error, dgettext("errors", "Could not remove reaction emoji")}
280 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
281 with :ok <- validate_not_author(object, user),
282 :ok <- validate_existing_votes(user, object),
283 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
285 Enum.map(choices, fn index ->
286 {:ok, answer_object, _meta} =
287 Builder.answer(user, object, Enum.at(options, index)["name"])
289 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
291 {:ok, activity, _meta} =
293 |> Map.put("cc", answer_object["cc"])
294 |> Map.put("context", answer_object["context"])
295 |> Pipeline.common_pipeline(local: true)
297 # TODO: Do preload of Pleroma.Object in Pipeline
298 Activity.normalize(activity.data)
301 object = Object.get_cached_by_ap_id(object.data["id"])
302 {:ok, answer_activities, object}
306 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
307 do: {:error, dgettext("errors", "Poll's author can't vote")}
309 defp validate_not_author(_, _), do: :ok
311 defp validate_existing_votes(%{ap_id: ap_id}, object) do
312 if Utils.get_existing_votes(ap_id, object) == [] do
315 {:error, dgettext("errors", "Already voted")}
319 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
320 when is_list(any_of) and any_of != [],
321 do: {any_of, Enum.count(any_of)}
323 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
324 when is_list(one_of) and one_of != [],
327 defp normalize_and_validate_choices(choices, object) do
328 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
329 {options, max_count} = get_options_and_max_count(object)
330 count = Enum.count(options)
332 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
333 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
334 {:ok, options, choices}
336 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
337 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
341 def public_announce?(_, %{visibility: visibility})
342 when visibility in ~w{public unlisted private direct},
343 do: visibility in ~w(public unlisted)
345 def public_announce?(object, _) do
346 Visibility.is_public?(object)
349 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
351 def get_visibility(%{visibility: visibility}, in_reply_to, _)
352 when visibility in ~w{public local unlisted private direct},
353 do: {visibility, get_replied_to_visibility(in_reply_to)}
355 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
356 visibility = {:list, String.to_integer(list_id)}
357 {visibility, get_replied_to_visibility(in_reply_to)}
360 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
361 visibility = get_replied_to_visibility(in_reply_to)
362 {visibility, visibility}
365 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
367 def get_replied_to_visibility(nil), do: nil
369 def get_replied_to_visibility(activity) do
370 with %Object{} = object <- Object.normalize(activity, fetch: false) do
371 Visibility.get_visibility(object)
375 def check_expiry_date({:ok, nil} = res), do: res
377 def check_expiry_date({:ok, in_seconds}) do
378 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
380 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
383 {:error, "Expiry date is too soon"}
387 def check_expiry_date(expiry_str) do
388 Ecto.Type.cast(:integer, expiry_str)
389 |> check_expiry_date()
392 def listen(user, data) do
393 with {:ok, draft} <- ActivityDraft.listen(user, data) do
394 ActivityPub.listen(draft.changes)
398 def post(user, %{status: _} = data) do
399 with {:ok, draft} <- ActivityDraft.create(user, data) do
400 activity = ActivityPub.create(draft.changes, draft.preview?)
401 unless draft.preview? do
402 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
408 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
409 def pin(id, %User{} = user) do
410 with %Activity{} = activity <- create_activity_by_id(id),
411 true <- activity_belongs_to_actor(activity, user.ap_id),
412 true <- object_type_is_allowed_for_pin(activity.object),
413 true <- activity_is_public(activity),
414 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
416 Pipeline.common_pipeline(pin_data,
422 {:error, {:side_effects, error}} -> error
427 defp create_activity_by_id(id) do
428 with nil <- Activity.create_by_id_with_object(id) do
433 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
434 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
436 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
437 with false <- type in ["Note", "Article", "Question"] do
438 {:error, :not_allowed}
442 defp activity_is_public(activity) do
443 with false <- Visibility.is_public?(activity) do
444 {:error, :visibility_error}
448 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
449 def unpin(id, user) do
450 with %Activity{} = activity <- create_activity_by_id(id),
451 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
453 Pipeline.common_pipeline(unpin_data,
455 activity_id: activity.id,
456 expires_at: activity.data["expires_at"],
457 featured_address: user.featured_address
463 def add_mute(user, activity, params \\ %{}) do
464 expires_in = Map.get(params, :expires_in, 0)
466 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
467 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
469 Pleroma.Workers.MuteExpireWorker.enqueue(
470 "unmute_conversation",
471 %{"user_id" => user.id, "activity_id" => activity.id},
472 schedule_in: expires_in
478 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
482 def remove_mute(%User{} = user, %Activity{} = activity) do
483 ThreadMute.remove_mute(user.id, activity.data["context"])
487 def remove_mute(user_id, activity_id) do
488 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
489 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
490 remove_mute(user, activity)
492 {what, result} = error ->
494 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
501 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
502 when is_binary(context) do
503 ThreadMute.exists?(user_id, context)
506 def thread_muted?(_, _), do: false
508 def report(user, data) do
509 with {:ok, account} <- get_reported_account(data.account_id),
510 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
511 {:ok, statuses} <- get_report_statuses(account, data) do
513 context: Utils.generate_context_id(),
517 content: content_html,
518 forward: Map.get(data, :forward, false)
523 defp get_reported_account(account_id) do
524 case User.get_cached_by_id(account_id) do
525 %User{} = account -> {:ok, account}
526 _ -> {:error, dgettext("errors", "Account not found")}
530 def update_report_state(activity_ids, state) when is_list(activity_ids) do
531 case Utils.update_report_state(activity_ids, state) do
532 :ok -> {:ok, activity_ids}
533 _ -> {:error, dgettext("errors", "Could not update state")}
537 def update_report_state(activity_id, state) do
538 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
539 Utils.update_report_state(activity, state)
541 nil -> {:error, :not_found}
542 _ -> {:error, dgettext("errors", "Could not update state")}
546 def update_activity_scope(activity_id, opts \\ %{}) do
547 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
548 {:ok, activity} <- toggle_sensitive(activity, opts) do
549 set_visibility(activity, opts)
551 nil -> {:error, :not_found}
552 {:error, reason} -> {:error, reason}
556 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
557 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
560 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
561 when is_boolean(sensitive) do
562 new_data = Map.put(object.data, "sensitive", sensitive)
566 |> Object.change(%{data: new_data})
567 |> Object.update_and_set_cache()
569 {:ok, Map.put(activity, :object, object)}
572 defp toggle_sensitive(activity, _), do: {:ok, activity}
574 defp set_visibility(activity, %{visibility: visibility}) do
575 Utils.update_activity_visibility(activity, visibility)
578 defp set_visibility(activity, _), do: {:ok, activity}
580 def hide_reblogs(%User{} = user, %User{} = target) do
581 UserRelationship.create_reblog_mute(user, target)
584 def show_reblogs(%User{} = user, %User{} = target) do
585 UserRelationship.delete_reblog_mute(user, target)
588 def get_user(ap_id, fake_record_fallback \\ true) do
590 user = User.get_cached_by_ap_id(ap_id) ->
593 user = User.get_by_guessed_nickname(ap_id) ->
596 fake_record_fallback ->
597 # TODO: refactor (fake records is never a good idea)
598 User.error_user(ap_id)