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 check_expiry_date({:ok, nil} = res), do: res
324 def check_expiry_date({:ok, in_seconds}) do
325 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
327 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
330 {:error, "Expiry date is too soon"}
334 def check_expiry_date(expiry_str) do
335 Ecto.Type.cast(:integer, expiry_str)
336 |> check_expiry_date()
339 def post(user, %{status: _} = data) do
340 with {:ok, draft} <- ActivityDraft.create(user, data) do
341 ActivityPub.create(draft.changes, draft.preview?)
345 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
346 def pin(id, %User{} = user) do
347 with %Activity{} = activity <- create_activity_by_id(id),
348 true <- activity_belongs_to_actor(activity, user.ap_id),
349 true <- object_type_is_allowed_for_pin(activity.object),
350 true <- activity_is_public(activity),
351 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
353 Pipeline.common_pipeline(pin_data,
359 {:error, {:side_effects, error}} -> error
364 defp create_activity_by_id(id) do
365 with nil <- Activity.create_by_id_with_object(id) do
370 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
371 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
373 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
374 with false <- type in ["Note", "Article", "Question"] do
375 {:error, :not_allowed}
379 defp activity_is_public(activity) do
380 with false <- Visibility.is_public?(activity) do
381 {:error, :visibility_error}
385 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
386 def unpin(id, user) do
387 with %Activity{} = activity <- create_activity_by_id(id),
388 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
390 Pipeline.common_pipeline(unpin_data,
392 activity_id: activity.id,
393 expires_at: activity.data["expires_at"],
394 featured_address: user.featured_address
400 def add_mute(user, activity, params \\ %{}) do
401 expires_in = Map.get(params, :expires_in, 0)
403 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
404 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
406 Pleroma.Workers.MuteExpireWorker.enqueue(
407 "unmute_conversation",
408 %{"user_id" => user.id, "activity_id" => activity.id},
409 schedule_in: expires_in
415 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
419 def remove_mute(%User{} = user, %Activity{} = activity) do
420 ThreadMute.remove_mute(user.id, activity.data["context"])
424 def remove_mute(user_id, activity_id) do
425 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
426 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
427 remove_mute(user, activity)
429 {what, result} = error ->
431 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
438 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
439 when is_binary(context) do
440 ThreadMute.exists?(user_id, context)
443 def thread_muted?(_, _), do: false
445 def report(user, data) do
446 with {:ok, account} <- get_reported_account(data.account_id),
447 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
448 {:ok, statuses} <- get_report_statuses(account, data) do
450 context: Utils.generate_context_id(),
454 content: content_html,
455 forward: Map.get(data, :forward, false)
460 defp get_reported_account(account_id) do
461 case User.get_cached_by_id(account_id) do
462 %User{} = account -> {:ok, account}
463 _ -> {:error, dgettext("errors", "Account not found")}
467 def update_report_state(activity_ids, state) when is_list(activity_ids) do
468 case Utils.update_report_state(activity_ids, state) do
469 :ok -> {:ok, activity_ids}
470 _ -> {:error, dgettext("errors", "Could not update state")}
474 def update_report_state(activity_id, state) do
475 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
476 Utils.update_report_state(activity, state)
478 nil -> {:error, :not_found}
479 _ -> {:error, dgettext("errors", "Could not update state")}
483 def update_activity_scope(activity_id, opts \\ %{}) do
484 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
485 {:ok, activity} <- toggle_sensitive(activity, opts) do
486 set_visibility(activity, opts)
488 nil -> {:error, :not_found}
489 {:error, reason} -> {:error, reason}
493 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
494 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
497 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
498 when is_boolean(sensitive) do
499 new_data = Map.put(object.data, "sensitive", sensitive)
503 |> Object.change(%{data: new_data})
504 |> Object.update_and_set_cache()
506 {:ok, Map.put(activity, :object, object)}
509 defp toggle_sensitive(activity, _), do: {:ok, activity}
511 defp set_visibility(activity, %{visibility: visibility}) do
512 Utils.update_activity_visibility(activity, visibility)
515 defp set_visibility(activity, _), do: {:ok, activity}
517 def hide_reblogs(%User{} = user, %User{} = target) do
518 UserRelationship.create_reblog_mute(user, target)
521 def show_reblogs(%User{} = user, %User{} = target) do
522 UserRelationship.delete_reblog_mute(user, target)
525 def get_user(ap_id, fake_record_fallback \\ true) do
527 user = User.get_cached_by_ap_id(ap_id) ->
530 user = User.get_by_guessed_nickname(ap_id) ->
533 fake_record_fallback ->
534 # TODO: refactor (fake records is never a good idea)
535 User.error_user(ap_id)