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, follow_data, _} <- Builder.follow(follower, followed),
105 {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
106 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
107 if activity.data["state"] == "reject" do
110 {:ok, follower, followed, activity}
115 def unfollow(follower, unfollowed) do
116 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
117 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
118 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
123 def accept_follow_request(follower, followed) do
124 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
125 {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
126 {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
131 def reject_follow_request(follower, followed) do
132 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
133 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
134 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
135 {:ok, _notifications} <- Notification.dismiss(follow_activity),
137 ActivityPub.reject(%{
138 to: [follower.ap_id],
140 object: follow_activity.data["id"],
147 def delete(activity_id, user) do
148 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
149 {:find_activity, Activity.get_by_id(activity_id)},
150 {_, %Object{} = object, _} <-
151 {:find_object, Object.normalize(activity, false), activity},
152 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
153 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
154 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
157 {:find_activity, _} ->
160 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
161 # We have the create activity, but not the object, it was probably pruned.
162 # Insert a tombstone and try again
163 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
164 {:ok, _tombstone} <- Object.create(tombstone_data) do
165 delete(activity_id, user)
169 "Could not insert tombstone for missing object on deletion. Object is #{object}."
172 {:error, dgettext("errors", "Could not delete")}
176 {:error, dgettext("errors", "Could not delete")}
180 def repeat(id, user, params \\ %{}) do
181 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
182 object = %Object{} <- Object.normalize(activity, false),
183 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
184 public = public_announce?(object, params),
185 {:ok, announce, _} <- Builder.announce(user, object, public: public),
186 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
189 {:existing_announce, %Activity{} = announce} ->
197 def unrepeat(id, user) do
198 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
199 {:find_activity, Activity.get_by_id(id)},
200 %Object{} = note <- Object.normalize(activity, false),
201 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
202 {:ok, undo, _} <- Builder.undo(user, announce),
203 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
206 {:find_activity, _} -> {:error, :not_found}
207 _ -> {:error, dgettext("errors", "Could not unrepeat")}
211 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
212 def favorite(%User{} = user, id) do
213 case favorite_helper(user, id) do
217 {:error, :not_found} = res ->
221 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
222 {:error, dgettext("errors", "Could not favorite")}
226 def favorite_helper(user, id) do
227 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
228 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
229 {_, {:ok, %Activity{} = activity, _meta}} <-
231 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
248 if {:object, {"already liked by this actor", []}} in changeset.errors do
249 {:ok, :already_liked}
259 def unfavorite(id, user) do
260 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
261 {:find_activity, Activity.get_by_id(id)},
262 %Object{} = note <- Object.normalize(activity, false),
263 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
264 {:ok, undo, _} <- Builder.undo(user, like),
265 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
268 {:find_activity, _} -> {:error, :not_found}
269 _ -> {:error, dgettext("errors", "Could not unfavorite")}
273 def react_with_emoji(id, user, emoji) do
274 with %Activity{} = activity <- Activity.get_by_id(id),
275 object <- Object.normalize(activity),
276 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
277 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
281 {:error, dgettext("errors", "Could not add reaction emoji")}
285 def unreact_with_emoji(id, user, emoji) do
286 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
287 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
288 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
292 {:error, dgettext("errors", "Could not remove reaction emoji")}
296 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
297 with :ok <- validate_not_author(object, user),
298 :ok <- validate_existing_votes(user, object),
299 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
301 Enum.map(choices, fn index ->
302 {:ok, answer_object, _meta} =
303 Builder.answer(user, object, Enum.at(options, index)["name"])
305 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
307 {:ok, activity, _meta} =
309 |> Map.put("cc", answer_object["cc"])
310 |> Map.put("context", answer_object["context"])
311 |> Pipeline.common_pipeline(local: true)
313 # TODO: Do preload of Pleroma.Object in Pipeline
314 Activity.normalize(activity.data)
317 object = Object.get_cached_by_ap_id(object.data["id"])
318 {:ok, answer_activities, object}
322 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
323 do: {:error, dgettext("errors", "Poll's author can't vote")}
325 defp validate_not_author(_, _), do: :ok
327 defp validate_existing_votes(%{ap_id: ap_id}, object) do
328 if Utils.get_existing_votes(ap_id, object) == [] do
331 {:error, dgettext("errors", "Already voted")}
335 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
336 when is_list(any_of) and any_of != [],
337 do: {any_of, Enum.count(any_of)}
339 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
340 when is_list(one_of) and one_of != [],
343 defp normalize_and_validate_choices(choices, object) do
344 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
345 {options, max_count} = get_options_and_max_count(object)
346 count = Enum.count(options)
348 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
349 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
350 {:ok, options, choices}
352 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
353 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
357 def public_announce?(_, %{visibility: visibility})
358 when visibility in ~w{public unlisted private direct},
359 do: visibility in ~w(public unlisted)
361 def public_announce?(object, _) do
362 Visibility.is_public?(object)
365 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
367 def get_visibility(%{visibility: visibility}, in_reply_to, _)
368 when visibility in ~w{public unlisted private direct},
369 do: {visibility, get_replied_to_visibility(in_reply_to)}
371 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
372 visibility = {:list, String.to_integer(list_id)}
373 {visibility, get_replied_to_visibility(in_reply_to)}
376 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
377 visibility = get_replied_to_visibility(in_reply_to)
378 {visibility, visibility}
381 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
383 def get_replied_to_visibility(nil), do: nil
385 def get_replied_to_visibility(activity) do
386 with %Object{} = object <- Object.normalize(activity) do
387 Visibility.get_visibility(object)
391 def check_expiry_date({:ok, nil} = res), do: res
393 def check_expiry_date({:ok, in_seconds}) do
394 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
396 if ActivityExpiration.expires_late_enough?(expiry) do
399 {:error, "Expiry date is too soon"}
403 def check_expiry_date(expiry_str) do
404 Ecto.Type.cast(:integer, expiry_str)
405 |> check_expiry_date()
408 def listen(user, data) do
409 visibility = Map.get(data, :visibility, "public")
411 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
414 |> Map.take([:album, :artist, :title, :length])
415 |> Map.new(fn {key, value} -> {to_string(key), value} end)
416 |> Map.put("type", "Audio")
419 |> Map.put("actor", user.ap_id),
421 ActivityPub.listen(%{
425 context: Utils.generate_context_id(),
426 additional: %{"cc" => cc}
432 def post(user, %{status: _} = data) do
433 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
434 ActivityPub.create(draft.changes, draft.preview?)
438 def pin(id, %{ap_id: user_ap_id} = user) do
441 data: %{"type" => "Create"},
442 object: %Object{data: %{"type" => object_type}}
443 } = activity <- Activity.get_by_id_with_object(id),
444 true <- object_type in ["Note", "Article", "Question"],
445 true <- Visibility.is_public?(activity),
446 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
449 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
450 _ -> {:error, dgettext("errors", "Could not pin")}
454 def unpin(id, user) do
455 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
456 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
459 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
460 _ -> {:error, dgettext("errors", "Could not unpin")}
464 def add_mute(user, activity) do
465 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
468 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
472 def remove_mute(user, activity) do
473 ThreadMute.remove_mute(user.id, activity.data["context"])
477 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
478 when is_binary("context") do
479 ThreadMute.exists?(user_id, context)
482 def thread_muted?(_, _), do: false
484 def report(user, data) do
485 with {:ok, account} <- get_reported_account(data.account_id),
486 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
487 {:ok, statuses} <- get_report_statuses(account, data) do
489 context: Utils.generate_context_id(),
493 content: content_html,
494 forward: Map.get(data, :forward, false)
499 defp get_reported_account(account_id) do
500 case User.get_cached_by_id(account_id) do
501 %User{} = account -> {:ok, account}
502 _ -> {:error, dgettext("errors", "Account not found")}
506 def update_report_state(activity_ids, state) when is_list(activity_ids) do
507 case Utils.update_report_state(activity_ids, state) do
508 :ok -> {:ok, activity_ids}
509 _ -> {:error, dgettext("errors", "Could not update state")}
513 def update_report_state(activity_id, state) do
514 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
515 Utils.update_report_state(activity, state)
517 nil -> {:error, :not_found}
518 _ -> {:error, dgettext("errors", "Could not update state")}
522 def update_activity_scope(activity_id, opts \\ %{}) do
523 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
524 {:ok, activity} <- toggle_sensitive(activity, opts) do
525 set_visibility(activity, opts)
527 nil -> {:error, :not_found}
528 {:error, reason} -> {:error, reason}
532 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
533 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
536 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
537 when is_boolean(sensitive) do
538 new_data = Map.put(object.data, "sensitive", sensitive)
542 |> Object.change(%{data: new_data})
543 |> Object.update_and_set_cache()
545 {:ok, Map.put(activity, :object, object)}
548 defp toggle_sensitive(activity, _), do: {:ok, activity}
550 defp set_visibility(activity, %{visibility: visibility}) do
551 Utils.update_activity_visibility(activity, visibility)
554 defp set_visibility(activity, _), do: {:ok, activity}
556 def hide_reblogs(%User{} = user, %User{} = target) do
557 UserRelationship.create_reblog_mute(user, target)
560 def show_reblogs(%User{} = user, %User{} = target) do
561 UserRelationship.delete_reblog_mute(user, target)