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) do
131 object = Object.normalize(activity)
132 announce_activity = Utils.get_existing_announce(user.ap_id, object)
133 public = public_announce?(object, params)
135 if announce_activity do
136 {:ok, announce_activity, object}
138 ActivityPub.announce(user, object, nil, true, public)
141 _ -> {:error, :not_found}
145 def unrepeat(id, user) do
146 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
147 {:find_activity, Activity.get_by_id(id)},
148 %Object{} = note <- Object.normalize(activity, false),
149 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
150 {:ok, undo, _} <- Builder.undo(user, announce),
151 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
154 {:find_activity, _} -> {:error, :not_found}
155 _ -> {:error, dgettext("errors", "Could not unrepeat")}
159 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
160 def favorite(%User{} = user, id) do
161 case favorite_helper(user, id) do
165 {:error, :not_found} = res ->
169 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
170 {:error, dgettext("errors", "Could not favorite")}
174 def favorite_helper(user, id) do
175 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
176 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
177 {_, {:ok, %Activity{} = activity, _meta}} <-
179 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
196 if {:object, {"already liked by this actor", []}} in changeset.errors do
197 {:ok, :already_liked}
207 def unfavorite(id, user) do
208 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
209 {:find_activity, Activity.get_by_id(id)},
210 %Object{} = note <- Object.normalize(activity, false),
211 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
212 {:ok, undo, _} <- Builder.undo(user, like),
213 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
216 {:find_activity, _} -> {:error, :not_found}
217 _ -> {:error, dgettext("errors", "Could not unfavorite")}
221 def react_with_emoji(id, user, emoji) do
222 with %Activity{} = activity <- Activity.get_by_id(id),
223 object <- Object.normalize(activity),
224 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
225 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
229 {:error, dgettext("errors", "Could not add reaction emoji")}
233 def unreact_with_emoji(id, user, emoji) do
234 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
235 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
236 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
240 {:error, dgettext("errors", "Could not remove reaction emoji")}
244 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
245 with :ok <- validate_not_author(object, user),
246 :ok <- validate_existing_votes(user, object),
247 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
249 Enum.map(choices, fn index ->
250 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
253 ActivityPub.create(%{
254 to: answer_data["to"],
256 context: object.data["context"],
258 additional: %{"cc" => answer_data["cc"]}
264 object = Object.get_cached_by_ap_id(object.data["id"])
265 {:ok, answer_activities, object}
269 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
270 do: {:error, dgettext("errors", "Poll's author can't vote")}
272 defp validate_not_author(_, _), do: :ok
274 defp validate_existing_votes(%{ap_id: ap_id}, object) do
275 if Utils.get_existing_votes(ap_id, object) == [] do
278 {:error, dgettext("errors", "Already voted")}
282 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
283 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
285 defp normalize_and_validate_choices(choices, object) do
286 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
287 {options, max_count} = get_options_and_max_count(object)
288 count = Enum.count(options)
290 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
291 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
292 {:ok, options, choices}
294 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
295 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
299 def public_announce?(_, %{visibility: visibility})
300 when visibility in ~w{public unlisted private direct},
301 do: visibility in ~w(public unlisted)
303 def public_announce?(object, _) do
304 Visibility.is_public?(object)
307 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
309 def get_visibility(%{visibility: visibility}, in_reply_to, _)
310 when visibility in ~w{public unlisted private direct},
311 do: {visibility, get_replied_to_visibility(in_reply_to)}
313 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
314 visibility = {:list, String.to_integer(list_id)}
315 {visibility, get_replied_to_visibility(in_reply_to)}
318 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
319 visibility = get_replied_to_visibility(in_reply_to)
320 {visibility, visibility}
323 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
325 def get_replied_to_visibility(nil), do: nil
327 def get_replied_to_visibility(activity) do
328 with %Object{} = object <- Object.normalize(activity) do
329 Visibility.get_visibility(object)
333 def check_expiry_date({:ok, nil} = res), do: res
335 def check_expiry_date({:ok, in_seconds}) do
336 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
338 if ActivityExpiration.expires_late_enough?(expiry) do
341 {:error, "Expiry date is too soon"}
345 def check_expiry_date(expiry_str) do
346 Ecto.Type.cast(:integer, expiry_str)
347 |> check_expiry_date()
350 def listen(user, %{"title" => _} = data) do
351 with visibility <- data["visibility"] || "public",
352 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
354 Map.take(data, ["album", "artist", "title", "length"])
355 |> Map.put("type", "Audio")
358 |> Map.put("actor", user.ap_id),
360 ActivityPub.listen(%{
364 context: Utils.generate_context_id(),
365 additional: %{"cc" => cc}
371 def post(user, %{status: _} = data) do
372 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
374 |> ActivityPub.create(draft.preview?)
375 |> maybe_create_activity_expiration(draft.expires_at)
379 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
380 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
385 defp maybe_create_activity_expiration(result, _), do: result
387 def pin(id, %{ap_id: user_ap_id} = user) do
390 data: %{"type" => "Create"},
391 object: %Object{data: %{"type" => object_type}}
392 } = activity <- Activity.get_by_id_with_object(id),
393 true <- object_type in ["Note", "Article", "Question"],
394 true <- Visibility.is_public?(activity),
395 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
398 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
399 _ -> {:error, dgettext("errors", "Could not pin")}
403 def unpin(id, user) do
404 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
405 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
408 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
409 _ -> {:error, dgettext("errors", "Could not unpin")}
413 def add_mute(user, activity) do
414 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
417 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
421 def remove_mute(user, activity) do
422 ThreadMute.remove_mute(user.id, activity.data["context"])
426 def thread_muted?(%{id: nil} = _user, _activity), do: false
428 def thread_muted?(user, activity) do
429 ThreadMute.exists?(user.id, activity.data["context"])
432 def report(user, data) do
433 with {:ok, account} <- get_reported_account(data.account_id),
434 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
435 {:ok, statuses} <- get_report_statuses(account, data) do
437 context: Utils.generate_context_id(),
441 content: content_html,
442 forward: Map.get(data, :forward, false)
447 defp get_reported_account(account_id) do
448 case User.get_cached_by_id(account_id) do
449 %User{} = account -> {:ok, account}
450 _ -> {:error, dgettext("errors", "Account not found")}
454 def update_report_state(activity_ids, state) when is_list(activity_ids) do
455 case Utils.update_report_state(activity_ids, state) do
456 :ok -> {:ok, activity_ids}
457 _ -> {:error, dgettext("errors", "Could not update state")}
461 def update_report_state(activity_id, state) do
462 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
463 Utils.update_report_state(activity, state)
465 nil -> {:error, :not_found}
466 _ -> {:error, dgettext("errors", "Could not update state")}
470 def update_activity_scope(activity_id, opts \\ %{}) do
471 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
472 {:ok, activity} <- toggle_sensitive(activity, opts) do
473 set_visibility(activity, opts)
475 nil -> {:error, :not_found}
476 {:error, reason} -> {:error, reason}
480 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
481 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
484 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
485 when is_boolean(sensitive) do
486 new_data = Map.put(object.data, "sensitive", sensitive)
490 |> Object.change(%{data: new_data})
491 |> Object.update_and_set_cache()
493 {:ok, Map.put(activity, :object, object)}
496 defp toggle_sensitive(activity, _), do: {:ok, activity}
498 defp set_visibility(activity, %{visibility: visibility}) do
499 Utils.update_activity_visibility(activity, visibility)
502 defp set_visibility(activity, _), do: {:ok, activity}
504 def hide_reblogs(%User{} = user, %User{} = target) do
505 UserRelationship.create_reblog_mute(user, target)
508 def show_reblogs(%User{} = user, %User{} = target) do
509 UserRelationship.delete_reblog_mute(user, target)