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 maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
30 :ok <- validate_chat_content_length(content, !!maybe_attachment),
31 {_, {:ok, chat_message_data, _meta}} <-
36 content |> format_chat_content,
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 format_chat_content(nil), do: nil
52 defp format_chat_content(content) do
55 |> Formatter.html_escape("text/plain")
56 |> Formatter.linkify()
61 defp validate_chat_content_length(_, true), do: :ok
62 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
64 defp validate_chat_content_length(content, _) do
65 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
68 {:error, :content_too_long}
72 def unblock(blocker, blocked) do
73 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
74 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
75 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
78 {:fetch_block, nil} ->
79 if User.blocks?(blocker, blocked) do
80 User.unblock(blocker, blocked)
83 {:error, :not_blocking}
91 def follow(follower, followed) do
92 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
94 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
95 {:ok, activity} <- ActivityPub.follow(follower, followed),
96 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
97 {:ok, follower, followed, activity}
101 def unfollow(follower, unfollowed) do
102 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
103 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
104 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
109 def accept_follow_request(follower, followed) do
110 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
111 {:ok, follower} <- User.follow(follower, followed),
112 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
113 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
115 ActivityPub.accept(%{
116 to: [follower.ap_id],
118 object: follow_activity.data["id"],
125 def reject_follow_request(follower, followed) do
126 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
127 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
128 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
129 {:ok, _notifications} <- Notification.dismiss(follow_activity),
131 ActivityPub.reject(%{
132 to: [follower.ap_id],
134 object: follow_activity.data["id"],
141 def delete(activity_id, user) do
142 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
143 {:find_activity, Activity.get_by_id(activity_id)},
144 {_, %Object{} = object, _} <-
145 {:find_object, Object.normalize(activity, false), activity},
146 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
147 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
148 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
151 {:find_activity, _} ->
154 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
155 # We have the create activity, but not the object, it was probably pruned.
156 # Insert a tombstone and try again
157 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
158 {:ok, _tombstone} <- Object.create(tombstone_data) do
159 delete(activity_id, user)
163 "Could not insert tombstone for missing object on deletion. Object is #{object}."
166 {:error, dgettext("errors", "Could not delete")}
170 {:error, dgettext("errors", "Could not delete")}
174 def repeat(id, user, params \\ %{}) do
175 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
176 object = %Object{} <- Object.normalize(activity, false),
177 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
178 public = public_announce?(object, params),
179 {:ok, announce, _} <- Builder.announce(user, object, public: public),
180 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
183 {:existing_announce, %Activity{} = announce} ->
191 def unrepeat(id, user) do
192 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
193 {:find_activity, Activity.get_by_id(id)},
194 %Object{} = note <- Object.normalize(activity, false),
195 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
196 {:ok, undo, _} <- Builder.undo(user, announce),
197 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
200 {:find_activity, _} -> {:error, :not_found}
201 _ -> {:error, dgettext("errors", "Could not unrepeat")}
205 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
206 def favorite(%User{} = user, id) do
207 case favorite_helper(user, id) do
211 {:error, :not_found} = res ->
215 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
216 {:error, dgettext("errors", "Could not favorite")}
220 def favorite_helper(user, id) do
221 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
222 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
223 {_, {:ok, %Activity{} = activity, _meta}} <-
225 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
242 if {:object, {"already liked by this actor", []}} in changeset.errors do
243 {:ok, :already_liked}
253 def unfavorite(id, user) do
254 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
255 {:find_activity, Activity.get_by_id(id)},
256 %Object{} = note <- Object.normalize(activity, false),
257 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
258 {:ok, undo, _} <- Builder.undo(user, like),
259 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
262 {:find_activity, _} -> {:error, :not_found}
263 _ -> {:error, dgettext("errors", "Could not unfavorite")}
267 def react_with_emoji(id, user, emoji) do
268 with %Activity{} = activity <- Activity.get_by_id(id),
269 object <- Object.normalize(activity),
270 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
271 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
275 {:error, dgettext("errors", "Could not add reaction emoji")}
279 def unreact_with_emoji(id, user, emoji) do
280 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
281 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
282 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
286 {:error, dgettext("errors", "Could not remove reaction emoji")}
290 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
291 with :ok <- validate_not_author(object, user),
292 :ok <- validate_existing_votes(user, object),
293 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
295 Enum.map(choices, fn index ->
296 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
299 ActivityPub.create(%{
300 to: answer_data["to"],
302 context: object.data["context"],
304 additional: %{"cc" => answer_data["cc"]}
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}}), do: {any_of, Enum.count(any_of)}
329 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
331 defp normalize_and_validate_choices(choices, object) do
332 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
333 {options, max_count} = get_options_and_max_count(object)
334 count = Enum.count(options)
336 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
337 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
338 {:ok, options, choices}
340 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
341 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
345 def public_announce?(_, %{visibility: visibility})
346 when visibility in ~w{public unlisted private direct},
347 do: visibility in ~w(public unlisted)
349 def public_announce?(object, _) do
350 Visibility.is_public?(object)
353 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
355 def get_visibility(%{visibility: visibility}, in_reply_to, _)
356 when visibility in ~w{public unlisted private direct},
357 do: {visibility, get_replied_to_visibility(in_reply_to)}
359 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
360 visibility = {:list, String.to_integer(list_id)}
361 {visibility, get_replied_to_visibility(in_reply_to)}
364 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
365 visibility = get_replied_to_visibility(in_reply_to)
366 {visibility, visibility}
369 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
371 def get_replied_to_visibility(nil), do: nil
373 def get_replied_to_visibility(activity) do
374 with %Object{} = object <- Object.normalize(activity) do
375 Visibility.get_visibility(object)
379 def check_expiry_date({:ok, nil} = res), do: res
381 def check_expiry_date({:ok, in_seconds}) do
382 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
384 if ActivityExpiration.expires_late_enough?(expiry) do
387 {:error, "Expiry date is too soon"}
391 def check_expiry_date(expiry_str) do
392 Ecto.Type.cast(:integer, expiry_str)
393 |> check_expiry_date()
396 def listen(user, data) do
397 visibility = Map.get(data, :visibility, "public")
399 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
402 |> Map.take([:album, :artist, :title, :length])
403 |> Map.new(fn {key, value} -> {to_string(key), value} end)
404 |> Map.put("type", "Audio")
407 |> Map.put("actor", user.ap_id),
409 ActivityPub.listen(%{
413 context: Utils.generate_context_id(),
414 additional: %{"cc" => cc}
420 def post(user, %{status: _} = data) do
421 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
423 |> ActivityPub.create(draft.preview?)
424 |> maybe_create_activity_expiration(draft.expires_at)
428 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
429 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
434 defp maybe_create_activity_expiration(result, _), do: result
436 def pin(id, %{ap_id: user_ap_id} = user) do
439 data: %{"type" => "Create"},
440 object: %Object{data: %{"type" => object_type}}
441 } = activity <- Activity.get_by_id_with_object(id),
442 true <- object_type in ["Note", "Article", "Question"],
443 true <- Visibility.is_public?(activity),
444 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
447 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
448 _ -> {:error, dgettext("errors", "Could not pin")}
452 def unpin(id, user) do
453 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
454 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
457 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
458 _ -> {:error, dgettext("errors", "Could not unpin")}
462 def add_mute(user, activity) do
463 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
466 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
470 def remove_mute(user, activity) do
471 ThreadMute.remove_mute(user.id, activity.data["context"])
475 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
476 when is_binary("context") do
477 ThreadMute.exists?(user_id, context)
480 def thread_muted?(_, _), do: false
482 def report(user, data) do
483 with {:ok, account} <- get_reported_account(data.account_id),
484 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
485 {:ok, statuses} <- get_report_statuses(account, data) do
487 context: Utils.generate_context_id(),
491 content: content_html,
492 forward: Map.get(data, :forward, false)
497 defp get_reported_account(account_id) do
498 case User.get_cached_by_id(account_id) do
499 %User{} = account -> {:ok, account}
500 _ -> {:error, dgettext("errors", "Account not found")}
504 def update_report_state(activity_ids, state) when is_list(activity_ids) do
505 case Utils.update_report_state(activity_ids, state) do
506 :ok -> {:ok, activity_ids}
507 _ -> {:error, dgettext("errors", "Could not update state")}
511 def update_report_state(activity_id, state) do
512 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
513 Utils.update_report_state(activity, state)
515 nil -> {:error, :not_found}
516 _ -> {:error, dgettext("errors", "Could not update state")}
520 def update_activity_scope(activity_id, opts \\ %{}) do
521 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
522 {:ok, activity} <- toggle_sensitive(activity, opts) do
523 set_visibility(activity, opts)
525 nil -> {:error, :not_found}
526 {:error, reason} -> {:error, reason}
530 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
531 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
534 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
535 when is_boolean(sensitive) do
536 new_data = Map.put(object.data, "sensitive", sensitive)
540 |> Object.change(%{data: new_data})
541 |> Object.update_and_set_cache()
543 {:ok, Map.put(activity, :object, object)}
546 defp toggle_sensitive(activity, _), do: {:ok, activity}
548 defp set_visibility(activity, %{visibility: visibility}) do
549 Utils.update_activity_visibility(activity, visibility)
552 defp set_visibility(activity, _), do: {:ok, activity}
554 def hide_reblogs(%User{} = user, %User{} = target) do
555 UserRelationship.create_reblog_mute(user, target)
558 def show_reblogs(%User{} = user, %User{} = target) do
559 UserRelationship.delete_reblog_mute(user, target)