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)},
170 %Object{} = note <- Object.normalize(activity, false),
171 %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
172 {:ok, undo, _} <- Builder.undo(user, like),
173 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
176 {:find_activity, _} -> {:error, :not_found}
177 _ -> {:error, dgettext("errors", "Could not unfavorite")}
181 def react_with_emoji(id, user, emoji) do
182 with %Activity{} = activity <- Activity.get_by_id(id),
183 object <- Object.normalize(activity) do
184 ActivityPub.react_with_emoji(user, object, emoji)
187 {:error, dgettext("errors", "Could not add reaction emoji")}
191 def unreact_with_emoji(id, user, emoji) do
192 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
193 {:ok, undo, _} <- Builder.undo(user, reaction_activity),
194 {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
198 {:error, dgettext("errors", "Could not remove reaction emoji")}
202 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
203 with :ok <- validate_not_author(object, user),
204 :ok <- validate_existing_votes(user, object),
205 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
207 Enum.map(choices, fn index ->
208 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
211 ActivityPub.create(%{
212 to: answer_data["to"],
214 context: object.data["context"],
216 additional: %{"cc" => answer_data["cc"]}
222 object = Object.get_cached_by_ap_id(object.data["id"])
223 {:ok, answer_activities, object}
227 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
228 do: {:error, dgettext("errors", "Poll's author can't vote")}
230 defp validate_not_author(_, _), do: :ok
232 defp validate_existing_votes(%{ap_id: ap_id}, object) do
233 if Utils.get_existing_votes(ap_id, object) == [] do
236 {:error, dgettext("errors", "Already voted")}
240 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
241 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
243 defp normalize_and_validate_choices(choices, object) do
244 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
245 {options, max_count} = get_options_and_max_count(object)
246 count = Enum.count(options)
248 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
249 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
250 {:ok, options, choices}
252 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
253 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
257 def public_announce?(_, %{"visibility" => visibility})
258 when visibility in ~w{public unlisted private direct},
259 do: visibility in ~w(public unlisted)
261 def public_announce?(object, _) do
262 Visibility.is_public?(object)
265 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
267 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
268 when visibility in ~w{public unlisted private direct},
269 do: {visibility, get_replied_to_visibility(in_reply_to)}
271 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
272 visibility = {:list, String.to_integer(list_id)}
273 {visibility, get_replied_to_visibility(in_reply_to)}
276 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
277 visibility = get_replied_to_visibility(in_reply_to)
278 {visibility, visibility}
281 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
283 def get_replied_to_visibility(nil), do: nil
285 def get_replied_to_visibility(activity) do
286 with %Object{} = object <- Object.normalize(activity) do
287 Visibility.get_visibility(object)
291 def check_expiry_date({:ok, nil} = res), do: res
293 def check_expiry_date({:ok, in_seconds}) do
294 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
296 if ActivityExpiration.expires_late_enough?(expiry) do
299 {:error, "Expiry date is too soon"}
303 def check_expiry_date(expiry_str) do
304 Ecto.Type.cast(:integer, expiry_str)
305 |> check_expiry_date()
308 def listen(user, %{"title" => _} = data) do
309 with visibility <- data["visibility"] || "public",
310 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
312 Map.take(data, ["album", "artist", "title", "length"])
313 |> Map.put("type", "Audio")
316 |> Map.put("actor", user.ap_id),
318 ActivityPub.listen(%{
322 context: Utils.generate_context_id(),
323 additional: %{"cc" => cc}
329 def post(user, %{"status" => _} = data) do
330 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
332 |> ActivityPub.create(draft.preview?)
333 |> maybe_create_activity_expiration(draft.expires_at)
337 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
338 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
343 defp maybe_create_activity_expiration(result, _), do: result
345 def pin(id, %{ap_id: user_ap_id} = user) do
348 data: %{"type" => "Create"},
349 object: %Object{data: %{"type" => object_type}}
350 } = activity <- Activity.get_by_id_with_object(id),
351 true <- object_type in ["Note", "Article", "Question"],
352 true <- Visibility.is_public?(activity),
353 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
356 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
357 _ -> {:error, dgettext("errors", "Could not pin")}
361 def unpin(id, user) do
362 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
363 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
366 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
367 _ -> {:error, dgettext("errors", "Could not unpin")}
371 def add_mute(user, activity) do
372 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
375 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
379 def remove_mute(user, activity) do
380 ThreadMute.remove_mute(user.id, activity.data["context"])
384 def thread_muted?(%{id: nil} = _user, _activity), do: false
386 def thread_muted?(user, activity) do
387 ThreadMute.exists?(user.id, activity.data["context"])
390 def report(user, data) do
391 with {:ok, account} <- get_reported_account(data.account_id),
392 {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
393 {:ok, statuses} <- get_report_statuses(account, data) do
395 context: Utils.generate_context_id(),
399 content: content_html,
400 forward: Map.get(data, :forward, false)
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)