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) do
195 ActivityPub.react_with_emoji(user, object, emoji)
198 {:error, dgettext("errors", "Could not add reaction emoji")}
202 def unreact_with_emoji(id, user, emoji) do
203 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
204 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
205 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
209 {:error, dgettext("errors", "Could not remove reaction emoji")}
213 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
214 with :ok <- validate_not_author(object, user),
215 :ok <- validate_existing_votes(user, object),
216 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
218 Enum.map(choices, fn index ->
219 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
222 ActivityPub.create(%{
223 to: answer_data["to"],
225 context: object.data["context"],
227 additional: %{"cc" => answer_data["cc"]}
233 object = Object.get_cached_by_ap_id(object.data["id"])
234 {:ok, answer_activities, object}
238 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
239 do: {:error, dgettext("errors", "Poll's author can't vote")}
241 defp validate_not_author(_, _), do: :ok
243 defp validate_existing_votes(%{ap_id: ap_id}, object) do
244 if Utils.get_existing_votes(ap_id, object) == [] do
247 {:error, dgettext("errors", "Already voted")}
251 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
252 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
254 defp normalize_and_validate_choices(choices, object) do
255 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
256 {options, max_count} = get_options_and_max_count(object)
257 count = Enum.count(options)
259 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
260 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
261 {:ok, options, choices}
263 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
264 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
268 def public_announce?(_, %{"visibility" => visibility})
269 when visibility in ~w{public unlisted private direct},
270 do: visibility in ~w(public unlisted)
272 def public_announce?(object, _) do
273 Visibility.is_public?(object)
276 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
278 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
279 when visibility in ~w{public unlisted private direct},
280 do: {visibility, get_replied_to_visibility(in_reply_to)}
282 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
283 visibility = {:list, String.to_integer(list_id)}
284 {visibility, get_replied_to_visibility(in_reply_to)}
287 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
288 visibility = get_replied_to_visibility(in_reply_to)
289 {visibility, visibility}
292 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
294 def get_replied_to_visibility(nil), do: nil
296 def get_replied_to_visibility(activity) do
297 with %Object{} = object <- Object.normalize(activity) do
298 Visibility.get_visibility(object)
302 def check_expiry_date({:ok, nil} = res), do: res
304 def check_expiry_date({:ok, in_seconds}) do
305 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
307 if ActivityExpiration.expires_late_enough?(expiry) do
310 {:error, "Expiry date is too soon"}
314 def check_expiry_date(expiry_str) do
315 Ecto.Type.cast(:integer, expiry_str)
316 |> check_expiry_date()
319 def listen(user, %{"title" => _} = data) do
320 with visibility <- data["visibility"] || "public",
321 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
323 Map.take(data, ["album", "artist", "title", "length"])
324 |> Map.put("type", "Audio")
327 |> Map.put("actor", user.ap_id),
329 ActivityPub.listen(%{
333 context: Utils.generate_context_id(),
334 additional: %{"cc" => cc}
340 def post(user, %{"status" => _} = data) do
341 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
343 |> ActivityPub.create(draft.preview?)
344 |> maybe_create_activity_expiration(draft.expires_at)
348 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
349 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
354 defp maybe_create_activity_expiration(result, _), do: result
356 def pin(id, %{ap_id: user_ap_id} = user) do
359 data: %{"type" => "Create"},
360 object: %Object{data: %{"type" => object_type}}
361 } = activity <- Activity.get_by_id_with_object(id),
362 true <- object_type in ["Note", "Article", "Question"],
363 true <- Visibility.is_public?(activity),
364 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
367 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
368 _ -> {:error, dgettext("errors", "Could not pin")}
372 def unpin(id, user) do
373 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
374 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
377 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
378 _ -> {:error, dgettext("errors", "Could not unpin")}
382 def add_mute(user, activity) do
383 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
386 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
390 def remove_mute(user, activity) do
391 ThreadMute.remove_mute(user.id, activity.data["context"])
395 def thread_muted?(%{id: nil} = _user, _activity), do: false
397 def thread_muted?(user, activity) do
398 ThreadMute.exists?(user.id, activity.data["context"])
401 def report(user, data) do
402 with {:ok, account} <- get_reported_account(data.account_id),
403 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
404 {:ok, statuses} <- get_report_statuses(account, data) do
406 context: Utils.generate_context_id(),
410 content: content_html,
411 forward: Map.get(data, :forward, false)
416 defp get_reported_account(account_id) do
417 case User.get_cached_by_id(account_id) do
418 %User{} = account -> {:ok, account}
419 _ -> {:error, dgettext("errors", "Account not found")}
423 def update_report_state(activity_ids, state) when is_list(activity_ids) do
424 case Utils.update_report_state(activity_ids, state) do
425 :ok -> {:ok, activity_ids}
426 _ -> {:error, dgettext("errors", "Could not update state")}
430 def update_report_state(activity_id, state) do
431 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
432 Utils.update_report_state(activity, state)
434 nil -> {:error, :not_found}
435 _ -> {:error, dgettext("errors", "Could not update state")}
439 def update_activity_scope(activity_id, opts \\ %{}) do
440 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
441 {:ok, activity} <- toggle_sensitive(activity, opts) do
442 set_visibility(activity, opts)
444 nil -> {:error, :not_found}
445 {:error, reason} -> {:error, reason}
449 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
450 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
453 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
454 when is_boolean(sensitive) do
455 new_data = Map.put(object.data, "sensitive", sensitive)
459 |> Object.change(%{data: new_data})
460 |> Object.update_and_set_cache()
462 {:ok, Map.put(activity, :object, object)}
465 defp toggle_sensitive(activity, _), do: {:ok, activity}
467 defp set_visibility(activity, %{"visibility" => visibility}) do
468 Utils.update_activity_visibility(activity, visibility)
471 defp set_visibility(activity, _), do: {:ok, activity}
473 def hide_reblogs(%User{} = user, %User{} = target) do
474 UserRelationship.create_reblog_mute(user, target)
477 def show_reblogs(%User{} = user, %User{} = target) do
478 UserRelationship.delete_reblog_mute(user, target)