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,
52 {:common_pipeline, {:reject, _} = e} -> e
57 defp format_chat_content(nil), do: nil
59 defp format_chat_content(content) do
62 |> Formatter.html_escape("text/plain")
63 |> Formatter.linkify()
64 |> (fn {text, mentions, tags} ->
65 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
71 defp validate_chat_content_length(_, true), do: :ok
72 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
74 defp validate_chat_content_length(content, _) do
75 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
78 {:error, :content_too_long}
82 def unblock(blocker, blocked) do
83 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
84 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
85 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
88 {:fetch_block, nil} ->
89 if User.blocks?(blocker, blocked) do
90 User.unblock(blocker, blocked)
93 {:error, :not_blocking}
101 def follow(follower, followed) do
102 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
104 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
105 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
106 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
107 if activity.data["state"] == "reject" do
110 {:ok, follower, followed, activity}
115 def unfollow(follower, unfollowed) do
116 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
117 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
118 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
123 def accept_follow_request(follower, followed) do
124 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
125 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
126 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
131 def reject_follow_request(follower, followed) do
132 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
133 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
134 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
139 def delete(activity_id, user) do
140 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
141 {:find_activity, Activity.get_by_id(activity_id)},
142 {_, %Object{} = object, _} <-
143 {:find_object, Object.normalize(activity, false), activity},
144 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
145 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
146 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
149 {:find_activity, _} ->
152 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
153 # We have the create activity, but not the object, it was probably pruned.
154 # Insert a tombstone and try again
155 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
156 {:ok, _tombstone} <- Object.create(tombstone_data) do
157 delete(activity_id, user)
161 "Could not insert tombstone for missing object on deletion. Object is #{object}."
164 {:error, dgettext("errors", "Could not delete")}
168 {:error, dgettext("errors", "Could not delete")}
172 def repeat(id, user, params \\ %{}) do
173 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
174 object = %Object{} <- Object.normalize(activity, false),
175 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
176 public = public_announce?(object, params),
177 {:ok, announce, _} <- Builder.announce(user, object, public: public),
178 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
181 {:existing_announce, %Activity{} = announce} ->
189 def unrepeat(id, user) do
190 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
191 {:find_activity, Activity.get_by_id(id)},
192 %Object{} = note <- Object.normalize(activity, false),
193 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
194 {:ok, undo, _} <- Builder.undo(user, announce),
195 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
198 {:find_activity, _} -> {:error, :not_found}
199 _ -> {:error, dgettext("errors", "Could not unrepeat")}
203 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
204 def favorite(%User{} = user, id) do
205 case favorite_helper(user, id) do
209 {:error, :not_found} = res ->
213 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
214 {:error, dgettext("errors", "Could not favorite")}
218 def favorite_helper(user, id) do
219 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
220 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
221 {_, {:ok, %Activity{} = activity, _meta}} <-
223 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
240 if {:object, {"already liked by this actor", []}} in changeset.errors do
241 {:ok, :already_liked}
251 def unfavorite(id, user) do
252 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
253 {:find_activity, Activity.get_by_id(id)},
254 %Object{} = note <- Object.normalize(activity, false),
255 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
256 {:ok, undo, _} <- Builder.undo(user, like),
257 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
260 {:find_activity, _} -> {:error, :not_found}
261 _ -> {:error, dgettext("errors", "Could not unfavorite")}
265 def react_with_emoji(id, user, emoji) do
266 with %Activity{} = activity <- Activity.get_by_id(id),
267 object <- Object.normalize(activity),
268 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
269 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
273 {:error, dgettext("errors", "Could not add reaction emoji")}
277 def unreact_with_emoji(id, user, emoji) do
278 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
279 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
280 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
284 {:error, dgettext("errors", "Could not remove reaction emoji")}
288 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
289 with :ok <- validate_not_author(object, user),
290 :ok <- validate_existing_votes(user, object),
291 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
293 Enum.map(choices, fn index ->
294 {:ok, answer_object, _meta} =
295 Builder.answer(user, object, Enum.at(options, index)["name"])
297 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
299 {:ok, activity, _meta} =
301 |> Map.put("cc", answer_object["cc"])
302 |> Map.put("context", answer_object["context"])
303 |> Pipeline.common_pipeline(local: true)
305 # TODO: Do preload of Pleroma.Object in Pipeline
306 Activity.normalize(activity.data)
309 object = Object.get_cached_by_ap_id(object.data["id"])
310 {:ok, answer_activities, object}
314 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
315 do: {:error, dgettext("errors", "Poll's author can't vote")}
317 defp validate_not_author(_, _), do: :ok
319 defp validate_existing_votes(%{ap_id: ap_id}, object) do
320 if Utils.get_existing_votes(ap_id, object) == [] do
323 {:error, dgettext("errors", "Already voted")}
327 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
328 when is_list(any_of) and any_of != [],
329 do: {any_of, Enum.count(any_of)}
331 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
332 when is_list(one_of) and one_of != [],
335 defp normalize_and_validate_choices(choices, object) do
336 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
337 {options, max_count} = get_options_and_max_count(object)
338 count = Enum.count(options)
340 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
341 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
342 {:ok, options, choices}
344 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
345 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
349 def public_announce?(_, %{visibility: visibility})
350 when visibility in ~w{public unlisted private direct},
351 do: visibility in ~w(public unlisted)
353 def public_announce?(object, _) do
354 Visibility.is_public?(object)
357 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
359 def get_visibility(%{visibility: visibility}, in_reply_to, _)
360 when visibility in ~w{public unlisted private direct},
361 do: {visibility, get_replied_to_visibility(in_reply_to)}
363 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
364 visibility = {:list, String.to_integer(list_id)}
365 {visibility, get_replied_to_visibility(in_reply_to)}
368 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
369 visibility = get_replied_to_visibility(in_reply_to)
370 {visibility, visibility}
373 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
375 def get_replied_to_visibility(nil), do: nil
377 def get_replied_to_visibility(activity) do
378 with %Object{} = object <- Object.normalize(activity) do
379 Visibility.get_visibility(object)
383 def check_expiry_date({:ok, nil} = res), do: res
385 def check_expiry_date({:ok, in_seconds}) do
386 expiry = DateTime.add(DateTime.utc_now(), in_seconds)
388 if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
391 {:error, "Expiry date is too soon"}
395 def check_expiry_date(expiry_str) do
396 Ecto.Type.cast(:integer, expiry_str)
397 |> check_expiry_date()
400 def listen(user, data) do
401 visibility = Map.get(data, :visibility, "public")
403 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
406 |> Map.take([:album, :artist, :title, :length])
407 |> Map.new(fn {key, value} -> {to_string(key), value} end)
408 |> Map.put("type", "Audio")
411 |> Map.put("actor", user.ap_id),
413 ActivityPub.listen(%{
417 context: Utils.generate_context_id(),
418 additional: %{"cc" => cc}
424 def post(user, %{status: _} = data) do
425 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
426 ActivityPub.create(draft.changes, draft.preview?)
430 def pin(id, %{ap_id: user_ap_id} = user) do
433 data: %{"type" => "Create"},
434 object: %Object{data: %{"type" => object_type}}
435 } = activity <- Activity.get_by_id_with_object(id),
436 true <- object_type in ["Note", "Article", "Question"],
437 true <- Visibility.is_public?(activity),
438 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
441 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
442 _ -> {:error, dgettext("errors", "Could not pin")}
446 def unpin(id, user) do
447 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
448 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
451 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
452 _ -> {:error, dgettext("errors", "Could not unpin")}
456 def add_mute(user, activity) do
457 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
458 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
461 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
465 def remove_mute(user, activity) do
466 ThreadMute.remove_mute(user.id, activity.data["context"])
470 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
471 when is_binary(context) do
472 ThreadMute.exists?(user_id, context)
475 def thread_muted?(_, _), do: false
477 def report(user, data) do
478 with {:ok, account} <- get_reported_account(data.account_id),
479 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
480 {:ok, statuses} <- get_report_statuses(account, data) do
482 context: Utils.generate_context_id(),
486 content: content_html,
487 forward: Map.get(data, :forward, false)
492 defp get_reported_account(account_id) do
493 case User.get_cached_by_id(account_id) do
494 %User{} = account -> {:ok, account}
495 _ -> {:error, dgettext("errors", "Account not found")}
499 def update_report_state(activity_ids, state) when is_list(activity_ids) do
500 case Utils.update_report_state(activity_ids, state) do
501 :ok -> {:ok, activity_ids}
502 _ -> {:error, dgettext("errors", "Could not update state")}
506 def update_report_state(activity_id, state) do
507 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
508 Utils.update_report_state(activity, state)
510 nil -> {:error, :not_found}
511 _ -> {:error, dgettext("errors", "Could not update state")}
515 def update_activity_scope(activity_id, opts \\ %{}) do
516 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
517 {:ok, activity} <- toggle_sensitive(activity, opts) do
518 set_visibility(activity, opts)
520 nil -> {:error, :not_found}
521 {:error, reason} -> {:error, reason}
525 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
526 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
529 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
530 when is_boolean(sensitive) do
531 new_data = Map.put(object.data, "sensitive", sensitive)
535 |> Object.change(%{data: new_data})
536 |> Object.update_and_set_cache()
538 {:ok, Map.put(activity, :object, object)}
541 defp toggle_sensitive(activity, _), do: {:ok, activity}
543 defp set_visibility(activity, %{visibility: visibility}) do
544 Utils.update_activity_visibility(activity, visibility)
547 defp set_visibility(activity, _), do: {:ok, activity}
549 def hide_reblogs(%User{} = user, %User{} = target) do
550 UserRelationship.create_reblog_mute(user, target)
553 def show_reblogs(%User{} = user, %User{} = target) do
554 UserRelationship.delete_reblog_mute(user, target)
557 def get_user(ap_id, fake_record_fallback \\ true) do
559 user = User.get_cached_by_ap_id(ap_id) ->
562 user = User.get_by_guessed_nickname(ap_id) ->
565 fake_record_fallback ->
566 # TODO: refactor (fake records is never a good idea)
567 User.error_user(ap_id)