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.Formatter
13 alias Pleroma.ThreadMute
15 alias Pleroma.UserRelationship
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Builder
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Utils
20 alias Pleroma.Web.ActivityPub.Visibility
22 import Pleroma.Web.Gettext
23 import Pleroma.Web.CommonAPI.Utils
25 require Pleroma.Constants
28 def post_chat_message(%User{} = user, %User{} = recipient, content) do
30 Repo.transaction(fn ->
33 String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])},
34 {_, {:ok, chat_message_data, _meta}} <-
39 content |> Formatter.html_escape("text/plain")
41 {_, {:ok, chat_message_object}} <-
42 {:create_object, Object.create(chat_message_data)},
43 {_, {:ok, create_activity_data, _meta}} <-
44 {:build_create_activity,
45 Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])},
46 {_, {:ok, %Activity{} = activity, _meta}} <-
47 {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do
50 {:content_length, false} -> {:error, :content_too_long}
61 def follow(follower, followed) do
62 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
64 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
65 {:ok, activity} <- ActivityPub.follow(follower, followed),
66 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
67 {:ok, follower, followed, activity}
71 def unfollow(follower, unfollowed) do
72 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
73 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
74 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
79 def accept_follow_request(follower, followed) do
80 with {:ok, follower} <- User.follow(follower, followed),
81 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
82 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
83 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
88 object: follow_activity.data["id"],
95 def reject_follow_request(follower, followed) do
96 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
97 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
98 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
100 ActivityPub.reject(%{
101 to: [follower.ap_id],
103 object: follow_activity.data["id"],
110 def delete(activity_id, user) do
111 with {_, %Activity{data: %{"object" => _}} = activity} <-
112 {:find_activity, Activity.get_by_id_with_object(activity_id)},
113 %Object{} = object <- Object.normalize(activity),
114 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
115 {:ok, _} <- unpin(activity_id, user),
116 {:ok, delete} <- ActivityPub.delete(object) do
119 {:find_activity, _} -> {:error, :not_found}
120 _ -> {:error, dgettext("errors", "Could not delete")}
124 def repeat(id_or_ap_id, user, params \\ %{}) do
125 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
126 object <- Object.normalize(activity),
127 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
128 public <- public_announce?(object, params) do
129 if announce_activity do
130 {:ok, announce_activity, object}
132 ActivityPub.announce(user, object, nil, true, public)
135 {:find_activity, _} -> {:error, :not_found}
136 _ -> {:error, dgettext("errors", "Could not repeat")}
140 def unrepeat(id_or_ap_id, user) do
141 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
142 object = Object.normalize(activity)
143 ActivityPub.unannounce(user, object)
145 {:find_activity, _} -> {:error, :not_found}
146 _ -> {:error, dgettext("errors", "Could not unrepeat")}
150 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
151 def favorite(%User{} = user, id) do
152 case favorite_helper(user, id) do
156 {:error, :not_found} = res ->
160 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
161 {:error, dgettext("errors", "Could not favorite")}
165 def favorite_helper(user, id) do
166 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
167 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
168 {_, {:ok, %Activity{} = activity, _meta}} <-
170 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
187 if {:object, {"already liked by this actor", []}} in changeset.errors do
188 {:ok, :already_liked}
198 def unfavorite(id_or_ap_id, user) do
199 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
200 object = Object.normalize(activity)
201 ActivityPub.unlike(user, object)
203 {:find_activity, _} -> {:error, :not_found}
204 _ -> {:error, dgettext("errors", "Could not unfavorite")}
208 def react_with_emoji(id, user, emoji) do
209 with %Activity{} = activity <- Activity.get_by_id(id),
210 object <- Object.normalize(activity) do
211 ActivityPub.react_with_emoji(user, object, emoji)
214 {:error, dgettext("errors", "Could not add reaction emoji")}
218 def unreact_with_emoji(id, user, emoji) do
219 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
220 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
223 {:error, dgettext("errors", "Could not remove reaction emoji")}
227 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
228 with :ok <- validate_not_author(object, user),
229 :ok <- validate_existing_votes(user, object),
230 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
232 Enum.map(choices, fn index ->
233 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
236 ActivityPub.create(%{
237 to: answer_data["to"],
239 context: object.data["context"],
241 additional: %{"cc" => answer_data["cc"]}
247 object = Object.get_cached_by_ap_id(object.data["id"])
248 {:ok, answer_activities, object}
252 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
253 do: {:error, dgettext("errors", "Poll's author can't vote")}
255 defp validate_not_author(_, _), do: :ok
257 defp validate_existing_votes(%{ap_id: ap_id}, object) do
258 if Utils.get_existing_votes(ap_id, object) == [] do
261 {:error, dgettext("errors", "Already voted")}
265 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
266 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
268 defp normalize_and_validate_choices(choices, object) do
269 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
270 {options, max_count} = get_options_and_max_count(object)
271 count = Enum.count(options)
273 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
274 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
275 {:ok, options, choices}
277 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
278 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
282 def public_announce?(_, %{"visibility" => visibility})
283 when visibility in ~w{public unlisted private direct},
284 do: visibility in ~w(public unlisted)
286 def public_announce?(object, _) do
287 Visibility.is_public?(object)
290 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
292 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
293 when visibility in ~w{public unlisted private direct},
294 do: {visibility, get_replied_to_visibility(in_reply_to)}
296 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
297 visibility = {:list, String.to_integer(list_id)}
298 {visibility, get_replied_to_visibility(in_reply_to)}
301 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
302 visibility = get_replied_to_visibility(in_reply_to)
303 {visibility, visibility}
306 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
308 def get_replied_to_visibility(nil), do: nil
310 def get_replied_to_visibility(activity) do
311 with %Object{} = object <- Object.normalize(activity) do
312 Visibility.get_visibility(object)
316 def check_expiry_date({:ok, nil} = res), do: res
318 def check_expiry_date({:ok, in_seconds}) do
319 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
321 if ActivityExpiration.expires_late_enough?(expiry) do
324 {:error, "Expiry date is too soon"}
328 def check_expiry_date(expiry_str) do
329 Ecto.Type.cast(:integer, expiry_str)
330 |> check_expiry_date()
333 def listen(user, %{"title" => _} = data) do
334 with visibility <- data["visibility"] || "public",
335 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
337 Map.take(data, ["album", "artist", "title", "length"])
338 |> Map.put("type", "Audio")
341 |> Map.put("actor", user.ap_id),
343 ActivityPub.listen(%{
347 context: Utils.generate_context_id(),
348 additional: %{"cc" => cc}
354 def post(user, %{"status" => _} = data) do
355 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
357 |> ActivityPub.create(draft.preview?)
358 |> maybe_create_activity_expiration(draft.expires_at)
362 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
363 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
368 defp maybe_create_activity_expiration(result, _), do: result
370 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
373 data: %{"type" => "Create"},
374 object: %Object{data: %{"type" => object_type}}
375 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
376 true <- object_type in ["Note", "Article", "Question"],
377 true <- Visibility.is_public?(activity),
378 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
381 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
382 _ -> {:error, dgettext("errors", "Could not pin")}
386 def unpin(id_or_ap_id, user) do
387 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
388 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
391 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
392 _ -> {:error, dgettext("errors", "Could not unpin")}
396 def add_mute(user, activity) do
397 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
400 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
404 def remove_mute(user, activity) do
405 ThreadMute.remove_mute(user.id, activity.data["context"])
409 def thread_muted?(%{id: nil} = _user, _activity), do: false
411 def thread_muted?(user, activity) do
412 ThreadMute.exists?(user.id, activity.data["context"])
415 def report(user, %{"account_id" => account_id} = data) do
416 with {:ok, account} <- get_reported_account(account_id),
417 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
418 {:ok, statuses} <- get_report_statuses(account, data) do
420 context: Utils.generate_context_id(),
424 content: content_html,
425 forward: data["forward"] || false
430 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
432 defp get_reported_account(account_id) do
433 case User.get_cached_by_id(account_id) do
434 %User{} = account -> {:ok, account}
435 _ -> {:error, dgettext("errors", "Account not found")}
439 def update_report_state(activity_ids, state) when is_list(activity_ids) do
440 case Utils.update_report_state(activity_ids, state) do
441 :ok -> {:ok, activity_ids}
442 _ -> {:error, dgettext("errors", "Could not update state")}
446 def update_report_state(activity_id, state) do
447 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
448 Utils.update_report_state(activity, state)
450 nil -> {:error, :not_found}
451 _ -> {:error, dgettext("errors", "Could not update state")}
455 def update_activity_scope(activity_id, opts \\ %{}) do
456 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
457 {:ok, activity} <- toggle_sensitive(activity, opts) do
458 set_visibility(activity, opts)
460 nil -> {:error, :not_found}
461 {:error, reason} -> {:error, reason}
465 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
466 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
469 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
470 when is_boolean(sensitive) do
471 new_data = Map.put(object.data, "sensitive", sensitive)
475 |> Object.change(%{data: new_data})
476 |> Object.update_and_set_cache()
478 {:ok, Map.put(activity, :object, object)}
481 defp toggle_sensitive(activity, _), do: {:ok, activity}
483 defp set_visibility(activity, %{"visibility" => visibility}) do
484 Utils.update_activity_visibility(activity, visibility)
487 defp set_visibility(activity, _), do: {:ok, activity}
489 def hide_reblogs(%User{} = user, %User{} = target) do
490 UserRelationship.create_reblog_mute(user, target)
493 def show_reblogs(%User{} = user, %User{} = target) do
494 UserRelationship.delete_reblog_mute(user, target)