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) do
171 object = Object.normalize(activity)
172 announce_activity = Utils.get_existing_announce(user.ap_id, object)
173 public = public_announce?(object, params)
175 if announce_activity do
176 {:ok, announce_activity, object}
178 ActivityPub.announce(user, object, nil, true, public)
181 _ -> {:error, :not_found}
185 def unrepeat(id, user) do
186 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
187 {:find_activity, Activity.get_by_id(id)},
188 %Object{} = note <- Object.normalize(activity, false),
189 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
190 {:ok, undo, _} <- Builder.undo(user, announce),
191 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
194 {:find_activity, _} -> {:error, :not_found}
195 _ -> {:error, dgettext("errors", "Could not unrepeat")}
199 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
200 def favorite(%User{} = user, id) do
201 case favorite_helper(user, id) do
205 {:error, :not_found} = res ->
209 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
210 {:error, dgettext("errors", "Could not favorite")}
214 def favorite_helper(user, id) do
215 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
216 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
217 {_, {:ok, %Activity{} = activity, _meta}} <-
219 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
236 if {:object, {"already liked by this actor", []}} in changeset.errors do
237 {:ok, :already_liked}
247 def unfavorite(id, user) do
248 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
249 {:find_activity, Activity.get_by_id(id)},
250 %Object{} = note <- Object.normalize(activity, false),
251 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
252 {:ok, undo, _} <- Builder.undo(user, like),
253 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
256 {:find_activity, _} -> {:error, :not_found}
257 _ -> {:error, dgettext("errors", "Could not unfavorite")}
261 def react_with_emoji(id, user, emoji) do
262 with %Activity{} = activity <- Activity.get_by_id(id),
263 object <- Object.normalize(activity),
264 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
265 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
269 {:error, dgettext("errors", "Could not add reaction emoji")}
273 def unreact_with_emoji(id, user, emoji) do
274 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
275 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
276 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
280 {:error, dgettext("errors", "Could not remove reaction emoji")}
284 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
285 with :ok <- validate_not_author(object, user),
286 :ok <- validate_existing_votes(user, object),
287 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
289 Enum.map(choices, fn index ->
290 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
293 ActivityPub.create(%{
294 to: answer_data["to"],
296 context: object.data["context"],
298 additional: %{"cc" => answer_data["cc"]}
304 object = Object.get_cached_by_ap_id(object.data["id"])
305 {:ok, answer_activities, object}
309 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
310 do: {:error, dgettext("errors", "Poll's author can't vote")}
312 defp validate_not_author(_, _), do: :ok
314 defp validate_existing_votes(%{ap_id: ap_id}, object) do
315 if Utils.get_existing_votes(ap_id, object) == [] do
318 {:error, dgettext("errors", "Already voted")}
322 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
323 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
325 defp normalize_and_validate_choices(choices, object) do
326 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
327 {options, max_count} = get_options_and_max_count(object)
328 count = Enum.count(options)
330 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
331 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
332 {:ok, options, choices}
334 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
335 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
339 def public_announce?(_, %{visibility: visibility})
340 when visibility in ~w{public unlisted private direct},
341 do: visibility in ~w(public unlisted)
343 def public_announce?(object, _) do
344 Visibility.is_public?(object)
347 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
349 def get_visibility(%{visibility: visibility}, in_reply_to, _)
350 when visibility in ~w{public unlisted private direct},
351 do: {visibility, get_replied_to_visibility(in_reply_to)}
353 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
354 visibility = {:list, String.to_integer(list_id)}
355 {visibility, get_replied_to_visibility(in_reply_to)}
358 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
359 visibility = get_replied_to_visibility(in_reply_to)
360 {visibility, visibility}
363 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
365 def get_replied_to_visibility(nil), do: nil
367 def get_replied_to_visibility(activity) do
368 with %Object{} = object <- Object.normalize(activity) do
369 Visibility.get_visibility(object)
373 def check_expiry_date({:ok, nil} = res), do: res
375 def check_expiry_date({:ok, in_seconds}) do
376 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
378 if ActivityExpiration.expires_late_enough?(expiry) do
381 {:error, "Expiry date is too soon"}
385 def check_expiry_date(expiry_str) do
386 Ecto.Type.cast(:integer, expiry_str)
387 |> check_expiry_date()
390 def listen(user, %{"title" => _} = data) do
391 with visibility <- data["visibility"] || "public",
392 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
394 Map.take(data, ["album", "artist", "title", "length"])
395 |> Map.put("type", "Audio")
398 |> Map.put("actor", user.ap_id),
400 ActivityPub.listen(%{
404 context: Utils.generate_context_id(),
405 additional: %{"cc" => cc}
411 def post(user, %{status: _} = data) do
412 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
414 |> ActivityPub.create(draft.preview?)
415 |> maybe_create_activity_expiration(draft.expires_at)
419 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
420 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
425 defp maybe_create_activity_expiration(result, _), do: result
427 def pin(id, %{ap_id: user_ap_id} = user) do
430 data: %{"type" => "Create"},
431 object: %Object{data: %{"type" => object_type}}
432 } = activity <- Activity.get_by_id_with_object(id),
433 true <- object_type in ["Note", "Article", "Question"],
434 true <- Visibility.is_public?(activity),
435 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
438 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
439 _ -> {:error, dgettext("errors", "Could not pin")}
443 def unpin(id, user) do
444 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
445 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
448 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
449 _ -> {:error, dgettext("errors", "Could not unpin")}
453 def add_mute(user, activity) do
454 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
457 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
461 def remove_mute(user, activity) do
462 ThreadMute.remove_mute(user.id, activity.data["context"])
466 def thread_muted?(%{id: nil} = _user, _activity), do: false
468 def thread_muted?(user, activity) do
469 ThreadMute.exists?(user.id, activity.data["context"])
472 def report(user, data) do
473 with {:ok, account} <- get_reported_account(data.account_id),
474 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
475 {:ok, statuses} <- get_report_statuses(account, data) do
477 context: Utils.generate_context_id(),
481 content: content_html,
482 forward: Map.get(data, :forward, false)
487 defp get_reported_account(account_id) do
488 case User.get_cached_by_id(account_id) do
489 %User{} = account -> {:ok, account}
490 _ -> {:error, dgettext("errors", "Account not found")}
494 def update_report_state(activity_ids, state) when is_list(activity_ids) do
495 case Utils.update_report_state(activity_ids, state) do
496 :ok -> {:ok, activity_ids}
497 _ -> {:error, dgettext("errors", "Could not update state")}
501 def update_report_state(activity_id, state) do
502 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
503 Utils.update_report_state(activity, state)
505 nil -> {:error, :not_found}
506 _ -> {:error, dgettext("errors", "Could not update state")}
510 def update_activity_scope(activity_id, opts \\ %{}) do
511 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
512 {:ok, activity} <- toggle_sensitive(activity, opts) do
513 set_visibility(activity, opts)
515 nil -> {:error, :not_found}
516 {:error, reason} -> {:error, reason}
520 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
521 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
524 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
525 when is_boolean(sensitive) do
526 new_data = Map.put(object.data, "sensitive", sensitive)
530 |> Object.change(%{data: new_data})
531 |> Object.update_and_set_cache()
533 {:ok, Map.put(activity, :object, object)}
536 defp toggle_sensitive(activity, _), do: {:ok, activity}
538 defp set_visibility(activity, %{visibility: visibility}) do
539 Utils.update_activity_visibility(activity, visibility)
542 defp set_visibility(activity, _), do: {:ok, activity}
544 def hide_reblogs(%User{} = user, %User{} = target) do
545 UserRelationship.create_reblog_mute(user, target)
548 def show_reblogs(%User{} = user, %User{} = target) do
549 UserRelationship.delete_reblog_mute(user, target)