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
10 alias Pleroma.Notification
12 alias Pleroma.ThreadMute
14 alias Pleroma.UserRelationship
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.ActivityPub.Utils
19 alias Pleroma.Web.ActivityPub.Visibility
21 import Pleroma.Web.Gettext
22 import Pleroma.Web.CommonAPI.Utils
24 require Pleroma.Constants
27 def follow(follower, followed) do
28 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
30 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
31 {:ok, activity} <- ActivityPub.follow(follower, followed),
32 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
33 {:ok, follower, followed, activity}
37 def unfollow(follower, unfollowed) do
38 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
39 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
40 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
45 def accept_follow_request(follower, followed) do
46 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
47 {:ok, follower} <- User.follow(follower, followed),
48 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
49 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
54 object: follow_activity.data["id"],
61 def reject_follow_request(follower, followed) do
62 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
63 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
64 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
65 {:ok, _notifications} <- Notification.dismiss(follow_activity),
70 object: follow_activity.data["id"],
77 def delete(activity_id, user) do
78 with {_, %Activity{data: %{"object" => _}} = activity} <-
79 {:find_activity, Activity.get_by_id_with_object(activity_id)},
80 %Object{} = object <- Object.normalize(activity),
81 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
82 {:ok, _} <- unpin(activity_id, user),
83 {:ok, delete} <- ActivityPub.delete(object) do
86 {:find_activity, _} -> {:error, :not_found}
87 _ -> {:error, dgettext("errors", "Could not delete")}
91 def repeat(id, user, params \\ %{}) do
92 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
93 {:find_activity, Activity.get_by_id(id)},
94 object <- Object.normalize(activity),
95 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
96 public <- public_announce?(object, params) do
97 if announce_activity do
98 {:ok, announce_activity, object}
100 ActivityPub.announce(user, object, nil, true, public)
103 {:find_activity, _} -> {:error, :not_found}
104 _ -> {:error, dgettext("errors", "Could not repeat")}
108 def unrepeat(id, user) do
109 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
110 {:find_activity, Activity.get_by_id(id)} do
111 object = Object.normalize(activity)
112 ActivityPub.unannounce(user, object)
114 {:find_activity, _} -> {:error, :not_found}
115 _ -> {:error, dgettext("errors", "Could not unrepeat")}
119 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
120 def favorite(%User{} = user, id) do
121 case favorite_helper(user, id) do
125 {:error, :not_found} = res ->
129 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
130 {:error, dgettext("errors", "Could not favorite")}
134 def favorite_helper(user, id) do
135 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
136 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
137 {_, {:ok, %Activity{} = activity, _meta}} <-
139 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
156 if {:object, {"already liked by this actor", []}} in changeset.errors do
157 {:ok, :already_liked}
167 def unfavorite(id, user) do
168 with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
169 {:find_activity, Activity.get_by_id(id)} do
170 object = Object.normalize(activity)
171 ActivityPub.unlike(user, object)
173 {:find_activity, _} -> {:error, :not_found}
174 _ -> {:error, dgettext("errors", "Could not unfavorite")}
178 def react_with_emoji(id, user, emoji) do
179 with %Activity{} = activity <- Activity.get_by_id(id),
180 object <- Object.normalize(activity) do
181 ActivityPub.react_with_emoji(user, object, emoji)
184 {:error, dgettext("errors", "Could not add reaction emoji")}
188 def unreact_with_emoji(id, user, emoji) do
189 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
190 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
193 {:error, dgettext("errors", "Could not remove reaction emoji")}
197 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
198 with :ok <- validate_not_author(object, user),
199 :ok <- validate_existing_votes(user, object),
200 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
202 Enum.map(choices, fn index ->
203 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
206 ActivityPub.create(%{
207 to: answer_data["to"],
209 context: object.data["context"],
211 additional: %{"cc" => answer_data["cc"]}
217 object = Object.get_cached_by_ap_id(object.data["id"])
218 {:ok, answer_activities, object}
222 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
223 do: {:error, dgettext("errors", "Poll's author can't vote")}
225 defp validate_not_author(_, _), do: :ok
227 defp validate_existing_votes(%{ap_id: ap_id}, object) do
228 if Utils.get_existing_votes(ap_id, object) == [] do
231 {:error, dgettext("errors", "Already voted")}
235 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
236 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
238 defp normalize_and_validate_choices(choices, object) do
239 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
240 {options, max_count} = get_options_and_max_count(object)
241 count = Enum.count(options)
243 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
244 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
245 {:ok, options, choices}
247 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
248 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
252 def public_announce?(_, %{"visibility" => visibility})
253 when visibility in ~w{public unlisted private direct},
254 do: visibility in ~w(public unlisted)
256 def public_announce?(object, _) do
257 Visibility.is_public?(object)
260 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
262 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
263 when visibility in ~w{public unlisted private direct},
264 do: {visibility, get_replied_to_visibility(in_reply_to)}
266 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
267 visibility = {:list, String.to_integer(list_id)}
268 {visibility, get_replied_to_visibility(in_reply_to)}
271 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
272 visibility = get_replied_to_visibility(in_reply_to)
273 {visibility, visibility}
276 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
278 def get_replied_to_visibility(nil), do: nil
280 def get_replied_to_visibility(activity) do
281 with %Object{} = object <- Object.normalize(activity) do
282 Visibility.get_visibility(object)
286 def check_expiry_date({:ok, nil} = res), do: res
288 def check_expiry_date({:ok, in_seconds}) do
289 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
291 if ActivityExpiration.expires_late_enough?(expiry) do
294 {:error, "Expiry date is too soon"}
298 def check_expiry_date(expiry_str) do
299 Ecto.Type.cast(:integer, expiry_str)
300 |> check_expiry_date()
303 def listen(user, %{"title" => _} = data) do
304 with visibility <- data["visibility"] || "public",
305 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
307 Map.take(data, ["album", "artist", "title", "length"])
308 |> Map.put("type", "Audio")
311 |> Map.put("actor", user.ap_id),
313 ActivityPub.listen(%{
317 context: Utils.generate_context_id(),
318 additional: %{"cc" => cc}
324 def post(user, %{"status" => _} = data) do
325 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
326 ActivityPub.create(draft.changes, draft.preview?)
330 def pin(id, %{ap_id: user_ap_id} = user) do
333 data: %{"type" => "Create"},
334 object: %Object{data: %{"type" => object_type}}
335 } = activity <- Activity.get_by_id_with_object(id),
336 true <- object_type in ["Note", "Article", "Question"],
337 true <- Visibility.is_public?(activity),
338 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
341 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
342 _ -> {:error, dgettext("errors", "Could not pin")}
346 def unpin(id, user) do
347 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
348 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
351 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
352 _ -> {:error, dgettext("errors", "Could not unpin")}
356 def add_mute(user, activity) do
357 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
360 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
364 def remove_mute(user, activity) do
365 ThreadMute.remove_mute(user.id, activity.data["context"])
369 def thread_muted?(%{id: nil} = _user, _activity), do: false
371 def thread_muted?(user, activity) do
372 ThreadMute.exists?(user.id, activity.data["context"])
375 def report(user, data) do
376 with {:ok, account} <- get_reported_account(data.account_id),
377 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
378 {:ok, statuses} <- get_report_statuses(account, data) do
380 context: Utils.generate_context_id(),
384 content: content_html,
385 forward: Map.get(data, :forward, false)
390 defp get_reported_account(account_id) do
391 case User.get_cached_by_id(account_id) do
392 %User{} = account -> {:ok, account}
393 _ -> {:error, dgettext("errors", "Account not found")}
397 def update_report_state(activity_ids, state) when is_list(activity_ids) do
398 case Utils.update_report_state(activity_ids, state) do
399 :ok -> {:ok, activity_ids}
400 _ -> {:error, dgettext("errors", "Could not update state")}
404 def update_report_state(activity_id, state) do
405 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
406 Utils.update_report_state(activity, state)
408 nil -> {:error, :not_found}
409 _ -> {:error, dgettext("errors", "Could not update state")}
413 def update_activity_scope(activity_id, opts \\ %{}) do
414 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
415 {:ok, activity} <- toggle_sensitive(activity, opts) do
416 set_visibility(activity, opts)
418 nil -> {:error, :not_found}
419 {:error, reason} -> {:error, reason}
423 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
424 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
427 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
428 when is_boolean(sensitive) do
429 new_data = Map.put(object.data, "sensitive", sensitive)
433 |> Object.change(%{data: new_data})
434 |> Object.update_and_set_cache()
436 {:ok, Map.put(activity, :object, object)}
439 defp toggle_sensitive(activity, _), do: {:ok, activity}
441 defp set_visibility(activity, %{"visibility" => visibility}) do
442 Utils.update_activity_visibility(activity, visibility)
445 defp set_visibility(activity, _), do: {:ok, activity}
447 def hide_reblogs(%User{} = user, %User{} = target) do
448 UserRelationship.create_reblog_mute(user, target)
451 def show_reblogs(%User{} = user, %User{} = target) do
452 UserRelationship.delete_reblog_mute(user, target)