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
11 alias Pleroma.ThreadMute
13 alias Pleroma.UserRelationship
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Builder
16 alias Pleroma.Web.ActivityPub.Pipeline
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.ActivityPub.Visibility
20 import Pleroma.Web.Gettext
21 import Pleroma.Web.CommonAPI.Utils
23 require Pleroma.Constants
26 def follow(follower, followed) do
27 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
29 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
30 {:ok, activity} <- ActivityPub.follow(follower, followed),
31 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
32 {:ok, follower, followed, activity}
36 def unfollow(follower, unfollowed) do
37 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
38 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
39 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
44 def accept_follow_request(follower, followed) do
45 with {:ok, follower} <- User.follow(follower, followed),
46 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
47 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
48 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
53 object: follow_activity.data["id"],
60 def reject_follow_request(follower, followed) do
61 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
62 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
63 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
68 object: follow_activity.data["id"],
75 def delete(activity_id, user) do
76 with {_, %Activity{data: %{"object" => _}} = activity} <-
77 {:find_activity, Activity.get_by_id_with_object(activity_id)},
78 %Object{} = object <- Object.normalize(activity),
79 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
80 {:ok, _} <- unpin(activity_id, user),
81 {:ok, delete} <- ActivityPub.delete(object) do
84 {:find_activity, _} -> {:error, :not_found}
85 _ -> {:error, dgettext("errors", "Could not delete")}
89 def repeat(id_or_ap_id, user, params \\ %{}) do
90 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
91 object <- Object.normalize(activity),
92 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
93 public <- public_announce?(object, params) do
94 if announce_activity do
95 {:ok, announce_activity, object}
97 ActivityPub.announce(user, object, nil, true, public)
100 {:find_activity, _} -> {:error, :not_found}
101 _ -> {:error, dgettext("errors", "Could not repeat")}
105 def unrepeat(id_or_ap_id, user) do
106 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
107 object = Object.normalize(activity)
108 ActivityPub.unannounce(user, object)
110 {:find_activity, _} -> {:error, :not_found}
111 _ -> {:error, dgettext("errors", "Could not unrepeat")}
115 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
116 def favorite(%User{} = user, id) do
117 case favorite_helper(user, id) do
121 {:error, :not_found} = res ->
125 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
126 {:error, dgettext("errors", "Could not favorite")}
130 def favorite_helper(user, id) do
131 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
132 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
133 {_, {:ok, %Activity{} = activity, _meta}} <-
135 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
152 if {:object, {"already liked by this actor", []}} in changeset.errors do
153 {:ok, :already_liked}
163 def unfavorite(id_or_ap_id, user) do
164 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
165 object = Object.normalize(activity)
166 ActivityPub.unlike(user, object)
168 {:find_activity, _} -> {:error, :not_found}
169 _ -> {:error, dgettext("errors", "Could not unfavorite")}
173 def react_with_emoji(id, user, emoji) do
174 with %Activity{} = activity <- Activity.get_by_id(id),
175 object <- Object.normalize(activity) do
176 ActivityPub.react_with_emoji(user, object, emoji)
179 {:error, dgettext("errors", "Could not add reaction emoji")}
183 def unreact_with_emoji(id, user, emoji) do
184 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
185 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
188 {:error, dgettext("errors", "Could not remove reaction emoji")}
192 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
193 with :ok <- validate_not_author(object, user),
194 :ok <- validate_existing_votes(user, object),
195 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
197 Enum.map(choices, fn index ->
198 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
201 ActivityPub.create(%{
202 to: answer_data["to"],
204 context: object.data["context"],
206 additional: %{"cc" => answer_data["cc"]}
212 object = Object.get_cached_by_ap_id(object.data["id"])
213 {:ok, answer_activities, object}
217 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
218 do: {:error, dgettext("errors", "Poll's author can't vote")}
220 defp validate_not_author(_, _), do: :ok
222 defp validate_existing_votes(%{ap_id: ap_id}, object) do
223 if Utils.get_existing_votes(ap_id, object) == [] do
226 {:error, dgettext("errors", "Already voted")}
230 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
231 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
233 defp normalize_and_validate_choices(choices, object) do
234 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
235 {options, max_count} = get_options_and_max_count(object)
236 count = Enum.count(options)
238 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
239 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
240 {:ok, options, choices}
242 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
243 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
247 def public_announce?(_, %{"visibility" => visibility})
248 when visibility in ~w{public unlisted private direct},
249 do: visibility in ~w(public unlisted)
251 def public_announce?(object, _) do
252 Visibility.is_public?(object)
255 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
257 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
258 when visibility in ~w{public unlisted private direct},
259 do: {visibility, get_replied_to_visibility(in_reply_to)}
261 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
262 visibility = {:list, String.to_integer(list_id)}
263 {visibility, get_replied_to_visibility(in_reply_to)}
266 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
267 visibility = get_replied_to_visibility(in_reply_to)
268 {visibility, visibility}
271 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
273 def get_replied_to_visibility(nil), do: nil
275 def get_replied_to_visibility(activity) do
276 with %Object{} = object <- Object.normalize(activity) do
277 Visibility.get_visibility(object)
281 def check_expiry_date({:ok, nil} = res), do: res
283 def check_expiry_date({:ok, in_seconds}) do
284 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
286 if ActivityExpiration.expires_late_enough?(expiry) do
289 {:error, "Expiry date is too soon"}
293 def check_expiry_date(expiry_str) do
294 Ecto.Type.cast(:integer, expiry_str)
295 |> check_expiry_date()
298 def listen(user, %{"title" => _} = data) do
299 with visibility <- data["visibility"] || "public",
300 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
302 Map.take(data, ["album", "artist", "title", "length"])
303 |> Map.put("type", "Audio")
306 |> Map.put("actor", user.ap_id),
308 ActivityPub.listen(%{
312 context: Utils.generate_context_id(),
313 additional: %{"cc" => cc}
319 def post(user, %{"status" => _} = data) do
320 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
322 |> ActivityPub.create(draft.preview?)
323 |> maybe_create_activity_expiration(draft.expires_at)
327 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
328 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
333 defp maybe_create_activity_expiration(result, _), do: result
335 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
338 data: %{"type" => "Create"},
339 object: %Object{data: %{"type" => object_type}}
340 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
341 true <- object_type in ["Note", "Article", "Question"],
342 true <- Visibility.is_public?(activity),
343 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
346 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
347 _ -> {:error, dgettext("errors", "Could not pin")}
351 def unpin(id_or_ap_id, user) do
352 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
353 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
356 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
357 _ -> {:error, dgettext("errors", "Could not unpin")}
361 def add_mute(user, activity) do
362 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
365 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
369 def remove_mute(user, activity) do
370 ThreadMute.remove_mute(user.id, activity.data["context"])
374 def thread_muted?(%{id: nil} = _user, _activity), do: false
376 def thread_muted?(user, activity) do
377 ThreadMute.exists?(user.id, activity.data["context"])
380 def report(user, %{"account_id" => account_id} = data) do
381 with {:ok, account} <- get_reported_account(account_id),
382 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
383 {:ok, statuses} <- get_report_statuses(account, data) do
385 context: Utils.generate_context_id(),
389 content: content_html,
390 forward: data["forward"] || false
395 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
397 defp get_reported_account(account_id) do
398 case User.get_cached_by_id(account_id) do
399 %User{} = account -> {:ok, account}
400 _ -> {:error, dgettext("errors", "Account not found")}
404 def update_report_state(activity_ids, state) when is_list(activity_ids) do
405 case Utils.update_report_state(activity_ids, state) do
406 :ok -> {:ok, activity_ids}
407 _ -> {:error, dgettext("errors", "Could not update state")}
411 def update_report_state(activity_id, state) do
412 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
413 Utils.update_report_state(activity, state)
415 nil -> {:error, :not_found}
416 _ -> {:error, dgettext("errors", "Could not update state")}
420 def update_activity_scope(activity_id, opts \\ %{}) do
421 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
422 {:ok, activity} <- toggle_sensitive(activity, opts) do
423 set_visibility(activity, opts)
425 nil -> {:error, :not_found}
426 {:error, reason} -> {:error, reason}
430 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
431 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
434 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
435 when is_boolean(sensitive) do
436 new_data = Map.put(object.data, "sensitive", sensitive)
440 |> Object.change(%{data: new_data})
441 |> Object.update_and_set_cache()
443 {:ok, Map.put(activity, :object, object)}
446 defp toggle_sensitive(activity, _), do: {:ok, activity}
448 defp set_visibility(activity, %{"visibility" => visibility}) do
449 Utils.update_activity_visibility(activity, visibility)
452 defp set_visibility(activity, _), do: {:ok, activity}
454 def hide_reblogs(%User{} = user, %User{} = target) do
455 UserRelationship.create_reblog_mute(user, target)
458 def show_reblogs(%User{} = user, %User{} = target) do
459 UserRelationship.delete_reblog_mute(user, target)