1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.CommonAPI do
7 alias Pleroma.ActivityExpiration
8 alias Pleroma.Conversation.Participation
9 alias Pleroma.FollowingRelationship
10 alias Pleroma.Notification
12 alias Pleroma.ThreadMute
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
24 require Pleroma.Constants
27 def unblock(blocker, blocked) do
28 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
29 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
30 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
33 {:fetch_block, nil} ->
34 if User.blocks?(blocker, blocked) do
35 User.unblock(blocker, blocked)
38 {:error, :not_blocking}
46 def follow(follower, followed) do
47 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
49 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
50 {:ok, activity} <- ActivityPub.follow(follower, followed),
51 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
52 {:ok, follower, followed, activity}
56 def unfollow(follower, unfollowed) do
57 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
58 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
59 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
64 def accept_follow_request(follower, followed) do
65 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
66 {:ok, follower} <- User.follow(follower, followed),
67 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
68 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
73 object: follow_activity.data["id"],
80 def reject_follow_request(follower, followed) do
81 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
82 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
83 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
84 {:ok, _notifications} <- Notification.dismiss(follow_activity),
89 object: follow_activity.data["id"],
96 def delete(activity_id, user) do
97 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
98 {:find_activity, Activity.get_by_id(activity_id)},
99 {_, %Object{} = object, _} <-
100 {:find_object, Object.normalize(activity, false), activity},
101 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
102 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
103 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
106 {:find_activity, _} ->
109 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
110 # We have the create activity, but not the object, it was probably pruned.
111 # Insert a tombstone and try again
112 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
113 {:ok, _tombstone} <- Object.create(tombstone_data) do
114 delete(activity_id, user)
118 "Could not insert tombstone for missing object on deletion. Object is #{object}."
121 {:error, dgettext("errors", "Could not delete")}
125 {:error, dgettext("errors", "Could not delete")}
129 def repeat(id, user, params \\ %{}) do
130 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
131 object = %Object{} <- Object.normalize(activity, false),
132 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
133 public = public_announce?(object, params),
134 {:ok, announce, _} <- Builder.announce(user, object, public: public),
135 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
138 {:existing_announce, %Activity{} = announce} ->
146 def unrepeat(id, user) do
147 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
148 {:find_activity, Activity.get_by_id(id)},
149 %Object{} = note <- Object.normalize(activity, false),
150 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
151 {:ok, undo, _} <- Builder.undo(user, announce),
152 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
155 {:find_activity, _} -> {:error, :not_found}
156 _ -> {:error, dgettext("errors", "Could not unrepeat")}
160 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
161 def favorite(%User{} = user, id) do
162 case favorite_helper(user, id) do
166 {:error, :not_found} = res ->
170 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
171 {:error, dgettext("errors", "Could not favorite")}
175 def favorite_helper(user, id) do
176 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
177 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
178 {_, {:ok, %Activity{} = activity, _meta}} <-
180 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
197 if {:object, {"already liked by this actor", []}} in changeset.errors do
198 {:ok, :already_liked}
208 def unfavorite(id, user) do
209 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
210 {:find_activity, Activity.get_by_id(id)},
211 %Object{} = note <- Object.normalize(activity, false),
212 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
213 {:ok, undo, _} <- Builder.undo(user, like),
214 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
217 {:find_activity, _} -> {:error, :not_found}
218 _ -> {:error, dgettext("errors", "Could not unfavorite")}
222 def react_with_emoji(id, user, emoji) do
223 with %Activity{} = activity <- Activity.get_by_id(id),
224 object <- Object.normalize(activity),
225 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
226 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
230 {:error, dgettext("errors", "Could not add reaction emoji")}
234 def unreact_with_emoji(id, user, emoji) do
235 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
236 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
237 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
241 {:error, dgettext("errors", "Could not remove reaction emoji")}
245 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
246 with :ok <- validate_not_author(object, user),
247 :ok <- validate_existing_votes(user, object),
248 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
250 Enum.map(choices, fn index ->
251 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
254 ActivityPub.create(%{
255 to: answer_data["to"],
257 context: object.data["context"],
259 additional: %{"cc" => answer_data["cc"]}
265 object = Object.get_cached_by_ap_id(object.data["id"])
266 {:ok, answer_activities, object}
270 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
271 do: {:error, dgettext("errors", "Poll's author can't vote")}
273 defp validate_not_author(_, _), do: :ok
275 defp validate_existing_votes(%{ap_id: ap_id}, object) do
276 if Utils.get_existing_votes(ap_id, object) == [] do
279 {:error, dgettext("errors", "Already voted")}
283 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
284 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
286 defp normalize_and_validate_choices(choices, object) do
287 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
288 {options, max_count} = get_options_and_max_count(object)
289 count = Enum.count(options)
291 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
292 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
293 {:ok, options, choices}
295 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
296 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
300 def public_announce?(_, %{visibility: visibility})
301 when visibility in ~w{public unlisted private direct},
302 do: visibility in ~w(public unlisted)
304 def public_announce?(object, _) do
305 Visibility.is_public?(object)
308 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
310 def get_visibility(%{visibility: visibility}, in_reply_to, _)
311 when visibility in ~w{public unlisted private direct},
312 do: {visibility, get_replied_to_visibility(in_reply_to)}
314 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
315 visibility = {:list, String.to_integer(list_id)}
316 {visibility, get_replied_to_visibility(in_reply_to)}
319 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
320 visibility = get_replied_to_visibility(in_reply_to)
321 {visibility, visibility}
324 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
326 def get_replied_to_visibility(nil), do: nil
328 def get_replied_to_visibility(activity) do
329 with %Object{} = object <- Object.normalize(activity) do
330 Visibility.get_visibility(object)
334 def check_expiry_date({:ok, nil} = res), do: res
336 def check_expiry_date({:ok, in_seconds}) do
337 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
339 if ActivityExpiration.expires_late_enough?(expiry) do
342 {:error, "Expiry date is too soon"}
346 def check_expiry_date(expiry_str) do
347 Ecto.Type.cast(:integer, expiry_str)
348 |> check_expiry_date()
351 def listen(user, data) do
352 visibility = Map.get(data, :visibility, "public")
354 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
357 |> Map.take([:album, :artist, :title, :length])
358 |> Map.new(fn {key, value} -> {to_string(key), value} end)
359 |> Map.put("type", "Audio")
362 |> Map.put("actor", user.ap_id),
364 ActivityPub.listen(%{
368 context: Utils.generate_context_id(),
369 additional: %{"cc" => cc}
375 def post(user, %{status: _} = data) do
376 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
377 ActivityPub.create(draft.changes, draft.preview?)
381 def pin(id, %{ap_id: user_ap_id} = user) do
384 data: %{"type" => "Create"},
385 object: %Object{data: %{"type" => object_type}}
386 } = activity <- Activity.get_by_id_with_object(id),
387 true <- object_type in ["Note", "Article", "Question"],
388 true <- Visibility.is_public?(activity),
389 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
392 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
393 _ -> {:error, dgettext("errors", "Could not pin")}
397 def unpin(id, user) do
398 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
399 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
402 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
403 _ -> {:error, dgettext("errors", "Could not unpin")}
407 def add_mute(user, activity) do
408 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
411 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
415 def remove_mute(user, activity) do
416 ThreadMute.remove_mute(user.id, activity.data["context"])
420 def thread_muted?(%{id: nil} = _user, _activity), do: false
422 def thread_muted?(user, activity) do
423 ThreadMute.exists?(user.id, activity.data["context"])
426 def report(user, data) do
427 with {:ok, account} <- get_reported_account(data.account_id),
428 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
429 {:ok, statuses} <- get_report_statuses(account, data) do
431 context: Utils.generate_context_id(),
435 content: content_html,
436 forward: Map.get(data, :forward, false)
441 defp get_reported_account(account_id) do
442 case User.get_cached_by_id(account_id) do
443 %User{} = account -> {:ok, account}
444 _ -> {:error, dgettext("errors", "Account not found")}
448 def update_report_state(activity_ids, state) when is_list(activity_ids) do
449 case Utils.update_report_state(activity_ids, state) do
450 :ok -> {:ok, activity_ids}
451 _ -> {:error, dgettext("errors", "Could not update state")}
455 def update_report_state(activity_id, state) do
456 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
457 Utils.update_report_state(activity, state)
459 nil -> {:error, :not_found}
460 _ -> {:error, dgettext("errors", "Could not update state")}
464 def update_activity_scope(activity_id, opts \\ %{}) do
465 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
466 {:ok, activity} <- toggle_sensitive(activity, opts) do
467 set_visibility(activity, opts)
469 nil -> {:error, :not_found}
470 {:error, reason} -> {:error, reason}
474 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
475 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
478 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
479 when is_boolean(sensitive) do
480 new_data = Map.put(object.data, "sensitive", sensitive)
484 |> Object.change(%{data: new_data})
485 |> Object.update_and_set_cache()
487 {:ok, Map.put(activity, :object, object)}
490 defp toggle_sensitive(activity, _), do: {:ok, activity}
492 defp set_visibility(activity, %{visibility: visibility}) do
493 Utils.update_activity_visibility(activity, visibility)
496 defp set_visibility(activity, _), do: {:ok, activity}
498 def hide_reblogs(%User{} = user, %User{} = target) do
499 UserRelationship.create_reblog_mute(user, target)
502 def show_reblogs(%User{} = user, %User{} = target) do
503 UserRelationship.delete_reblog_mute(user, target)