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.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.Builder
15 alias Pleroma.Web.ActivityPub.Pipeline
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
25 def follow(follower, followed) do
26 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
28 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
29 {:ok, activity} <- ActivityPub.follow(follower, followed),
30 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
31 {:ok, follower, followed, activity}
35 def unfollow(follower, unfollowed) do
36 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
37 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
38 {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
43 def accept_follow_request(follower, followed) do
44 with {:ok, follower} <- User.follow(follower, followed),
45 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
46 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
47 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
52 object: follow_activity.data["id"],
59 def reject_follow_request(follower, followed) do
60 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
61 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
62 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
67 object: follow_activity.data["id"],
74 def delete(activity_id, user) do
75 with %Activity{data: %{"object" => _}} = activity <-
76 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 _ -> {:error, dgettext("errors", "Could not delete")}
87 def repeat(id_or_ap_id, user, params \\ %{}) do
88 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
89 object <- Object.normalize(activity),
90 nil <- Utils.get_existing_announce(user.ap_id, object),
91 public <- public_announce?(object, params) do
92 ActivityPub.announce(user, object, nil, true, public)
94 _ -> {:error, dgettext("errors", "Could not repeat")}
98 def unrepeat(id_or_ap_id, user) do
99 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
100 object = Object.normalize(activity)
101 ActivityPub.unannounce(user, object)
103 _ -> {:error, dgettext("errors", "Could not unrepeat")}
107 @spec favorite(User.t(), binary()) :: {:ok, Activity.t()} | {:error, any()}
108 def favorite(%User{} = user, id) do
109 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
110 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
111 {_, {:ok, %Activity{} = activity, _meta}} <-
113 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
117 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
118 {:error, dgettext("errors", "Could not favorite")}
122 def unfavorite(id_or_ap_id, user) do
123 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
124 object = Object.normalize(activity)
125 ActivityPub.unlike(user, object)
127 _ -> {:error, dgettext("errors", "Could not unfavorite")}
131 def react_with_emoji(id, user, emoji) do
132 with %Activity{} = activity <- Activity.get_by_id(id),
133 object <- Object.normalize(activity) do
134 ActivityPub.react_with_emoji(user, object, emoji)
137 {:error, dgettext("errors", "Could not add reaction emoji")}
141 def unreact_with_emoji(id, user, emoji) do
142 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
143 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
146 {:error, dgettext("errors", "Could not remove reaction emoji")}
150 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
151 with :ok <- validate_not_author(object, user),
152 :ok <- validate_existing_votes(user, object),
153 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
155 Enum.map(choices, fn index ->
156 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
159 ActivityPub.create(%{
160 to: answer_data["to"],
162 context: object.data["context"],
164 additional: %{"cc" => answer_data["cc"]}
170 object = Object.get_cached_by_ap_id(object.data["id"])
171 {:ok, answer_activities, object}
175 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
176 do: {:error, dgettext("errors", "Poll's author can't vote")}
178 defp validate_not_author(_, _), do: :ok
180 defp validate_existing_votes(%{ap_id: ap_id}, object) do
181 if Utils.get_existing_votes(ap_id, object) == [] do
184 {:error, dgettext("errors", "Already voted")}
188 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
189 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
191 defp normalize_and_validate_choices(choices, object) do
192 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
193 {options, max_count} = get_options_and_max_count(object)
194 count = Enum.count(options)
196 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
197 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
198 {:ok, options, choices}
200 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
201 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
205 def public_announce?(_, %{"visibility" => visibility})
206 when visibility in ~w{public unlisted private direct},
207 do: visibility in ~w(public unlisted)
209 def public_announce?(object, _) do
210 Visibility.is_public?(object)
213 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
215 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
216 when visibility in ~w{public unlisted private direct},
217 do: {visibility, get_replied_to_visibility(in_reply_to)}
219 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
220 visibility = {:list, String.to_integer(list_id)}
221 {visibility, get_replied_to_visibility(in_reply_to)}
224 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
225 visibility = get_replied_to_visibility(in_reply_to)
226 {visibility, visibility}
229 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
231 def get_replied_to_visibility(nil), do: nil
233 def get_replied_to_visibility(activity) do
234 with %Object{} = object <- Object.normalize(activity) do
235 Visibility.get_visibility(object)
239 def check_expiry_date({:ok, nil} = res), do: res
241 def check_expiry_date({:ok, in_seconds}) do
242 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
244 if ActivityExpiration.expires_late_enough?(expiry) do
247 {:error, "Expiry date is too soon"}
251 def check_expiry_date(expiry_str) do
252 Ecto.Type.cast(:integer, expiry_str)
253 |> check_expiry_date()
256 def listen(user, %{"title" => _} = data) do
257 with visibility <- data["visibility"] || "public",
258 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
260 Map.take(data, ["album", "artist", "title", "length"])
261 |> Map.put("type", "Audio")
264 |> Map.put("actor", user.ap_id),
266 ActivityPub.listen(%{
270 context: Utils.generate_context_id(),
271 additional: %{"cc" => cc}
277 def post(user, %{"status" => _} = data) do
278 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
280 |> ActivityPub.create(draft.preview?)
281 |> maybe_create_activity_expiration(draft.expires_at)
285 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
286 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
291 defp maybe_create_activity_expiration(result, _), do: result
293 # Updates the emojis for a user based on their profile
295 emoji = emoji_from_profile(user)
296 source_data = Map.put(user.source_data, "tag", emoji)
299 case User.update_source_data(user, source_data) do
304 ActivityPub.update(%{
306 to: [Pleroma.Constants.as_public(), user.follower_address],
309 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
313 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
316 data: %{"type" => "Create"},
317 object: %Object{data: %{"type" => "Note"}}
318 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
319 true <- Visibility.is_public?(activity),
320 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
323 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
324 _ -> {:error, dgettext("errors", "Could not pin")}
328 def unpin(id_or_ap_id, user) do
329 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
330 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
333 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
334 _ -> {:error, dgettext("errors", "Could not unpin")}
338 def add_mute(user, activity) do
339 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
342 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
346 def remove_mute(user, activity) do
347 ThreadMute.remove_mute(user.id, activity.data["context"])
351 def thread_muted?(%{id: nil} = _user, _activity), do: false
353 def thread_muted?(user, activity) do
354 ThreadMute.check_muted(user.id, activity.data["context"]) != []
357 def report(user, %{"account_id" => account_id} = data) do
358 with {:ok, account} <- get_reported_account(account_id),
359 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
360 {:ok, statuses} <- get_report_statuses(account, data) do
362 context: Utils.generate_context_id(),
366 content: content_html,
367 forward: data["forward"] || false
372 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
374 defp get_reported_account(account_id) do
375 case User.get_cached_by_id(account_id) do
376 %User{} = account -> {:ok, account}
377 _ -> {:error, dgettext("errors", "Account not found")}
381 def update_report_state(activity_ids, state) when is_list(activity_ids) do
382 case Utils.update_report_state(activity_ids, state) do
383 :ok -> {:ok, activity_ids}
384 _ -> {:error, dgettext("errors", "Could not update state")}
388 def update_report_state(activity_id, state) do
389 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
390 Utils.update_report_state(activity, state)
392 nil -> {:error, :not_found}
393 _ -> {:error, dgettext("errors", "Could not update state")}
397 def update_activity_scope(activity_id, opts \\ %{}) do
398 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
399 {:ok, activity} <- toggle_sensitive(activity, opts) do
400 set_visibility(activity, opts)
402 nil -> {:error, :not_found}
403 {:error, reason} -> {:error, reason}
407 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
408 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
411 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
412 when is_boolean(sensitive) do
413 new_data = Map.put(object.data, "sensitive", sensitive)
417 |> Object.change(%{data: new_data})
418 |> Object.update_and_set_cache()
420 {:ok, Map.put(activity, :object, object)}
423 defp toggle_sensitive(activity, _), do: {:ok, activity}
425 defp set_visibility(activity, %{"visibility" => visibility}) do
426 Utils.update_activity_visibility(activity, visibility)
429 defp set_visibility(activity, _), do: {:ok, activity}
431 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
432 if ap_id not in user.muted_reblogs do
433 User.add_reblog_mute(user, ap_id)
437 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
438 if ap_id in user.muted_reblogs do
439 User.remove_reblog_mute(user, ap_id)