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 post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
29 with :ok <- validate_chat_content_length(content),
30 maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
31 {_, {:ok, chat_message_data, _meta}} <-
36 content |> Formatter.html_escape("text/plain"),
37 attachment: maybe_attachment
39 {_, {:ok, create_activity_data, _meta}} <-
40 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
41 {_, {:ok, %Activity{} = activity, _meta}} <-
43 Pipeline.common_pipeline(create_activity_data,
50 defp validate_chat_content_length(content) do
51 if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
54 {:error, :content_too_long}
58 def unblock(blocker, blocked) do
59 with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked),
60 {:ok, unblock_data, _} <- Builder.undo(blocker, block),
61 {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
66 def follow(follower, followed) do
67 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
69 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
70 {:ok, activity} <- ActivityPub.follow(follower, followed),
71 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
72 {:ok, follower, followed, activity}
76 def unfollow(follower, unfollowed) do
77 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
78 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
79 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
84 def accept_follow_request(follower, followed) do
85 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
86 {:ok, follower} <- User.follow(follower, followed),
87 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
88 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
93 object: follow_activity.data["id"],
100 def reject_follow_request(follower, followed) do
101 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
102 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
103 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
104 {:ok, _notifications} <- Notification.dismiss(follow_activity),
106 ActivityPub.reject(%{
107 to: [follower.ap_id],
109 object: follow_activity.data["id"],
116 def delete(activity_id, user) do
117 with {_, %Activity{data: %{"object" => _}} = activity} <-
118 {:find_activity, Activity.get_by_id_with_object(activity_id)},
119 %Object{} = object <- Object.normalize(activity),
120 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
121 {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
122 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
125 {:find_activity, _} -> {:error, :not_found}
126 _ -> {:error, dgettext("errors", "Could not delete")}
130 def repeat(id, user, params \\ %{}) do
131 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
132 {:find_activity, Activity.get_by_id(id)},
133 object <- Object.normalize(activity),
134 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
135 public <- public_announce?(object, params) do
136 if announce_activity do
137 {:ok, announce_activity, object}
139 ActivityPub.announce(user, object, nil, true, public)
142 {:find_activity, _} -> {:error, :not_found}
143 _ -> {:error, dgettext("errors", "Could not repeat")}
147 def unrepeat(id, user) do
148 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
149 {:find_activity, Activity.get_by_id(id)},
150 %Object{} = note <- Object.normalize(activity, false),
151 %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
152 {:ok, undo, _} <- Builder.undo(user, announce),
153 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
156 {:find_activity, _} -> {:error, :not_found}
157 _ -> {:error, dgettext("errors", "Could not unrepeat")}
161 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
162 def favorite(%User{} = user, id) do
163 case favorite_helper(user, id) do
167 {:error, :not_found} = res ->
171 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
172 {:error, dgettext("errors", "Could not favorite")}
176 def favorite_helper(user, id) do
177 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
178 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
179 {_, {:ok, %Activity{} = activity, _meta}} <-
181 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
198 if {:object, {"already liked by this actor", []}} in changeset.errors do
199 {:ok, :already_liked}
209 def unfavorite(id, user) do
210 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
211 {:find_activity, Activity.get_by_id(id)},
212 %Object{} = note <- Object.normalize(activity, false),
213 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
214 {:ok, undo, _} <- Builder.undo(user, like),
215 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
218 {:find_activity, _} -> {:error, :not_found}
219 _ -> {:error, dgettext("errors", "Could not unfavorite")}
223 def react_with_emoji(id, user, emoji) do
224 with %Activity{} = activity <- Activity.get_by_id(id),
225 object <- Object.normalize(activity),
226 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
227 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
231 {:error, dgettext("errors", "Could not add reaction emoji")}
235 def unreact_with_emoji(id, user, emoji) do
236 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
237 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
238 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
242 {:error, dgettext("errors", "Could not remove reaction emoji")}
246 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
247 with :ok <- validate_not_author(object, user),
248 :ok <- validate_existing_votes(user, object),
249 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
251 Enum.map(choices, fn index ->
252 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
255 ActivityPub.create(%{
256 to: answer_data["to"],
258 context: object.data["context"],
260 additional: %{"cc" => answer_data["cc"]}
266 object = Object.get_cached_by_ap_id(object.data["id"])
267 {:ok, answer_activities, object}
271 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
272 do: {:error, dgettext("errors", "Poll's author can't vote")}
274 defp validate_not_author(_, _), do: :ok
276 defp validate_existing_votes(%{ap_id: ap_id}, object) do
277 if Utils.get_existing_votes(ap_id, object) == [] do
280 {:error, dgettext("errors", "Already voted")}
284 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
285 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
287 defp normalize_and_validate_choices(choices, object) do
288 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
289 {options, max_count} = get_options_and_max_count(object)
290 count = Enum.count(options)
292 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
293 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
294 {:ok, options, choices}
296 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
297 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
301 def public_announce?(_, %{"visibility" => visibility})
302 when visibility in ~w{public unlisted private direct},
303 do: visibility in ~w(public unlisted)
305 def public_announce?(object, _) do
306 Visibility.is_public?(object)
309 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
311 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
312 when visibility in ~w{public unlisted private direct},
313 do: {visibility, get_replied_to_visibility(in_reply_to)}
315 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
316 visibility = {:list, String.to_integer(list_id)}
317 {visibility, get_replied_to_visibility(in_reply_to)}
320 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
321 visibility = get_replied_to_visibility(in_reply_to)
322 {visibility, visibility}
325 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
327 def get_replied_to_visibility(nil), do: nil
329 def get_replied_to_visibility(activity) do
330 with %Object{} = object <- Object.normalize(activity) do
331 Visibility.get_visibility(object)
335 def check_expiry_date({:ok, nil} = res), do: res
337 def check_expiry_date({:ok, in_seconds}) do
338 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
340 if ActivityExpiration.expires_late_enough?(expiry) do
343 {:error, "Expiry date is too soon"}
347 def check_expiry_date(expiry_str) do
348 Ecto.Type.cast(:integer, expiry_str)
349 |> check_expiry_date()
352 def listen(user, %{"title" => _} = data) do
353 with visibility <- data["visibility"] || "public",
354 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
356 Map.take(data, ["album", "artist", "title", "length"])
357 |> Map.put("type", "Audio")
360 |> Map.put("actor", user.ap_id),
362 ActivityPub.listen(%{
366 context: Utils.generate_context_id(),
367 additional: %{"cc" => cc}
373 def post(user, %{"status" => _} = data) do
374 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
376 |> ActivityPub.create(draft.preview?)
377 |> maybe_create_activity_expiration(draft.expires_at)
381 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
382 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
387 defp maybe_create_activity_expiration(result, _), do: result
389 def pin(id, %{ap_id: user_ap_id} = user) do
392 data: %{"type" => "Create"},
393 object: %Object{data: %{"type" => object_type}}
394 } = activity <- Activity.get_by_id_with_object(id),
395 true <- object_type in ["Note", "Article", "Question"],
396 true <- Visibility.is_public?(activity),
397 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
400 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
401 _ -> {:error, dgettext("errors", "Could not pin")}
405 def unpin(id, user) do
406 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
407 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
410 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
411 _ -> {:error, dgettext("errors", "Could not unpin")}
415 def add_mute(user, activity) do
416 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
419 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
423 def remove_mute(user, activity) do
424 ThreadMute.remove_mute(user.id, activity.data["context"])
428 def thread_muted?(%{id: nil} = _user, _activity), do: false
430 def thread_muted?(user, activity) do
431 ThreadMute.exists?(user.id, activity.data["context"])
434 def report(user, data) do
435 with {:ok, account} <- get_reported_account(data.account_id),
436 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
437 {:ok, statuses} <- get_report_statuses(account, data) do
439 context: Utils.generate_context_id(),
443 content: content_html,
444 forward: Map.get(data, :forward, false)
449 defp get_reported_account(account_id) do
450 case User.get_cached_by_id(account_id) do
451 %User{} = account -> {:ok, account}
452 _ -> {:error, dgettext("errors", "Account not found")}
456 def update_report_state(activity_ids, state) when is_list(activity_ids) do
457 case Utils.update_report_state(activity_ids, state) do
458 :ok -> {:ok, activity_ids}
459 _ -> {:error, dgettext("errors", "Could not update state")}
463 def update_report_state(activity_id, state) do
464 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
465 Utils.update_report_state(activity, state)
467 nil -> {:error, :not_found}
468 _ -> {:error, dgettext("errors", "Could not update state")}
472 def update_activity_scope(activity_id, opts \\ %{}) do
473 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
474 {:ok, activity} <- toggle_sensitive(activity, opts) do
475 set_visibility(activity, opts)
477 nil -> {:error, :not_found}
478 {:error, reason} -> {:error, reason}
482 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
483 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
486 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
487 when is_boolean(sensitive) do
488 new_data = Map.put(object.data, "sensitive", sensitive)
492 |> Object.change(%{data: new_data})
493 |> Object.update_and_set_cache()
495 {:ok, Map.put(activity, :object, object)}
498 defp toggle_sensitive(activity, _), do: {:ok, activity}
500 defp set_visibility(activity, %{"visibility" => visibility}) do
501 Utils.update_activity_visibility(activity, visibility)
504 defp set_visibility(activity, _), do: {:ok, activity}
506 def hide_reblogs(%User{} = user, %User{} = target) do
507 UserRelationship.create_reblog_mute(user, target)
510 def show_reblogs(%User{} = user, %User{} = target) do
511 UserRelationship.delete_reblog_mute(user, target)