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
11 alias Pleroma.ThreadMute
13 alias Pleroma.UserRelationship
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
18 import Pleroma.Web.Gettext
19 import Pleroma.Web.CommonAPI.Utils
21 require Pleroma.Constants
23 def follow(follower, followed) do
24 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
26 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
27 {:ok, activity} <- ActivityPub.follow(follower, followed),
28 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
29 {:ok, follower, followed, activity}
33 def unfollow(follower, unfollowed) do
34 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
35 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
36 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
41 def accept_follow_request(follower, followed) do
42 with {:ok, follower} <- User.follow(follower, followed),
43 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
44 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
45 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
50 object: follow_activity.data["id"],
57 def reject_follow_request(follower, followed) do
58 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
59 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
60 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
65 object: follow_activity.data["id"],
72 def delete(activity_id, user) do
73 with %Activity{data: %{"object" => _}} = activity <-
74 Activity.get_by_id_with_object(activity_id),
75 %Object{} = object <- Object.normalize(activity),
76 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
77 {:ok, _} <- unpin(activity_id, user),
78 {:ok, delete} <- ActivityPub.delete(object) do
81 _ -> {:error, dgettext("errors", "Could not delete")}
85 def repeat(id_or_ap_id, user, params \\ %{}) do
86 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
87 object <- Object.normalize(activity),
88 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
89 public <- public_announce?(object, params) do
90 if announce_activity do
91 {:ok, announce_activity, object}
93 ActivityPub.announce(user, object, nil, true, public)
96 _ -> {:error, dgettext("errors", "Could not repeat")}
100 def unrepeat(id_or_ap_id, user) do
101 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
102 object = Object.normalize(activity)
103 ActivityPub.unannounce(user, object)
105 _ -> {:error, dgettext("errors", "Could not unrepeat")}
109 def favorite(id_or_ap_id, user) do
110 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
111 object <- Object.normalize(activity),
112 like_activity <- Utils.get_existing_like(user.ap_id, object) do
114 {:ok, like_activity, object}
116 ActivityPub.like(user, object)
119 _ -> {:error, dgettext("errors", "Could not favorite")}
123 def unfavorite(id_or_ap_id, user) do
124 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
125 object = Object.normalize(activity)
126 ActivityPub.unlike(user, object)
128 _ -> {:error, dgettext("errors", "Could not unfavorite")}
132 def react_with_emoji(id, user, emoji) do
133 with %Activity{} = activity <- Activity.get_by_id(id),
134 object <- Object.normalize(activity) do
135 ActivityPub.react_with_emoji(user, object, emoji)
138 {:error, dgettext("errors", "Could not add reaction emoji")}
142 def unreact_with_emoji(id, user, emoji) do
143 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
144 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
147 {:error, dgettext("errors", "Could not remove reaction emoji")}
151 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
152 with :ok <- validate_not_author(object, user),
153 :ok <- validate_existing_votes(user, object),
154 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
156 Enum.map(choices, fn index ->
157 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
160 ActivityPub.create(%{
161 to: answer_data["to"],
163 context: object.data["context"],
165 additional: %{"cc" => answer_data["cc"]}
171 object = Object.get_cached_by_ap_id(object.data["id"])
172 {:ok, answer_activities, object}
176 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
177 do: {:error, dgettext("errors", "Poll's author can't vote")}
179 defp validate_not_author(_, _), do: :ok
181 defp validate_existing_votes(%{ap_id: ap_id}, object) do
182 if Utils.get_existing_votes(ap_id, object) == [] do
185 {:error, dgettext("errors", "Already voted")}
189 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
190 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
192 defp normalize_and_validate_choices(choices, object) do
193 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
194 {options, max_count} = get_options_and_max_count(object)
195 count = Enum.count(options)
197 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
198 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
199 {:ok, options, choices}
201 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
202 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
206 def public_announce?(_, %{"visibility" => visibility})
207 when visibility in ~w{public unlisted private direct},
208 do: visibility in ~w(public unlisted)
210 def public_announce?(object, _) do
211 Visibility.is_public?(object)
214 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
216 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
217 when visibility in ~w{public unlisted private direct},
218 do: {visibility, get_replied_to_visibility(in_reply_to)}
220 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
221 visibility = {:list, String.to_integer(list_id)}
222 {visibility, get_replied_to_visibility(in_reply_to)}
225 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
226 visibility = get_replied_to_visibility(in_reply_to)
227 {visibility, visibility}
230 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
232 def get_replied_to_visibility(nil), do: nil
234 def get_replied_to_visibility(activity) do
235 with %Object{} = object <- Object.normalize(activity) do
236 Visibility.get_visibility(object)
240 def check_expiry_date({:ok, nil} = res), do: res
242 def check_expiry_date({:ok, in_seconds}) do
243 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
245 if ActivityExpiration.expires_late_enough?(expiry) do
248 {:error, "Expiry date is too soon"}
252 def check_expiry_date(expiry_str) do
253 Ecto.Type.cast(:integer, expiry_str)
254 |> check_expiry_date()
257 def listen(user, %{"title" => _} = data) do
258 with visibility <- data["visibility"] || "public",
259 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
261 Map.take(data, ["album", "artist", "title", "length"])
262 |> Map.put("type", "Audio")
265 |> Map.put("actor", user.ap_id),
267 ActivityPub.listen(%{
271 context: Utils.generate_context_id(),
272 additional: %{"cc" => cc}
278 def post(user, %{"status" => _} = data) do
279 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
281 |> ActivityPub.create(draft.preview?)
282 |> maybe_create_activity_expiration(draft.expires_at)
286 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
287 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
292 defp maybe_create_activity_expiration(result, _), do: result
294 # Updates the emojis for a user based on their profile
296 emoji = emoji_from_profile(user)
297 source_data = Map.put(user.source_data, "tag", emoji)
300 case User.update_source_data(user, source_data) do
305 ActivityPub.update(%{
307 to: [Pleroma.Constants.as_public(), user.follower_address],
310 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
314 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
317 data: %{"type" => "Create"},
318 object: %Object{data: %{"type" => object_type}}
319 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
320 true <- object_type in ["Note", "Article", "Question"],
321 true <- Visibility.is_public?(activity),
322 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
325 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
326 _ -> {:error, dgettext("errors", "Could not pin")}
330 def unpin(id_or_ap_id, user) do
331 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
332 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
335 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
336 _ -> {:error, dgettext("errors", "Could not unpin")}
340 def add_mute(user, activity) do
341 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
344 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
348 def remove_mute(user, activity) do
349 ThreadMute.remove_mute(user.id, activity.data["context"])
353 def thread_muted?(%{id: nil} = _user, _activity), do: false
355 def thread_muted?(user, activity) do
356 ThreadMute.check_muted(user.id, activity.data["context"]) != []
359 def report(user, %{"account_id" => account_id} = data) do
360 with {:ok, account} <- get_reported_account(account_id),
361 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
362 {:ok, statuses} <- get_report_statuses(account, data) do
364 context: Utils.generate_context_id(),
368 content: content_html,
369 forward: data["forward"] || false
374 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
376 defp get_reported_account(account_id) do
377 case User.get_cached_by_id(account_id) do
378 %User{} = account -> {:ok, account}
379 _ -> {:error, dgettext("errors", "Account not found")}
383 def update_report_state(activity_ids, state) when is_list(activity_ids) do
384 case Utils.update_report_state(activity_ids, state) do
385 :ok -> {:ok, activity_ids}
386 _ -> {:error, dgettext("errors", "Could not update state")}
390 def update_report_state(activity_id, state) do
391 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
392 Utils.update_report_state(activity, state)
394 nil -> {:error, :not_found}
395 _ -> {:error, dgettext("errors", "Could not update state")}
399 def update_activity_scope(activity_id, opts \\ %{}) do
400 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
401 {:ok, activity} <- toggle_sensitive(activity, opts) do
402 set_visibility(activity, opts)
404 nil -> {:error, :not_found}
405 {:error, reason} -> {:error, reason}
409 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
410 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
413 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
414 when is_boolean(sensitive) do
415 new_data = Map.put(object.data, "sensitive", sensitive)
419 |> Object.change(%{data: new_data})
420 |> Object.update_and_set_cache()
422 {:ok, Map.put(activity, :object, object)}
425 defp toggle_sensitive(activity, _), do: {:ok, activity}
427 defp set_visibility(activity, %{"visibility" => visibility}) do
428 Utils.update_activity_visibility(activity, visibility)
431 defp set_visibility(activity, _), do: {:ok, activity}
433 def hide_reblogs(%User{} = user, %User{} = target) do
434 UserRelationship.create_reblog_mute(user, target)
437 def show_reblogs(%User{} = user, %User{} = target) do
438 UserRelationship.delete_reblog_mute(user, target)