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
53 content |> Formatter.html_escape("text/plain")
56 defp validate_chat_content_length(_, true), do: :ok
57 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
59 defp validate_chat_content_length(content, _) do
60 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
63 {:error, :content_too_long}
67 def unblock(blocker, blocked) do
68 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
69 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
70 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
73 {:fetch_block, nil} ->
74 if User.blocks?(blocker, blocked) do
75 User.unblock(blocker, blocked)
78 {:error, :not_blocking}
86 def follow(follower, followed) do
87 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
89 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
90 {:ok, activity} <- ActivityPub.follow(follower, followed),
91 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
92 {:ok, follower, followed, activity}
96 def unfollow(follower, unfollowed) do
97 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
98 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
99 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
104 def accept_follow_request(follower, followed) do
105 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
106 {:ok, follower} <- User.follow(follower, followed),
107 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
108 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
110 ActivityPub.accept(%{
111 to: [follower.ap_id],
113 object: follow_activity.data["id"],
120 def reject_follow_request(follower, followed) do
121 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
122 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
123 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
124 {:ok, _notifications} <- Notification.dismiss(follow_activity),
126 ActivityPub.reject(%{
127 to: [follower.ap_id],
129 object: follow_activity.data["id"],
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 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
294 ActivityPub.create(%{
295 to: answer_data["to"],
297 context: object.data["context"],
299 additional: %{"cc" => answer_data["cc"]}
305 object = Object.get_cached_by_ap_id(object.data["id"])
306 {:ok, answer_activities, object}
310 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
311 do: {:error, dgettext("errors", "Poll's author can't vote")}
313 defp validate_not_author(_, _), do: :ok
315 defp validate_existing_votes(%{ap_id: ap_id}, object) do
316 if Utils.get_existing_votes(ap_id, object) == [] do
319 {:error, dgettext("errors", "Already voted")}
323 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
324 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
326 defp normalize_and_validate_choices(choices, object) do
327 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
328 {options, max_count} = get_options_and_max_count(object)
329 count = Enum.count(options)
331 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
332 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
333 {:ok, options, choices}
335 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
336 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
340 def public_announce?(_, %{visibility: visibility})
341 when visibility in ~w{public unlisted private direct},
342 do: visibility in ~w(public unlisted)
344 def public_announce?(object, _) do
345 Visibility.is_public?(object)
348 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
350 def get_visibility(%{visibility: visibility}, in_reply_to, _)
351 when visibility in ~w{public unlisted private direct},
352 do: {visibility, get_replied_to_visibility(in_reply_to)}
354 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
355 visibility = {:list, String.to_integer(list_id)}
356 {visibility, get_replied_to_visibility(in_reply_to)}
359 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
360 visibility = get_replied_to_visibility(in_reply_to)
361 {visibility, visibility}
364 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
366 def get_replied_to_visibility(nil), do: nil
368 def get_replied_to_visibility(activity) do
369 with %Object{} = object <- Object.normalize(activity) do
370 Visibility.get_visibility(object)
374 def check_expiry_date({:ok, nil} = res), do: res
376 def check_expiry_date({:ok, in_seconds}) do
377 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
379 if ActivityExpiration.expires_late_enough?(expiry) do
382 {:error, "Expiry date is too soon"}
386 def check_expiry_date(expiry_str) do
387 Ecto.Type.cast(:integer, expiry_str)
388 |> check_expiry_date()
391 def listen(user, data) do
392 visibility = Map.get(data, :visibility, "public")
394 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
397 |> Map.take([:album, :artist, :title, :length])
398 |> Map.new(fn {key, value} -> {to_string(key), value} end)
399 |> Map.put("type", "Audio")
402 |> Map.put("actor", user.ap_id),
404 ActivityPub.listen(%{
408 context: Utils.generate_context_id(),
409 additional: %{"cc" => cc}
415 def post(user, %{status: _} = data) do
416 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
418 |> ActivityPub.create(draft.preview?)
419 |> maybe_create_activity_expiration(draft.expires_at)
423 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
424 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
429 defp maybe_create_activity_expiration(result, _), do: result
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) do
458 with {:ok, _} <- ThreadMute.add_mute(user.id, 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)