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
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 post_chat_message(%User{} = user, %User{} = recipient, content) do
29 Repo.transaction(fn ->
30 with {_, {:ok, chat_message_data, _meta}} <-
31 {:build_object, Builder.chat_message(user, recipient.ap_id, content)},
32 {_, {:ok, chat_message_object}} <-
33 {:create_object, Object.create(chat_message_data)},
34 {_, {:ok, create_activity_data, _meta}} <-
35 {:build_create_activity,
36 Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])},
37 {_, {:ok, %Activity{} = activity, _meta}} <-
38 {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do
49 def follow(follower, followed) do
50 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
52 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
53 {:ok, activity} <- ActivityPub.follow(follower, followed),
54 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
55 {:ok, follower, followed, activity}
59 def unfollow(follower, unfollowed) do
60 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
61 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
62 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
67 def accept_follow_request(follower, followed) do
68 with {:ok, follower} <- User.follow(follower, followed),
69 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
70 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
71 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
76 object: follow_activity.data["id"],
83 def reject_follow_request(follower, followed) do
84 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
85 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
86 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
91 object: follow_activity.data["id"],
98 def delete(activity_id, user) do
99 with {_, %Activity{data: %{"object" => _}} = activity} <-
100 {:find_activity, Activity.get_by_id_with_object(activity_id)},
101 %Object{} = object <- Object.normalize(activity),
102 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
103 {:ok, _} <- unpin(activity_id, user),
104 {:ok, delete} <- ActivityPub.delete(object) do
107 {:find_activity, _} -> {:error, :not_found}
108 _ -> {:error, dgettext("errors", "Could not delete")}
112 def repeat(id_or_ap_id, user, params \\ %{}) do
113 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
114 object <- Object.normalize(activity),
115 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
116 public <- public_announce?(object, params) do
117 if announce_activity do
118 {:ok, announce_activity, object}
120 ActivityPub.announce(user, object, nil, true, public)
123 {:find_activity, _} -> {:error, :not_found}
124 _ -> {:error, dgettext("errors", "Could not repeat")}
128 def unrepeat(id_or_ap_id, user) do
129 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
130 object = Object.normalize(activity)
131 ActivityPub.unannounce(user, object)
133 {:find_activity, _} -> {:error, :not_found}
134 _ -> {:error, dgettext("errors", "Could not unrepeat")}
138 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
139 def favorite(%User{} = user, id) do
140 case favorite_helper(user, id) do
144 {:error, :not_found} = res ->
148 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
149 {:error, dgettext("errors", "Could not favorite")}
153 def favorite_helper(user, id) do
154 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
155 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
156 {_, {:ok, %Activity{} = activity, _meta}} <-
158 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
175 if {:object, {"already liked by this actor", []}} in changeset.errors do
176 {:ok, :already_liked}
186 def unfavorite(id_or_ap_id, user) do
187 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
188 object = Object.normalize(activity)
189 ActivityPub.unlike(user, object)
191 {:find_activity, _} -> {:error, :not_found}
192 _ -> {:error, dgettext("errors", "Could not unfavorite")}
196 def react_with_emoji(id, user, emoji) do
197 with %Activity{} = activity <- Activity.get_by_id(id),
198 object <- Object.normalize(activity) do
199 ActivityPub.react_with_emoji(user, object, emoji)
202 {:error, dgettext("errors", "Could not add reaction emoji")}
206 def unreact_with_emoji(id, user, emoji) do
207 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
208 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
211 {:error, dgettext("errors", "Could not remove reaction emoji")}
215 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
216 with :ok <- validate_not_author(object, user),
217 :ok <- validate_existing_votes(user, object),
218 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
220 Enum.map(choices, fn index ->
221 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
224 ActivityPub.create(%{
225 to: answer_data["to"],
227 context: object.data["context"],
229 additional: %{"cc" => answer_data["cc"]}
235 object = Object.get_cached_by_ap_id(object.data["id"])
236 {:ok, answer_activities, object}
240 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
241 do: {:error, dgettext("errors", "Poll's author can't vote")}
243 defp validate_not_author(_, _), do: :ok
245 defp validate_existing_votes(%{ap_id: ap_id}, object) do
246 if Utils.get_existing_votes(ap_id, object) == [] do
249 {:error, dgettext("errors", "Already voted")}
253 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
254 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
256 defp normalize_and_validate_choices(choices, object) do
257 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
258 {options, max_count} = get_options_and_max_count(object)
259 count = Enum.count(options)
261 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
262 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
263 {:ok, options, choices}
265 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
266 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
270 def public_announce?(_, %{"visibility" => visibility})
271 when visibility in ~w{public unlisted private direct},
272 do: visibility in ~w(public unlisted)
274 def public_announce?(object, _) do
275 Visibility.is_public?(object)
278 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
280 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
281 when visibility in ~w{public unlisted private direct},
282 do: {visibility, get_replied_to_visibility(in_reply_to)}
284 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
285 visibility = {:list, String.to_integer(list_id)}
286 {visibility, get_replied_to_visibility(in_reply_to)}
289 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
290 visibility = get_replied_to_visibility(in_reply_to)
291 {visibility, visibility}
294 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
296 def get_replied_to_visibility(nil), do: nil
298 def get_replied_to_visibility(activity) do
299 with %Object{} = object <- Object.normalize(activity) do
300 Visibility.get_visibility(object)
304 def check_expiry_date({:ok, nil} = res), do: res
306 def check_expiry_date({:ok, in_seconds}) do
307 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
309 if ActivityExpiration.expires_late_enough?(expiry) do
312 {:error, "Expiry date is too soon"}
316 def check_expiry_date(expiry_str) do
317 Ecto.Type.cast(:integer, expiry_str)
318 |> check_expiry_date()
321 def listen(user, %{"title" => _} = data) do
322 with visibility <- data["visibility"] || "public",
323 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
325 Map.take(data, ["album", "artist", "title", "length"])
326 |> Map.put("type", "Audio")
329 |> Map.put("actor", user.ap_id),
331 ActivityPub.listen(%{
335 context: Utils.generate_context_id(),
336 additional: %{"cc" => cc}
342 def post(user, %{"status" => _} = data) do
343 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
345 |> ActivityPub.create(draft.preview?)
346 |> maybe_create_activity_expiration(draft.expires_at)
350 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
351 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
356 defp maybe_create_activity_expiration(result, _), do: result
358 # Updates the emojis for a user based on their profile
360 emoji = emoji_from_profile(user)
361 source_data = Map.put(user.source_data, "tag", emoji)
364 case User.update_source_data(user, source_data) do
369 ActivityPub.update(%{
371 to: [Pleroma.Constants.as_public(), user.follower_address],
374 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
378 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
381 data: %{"type" => "Create"},
382 object: %Object{data: %{"type" => object_type}}
383 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
384 true <- object_type in ["Note", "Article", "Question"],
385 true <- Visibility.is_public?(activity),
386 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
389 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
390 _ -> {:error, dgettext("errors", "Could not pin")}
394 def unpin(id_or_ap_id, user) do
395 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
396 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
399 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
400 _ -> {:error, dgettext("errors", "Could not unpin")}
404 def add_mute(user, activity) do
405 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
408 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
412 def remove_mute(user, activity) do
413 ThreadMute.remove_mute(user.id, activity.data["context"])
417 def thread_muted?(%{id: nil} = _user, _activity), do: false
419 def thread_muted?(user, activity) do
420 ThreadMute.exists?(user.id, activity.data["context"])
423 def report(user, %{"account_id" => account_id} = data) do
424 with {:ok, account} <- get_reported_account(account_id),
425 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
426 {:ok, statuses} <- get_report_statuses(account, data) do
428 context: Utils.generate_context_id(),
432 content: content_html,
433 forward: data["forward"] || false
438 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
440 defp get_reported_account(account_id) do
441 case User.get_cached_by_id(account_id) do
442 %User{} = account -> {:ok, account}
443 _ -> {:error, dgettext("errors", "Account not found")}
447 def update_report_state(activity_ids, state) when is_list(activity_ids) do
448 case Utils.update_report_state(activity_ids, state) do
449 :ok -> {:ok, activity_ids}
450 _ -> {:error, dgettext("errors", "Could not update state")}
454 def update_report_state(activity_id, state) do
455 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
456 Utils.update_report_state(activity, state)
458 nil -> {:error, :not_found}
459 _ -> {:error, dgettext("errors", "Could not update state")}
463 def update_activity_scope(activity_id, opts \\ %{}) do
464 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
465 {:ok, activity} <- toggle_sensitive(activity, opts) do
466 set_visibility(activity, opts)
468 nil -> {:error, :not_found}
469 {:error, reason} -> {:error, reason}
473 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
474 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
477 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
478 when is_boolean(sensitive) do
479 new_data = Map.put(object.data, "sensitive", sensitive)
483 |> Object.change(%{data: new_data})
484 |> Object.update_and_set_cache()
486 {:ok, Map.put(activity, :object, object)}
489 defp toggle_sensitive(activity, _), do: {:ok, activity}
491 defp set_visibility(activity, %{"visibility" => visibility}) do
492 Utils.update_activity_visibility(activity, visibility)
495 defp set_visibility(activity, _), do: {:ok, activity}
497 def hide_reblogs(%User{} = user, %User{} = target) do
498 UserRelationship.create_reblog_mute(user, target)
501 def show_reblogs(%User{} = user, %User{} = target) do
502 UserRelationship.delete_reblog_mute(user, target)