Skip to content

Commit d5357d0

Browse files
authored
Comments (#56)
* implements `users/:id/comments` and `tracks/:id/comments` * implements `unclaimed_id` endpoints <img width="673" alt="image" src="https://github.com/user-attachments/assets/076dc7ba-0600-4421-abf2-17e2f23c3638" />
1 parent c6bfb4b commit d5357d0

17 files changed

Lines changed: 723 additions & 46 deletions

TIPS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Quickly scan some sql into a map and respond:
2+
3+
```go
4+
rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{
5+
"user_id": c.Locals("userId"),
6+
})
7+
if err != nil {
8+
return err
9+
}
10+
11+
stuff, err := pgx.CollectRows(rows, pgx.RowToMap)
12+
if err != nil {
13+
return err
14+
}
15+
16+
return c.JSON(fiber.Map{
17+
"data": stuff,
18+
})
19+
```

api/dbv1/full_comments.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package dbv1
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"bridgerton.audius.co/trashid"
8+
"github.com/jackc/pgx/v5"
9+
"github.com/jackc/pgx/v5/pgtype"
10+
)
11+
12+
type GetCommentsParams struct {
13+
MyID interface{} `json:"my_id"`
14+
Ids []int32 `json:"ids"`
15+
}
16+
17+
type FullComment struct {
18+
Id trashid.HashId `json:"id"`
19+
EntityType string `json:"entity_type"`
20+
EntityId trashid.HashId `json:"entity_id"`
21+
UserId trashid.HashId `json:"user_id"`
22+
Message string `json:"message"`
23+
Mentions []struct {
24+
UserId int `json:"user_id"`
25+
Handle string `json:"handle"`
26+
} `json:"mentions"`
27+
TrackTimestampS pgtype.Int4 `json:"track_timestamp_s"`
28+
IsMuted bool `json:"is_muted"`
29+
IsEdited bool `json:"is_edited"`
30+
IsCurrentUserReacted bool `json:"is_current_user_reacted"`
31+
IsArtistReacted bool `json:"is_artist_reacted"`
32+
IsDelete bool `json:"-"`
33+
IsTombstone bool `json:"is_tombstone"`
34+
ReactCount int `json:"react_count"`
35+
CreatedAt time.Time `json:"created_at"`
36+
UpdatedAt time.Time `json:"updated_at"`
37+
38+
ReplyCount int `json:"reply_count"`
39+
Replies []FullComment `json:"replies"`
40+
41+
// this should be omitted
42+
ReplyIds []int32 `db:"reply_ids" json:"-"`
43+
ParentCommentId pgtype.Int4 `json:"-"`
44+
}
45+
46+
func (q *Queries) FullCommentsKeyed(ctx context.Context, arg GetCommentsParams) (map[int32]FullComment, error) {
47+
if len(arg.Ids) == 0 {
48+
return nil, nil
49+
}
50+
51+
sql := `
52+
SELECT
53+
comment_id as id,
54+
parent_comment_id,
55+
entity_type,
56+
entity_id,
57+
user_id,
58+
text as message,
59+
60+
(
61+
SELECT json_agg(
62+
json_build_object(
63+
'user_id', m.user_id,
64+
'handle', handle
65+
)
66+
)
67+
FROM (
68+
SELECT user_id, handle FROM comment_mentions
69+
JOIN users USING (user_id)
70+
WHERE comment_id = comments.comment_id
71+
) m
72+
)::jsonb as mentions,
73+
74+
track_timestamp_s,
75+
76+
(
77+
SELECT count(*)
78+
FROM comment_reactions
79+
WHERE comment_id = comments.comment_id
80+
AND is_delete = false
81+
) as react_count,
82+
83+
84+
(
85+
SELECT array_agg(comment_id)
86+
FROM comment_threads
87+
JOIN comments cc USING (comment_id)
88+
WHERE parent_comment_id = comments.comment_id
89+
AND cc.is_delete = false
90+
) as reply_ids,
91+
92+
is_edited,
93+
94+
EXISTS (
95+
SELECT 1
96+
FROM comment_reactions
97+
WHERE comment_id = comments.comment_id
98+
AND user_id = @my_id
99+
AND is_delete = false
100+
) AS is_current_user_reacted,
101+
102+
EXISTS (
103+
SELECT 1
104+
FROM comment_reactions
105+
WHERE comment_id = comments.comment_id
106+
AND user_id = tracks.owner_id
107+
AND is_delete = false
108+
) AS is_artist_reacted,
109+
110+
comments.is_delete,
111+
112+
coalesce((
113+
SELECT is_muted
114+
FROM comment_notification_settings mutes
115+
WHERE @my_id > 0
116+
AND mutes.user_id = @my_id
117+
AND mutes.entity_type = entity_type
118+
AND mutes.entity_id = entity_id
119+
LIMIT 1
120+
), false) as is_muted,
121+
122+
comments.created_at,
123+
comments.updated_at
124+
125+
FROM comments
126+
JOIN tracks ON entity_id = track_id
127+
LEFT JOIN comment_threads USING (comment_id)
128+
WHERE comment_id = ANY(@ids::int[])
129+
ORDER BY comments.created_at DESC
130+
`
131+
132+
rows, err := q.db.Query(ctx, sql, pgx.NamedArgs{
133+
"ids": arg.Ids,
134+
"my_id": arg.MyID,
135+
})
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
comments, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[FullComment])
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
commentMap := map[int32]FullComment{}
146+
for _, comment := range comments {
147+
commentMap[int32(comment.Id)] = comment
148+
}
149+
150+
// fetch replies
151+
replyIds := []int32{}
152+
for _, comment := range comments {
153+
replyIds = append(replyIds, comment.ReplyIds...)
154+
}
155+
replyMap, err := q.FullCommentsKeyed(ctx, GetCommentsParams{
156+
MyID: arg.MyID,
157+
Ids: replyIds,
158+
})
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
for id, comment := range commentMap {
164+
for _, replyId := range comment.ReplyIds {
165+
if reply, ok := replyMap[replyId]; ok {
166+
comment.Replies = append(comment.Replies, reply)
167+
}
168+
}
169+
// todo: sort replies?
170+
comment.ReplyCount = len(comment.Replies)
171+
172+
if comment.IsDelete {
173+
comment.Message = "[Removed]"
174+
if comment.ReplyCount > 0 {
175+
comment.IsTombstone = true
176+
}
177+
}
178+
commentMap[id] = comment
179+
}
180+
181+
return commentMap, nil
182+
183+
}
184+
185+
func (q *Queries) FullComments(ctx context.Context, arg GetCommentsParams) ([]FullComment, error) {
186+
commentMap, err := q.FullCommentsKeyed(ctx, arg)
187+
if err != nil {
188+
return nil, err
189+
}
190+
191+
comments := make([]FullComment, 0, len(arg.Ids))
192+
for _, id := range arg.Ids {
193+
if c, ok := commentMap[id]; ok {
194+
comments = append(comments, c)
195+
}
196+
}
197+
return comments, nil
198+
}

api/dbv1/parallel.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,27 @@ func (q *Queries) Parallel(ctx context.Context, arg ParallelParams) (*ParallelRe
7272

7373
return result, nil
7474
}
75+
76+
func (r *ParallelResult) UserList() []FullUser {
77+
userList := make([]FullUser, 0, len(r.UserMap))
78+
for _, u := range r.UserMap {
79+
userList = append(userList, u)
80+
}
81+
return userList
82+
}
83+
84+
func (r *ParallelResult) TrackList() []FullTrack {
85+
trackList := make([]FullTrack, 0, len(r.TrackMap))
86+
for _, t := range r.TrackMap {
87+
trackList = append(trackList, t)
88+
}
89+
return trackList
90+
}
91+
92+
func (r *ParallelResult) PlaylistList() []FullPlaylist {
93+
playlistList := make([]FullPlaylist, 0, len(r.PlaylistMap))
94+
for _, p := range r.PlaylistMap {
95+
playlistList = append(playlistList, p)
96+
}
97+
return playlistList
98+
}

api/fixture_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ var (
161161
"updated_at": time.Now(),
162162
"txhash": "tx123",
163163
}
164+
165+
commentBaseRow = map[string]any{
166+
"entity_type": "Track",
167+
"created_at": time.Now(),
168+
"updated_at": time.Now(),
169+
"txhash": "0x1",
170+
"blockhash": "0x2",
171+
}
164172
)
165173

166174
func insertFixtures(table string, baseRow map[string]any, csvFile string) {

api/server.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,15 @@ func NewApiServer(config config.Config) *ApiServer {
7676
logger.Error("db connect failed", zap.Error(err))
7777
}
7878

79-
connConfig.ConnConfig.Tracer = &tracelog.TraceLog{
80-
Logger: pgxzap.NewLogger(logger),
81-
LogLevel: tracelog.LogLevelInfo,
79+
// disable sql logging in ENV "test"
80+
if config.Env != "test" {
81+
connConfig.ConnConfig.Tracer = &tracelog.TraceLog{
82+
Logger: pgxzap.NewLogger(logger),
83+
LogLevel: tracelog.LogLevelInfo,
84+
}
8285
}
8386

8487
pool, err := pgxpool.NewWithConfig(context.Background(), connConfig)
85-
// To turn off pgx logging, use this:
86-
// pool, err := pgxpool.New(context.Background(), config.DbUrl)
8788

8889
if err != nil {
8990
logger.Fatal("db connect failed", zap.Error(err))
@@ -195,7 +196,7 @@ func NewApiServer(config config.Config) *ApiServer {
195196
for _, g := range []fiber.Router{v1, v1Full} {
196197
// Users
197198
g.Get("/users", app.v1Users)
198-
199+
g.Get("/users/unclaimed_id", app.v1UsersUnclaimedId)
199200
g.Get("/users/account/:wallet", app.requireAuthMiddleware, app.v1UsersAccount)
200201

201202
g.Use("/users/handle/:handle", app.requireHandleMiddleware)
@@ -205,6 +206,7 @@ func NewApiServer(config config.Config) *ApiServer {
205206

206207
g.Use("/users/:userId", app.requireUserIdMiddleware)
207208
g.Get("/users/:userId", app.v1User)
209+
g.Get("/users/:userId/comments", app.v1UsersComments)
208210
g.Get("/users/:userId/followers", app.v1UsersFollowers)
209211
g.Get("/users/:userId/following", app.v1UsersFollowing)
210212
g.Get("/users/:userId/library/tracks", app.v1UsersLibraryTracks)
@@ -221,6 +223,7 @@ func NewApiServer(config config.Config) *ApiServer {
221223

222224
// Tracks
223225
g.Get("/tracks", app.v1Tracks)
226+
g.Get("/tracks/unclaimed_id", app.v1TracksUnclaimedId)
224227

225228
g.Get("/tracks/trending", app.v1TracksTrending)
226229
g.Get("/tracks/trending/ids", app.v1TracksTrendingIds)
@@ -230,9 +233,11 @@ func NewApiServer(config config.Config) *ApiServer {
230233
g.Get("/tracks/:trackId", app.v1Track)
231234
g.Get("/tracks/:trackId/reposts", app.v1TracksReposts)
232235
g.Get("/tracks/:trackId/favorites", app.v1TracksFavorites)
236+
g.Get("/tracks/:trackId/comments", app.v1TracksComments)
233237

234238
// Playlists
235239
g.Get("/playlists", app.v1playlists)
240+
g.Get("/playlists/unclaimed_id", app.v1PlaylistsUnclaimedId)
236241

237242
g.Use("/playlists/:playlistId", app.requirePlaylistIdMiddleware)
238243
g.Get("/playlists/:playlistId", app.v1Playlist)
@@ -244,6 +249,9 @@ func NewApiServer(config config.Config) *ApiServer {
244249

245250
// Rewards
246251
g.Get("/rewards/claim", app.v1ClaimRewards)
252+
253+
// Comments
254+
g.Get("/comments/unclaimed_id", app.v1CommentsUnclaimedId)
247255
}
248256

249257
app.Static("/", "./static")

api/server_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func TestMain(m *testing.M) {
4444
}
4545

4646
app = NewApiServer(config.Config{
47+
Env: "test",
4748
DbUrl: "postgres://postgres:example@localhost:21300/test",
4849
DelegatePrivateKey: "0633fddb74e32b3cbc64382e405146319c11a1a52dc96598e557c5dbe2f31468",
4950
})
@@ -79,6 +80,8 @@ func TestMain(m *testing.M) {
7980
insertFixtures("usdc_purchases", usdcPurchaseBaseRow, "testdata/usdc_purchases_fixtures.csv")
8081
insertFixtures("track_routes", map[string]any{}, "testdata/track_routes_fixtures.csv")
8182
insertFixtures("grants", grantBaseRow, "testdata/grants_fixtures.csv")
83+
insertFixtures("comments", commentBaseRow, "testdata/comment_fixtures.csv")
84+
insertFixtures("comment_threads", map[string]any{}, "testdata/comment_thread_fixtures.csv")
8285

8386
// index to es / os
8487

@@ -135,6 +138,12 @@ func Test200UnAuthed(t *testing.T) {
135138
"/v1/full/playlists?id=7eP5n",
136139
"/v1/full/playlists/7eP5n/reposts",
137140
"/v1/full/playlists/7eP5n/favorites",
141+
142+
// unclaimed ids
143+
"/v1/users/unclaimed_id",
144+
"/v1/tracks/unclaimed_id",
145+
"/v1/playlists/unclaimed_id",
146+
"/v1/comments/unclaimed_id",
138147
}
139148

140149
for _, u := range urls {

api/testdata/comment_fixtures.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
comment_id,user_id,entity_id,text
2+
1,1,201,flame emoji
3+
2,2,201,thanks for the emoji
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
comment_id,parent_comment_id
2+
2,1

0 commit comments

Comments
 (0)