Mastodon API: ensure the notification endpoint doesn't return less than the requested amount of records unless it's the last page
This commit is contained in:
parent
271ea5068f
commit
b15cfc3d36
8 changed files with 112 additions and 41 deletions
|
@ -49,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Filtering of push notifications on activities from blocked domains
|
- Filtering of push notifications on activities from blocked domains
|
||||||
- Resolving Peertube accounts with Webfinger
|
- Resolving Peertube accounts with Webfinger
|
||||||
- `blob:` urls not being allowed by connect-src CSP
|
- `blob:` urls not being allowed by connect-src CSP
|
||||||
|
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
|
||||||
|
|
||||||
## [Unreleased (patch)]
|
## [Unreleased (patch)]
|
||||||
|
|
||||||
|
|
|
@ -166,8 +166,16 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||||
query
|
query
|
||||||
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
|
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
|
||||||
on:
|
on:
|
||||||
fragment("?->>'context'", a.data) ==
|
fragment(
|
||||||
fragment("?->>'context'", mutated_activity.data) and
|
"COALESCE((?->'object')->>'id', ?->>'object')",
|
||||||
|
a.data,
|
||||||
|
a.data
|
||||||
|
) ==
|
||||||
|
fragment(
|
||||||
|
"COALESCE((?->'object')->>'id', ?->>'object')",
|
||||||
|
mutated_activity.data,
|
||||||
|
mutated_activity.data
|
||||||
|
) and
|
||||||
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
|
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
|
||||||
fragment("?->>'type'", mutated_activity.data) == "Create",
|
fragment("?->>'type'", mutated_activity.data) == "Create",
|
||||||
as: :mutated_activity
|
as: :mutated_activity
|
||||||
|
@ -541,6 +549,7 @@ def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
|
||||||
def skip?(%Activity{} = activity, %User{} = user) do
|
def skip?(%Activity{} = activity, %User{} = user) do
|
||||||
[
|
[
|
||||||
:self,
|
:self,
|
||||||
|
:invisible,
|
||||||
:followers,
|
:followers,
|
||||||
:follows,
|
:follows,
|
||||||
:non_followers,
|
:non_followers,
|
||||||
|
@ -557,6 +566,12 @@ def skip?(:self, %Activity{} = activity, %User{} = user) do
|
||||||
activity.data["actor"] == user.ap_id
|
activity.data["actor"] == user.ap_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def skip?(:invisible, %Activity{} = activity, _) do
|
||||||
|
actor = activity.data["actor"]
|
||||||
|
user = User.get_cached_by_ap_id(actor)
|
||||||
|
User.invisible?(user)
|
||||||
|
end
|
||||||
|
|
||||||
def skip?(
|
def skip?(
|
||||||
:followers,
|
:followers,
|
||||||
%Activity{} = activity,
|
%Activity{} = activity,
|
||||||
|
|
|
@ -1488,6 +1488,7 @@ def perform(:delete, %User{} = user) do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
delete_user_activities(user)
|
delete_user_activities(user)
|
||||||
|
delete_notifications_from_user_activities(user)
|
||||||
|
|
||||||
delete_outgoing_pending_follow_requests(user)
|
delete_outgoing_pending_follow_requests(user)
|
||||||
|
|
||||||
|
@ -1576,6 +1577,13 @@ def follow_import(%User{} = follower, followed_identifiers)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
|
||||||
|
Notification
|
||||||
|
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||||
|
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
|
||||||
|
|> Repo.delete_all()
|
||||||
|
end
|
||||||
|
|
||||||
def delete_user_activities(%User{ap_id: ap_id} = user) do
|
def delete_user_activities(%User{ap_id: ap_id} = user) do
|
||||||
ap_id
|
ap_id
|
||||||
|> Activity.Queries.by_actor()
|
|> Activity.Queries.by_actor()
|
||||||
|
|
|
@ -46,6 +46,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
|
||||||
activities
|
activities
|
||||||
|> Enum.filter(&(&1.data["type"] == "Move"))
|
|> Enum.filter(&(&1.data["type"] == "Move"))
|
||||||
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
|
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
actors =
|
actors =
|
||||||
activities
|
activities
|
||||||
|
@ -84,50 +85,45 @@ def render(
|
||||||
# Note: :relationships contain user mutes (needed for :muted flag in :status)
|
# Note: :relationships contain user mutes (needed for :muted flag in :status)
|
||||||
status_render_opts = %{relationships: opts[:relationships]}
|
status_render_opts = %{relationships: opts[:relationships]}
|
||||||
|
|
||||||
with %{id: _} = account <-
|
account =
|
||||||
AccountView.render(
|
AccountView.render(
|
||||||
"show.json",
|
"show.json",
|
||||||
%{user: actor, for: reading_user}
|
%{user: actor, for: reading_user}
|
||||||
) do
|
)
|
||||||
response = %{
|
|
||||||
id: to_string(notification.id),
|
response = %{
|
||||||
type: notification.type,
|
id: to_string(notification.id),
|
||||||
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
type: notification.type,
|
||||||
account: account,
|
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
||||||
pleroma: %{
|
account: account,
|
||||||
is_seen: notification.seen
|
pleroma: %{
|
||||||
}
|
is_seen: notification.seen
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case notification.type do
|
case notification.type do
|
||||||
"mention" ->
|
"mention" ->
|
||||||
put_status(response, activity, reading_user, status_render_opts)
|
put_status(response, activity, reading_user, status_render_opts)
|
||||||
|
|
||||||
"favourite" ->
|
"favourite" ->
|
||||||
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
||||||
|
|
||||||
"reblog" ->
|
"reblog" ->
|
||||||
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
|
||||||
|
|
||||||
"move" ->
|
"move" ->
|
||||||
put_target(response, activity, reading_user, %{})
|
put_target(response, activity, reading_user, %{})
|
||||||
|
|
||||||
"pleroma:emoji_reaction" ->
|
"pleroma:emoji_reaction" ->
|
||||||
response
|
response
|
||||||
|> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|
|> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|
||||||
|> put_emoji(activity)
|
|> put_emoji(activity)
|
||||||
|
|
||||||
"pleroma:chat_mention" ->
|
"pleroma:chat_mention" ->
|
||||||
put_chat_message(response, activity, reading_user, status_render_opts)
|
put_chat_message(response, activity, reading_user, status_render_opts)
|
||||||
|
|
||||||
type when type in ["follow", "follow_request"] ->
|
type when type in ["follow", "follow_request"] ->
|
||||||
response
|
response
|
||||||
|
|
||||||
_ ->
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
else
|
|
||||||
_ -> nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.DeleteNotificationsFromInvisibleUsers do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
alias Pleroma.Repo
|
||||||
|
|
||||||
|
def up do
|
||||||
|
Pleroma.Notification
|
||||||
|
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||||
|
|> where(
|
||||||
|
[n, a],
|
||||||
|
fragment("? in (SELECT ap_id FROM users WHERE invisible = true)", a.actor)
|
||||||
|
)
|
||||||
|
|> Repo.delete_all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def down, do: :ok
|
||||||
|
end
|
|
@ -306,6 +306,14 @@ test "it doesn't create subscription notifications if the recipient cannot see t
|
||||||
|
|
||||||
assert {:ok, []} == Notification.create_notifications(status)
|
assert {:ok, []} == Notification.create_notifications(status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it disables notifications from people who are invisible" do
|
||||||
|
author = insert(:user, invisible: true)
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"})
|
||||||
|
refute Notification.create_notification(status, user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "follow / follow_request notifications" do
|
describe "follow / follow_request notifications" do
|
||||||
|
|
|
@ -313,6 +313,33 @@ test "filters notifications for Announce activities" do
|
||||||
assert public_activity.id in activity_ids
|
assert public_activity.id in activity_ids
|
||||||
refute unlisted_activity.id in activity_ids
|
refute unlisted_activity.id in activity_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "doesn't return less than the requested amount of records when the user's reply is liked" do
|
||||||
|
user = insert(:user)
|
||||||
|
%{user: other_user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
|
||||||
|
{:ok, mention} =
|
||||||
|
CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"})
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
|
||||||
|
|
||||||
|
{:ok, reply} =
|
||||||
|
CommonAPI.post(other_user, %{
|
||||||
|
status: ".",
|
||||||
|
visibility: "public",
|
||||||
|
in_reply_to_status_id: activity.id
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, _favorite} = CommonAPI.favorite(user, reply.id)
|
||||||
|
|
||||||
|
activity_ids =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|> Enum.map(& &1["status"]["id"])
|
||||||
|
|
||||||
|
assert [reply.id, mention.id] == activity_ids
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "filters notifications using exclude_types" do
|
test "filters notifications using exclude_types" do
|
||||||
|
|
|
@ -139,9 +139,7 @@ test "Follow notification" do
|
||||||
test_notifications_rendering([notification], followed, [expected])
|
test_notifications_rendering([notification], followed, [expected])
|
||||||
|
|
||||||
User.perform(:delete, follower)
|
User.perform(:delete, follower)
|
||||||
notification = Notification |> Repo.one() |> Repo.preload(:activity)
|
refute Repo.one(Notification)
|
||||||
|
|
||||||
test_notifications_rendering([notification], followed, [])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag capture_log: true
|
@tag capture_log: true
|
||||||
|
|
Loading…
Reference in a new issue