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, user, params \\ %{}) do
90 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
91 {:find_activity, Activity.get_by_id(id)},
92 object <- Object.normalize(activity),
93 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
94 public <- public_announce?(object, params) do
95 if announce_activity do
96 {:ok, announce_activity, object}
98 ActivityPub.announce(user, object, nil, true, public)
101 {:find_activity, _} -> {:error, :not_found}
102 _ -> {:error, dgettext("errors", "Could not repeat")}
106 def unrepeat(id, user) do
107 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
108 {:find_activity, Activity.get_by_id(id)} do
109 object = Object.normalize(activity)
110 ActivityPub.unannounce(user, object)
112 {:find_activity, _} -> {:error, :not_found}
113 _ -> {:error, dgettext("errors", "Could not unrepeat")}
117 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
118 def favorite(%User{} = user, id) do
119 case favorite_helper(user, id) do
123 {:error, :not_found} = res ->
127 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
128 {:error, dgettext("errors", "Could not favorite")}
132 def favorite_helper(user, id) do
133 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
134 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
135 {_, {:ok, %Activity{} = activity, _meta}} <-
137 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
154 if {:object, {"already liked by this actor", []}} in changeset.errors do
155 {:ok, :already_liked}
165 def unfavorite(id, user) do
166 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
167 {:find_activity, Activity.get_by_id(id)} do
168 object = Object.normalize(activity)
169 ActivityPub.unlike(user, object)
171 {:find_activity, _} -> {:error, :not_found}
172 _ -> {:error, dgettext("errors", "Could not unfavorite")}
176 def react_with_emoji(id, user, emoji) do
177 with %Activity{} = activity <- Activity.get_by_id(id),
178 object <- Object.normalize(activity) do
179 ActivityPub.react_with_emoji(user, object, emoji)
182 {:error, dgettext("errors", "Could not add reaction emoji")}
186 def unreact_with_emoji(id, user, emoji) do
187 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
188 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
191 {:error, dgettext("errors", "Could not remove reaction emoji")}
195 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
196 with :ok <- validate_not_author(object, user),
197 :ok <- validate_existing_votes(user, object),
198 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
200 Enum.map(choices, fn index ->
201 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
204 ActivityPub.create(%{
205 to: answer_data["to"],
207 context: object.data["context"],
209 additional: %{"cc" => answer_data["cc"]}
215 object = Object.get_cached_by_ap_id(object.data["id"])
216 {:ok, answer_activities, object}
220 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
221 do: {:error, dgettext("errors", "Poll's author can't vote")}
223 defp validate_not_author(_, _), do: :ok
225 defp validate_existing_votes(%{ap_id: ap_id}, object) do
226 if Utils.get_existing_votes(ap_id, object) == [] do
229 {:error, dgettext("errors", "Already voted")}
233 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
234 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
236 defp normalize_and_validate_choices(choices, object) do
237 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
238 {options, max_count} = get_options_and_max_count(object)
239 count = Enum.count(options)
241 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
242 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
243 {:ok, options, choices}
245 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
246 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
250 def public_announce?(_, %{"visibility" => visibility})
251 when visibility in ~w{public unlisted private direct},
252 do: visibility in ~w(public unlisted)
254 def public_announce?(object, _) do
255 Visibility.is_public?(object)
258 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
260 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
261 when visibility in ~w{public unlisted private direct},
262 do: {visibility, get_replied_to_visibility(in_reply_to)}
264 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
265 visibility = {:list, String.to_integer(list_id)}
266 {visibility, get_replied_to_visibility(in_reply_to)}
269 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
270 visibility = get_replied_to_visibility(in_reply_to)
271 {visibility, visibility}
274 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
276 def get_replied_to_visibility(nil), do: nil
278 def get_replied_to_visibility(activity) do
279 with %Object{} = object <- Object.normalize(activity) do
280 Visibility.get_visibility(object)
284 def check_expiry_date({:ok, nil} = res), do: res
286 def check_expiry_date({:ok, in_seconds}) do
287 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
289 if ActivityExpiration.expires_late_enough?(expiry) do
292 {:error, "Expiry date is too soon"}
296 def check_expiry_date(expiry_str) do
297 Ecto.Type.cast(:integer, expiry_str)
298 |> check_expiry_date()
301 def listen(user, %{"title" => _} = data) do
302 with visibility <- data["visibility"] || "public",
303 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
305 Map.take(data, ["album", "artist", "title", "length"])
306 |> Map.put("type", "Audio")
309 |> Map.put("actor", user.ap_id),
311 ActivityPub.listen(%{
315 context: Utils.generate_context_id(),
316 additional: %{"cc" => cc}
322 def post(user, %{"status" => _} = data) do
323 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
325 |> ActivityPub.create(draft.preview?)
326 |> maybe_create_activity_expiration(draft.expires_at)
330 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
331 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
336 defp maybe_create_activity_expiration(result, _), do: result
338 def pin(id, %{ap_id: user_ap_id} = user) do
341 data: %{"type" => "Create"},
342 object: %Object{data: %{"type" => object_type}}
343 } = activity <- Activity.get_by_id_with_object(id),
344 true <- object_type in ["Note", "Article", "Question"],
345 true <- Visibility.is_public?(activity),
346 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
349 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
350 _ -> {:error, dgettext("errors", "Could not pin")}
354 def unpin(id, user) do
355 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
356 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
359 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
360 _ -> {:error, dgettext("errors", "Could not unpin")}
364 def add_mute(user, activity) do
365 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
368 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
372 def remove_mute(user, activity) do
373 ThreadMute.remove_mute(user.id, activity.data["context"])
377 def thread_muted?(%{id: nil} = _user, _activity), do: false
379 def thread_muted?(user, activity) do
380 ThreadMute.exists?(user.id, activity.data["context"])
383 def report(user, %{"account_id" => account_id} = data) do
384 with {:ok, account} <- get_reported_account(account_id),
385 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
386 {:ok, statuses} <- get_report_statuses(account, data) do
388 context: Utils.generate_context_id(),
392 content: content_html,
393 forward: data["forward"] || false
398 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
400 defp get_reported_account(account_id) do
401 case User.get_cached_by_id(account_id) do
402 %User{} = account -> {:ok, account}
403 _ -> {:error, dgettext("errors", "Account not found")}
407 def update_report_state(activity_ids, state) when is_list(activity_ids) do
408 case Utils.update_report_state(activity_ids, state) do
409 :ok -> {:ok, activity_ids}
410 _ -> {:error, dgettext("errors", "Could not update state")}
414 def update_report_state(activity_id, state) do
415 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
416 Utils.update_report_state(activity, state)
418 nil -> {:error, :not_found}
419 _ -> {:error, dgettext("errors", "Could not update state")}
423 def update_activity_scope(activity_id, opts \\ %{}) do
424 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
425 {:ok, activity} <- toggle_sensitive(activity, opts) do
426 set_visibility(activity, opts)
428 nil -> {:error, :not_found}
429 {:error, reason} -> {:error, reason}
433 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
434 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
437 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
438 when is_boolean(sensitive) do
439 new_data = Map.put(object.data, "sensitive", sensitive)
443 |> Object.change(%{data: new_data})
444 |> Object.update_and_set_cache()
446 {:ok, Map.put(activity, :object, object)}
449 defp toggle_sensitive(activity, _), do: {:ok, activity}
451 defp set_visibility(activity, %{"visibility" => visibility}) do
452 Utils.update_activity_visibility(activity, visibility)
455 defp set_visibility(activity, _), do: {:ok, activity}
457 def hide_reblogs(%User{} = user, %User{} = target) do
458 UserRelationship.create_reblog_mute(user, target)
461 def show_reblogs(%User{} = user, %User{} = target) do
462 UserRelationship.delete_reblog_mute(user, target)