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, create_activity_data, _meta}} <-
42 {:build_create_activity,
43 Builder.create(user, chat_message_data, [recipient.ap_id])},
44 {_, {:ok, %Activity{} = activity, _meta}} <-
46 Pipeline.common_pipeline(create_activity_data,
51 {:content_length, false} -> {:error, :content_too_long}
62 def follow(follower, followed) do
63 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
65 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
66 {:ok, activity} <- ActivityPub.follow(follower, followed),
67 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
68 {:ok, follower, followed, activity}
72 def unfollow(follower, unfollowed) do
73 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
74 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
75 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
80 def accept_follow_request(follower, followed) do
81 with {:ok, follower} <- User.follow(follower, followed),
82 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
83 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
84 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
89 object: follow_activity.data["id"],
96 def reject_follow_request(follower, followed) do
97 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
98 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
99 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
101 ActivityPub.reject(%{
102 to: [follower.ap_id],
104 object: follow_activity.data["id"],
111 def delete(activity_id, user) do
112 with {_, %Activity{data: %{"object" => _}} = activity} <-
113 {:find_activity, Activity.get_by_id_with_object(activity_id)},
114 %Object{} = object <- Object.normalize(activity),
115 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
116 {:ok, _} <- unpin(activity_id, user),
117 {:ok, delete} <- ActivityPub.delete(object) do
120 {:find_activity, _} -> {:error, :not_found}
121 _ -> {:error, dgettext("errors", "Could not delete")}
125 def repeat(id, user, params \\ %{}) do
126 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
127 {:find_activity, Activity.get_by_id(id)},
128 object <- Object.normalize(activity),
129 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
130 public <- public_announce?(object, params) do
131 if announce_activity do
132 {:ok, announce_activity, object}
134 ActivityPub.announce(user, object, nil, true, public)
137 {:find_activity, _} -> {:error, :not_found}
138 _ -> {:error, dgettext("errors", "Could not repeat")}
142 def unrepeat(id, user) do
143 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
144 {:find_activity, Activity.get_by_id(id)} do
145 object = Object.normalize(activity)
146 ActivityPub.unannounce(user, object)
148 {:find_activity, _} -> {:error, :not_found}
149 _ -> {:error, dgettext("errors", "Could not unrepeat")}
153 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
154 def favorite(%User{} = user, id) do
155 case favorite_helper(user, id) do
159 {:error, :not_found} = res ->
163 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
164 {:error, dgettext("errors", "Could not favorite")}
168 def favorite_helper(user, id) do
169 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
170 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
171 {_, {:ok, %Activity{} = activity, _meta}} <-
173 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
190 if {:object, {"already liked by this actor", []}} in changeset.errors do
191 {:ok, :already_liked}
201 def unfavorite(id, user) do
202 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
203 {:find_activity, Activity.get_by_id(id)} do
204 object = Object.normalize(activity)
205 ActivityPub.unlike(user, object)
207 {:find_activity, _} -> {:error, :not_found}
208 _ -> {:error, dgettext("errors", "Could not unfavorite")}
212 def react_with_emoji(id, user, emoji) do
213 with %Activity{} = activity <- Activity.get_by_id(id),
214 object <- Object.normalize(activity) do
215 ActivityPub.react_with_emoji(user, object, emoji)
218 {:error, dgettext("errors", "Could not add reaction emoji")}
222 def unreact_with_emoji(id, user, emoji) do
223 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
224 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
227 {:error, dgettext("errors", "Could not remove reaction emoji")}
231 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
232 with :ok <- validate_not_author(object, user),
233 :ok <- validate_existing_votes(user, object),
234 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
236 Enum.map(choices, fn index ->
237 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
240 ActivityPub.create(%{
241 to: answer_data["to"],
243 context: object.data["context"],
245 additional: %{"cc" => answer_data["cc"]}
251 object = Object.get_cached_by_ap_id(object.data["id"])
252 {:ok, answer_activities, object}
256 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
257 do: {:error, dgettext("errors", "Poll's author can't vote")}
259 defp validate_not_author(_, _), do: :ok
261 defp validate_existing_votes(%{ap_id: ap_id}, object) do
262 if Utils.get_existing_votes(ap_id, object) == [] do
265 {:error, dgettext("errors", "Already voted")}
269 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
270 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
272 defp normalize_and_validate_choices(choices, object) do
273 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
274 {options, max_count} = get_options_and_max_count(object)
275 count = Enum.count(options)
277 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
278 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
279 {:ok, options, choices}
281 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
282 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
286 def public_announce?(_, %{"visibility" => visibility})
287 when visibility in ~w{public unlisted private direct},
288 do: visibility in ~w(public unlisted)
290 def public_announce?(object, _) do
291 Visibility.is_public?(object)
294 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
296 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
297 when visibility in ~w{public unlisted private direct},
298 do: {visibility, get_replied_to_visibility(in_reply_to)}
300 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
301 visibility = {:list, String.to_integer(list_id)}
302 {visibility, get_replied_to_visibility(in_reply_to)}
305 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
306 visibility = get_replied_to_visibility(in_reply_to)
307 {visibility, visibility}
310 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
312 def get_replied_to_visibility(nil), do: nil
314 def get_replied_to_visibility(activity) do
315 with %Object{} = object <- Object.normalize(activity) do
316 Visibility.get_visibility(object)
320 def check_expiry_date({:ok, nil} = res), do: res
322 def check_expiry_date({:ok, in_seconds}) do
323 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
325 if ActivityExpiration.expires_late_enough?(expiry) do
328 {:error, "Expiry date is too soon"}
332 def check_expiry_date(expiry_str) do
333 Ecto.Type.cast(:integer, expiry_str)
334 |> check_expiry_date()
337 def listen(user, %{"title" => _} = data) do
338 with visibility <- data["visibility"] || "public",
339 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
341 Map.take(data, ["album", "artist", "title", "length"])
342 |> Map.put("type", "Audio")
345 |> Map.put("actor", user.ap_id),
347 ActivityPub.listen(%{
351 context: Utils.generate_context_id(),
352 additional: %{"cc" => cc}
358 def post(user, %{"status" => _} = data) do
359 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
361 |> ActivityPub.create(draft.preview?)
362 |> maybe_create_activity_expiration(draft.expires_at)
366 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
367 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
372 defp maybe_create_activity_expiration(result, _), do: result
374 def pin(id, %{ap_id: user_ap_id} = user) do
377 data: %{"type" => "Create"},
378 object: %Object{data: %{"type" => object_type}}
379 } = activity <- Activity.get_by_id_with_object(id),
380 true <- object_type in ["Note", "Article", "Question"],
381 true <- Visibility.is_public?(activity),
382 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
385 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
386 _ -> {:error, dgettext("errors", "Could not pin")}
390 def unpin(id, user) do
391 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
392 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
395 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
396 _ -> {:error, dgettext("errors", "Could not unpin")}
400 def add_mute(user, activity) do
401 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
404 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
408 def remove_mute(user, activity) do
409 ThreadMute.remove_mute(user.id, activity.data["context"])
413 def thread_muted?(%{id: nil} = _user, _activity), do: false
415 def thread_muted?(user, activity) do
416 ThreadMute.exists?(user.id, activity.data["context"])
419 def report(user, %{"account_id" => account_id} = data) do
420 with {:ok, account} <- get_reported_account(account_id),
421 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
422 {:ok, statuses} <- get_report_statuses(account, data) do
424 context: Utils.generate_context_id(),
428 content: content_html,
429 forward: data["forward"] || false
434 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
436 defp get_reported_account(account_id) do
437 case User.get_cached_by_id(account_id) do
438 %User{} = account -> {:ok, account}
439 _ -> {:error, dgettext("errors", "Account not found")}
443 def update_report_state(activity_ids, state) when is_list(activity_ids) do
444 case Utils.update_report_state(activity_ids, state) do
445 :ok -> {:ok, activity_ids}
446 _ -> {:error, dgettext("errors", "Could not update state")}
450 def update_report_state(activity_id, state) do
451 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
452 Utils.update_report_state(activity, state)
454 nil -> {:error, :not_found}
455 _ -> {:error, dgettext("errors", "Could not update state")}
459 def update_activity_scope(activity_id, opts \\ %{}) do
460 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
461 {:ok, activity} <- toggle_sensitive(activity, opts) do
462 set_visibility(activity, opts)
464 nil -> {:error, :not_found}
465 {:error, reason} -> {:error, reason}
469 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
470 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
473 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
474 when is_boolean(sensitive) do
475 new_data = Map.put(object.data, "sensitive", sensitive)
479 |> Object.change(%{data: new_data})
480 |> Object.update_and_set_cache()
482 {:ok, Map.put(activity, :object, object)}
485 defp toggle_sensitive(activity, _), do: {:ok, activity}
487 defp set_visibility(activity, %{"visibility" => visibility}) do
488 Utils.update_activity_visibility(activity, visibility)
491 defp set_visibility(activity, _), do: {:ok, activity}
493 def hide_reblogs(%User{} = user, %User{} = target) do
494 UserRelationship.create_reblog_mute(user, target)
497 def show_reblogs(%User{} = user, %User{} = target) do
498 UserRelationship.delete_reblog_mute(user, target)