1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.Builder
16 alias Pleroma.Web.ActivityPub.Pipeline
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.ActivityPub.Visibility
20 import Pleroma.Web.Gettext
21 import Pleroma.Web.CommonAPI.Utils
23 require Pleroma.Constants
26 def follow(follower, followed) do
27 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
29 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
30 {:ok, activity} <- ActivityPub.follow(follower, followed),
31 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
32 {:ok, follower, followed, activity}
36 def unfollow(follower, unfollowed) do
37 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
38 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
39 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
44 def accept_follow_request(follower, followed) do
45 with {:ok, follower} <- User.follow(follower, followed),
46 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
47 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
48 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
53 object: follow_activity.data["id"],
60 def reject_follow_request(follower, followed) do
61 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
62 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
63 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
68 object: follow_activity.data["id"],
75 def delete(activity_id, user) do
76 with {_, %Activity{data: %{"object" => _}} = activity} <-
77 {:find_activity, Activity.get_by_id_with_object(activity_id)},
78 %Object{} = object <- Object.normalize(activity),
79 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
80 {:ok, _} <- unpin(activity_id, user),
81 {:ok, delete} <- ActivityPub.delete(object) do
84 {:find_activity, _} -> {:error, :not_found}
85 _ -> {:error, dgettext("errors", "Could not delete")}
89 def repeat(id_or_ap_id, user, params \\ %{}) do
90 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
91 object <- Object.normalize(activity),
92 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
93 public <- public_announce?(object, params) do
94 if announce_activity do
95 {:ok, announce_activity, object}
97 ActivityPub.announce(user, object, nil, true, public)
100 {:find_activity, _} -> {:error, :not_found}
101 _ -> {:error, dgettext("errors", "Could not repeat")}
105 def unrepeat(id_or_ap_id, user) do
106 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
107 object = Object.normalize(activity)
108 ActivityPub.unannounce(user, object)
110 {:find_activity, _} -> {:error, :not_found}
111 _ -> {:error, dgettext("errors", "Could not unrepeat")}
115 @spec favorite(User.t(), binary()) :: {:ok, Activity.t()} | {:error, any()}
116 def favorite(%User{} = user, id) do
117 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
118 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
119 {_, {:ok, %Activity{} = activity, _meta}} <-
121 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
138 if {:object, {"already liked by this actor", []}} in changeset.errors do
139 {:ok, :already_liked}
141 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
142 {:error, dgettext("errors", "Could not favorite"), e}
146 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
147 {:error, dgettext("errors", "Could not favorite"), e}
151 def unfavorite(id_or_ap_id, user) do
152 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
153 object = Object.normalize(activity)
154 ActivityPub.unlike(user, object)
156 {:find_activity, _} -> {:error, :not_found}
157 _ -> {:error, dgettext("errors", "Could not unfavorite")}
161 def react_with_emoji(id, user, emoji) do
162 with %Activity{} = activity <- Activity.get_by_id(id),
163 object <- Object.normalize(activity) do
164 ActivityPub.react_with_emoji(user, object, emoji)
167 {:error, dgettext("errors", "Could not add reaction emoji")}
171 def unreact_with_emoji(id, user, emoji) do
172 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
173 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
176 {:error, dgettext("errors", "Could not remove reaction emoji")}
180 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
181 with :ok <- validate_not_author(object, user),
182 :ok <- validate_existing_votes(user, object),
183 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
185 Enum.map(choices, fn index ->
186 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
189 ActivityPub.create(%{
190 to: answer_data["to"],
192 context: object.data["context"],
194 additional: %{"cc" => answer_data["cc"]}
200 object = Object.get_cached_by_ap_id(object.data["id"])
201 {:ok, answer_activities, object}
205 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
206 do: {:error, dgettext("errors", "Poll's author can't vote")}
208 defp validate_not_author(_, _), do: :ok
210 defp validate_existing_votes(%{ap_id: ap_id}, object) do
211 if Utils.get_existing_votes(ap_id, object) == [] do
214 {:error, dgettext("errors", "Already voted")}
218 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
219 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
221 defp normalize_and_validate_choices(choices, object) do
222 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
223 {options, max_count} = get_options_and_max_count(object)
224 count = Enum.count(options)
226 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
227 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
228 {:ok, options, choices}
230 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
231 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
235 def public_announce?(_, %{"visibility" => visibility})
236 when visibility in ~w{public unlisted private direct},
237 do: visibility in ~w(public unlisted)
239 def public_announce?(object, _) do
240 Visibility.is_public?(object)
243 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
245 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
246 when visibility in ~w{public unlisted private direct},
247 do: {visibility, get_replied_to_visibility(in_reply_to)}
249 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
250 visibility = {:list, String.to_integer(list_id)}
251 {visibility, get_replied_to_visibility(in_reply_to)}
254 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
255 visibility = get_replied_to_visibility(in_reply_to)
256 {visibility, visibility}
259 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
261 def get_replied_to_visibility(nil), do: nil
263 def get_replied_to_visibility(activity) do
264 with %Object{} = object <- Object.normalize(activity) do
265 Visibility.get_visibility(object)
269 def check_expiry_date({:ok, nil} = res), do: res
271 def check_expiry_date({:ok, in_seconds}) do
272 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
274 if ActivityExpiration.expires_late_enough?(expiry) do
277 {:error, "Expiry date is too soon"}
281 def check_expiry_date(expiry_str) do
282 Ecto.Type.cast(:integer, expiry_str)
283 |> check_expiry_date()
286 def listen(user, %{"title" => _} = data) do
287 with visibility <- data["visibility"] || "public",
288 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
290 Map.take(data, ["album", "artist", "title", "length"])
291 |> Map.put("type", "Audio")
294 |> Map.put("actor", user.ap_id),
296 ActivityPub.listen(%{
300 context: Utils.generate_context_id(),
301 additional: %{"cc" => cc}
307 def post(user, %{"status" => _} = data) do
308 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
310 |> ActivityPub.create(draft.preview?)
311 |> maybe_create_activity_expiration(draft.expires_at)
315 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
316 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
321 defp maybe_create_activity_expiration(result, _), do: result
323 # Updates the emojis for a user based on their profile
325 emoji = emoji_from_profile(user)
326 source_data = Map.put(user.source_data, "tag", emoji)
329 case User.update_source_data(user, source_data) do
334 ActivityPub.update(%{
336 to: [Pleroma.Constants.as_public(), user.follower_address],
339 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
343 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
346 data: %{"type" => "Create"},
347 object: %Object{data: %{"type" => object_type}}
348 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
349 true <- object_type in ["Note", "Article", "Question"],
350 true <- Visibility.is_public?(activity),
351 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
354 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
355 _ -> {:error, dgettext("errors", "Could not pin")}
359 def unpin(id_or_ap_id, user) do
360 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
361 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
364 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
365 _ -> {:error, dgettext("errors", "Could not unpin")}
369 def add_mute(user, activity) do
370 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
373 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
377 def remove_mute(user, activity) do
378 ThreadMute.remove_mute(user.id, activity.data["context"])
382 def thread_muted?(%{id: nil} = _user, _activity), do: false
384 def thread_muted?(user, activity) do
385 ThreadMute.check_muted(user.id, activity.data["context"]) != []
388 def report(user, %{"account_id" => account_id} = data) do
389 with {:ok, account} <- get_reported_account(account_id),
390 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
391 {:ok, statuses} <- get_report_statuses(account, data) do
393 context: Utils.generate_context_id(),
397 content: content_html,
398 forward: data["forward"] || false
403 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
405 defp get_reported_account(account_id) do
406 case User.get_cached_by_id(account_id) do
407 %User{} = account -> {:ok, account}
408 _ -> {:error, dgettext("errors", "Account not found")}
412 def update_report_state(activity_ids, state) when is_list(activity_ids) do
413 case Utils.update_report_state(activity_ids, state) do
414 :ok -> {:ok, activity_ids}
415 _ -> {:error, dgettext("errors", "Could not update state")}
419 def update_report_state(activity_id, state) do
420 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
421 Utils.update_report_state(activity, state)
423 nil -> {:error, :not_found}
424 _ -> {:error, dgettext("errors", "Could not update state")}
428 def update_activity_scope(activity_id, opts \\ %{}) do
429 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
430 {:ok, activity} <- toggle_sensitive(activity, opts) do
431 set_visibility(activity, opts)
433 nil -> {:error, :not_found}
434 {:error, reason} -> {:error, reason}
438 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
439 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
442 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
443 when is_boolean(sensitive) do
444 new_data = Map.put(object.data, "sensitive", sensitive)
448 |> Object.change(%{data: new_data})
449 |> Object.update_and_set_cache()
451 {:ok, Map.put(activity, :object, object)}
454 defp toggle_sensitive(activity, _), do: {:ok, activity}
456 defp set_visibility(activity, %{"visibility" => visibility}) do
457 Utils.update_activity_visibility(activity, visibility)
460 defp set_visibility(activity, _), do: {:ok, activity}
462 def hide_reblogs(%User{} = user, %User{} = target) do
463 UserRelationship.create_reblog_mute(user, target)
466 def show_reblogs(%User{} = user, %User{} = target) do
467 UserRelationship.delete_reblog_mute(user, target)