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 {:find_activity, 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 {:find_activity, _} -> {:error, :not_found}
82 _ -> {:error, dgettext("errors", "Could not delete")}
86 def repeat(id_or_ap_id, user, params \\ %{}) do
87 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
88 object <- Object.normalize(activity),
89 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
90 public <- public_announce?(object, params) do
91 if announce_activity do
92 {:ok, announce_activity, object}
94 ActivityPub.announce(user, object, nil, true, public)
97 {:find_activity, _} -> {:error, :not_found}
98 _ -> {:error, dgettext("errors", "Could not repeat")}
102 def unrepeat(id_or_ap_id, user) do
103 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
104 object = Object.normalize(activity)
105 ActivityPub.unannounce(user, object)
107 {:find_activity, _} -> {:error, :not_found}
108 _ -> {:error, dgettext("errors", "Could not unrepeat")}
112 def favorite(id_or_ap_id, user) do
113 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
114 object <- Object.normalize(activity),
115 like_activity <- Utils.get_existing_like(user.ap_id, object) do
117 {:ok, like_activity, object}
119 ActivityPub.like(user, object)
122 {:find_activity, _} -> {:error, :not_found}
123 _ -> {:error, dgettext("errors", "Could not favorite")}
127 def unfavorite(id_or_ap_id, user) do
128 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
129 object = Object.normalize(activity)
130 ActivityPub.unlike(user, object)
132 {:find_activity, _} -> {:error, :not_found}
133 _ -> {:error, dgettext("errors", "Could not unfavorite")}
137 def react_with_emoji(id, user, emoji) do
138 with %Activity{} = activity <- Activity.get_by_id(id),
139 object <- Object.normalize(activity) do
140 ActivityPub.react_with_emoji(user, object, emoji)
143 {:error, dgettext("errors", "Could not add reaction emoji")}
147 def unreact_with_emoji(id, user, emoji) do
148 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
149 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
152 {:error, dgettext("errors", "Could not remove reaction emoji")}
156 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
157 with :ok <- validate_not_author(object, user),
158 :ok <- validate_existing_votes(user, object),
159 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
161 Enum.map(choices, fn index ->
162 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
165 ActivityPub.create(%{
166 to: answer_data["to"],
168 context: object.data["context"],
170 additional: %{"cc" => answer_data["cc"]}
176 object = Object.get_cached_by_ap_id(object.data["id"])
177 {:ok, answer_activities, object}
181 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
182 do: {:error, dgettext("errors", "Poll's author can't vote")}
184 defp validate_not_author(_, _), do: :ok
186 defp validate_existing_votes(%{ap_id: ap_id}, object) do
187 if Utils.get_existing_votes(ap_id, object) == [] do
190 {:error, dgettext("errors", "Already voted")}
194 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
195 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
197 defp normalize_and_validate_choices(choices, object) do
198 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
199 {options, max_count} = get_options_and_max_count(object)
200 count = Enum.count(options)
202 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
203 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
204 {:ok, options, choices}
206 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
207 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
211 def public_announce?(_, %{"visibility" => visibility})
212 when visibility in ~w{public unlisted private direct},
213 do: visibility in ~w(public unlisted)
215 def public_announce?(object, _) do
216 Visibility.is_public?(object)
219 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
221 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
222 when visibility in ~w{public unlisted private direct},
223 do: {visibility, get_replied_to_visibility(in_reply_to)}
225 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
226 visibility = {:list, String.to_integer(list_id)}
227 {visibility, get_replied_to_visibility(in_reply_to)}
230 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
231 visibility = get_replied_to_visibility(in_reply_to)
232 {visibility, visibility}
235 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
237 def get_replied_to_visibility(nil), do: nil
239 def get_replied_to_visibility(activity) do
240 with %Object{} = object <- Object.normalize(activity) do
241 Visibility.get_visibility(object)
245 def check_expiry_date({:ok, nil} = res), do: res
247 def check_expiry_date({:ok, in_seconds}) do
248 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
250 if ActivityExpiration.expires_late_enough?(expiry) do
253 {:error, "Expiry date is too soon"}
257 def check_expiry_date(expiry_str) do
258 Ecto.Type.cast(:integer, expiry_str)
259 |> check_expiry_date()
262 def listen(user, %{"title" => _} = data) do
263 with visibility <- data["visibility"] || "public",
264 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
266 Map.take(data, ["album", "artist", "title", "length"])
267 |> Map.put("type", "Audio")
270 |> Map.put("actor", user.ap_id),
272 ActivityPub.listen(%{
276 context: Utils.generate_context_id(),
277 additional: %{"cc" => cc}
283 def post(user, %{"status" => _} = data) do
284 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
286 |> ActivityPub.create(draft.preview?)
287 |> maybe_create_activity_expiration(draft.expires_at)
291 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
292 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
297 defp maybe_create_activity_expiration(result, _), do: result
299 # Updates the emojis for a user based on their profile
301 emoji = emoji_from_profile(user)
302 source_data = Map.put(user.source_data, "tag", emoji)
305 case User.update_source_data(user, source_data) do
310 ActivityPub.update(%{
312 to: [Pleroma.Constants.as_public(), user.follower_address],
315 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
319 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
322 data: %{"type" => "Create"},
323 object: %Object{data: %{"type" => object_type}}
324 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
325 true <- object_type in ["Note", "Article", "Question"],
326 true <- Visibility.is_public?(activity),
327 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
330 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
331 _ -> {:error, dgettext("errors", "Could not pin")}
335 def unpin(id_or_ap_id, user) do
336 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
337 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
340 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
341 _ -> {:error, dgettext("errors", "Could not unpin")}
345 def add_mute(user, activity) do
346 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
349 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
353 def remove_mute(user, activity) do
354 ThreadMute.remove_mute(user.id, activity.data["context"])
358 def thread_muted?(%{id: nil} = _user, _activity), do: false
360 def thread_muted?(user, activity) do
361 ThreadMute.check_muted(user.id, activity.data["context"]) != []
364 def report(user, %{"account_id" => account_id} = data) do
365 with {:ok, account} <- get_reported_account(account_id),
366 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
367 {:ok, statuses} <- get_report_statuses(account, data) do
369 context: Utils.generate_context_id(),
373 content: content_html,
374 forward: data["forward"] || false
379 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
381 defp get_reported_account(account_id) do
382 case User.get_cached_by_id(account_id) do
383 %User{} = account -> {:ok, account}
384 _ -> {:error, dgettext("errors", "Account not found")}
388 def update_report_state(activity_ids, state) when is_list(activity_ids) do
389 case Utils.update_report_state(activity_ids, state) do
390 :ok -> {:ok, activity_ids}
391 _ -> {:error, dgettext("errors", "Could not update state")}
395 def update_report_state(activity_id, state) do
396 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
397 Utils.update_report_state(activity, state)
399 nil -> {:error, :not_found}
400 _ -> {:error, dgettext("errors", "Could not update state")}
404 def update_activity_scope(activity_id, opts \\ %{}) do
405 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
406 {:ok, activity} <- toggle_sensitive(activity, opts) do
407 set_visibility(activity, opts)
409 nil -> {:error, :not_found}
410 {:error, reason} -> {:error, reason}
414 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
415 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
418 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
419 when is_boolean(sensitive) do
420 new_data = Map.put(object.data, "sensitive", sensitive)
424 |> Object.change(%{data: new_data})
425 |> Object.update_and_set_cache()
427 {:ok, Map.put(activity, :object, object)}
430 defp toggle_sensitive(activity, _), do: {:ok, activity}
432 defp set_visibility(activity, %{"visibility" => visibility}) do
433 Utils.update_activity_visibility(activity, visibility)
436 defp set_visibility(activity, _), do: {:ok, activity}
438 def hide_reblogs(%User{} = user, %User{} = target) do
439 UserRelationship.create_reblog_mute(user, target)
442 def show_reblogs(%User{} = user, %User{} = target) do
443 UserRelationship.delete_reblog_mute(user, target)