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}}), do: {any_of, Enum.count(any_of)}
344 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
346 defp normalize_and_validate_choices(choices, object) do
347 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
348 {options, max_count} = get_options_and_max_count(object)
349 count = Enum.count(options)
351 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
352 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
353 {:ok, options, choices}
355 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
356 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
360 def public_announce?(_, %{visibility: visibility})
361 when visibility in ~w{public unlisted private direct},
362 do: visibility in ~w(public unlisted)
364 def public_announce?(object, _) do
365 Visibility.is_public?(object)
368 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
370 def get_visibility(%{visibility: visibility}, in_reply_to, _)
371 when visibility in ~w{public unlisted private direct},
372 do: {visibility, get_replied_to_visibility(in_reply_to)}
374 def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
375 visibility = {:list, String.to_integer(list_id)}
376 {visibility, get_replied_to_visibility(in_reply_to)}
379 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
380 visibility = get_replied_to_visibility(in_reply_to)
381 {visibility, visibility}
384 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
386 def get_replied_to_visibility(nil), do: nil
388 def get_replied_to_visibility(activity) do
389 with %Object{} = object <- Object.normalize(activity) do
390 Visibility.get_visibility(object)
394 def check_expiry_date({:ok, nil} = res), do: res
396 def check_expiry_date({:ok, in_seconds}) do
397 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
399 if ActivityExpiration.expires_late_enough?(expiry) do
402 {:error, "Expiry date is too soon"}
406 def check_expiry_date(expiry_str) do
407 Ecto.Type.cast(:integer, expiry_str)
408 |> check_expiry_date()
411 def listen(user, data) do
412 visibility = Map.get(data, :visibility, "public")
414 with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
417 |> Map.take([:album, :artist, :title, :length])
418 |> Map.new(fn {key, value} -> {to_string(key), value} end)
419 |> Map.put("type", "Audio")
422 |> Map.put("actor", user.ap_id),
424 ActivityPub.listen(%{
428 context: Utils.generate_context_id(),
429 additional: %{"cc" => cc}
435 def post(user, %{status: _} = data) do
436 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
437 ActivityPub.create(draft.changes, draft.preview?)
441 def pin(id, %{ap_id: user_ap_id} = user) do
444 data: %{"type" => "Create"},
445 object: %Object{data: %{"type" => object_type}}
446 } = activity <- Activity.get_by_id_with_object(id),
447 true <- object_type in ["Note", "Article", "Question"],
448 true <- Visibility.is_public?(activity),
449 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
452 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
453 _ -> {:error, dgettext("errors", "Could not pin")}
457 def unpin(id, user) do
458 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
459 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
462 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
463 _ -> {:error, dgettext("errors", "Could not unpin")}
467 def add_mute(user, activity) do
468 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
471 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
475 def remove_mute(user, activity) do
476 ThreadMute.remove_mute(user.id, activity.data["context"])
480 def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
481 when is_binary("context") do
482 ThreadMute.exists?(user_id, context)
485 def thread_muted?(_, _), do: false
487 def report(user, data) do
488 with {:ok, account} <- get_reported_account(data.account_id),
489 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
490 {:ok, statuses} <- get_report_statuses(account, data) do
492 context: Utils.generate_context_id(),
496 content: content_html,
497 forward: Map.get(data, :forward, false)
502 defp get_reported_account(account_id) do
503 case User.get_cached_by_id(account_id) do
504 %User{} = account -> {:ok, account}
505 _ -> {:error, dgettext("errors", "Account not found")}
509 def update_report_state(activity_ids, state) when is_list(activity_ids) do
510 case Utils.update_report_state(activity_ids, state) do
511 :ok -> {:ok, activity_ids}
512 _ -> {:error, dgettext("errors", "Could not update state")}
516 def update_report_state(activity_id, state) do
517 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
518 Utils.update_report_state(activity, state)
520 nil -> {:error, :not_found}
521 _ -> {:error, dgettext("errors", "Could not update state")}
525 def update_activity_scope(activity_id, opts \\ %{}) do
526 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
527 {:ok, activity} <- toggle_sensitive(activity, opts) do
528 set_visibility(activity, opts)
530 nil -> {:error, :not_found}
531 {:error, reason} -> {:error, reason}
535 defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
536 toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
539 defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
540 when is_boolean(sensitive) do
541 new_data = Map.put(object.data, "sensitive", sensitive)
545 |> Object.change(%{data: new_data})
546 |> Object.update_and_set_cache()
548 {:ok, Map.put(activity, :object, object)}
551 defp toggle_sensitive(activity, _), do: {:ok, activity}
553 defp set_visibility(activity, %{visibility: visibility}) do
554 Utils.update_activity_visibility(activity, visibility)
557 defp set_visibility(activity, _), do: {:ok, activity}
559 def hide_reblogs(%User{} = user, %User{} = target) do
560 UserRelationship.create_reblog_mute(user, target)
563 def show_reblogs(%User{} = user, %User{} = target) do
564 UserRelationship.delete_reblog_mute(user, target)