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" => _, "type" => "Create"}} = activity} <-
87 {:find_activity, Activity.get_by_id(activity_id)},
88 {_, %Object{} = object, _} <-
89 {:find_object, Object.normalize(activity, false), activity},
90 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
91 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
92 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
95 {:find_activity, _} ->
98 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
99 # We have the create activity, but not the object, it was probably pruned.
100 # Insert a tombstone and try again
101 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
102 {:ok, _tombstone} <- Object.create(tombstone_data) do
103 delete(activity_id, user)
107 "Could not insert tombstone for missing object on deletion. Object is #{object}."
110 {:error, dgettext("errors", "Could not delete")}
114 {:error, dgettext("errors", "Could not delete")}
118 def repeat(id, user, params \\ %{}) do
119 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
120 {:find_activity, Activity.get_by_id(id)},
121 object <- Object.normalize(activity),
122 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
123 public <- public_announce?(object, params) do
124 if announce_activity do
125 {:ok, announce_activity, object}
127 ActivityPub.announce(user, object, nil, true, public)
130 {:find_activity, _} -> {:error, :not_found}
131 _ -> {:error, dgettext("errors", "Could not repeat")}
135 def unrepeat(id, user) do
136 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
137 {:find_activity, Activity.get_by_id(id)},
138 %Object{} = note <- Object.normalize(activity, false),
139 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
140 {:ok, undo, _} <- Builder.undo(user, announce),
141 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
144 {:find_activity, _} -> {:error, :not_found}
145 _ -> {:error, dgettext("errors", "Could not unrepeat")}
149 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
150 def favorite(%User{} = user, id) do
151 case favorite_helper(user, id) do
155 {:error, :not_found} = res ->
159 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
160 {:error, dgettext("errors", "Could not favorite")}
164 def favorite_helper(user, id) do
165 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
166 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
167 {_, {:ok, %Activity{} = activity, _meta}} <-
169 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
186 if {:object, {"already liked by this actor", []}} in changeset.errors do
187 {:ok, :already_liked}
197 def unfavorite(id, user) do
198 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
199 {:find_activity, Activity.get_by_id(id)},
200 %Object{} = note <- Object.normalize(activity, false),
201 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
202 {:ok, undo, _} <- Builder.undo(user, like),
203 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
206 {:find_activity, _} -> {:error, :not_found}
207 _ -> {:error, dgettext("errors", "Could not unfavorite")}
211 def react_with_emoji(id, user, emoji) do
212 with %Activity{} = activity <- Activity.get_by_id(id),
213 object <- Object.normalize(activity),
214 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
215 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
219 {:error, dgettext("errors", "Could not add reaction emoji")}
223 def unreact_with_emoji(id, user, emoji) do
224 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
225 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
226 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
230 {:error, dgettext("errors", "Could not remove reaction emoji")}
234 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
235 with :ok <- validate_not_author(object, user),
236 :ok <- validate_existing_votes(user, object),
237 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
239 Enum.map(choices, fn index ->
240 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
243 ActivityPub.create(%{
244 to: answer_data["to"],
246 context: object.data["context"],
248 additional: %{"cc" => answer_data["cc"]}
254 object = Object.get_cached_by_ap_id(object.data["id"])
255 {:ok, answer_activities, object}
259 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
260 do: {:error, dgettext("errors", "Poll's author can't vote")}
262 defp validate_not_author(_, _), do: :ok
264 defp validate_existing_votes(%{ap_id: ap_id}, object) do
265 if Utils.get_existing_votes(ap_id, object) == [] do
268 {:error, dgettext("errors", "Already voted")}
272 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
273 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
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 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) do
319 Visibility.get_visibility(object)
323 def check_expiry_date({:ok, nil} = res), do: res
325 def check_expiry_date({:ok, in_seconds}) do
326 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
328 if ActivityExpiration.expires_late_enough?(expiry) do
331 {:error, "Expiry date is too soon"}
335 def check_expiry_date(expiry_str) do
336 Ecto.Type.cast(:integer, expiry_str)
337 |> check_expiry_date()
340 def listen(user, %{"title" => _} = data) do
341 with visibility <- data["visibility"] || "public",
342 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
344 Map.take(data, ["album", "artist", "title", "length"])
345 |> Map.put("type", "Audio")
348 |> Map.put("actor", user.ap_id),
350 ActivityPub.listen(%{
354 context: Utils.generate_context_id(),
355 additional: %{"cc" => cc}
361 def post(user, %{"status" => _} = data) do
362 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
364 |> ActivityPub.create(draft.preview?)
365 |> maybe_create_activity_expiration(draft.expires_at)
369 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
370 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
375 defp maybe_create_activity_expiration(result, _), do: result
377 def pin(id, %{ap_id: user_ap_id} = user) do
380 data: %{"type" => "Create"},
381 object: %Object{data: %{"type" => object_type}}
382 } = activity <- Activity.get_by_id_with_object(id),
383 true <- object_type in ["Note", "Article", "Question"],
384 true <- Visibility.is_public?(activity),
385 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
388 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
389 _ -> {:error, dgettext("errors", "Could not pin")}
393 def unpin(id, user) do
394 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
395 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
398 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
399 _ -> {:error, dgettext("errors", "Could not unpin")}
403 def add_mute(user, activity) do
404 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
407 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
411 def remove_mute(user, activity) do
412 ThreadMute.remove_mute(user.id, activity.data["context"])
416 def thread_muted?(%{id: nil} = _user, _activity), do: false
418 def thread_muted?(user, activity) do
419 ThreadMute.exists?(user.id, activity.data["context"])
422 def report(user, data) do
423 with {:ok, account} <- get_reported_account(data.account_id),
424 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
425 {:ok, statuses} <- get_report_statuses(account, data) do
427 context: Utils.generate_context_id(),
431 content: content_html,
432 forward: Map.get(data, :forward, false)
437 defp get_reported_account(account_id) do
438 case User.get_cached_by_id(account_id) do
439 %User{} = account -> {:ok, account}
440 _ -> {:error, dgettext("errors", "Account not found")}
444 def update_report_state(activity_ids, state) when is_list(activity_ids) do
445 case Utils.update_report_state(activity_ids, state) do
446 :ok -> {:ok, activity_ids}
447 _ -> {:error, dgettext("errors", "Could not update state")}
451 def update_report_state(activity_id, state) do
452 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
453 Utils.update_report_state(activity, state)
455 nil -> {:error, :not_found}
456 _ -> {:error, dgettext("errors", "Could not update state")}
460 def update_activity_scope(activity_id, opts \\ %{}) do
461 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
462 {:ok, activity} <- toggle_sensitive(activity, opts) do
463 set_visibility(activity, opts)
465 nil -> {:error, :not_found}
466 {:error, reason} -> {:error, reason}
470 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
471 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
474 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
475 when is_boolean(sensitive) do
476 new_data = Map.put(object.data, "sensitive", sensitive)
480 |> Object.change(%{data: new_data})
481 |> Object.update_and_set_cache()
483 {:ok, Map.put(activity, :object, object)}
486 defp toggle_sensitive(activity, _), do: {:ok, activity}
488 defp set_visibility(activity, %{"visibility" => visibility}) do
489 Utils.update_activity_visibility(activity, visibility)
492 defp set_visibility(activity, _), do: {:ok, activity}
494 def hide_reblogs(%User{} = user, %User{} = target) do
495 UserRelationship.create_reblog_mute(user, target)
498 def show_reblogs(%User{} = user, %User{} = target) do
499 UserRelationship.delete_reblog_mute(user, target)