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
10 alias Pleroma.Notification
12 alias Pleroma.ThreadMute
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Utils
17 alias Pleroma.Web.ActivityPub.Visibility
19 import Pleroma.Web.Gettext
20 import Pleroma.Web.CommonAPI.Utils
22 require Pleroma.Constants
24 def follow(follower, followed) do
25 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
27 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
28 {:ok, activity} <- ActivityPub.follow(follower, followed),
29 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
30 {:ok, follower, followed, activity}
34 def unfollow(follower, unfollowed) do
35 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
36 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
37 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
42 def accept_follow_request(follower, followed) do
43 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
44 {:ok, follower} <- User.follow(follower, followed),
45 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
46 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
51 object: follow_activity.data["id"],
58 def reject_follow_request(follower, followed) do
59 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
60 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
61 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
62 {:ok, _notifications} <- Notification.dismiss(follow_activity),
67 object: follow_activity.data["id"],
74 def delete(activity_id, user) do
75 with {_, %Activity{data: %{"object" => _}} = activity} <-
76 {:find_activity, Activity.get_by_id_with_object(activity_id)},
77 %Object{} = object <- Object.normalize(activity),
78 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
79 {:ok, _} <- unpin(activity_id, user),
80 {:ok, delete} <- ActivityPub.delete(object) do
83 {:find_activity, _} -> {:error, :not_found}
84 _ -> {:error, dgettext("errors", "Could not delete")}
88 def repeat(id, user, params \\ %{}) do
89 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
90 {:find_activity, Activity.get_by_id(id)},
91 object <- Object.normalize(activity),
92 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
93 public <- public_announce?(object, params) do
94 if announce_activity do
95 {:ok, announce_activity, object}
97 ActivityPub.announce(user, object, nil, true, public)
100 {:find_activity, _} -> {:error, :not_found}
101 _ -> {:error, dgettext("errors", "Could not repeat")}
105 def unrepeat(id, user) do
106 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
107 {:find_activity, Activity.get_by_id(id)} do
108 object = Object.normalize(activity)
109 ActivityPub.unannounce(user, object)
111 {:find_activity, _} -> {:error, :not_found}
112 _ -> {:error, dgettext("errors", "Could not unrepeat")}
116 def favorite(id, user) do
117 with {_, %Activity{} = activity} <- {:find_activity, Activity.get_by_id(id)},
118 object <- Object.normalize(activity),
119 like_activity <- Utils.get_existing_like(user.ap_id, object) do
121 {:ok, like_activity, object}
123 ActivityPub.like(user, object)
126 {:find_activity, _} -> {:error, :not_found}
127 _ -> {:error, dgettext("errors", "Could not favorite")}
131 def unfavorite(id, user) do
132 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
133 {:find_activity, Activity.get_by_id(id)} do
134 object = Object.normalize(activity)
135 ActivityPub.unlike(user, object)
137 {:find_activity, _} -> {:error, :not_found}
138 _ -> {:error, dgettext("errors", "Could not unfavorite")}
142 def react_with_emoji(id, user, emoji) do
143 with %Activity{} = activity <- Activity.get_by_id(id),
144 object <- Object.normalize(activity) do
145 ActivityPub.react_with_emoji(user, object, emoji)
148 {:error, dgettext("errors", "Could not add reaction emoji")}
152 def unreact_with_emoji(id, user, emoji) do
153 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
154 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
157 {:error, dgettext("errors", "Could not remove reaction emoji")}
161 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
162 with :ok <- validate_not_author(object, user),
163 :ok <- validate_existing_votes(user, object),
164 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
166 Enum.map(choices, fn index ->
167 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
170 ActivityPub.create(%{
171 to: answer_data["to"],
173 context: object.data["context"],
175 additional: %{"cc" => answer_data["cc"]}
181 object = Object.get_cached_by_ap_id(object.data["id"])
182 {:ok, answer_activities, object}
186 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
187 do: {:error, dgettext("errors", "Poll's author can't vote")}
189 defp validate_not_author(_, _), do: :ok
191 defp validate_existing_votes(%{ap_id: ap_id}, object) do
192 if Utils.get_existing_votes(ap_id, object) == [] do
195 {:error, dgettext("errors", "Already voted")}
199 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
200 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
202 defp normalize_and_validate_choices(choices, object) do
203 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
204 {options, max_count} = get_options_and_max_count(object)
205 count = Enum.count(options)
207 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
208 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
209 {:ok, options, choices}
211 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
212 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
216 def public_announce?(_, %{"visibility" => visibility})
217 when visibility in ~w{public unlisted private direct},
218 do: visibility in ~w(public unlisted)
220 def public_announce?(object, _) do
221 Visibility.is_public?(object)
224 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
226 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
227 when visibility in ~w{public unlisted private direct},
228 do: {visibility, get_replied_to_visibility(in_reply_to)}
230 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
231 visibility = {:list, String.to_integer(list_id)}
232 {visibility, get_replied_to_visibility(in_reply_to)}
235 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
236 visibility = get_replied_to_visibility(in_reply_to)
237 {visibility, visibility}
240 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
242 def get_replied_to_visibility(nil), do: nil
244 def get_replied_to_visibility(activity) do
245 with %Object{} = object <- Object.normalize(activity) do
246 Visibility.get_visibility(object)
250 def check_expiry_date({:ok, nil} = res), do: res
252 def check_expiry_date({:ok, in_seconds}) do
253 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
255 if ActivityExpiration.expires_late_enough?(expiry) do
258 {:error, "Expiry date is too soon"}
262 def check_expiry_date(expiry_str) do
263 Ecto.Type.cast(:integer, expiry_str)
264 |> check_expiry_date()
267 def listen(user, %{"title" => _} = data) do
268 with visibility <- data["visibility"] || "public",
269 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
271 Map.take(data, ["album", "artist", "title", "length"])
272 |> Map.put("type", "Audio")
275 |> Map.put("actor", user.ap_id),
277 ActivityPub.listen(%{
281 context: Utils.generate_context_id(),
282 additional: %{"cc" => cc}
288 def post(user, %{"status" => _} = data) do
289 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
291 |> ActivityPub.create(draft.preview?)
292 |> maybe_create_activity_expiration(draft.expires_at)
296 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
297 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
302 defp maybe_create_activity_expiration(result, _), do: result
304 # Updates the emojis for a user based on their profile
306 emoji = emoji_from_profile(user)
307 source_data = Map.put(user.source_data, "tag", emoji)
310 case User.update_source_data(user, source_data) do
315 ActivityPub.update(%{
317 to: [Pleroma.Constants.as_public(), user.follower_address],
320 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
324 def pin(id, %{ap_id: user_ap_id} = user) do
327 data: %{"type" => "Create"},
328 object: %Object{data: %{"type" => object_type}}
329 } = activity <- Activity.get_by_id_with_object(id),
330 true <- object_type in ["Note", "Article", "Question"],
331 true <- Visibility.is_public?(activity),
332 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
335 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
336 _ -> {:error, dgettext("errors", "Could not pin")}
340 def unpin(id, user) do
341 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
342 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
345 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
346 _ -> {:error, dgettext("errors", "Could not unpin")}
350 def add_mute(user, activity) do
351 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
354 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
358 def remove_mute(user, activity) do
359 ThreadMute.remove_mute(user.id, activity.data["context"])
363 def thread_muted?(%{id: nil} = _user, _activity), do: false
365 def thread_muted?(user, activity) do
366 ThreadMute.check_muted(user.id, activity.data["context"]) != []
369 def report(user, %{"account_id" => account_id} = data) do
370 with {:ok, account} <- get_reported_account(account_id),
371 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
372 {:ok, statuses} <- get_report_statuses(account, data) do
374 context: Utils.generate_context_id(),
378 content: content_html,
379 forward: data["forward"] || false
384 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
386 defp get_reported_account(account_id) do
387 case User.get_cached_by_id(account_id) do
388 %User{} = account -> {:ok, account}
389 _ -> {:error, dgettext("errors", "Account not found")}
393 def update_report_state(activity_ids, state) when is_list(activity_ids) do
394 case Utils.update_report_state(activity_ids, state) do
395 :ok -> {:ok, activity_ids}
396 _ -> {:error, dgettext("errors", "Could not update state")}
400 def update_report_state(activity_id, state) do
401 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
402 Utils.update_report_state(activity, state)
404 nil -> {:error, :not_found}
405 _ -> {:error, dgettext("errors", "Could not update state")}
409 def update_activity_scope(activity_id, opts \\ %{}) do
410 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
411 {:ok, activity} <- toggle_sensitive(activity, opts) do
412 set_visibility(activity, opts)
414 nil -> {:error, :not_found}
415 {:error, reason} -> {:error, reason}
419 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
420 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
423 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
424 when is_boolean(sensitive) do
425 new_data = Map.put(object.data, "sensitive", sensitive)
429 |> Object.change(%{data: new_data})
430 |> Object.update_and_set_cache()
432 {:ok, Map.put(activity, :object, object)}
435 defp toggle_sensitive(activity, _), do: {:ok, activity}
437 defp set_visibility(activity, %{"visibility" => visibility}) do
438 Utils.update_activity_visibility(activity, visibility)
441 defp set_visibility(activity, _), do: {:ok, activity}
443 def hide_reblogs(%User{} = user, %User{} = target) do
444 UserRelationship.create_reblog_mute(user, target)
447 def show_reblogs(%User{} = user, %User{} = target) do
448 UserRelationship.delete_reblog_mute(user, target)