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
12 alias Pleroma.ThreadMute
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
24 require Pleroma.Constants
27 def post_chat_message(%User{} = user, %User{} = recipient, content) do
30 String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])},
31 {_, {:ok, chat_message_data, _meta}} <-
36 content |> Formatter.html_escape("text/plain")
38 {_, {:ok, create_activity_data, _meta}} <-
39 {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
40 {_, {:ok, %Activity{} = activity, _meta}} <-
42 Pipeline.common_pipeline(create_activity_data,
47 {:content_length, false} -> {:error, :content_too_long}
52 def follow(follower, followed) do
53 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
55 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
56 {:ok, activity} <- ActivityPub.follow(follower, followed),
57 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
58 {:ok, follower, followed, activity}
62 def unfollow(follower, unfollowed) do
63 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
64 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
65 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
70 def accept_follow_request(follower, followed) do
71 with {:ok, follower} <- User.follow(follower, followed),
72 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
73 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
74 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
79 object: follow_activity.data["id"],
86 def reject_follow_request(follower, followed) do
87 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
88 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
89 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
94 object: follow_activity.data["id"],
101 def delete(activity_id, user) do
102 with {_, %Activity{data: %{"object" => _}} = activity} <-
103 {:find_activity, Activity.get_by_id_with_object(activity_id)},
104 %Object{} = object <- Object.normalize(activity),
105 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
106 {:ok, _} <- unpin(activity_id, user),
107 {:ok, delete} <- ActivityPub.delete(object) do
110 {:find_activity, _} -> {:error, :not_found}
111 _ -> {:error, dgettext("errors", "Could not delete")}
115 def repeat(id, user, params \\ %{}) do
116 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
117 {:find_activity, Activity.get_by_id(id)},
118 object <- Object.normalize(activity),
119 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
120 public <- public_announce?(object, params) do
121 if announce_activity do
122 {:ok, announce_activity, object}
124 ActivityPub.announce(user, object, nil, true, public)
127 {:find_activity, _} -> {:error, :not_found}
128 _ -> {:error, dgettext("errors", "Could not repeat")}
132 def unrepeat(id, user) do
133 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
134 {:find_activity, Activity.get_by_id(id)} do
135 object = Object.normalize(activity)
136 ActivityPub.unannounce(user, object)
138 {:find_activity, _} -> {:error, :not_found}
139 _ -> {:error, dgettext("errors", "Could not unrepeat")}
143 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
144 def favorite(%User{} = user, id) do
145 case favorite_helper(user, id) do
149 {:error, :not_found} = res ->
153 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
154 {:error, dgettext("errors", "Could not favorite")}
158 def favorite_helper(user, id) do
159 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
160 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
161 {_, {:ok, %Activity{} = activity, _meta}} <-
163 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
180 if {:object, {"already liked by this actor", []}} in changeset.errors do
181 {:ok, :already_liked}
191 def unfavorite(id, user) do
192 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
193 {:find_activity, Activity.get_by_id(id)} do
194 object = Object.normalize(activity)
195 ActivityPub.unlike(user, object)
197 {:find_activity, _} -> {:error, :not_found}
198 _ -> {:error, dgettext("errors", "Could not unfavorite")}
202 def react_with_emoji(id, user, emoji) do
203 with %Activity{} = activity <- Activity.get_by_id(id),
204 object <- Object.normalize(activity) do
205 ActivityPub.react_with_emoji(user, object, emoji)
208 {:error, dgettext("errors", "Could not add reaction emoji")}
212 def unreact_with_emoji(id, user, emoji) do
213 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
214 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
217 {:error, dgettext("errors", "Could not remove reaction emoji")}
221 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
222 with :ok <- validate_not_author(object, user),
223 :ok <- validate_existing_votes(user, object),
224 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
226 Enum.map(choices, fn index ->
227 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
230 ActivityPub.create(%{
231 to: answer_data["to"],
233 context: object.data["context"],
235 additional: %{"cc" => answer_data["cc"]}
241 object = Object.get_cached_by_ap_id(object.data["id"])
242 {:ok, answer_activities, object}
246 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
247 do: {:error, dgettext("errors", "Poll's author can't vote")}
249 defp validate_not_author(_, _), do: :ok
251 defp validate_existing_votes(%{ap_id: ap_id}, object) do
252 if Utils.get_existing_votes(ap_id, object) == [] do
255 {:error, dgettext("errors", "Already voted")}
259 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
260 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
262 defp normalize_and_validate_choices(choices, object) do
263 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
264 {options, max_count} = get_options_and_max_count(object)
265 count = Enum.count(options)
267 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
268 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
269 {:ok, options, choices}
271 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
272 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
276 def public_announce?(_, %{"visibility" => visibility})
277 when visibility in ~w{public unlisted private direct},
278 do: visibility in ~w(public unlisted)
280 def public_announce?(object, _) do
281 Visibility.is_public?(object)
284 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
286 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
287 when visibility in ~w{public unlisted private direct},
288 do: {visibility, get_replied_to_visibility(in_reply_to)}
290 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
291 visibility = {:list, String.to_integer(list_id)}
292 {visibility, get_replied_to_visibility(in_reply_to)}
295 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
296 visibility = get_replied_to_visibility(in_reply_to)
297 {visibility, visibility}
300 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
302 def get_replied_to_visibility(nil), do: nil
304 def get_replied_to_visibility(activity) do
305 with %Object{} = object <- Object.normalize(activity) do
306 Visibility.get_visibility(object)
310 def check_expiry_date({:ok, nil} = res), do: res
312 def check_expiry_date({:ok, in_seconds}) do
313 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
315 if ActivityExpiration.expires_late_enough?(expiry) do
318 {:error, "Expiry date is too soon"}
322 def check_expiry_date(expiry_str) do
323 Ecto.Type.cast(:integer, expiry_str)
324 |> check_expiry_date()
327 def listen(user, %{"title" => _} = data) do
328 with visibility <- data["visibility"] || "public",
329 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
331 Map.take(data, ["album", "artist", "title", "length"])
332 |> Map.put("type", "Audio")
335 |> Map.put("actor", user.ap_id),
337 ActivityPub.listen(%{
341 context: Utils.generate_context_id(),
342 additional: %{"cc" => cc}
348 def post(user, %{"status" => _} = data) do
349 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
351 |> ActivityPub.create(draft.preview?)
352 |> maybe_create_activity_expiration(draft.expires_at)
356 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
357 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
362 defp maybe_create_activity_expiration(result, _), do: result
364 def pin(id, %{ap_id: user_ap_id} = user) do
367 data: %{"type" => "Create"},
368 object: %Object{data: %{"type" => object_type}}
369 } = activity <- Activity.get_by_id_with_object(id),
370 true <- object_type in ["Note", "Article", "Question"],
371 true <- Visibility.is_public?(activity),
372 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
375 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
376 _ -> {:error, dgettext("errors", "Could not pin")}
380 def unpin(id, user) do
381 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
382 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
385 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
386 _ -> {:error, dgettext("errors", "Could not unpin")}
390 def add_mute(user, activity) do
391 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
394 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
398 def remove_mute(user, activity) do
399 ThreadMute.remove_mute(user.id, activity.data["context"])
403 def thread_muted?(%{id: nil} = _user, _activity), do: false
405 def thread_muted?(user, activity) do
406 ThreadMute.exists?(user.id, activity.data["context"])
409 def report(user, %{"account_id" => account_id} = data) do
410 with {:ok, account} <- get_reported_account(account_id),
411 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
412 {:ok, statuses} <- get_report_statuses(account, data) do
414 context: Utils.generate_context_id(),
418 content: content_html,
419 forward: data["forward"] || false
424 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
426 defp get_reported_account(account_id) do
427 case User.get_cached_by_id(account_id) do
428 %User{} = account -> {:ok, account}
429 _ -> {:error, dgettext("errors", "Account not found")}
433 def update_report_state(activity_ids, state) when is_list(activity_ids) do
434 case Utils.update_report_state(activity_ids, state) do
435 :ok -> {:ok, activity_ids}
436 _ -> {:error, dgettext("errors", "Could not update state")}
440 def update_report_state(activity_id, state) do
441 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
442 Utils.update_report_state(activity, state)
444 nil -> {:error, :not_found}
445 _ -> {:error, dgettext("errors", "Could not update state")}
449 def update_activity_scope(activity_id, opts \\ %{}) do
450 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
451 {:ok, activity} <- toggle_sensitive(activity, opts) do
452 set_visibility(activity, opts)
454 nil -> {:error, :not_found}
455 {:error, reason} -> {:error, reason}
459 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
460 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
463 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
464 when is_boolean(sensitive) do
465 new_data = Map.put(object.data, "sensitive", sensitive)
469 |> Object.change(%{data: new_data})
470 |> Object.update_and_set_cache()
472 {:ok, Map.put(activity, :object, object)}
475 defp toggle_sensitive(activity, _), do: {:ok, activity}
477 defp set_visibility(activity, %{"visibility" => visibility}) do
478 Utils.update_activity_visibility(activity, visibility)
481 defp set_visibility(activity, _), do: {:ok, activity}
483 def hide_reblogs(%User{} = user, %User{} = target) do
484 UserRelationship.create_reblog_mute(user, target)
487 def show_reblogs(%User{} = user, %User{} = target) do
488 UserRelationship.delete_reblog_mute(user, target)