Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions api/v1_events_remix_contests.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ func (app *ApiServer) v1EventsRemixContests(c *fiber.Ctx) error {
"u.is_deactivated = false",
"u.is_available = true",
"(e.entity_type != 'track' OR (t.track_id IS NOT NULL AND t.is_delete = false AND t.is_unlisted = false))",
// Shadow-ban filters — mirror what v1_event_comments.go applies to
// comment authors. Two parallel signals so the filter catches the
// full population: low-quality / impersonator / bot accounts via
// `aggregate_user.score < 0`, and community-flagged users via the
// karma-muted set (sum of muters' follower_count crosses the
// karmaCommentCountThreshold). Hosts in either bucket disappear
// from the discovery list.
"e.user_id NOT IN (SELECT user_id FROM low_abuse_score)",
"e.user_id NOT IN (SELECT muted_user_id FROM muted_by_karma)",
}

switch params.Status {
Expand All @@ -49,6 +58,18 @@ func (app *ApiServer) v1EventsRemixContests(c *fiber.Ctx) error {
// (only_contest_entries=true): a child track is an entry iff it was created
// after the contest started, before its end_date, and is currently listed.
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
e.event_id,
e.entity_type::event_entity_type AS entity_type,
Expand Down Expand Up @@ -92,9 +113,10 @@ func (app *ApiServer) v1EventsRemixContests(c *fiber.Ctx) error {
`

rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{
"limit": params.Limit,
"offset": params.Offset,
"featured_user_id": config.Cfg.FeaturedAudienceUserID,
"limit": params.Limit,
"offset": params.Offset,
"featured_user_id": config.Cfg.FeaturedAudienceUserID,
"karmaCommentCountThreshold": karmaCommentCountThreshold,
})
if err != nil {
return err
Expand Down
104 changes: 104 additions & 0 deletions api/v1_events_remix_contests_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"context"
"testing"

"api.audius.co/config"
Expand Down Expand Up @@ -460,3 +461,106 @@ func TestRemixContestsExcludesUnavailableContent(t *testing.T) {
"contest hosted by a user with is_available=false must not be returned")
})
}

// TestRemixContestsExcludesShadowbannedHosts covers the two parallel
// shadow-ban signals applied to the discovery list:
// 1. `aggregate_user.score < 0` (account-quality signal — bots,
// impersonators, fast-challenge runners).
// 2. The karma-muted set — host has been muted by users whose combined
// follower_count crosses karmaCommentCountThreshold (community-driven
// signal). Same shape used in v1_event_comments for comment authors.
func TestRemixContestsExcludesShadowbannedHosts(t *testing.T) {
app := emptyTestApp(t)

cleanHostID := 9601
lowScoreHostID := 9602
karmaMutedHostID := 9603
highKarmaMuterID := 9604

cleanTrackID := 8601
lowScoreTrackID := 8602
karmaMutedTrackID := 8603

start := parseTime(t, "2024-01-02")
end := parseTime(t, "2099-01-01")

fixtures := database.FixtureMap{
"events": []map[string]any{
{
"event_id": 801, "event_type": "remix_contest", "entity_type": "track",
"entity_id": cleanTrackID, "user_id": cleanHostID,
"created_at": start, "end_date": end,
},
{
"event_id": 802, "event_type": "remix_contest", "entity_type": "track",
"entity_id": lowScoreTrackID, "user_id": lowScoreHostID,
"created_at": start, "end_date": end,
},
{
"event_id": 803, "event_type": "remix_contest", "entity_type": "track",
"entity_id": karmaMutedTrackID, "user_id": karmaMutedHostID,
"created_at": start, "end_date": end,
},
},
"users": []map[string]any{
{"user_id": cleanHostID, "handle": "clean_host"},
{"user_id": lowScoreHostID, "handle": "low_score_host"},
{"user_id": karmaMutedHostID, "handle": "karma_muted_host"},
{"user_id": highKarmaMuterID, "handle": "high_karma_muter"},
},
"tracks": []map[string]any{
{"track_id": cleanTrackID, "owner_id": cleanHostID, "created_at": start},
{"track_id": lowScoreTrackID, "owner_id": lowScoreHostID, "created_at": start},
{"track_id": karmaMutedTrackID, "owner_id": karmaMutedHostID, "created_at": start},
},
"muted_users": []map[string]any{
// High-karma muter mutes the karma-muted host — combined with the
// follower_count bump below, this should cross the threshold.
{"user_id": highKarmaMuterID, "muted_user_id": karmaMutedHostID},
},
}
database.Seed(app.pool.Replicas[0], fixtures)

// `aggregate_user` rows are created by the users trigger; tweak the two
// fields we care about: score on the low-score host, and the muter's
// follower_count so the karma-muted CTE actually trips.
_, err := app.pool.Exec(context.Background(),
`UPDATE aggregate_user SET score = $1 WHERE user_id = $2`,
-1, lowScoreHostID,
)
if err != nil {
t.Fatal(err)
}
_, err = app.pool.Exec(context.Background(),
`UPDATE aggregate_user SET follower_count = $1 WHERE user_id = $2`,
karmaCommentCountThreshold+1, highKarmaMuterID,
)
if err != nil {
t.Fatal(err)
}

cleanEventHash := trashid.MustEncodeHashID(801)

t.Run("only the clean host's contest is returned", func(t *testing.T) {
status, body := testGet(t, app, "/v1/events/remix-contests")
assert.Equal(t, 200, status)
jsonAssert(t, body, map[string]any{
"data.#": 1,
"data.0.event_id": cleanEventHash,
})
})

t.Run("host with score < 0 is excluded", func(t *testing.T) {
_, body := testGet(t, app, "/v1/events/remix-contests")
eventIds := pluckStrings(body, "data.#.event_id")
assert.NotContains(t, eventIds, trashid.MustEncodeHashID(802),
"contest hosted by a user with aggregate_user.score < 0 must not be returned")
})

t.Run("karma-muted host is excluded", func(t *testing.T) {
_, body := testGet(t, app, "/v1/events/remix-contests")
eventIds := pluckStrings(body, "data.#.event_id")
assert.NotContains(t, eventIds, trashid.MustEncodeHashID(803),
"contest hosted by a user in muted_by_karma must not be returned")
})
}
Loading