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, follower} <- User.follow(follower, followed),
126 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
127 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
129 ActivityPub.accept(%{
130 to: [follower.ap_id],
132 object: follow_activity.data["id"],
135 Notification.update_notification_type(followed, follow_activity)
140 def reject_follow_request(follower, followed) do
141 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
142 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
143 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
144 {:ok, _notifications} <- Notification.dismiss(follow_activity),
146 ActivityPub.reject(%{
147 to: [follower.ap_id],
149 object: follow_activity.data["id"],
156 def delete(activity_id, user) do
157 with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
158 {:find_activity, Activity.get_by_id(activity_id)},
159 {_, %Object{} = object, _} <-
160 {:find_object, Object.normalize(activity, false), activity},
161 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
162 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
163 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
166 {:find_activity, _} ->
169 {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
170 # We have the create activity, but not the object, it was probably pruned.
171 # Insert a tombstone and try again
172 with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
173 {:ok, _tombstone} <- Object.create(tombstone_data) do
174 delete(activity_id, user)
178 "Could not insert tombstone for missing object on deletion. Object is #{object}."
181 {:error, dgettext("errors", "Could not delete")}
185 {:error, dgettext("errors", "Could not delete")}
189 def repeat(id, user, params \\ %{}) do
190 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
191 object = %Object{} <- Object.normalize(activity, false),
192 {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
193 public = public_announce?(object, params),
194 {:ok, announce, _} <- Builder.announce(user, object, public: public),
195 {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
198 {:existing_announce, %Activity{} = announce} ->
206 def unrepeat(id, user) do
207 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
208 {:find_activity, Activity.get_by_id(id)},
209 %Object{} = note <- Object.normalize(activity, false),
210 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
211 {:ok, undo, _} <- Builder.undo(user, announce),
212 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
215 {:find_activity, _} -> {:error, :not_found}
216 _ -> {:error, dgettext("errors", "Could not unrepeat")}
220 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
221 def favorite(%User{} = user, id) do
222 case favorite_helper(user, id) do
226 {:error, :not_found} = res ->
230 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
231 {:error, dgettext("errors", "Could not favorite")}
235 def favorite_helper(user, id) do
236 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
237 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
238 {_, {:ok, %Activity{} = activity, _meta}} <-
240 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
257 if {:object, {"already liked by this actor", []}} in changeset.errors do
258 {:ok, :already_liked}
268 def unfavorite(id, user) do
269 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
270 {:find_activity, Activity.get_by_id(id)},
271 %Object{} = note <- Object.normalize(activity, false),
272 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
273 {:ok, undo, _} <- Builder.undo(user, like),
274 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
277 {:find_activity, _} -> {:error, :not_found}
278 _ -> {:error, dgettext("errors", "Could not unfavorite")}
282 def react_with_emoji(id, user, emoji) do
283 with %Activity{} = activity <- Activity.get_by_id(id),
284 object <- Object.normalize(activity),
285 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
286 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
290 {:error, dgettext("errors", "Could not add reaction emoji")}
294 def unreact_with_emoji(id, user, emoji) do
295 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
296 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
297 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
301 {:error, dgettext("errors", "Could not remove reaction emoji")}
305 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
306 with :ok <- validate_not_author(object, user),
307 :ok <- validate_existing_votes(user, object),
308 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
310 Enum.map(choices, fn index ->
311 {:ok, answer_object, _meta} =
312 Builder.answer(user, object, Enum.at(options, index)["name"])
314 {:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
316 {:ok, activity, _meta} =
318 |> Map.put("cc", answer_object["cc"])
319 |> Map.put("context", answer_object["context"])
320 |> Pipeline.common_pipeline(local: true)
322 # TODO: Do preload of Pleroma.Object in Pipeline
323 Activity.normalize(activity.data)
326 object = Object.get_cached_by_ap_id(object.data["id"])
327 {:ok, answer_activities, object}
331 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
332 do: {:error, dgettext("errors", "Poll's author can't vote")}
334 defp validate_not_author(_, _), do: :ok
336 defp validate_existing_votes(%{ap_id: ap_id}, object) do
337 if Utils.get_existing_votes(ap_id, object) == [] do
340 {:error, dgettext("errors", "Already voted")}
344 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
345 when is_list(any_of) and any_of != [],
346 do: {any_of, Enum.count(any_of)}
348 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
349 when is_list(one_of) and one_of != [],
352 defp normalize_and_validate_choices(choices, object) do
353 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
354 {options, max_count} = get_options_and_max_count(object)
355 count = Enum.count(options)
357 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
358 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
359 {:ok, options, choices}
361 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
362 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
366 def public_announce?(_, %{visibility: visibility})
367 when visibility in ~w{public unlisted private direct},
368 do: visibility in ~w(public unlisted)
370 def public_announce?(object, _) do
371 Visibility.is_public?(object)
374 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
376 def get_visibility(%{visibility: visibility}, in_reply_to, _)
377 when visibility in ~w{public unlisted private direct},
378 do: {visibility, get_replied_to_visibility(in_reply_to)}
380 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
381 visibility = {:list, String.to_integer(list_id)}
382 {visibility, get_replied_to_visibility(in_reply_to)}
385 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
386 visibility = get_replied_to_visibility(in_reply_to)
387 {visibility, visibility}
390 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
392 def get_replied_to_visibility(nil), do: nil
394 def get_replied_to_visibility(activity) do
395 with %Object{} = object <- Object.normalize(activity) do
396 Visibility.get_visibility(object)
400 def check_expiry_date({:ok, nil} = res), do: res
402 def check_expiry_date({:ok, in_seconds}) do
403 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
405 if ActivityExpiration.expires_late_enough?(expiry) do
408 {:error, "Expiry date is too soon"}
412 def check_expiry_date(expiry_str) do
413 Ecto.Type.cast(:integer, expiry_str)
414 |> check_expiry_date()
417 def listen(user, data) do
418 visibility = Map.get(data, :visibility, "public")
420 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
423 |> Map.take([:album, :artist, :title, :length])
424 |> Map.new(fn {key, value} -> {to_string(key), value} end)
425 |> Map.put("type", "Audio")
428 |> Map.put("actor", user.ap_id),
430 ActivityPub.listen(%{
434 context: Utils.generate_context_id(),
435 additional: %{"cc" => cc}
441 def post(user, %{status: _} = data) do
442 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
443 ActivityPub.create(draft.changes, draft.preview?)
447 def pin(id, %{ap_id: user_ap_id} = user) do
450 data: %{"type" => "Create"},
451 object: %Object{data: %{"type" => object_type}}
452 } = activity <- Activity.get_by_id_with_object(id),
453 true <- object_type in ["Note", "Article", "Question"],
454 true <- Visibility.is_public?(activity),
455 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
458 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
459 _ -> {:error, dgettext("errors", "Could not pin")}
463 def unpin(id, user) do
464 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
465 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
468 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
469 _ -> {:error, dgettext("errors", "Could not unpin")}
473 def add_mute(user, activity) do
474 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
477 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
481 def remove_mute(user, activity) do
482 ThreadMute.remove_mute(user.id, activity.data["context"])
486 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
487 when is_binary("context") do
488 ThreadMute.exists?(user_id, context)
491 def thread_muted?(_, _), do: false
493 def report(user, data) do
494 with {:ok, account} <- get_reported_account(data.account_id),
495 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
496 {:ok, statuses} <- get_report_statuses(account, data) do
498 context: Utils.generate_context_id(),
502 content: content_html,
503 forward: Map.get(data, :forward, false)
508 defp get_reported_account(account_id) do
509 case User.get_cached_by_id(account_id) do
510 %User{} = account -> {:ok, account}
511 _ -> {:error, dgettext("errors", "Account not found")}
515 def update_report_state(activity_ids, state) when is_list(activity_ids) do
516 case Utils.update_report_state(activity_ids, state) do
517 :ok -> {:ok, activity_ids}
518 _ -> {:error, dgettext("errors", "Could not update state")}
522 def update_report_state(activity_id, state) do
523 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
524 Utils.update_report_state(activity, state)
526 nil -> {:error, :not_found}
527 _ -> {:error, dgettext("errors", "Could not update state")}
531 def update_activity_scope(activity_id, opts \\ %{}) do
532 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
533 {:ok, activity} <- toggle_sensitive(activity, opts) do
534 set_visibility(activity, opts)
536 nil -> {:error, :not_found}
537 {:error, reason} -> {:error, reason}
541 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
542 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
545 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
546 when is_boolean(sensitive) do
547 new_data = Map.put(object.data, "sensitive", sensitive)
551 |> Object.change(%{data: new_data})
552 |> Object.update_and_set_cache()
554 {:ok, Map.put(activity, :object, object)}
557 defp toggle_sensitive(activity, _), do: {:ok, activity}
559 defp set_visibility(activity, %{visibility: visibility}) do
560 Utils.update_activity_visibility(activity, visibility)
563 defp set_visibility(activity, _), do: {:ok, activity}
565 def hide_reblogs(%User{} = user, %User{} = target) do
566 UserRelationship.create_reblog_mute(user, target)
569 def show_reblogs(%User{} = user, %User{} = target) do
570 UserRelationship.delete_reblog_mute(user, target)