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 # Updates the emojis for a user based on their profile
337 emoji = emoji_from_profile(user)
338 source_data = Map.put(user.source_data, "tag", emoji)
341 case User.update_source_data(user, source_data) do
346 ActivityPub.update(%{
348 to: [Pleroma.Constants.as_public(), user.follower_address],
351 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
355 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
358 data: %{"type" => "Create"},
359 object: %Object{data: %{"type" => object_type}}
360 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
361 true <- object_type in ["Note", "Article", "Question"],
362 true <- Visibility.is_public?(activity),
363 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
366 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
367 _ -> {:error, dgettext("errors", "Could not pin")}
371 def unpin(id_or_ap_id, user) do
372 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
373 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
376 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
377 _ -> {:error, dgettext("errors", "Could not unpin")}
381 def add_mute(user, activity) do
382 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
385 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
389 def remove_mute(user, activity) do
390 ThreadMute.remove_mute(user.id, activity.data["context"])
394 def thread_muted?(%{id: nil} = _user, _activity), do: false
396 def thread_muted?(user, activity) do
397 ThreadMute.check_muted(user.id, activity.data["context"]) != []
400 def report(user, %{"account_id" => account_id} = data) do
401 with {:ok, account} <- get_reported_account(account_id),
402 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
403 {:ok, statuses} <- get_report_statuses(account, data) do
405 context: Utils.generate_context_id(),
409 content: content_html,
410 forward: data["forward"] || false
415 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
417 defp get_reported_account(account_id) do
418 case User.get_cached_by_id(account_id) do
419 %User{} = account -> {:ok, account}
420 _ -> {:error, dgettext("errors", "Account not found")}
424 def update_report_state(activity_ids, state) when is_list(activity_ids) do
425 case Utils.update_report_state(activity_ids, state) do
426 :ok -> {:ok, activity_ids}
427 _ -> {:error, dgettext("errors", "Could not update state")}
431 def update_report_state(activity_id, state) do
432 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
433 Utils.update_report_state(activity, state)
435 nil -> {:error, :not_found}
436 _ -> {:error, dgettext("errors", "Could not update state")}
440 def update_activity_scope(activity_id, opts \\ %{}) do
441 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
442 {:ok, activity} <- toggle_sensitive(activity, opts) do
443 set_visibility(activity, opts)
445 nil -> {:error, :not_found}
446 {:error, reason} -> {:error, reason}
450 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
451 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
454 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
455 when is_boolean(sensitive) do
456 new_data = Map.put(object.data, "sensitive", sensitive)
460 |> Object.change(%{data: new_data})
461 |> Object.update_and_set_cache()
463 {:ok, Map.put(activity, :object, object)}
466 defp toggle_sensitive(activity, _), do: {:ok, activity}
468 defp set_visibility(activity, %{"visibility" => visibility}) do
469 Utils.update_activity_visibility(activity, visibility)
472 defp set_visibility(activity, _), do: {:ok, activity}
474 def hide_reblogs(%User{} = user, %User{} = target) do
475 UserRelationship.create_reblog_mute(user, target)
478 def show_reblogs(%User{} = user, %User{} = target) do
479 UserRelationship.delete_reblog_mute(user, target)