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 @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
351 def pin(id, %User{} = user) do
352 with %Activity{} = activity <- create_activity_by_id(id),
353 true <- activity_belongs_to_actor(activity, user.ap_id),
354 true <- object_type_is_allowed_for_pin(activity.object),
355 true <- activity_is_public(activity),
356 {:ok, pin_data, _} <- Builder.pin(user, activity.object),
358 Pipeline.common_pipeline(pin_data,
364 {:error, {:side_effects, error}} -> error
369 defp create_activity_by_id(id) do
370 with nil <- Activity.create_by_id_with_object(id) do
375 defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
376 defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
378 defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
379 with false <- type in ["Note", "Article", "Question"] do
380 {:error, :not_allowed}
384 defp activity_is_public(activity) do
385 with false <- Visibility.is_public?(activity) do
386 {:error, :visibility_error}
390 @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
391 def unpin(id, user) do
392 with %Activity{} = activity <- create_activity_by_id(id),
393 {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
395 Pipeline.common_pipeline(unpin_data,
397 activity_id: activity.id,
398 expires_at: activity.data["expires_at"],
399 featured_address: user.featured_address
405 def add_mute(user, activity, params \\ %{}) do
406 expires_in = Map.get(params, :expires_in, 0)
408 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
409 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
411 Pleroma.Workers.MuteExpireWorker.enqueue(
412 "unmute_conversation",
413 %{"user_id" => user.id, "activity_id" => activity.id},
414 schedule_in: expires_in
420 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
424 def remove_mute(%User{} = user, %Activity{} = activity) do
425 ThreadMute.remove_mute(user.id, activity.data["context"])
429 def remove_mute(user_id, activity_id) do
430 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
431 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
432 remove_mute(user, activity)
434 {what, result} = error ->
436 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
443 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
444 when is_binary(context) do
445 ThreadMute.exists?(user_id, context)
448 def thread_muted?(_, _), do: false
450 def report(user, data) do
451 with {:ok, account} <- get_reported_account(data.account_id),
452 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
453 {:ok, statuses} <- get_report_statuses(account, data) do
455 context: Utils.generate_context_id(),
459 content: content_html,
460 forward: Map.get(data, :forward, false)
465 defp get_reported_account(account_id) do
466 case User.get_cached_by_id(account_id) do
467 %User{} = account -> {:ok, account}
468 _ -> {:error, dgettext("errors", "Account not found")}
472 def update_report_state(activity_ids, state) when is_list(activity_ids) do
473 case Utils.update_report_state(activity_ids, state) do
474 :ok -> {:ok, activity_ids}
475 _ -> {:error, dgettext("errors", "Could not update state")}
479 def update_report_state(activity_id, state) do
480 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
481 Utils.update_report_state(activity, state)
483 nil -> {:error, :not_found}
484 _ -> {:error, dgettext("errors", "Could not update state")}
488 def update_activity_scope(activity_id, opts \\ %{}) do
489 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
490 {:ok, activity} <- toggle_sensitive(activity, opts) do
491 set_visibility(activity, opts)
493 nil -> {:error, :not_found}
494 {:error, reason} -> {:error, reason}
498 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
499 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
502 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
503 when is_boolean(sensitive) do
504 new_data = Map.put(object.data, "sensitive", sensitive)
508 |> Object.change(%{data: new_data})
509 |> Object.update_and_set_cache()
511 {:ok, Map.put(activity, :object, object)}
514 defp toggle_sensitive(activity, _), do: {:ok, activity}
516 defp set_visibility(activity, %{visibility: visibility}) do
517 Utils.update_activity_visibility(activity, visibility)
520 defp set_visibility(activity, _), do: {:ok, activity}
522 def hide_reblogs(%User{} = user, %User{} = target) do
523 UserRelationship.create_reblog_mute(user, target)
526 def show_reblogs(%User{} = user, %User{} = target) do
527 UserRelationship.delete_reblog_mute(user, target)
530 def get_user(ap_id, fake_record_fallback \\ true) do
532 user = User.get_cached_by_ap_id(ap_id) ->
535 user = User.get_by_guessed_nickname(ap_id) ->
538 fake_record_fallback ->
539 # TODO: refactor (fake records is never a good idea)
540 User.error_user(ap_id)