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, data) do
391 visibility = Map.get(data, :visibility, "public")
393 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
396 |> Map.take([:album, :artist, :title, :length])
397 |> Map.new(fn {key, value} -> {to_string(key), value} end)
398 |> Map.put("type", "Audio")
401 |> Map.put("actor", user.ap_id),
403 ActivityPub.listen(%{
407 context: Utils.generate_context_id(),
408 additional: %{"cc" => cc}
414 def post(user, %{status: _} = data) do
415 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
417 |> ActivityPub.create(draft.preview?)
418 |> maybe_create_activity_expiration(draft.expires_at)
422 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
423 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
428 defp maybe_create_activity_expiration(result, _), do: result
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"]) do
460 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
464 def remove_mute(user, activity) do
465 ThreadMute.remove_mute(user.id, activity.data["context"])
469 def thread_muted?(%{id: nil} = _user, _activity), do: false
471 def thread_muted?(user, activity) do
472 ThreadMute.exists?(user.id, activity.data["context"])
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)