Skip to content

Commit 902cbb1

Browse files
committed
[PE-7264] Add gate conditions filter to user tracks
1 parent b021ad0 commit 902cbb1

3 files changed

Lines changed: 263 additions & 1 deletion

File tree

api/swagger/swagger-v1.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3354,6 +3354,22 @@ paths:
33543354
- all
33553355
- public
33563356
- unlisted
3357+
- name: gate_condition
3358+
in: query
3359+
description: Filter tracks by gate condition type. Multiple values can be provided to filter by multiple gate types (OR logic)
3360+
style: form
3361+
explode: true
3362+
schema:
3363+
type: array
3364+
items:
3365+
type: string
3366+
enum:
3367+
- ungated
3368+
- usdc_purchase
3369+
- follow
3370+
- tip
3371+
- nft
3372+
- token
33573373
- name: Encoded-Data-Message
33583374
in: header
33593375
description: The data that was signed by the user for signature recovery

api/v1_users_tracks.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"fmt"
5+
"strings"
56

67
"api.audius.co/api/dbv1"
78
"github.com/gofiber/fiber/v2"
@@ -61,6 +62,10 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error {
6162
trackFilter = "t.is_unlisted = true"
6263
}
6364

65+
// Gate condition filtering
66+
gateConditions := queryMulti(c, "gate_condition")
67+
gateFilter := buildGateConditionFilter(gateConditions)
68+
6469
sql := `
6570
SELECT track_id
6671
FROM tracks t
@@ -72,7 +77,7 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error {
7277
AND t.is_delete = false
7378
AND t.is_available = true
7479
AND ` + trackFilter + `
75-
AND t.stem_of is null
80+
AND t.stem_of is null` + gateFilter + `
7681
ORDER BY (CASE WHEN t.track_id = u.artist_pick_track_id THEN 0 ELSE 1 END), ` + orderClause + `
7782
LIMIT @limit
7883
OFFSET @offset
@@ -109,3 +114,42 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error {
109114
"data": tracks,
110115
})
111116
}
117+
118+
// buildGateConditionFilter builds SQL filter conditions based on gate_condition query parameters.
119+
// Supports multiple conditions:
120+
// - ungated: tracks with no gate
121+
// - usdc_purchase: tracks with USDC purchase gate
122+
// - follow: tracks with follow gate
123+
// - tip: tracks with tip gate
124+
// - nft: tracks with NFT gate
125+
// - token: tracks with token gate
126+
func buildGateConditionFilter(gateConditions []string) string {
127+
if len(gateConditions) == 0 {
128+
return ""
129+
}
130+
131+
var conditions []string
132+
for _, condition := range gateConditions {
133+
switch condition {
134+
case "ungated":
135+
conditions = append(conditions, "(t.is_stream_gated = false)")
136+
case "usdc_purchase":
137+
conditions = append(conditions, "(t.is_stream_gated = true AND t.stream_conditions->>'usdc_purchase' IS NOT NULL)")
138+
case "follow":
139+
conditions = append(conditions, "(t.is_stream_gated = true AND t.stream_conditions->>'follow_user_id' IS NOT NULL)")
140+
case "tip":
141+
conditions = append(conditions, "(t.is_stream_gated = true AND t.stream_conditions->>'tip_user_id' IS NOT NULL)")
142+
case "nft":
143+
conditions = append(conditions, "(t.is_stream_gated = true AND t.stream_conditions->>'nft_collection' IS NOT NULL)")
144+
case "token":
145+
conditions = append(conditions, "(t.is_stream_gated = true AND t.stream_conditions->>'token_gate' IS NOT NULL)")
146+
}
147+
}
148+
149+
if len(conditions) == 0 {
150+
return ""
151+
}
152+
153+
// Join multiple conditions with OR and wrap in parentheses
154+
return "\n AND (" + strings.Join(conditions, " OR ") + ")"
155+
}

api/v1_users_tracks_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"api.audius.co/api/dbv1"
8+
"api.audius.co/database"
89
"api.audius.co/trashid"
910
"github.com/stretchr/testify/assert"
1011
)
@@ -247,3 +248,204 @@ func TestGetUserTracksInvalidParams(t *testing.T) {
247248
status, _ = testGet(t, app, url)
248249
assert.Equal(t, 400, status)
249250
}
251+
252+
func TestGetUserTracksWithGateConditionFilter(t *testing.T) {
253+
app := emptyTestApp(t)
254+
fixtures := testTrackGateFixtures()
255+
database.Seed(app.pool.Replicas[0], fixtures)
256+
257+
var userTracksResponse struct {
258+
Data []dbv1.FullTrack
259+
}
260+
261+
baseUrl := fmt.Sprintf("/v1/full/users/%s/tracks", trashid.MustEncodeHashID(600))
262+
263+
// Test without filter - should return all tracks
264+
status, _ := testGet(t, app, baseUrl, &userTracksResponse)
265+
assert.Equal(t, 200, status)
266+
assert.Equal(t, 6, len(userTracksResponse.Data))
267+
268+
// Test filter for ungated tracks only
269+
url := fmt.Sprintf("%s?gate_condition=ungated", baseUrl)
270+
status, _ = testGet(t, app, url, &userTracksResponse)
271+
assert.Equal(t, 200, status)
272+
assert.Equal(t, 1, len(userTracksResponse.Data))
273+
assert.Equal(t, trashid.MustEncodeHashID(800), userTracksResponse.Data[0].ID)
274+
275+
// Test filter for usdc_purchase gated tracks
276+
url = fmt.Sprintf("%s?gate_condition=usdc_purchase", baseUrl)
277+
status, _ = testGet(t, app, url, &userTracksResponse)
278+
assert.Equal(t, 200, status)
279+
assert.Equal(t, 1, len(userTracksResponse.Data))
280+
assert.Equal(t, trashid.MustEncodeHashID(801), userTracksResponse.Data[0].ID)
281+
282+
// Test filter for follow gated tracks
283+
url = fmt.Sprintf("%s?gate_condition=follow", baseUrl)
284+
status, _ = testGet(t, app, url, &userTracksResponse)
285+
assert.Equal(t, 200, status)
286+
assert.Equal(t, 1, len(userTracksResponse.Data))
287+
assert.Equal(t, trashid.MustEncodeHashID(802), userTracksResponse.Data[0].ID)
288+
289+
// Test filter for tip gated tracks
290+
url = fmt.Sprintf("%s?gate_condition=tip", baseUrl)
291+
status, _ = testGet(t, app, url, &userTracksResponse)
292+
assert.Equal(t, 200, status)
293+
assert.Equal(t, 1, len(userTracksResponse.Data))
294+
assert.Equal(t, trashid.MustEncodeHashID(803), userTracksResponse.Data[0].ID)
295+
296+
// Test filter for nft gated tracks
297+
url = fmt.Sprintf("%s?gate_condition=nft", baseUrl)
298+
status, _ = testGet(t, app, url, &userTracksResponse)
299+
assert.Equal(t, 200, status)
300+
assert.Equal(t, 1, len(userTracksResponse.Data))
301+
assert.Equal(t, trashid.MustEncodeHashID(804), userTracksResponse.Data[0].ID)
302+
303+
// Test filter for token gated tracks
304+
url = fmt.Sprintf("%s?gate_condition=token", baseUrl)
305+
status, _ = testGet(t, app, url, &userTracksResponse)
306+
assert.Equal(t, 200, status)
307+
assert.Equal(t, 1, len(userTracksResponse.Data))
308+
assert.Equal(t, trashid.MustEncodeHashID(805), userTracksResponse.Data[0].ID)
309+
310+
// Test multiple gate conditions (usdc_purchase OR tip)
311+
url = fmt.Sprintf("%s?gate_condition=usdc_purchase&gate_condition=tip", baseUrl)
312+
status, _ = testGet(t, app, url, &userTracksResponse)
313+
assert.Equal(t, 200, status)
314+
assert.Equal(t, 2, len(userTracksResponse.Data))
315+
316+
// Test multiple gate conditions (ungated OR follow OR nft)
317+
url = fmt.Sprintf("%s?gate_condition=ungated&gate_condition=follow&gate_condition=nft", baseUrl)
318+
status, _ = testGet(t, app, url, &userTracksResponse)
319+
assert.Equal(t, 200, status)
320+
assert.Equal(t, 3, len(userTracksResponse.Data))
321+
}
322+
323+
func testTrackGateFixtures() map[string][]map[string]any {
324+
return map[string][]map[string]any{
325+
"users": {
326+
{
327+
"user_id": 600,
328+
"handle": "gatedtrackstester",
329+
"handle_lc": "gatedtrackstester",
330+
"wallet": "0xd4302f79457d5f5fcd54afd9e5a1a399723e7c30",
331+
},
332+
},
333+
"tracks": {
334+
{
335+
"track_id": 800,
336+
"owner_id": 600,
337+
"title": "Ungated Track",
338+
"is_stream_gated": false,
339+
"created_at": "2021-01-01 00:00:00",
340+
},
341+
{
342+
"track_id": 801,
343+
"owner_id": 600,
344+
"title": "USDC Purchase Gated Track",
345+
"is_stream_gated": true,
346+
"stream_conditions": map[string]any{
347+
"usdc_purchase": map[string]any{
348+
"price": 100.0,
349+
"splits": []map[string]any{
350+
{
351+
"user_id": 600,
352+
"percentage": 100.0,
353+
},
354+
},
355+
},
356+
},
357+
"created_at": "2021-01-02 00:00:00",
358+
},
359+
{
360+
"track_id": 802,
361+
"owner_id": 600,
362+
"title": "Follow Gated Track",
363+
"is_stream_gated": true,
364+
"stream_conditions": map[string]any{"follow_user_id": 600},
365+
"created_at": "2021-01-03 00:00:00",
366+
},
367+
{
368+
"track_id": 803,
369+
"owner_id": 600,
370+
"title": "Tip Gated Track",
371+
"is_stream_gated": true,
372+
"stream_conditions": map[string]any{"tip_user_id": 600},
373+
"created_at": "2021-01-04 00:00:00",
374+
},
375+
{
376+
"track_id": 804,
377+
"owner_id": 600,
378+
"title": "NFT Gated Track",
379+
"is_stream_gated": true,
380+
"stream_conditions": map[string]any{
381+
"nft_collection": map[string]any{
382+
"chain": "eth",
383+
"address": "0x1234567890123456789012345678901234567890",
384+
},
385+
},
386+
"created_at": "2021-01-05 00:00:00",
387+
},
388+
{
389+
"track_id": 805,
390+
"owner_id": 600,
391+
"title": "Token Gated Track",
392+
"is_stream_gated": true,
393+
"stream_conditions": map[string]any{
394+
"token_gate": map[string]any{
395+
"token_mint": "7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx",
396+
"token_amount": 100,
397+
},
398+
},
399+
"created_at": "2021-01-06 00:00:00",
400+
},
401+
},
402+
}
403+
}
404+
405+
func TestBuildGateConditionFilter(t *testing.T) {
406+
// Test with no conditions
407+
result := buildGateConditionFilter([]string{})
408+
assert.Equal(t, "", result)
409+
410+
// Test with single ungated condition
411+
result = buildGateConditionFilter([]string{"ungated"})
412+
assert.Contains(t, result, "t.is_stream_gated = false")
413+
414+
// Test with single usdc_purchase condition
415+
result = buildGateConditionFilter([]string{"usdc_purchase"})
416+
assert.Contains(t, result, "t.is_stream_gated = true")
417+
assert.Contains(t, result, "t.stream_conditions->>'usdc_purchase' IS NOT NULL")
418+
419+
// Test with single follow condition
420+
result = buildGateConditionFilter([]string{"follow"})
421+
assert.Contains(t, result, "t.stream_conditions->>'follow_user_id' IS NOT NULL")
422+
423+
// Test with single tip condition
424+
result = buildGateConditionFilter([]string{"tip"})
425+
assert.Contains(t, result, "t.stream_conditions->>'tip_user_id' IS NOT NULL")
426+
427+
// Test with single nft condition
428+
result = buildGateConditionFilter([]string{"nft"})
429+
assert.Contains(t, result, "t.stream_conditions->>'nft_collection' IS NOT NULL")
430+
431+
// Test with single token condition
432+
result = buildGateConditionFilter([]string{"token"})
433+
assert.Contains(t, result, "t.stream_conditions->>'token_gate' IS NOT NULL")
434+
435+
// Test with multiple conditions
436+
result = buildGateConditionFilter([]string{"ungated", "usdc_purchase"})
437+
assert.Contains(t, result, "t.is_stream_gated = false")
438+
assert.Contains(t, result, "t.stream_conditions->>'usdc_purchase' IS NOT NULL")
439+
assert.Contains(t, result, " OR ")
440+
441+
// Test with invalid condition (should be ignored)
442+
result = buildGateConditionFilter([]string{"invalid_condition"})
443+
assert.Equal(t, "", result)
444+
445+
// Test with mix of valid and invalid conditions
446+
result = buildGateConditionFilter([]string{"follow", "invalid", "tip"})
447+
assert.Contains(t, result, "t.stream_conditions->>'follow_user_id' IS NOT NULL")
448+
assert.Contains(t, result, "t.stream_conditions->>'tip_user_id' IS NOT NULL")
449+
assert.Contains(t, result, " OR ")
450+
assert.NotContains(t, result, "invalid")
451+
}

0 commit comments

Comments
 (0)