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 block(blocker, blocked) do
29 with {:ok, block_data, _} <- Builder.block(blocker, blocked),
30 {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
35 def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
36 with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
37 :ok <- validate_chat_content_length(content, !!maybe_attachment),
38 {_, {:ok, chat_message_data, _meta}} <-
43 content |> format_chat_content,
44 attachment: maybe_attachment
46 {_, {:ok, create_activity_data, _meta}} <-
47 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
48 {_, {:ok, %Activity{} = activity, _meta}} <-
50 Pipeline.common_pipeline(create_activity_data,
57 defp format_chat_content(nil), do: nil
59 defp format_chat_content(content) do
62 |> Formatter.html_escape("text/plain")
63 |> Formatter.linkify()
64 |> (fn {text, mentions, tags} ->
65 {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
71 defp validate_chat_content_length(_, true), do: :ok
72 defp validate_chat_content_length(nil, false), do: {:error, :no_content}
74 defp validate_chat_content_length(content, _) do
75 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
78 {:error, :content_too_long}
82 def unblock(blocker, blocked) do
83 with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
84 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
85 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
88 {:fetch_block, nil} ->
89 if User.blocks?(blocker, blocked) do
90 User.unblock(blocker, blocked)
93 {:error, :not_blocking}
101 def follow(follower, followed) do
102 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
104 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
105 {:ok, activity} <- ActivityPub.follow(follower, followed),
106 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
107 {:ok, follower, followed, activity}
111 def unfollow(follower, unfollowed) do
112 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
113 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
114 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
119 def accept_follow_request(follower, followed) do
120 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
121 {:ok, follower} <- User.follow(follower, followed),
122 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
123 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
125 ActivityPub.accept(%{
126 to: [follower.ap_id],
128 object: follow_activity.data["id"],
131 Notification.update_notification_type(followed, follow_activity)
136 def reject_follow_request(follower, followed) do
137 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
138 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
139 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
140 {:ok, _notifications} <- Notification.dismiss(follow_activity),
142 ActivityPub.reject(%{
143 to: [follower.ap_id],
145 object: follow_activity.data["id"],
152 def delete(activity_id, user) do
153 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
154 {:find_activity, Activity.get_by_id(activity_id)},
155 {_, %Object{} = object, _} <-
156 {:find_object, Object.normalize(activity, false), activity},
157 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
158 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
159 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
162 {:find_activity, _} ->
165 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
166 # We have the create activity, but not the object, it was probably pruned.
167 # Insert a tombstone and try again
168 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
169 {:ok, _tombstone} <- Object.create(tombstone_data) do
170 delete(activity_id, user)
174 "Could not insert tombstone for missing object on deletion. Object is #{object}."
177 {:error, dgettext("errors", "Could not delete")}
181 {:error, dgettext("errors", "Could not delete")}
185 def repeat(id, user, params \\ %{}) do
186 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
187 object = %Object{} <- Object.normalize(activity, false),
188 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
189 public = public_announce?(object, params),
190 {:ok, announce, _} <- Builder.announce(user, object, public: public),
191 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
194 {:existing_announce, %Activity{} = announce} ->
202 def unrepeat(id, user) do
203 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
204 {:find_activity, Activity.get_by_id(id)},
205 %Object{} = note <- Object.normalize(activity, false),
206 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
207 {:ok, undo, _} <- Builder.undo(user, announce),
208 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
211 {:find_activity, _} -> {:error, :not_found}
212 _ -> {:error, dgettext("errors", "Could not unrepeat")}
216 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
217 def favorite(%User{} = user, id) do
218 case favorite_helper(user, id) do
222 {:error, :not_found} = res ->
226 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
227 {:error, dgettext("errors", "Could not favorite")}
231 def favorite_helper(user, id) do
232 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
233 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
234 {_, {:ok, %Activity{} = activity, _meta}} <-
236 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
253 if {:object, {"already liked by this actor", []}} in changeset.errors do
254 {:ok, :already_liked}
264 def unfavorite(id, user) do
265 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
266 {:find_activity, Activity.get_by_id(id)},
267 %Object{} = note <- Object.normalize(activity, false),
268 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
269 {:ok, undo, _} <- Builder.undo(user, like),
270 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
273 {:find_activity, _} -> {:error, :not_found}
274 _ -> {:error, dgettext("errors", "Could not unfavorite")}
278 def react_with_emoji(id, user, emoji) do
279 with %Activity{} = activity <- Activity.get_by_id(id),
280 object <- Object.normalize(activity),
281 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
282 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
286 {:error, dgettext("errors", "Could not add reaction emoji")}
290 def unreact_with_emoji(id, user, emoji) do
291 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
292 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
293 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
297 {:error, dgettext("errors", "Could not remove reaction emoji")}
301 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
302 with :ok <- validate_not_author(object, user),
303 :ok <- validate_existing_votes(user, object),
304 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
306 Enum.map(choices, fn index ->
307 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
310 ActivityPub.create(%{
311 to: answer_data["to"],
313 context: object.data["context"],
315 additional: %{"cc" => answer_data["cc"]}
321 object = Object.get_cached_by_ap_id(object.data["id"])
322 {:ok, answer_activities, object}
326 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
327 do: {:error, dgettext("errors", "Poll's author can't vote")}
329 defp validate_not_author(_, _), do: :ok
331 defp validate_existing_votes(%{ap_id: ap_id}, object) do
332 if Utils.get_existing_votes(ap_id, object) == [] do
335 {:error, dgettext("errors", "Already voted")}
339 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
340 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
342 defp normalize_and_validate_choices(choices, object) do
343 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
344 {options, max_count} = get_options_and_max_count(object)
345 count = Enum.count(options)
347 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
348 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
349 {:ok, options, choices}
351 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
352 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
356 def public_announce?(_, %{visibility: visibility})
357 when visibility in ~w{public unlisted private direct},
358 do: visibility in ~w(public unlisted)
360 def public_announce?(object, _) do
361 Visibility.is_public?(object)
364 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
366 def get_visibility(%{visibility: visibility}, in_reply_to, _)
367 when visibility in ~w{public unlisted private direct},
368 do: {visibility, get_replied_to_visibility(in_reply_to)}
370 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
371 visibility = {:list, String.to_integer(list_id)}
372 {visibility, get_replied_to_visibility(in_reply_to)}
375 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
376 visibility = get_replied_to_visibility(in_reply_to)
377 {visibility, visibility}
380 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
382 def get_replied_to_visibility(nil), do: nil
384 def get_replied_to_visibility(activity) do
385 with %Object{} = object <- Object.normalize(activity) do
386 Visibility.get_visibility(object)
390 def check_expiry_date({:ok, nil} = res), do: res
392 def check_expiry_date({:ok, in_seconds}) do
393 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
395 if ActivityExpiration.expires_late_enough?(expiry) do
398 {:error, "Expiry date is too soon"}
402 def check_expiry_date(expiry_str) do
403 Ecto.Type.cast(:integer, expiry_str)
404 |> check_expiry_date()
407 def listen(user, data) do
408 visibility = Map.get(data, :visibility, "public")
410 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
413 |> Map.take([:album, :artist, :title, :length])
414 |> Map.new(fn {key, value} -> {to_string(key), value} end)
415 |> Map.put("type", "Audio")
418 |> Map.put("actor", user.ap_id),
420 ActivityPub.listen(%{
424 context: Utils.generate_context_id(),
425 additional: %{"cc" => cc}
431 def post(user, %{status: _} = data) do
432 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
433 ActivityPub.create(draft.changes, draft.preview?)
437 def pin(id, %{ap_id: user_ap_id} = user) do
440 data: %{"type" => "Create"},
441 object: %Object{data: %{"type" => object_type}}
442 } = activity <- Activity.get_by_id_with_object(id),
443 true <- object_type in ["Note", "Article", "Question"],
444 true <- Visibility.is_public?(activity),
445 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
448 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
449 _ -> {:error, dgettext("errors", "Could not pin")}
453 def unpin(id, user) do
454 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
455 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
458 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
459 _ -> {:error, dgettext("errors", "Could not unpin")}
463 def add_mute(user, activity) do
464 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
467 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
471 def remove_mute(user, activity) do
472 ThreadMute.remove_mute(user.id, activity.data["context"])
476 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
477 when is_binary("context") do
478 ThreadMute.exists?(user_id, context)
481 def thread_muted?(_, _), do: false
483 def report(user, data) do
484 with {:ok, account} <- get_reported_account(data.account_id),
485 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
486 {:ok, statuses} <- get_report_statuses(account, data) do
488 context: Utils.generate_context_id(),
492 content: content_html,
493 forward: Map.get(data, :forward, false)
498 defp get_reported_account(account_id) do
499 case User.get_cached_by_id(account_id) do
500 %User{} = account -> {:ok, account}
501 _ -> {:error, dgettext("errors", "Account not found")}
505 def update_report_state(activity_ids, state) when is_list(activity_ids) do
506 case Utils.update_report_state(activity_ids, state) do
507 :ok -> {:ok, activity_ids}
508 _ -> {:error, dgettext("errors", "Could not update state")}
512 def update_report_state(activity_id, state) do
513 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
514 Utils.update_report_state(activity, state)
516 nil -> {:error, :not_found}
517 _ -> {:error, dgettext("errors", "Could not update state")}
521 def update_activity_scope(activity_id, opts \\ %{}) do
522 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
523 {:ok, activity} <- toggle_sensitive(activity, opts) do
524 set_visibility(activity, opts)
526 nil -> {:error, :not_found}
527 {:error, reason} -> {:error, reason}
531 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
532 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
535 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
536 when is_boolean(sensitive) do
537 new_data = Map.put(object.data, "sensitive", sensitive)
541 |> Object.change(%{data: new_data})
542 |> Object.update_and_set_cache()
544 {:ok, Map.put(activity, :object, object)}
547 defp toggle_sensitive(activity, _), do: {:ok, activity}
549 defp set_visibility(activity, %{visibility: visibility}) do
550 Utils.update_activity_visibility(activity, visibility)
553 defp set_visibility(activity, _), do: {:ok, activity}
555 def hide_reblogs(%User{} = user, %User{} = target) do
556 UserRelationship.create_reblog_mute(user, target)
559 def show_reblogs(%User{} = user, %User{} = target) do
560 UserRelationship.delete_reblog_mute(user, target)