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, delete_data, _} <- Builder.delete(user, object.data["id"]),
83 {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) 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),
181 {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
182 {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
186 {:error, dgettext("errors", "Could not add reaction emoji")}
190 def unreact_with_emoji(id, user, emoji) do
191 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
192 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
195 {:error, dgettext("errors", "Could not remove reaction emoji")}
199 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
200 with :ok <- validate_not_author(object, user),
201 :ok <- validate_existing_votes(user, object),
202 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
204 Enum.map(choices, fn index ->
205 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
208 ActivityPub.create(%{
209 to: answer_data["to"],
211 context: object.data["context"],
213 additional: %{"cc" => answer_data["cc"]}
219 object = Object.get_cached_by_ap_id(object.data["id"])
220 {:ok, answer_activities, object}
224 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
225 do: {:error, dgettext("errors", "Poll's author can't vote")}
227 defp validate_not_author(_, _), do: :ok
229 defp validate_existing_votes(%{ap_id: ap_id}, object) do
230 if Utils.get_existing_votes(ap_id, object) == [] do
233 {:error, dgettext("errors", "Already voted")}
237 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
238 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
240 defp normalize_and_validate_choices(choices, object) do
241 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
242 {options, max_count} = get_options_and_max_count(object)
243 count = Enum.count(options)
245 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
246 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
247 {:ok, options, choices}
249 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
250 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
254 def public_announce?(_, %{"visibility" => visibility})
255 when visibility in ~w{public unlisted private direct},
256 do: visibility in ~w(public unlisted)
258 def public_announce?(object, _) do
259 Visibility.is_public?(object)
262 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
264 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
265 when visibility in ~w{public unlisted private direct},
266 do: {visibility, get_replied_to_visibility(in_reply_to)}
268 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
269 visibility = {:list, String.to_integer(list_id)}
270 {visibility, get_replied_to_visibility(in_reply_to)}
273 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
274 visibility = get_replied_to_visibility(in_reply_to)
275 {visibility, visibility}
278 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
280 def get_replied_to_visibility(nil), do: nil
282 def get_replied_to_visibility(activity) do
283 with %Object{} = object <- Object.normalize(activity) do
284 Visibility.get_visibility(object)
288 def check_expiry_date({:ok, nil} = res), do: res
290 def check_expiry_date({:ok, in_seconds}) do
291 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
293 if ActivityExpiration.expires_late_enough?(expiry) do
296 {:error, "Expiry date is too soon"}
300 def check_expiry_date(expiry_str) do
301 Ecto.Type.cast(:integer, expiry_str)
302 |> check_expiry_date()
305 def listen(user, %{"title" => _} = data) do
306 with visibility <- data["visibility"] || "public",
307 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
309 Map.take(data, ["album", "artist", "title", "length"])
310 |> Map.put("type", "Audio")
313 |> Map.put("actor", user.ap_id),
315 ActivityPub.listen(%{
319 context: Utils.generate_context_id(),
320 additional: %{"cc" => cc}
326 def post(user, %{"status" => _} = data) do
327 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
329 |> ActivityPub.create(draft.preview?)
330 |> maybe_create_activity_expiration(draft.expires_at)
334 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
335 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
340 defp maybe_create_activity_expiration(result, _), do: result
342 def pin(id, %{ap_id: user_ap_id} = user) do
345 data: %{"type" => "Create"},
346 object: %Object{data: %{"type" => object_type}}
347 } = activity <- Activity.get_by_id_with_object(id),
348 true <- object_type in ["Note", "Article", "Question"],
349 true <- Visibility.is_public?(activity),
350 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
353 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
354 _ -> {:error, dgettext("errors", "Could not pin")}
358 def unpin(id, user) do
359 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
360 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
363 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
364 _ -> {:error, dgettext("errors", "Could not unpin")}
368 def add_mute(user, activity) do
369 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
372 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
376 def remove_mute(user, activity) do
377 ThreadMute.remove_mute(user.id, activity.data["context"])
381 def thread_muted?(%{id: nil} = _user, _activity), do: false
383 def thread_muted?(user, activity) do
384 ThreadMute.exists?(user.id, activity.data["context"])
387 def report(user, data) do
388 with {:ok, account} <- get_reported_account(data.account_id),
389 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
390 {:ok, statuses} <- get_report_statuses(account, data) do
392 context: Utils.generate_context_id(),
396 content: content_html,
397 forward: Map.get(data, :forward, false)
402 defp get_reported_account(account_id) do
403 case User.get_cached_by_id(account_id) do
404 %User{} = account -> {:ok, account}
405 _ -> {:error, dgettext("errors", "Account not found")}
409 def update_report_state(activity_ids, state) when is_list(activity_ids) do
410 case Utils.update_report_state(activity_ids, state) do
411 :ok -> {:ok, activity_ids}
412 _ -> {:error, dgettext("errors", "Could not update state")}
416 def update_report_state(activity_id, state) do
417 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
418 Utils.update_report_state(activity, state)
420 nil -> {:error, :not_found}
421 _ -> {:error, dgettext("errors", "Could not update state")}
425 def update_activity_scope(activity_id, opts \\ %{}) do
426 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
427 {:ok, activity} <- toggle_sensitive(activity, opts) do
428 set_visibility(activity, opts)
430 nil -> {:error, :not_found}
431 {:error, reason} -> {:error, reason}
435 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
436 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
439 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
440 when is_boolean(sensitive) do
441 new_data = Map.put(object.data, "sensitive", sensitive)
445 |> Object.change(%{data: new_data})
446 |> Object.update_and_set_cache()
448 {:ok, Map.put(activity, :object, object)}
451 defp toggle_sensitive(activity, _), do: {:ok, activity}
453 defp set_visibility(activity, %{"visibility" => visibility}) do
454 Utils.update_activity_visibility(activity, visibility)
457 defp set_visibility(activity, _), do: {:ok, activity}
459 def hide_reblogs(%User{} = user, %User{} = target) do
460 UserRelationship.create_reblog_mute(user, target)
463 def show_reblogs(%User{} = user, %User{} = target) do
464 UserRelationship.delete_reblog_mute(user, target)