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
324 ActivityPub.create(draft.changes, draft.preview?)
328 def pin(id, %{ap_id: user_ap_id} = user) do
331 data: %{"type" => "Create"},
332 object: %Object{data: %{"type" => object_type}}
333 } = activity <- Activity.get_by_id_with_object(id),
334 true <- object_type in ["Note", "Article", "Question"],
335 true <- Visibility.is_public?(activity),
336 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
339 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
340 _ -> {:error, dgettext("errors", "Could not pin")}
344 def unpin(id, user) do
345 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
346 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
349 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
350 _ -> {:error, dgettext("errors", "Could not unpin")}
354 def add_mute(user, activity) do
355 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
358 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
362 def remove_mute(user, activity) do
363 ThreadMute.remove_mute(user.id, activity.data["context"])
367 def thread_muted?(%{id: nil} = _user, _activity), do: false
369 def thread_muted?(user, activity) do
370 ThreadMute.exists?(user.id, activity.data["context"])
373 def report(user, %{"account_id" => account_id} = data) do
374 with {:ok, account} <- get_reported_account(account_id),
375 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
376 {:ok, statuses} <- get_report_statuses(account, data) do
378 context: Utils.generate_context_id(),
382 content: content_html,
383 forward: data["forward"] || false
388 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
390 defp get_reported_account(account_id) do
391 case User.get_cached_by_id(account_id) do
392 %User{} = account -> {:ok, account}
393 _ -> {:error, dgettext("errors", "Account not found")}
397 def update_report_state(activity_ids, state) when is_list(activity_ids) do
398 case Utils.update_report_state(activity_ids, state) do
399 :ok -> {:ok, activity_ids}
400 _ -> {:error, dgettext("errors", "Could not update state")}
404 def update_report_state(activity_id, state) do
405 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
406 Utils.update_report_state(activity, state)
408 nil -> {:error, :not_found}
409 _ -> {:error, dgettext("errors", "Could not update state")}
413 def update_activity_scope(activity_id, opts \\ %{}) do
414 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
415 {:ok, activity} <- toggle_sensitive(activity, opts) do
416 set_visibility(activity, opts)
418 nil -> {:error, :not_found}
419 {:error, reason} -> {:error, reason}
423 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
424 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
427 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
428 when is_boolean(sensitive) do
429 new_data = Map.put(object.data, "sensitive", sensitive)
433 |> Object.change(%{data: new_data})
434 |> Object.update_and_set_cache()
436 {:ok, Map.put(activity, :object, object)}
439 defp toggle_sensitive(activity, _), do: {:ok, activity}
441 defp set_visibility(activity, %{"visibility" => visibility}) do
442 Utils.update_activity_visibility(activity, visibility)
445 defp set_visibility(activity, _), do: {:ok, activity}
447 def hide_reblogs(%User{} = user, %User{} = target) do
448 UserRelationship.create_reblog_mute(user, target)
451 def show_reblogs(%User{} = user, %User{} = target) do
452 UserRelationship.delete_reblog_mute(user, target)