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
285 ActivityPub.create(draft.changes, draft.preview?)
289 # Updates the emojis for a user based on their profile
291 emoji = emoji_from_profile(user)
292 source_data = Map.put(user.source_data, "tag", emoji)
295 case User.update_source_data(user, source_data) do
300 ActivityPub.update(%{
302 to: [Pleroma.Constants.as_public(), user.follower_address],
305 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
309 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
312 data: %{"type" => "Create"},
313 object: %Object{data: %{"type" => object_type}}
314 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
315 true <- object_type in ["Note", "Article", "Question"],
316 true <- Visibility.is_public?(activity),
317 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
320 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
321 _ -> {:error, dgettext("errors", "Could not pin")}
325 def unpin(id_or_ap_id, user) do
326 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
327 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
330 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
331 _ -> {:error, dgettext("errors", "Could not unpin")}
335 def add_mute(user, activity) do
336 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
339 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
343 def remove_mute(user, activity) do
344 ThreadMute.remove_mute(user.id, activity.data["context"])
348 def thread_muted?(%{id: nil} = _user, _activity), do: false
350 def thread_muted?(user, activity) do
351 ThreadMute.check_muted(user.id, activity.data["context"]) != []
354 def report(user, %{"account_id" => account_id} = data) do
355 with {:ok, account} <- get_reported_account(account_id),
356 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
357 {:ok, statuses} <- get_report_statuses(account, data) do
359 context: Utils.generate_context_id(),
363 content: content_html,
364 forward: data["forward"] || false
369 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
371 defp get_reported_account(account_id) do
372 case User.get_cached_by_id(account_id) do
373 %User{} = account -> {:ok, account}
374 _ -> {:error, dgettext("errors", "Account not found")}
378 def update_report_state(activity_ids, state) when is_list(activity_ids) do
379 case Utils.update_report_state(activity_ids, state) do
380 :ok -> {:ok, activity_ids}
381 _ -> {:error, dgettext("errors", "Could not update state")}
385 def update_report_state(activity_id, state) do
386 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
387 Utils.update_report_state(activity, state)
389 nil -> {:error, :not_found}
390 _ -> {:error, dgettext("errors", "Could not update state")}
394 def update_activity_scope(activity_id, opts \\ %{}) do
395 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
396 {:ok, activity} <- toggle_sensitive(activity, opts) do
397 set_visibility(activity, opts)
399 nil -> {:error, :not_found}
400 {:error, reason} -> {:error, reason}
404 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
405 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
408 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
409 when is_boolean(sensitive) do
410 new_data = Map.put(object.data, "sensitive", sensitive)
414 |> Object.change(%{data: new_data})
415 |> Object.update_and_set_cache()
417 {:ok, Map.put(activity, :object, object)}
420 defp toggle_sensitive(activity, _), do: {:ok, activity}
422 defp set_visibility(activity, %{"visibility" => visibility}) do
423 Utils.update_activity_visibility(activity, visibility)
426 defp set_visibility(activity, _), do: {:ok, activity}
428 def hide_reblogs(%User{} = user, %User{} = target) do
429 UserRelationship.create_reblog_mute(user, target)
432 def show_reblogs(%User{} = user, %User{} = target) do
433 UserRelationship.delete_reblog_mute(user, target)