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 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
280 ActivityPub.create(draft.changes, draft.preview?)
284 # Updates the emojis for a user based on their profile
286 emoji = emoji_from_profile(user)
287 source_data = Map.put(user.source_data, "tag", emoji)
290 case User.update_source_data(user, source_data) do
295 ActivityPub.update(%{
297 to: [Pleroma.Constants.as_public(), user.follower_address],
300 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
304 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
307 data: %{"type" => "Create"},
308 object: %Object{data: %{"type" => object_type}}
309 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
310 true <- object_type in ["Note", "Article", "Question"],
311 true <- Visibility.is_public?(activity),
312 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
315 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
316 _ -> {:error, dgettext("errors", "Could not pin")}
320 def unpin(id_or_ap_id, user) do
321 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
322 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
325 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
326 _ -> {:error, dgettext("errors", "Could not unpin")}
330 def add_mute(user, activity) do
331 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
334 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
338 def remove_mute(user, activity) do
339 ThreadMute.remove_mute(user.id, activity.data["context"])
343 def thread_muted?(%{id: nil} = _user, _activity), do: false
345 def thread_muted?(user, activity) do
346 ThreadMute.check_muted(user.id, activity.data["context"]) != []
349 def report(user, %{"account_id" => account_id} = data) do
350 with {:ok, account} <- get_reported_account(account_id),
351 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
352 {:ok, statuses} <- get_report_statuses(account, data) do
354 context: Utils.generate_context_id(),
358 content: content_html,
359 forward: data["forward"] || false
364 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
366 defp get_reported_account(account_id) do
367 case User.get_cached_by_id(account_id) do
368 %User{} = account -> {:ok, account}
369 _ -> {:error, dgettext("errors", "Account not found")}
373 def update_report_state(activity_ids, state) when is_list(activity_ids) do
374 case Utils.update_report_state(activity_ids, state) do
375 :ok -> {:ok, activity_ids}
376 _ -> {:error, dgettext("errors", "Could not update state")}
380 def update_report_state(activity_id, state) do
381 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
382 Utils.update_report_state(activity, state)
384 nil -> {:error, :not_found}
385 _ -> {:error, dgettext("errors", "Could not update state")}
389 def update_activity_scope(activity_id, opts \\ %{}) do
390 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
391 {:ok, activity} <- toggle_sensitive(activity, opts) do
392 set_visibility(activity, opts)
394 nil -> {:error, :not_found}
395 {:error, reason} -> {:error, reason}
399 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
400 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
403 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
404 when is_boolean(sensitive) do
405 new_data = Map.put(object.data, "sensitive", sensitive)
409 |> Object.change(%{data: new_data})
410 |> Object.update_and_set_cache()
412 {:ok, Map.put(activity, :object, object)}
415 defp toggle_sensitive(activity, _), do: {:ok, activity}
417 defp set_visibility(activity, %{"visibility" => visibility}) do
418 Utils.update_activity_visibility(activity, visibility)
421 defp set_visibility(activity, _), do: {:ok, activity}
423 def hide_reblogs(%User{} = user, %User{} = target) do
424 UserRelationship.create_reblog_mute(user, target)
427 def show_reblogs(%User{} = user, %User{} = target) do
428 UserRelationship.delete_reblog_mute(user, target)