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
10 alias Pleroma.Notification
12 alias Pleroma.ThreadMute
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
24 require Pleroma.Constants
27 def follow(follower, followed) do
28 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
30 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
31 {:ok, activity} <- ActivityPub.follow(follower, followed),
32 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
33 {:ok, follower, followed, activity}
37 def unfollow(follower, unfollowed) do
38 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
39 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
40 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
45 def accept_follow_request(follower, followed) do
46 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
47 {:ok, follower} <- User.follow(follower, followed),
48 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
49 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
54 object: follow_activity.data["id"],
61 def reject_follow_request(follower, followed) do
62 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
63 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
64 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
65 {:ok, _notifications} <- Notification.dismiss(follow_activity),
70 object: follow_activity.data["id"],
77 def delete(activity_id, user) do
78 with {_, %Activity{data: %{"object" => _}} = activity} <-
79 {:find_activity, Activity.get_by_id_with_object(activity_id)},
80 %Object{} = object <- Object.normalize(activity),
81 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
82 {:ok, _} <- unpin(activity_id, user),
83 {:ok, delete} <- ActivityPub.delete(object) do
86 {:find_activity, _} -> {:error, :not_found}
87 _ -> {:error, dgettext("errors", "Could not delete")}
91 def repeat(id, user, params \\ %{}) do
92 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
93 {:find_activity, Activity.get_by_id(id)},
94 object <- Object.normalize(activity),
95 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
96 public <- public_announce?(object, params) do
97 if announce_activity do
98 {:ok, announce_activity, object}
100 ActivityPub.announce(user, object, nil, true, public)
103 {:find_activity, _} -> {:error, :not_found}
104 _ -> {:error, dgettext("errors", "Could not repeat")}
108 def unrepeat(id, user) do
109 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
110 {:find_activity, Activity.get_by_id(id)},
111 %Object{} = note <- Object.normalize(activity, false),
112 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
113 {:ok, undo, _} <- Builder.undo(user, announce),
114 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
117 {:find_activity, _} -> {:error, :not_found}
118 _ -> {:error, dgettext("errors", "Could not unrepeat")}
122 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
123 def favorite(%User{} = user, id) do
124 case favorite_helper(user, id) do
128 {:error, :not_found} = res ->
132 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
133 {:error, dgettext("errors", "Could not favorite")}
137 def favorite_helper(user, id) do
138 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
139 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
140 {_, {:ok, %Activity{} = activity, _meta}} <-
142 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
159 if {:object, {"already liked by this actor", []}} in changeset.errors do
160 {:ok, :already_liked}
170 def unfavorite(id, user) do
171 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
172 {:find_activity, Activity.get_by_id(id)},
173 %Object{} = note <- Object.normalize(activity, false),
174 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
175 {:ok, undo, _} <- Builder.undo(user, like),
176 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
179 {:find_activity, _} -> {:error, :not_found}
180 _ -> {:error, dgettext("errors", "Could not unfavorite")}
184 def react_with_emoji(id, user, emoji) do
185 with %Activity{} = activity <- Activity.get_by_id(id),
186 object <- Object.normalize(activity) do
187 ActivityPub.react_with_emoji(user, object, emoji)
190 {:error, dgettext("errors", "Could not add reaction emoji")}
194 def unreact_with_emoji(id, user, emoji) do
195 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
196 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
197 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
201 {:error, dgettext("errors", "Could not remove reaction emoji")}
205 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
206 with :ok <- validate_not_author(object, user),
207 :ok <- validate_existing_votes(user, object),
208 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
210 Enum.map(choices, fn index ->
211 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
214 ActivityPub.create(%{
215 to: answer_data["to"],
217 context: object.data["context"],
219 additional: %{"cc" => answer_data["cc"]}
225 object = Object.get_cached_by_ap_id(object.data["id"])
226 {:ok, answer_activities, object}
230 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
231 do: {:error, dgettext("errors", "Poll's author can't vote")}
233 defp validate_not_author(_, _), do: :ok
235 defp validate_existing_votes(%{ap_id: ap_id}, object) do
236 if Utils.get_existing_votes(ap_id, object) == [] do
239 {:error, dgettext("errors", "Already voted")}
243 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
244 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
246 defp normalize_and_validate_choices(choices, object) do
247 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
248 {options, max_count} = get_options_and_max_count(object)
249 count = Enum.count(options)
251 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
252 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
253 {:ok, options, choices}
255 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
256 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
260 def public_announce?(_, %{"visibility" => visibility})
261 when visibility in ~w{public unlisted private direct},
262 do: visibility in ~w(public unlisted)
264 def public_announce?(object, _) do
265 Visibility.is_public?(object)
268 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
270 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
271 when visibility in ~w{public unlisted private direct},
272 do: {visibility, get_replied_to_visibility(in_reply_to)}
274 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
275 visibility = {:list, String.to_integer(list_id)}
276 {visibility, get_replied_to_visibility(in_reply_to)}
279 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
280 visibility = get_replied_to_visibility(in_reply_to)
281 {visibility, visibility}
284 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
286 def get_replied_to_visibility(nil), do: nil
288 def get_replied_to_visibility(activity) do
289 with %Object{} = object <- Object.normalize(activity) do
290 Visibility.get_visibility(object)
294 def check_expiry_date({:ok, nil} = res), do: res
296 def check_expiry_date({:ok, in_seconds}) do
297 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
299 if ActivityExpiration.expires_late_enough?(expiry) do
302 {:error, "Expiry date is too soon"}
306 def check_expiry_date(expiry_str) do
307 Ecto.Type.cast(:integer, expiry_str)
308 |> check_expiry_date()
311 def listen(user, %{"title" => _} = data) do
312 with visibility <- data["visibility"] || "public",
313 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
315 Map.take(data, ["album", "artist", "title", "length"])
316 |> Map.put("type", "Audio")
319 |> Map.put("actor", user.ap_id),
321 ActivityPub.listen(%{
325 context: Utils.generate_context_id(),
326 additional: %{"cc" => cc}
332 def post(user, %{"status" => _} = data) do
333 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
335 |> ActivityPub.create(draft.preview?)
336 |> maybe_create_activity_expiration(draft.expires_at)
340 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
341 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
346 defp maybe_create_activity_expiration(result, _), do: result
348 def pin(id, %{ap_id: user_ap_id} = user) do
351 data: %{"type" => "Create"},
352 object: %Object{data: %{"type" => object_type}}
353 } = activity <- Activity.get_by_id_with_object(id),
354 true <- object_type in ["Note", "Article", "Question"],
355 true <- Visibility.is_public?(activity),
356 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
359 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
360 _ -> {:error, dgettext("errors", "Could not pin")}
364 def unpin(id, user) do
365 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
366 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
369 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
370 _ -> {:error, dgettext("errors", "Could not unpin")}
374 def add_mute(user, activity) do
375 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
378 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
382 def remove_mute(user, activity) do
383 ThreadMute.remove_mute(user.id, activity.data["context"])
387 def thread_muted?(%{id: nil} = _user, _activity), do: false
389 def thread_muted?(user, activity) do
390 ThreadMute.exists?(user.id, activity.data["context"])
393 def report(user, data) do
394 with {:ok, account} <- get_reported_account(data.account_id),
395 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
396 {:ok, statuses} <- get_report_statuses(account, data) do
398 context: Utils.generate_context_id(),
402 content: content_html,
403 forward: Map.get(data, :forward, false)
408 defp get_reported_account(account_id) do
409 case User.get_cached_by_id(account_id) do
410 %User{} = account -> {:ok, account}
411 _ -> {:error, dgettext("errors", "Account not found")}
415 def update_report_state(activity_ids, state) when is_list(activity_ids) do
416 case Utils.update_report_state(activity_ids, state) do
417 :ok -> {:ok, activity_ids}
418 _ -> {:error, dgettext("errors", "Could not update state")}
422 def update_report_state(activity_id, state) do
423 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
424 Utils.update_report_state(activity, state)
426 nil -> {:error, :not_found}
427 _ -> {:error, dgettext("errors", "Could not update state")}
431 def update_activity_scope(activity_id, opts \\ %{}) do
432 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
433 {:ok, activity} <- toggle_sensitive(activity, opts) do
434 set_visibility(activity, opts)
436 nil -> {:error, :not_found}
437 {:error, reason} -> {:error, reason}
441 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
442 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
445 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
446 when is_boolean(sensitive) do
447 new_data = Map.put(object.data, "sensitive", sensitive)
451 |> Object.change(%{data: new_data})
452 |> Object.update_and_set_cache()
454 {:ok, Map.put(activity, :object, object)}
457 defp toggle_sensitive(activity, _), do: {:ok, activity}
459 defp set_visibility(activity, %{"visibility" => visibility}) do
460 Utils.update_activity_visibility(activity, visibility)
463 defp set_visibility(activity, _), do: {:ok, activity}
465 def hide_reblogs(%User{} = user, %User{} = target) do
466 UserRelationship.create_reblog_mute(user, target)
469 def show_reblogs(%User{} = user, %User{} = target) do
470 UserRelationship.delete_reblog_mute(user, target)