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
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
20 alias Pleroma.Formatter
22 import Pleroma.Web.Gettext
23 import Pleroma.Web.CommonAPI.Utils
25 require Pleroma.Constants
28 def post_chat_message(%User{} = user, %User{} = recipient, content) do
30 Repo.transaction(fn ->
31 with {_, {:ok, chat_message_data, _meta}} <-
36 content |> Formatter.html_escape("text/plain")
38 {_, {:ok, chat_message_object}} <-
39 {:create_object, Object.create(chat_message_data)},
40 {_, {:ok, create_activity_data, _meta}} <-
41 {:build_create_activity,
42 Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])},
43 {_, {:ok, %Activity{} = activity, _meta}} <-
44 {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do
55 def follow(follower, followed) do
56 timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
58 with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
59 {:ok, activity} <- ActivityPub.follow(follower, followed),
60 {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
61 {:ok, follower, followed, activity}
65 def unfollow(follower, unfollowed) do
66 with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
67 {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
68 {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
73 def accept_follow_request(follower, followed) do
74 with {:ok, follower} <- User.follow(follower, followed),
75 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
76 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
77 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
82 object: follow_activity.data["id"],
89 def reject_follow_request(follower, followed) do
90 with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
91 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
92 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
97 object: follow_activity.data["id"],
104 def delete(activity_id, user) do
105 with {_, %Activity{data: %{"object" => _}} = activity} <-
106 {:find_activity, Activity.get_by_id_with_object(activity_id)},
107 %Object{} = object <- Object.normalize(activity),
108 true <- User.superuser?(user) || user.ap_id == object.data["actor"],
109 {:ok, _} <- unpin(activity_id, user),
110 {:ok, delete} <- ActivityPub.delete(object) do
113 {:find_activity, _} -> {:error, :not_found}
114 _ -> {:error, dgettext("errors", "Could not delete")}
118 def repeat(id_or_ap_id, user, params \\ %{}) do
119 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
120 object <- Object.normalize(activity),
121 announce_activity <- Utils.get_existing_announce(user.ap_id, object),
122 public <- public_announce?(object, params) do
123 if announce_activity do
124 {:ok, announce_activity, object}
126 ActivityPub.announce(user, object, nil, true, public)
129 {:find_activity, _} -> {:error, :not_found}
130 _ -> {:error, dgettext("errors", "Could not repeat")}
134 def unrepeat(id_or_ap_id, user) do
135 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
136 object = Object.normalize(activity)
137 ActivityPub.unannounce(user, object)
139 {:find_activity, _} -> {:error, :not_found}
140 _ -> {:error, dgettext("errors", "Could not unrepeat")}
144 @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
145 def favorite(%User{} = user, id) do
146 case favorite_helper(user, id) do
150 {:error, :not_found} = res ->
154 Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
155 {:error, dgettext("errors", "Could not favorite")}
159 def favorite_helper(user, id) do
160 with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
161 {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
162 {_, {:ok, %Activity{} = activity, _meta}} <-
164 Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
181 if {:object, {"already liked by this actor", []}} in changeset.errors do
182 {:ok, :already_liked}
192 def unfavorite(id_or_ap_id, user) do
193 with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do
194 object = Object.normalize(activity)
195 ActivityPub.unlike(user, object)
197 {:find_activity, _} -> {:error, :not_found}
198 _ -> {:error, dgettext("errors", "Could not unfavorite")}
202 def react_with_emoji(id, user, emoji) do
203 with %Activity{} = activity <- Activity.get_by_id(id),
204 object <- Object.normalize(activity) do
205 ActivityPub.react_with_emoji(user, object, emoji)
208 {:error, dgettext("errors", "Could not add reaction emoji")}
212 def unreact_with_emoji(id, user, emoji) do
213 with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
214 ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
217 {:error, dgettext("errors", "Could not remove reaction emoji")}
221 def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
222 with :ok <- validate_not_author(object, user),
223 :ok <- validate_existing_votes(user, object),
224 {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
226 Enum.map(choices, fn index ->
227 answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
230 ActivityPub.create(%{
231 to: answer_data["to"],
233 context: object.data["context"],
235 additional: %{"cc" => answer_data["cc"]}
241 object = Object.get_cached_by_ap_id(object.data["id"])
242 {:ok, answer_activities, object}
246 defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
247 do: {:error, dgettext("errors", "Poll's author can't vote")}
249 defp validate_not_author(_, _), do: :ok
251 defp validate_existing_votes(%{ap_id: ap_id}, object) do
252 if Utils.get_existing_votes(ap_id, object) == [] do
255 {:error, dgettext("errors", "Already voted")}
259 defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
260 defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
262 defp normalize_and_validate_choices(choices, object) do
263 choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
264 {options, max_count} = get_options_and_max_count(object)
265 count = Enum.count(options)
267 with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
268 {_, true} <- {:count_check, Enum.count(choices) <= max_count} do
269 {:ok, options, choices}
271 {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
272 {:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
276 def public_announce?(_, %{"visibility" => visibility})
277 when visibility in ~w{public unlisted private direct},
278 do: visibility in ~w(public unlisted)
280 def public_announce?(object, _) do
281 Visibility.is_public?(object)
284 def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
286 def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
287 when visibility in ~w{public unlisted private direct},
288 do: {visibility, get_replied_to_visibility(in_reply_to)}
290 def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
291 visibility = {:list, String.to_integer(list_id)}
292 {visibility, get_replied_to_visibility(in_reply_to)}
295 def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
296 visibility = get_replied_to_visibility(in_reply_to)
297 {visibility, visibility}
300 def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
302 def get_replied_to_visibility(nil), do: nil
304 def get_replied_to_visibility(activity) do
305 with %Object{} = object <- Object.normalize(activity) do
306 Visibility.get_visibility(object)
310 def check_expiry_date({:ok, nil} = res), do: res
312 def check_expiry_date({:ok, in_seconds}) do
313 expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
315 if ActivityExpiration.expires_late_enough?(expiry) do
318 {:error, "Expiry date is too soon"}
322 def check_expiry_date(expiry_str) do
323 Ecto.Type.cast(:integer, expiry_str)
324 |> check_expiry_date()
327 def listen(user, %{"title" => _} = data) do
328 with visibility <- data["visibility"] || "public",
329 {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
331 Map.take(data, ["album", "artist", "title", "length"])
332 |> Map.put("type", "Audio")
335 |> Map.put("actor", user.ap_id),
337 ActivityPub.listen(%{
341 context: Utils.generate_context_id(),
342 additional: %{"cc" => cc}
348 def post(user, %{"status" => _} = data) do
349 with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
351 |> ActivityPub.create(draft.preview?)
352 |> maybe_create_activity_expiration(draft.expires_at)
356 defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
357 with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
362 defp maybe_create_activity_expiration(result, _), do: result
364 # Updates the emojis for a user based on their profile
366 emoji = emoji_from_profile(user)
367 source_data = Map.put(user.source_data, "tag", emoji)
370 case User.update_source_data(user, source_data) do
375 ActivityPub.update(%{
377 to: [Pleroma.Constants.as_public(), user.follower_address],
380 object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
384 def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
387 data: %{"type" => "Create"},
388 object: %Object{data: %{"type" => object_type}}
389 } = activity <- get_by_id_or_ap_id(id_or_ap_id),
390 true <- object_type in ["Note", "Article", "Question"],
391 true <- Visibility.is_public?(activity),
392 {:ok, _user} <- User.add_pinnned_activity(user, activity) do
395 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
396 _ -> {:error, dgettext("errors", "Could not pin")}
400 def unpin(id_or_ap_id, user) do
401 with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
402 {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
405 {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
406 _ -> {:error, dgettext("errors", "Could not unpin")}
410 def add_mute(user, activity) do
411 with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
414 {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
418 def remove_mute(user, activity) do
419 ThreadMute.remove_mute(user.id, activity.data["context"])
423 def thread_muted?(%{id: nil} = _user, _activity), do: false
425 def thread_muted?(user, activity) do
426 ThreadMute.exists?(user.id, activity.data["context"])
429 def report(user, %{"account_id" => account_id} = data) do
430 with {:ok, account} <- get_reported_account(account_id),
431 {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
432 {:ok, statuses} <- get_report_statuses(account, data) do
434 context: Utils.generate_context_id(),
438 content: content_html,
439 forward: data["forward"] || false
444 def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
446 defp get_reported_account(account_id) do
447 case User.get_cached_by_id(account_id) do
448 %User{} = account -> {:ok, account}
449 _ -> {:error, dgettext("errors", "Account not found")}
453 def update_report_state(activity_ids, state) when is_list(activity_ids) do
454 case Utils.update_report_state(activity_ids, state) do
455 :ok -> {:ok, activity_ids}
456 _ -> {:error, dgettext("errors", "Could not update state")}
460 def update_report_state(activity_id, state) do
461 with %Activity{} = activity <- Activity.get_by_id(activity_id) do
462 Utils.update_report_state(activity, state)
464 nil -> {:error, :not_found}
465 _ -> {:error, dgettext("errors", "Could not update state")}
469 def update_activity_scope(activity_id, opts \\ %{}) do
470 with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
471 {:ok, activity} <- toggle_sensitive(activity, opts) do
472 set_visibility(activity, opts)
474 nil -> {:error, :not_found}
475 {:error, reason} -> {:error, reason}
479 defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
480 toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
483 defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
484 when is_boolean(sensitive) do
485 new_data = Map.put(object.data, "sensitive", sensitive)
489 |> Object.change(%{data: new_data})
490 |> Object.update_and_set_cache()
492 {:ok, Map.put(activity, :object, object)}
495 defp toggle_sensitive(activity, _), do: {:ok, activity}
497 defp set_visibility(activity, %{"visibility" => visibility}) do
498 Utils.update_activity_visibility(activity, visibility)
501 defp set_visibility(activity, _), do: {:ok, activity}
503 def hide_reblogs(%User{} = user, %User{} = target) do
504 UserRelationship.create_reblog_mute(user, target)
507 def show_reblogs(%User{} = user, %User{} = target) do
508 UserRelationship.delete_reblog_mute(user, target)