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 favorite(id_or_ap_id, user) do
119 # with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
120 # object <- Object.normalize(activity),
121 # nil <- Utils.get_existing_like(user.ap_id, object) do
122 # ActivityPub.like(user, object)
124 # _ -> {:error, dgettext("errors", "Could not favorite")}
128 def unfavorite(id_or_ap_id, user) do
129 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
130 object = Object.normalize(activity)
131 ActivityPub.unlike(user, object)
133 _ -> {:error, dgettext("errors", "Could not unfavorite")}
137 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
138 with :ok <- validate_not_author(object, user),
139 :ok <- validate_existing_votes(user, object),
140 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
142 Enum.map(choices, fn index ->
143 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
146 ActivityPub.create(%{
147 to: answer_data["to"],
149 context: object.data["context"],
151 additional: %{"cc" => answer_data["cc"]}
157 object = Object.get_cached_by_ap_id(object.data["id"])
158 {:ok, answer_activities, object}
162 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
163 do: {:error, dgettext("errors", "Poll's author can't vote")}
165 defp validate_not_author(_, _), do: :ok
167 defp validate_existing_votes(%{ap_id: ap_id}, object) do
168 if Utils.get_existing_votes(ap_id, object) == [] do
171 {:error, dgettext("errors", "Already voted")}
175 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
176 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
178 defp normalize_and_validate_choices(choices, object) do
179 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
180 {options, max_count} = get_options_and_max_count(object)
181 count = Enum.count(options)
183 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
184 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
185 {:ok, options, choices}
187 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
188 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
192 def public_announce?(_, %{"visibility" => visibility})
193 when visibility in ~w{public unlisted private direct},
194 do: visibility in ~w(public unlisted)
196 def public_announce?(object, _) do
197 Visibility.is_public?(object)
200 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
202 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
203 when visibility in ~w{public unlisted private direct},
204 do: {visibility, get_replied_to_visibility(in_reply_to)}
206 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
207 visibility = {:list, String.to_integer(list_id)}
208 {visibility, get_replied_to_visibility(in_reply_to)}
211 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
212 visibility = get_replied_to_visibility(in_reply_to)
213 {visibility, visibility}
216 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
218 def get_replied_to_visibility(nil), do: nil
220 def get_replied_to_visibility(activity) do
221 with %Object{} = object <- Object.normalize(activity) do
222 Visibility.get_visibility(object)
226 def check_expiry_date({:ok, nil} = res), do: res
228 def check_expiry_date({:ok, in_seconds}) do
229 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
231 if ActivityExpiration.expires_late_enough?(expiry) do
234 {:error, "Expiry date is too soon"}
238 def check_expiry_date(expiry_str) do
239 Ecto.Type.cast(:integer, expiry_str)
240 |> check_expiry_date()
243 def listen(user, %{"title" => _} = data) do
244 with visibility <- data["visibility"] || "public",
245 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
247 Map.take(data, ["album", "artist", "title", "length"])
248 |> Map.put("type", "Audio")
251 |> Map.put("actor", user.ap_id),
253 ActivityPub.listen(%{
257 context: Utils.generate_context_id(),
258 additional: %{"cc" => cc}
264 def post(user, %{"status" => _} = data) do
265 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
267 |> ActivityPub.create(draft.preview?)
268 |> maybe_create_activity_expiration(draft.expires_at)
272 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
273 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
278 defp maybe_create_activity_expiration(result, _), do: result
280 # Updates the emojis for a user based on their profile
282 emoji = emoji_from_profile(user)
283 source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)
286 case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
291 ActivityPub.update(%{
293 to: [Pleroma.Constants.as_public(), user.follower_address],
296 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
300 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
303 data: %{"type" => "Create"},
304 object: %Object{data: %{"type" => "Note"}}
305 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
306 true <- Visibility.is_public?(activity),
307 {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
310 {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
311 _ -> {:error, dgettext("errors", "Could not pin")}
315 def unpin(id_or_ap_id, user) do
316 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
317 {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
320 %{errors: [pinned_activities: {err, _}]} -> {:error, err}
321 _ -> {:error, dgettext("errors", "Could not unpin")}
325 def add_mute(user, activity) do
326 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
329 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
333 def remove_mute(user, activity) do
334 ThreadMute.remove_mute(user.id, activity.data["context"])
338 def thread_muted?(%{id: nil} = _user, _activity), do: false
340 def thread_muted?(user, activity) do
341 ThreadMute.check_muted(user.id, activity.data["context"]) != []
344 def report(user, %{"account_id" => account_id} = data) do
345 with {:ok, account} <- get_reported_account(account_id),
346 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
347 {:ok, statuses} <- get_report_statuses(account, data) do
349 context: Utils.generate_context_id(),
353 content: content_html,
354 forward: data["forward"] || false
359 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
361 defp get_reported_account(account_id) do
362 case User.get_cached_by_id(account_id) do
363 %User{} = account -> {:ok, account}
364 _ -> {:error, dgettext("errors", "Account not found")}
368 def update_report_state(activity_id, state) do
369 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
370 Utils.update_report_state(activity, state)
372 nil -> {:error, :not_found}
373 _ -> {:error, dgettext("errors", "Could not update state")}
377 def update_activity_scope(activity_id, opts \\ %{}) do
378 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
379 {:ok, activity} <- toggle_sensitive(activity, opts) do
380 set_visibility(activity, opts)
382 nil -> {:error, :not_found}
383 {:error, reason} -> {:error, reason}
387 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
388 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
391 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
392 when is_boolean(sensitive) do
393 new_data = Map.put(object.data, "sensitive", sensitive)
397 |> Object.change(%{data: new_data})
398 |> Object.update_and_set_cache()
400 {:ok, Map.put(activity, :object, object)}
403 defp toggle_sensitive(activity, _), do: {:ok, activity}
405 defp set_visibility(activity, %{"visibility" => visibility}) do
406 Utils.update_activity_visibility(activity, visibility)
409 defp set_visibility(activity, _), do: {:ok, activity}
411 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
412 if ap_id not in user.info.muted_reblogs do
413 User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
417 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
418 if ap_id in user.info.muted_reblogs do
419 User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))