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 <- 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
35 def follow(follower, followed) do
36 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
38 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
39 {:ok, activity} <- ActivityPub.follow(follower, followed),
40 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
41 {:ok, follower, followed, activity}
45 def unfollow(follower, unfollowed) do
46 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
47 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
48 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
53 def accept_follow_request(follower, followed) do
54 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
55 {:ok, follower} <- User.follow(follower, followed),
56 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
57 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
62 object: follow_activity.data["id"],
69 def reject_follow_request(follower, followed) do
70 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
71 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
72 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
73 {:ok, _notifications} <- Notification.dismiss(follow_activity),
78 object: follow_activity.data["id"],
85 def delete(activity_id, user) do
86 with {_, %Activity{data: %{"object" => _}} = activity} <-
87 {:find_activity, Activity.get_by_id_with_object(activity_id)},
88 %Object{} = object <- Object.normalize(activity),
89 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
90 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
91 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
94 {:find_activity, _} -> {:error, :not_found}
95 _ -> {:error, dgettext("errors", "Could not delete")}
99 def repeat(id, user, params \\ %{}) do
100 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
101 {:find_activity, Activity.get_by_id(id)},
102 object <- Object.normalize(activity),
103 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
104 public <- public_announce?(object, params) do
105 if announce_activity do
106 {:ok, announce_activity, object}
108 ActivityPub.announce(user, object, nil, true, public)
111 {:find_activity, _} -> {:error, :not_found}
112 _ -> {:error, dgettext("errors", "Could not repeat")}
116 def unrepeat(id, user) do
117 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
118 {:find_activity, Activity.get_by_id(id)},
119 %Object{} = note <- Object.normalize(activity, false),
120 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
121 {:ok, undo, _} <- Builder.undo(user, announce),
122 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
125 {:find_activity, _} -> {:error, :not_found}
126 _ -> {:error, dgettext("errors", "Could not unrepeat")}
130 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
131 def favorite(%User{} = user, id) do
132 case favorite_helper(user, id) do
136 {:error, :not_found} = res ->
140 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
141 {:error, dgettext("errors", "Could not favorite")}
145 def favorite_helper(user, id) do
146 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
147 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
148 {_, {:ok, %Activity{} = activity, _meta}} <-
150 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
167 if {:object, {"already liked by this actor", []}} in changeset.errors do
168 {:ok, :already_liked}
178 def unfavorite(id, user) do
179 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
180 {:find_activity, Activity.get_by_id(id)},
181 %Object{} = note <- Object.normalize(activity, false),
182 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
183 {:ok, undo, _} <- Builder.undo(user, like),
184 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
187 {:find_activity, _} -> {:error, :not_found}
188 _ -> {:error, dgettext("errors", "Could not unfavorite")}
192 def react_with_emoji(id, user, emoji) do
193 with %Activity{} = activity <- Activity.get_by_id(id),
194 object <- Object.normalize(activity),
195 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
196 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
200 {:error, dgettext("errors", "Could not add reaction emoji")}
204 def unreact_with_emoji(id, user, emoji) do
205 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
206 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
207 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
211 {:error, dgettext("errors", "Could not remove reaction emoji")}
215 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
216 with :ok <- validate_not_author(object, user),
217 :ok <- validate_existing_votes(user, object),
218 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
220 Enum.map(choices, fn index ->
221 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
224 ActivityPub.create(%{
225 to: answer_data["to"],
227 context: object.data["context"],
229 additional: %{"cc" => answer_data["cc"]}
235 object = Object.get_cached_by_ap_id(object.data["id"])
236 {:ok, answer_activities, object}
240 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
241 do: {:error, dgettext("errors", "Poll's author can't vote")}
243 defp validate_not_author(_, _), do: :ok
245 defp validate_existing_votes(%{ap_id: ap_id}, object) do
246 if Utils.get_existing_votes(ap_id, object) == [] do
249 {:error, dgettext("errors", "Already voted")}
253 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
254 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
256 defp normalize_and_validate_choices(choices, object) do
257 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
258 {options, max_count} = get_options_and_max_count(object)
259 count = Enum.count(options)
261 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
262 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
263 {:ok, options, choices}
265 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
266 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
270 def public_announce?(_, %{"visibility" => visibility})
271 when visibility in ~w{public unlisted private direct},
272 do: visibility in ~w(public unlisted)
274 def public_announce?(object, _) do
275 Visibility.is_public?(object)
278 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
280 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
281 when visibility in ~w{public unlisted private direct},
282 do: {visibility, get_replied_to_visibility(in_reply_to)}
284 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
285 visibility = {:list, String.to_integer(list_id)}
286 {visibility, get_replied_to_visibility(in_reply_to)}
289 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
290 visibility = get_replied_to_visibility(in_reply_to)
291 {visibility, visibility}
294 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
296 def get_replied_to_visibility(nil), do: nil
298 def get_replied_to_visibility(activity) do
299 with %Object{} = object <- Object.normalize(activity) do
300 Visibility.get_visibility(object)
304 def check_expiry_date({:ok, nil} = res), do: res
306 def check_expiry_date({:ok, in_seconds}) do
307 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
309 if ActivityExpiration.expires_late_enough?(expiry) do
312 {:error, "Expiry date is too soon"}
316 def check_expiry_date(expiry_str) do
317 Ecto.Type.cast(:integer, expiry_str)
318 |> check_expiry_date()
321 def listen(user, %{"title" => _} = data) do
322 with visibility <- data["visibility"] || "public",
323 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
325 Map.take(data, ["album", "artist", "title", "length"])
326 |> Map.put("type", "Audio")
329 |> Map.put("actor", user.ap_id),
331 ActivityPub.listen(%{
335 context: Utils.generate_context_id(),
336 additional: %{"cc" => cc}
342 def post(user, %{"status" => _} = data) do
343 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
345 |> ActivityPub.create(draft.preview?)
346 |> maybe_create_activity_expiration(draft.expires_at)
350 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
351 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
356 defp maybe_create_activity_expiration(result, _), do: result
358 def pin(id, %{ap_id: user_ap_id} = user) do
361 data: %{"type" => "Create"},
362 object: %Object{data: %{"type" => object_type}}
363 } = activity <- Activity.get_by_id_with_object(id),
364 true <- object_type in ["Note", "Article", "Question"],
365 true <- Visibility.is_public?(activity),
366 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
369 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
370 _ -> {:error, dgettext("errors", "Could not pin")}
374 def unpin(id, user) do
375 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
376 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
379 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
380 _ -> {:error, dgettext("errors", "Could not unpin")}
384 def add_mute(user, activity) do
385 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
388 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
392 def remove_mute(user, activity) do
393 ThreadMute.remove_mute(user.id, activity.data["context"])
397 def thread_muted?(%{id: nil} = _user, _activity), do: false
399 def thread_muted?(user, activity) do
400 ThreadMute.exists?(user.id, activity.data["context"])
403 def report(user, data) do
404 with {:ok, account} <- get_reported_account(data.account_id),
405 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
406 {:ok, statuses} <- get_report_statuses(account, data) do
408 context: Utils.generate_context_id(),
412 content: content_html,
413 forward: Map.get(data, :forward, false)
418 defp get_reported_account(account_id) do
419 case User.get_cached_by_id(account_id) do
420 %User{} = account -> {:ok, account}
421 _ -> {:error, dgettext("errors", "Account not found")}
425 def update_report_state(activity_ids, state) when is_list(activity_ids) do
426 case Utils.update_report_state(activity_ids, state) do
427 :ok -> {:ok, activity_ids}
428 _ -> {:error, dgettext("errors", "Could not update state")}
432 def update_report_state(activity_id, state) do
433 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
434 Utils.update_report_state(activity, state)
436 nil -> {:error, :not_found}
437 _ -> {:error, dgettext("errors", "Could not update state")}
441 def update_activity_scope(activity_id, opts \\ %{}) do
442 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
443 {:ok, activity} <- toggle_sensitive(activity, opts) do
444 set_visibility(activity, opts)
446 nil -> {:error, :not_found}
447 {:error, reason} -> {:error, reason}
451 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
452 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
455 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
456 when is_boolean(sensitive) do
457 new_data = Map.put(object.data, "sensitive", sensitive)
461 |> Object.change(%{data: new_data})
462 |> Object.update_and_set_cache()
464 {:ok, Map.put(activity, :object, object)}
467 defp toggle_sensitive(activity, _), do: {:ok, activity}
469 defp set_visibility(activity, %{"visibility" => visibility}) do
470 Utils.update_activity_visibility(activity, visibility)
473 defp set_visibility(activity, _), do: {:ok, activity}
475 def hide_reblogs(%User{} = user, %User{} = target) do
476 UserRelationship.create_reblog_mute(user, target)
479 def show_reblogs(%User{} = user, %User{} = target) do
480 UserRelationship.delete_reblog_mute(user, target)