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.Formatter
11 alias Pleroma.ThreadMute
13 alias Pleroma.UserRelationship
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Builder
16 alias Pleroma.Web.ActivityPub.Pipeline
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.ActivityPub.Visibility
20 import Pleroma.Web.Gettext
21 import Pleroma.Web.CommonAPI.Utils
23 require Pleroma.Constants
26 def block(blocker, blocked) do
27 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
28 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
33 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
34 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
35 :ok <- validate_chat_content_length(content, !!maybe_attachment),
36 {_, {:ok, chat_message_data, _meta}} <-
41 content |> format_chat_content,
42 attachment: maybe_attachment
44 {_, {:ok, create_activity_data, _meta}} <-
45 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
46 {_, {:ok, %Activity{} = activity, _meta}} <-
48 Pipeline.common_pipeline(create_activity_data,
55 defp format_chat_content(nil), do: nil
57 defp format_chat_content(content) do
60 |> Formatter.html_escape("text/plain")
61 |> Formatter.linkify()
62 |> (fn {text, mentions, tags} ->
63 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
69 defp validate_chat_content_length(_, true), do: :ok
70 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
72 defp validate_chat_content_length(content, _) do
73 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
76 {:error, :content_too_long}
80 def unblock(blocker, blocked) do
81 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
82 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
83 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
86 {:fetch_block, nil} ->
87 if User.blocks?(blocker, blocked) do
88 User.unblock(blocker, blocked)
91 {:error, :not_blocking}
99 def follow(follower, followed) do
100 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
102 with {:ok, follow_data, _} <- Builder.follow(follower, followed),
103 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
104 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
105 if activity.data["state"] == "reject" do
108 {:ok, follower, followed, activity}
113 def unfollow(follower, unfollowed) do
114 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
115 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
116 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
121 def accept_follow_request(follower, followed) do
122 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
123 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
124 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
129 def reject_follow_request(follower, followed) do
130 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
131 {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
132 {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
137 def delete(activity_id, user) do
138 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
139 {:find_activity, Activity.get_by_id(activity_id)},
140 {_, %Object{} = object, _} <-
141 {:find_object, Object.normalize(activity, false), activity},
142 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
143 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
144 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
147 {:find_activity, _} ->
150 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
151 # We have the create activity, but not the object, it was probably pruned.
152 # Insert a tombstone and try again
153 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
154 {:ok, _tombstone} <- Object.create(tombstone_data) do
155 delete(activity_id, user)
159 "Could not insert tombstone for missing object on deletion. Object is #{object}."
162 {:error, dgettext("errors", "Could not delete")}
166 {:error, dgettext("errors", "Could not delete")}
170 def repeat(id, user, params \\ %{}) do
171 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
172 object = %Object{} <- Object.normalize(activity, false),
173 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
174 public = public_announce?(object, params),
175 {:ok, announce, _} <- Builder.announce(user, object, public: public),
176 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
179 {:existing_announce, %Activity{} = announce} ->
187 def unrepeat(id, user) do
188 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
189 {:find_activity, Activity.get_by_id(id)},
190 %Object{} = note <- Object.normalize(activity, false),
191 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
192 {:ok, undo, _} <- Builder.undo(user, announce),
193 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
196 {:find_activity, _} -> {:error, :not_found}
197 _ -> {:error, dgettext("errors", "Could not unrepeat")}
201 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
202 def favorite(%User{} = user, id) do
203 case favorite_helper(user, id) do
207 {:error, :not_found} = res ->
211 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
212 {:error, dgettext("errors", "Could not favorite")}
216 def favorite_helper(user, id) do
217 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
218 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
219 {_, {:ok, %Activity{} = activity, _meta}} <-
221 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
238 if {:object, {"already liked by this actor", []}} in changeset.errors do
239 {:ok, :already_liked}
249 def unfavorite(id, user) do
250 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
251 {:find_activity, Activity.get_by_id(id)},
252 %Object{} = note <- Object.normalize(activity, false),
253 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
254 {:ok, undo, _} <- Builder.undo(user, like),
255 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
258 {:find_activity, _} -> {:error, :not_found}
259 _ -> {:error, dgettext("errors", "Could not unfavorite")}
263 def react_with_emoji(id, user, emoji) do
264 with %Activity{} = activity <- Activity.get_by_id(id),
265 object <- Object.normalize(activity),
266 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
267 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
271 {:error, dgettext("errors", "Could not add reaction emoji")}
275 def unreact_with_emoji(id, user, emoji) do
276 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
277 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
278 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
282 {:error, dgettext("errors", "Could not remove reaction emoji")}
286 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
287 with :ok <- validate_not_author(object, user),
288 :ok <- validate_existing_votes(user, object),
289 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
291 Enum.map(choices, fn index ->
292 {:ok, answer_object, _meta} =
293 Builder.answer(user, object, Enum.at(options, index)["name"])
295 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
297 {:ok, activity, _meta} =
299 |> Map.put("cc", answer_object["cc"])
300 |> Map.put("context", answer_object["context"])
301 |> Pipeline.common_pipeline(local: true)
303 # TODO: Do preload of Pleroma.Object in Pipeline
304 Activity.normalize(activity.data)
307 object = Object.get_cached_by_ap_id(object.data["id"])
308 {:ok, answer_activities, object}
312 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
313 do: {:error, dgettext("errors", "Poll's author can't vote")}
315 defp validate_not_author(_, _), do: :ok
317 defp validate_existing_votes(%{ap_id: ap_id}, object) do
318 if Utils.get_existing_votes(ap_id, object) == [] do
321 {:error, dgettext("errors", "Already voted")}
325 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
326 when is_list(any_of) and any_of != [],
327 do: {any_of, Enum.count(any_of)}
329 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
330 when is_list(one_of) and one_of != [],
333 defp normalize_and_validate_choices(choices, object) do
334 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
335 {options, max_count} = get_options_and_max_count(object)
336 count = Enum.count(options)
338 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
339 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
340 {:ok, options, choices}
342 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
343 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
347 def public_announce?(_, %{visibility: visibility})
348 when visibility in ~w{public unlisted private direct},
349 do: visibility in ~w(public unlisted)
351 def public_announce?(object, _) do
352 Visibility.is_public?(object)
355 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
357 def get_visibility(%{visibility: visibility}, in_reply_to, _)
358 when visibility in ~w{public unlisted private direct},
359 do: {visibility, get_replied_to_visibility(in_reply_to)}
361 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
362 visibility = {:list, String.to_integer(list_id)}
363 {visibility, get_replied_to_visibility(in_reply_to)}
366 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
367 visibility = get_replied_to_visibility(in_reply_to)
368 {visibility, visibility}
371 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
373 def get_replied_to_visibility(nil), do: nil
375 def get_replied_to_visibility(activity) do
376 with %Object{} = object <- Object.normalize(activity) do
377 Visibility.get_visibility(object)
381 def check_expiry_date({:ok, nil} = res), do: res
383 def check_expiry_date({:ok, in_seconds}) do
384 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
386 if ActivityExpiration.expires_late_enough?(expiry) do
389 {:error, "Expiry date is too soon"}
393 def check_expiry_date(expiry_str) do
394 Ecto.Type.cast(:integer, expiry_str)
395 |> check_expiry_date()
398 def listen(user, data) do
399 visibility = Map.get(data, :visibility, "public")
401 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
404 |> Map.take([:album, :artist, :title, :length])
405 |> Map.new(fn {key, value} -> {to_string(key), value} end)
406 |> Map.put("type", "Audio")
409 |> Map.put("actor", user.ap_id),
411 ActivityPub.listen(%{
415 context: Utils.generate_context_id(),
416 additional: %{"cc" => cc}
422 def post(user, %{status: _} = data) do
423 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
424 ActivityPub.create(draft.changes, draft.preview?)
428 def pin(id, %{ap_id: user_ap_id} = user) do
431 data: %{"type" => "Create"},
432 object: %Object{data: %{"type" => object_type}}
433 } = activity <- Activity.get_by_id_with_object(id),
434 true <- object_type in ["Note", "Article", "Question"],
435 true <- Visibility.is_public?(activity),
436 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
439 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
440 _ -> {:error, dgettext("errors", "Could not pin")}
444 def unpin(id, user) do
445 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
446 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
449 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
450 _ -> {:error, dgettext("errors", "Could not unpin")}
454 def add_mute(user, activity) do
455 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
456 _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
459 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
463 def remove_mute(user, activity) do
464 ThreadMute.remove_mute(user.id, activity.data["context"])
468 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
469 when is_binary(context) do
470 ThreadMute.exists?(user_id, context)
473 def thread_muted?(_, _), do: false
475 def report(user, data) do
476 with {:ok, account} <- get_reported_account(data.account_id),
477 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
478 {:ok, statuses} <- get_report_statuses(account, data) do
480 context: Utils.generate_context_id(),
484 content: content_html,
485 forward: Map.get(data, :forward, false)
490 defp get_reported_account(account_id) do
491 case User.get_cached_by_id(account_id) do
492 %User{} = account -> {:ok, account}
493 _ -> {:error, dgettext("errors", "Account not found")}
497 def update_report_state(activity_ids, state) when is_list(activity_ids) do
498 case Utils.update_report_state(activity_ids, state) do
499 :ok -> {:ok, activity_ids}
500 _ -> {:error, dgettext("errors", "Could not update state")}
504 def update_report_state(activity_id, state) do
505 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
506 Utils.update_report_state(activity, state)
508 nil -> {:error, :not_found}
509 _ -> {:error, dgettext("errors", "Could not update state")}
513 def update_activity_scope(activity_id, opts \\ %{}) do
514 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
515 {:ok, activity} <- toggle_sensitive(activity, opts) do
516 set_visibility(activity, opts)
518 nil -> {:error, :not_found}
519 {:error, reason} -> {:error, reason}
523 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
524 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
527 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
528 when is_boolean(sensitive) do
529 new_data = Map.put(object.data, "sensitive", sensitive)
533 |> Object.change(%{data: new_data})
534 |> Object.update_and_set_cache()
536 {:ok, Map.put(activity, :object, object)}
539 defp toggle_sensitive(activity, _), do: {:ok, activity}
541 defp set_visibility(activity, %{visibility: visibility}) do
542 Utils.update_activity_visibility(activity, visibility)
545 defp set_visibility(activity, _), do: {:ok, activity}
547 def hide_reblogs(%User{} = user, %User{} = target) do
548 UserRelationship.create_reblog_mute(user, target)
551 def show_reblogs(%User{} = user, %User{} = target) do
552 UserRelationship.delete_reblog_mute(user, target)