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.Conversation.Participation
8 alias Pleroma.Formatter
10 alias Pleroma.ThreadMute
12 alias Pleroma.UserRelationship
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.Builder
15 alias Pleroma.Web.ActivityPub.Pipeline
16 alias Pleroma.Web.ActivityPub.Utils
17 alias Pleroma.Web.ActivityPub.Visibility
19 import Pleroma.Web.Gettext
20 import Pleroma.Web.CommonAPI.Utils
22 require Pleroma.Constants
25 def block(blocker, blocked) do
26 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
27 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
32 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
33 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
34 :ok <- validate_chat_content_length(content, !!maybe_attachment),
35 {_, {:ok, chat_message_data, _meta}} <-
40 content |> format_chat_content,
41 attachment: maybe_attachment
43 {_, {:ok, create_activity_data, _meta}} <-
44 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
45 {_, {:ok, %Activity{} = activity, _meta}} <-
47 Pipeline.common_pipeline(create_activity_data,
54 defp format_chat_content(nil), do: nil
56 defp format_chat_content(content) do
59 |> Formatter.html_escape("text/plain")
60 |> Formatter.linkify()
61 |> (fn {text, mentions, tags} ->
62 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
68 defp validate_chat_content_length(_, true), do: :ok
69 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
71 defp validate_chat_content_length(content, _) do
72 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
75 {:error, :content_too_long}
79 def unblock(blocker, blocked) do
80 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
81 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
82 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
85 {:fetch_block, nil} ->
86 if User.blocks?(blocker, blocked) do
87 User.unblock(blocker, blocked)
90 {:error, :not_blocking}
98 def follow(follower, followed) do
99 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
101 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
102 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
103 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
104 if activity.data["state"] == "reject" do
107 {:ok, follower, followed, activity}
112 def unfollow(follower, unfollowed) do
113 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
114 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
115 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
120 def accept_follow_request(follower, followed) do
121 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
122 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
123 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
128 def reject_follow_request(follower, followed) do
129 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
130 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
131 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
136 def delete(activity_id, user) do
137 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
138 {:find_activity, Activity.get_by_id(activity_id)},
139 {_, %Object{} = object, _} <-
140 {:find_object, Object.normalize(activity, false), activity},
141 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
142 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
143 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
146 {:find_activity, _} ->
149 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
150 # We have the create activity, but not the object, it was probably pruned.
151 # Insert a tombstone and try again
152 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
153 {:ok, _tombstone} <- Object.create(tombstone_data) do
154 delete(activity_id, user)
158 "Could not insert tombstone for missing object on deletion. Object is #{object}."
161 {:error, dgettext("errors", "Could not delete")}
165 {:error, dgettext("errors", "Could not delete")}
169 def repeat(id, user, params \\ %{}) do
170 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
171 object = %Object{} <- Object.normalize(activity, false),
172 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
173 public = public_announce?(object, params),
174 {:ok, announce, _} <- Builder.announce(user, object, public: public),
175 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
178 {:existing_announce, %Activity{} = announce} ->
186 def unrepeat(id, user) do
187 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
188 {:find_activity, Activity.get_by_id(id)},
189 %Object{} = note <- Object.normalize(activity, false),
190 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
191 {:ok, undo, _} <- Builder.undo(user, announce),
192 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
195 {:find_activity, _} -> {:error, :not_found}
196 _ -> {:error, dgettext("errors", "Could not unrepeat")}
200 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
201 def favorite(%User{} = user, id) do
202 case favorite_helper(user, id) do
206 {:error, :not_found} = res ->
210 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
211 {:error, dgettext("errors", "Could not favorite")}
215 def favorite_helper(user, id) do
216 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
217 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
218 {_, {:ok, %Activity{} = activity, _meta}} <-
220 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
237 if {:object, {"already liked by this actor", []}} in changeset.errors do
238 {:ok, :already_liked}
248 def unfavorite(id, user) do
249 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
250 {:find_activity, Activity.get_by_id(id)},
251 %Object{} = note <- Object.normalize(activity, false),
252 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
253 {:ok, undo, _} <- Builder.undo(user, like),
254 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
257 {:find_activity, _} -> {:error, :not_found}
258 _ -> {:error, dgettext("errors", "Could not unfavorite")}
262 def react_with_emoji(id, user, emoji) do
263 with %Activity{} = activity <- Activity.get_by_id(id),
264 object <- Object.normalize(activity),
265 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
266 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
270 {:error, dgettext("errors", "Could not add reaction emoji")}
274 def unreact_with_emoji(id, user, emoji) do
275 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
276 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
277 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
281 {:error, dgettext("errors", "Could not remove reaction emoji")}
285 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
286 with :ok <- validate_not_author(object, user),
287 :ok <- validate_existing_votes(user, object),
288 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
290 Enum.map(choices, fn index ->
291 {:ok, answer_object, _meta} =
292 Builder.answer(user, object, Enum.at(options, index)["name"])
294 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
296 {:ok, activity, _meta} =
298 |> Map.put("cc", answer_object["cc"])
299 |> Map.put("context", answer_object["context"])
300 |> Pipeline.common_pipeline(local: true)
302 # TODO: Do preload of Pleroma.Object in Pipeline
303 Activity.normalize(activity.data)
306 object = Object.get_cached_by_ap_id(object.data["id"])
307 {:ok, answer_activities, object}
311 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
312 do: {:error, dgettext("errors", "Poll's author can't vote")}
314 defp validate_not_author(_, _), do: :ok
316 defp validate_existing_votes(%{ap_id: ap_id}, object) do
317 if Utils.get_existing_votes(ap_id, object) == [] do
320 {:error, dgettext("errors", "Already voted")}
324 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
325 when is_list(any_of) and any_of != [],
326 do: {any_of, Enum.count(any_of)}
328 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
329 when is_list(one_of) and one_of != [],
332 defp normalize_and_validate_choices(choices, object) do
333 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
334 {options, max_count} = get_options_and_max_count(object)
335 count = Enum.count(options)
337 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
338 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
339 {:ok, options, choices}
341 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
342 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
346 def public_announce?(_, %{visibility: visibility})
347 when visibility in ~w{public unlisted private direct},
348 do: visibility in ~w(public unlisted)
350 def public_announce?(object, _) do
351 Visibility.is_public?(object)
354 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
356 def get_visibility(%{visibility: visibility}, in_reply_to, _)
357 when visibility in ~w{public unlisted private direct},
358 do: {visibility, get_replied_to_visibility(in_reply_to)}
360 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
361 visibility = {:list, String.to_integer(list_id)}
362 {visibility, get_replied_to_visibility(in_reply_to)}
365 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
366 visibility = get_replied_to_visibility(in_reply_to)
367 {visibility, visibility}
370 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
372 def get_replied_to_visibility(nil), do: nil
374 def get_replied_to_visibility(activity) do
375 with %Object{} = object <- Object.normalize(activity) do
376 Visibility.get_visibility(object)
380 def check_expiry_date({:ok, nil} = res), do: res
382 def check_expiry_date({:ok, in_seconds}) do
383 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
385 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
388 {:error, "Expiry date is too soon"}
392 def check_expiry_date(expiry_str) do
393 Ecto.Type.cast(:integer, expiry_str)
394 |> check_expiry_date()
397 def listen(user, data) do
398 visibility = Map.get(data, :visibility, "public")
400 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
403 |> Map.take([:album, :artist, :title, :length])
404 |> Map.new(fn {key, value} -> {to_string(key), value} end)
405 |> Map.put("type", "Audio")
408 |> Map.put("actor", user.ap_id),
410 ActivityPub.listen(%{
414 context: Utils.generate_context_id(),
415 additional: %{"cc" => cc}
421 def post(user, %{status: _} = data) do
422 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
423 ActivityPub.create(draft.changes, draft.preview?)
427 def pin(id, %{ap_id: user_ap_id} = user) do
430 data: %{"type" => "Create"},
431 object: %Object{data: %{"type" => object_type}}
432 } = activity <- Activity.get_by_id_with_object(id),
433 true <- object_type in ["Note", "Article", "Question"],
434 true <- Visibility.is_public?(activity),
435 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
438 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
439 _ -> {:error, dgettext("errors", "Could not pin")}
443 def unpin(id, user) do
444 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
445 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
448 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
449 _ -> {:error, dgettext("errors", "Could not unpin")}
453 def add_mute(user, activity, params \\ %{}) do
454 expires_in = Map.get(params, :expires_in, 0)
456 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
457 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
459 Pleroma.Workers.MuteExpireWorker.enqueue(
460 "unmute_conversation",
461 %{"user_id" => user.id, "activity_id" => activity.id},
462 schedule_in: expires_in
468 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
472 def remove_mute(user, activity) do
473 ThreadMute.remove_mute(user.id, activity.data["context"])
477 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
478 when is_binary(context) do
479 ThreadMute.exists?(user_id, context)
482 def thread_muted?(_, _), do: false
484 def report(user, data) do
485 with {:ok, account} <- get_reported_account(data.account_id),
486 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
487 {:ok, statuses} <- get_report_statuses(account, data) do
489 context: Utils.generate_context_id(),
493 content: content_html,
494 forward: Map.get(data, :forward, false)
499 defp get_reported_account(account_id) do
500 case User.get_cached_by_id(account_id) do
501 %User{} = account -> {:ok, account}
502 _ -> {:error, dgettext("errors", "Account not found")}
506 def update_report_state(activity_ids, state) when is_list(activity_ids) do
507 case Utils.update_report_state(activity_ids, state) do
508 :ok -> {:ok, activity_ids}
509 _ -> {:error, dgettext("errors", "Could not update state")}
513 def update_report_state(activity_id, state) do
514 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
515 Utils.update_report_state(activity, state)
517 nil -> {:error, :not_found}
518 _ -> {:error, dgettext("errors", "Could not update state")}
522 def update_activity_scope(activity_id, opts \\ %{}) do
523 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
524 {:ok, activity} <- toggle_sensitive(activity, opts) do
525 set_visibility(activity, opts)
527 nil -> {:error, :not_found}
528 {:error, reason} -> {:error, reason}
532 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
533 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
536 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
537 when is_boolean(sensitive) do
538 new_data = Map.put(object.data, "sensitive", sensitive)
542 |> Object.change(%{data: new_data})
543 |> Object.update_and_set_cache()
545 {:ok, Map.put(activity, :object, object)}
548 defp toggle_sensitive(activity, _), do: {:ok, activity}
550 defp set_visibility(activity, %{visibility: visibility}) do
551 Utils.update_activity_visibility(activity, visibility)
554 defp set_visibility(activity, _), do: {:ok, activity}
556 def hide_reblogs(%User{} = user, %User{} = target) do
557 UserRelationship.create_reblog_mute(user, target)
560 def show_reblogs(%User{} = user, %User{} = target) do
561 UserRelationship.delete_reblog_mute(user, target)