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,
49 idempotency_key: opts[:idempotency_key]
53 {:common_pipeline, {:reject, _} = e} -> e
58 defp format_chat_content(nil), do: nil
60 defp format_chat_content(content) do
63 |> Formatter.html_escape("text/plain")
64 |> Formatter.linkify()
65 |> (fn {text, mentions, tags} ->
66 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
72 defp validate_chat_content_length(_, true), do: :ok
73 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
75 defp validate_chat_content_length(content, _) do
76 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
79 {:error, :content_too_long}
83 def unblock(blocker, blocked) do
84 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
85 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
86 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
89 {:fetch_block, nil} ->
90 if User.blocks?(blocker, blocked) do
91 User.unblock(blocker, blocked)
94 {:error, :not_blocking}
102 def follow(follower, followed) do
103 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
105 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
106 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
107 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
108 if activity.data["state"] == "reject" do
111 {:ok, follower, followed, activity}
116 def unfollow(follower, unfollowed) do
117 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
118 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
119 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
124 def accept_follow_request(follower, followed) do
125 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
126 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
127 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
132 def reject_follow_request(follower, followed) do
133 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
134 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
135 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
140 def delete(activity_id, user) do
141 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
142 {:find_activity, Activity.get_by_id(activity_id)},
143 {_, %Object{} = object, _} <-
144 {:find_object, Object.normalize(activity, false), activity},
145 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
146 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
147 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
150 {:find_activity, _} ->
153 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
154 # We have the create activity, but not the object, it was probably pruned.
155 # Insert a tombstone and try again
156 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
157 {:ok, _tombstone} <- Object.create(tombstone_data) do
158 delete(activity_id, user)
162 "Could not insert tombstone for missing object on deletion. Object is #{object}."
165 {:error, dgettext("errors", "Could not delete")}
169 {:error, dgettext("errors", "Could not delete")}
173 def repeat(id, user, params \\ %{}) do
174 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
175 object = %Object{} <- Object.normalize(activity, false),
176 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
177 public = public_announce?(object, params),
178 {:ok, announce, _} <- Builder.announce(user, object, public: public),
179 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
182 {:existing_announce, %Activity{} = announce} ->
190 def unrepeat(id, user) do
191 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
192 {:find_activity, Activity.get_by_id(id)},
193 %Object{} = note <- Object.normalize(activity, false),
194 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
195 {:ok, undo, _} <- Builder.undo(user, announce),
196 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
199 {:find_activity, _} -> {:error, :not_found}
200 _ -> {:error, dgettext("errors", "Could not unrepeat")}
204 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
205 def favorite(%User{} = user, id) do
206 case favorite_helper(user, id) do
210 {:error, :not_found} = res ->
214 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
215 {:error, dgettext("errors", "Could not favorite")}
219 def favorite_helper(user, id) do
220 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
221 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
222 {_, {:ok, %Activity{} = activity, _meta}} <-
224 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
241 if {:object, {"already liked by this actor", []}} in changeset.errors do
242 {:ok, :already_liked}
252 def unfavorite(id, user) do
253 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
254 {:find_activity, Activity.get_by_id(id)},
255 %Object{} = note <- Object.normalize(activity, false),
256 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
257 {:ok, undo, _} <- Builder.undo(user, like),
258 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
261 {:find_activity, _} -> {:error, :not_found}
262 _ -> {:error, dgettext("errors", "Could not unfavorite")}
266 def react_with_emoji(id, user, emoji) do
267 with %Activity{} = activity <- Activity.get_by_id(id),
268 object <- Object.normalize(activity),
269 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
270 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
274 {:error, dgettext("errors", "Could not add reaction emoji")}
278 def unreact_with_emoji(id, user, emoji) do
279 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
280 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
281 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
285 {:error, dgettext("errors", "Could not remove reaction emoji")}
289 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
290 with :ok <- validate_not_author(object, user),
291 :ok <- validate_existing_votes(user, object),
292 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
294 Enum.map(choices, fn index ->
295 {:ok, answer_object, _meta} =
296 Builder.answer(user, object, Enum.at(options, index)["name"])
298 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
300 {:ok, activity, _meta} =
302 |> Map.put("cc", answer_object["cc"])
303 |> Map.put("context", answer_object["context"])
304 |> Pipeline.common_pipeline(local: true)
306 # TODO: Do preload of Pleroma.Object in Pipeline
307 Activity.normalize(activity.data)
310 object = Object.get_cached_by_ap_id(object.data["id"])
311 {:ok, answer_activities, object}
315 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
316 do: {:error, dgettext("errors", "Poll's author can't vote")}
318 defp validate_not_author(_, _), do: :ok
320 defp validate_existing_votes(%{ap_id: ap_id}, object) do
321 if Utils.get_existing_votes(ap_id, object) == [] do
324 {:error, dgettext("errors", "Already voted")}
328 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
329 when is_list(any_of) and any_of != [],
330 do: {any_of, Enum.count(any_of)}
332 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
333 when is_list(one_of) and one_of != [],
336 defp normalize_and_validate_choices(choices, object) do
337 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
338 {options, max_count} = get_options_and_max_count(object)
339 count = Enum.count(options)
341 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
342 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
343 {:ok, options, choices}
345 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
346 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
350 def public_announce?(_, %{visibility: visibility})
351 when visibility in ~w{public unlisted private direct},
352 do: visibility in ~w(public unlisted)
354 def public_announce?(object, _) do
355 Visibility.is_public?(object)
358 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
360 def get_visibility(%{visibility: visibility}, in_reply_to, _)
361 when visibility in ~w{public unlisted private direct},
362 do: {visibility, get_replied_to_visibility(in_reply_to)}
364 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
365 visibility = {:list, String.to_integer(list_id)}
366 {visibility, get_replied_to_visibility(in_reply_to)}
369 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
370 visibility = get_replied_to_visibility(in_reply_to)
371 {visibility, visibility}
374 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
376 def get_replied_to_visibility(nil), do: nil
378 def get_replied_to_visibility(activity) do
379 with %Object{} = object <- Object.normalize(activity) do
380 Visibility.get_visibility(object)
384 def check_expiry_date({:ok, nil} = res), do: res
386 def check_expiry_date({:ok, in_seconds}) do
387 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
389 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
392 {:error, "Expiry date is too soon"}
396 def check_expiry_date(expiry_str) do
397 Ecto.Type.cast(:integer, expiry_str)
398 |> check_expiry_date()
401 def listen(user, data) do
402 visibility = Map.get(data, :visibility, "public")
404 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
407 |> Map.take([:album, :artist, :title, :length])
408 |> Map.new(fn {key, value} -> {to_string(key), value} end)
409 |> Map.put("type", "Audio")
412 |> Map.put("actor", user.ap_id),
414 ActivityPub.listen(%{
418 context: Utils.generate_context_id(),
419 additional: %{"cc" => cc}
425 def post(user, %{status: _} = data) do
426 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
427 ActivityPub.create(draft.changes, draft.preview?)
431 def pin(id, %{ap_id: user_ap_id} = user) do
434 data: %{"type" => "Create"},
435 object: %Object{data: %{"type" => object_type}}
436 } = activity <- Activity.get_by_id_with_object(id),
437 true <- object_type in ["Note", "Article", "Question"],
438 true <- Visibility.is_public?(activity),
439 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
442 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
443 _ -> {:error, dgettext("errors", "Could not pin")}
447 def unpin(id, user) do
448 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
449 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
452 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
453 _ -> {:error, dgettext("errors", "Could not unpin")}
457 def add_mute(user, activity, params \\ %{}) do
458 expires_in = Map.get(params, :expires_in, 0)
460 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
461 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
463 Pleroma.Workers.MuteExpireWorker.enqueue(
464 "unmute_conversation",
465 %{"user_id" => user.id, "activity_id" => activity.id},
466 schedule_in: expires_in
472 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
476 def remove_mute(%User{} = user, %Activity{} = activity) do
477 ThreadMute.remove_mute(user.id, activity.data["context"])
481 def remove_mute(user_id, activity_id) do
482 with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
483 {:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
484 remove_mute(user, activity)
486 {what, result} = error ->
488 "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{
497 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
498 when is_binary(context) do
499 ThreadMute.exists?(user_id, context)
502 def thread_muted?(_, _), do: false
504 def report(user, data) do
505 with {:ok, account} <- get_reported_account(data.account_id),
506 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
507 {:ok, statuses} <- get_report_statuses(account, data) do
509 context: Utils.generate_context_id(),
513 content: content_html,
514 forward: Map.get(data, :forward, false)
519 defp get_reported_account(account_id) do
520 case User.get_cached_by_id(account_id) do
521 %User{} = account -> {:ok, account}
522 _ -> {:error, dgettext("errors", "Account not found")}
526 def update_report_state(activity_ids, state) when is_list(activity_ids) do
527 case Utils.update_report_state(activity_ids, state) do
528 :ok -> {:ok, activity_ids}
529 _ -> {:error, dgettext("errors", "Could not update state")}
533 def update_report_state(activity_id, state) do
534 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
535 Utils.update_report_state(activity, state)
537 nil -> {:error, :not_found}
538 _ -> {:error, dgettext("errors", "Could not update state")}
542 def update_activity_scope(activity_id, opts \\ %{}) do
543 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
544 {:ok, activity} <- toggle_sensitive(activity, opts) do
545 set_visibility(activity, opts)
547 nil -> {:error, :not_found}
548 {:error, reason} -> {:error, reason}
552 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
553 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
556 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
557 when is_boolean(sensitive) do
558 new_data = Map.put(object.data, "sensitive", sensitive)
562 |> Object.change(%{data: new_data})
563 |> Object.update_and_set_cache()
565 {:ok, Map.put(activity, :object, object)}
568 defp toggle_sensitive(activity, _), do: {:ok, activity}
570 defp set_visibility(activity, %{visibility: visibility}) do
571 Utils.update_activity_visibility(activity, visibility)
574 defp set_visibility(activity, _), do: {:ok, activity}
576 def hide_reblogs(%User{} = user, %User{} = target) do
577 UserRelationship.create_reblog_mute(user, target)
580 def show_reblogs(%User{} = user, %User{} = target) do
581 UserRelationship.delete_reblog_mute(user, target)
584 def get_user(ap_id, fake_record_fallback \\ true) do
586 user = User.get_cached_by_ap_id(ap_id) ->
589 user = User.get_by_guessed_nickname(ap_id) ->
592 fake_record_fallback ->
593 # TODO: refactor (fake records is never a good idea)
594 User.error_user(ap_id)