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 # Updates the emojis for a user based on their profile
327 emoji = emoji_from_profile(user)
328 source_data = Map.put(user.source_data, "tag", emoji)
331 case User.update_source_data(user, source_data) do
336 ActivityPub.update(%{
338 to: [Pleroma.Constants.as_public(), user.follower_address],
341 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
345 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
348 data: %{"type" => "Create"},
349 object: %Object{data: %{"type" => object_type}}
350 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
351 true <- object_type in ["Note", "Article", "Question"],
352 true <- Visibility.is_public?(activity),
353 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
356 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
357 _ -> {:error, dgettext("errors", "Could not pin")}
361 def unpin(id_or_ap_id, user) do
362 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
363 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
366 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
367 _ -> {:error, dgettext("errors", "Could not unpin")}
371 def add_mute(user, activity) do
372 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
375 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
379 def remove_mute(user, activity) do
380 ThreadMute.remove_mute(user.id, activity.data["context"])
384 def thread_muted?(%{id: nil} = _user, _activity), do: false
386 def thread_muted?(user, activity) do
387 ThreadMute.exists?(user.id, activity.data["context"])
390 def report(user, %{"account_id" => account_id} = data) do
391 with {:ok, account} <- get_reported_account(account_id),
392 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
393 {:ok, statuses} <- get_report_statuses(account, data) do
395 context: Utils.generate_context_id(),
399 content: content_html,
400 forward: data["forward"] || false
405 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
407 defp get_reported_account(account_id) do
408 case User.get_cached_by_id(account_id) do
409 %User{} = account -> {:ok, account}
410 _ -> {:error, dgettext("errors", "Account not found")}
414 def update_report_state(activity_ids, state) when is_list(activity_ids) do
415 case Utils.update_report_state(activity_ids, state) do
416 :ok -> {:ok, activity_ids}
417 _ -> {:error, dgettext("errors", "Could not update state")}
421 def update_report_state(activity_id, state) do
422 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
423 Utils.update_report_state(activity, state)
425 nil -> {:error, :not_found}
426 _ -> {:error, dgettext("errors", "Could not update state")}
430 def update_activity_scope(activity_id, opts \\ %{}) do
431 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
432 {:ok, activity} <- toggle_sensitive(activity, opts) do
433 set_visibility(activity, opts)
435 nil -> {:error, :not_found}
436 {:error, reason} -> {:error, reason}
440 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
441 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
444 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
445 when is_boolean(sensitive) do
446 new_data = Map.put(object.data, "sensitive", sensitive)
450 |> Object.change(%{data: new_data})
451 |> Object.update_and_set_cache()
453 {:ok, Map.put(activity, :object, object)}
456 defp toggle_sensitive(activity, _), do: {:ok, activity}
458 defp set_visibility(activity, %{"visibility" => visibility}) do
459 Utils.update_activity_visibility(activity, visibility)
462 defp set_visibility(activity, _), do: {:ok, activity}
464 def hide_reblogs(%User{} = user, %User{} = target) do
465 UserRelationship.create_reblog_mute(user, target)
468 def show_reblogs(%User{} = user, %User{} = target) do
469 UserRelationship.delete_reblog_mute(user, target)