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
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) do
30 Repo.transaction(fn ->
33 String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])},
34 {_, {:ok, chat_message_data, _meta}} <-
39 content |> Formatter.html_escape("text/plain")
41 {_, {:ok, chat_message_object}} <-
42 {:create_object, Object.create(chat_message_data)},
43 {_, {:ok, create_activity_data, _meta}} <-
44 {:build_create_activity,
45 Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])},
46 {_, {:ok, %Activity{} = activity, _meta}} <-
47 {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do
50 {:content_length, false} -> {:error, :content_too_long}
61 def follow(follower, followed) do
62 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
64 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
65 {:ok, activity} <- ActivityPub.follow(follower, followed),
66 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
67 {:ok, follower, followed, activity}
71 def unfollow(follower, unfollowed) do
72 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
73 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
74 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
79 def accept_follow_request(follower, followed) do
80 with {:ok, follower} <- User.follow(follower, followed),
81 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
82 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
83 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
88 object: follow_activity.data["id"],
95 def reject_follow_request(follower, followed) do
96 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
97 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
98 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
100 ActivityPub.reject(%{
101 to: [follower.ap_id],
103 object: follow_activity.data["id"],
110 def delete(activity_id, user) do
111 with {_, %Activity{data: %{"object" => _}} = activity} <-
112 {:find_activity, Activity.get_by_id_with_object(activity_id)},
113 %Object{} = object <- Object.normalize(activity),
114 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
115 {:ok, _} <- unpin(activity_id, user),
116 {:ok, delete} <- ActivityPub.delete(object) do
119 {:find_activity, _} -> {:error, :not_found}
120 _ -> {:error, dgettext("errors", "Could not delete")}
124 def repeat(id, user, params \\ %{}) do
125 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
126 {:find_activity, Activity.get_by_id(id)},
127 object <- Object.normalize(activity),
128 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
129 public <- public_announce?(object, params) do
130 if announce_activity do
131 {:ok, announce_activity, object}
133 ActivityPub.announce(user, object, nil, true, public)
136 {:find_activity, _} -> {:error, :not_found}
137 _ -> {:error, dgettext("errors", "Could not repeat")}
141 def unrepeat(id, user) do
142 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
143 {:find_activity, Activity.get_by_id(id)} do
144 object = Object.normalize(activity)
145 ActivityPub.unannounce(user, object)
147 {:find_activity, _} -> {:error, :not_found}
148 _ -> {:error, dgettext("errors", "Could not unrepeat")}
152 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
153 def favorite(%User{} = user, id) do
154 case favorite_helper(user, id) do
158 {:error, :not_found} = res ->
162 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
163 {:error, dgettext("errors", "Could not favorite")}
167 def favorite_helper(user, id) do
168 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
169 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
170 {_, {:ok, %Activity{} = activity, _meta}} <-
172 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
189 if {:object, {"already liked by this actor", []}} in changeset.errors do
190 {:ok, :already_liked}
200 def unfavorite(id, user) do
201 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
202 {:find_activity, Activity.get_by_id(id)} do
203 object = Object.normalize(activity)
204 ActivityPub.unlike(user, object)
206 {:find_activity, _} -> {:error, :not_found}
207 _ -> {:error, dgettext("errors", "Could not unfavorite")}
211 def react_with_emoji(id, user, emoji) do
212 with %Activity{} = activity <- Activity.get_by_id(id),
213 object <- Object.normalize(activity) do
214 ActivityPub.react_with_emoji(user, object, emoji)
217 {:error, dgettext("errors", "Could not add reaction emoji")}
221 def unreact_with_emoji(id, user, emoji) do
222 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
223 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
226 {:error, dgettext("errors", "Could not remove reaction emoji")}
230 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
231 with :ok <- validate_not_author(object, user),
232 :ok <- validate_existing_votes(user, object),
233 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
235 Enum.map(choices, fn index ->
236 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
239 ActivityPub.create(%{
240 to: answer_data["to"],
242 context: object.data["context"],
244 additional: %{"cc" => answer_data["cc"]}
250 object = Object.get_cached_by_ap_id(object.data["id"])
251 {:ok, answer_activities, object}
255 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
256 do: {:error, dgettext("errors", "Poll's author can't vote")}
258 defp validate_not_author(_, _), do: :ok
260 defp validate_existing_votes(%{ap_id: ap_id}, object) do
261 if Utils.get_existing_votes(ap_id, object) == [] do
264 {:error, dgettext("errors", "Already voted")}
268 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
269 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
271 defp normalize_and_validate_choices(choices, object) do
272 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
273 {options, max_count} = get_options_and_max_count(object)
274 count = Enum.count(options)
276 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
277 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
278 {:ok, options, choices}
280 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
281 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
285 def public_announce?(_, %{"visibility" => visibility})
286 when visibility in ~w{public unlisted private direct},
287 do: visibility in ~w(public unlisted)
289 def public_announce?(object, _) do
290 Visibility.is_public?(object)
293 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
295 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
296 when visibility in ~w{public unlisted private direct},
297 do: {visibility, get_replied_to_visibility(in_reply_to)}
299 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
300 visibility = {:list, String.to_integer(list_id)}
301 {visibility, get_replied_to_visibility(in_reply_to)}
304 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
305 visibility = get_replied_to_visibility(in_reply_to)
306 {visibility, visibility}
309 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
311 def get_replied_to_visibility(nil), do: nil
313 def get_replied_to_visibility(activity) do
314 with %Object{} = object <- Object.normalize(activity) do
315 Visibility.get_visibility(object)
319 def check_expiry_date({:ok, nil} = res), do: res
321 def check_expiry_date({:ok, in_seconds}) do
322 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
324 if ActivityExpiration.expires_late_enough?(expiry) do
327 {:error, "Expiry date is too soon"}
331 def check_expiry_date(expiry_str) do
332 Ecto.Type.cast(:integer, expiry_str)
333 |> check_expiry_date()
336 def listen(user, %{"title" => _} = data) do
337 with visibility <- data["visibility"] || "public",
338 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
340 Map.take(data, ["album", "artist", "title", "length"])
341 |> Map.put("type", "Audio")
344 |> Map.put("actor", user.ap_id),
346 ActivityPub.listen(%{
350 context: Utils.generate_context_id(),
351 additional: %{"cc" => cc}
357 def post(user, %{"status" => _} = data) do
358 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
360 |> ActivityPub.create(draft.preview?)
361 |> maybe_create_activity_expiration(draft.expires_at)
365 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
366 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
371 defp maybe_create_activity_expiration(result, _), do: result
373 def pin(id, %{ap_id: user_ap_id} = user) do
376 data: %{"type" => "Create"},
377 object: %Object{data: %{"type" => object_type}}
378 } = activity <- Activity.get_by_id_with_object(id),
379 true <- object_type in ["Note", "Article", "Question"],
380 true <- Visibility.is_public?(activity),
381 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
384 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
385 _ -> {:error, dgettext("errors", "Could not pin")}
389 def unpin(id, user) do
390 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
391 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
394 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
395 _ -> {:error, dgettext("errors", "Could not unpin")}
399 def add_mute(user, activity) do
400 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
403 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
407 def remove_mute(user, activity) do
408 ThreadMute.remove_mute(user.id, activity.data["context"])
412 def thread_muted?(%{id: nil} = _user, _activity), do: false
414 def thread_muted?(user, activity) do
415 ThreadMute.exists?(user.id, activity.data["context"])
418 def report(user, %{"account_id" => account_id} = data) do
419 with {:ok, account} <- get_reported_account(account_id),
420 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
421 {:ok, statuses} <- get_report_statuses(account, data) do
423 context: Utils.generate_context_id(),
427 content: content_html,
428 forward: data["forward"] || false
433 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
435 defp get_reported_account(account_id) do
436 case User.get_cached_by_id(account_id) do
437 %User{} = account -> {:ok, account}
438 _ -> {:error, dgettext("errors", "Account not found")}
442 def update_report_state(activity_ids, state) when is_list(activity_ids) do
443 case Utils.update_report_state(activity_ids, state) do
444 :ok -> {:ok, activity_ids}
445 _ -> {:error, dgettext("errors", "Could not update state")}
449 def update_report_state(activity_id, state) do
450 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
451 Utils.update_report_state(activity, state)
453 nil -> {:error, :not_found}
454 _ -> {:error, dgettext("errors", "Could not update state")}
458 def update_activity_scope(activity_id, opts \\ %{}) do
459 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
460 {:ok, activity} <- toggle_sensitive(activity, opts) do
461 set_visibility(activity, opts)
463 nil -> {:error, :not_found}
464 {:error, reason} -> {:error, reason}
468 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
469 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
472 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
473 when is_boolean(sensitive) do
474 new_data = Map.put(object.data, "sensitive", sensitive)
478 |> Object.change(%{data: new_data})
479 |> Object.update_and_set_cache()
481 {:ok, Map.put(activity, :object, object)}
484 defp toggle_sensitive(activity, _), do: {:ok, activity}
486 defp set_visibility(activity, %{"visibility" => visibility}) do
487 Utils.update_activity_visibility(activity, visibility)
490 defp set_visibility(activity, _), do: {:ok, activity}
492 def hide_reblogs(%User{} = user, %User{} = target) do
493 UserRelationship.create_reblog_mute(user, target)
496 def show_reblogs(%User{} = user, %User{} = target) do
497 UserRelationship.delete_reblog_mute(user, target)