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
10 alias Pleroma.ThreadMute
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Builder
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.ActivityPub.Visibility
17 import Pleroma.Web.Gettext
18 import Pleroma.Web.CommonAPI.Utils
20 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, _unfollowed} <- 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"),
49 object: follow_activity.data["id"],
56 def reject_follow_request(follower, followed) do
57 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
58 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
63 object: follow_activity.data["id"],
70 def delete(activity_id, user) do
71 with %Activity{data: %{"object" => _}} = activity <-
72 Activity.get_by_id_with_object(activity_id),
73 %Object{} = object <- Object.normalize(activity),
74 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
75 {:ok, _} <- unpin(activity_id, user),
76 {:ok, delete} <- ActivityPub.delete(object) do
79 _ -> {:error, dgettext("errors", "Could not delete")}
83 def repeat(id_or_ap_id, user, params \\ %{}) do
84 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
85 object <- Object.normalize(activity),
86 nil <- Utils.get_existing_announce(user.ap_id, object),
87 public <- public_announce?(object, params) do
88 ActivityPub.announce(user, object, nil, true, public)
90 _ -> {:error, dgettext("errors", "Could not repeat")}
94 def unrepeat(id_or_ap_id, user) do
95 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
96 object = Object.normalize(activity)
97 ActivityPub.unannounce(user, object)
99 _ -> {:error, dgettext("errors", "Could not unrepeat")}
103 @spec favorite(User.t(), binary()) :: {:ok, Activity.t()} | {:error, any()}
104 def favorite(%User{} = user, id) do
105 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
106 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
107 {_, {:ok, %Activity{} = activity, _meta}} <-
109 ActivityPub.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
113 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
114 {:error, dgettext("errors", "Could not favorite")}
118 def unfavorite(id_or_ap_id, user) do
119 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
120 object = Object.normalize(activity)
121 ActivityPub.unlike(user, object)
123 _ -> {:error, dgettext("errors", "Could not unfavorite")}
127 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
128 with :ok <- validate_not_author(object, user),
129 :ok <- validate_existing_votes(user, object),
130 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
132 Enum.map(choices, fn index ->
133 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
136 ActivityPub.create(%{
137 to: answer_data["to"],
139 context: object.data["context"],
141 additional: %{"cc" => answer_data["cc"]}
147 object = Object.get_cached_by_ap_id(object.data["id"])
148 {:ok, answer_activities, object}
152 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
153 do: {:error, dgettext("errors", "Poll's author can't vote")}
155 defp validate_not_author(_, _), do: :ok
157 defp validate_existing_votes(%{ap_id: ap_id}, object) do
158 if Utils.get_existing_votes(ap_id, object) == [] do
161 {:error, dgettext("errors", "Already voted")}
165 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
166 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
168 defp normalize_and_validate_choices(choices, object) do
169 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
170 {options, max_count} = get_options_and_max_count(object)
171 count = Enum.count(options)
173 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
174 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
175 {:ok, options, choices}
177 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
178 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
182 def public_announce?(_, %{"visibility" => visibility})
183 when visibility in ~w{public unlisted private direct},
184 do: visibility in ~w(public unlisted)
186 def public_announce?(object, _) do
187 Visibility.is_public?(object)
190 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
192 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
193 when visibility in ~w{public unlisted private direct},
194 do: {visibility, get_replied_to_visibility(in_reply_to)}
196 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
197 visibility = {:list, String.to_integer(list_id)}
198 {visibility, get_replied_to_visibility(in_reply_to)}
201 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
202 visibility = get_replied_to_visibility(in_reply_to)
203 {visibility, visibility}
206 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
208 def get_replied_to_visibility(nil), do: nil
210 def get_replied_to_visibility(activity) do
211 with %Object{} = object <- Object.normalize(activity) do
212 Visibility.get_visibility(object)
216 def check_expiry_date({:ok, nil} = res), do: res
218 def check_expiry_date({:ok, in_seconds}) do
219 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
221 if ActivityExpiration.expires_late_enough?(expiry) do
224 {:error, "Expiry date is too soon"}
228 def check_expiry_date(expiry_str) do
229 Ecto.Type.cast(:integer, expiry_str)
230 |> check_expiry_date()
233 def listen(user, %{"title" => _} = data) do
234 with visibility <- data["visibility"] || "public",
235 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
237 Map.take(data, ["album", "artist", "title", "length"])
238 |> Map.put("type", "Audio")
241 |> Map.put("actor", user.ap_id),
243 ActivityPub.listen(%{
247 context: Utils.generate_context_id(),
248 additional: %{"cc" => cc}
254 def post(user, %{"status" => _} = data) do
255 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
257 |> ActivityPub.create(draft.preview?)
258 |> maybe_create_activity_expiration(draft.expires_at)
262 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
263 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
268 defp maybe_create_activity_expiration(result, _), do: result
270 # Updates the emojis for a user based on their profile
272 emoji = emoji_from_profile(user)
273 source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)
276 case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
281 ActivityPub.update(%{
283 to: [Pleroma.Constants.as_public(), user.follower_address],
286 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
290 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
293 data: %{"type" => "Create"},
294 object: %Object{data: %{"type" => "Note"}}
295 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
296 true <- Visibility.is_public?(activity),
297 {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
300 {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
301 _ -> {:error, dgettext("errors", "Could not pin")}
305 def unpin(id_or_ap_id, user) do
306 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
307 {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
310 %{errors: [pinned_activities: {err, _}]} -> {:error, err}
311 _ -> {:error, dgettext("errors", "Could not unpin")}
315 def add_mute(user, activity) do
316 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
319 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
323 def remove_mute(user, activity) do
324 ThreadMute.remove_mute(user.id, activity.data["context"])
328 def thread_muted?(%{id: nil} = _user, _activity), do: false
330 def thread_muted?(user, activity) do
331 ThreadMute.check_muted(user.id, activity.data["context"]) != []
334 def report(user, %{"account_id" => account_id} = data) do
335 with {:ok, account} <- get_reported_account(account_id),
336 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
337 {:ok, statuses} <- get_report_statuses(account, data) do
339 context: Utils.generate_context_id(),
343 content: content_html,
344 forward: data["forward"] || false
349 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
351 defp get_reported_account(account_id) do
352 case User.get_cached_by_id(account_id) do
353 %User{} = account -> {:ok, account}
354 _ -> {:error, dgettext("errors", "Account not found")}
358 def update_report_state(activity_id, state) do
359 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
360 Utils.update_report_state(activity, state)
362 nil -> {:error, :not_found}
363 _ -> {:error, dgettext("errors", "Could not update state")}
367 def update_activity_scope(activity_id, opts \\ %{}) do
368 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
369 {:ok, activity} <- toggle_sensitive(activity, opts) do
370 set_visibility(activity, opts)
372 nil -> {:error, :not_found}
373 {:error, reason} -> {:error, reason}
377 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
378 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
381 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
382 when is_boolean(sensitive) do
383 new_data = Map.put(object.data, "sensitive", sensitive)
387 |> Object.change(%{data: new_data})
388 |> Object.update_and_set_cache()
390 {:ok, Map.put(activity, :object, object)}
393 defp toggle_sensitive(activity, _), do: {:ok, activity}
395 defp set_visibility(activity, %{"visibility" => visibility}) do
396 Utils.update_activity_visibility(activity, visibility)
399 defp set_visibility(activity, _), do: {:ok, activity}
401 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
402 if ap_id not in user.info.muted_reblogs do
403 User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
407 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
408 if ap_id in user.info.muted_reblogs do
409 User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))