1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 nil <- Utils.get_existing_announce(user.ap_id, object),
89 public <- public_announce?(object, params) do
90 ActivityPub.announce(user, object, nil, true, public)
92 _ -> {:error, dgettext("errors", "Could not repeat")}
96 def unrepeat(id_or_ap_id, user) do
97 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
98 object = Object.normalize(activity)
99 ActivityPub.unannounce(user, object)
101 _ -> {:error, dgettext("errors", "Could not unrepeat")}
105 def favorite(id_or_ap_id, user) do
106 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
107 object <- Object.normalize(activity),
108 nil <- Utils.get_existing_like(user.ap_id, object) do
109 ActivityPub.like(user, object)
111 _ -> {:error, dgettext("errors", "Could not favorite")}
115 def unfavorite(id_or_ap_id, user) do
116 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
117 object = Object.normalize(activity)
118 ActivityPub.unlike(user, object)
120 _ -> {:error, dgettext("errors", "Could not unfavorite")}
124 def react_with_emoji(id, user, emoji) do
125 with %Activity{} = activity <- Activity.get_by_id(id),
126 object <- Object.normalize(activity) do
127 ActivityPub.react_with_emoji(user, object, emoji)
130 {:error, dgettext("errors", "Could not add reaction emoji")}
134 def unreact_with_emoji(id, user, emoji) do
135 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
136 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
139 {:error, dgettext("errors", "Could not remove reaction emoji")}
143 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
144 with :ok <- validate_not_author(object, user),
145 :ok <- validate_existing_votes(user, object),
146 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
148 Enum.map(choices, fn index ->
149 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
152 ActivityPub.create(%{
153 to: answer_data["to"],
155 context: object.data["context"],
157 additional: %{"cc" => answer_data["cc"]}
163 object = Object.get_cached_by_ap_id(object.data["id"])
164 {:ok, answer_activities, object}
168 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
169 do: {:error, dgettext("errors", "Poll's author can't vote")}
171 defp validate_not_author(_, _), do: :ok
173 defp validate_existing_votes(%{ap_id: ap_id}, object) do
174 if Utils.get_existing_votes(ap_id, object) == [] do
177 {:error, dgettext("errors", "Already voted")}
181 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
182 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
184 defp normalize_and_validate_choices(choices, object) do
185 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
186 {options, max_count} = get_options_and_max_count(object)
187 count = Enum.count(options)
189 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
190 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
191 {:ok, options, choices}
193 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
194 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
198 def public_announce?(_, %{"visibility" => visibility})
199 when visibility in ~w{public unlisted private direct},
200 do: visibility in ~w(public unlisted)
202 def public_announce?(object, _) do
203 Visibility.is_public?(object)
206 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
208 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
209 when visibility in ~w{public unlisted private direct},
210 do: {visibility, get_replied_to_visibility(in_reply_to)}
212 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
213 visibility = {:list, String.to_integer(list_id)}
214 {visibility, get_replied_to_visibility(in_reply_to)}
217 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
218 visibility = get_replied_to_visibility(in_reply_to)
219 {visibility, visibility}
222 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
224 def get_replied_to_visibility(nil), do: nil
226 def get_replied_to_visibility(activity) do
227 with %Object{} = object <- Object.normalize(activity) do
228 Visibility.get_visibility(object)
232 def check_expiry_date({:ok, nil} = res), do: res
234 def check_expiry_date({:ok, in_seconds}) do
235 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
237 if ActivityExpiration.expires_late_enough?(expiry) do
240 {:error, "Expiry date is too soon"}
244 def check_expiry_date(expiry_str) do
245 Ecto.Type.cast(:integer, expiry_str)
246 |> check_expiry_date()
249 def listen(user, %{"title" => _} = data) do
250 with visibility <- data["visibility"] || "public",
251 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
253 Map.take(data, ["album", "artist", "title", "length"])
254 |> Map.put("type", "Audio")
257 |> Map.put("actor", user.ap_id),
259 ActivityPub.listen(%{
263 context: Utils.generate_context_id(),
264 additional: %{"cc" => cc}
270 def post(user, %{"status" => _} = data) do
271 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
273 |> ActivityPub.create(draft.preview?)
274 |> maybe_create_activity_expiration(draft.expires_at)
278 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
279 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
284 defp maybe_create_activity_expiration(result, _), do: result
286 # Updates the emojis for a user based on their profile
288 emoji = emoji_from_profile(user)
289 source_data = Map.put(user.source_data, "tag", emoji)
292 case User.update_source_data(user, source_data) do
297 ActivityPub.update(%{
299 to: [Pleroma.Constants.as_public(), user.follower_address],
302 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
306 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
309 data: %{"type" => "Create"},
310 object: %Object{data: %{"type" => "Note"}}
311 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
312 true <- Visibility.is_public?(activity),
313 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
316 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
317 _ -> {:error, dgettext("errors", "Could not pin")}
321 def unpin(id_or_ap_id, user) do
322 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
323 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
326 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
327 _ -> {:error, dgettext("errors", "Could not unpin")}
331 def add_mute(user, activity) do
332 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
335 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
339 def remove_mute(user, activity) do
340 ThreadMute.remove_mute(user.id, activity.data["context"])
344 def thread_muted?(%{id: nil} = _user, _activity), do: false
346 def thread_muted?(user, activity) do
347 ThreadMute.check_muted(user.id, activity.data["context"]) != []
350 def report(user, %{"account_id" => account_id} = data) do
351 with {:ok, account} <- get_reported_account(account_id),
352 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
353 {:ok, statuses} <- get_report_statuses(account, data) do
355 context: Utils.generate_context_id(),
359 content: content_html,
360 forward: data["forward"] || false
365 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
367 defp get_reported_account(account_id) do
368 case User.get_cached_by_id(account_id) do
369 %User{} = account -> {:ok, account}
370 _ -> {:error, dgettext("errors", "Account not found")}
374 def update_report_state(activity_ids, state) when is_list(activity_ids) do
375 case Utils.update_report_state(activity_ids, state) do
376 :ok -> {:ok, activity_ids}
377 _ -> {:error, dgettext("errors", "Could not update state")}
381 def update_report_state(activity_id, state) do
382 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
383 Utils.update_report_state(activity, state)
385 nil -> {:error, :not_found}
386 _ -> {:error, dgettext("errors", "Could not update state")}
390 def update_activity_scope(activity_id, opts \\ %{}) do
391 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
392 {:ok, activity} <- toggle_sensitive(activity, opts) do
393 set_visibility(activity, opts)
395 nil -> {:error, :not_found}
396 {:error, reason} -> {:error, reason}
400 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
401 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
404 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
405 when is_boolean(sensitive) do
406 new_data = Map.put(object.data, "sensitive", sensitive)
410 |> Object.change(%{data: new_data})
411 |> Object.update_and_set_cache()
413 {:ok, Map.put(activity, :object, object)}
416 defp toggle_sensitive(activity, _), do: {:ok, activity}
418 defp set_visibility(activity, %{"visibility" => visibility}) do
419 Utils.update_activity_visibility(activity, visibility)
422 defp set_visibility(activity, _), do: {:ok, activity}
424 def hide_reblogs(%User{} = user, %User{} = target) do
425 UserRelationship.create_reblog_mute(user, target)
428 def show_reblogs(%User{} = user, %User{} = target) do
429 UserRelationship.delete_reblog_mute(user, target)