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 {:ok, follower} <- User.follow(follower, followed),
47 %Activity{} = follow_activity <- Utils.fetch_latest_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)} do
111 object = Object.normalize(activity)
112 ActivityPub.unannounce(user, object)
114 {:find_activity, _} -> {:error, :not_found}
115 _ -> {:error, dgettext("errors", "Could not unrepeat")}
119 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
120 def favorite(%User{} = user, id) do
121 case favorite_helper(user, id) do
125 {:error, :not_found} = res ->
129 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
130 {:error, dgettext("errors", "Could not favorite")}
134 def favorite_helper(user, id) do
135 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
136 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
137 {_, {:ok, %Activity{} = activity, _meta}} <-
139 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
156 if {:object, {"already liked by this actor", []}} in changeset.errors do
157 {:ok, :already_liked}
167 def unfavorite(id, user) do
168 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
169 {:find_activity, Activity.get_by_id(id)} do
170 object = Object.normalize(activity)
171 ActivityPub.unlike(user, object)
173 {:find_activity, _} -> {:error, :not_found}
174 _ -> {:error, dgettext("errors", "Could not unfavorite")}
178 def react_with_emoji(id, user, emoji) do
179 with %Activity{} = activity <- Activity.get_by_id(id),
180 object <- Object.normalize(activity) do
181 ActivityPub.react_with_emoji(user, object, emoji)
184 {:error, dgettext("errors", "Could not add reaction emoji")}
188 def unreact_with_emoji(id, user, emoji) do
189 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
190 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
193 {:error, dgettext("errors", "Could not remove reaction emoji")}
197 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
198 with :ok <- validate_not_author(object, user),
199 :ok <- validate_existing_votes(user, object),
200 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
202 Enum.map(choices, fn index ->
203 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
206 ActivityPub.create(%{
207 to: answer_data["to"],
209 context: object.data["context"],
211 additional: %{"cc" => answer_data["cc"]}
217 object = Object.get_cached_by_ap_id(object.data["id"])
218 {:ok, answer_activities, object}
222 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
223 do: {:error, dgettext("errors", "Poll's author can't vote")}
225 defp validate_not_author(_, _), do: :ok
227 defp validate_existing_votes(%{ap_id: ap_id}, object) do
228 if Utils.get_existing_votes(ap_id, object) == [] do
231 {:error, dgettext("errors", "Already voted")}
235 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
236 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
238 defp normalize_and_validate_choices(choices, object) do
239 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
240 {options, max_count} = get_options_and_max_count(object)
241 count = Enum.count(options)
243 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
244 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
245 {:ok, options, choices}
247 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
248 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
252 def public_announce?(_, %{"visibility" => visibility})
253 when visibility in ~w{public unlisted private direct},
254 do: visibility in ~w(public unlisted)
256 def public_announce?(object, _) do
257 Visibility.is_public?(object)
260 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
262 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
263 when visibility in ~w{public unlisted private direct},
264 do: {visibility, get_replied_to_visibility(in_reply_to)}
266 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
267 visibility = {:list, String.to_integer(list_id)}
268 {visibility, get_replied_to_visibility(in_reply_to)}
271 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
272 visibility = get_replied_to_visibility(in_reply_to)
273 {visibility, visibility}
276 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
278 def get_replied_to_visibility(nil), do: nil
280 def get_replied_to_visibility(activity) do
281 with %Object{} = object <- Object.normalize(activity) do
282 Visibility.get_visibility(object)
286 def check_expiry_date({:ok, nil} = res), do: res
288 def check_expiry_date({:ok, in_seconds}) do
289 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
291 if ActivityExpiration.expires_late_enough?(expiry) do
294 {:error, "Expiry date is too soon"}
298 def check_expiry_date(expiry_str) do
299 Ecto.Type.cast(:integer, expiry_str)
300 |> check_expiry_date()
303 def listen(user, %{"title" => _} = data) do
304 with visibility <- data["visibility"] || "public",
305 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
307 Map.take(data, ["album", "artist", "title", "length"])
308 |> Map.put("type", "Audio")
311 |> Map.put("actor", user.ap_id),
313 ActivityPub.listen(%{
317 context: Utils.generate_context_id(),
318 additional: %{"cc" => cc}
324 def post(user, %{"status" => _} = data) do
325 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
327 |> ActivityPub.create(draft.preview?)
328 |> maybe_create_activity_expiration(draft.expires_at)
332 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
333 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
338 defp maybe_create_activity_expiration(result, _), do: result
340 def pin(id, %{ap_id: user_ap_id} = user) do
343 data: %{"type" => "Create"},
344 object: %Object{data: %{"type" => object_type}}
345 } = activity <- Activity.get_by_id_with_object(id),
346 true <- object_type in ["Note", "Article", "Question"],
347 true <- Visibility.is_public?(activity),
348 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
351 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
352 _ -> {:error, dgettext("errors", "Could not pin")}
356 def unpin(id, user) do
357 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
358 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
361 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
362 _ -> {:error, dgettext("errors", "Could not unpin")}
366 def add_mute(user, activity) do
367 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
370 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
374 def remove_mute(user, activity) do
375 ThreadMute.remove_mute(user.id, activity.data["context"])
379 def thread_muted?(%{id: nil} = _user, _activity), do: false
381 def thread_muted?(user, activity) do
382 ThreadMute.exists?(user.id, activity.data["context"])
385 def report(user, data) do
386 with {:ok, account} <- get_reported_account(data.account_id),
387 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
388 {:ok, statuses} <- get_report_statuses(account, data) do
390 context: Utils.generate_context_id(),
394 content: content_html,
395 forward: Map.get(data, :forward, false)
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)