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 <- Activity.get_by_id(id) do
120 object = Object.normalize(activity)
121 announce_activity = Utils.get_existing_announce(user.ap_id, object)
122 public = public_announce?(object, params)
124 if announce_activity do
125 {:ok, announce_activity, object}
127 ActivityPub.announce(user, object, nil, true, public)
130 _ -> {:error, :not_found}
134 def unrepeat(id, user) do
135 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
136 {:find_activity, Activity.get_by_id(id)},
137 %Object{} = note <- Object.normalize(activity, false),
138 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
139 {:ok, undo, _} <- Builder.undo(user, announce),
140 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
143 {:find_activity, _} -> {:error, :not_found}
144 _ -> {:error, dgettext("errors", "Could not unrepeat")}
148 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
149 def favorite(%User{} = user, id) do
150 case favorite_helper(user, id) do
154 {:error, :not_found} = res ->
158 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
159 {:error, dgettext("errors", "Could not favorite")}
163 def favorite_helper(user, id) do
164 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
165 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
166 {_, {:ok, %Activity{} = activity, _meta}} <-
168 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
185 if {:object, {"already liked by this actor", []}} in changeset.errors do
186 {:ok, :already_liked}
196 def unfavorite(id, user) do
197 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
198 {:find_activity, Activity.get_by_id(id)},
199 %Object{} = note <- Object.normalize(activity, false),
200 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
201 {:ok, undo, _} <- Builder.undo(user, like),
202 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
205 {:find_activity, _} -> {:error, :not_found}
206 _ -> {:error, dgettext("errors", "Could not unfavorite")}
210 def react_with_emoji(id, user, emoji) do
211 with %Activity{} = activity <- Activity.get_by_id(id),
212 object <- Object.normalize(activity),
213 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
214 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
218 {:error, dgettext("errors", "Could not add reaction emoji")}
222 def unreact_with_emoji(id, user, emoji) do
223 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
224 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
225 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
229 {:error, dgettext("errors", "Could not remove reaction emoji")}
233 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
234 with :ok <- validate_not_author(object, user),
235 :ok <- validate_existing_votes(user, object),
236 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
238 Enum.map(choices, fn index ->
239 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
242 ActivityPub.create(%{
243 to: answer_data["to"],
245 context: object.data["context"],
247 additional: %{"cc" => answer_data["cc"]}
253 object = Object.get_cached_by_ap_id(object.data["id"])
254 {:ok, answer_activities, object}
258 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
259 do: {:error, dgettext("errors", "Poll's author can't vote")}
261 defp validate_not_author(_, _), do: :ok
263 defp validate_existing_votes(%{ap_id: ap_id}, object) do
264 if Utils.get_existing_votes(ap_id, object) == [] do
267 {:error, dgettext("errors", "Already voted")}
271 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
272 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
274 defp normalize_and_validate_choices(choices, object) do
275 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
276 {options, max_count} = get_options_and_max_count(object)
277 count = Enum.count(options)
279 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
280 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
281 {:ok, options, choices}
283 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
284 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
288 def public_announce?(_, %{visibility: visibility})
289 when visibility in ~w{public unlisted private direct},
290 do: visibility in ~w(public unlisted)
292 def public_announce?(object, _) do
293 Visibility.is_public?(object)
296 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
298 def get_visibility(%{visibility: visibility}, in_reply_to, _)
299 when visibility in ~w{public unlisted private direct},
300 do: {visibility, get_replied_to_visibility(in_reply_to)}
302 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
303 visibility = {:list, String.to_integer(list_id)}
304 {visibility, get_replied_to_visibility(in_reply_to)}
307 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
308 visibility = get_replied_to_visibility(in_reply_to)
309 {visibility, visibility}
312 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
314 def get_replied_to_visibility(nil), do: nil
316 def get_replied_to_visibility(activity) do
317 with %Object{} = object <- Object.normalize(activity) do
318 Visibility.get_visibility(object)
322 def check_expiry_date({:ok, nil} = res), do: res
324 def check_expiry_date({:ok, in_seconds}) do
325 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
327 if ActivityExpiration.expires_late_enough?(expiry) do
330 {:error, "Expiry date is too soon"}
334 def check_expiry_date(expiry_str) do
335 Ecto.Type.cast(:integer, expiry_str)
336 |> check_expiry_date()
339 def listen(user, %{"title" => _} = data) do
340 with visibility <- data["visibility"] || "public",
341 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
343 Map.take(data, ["album", "artist", "title", "length"])
344 |> Map.put("type", "Audio")
347 |> Map.put("actor", user.ap_id),
349 ActivityPub.listen(%{
353 context: Utils.generate_context_id(),
354 additional: %{"cc" => cc}
360 def post(user, %{status: _} = data) do
361 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
363 |> ActivityPub.create(draft.preview?)
364 |> maybe_create_activity_expiration(draft.expires_at)
368 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
369 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
374 defp maybe_create_activity_expiration(result, _), do: result
376 def pin(id, %{ap_id: user_ap_id} = user) do
379 data: %{"type" => "Create"},
380 object: %Object{data: %{"type" => object_type}}
381 } = activity <- Activity.get_by_id_with_object(id),
382 true <- object_type in ["Note", "Article", "Question"],
383 true <- Visibility.is_public?(activity),
384 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
387 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
388 _ -> {:error, dgettext("errors", "Could not pin")}
392 def unpin(id, user) do
393 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
394 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
397 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
398 _ -> {:error, dgettext("errors", "Could not unpin")}
402 def add_mute(user, activity) do
403 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
406 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
410 def remove_mute(user, activity) do
411 ThreadMute.remove_mute(user.id, activity.data["context"])
415 def thread_muted?(%{id: nil} = _user, _activity), do: false
417 def thread_muted?(user, activity) do
418 ThreadMute.exists?(user.id, activity.data["context"])
421 def report(user, data) do
422 with {:ok, account} <- get_reported_account(data.account_id),
423 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
424 {:ok, statuses} <- get_report_statuses(account, data) do
426 context: Utils.generate_context_id(),
430 content: content_html,
431 forward: Map.get(data, :forward, false)
436 defp get_reported_account(account_id) do
437 case User.get_cached_by_id(account_id) do
438 %User{} = account -> {:ok, account}
439 _ -> {:error, dgettext("errors", "Account not found")}
443 def update_report_state(activity_ids, state) when is_list(activity_ids) do
444 case Utils.update_report_state(activity_ids, state) do
445 :ok -> {:ok, activity_ids}
446 _ -> {:error, dgettext("errors", "Could not update state")}
450 def update_report_state(activity_id, state) do
451 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
452 Utils.update_report_state(activity, state)
454 nil -> {:error, :not_found}
455 _ -> {:error, dgettext("errors", "Could not update state")}
459 def update_activity_scope(activity_id, opts \\ %{}) do
460 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
461 {:ok, activity} <- toggle_sensitive(activity, opts) do
462 set_visibility(activity, opts)
464 nil -> {:error, :not_found}
465 {:error, reason} -> {:error, reason}
469 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
470 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
473 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
474 when is_boolean(sensitive) do
475 new_data = Map.put(object.data, "sensitive", sensitive)
479 |> Object.change(%{data: new_data})
480 |> Object.update_and_set_cache()
482 {:ok, Map.put(activity, :object, object)}
485 defp toggle_sensitive(activity, _), do: {:ok, activity}
487 defp set_visibility(activity, %{visibility: visibility}) do
488 Utils.update_activity_visibility(activity, visibility)
491 defp set_visibility(activity, _), do: {:ok, activity}
493 def hide_reblogs(%User{} = user, %User{} = target) do
494 UserRelationship.create_reblog_mute(user, target)
497 def show_reblogs(%User{} = user, %User{} = target) do
498 UserRelationship.delete_reblog_mute(user, target)