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
378 |> ActivityPub.create(draft.preview?)
379 |> maybe_create_activity_expiration(draft.expires_at)
383 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
384 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
389 defp maybe_create_activity_expiration(result, _), do: result
391 def pin(id, %{ap_id: user_ap_id} = user) do
394 data: %{"type" => "Create"},
395 object: %Object{data: %{"type" => object_type}}
396 } = activity <- Activity.get_by_id_with_object(id),
397 true <- object_type in ["Note", "Article", "Question"],
398 true <- Visibility.is_public?(activity),
399 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
402 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
403 _ -> {:error, dgettext("errors", "Could not pin")}
407 def unpin(id, user) do
408 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
409 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
412 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
413 _ -> {:error, dgettext("errors", "Could not unpin")}
417 def add_mute(user, activity) do
418 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
421 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
425 def remove_mute(user, activity) do
426 ThreadMute.remove_mute(user.id, activity.data["context"])
430 def thread_muted?(%{id: nil} = _user, _activity), do: false
432 def thread_muted?(user, activity) do
433 ThreadMute.exists?(user.id, activity.data["context"])
436 def report(user, data) do
437 with {:ok, account} <- get_reported_account(data.account_id),
438 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
439 {:ok, statuses} <- get_report_statuses(account, data) do
441 context: Utils.generate_context_id(),
445 content: content_html,
446 forward: Map.get(data, :forward, false)
451 defp get_reported_account(account_id) do
452 case User.get_cached_by_id(account_id) do
453 %User{} = account -> {:ok, account}
454 _ -> {:error, dgettext("errors", "Account not found")}
458 def update_report_state(activity_ids, state) when is_list(activity_ids) do
459 case Utils.update_report_state(activity_ids, state) do
460 :ok -> {:ok, activity_ids}
461 _ -> {:error, dgettext("errors", "Could not update state")}
465 def update_report_state(activity_id, state) do
466 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
467 Utils.update_report_state(activity, state)
469 nil -> {:error, :not_found}
470 _ -> {:error, dgettext("errors", "Could not update state")}
474 def update_activity_scope(activity_id, opts \\ %{}) do
475 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
476 {:ok, activity} <- toggle_sensitive(activity, opts) do
477 set_visibility(activity, opts)
479 nil -> {:error, :not_found}
480 {:error, reason} -> {:error, reason}
484 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
485 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
488 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
489 when is_boolean(sensitive) do
490 new_data = Map.put(object.data, "sensitive", sensitive)
494 |> Object.change(%{data: new_data})
495 |> Object.update_and_set_cache()
497 {:ok, Map.put(activity, :object, object)}
500 defp toggle_sensitive(activity, _), do: {:ok, activity}
502 defp set_visibility(activity, %{visibility: visibility}) do
503 Utils.update_activity_visibility(activity, visibility)
506 defp set_visibility(activity, _), do: {:ok, activity}
508 def hide_reblogs(%User{} = user, %User{} = target) do
509 UserRelationship.create_reblog_mute(user, target)
512 def show_reblogs(%User{} = user, %User{} = target) do
513 UserRelationship.delete_reblog_mute(user, target)