Skip to content

contests: exclude shadow-banned hosts from /v1/events/remix-contests#803

Merged
dylanjeffers merged 1 commit into
mainfrom
claude/filter-shadowbanned-hosts
May 12, 2026
Merged

contests: exclude shadow-banned hosts from /v1/events/remix-contests#803
dylanjeffers merged 1 commit into
mainfrom
claude/filter-shadowbanned-hosts

Conversation

@dylanjeffers
Copy link
Copy Markdown
Contributor

Summary

v1EventsRemixContests (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
muted_by_karma CTE hosts muted by users whose combined follower_count crosses karmaCommentCountThreshold Same CTE used at v1_event_comments.go:66-73

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).

Implementation

Added two CTEs at the top of the SQL, two NOT IN filters to the existing filters slice, and bound the threshold constant:

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

The discovery list previously surfaced contests whose host was
shadow-banned. v1EventComments already applies the two-signal
shadow-ban filter (low_abuse_score + muted_by_karma) to comment
authors; this mirrors the exact same pair against contest hosts.

- Low-quality / impersonator / bot accounts → `aggregate_user.score < 0`
  (same `low_abuse_score` CTE used in v1_event_comments).
- Community-flagged users → `muted_by_karma`, where the muters'
  combined follower_count crosses karmaCommentCountThreshold (same
  CTE shape, same threshold constant).

Both CTEs are lifted verbatim from v1_event_comments.go so the
contest-discovery and comment-author filters stay in lockstep.
`get_events_by_ids`-style direct event lookups are unaffected — this
filter only applies to the discovery list.

Tests in TestRemixContestsExcludesShadowbannedHosts cover both
signals: one host with score=-1 and one host muted by a follower-rich
account. Both contests should disappear from the response while a
clean host's contest is still returned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dylanjeffers dylanjeffers merged commit 386ac75 into main May 12, 2026
5 checks passed
@dylanjeffers dylanjeffers deleted the claude/filter-shadowbanned-hosts branch May 12, 2026 22:13
dylanjeffers added a commit that referenced this pull request May 15, 2026
`/v1/events/remix-contests?status=all` was hanging the contests page in
production with ~22s cold-cache calls (warm: ~100ms). PR #803's shadowban
filter added:

    low_abuse_score AS (SELECT user_id FROM aggregate_user WHERE score < 0)

`aggregate_user` has one row per user (millions of rows) and no index
covering `score`, so the CTE ran a full sequential scan on every cold
call. Pages then stuck in shared_buffers, which is why warm calls were
fast.

The same CTE is reused in v1_event_comments, v1_fan_club_feed,
v1_track_comments, v1_track_comment_count — all pay the same cost. The
contests endpoint hits hardest because status=all/status=ended keeps
most events past the WHERE filter and the sort then forces a per-row
LATERAL entry_count count, but the seq scan is the dominant fixed cost.

Fix: partial index on (user_id) WHERE score < 0. The shadowban set is a
tiny fraction of users, so the index is tens of KB. CREATE INDEX
CONCURRENTLY avoids holding ACCESS EXCLUSIVE on aggregate_user; the
migration follows the existing 0197_playlists_albums_partial_idx.sql
pattern (no BEGIN/COMMIT, IF NOT EXISTS for idempotency).

aggregate_user.score is the canonical shadowban signal — driven by the
AAO `anti_abuse_blocked_users` admin list and the AAO score formula, and
written back to aggregate_user.score by refresh_all_user_scores(). It is
the correct table for this filter; the only issue was the missing index.

Closes #813.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant