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
9 alias Pleroma.ThreadMute
11 alias Pleroma.UserRelationship
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Builder
14 alias Pleroma.Web.ActivityPub.Pipeline
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.CommonAPI.ActivityDraft
19 import Pleroma.Web.Gettext
20 import Pleroma.Web.CommonAPI.Utils
22 require Pleroma.Constants
25 def block(blocker, blocked) do
26 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
27 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
32 def unblock(blocker, blocked) do
33 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
34 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
35 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
38 {:fetch_block, nil} ->
39 if User.blocks?(blocker, blocked) do
40 User.unblock(blocker, blocked)
43 {:error, :not_blocking}
51 def follow(follower, followed) do
52 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
54 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
55 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
56 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
57 if activity.data["state"] == "reject" do
60 {:ok, follower, followed, activity}
65 def unfollow(follower, unfollowed) do
66 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
67 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
68 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
73 def accept_follow_request(follower, followed) do
74 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
75 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
76 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
81 def reject_follow_request(follower, followed) do
82 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
83 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
84 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
89 def delete(activity_id, user) do
90 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
91 {:find_activity, Activity.get_by_id(activity_id)},
92 {_, %Object{} = object, _} <-
93 {:find_object, Object.normalize(activity, fetch: false), activity},
94 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
95 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
96 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
99 {:find_activity, _} ->
102 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
103 # We have the create activity, but not the object, it was probably pruned.
104 # Insert a tombstone and try again
105 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
106 {:ok, _tombstone} <- Object.create(tombstone_data) do
107 delete(activity_id, user)
111 "Could not insert tombstone for missing object on deletion. Object is #{object}."
114 {:error, dgettext("errors", "Could not delete")}
118 {:error, dgettext("errors", "Could not delete")}
122 def repeat(id, user, params \\ %{}) do
123 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
124 object = %Object{} <- Object.normalize(activity, fetch: false),
125 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
126 public = public_announce?(object, params),
127 {:ok, announce, _} <- Builder.announce(user, object, public: public),
128 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
131 {:existing_announce, %Activity{} = announce} ->
139 def unrepeat(id, user) do
140 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
141 {:find_activity, Activity.get_by_id(id)},
142 %Object{} = note <- Object.normalize(activity, fetch: false),
143 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
144 {:ok, undo, _} <- Builder.undo(user, announce),
145 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
148 {:find_activity, _} -> {:error, :not_found}
149 _ -> {:error, dgettext("errors", "Could not unrepeat")}
153 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
154 def favorite(%User{} = user, id) do
155 case favorite_helper(user, id) do
159 {:error, :not_found} = res ->
163 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
164 {:error, dgettext("errors", "Could not favorite")}
168 def favorite_helper(user, id) do
169 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
170 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
171 {_, {:ok, %Activity{} = activity, _meta}} <-
173 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
179 {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
180 if {:object, {"already liked by this actor", []}} in changeset.errors do
181 {:ok, :already_liked}
191 def unfavorite(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{} = like <- Utils.get_existing_like(user.ap_id, note),
196 {:ok, undo, _} <- Builder.undo(user, like),
197 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
200 {:find_activity, _} -> {:error, :not_found}
201 _ -> {:error, dgettext("errors", "Could not unfavorite")}
205 def react_with_emoji(id, user, emoji) do
206 with %Activity{} = activity <- Activity.get_by_id(id),
207 object <- Object.normalize(activity, fetch: false),
208 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
209 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
213 {:error, dgettext("errors", "Could not add reaction emoji")}
217 def unreact_with_emoji(id, user, emoji) do
218 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
219 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
220 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
224 {:error, dgettext("errors", "Could not remove reaction emoji")}
228 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
229 with :ok <- validate_not_author(object, user),
230 :ok <- validate_existing_votes(user, object),
231 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
233 Enum.map(choices, fn index ->
234 {:ok, answer_object, _meta} =
235 Builder.answer(user, object, Enum.at(options, index)["name"])
237 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
239 {:ok, activity, _meta} =
241 |> Map.put("cc", answer_object["cc"])
242 |> Map.put("context", answer_object["context"])
243 |> Pipeline.common_pipeline(local: true)
245 # TODO: Do preload of Pleroma.Object in Pipeline
246 Activity.normalize(activity.data)
249 object = Object.get_cached_by_ap_id(object.data["id"])
250 {:ok, answer_activities, object}
254 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
255 do: {:error, dgettext("errors", "Poll's author can't vote")}
257 defp validate_not_author(_, _), do: :ok
259 defp validate_existing_votes(%{ap_id: ap_id}, object) do
260 if Utils.get_existing_votes(ap_id, object) == [] do
263 {:error, dgettext("errors", "Already voted")}
267 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
268 when is_list(any_of) and any_of != [],
269 do: {any_of, Enum.count(any_of)}
271 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
272 when is_list(one_of) and one_of != [],
275 defp normalize_and_validate_choices(choices, object) do
276 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
277 {options, max_count} = get_options_and_max_count(object)
278 count = Enum.count(options)
280 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
281 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
282 {:ok, options, choices}
284 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
285 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
289 def public_announce?(_, %{visibility: visibility})
290 when visibility in ~w{public unlisted private direct},
291 do: visibility in ~w(public unlisted)
293 def public_announce?(object, _) do
294 Visibility.is_public?(object)
297 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
299 def get_visibility(%{visibility: visibility}, in_reply_to, _)
300 when visibility in ~w{public local unlisted private direct},
301 do: {visibility, get_replied_to_visibility(in_reply_to)}
303 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
304 visibility = {:list, String.to_integer(list_id)}
305 {visibility, get_replied_to_visibility(in_reply_to)}
308 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
309 visibility = get_replied_to_visibility(in_reply_to)
310 {visibility, visibility}
313 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
315 def get_replied_to_visibility(nil), do: nil
317 def get_replied_to_visibility(activity) do
318 with %Object{} = object <- Object.normalize(activity, fetch: false) do
319 Visibility.get_visibility(object)
323 def get_quoted_visibility(nil), do: nil
325 def get_quoted_visibility(activity), do: get_replied_to_visibility(activity)
327 def check_expiry_date({:ok, nil} = res), do: res
329 def check_expiry_date({:ok, in_seconds}) do
330 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
332 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
335 {:error, "Expiry date is too soon"}
339 def check_expiry_date(expiry_str) do
340 Ecto.Type.cast(:integer, expiry_str)
341 |> check_expiry_date()
344 def post(user, %{status: _} = data) do
345 with {:ok, draft} <- ActivityDraft.create(user, data) do
346 ActivityPub.create(draft.changes, draft.preview?)
350 def update(user, orig_activity, changes) do
351 with orig_object <- Object.normalize(orig_activity),
352 {:ok, new_object} <- make_update_data(user, orig_object, changes),
353 {:ok, update_data, _} <- Builder.update(user, new_object),
354 {:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
361 defp make_update_data(user, orig_object, changes) do
363 visibility: Visibility.get_visibility(orig_object),
365 with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
366 %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
373 params = Map.merge(changes, kept_params)
375 with {:ok, draft} <- ActivityDraft.create(user, params) do
377 Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
385 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
386 def pin(id, %User{} = user) do
387 with %Activity{} = activity <- create_activity_by_id(id),
388 true <- activity_belongs_to_actor(activity, user.ap_id),
389 true <- object_type_is_allowed_for_pin(activity.object),
390 true <- activity_is_public(activity),
391 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
393 Pipeline.common_pipeline(pin_data,
399 {:error, {:side_effects, error}} -> error
404 defp create_activity_by_id(id) do
405 with nil <- Activity.create_by_id_with_object(id) do
410 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
411 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
413 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
414 with false <- type in ["Note", "Article", "Question"] do
415 {:error, :not_allowed}
419 defp activity_is_public(activity) do
420 with false <- Visibility.is_public?(activity) do
421 {:error, :visibility_error}
425 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
426 def unpin(id, user) do
427 with %Activity{} = activity <- create_activity_by_id(id),
428 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
430 Pipeline.common_pipeline(unpin_data,
432 activity_id: activity.id,
433 expires_at: activity.data["expires_at"],
434 featured_address: user.featured_address
440 def add_mute(user, activity, params \\ %{}) do
441 expires_in = Map.get(params, :expires_in, 0)
443 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
444 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
446 Pleroma.Workers.MuteExpireWorker.enqueue(
447 "unmute_conversation",
448 %{"user_id" => user.id, "activity_id" => activity.id},
449 schedule_in: expires_in
455 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
459 def remove_mute(%User{} = user, %Activity{} = activity) do
460 ThreadMute.remove_mute(user.id, activity.data["context"])
464 def remove_mute(user_id, activity_id) do
465 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
466 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
467 remove_mute(user, activity)
469 {what, result} = error ->
471 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
478 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
479 when is_binary(context) do
480 ThreadMute.exists?(user_id, context)
483 def thread_muted?(_, _), do: false
485 def report(user, data) do
486 with {:ok, account} <- get_reported_account(data.account_id),
487 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
488 {:ok, statuses} <- get_report_statuses(account, data) do
490 context: Utils.generate_context_id(),
494 content: content_html,
495 forward: Map.get(data, :forward, false)
500 defp get_reported_account(account_id) do
501 case User.get_cached_by_id(account_id) do
502 %User{} = account -> {:ok, account}
503 _ -> {:error, dgettext("errors", "Account not found")}
507 def update_report_state(activity_ids, state) when is_list(activity_ids) do
508 case Utils.update_report_state(activity_ids, state) do
509 :ok -> {:ok, activity_ids}
510 _ -> {:error, dgettext("errors", "Could not update state")}
514 def update_report_state(activity_id, state) do
515 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
516 Utils.update_report_state(activity, state)
518 nil -> {:error, :not_found}
519 _ -> {:error, dgettext("errors", "Could not update state")}
523 def update_activity_scope(activity_id, opts \\ %{}) do
524 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
525 {:ok, activity} <- toggle_sensitive(activity, opts) do
526 set_visibility(activity, opts)
528 nil -> {:error, :not_found}
529 {:error, reason} -> {:error, reason}
533 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
534 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
537 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
538 when is_boolean(sensitive) do
539 new_data = Map.put(object.data, "sensitive", sensitive)
543 |> Object.change(%{data: new_data})
544 |> Object.update_and_set_cache()
546 {:ok, Map.put(activity, :object, object)}
549 defp toggle_sensitive(activity, _), do: {:ok, activity}
551 defp set_visibility(activity, %{visibility: visibility}) do
552 Utils.update_activity_visibility(activity, visibility)
555 defp set_visibility(activity, _), do: {:ok, activity}
557 def hide_reblogs(%User{} = user, %User{} = target) do
558 UserRelationship.create_reblog_mute(user, target)
561 def show_reblogs(%User{} = user, %User{} = target) do
562 UserRelationship.delete_reblog_mute(user, target)
565 def get_user(ap_id, fake_record_fallback \\ true) do
567 user = User.get_cached_by_ap_id(ap_id) ->
570 user = User.get_by_guessed_nickname(ap_id) ->
573 fake_record_fallback ->
574 # TODO: refactor (fake records is never a good idea)
575 User.error_user(ap_id)