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 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
314 ActivityPub.create(%{
315 to: answer_data["to"],
317 context: object.data["context"],
319 additional: %{"cc" => answer_data["cc"]}
325 object = Object.get_cached_by_ap_id(object.data["id"])
326 {:ok, answer_activities, object}
330 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
331 do: {:error, dgettext("errors", "Poll's author can't vote")}
333 defp validate_not_author(_, _), do: :ok
335 defp validate_existing_votes(%{ap_id: ap_id}, object) do
336 if Utils.get_existing_votes(ap_id, object) == [] do
339 {:error, dgettext("errors", "Already voted")}
343 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
344 when is_list(any_of) and any_of != [],
345 do: {any_of, Enum.count(any_of)}
347 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
348 when is_list(one_of) and one_of != [],
351 defp normalize_and_validate_choices(choices, object) do
352 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
353 {options, max_count} = get_options_and_max_count(object)
354 count = Enum.count(options)
356 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
357 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
358 {:ok, options, choices}
360 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
361 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
365 def public_announce?(_, %{visibility: visibility})
366 when visibility in ~w{public unlisted private direct},
367 do: visibility in ~w(public unlisted)
369 def public_announce?(object, _) do
370 Visibility.is_public?(object)
373 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
375 def get_visibility(%{visibility: visibility}, in_reply_to, _)
376 when visibility in ~w{public unlisted private direct},
377 do: {visibility, get_replied_to_visibility(in_reply_to)}
379 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
380 visibility = {:list, String.to_integer(list_id)}
381 {visibility, get_replied_to_visibility(in_reply_to)}
384 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
385 visibility = get_replied_to_visibility(in_reply_to)
386 {visibility, visibility}
389 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
391 def get_replied_to_visibility(nil), do: nil
393 def get_replied_to_visibility(activity) do
394 with %Object{} = object <- Object.normalize(activity) do
395 Visibility.get_visibility(object)
399 def check_expiry_date({:ok, nil} = res), do: res
401 def check_expiry_date({:ok, in_seconds}) do
402 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
404 if ActivityExpiration.expires_late_enough?(expiry) do
407 {:error, "Expiry date is too soon"}
411 def check_expiry_date(expiry_str) do
412 Ecto.Type.cast(:integer, expiry_str)
413 |> check_expiry_date()
416 def listen(user, data) do
417 visibility = Map.get(data, :visibility, "public")
419 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
422 |> Map.take([:album, :artist, :title, :length])
423 |> Map.new(fn {key, value} -> {to_string(key), value} end)
424 |> Map.put("type", "Audio")
427 |> Map.put("actor", user.ap_id),
429 ActivityPub.listen(%{
433 context: Utils.generate_context_id(),
434 additional: %{"cc" => cc}
440 def post(user, %{status: _} = data) do
441 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
442 ActivityPub.create(draft.changes, draft.preview?)
446 def pin(id, %{ap_id: user_ap_id} = user) do
449 data: %{"type" => "Create"},
450 object: %Object{data: %{"type" => object_type}}
451 } = activity <- Activity.get_by_id_with_object(id),
452 true <- object_type in ["Note", "Article", "Question"],
453 true <- Visibility.is_public?(activity),
454 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
457 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
458 _ -> {:error, dgettext("errors", "Could not pin")}
462 def unpin(id, user) do
463 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
464 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
467 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
468 _ -> {:error, dgettext("errors", "Could not unpin")}
472 def add_mute(user, activity) do
473 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
476 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
480 def remove_mute(user, activity) do
481 ThreadMute.remove_mute(user.id, activity.data["context"])
485 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
486 when is_binary("context") do
487 ThreadMute.exists?(user_id, context)
490 def thread_muted?(_, _), do: false
492 def report(user, data) do
493 with {:ok, account} <- get_reported_account(data.account_id),
494 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
495 {:ok, statuses} <- get_report_statuses(account, data) do
497 context: Utils.generate_context_id(),
501 content: content_html,
502 forward: Map.get(data, :forward, false)
507 defp get_reported_account(account_id) do
508 case User.get_cached_by_id(account_id) do
509 %User{} = account -> {:ok, account}
510 _ -> {:error, dgettext("errors", "Account not found")}
514 def update_report_state(activity_ids, state) when is_list(activity_ids) do
515 case Utils.update_report_state(activity_ids, state) do
516 :ok -> {:ok, activity_ids}
517 _ -> {:error, dgettext("errors", "Could not update state")}
521 def update_report_state(activity_id, state) do
522 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
523 Utils.update_report_state(activity, state)
525 nil -> {:error, :not_found}
526 _ -> {:error, dgettext("errors", "Could not update state")}
530 def update_activity_scope(activity_id, opts \\ %{}) do
531 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
532 {:ok, activity} <- toggle_sensitive(activity, opts) do
533 set_visibility(activity, opts)
535 nil -> {:error, :not_found}
536 {:error, reason} -> {:error, reason}
540 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
541 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
544 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
545 when is_boolean(sensitive) do
546 new_data = Map.put(object.data, "sensitive", sensitive)
550 |> Object.change(%{data: new_data})
551 |> Object.update_and_set_cache()
553 {:ok, Map.put(activity, :object, object)}
556 defp toggle_sensitive(activity, _), do: {:ok, activity}
558 defp set_visibility(activity, %{visibility: visibility}) do
559 Utils.update_activity_visibility(activity, visibility)
562 defp set_visibility(activity, _), do: {:ok, activity}
564 def hide_reblogs(%User{} = user, %User{} = target) do
565 UserRelationship.create_reblog_mute(user, target)
568 def show_reblogs(%User{} = user, %User{} = target) do
569 UserRelationship.delete_reblog_mute(user, target)