From f5f18de3323eaf5562b0683456f00e345030cca4 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 12 May 2026 14:25:15 -0700 Subject: [PATCH 1/2] discovery: filter shadow-banned hosts from event listings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get_events` (the query backing the contests discovery list, among other event-list consumers) previously surfaced contests whose host had been shadow-banned. The SQL feed handlers (handle_save.sql, handle_follow.sql, handle_repost.sql) already gate notifications on `aggregate_user.score < 0`; this applies the same check to the event listing so shadow-banned users' contests don't appear in discovery. Implementation: - OUTER JOIN `aggregate_user` on `Event.user_id` so users without an aggregate row (e.g. brand-new accounts that haven't been rolled up yet) aren't accidentally filtered out — only users with a confirmed negative score are excluded. - Filter sits in `_get_events` alongside the existing `is_deleted` check. `get_events_by_ids` (the by-ID lookup) is unchanged on purpose: direct deep-link lookups for known events shouldn't be silently broken by a discovery-time filter. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../discovery-provider/src/queries/get_events.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/discovery-provider/src/queries/get_events.py b/packages/discovery-provider/src/queries/get_events.py index bd711ff3ada..ca919d81910 100644 --- a/packages/discovery-provider/src/queries/get_events.py +++ b/packages/discovery-provider/src/queries/get_events.py @@ -1,9 +1,10 @@ import logging from typing import List, Optional, TypedDict -from sqlalchemy import desc +from sqlalchemy import desc, or_ from src.models.events.event import Event, EventEntityType, EventType +from src.models.users.aggregate_user import AggregateUser from src.queries.query_helpers import add_query_pagination, get_pagination_vars from src.utils import helpers from src.utils.db_session import get_db_read_replica @@ -90,6 +91,16 @@ def _get_events(session, args): if args.get("filter_deleted", True): base_query = base_query.filter(Event.is_deleted == False) + # Hide events whose host has been shadow-banned. Mirrors the + # `score < 0` check used in the SQL feed handlers (handle_save, + # handle_follow, handle_repost). Uses an OUTER JOIN so users + # without an aggregate_user row (e.g. brand-new accounts that + # haven't been rolled up yet) aren't excluded — only users with a + # confirmed negative score get filtered out. + base_query = base_query.outerjoin( + AggregateUser, AggregateUser.user_id == Event.user_id + ).filter(or_(AggregateUser.score >= 0, AggregateUser.score.is_(None))) + # Order by created_at desc by default base_query = base_query.order_by(desc(Event.created_at)) From f42829b004a407dc7a6bb87df3f12d77f3fc9ece Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 12 May 2026 14:41:45 -0700 Subject: [PATCH 2/2] discovery: also exclude muted_by_karma hosts from event listings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review on #14297: the discovery-provider has two parallel shadow-ban signals — `aggregate_user.score < 0` (used by the SQL feed handlers handle_save, handle_follow, handle_repost) and karma-based muting (used by `get_track_comment_count.py` for per-user comment hiding). The previous commit applied only the first; this adds the second so the filter catches the full shadow-banned population: - Bots / impersonators / low-quality accounts → caught by `score < 0` - Community-flagged users muted by high-follower-count accounts → caught by the `muted_by_karma` subquery The `muted_by_karma` subquery is lifted from `get_track_comment_count.py` (same shape, same threshold constant COMMENT_KARMA_THRESHOLD) so the contest discovery filter applies the exact same per-user check the comment system uses. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/queries/get_events.py | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/discovery-provider/src/queries/get_events.py b/packages/discovery-provider/src/queries/get_events.py index ca919d81910..572948469bc 100644 --- a/packages/discovery-provider/src/queries/get_events.py +++ b/packages/discovery-provider/src/queries/get_events.py @@ -1,9 +1,11 @@ import logging from typing import List, Optional, TypedDict -from sqlalchemy import desc, or_ +from sqlalchemy import desc, func, or_ +from src.models.comments.comment_report import COMMENT_KARMA_THRESHOLD from src.models.events.event import Event, EventEntityType, EventType +from src.models.moderation.muted_user import MutedUser from src.models.users.aggregate_user import AggregateUser from src.queries.query_helpers import add_query_pagination, get_pagination_vars from src.utils import helpers @@ -91,15 +93,33 @@ def _get_events(session, args): if args.get("filter_deleted", True): base_query = base_query.filter(Event.is_deleted == False) - # Hide events whose host has been shadow-banned. Mirrors the - # `score < 0` check used in the SQL feed handlers (handle_save, - # handle_follow, handle_repost). Uses an OUTER JOIN so users - # without an aggregate_user row (e.g. brand-new accounts that - # haven't been rolled up yet) aren't excluded — only users with a - # confirmed negative score get filtered out. + # Hide events whose host is shadow-banned. The discovery-provider + # has two parallel shadow-ban signals; we apply both so the filter + # catches the full population: + # + # 1. `aggregate_user.score < 0` — composite account-quality signal + # (impersonators, low engagement, chat-blocks). Same check used + # by the SQL feed handlers handle_save, handle_follow, + # handle_repost. OUTER JOIN with a NULL-pass so brand-new users + # without an aggregate_user row aren't excluded. + # 2. `muted_by_karma` — host has been comment-muted by users whose + # combined follower counts cross COMMENT_KARMA_THRESHOLD. Same + # subquery shape used in `get_track_comment_count.py` for + # per-user comment hiding. + muted_by_karma = ( + session.query(MutedUser.muted_user_id) + .join(AggregateUser, MutedUser.user_id == AggregateUser.user_id) + .filter(MutedUser.is_delete == False) + .group_by(MutedUser.muted_user_id) + .having(func.sum(AggregateUser.follower_count) >= COMMENT_KARMA_THRESHOLD) + .subquery() + ) base_query = base_query.outerjoin( AggregateUser, AggregateUser.user_id == Event.user_id - ).filter(or_(AggregateUser.score >= 0, AggregateUser.score.is_(None))) + ).filter( + or_(AggregateUser.score >= 0, AggregateUser.score.is_(None)), + ~Event.user_id.in_(muted_by_karma), + ) # Order by created_at desc by default base_query = base_query.order_by(desc(Event.created_at))