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 vote(user, %{data: %{"type" => "Question"}} = object, choices) do
132 with :ok <- validate_not_author(object, user),
133 :ok <- validate_existing_votes(user, object),
134 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
136 Enum.map(choices, fn index ->
137 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
140 ActivityPub.create(%{
141 to: answer_data["to"],
143 context: object.data["context"],
145 additional: %{"cc" => answer_data["cc"]}
151 object = Object.get_cached_by_ap_id(object.data["id"])
152 {:ok, answer_activities, object}
156 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
157 do: {:error, dgettext("errors", "Poll's author can't vote")}
159 defp validate_not_author(_, _), do: :ok
161 defp validate_existing_votes(%{ap_id: ap_id}, object) do
162 if Utils.get_existing_votes(ap_id, object) == [] do
165 {:error, dgettext("errors", "Already voted")}
169 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
170 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
172 defp normalize_and_validate_choices(choices, object) do
173 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
174 {options, max_count} = get_options_and_max_count(object)
175 count = Enum.count(options)
177 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
178 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
179 {:ok, options, choices}
181 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
182 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
186 def public_announce?(_, %{"visibility" => visibility})
187 when visibility in ~w{public unlisted private direct},
188 do: visibility in ~w(public unlisted)
190 def public_announce?(object, _) do
191 Visibility.is_public?(object)
194 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
196 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
197 when visibility in ~w{public unlisted private direct},
198 do: {visibility, get_replied_to_visibility(in_reply_to)}
200 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
201 visibility = {:list, String.to_integer(list_id)}
202 {visibility, get_replied_to_visibility(in_reply_to)}
205 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
206 visibility = get_replied_to_visibility(in_reply_to)
207 {visibility, visibility}
210 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
212 def get_replied_to_visibility(nil), do: nil
214 def get_replied_to_visibility(activity) do
215 with %Object{} = object <- Object.normalize(activity) do
216 Visibility.get_visibility(object)
220 def check_expiry_date({:ok, nil} = res), do: res
222 def check_expiry_date({:ok, in_seconds}) do
223 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
225 if ActivityExpiration.expires_late_enough?(expiry) do
228 {:error, "Expiry date is too soon"}
232 def check_expiry_date(expiry_str) do
233 Ecto.Type.cast(:integer, expiry_str)
234 |> check_expiry_date()
237 def listen(user, %{"title" => _} = data) do
238 with visibility <- data["visibility"] || "public",
239 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
241 Map.take(data, ["album", "artist", "title", "length"])
242 |> Map.put("type", "Audio")
245 |> Map.put("actor", user.ap_id),
247 ActivityPub.listen(%{
251 context: Utils.generate_context_id(),
252 additional: %{"cc" => cc}
258 def post(user, %{"status" => _} = data) do
259 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
261 |> ActivityPub.create(draft.preview?)
262 |> maybe_create_activity_expiration(draft.expires_at)
266 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
267 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
272 defp maybe_create_activity_expiration(result, _), do: result
274 # Updates the emojis for a user based on their profile
276 emoji = emoji_from_profile(user)
277 source_data = Map.put(user.source_data, "tag", emoji)
280 case User.update_source_data(user, source_data) do
285 ActivityPub.update(%{
287 to: [Pleroma.Constants.as_public(), user.follower_address],
290 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
294 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
297 data: %{"type" => "Create"},
298 object: %Object{data: %{"type" => "Note"}}
299 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
300 true <- Visibility.is_public?(activity),
301 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
304 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
305 _ -> {:error, dgettext("errors", "Could not pin")}
309 def unpin(id_or_ap_id, user) do
310 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
311 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
314 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
315 _ -> {:error, dgettext("errors", "Could not unpin")}
319 def add_mute(user, activity) do
320 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
323 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
327 def remove_mute(user, activity) do
328 ThreadMute.remove_mute(user.id, activity.data["context"])
332 def thread_muted?(%{id: nil} = _user, _activity), do: false
334 def thread_muted?(user, activity) do
335 ThreadMute.check_muted(user.id, activity.data["context"]) != []
338 def report(user, %{"account_id" => account_id} = data) do
339 with {:ok, account} <- get_reported_account(account_id),
340 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
341 {:ok, statuses} <- get_report_statuses(account, data) do
343 context: Utils.generate_context_id(),
347 content: content_html,
348 forward: data["forward"] || false
353 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
355 defp get_reported_account(account_id) do
356 case User.get_cached_by_id(account_id) do
357 %User{} = account -> {:ok, account}
358 _ -> {:error, dgettext("errors", "Account not found")}
362 def update_report_state(activity_id, state) do
363 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
364 Utils.update_report_state(activity, state)
366 nil -> {:error, :not_found}
367 _ -> {:error, dgettext("errors", "Could not update state")}
371 def update_activity_scope(activity_id, opts \\ %{}) do
372 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
373 {:ok, activity} <- toggle_sensitive(activity, opts) do
374 set_visibility(activity, opts)
376 nil -> {:error, :not_found}
377 {:error, reason} -> {:error, reason}
381 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
382 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
385 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
386 when is_boolean(sensitive) do
387 new_data = Map.put(object.data, "sensitive", sensitive)
391 |> Object.change(%{data: new_data})
392 |> Object.update_and_set_cache()
394 {:ok, Map.put(activity, :object, object)}
397 defp toggle_sensitive(activity, _), do: {:ok, activity}
399 defp set_visibility(activity, %{"visibility" => visibility}) do
400 Utils.update_activity_visibility(activity, visibility)
403 defp set_visibility(activity, _), do: {:ok, activity}
405 def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
406 if ap_id not in user.muted_reblogs do
407 User.add_reblog_mute(user, ap_id)
411 def show_reblogs(user, %{ap_id: ap_id} = _muted) do
412 if ap_id in user.muted_reblogs do
413 User.remove_reblog_mute(user, ap_id)