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
11 alias Pleroma.Notification
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, opts \\ []) do
29 with :ok <- validate_chat_content_length(content),
30 maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
31 {_, {:ok, chat_message_data, _meta}} <-
36 content |> Formatter.html_escape("text/plain"),
37 attachment: maybe_attachment
39 {_, {:ok, create_activity_data, _meta}} <-
40 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
41 {_, {:ok, %Activity{} = activity, _meta}} <-
43 Pipeline.common_pipeline(create_activity_data,
50 defp validate_chat_content_length(content) do
51 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
54 {:error, :content_too_long}
58 def follow(follower, followed) do
59 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
61 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
62 {:ok, activity} <- ActivityPub.follow(follower, followed),
63 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
64 {:ok, follower, followed, activity}
68 def unfollow(follower, unfollowed) do
69 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
70 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
71 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
76 def accept_follow_request(follower, followed) do
77 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
78 {:ok, follower} <- User.follow(follower, followed),
79 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
80 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
85 object: follow_activity.data["id"],
92 def reject_follow_request(follower, followed) do
93 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
94 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
95 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
96 {:ok, _notifications} <- Notification.dismiss(follow_activity),
101 object: follow_activity.data["id"],
108 def delete(activity_id, user) do
109 with {_, %Activity{data: %{"object" => _}} = activity} <-
110 {:find_activity, Activity.get_by_id_with_object(activity_id)},
111 %Object{} = object <- Object.normalize(activity),
112 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
113 {:ok, _} <- unpin(activity_id, user),
114 {:ok, delete} <- ActivityPub.delete(object) do
117 {:find_activity, _} -> {:error, :not_found}
118 _ -> {:error, dgettext("errors", "Could not delete")}
122 def repeat(id, user, params \\ %{}) do
123 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
124 {:find_activity, Activity.get_by_id(id)},
125 object <- Object.normalize(activity),
126 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
127 public <- public_announce?(object, params) do
128 if announce_activity do
129 {:ok, announce_activity, object}
131 ActivityPub.announce(user, object, nil, true, public)
134 {:find_activity, _} -> {:error, :not_found}
135 _ -> {:error, dgettext("errors", "Could not repeat")}
139 def unrepeat(id, user) do
140 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
141 {:find_activity, Activity.get_by_id(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, user) do
199 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
200 {:find_activity, Activity.get_by_id(id)} do
201 object = Object.normalize(activity)
202 ActivityPub.unlike(user, object)
204 {:find_activity, _} -> {:error, :not_found}
205 _ -> {:error, dgettext("errors", "Could not unfavorite")}
209 def react_with_emoji(id, user, emoji) do
210 with %Activity{} = activity <- Activity.get_by_id(id),
211 object <- Object.normalize(activity) do
212 ActivityPub.react_with_emoji(user, object, emoji)
215 {:error, dgettext("errors", "Could not add reaction emoji")}
219 def unreact_with_emoji(id, user, emoji) do
220 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
221 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
224 {:error, dgettext("errors", "Could not remove reaction emoji")}
228 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
229 with :ok <- validate_not_author(object, user),
230 :ok <- validate_existing_votes(user, object),
231 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
233 Enum.map(choices, fn index ->
234 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
237 ActivityPub.create(%{
238 to: answer_data["to"],
240 context: object.data["context"],
242 additional: %{"cc" => answer_data["cc"]}
248 object = Object.get_cached_by_ap_id(object.data["id"])
249 {:ok, answer_activities, object}
253 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
254 do: {:error, dgettext("errors", "Poll's author can't vote")}
256 defp validate_not_author(_, _), do: :ok
258 defp validate_existing_votes(%{ap_id: ap_id}, object) do
259 if Utils.get_existing_votes(ap_id, object) == [] do
262 {:error, dgettext("errors", "Already voted")}
266 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
267 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
269 defp normalize_and_validate_choices(choices, object) do
270 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
271 {options, max_count} = get_options_and_max_count(object)
272 count = Enum.count(options)
274 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
275 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
276 {:ok, options, choices}
278 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
279 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
283 def public_announce?(_, %{"visibility" => visibility})
284 when visibility in ~w{public unlisted private direct},
285 do: visibility in ~w(public unlisted)
287 def public_announce?(object, _) do
288 Visibility.is_public?(object)
291 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
293 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
294 when visibility in ~w{public unlisted private direct},
295 do: {visibility, get_replied_to_visibility(in_reply_to)}
297 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
298 visibility = {:list, String.to_integer(list_id)}
299 {visibility, get_replied_to_visibility(in_reply_to)}
302 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
303 visibility = get_replied_to_visibility(in_reply_to)
304 {visibility, visibility}
307 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
309 def get_replied_to_visibility(nil), do: nil
311 def get_replied_to_visibility(activity) do
312 with %Object{} = object <- Object.normalize(activity) do
313 Visibility.get_visibility(object)
317 def check_expiry_date({:ok, nil} = res), do: res
319 def check_expiry_date({:ok, in_seconds}) do
320 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
322 if ActivityExpiration.expires_late_enough?(expiry) do
325 {:error, "Expiry date is too soon"}
329 def check_expiry_date(expiry_str) do
330 Ecto.Type.cast(:integer, expiry_str)
331 |> check_expiry_date()
334 def listen(user, %{"title" => _} = data) do
335 with visibility <- data["visibility"] || "public",
336 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
338 Map.take(data, ["album", "artist", "title", "length"])
339 |> Map.put("type", "Audio")
342 |> Map.put("actor", user.ap_id),
344 ActivityPub.listen(%{
348 context: Utils.generate_context_id(),
349 additional: %{"cc" => cc}
355 def post(user, %{"status" => _} = data) do
356 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
358 |> ActivityPub.create(draft.preview?)
359 |> maybe_create_activity_expiration(draft.expires_at)
363 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
364 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
369 defp maybe_create_activity_expiration(result, _), do: result
371 def pin(id, %{ap_id: user_ap_id} = user) do
374 data: %{"type" => "Create"},
375 object: %Object{data: %{"type" => object_type}}
376 } = activity <- Activity.get_by_id_with_object(id),
377 true <- object_type in ["Note", "Article", "Question"],
378 true <- Visibility.is_public?(activity),
379 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
382 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
383 _ -> {:error, dgettext("errors", "Could not pin")}
387 def unpin(id, user) do
388 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
389 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
392 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
393 _ -> {:error, dgettext("errors", "Could not unpin")}
397 def add_mute(user, activity) do
398 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
401 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
405 def remove_mute(user, activity) do
406 ThreadMute.remove_mute(user.id, activity.data["context"])
410 def thread_muted?(%{id: nil} = _user, _activity), do: false
412 def thread_muted?(user, activity) do
413 ThreadMute.exists?(user.id, activity.data["context"])
416 def report(user, data) do
417 with {:ok, account} <- get_reported_account(data.account_id),
418 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
419 {:ok, statuses} <- get_report_statuses(account, data) do
421 context: Utils.generate_context_id(),
425 content: content_html,
426 forward: Map.get(data, :forward, false)
431 defp get_reported_account(account_id) do
432 case User.get_cached_by_id(account_id) do
433 %User{} = account -> {:ok, account}
434 _ -> {:error, dgettext("errors", "Account not found")}
438 def update_report_state(activity_ids, state) when is_list(activity_ids) do
439 case Utils.update_report_state(activity_ids, state) do
440 :ok -> {:ok, activity_ids}
441 _ -> {:error, dgettext("errors", "Could not update state")}
445 def update_report_state(activity_id, state) do
446 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
447 Utils.update_report_state(activity, state)
449 nil -> {:error, :not_found}
450 _ -> {:error, dgettext("errors", "Could not update state")}
454 def update_activity_scope(activity_id, opts \\ %{}) do
455 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
456 {:ok, activity} <- toggle_sensitive(activity, opts) do
457 set_visibility(activity, opts)
459 nil -> {:error, :not_found}
460 {:error, reason} -> {:error, reason}
464 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
465 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
468 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
469 when is_boolean(sensitive) do
470 new_data = Map.put(object.data, "sensitive", sensitive)
474 |> Object.change(%{data: new_data})
475 |> Object.update_and_set_cache()
477 {:ok, Map.put(activity, :object, object)}
480 defp toggle_sensitive(activity, _), do: {:ok, activity}
482 defp set_visibility(activity, %{"visibility" => visibility}) do
483 Utils.update_activity_visibility(activity, visibility)
486 defp set_visibility(activity, _), do: {:ok, activity}
488 def hide_reblogs(%User{} = user, %User{} = target) do
489 UserRelationship.create_reblog_mute(user, target)
492 def show_reblogs(%User{} = user, %User{} = target) do
493 UserRelationship.delete_reblog_mute(user, target)