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 unblock(blocker, blocked) do
59 with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked),
60 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
61 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
66 def follow(follower, followed) do
67 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
69 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
70 {:ok, activity} <- ActivityPub.follow(follower, followed),
71 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
72 {:ok, follower, followed, activity}
76 def unfollow(follower, unfollowed) do
77 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
78 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
79 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
84 def accept_follow_request(follower, followed) do
85 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
86 {:ok, follower} <- User.follow(follower, followed),
87 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
88 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
93 object: follow_activity.data["id"],
100 def reject_follow_request(follower, followed) do
101 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
102 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
103 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
104 {:ok, _notifications} <- Notification.dismiss(follow_activity),
106 ActivityPub.reject(%{
107 to: [follower.ap_id],
109 object: follow_activity.data["id"],
116 def delete(activity_id, user) do
117 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
118 {:find_activity, Activity.get_by_id(activity_id)},
119 {_, %Object{} = object, _} <-
120 {:find_object, Object.normalize(activity, false), activity},
121 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
122 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
123 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
126 {:find_activity, _} ->
129 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
130 # We have the create activity, but not the object, it was probably pruned.
131 # Insert a tombstone and try again
132 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
133 {:ok, _tombstone} <- Object.create(tombstone_data) do
134 delete(activity_id, user)
138 "Could not insert tombstone for missing object on deletion. Object is #{object}."
141 {:error, dgettext("errors", "Could not delete")}
145 {:error, dgettext("errors", "Could not delete")}
149 def repeat(id, user, params \\ %{}) do
150 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
151 {:find_activity, Activity.get_by_id(id)},
152 object <- Object.normalize(activity),
153 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
154 public <- public_announce?(object, params) do
155 if announce_activity do
156 {:ok, announce_activity, object}
158 ActivityPub.announce(user, object, nil, true, public)
161 {:find_activity, _} -> {:error, :not_found}
162 _ -> {:error, dgettext("errors", "Could not repeat")}
166 def unrepeat(id, user) do
167 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
168 {:find_activity, Activity.get_by_id(id)},
169 %Object{} = note <- Object.normalize(activity, false),
170 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
171 {:ok, undo, _} <- Builder.undo(user, announce),
172 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
175 {:find_activity, _} -> {:error, :not_found}
176 _ -> {:error, dgettext("errors", "Could not unrepeat")}
180 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
181 def favorite(%User{} = user, id) do
182 case favorite_helper(user, id) do
186 {:error, :not_found} = res ->
190 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
191 {:error, dgettext("errors", "Could not favorite")}
195 def favorite_helper(user, id) do
196 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
197 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
198 {_, {:ok, %Activity{} = activity, _meta}} <-
200 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
217 if {:object, {"already liked by this actor", []}} in changeset.errors do
218 {:ok, :already_liked}
228 def unfavorite(id, user) do
229 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
230 {:find_activity, Activity.get_by_id(id)},
231 %Object{} = note <- Object.normalize(activity, false),
232 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
233 {:ok, undo, _} <- Builder.undo(user, like),
234 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
237 {:find_activity, _} -> {:error, :not_found}
238 _ -> {:error, dgettext("errors", "Could not unfavorite")}
242 def react_with_emoji(id, user, emoji) do
243 with %Activity{} = activity <- Activity.get_by_id(id),
244 object <- Object.normalize(activity),
245 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
246 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
250 {:error, dgettext("errors", "Could not add reaction emoji")}
254 def unreact_with_emoji(id, user, emoji) do
255 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
256 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
257 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
261 {:error, dgettext("errors", "Could not remove reaction emoji")}
265 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
266 with :ok <- validate_not_author(object, user),
267 :ok <- validate_existing_votes(user, object),
268 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
270 Enum.map(choices, fn index ->
271 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
274 ActivityPub.create(%{
275 to: answer_data["to"],
277 context: object.data["context"],
279 additional: %{"cc" => answer_data["cc"]}
285 object = Object.get_cached_by_ap_id(object.data["id"])
286 {:ok, answer_activities, object}
290 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
291 do: {:error, dgettext("errors", "Poll's author can't vote")}
293 defp validate_not_author(_, _), do: :ok
295 defp validate_existing_votes(%{ap_id: ap_id}, object) do
296 if Utils.get_existing_votes(ap_id, object) == [] do
299 {:error, dgettext("errors", "Already voted")}
303 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
304 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
306 defp normalize_and_validate_choices(choices, object) do
307 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
308 {options, max_count} = get_options_and_max_count(object)
309 count = Enum.count(options)
311 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
312 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
313 {:ok, options, choices}
315 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
316 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
320 def public_announce?(_, %{"visibility" => visibility})
321 when visibility in ~w{public unlisted private direct},
322 do: visibility in ~w(public unlisted)
324 def public_announce?(object, _) do
325 Visibility.is_public?(object)
328 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
330 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
331 when visibility in ~w{public unlisted private direct},
332 do: {visibility, get_replied_to_visibility(in_reply_to)}
334 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
335 visibility = {:list, String.to_integer(list_id)}
336 {visibility, get_replied_to_visibility(in_reply_to)}
339 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
340 visibility = get_replied_to_visibility(in_reply_to)
341 {visibility, visibility}
344 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
346 def get_replied_to_visibility(nil), do: nil
348 def get_replied_to_visibility(activity) do
349 with %Object{} = object <- Object.normalize(activity) do
350 Visibility.get_visibility(object)
354 def check_expiry_date({:ok, nil} = res), do: res
356 def check_expiry_date({:ok, in_seconds}) do
357 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
359 if ActivityExpiration.expires_late_enough?(expiry) do
362 {:error, "Expiry date is too soon"}
366 def check_expiry_date(expiry_str) do
367 Ecto.Type.cast(:integer, expiry_str)
368 |> check_expiry_date()
371 def listen(user, %{"title" => _} = data) do
372 with visibility <- data["visibility"] || "public",
373 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
375 Map.take(data, ["album", "artist", "title", "length"])
376 |> Map.put("type", "Audio")
379 |> Map.put("actor", user.ap_id),
381 ActivityPub.listen(%{
385 context: Utils.generate_context_id(),
386 additional: %{"cc" => cc}
392 def post(user, %{"status" => _} = data) do
393 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
395 |> ActivityPub.create(draft.preview?)
396 |> maybe_create_activity_expiration(draft.expires_at)
400 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
401 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
406 defp maybe_create_activity_expiration(result, _), do: result
408 def pin(id, %{ap_id: user_ap_id} = user) do
411 data: %{"type" => "Create"},
412 object: %Object{data: %{"type" => object_type}}
413 } = activity <- Activity.get_by_id_with_object(id),
414 true <- object_type in ["Note", "Article", "Question"],
415 true <- Visibility.is_public?(activity),
416 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
419 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
420 _ -> {:error, dgettext("errors", "Could not pin")}
424 def unpin(id, user) do
425 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
426 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
429 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
430 _ -> {:error, dgettext("errors", "Could not unpin")}
434 def add_mute(user, activity) do
435 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
438 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
442 def remove_mute(user, activity) do
443 ThreadMute.remove_mute(user.id, activity.data["context"])
447 def thread_muted?(%{id: nil} = _user, _activity), do: false
449 def thread_muted?(user, activity) do
450 ThreadMute.exists?(user.id, activity.data["context"])
453 def report(user, data) do
454 with {:ok, account} <- get_reported_account(data.account_id),
455 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
456 {:ok, statuses} <- get_report_statuses(account, data) do
458 context: Utils.generate_context_id(),
462 content: content_html,
463 forward: Map.get(data, :forward, false)
468 defp get_reported_account(account_id) do
469 case User.get_cached_by_id(account_id) do
470 %User{} = account -> {:ok, account}
471 _ -> {:error, dgettext("errors", "Account not found")}
475 def update_report_state(activity_ids, state) when is_list(activity_ids) do
476 case Utils.update_report_state(activity_ids, state) do
477 :ok -> {:ok, activity_ids}
478 _ -> {:error, dgettext("errors", "Could not update state")}
482 def update_report_state(activity_id, state) do
483 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
484 Utils.update_report_state(activity, state)
486 nil -> {:error, :not_found}
487 _ -> {:error, dgettext("errors", "Could not update state")}
491 def update_activity_scope(activity_id, opts \\ %{}) do
492 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
493 {:ok, activity} <- toggle_sensitive(activity, opts) do
494 set_visibility(activity, opts)
496 nil -> {:error, :not_found}
497 {:error, reason} -> {:error, reason}
501 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
502 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
505 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
506 when is_boolean(sensitive) do
507 new_data = Map.put(object.data, "sensitive", sensitive)
511 |> Object.change(%{data: new_data})
512 |> Object.update_and_set_cache()
514 {:ok, Map.put(activity, :object, object)}
517 defp toggle_sensitive(activity, _), do: {:ok, activity}
519 defp set_visibility(activity, %{"visibility" => visibility}) do
520 Utils.update_activity_visibility(activity, visibility)
523 defp set_visibility(activity, _), do: {:ok, activity}
525 def hide_reblogs(%User{} = user, %User{} = target) do
526 UserRelationship.create_reblog_mute(user, target)
529 def show_reblogs(%User{} = user, %User{} = target) do
530 UserRelationship.delete_reblog_mute(user, target)