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
281 |> ActivityPub.create(draft.preview?)
282 |> maybe_create_activity_expiration(draft.expires_at)
286 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
287 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
292 defp maybe_create_activity_expiration(result, _), do: result
294 # Updates the emojis for a user based on their profile
296 emoji = emoji_from_profile(user)
297 source_data = Map.put(user.source_data, "tag", emoji)
300 case User.update_source_data(user, source_data) do
305 ActivityPub.update(%{
307 to: [Pleroma.Constants.as_public(), user.follower_address],
310 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
314 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
317 data: %{"type" => "Create"},
318 object: %Object{data: %{"type" => "Note"}}
319 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
320 true <- Visibility.is_public?(activity),
321 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
324 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
325 _ -> {:error, dgettext("errors", "Could not pin")}
329 def unpin(id_or_ap_id, user) do
330 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
331 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
334 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
335 _ -> {:error, dgettext("errors", "Could not unpin")}
339 def add_mute(user, activity) do
340 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
343 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
347 def remove_mute(user, activity) do
348 ThreadMute.remove_mute(user.id, activity.data["context"])
352 def thread_muted?(%{id: nil} = _user, _activity), do: false
354 def thread_muted?(user, activity) do
355 ThreadMute.check_muted(user.id, activity.data["context"]) != []
358 def report(user, %{"account_id" => account_id} = data) do
359 with {:ok, account} <- get_reported_account(account_id),
360 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
361 {:ok, statuses} <- get_report_statuses(account, data) do
363 context: Utils.generate_context_id(),
367 content: content_html,
368 forward: data["forward"] || false
373 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
375 defp get_reported_account(account_id) do
376 case User.get_cached_by_id(account_id) do
377 %User{} = account -> {:ok, account}
378 _ -> {:error, dgettext("errors", "Account not found")}
382 def update_report_state(activity_ids, state) when is_list(activity_ids) do
383 case Utils.update_report_state(activity_ids, state) do
384 :ok -> {:ok, activity_ids}
385 _ -> {:error, dgettext("errors", "Could not update state")}
389 def update_report_state(activity_id, state) do
390 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
391 Utils.update_report_state(activity, state)
393 nil -> {:error, :not_found}
394 _ -> {:error, dgettext("errors", "Could not update state")}
398 def update_activity_scope(activity_id, opts \\ %{}) do
399 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
400 {:ok, activity} <- toggle_sensitive(activity, opts) do
401 set_visibility(activity, opts)
403 nil -> {:error, :not_found}
404 {:error, reason} -> {:error, reason}
408 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
409 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
412 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
413 when is_boolean(sensitive) do
414 new_data = Map.put(object.data, "sensitive", sensitive)
418 |> Object.change(%{data: new_data})
419 |> Object.update_and_set_cache()
421 {:ok, Map.put(activity, :object, object)}
424 defp toggle_sensitive(activity, _), do: {:ok, activity}
426 defp set_visibility(activity, %{"visibility" => visibility}) do
427 Utils.update_activity_visibility(activity, visibility)
430 defp set_visibility(activity, _), do: {:ok, activity}
432 def hide_reblogs(%User{} = user, %User{} = target) do
433 UserRelationship.create_reblog_mute(user, target)
436 def show_reblogs(%User{} = user, %User{} = target) do
437 UserRelationship.delete_reblog_mute(user, target)