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 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
43 {:ok, follower} <- User.follow(follower, followed),
44 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
45 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_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, :follow_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, user, params \\ %{}) do
87 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
88 {:find_activity, Activity.get_by_id(id)},
89 object <- Object.normalize(activity),
90 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
91 public <- public_announce?(object, params) do
92 if announce_activity do
93 {:ok, announce_activity, object}
95 ActivityPub.announce(user, object, nil, true, public)
98 {:find_activity, _} -> {:error, :not_found}
99 _ -> {:error, dgettext("errors", "Could not repeat")}
103 def unrepeat(id, user) do
104 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
105 {:find_activity, Activity.get_by_id(id)} do
106 object = Object.normalize(activity)
107 ActivityPub.unannounce(user, object)
109 {:find_activity, _} -> {:error, :not_found}
110 _ -> {:error, dgettext("errors", "Could not unrepeat")}
114 def favorite(id, user) do
115 with {_, %Activity{} = activity} <- {:find_activity, Activity.get_by_id(id)},
116 object <- Object.normalize(activity),
117 like_activity <- Utils.get_existing_like(user.ap_id, object) do
119 {:ok, like_activity, object}
121 ActivityPub.like(user, object)
124 {:find_activity, _} -> {:error, :not_found}
125 _ -> {:error, dgettext("errors", "Could not favorite")}
129 def unfavorite(id, user) do
130 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
131 {:find_activity, Activity.get_by_id(id)} do
132 object = Object.normalize(activity)
133 ActivityPub.unlike(user, object)
135 {:find_activity, _} -> {:error, :not_found}
136 _ -> {:error, dgettext("errors", "Could not unfavorite")}
140 def react_with_emoji(id, user, emoji) do
141 with %Activity{} = activity <- Activity.get_by_id(id),
142 object <- Object.normalize(activity) do
143 ActivityPub.react_with_emoji(user, object, emoji)
146 {:error, dgettext("errors", "Could not add reaction emoji")}
150 def unreact_with_emoji(id, user, emoji) do
151 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
152 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
155 {:error, dgettext("errors", "Could not remove reaction emoji")}
159 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
160 with :ok <- validate_not_author(object, user),
161 :ok <- validate_existing_votes(user, object),
162 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
164 Enum.map(choices, fn index ->
165 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
168 ActivityPub.create(%{
169 to: answer_data["to"],
171 context: object.data["context"],
173 additional: %{"cc" => answer_data["cc"]}
179 object = Object.get_cached_by_ap_id(object.data["id"])
180 {:ok, answer_activities, object}
184 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
185 do: {:error, dgettext("errors", "Poll's author can't vote")}
187 defp validate_not_author(_, _), do: :ok
189 defp validate_existing_votes(%{ap_id: ap_id}, object) do
190 if Utils.get_existing_votes(ap_id, object) == [] do
193 {:error, dgettext("errors", "Already voted")}
197 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
198 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
200 defp normalize_and_validate_choices(choices, object) do
201 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
202 {options, max_count} = get_options_and_max_count(object)
203 count = Enum.count(options)
205 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
206 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
207 {:ok, options, choices}
209 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
210 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
214 def public_announce?(_, %{"visibility" => visibility})
215 when visibility in ~w{public unlisted private direct},
216 do: visibility in ~w(public unlisted)
218 def public_announce?(object, _) do
219 Visibility.is_public?(object)
222 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
224 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
225 when visibility in ~w{public unlisted private direct},
226 do: {visibility, get_replied_to_visibility(in_reply_to)}
228 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
229 visibility = {:list, String.to_integer(list_id)}
230 {visibility, get_replied_to_visibility(in_reply_to)}
233 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
234 visibility = get_replied_to_visibility(in_reply_to)
235 {visibility, visibility}
238 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
240 def get_replied_to_visibility(nil), do: nil
242 def get_replied_to_visibility(activity) do
243 with %Object{} = object <- Object.normalize(activity) do
244 Visibility.get_visibility(object)
248 def check_expiry_date({:ok, nil} = res), do: res
250 def check_expiry_date({:ok, in_seconds}) do
251 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
253 if ActivityExpiration.expires_late_enough?(expiry) do
256 {:error, "Expiry date is too soon"}
260 def check_expiry_date(expiry_str) do
261 Ecto.Type.cast(:integer, expiry_str)
262 |> check_expiry_date()
265 def listen(user, %{"title" => _} = data) do
266 with visibility <- data["visibility"] || "public",
267 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
269 Map.take(data, ["album", "artist", "title", "length"])
270 |> Map.put("type", "Audio")
273 |> Map.put("actor", user.ap_id),
275 ActivityPub.listen(%{
279 context: Utils.generate_context_id(),
280 additional: %{"cc" => cc}
286 def post(user, %{"status" => _} = data) do
287 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
289 |> ActivityPub.create(draft.preview?)
290 |> maybe_create_activity_expiration(draft.expires_at)
294 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
295 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
300 defp maybe_create_activity_expiration(result, _), do: result
302 # Updates the emojis for a user based on their profile
304 emoji = emoji_from_profile(user)
305 source_data = Map.put(user.source_data, "tag", emoji)
308 case User.update_source_data(user, source_data) do
313 ActivityPub.update(%{
315 to: [Pleroma.Constants.as_public(), user.follower_address],
318 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
322 def pin(id, %{ap_id: user_ap_id} = user) do
325 data: %{"type" => "Create"},
326 object: %Object{data: %{"type" => object_type}}
327 } = activity <- Activity.get_by_id_with_object(id),
328 true <- object_type in ["Note", "Article", "Question"],
329 true <- Visibility.is_public?(activity),
330 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
333 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
334 _ -> {:error, dgettext("errors", "Could not pin")}
338 def unpin(id, user) do
339 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
340 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
343 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
344 _ -> {:error, dgettext("errors", "Could not unpin")}
348 def add_mute(user, activity) do
349 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
352 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
356 def remove_mute(user, activity) do
357 ThreadMute.remove_mute(user.id, activity.data["context"])
361 def thread_muted?(%{id: nil} = _user, _activity), do: false
363 def thread_muted?(user, activity) do
364 ThreadMute.check_muted(user.id, activity.data["context"]) != []
367 def report(user, %{"account_id" => account_id} = data) do
368 with {:ok, account} <- get_reported_account(account_id),
369 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
370 {:ok, statuses} <- get_report_statuses(account, data) do
372 context: Utils.generate_context_id(),
376 content: content_html,
377 forward: data["forward"] || false
382 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
384 defp get_reported_account(account_id) do
385 case User.get_cached_by_id(account_id) do
386 %User{} = account -> {:ok, account}
387 _ -> {:error, dgettext("errors", "Account not found")}
391 def update_report_state(activity_ids, state) when is_list(activity_ids) do
392 case Utils.update_report_state(activity_ids, state) do
393 :ok -> {:ok, activity_ids}
394 _ -> {:error, dgettext("errors", "Could not update state")}
398 def update_report_state(activity_id, state) do
399 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
400 Utils.update_report_state(activity, state)
402 nil -> {:error, :not_found}
403 _ -> {:error, dgettext("errors", "Could not update state")}
407 def update_activity_scope(activity_id, opts \\ %{}) do
408 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
409 {:ok, activity} <- toggle_sensitive(activity, opts) do
410 set_visibility(activity, opts)
412 nil -> {:error, :not_found}
413 {:error, reason} -> {:error, reason}
417 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
418 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
421 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
422 when is_boolean(sensitive) do
423 new_data = Map.put(object.data, "sensitive", sensitive)
427 |> Object.change(%{data: new_data})
428 |> Object.update_and_set_cache()
430 {:ok, Map.put(activity, :object, object)}
433 defp toggle_sensitive(activity, _), do: {:ok, activity}
435 defp set_visibility(activity, %{"visibility" => visibility}) do
436 Utils.update_activity_visibility(activity, visibility)
439 defp set_visibility(activity, _), do: {:ok, activity}
441 def hide_reblogs(%User{} = user, %User{} = target) do
442 UserRelationship.create_reblog_mute(user, target)
445 def show_reblogs(%User{} = user, %User{} = target) do
446 UserRelationship.delete_reblog_mute(user, target)