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()
57 |> (fn {text, mentions, tags} ->
58 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
64 defp validate_chat_content_length(_, true), do: :ok
65 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
67 defp validate_chat_content_length(content, _) do
68 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
71 {:error, :content_too_long}
75 def unblock(blocker, blocked) do
76 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
77 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
78 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
81 {:fetch_block, nil} ->
82 if User.blocks?(blocker, blocked) do
83 User.unblock(blocker, blocked)
86 {:error, :not_blocking}
94 def follow(follower, followed) do
95 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
97 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
98 {:ok, activity} <- ActivityPub.follow(follower, followed),
99 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
100 {:ok, follower, followed, activity}
104 def unfollow(follower, unfollowed) do
105 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
106 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
107 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
112 def accept_follow_request(follower, followed) do
113 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
114 {:ok, follower} <- User.follow(follower, followed),
115 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
116 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
118 ActivityPub.accept(%{
119 to: [follower.ap_id],
121 object: follow_activity.data["id"],
124 Notification.update_notification_type(followed, follow_activity)
129 def reject_follow_request(follower, followed) do
130 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
131 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
132 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
133 {:ok, _notifications} <- Notification.dismiss(follow_activity),
135 ActivityPub.reject(%{
136 to: [follower.ap_id],
138 object: follow_activity.data["id"],
145 def delete(activity_id, user) do
146 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
147 {:find_activity, Activity.get_by_id(activity_id)},
148 {_, %Object{} = object, _} <-
149 {:find_object, Object.normalize(activity, false), activity},
150 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
151 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
152 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
155 {:find_activity, _} ->
158 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
159 # We have the create activity, but not the object, it was probably pruned.
160 # Insert a tombstone and try again
161 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
162 {:ok, _tombstone} <- Object.create(tombstone_data) do
163 delete(activity_id, user)
167 "Could not insert tombstone for missing object on deletion. Object is #{object}."
170 {:error, dgettext("errors", "Could not delete")}
174 {:error, dgettext("errors", "Could not delete")}
178 def repeat(id, user, params \\ %{}) do
179 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
180 object = %Object{} <- Object.normalize(activity, false),
181 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
182 public = public_announce?(object, params),
183 {:ok, announce, _} <- Builder.announce(user, object, public: public),
184 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
187 {:existing_announce, %Activity{} = announce} ->
195 def unrepeat(id, user) do
196 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
197 {:find_activity, Activity.get_by_id(id)},
198 %Object{} = note <- Object.normalize(activity, false),
199 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
200 {:ok, undo, _} <- Builder.undo(user, announce),
201 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
204 {:find_activity, _} -> {:error, :not_found}
205 _ -> {:error, dgettext("errors", "Could not unrepeat")}
209 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
210 def favorite(%User{} = user, id) do
211 case favorite_helper(user, id) do
215 {:error, :not_found} = res ->
219 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
220 {:error, dgettext("errors", "Could not favorite")}
224 def favorite_helper(user, id) do
225 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
226 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
227 {_, {:ok, %Activity{} = activity, _meta}} <-
229 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
246 if {:object, {"already liked by this actor", []}} in changeset.errors do
247 {:ok, :already_liked}
257 def unfavorite(id, user) do
258 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
259 {:find_activity, Activity.get_by_id(id)},
260 %Object{} = note <- Object.normalize(activity, false),
261 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
262 {:ok, undo, _} <- Builder.undo(user, like),
263 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
266 {:find_activity, _} -> {:error, :not_found}
267 _ -> {:error, dgettext("errors", "Could not unfavorite")}
271 def react_with_emoji(id, user, emoji) do
272 with %Activity{} = activity <- Activity.get_by_id(id),
273 object <- Object.normalize(activity),
274 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
275 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
279 {:error, dgettext("errors", "Could not add reaction emoji")}
283 def unreact_with_emoji(id, user, emoji) do
284 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
285 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
286 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
290 {:error, dgettext("errors", "Could not remove reaction emoji")}
294 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
295 with :ok <- validate_not_author(object, user),
296 :ok <- validate_existing_votes(user, object),
297 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
299 Enum.map(choices, fn index ->
300 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
303 ActivityPub.create(%{
304 to: answer_data["to"],
306 context: object.data["context"],
308 additional: %{"cc" => answer_data["cc"]}
314 object = Object.get_cached_by_ap_id(object.data["id"])
315 {:ok, answer_activities, object}
319 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
320 do: {:error, dgettext("errors", "Poll's author can't vote")}
322 defp validate_not_author(_, _), do: :ok
324 defp validate_existing_votes(%{ap_id: ap_id}, object) do
325 if Utils.get_existing_votes(ap_id, object) == [] do
328 {:error, dgettext("errors", "Already voted")}
332 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
333 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
335 defp normalize_and_validate_choices(choices, object) do
336 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
337 {options, max_count} = get_options_and_max_count(object)
338 count = Enum.count(options)
340 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
341 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
342 {:ok, options, choices}
344 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
345 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
349 def public_announce?(_, %{visibility: visibility})
350 when visibility in ~w{public unlisted private direct},
351 do: visibility in ~w(public unlisted)
353 def public_announce?(object, _) do
354 Visibility.is_public?(object)
357 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
359 def get_visibility(%{visibility: visibility}, in_reply_to, _)
360 when visibility in ~w{public unlisted private direct},
361 do: {visibility, get_replied_to_visibility(in_reply_to)}
363 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
364 visibility = {:list, String.to_integer(list_id)}
365 {visibility, get_replied_to_visibility(in_reply_to)}
368 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
369 visibility = get_replied_to_visibility(in_reply_to)
370 {visibility, visibility}
373 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
375 def get_replied_to_visibility(nil), do: nil
377 def get_replied_to_visibility(activity) do
378 with %Object{} = object <- Object.normalize(activity) do
379 Visibility.get_visibility(object)
383 def check_expiry_date({:ok, nil} = res), do: res
385 def check_expiry_date({:ok, in_seconds}) do
386 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
388 if ActivityExpiration.expires_late_enough?(expiry) do
391 {:error, "Expiry date is too soon"}
395 def check_expiry_date(expiry_str) do
396 Ecto.Type.cast(:integer, expiry_str)
397 |> check_expiry_date()
400 def listen(user, data) do
401 visibility = Map.get(data, :visibility, "public")
403 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
406 |> Map.take([:album, :artist, :title, :length])
407 |> Map.new(fn {key, value} -> {to_string(key), value} end)
408 |> Map.put("type", "Audio")
411 |> Map.put("actor", user.ap_id),
413 ActivityPub.listen(%{
417 context: Utils.generate_context_id(),
418 additional: %{"cc" => cc}
424 def post(user, %{status: _} = data) do
425 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
427 |> ActivityPub.create(draft.preview?)
428 |> maybe_create_activity_expiration(draft.expires_at)
432 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
433 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
438 defp maybe_create_activity_expiration(result, _), do: result
440 def pin(id, %{ap_id: user_ap_id} = user) do
443 data: %{"type" => "Create"},
444 object: %Object{data: %{"type" => object_type}}
445 } = activity <- Activity.get_by_id_with_object(id),
446 true <- object_type in ["Note", "Article", "Question"],
447 true <- Visibility.is_public?(activity),
448 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
451 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
452 _ -> {:error, dgettext("errors", "Could not pin")}
456 def unpin(id, user) do
457 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
458 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
461 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
462 _ -> {:error, dgettext("errors", "Could not unpin")}
466 def add_mute(user, activity) do
467 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
470 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
474 def remove_mute(user, activity) do
475 ThreadMute.remove_mute(user.id, activity.data["context"])
479 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
480 when is_binary("context") do
481 ThreadMute.exists?(user_id, context)
484 def thread_muted?(_, _), do: false
486 def report(user, data) do
487 with {:ok, account} <- get_reported_account(data.account_id),
488 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
489 {:ok, statuses} <- get_report_statuses(account, data) do
491 context: Utils.generate_context_id(),
495 content: content_html,
496 forward: Map.get(data, :forward, false)
501 defp get_reported_account(account_id) do
502 case User.get_cached_by_id(account_id) do
503 %User{} = account -> {:ok, account}
504 _ -> {:error, dgettext("errors", "Account not found")}
508 def update_report_state(activity_ids, state) when is_list(activity_ids) do
509 case Utils.update_report_state(activity_ids, state) do
510 :ok -> {:ok, activity_ids}
511 _ -> {:error, dgettext("errors", "Could not update state")}
515 def update_report_state(activity_id, state) do
516 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
517 Utils.update_report_state(activity, state)
519 nil -> {:error, :not_found}
520 _ -> {:error, dgettext("errors", "Could not update state")}
524 def update_activity_scope(activity_id, opts \\ %{}) do
525 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
526 {:ok, activity} <- toggle_sensitive(activity, opts) do
527 set_visibility(activity, opts)
529 nil -> {:error, :not_found}
530 {:error, reason} -> {:error, reason}
534 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
535 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
538 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
539 when is_boolean(sensitive) do
540 new_data = Map.put(object.data, "sensitive", sensitive)
544 |> Object.change(%{data: new_data})
545 |> Object.update_and_set_cache()
547 {:ok, Map.put(activity, :object, object)}
550 defp toggle_sensitive(activity, _), do: {:ok, activity}
552 defp set_visibility(activity, %{visibility: visibility}) do
553 Utils.update_activity_visibility(activity, visibility)
556 defp set_visibility(activity, _), do: {:ok, activity}
558 def hide_reblogs(%User{} = user, %User{} = target) do
559 UserRelationship.create_reblog_mute(user, target)
562 def show_reblogs(%User{} = user, %User{} = target) do
563 UserRelationship.delete_reblog_mute(user, target)