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
212 _ -> {:error, dgettext("errors", "Could not add reaction emoji")}
216 def unreact_with_emoji(id, user, emoji) do
217 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
218 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
219 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
223 {:error, dgettext("errors", "Could not remove reaction emoji")}
227 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
228 with :ok <- validate_not_author(object, user),
229 :ok <- validate_existing_votes(user, object),
230 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
232 Enum.map(choices, fn index ->
233 {:ok, answer_object, _meta} =
234 Builder.answer(user, object, Enum.at(options, index)["name"])
236 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
238 {:ok, activity, _meta} =
240 |> Map.put("cc", answer_object["cc"])
241 |> Map.put("context", answer_object["context"])
242 |> Pipeline.common_pipeline(local: true)
244 # TODO: Do preload of Pleroma.Object in Pipeline
245 Activity.normalize(activity.data)
248 object = Object.get_cached_by_ap_id(object.data["id"])
249 {:ok, answer_activities, object}
253 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
254 do: {:error, dgettext("errors", "Poll's author can't vote")}
256 defp validate_not_author(_, _), do: :ok
258 defp validate_existing_votes(%{ap_id: ap_id}, object) do
259 if Utils.get_existing_votes(ap_id, object) == [] do
262 {:error, dgettext("errors", "Already voted")}
266 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
267 when is_list(any_of) and any_of != [],
268 do: {any_of, Enum.count(any_of)}
270 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
271 when is_list(one_of) and one_of != [],
274 defp normalize_and_validate_choices(choices, object) do
275 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
276 {options, max_count} = get_options_and_max_count(object)
277 count = Enum.count(options)
279 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
280 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
281 {:ok, options, choices}
283 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
284 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
288 def public_announce?(_, %{visibility: visibility})
289 when visibility in ~w{public unlisted private direct},
290 do: visibility in ~w(public unlisted)
292 def public_announce?(object, _) do
293 Visibility.is_public?(object)
296 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
298 def get_visibility(%{visibility: visibility}, in_reply_to, _)
299 when visibility in ~w{public local unlisted private direct},
300 do: {visibility, get_replied_to_visibility(in_reply_to)}
302 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
303 visibility = {:list, String.to_integer(list_id)}
304 {visibility, get_replied_to_visibility(in_reply_to)}
307 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
308 visibility = get_replied_to_visibility(in_reply_to)
309 {visibility, visibility}
312 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
314 def get_replied_to_visibility(nil), do: nil
316 def get_replied_to_visibility(activity) do
317 with %Object{} = object <- Object.normalize(activity, fetch: false) do
318 Visibility.get_visibility(object)
322 def get_quoted_visibility(nil), do: nil
324 def get_quoted_visibility(activity), do: get_replied_to_visibility(activity)
326 def check_expiry_date({:ok, nil} = res), do: res
328 def check_expiry_date({:ok, in_seconds}) do
329 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
331 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
334 {:error, "Expiry date is too soon"}
338 def check_expiry_date(expiry_str) do
339 Ecto.Type.cast(:integer, expiry_str)
340 |> check_expiry_date()
343 def post(user, %{status: _} = data) do
344 with {:ok, draft} <- ActivityDraft.create(user, data) do
345 ActivityPub.create(draft.changes, draft.preview?)
349 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
350 def pin(id, %User{} = user) do
351 with %Activity{} = activity <- create_activity_by_id(id),
352 true <- activity_belongs_to_actor(activity, user.ap_id),
353 true <- object_type_is_allowed_for_pin(activity.object),
354 true <- activity_is_public(activity),
355 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
357 Pipeline.common_pipeline(pin_data,
363 {:error, {:side_effects, error}} -> error
368 defp create_activity_by_id(id) do
369 with nil <- Activity.create_by_id_with_object(id) do
374 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
375 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
377 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
378 with false <- type in ["Note", "Article", "Question"] do
379 {:error, :not_allowed}
383 defp activity_is_public(activity) do
384 with false <- Visibility.is_public?(activity) do
385 {:error, :visibility_error}
389 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
390 def unpin(id, user) do
391 with %Activity{} = activity <- create_activity_by_id(id),
392 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
394 Pipeline.common_pipeline(unpin_data,
396 activity_id: activity.id,
397 expires_at: activity.data["expires_at"],
398 featured_address: user.featured_address
404 def add_mute(user, activity, params \\ %{}) do
405 expires_in = Map.get(params, :expires_in, 0)
407 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
408 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
410 Pleroma.Workers.MuteExpireWorker.enqueue(
411 "unmute_conversation",
412 %{"user_id" => user.id, "activity_id" => activity.id},
413 schedule_in: expires_in
419 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
423 def remove_mute(%User{} = user, %Activity{} = activity) do
424 ThreadMute.remove_mute(user.id, activity.data["context"])
428 def remove_mute(user_id, activity_id) do
429 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
430 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
431 remove_mute(user, activity)
433 {what, result} = error ->
435 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
442 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
443 when is_binary(context) do
444 ThreadMute.exists?(user_id, context)
447 def thread_muted?(_, _), do: false
449 def report(user, data) do
450 with {:ok, account} <- get_reported_account(data.account_id),
451 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
452 {:ok, statuses} <- get_report_statuses(account, data) do
454 context: Utils.generate_context_id(),
458 content: content_html,
459 forward: Map.get(data, :forward, false)
464 defp get_reported_account(account_id) do
465 case User.get_cached_by_id(account_id) do
466 %User{} = account -> {:ok, account}
467 _ -> {:error, dgettext("errors", "Account not found")}
471 def update_report_state(activity_ids, state) when is_list(activity_ids) do
472 case Utils.update_report_state(activity_ids, state) do
473 :ok -> {:ok, activity_ids}
474 _ -> {:error, dgettext("errors", "Could not update state")}
478 def update_report_state(activity_id, state) do
479 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
480 Utils.update_report_state(activity, state)
482 nil -> {:error, :not_found}
483 _ -> {:error, dgettext("errors", "Could not update state")}
487 def update_activity_scope(activity_id, opts \\ %{}) do
488 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
489 {:ok, activity} <- toggle_sensitive(activity, opts) do
490 set_visibility(activity, opts)
492 nil -> {:error, :not_found}
493 {:error, reason} -> {:error, reason}
497 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
498 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
501 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
502 when is_boolean(sensitive) do
503 new_data = Map.put(object.data, "sensitive", sensitive)
507 |> Object.change(%{data: new_data})
508 |> Object.update_and_set_cache()
510 {:ok, Map.put(activity, :object, object)}
513 defp toggle_sensitive(activity, _), do: {:ok, activity}
515 defp set_visibility(activity, %{visibility: visibility}) do
516 Utils.update_activity_visibility(activity, visibility)
519 defp set_visibility(activity, _), do: {:ok, activity}
521 def hide_reblogs(%User{} = user, %User{} = target) do
522 UserRelationship.create_reblog_mute(user, target)
525 def show_reblogs(%User{} = user, %User{} = target) do
526 UserRelationship.delete_reblog_mute(user, target)
529 def get_user(ap_id, fake_record_fallback \\ true) do
531 user = User.get_cached_by_ap_id(ap_id) ->
534 user = User.get_by_guessed_nickname(ap_id) ->
537 fake_record_fallback ->
538 # TODO: refactor (fake records is never a good idea)
539 User.error_user(ap_id)