…803)
## Summary
`v1EventsRemixContests`
([api/v1_events_remix_contests.go](api/v1_events_remix_contests.go)) —
the endpoint backing the contests discovery page on mobile + web —
previously surfaced contests whose host was shadow-banned.
`v1EventComments` already applies the two-signal shadow-ban filter to
comment authors; this PR mirrors the exact same pair against contest
hosts so the discovery list and comment list stay in lockstep.
This was originally drafted against
\`packages/discovery-provider/src/queries/get_events.py\` in the apps
monorepo (PR AudiusProject/apps#14297), but that Flask API was removed
in #14236 and the file is dead code on main. Reopening here in the
correct repo.
## The two signals
| Signal | What it catches | Source pattern |
|---|---|---|
| `aggregate_user.score < 0` (`low_abuse_score` CTE) | bots,
Audius-impersonators, fast-challenge-runners, low-engagement accounts |
Same CTE used at
[v1_event_comments.go:74-76](api/v1_event_comments.go#L74) |
| `muted_by_karma` CTE | hosts muted by users whose combined
`follower_count` crosses `karmaCommentCountThreshold` | Same CTE used at
[v1_event_comments.go:66-73](api/v1_event_comments.go#L66) |
Both CTEs are lifted verbatim from `v1_event_comments.go` so the filter
is byte-for-byte identical to what the comment system applies to comment
authors. Reuses the existing `karmaCommentCountThreshold` constant
(defined at
[v1_track_comment_count.go:8](api/v1_track_comment_count.go#L8)).
## Implementation
Added two CTEs at the top of the SQL, two `NOT IN` filters to the
existing `filters` slice, and bound the threshold constant:
```sql
WITH
muted_by_karma AS (
SELECT muted_user_id
FROM muted_users
JOIN aggregate_user ON muted_users.user_id = aggregate_user.user_id
WHERE muted_users.is_delete = false
GROUP BY muted_user_id
HAVING SUM(aggregate_user.follower_count) >= @karmaCommentCountThreshold
),
low_abuse_score AS (
SELECT user_id FROM aggregate_user WHERE score < 0
)
SELECT ...
WHERE ...
AND e.user_id NOT IN (SELECT user_id FROM low_abuse_score)
AND e.user_id NOT IN (SELECT muted_user_id FROM muted_by_karma)
```
- The existing `u.is_deactivated = false` and `u.is_available = true`
filters stay in place. Shadow-ban filtering layers on top.
- The contest's parent track filter (`e.entity_type != 'track' OR ...`)
is untouched.
- The sort priority, pagination, status filter, and `entry_counts`
LATERAL subquery are untouched.
- The `users` and `tracks` related lookups downstream are unaffected —
they just see fewer rows.
## Tests
New `TestRemixContestsExcludesShadowbannedHosts` test follows the exact
pattern of `TestRemixContestsExcludesUnavailableContent` already in this
file. Seeds three contests:
- clean host (score=0, no mutes)
- low-score host (score=-1)
- karma-muted host (muted by a high-follower user crossing the
threshold)
Three sub-assertions: only the clean contest is returned; low-score
contest absent; karma-muted contest absent.
\`go build ./api/...\` and \`go vet ./api/...\` both clean locally.
Integration test couldn't be run end-to-end without a local Postgres at
port 21300, but the test compiles fine and CI will run it against a
fresh DB.
## Test plan
- [ ] CI green on `go test ./api/...`
- [ ] Manual smoke after deploy: hit `/v1/events/remix-contests` on
staging, confirm a known shadow-banned account's contest no longer
appears in the response
- [ ] Confirm `useAllRemixContests` on mobile + web still returns the
expected (non-shadowbanned) contests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
get_events(packages/discovery-provider/src/queries/get_events.py) backs the contests discovery list (and any other event-list consumer). It previously surfaced contests whose host had been shadow-banned, because the existing filter chain only excludedEvent.is_deleted == True.This PR adds a host-shadow-ban filter that combines the two parallel shadow-ban signals the discovery-provider already uses elsewhere — applying both so the filter catches the full shadow-banned population.
The two signals (and why both)
aggregate_user.score < 0muted_by_karmasubquery (sum of muters' follower_count ≥COMMENT_KARMA_THRESHOLD)muted_userstableThe two catch different (but overlapping) populations. Score-based catches bots and impersonators who may never have been actively reported; karma-mute catches users that influential accounts have explicitly flagged. Applying both means contest discovery hides anyone who falls into either bucket.
Implementation
Inside
_get_events, after the existingfilter_deletedstep:aggregate_userso users without an aggregate row yet (brand-new accounts) aren't accidentally filtered — theor_(... is_(None))lets them through; only confirmed negative scores are excluded.muted_by_karmais lifted fromget_track_comment_count.pyverbatim (same shape, sameCOMMENT_KARMA_THRESHOLDconstant), so the contest discovery filter applies the exact same per-user check the comment system uses.aggregate_user.user_idis a primary key, so the outer join is 1:1 per Event. TheIN-subquery doesn't join, so it can't duplicate either.add_query_paginationis plainLIMIT/OFFSETwith optionalinclude_count; both work correctly with the dual filter.get_events_by_ids(the by-ID lookup) is unchanged on purpose. Deep-link lookups for known events shouldn't be silently broken by a discovery-time filter — comment/action surfaces on those pages have their own enforcement paths.Risk / blast radius
get_eventsis the single entry point for event-list queries (confirmed via grep — no other callers insrc/). Any consumer that wanted to see shadow-banned hosts' events explicitly would need a new flag likeinclude_shadowbanned: bool— I didn't add one because no caller asks for it today; happy to extend if there's a known admin/internal need.Test plan
aggregate_user.score = -1. Callget_events(...)→ only the non-shadow-banned event is returned.COMMENT_KARMA_THRESHOLD(but whose ownscore >= 0). Confirm that event is also excluded.aggregate_userrow. Confirm their event still appears (the outer join's NULL is allowed through; they're not inmuted_by_karmabecause no one has muted them yet).get_events_by_ids(id=[shadowbanned_event_id])still returns the event (filter not applied on that path).🤖 Generated with Claude Code