akkoma/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
Oneric dff532ac72 api: ensure only visible posts are interactable
It doesn't make sense to like, react, reply, etc to something you cannot
see and is unexpected for the author of the interacted with post and
might make them believe the reacting user actually _can_ see the post.

Wrt to fav, reblog, reaction indexes the missing visibility check was
also leaking some (presumably/hopefully) low-severity data.

Add full-API test for all modes of interactions with private posts.
2025-11-22 00:00:00 +00:00

132 lines
4.1 KiB
Elixir

# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete])
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
when action == :index
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
Object.normalize(activity, fetch: false) do
reactions =
reactions
|> filter(params)
|> filter_allowed_users(user, Map.get(params, :with_muted, false))
render(conn, "index.json", emoji_reactions: reactions, user: user)
else
{:visible, _} -> {:error, :forbidden}
_e -> json(conn, [])
end
end
defp filter_allowed_user_by_ap_id(ap_ids, excluded_ap_ids) do
Enum.reject(ap_ids, fn ap_id ->
with false <- ap_id in excluded_ap_ids,
%{is_active: true} <- User.get_cached_by_ap_id(ap_id) do
false
else
_ -> true
end
end)
end
defp filter_allowed_users_by_domain(ap_ids, %User{} = for_user) do
Enum.reject(ap_ids, fn ap_id ->
User.blocks_domain?(for_user, ap_id)
end)
end
defp filter_allowed_users_by_domain(ap_ids, nil), do: ap_ids
def filter_allowed_users(reactions, user, with_muted) do
exclude_ap_ids =
if is_nil(user) do
[]
else
User.cached_blocked_users_ap_ids(user) ++
if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []
end
filter_emoji = fn emoji, users, url ->
users
|> filter_allowed_user_by_ap_id(exclude_ap_ids)
|> filter_allowed_users_by_domain(user)
|> case do
[] -> nil
users -> {emoji, users, url}
end
end
reactions
|> Stream.map(fn
[emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url)
{emoji, users, url} when is_list(users) -> filter_emoji.(emoji, users, url)
{emoji, users} when is_list(users) -> filter_emoji.(emoji, users, nil)
_ -> nil
end)
|> Stream.reject(&is_nil/1)
end
defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
Enum.filter(reactions, fn [e, _, _] -> e == emoji end)
end
defp filter(reactions, _), do: reactions
def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
emoji =
emoji
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()
# CommonAPI checks if allowed to react
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id)
conn
|> put_view(StatusView)
|> render("show.json", activity: activity, for: user, as: :activity)
end
end
def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
emoji =
emoji
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()
# CommonAPI checks only author can revoke reactions
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id)
conn
|> put_view(StatusView)
|> render("show.json", activity: activity, for: user, as: :activity)
end
end
end