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, :follow_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, :follow_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
321 ActivityPub.create(draft.changes, draft.preview?)
325 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
328 data: %{"type" => "Create"},
329 object: %Object{data: %{"type" => object_type}}
330 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
331 true <- object_type in ["Note", "Article", "Question"],
332 true <- Visibility.is_public?(activity),
333 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
336 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
337 _ -> {:error, dgettext("errors", "Could not pin")}
341 def unpin(id_or_ap_id, user) do
342 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
343 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
346 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
347 _ -> {:error, dgettext("errors", "Could not unpin")}
351 def add_mute(user, activity) do
352 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
355 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
359 def remove_mute(user, activity) do
360 ThreadMute.remove_mute(user.id, activity.data["context"])
364 def thread_muted?(%{id: nil} = _user, _activity), do: false
366 def thread_muted?(user, activity) do
367 ThreadMute.exists?(user.id, activity.data["context"])
370 def report(user, %{"account_id" => account_id} = data) do
371 with {:ok, account} <- get_reported_account(account_id),
372 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
373 {:ok, statuses} <- get_report_statuses(account, data) do
375 context: Utils.generate_context_id(),
379 content: content_html,
380 forward: data["forward"] || false
385 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
387 defp get_reported_account(account_id) do
388 case User.get_cached_by_id(account_id) do
389 %User{} = account -> {:ok, account}
390 _ -> {:error, dgettext("errors", "Account not found")}
394 def update_report_state(activity_ids, state) when is_list(activity_ids) do
395 case Utils.update_report_state(activity_ids, state) do
396 :ok -> {:ok, activity_ids}
397 _ -> {:error, dgettext("errors", "Could not update state")}
401 def update_report_state(activity_id, state) do
402 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
403 Utils.update_report_state(activity, state)
405 nil -> {:error, :not_found}
406 _ -> {:error, dgettext("errors", "Could not update state")}
410 def update_activity_scope(activity_id, opts \\ %{}) do
411 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
412 {:ok, activity} <- toggle_sensitive(activity, opts) do
413 set_visibility(activity, opts)
415 nil -> {:error, :not_found}
416 {:error, reason} -> {:error, reason}
420 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
421 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
424 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
425 when is_boolean(sensitive) do
426 new_data = Map.put(object.data, "sensitive", sensitive)
430 |> Object.change(%{data: new_data})
431 |> Object.update_and_set_cache()
433 {:ok, Map.put(activity, :object, object)}
436 defp toggle_sensitive(activity, _), do: {:ok, activity}
438 defp set_visibility(activity, %{"visibility" => visibility}) do
439 Utils.update_activity_visibility(activity, visibility)
442 defp set_visibility(activity, _), do: {:ok, activity}
444 def hide_reblogs(%User{} = user, %User{} = target) do
445 UserRelationship.create_reblog_mute(user, target)
448 def show_reblogs(%User{} = user, %User{} = target) do
449 UserRelationship.delete_reblog_mute(user, target)