+2
-7
README.md
+2
-7
README.md
···
51
51
- Go
52
52
- Turbo
53
53
- Docker
54
-
- Wasm Pack https://github.com/drager/wasm-pack
54
+
- Wasm Pack https://rustwasm.github.io/wasm-pack/installer/
55
55
- DuckDB https://duckdb.org/docs/installation `1.2.0`
56
56
- Spotify `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` from setup in [Spotify developer dashboard](https://developer.spotify.com/documentation/web-api/tutorials/getting-started)
57
57
···
72
72
```bash
73
73
cp apps/api/.env.example apps/api/.env
74
74
cp apps/web/.env.example apps/web/.env
75
-
cp apps/feeds/.env.example apps/feeds/.env
76
75
cp .env.example .env
77
76
# Edit the .env files to add your configurations
78
77
```
···
106
105
```bash
107
106
bun run mb
108
107
```
109
-
11. Start feeds:
110
-
```bash
111
-
bun run feeds
112
-
```
113
-
12. Start the development server:
108
+
11. Start the development server:
114
109
```bash
115
110
turbo dev --filter=@rocksky/api --filter=@rocksky/web
116
111
```
-12
apps/api/drizzle/0002_robust_wong.sql
-12
apps/api/drizzle/0002_robust_wong.sql
···
1
-
CREATE TABLE "follows" (
2
-
"xata_id" text PRIMARY KEY DEFAULT xata_id() NOT NULL,
3
-
"uri" text NOT NULL,
4
-
"follower_did" text NOT NULL,
5
-
"subject_did" text NOT NULL,
6
-
"xata_version" integer,
7
-
"xata_createdat" timestamp DEFAULT now() NOT NULL,
8
-
"xata_updatedat" timestamp DEFAULT now() NOT NULL,
9
-
CONSTRAINT "follows_uri_unique" UNIQUE("uri")
10
-
);
11
-
--> statement-breakpoint
12
-
CREATE UNIQUE INDEX "follows_follower_subject_unique" ON "follows" USING btree ("follower_did","subject_did");
-3424
apps/api/drizzle/meta/0002_snapshot.json
-3424
apps/api/drizzle/meta/0002_snapshot.json
···
1
-
{
2
-
"id": "ba060d7c-5d31-44b1-abd4-8de1cd5357a7",
3
-
"prevId": "6d2ede58-9041-48a4-92fc-e8995d6bc049",
4
-
"version": "7",
5
-
"dialect": "postgresql",
6
-
"tables": {
7
-
"public.album_tracks": {
8
-
"name": "album_tracks",
9
-
"schema": "",
10
-
"columns": {
11
-
"xata_id": {
12
-
"name": "xata_id",
13
-
"type": "text",
14
-
"primaryKey": true,
15
-
"notNull": true,
16
-
"default": "xata_id()"
17
-
},
18
-
"album_id": {
19
-
"name": "album_id",
20
-
"type": "text",
21
-
"primaryKey": false,
22
-
"notNull": true
23
-
},
24
-
"track_id": {
25
-
"name": "track_id",
26
-
"type": "text",
27
-
"primaryKey": false,
28
-
"notNull": true
29
-
},
30
-
"xata_createdat": {
31
-
"name": "xata_createdat",
32
-
"type": "timestamp",
33
-
"primaryKey": false,
34
-
"notNull": true,
35
-
"default": "now()"
36
-
},
37
-
"xata_updatedat": {
38
-
"name": "xata_updatedat",
39
-
"type": "timestamp",
40
-
"primaryKey": false,
41
-
"notNull": true,
42
-
"default": "now()"
43
-
},
44
-
"xata_version": {
45
-
"name": "xata_version",
46
-
"type": "integer",
47
-
"primaryKey": false,
48
-
"notNull": false
49
-
}
50
-
},
51
-
"indexes": {},
52
-
"foreignKeys": {
53
-
"album_tracks_album_id_albums_xata_id_fk": {
54
-
"name": "album_tracks_album_id_albums_xata_id_fk",
55
-
"tableFrom": "album_tracks",
56
-
"tableTo": "albums",
57
-
"columnsFrom": [
58
-
"album_id"
59
-
],
60
-
"columnsTo": [
61
-
"xata_id"
62
-
],
63
-
"onDelete": "no action",
64
-
"onUpdate": "no action"
65
-
},
66
-
"album_tracks_track_id_tracks_xata_id_fk": {
67
-
"name": "album_tracks_track_id_tracks_xata_id_fk",
68
-
"tableFrom": "album_tracks",
69
-
"tableTo": "tracks",
70
-
"columnsFrom": [
71
-
"track_id"
72
-
],
73
-
"columnsTo": [
74
-
"xata_id"
75
-
],
76
-
"onDelete": "no action",
77
-
"onUpdate": "no action"
78
-
}
79
-
},
80
-
"compositePrimaryKeys": {},
81
-
"uniqueConstraints": {},
82
-
"policies": {},
83
-
"checkConstraints": {},
84
-
"isRLSEnabled": false
85
-
},
86
-
"public.albums": {
87
-
"name": "albums",
88
-
"schema": "",
89
-
"columns": {
90
-
"xata_id": {
91
-
"name": "xata_id",
92
-
"type": "text",
93
-
"primaryKey": true,
94
-
"notNull": true,
95
-
"default": "xata_id()"
96
-
},
97
-
"title": {
98
-
"name": "title",
99
-
"type": "text",
100
-
"primaryKey": false,
101
-
"notNull": true
102
-
},
103
-
"artist": {
104
-
"name": "artist",
105
-
"type": "text",
106
-
"primaryKey": false,
107
-
"notNull": true
108
-
},
109
-
"release_date": {
110
-
"name": "release_date",
111
-
"type": "text",
112
-
"primaryKey": false,
113
-
"notNull": false
114
-
},
115
-
"year": {
116
-
"name": "year",
117
-
"type": "integer",
118
-
"primaryKey": false,
119
-
"notNull": false
120
-
},
121
-
"album_art": {
122
-
"name": "album_art",
123
-
"type": "text",
124
-
"primaryKey": false,
125
-
"notNull": false
126
-
},
127
-
"uri": {
128
-
"name": "uri",
129
-
"type": "text",
130
-
"primaryKey": false,
131
-
"notNull": false
132
-
},
133
-
"artist_uri": {
134
-
"name": "artist_uri",
135
-
"type": "text",
136
-
"primaryKey": false,
137
-
"notNull": false
138
-
},
139
-
"apple_music_link": {
140
-
"name": "apple_music_link",
141
-
"type": "text",
142
-
"primaryKey": false,
143
-
"notNull": false
144
-
},
145
-
"spotify_link": {
146
-
"name": "spotify_link",
147
-
"type": "text",
148
-
"primaryKey": false,
149
-
"notNull": false
150
-
},
151
-
"tidal_link": {
152
-
"name": "tidal_link",
153
-
"type": "text",
154
-
"primaryKey": false,
155
-
"notNull": false
156
-
},
157
-
"youtube_link": {
158
-
"name": "youtube_link",
159
-
"type": "text",
160
-
"primaryKey": false,
161
-
"notNull": false
162
-
},
163
-
"sha256": {
164
-
"name": "sha256",
165
-
"type": "text",
166
-
"primaryKey": false,
167
-
"notNull": true
168
-
},
169
-
"xata_createdat": {
170
-
"name": "xata_createdat",
171
-
"type": "timestamp",
172
-
"primaryKey": false,
173
-
"notNull": true,
174
-
"default": "now()"
175
-
},
176
-
"xata_updatedat": {
177
-
"name": "xata_updatedat",
178
-
"type": "timestamp",
179
-
"primaryKey": false,
180
-
"notNull": true,
181
-
"default": "now()"
182
-
},
183
-
"xata_version": {
184
-
"name": "xata_version",
185
-
"type": "integer",
186
-
"primaryKey": false,
187
-
"notNull": false
188
-
}
189
-
},
190
-
"indexes": {},
191
-
"foreignKeys": {},
192
-
"compositePrimaryKeys": {},
193
-
"uniqueConstraints": {
194
-
"albums_uri_unique": {
195
-
"name": "albums_uri_unique",
196
-
"nullsNotDistinct": false,
197
-
"columns": [
198
-
"uri"
199
-
]
200
-
},
201
-
"albums_apple_music_link_unique": {
202
-
"name": "albums_apple_music_link_unique",
203
-
"nullsNotDistinct": false,
204
-
"columns": [
205
-
"apple_music_link"
206
-
]
207
-
},
208
-
"albums_spotify_link_unique": {
209
-
"name": "albums_spotify_link_unique",
210
-
"nullsNotDistinct": false,
211
-
"columns": [
212
-
"spotify_link"
213
-
]
214
-
},
215
-
"albums_tidal_link_unique": {
216
-
"name": "albums_tidal_link_unique",
217
-
"nullsNotDistinct": false,
218
-
"columns": [
219
-
"tidal_link"
220
-
]
221
-
},
222
-
"albums_youtube_link_unique": {
223
-
"name": "albums_youtube_link_unique",
224
-
"nullsNotDistinct": false,
225
-
"columns": [
226
-
"youtube_link"
227
-
]
228
-
},
229
-
"albums_sha256_unique": {
230
-
"name": "albums_sha256_unique",
231
-
"nullsNotDistinct": false,
232
-
"columns": [
233
-
"sha256"
234
-
]
235
-
}
236
-
},
237
-
"policies": {},
238
-
"checkConstraints": {},
239
-
"isRLSEnabled": false
240
-
},
241
-
"public.api_keys": {
242
-
"name": "api_keys",
243
-
"schema": "",
244
-
"columns": {
245
-
"xata_id": {
246
-
"name": "xata_id",
247
-
"type": "text",
248
-
"primaryKey": true,
249
-
"notNull": true,
250
-
"default": "xata_id()"
251
-
},
252
-
"name": {
253
-
"name": "name",
254
-
"type": "text",
255
-
"primaryKey": false,
256
-
"notNull": true
257
-
},
258
-
"api_key": {
259
-
"name": "api_key",
260
-
"type": "text",
261
-
"primaryKey": false,
262
-
"notNull": true
263
-
},
264
-
"shared_secret": {
265
-
"name": "shared_secret",
266
-
"type": "text",
267
-
"primaryKey": false,
268
-
"notNull": true
269
-
},
270
-
"description": {
271
-
"name": "description",
272
-
"type": "text",
273
-
"primaryKey": false,
274
-
"notNull": false
275
-
},
276
-
"enabled": {
277
-
"name": "enabled",
278
-
"type": "boolean",
279
-
"primaryKey": false,
280
-
"notNull": true,
281
-
"default": true
282
-
},
283
-
"user_id": {
284
-
"name": "user_id",
285
-
"type": "text",
286
-
"primaryKey": false,
287
-
"notNull": true
288
-
},
289
-
"xata_createdat": {
290
-
"name": "xata_createdat",
291
-
"type": "timestamp",
292
-
"primaryKey": false,
293
-
"notNull": true,
294
-
"default": "now()"
295
-
},
296
-
"xata_updatedat": {
297
-
"name": "xata_updatedat",
298
-
"type": "timestamp",
299
-
"primaryKey": false,
300
-
"notNull": true,
301
-
"default": "now()"
302
-
}
303
-
},
304
-
"indexes": {},
305
-
"foreignKeys": {
306
-
"api_keys_user_id_users_xata_id_fk": {
307
-
"name": "api_keys_user_id_users_xata_id_fk",
308
-
"tableFrom": "api_keys",
309
-
"tableTo": "users",
310
-
"columnsFrom": [
311
-
"user_id"
312
-
],
313
-
"columnsTo": [
314
-
"xata_id"
315
-
],
316
-
"onDelete": "no action",
317
-
"onUpdate": "no action"
318
-
}
319
-
},
320
-
"compositePrimaryKeys": {},
321
-
"uniqueConstraints": {},
322
-
"policies": {},
323
-
"checkConstraints": {},
324
-
"isRLSEnabled": false
325
-
},
326
-
"public.artist_albums": {
327
-
"name": "artist_albums",
328
-
"schema": "",
329
-
"columns": {
330
-
"xata_id": {
331
-
"name": "xata_id",
332
-
"type": "text",
333
-
"primaryKey": true,
334
-
"notNull": true,
335
-
"default": "xata_id()"
336
-
},
337
-
"artist_id": {
338
-
"name": "artist_id",
339
-
"type": "text",
340
-
"primaryKey": false,
341
-
"notNull": true
342
-
},
343
-
"album_id": {
344
-
"name": "album_id",
345
-
"type": "text",
346
-
"primaryKey": false,
347
-
"notNull": true
348
-
},
349
-
"xata_createdat": {
350
-
"name": "xata_createdat",
351
-
"type": "timestamp",
352
-
"primaryKey": false,
353
-
"notNull": true,
354
-
"default": "now()"
355
-
},
356
-
"xata_updatedat": {
357
-
"name": "xata_updatedat",
358
-
"type": "timestamp",
359
-
"primaryKey": false,
360
-
"notNull": true,
361
-
"default": "now()"
362
-
},
363
-
"xata_version": {
364
-
"name": "xata_version",
365
-
"type": "integer",
366
-
"primaryKey": false,
367
-
"notNull": false
368
-
}
369
-
},
370
-
"indexes": {},
371
-
"foreignKeys": {
372
-
"artist_albums_artist_id_artists_xata_id_fk": {
373
-
"name": "artist_albums_artist_id_artists_xata_id_fk",
374
-
"tableFrom": "artist_albums",
375
-
"tableTo": "artists",
376
-
"columnsFrom": [
377
-
"artist_id"
378
-
],
379
-
"columnsTo": [
380
-
"xata_id"
381
-
],
382
-
"onDelete": "no action",
383
-
"onUpdate": "no action"
384
-
},
385
-
"artist_albums_album_id_albums_xata_id_fk": {
386
-
"name": "artist_albums_album_id_albums_xata_id_fk",
387
-
"tableFrom": "artist_albums",
388
-
"tableTo": "albums",
389
-
"columnsFrom": [
390
-
"album_id"
391
-
],
392
-
"columnsTo": [
393
-
"xata_id"
394
-
],
395
-
"onDelete": "no action",
396
-
"onUpdate": "no action"
397
-
}
398
-
},
399
-
"compositePrimaryKeys": {},
400
-
"uniqueConstraints": {},
401
-
"policies": {},
402
-
"checkConstraints": {},
403
-
"isRLSEnabled": false
404
-
},
405
-
"public.artist_tracks": {
406
-
"name": "artist_tracks",
407
-
"schema": "",
408
-
"columns": {
409
-
"xata_id": {
410
-
"name": "xata_id",
411
-
"type": "text",
412
-
"primaryKey": true,
413
-
"notNull": true,
414
-
"default": "xata_id()"
415
-
},
416
-
"artist_id": {
417
-
"name": "artist_id",
418
-
"type": "text",
419
-
"primaryKey": false,
420
-
"notNull": true
421
-
},
422
-
"track_id": {
423
-
"name": "track_id",
424
-
"type": "text",
425
-
"primaryKey": false,
426
-
"notNull": true
427
-
},
428
-
"xata_createdat": {
429
-
"name": "xata_createdat",
430
-
"type": "timestamp",
431
-
"primaryKey": false,
432
-
"notNull": true,
433
-
"default": "now()"
434
-
},
435
-
"xata_updatedat": {
436
-
"name": "xata_updatedat",
437
-
"type": "timestamp",
438
-
"primaryKey": false,
439
-
"notNull": true,
440
-
"default": "now()"
441
-
},
442
-
"xata_version": {
443
-
"name": "xata_version",
444
-
"type": "integer",
445
-
"primaryKey": false,
446
-
"notNull": false
447
-
}
448
-
},
449
-
"indexes": {},
450
-
"foreignKeys": {
451
-
"artist_tracks_artist_id_artists_xata_id_fk": {
452
-
"name": "artist_tracks_artist_id_artists_xata_id_fk",
453
-
"tableFrom": "artist_tracks",
454
-
"tableTo": "artists",
455
-
"columnsFrom": [
456
-
"artist_id"
457
-
],
458
-
"columnsTo": [
459
-
"xata_id"
460
-
],
461
-
"onDelete": "no action",
462
-
"onUpdate": "no action"
463
-
},
464
-
"artist_tracks_track_id_tracks_xata_id_fk": {
465
-
"name": "artist_tracks_track_id_tracks_xata_id_fk",
466
-
"tableFrom": "artist_tracks",
467
-
"tableTo": "tracks",
468
-
"columnsFrom": [
469
-
"track_id"
470
-
],
471
-
"columnsTo": [
472
-
"xata_id"
473
-
],
474
-
"onDelete": "no action",
475
-
"onUpdate": "no action"
476
-
}
477
-
},
478
-
"compositePrimaryKeys": {},
479
-
"uniqueConstraints": {},
480
-
"policies": {},
481
-
"checkConstraints": {},
482
-
"isRLSEnabled": false
483
-
},
484
-
"public.artists": {
485
-
"name": "artists",
486
-
"schema": "",
487
-
"columns": {
488
-
"xata_id": {
489
-
"name": "xata_id",
490
-
"type": "text",
491
-
"primaryKey": true,
492
-
"notNull": true,
493
-
"default": "xata_id()"
494
-
},
495
-
"name": {
496
-
"name": "name",
497
-
"type": "text",
498
-
"primaryKey": false,
499
-
"notNull": true
500
-
},
501
-
"biography": {
502
-
"name": "biography",
503
-
"type": "text",
504
-
"primaryKey": false,
505
-
"notNull": false
506
-
},
507
-
"born": {
508
-
"name": "born",
509
-
"type": "timestamp",
510
-
"primaryKey": false,
511
-
"notNull": false
512
-
},
513
-
"born_in": {
514
-
"name": "born_in",
515
-
"type": "text",
516
-
"primaryKey": false,
517
-
"notNull": false
518
-
},
519
-
"died": {
520
-
"name": "died",
521
-
"type": "timestamp",
522
-
"primaryKey": false,
523
-
"notNull": false
524
-
},
525
-
"picture": {
526
-
"name": "picture",
527
-
"type": "text",
528
-
"primaryKey": false,
529
-
"notNull": false
530
-
},
531
-
"sha256": {
532
-
"name": "sha256",
533
-
"type": "text",
534
-
"primaryKey": false,
535
-
"notNull": true
536
-
},
537
-
"uri": {
538
-
"name": "uri",
539
-
"type": "text",
540
-
"primaryKey": false,
541
-
"notNull": false
542
-
},
543
-
"apple_music_link": {
544
-
"name": "apple_music_link",
545
-
"type": "text",
546
-
"primaryKey": false,
547
-
"notNull": false
548
-
},
549
-
"spotify_link": {
550
-
"name": "spotify_link",
551
-
"type": "text",
552
-
"primaryKey": false,
553
-
"notNull": false
554
-
},
555
-
"tidal_link": {
556
-
"name": "tidal_link",
557
-
"type": "text",
558
-
"primaryKey": false,
559
-
"notNull": false
560
-
},
561
-
"youtube_link": {
562
-
"name": "youtube_link",
563
-
"type": "text",
564
-
"primaryKey": false,
565
-
"notNull": false
566
-
},
567
-
"genres": {
568
-
"name": "genres",
569
-
"type": "text[]",
570
-
"primaryKey": false,
571
-
"notNull": false
572
-
},
573
-
"xata_createdat": {
574
-
"name": "xata_createdat",
575
-
"type": "timestamp",
576
-
"primaryKey": false,
577
-
"notNull": true,
578
-
"default": "now()"
579
-
},
580
-
"xata_updatedat": {
581
-
"name": "xata_updatedat",
582
-
"type": "timestamp",
583
-
"primaryKey": false,
584
-
"notNull": true,
585
-
"default": "now()"
586
-
},
587
-
"xata_version": {
588
-
"name": "xata_version",
589
-
"type": "integer",
590
-
"primaryKey": false,
591
-
"notNull": false
592
-
}
593
-
},
594
-
"indexes": {},
595
-
"foreignKeys": {},
596
-
"compositePrimaryKeys": {},
597
-
"uniqueConstraints": {
598
-
"artists_sha256_unique": {
599
-
"name": "artists_sha256_unique",
600
-
"nullsNotDistinct": false,
601
-
"columns": [
602
-
"sha256"
603
-
]
604
-
},
605
-
"artists_uri_unique": {
606
-
"name": "artists_uri_unique",
607
-
"nullsNotDistinct": false,
608
-
"columns": [
609
-
"uri"
610
-
]
611
-
}
612
-
},
613
-
"policies": {},
614
-
"checkConstraints": {},
615
-
"isRLSEnabled": false
616
-
},
617
-
"public.dropbox_accounts": {
618
-
"name": "dropbox_accounts",
619
-
"schema": "",
620
-
"columns": {
621
-
"xata_id": {
622
-
"name": "xata_id",
623
-
"type": "text",
624
-
"primaryKey": true,
625
-
"notNull": true,
626
-
"default": "xata_id()"
627
-
},
628
-
"email": {
629
-
"name": "email",
630
-
"type": "text",
631
-
"primaryKey": false,
632
-
"notNull": true
633
-
},
634
-
"is_beta_user": {
635
-
"name": "is_beta_user",
636
-
"type": "boolean",
637
-
"primaryKey": false,
638
-
"notNull": true,
639
-
"default": false
640
-
},
641
-
"user_id": {
642
-
"name": "user_id",
643
-
"type": "text",
644
-
"primaryKey": false,
645
-
"notNull": true
646
-
},
647
-
"xata_version": {
648
-
"name": "xata_version",
649
-
"type": "text",
650
-
"primaryKey": false,
651
-
"notNull": false
652
-
},
653
-
"xata_createdat": {
654
-
"name": "xata_createdat",
655
-
"type": "timestamp",
656
-
"primaryKey": false,
657
-
"notNull": true,
658
-
"default": "now()"
659
-
},
660
-
"xata_updatedat": {
661
-
"name": "xata_updatedat",
662
-
"type": "timestamp",
663
-
"primaryKey": false,
664
-
"notNull": true,
665
-
"default": "now()"
666
-
}
667
-
},
668
-
"indexes": {},
669
-
"foreignKeys": {
670
-
"dropbox_accounts_user_id_users_xata_id_fk": {
671
-
"name": "dropbox_accounts_user_id_users_xata_id_fk",
672
-
"tableFrom": "dropbox_accounts",
673
-
"tableTo": "users",
674
-
"columnsFrom": [
675
-
"user_id"
676
-
],
677
-
"columnsTo": [
678
-
"xata_id"
679
-
],
680
-
"onDelete": "no action",
681
-
"onUpdate": "no action"
682
-
}
683
-
},
684
-
"compositePrimaryKeys": {},
685
-
"uniqueConstraints": {
686
-
"dropbox_accounts_email_unique": {
687
-
"name": "dropbox_accounts_email_unique",
688
-
"nullsNotDistinct": false,
689
-
"columns": [
690
-
"email"
691
-
]
692
-
}
693
-
},
694
-
"policies": {},
695
-
"checkConstraints": {},
696
-
"isRLSEnabled": false
697
-
},
698
-
"public.dropbox_directories": {
699
-
"name": "dropbox_directories",
700
-
"schema": "",
701
-
"columns": {
702
-
"xata_id": {
703
-
"name": "xata_id",
704
-
"type": "text",
705
-
"primaryKey": true,
706
-
"notNull": true,
707
-
"default": "xata_id()"
708
-
},
709
-
"name": {
710
-
"name": "name",
711
-
"type": "text",
712
-
"primaryKey": false,
713
-
"notNull": true
714
-
},
715
-
"path": {
716
-
"name": "path",
717
-
"type": "text",
718
-
"primaryKey": false,
719
-
"notNull": true
720
-
},
721
-
"parent_id": {
722
-
"name": "parent_id",
723
-
"type": "text",
724
-
"primaryKey": false,
725
-
"notNull": false
726
-
},
727
-
"dropbox_id": {
728
-
"name": "dropbox_id",
729
-
"type": "text",
730
-
"primaryKey": false,
731
-
"notNull": true
732
-
},
733
-
"file_id": {
734
-
"name": "file_id",
735
-
"type": "text",
736
-
"primaryKey": false,
737
-
"notNull": true
738
-
},
739
-
"xata_version": {
740
-
"name": "xata_version",
741
-
"type": "text",
742
-
"primaryKey": false,
743
-
"notNull": false
744
-
},
745
-
"xata_createdat": {
746
-
"name": "xata_createdat",
747
-
"type": "timestamp",
748
-
"primaryKey": false,
749
-
"notNull": true,
750
-
"default": "now()"
751
-
},
752
-
"xata_updatedat": {
753
-
"name": "xata_updatedat",
754
-
"type": "timestamp",
755
-
"primaryKey": false,
756
-
"notNull": true,
757
-
"default": "now()"
758
-
}
759
-
},
760
-
"indexes": {},
761
-
"foreignKeys": {
762
-
"dropbox_directories_parent_id_dropbox_directories_xata_id_fk": {
763
-
"name": "dropbox_directories_parent_id_dropbox_directories_xata_id_fk",
764
-
"tableFrom": "dropbox_directories",
765
-
"tableTo": "dropbox_directories",
766
-
"columnsFrom": [
767
-
"parent_id"
768
-
],
769
-
"columnsTo": [
770
-
"xata_id"
771
-
],
772
-
"onDelete": "no action",
773
-
"onUpdate": "no action"
774
-
}
775
-
},
776
-
"compositePrimaryKeys": {},
777
-
"uniqueConstraints": {
778
-
"dropbox_directories_file_id_unique": {
779
-
"name": "dropbox_directories_file_id_unique",
780
-
"nullsNotDistinct": false,
781
-
"columns": [
782
-
"file_id"
783
-
]
784
-
}
785
-
},
786
-
"policies": {},
787
-
"checkConstraints": {},
788
-
"isRLSEnabled": false
789
-
},
790
-
"public.dropbox_paths": {
791
-
"name": "dropbox_paths",
792
-
"schema": "",
793
-
"columns": {
794
-
"xata_id": {
795
-
"name": "xata_id",
796
-
"type": "text",
797
-
"primaryKey": true,
798
-
"notNull": true,
799
-
"default": "xata_id()"
800
-
},
801
-
"path": {
802
-
"name": "path",
803
-
"type": "text",
804
-
"primaryKey": false,
805
-
"notNull": true
806
-
},
807
-
"name": {
808
-
"name": "name",
809
-
"type": "text",
810
-
"primaryKey": false,
811
-
"notNull": true
812
-
},
813
-
"dropbox_id": {
814
-
"name": "dropbox_id",
815
-
"type": "text",
816
-
"primaryKey": false,
817
-
"notNull": true
818
-
},
819
-
"track_id": {
820
-
"name": "track_id",
821
-
"type": "text",
822
-
"primaryKey": false,
823
-
"notNull": true
824
-
},
825
-
"directory_id": {
826
-
"name": "directory_id",
827
-
"type": "text",
828
-
"primaryKey": false,
829
-
"notNull": false
830
-
},
831
-
"file_id": {
832
-
"name": "file_id",
833
-
"type": "text",
834
-
"primaryKey": false,
835
-
"notNull": true
836
-
},
837
-
"xata_version": {
838
-
"name": "xata_version",
839
-
"type": "text",
840
-
"primaryKey": false,
841
-
"notNull": false
842
-
},
843
-
"xata_createdat": {
844
-
"name": "xata_createdat",
845
-
"type": "timestamp",
846
-
"primaryKey": false,
847
-
"notNull": true,
848
-
"default": "now()"
849
-
},
850
-
"xata_updatedat": {
851
-
"name": "xata_updatedat",
852
-
"type": "timestamp",
853
-
"primaryKey": false,
854
-
"notNull": true,
855
-
"default": "now()"
856
-
}
857
-
},
858
-
"indexes": {},
859
-
"foreignKeys": {
860
-
"dropbox_paths_directory_id_dropbox_directories_xata_id_fk": {
861
-
"name": "dropbox_paths_directory_id_dropbox_directories_xata_id_fk",
862
-
"tableFrom": "dropbox_paths",
863
-
"tableTo": "dropbox_directories",
864
-
"columnsFrom": [
865
-
"directory_id"
866
-
],
867
-
"columnsTo": [
868
-
"xata_id"
869
-
],
870
-
"onDelete": "no action",
871
-
"onUpdate": "no action"
872
-
}
873
-
},
874
-
"compositePrimaryKeys": {},
875
-
"uniqueConstraints": {
876
-
"dropbox_paths_file_id_unique": {
877
-
"name": "dropbox_paths_file_id_unique",
878
-
"nullsNotDistinct": false,
879
-
"columns": [
880
-
"file_id"
881
-
]
882
-
}
883
-
},
884
-
"policies": {},
885
-
"checkConstraints": {},
886
-
"isRLSEnabled": false
887
-
},
888
-
"public.dropbox_tokens": {
889
-
"name": "dropbox_tokens",
890
-
"schema": "",
891
-
"columns": {
892
-
"xata_id": {
893
-
"name": "xata_id",
894
-
"type": "text",
895
-
"primaryKey": true,
896
-
"notNull": true,
897
-
"default": "xata_id()"
898
-
},
899
-
"refresh_token": {
900
-
"name": "refresh_token",
901
-
"type": "text",
902
-
"primaryKey": false,
903
-
"notNull": true
904
-
},
905
-
"xata_createdat": {
906
-
"name": "xata_createdat",
907
-
"type": "timestamp",
908
-
"primaryKey": false,
909
-
"notNull": true,
910
-
"default": "now()"
911
-
},
912
-
"xata_updatedat": {
913
-
"name": "xata_updatedat",
914
-
"type": "timestamp",
915
-
"primaryKey": false,
916
-
"notNull": true,
917
-
"default": "now()"
918
-
}
919
-
},
920
-
"indexes": {},
921
-
"foreignKeys": {},
922
-
"compositePrimaryKeys": {},
923
-
"uniqueConstraints": {},
924
-
"policies": {},
925
-
"checkConstraints": {},
926
-
"isRLSEnabled": false
927
-
},
928
-
"public.dropbox": {
929
-
"name": "dropbox",
930
-
"schema": "",
931
-
"columns": {
932
-
"xata_id": {
933
-
"name": "xata_id",
934
-
"type": "text",
935
-
"primaryKey": true,
936
-
"notNull": true,
937
-
"default": "xata_id()"
938
-
},
939
-
"user_id": {
940
-
"name": "user_id",
941
-
"type": "text",
942
-
"primaryKey": false,
943
-
"notNull": true
944
-
},
945
-
"dropbox_token_id": {
946
-
"name": "dropbox_token_id",
947
-
"type": "text",
948
-
"primaryKey": false,
949
-
"notNull": true
950
-
},
951
-
"xata_version": {
952
-
"name": "xata_version",
953
-
"type": "text",
954
-
"primaryKey": false,
955
-
"notNull": false
956
-
},
957
-
"xata_createdat": {
958
-
"name": "xata_createdat",
959
-
"type": "timestamp",
960
-
"primaryKey": false,
961
-
"notNull": true,
962
-
"default": "now()"
963
-
},
964
-
"xata_updatedat": {
965
-
"name": "xata_updatedat",
966
-
"type": "timestamp",
967
-
"primaryKey": false,
968
-
"notNull": true,
969
-
"default": "now()"
970
-
}
971
-
},
972
-
"indexes": {},
973
-
"foreignKeys": {
974
-
"dropbox_user_id_users_xata_id_fk": {
975
-
"name": "dropbox_user_id_users_xata_id_fk",
976
-
"tableFrom": "dropbox",
977
-
"tableTo": "users",
978
-
"columnsFrom": [
979
-
"user_id"
980
-
],
981
-
"columnsTo": [
982
-
"xata_id"
983
-
],
984
-
"onDelete": "no action",
985
-
"onUpdate": "no action"
986
-
},
987
-
"dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk": {
988
-
"name": "dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk",
989
-
"tableFrom": "dropbox",
990
-
"tableTo": "dropbox_tokens",
991
-
"columnsFrom": [
992
-
"dropbox_token_id"
993
-
],
994
-
"columnsTo": [
995
-
"xata_id"
996
-
],
997
-
"onDelete": "no action",
998
-
"onUpdate": "no action"
999
-
}
1000
-
},
1001
-
"compositePrimaryKeys": {},
1002
-
"uniqueConstraints": {},
1003
-
"policies": {},
1004
-
"checkConstraints": {},
1005
-
"isRLSEnabled": false
1006
-
},
1007
-
"public.feeds": {
1008
-
"name": "feeds",
1009
-
"schema": "",
1010
-
"columns": {
1011
-
"xata_id": {
1012
-
"name": "xata_id",
1013
-
"type": "text",
1014
-
"primaryKey": true,
1015
-
"notNull": true,
1016
-
"default": "xata_id()"
1017
-
},
1018
-
"display_name": {
1019
-
"name": "display_name",
1020
-
"type": "text",
1021
-
"primaryKey": false,
1022
-
"notNull": true
1023
-
},
1024
-
"description": {
1025
-
"name": "description",
1026
-
"type": "text",
1027
-
"primaryKey": false,
1028
-
"notNull": false
1029
-
},
1030
-
"did": {
1031
-
"name": "did",
1032
-
"type": "text",
1033
-
"primaryKey": false,
1034
-
"notNull": true
1035
-
},
1036
-
"uri": {
1037
-
"name": "uri",
1038
-
"type": "text",
1039
-
"primaryKey": false,
1040
-
"notNull": true
1041
-
},
1042
-
"avatar": {
1043
-
"name": "avatar",
1044
-
"type": "text",
1045
-
"primaryKey": false,
1046
-
"notNull": false
1047
-
},
1048
-
"user_id": {
1049
-
"name": "user_id",
1050
-
"type": "text",
1051
-
"primaryKey": false,
1052
-
"notNull": true
1053
-
},
1054
-
"xata_version": {
1055
-
"name": "xata_version",
1056
-
"type": "integer",
1057
-
"primaryKey": false,
1058
-
"notNull": false
1059
-
},
1060
-
"xata_createdat": {
1061
-
"name": "xata_createdat",
1062
-
"type": "timestamp",
1063
-
"primaryKey": false,
1064
-
"notNull": true,
1065
-
"default": "now()"
1066
-
},
1067
-
"xata_updatedat": {
1068
-
"name": "xata_updatedat",
1069
-
"type": "timestamp",
1070
-
"primaryKey": false,
1071
-
"notNull": true,
1072
-
"default": "now()"
1073
-
}
1074
-
},
1075
-
"indexes": {},
1076
-
"foreignKeys": {
1077
-
"feeds_user_id_users_xata_id_fk": {
1078
-
"name": "feeds_user_id_users_xata_id_fk",
1079
-
"tableFrom": "feeds",
1080
-
"tableTo": "users",
1081
-
"columnsFrom": [
1082
-
"user_id"
1083
-
],
1084
-
"columnsTo": [
1085
-
"xata_id"
1086
-
],
1087
-
"onDelete": "no action",
1088
-
"onUpdate": "no action"
1089
-
}
1090
-
},
1091
-
"compositePrimaryKeys": {},
1092
-
"uniqueConstraints": {
1093
-
"feeds_uri_unique": {
1094
-
"name": "feeds_uri_unique",
1095
-
"nullsNotDistinct": false,
1096
-
"columns": [
1097
-
"uri"
1098
-
]
1099
-
}
1100
-
},
1101
-
"policies": {},
1102
-
"checkConstraints": {},
1103
-
"isRLSEnabled": false
1104
-
},
1105
-
"public.follows": {
1106
-
"name": "follows",
1107
-
"schema": "",
1108
-
"columns": {
1109
-
"xata_id": {
1110
-
"name": "xata_id",
1111
-
"type": "text",
1112
-
"primaryKey": true,
1113
-
"notNull": true,
1114
-
"default": "xata_id()"
1115
-
},
1116
-
"uri": {
1117
-
"name": "uri",
1118
-
"type": "text",
1119
-
"primaryKey": false,
1120
-
"notNull": true
1121
-
},
1122
-
"follower_did": {
1123
-
"name": "follower_did",
1124
-
"type": "text",
1125
-
"primaryKey": false,
1126
-
"notNull": true
1127
-
},
1128
-
"subject_did": {
1129
-
"name": "subject_did",
1130
-
"type": "text",
1131
-
"primaryKey": false,
1132
-
"notNull": true
1133
-
},
1134
-
"xata_version": {
1135
-
"name": "xata_version",
1136
-
"type": "integer",
1137
-
"primaryKey": false,
1138
-
"notNull": false
1139
-
},
1140
-
"xata_createdat": {
1141
-
"name": "xata_createdat",
1142
-
"type": "timestamp",
1143
-
"primaryKey": false,
1144
-
"notNull": true,
1145
-
"default": "now()"
1146
-
},
1147
-
"xata_updatedat": {
1148
-
"name": "xata_updatedat",
1149
-
"type": "timestamp",
1150
-
"primaryKey": false,
1151
-
"notNull": true,
1152
-
"default": "now()"
1153
-
}
1154
-
},
1155
-
"indexes": {
1156
-
"follows_follower_subject_unique": {
1157
-
"name": "follows_follower_subject_unique",
1158
-
"columns": [
1159
-
{
1160
-
"expression": "follower_did",
1161
-
"isExpression": false,
1162
-
"asc": true,
1163
-
"nulls": "last"
1164
-
},
1165
-
{
1166
-
"expression": "subject_did",
1167
-
"isExpression": false,
1168
-
"asc": true,
1169
-
"nulls": "last"
1170
-
}
1171
-
],
1172
-
"isUnique": true,
1173
-
"concurrently": false,
1174
-
"method": "btree",
1175
-
"with": {}
1176
-
}
1177
-
},
1178
-
"foreignKeys": {},
1179
-
"compositePrimaryKeys": {},
1180
-
"uniqueConstraints": {
1181
-
"follows_uri_unique": {
1182
-
"name": "follows_uri_unique",
1183
-
"nullsNotDistinct": false,
1184
-
"columns": [
1185
-
"uri"
1186
-
]
1187
-
}
1188
-
},
1189
-
"policies": {},
1190
-
"checkConstraints": {},
1191
-
"isRLSEnabled": false
1192
-
},
1193
-
"public.google_drive_accounts": {
1194
-
"name": "google_drive_accounts",
1195
-
"schema": "",
1196
-
"columns": {
1197
-
"xata_id": {
1198
-
"name": "xata_id",
1199
-
"type": "text",
1200
-
"primaryKey": true,
1201
-
"notNull": true,
1202
-
"default": "xata_id()"
1203
-
},
1204
-
"email": {
1205
-
"name": "email",
1206
-
"type": "text",
1207
-
"primaryKey": false,
1208
-
"notNull": true
1209
-
},
1210
-
"is_beta_user": {
1211
-
"name": "is_beta_user",
1212
-
"type": "boolean",
1213
-
"primaryKey": false,
1214
-
"notNull": true,
1215
-
"default": false
1216
-
},
1217
-
"user_id": {
1218
-
"name": "user_id",
1219
-
"type": "text",
1220
-
"primaryKey": false,
1221
-
"notNull": true
1222
-
},
1223
-
"xata_version": {
1224
-
"name": "xata_version",
1225
-
"type": "text",
1226
-
"primaryKey": false,
1227
-
"notNull": false
1228
-
},
1229
-
"xata_createdat": {
1230
-
"name": "xata_createdat",
1231
-
"type": "timestamp",
1232
-
"primaryKey": false,
1233
-
"notNull": true,
1234
-
"default": "now()"
1235
-
},
1236
-
"xata_updatedat": {
1237
-
"name": "xata_updatedat",
1238
-
"type": "timestamp",
1239
-
"primaryKey": false,
1240
-
"notNull": true,
1241
-
"default": "now()"
1242
-
}
1243
-
},
1244
-
"indexes": {},
1245
-
"foreignKeys": {
1246
-
"google_drive_accounts_user_id_users_xata_id_fk": {
1247
-
"name": "google_drive_accounts_user_id_users_xata_id_fk",
1248
-
"tableFrom": "google_drive_accounts",
1249
-
"tableTo": "users",
1250
-
"columnsFrom": [
1251
-
"user_id"
1252
-
],
1253
-
"columnsTo": [
1254
-
"xata_id"
1255
-
],
1256
-
"onDelete": "no action",
1257
-
"onUpdate": "no action"
1258
-
}
1259
-
},
1260
-
"compositePrimaryKeys": {},
1261
-
"uniqueConstraints": {
1262
-
"google_drive_accounts_email_unique": {
1263
-
"name": "google_drive_accounts_email_unique",
1264
-
"nullsNotDistinct": false,
1265
-
"columns": [
1266
-
"email"
1267
-
]
1268
-
}
1269
-
},
1270
-
"policies": {},
1271
-
"checkConstraints": {},
1272
-
"isRLSEnabled": false
1273
-
},
1274
-
"public.google_drive_directories": {
1275
-
"name": "google_drive_directories",
1276
-
"schema": "",
1277
-
"columns": {
1278
-
"xata_id": {
1279
-
"name": "xata_id",
1280
-
"type": "text",
1281
-
"primaryKey": true,
1282
-
"notNull": true,
1283
-
"default": "xata_id()"
1284
-
},
1285
-
"name": {
1286
-
"name": "name",
1287
-
"type": "text",
1288
-
"primaryKey": false,
1289
-
"notNull": true
1290
-
},
1291
-
"path": {
1292
-
"name": "path",
1293
-
"type": "text",
1294
-
"primaryKey": false,
1295
-
"notNull": true
1296
-
},
1297
-
"parent_id": {
1298
-
"name": "parent_id",
1299
-
"type": "text",
1300
-
"primaryKey": false,
1301
-
"notNull": false
1302
-
},
1303
-
"google_drive_id": {
1304
-
"name": "google_drive_id",
1305
-
"type": "text",
1306
-
"primaryKey": false,
1307
-
"notNull": true
1308
-
},
1309
-
"file_id": {
1310
-
"name": "file_id",
1311
-
"type": "text",
1312
-
"primaryKey": false,
1313
-
"notNull": true
1314
-
},
1315
-
"xata_version": {
1316
-
"name": "xata_version",
1317
-
"type": "text",
1318
-
"primaryKey": false,
1319
-
"notNull": false
1320
-
},
1321
-
"xata_createdat": {
1322
-
"name": "xata_createdat",
1323
-
"type": "timestamp",
1324
-
"primaryKey": false,
1325
-
"notNull": true,
1326
-
"default": "now()"
1327
-
},
1328
-
"xata_updatedat": {
1329
-
"name": "xata_updatedat",
1330
-
"type": "timestamp",
1331
-
"primaryKey": false,
1332
-
"notNull": true,
1333
-
"default": "now()"
1334
-
}
1335
-
},
1336
-
"indexes": {},
1337
-
"foreignKeys": {
1338
-
"google_drive_directories_parent_id_google_drive_directories_xata_id_fk": {
1339
-
"name": "google_drive_directories_parent_id_google_drive_directories_xata_id_fk",
1340
-
"tableFrom": "google_drive_directories",
1341
-
"tableTo": "google_drive_directories",
1342
-
"columnsFrom": [
1343
-
"parent_id"
1344
-
],
1345
-
"columnsTo": [
1346
-
"xata_id"
1347
-
],
1348
-
"onDelete": "no action",
1349
-
"onUpdate": "no action"
1350
-
}
1351
-
},
1352
-
"compositePrimaryKeys": {},
1353
-
"uniqueConstraints": {
1354
-
"google_drive_directories_file_id_unique": {
1355
-
"name": "google_drive_directories_file_id_unique",
1356
-
"nullsNotDistinct": false,
1357
-
"columns": [
1358
-
"file_id"
1359
-
]
1360
-
}
1361
-
},
1362
-
"policies": {},
1363
-
"checkConstraints": {},
1364
-
"isRLSEnabled": false
1365
-
},
1366
-
"public.google_drive_paths": {
1367
-
"name": "google_drive_paths",
1368
-
"schema": "",
1369
-
"columns": {
1370
-
"xata_id": {
1371
-
"name": "xata_id",
1372
-
"type": "text",
1373
-
"primaryKey": true,
1374
-
"notNull": true,
1375
-
"default": "xata_id()"
1376
-
},
1377
-
"google_drive_id": {
1378
-
"name": "google_drive_id",
1379
-
"type": "text",
1380
-
"primaryKey": false,
1381
-
"notNull": true
1382
-
},
1383
-
"track_id": {
1384
-
"name": "track_id",
1385
-
"type": "text",
1386
-
"primaryKey": false,
1387
-
"notNull": true
1388
-
},
1389
-
"name": {
1390
-
"name": "name",
1391
-
"type": "text",
1392
-
"primaryKey": false,
1393
-
"notNull": true
1394
-
},
1395
-
"directory_id": {
1396
-
"name": "directory_id",
1397
-
"type": "text",
1398
-
"primaryKey": false,
1399
-
"notNull": false
1400
-
},
1401
-
"file_id": {
1402
-
"name": "file_id",
1403
-
"type": "text",
1404
-
"primaryKey": false,
1405
-
"notNull": true
1406
-
},
1407
-
"xata_version": {
1408
-
"name": "xata_version",
1409
-
"type": "text",
1410
-
"primaryKey": false,
1411
-
"notNull": false
1412
-
},
1413
-
"xata_createdat": {
1414
-
"name": "xata_createdat",
1415
-
"type": "timestamp",
1416
-
"primaryKey": false,
1417
-
"notNull": true,
1418
-
"default": "now()"
1419
-
},
1420
-
"xata_updatedat": {
1421
-
"name": "xata_updatedat",
1422
-
"type": "timestamp",
1423
-
"primaryKey": false,
1424
-
"notNull": true,
1425
-
"default": "now()"
1426
-
}
1427
-
},
1428
-
"indexes": {},
1429
-
"foreignKeys": {
1430
-
"google_drive_paths_directory_id_google_drive_directories_xata_id_fk": {
1431
-
"name": "google_drive_paths_directory_id_google_drive_directories_xata_id_fk",
1432
-
"tableFrom": "google_drive_paths",
1433
-
"tableTo": "google_drive_directories",
1434
-
"columnsFrom": [
1435
-
"directory_id"
1436
-
],
1437
-
"columnsTo": [
1438
-
"xata_id"
1439
-
],
1440
-
"onDelete": "no action",
1441
-
"onUpdate": "no action"
1442
-
}
1443
-
},
1444
-
"compositePrimaryKeys": {},
1445
-
"uniqueConstraints": {
1446
-
"google_drive_paths_file_id_unique": {
1447
-
"name": "google_drive_paths_file_id_unique",
1448
-
"nullsNotDistinct": false,
1449
-
"columns": [
1450
-
"file_id"
1451
-
]
1452
-
}
1453
-
},
1454
-
"policies": {},
1455
-
"checkConstraints": {},
1456
-
"isRLSEnabled": false
1457
-
},
1458
-
"public.google_drive_tokens": {
1459
-
"name": "google_drive_tokens",
1460
-
"schema": "",
1461
-
"columns": {
1462
-
"xata_id": {
1463
-
"name": "xata_id",
1464
-
"type": "text",
1465
-
"primaryKey": true,
1466
-
"notNull": true,
1467
-
"default": "xata_id()"
1468
-
},
1469
-
"refresh_token": {
1470
-
"name": "refresh_token",
1471
-
"type": "text",
1472
-
"primaryKey": false,
1473
-
"notNull": true
1474
-
},
1475
-
"xata_createdat": {
1476
-
"name": "xata_createdat",
1477
-
"type": "timestamp",
1478
-
"primaryKey": false,
1479
-
"notNull": true,
1480
-
"default": "now()"
1481
-
},
1482
-
"xata_updatedat": {
1483
-
"name": "xata_updatedat",
1484
-
"type": "timestamp",
1485
-
"primaryKey": false,
1486
-
"notNull": true,
1487
-
"default": "now()"
1488
-
}
1489
-
},
1490
-
"indexes": {},
1491
-
"foreignKeys": {},
1492
-
"compositePrimaryKeys": {},
1493
-
"uniqueConstraints": {},
1494
-
"policies": {},
1495
-
"checkConstraints": {},
1496
-
"isRLSEnabled": false
1497
-
},
1498
-
"public.google_drive": {
1499
-
"name": "google_drive",
1500
-
"schema": "",
1501
-
"columns": {
1502
-
"xata_id": {
1503
-
"name": "xata_id",
1504
-
"type": "text",
1505
-
"primaryKey": true,
1506
-
"notNull": true,
1507
-
"default": "xata_id()"
1508
-
},
1509
-
"google_drive_token_id": {
1510
-
"name": "google_drive_token_id",
1511
-
"type": "text",
1512
-
"primaryKey": false,
1513
-
"notNull": true
1514
-
},
1515
-
"user_id": {
1516
-
"name": "user_id",
1517
-
"type": "text",
1518
-
"primaryKey": false,
1519
-
"notNull": true
1520
-
},
1521
-
"xata_version": {
1522
-
"name": "xata_version",
1523
-
"type": "text",
1524
-
"primaryKey": false,
1525
-
"notNull": false
1526
-
},
1527
-
"xata_createdat": {
1528
-
"name": "xata_createdat",
1529
-
"type": "timestamp",
1530
-
"primaryKey": false,
1531
-
"notNull": true,
1532
-
"default": "now()"
1533
-
},
1534
-
"xata_updatedat": {
1535
-
"name": "xata_updatedat",
1536
-
"type": "timestamp",
1537
-
"primaryKey": false,
1538
-
"notNull": true,
1539
-
"default": "now()"
1540
-
}
1541
-
},
1542
-
"indexes": {},
1543
-
"foreignKeys": {
1544
-
"google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk": {
1545
-
"name": "google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk",
1546
-
"tableFrom": "google_drive",
1547
-
"tableTo": "google_drive_tokens",
1548
-
"columnsFrom": [
1549
-
"google_drive_token_id"
1550
-
],
1551
-
"columnsTo": [
1552
-
"xata_id"
1553
-
],
1554
-
"onDelete": "no action",
1555
-
"onUpdate": "no action"
1556
-
},
1557
-
"google_drive_user_id_users_xata_id_fk": {
1558
-
"name": "google_drive_user_id_users_xata_id_fk",
1559
-
"tableFrom": "google_drive",
1560
-
"tableTo": "users",
1561
-
"columnsFrom": [
1562
-
"user_id"
1563
-
],
1564
-
"columnsTo": [
1565
-
"xata_id"
1566
-
],
1567
-
"onDelete": "no action",
1568
-
"onUpdate": "no action"
1569
-
}
1570
-
},
1571
-
"compositePrimaryKeys": {},
1572
-
"uniqueConstraints": {},
1573
-
"policies": {},
1574
-
"checkConstraints": {},
1575
-
"isRLSEnabled": false
1576
-
},
1577
-
"public.loved_tracks": {
1578
-
"name": "loved_tracks",
1579
-
"schema": "",
1580
-
"columns": {
1581
-
"xata_id": {
1582
-
"name": "xata_id",
1583
-
"type": "text",
1584
-
"primaryKey": true,
1585
-
"notNull": true,
1586
-
"default": "xata_id()"
1587
-
},
1588
-
"user_id": {
1589
-
"name": "user_id",
1590
-
"type": "text",
1591
-
"primaryKey": false,
1592
-
"notNull": true
1593
-
},
1594
-
"track_id": {
1595
-
"name": "track_id",
1596
-
"type": "text",
1597
-
"primaryKey": false,
1598
-
"notNull": true
1599
-
},
1600
-
"uri": {
1601
-
"name": "uri",
1602
-
"type": "text",
1603
-
"primaryKey": false,
1604
-
"notNull": false
1605
-
},
1606
-
"xata_createdat": {
1607
-
"name": "xata_createdat",
1608
-
"type": "timestamp",
1609
-
"primaryKey": false,
1610
-
"notNull": true,
1611
-
"default": "now()"
1612
-
}
1613
-
},
1614
-
"indexes": {},
1615
-
"foreignKeys": {
1616
-
"loved_tracks_user_id_users_xata_id_fk": {
1617
-
"name": "loved_tracks_user_id_users_xata_id_fk",
1618
-
"tableFrom": "loved_tracks",
1619
-
"tableTo": "users",
1620
-
"columnsFrom": [
1621
-
"user_id"
1622
-
],
1623
-
"columnsTo": [
1624
-
"xata_id"
1625
-
],
1626
-
"onDelete": "no action",
1627
-
"onUpdate": "no action"
1628
-
},
1629
-
"loved_tracks_track_id_tracks_xata_id_fk": {
1630
-
"name": "loved_tracks_track_id_tracks_xata_id_fk",
1631
-
"tableFrom": "loved_tracks",
1632
-
"tableTo": "tracks",
1633
-
"columnsFrom": [
1634
-
"track_id"
1635
-
],
1636
-
"columnsTo": [
1637
-
"xata_id"
1638
-
],
1639
-
"onDelete": "no action",
1640
-
"onUpdate": "no action"
1641
-
}
1642
-
},
1643
-
"compositePrimaryKeys": {},
1644
-
"uniqueConstraints": {
1645
-
"loved_tracks_uri_unique": {
1646
-
"name": "loved_tracks_uri_unique",
1647
-
"nullsNotDistinct": false,
1648
-
"columns": [
1649
-
"uri"
1650
-
]
1651
-
}
1652
-
},
1653
-
"policies": {},
1654
-
"checkConstraints": {},
1655
-
"isRLSEnabled": false
1656
-
},
1657
-
"public.playlist_tracks": {
1658
-
"name": "playlist_tracks",
1659
-
"schema": "",
1660
-
"columns": {
1661
-
"xata_id": {
1662
-
"name": "xata_id",
1663
-
"type": "text",
1664
-
"primaryKey": true,
1665
-
"notNull": true,
1666
-
"default": "xata_id()"
1667
-
},
1668
-
"playlist_id": {
1669
-
"name": "playlist_id",
1670
-
"type": "text",
1671
-
"primaryKey": false,
1672
-
"notNull": true
1673
-
},
1674
-
"track_id": {
1675
-
"name": "track_id",
1676
-
"type": "text",
1677
-
"primaryKey": false,
1678
-
"notNull": true
1679
-
},
1680
-
"xata_createdat": {
1681
-
"name": "xata_createdat",
1682
-
"type": "timestamp",
1683
-
"primaryKey": false,
1684
-
"notNull": true,
1685
-
"default": "now()"
1686
-
}
1687
-
},
1688
-
"indexes": {},
1689
-
"foreignKeys": {
1690
-
"playlist_tracks_playlist_id_playlists_xata_id_fk": {
1691
-
"name": "playlist_tracks_playlist_id_playlists_xata_id_fk",
1692
-
"tableFrom": "playlist_tracks",
1693
-
"tableTo": "playlists",
1694
-
"columnsFrom": [
1695
-
"playlist_id"
1696
-
],
1697
-
"columnsTo": [
1698
-
"xata_id"
1699
-
],
1700
-
"onDelete": "no action",
1701
-
"onUpdate": "no action"
1702
-
},
1703
-
"playlist_tracks_track_id_tracks_xata_id_fk": {
1704
-
"name": "playlist_tracks_track_id_tracks_xata_id_fk",
1705
-
"tableFrom": "playlist_tracks",
1706
-
"tableTo": "tracks",
1707
-
"columnsFrom": [
1708
-
"track_id"
1709
-
],
1710
-
"columnsTo": [
1711
-
"xata_id"
1712
-
],
1713
-
"onDelete": "no action",
1714
-
"onUpdate": "no action"
1715
-
}
1716
-
},
1717
-
"compositePrimaryKeys": {},
1718
-
"uniqueConstraints": {},
1719
-
"policies": {},
1720
-
"checkConstraints": {},
1721
-
"isRLSEnabled": false
1722
-
},
1723
-
"public.playlists": {
1724
-
"name": "playlists",
1725
-
"schema": "",
1726
-
"columns": {
1727
-
"xata_id": {
1728
-
"name": "xata_id",
1729
-
"type": "text",
1730
-
"primaryKey": true,
1731
-
"notNull": true,
1732
-
"default": "xata_id()"
1733
-
},
1734
-
"name": {
1735
-
"name": "name",
1736
-
"type": "text",
1737
-
"primaryKey": false,
1738
-
"notNull": true
1739
-
},
1740
-
"picture": {
1741
-
"name": "picture",
1742
-
"type": "text",
1743
-
"primaryKey": false,
1744
-
"notNull": false
1745
-
},
1746
-
"description": {
1747
-
"name": "description",
1748
-
"type": "text",
1749
-
"primaryKey": false,
1750
-
"notNull": false
1751
-
},
1752
-
"uri": {
1753
-
"name": "uri",
1754
-
"type": "text",
1755
-
"primaryKey": false,
1756
-
"notNull": false
1757
-
},
1758
-
"spotify_link": {
1759
-
"name": "spotify_link",
1760
-
"type": "text",
1761
-
"primaryKey": false,
1762
-
"notNull": false
1763
-
},
1764
-
"tidal_link": {
1765
-
"name": "tidal_link",
1766
-
"type": "text",
1767
-
"primaryKey": false,
1768
-
"notNull": false
1769
-
},
1770
-
"apple_music_link": {
1771
-
"name": "apple_music_link",
1772
-
"type": "text",
1773
-
"primaryKey": false,
1774
-
"notNull": false
1775
-
},
1776
-
"created_by": {
1777
-
"name": "created_by",
1778
-
"type": "text",
1779
-
"primaryKey": false,
1780
-
"notNull": true
1781
-
},
1782
-
"xata_createdat": {
1783
-
"name": "xata_createdat",
1784
-
"type": "timestamp",
1785
-
"primaryKey": false,
1786
-
"notNull": true,
1787
-
"default": "now()"
1788
-
},
1789
-
"xata_updatedat": {
1790
-
"name": "xata_updatedat",
1791
-
"type": "timestamp",
1792
-
"primaryKey": false,
1793
-
"notNull": true,
1794
-
"default": "now()"
1795
-
}
1796
-
},
1797
-
"indexes": {},
1798
-
"foreignKeys": {
1799
-
"playlists_created_by_users_xata_id_fk": {
1800
-
"name": "playlists_created_by_users_xata_id_fk",
1801
-
"tableFrom": "playlists",
1802
-
"tableTo": "users",
1803
-
"columnsFrom": [
1804
-
"created_by"
1805
-
],
1806
-
"columnsTo": [
1807
-
"xata_id"
1808
-
],
1809
-
"onDelete": "no action",
1810
-
"onUpdate": "no action"
1811
-
}
1812
-
},
1813
-
"compositePrimaryKeys": {},
1814
-
"uniqueConstraints": {
1815
-
"playlists_uri_unique": {
1816
-
"name": "playlists_uri_unique",
1817
-
"nullsNotDistinct": false,
1818
-
"columns": [
1819
-
"uri"
1820
-
]
1821
-
}
1822
-
},
1823
-
"policies": {},
1824
-
"checkConstraints": {},
1825
-
"isRLSEnabled": false
1826
-
},
1827
-
"public.profile_shouts": {
1828
-
"name": "profile_shouts",
1829
-
"schema": "",
1830
-
"columns": {
1831
-
"xata_id": {
1832
-
"name": "xata_id",
1833
-
"type": "text",
1834
-
"primaryKey": true,
1835
-
"notNull": true,
1836
-
"default": "xata_id()"
1837
-
},
1838
-
"user_id": {
1839
-
"name": "user_id",
1840
-
"type": "text",
1841
-
"primaryKey": false,
1842
-
"notNull": true
1843
-
},
1844
-
"shout_id": {
1845
-
"name": "shout_id",
1846
-
"type": "text",
1847
-
"primaryKey": false,
1848
-
"notNull": true
1849
-
},
1850
-
"xata_createdat": {
1851
-
"name": "xata_createdat",
1852
-
"type": "timestamp",
1853
-
"primaryKey": false,
1854
-
"notNull": true,
1855
-
"default": "now()"
1856
-
}
1857
-
},
1858
-
"indexes": {},
1859
-
"foreignKeys": {
1860
-
"profile_shouts_user_id_users_xata_id_fk": {
1861
-
"name": "profile_shouts_user_id_users_xata_id_fk",
1862
-
"tableFrom": "profile_shouts",
1863
-
"tableTo": "users",
1864
-
"columnsFrom": [
1865
-
"user_id"
1866
-
],
1867
-
"columnsTo": [
1868
-
"xata_id"
1869
-
],
1870
-
"onDelete": "no action",
1871
-
"onUpdate": "no action"
1872
-
},
1873
-
"profile_shouts_shout_id_shouts_xata_id_fk": {
1874
-
"name": "profile_shouts_shout_id_shouts_xata_id_fk",
1875
-
"tableFrom": "profile_shouts",
1876
-
"tableTo": "shouts",
1877
-
"columnsFrom": [
1878
-
"shout_id"
1879
-
],
1880
-
"columnsTo": [
1881
-
"xata_id"
1882
-
],
1883
-
"onDelete": "no action",
1884
-
"onUpdate": "no action"
1885
-
}
1886
-
},
1887
-
"compositePrimaryKeys": {},
1888
-
"uniqueConstraints": {},
1889
-
"policies": {},
1890
-
"checkConstraints": {},
1891
-
"isRLSEnabled": false
1892
-
},
1893
-
"public.queue_tracks": {
1894
-
"name": "queue_tracks",
1895
-
"schema": "",
1896
-
"columns": {
1897
-
"xata_id": {
1898
-
"name": "xata_id",
1899
-
"type": "text",
1900
-
"primaryKey": true,
1901
-
"notNull": true,
1902
-
"default": "xata_id()"
1903
-
},
1904
-
"user_id": {
1905
-
"name": "user_id",
1906
-
"type": "text",
1907
-
"primaryKey": false,
1908
-
"notNull": true
1909
-
},
1910
-
"track_id": {
1911
-
"name": "track_id",
1912
-
"type": "text",
1913
-
"primaryKey": false,
1914
-
"notNull": true
1915
-
},
1916
-
"position": {
1917
-
"name": "position",
1918
-
"type": "integer",
1919
-
"primaryKey": false,
1920
-
"notNull": true
1921
-
},
1922
-
"file_uri": {
1923
-
"name": "file_uri",
1924
-
"type": "text",
1925
-
"primaryKey": false,
1926
-
"notNull": true
1927
-
},
1928
-
"xata_version": {
1929
-
"name": "xata_version",
1930
-
"type": "integer",
1931
-
"primaryKey": false,
1932
-
"notNull": true,
1933
-
"default": 0
1934
-
},
1935
-
"xata_createdat": {
1936
-
"name": "xata_createdat",
1937
-
"type": "timestamp",
1938
-
"primaryKey": false,
1939
-
"notNull": true,
1940
-
"default": "now()"
1941
-
},
1942
-
"xata_updatedat": {
1943
-
"name": "xata_updatedat",
1944
-
"type": "timestamp",
1945
-
"primaryKey": false,
1946
-
"notNull": true,
1947
-
"default": "now()"
1948
-
}
1949
-
},
1950
-
"indexes": {},
1951
-
"foreignKeys": {
1952
-
"queue_tracks_user_id_users_xata_id_fk": {
1953
-
"name": "queue_tracks_user_id_users_xata_id_fk",
1954
-
"tableFrom": "queue_tracks",
1955
-
"tableTo": "users",
1956
-
"columnsFrom": [
1957
-
"user_id"
1958
-
],
1959
-
"columnsTo": [
1960
-
"xata_id"
1961
-
],
1962
-
"onDelete": "no action",
1963
-
"onUpdate": "no action"
1964
-
},
1965
-
"queue_tracks_track_id_tracks_xata_id_fk": {
1966
-
"name": "queue_tracks_track_id_tracks_xata_id_fk",
1967
-
"tableFrom": "queue_tracks",
1968
-
"tableTo": "tracks",
1969
-
"columnsFrom": [
1970
-
"track_id"
1971
-
],
1972
-
"columnsTo": [
1973
-
"xata_id"
1974
-
],
1975
-
"onDelete": "no action",
1976
-
"onUpdate": "no action"
1977
-
}
1978
-
},
1979
-
"compositePrimaryKeys": {},
1980
-
"uniqueConstraints": {},
1981
-
"policies": {},
1982
-
"checkConstraints": {},
1983
-
"isRLSEnabled": false
1984
-
},
1985
-
"public.scrobbles": {
1986
-
"name": "scrobbles",
1987
-
"schema": "",
1988
-
"columns": {
1989
-
"xata_id": {
1990
-
"name": "xata_id",
1991
-
"type": "text",
1992
-
"primaryKey": true,
1993
-
"notNull": true,
1994
-
"default": "xata_id()"
1995
-
},
1996
-
"user_id": {
1997
-
"name": "user_id",
1998
-
"type": "text",
1999
-
"primaryKey": false,
2000
-
"notNull": false
2001
-
},
2002
-
"track_id": {
2003
-
"name": "track_id",
2004
-
"type": "text",
2005
-
"primaryKey": false,
2006
-
"notNull": false
2007
-
},
2008
-
"album_id": {
2009
-
"name": "album_id",
2010
-
"type": "text",
2011
-
"primaryKey": false,
2012
-
"notNull": false
2013
-
},
2014
-
"artist_id": {
2015
-
"name": "artist_id",
2016
-
"type": "text",
2017
-
"primaryKey": false,
2018
-
"notNull": false
2019
-
},
2020
-
"uri": {
2021
-
"name": "uri",
2022
-
"type": "text",
2023
-
"primaryKey": false,
2024
-
"notNull": false
2025
-
},
2026
-
"xata_createdat": {
2027
-
"name": "xata_createdat",
2028
-
"type": "timestamp",
2029
-
"primaryKey": false,
2030
-
"notNull": true,
2031
-
"default": "now()"
2032
-
},
2033
-
"xata_updatedat": {
2034
-
"name": "xata_updatedat",
2035
-
"type": "timestamp",
2036
-
"primaryKey": false,
2037
-
"notNull": true,
2038
-
"default": "now()"
2039
-
},
2040
-
"xata_version": {
2041
-
"name": "xata_version",
2042
-
"type": "integer",
2043
-
"primaryKey": false,
2044
-
"notNull": false
2045
-
},
2046
-
"timestamp": {
2047
-
"name": "timestamp",
2048
-
"type": "timestamp",
2049
-
"primaryKey": false,
2050
-
"notNull": true,
2051
-
"default": "now()"
2052
-
}
2053
-
},
2054
-
"indexes": {},
2055
-
"foreignKeys": {
2056
-
"scrobbles_user_id_users_xata_id_fk": {
2057
-
"name": "scrobbles_user_id_users_xata_id_fk",
2058
-
"tableFrom": "scrobbles",
2059
-
"tableTo": "users",
2060
-
"columnsFrom": [
2061
-
"user_id"
2062
-
],
2063
-
"columnsTo": [
2064
-
"xata_id"
2065
-
],
2066
-
"onDelete": "no action",
2067
-
"onUpdate": "no action"
2068
-
},
2069
-
"scrobbles_track_id_tracks_xata_id_fk": {
2070
-
"name": "scrobbles_track_id_tracks_xata_id_fk",
2071
-
"tableFrom": "scrobbles",
2072
-
"tableTo": "tracks",
2073
-
"columnsFrom": [
2074
-
"track_id"
2075
-
],
2076
-
"columnsTo": [
2077
-
"xata_id"
2078
-
],
2079
-
"onDelete": "no action",
2080
-
"onUpdate": "no action"
2081
-
},
2082
-
"scrobbles_album_id_albums_xata_id_fk": {
2083
-
"name": "scrobbles_album_id_albums_xata_id_fk",
2084
-
"tableFrom": "scrobbles",
2085
-
"tableTo": "albums",
2086
-
"columnsFrom": [
2087
-
"album_id"
2088
-
],
2089
-
"columnsTo": [
2090
-
"xata_id"
2091
-
],
2092
-
"onDelete": "no action",
2093
-
"onUpdate": "no action"
2094
-
},
2095
-
"scrobbles_artist_id_artists_xata_id_fk": {
2096
-
"name": "scrobbles_artist_id_artists_xata_id_fk",
2097
-
"tableFrom": "scrobbles",
2098
-
"tableTo": "artists",
2099
-
"columnsFrom": [
2100
-
"artist_id"
2101
-
],
2102
-
"columnsTo": [
2103
-
"xata_id"
2104
-
],
2105
-
"onDelete": "no action",
2106
-
"onUpdate": "no action"
2107
-
}
2108
-
},
2109
-
"compositePrimaryKeys": {},
2110
-
"uniqueConstraints": {
2111
-
"scrobbles_uri_unique": {
2112
-
"name": "scrobbles_uri_unique",
2113
-
"nullsNotDistinct": false,
2114
-
"columns": [
2115
-
"uri"
2116
-
]
2117
-
}
2118
-
},
2119
-
"policies": {},
2120
-
"checkConstraints": {},
2121
-
"isRLSEnabled": false
2122
-
},
2123
-
"public.shout_likes": {
2124
-
"name": "shout_likes",
2125
-
"schema": "",
2126
-
"columns": {
2127
-
"xata_id": {
2128
-
"name": "xata_id",
2129
-
"type": "text",
2130
-
"primaryKey": true,
2131
-
"notNull": true,
2132
-
"default": "xata_id()"
2133
-
},
2134
-
"user_id": {
2135
-
"name": "user_id",
2136
-
"type": "text",
2137
-
"primaryKey": false,
2138
-
"notNull": true
2139
-
},
2140
-
"shout_id": {
2141
-
"name": "shout_id",
2142
-
"type": "text",
2143
-
"primaryKey": false,
2144
-
"notNull": true
2145
-
},
2146
-
"xata_createdat": {
2147
-
"name": "xata_createdat",
2148
-
"type": "timestamp",
2149
-
"primaryKey": false,
2150
-
"notNull": true,
2151
-
"default": "now()"
2152
-
},
2153
-
"uri": {
2154
-
"name": "uri",
2155
-
"type": "text",
2156
-
"primaryKey": false,
2157
-
"notNull": true
2158
-
}
2159
-
},
2160
-
"indexes": {},
2161
-
"foreignKeys": {
2162
-
"shout_likes_user_id_users_xata_id_fk": {
2163
-
"name": "shout_likes_user_id_users_xata_id_fk",
2164
-
"tableFrom": "shout_likes",
2165
-
"tableTo": "users",
2166
-
"columnsFrom": [
2167
-
"user_id"
2168
-
],
2169
-
"columnsTo": [
2170
-
"xata_id"
2171
-
],
2172
-
"onDelete": "no action",
2173
-
"onUpdate": "no action"
2174
-
},
2175
-
"shout_likes_shout_id_shouts_xata_id_fk": {
2176
-
"name": "shout_likes_shout_id_shouts_xata_id_fk",
2177
-
"tableFrom": "shout_likes",
2178
-
"tableTo": "shouts",
2179
-
"columnsFrom": [
2180
-
"shout_id"
2181
-
],
2182
-
"columnsTo": [
2183
-
"xata_id"
2184
-
],
2185
-
"onDelete": "no action",
2186
-
"onUpdate": "no action"
2187
-
}
2188
-
},
2189
-
"compositePrimaryKeys": {},
2190
-
"uniqueConstraints": {
2191
-
"shout_likes_uri_unique": {
2192
-
"name": "shout_likes_uri_unique",
2193
-
"nullsNotDistinct": false,
2194
-
"columns": [
2195
-
"uri"
2196
-
]
2197
-
}
2198
-
},
2199
-
"policies": {},
2200
-
"checkConstraints": {},
2201
-
"isRLSEnabled": false
2202
-
},
2203
-
"public.shout_reports": {
2204
-
"name": "shout_reports",
2205
-
"schema": "",
2206
-
"columns": {
2207
-
"xata_id": {
2208
-
"name": "xata_id",
2209
-
"type": "text",
2210
-
"primaryKey": true,
2211
-
"notNull": true,
2212
-
"default": "xata_id()"
2213
-
},
2214
-
"user_id": {
2215
-
"name": "user_id",
2216
-
"type": "text",
2217
-
"primaryKey": false,
2218
-
"notNull": true
2219
-
},
2220
-
"shout_id": {
2221
-
"name": "shout_id",
2222
-
"type": "text",
2223
-
"primaryKey": false,
2224
-
"notNull": true
2225
-
},
2226
-
"xata_createdat": {
2227
-
"name": "xata_createdat",
2228
-
"type": "timestamp",
2229
-
"primaryKey": false,
2230
-
"notNull": true,
2231
-
"default": "now()"
2232
-
}
2233
-
},
2234
-
"indexes": {},
2235
-
"foreignKeys": {
2236
-
"shout_reports_user_id_users_xata_id_fk": {
2237
-
"name": "shout_reports_user_id_users_xata_id_fk",
2238
-
"tableFrom": "shout_reports",
2239
-
"tableTo": "users",
2240
-
"columnsFrom": [
2241
-
"user_id"
2242
-
],
2243
-
"columnsTo": [
2244
-
"xata_id"
2245
-
],
2246
-
"onDelete": "no action",
2247
-
"onUpdate": "no action"
2248
-
},
2249
-
"shout_reports_shout_id_shouts_xata_id_fk": {
2250
-
"name": "shout_reports_shout_id_shouts_xata_id_fk",
2251
-
"tableFrom": "shout_reports",
2252
-
"tableTo": "shouts",
2253
-
"columnsFrom": [
2254
-
"shout_id"
2255
-
],
2256
-
"columnsTo": [
2257
-
"xata_id"
2258
-
],
2259
-
"onDelete": "no action",
2260
-
"onUpdate": "no action"
2261
-
}
2262
-
},
2263
-
"compositePrimaryKeys": {},
2264
-
"uniqueConstraints": {},
2265
-
"policies": {},
2266
-
"checkConstraints": {},
2267
-
"isRLSEnabled": false
2268
-
},
2269
-
"public.shouts": {
2270
-
"name": "shouts",
2271
-
"schema": "",
2272
-
"columns": {
2273
-
"xata_id": {
2274
-
"name": "xata_id",
2275
-
"type": "text",
2276
-
"primaryKey": true,
2277
-
"notNull": true,
2278
-
"default": "xata_id()"
2279
-
},
2280
-
"content": {
2281
-
"name": "content",
2282
-
"type": "text",
2283
-
"primaryKey": false,
2284
-
"notNull": true
2285
-
},
2286
-
"track_id": {
2287
-
"name": "track_id",
2288
-
"type": "text",
2289
-
"primaryKey": false,
2290
-
"notNull": false
2291
-
},
2292
-
"artist_id": {
2293
-
"name": "artist_id",
2294
-
"type": "text",
2295
-
"primaryKey": false,
2296
-
"notNull": false
2297
-
},
2298
-
"album_id": {
2299
-
"name": "album_id",
2300
-
"type": "text",
2301
-
"primaryKey": false,
2302
-
"notNull": false
2303
-
},
2304
-
"scrobble_id": {
2305
-
"name": "scrobble_id",
2306
-
"type": "text",
2307
-
"primaryKey": false,
2308
-
"notNull": false
2309
-
},
2310
-
"uri": {
2311
-
"name": "uri",
2312
-
"type": "text",
2313
-
"primaryKey": false,
2314
-
"notNull": true
2315
-
},
2316
-
"author_id": {
2317
-
"name": "author_id",
2318
-
"type": "text",
2319
-
"primaryKey": false,
2320
-
"notNull": true
2321
-
},
2322
-
"parent_id": {
2323
-
"name": "parent_id",
2324
-
"type": "text",
2325
-
"primaryKey": false,
2326
-
"notNull": false
2327
-
},
2328
-
"xata_createdat": {
2329
-
"name": "xata_createdat",
2330
-
"type": "timestamp",
2331
-
"primaryKey": false,
2332
-
"notNull": true,
2333
-
"default": "now()"
2334
-
},
2335
-
"xata_updatedat": {
2336
-
"name": "xata_updatedat",
2337
-
"type": "timestamp",
2338
-
"primaryKey": false,
2339
-
"notNull": true,
2340
-
"default": "now()"
2341
-
}
2342
-
},
2343
-
"indexes": {},
2344
-
"foreignKeys": {
2345
-
"shouts_track_id_tracks_xata_id_fk": {
2346
-
"name": "shouts_track_id_tracks_xata_id_fk",
2347
-
"tableFrom": "shouts",
2348
-
"tableTo": "tracks",
2349
-
"columnsFrom": [
2350
-
"track_id"
2351
-
],
2352
-
"columnsTo": [
2353
-
"xata_id"
2354
-
],
2355
-
"onDelete": "no action",
2356
-
"onUpdate": "no action"
2357
-
},
2358
-
"shouts_artist_id_users_xata_id_fk": {
2359
-
"name": "shouts_artist_id_users_xata_id_fk",
2360
-
"tableFrom": "shouts",
2361
-
"tableTo": "users",
2362
-
"columnsFrom": [
2363
-
"artist_id"
2364
-
],
2365
-
"columnsTo": [
2366
-
"xata_id"
2367
-
],
2368
-
"onDelete": "no action",
2369
-
"onUpdate": "no action"
2370
-
},
2371
-
"shouts_album_id_albums_xata_id_fk": {
2372
-
"name": "shouts_album_id_albums_xata_id_fk",
2373
-
"tableFrom": "shouts",
2374
-
"tableTo": "albums",
2375
-
"columnsFrom": [
2376
-
"album_id"
2377
-
],
2378
-
"columnsTo": [
2379
-
"xata_id"
2380
-
],
2381
-
"onDelete": "no action",
2382
-
"onUpdate": "no action"
2383
-
},
2384
-
"shouts_scrobble_id_scrobbles_xata_id_fk": {
2385
-
"name": "shouts_scrobble_id_scrobbles_xata_id_fk",
2386
-
"tableFrom": "shouts",
2387
-
"tableTo": "scrobbles",
2388
-
"columnsFrom": [
2389
-
"scrobble_id"
2390
-
],
2391
-
"columnsTo": [
2392
-
"xata_id"
2393
-
],
2394
-
"onDelete": "no action",
2395
-
"onUpdate": "no action"
2396
-
},
2397
-
"shouts_author_id_users_xata_id_fk": {
2398
-
"name": "shouts_author_id_users_xata_id_fk",
2399
-
"tableFrom": "shouts",
2400
-
"tableTo": "users",
2401
-
"columnsFrom": [
2402
-
"author_id"
2403
-
],
2404
-
"columnsTo": [
2405
-
"xata_id"
2406
-
],
2407
-
"onDelete": "no action",
2408
-
"onUpdate": "no action"
2409
-
},
2410
-
"shouts_parent_id_shouts_xata_id_fk": {
2411
-
"name": "shouts_parent_id_shouts_xata_id_fk",
2412
-
"tableFrom": "shouts",
2413
-
"tableTo": "shouts",
2414
-
"columnsFrom": [
2415
-
"parent_id"
2416
-
],
2417
-
"columnsTo": [
2418
-
"xata_id"
2419
-
],
2420
-
"onDelete": "no action",
2421
-
"onUpdate": "no action"
2422
-
}
2423
-
},
2424
-
"compositePrimaryKeys": {},
2425
-
"uniqueConstraints": {
2426
-
"shouts_uri_unique": {
2427
-
"name": "shouts_uri_unique",
2428
-
"nullsNotDistinct": false,
2429
-
"columns": [
2430
-
"uri"
2431
-
]
2432
-
}
2433
-
},
2434
-
"policies": {},
2435
-
"checkConstraints": {},
2436
-
"isRLSEnabled": false
2437
-
},
2438
-
"public.spotify_accounts": {
2439
-
"name": "spotify_accounts",
2440
-
"schema": "",
2441
-
"columns": {
2442
-
"xata_id": {
2443
-
"name": "xata_id",
2444
-
"type": "text",
2445
-
"primaryKey": true,
2446
-
"notNull": true,
2447
-
"default": "xata_id()"
2448
-
},
2449
-
"xata_version": {
2450
-
"name": "xata_version",
2451
-
"type": "integer",
2452
-
"primaryKey": false,
2453
-
"notNull": false
2454
-
},
2455
-
"email": {
2456
-
"name": "email",
2457
-
"type": "text",
2458
-
"primaryKey": false,
2459
-
"notNull": true
2460
-
},
2461
-
"user_id": {
2462
-
"name": "user_id",
2463
-
"type": "text",
2464
-
"primaryKey": false,
2465
-
"notNull": true
2466
-
},
2467
-
"is_beta_user": {
2468
-
"name": "is_beta_user",
2469
-
"type": "boolean",
2470
-
"primaryKey": false,
2471
-
"notNull": true,
2472
-
"default": false
2473
-
},
2474
-
"spotify_app_id": {
2475
-
"name": "spotify_app_id",
2476
-
"type": "text",
2477
-
"primaryKey": false,
2478
-
"notNull": false
2479
-
},
2480
-
"xata_createdat": {
2481
-
"name": "xata_createdat",
2482
-
"type": "timestamp",
2483
-
"primaryKey": false,
2484
-
"notNull": true,
2485
-
"default": "now()"
2486
-
},
2487
-
"xata_updatedat": {
2488
-
"name": "xata_updatedat",
2489
-
"type": "timestamp",
2490
-
"primaryKey": false,
2491
-
"notNull": true,
2492
-
"default": "now()"
2493
-
}
2494
-
},
2495
-
"indexes": {},
2496
-
"foreignKeys": {
2497
-
"spotify_accounts_user_id_users_xata_id_fk": {
2498
-
"name": "spotify_accounts_user_id_users_xata_id_fk",
2499
-
"tableFrom": "spotify_accounts",
2500
-
"tableTo": "users",
2501
-
"columnsFrom": [
2502
-
"user_id"
2503
-
],
2504
-
"columnsTo": [
2505
-
"xata_id"
2506
-
],
2507
-
"onDelete": "no action",
2508
-
"onUpdate": "no action"
2509
-
}
2510
-
},
2511
-
"compositePrimaryKeys": {},
2512
-
"uniqueConstraints": {},
2513
-
"policies": {},
2514
-
"checkConstraints": {},
2515
-
"isRLSEnabled": false
2516
-
},
2517
-
"public.spotify_apps": {
2518
-
"name": "spotify_apps",
2519
-
"schema": "",
2520
-
"columns": {
2521
-
"xata_id": {
2522
-
"name": "xata_id",
2523
-
"type": "text",
2524
-
"primaryKey": true,
2525
-
"notNull": true,
2526
-
"default": "xata_id()"
2527
-
},
2528
-
"xata_version": {
2529
-
"name": "xata_version",
2530
-
"type": "integer",
2531
-
"primaryKey": false,
2532
-
"notNull": false
2533
-
},
2534
-
"spotify_app_id": {
2535
-
"name": "spotify_app_id",
2536
-
"type": "text",
2537
-
"primaryKey": false,
2538
-
"notNull": true
2539
-
},
2540
-
"spotify_secret": {
2541
-
"name": "spotify_secret",
2542
-
"type": "text",
2543
-
"primaryKey": false,
2544
-
"notNull": true
2545
-
},
2546
-
"xata_createdat": {
2547
-
"name": "xata_createdat",
2548
-
"type": "timestamp",
2549
-
"primaryKey": false,
2550
-
"notNull": true,
2551
-
"default": "now()"
2552
-
},
2553
-
"xata_updatedat": {
2554
-
"name": "xata_updatedat",
2555
-
"type": "timestamp",
2556
-
"primaryKey": false,
2557
-
"notNull": true,
2558
-
"default": "now()"
2559
-
}
2560
-
},
2561
-
"indexes": {},
2562
-
"foreignKeys": {},
2563
-
"compositePrimaryKeys": {},
2564
-
"uniqueConstraints": {
2565
-
"spotify_apps_spotify_app_id_unique": {
2566
-
"name": "spotify_apps_spotify_app_id_unique",
2567
-
"nullsNotDistinct": false,
2568
-
"columns": [
2569
-
"spotify_app_id"
2570
-
]
2571
-
}
2572
-
},
2573
-
"policies": {},
2574
-
"checkConstraints": {},
2575
-
"isRLSEnabled": false
2576
-
},
2577
-
"public.spotify_tokens": {
2578
-
"name": "spotify_tokens",
2579
-
"schema": "",
2580
-
"columns": {
2581
-
"xata_id": {
2582
-
"name": "xata_id",
2583
-
"type": "text",
2584
-
"primaryKey": true,
2585
-
"notNull": true,
2586
-
"default": "xata_id()"
2587
-
},
2588
-
"xata_version": {
2589
-
"name": "xata_version",
2590
-
"type": "integer",
2591
-
"primaryKey": false,
2592
-
"notNull": false
2593
-
},
2594
-
"access_token": {
2595
-
"name": "access_token",
2596
-
"type": "text",
2597
-
"primaryKey": false,
2598
-
"notNull": true
2599
-
},
2600
-
"refresh_token": {
2601
-
"name": "refresh_token",
2602
-
"type": "text",
2603
-
"primaryKey": false,
2604
-
"notNull": true
2605
-
},
2606
-
"user_id": {
2607
-
"name": "user_id",
2608
-
"type": "text",
2609
-
"primaryKey": false,
2610
-
"notNull": true
2611
-
},
2612
-
"spotify_app_id": {
2613
-
"name": "spotify_app_id",
2614
-
"type": "text",
2615
-
"primaryKey": false,
2616
-
"notNull": true
2617
-
},
2618
-
"xata_createdat": {
2619
-
"name": "xata_createdat",
2620
-
"type": "timestamp",
2621
-
"primaryKey": false,
2622
-
"notNull": true,
2623
-
"default": "now()"
2624
-
},
2625
-
"xata_updatedat": {
2626
-
"name": "xata_updatedat",
2627
-
"type": "timestamp",
2628
-
"primaryKey": false,
2629
-
"notNull": true,
2630
-
"default": "now()"
2631
-
}
2632
-
},
2633
-
"indexes": {},
2634
-
"foreignKeys": {
2635
-
"spotify_tokens_user_id_users_xata_id_fk": {
2636
-
"name": "spotify_tokens_user_id_users_xata_id_fk",
2637
-
"tableFrom": "spotify_tokens",
2638
-
"tableTo": "users",
2639
-
"columnsFrom": [
2640
-
"user_id"
2641
-
],
2642
-
"columnsTo": [
2643
-
"xata_id"
2644
-
],
2645
-
"onDelete": "no action",
2646
-
"onUpdate": "no action"
2647
-
}
2648
-
},
2649
-
"compositePrimaryKeys": {},
2650
-
"uniqueConstraints": {},
2651
-
"policies": {},
2652
-
"checkConstraints": {},
2653
-
"isRLSEnabled": false
2654
-
},
2655
-
"public.tracks": {
2656
-
"name": "tracks",
2657
-
"schema": "",
2658
-
"columns": {
2659
-
"xata_id": {
2660
-
"name": "xata_id",
2661
-
"type": "text",
2662
-
"primaryKey": true,
2663
-
"notNull": true,
2664
-
"default": "xata_id()"
2665
-
},
2666
-
"title": {
2667
-
"name": "title",
2668
-
"type": "text",
2669
-
"primaryKey": false,
2670
-
"notNull": true
2671
-
},
2672
-
"artist": {
2673
-
"name": "artist",
2674
-
"type": "text",
2675
-
"primaryKey": false,
2676
-
"notNull": true
2677
-
},
2678
-
"album_artist": {
2679
-
"name": "album_artist",
2680
-
"type": "text",
2681
-
"primaryKey": false,
2682
-
"notNull": true
2683
-
},
2684
-
"album_art": {
2685
-
"name": "album_art",
2686
-
"type": "text",
2687
-
"primaryKey": false,
2688
-
"notNull": false
2689
-
},
2690
-
"album": {
2691
-
"name": "album",
2692
-
"type": "text",
2693
-
"primaryKey": false,
2694
-
"notNull": true
2695
-
},
2696
-
"track_number": {
2697
-
"name": "track_number",
2698
-
"type": "integer",
2699
-
"primaryKey": false,
2700
-
"notNull": false
2701
-
},
2702
-
"duration": {
2703
-
"name": "duration",
2704
-
"type": "integer",
2705
-
"primaryKey": false,
2706
-
"notNull": true
2707
-
},
2708
-
"mb_id": {
2709
-
"name": "mb_id",
2710
-
"type": "text",
2711
-
"primaryKey": false,
2712
-
"notNull": false
2713
-
},
2714
-
"youtube_link": {
2715
-
"name": "youtube_link",
2716
-
"type": "text",
2717
-
"primaryKey": false,
2718
-
"notNull": false
2719
-
},
2720
-
"spotify_link": {
2721
-
"name": "spotify_link",
2722
-
"type": "text",
2723
-
"primaryKey": false,
2724
-
"notNull": false
2725
-
},
2726
-
"apple_music_link": {
2727
-
"name": "apple_music_link",
2728
-
"type": "text",
2729
-
"primaryKey": false,
2730
-
"notNull": false
2731
-
},
2732
-
"tidal_link": {
2733
-
"name": "tidal_link",
2734
-
"type": "text",
2735
-
"primaryKey": false,
2736
-
"notNull": false
2737
-
},
2738
-
"sha256": {
2739
-
"name": "sha256",
2740
-
"type": "text",
2741
-
"primaryKey": false,
2742
-
"notNull": true
2743
-
},
2744
-
"disc_number": {
2745
-
"name": "disc_number",
2746
-
"type": "integer",
2747
-
"primaryKey": false,
2748
-
"notNull": false
2749
-
},
2750
-
"lyrics": {
2751
-
"name": "lyrics",
2752
-
"type": "text",
2753
-
"primaryKey": false,
2754
-
"notNull": false
2755
-
},
2756
-
"composer": {
2757
-
"name": "composer",
2758
-
"type": "text",
2759
-
"primaryKey": false,
2760
-
"notNull": false
2761
-
},
2762
-
"genre": {
2763
-
"name": "genre",
2764
-
"type": "text",
2765
-
"primaryKey": false,
2766
-
"notNull": false
2767
-
},
2768
-
"label": {
2769
-
"name": "label",
2770
-
"type": "text",
2771
-
"primaryKey": false,
2772
-
"notNull": false
2773
-
},
2774
-
"copyright_message": {
2775
-
"name": "copyright_message",
2776
-
"type": "text",
2777
-
"primaryKey": false,
2778
-
"notNull": false
2779
-
},
2780
-
"uri": {
2781
-
"name": "uri",
2782
-
"type": "text",
2783
-
"primaryKey": false,
2784
-
"notNull": false
2785
-
},
2786
-
"album_uri": {
2787
-
"name": "album_uri",
2788
-
"type": "text",
2789
-
"primaryKey": false,
2790
-
"notNull": false
2791
-
},
2792
-
"artist_uri": {
2793
-
"name": "artist_uri",
2794
-
"type": "text",
2795
-
"primaryKey": false,
2796
-
"notNull": false
2797
-
},
2798
-
"xata_createdat": {
2799
-
"name": "xata_createdat",
2800
-
"type": "timestamp",
2801
-
"primaryKey": false,
2802
-
"notNull": true,
2803
-
"default": "now()"
2804
-
},
2805
-
"xata_updatedat": {
2806
-
"name": "xata_updatedat",
2807
-
"type": "timestamp",
2808
-
"primaryKey": false,
2809
-
"notNull": true,
2810
-
"default": "now()"
2811
-
},
2812
-
"xata_version": {
2813
-
"name": "xata_version",
2814
-
"type": "integer",
2815
-
"primaryKey": false,
2816
-
"notNull": false
2817
-
}
2818
-
},
2819
-
"indexes": {},
2820
-
"foreignKeys": {},
2821
-
"compositePrimaryKeys": {},
2822
-
"uniqueConstraints": {
2823
-
"tracks_mb_id_unique": {
2824
-
"name": "tracks_mb_id_unique",
2825
-
"nullsNotDistinct": false,
2826
-
"columns": [
2827
-
"mb_id"
2828
-
]
2829
-
},
2830
-
"tracks_youtube_link_unique": {
2831
-
"name": "tracks_youtube_link_unique",
2832
-
"nullsNotDistinct": false,
2833
-
"columns": [
2834
-
"youtube_link"
2835
-
]
2836
-
},
2837
-
"tracks_spotify_link_unique": {
2838
-
"name": "tracks_spotify_link_unique",
2839
-
"nullsNotDistinct": false,
2840
-
"columns": [
2841
-
"spotify_link"
2842
-
]
2843
-
},
2844
-
"tracks_apple_music_link_unique": {
2845
-
"name": "tracks_apple_music_link_unique",
2846
-
"nullsNotDistinct": false,
2847
-
"columns": [
2848
-
"apple_music_link"
2849
-
]
2850
-
},
2851
-
"tracks_tidal_link_unique": {
2852
-
"name": "tracks_tidal_link_unique",
2853
-
"nullsNotDistinct": false,
2854
-
"columns": [
2855
-
"tidal_link"
2856
-
]
2857
-
},
2858
-
"tracks_sha256_unique": {
2859
-
"name": "tracks_sha256_unique",
2860
-
"nullsNotDistinct": false,
2861
-
"columns": [
2862
-
"sha256"
2863
-
]
2864
-
},
2865
-
"tracks_uri_unique": {
2866
-
"name": "tracks_uri_unique",
2867
-
"nullsNotDistinct": false,
2868
-
"columns": [
2869
-
"uri"
2870
-
]
2871
-
}
2872
-
},
2873
-
"policies": {},
2874
-
"checkConstraints": {},
2875
-
"isRLSEnabled": false
2876
-
},
2877
-
"public.user_albums": {
2878
-
"name": "user_albums",
2879
-
"schema": "",
2880
-
"columns": {
2881
-
"xata_id": {
2882
-
"name": "xata_id",
2883
-
"type": "text",
2884
-
"primaryKey": true,
2885
-
"notNull": true,
2886
-
"default": "xata_id()"
2887
-
},
2888
-
"user_id": {
2889
-
"name": "user_id",
2890
-
"type": "text",
2891
-
"primaryKey": false,
2892
-
"notNull": true
2893
-
},
2894
-
"album_id": {
2895
-
"name": "album_id",
2896
-
"type": "text",
2897
-
"primaryKey": false,
2898
-
"notNull": true
2899
-
},
2900
-
"xata_createdat": {
2901
-
"name": "xata_createdat",
2902
-
"type": "timestamp",
2903
-
"primaryKey": false,
2904
-
"notNull": true,
2905
-
"default": "now()"
2906
-
},
2907
-
"xata_updatedat": {
2908
-
"name": "xata_updatedat",
2909
-
"type": "timestamp",
2910
-
"primaryKey": false,
2911
-
"notNull": true,
2912
-
"default": "now()"
2913
-
},
2914
-
"xata_version": {
2915
-
"name": "xata_version",
2916
-
"type": "integer",
2917
-
"primaryKey": false,
2918
-
"notNull": false
2919
-
},
2920
-
"scrobbles": {
2921
-
"name": "scrobbles",
2922
-
"type": "integer",
2923
-
"primaryKey": false,
2924
-
"notNull": false
2925
-
},
2926
-
"uri": {
2927
-
"name": "uri",
2928
-
"type": "text",
2929
-
"primaryKey": false,
2930
-
"notNull": true
2931
-
}
2932
-
},
2933
-
"indexes": {},
2934
-
"foreignKeys": {
2935
-
"user_albums_user_id_users_xata_id_fk": {
2936
-
"name": "user_albums_user_id_users_xata_id_fk",
2937
-
"tableFrom": "user_albums",
2938
-
"tableTo": "users",
2939
-
"columnsFrom": [
2940
-
"user_id"
2941
-
],
2942
-
"columnsTo": [
2943
-
"xata_id"
2944
-
],
2945
-
"onDelete": "no action",
2946
-
"onUpdate": "no action"
2947
-
},
2948
-
"user_albums_album_id_albums_xata_id_fk": {
2949
-
"name": "user_albums_album_id_albums_xata_id_fk",
2950
-
"tableFrom": "user_albums",
2951
-
"tableTo": "albums",
2952
-
"columnsFrom": [
2953
-
"album_id"
2954
-
],
2955
-
"columnsTo": [
2956
-
"xata_id"
2957
-
],
2958
-
"onDelete": "no action",
2959
-
"onUpdate": "no action"
2960
-
}
2961
-
},
2962
-
"compositePrimaryKeys": {},
2963
-
"uniqueConstraints": {
2964
-
"user_albums_uri_unique": {
2965
-
"name": "user_albums_uri_unique",
2966
-
"nullsNotDistinct": false,
2967
-
"columns": [
2968
-
"uri"
2969
-
]
2970
-
}
2971
-
},
2972
-
"policies": {},
2973
-
"checkConstraints": {},
2974
-
"isRLSEnabled": false
2975
-
},
2976
-
"public.user_artists": {
2977
-
"name": "user_artists",
2978
-
"schema": "",
2979
-
"columns": {
2980
-
"xata_id": {
2981
-
"name": "xata_id",
2982
-
"type": "text",
2983
-
"primaryKey": true,
2984
-
"notNull": true,
2985
-
"default": "xata_id()"
2986
-
},
2987
-
"user_id": {
2988
-
"name": "user_id",
2989
-
"type": "text",
2990
-
"primaryKey": false,
2991
-
"notNull": true
2992
-
},
2993
-
"artist_id": {
2994
-
"name": "artist_id",
2995
-
"type": "text",
2996
-
"primaryKey": false,
2997
-
"notNull": true
2998
-
},
2999
-
"xata_createdat": {
3000
-
"name": "xata_createdat",
3001
-
"type": "timestamp",
3002
-
"primaryKey": false,
3003
-
"notNull": true,
3004
-
"default": "now()"
3005
-
},
3006
-
"xata_updatedat": {
3007
-
"name": "xata_updatedat",
3008
-
"type": "timestamp",
3009
-
"primaryKey": false,
3010
-
"notNull": true,
3011
-
"default": "now()"
3012
-
},
3013
-
"xata_version": {
3014
-
"name": "xata_version",
3015
-
"type": "integer",
3016
-
"primaryKey": false,
3017
-
"notNull": false
3018
-
},
3019
-
"scrobbles": {
3020
-
"name": "scrobbles",
3021
-
"type": "integer",
3022
-
"primaryKey": false,
3023
-
"notNull": false
3024
-
},
3025
-
"uri": {
3026
-
"name": "uri",
3027
-
"type": "text",
3028
-
"primaryKey": false,
3029
-
"notNull": true
3030
-
}
3031
-
},
3032
-
"indexes": {},
3033
-
"foreignKeys": {
3034
-
"user_artists_user_id_users_xata_id_fk": {
3035
-
"name": "user_artists_user_id_users_xata_id_fk",
3036
-
"tableFrom": "user_artists",
3037
-
"tableTo": "users",
3038
-
"columnsFrom": [
3039
-
"user_id"
3040
-
],
3041
-
"columnsTo": [
3042
-
"xata_id"
3043
-
],
3044
-
"onDelete": "no action",
3045
-
"onUpdate": "no action"
3046
-
},
3047
-
"user_artists_artist_id_artists_xata_id_fk": {
3048
-
"name": "user_artists_artist_id_artists_xata_id_fk",
3049
-
"tableFrom": "user_artists",
3050
-
"tableTo": "artists",
3051
-
"columnsFrom": [
3052
-
"artist_id"
3053
-
],
3054
-
"columnsTo": [
3055
-
"xata_id"
3056
-
],
3057
-
"onDelete": "no action",
3058
-
"onUpdate": "no action"
3059
-
}
3060
-
},
3061
-
"compositePrimaryKeys": {},
3062
-
"uniqueConstraints": {
3063
-
"user_artists_uri_unique": {
3064
-
"name": "user_artists_uri_unique",
3065
-
"nullsNotDistinct": false,
3066
-
"columns": [
3067
-
"uri"
3068
-
]
3069
-
}
3070
-
},
3071
-
"policies": {},
3072
-
"checkConstraints": {},
3073
-
"isRLSEnabled": false
3074
-
},
3075
-
"public.user_playlists": {
3076
-
"name": "user_playlists",
3077
-
"schema": "",
3078
-
"columns": {
3079
-
"xata_id": {
3080
-
"name": "xata_id",
3081
-
"type": "text",
3082
-
"primaryKey": true,
3083
-
"notNull": true,
3084
-
"default": "xata_id()"
3085
-
},
3086
-
"user_id": {
3087
-
"name": "user_id",
3088
-
"type": "text",
3089
-
"primaryKey": false,
3090
-
"notNull": true
3091
-
},
3092
-
"playlist_id": {
3093
-
"name": "playlist_id",
3094
-
"type": "text",
3095
-
"primaryKey": false,
3096
-
"notNull": true
3097
-
},
3098
-
"xata_createdat": {
3099
-
"name": "xata_createdat",
3100
-
"type": "timestamp",
3101
-
"primaryKey": false,
3102
-
"notNull": true,
3103
-
"default": "now()"
3104
-
},
3105
-
"uri": {
3106
-
"name": "uri",
3107
-
"type": "text",
3108
-
"primaryKey": false,
3109
-
"notNull": false
3110
-
}
3111
-
},
3112
-
"indexes": {},
3113
-
"foreignKeys": {
3114
-
"user_playlists_user_id_users_xata_id_fk": {
3115
-
"name": "user_playlists_user_id_users_xata_id_fk",
3116
-
"tableFrom": "user_playlists",
3117
-
"tableTo": "users",
3118
-
"columnsFrom": [
3119
-
"user_id"
3120
-
],
3121
-
"columnsTo": [
3122
-
"xata_id"
3123
-
],
3124
-
"onDelete": "no action",
3125
-
"onUpdate": "no action"
3126
-
},
3127
-
"user_playlists_playlist_id_playlists_xata_id_fk": {
3128
-
"name": "user_playlists_playlist_id_playlists_xata_id_fk",
3129
-
"tableFrom": "user_playlists",
3130
-
"tableTo": "playlists",
3131
-
"columnsFrom": [
3132
-
"playlist_id"
3133
-
],
3134
-
"columnsTo": [
3135
-
"xata_id"
3136
-
],
3137
-
"onDelete": "no action",
3138
-
"onUpdate": "no action"
3139
-
}
3140
-
},
3141
-
"compositePrimaryKeys": {},
3142
-
"uniqueConstraints": {
3143
-
"user_playlists_uri_unique": {
3144
-
"name": "user_playlists_uri_unique",
3145
-
"nullsNotDistinct": false,
3146
-
"columns": [
3147
-
"uri"
3148
-
]
3149
-
}
3150
-
},
3151
-
"policies": {},
3152
-
"checkConstraints": {},
3153
-
"isRLSEnabled": false
3154
-
},
3155
-
"public.user_tracks": {
3156
-
"name": "user_tracks",
3157
-
"schema": "",
3158
-
"columns": {
3159
-
"xata_id": {
3160
-
"name": "xata_id",
3161
-
"type": "text",
3162
-
"primaryKey": true,
3163
-
"notNull": true,
3164
-
"default": "xata_id()"
3165
-
},
3166
-
"user_id": {
3167
-
"name": "user_id",
3168
-
"type": "text",
3169
-
"primaryKey": false,
3170
-
"notNull": true
3171
-
},
3172
-
"track_id": {
3173
-
"name": "track_id",
3174
-
"type": "text",
3175
-
"primaryKey": false,
3176
-
"notNull": true
3177
-
},
3178
-
"xata_createdat": {
3179
-
"name": "xata_createdat",
3180
-
"type": "timestamp",
3181
-
"primaryKey": false,
3182
-
"notNull": true,
3183
-
"default": "now()"
3184
-
},
3185
-
"xata_updatedat": {
3186
-
"name": "xata_updatedat",
3187
-
"type": "timestamp",
3188
-
"primaryKey": false,
3189
-
"notNull": true,
3190
-
"default": "now()"
3191
-
},
3192
-
"xata_version": {
3193
-
"name": "xata_version",
3194
-
"type": "integer",
3195
-
"primaryKey": false,
3196
-
"notNull": false
3197
-
},
3198
-
"uri": {
3199
-
"name": "uri",
3200
-
"type": "text",
3201
-
"primaryKey": false,
3202
-
"notNull": true
3203
-
},
3204
-
"scrobbles": {
3205
-
"name": "scrobbles",
3206
-
"type": "integer",
3207
-
"primaryKey": false,
3208
-
"notNull": false
3209
-
}
3210
-
},
3211
-
"indexes": {},
3212
-
"foreignKeys": {
3213
-
"user_tracks_user_id_users_xata_id_fk": {
3214
-
"name": "user_tracks_user_id_users_xata_id_fk",
3215
-
"tableFrom": "user_tracks",
3216
-
"tableTo": "users",
3217
-
"columnsFrom": [
3218
-
"user_id"
3219
-
],
3220
-
"columnsTo": [
3221
-
"xata_id"
3222
-
],
3223
-
"onDelete": "no action",
3224
-
"onUpdate": "no action"
3225
-
},
3226
-
"user_tracks_track_id_tracks_xata_id_fk": {
3227
-
"name": "user_tracks_track_id_tracks_xata_id_fk",
3228
-
"tableFrom": "user_tracks",
3229
-
"tableTo": "tracks",
3230
-
"columnsFrom": [
3231
-
"track_id"
3232
-
],
3233
-
"columnsTo": [
3234
-
"xata_id"
3235
-
],
3236
-
"onDelete": "no action",
3237
-
"onUpdate": "no action"
3238
-
}
3239
-
},
3240
-
"compositePrimaryKeys": {},
3241
-
"uniqueConstraints": {
3242
-
"user_tracks_uri_unique": {
3243
-
"name": "user_tracks_uri_unique",
3244
-
"nullsNotDistinct": false,
3245
-
"columns": [
3246
-
"uri"
3247
-
]
3248
-
}
3249
-
},
3250
-
"policies": {},
3251
-
"checkConstraints": {},
3252
-
"isRLSEnabled": false
3253
-
},
3254
-
"public.users": {
3255
-
"name": "users",
3256
-
"schema": "",
3257
-
"columns": {
3258
-
"xata_id": {
3259
-
"name": "xata_id",
3260
-
"type": "text",
3261
-
"primaryKey": true,
3262
-
"notNull": true,
3263
-
"default": "xata_id()"
3264
-
},
3265
-
"did": {
3266
-
"name": "did",
3267
-
"type": "text",
3268
-
"primaryKey": false,
3269
-
"notNull": true
3270
-
},
3271
-
"display_name": {
3272
-
"name": "display_name",
3273
-
"type": "text",
3274
-
"primaryKey": false,
3275
-
"notNull": false
3276
-
},
3277
-
"handle": {
3278
-
"name": "handle",
3279
-
"type": "text",
3280
-
"primaryKey": false,
3281
-
"notNull": true
3282
-
},
3283
-
"avatar": {
3284
-
"name": "avatar",
3285
-
"type": "text",
3286
-
"primaryKey": false,
3287
-
"notNull": true
3288
-
},
3289
-
"xata_createdat": {
3290
-
"name": "xata_createdat",
3291
-
"type": "timestamp",
3292
-
"primaryKey": false,
3293
-
"notNull": true,
3294
-
"default": "now()"
3295
-
},
3296
-
"xata_updatedat": {
3297
-
"name": "xata_updatedat",
3298
-
"type": "timestamp",
3299
-
"primaryKey": false,
3300
-
"notNull": true,
3301
-
"default": "now()"
3302
-
},
3303
-
"xata_version": {
3304
-
"name": "xata_version",
3305
-
"type": "integer",
3306
-
"primaryKey": false,
3307
-
"notNull": false
3308
-
}
3309
-
},
3310
-
"indexes": {},
3311
-
"foreignKeys": {},
3312
-
"compositePrimaryKeys": {},
3313
-
"uniqueConstraints": {
3314
-
"users_did_unique": {
3315
-
"name": "users_did_unique",
3316
-
"nullsNotDistinct": false,
3317
-
"columns": [
3318
-
"did"
3319
-
]
3320
-
},
3321
-
"users_handle_unique": {
3322
-
"name": "users_handle_unique",
3323
-
"nullsNotDistinct": false,
3324
-
"columns": [
3325
-
"handle"
3326
-
]
3327
-
}
3328
-
},
3329
-
"policies": {},
3330
-
"checkConstraints": {},
3331
-
"isRLSEnabled": false
3332
-
},
3333
-
"public.webscrobblers": {
3334
-
"name": "webscrobblers",
3335
-
"schema": "",
3336
-
"columns": {
3337
-
"xata_id": {
3338
-
"name": "xata_id",
3339
-
"type": "text",
3340
-
"primaryKey": true,
3341
-
"notNull": true,
3342
-
"default": "xata_id()"
3343
-
},
3344
-
"name": {
3345
-
"name": "name",
3346
-
"type": "text",
3347
-
"primaryKey": false,
3348
-
"notNull": true
3349
-
},
3350
-
"uuid": {
3351
-
"name": "uuid",
3352
-
"type": "text",
3353
-
"primaryKey": false,
3354
-
"notNull": true
3355
-
},
3356
-
"description": {
3357
-
"name": "description",
3358
-
"type": "text",
3359
-
"primaryKey": false,
3360
-
"notNull": false
3361
-
},
3362
-
"enabled": {
3363
-
"name": "enabled",
3364
-
"type": "boolean",
3365
-
"primaryKey": false,
3366
-
"notNull": true,
3367
-
"default": true
3368
-
},
3369
-
"user_id": {
3370
-
"name": "user_id",
3371
-
"type": "text",
3372
-
"primaryKey": false,
3373
-
"notNull": true
3374
-
},
3375
-
"xata_createdat": {
3376
-
"name": "xata_createdat",
3377
-
"type": "timestamp",
3378
-
"primaryKey": false,
3379
-
"notNull": true,
3380
-
"default": "now()"
3381
-
},
3382
-
"xata_updatedat": {
3383
-
"name": "xata_updatedat",
3384
-
"type": "timestamp",
3385
-
"primaryKey": false,
3386
-
"notNull": true,
3387
-
"default": "now()"
3388
-
}
3389
-
},
3390
-
"indexes": {},
3391
-
"foreignKeys": {
3392
-
"webscrobblers_user_id_users_xata_id_fk": {
3393
-
"name": "webscrobblers_user_id_users_xata_id_fk",
3394
-
"tableFrom": "webscrobblers",
3395
-
"tableTo": "users",
3396
-
"columnsFrom": [
3397
-
"user_id"
3398
-
],
3399
-
"columnsTo": [
3400
-
"xata_id"
3401
-
],
3402
-
"onDelete": "no action",
3403
-
"onUpdate": "no action"
3404
-
}
3405
-
},
3406
-
"compositePrimaryKeys": {},
3407
-
"uniqueConstraints": {},
3408
-
"policies": {},
3409
-
"checkConstraints": {},
3410
-
"isRLSEnabled": false
3411
-
}
3412
-
},
3413
-
"enums": {},
3414
-
"schemas": {},
3415
-
"sequences": {},
3416
-
"roles": {},
3417
-
"policies": {},
3418
-
"views": {},
3419
-
"_meta": {
3420
-
"columns": {},
3421
-
"schemas": {},
3422
-
"tables": {}
3423
-
}
3424
-
}
-7
apps/api/drizzle/meta/_journal.json
-7
apps/api/drizzle/meta/_journal.json
-106
apps/api/lexicons/actor/defs.json
-106
apps/api/lexicons/actor/defs.json
···
73
73
"format": "datetime"
74
74
}
75
75
}
76
-
},
77
-
"neighbourViewBasic": {
78
-
"type": "object",
79
-
"properties": {
80
-
"userId": {
81
-
"type": "string"
82
-
},
83
-
"did": {
84
-
"type": "string"
85
-
},
86
-
"handle": {
87
-
"type": "string"
88
-
},
89
-
"displayName": {
90
-
"type": "string"
91
-
},
92
-
"avatar": {
93
-
"type": "string",
94
-
"description": "The URL of the actor's avatar image.",
95
-
"format": "uri"
96
-
},
97
-
"sharedArtistsCount": {
98
-
"type": "integer",
99
-
"description": "The number of artists shared with the actor."
100
-
},
101
-
"similarityScore": {
102
-
"type": "integer",
103
-
"description": "The similarity score with the actor."
104
-
},
105
-
"topSharedArtistNames": {
106
-
"type": "array",
107
-
"description": "The top shared artist names with the actor.",
108
-
"items": {
109
-
"type": "string"
110
-
}
111
-
},
112
-
"topSharedArtistsDetails": {
113
-
"type": "array",
114
-
"description": "The top shared artist details with the actor.",
115
-
"items": {
116
-
"type": "ref",
117
-
"ref": "app.rocksky.artist.defs#artistViewBasic"
118
-
}
119
-
}
120
-
}
121
-
},
122
-
"compatibilityViewBasic": {
123
-
"type": "object",
124
-
"properties": {
125
-
"compatibilityLevel": {
126
-
"type": "integer"
127
-
},
128
-
"compatibilityPercentage": {
129
-
"type": "integer"
130
-
},
131
-
"sharedArtists": {
132
-
"type": "integer"
133
-
},
134
-
"topSharedArtistNames": {
135
-
"type": "array",
136
-
"items": {
137
-
"type": "string"
138
-
}
139
-
},
140
-
"topSharedDetailedArtists": {
141
-
"type": "array",
142
-
"items": {
143
-
"type": "ref",
144
-
"ref": "app.rocksky.actor.defs#artistViewBasic"
145
-
}
146
-
},
147
-
"user1ArtistCount": {
148
-
"type": "integer"
149
-
},
150
-
"user2ArtistCount": {
151
-
"type": "integer"
152
-
}
153
-
}
154
-
},
155
-
"artistViewBasic": {
156
-
"type": "object",
157
-
"properties": {
158
-
"id": {
159
-
"type": "string"
160
-
},
161
-
"name": {
162
-
"type": "string"
163
-
},
164
-
"picture": {
165
-
"type": "string",
166
-
"format": "uri"
167
-
},
168
-
"uri": {
169
-
"type": "string",
170
-
"format": "at-uri"
171
-
},
172
-
"user1Rank": {
173
-
"type": "integer"
174
-
},
175
-
"user2Rank": {
176
-
"type": "integer"
177
-
},
178
-
"weight": {
179
-
"type": "integer"
180
-
}
181
-
}
182
76
}
183
77
}
184
78
}
-35
apps/api/lexicons/actor/getActorCompatibility.json
-35
apps/api/lexicons/actor/getActorCompatibility.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.actor.getActorCompatibility",
4
-
"defs": {
5
-
"main": {
6
-
"type": "query",
7
-
"description": "Get compatibility for an actor",
8
-
"parameters": {
9
-
"type": "params",
10
-
"required": [
11
-
"did"
12
-
],
13
-
"properties": {
14
-
"did": {
15
-
"type": "string",
16
-
"description": "DID or handle to get compatibility for",
17
-
"format": "at-identifier"
18
-
}
19
-
}
20
-
},
21
-
"output": {
22
-
"encoding": "application/json",
23
-
"schema": {
24
-
"type": "object",
25
-
"properties": {
26
-
"compatibility": {
27
-
"type": "ref",
28
-
"ref": "app.rocksky.actor.defs#compatibilityViewBasic"
29
-
}
30
-
}
31
-
}
32
-
}
33
-
}
34
-
}
35
-
}
-38
apps/api/lexicons/actor/getActorNeighbours.json
-38
apps/api/lexicons/actor/getActorNeighbours.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.actor.getActorNeighbours",
4
-
"defs": {
5
-
"main": {
6
-
"type": "query",
7
-
"description": "Get neighbours for an actor",
8
-
"parameters": {
9
-
"type": "params",
10
-
"required": [
11
-
"did"
12
-
],
13
-
"properties": {
14
-
"did": {
15
-
"type": "string",
16
-
"description": "The DID or handle of the actor",
17
-
"format": "at-identifier"
18
-
}
19
-
}
20
-
},
21
-
"output": {
22
-
"encoding": "application/json",
23
-
"schema": {
24
-
"type": "object",
25
-
"properties": {
26
-
"neighbours": {
27
-
"type": "array",
28
-
"items": {
29
-
"type": "ref",
30
-
"ref": "app.rocksky.actor.defs#neighbourViewBasic"
31
-
}
32
-
}
33
-
}
34
-
}
35
-
}
36
-
}
37
-
}
38
-
}
-4
apps/api/lexicons/feed/defs.json
-4
apps/api/lexicons/feed/defs.json
-45
apps/api/lexicons/graph/defs.json
-45
apps/api/lexicons/graph/defs.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.graph.defs",
4
-
"defs": {
5
-
"notFoundActor": {
6
-
"type": "object",
7
-
"description": "indicates that a handle or DID could not be resolved",
8
-
"required": [
9
-
"actor",
10
-
"notFound"
11
-
],
12
-
"properties": {
13
-
"actor": {
14
-
"type": "string",
15
-
"format": "at-identifier"
16
-
},
17
-
"notFound": {
18
-
"type": "boolean"
19
-
}
20
-
}
21
-
},
22
-
"relationship": {
23
-
"type": "object",
24
-
"required": [
25
-
"did"
26
-
],
27
-
"properties": {
28
-
"did": {
29
-
"type": "string",
30
-
"format": "did"
31
-
},
32
-
"following": {
33
-
"type": "string",
34
-
"description": "if the actor follows this DID, this is the AT-URI of the follow record",
35
-
"format": "at-uri"
36
-
},
37
-
"followedBy": {
38
-
"type": "string",
39
-
"description": "if the actor is followed by this DID, contains the AT-URI of the follow record",
40
-
"format": "at-uri"
41
-
}
42
-
}
43
-
}
44
-
}
45
-
}
-32
apps/api/lexicons/graph/follow.json
-32
apps/api/lexicons/graph/follow.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.graph.follow",
4
-
"defs": {
5
-
"main": {
6
-
"type": "record",
7
-
"description": "Record declaring a social 'follow' relationship of another account.",
8
-
"key": "tid",
9
-
"record": {
10
-
"type": "object",
11
-
"required": [
12
-
"createdAt",
13
-
"subject"
14
-
],
15
-
"properties": {
16
-
"createdAt": {
17
-
"type": "string",
18
-
"format": "datetime"
19
-
},
20
-
"subject": {
21
-
"type": "string",
22
-
"format": "did"
23
-
},
24
-
"via": {
25
-
"type": "ref",
26
-
"ref": "com.atproto.repo.strongRef"
27
-
}
28
-
}
29
-
}
30
-
}
31
-
}
32
-
}
-49
apps/api/lexicons/graph/followAccount.json
-49
apps/api/lexicons/graph/followAccount.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.graph.followAccount",
4
-
"defs": {
5
-
"main": {
6
-
"type": "procedure",
7
-
"description": "Creates a 'follow' relationship from the authenticated account to a specified account.",
8
-
"parameters": {
9
-
"type": "params",
10
-
"required": [
11
-
"account"
12
-
],
13
-
"properties": {
14
-
"account": {
15
-
"type": "string",
16
-
"format": "at-identifier"
17
-
}
18
-
}
19
-
},
20
-
"output": {
21
-
"encoding": "application/json",
22
-
"schema": {
23
-
"type": "object",
24
-
"required": [
25
-
"subject",
26
-
"followers"
27
-
],
28
-
"properties": {
29
-
"subject": {
30
-
"type": "ref",
31
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
32
-
},
33
-
"followers": {
34
-
"type": "array",
35
-
"items": {
36
-
"type": "ref",
37
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
38
-
}
39
-
},
40
-
"cursor": {
41
-
"type": "string",
42
-
"description": "A cursor value to pass to subsequent calls to get the next page of results."
43
-
}
44
-
}
45
-
}
46
-
}
47
-
}
48
-
}
49
-
}
-70
apps/api/lexicons/graph/getFollowers.json
-70
apps/api/lexicons/graph/getFollowers.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.graph.getFollowers",
4
-
"defs": {
5
-
"main": {
6
-
"type": "query",
7
-
"description": "Enumerates accounts which follow a specified account (actor).",
8
-
"parameters": {
9
-
"type": "params",
10
-
"required": [
11
-
"actor"
12
-
],
13
-
"properties": {
14
-
"actor": {
15
-
"type": "string",
16
-
"format": "at-identifier"
17
-
},
18
-
"limit": {
19
-
"type": "integer",
20
-
"maximum": 100,
21
-
"minimum": 1,
22
-
"default": 50
23
-
},
24
-
"dids": {
25
-
"type": "array",
26
-
"description": "If provided, filters the followers to only include those with DIDs in this list.",
27
-
"items": {
28
-
"type": "string",
29
-
"format": "did"
30
-
}
31
-
},
32
-
"cursor": {
33
-
"type": "string"
34
-
}
35
-
}
36
-
},
37
-
"output": {
38
-
"encoding": "application/json",
39
-
"schema": {
40
-
"type": "object",
41
-
"required": [
42
-
"subject",
43
-
"followers"
44
-
],
45
-
"properties": {
46
-
"subject": {
47
-
"type": "ref",
48
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
49
-
},
50
-
"followers": {
51
-
"type": "array",
52
-
"items": {
53
-
"type": "ref",
54
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
55
-
}
56
-
},
57
-
"cursor": {
58
-
"type": "string",
59
-
"description": "A cursor value to pass to subsequent calls to get the next page of results."
60
-
},
61
-
"count": {
62
-
"type": "integer",
63
-
"description": "The total number of followers."
64
-
}
65
-
}
66
-
}
67
-
}
68
-
}
69
-
}
70
-
}
-70
apps/api/lexicons/graph/getFollows.json
-70
apps/api/lexicons/graph/getFollows.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.graph.getFollows",
4
-
"defs": {
5
-
"main": {
6
-
"type": "query",
7
-
"description": "Enumerates accounts which a specified account (actor) follows.",
8
-
"parameters": {
9
-
"type": "params",
10
-
"required": [
11
-
"actor"
12
-
],
13
-
"properties": {
14
-
"actor": {
15
-
"type": "string",
16
-
"format": "at-identifier"
17
-
},
18
-
"limit": {
19
-
"type": "integer",
20
-
"maximum": 100,
21
-
"minimum": 1,
22
-
"default": 50
23
-
},
24
-
"dids": {
25
-
"type": "array",
26
-
"description": "If provided, filters the follows to only include those with DIDs in this list.",
27
-
"items": {
28
-
"type": "string",
29
-
"format": "did"
30
-
}
31
-
},
32
-
"cursor": {
33
-
"type": "string"
34
-
}
35
-
}
36
-
},
37
-
"output": {
38
-
"encoding": "application/json",
39
-
"schema": {
40
-
"type": "object",
41
-
"required": [
42
-
"subject",
43
-
"follows"
44
-
],
45
-
"properties": {
46
-
"subject": {
47
-
"type": "ref",
48
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
49
-
},
50
-
"follows": {
51
-
"type": "array",
52
-
"items": {
53
-
"type": "ref",
54
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
55
-
}
56
-
},
57
-
"cursor": {
58
-
"type": "string",
59
-
"description": "A cursor value to pass to subsequent calls to get the next page of results."
60
-
},
61
-
"count": {
62
-
"type": "integer",
63
-
"description": "The total number of follows."
64
-
}
65
-
}
66
-
}
67
-
}
68
-
}
69
-
}
70
-
}
-58
apps/api/lexicons/graph/getKnownFollowers.json
-58
apps/api/lexicons/graph/getKnownFollowers.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.graph.getKnownFollowers",
4
-
"defs": {
5
-
"main": {
6
-
"type": "query",
7
-
"description": "Enumerates accounts which follow a specified account (actor) and are followed by the viewer.",
8
-
"parameters": {
9
-
"type": "params",
10
-
"required": [
11
-
"actor"
12
-
],
13
-
"properties": {
14
-
"actor": {
15
-
"type": "string",
16
-
"format": "at-identifier"
17
-
},
18
-
"limit": {
19
-
"type": "integer",
20
-
"maximum": 100,
21
-
"minimum": 1,
22
-
"default": 50
23
-
},
24
-
"cursor": {
25
-
"type": "string"
26
-
}
27
-
}
28
-
},
29
-
"output": {
30
-
"encoding": "application/json",
31
-
"schema": {
32
-
"type": "object",
33
-
"required": [
34
-
"subject",
35
-
"followers"
36
-
],
37
-
"properties": {
38
-
"subject": {
39
-
"type": "ref",
40
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
41
-
},
42
-
"followers": {
43
-
"type": "array",
44
-
"items": {
45
-
"type": "ref",
46
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
47
-
}
48
-
},
49
-
"cursor": {
50
-
"type": "string",
51
-
"description": "A cursor value to pass to subsequent calls to get the next page of results."
52
-
}
53
-
}
54
-
}
55
-
}
56
-
}
57
-
}
58
-
}
-49
apps/api/lexicons/graph/unfollowAccount.json
-49
apps/api/lexicons/graph/unfollowAccount.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.rocksky.graph.unfollowAccount",
4
-
"defs": {
5
-
"main": {
6
-
"type": "procedure",
7
-
"description": "Removes a 'follow' relationship from the authenticated account to a specified account.",
8
-
"parameters": {
9
-
"type": "params",
10
-
"required": [
11
-
"account"
12
-
],
13
-
"properties": {
14
-
"account": {
15
-
"type": "string",
16
-
"format": "at-identifier"
17
-
}
18
-
}
19
-
},
20
-
"output": {
21
-
"encoding": "application/json",
22
-
"schema": {
23
-
"type": "object",
24
-
"required": [
25
-
"subject",
26
-
"followers"
27
-
],
28
-
"properties": {
29
-
"subject": {
30
-
"type": "ref",
31
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
32
-
},
33
-
"followers": {
34
-
"type": "array",
35
-
"items": {
36
-
"type": "ref",
37
-
"ref": "app.rocksky.actor.defs#profileViewBasic"
38
-
}
39
-
},
40
-
"cursor": {
41
-
"type": "string",
42
-
"description": "A cursor value to pass to subsequent calls to get the next page of results."
43
-
}
44
-
}
45
-
}
46
-
}
47
-
}
48
-
}
49
-
}
-4
apps/api/lexicons/scrobble/getScrobbles.json
-4
apps/api/lexicons/scrobble/getScrobbles.json
···
13
13
"description": "The DID or handle of the actor",
14
14
"format": "at-identifier"
15
15
},
16
-
"following": {
17
-
"type": "boolean",
18
-
"description": "If true, only return scrobbles from actors the viewer is following."
19
-
},
20
16
"limit": {
21
17
"type": "integer",
22
18
"description": "The maximum number of scrobbles to return",
+1
-2
apps/api/package.json
+1
-2
apps/api/package.json
···
27
27
"lint": "biome lint src",
28
28
"feed": "tsx ./src/scripts/feed.ts",
29
29
"dedup": "bun ./src/scripts/dedup.ts",
30
-
"seed:feed": "tsx ./src/scripts/seed-feed.ts",
31
-
"likes": "tsx ./src/scripts/likes.ts"
30
+
"seed:feed": "tsx ./src/scripts/seed-feed.ts"
32
31
},
33
32
"dependencies": {
34
33
"@atproto/api": "^0.13.31",
+3
-120
apps/api/pkl/defs/actor/defs.pkl
+3
-120
apps/api/pkl/defs/actor/defs.pkl
···
1
-
amends "../../schema/lexicon.pkl"
1
+
amends "../../schema/lexicon.pkl"
2
2
3
3
lexicon = 1
4
4
id = "app.rocksky.actor.defs"
···
43
43
format = "datetime"
44
44
description = "The date and time when the actor was last updated."
45
45
}
46
+
46
47
}
47
48
}
48
49
["profileViewBasic"] {
···
87
88
}
88
89
}
89
90
}
90
-
["neighbourViewBasic"] {
91
-
type = "object"
92
-
properties {
93
-
["userId"] = new StringType {
94
-
type = "string"
95
-
}
96
-
97
-
["did"] = new StringType {
98
-
type = "string"
99
-
}
100
-
101
-
["handle"] = new StringType {
102
-
type = "string"
103
-
}
104
-
105
-
["displayName"] = new StringType {
106
-
type = "string"
107
-
}
108
-
109
-
["avatar"] = new StringType {
110
-
type = "string"
111
-
format = "uri"
112
-
description = "The URL of the actor's avatar image."
113
-
}
114
-
115
-
["sharedArtistsCount"] = new IntegerType {
116
-
type = "integer"
117
-
description = "The number of artists shared with the actor."
118
-
}
119
-
120
-
["similarityScore"] = new IntegerType {
121
-
type = "integer"
122
-
description = "The similarity score with the actor."
123
-
}
124
-
125
-
["topSharedArtistNames"] = new Array {
126
-
type = "array"
127
-
items = new StringType {
128
-
type = "string"
129
-
}
130
-
description = "The top shared artist names with the actor."
131
-
}
132
-
133
-
["topSharedArtistsDetails"] = new Array {
134
-
type = "array"
135
-
items = new Ref {
136
-
ref = "app.rocksky.artist.defs#artistViewBasic"
137
-
}
138
-
description = "The top shared artist details with the actor."
139
-
}
140
-
}
141
-
}
142
-
["compatibilityViewBasic"] {
143
-
type = "object"
144
-
properties {
145
-
["compatibilityLevel"] = new IntegerType {
146
-
type = "integer"
147
-
}
148
-
["compatibilityPercentage"] = new IntegerType {
149
-
type = "integer"
150
-
}
151
-
["sharedArtists"] = new IntegerType {
152
-
type = "integer"
153
-
}
154
-
["topSharedArtistNames"] = new Array {
155
-
type = "array"
156
-
items = new StringType {
157
-
type = "string"
158
-
}
159
-
}
160
-
["topSharedDetailedArtists"] = new Array {
161
-
type = "array"
162
-
items = new Ref {
163
-
ref = "app.rocksky.actor.defs#artistViewBasic"
164
-
}
165
-
}
166
-
["user1ArtistCount"] = new IntegerType {
167
-
type = "integer"
168
-
}
169
-
["user2ArtistCount"] = new IntegerType {
170
-
type = "integer"
171
-
}
172
-
}
173
-
}
174
-
["artistViewBasic"] {
175
-
type = "object"
176
-
properties {
177
-
["id"] = new StringType {
178
-
type = "string"
179
-
}
180
-
181
-
["name"] = new StringType {
182
-
type = "string"
183
-
}
184
-
185
-
["picture"] = new StringType {
186
-
type = "string"
187
-
format = "uri"
188
-
}
189
-
190
-
["uri"] = new StringType {
191
-
type = "string"
192
-
format = "at-uri"
193
-
}
194
-
195
-
["user1Rank"] = new IntegerType {
196
-
type = "integer"
197
-
}
198
-
199
-
["user2Rank"] = new IntegerType {
200
-
type = "integer"
201
-
}
202
-
203
-
["weight"] = new IntegerType {
204
-
type = "integer"
205
-
}
206
-
}
207
-
}
208
-
}
91
+
}
-31
apps/api/pkl/defs/actor/getActorCompatibility.pkl
-31
apps/api/pkl/defs/actor/getActorCompatibility.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.actor.getActorCompatibility"
5
-
defs = new Mapping<String, Query> {
6
-
["main"] {
7
-
type = "query"
8
-
description = "Get compatibility for an actor"
9
-
parameters = new Params {
10
-
required = List("did")
11
-
properties {
12
-
["did"] = new StringType {
13
-
description = "DID or handle to get compatibility for"
14
-
format = "at-identifier"
15
-
}
16
-
}
17
-
}
18
-
output {
19
-
encoding = "application/json"
20
-
schema = new ObjectType {
21
-
type = "object"
22
-
properties = new Mapping<String, Ref> {
23
-
["compatibility"] = new Ref {
24
-
type = "ref"
25
-
ref = "app.rocksky.actor.defs#compatibilityViewBasic"
26
-
}
27
-
}
28
-
}
29
-
}
30
-
}
31
-
}
-33
apps/api/pkl/defs/actor/getActorNeighbours.pkl
-33
apps/api/pkl/defs/actor/getActorNeighbours.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.actor.getActorNeighbours"
5
-
defs = new Mapping<String, Query> {
6
-
["main"] {
7
-
type = "query"
8
-
description = "Get neighbours for an actor"
9
-
parameters = new Params {
10
-
required = List("did")
11
-
properties {
12
-
["did"] = new StringType {
13
-
description = "The DID or handle of the actor"
14
-
format = "at-identifier"
15
-
}
16
-
}
17
-
}
18
-
output {
19
-
encoding = "application/json"
20
-
schema = new ObjectType {
21
-
type = "object"
22
-
properties = new Mapping<String, Array> {
23
-
["neighbours"] = new Array {
24
-
type = "array"
25
-
items = new Ref {
26
-
ref = "app.rocksky.actor.defs#neighbourViewBasic"
27
-
}
28
-
}
29
-
}
30
-
}
31
-
}
32
-
}
33
-
}
-4
apps/api/pkl/defs/feed/defs.pkl
-4
apps/api/pkl/defs/feed/defs.pkl
-41
apps/api/pkl/defs/graph/defs.pkl
-41
apps/api/pkl/defs/graph/defs.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.graph.defs"
5
-
defs = new Mapping<String, ObjectType> {
6
-
["notFoundActor"] = new ObjectType {
7
-
type = "object"
8
-
description = "indicates that a handle or DID could not be resolved"
9
-
required = List("actor", "notFound")
10
-
properties {
11
-
["actor"] = new StringType {
12
-
type = "string"
13
-
format = "at-identifier"
14
-
}
15
-
["notFound"] = new BooleanType {
16
-
type = "boolean"
17
-
}
18
-
}
19
-
}
20
-
["relationship"] {
21
-
type = "object"
22
-
required = List("did")
23
-
properties {
24
-
["did"] = new StringType {
25
-
type = "string"
26
-
format = "did"
27
-
}
28
-
["following"] = new StringType {
29
-
type = "string"
30
-
format = "at-uri"
31
-
description = "if the actor follows this DID, this is the AT-URI of the follow record"
32
-
}
33
-
["followedBy"] = new StringType {
34
-
type = "string"
35
-
format = "at-uri"
36
-
description =
37
-
"if the actor is followed by this DID, contains the AT-URI of the follow record"
38
-
}
39
-
}
40
-
}
41
-
}
-31
apps/api/pkl/defs/graph/follow.pkl
-31
apps/api/pkl/defs/graph/follow.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.graph.follow"
5
-
defs = new Mapping<String, Record> {
6
-
["main"] {
7
-
type = "record"
8
-
key = "tid"
9
-
description = "Record declaring a social 'follow' relationship of another account."
10
-
`record` {
11
-
type = "object"
12
-
required = List("createdAt", "subject")
13
-
properties {
14
-
["createdAt"] = new StringType {
15
-
type = "string"
16
-
format = "datetime"
17
-
}
18
-
19
-
["subject"] = new StringType {
20
-
type = "string"
21
-
format = "did"
22
-
}
23
-
24
-
["via"] = new Ref {
25
-
type = "ref"
26
-
ref = "com.atproto.repo.strongRef"
27
-
}
28
-
}
29
-
}
30
-
}
31
-
}
-46
apps/api/pkl/defs/graph/followAccount.pkl
-46
apps/api/pkl/defs/graph/followAccount.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.graph.followAccount"
5
-
defs = new Mapping<String, Procedure> {
6
-
["main"] {
7
-
type = "procedure"
8
-
description =
9
-
"Creates a 'follow' relationship from the authenticated account to a specified account."
10
-
parameters = new Params {
11
-
required = List("account")
12
-
properties {
13
-
["account"] = new StringType {
14
-
type = "string"
15
-
format = "at-identifier"
16
-
}
17
-
}
18
-
}
19
-
20
-
output {
21
-
encoding = "application/json"
22
-
schema = new ObjectType {
23
-
type = "object"
24
-
required = List("subject", "followers")
25
-
properties {
26
-
["subject"] = new Ref {
27
-
type = "ref"
28
-
ref = "app.rocksky.actor.defs#profileViewBasic"
29
-
}
30
-
["followers"] = new Array {
31
-
type = "array"
32
-
items = new Ref {
33
-
type = "ref"
34
-
ref = "app.rocksky.actor.defs#profileViewBasic"
35
-
}
36
-
}
37
-
["cursor"] = new StringType {
38
-
type = "string"
39
-
description =
40
-
"A cursor value to pass to subsequent calls to get the next page of results."
41
-
}
42
-
}
43
-
}
44
-
}
45
-
}
46
-
}
-67
apps/api/pkl/defs/graph/getFollowers.pkl
-67
apps/api/pkl/defs/graph/getFollowers.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.graph.getFollowers"
5
-
defs = new Mapping<String, Query> {
6
-
["main"] {
7
-
type = "query"
8
-
description = "Enumerates accounts which follow a specified account (actor)."
9
-
parameters = new Params {
10
-
required = List("actor")
11
-
properties {
12
-
["actor"] = new StringType {
13
-
type = "string"
14
-
format = "at-identifier"
15
-
}
16
-
["limit"] = new IntegerType {
17
-
type = "integer"
18
-
maximum = 100
19
-
minimum = 1
20
-
default = 50
21
-
}
22
-
["dids"] = new Array {
23
-
type = "array"
24
-
items = new StringType {
25
-
type = "string"
26
-
format = "did"
27
-
}
28
-
description =
29
-
"If provided, filters the followers to only include those with DIDs in this list."
30
-
}
31
-
["cursor"] = new StringType {
32
-
type = "string"
33
-
}
34
-
}
35
-
}
36
-
37
-
output {
38
-
encoding = "application/json"
39
-
schema = new ObjectType {
40
-
type = "object"
41
-
required = List("subject", "followers")
42
-
properties {
43
-
["subject"] = new Ref {
44
-
type = "ref"
45
-
ref = "app.rocksky.actor.defs#profileViewBasic"
46
-
}
47
-
["followers"] = new Array {
48
-
type = "array"
49
-
items = new Ref {
50
-
type = "ref"
51
-
ref = "app.rocksky.actor.defs#profileViewBasic"
52
-
}
53
-
}
54
-
["cursor"] = new StringType {
55
-
type = "string"
56
-
description =
57
-
"A cursor value to pass to subsequent calls to get the next page of results."
58
-
}
59
-
["count"] = new IntegerType {
60
-
type = "integer"
61
-
description = "The total number of followers."
62
-
}
63
-
}
64
-
}
65
-
}
66
-
}
67
-
}
-67
apps/api/pkl/defs/graph/getFollows.pkl
-67
apps/api/pkl/defs/graph/getFollows.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.graph.getFollows"
5
-
defs = new Mapping<String, Query> {
6
-
["main"] {
7
-
type = "query"
8
-
description = "Enumerates accounts which a specified account (actor) follows."
9
-
parameters = new Params {
10
-
required = List("actor")
11
-
properties {
12
-
["actor"] = new StringType {
13
-
type = "string"
14
-
format = "at-identifier"
15
-
}
16
-
["limit"] = new IntegerType {
17
-
type = "integer"
18
-
maximum = 100
19
-
minimum = 1
20
-
default = 50
21
-
}
22
-
["dids"] = new Array {
23
-
type = "array"
24
-
items = new StringType {
25
-
type = "string"
26
-
format = "did"
27
-
}
28
-
description =
29
-
"If provided, filters the follows to only include those with DIDs in this list."
30
-
}
31
-
["cursor"] = new StringType {
32
-
type = "string"
33
-
}
34
-
}
35
-
}
36
-
37
-
output {
38
-
encoding = "application/json"
39
-
schema = new ObjectType {
40
-
type = "object"
41
-
required = List("subject", "follows")
42
-
properties {
43
-
["subject"] = new Ref {
44
-
type = "ref"
45
-
ref = "app.rocksky.actor.defs#profileViewBasic"
46
-
}
47
-
["follows"] = new Array {
48
-
type = "array"
49
-
items = new Ref {
50
-
type = "ref"
51
-
ref = "app.rocksky.actor.defs#profileViewBasic"
52
-
}
53
-
}
54
-
["cursor"] = new StringType {
55
-
type = "string"
56
-
description =
57
-
"A cursor value to pass to subsequent calls to get the next page of results."
58
-
}
59
-
["count"] = new IntegerType {
60
-
type = "integer"
61
-
description = "The total number of follows."
62
-
}
63
-
}
64
-
}
65
-
}
66
-
}
67
-
}
-55
apps/api/pkl/defs/graph/getKnownFollowers.pkl
-55
apps/api/pkl/defs/graph/getKnownFollowers.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.graph.getKnownFollowers"
5
-
defs = new Mapping<String, Query> {
6
-
["main"] {
7
-
type = "query"
8
-
description =
9
-
"Enumerates accounts which follow a specified account (actor) and are followed by the viewer."
10
-
parameters = new Params {
11
-
required = List("actor")
12
-
properties {
13
-
["actor"] = new StringType {
14
-
type = "string"
15
-
format = "at-identifier"
16
-
}
17
-
["limit"] = new IntegerType {
18
-
type = "integer"
19
-
maximum = 100
20
-
minimum = 1
21
-
default = 50
22
-
}
23
-
["cursor"] = new StringType {
24
-
type = "string"
25
-
}
26
-
}
27
-
}
28
-
29
-
output {
30
-
encoding = "application/json"
31
-
schema = new ObjectType {
32
-
type = "object"
33
-
required = List("subject", "followers")
34
-
properties {
35
-
["subject"] = new Ref {
36
-
type = "ref"
37
-
ref = "app.rocksky.actor.defs#profileViewBasic"
38
-
}
39
-
["followers"] = new Array {
40
-
type = "array"
41
-
items = new Ref {
42
-
type = "ref"
43
-
ref = "app.rocksky.actor.defs#profileViewBasic"
44
-
}
45
-
}
46
-
["cursor"] = new StringType {
47
-
type = "string"
48
-
description =
49
-
"A cursor value to pass to subsequent calls to get the next page of results."
50
-
}
51
-
}
52
-
}
53
-
}
54
-
}
55
-
}
-46
apps/api/pkl/defs/graph/unfollowAccount.pkl
-46
apps/api/pkl/defs/graph/unfollowAccount.pkl
···
1
-
amends "../../schema/lexicon.pkl"
2
-
3
-
lexicon = 1
4
-
id = "app.rocksky.graph.unfollowAccount"
5
-
defs = new Mapping<String, Procedure> {
6
-
["main"] {
7
-
type = "procedure"
8
-
description =
9
-
"Removes a 'follow' relationship from the authenticated account to a specified account."
10
-
parameters = new Params {
11
-
required = List("account")
12
-
properties {
13
-
["account"] = new StringType {
14
-
type = "string"
15
-
format = "at-identifier"
16
-
}
17
-
}
18
-
}
19
-
20
-
output {
21
-
encoding = "application/json"
22
-
schema = new ObjectType {
23
-
type = "object"
24
-
required = List("subject", "followers")
25
-
properties {
26
-
["subject"] = new Ref {
27
-
type = "ref"
28
-
ref = "app.rocksky.actor.defs#profileViewBasic"
29
-
}
30
-
["followers"] = new Array {
31
-
type = "array"
32
-
items = new Ref {
33
-
type = "ref"
34
-
ref = "app.rocksky.actor.defs#profileViewBasic"
35
-
}
36
-
}
37
-
["cursor"] = new StringType {
38
-
type = "string"
39
-
description =
40
-
"A cursor value to pass to subsequent calls to get the next page of results."
41
-
}
42
-
}
43
-
}
44
-
}
45
-
}
46
-
}
+2
-6
apps/api/pkl/defs/scrobble/getScrobbles.pkl
+2
-6
apps/api/pkl/defs/scrobble/getScrobbles.pkl
···
1
-
amends "../../schema/lexicon.pkl"
1
+
amends "../../schema/lexicon.pkl"
2
2
3
3
lexicon = 1
4
4
id = "app.rocksky.scrobble.getScrobbles"
···
11
11
["did"] = new StringType {
12
12
description = "The DID or handle of the actor"
13
13
format = "at-identifier"
14
-
}
15
-
["following"] = new BooleanType {
16
-
type = "boolean"
17
-
description = "If true, only return scrobbles from actors the viewer is following."
18
14
}
19
15
["limit"] = new IntegerType {
20
16
type = "integer"
···
43
39
}
44
40
}
45
41
}
46
-
}
42
+
}
+1
-3
apps/api/pkl/schema/lexicon.pkl
+1
-3
apps/api/pkl/schema/lexicon.pkl
···
13
13
14
14
class IntegerType extends BaseType {
15
15
type: "integer"
16
-
maximum: Int?
17
16
minimum: Int?
18
-
default: Int?
19
17
}
20
18
21
19
class BooleanType extends BaseType {
···
42
40
class ObjectType extends BaseType {
43
41
type: "object"
44
42
required: List<String>?
45
-
properties: Mapping<String, StringType | IntegerType | BooleanType | Blob | Ref | Union | Array>
43
+
properties: Mapping<String, StringType | IntegerType | Blob | Ref | Union | Array>
46
44
}
47
45
48
46
class Union extends BaseType {
+1
-50
apps/api/src/bsky/app.ts
+1
-50
apps/api/src/bsky/app.ts
···
1
-
import { AtpAgent } from "@atproto/api";
2
1
import type { BlobRef } from "@atproto/lexicon";
3
2
import { isValidHandle } from "@atproto/syntax";
4
3
import { ctx } from "context";
···
9
8
import { deepSnakeCaseKeys } from "lib";
10
9
import { createAgent } from "lib/agent";
11
10
import { env } from "lib/env";
12
-
import extractPdsFromDid from "lib/extractPdsFromDid";
13
11
import { requestCounter } from "metrics";
14
12
import dropboxAccounts from "schema/dropbox-accounts";
15
13
import googleDriveAccounts from "schema/google-drive-accounts";
···
42
40
43
41
app.post("/login", async (c) => {
44
42
requestCounter.add(1, { method: "POST", route: "/login" });
45
-
const { handle, cli, password } = await c.req.json();
43
+
const { handle, cli } = await c.req.json();
46
44
if (typeof handle !== "string" || !isValidHandle(handle)) {
47
45
c.status(400);
48
46
return c.text("Invalid handle");
49
47
}
50
48
51
49
try {
52
-
if (password) {
53
-
const defaultAgent = new AtpAgent({
54
-
service: new URL("https://bsky.social"),
55
-
});
56
-
const {
57
-
data: { did },
58
-
} = await defaultAgent.resolveHandle({ handle });
59
-
60
-
let pds = await ctx.redis.get(`pds:${did}`);
61
-
if (!pds) {
62
-
pds = await extractPdsFromDid(did);
63
-
await ctx.redis.setEx(`pds:${did}`, 60 * 15, pds);
64
-
}
65
-
66
-
const agent = new AtpAgent({
67
-
service: new URL(pds),
68
-
});
69
-
70
-
await agent.login({
71
-
identifier: handle,
72
-
password,
73
-
});
74
-
75
-
await ctx.sqliteDb
76
-
.insertInto("auth_session")
77
-
.values({
78
-
key: `atp:${did}`,
79
-
session: JSON.stringify(agent.session),
80
-
})
81
-
.onConflict((oc) =>
82
-
oc
83
-
.column("key")
84
-
.doUpdateSet({ session: JSON.stringify(agent.session) }),
85
-
)
86
-
.execute();
87
-
88
-
const token = jwt.sign(
89
-
{
90
-
did,
91
-
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7,
92
-
},
93
-
env.JWT_SECRET,
94
-
);
95
-
96
-
return c.text(`jwt:${token}`);
97
-
}
98
-
99
50
const url = await ctx.oauthClient.authorize(handle, {
100
51
scope: "atproto transition:generic",
101
52
});
-2
apps/api/src/context.ts
-2
apps/api/src/context.ts
-94
apps/api/src/lexicon/index.ts
-94
apps/api/src/lexicon/index.ts
···
16
16
import type * as FmTealAlphaFeedGetPlay from "./types/fm/teal/alpha/feed/getPlay";
17
17
import type * as AppRockskyActorGetActorAlbums from "./types/app/rocksky/actor/getActorAlbums";
18
18
import type * as AppRockskyActorGetActorArtists from "./types/app/rocksky/actor/getActorArtists";
19
-
import type * as AppRockskyActorGetActorCompatibility from "./types/app/rocksky/actor/getActorCompatibility";
20
19
import type * as AppRockskyActorGetActorLovedSongs from "./types/app/rocksky/actor/getActorLovedSongs";
21
-
import type * as AppRockskyActorGetActorNeighbours from "./types/app/rocksky/actor/getActorNeighbours";
22
20
import type * as AppRockskyActorGetActorPlaylists from "./types/app/rocksky/actor/getActorPlaylists";
23
21
import type * as AppRockskyActorGetActorScrobbles from "./types/app/rocksky/actor/getActorScrobbles";
24
22
import type * as AppRockskyActorGetActorSongs from "./types/app/rocksky/actor/getActorSongs";
···
50
48
import type * as AppRockskyGoogledriveDownloadFile from "./types/app/rocksky/googledrive/downloadFile";
51
49
import type * as AppRockskyGoogledriveGetFile from "./types/app/rocksky/googledrive/getFile";
52
50
import type * as AppRockskyGoogledriveGetFiles from "./types/app/rocksky/googledrive/getFiles";
53
-
import type * as AppRockskyGraphFollowAccount from "./types/app/rocksky/graph/followAccount";
54
-
import type * as AppRockskyGraphGetFollowers from "./types/app/rocksky/graph/getFollowers";
55
-
import type * as AppRockskyGraphGetFollows from "./types/app/rocksky/graph/getFollows";
56
-
import type * as AppRockskyGraphGetKnownFollowers from "./types/app/rocksky/graph/getKnownFollowers";
57
-
import type * as AppRockskyGraphUnfollowAccount from "./types/app/rocksky/graph/unfollowAccount";
58
51
import type * as AppRockskyLikeDislikeShout from "./types/app/rocksky/like/dislikeShout";
59
52
import type * as AppRockskyLikeDislikeSong from "./types/app/rocksky/like/dislikeSong";
60
53
import type * as AppRockskyLikeLikeShout from "./types/app/rocksky/like/likeShout";
···
244
237
dropbox: AppRockskyDropboxNS;
245
238
feed: AppRockskyFeedNS;
246
239
googledrive: AppRockskyGoogledriveNS;
247
-
graph: AppRockskyGraphNS;
248
240
like: AppRockskyLikeNS;
249
241
player: AppRockskyPlayerNS;
250
242
playlist: AppRockskyPlaylistNS;
···
264
256
this.dropbox = new AppRockskyDropboxNS(server);
265
257
this.feed = new AppRockskyFeedNS(server);
266
258
this.googledrive = new AppRockskyGoogledriveNS(server);
267
-
this.graph = new AppRockskyGraphNS(server);
268
259
this.like = new AppRockskyLikeNS(server);
269
260
this.player = new AppRockskyPlayerNS(server);
270
261
this.playlist = new AppRockskyPlaylistNS(server);
···
305
296
return this._server.xrpc.method(nsid, cfg);
306
297
}
307
298
308
-
getActorCompatibility<AV extends AuthVerifier>(
309
-
cfg: ConfigOf<
310
-
AV,
311
-
AppRockskyActorGetActorCompatibility.Handler<ExtractAuth<AV>>,
312
-
AppRockskyActorGetActorCompatibility.HandlerReqCtx<ExtractAuth<AV>>
313
-
>,
314
-
) {
315
-
const nsid = "app.rocksky.actor.getActorCompatibility"; // @ts-ignore
316
-
return this._server.xrpc.method(nsid, cfg);
317
-
}
318
-
319
299
getActorLovedSongs<AV extends AuthVerifier>(
320
300
cfg: ConfigOf<
321
301
AV,
···
324
304
>,
325
305
) {
326
306
const nsid = "app.rocksky.actor.getActorLovedSongs"; // @ts-ignore
327
-
return this._server.xrpc.method(nsid, cfg);
328
-
}
329
-
330
-
getActorNeighbours<AV extends AuthVerifier>(
331
-
cfg: ConfigOf<
332
-
AV,
333
-
AppRockskyActorGetActorNeighbours.Handler<ExtractAuth<AV>>,
334
-
AppRockskyActorGetActorNeighbours.HandlerReqCtx<ExtractAuth<AV>>
335
-
>,
336
-
) {
337
-
const nsid = "app.rocksky.actor.getActorNeighbours"; // @ts-ignore
338
307
return this._server.xrpc.method(nsid, cfg);
339
308
}
340
309
···
732
701
>,
733
702
) {
734
703
const nsid = "app.rocksky.googledrive.getFiles"; // @ts-ignore
735
-
return this._server.xrpc.method(nsid, cfg);
736
-
}
737
-
}
738
-
739
-
export class AppRockskyGraphNS {
740
-
_server: Server;
741
-
742
-
constructor(server: Server) {
743
-
this._server = server;
744
-
}
745
-
746
-
followAccount<AV extends AuthVerifier>(
747
-
cfg: ConfigOf<
748
-
AV,
749
-
AppRockskyGraphFollowAccount.Handler<ExtractAuth<AV>>,
750
-
AppRockskyGraphFollowAccount.HandlerReqCtx<ExtractAuth<AV>>
751
-
>,
752
-
) {
753
-
const nsid = "app.rocksky.graph.followAccount"; // @ts-ignore
754
-
return this._server.xrpc.method(nsid, cfg);
755
-
}
756
-
757
-
getFollowers<AV extends AuthVerifier>(
758
-
cfg: ConfigOf<
759
-
AV,
760
-
AppRockskyGraphGetFollowers.Handler<ExtractAuth<AV>>,
761
-
AppRockskyGraphGetFollowers.HandlerReqCtx<ExtractAuth<AV>>
762
-
>,
763
-
) {
764
-
const nsid = "app.rocksky.graph.getFollowers"; // @ts-ignore
765
-
return this._server.xrpc.method(nsid, cfg);
766
-
}
767
-
768
-
getFollows<AV extends AuthVerifier>(
769
-
cfg: ConfigOf<
770
-
AV,
771
-
AppRockskyGraphGetFollows.Handler<ExtractAuth<AV>>,
772
-
AppRockskyGraphGetFollows.HandlerReqCtx<ExtractAuth<AV>>
773
-
>,
774
-
) {
775
-
const nsid = "app.rocksky.graph.getFollows"; // @ts-ignore
776
-
return this._server.xrpc.method(nsid, cfg);
777
-
}
778
-
779
-
getKnownFollowers<AV extends AuthVerifier>(
780
-
cfg: ConfigOf<
781
-
AV,
782
-
AppRockskyGraphGetKnownFollowers.Handler<ExtractAuth<AV>>,
783
-
AppRockskyGraphGetKnownFollowers.HandlerReqCtx<ExtractAuth<AV>>
784
-
>,
785
-
) {
786
-
const nsid = "app.rocksky.graph.getKnownFollowers"; // @ts-ignore
787
-
return this._server.xrpc.method(nsid, cfg);
788
-
}
789
-
790
-
unfollowAccount<AV extends AuthVerifier>(
791
-
cfg: ConfigOf<
792
-
AV,
793
-
AppRockskyGraphUnfollowAccount.Handler<ExtractAuth<AV>>,
794
-
AppRockskyGraphUnfollowAccount.HandlerReqCtx<ExtractAuth<AV>>
795
-
>,
796
-
) {
797
-
const nsid = "app.rocksky.graph.unfollowAccount"; // @ts-ignore
798
704
return this._server.xrpc.method(nsid, cfg);
799
705
}
800
706
}
-549
apps/api/src/lexicon/lexicons.ts
-549
apps/api/src/lexicon/lexicons.ts
···
664
664
},
665
665
},
666
666
},
667
-
neighbourViewBasic: {
668
-
type: "object",
669
-
properties: {
670
-
userId: {
671
-
type: "string",
672
-
},
673
-
did: {
674
-
type: "string",
675
-
},
676
-
handle: {
677
-
type: "string",
678
-
},
679
-
displayName: {
680
-
type: "string",
681
-
},
682
-
avatar: {
683
-
type: "string",
684
-
description: "The URL of the actor's avatar image.",
685
-
format: "uri",
686
-
},
687
-
sharedArtistsCount: {
688
-
type: "integer",
689
-
description: "The number of artists shared with the actor.",
690
-
},
691
-
similarityScore: {
692
-
type: "integer",
693
-
description: "The similarity score with the actor.",
694
-
},
695
-
topSharedArtistNames: {
696
-
type: "array",
697
-
description: "The top shared artist names with the actor.",
698
-
items: {
699
-
type: "string",
700
-
},
701
-
},
702
-
topSharedArtistsDetails: {
703
-
type: "array",
704
-
description: "The top shared artist details with the actor.",
705
-
items: {
706
-
type: "ref",
707
-
ref: "lex:app.rocksky.artist.defs#artistViewBasic",
708
-
},
709
-
},
710
-
},
711
-
},
712
-
compatibilityViewBasic: {
713
-
type: "object",
714
-
properties: {
715
-
compatibilityLevel: {
716
-
type: "integer",
717
-
},
718
-
compatibilityPercentage: {
719
-
type: "integer",
720
-
},
721
-
sharedArtists: {
722
-
type: "integer",
723
-
},
724
-
topSharedArtistNames: {
725
-
type: "array",
726
-
items: {
727
-
type: "string",
728
-
},
729
-
},
730
-
topSharedDetailedArtists: {
731
-
type: "array",
732
-
items: {
733
-
type: "ref",
734
-
ref: "lex:app.rocksky.actor.defs#artistViewBasic",
735
-
},
736
-
},
737
-
user1ArtistCount: {
738
-
type: "integer",
739
-
},
740
-
user2ArtistCount: {
741
-
type: "integer",
742
-
},
743
-
},
744
-
},
745
-
artistViewBasic: {
746
-
type: "object",
747
-
properties: {
748
-
id: {
749
-
type: "string",
750
-
},
751
-
name: {
752
-
type: "string",
753
-
},
754
-
picture: {
755
-
type: "string",
756
-
format: "uri",
757
-
},
758
-
uri: {
759
-
type: "string",
760
-
format: "at-uri",
761
-
},
762
-
user1Rank: {
763
-
type: "integer",
764
-
},
765
-
user2Rank: {
766
-
type: "integer",
767
-
},
768
-
weight: {
769
-
type: "integer",
770
-
},
771
-
},
772
-
},
773
667
},
774
668
},
775
669
AppRockskyActorGetActorAlbums: {
···
864
758
},
865
759
},
866
760
},
867
-
AppRockskyActorGetActorCompatibility: {
868
-
lexicon: 1,
869
-
id: "app.rocksky.actor.getActorCompatibility",
870
-
defs: {
871
-
main: {
872
-
type: "query",
873
-
description: "Get compatibility for an actor",
874
-
parameters: {
875
-
type: "params",
876
-
required: ["did"],
877
-
properties: {
878
-
did: {
879
-
type: "string",
880
-
description: "DID or handle to get compatibility for",
881
-
format: "at-identifier",
882
-
},
883
-
},
884
-
},
885
-
output: {
886
-
encoding: "application/json",
887
-
schema: {
888
-
type: "object",
889
-
properties: {
890
-
compatibility: {
891
-
type: "ref",
892
-
ref: "lex:app.rocksky.actor.defs#compatibilityViewBasic",
893
-
},
894
-
},
895
-
},
896
-
},
897
-
},
898
-
},
899
-
},
900
761
AppRockskyActorGetActorLovedSongs: {
901
762
lexicon: 1,
902
763
id: "app.rocksky.actor.getActorLovedSongs",
···
935
796
items: {
936
797
type: "ref",
937
798
ref: "lex:app.rocksky.song.defs#songViewBasic",
938
-
},
939
-
},
940
-
},
941
-
},
942
-
},
943
-
},
944
-
},
945
-
},
946
-
AppRockskyActorGetActorNeighbours: {
947
-
lexicon: 1,
948
-
id: "app.rocksky.actor.getActorNeighbours",
949
-
defs: {
950
-
main: {
951
-
type: "query",
952
-
description: "Get neighbours for an actor",
953
-
parameters: {
954
-
type: "params",
955
-
required: ["did"],
956
-
properties: {
957
-
did: {
958
-
type: "string",
959
-
description: "The DID or handle of the actor",
960
-
format: "at-identifier",
961
-
},
962
-
},
963
-
},
964
-
output: {
965
-
encoding: "application/json",
966
-
schema: {
967
-
type: "object",
968
-
properties: {
969
-
neighbours: {
970
-
type: "array",
971
-
items: {
972
-
type: "ref",
973
-
ref: "lex:app.rocksky.actor.defs#neighbourViewBasic",
974
799
},
975
800
},
976
801
},
···
2518
2343
ref: "lex:app.rocksky.feed.defs#feedItemView",
2519
2344
},
2520
2345
},
2521
-
cursor: {
2522
-
type: "string",
2523
-
description: "The pagination cursor for the next set of results.",
2524
-
},
2525
2346
},
2526
2347
},
2527
2348
},
···
2914
2735
},
2915
2736
},
2916
2737
},
2917
-
AppRockskyGraphDefs: {
2918
-
lexicon: 1,
2919
-
id: "app.rocksky.graph.defs",
2920
-
defs: {
2921
-
notFoundActor: {
2922
-
type: "object",
2923
-
description: "indicates that a handle or DID could not be resolved",
2924
-
required: ["actor", "notFound"],
2925
-
properties: {
2926
-
actor: {
2927
-
type: "string",
2928
-
format: "at-identifier",
2929
-
},
2930
-
notFound: {
2931
-
type: "boolean",
2932
-
},
2933
-
},
2934
-
},
2935
-
relationship: {
2936
-
type: "object",
2937
-
required: ["did"],
2938
-
properties: {
2939
-
did: {
2940
-
type: "string",
2941
-
format: "did",
2942
-
},
2943
-
following: {
2944
-
type: "string",
2945
-
description:
2946
-
"if the actor follows this DID, this is the AT-URI of the follow record",
2947
-
format: "at-uri",
2948
-
},
2949
-
followedBy: {
2950
-
type: "string",
2951
-
description:
2952
-
"if the actor is followed by this DID, contains the AT-URI of the follow record",
2953
-
format: "at-uri",
2954
-
},
2955
-
},
2956
-
},
2957
-
},
2958
-
},
2959
-
AppRockskyGraphFollow: {
2960
-
lexicon: 1,
2961
-
id: "app.rocksky.graph.follow",
2962
-
defs: {
2963
-
main: {
2964
-
type: "record",
2965
-
description:
2966
-
"Record declaring a social 'follow' relationship of another account.",
2967
-
key: "tid",
2968
-
record: {
2969
-
type: "object",
2970
-
required: ["createdAt", "subject"],
2971
-
properties: {
2972
-
createdAt: {
2973
-
type: "string",
2974
-
format: "datetime",
2975
-
},
2976
-
subject: {
2977
-
type: "string",
2978
-
format: "did",
2979
-
},
2980
-
via: {
2981
-
type: "ref",
2982
-
ref: "lex:com.atproto.repo.strongRef",
2983
-
},
2984
-
},
2985
-
},
2986
-
},
2987
-
},
2988
-
},
2989
-
AppRockskyGraphFollowAccount: {
2990
-
lexicon: 1,
2991
-
id: "app.rocksky.graph.followAccount",
2992
-
defs: {
2993
-
main: {
2994
-
type: "procedure",
2995
-
description:
2996
-
"Creates a 'follow' relationship from the authenticated account to a specified account.",
2997
-
parameters: {
2998
-
type: "params",
2999
-
required: ["account"],
3000
-
properties: {
3001
-
account: {
3002
-
type: "string",
3003
-
format: "at-identifier",
3004
-
},
3005
-
},
3006
-
},
3007
-
output: {
3008
-
encoding: "application/json",
3009
-
schema: {
3010
-
type: "object",
3011
-
required: ["subject", "followers"],
3012
-
properties: {
3013
-
subject: {
3014
-
type: "ref",
3015
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3016
-
},
3017
-
followers: {
3018
-
type: "array",
3019
-
items: {
3020
-
type: "ref",
3021
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3022
-
},
3023
-
},
3024
-
cursor: {
3025
-
type: "string",
3026
-
description:
3027
-
"A cursor value to pass to subsequent calls to get the next page of results.",
3028
-
},
3029
-
},
3030
-
},
3031
-
},
3032
-
},
3033
-
},
3034
-
},
3035
-
AppRockskyGraphGetFollowers: {
3036
-
lexicon: 1,
3037
-
id: "app.rocksky.graph.getFollowers",
3038
-
defs: {
3039
-
main: {
3040
-
type: "query",
3041
-
description:
3042
-
"Enumerates accounts which follow a specified account (actor).",
3043
-
parameters: {
3044
-
type: "params",
3045
-
required: ["actor"],
3046
-
properties: {
3047
-
actor: {
3048
-
type: "string",
3049
-
format: "at-identifier",
3050
-
},
3051
-
limit: {
3052
-
type: "integer",
3053
-
maximum: 100,
3054
-
minimum: 1,
3055
-
default: 50,
3056
-
},
3057
-
dids: {
3058
-
type: "array",
3059
-
description:
3060
-
"If provided, filters the followers to only include those with DIDs in this list.",
3061
-
items: {
3062
-
type: "string",
3063
-
format: "did",
3064
-
},
3065
-
},
3066
-
cursor: {
3067
-
type: "string",
3068
-
},
3069
-
},
3070
-
},
3071
-
output: {
3072
-
encoding: "application/json",
3073
-
schema: {
3074
-
type: "object",
3075
-
required: ["subject", "followers"],
3076
-
properties: {
3077
-
subject: {
3078
-
type: "ref",
3079
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3080
-
},
3081
-
followers: {
3082
-
type: "array",
3083
-
items: {
3084
-
type: "ref",
3085
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3086
-
},
3087
-
},
3088
-
cursor: {
3089
-
type: "string",
3090
-
description:
3091
-
"A cursor value to pass to subsequent calls to get the next page of results.",
3092
-
},
3093
-
count: {
3094
-
type: "integer",
3095
-
description: "The total number of followers.",
3096
-
},
3097
-
},
3098
-
},
3099
-
},
3100
-
},
3101
-
},
3102
-
},
3103
-
AppRockskyGraphGetFollows: {
3104
-
lexicon: 1,
3105
-
id: "app.rocksky.graph.getFollows",
3106
-
defs: {
3107
-
main: {
3108
-
type: "query",
3109
-
description:
3110
-
"Enumerates accounts which a specified account (actor) follows.",
3111
-
parameters: {
3112
-
type: "params",
3113
-
required: ["actor"],
3114
-
properties: {
3115
-
actor: {
3116
-
type: "string",
3117
-
format: "at-identifier",
3118
-
},
3119
-
limit: {
3120
-
type: "integer",
3121
-
maximum: 100,
3122
-
minimum: 1,
3123
-
default: 50,
3124
-
},
3125
-
dids: {
3126
-
type: "array",
3127
-
description:
3128
-
"If provided, filters the follows to only include those with DIDs in this list.",
3129
-
items: {
3130
-
type: "string",
3131
-
format: "did",
3132
-
},
3133
-
},
3134
-
cursor: {
3135
-
type: "string",
3136
-
},
3137
-
},
3138
-
},
3139
-
output: {
3140
-
encoding: "application/json",
3141
-
schema: {
3142
-
type: "object",
3143
-
required: ["subject", "follows"],
3144
-
properties: {
3145
-
subject: {
3146
-
type: "ref",
3147
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3148
-
},
3149
-
follows: {
3150
-
type: "array",
3151
-
items: {
3152
-
type: "ref",
3153
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3154
-
},
3155
-
},
3156
-
cursor: {
3157
-
type: "string",
3158
-
description:
3159
-
"A cursor value to pass to subsequent calls to get the next page of results.",
3160
-
},
3161
-
count: {
3162
-
type: "integer",
3163
-
description: "The total number of follows.",
3164
-
},
3165
-
},
3166
-
},
3167
-
},
3168
-
},
3169
-
},
3170
-
},
3171
-
AppRockskyGraphGetKnownFollowers: {
3172
-
lexicon: 1,
3173
-
id: "app.rocksky.graph.getKnownFollowers",
3174
-
defs: {
3175
-
main: {
3176
-
type: "query",
3177
-
description:
3178
-
"Enumerates accounts which follow a specified account (actor) and are followed by the viewer.",
3179
-
parameters: {
3180
-
type: "params",
3181
-
required: ["actor"],
3182
-
properties: {
3183
-
actor: {
3184
-
type: "string",
3185
-
format: "at-identifier",
3186
-
},
3187
-
limit: {
3188
-
type: "integer",
3189
-
maximum: 100,
3190
-
minimum: 1,
3191
-
default: 50,
3192
-
},
3193
-
cursor: {
3194
-
type: "string",
3195
-
},
3196
-
},
3197
-
},
3198
-
output: {
3199
-
encoding: "application/json",
3200
-
schema: {
3201
-
type: "object",
3202
-
required: ["subject", "followers"],
3203
-
properties: {
3204
-
subject: {
3205
-
type: "ref",
3206
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3207
-
},
3208
-
followers: {
3209
-
type: "array",
3210
-
items: {
3211
-
type: "ref",
3212
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3213
-
},
3214
-
},
3215
-
cursor: {
3216
-
type: "string",
3217
-
description:
3218
-
"A cursor value to pass to subsequent calls to get the next page of results.",
3219
-
},
3220
-
},
3221
-
},
3222
-
},
3223
-
},
3224
-
},
3225
-
},
3226
-
AppRockskyGraphUnfollowAccount: {
3227
-
lexicon: 1,
3228
-
id: "app.rocksky.graph.unfollowAccount",
3229
-
defs: {
3230
-
main: {
3231
-
type: "procedure",
3232
-
description:
3233
-
"Removes a 'follow' relationship from the authenticated account to a specified account.",
3234
-
parameters: {
3235
-
type: "params",
3236
-
required: ["account"],
3237
-
properties: {
3238
-
account: {
3239
-
type: "string",
3240
-
format: "at-identifier",
3241
-
},
3242
-
},
3243
-
},
3244
-
output: {
3245
-
encoding: "application/json",
3246
-
schema: {
3247
-
type: "object",
3248
-
required: ["subject", "followers"],
3249
-
properties: {
3250
-
subject: {
3251
-
type: "ref",
3252
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3253
-
},
3254
-
followers: {
3255
-
type: "array",
3256
-
items: {
3257
-
type: "ref",
3258
-
ref: "lex:app.rocksky.actor.defs#profileViewBasic",
3259
-
},
3260
-
},
3261
-
cursor: {
3262
-
type: "string",
3263
-
description:
3264
-
"A cursor value to pass to subsequent calls to get the next page of results.",
3265
-
},
3266
-
},
3267
-
},
3268
-
},
3269
-
},
3270
-
},
3271
-
},
3272
2738
AppRockskyLikeDislikeShout: {
3273
2739
lexicon: 1,
3274
2740
id: "app.rocksky.like.dislikeShout",
···
4558
4024
type: "string",
4559
4025
description: "The DID or handle of the actor",
4560
4026
format: "at-identifier",
4561
-
},
4562
-
following: {
4563
-
type: "boolean",
4564
-
description:
4565
-
"If true, only return scrobbles from actors the viewer is following.",
4566
4027
},
4567
4028
limit: {
4568
4029
type: "integer",
···
5890
5351
AppRockskyActorDefs: "app.rocksky.actor.defs",
5891
5352
AppRockskyActorGetActorAlbums: "app.rocksky.actor.getActorAlbums",
5892
5353
AppRockskyActorGetActorArtists: "app.rocksky.actor.getActorArtists",
5893
-
AppRockskyActorGetActorCompatibility:
5894
-
"app.rocksky.actor.getActorCompatibility",
5895
5354
AppRockskyActorGetActorLovedSongs: "app.rocksky.actor.getActorLovedSongs",
5896
-
AppRockskyActorGetActorNeighbours: "app.rocksky.actor.getActorNeighbours",
5897
5355
AppRockskyActorGetActorPlaylists: "app.rocksky.actor.getActorPlaylists",
5898
5356
AppRockskyActorGetActorScrobbles: "app.rocksky.actor.getActorScrobbles",
5899
5357
AppRockskyActorGetActorSongs: "app.rocksky.actor.getActorSongs",
···
5937
5395
AppRockskyGoogledriveDownloadFile: "app.rocksky.googledrive.downloadFile",
5938
5396
AppRockskyGoogledriveGetFile: "app.rocksky.googledrive.getFile",
5939
5397
AppRockskyGoogledriveGetFiles: "app.rocksky.googledrive.getFiles",
5940
-
AppRockskyGraphDefs: "app.rocksky.graph.defs",
5941
-
AppRockskyGraphFollow: "app.rocksky.graph.follow",
5942
-
AppRockskyGraphFollowAccount: "app.rocksky.graph.followAccount",
5943
-
AppRockskyGraphGetFollowers: "app.rocksky.graph.getFollowers",
5944
-
AppRockskyGraphGetFollows: "app.rocksky.graph.getFollows",
5945
-
AppRockskyGraphGetKnownFollowers: "app.rocksky.graph.getKnownFollowers",
5946
-
AppRockskyGraphUnfollowAccount: "app.rocksky.graph.unfollowAccount",
5947
5398
AppRockskyLikeDislikeShout: "app.rocksky.like.dislikeShout",
5948
5399
AppRockskyLikeDislikeSong: "app.rocksky.like.dislikeSong",
5949
5400
AppRockskyLike: "app.rocksky.like",
-79
apps/api/src/lexicon/types/app/rocksky/actor/defs.ts
-79
apps/api/src/lexicon/types/app/rocksky/actor/defs.ts
···
5
5
import { lexicons } from "../../../../lexicons";
6
6
import { isObj, hasProp } from "../../../../util";
7
7
import { CID } from "multiformats/cid";
8
-
import type * as AppRockskyArtistDefs from "../artist/defs";
9
8
10
9
export interface ProfileViewDetailed {
11
10
/** The unique identifier of the actor. */
···
66
65
export function validateProfileViewBasic(v: unknown): ValidationResult {
67
66
return lexicons.validate("app.rocksky.actor.defs#profileViewBasic", v);
68
67
}
69
-
70
-
export interface NeighbourViewBasic {
71
-
userId?: string;
72
-
did?: string;
73
-
handle?: string;
74
-
displayName?: string;
75
-
/** The URL of the actor's avatar image. */
76
-
avatar?: string;
77
-
/** The number of artists shared with the actor. */
78
-
sharedArtistsCount?: number;
79
-
/** The similarity score with the actor. */
80
-
similarityScore?: number;
81
-
/** The top shared artist names with the actor. */
82
-
topSharedArtistNames?: string[];
83
-
/** The top shared artist details with the actor. */
84
-
topSharedArtistsDetails?: AppRockskyArtistDefs.ArtistViewBasic[];
85
-
[k: string]: unknown;
86
-
}
87
-
88
-
export function isNeighbourViewBasic(v: unknown): v is NeighbourViewBasic {
89
-
return (
90
-
isObj(v) &&
91
-
hasProp(v, "$type") &&
92
-
v.$type === "app.rocksky.actor.defs#neighbourViewBasic"
93
-
);
94
-
}
95
-
96
-
export function validateNeighbourViewBasic(v: unknown): ValidationResult {
97
-
return lexicons.validate("app.rocksky.actor.defs#neighbourViewBasic", v);
98
-
}
99
-
100
-
export interface CompatibilityViewBasic {
101
-
compatibilityLevel?: number;
102
-
compatibilityPercentage?: number;
103
-
sharedArtists?: number;
104
-
topSharedArtistNames?: string[];
105
-
topSharedDetailedArtists?: ArtistViewBasic[];
106
-
user1ArtistCount?: number;
107
-
user2ArtistCount?: number;
108
-
[k: string]: unknown;
109
-
}
110
-
111
-
export function isCompatibilityViewBasic(
112
-
v: unknown,
113
-
): v is CompatibilityViewBasic {
114
-
return (
115
-
isObj(v) &&
116
-
hasProp(v, "$type") &&
117
-
v.$type === "app.rocksky.actor.defs#compatibilityViewBasic"
118
-
);
119
-
}
120
-
121
-
export function validateCompatibilityViewBasic(v: unknown): ValidationResult {
122
-
return lexicons.validate("app.rocksky.actor.defs#compatibilityViewBasic", v);
123
-
}
124
-
125
-
export interface ArtistViewBasic {
126
-
id?: string;
127
-
name?: string;
128
-
picture?: string;
129
-
uri?: string;
130
-
user1Rank?: number;
131
-
user2Rank?: number;
132
-
weight?: number;
133
-
[k: string]: unknown;
134
-
}
135
-
136
-
export function isArtistViewBasic(v: unknown): v is ArtistViewBasic {
137
-
return (
138
-
isObj(v) &&
139
-
hasProp(v, "$type") &&
140
-
v.$type === "app.rocksky.actor.defs#artistViewBasic"
141
-
);
142
-
}
143
-
144
-
export function validateArtistViewBasic(v: unknown): ValidationResult {
145
-
return lexicons.validate("app.rocksky.actor.defs#artistViewBasic", v);
146
-
}
-48
apps/api/src/lexicon/types/app/rocksky/actor/getActorCompatibility.ts
-48
apps/api/src/lexicon/types/app/rocksky/actor/getActorCompatibility.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import type express from "express";
5
-
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
-
import { lexicons } from "../../../../lexicons";
7
-
import { isObj, hasProp } from "../../../../util";
8
-
import { CID } from "multiformats/cid";
9
-
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
-
import type * as AppRockskyActorDefs from "./defs";
11
-
12
-
export interface QueryParams {
13
-
/** DID or handle to get compatibility for */
14
-
did: string;
15
-
}
16
-
17
-
export type InputSchema = undefined;
18
-
19
-
export interface OutputSchema {
20
-
compatibility?: AppRockskyActorDefs.CompatibilityViewBasic;
21
-
[k: string]: unknown;
22
-
}
23
-
24
-
export type HandlerInput = undefined;
25
-
26
-
export interface HandlerSuccess {
27
-
encoding: "application/json";
28
-
body: OutputSchema;
29
-
headers?: { [key: string]: string };
30
-
}
31
-
32
-
export interface HandlerError {
33
-
status: number;
34
-
message?: string;
35
-
}
36
-
37
-
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
38
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
39
-
auth: HA;
40
-
params: QueryParams;
41
-
input: HandlerInput;
42
-
req: express.Request;
43
-
res: express.Response;
44
-
resetRouteRateLimits: () => Promise<void>;
45
-
};
46
-
export type Handler<HA extends HandlerAuth = never> = (
47
-
ctx: HandlerReqCtx<HA>,
48
-
) => Promise<HandlerOutput> | HandlerOutput;
-48
apps/api/src/lexicon/types/app/rocksky/actor/getActorNeighbours.ts
-48
apps/api/src/lexicon/types/app/rocksky/actor/getActorNeighbours.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import type express from "express";
5
-
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
-
import { lexicons } from "../../../../lexicons";
7
-
import { isObj, hasProp } from "../../../../util";
8
-
import { CID } from "multiformats/cid";
9
-
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
-
import type * as AppRockskyActorDefs from "./defs";
11
-
12
-
export interface QueryParams {
13
-
/** The DID or handle of the actor */
14
-
did: string;
15
-
}
16
-
17
-
export type InputSchema = undefined;
18
-
19
-
export interface OutputSchema {
20
-
neighbours?: AppRockskyActorDefs.NeighbourViewBasic[];
21
-
[k: string]: unknown;
22
-
}
23
-
24
-
export type HandlerInput = undefined;
25
-
26
-
export interface HandlerSuccess {
27
-
encoding: "application/json";
28
-
body: OutputSchema;
29
-
headers?: { [key: string]: string };
30
-
}
31
-
32
-
export interface HandlerError {
33
-
status: number;
34
-
message?: string;
35
-
}
36
-
37
-
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
38
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
39
-
auth: HA;
40
-
params: QueryParams;
41
-
input: HandlerInput;
42
-
req: express.Request;
43
-
res: express.Response;
44
-
resetRouteRateLimits: () => Promise<void>;
45
-
};
46
-
export type Handler<HA extends HandlerAuth = never> = (
47
-
ctx: HandlerReqCtx<HA>,
48
-
) => Promise<HandlerOutput> | HandlerOutput;
-2
apps/api/src/lexicon/types/app/rocksky/feed/defs.ts
-2
apps/api/src/lexicon/types/app/rocksky/feed/defs.ts
-47
apps/api/src/lexicon/types/app/rocksky/graph/defs.ts
-47
apps/api/src/lexicon/types/app/rocksky/graph/defs.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import { type ValidationResult, BlobRef } from "@atproto/lexicon";
5
-
import { lexicons } from "../../../../lexicons";
6
-
import { isObj, hasProp } from "../../../../util";
7
-
import { CID } from "multiformats/cid";
8
-
9
-
/** indicates that a handle or DID could not be resolved */
10
-
export interface NotFoundActor {
11
-
actor: string;
12
-
notFound: boolean;
13
-
[k: string]: unknown;
14
-
}
15
-
16
-
export function isNotFoundActor(v: unknown): v is NotFoundActor {
17
-
return (
18
-
isObj(v) &&
19
-
hasProp(v, "$type") &&
20
-
v.$type === "app.rocksky.graph.defs#notFoundActor"
21
-
);
22
-
}
23
-
24
-
export function validateNotFoundActor(v: unknown): ValidationResult {
25
-
return lexicons.validate("app.rocksky.graph.defs#notFoundActor", v);
26
-
}
27
-
28
-
export interface Relationship {
29
-
did: string;
30
-
/** if the actor follows this DID, this is the AT-URI of the follow record */
31
-
following?: string;
32
-
/** if the actor is followed by this DID, contains the AT-URI of the follow record */
33
-
followedBy?: string;
34
-
[k: string]: unknown;
35
-
}
36
-
37
-
export function isRelationship(v: unknown): v is Relationship {
38
-
return (
39
-
isObj(v) &&
40
-
hasProp(v, "$type") &&
41
-
v.$type === "app.rocksky.graph.defs#relationship"
42
-
);
43
-
}
44
-
45
-
export function validateRelationship(v: unknown): ValidationResult {
46
-
return lexicons.validate("app.rocksky.graph.defs#relationship", v);
47
-
}
-28
apps/api/src/lexicon/types/app/rocksky/graph/follow.ts
-28
apps/api/src/lexicon/types/app/rocksky/graph/follow.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import { type ValidationResult, BlobRef } from "@atproto/lexicon";
5
-
import { lexicons } from "../../../../lexicons";
6
-
import { isObj, hasProp } from "../../../../util";
7
-
import { CID } from "multiformats/cid";
8
-
import type * as ComAtprotoRepoStrongRef from "../../../com/atproto/repo/strongRef";
9
-
10
-
export interface Record {
11
-
createdAt: string;
12
-
subject: string;
13
-
via?: ComAtprotoRepoStrongRef.Main;
14
-
[k: string]: unknown;
15
-
}
16
-
17
-
export function isRecord(v: unknown): v is Record {
18
-
return (
19
-
isObj(v) &&
20
-
hasProp(v, "$type") &&
21
-
(v.$type === "app.rocksky.graph.follow#main" ||
22
-
v.$type === "app.rocksky.graph.follow")
23
-
);
24
-
}
25
-
26
-
export function validateRecord(v: unknown): ValidationResult {
27
-
return lexicons.validate("app.rocksky.graph.follow#main", v);
28
-
}
-50
apps/api/src/lexicon/types/app/rocksky/graph/followAccount.ts
-50
apps/api/src/lexicon/types/app/rocksky/graph/followAccount.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import type express from "express";
5
-
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
-
import { lexicons } from "../../../../lexicons";
7
-
import { isObj, hasProp } from "../../../../util";
8
-
import { CID } from "multiformats/cid";
9
-
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
-
import type * as AppRockskyActorDefs from "../actor/defs";
11
-
12
-
export interface QueryParams {
13
-
account: string;
14
-
}
15
-
16
-
export type InputSchema = undefined;
17
-
18
-
export interface OutputSchema {
19
-
subject: AppRockskyActorDefs.ProfileViewBasic;
20
-
followers: AppRockskyActorDefs.ProfileViewBasic[];
21
-
/** A cursor value to pass to subsequent calls to get the next page of results. */
22
-
cursor?: string;
23
-
[k: string]: unknown;
24
-
}
25
-
26
-
export type HandlerInput = undefined;
27
-
28
-
export interface HandlerSuccess {
29
-
encoding: "application/json";
30
-
body: OutputSchema;
31
-
headers?: { [key: string]: string };
32
-
}
33
-
34
-
export interface HandlerError {
35
-
status: number;
36
-
message?: string;
37
-
}
38
-
39
-
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
40
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
41
-
auth: HA;
42
-
params: QueryParams;
43
-
input: HandlerInput;
44
-
req: express.Request;
45
-
res: express.Response;
46
-
resetRouteRateLimits: () => Promise<void>;
47
-
};
48
-
export type Handler<HA extends HandlerAuth = never> = (
49
-
ctx: HandlerReqCtx<HA>,
50
-
) => Promise<HandlerOutput> | HandlerOutput;
-56
apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts
-56
apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import type express from "express";
5
-
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
-
import { lexicons } from "../../../../lexicons";
7
-
import { isObj, hasProp } from "../../../../util";
8
-
import { CID } from "multiformats/cid";
9
-
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
-
import type * as AppRockskyActorDefs from "../actor/defs";
11
-
12
-
export interface QueryParams {
13
-
actor: string;
14
-
limit: number;
15
-
/** If provided, filters the followers to only include those with DIDs in this list. */
16
-
dids?: string[];
17
-
cursor?: string;
18
-
}
19
-
20
-
export type InputSchema = undefined;
21
-
22
-
export interface OutputSchema {
23
-
subject: AppRockskyActorDefs.ProfileViewBasic;
24
-
followers: AppRockskyActorDefs.ProfileViewBasic[];
25
-
/** A cursor value to pass to subsequent calls to get the next page of results. */
26
-
cursor?: string;
27
-
/** The total number of followers. */
28
-
count?: number;
29
-
[k: string]: unknown;
30
-
}
31
-
32
-
export type HandlerInput = undefined;
33
-
34
-
export interface HandlerSuccess {
35
-
encoding: "application/json";
36
-
body: OutputSchema;
37
-
headers?: { [key: string]: string };
38
-
}
39
-
40
-
export interface HandlerError {
41
-
status: number;
42
-
message?: string;
43
-
}
44
-
45
-
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
46
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
47
-
auth: HA;
48
-
params: QueryParams;
49
-
input: HandlerInput;
50
-
req: express.Request;
51
-
res: express.Response;
52
-
resetRouteRateLimits: () => Promise<void>;
53
-
};
54
-
export type Handler<HA extends HandlerAuth = never> = (
55
-
ctx: HandlerReqCtx<HA>,
56
-
) => Promise<HandlerOutput> | HandlerOutput;
-56
apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts
-56
apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import type express from "express";
5
-
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
-
import { lexicons } from "../../../../lexicons";
7
-
import { isObj, hasProp } from "../../../../util";
8
-
import { CID } from "multiformats/cid";
9
-
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
-
import type * as AppRockskyActorDefs from "../actor/defs";
11
-
12
-
export interface QueryParams {
13
-
actor: string;
14
-
limit: number;
15
-
/** If provided, filters the follows to only include those with DIDs in this list. */
16
-
dids?: string[];
17
-
cursor?: string;
18
-
}
19
-
20
-
export type InputSchema = undefined;
21
-
22
-
export interface OutputSchema {
23
-
subject: AppRockskyActorDefs.ProfileViewBasic;
24
-
follows: AppRockskyActorDefs.ProfileViewBasic[];
25
-
/** A cursor value to pass to subsequent calls to get the next page of results. */
26
-
cursor?: string;
27
-
/** The total number of follows. */
28
-
count?: number;
29
-
[k: string]: unknown;
30
-
}
31
-
32
-
export type HandlerInput = undefined;
33
-
34
-
export interface HandlerSuccess {
35
-
encoding: "application/json";
36
-
body: OutputSchema;
37
-
headers?: { [key: string]: string };
38
-
}
39
-
40
-
export interface HandlerError {
41
-
status: number;
42
-
message?: string;
43
-
}
44
-
45
-
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
46
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
47
-
auth: HA;
48
-
params: QueryParams;
49
-
input: HandlerInput;
50
-
req: express.Request;
51
-
res: express.Response;
52
-
resetRouteRateLimits: () => Promise<void>;
53
-
};
54
-
export type Handler<HA extends HandlerAuth = never> = (
55
-
ctx: HandlerReqCtx<HA>,
56
-
) => Promise<HandlerOutput> | HandlerOutput;
-52
apps/api/src/lexicon/types/app/rocksky/graph/getKnownFollowers.ts
-52
apps/api/src/lexicon/types/app/rocksky/graph/getKnownFollowers.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import type express from "express";
5
-
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
-
import { lexicons } from "../../../../lexicons";
7
-
import { isObj, hasProp } from "../../../../util";
8
-
import { CID } from "multiformats/cid";
9
-
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
-
import type * as AppRockskyActorDefs from "../actor/defs";
11
-
12
-
export interface QueryParams {
13
-
actor: string;
14
-
limit: number;
15
-
cursor?: string;
16
-
}
17
-
18
-
export type InputSchema = undefined;
19
-
20
-
export interface OutputSchema {
21
-
subject: AppRockskyActorDefs.ProfileViewBasic;
22
-
followers: AppRockskyActorDefs.ProfileViewBasic[];
23
-
/** A cursor value to pass to subsequent calls to get the next page of results. */
24
-
cursor?: string;
25
-
[k: string]: unknown;
26
-
}
27
-
28
-
export type HandlerInput = undefined;
29
-
30
-
export interface HandlerSuccess {
31
-
encoding: "application/json";
32
-
body: OutputSchema;
33
-
headers?: { [key: string]: string };
34
-
}
35
-
36
-
export interface HandlerError {
37
-
status: number;
38
-
message?: string;
39
-
}
40
-
41
-
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
42
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
43
-
auth: HA;
44
-
params: QueryParams;
45
-
input: HandlerInput;
46
-
req: express.Request;
47
-
res: express.Response;
48
-
resetRouteRateLimits: () => Promise<void>;
49
-
};
50
-
export type Handler<HA extends HandlerAuth = never> = (
51
-
ctx: HandlerReqCtx<HA>,
52
-
) => Promise<HandlerOutput> | HandlerOutput;
-50
apps/api/src/lexicon/types/app/rocksky/graph/unfollowAccount.ts
-50
apps/api/src/lexicon/types/app/rocksky/graph/unfollowAccount.ts
···
1
-
/**
2
-
* GENERATED CODE - DO NOT MODIFY
3
-
*/
4
-
import type express from "express";
5
-
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
-
import { lexicons } from "../../../../lexicons";
7
-
import { isObj, hasProp } from "../../../../util";
8
-
import { CID } from "multiformats/cid";
9
-
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
-
import type * as AppRockskyActorDefs from "../actor/defs";
11
-
12
-
export interface QueryParams {
13
-
account: string;
14
-
}
15
-
16
-
export type InputSchema = undefined;
17
-
18
-
export interface OutputSchema {
19
-
subject: AppRockskyActorDefs.ProfileViewBasic;
20
-
followers: AppRockskyActorDefs.ProfileViewBasic[];
21
-
/** A cursor value to pass to subsequent calls to get the next page of results. */
22
-
cursor?: string;
23
-
[k: string]: unknown;
24
-
}
25
-
26
-
export type HandlerInput = undefined;
27
-
28
-
export interface HandlerSuccess {
29
-
encoding: "application/json";
30
-
body: OutputSchema;
31
-
headers?: { [key: string]: string };
32
-
}
33
-
34
-
export interface HandlerError {
35
-
status: number;
36
-
message?: string;
37
-
}
38
-
39
-
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
40
-
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
41
-
auth: HA;
42
-
params: QueryParams;
43
-
input: HandlerInput;
44
-
req: express.Request;
45
-
res: express.Response;
46
-
resetRouteRateLimits: () => Promise<void>;
47
-
};
48
-
export type Handler<HA extends HandlerAuth = never> = (
49
-
ctx: HandlerReqCtx<HA>,
50
-
) => Promise<HandlerOutput> | HandlerOutput;
-2
apps/api/src/lexicon/types/app/rocksky/scrobble/getScrobbles.ts
-2
apps/api/src/lexicon/types/app/rocksky/scrobble/getScrobbles.ts
···
12
12
export interface QueryParams {
13
13
/** The DID or handle of the actor */
14
14
did?: string;
15
-
/** If true, only return scrobbles from actors the viewer is following. */
16
-
following?: boolean;
17
15
/** The maximum number of scrobbles to return */
18
16
limit?: number;
19
17
/** The offset for pagination */
+2
-33
apps/api/src/lib/agent.ts
+2
-33
apps/api/src/lib/agent.ts
···
1
-
import { Agent, AtpAgent } from "@atproto/api";
1
+
import { Agent } from "@atproto/api";
2
2
import type { NodeOAuthClient } from "@atproto/oauth-client-node";
3
-
import extractPdsFromDid from "./extractPdsFromDid";
4
-
import { ctx } from "context";
5
3
6
4
export async function createAgent(
7
5
oauthClient: NodeOAuthClient,
8
6
did: string,
9
7
): Promise<Agent | null> {
10
-
let agent: Agent | null = null;
8
+
let agent = null;
11
9
let retry = 0;
12
10
do {
13
11
try {
14
-
const result = await ctx.sqliteDb
15
-
.selectFrom("auth_session")
16
-
.selectAll()
17
-
.where("key", "=", `atp:${did}`)
18
-
.executeTakeFirst();
19
-
if (result) {
20
-
let pds = await ctx.redis.get(`pds:${did}`);
21
-
if (!pds) {
22
-
pds = await extractPdsFromDid(did);
23
-
await ctx.redis.setEx(`pds:${did}`, 60 * 15, pds);
24
-
}
25
-
const atpAgent = new AtpAgent({
26
-
service: new URL(pds),
27
-
});
28
-
29
-
try {
30
-
await atpAgent.resumeSession(JSON.parse(result.session));
31
-
} catch (e) {
32
-
console.log("Error resuming session");
33
-
console.log(did);
34
-
console.log(e);
35
-
await ctx.sqliteDb
36
-
.deleteFrom("auth_session")
37
-
.where("key", "=", `atp:${did}`)
38
-
.execute();
39
-
}
40
-
41
-
return atpAgent;
42
-
}
43
12
const oauthSession = await oauthClient.restore(did);
44
13
agent = oauthSession ? new Agent(oauthSession) : null;
45
14
if (agent === null) {
-1
apps/api/src/lib/env.ts
-1
apps/api/src/lib/env.ts
-33
apps/api/src/lib/extractPdsFromDid.ts
-33
apps/api/src/lib/extractPdsFromDid.ts
···
1
-
export default async function extractPdsFromDid(
2
-
did: string,
3
-
): Promise<string | null> {
4
-
let didDocUrl: string;
5
-
6
-
if (did.startsWith("did:plc:")) {
7
-
didDocUrl = `https://plc.directory/${did}`;
8
-
} else if (did.startsWith("did:web:")) {
9
-
const domain = did.substring("did:web:".length);
10
-
didDocUrl = `https://${domain}/.well-known/did.json`;
11
-
} else {
12
-
throw new Error("Unsupported DID method");
13
-
}
14
-
15
-
const response = await fetch(didDocUrl);
16
-
if (!response.ok) throw new Error("Failed to fetch DID doc");
17
-
18
-
const doc: {
19
-
service?: Array<{
20
-
type: string;
21
-
id: string;
22
-
serviceEndpoint: string;
23
-
}>;
24
-
} = await response.json();
25
-
26
-
// Find the atproto PDS service
27
-
const pdsService = doc.service?.find(
28
-
(s: any) =>
29
-
s.type === "AtprotoPersonalDataServer" && s.id.endsWith("#atproto_pds"),
30
-
);
31
-
32
-
return pdsService?.serviceEndpoint ?? null;
33
-
}
+4
-32
apps/api/src/lovedtracks/lovedtracks.service.ts
+4
-32
apps/api/src/lovedtracks/lovedtracks.service.ts
···
1
-
import { AtpAgent, type Agent } from "@atproto/api";
1
+
import type { Agent } from "@atproto/api";
2
2
import { TID } from "@atproto/common";
3
3
import type { Context } from "context";
4
4
import { and, desc, eq, type SQLWrapper } from "drizzle-orm";
···
248
248
249
249
if (trackWithUri?.uri) {
250
250
const rkey = TID.nextStr();
251
-
const subjectAgent = new AtpAgent({
252
-
service: new URL("https://bsky.social"),
253
-
});
254
-
const subjectRecord = await subjectAgent.com.atproto.repo.getRecord({
251
+
const subjectRecord = await agent.com.atproto.repo.getRecord({
255
252
repo: trackWithUri.uri.split("/").slice(0, 3).join("/").split("at://")[1],
256
253
collection: "app.rocksky.song",
257
254
rkey: trackWithUri.uri.split("/").pop(),
···
297
294
}
298
295
}
299
296
300
-
const lovedTrack = await ctx.db
301
-
.select()
302
-
.from(lovedTracks)
303
-
.where(
304
-
and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, trackId)),
305
-
)
306
-
.limit(1)
307
-
.then((rows) => rows[0]);
308
-
309
-
const message = JSON.stringify({
310
-
uri: lovedTrack.uri,
311
-
user_id: { xata_id: user.id },
312
-
track_id: { xata_id: trackId },
313
-
xata_createdat: lovedTrack.createdAt.toISOString(),
314
-
xata_id: lovedTrack.id,
315
-
xata_updatedat: lovedTrack.createdAt.toISOString(),
316
-
xata_version: 0,
317
-
});
297
+
const message = JSON.stringify(created);
318
298
ctx.nc.publish("rocksky.like", Buffer.from(message));
319
299
320
300
return created;
···
363
343
ctx.db.delete(lovedTracks).where(eq(lovedTracks.id, lovedTrack.id)),
364
344
]);
365
345
366
-
const message = JSON.stringify({
367
-
uri: lovedTrack.uri,
368
-
user_id: { xata_id: user.id },
369
-
track_id: { xata_id: track.id },
370
-
xata_createdat: lovedTrack.createdAt.toISOString(),
371
-
xata_id: lovedTrack.id,
372
-
xata_updatedat: lovedTrack.createdAt.toISOString(),
373
-
xata_version: 0,
374
-
});
346
+
const message = JSON.stringify(lovedTrack);
375
347
ctx.nc.publish("rocksky.unlike", Buffer.from(message));
376
348
}
377
349
-33
apps/api/src/schema/follows.ts
-33
apps/api/src/schema/follows.ts
···
1
-
import type { InferInsertModel, InferSelectModel } from "drizzle-orm";
2
-
import { sql } from "drizzle-orm";
3
-
import {
4
-
integer,
5
-
pgTable,
6
-
text,
7
-
timestamp,
8
-
uniqueIndex,
9
-
} from "drizzle-orm/pg-core";
10
-
11
-
const follows = pgTable(
12
-
"follows",
13
-
{
14
-
id: text("xata_id").primaryKey().default(sql`xata_id()`),
15
-
uri: text("uri").notNull().unique(),
16
-
follower_did: text("follower_did").notNull(),
17
-
subject_did: text("subject_did").notNull(),
18
-
xataVersion: integer("xata_version"),
19
-
createdAt: timestamp("xata_createdat").defaultNow().notNull(),
20
-
updatedAt: timestamp("xata_updatedat").defaultNow().notNull(),
21
-
},
22
-
(t) => [
23
-
uniqueIndex("follows_follower_subject_unique").on(
24
-
t.follower_did,
25
-
t.subject_did,
26
-
),
27
-
],
28
-
);
29
-
30
-
export type SelectFollows = InferSelectModel<typeof follows>;
31
-
export type InsertFollows = InferInsertModel<typeof follows>;
32
-
33
-
export default follows;
-2
apps/api/src/schema/index.ts
-2
apps/api/src/schema/index.ts
···
10
10
import dropboxPaths from "./dropbox-paths";
11
11
import dropboxTokens from "./dropbox-tokens";
12
12
import feeds from "./feeds";
13
-
import follows from "./follows";
14
13
import googleDriveAccounts from "./google-drive-accounts";
15
14
import googleDriveDirectories from "./google-drive-directories";
16
15
import googleDrivePaths from "./google-drive-paths";
···
71
70
googleDrive,
72
71
queueTracks,
73
72
feeds,
74
-
follows,
75
73
};
+4
-1
apps/api/src/scripts/avatar.ts
+4
-1
apps/api/src/scripts/avatar.ts
-25
apps/api/src/scripts/likes.ts
-25
apps/api/src/scripts/likes.ts
···
1
-
import { ctx } from "context";
2
-
import lovedTracks from "../schema/loved-tracks";
3
-
import chalk from "chalk";
4
-
5
-
const likes = await ctx.db.select().from(lovedTracks).execute();
6
-
7
-
for (const like of likes) {
8
-
const message = JSON.stringify({
9
-
uri: like.uri,
10
-
user_id: { xata_id: like.userId },
11
-
track_id: { xata_id: like.trackId },
12
-
xata_createdat: like.createdAt.toISOString(),
13
-
xata_id: like.id,
14
-
xata_updatedat: like.createdAt.toISOString(),
15
-
xata_version: 0,
16
-
});
17
-
console.log("Publishing like:", chalk.cyanBright(like.uri));
18
-
ctx.nc.publish("rocksky.like", Buffer.from(message));
19
-
}
20
-
21
-
await ctx.nc.flush();
22
-
23
-
console.log("Done");
24
-
25
-
process.exit(0);
-6
apps/api/src/tealfm/index.ts
-6
apps/api/src/tealfm/index.ts
···
4
4
import type * as Status from "lexicon/types/fm/teal/alpha/actor/status";
5
5
import type { PlayView } from "lexicon/types/fm/teal/alpha/feed/defs";
6
6
import * as Play from "lexicon/types/fm/teal/alpha/feed/play";
7
-
import { env } from "lib/env";
8
7
import type { MusicbrainzTrack } from "types/track";
9
8
10
9
const SUBMISSION_CLIENT_AGENT = "rocksky/v0.0.1";
···
23
22
track: MusicbrainzTrack,
24
23
duration: number,
25
24
) {
26
-
if (env.DISABLED_TEALFM.includes(agent.assertDid)) {
27
-
console.log(`teal.fm is disabled for ${chalk.cyanBright(agent.assertDid)}`);
28
-
return;
29
-
}
30
-
31
25
try {
32
26
// wait 60 seconds to ensure the track is actually being played
33
27
await new Promise((resolve) => setTimeout(resolve, 60000));
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorAlbums.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorAlbums.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorArtists.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorArtists.ts
-108
apps/api/src/xrpc/app/rocksky/actor/getActorCompatibility.ts
-108
apps/api/src/xrpc/app/rocksky/actor/getActorCompatibility.ts
···
1
-
import type { Context } from "context";
2
-
import { Effect, pipe } from "effect";
3
-
import type { Server } from "lexicon";
4
-
import type { QueryParams } from "lexicon/types/app/rocksky/actor/getActorCompatibility";
5
-
import type { CompatibilityViewBasic } from "lexicon/types/app/rocksky/actor/defs";
6
-
import { deepCamelCaseKeys } from "lib";
7
-
import users from "schema/users";
8
-
import { eq, or } from "drizzle-orm";
9
-
import { HandlerAuth } from "@atproto/xrpc-server";
10
-
11
-
export default function (server: Server, ctx: Context) {
12
-
const getActorCompatibility = (params: QueryParams, auth: HandlerAuth) =>
13
-
pipe(
14
-
{ params, ctx, did: auth.credentials?.did },
15
-
retrieve,
16
-
Effect.flatMap(presentation),
17
-
Effect.retry({ times: 3 }),
18
-
Effect.timeout("120 seconds"),
19
-
Effect.catchAll((err) => {
20
-
console.error(err);
21
-
return Effect.succeed({ comptibility: null });
22
-
}),
23
-
);
24
-
server.app.rocksky.actor.getActorCompatibility({
25
-
auth: ctx.authVerifier,
26
-
handler: async ({ params, auth }) => {
27
-
const result = await Effect.runPromise(
28
-
getActorCompatibility(params, auth),
29
-
);
30
-
return {
31
-
encoding: "application/json",
32
-
body: result,
33
-
};
34
-
},
35
-
});
36
-
}
37
-
38
-
const retrieve = ({
39
-
params,
40
-
ctx,
41
-
did,
42
-
}: {
43
-
params: QueryParams;
44
-
ctx: Context;
45
-
did: string | undefined;
46
-
}): Effect.Effect<{ data: Compatibility[] }, Error> => {
47
-
return Effect.tryPromise({
48
-
try: async () => {
49
-
if (!did) {
50
-
throw new Error(`User not authenticated`);
51
-
}
52
-
53
-
const user1 = await ctx.db
54
-
.select()
55
-
.from(users)
56
-
.where(eq(users.did, did))
57
-
.execute()
58
-
.then((rows) => rows[0]);
59
-
60
-
if (!user1) {
61
-
throw new Error(`User1 not found`);
62
-
}
63
-
64
-
const user2 = await ctx.db
65
-
.select()
66
-
.from(users)
67
-
.where(or(eq(users.did, params.did), eq(users.handle, params.did)))
68
-
.execute()
69
-
.then((rows) => rows[0]);
70
-
71
-
if (!user2) {
72
-
throw new Error(`User2 not found`);
73
-
}
74
-
75
-
return ctx.analytics.post("library.getCompatibility", {
76
-
user_id1: user1.id,
77
-
user_id2: user2.id,
78
-
});
79
-
},
80
-
catch: (error) => new Error(`Failed to retrieve compatibility: ${error}`),
81
-
});
82
-
};
83
-
84
-
const presentation = ({
85
-
data,
86
-
}: {
87
-
data: Compatibility[];
88
-
}): Effect.Effect<{ compatibility: CompatibilityViewBasic }, never> => {
89
-
return Effect.sync(() => ({ compatibility: deepCamelCaseKeys(data) }));
90
-
};
91
-
92
-
type Compatibility = {
93
-
compatibility_level: number;
94
-
compatibility_percentage: number;
95
-
shared_artists: number;
96
-
top_shared_artists: string[];
97
-
top_shared_detailed_artists: {
98
-
id: string;
99
-
name: string;
100
-
picture: string;
101
-
uri: string;
102
-
user1_rank: number;
103
-
user2_rank: number;
104
-
weight: number;
105
-
}[];
106
-
user1_artist_count: number;
107
-
user2_artist_count: number;
108
-
};
+5
-8
apps/api/src/xrpc/app/rocksky/actor/getActorLovedSongs.ts
+5
-8
apps/api/src/xrpc/app/rocksky/actor/getActorLovedSongs.ts
···
1
1
import type { Context } from "context";
2
-
import { and, desc, eq, not, or } from "drizzle-orm";
2
+
import { desc, eq, or } from "drizzle-orm";
3
3
import { Effect, pipe } from "effect";
4
4
import type { Server } from "lexicon";
5
5
import type { QueryParams } from "lexicon/types/app/rocksky/actor/getActorLovedSongs";
···
8
8
import type { SelectTrack } from "schema/tracks";
9
9
10
10
export default function (server: Server, ctx: Context) {
11
-
const getActorLovedSongs = (params: QueryParams) =>
11
+
const getActorLovedSongs = (params) =>
12
12
pipe(
13
13
{ params, ctx },
14
14
retrieve,
···
49
49
)
50
50
.leftJoin(tables.users, eq(tables.lovedTracks.userId, tables.users.id))
51
51
.where(
52
-
and(
53
-
or(
54
-
eq(tables.users.did, params.did),
55
-
eq(tables.users.handle, params.did),
56
-
),
57
-
not(eq(tables.lovedTracks.uri, null)),
52
+
or(
53
+
eq(tables.users.did, params.did),
54
+
eq(tables.users.handle, params.did),
58
55
),
59
56
)
60
57
.limit(params.limit ?? 10)
-86
apps/api/src/xrpc/app/rocksky/actor/getActorNeighbours.ts
-86
apps/api/src/xrpc/app/rocksky/actor/getActorNeighbours.ts
···
1
-
import type { Context } from "context";
2
-
import { Effect, pipe } from "effect";
3
-
import type { Server } from "lexicon";
4
-
import type { QueryParams } from "lexicon/types/app/rocksky/actor/getActorNeighbours";
5
-
import type { NeighbourViewBasic } from "lexicon/types/app/rocksky/actor/defs";
6
-
import { deepCamelCaseKeys } from "lib";
7
-
import users from "schema/users";
8
-
import { eq, or } from "drizzle-orm";
9
-
10
-
export default function (server: Server, ctx: Context) {
11
-
const getActorNeighbours = (params: QueryParams) =>
12
-
pipe(
13
-
{ params, ctx },
14
-
retrieve,
15
-
Effect.flatMap(presentation),
16
-
Effect.retry({ times: 3 }),
17
-
Effect.timeout("120 seconds"),
18
-
Effect.catchAll((err) => {
19
-
console.error(err);
20
-
return Effect.succeed({ neighbours: [] });
21
-
}),
22
-
);
23
-
server.app.rocksky.actor.getActorNeighbours({
24
-
handler: async ({ params }) => {
25
-
const result = await Effect.runPromise(getActorNeighbours(params));
26
-
return {
27
-
encoding: "application/json",
28
-
body: result,
29
-
};
30
-
},
31
-
});
32
-
}
33
-
34
-
const retrieve = ({
35
-
params,
36
-
ctx,
37
-
}: {
38
-
params: QueryParams;
39
-
ctx: Context;
40
-
}): Effect.Effect<{ data: Neighbour[] }, Error> => {
41
-
return Effect.tryPromise({
42
-
try: async () => {
43
-
const user = await ctx.db
44
-
.select()
45
-
.from(users)
46
-
.where(or(eq(users.did, params.did), eq(users.handle, params.did)))
47
-
.execute()
48
-
.then((rows) => rows[0]);
49
-
50
-
if (!user) {
51
-
throw new Error(`User not found`);
52
-
}
53
-
54
-
return ctx.analytics.post("library.getNeighbours", {
55
-
user_id: user.id,
56
-
});
57
-
},
58
-
catch: (error) => new Error(`Failed to retrieve neighbours: ${error}`),
59
-
});
60
-
};
61
-
62
-
const presentation = ({
63
-
data,
64
-
}: {
65
-
data: Neighbour[];
66
-
}): Effect.Effect<{ neighbours: NeighbourViewBasic[] }, never> => {
67
-
return Effect.sync(() => ({ neighbours: deepCamelCaseKeys(data) }));
68
-
};
69
-
70
-
type Neighbour = {
71
-
id: string;
72
-
avatar: string;
73
-
did: string;
74
-
displayName: string;
75
-
handle: string;
76
-
sharedArtistsCount: number;
77
-
similarityScore: number;
78
-
topSharedArtistNames: string[];
79
-
topSharedArtistsDetails: {
80
-
id: string;
81
-
name: string;
82
-
picture: string;
83
-
uri: string;
84
-
}[];
85
-
userId: string;
86
-
};
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorPlaylists.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorPlaylists.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorScrobbles.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorScrobbles.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorSongs.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getActorSongs.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getProfile.ts
+1
-1
apps/api/src/xrpc/app/rocksky/actor/getProfile.ts
···
18
18
import type { SelectUser } from "schema/users";
19
19
20
20
export default function (server: Server, ctx: Context) {
21
-
const getActorProfile = (params: QueryParams, auth: HandlerAuth) =>
21
+
const getActorProfile = (params, auth: HandlerAuth) =>
22
22
pipe(
23
23
{ params, ctx, did: auth.credentials?.did },
24
24
resolveHandleToDid,
+23
-61
apps/api/src/xrpc/app/rocksky/feed/getFeed.ts
+23
-61
apps/api/src/xrpc/app/rocksky/feed/getFeed.ts
···
10
10
import type { SelectTrack } from "schema/tracks";
11
11
import type { SelectUser } from "schema/users";
12
12
import axios from "axios";
13
-
import type { HandlerAuth } from "@atproto/xrpc-server";
13
+
import { HandlerAuth } from "@atproto/xrpc-server";
14
14
import { env } from "lib/env";
15
15
16
16
export default function (server: Server, ctx: Context) {
···
62
62
? "http://localhost:8002"
63
63
: `https://${feed.did.split("did:web:")[1]}`;
64
64
const response = await axios.get<{
65
-
cursor?: string;
65
+
cusrsor: string;
66
66
feed: { scrobble: string }[];
67
67
}>(`${feedUrl}/xrpc/app.rocksky.feed.getFeedSkeleton`, {
68
68
params: {
···
73
73
});
74
74
return {
75
75
uris: response.data.feed.map(({ scrobble }) => scrobble),
76
-
cursor: response.data.cursor,
77
76
ctx,
78
77
did,
79
78
};
···
84
83
85
84
const hydrate = ({
86
85
uris,
87
-
cursor,
88
86
ctx,
89
87
did,
90
88
}: {
91
89
uris: string[];
92
-
cursor?: string;
93
90
ctx: Context;
94
91
did?: string;
95
-
}): Effect.Effect<ScrobblesWithCursor | undefined, Error> => {
92
+
}): Effect.Effect<Scrobbles | undefined, Error> => {
96
93
return Effect.tryPromise({
97
94
try: async () => {
98
95
const scrobbles = await ctx.db
···
131
128
liked: likesMap.get(row.tracks?.id)?.liked ?? false,
132
129
}));
133
130
134
-
if (did) {
135
-
const [u] = await ctx.db
136
-
.select()
137
-
.from(tables.users)
138
-
.where(eq(tables.users.did, did))
139
-
.limit(1)
140
-
.execute();
141
-
142
-
const userPayload = {
143
-
xata_id: u.id,
144
-
did: u.did,
145
-
handle: u.handle,
146
-
display_name: u.displayName,
147
-
avatar: u.avatar,
148
-
xata_createdat: u.createdAt.toISOString(),
149
-
xata_updatedat: u.updatedAt.toISOString(),
150
-
xata_version: u.xataVersion,
151
-
};
152
-
153
-
ctx.nc.publish(
154
-
"rocksky.user",
155
-
Buffer.from(JSON.stringify(userPayload)),
156
-
);
157
-
}
158
-
159
-
return { scrobbles: result, cursor };
131
+
return result;
160
132
},
161
133
162
134
catch: (error) => new Error(`Failed to hydrate feed: ${error}`),
163
135
});
164
136
};
165
137
166
-
const presentation = (
167
-
data: ScrobblesWithCursor,
168
-
): Effect.Effect<FeedView, never> => {
138
+
const presentation = (data: Scrobbles): Effect.Effect<FeedView, never> => {
169
139
return Effect.sync(() => ({
170
-
feed: data.scrobbles.map(
171
-
({ scrobbles, tracks, users, likesCount, liked }) => ({
172
-
scrobble: {
173
-
...R.omit(["albumArt", "id", "lyrics"])(tracks),
174
-
cover: tracks.albumArt,
175
-
date: scrobbles.timestamp.toISOString(),
176
-
user: users.handle,
177
-
userDisplayName: users.displayName,
178
-
userAvatar: users.avatar,
179
-
uri: scrobbles.uri,
180
-
tags: [],
181
-
likesCount,
182
-
liked,
183
-
trackUri: tracks.uri,
184
-
createdAt: scrobbles.createdAt.toISOString(),
185
-
updatedAt: scrobbles.updatedAt.toISOString(),
186
-
id: scrobbles.id,
187
-
},
188
-
}),
189
-
),
190
-
cursor: data.cursor,
140
+
feed: data.map(({ scrobbles, tracks, users, likesCount, liked }) => ({
141
+
scrobble: {
142
+
...R.omit(["albumArt", "id", "lyrics"])(tracks),
143
+
cover: tracks.albumArt,
144
+
date: scrobbles.timestamp.toISOString(),
145
+
user: users.handle,
146
+
userDisplayName: users.displayName,
147
+
userAvatar: users.avatar,
148
+
uri: scrobbles.uri,
149
+
tags: [],
150
+
likesCount,
151
+
liked,
152
+
trackUri: tracks.uri,
153
+
createdAt: scrobbles.createdAt.toISOString(),
154
+
updatedAt: scrobbles.updatedAt.toISOString(),
155
+
id: scrobbles.id,
156
+
},
157
+
})),
191
158
}));
192
159
};
193
160
···
198
165
likesCount: number;
199
166
liked: boolean;
200
167
}[];
201
-
202
-
type ScrobblesWithCursor = {
203
-
scrobbles: Scrobbles;
204
-
cursor?: string;
205
-
};
-173
apps/api/src/xrpc/app/rocksky/graph/followAccount.ts
-173
apps/api/src/xrpc/app/rocksky/graph/followAccount.ts
···
1
-
import { TID } from "@atproto/common";
2
-
import type { HandlerAuth } from "@atproto/xrpc-server";
3
-
import type { Context } from "context";
4
-
import { and, eq, desc } from "drizzle-orm";
5
-
import { Effect, pipe } from "effect";
6
-
import type { Server } from "lexicon";
7
-
import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs";
8
-
import type { QueryParams } from "lexicon/types/app/rocksky/graph/followAccount";
9
-
import { createAgent } from "lib/agent";
10
-
import tables from "schema";
11
-
import type { SelectUser } from "schema/users";
12
-
import * as FollowLexicon from "lexicon/types/app/rocksky/graph/follow";
13
-
14
-
export default function (server: Server, ctx: Context) {
15
-
const followAccount = (params: QueryParams, auth: HandlerAuth) =>
16
-
pipe(
17
-
{ params, ctx, did: auth.credentials?.did },
18
-
handleFollow,
19
-
Effect.flatMap(presentation),
20
-
Effect.retry({ times: 3 }),
21
-
Effect.timeout("120 seconds"),
22
-
Effect.catchAll((err) => {
23
-
console.error(err);
24
-
return Effect.succeed({
25
-
subject: {} satisfies ProfileViewBasic,
26
-
followers: [],
27
-
});
28
-
}),
29
-
);
30
-
server.app.rocksky.graph.followAccount({
31
-
auth: ctx.authVerifier,
32
-
handler: async ({ params, auth }) => {
33
-
const result = await Effect.runPromise(followAccount(params, auth));
34
-
return {
35
-
encoding: "application/json",
36
-
body: result,
37
-
};
38
-
},
39
-
});
40
-
}
41
-
42
-
const handleFollow = ({
43
-
params,
44
-
ctx,
45
-
did,
46
-
}: {
47
-
params: QueryParams;
48
-
ctx: Context;
49
-
did?: string;
50
-
}): Effect.Effect<[SelectUser | undefined, SelectUser[]], Error> => {
51
-
return Effect.tryPromise({
52
-
try: async () => {
53
-
if (!did) {
54
-
throw new Error("User is not authenticated");
55
-
}
56
-
if (params.account === did) {
57
-
throw new Error("User cannot follow themselves");
58
-
}
59
-
60
-
if (await isFollowing(ctx, did, params.account)) {
61
-
throw new Error("User is already following");
62
-
}
63
-
64
-
const agent = await createAgent(ctx.oauthClient, did);
65
-
if (!agent) {
66
-
throw new Error("Unauthorized");
67
-
}
68
-
69
-
const rkey = TID.nextStr();
70
-
71
-
const record = {
72
-
$type: "app.rocksky.graph.follow",
73
-
subject: params.account,
74
-
createdAt: new Date().toISOString(),
75
-
};
76
-
77
-
if (!FollowLexicon.validateRecord(record).success) {
78
-
console.log(FollowLexicon.validateRecord(record));
79
-
throw new Error("Invalid record");
80
-
}
81
-
82
-
const res = await agent.com.atproto.repo.createRecord({
83
-
repo: agent.assertDid,
84
-
collection: "app.rocksky.graph.follow",
85
-
rkey,
86
-
record,
87
-
validate: false,
88
-
});
89
-
const uri = res.data.uri;
90
-
console.log(`Follow record created at: ${uri}`);
91
-
92
-
await ctx.db
93
-
.insert(tables.follows)
94
-
.values({
95
-
uri,
96
-
subject_did: params.account,
97
-
follower_did: did,
98
-
})
99
-
.onConflictDoNothing()
100
-
.execute();
101
-
102
-
return Promise.all([
103
-
ctx.db
104
-
.select()
105
-
.from(tables.users)
106
-
.where(eq(tables.users.did, params.account))
107
-
.execute()
108
-
.then((rows) => rows[0]),
109
-
ctx.db
110
-
.select()
111
-
.from(tables.follows)
112
-
.where(eq(tables.follows.subject_did, params.account))
113
-
.leftJoin(
114
-
tables.users,
115
-
eq(tables.users.did, tables.follows.follower_did),
116
-
)
117
-
.orderBy(desc(tables.follows.createdAt))
118
-
.limit(50)
119
-
.execute()
120
-
.then((rows) => rows.map(({ users }) => users)),
121
-
]);
122
-
},
123
-
catch: (error) => new Error(`Failed to retrieve follow: ${error}`),
124
-
});
125
-
};
126
-
127
-
const presentation = ([user, followers]: [
128
-
SelectUser | undefined,
129
-
SelectUser[],
130
-
]): Effect.Effect<
131
-
{ subject: ProfileViewBasic; followers: ProfileViewBasic[] },
132
-
never
133
-
> => {
134
-
return Effect.sync(() => ({
135
-
subject: {
136
-
id: user?.id,
137
-
did: user?.did,
138
-
handle: user?.handle,
139
-
displayName: user?.displayName,
140
-
avatar: user?.avatar,
141
-
createdAt: user?.createdAt.toISOString(),
142
-
updatedAt: user?.updatedAt.toISOString(),
143
-
},
144
-
followers: followers.map((follower) => ({
145
-
id: follower.id,
146
-
did: follower.did,
147
-
handle: follower.handle,
148
-
displayName: follower.displayName,
149
-
avatar: follower.avatar,
150
-
createdAt: follower.createdAt.toISOString(),
151
-
updatedAt: follower.updatedAt.toISOString(),
152
-
})),
153
-
}));
154
-
};
155
-
156
-
const isFollowing = async (
157
-
ctx: Context,
158
-
followerDid: string,
159
-
subjectDid: string,
160
-
): Promise<boolean> => {
161
-
const result = await ctx.db
162
-
.select()
163
-
.from(tables.follows)
164
-
.where(
165
-
and(
166
-
eq(tables.follows.follower_did, followerDid),
167
-
eq(tables.follows.subject_did, subjectDid),
168
-
),
169
-
)
170
-
.execute();
171
-
172
-
return result.length > 0;
173
-
};
-179
apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts
-179
apps/api/src/xrpc/app/rocksky/graph/getFollowers.ts
···
1
-
import type { Context } from "context";
2
-
import { eq, desc, and, lt, inArray, count } from "drizzle-orm";
3
-
import { Effect, pipe } from "effect";
4
-
import type { Server } from "lexicon";
5
-
import type { QueryParams } from "lexicon/types/app/rocksky/graph/getFollowers";
6
-
import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs";
7
-
import tables from "schema";
8
-
import type { SelectUser } from "schema/users";
9
-
10
-
export default function (server: Server, ctx: Context) {
11
-
const getFollowers = (params: QueryParams) =>
12
-
pipe(
13
-
{ params, ctx },
14
-
retrieve,
15
-
Effect.flatMap(presentation),
16
-
Effect.retry({ times: 3 }),
17
-
Effect.timeout("120 seconds"),
18
-
Effect.catchAll((err) => {
19
-
console.error(err);
20
-
return Effect.succeed({
21
-
subject: {} satisfies ProfileViewBasic,
22
-
followers: [] as ProfileViewBasic[],
23
-
count: 0,
24
-
});
25
-
}),
26
-
);
27
-
server.app.rocksky.graph.getFollowers({
28
-
handler: async ({ params }) => {
29
-
const result = await Effect.runPromise(getFollowers(params));
30
-
return {
31
-
encoding: "application/json",
32
-
body: result,
33
-
};
34
-
},
35
-
});
36
-
}
37
-
38
-
const retrieve = ({
39
-
params,
40
-
ctx,
41
-
}: {
42
-
params: QueryParams;
43
-
ctx: Context;
44
-
}): Effect.Effect<
45
-
[SelectUser | undefined, SelectUser[], string | undefined, number],
46
-
Error
47
-
> => {
48
-
return Effect.tryPromise({
49
-
try: () =>
50
-
Promise.all([
51
-
ctx.db
52
-
.select()
53
-
.from(tables.users)
54
-
.where(eq(tables.users.did, params.actor))
55
-
.execute()
56
-
.then((rows) => rows[0]),
57
-
ctx.db
58
-
.select()
59
-
.from(tables.follows)
60
-
.where(
61
-
params.cursor
62
-
? and(
63
-
...[
64
-
lt(
65
-
tables.follows.createdAt,
66
-
new Date(Number(params.cursor)),
67
-
),
68
-
eq(tables.follows.subject_did, params.actor),
69
-
params.dids && params.dids?.length > 0
70
-
? inArray(tables.follows.follower_did, params.dids)
71
-
: undefined,
72
-
],
73
-
)
74
-
: and(
75
-
...[
76
-
eq(tables.follows.subject_did, params.actor),
77
-
params.dids && params.dids?.length > 0
78
-
? inArray(tables.follows.follower_did, params.dids)
79
-
: undefined,
80
-
],
81
-
),
82
-
)
83
-
.leftJoin(
84
-
tables.users,
85
-
eq(tables.users.did, tables.follows.follower_did),
86
-
)
87
-
.orderBy(desc(tables.follows.createdAt))
88
-
.limit(params.limit ?? 50)
89
-
.execute()
90
-
.then((rows) => rows.map(({ users }) => users)),
91
-
ctx.db
92
-
.select()
93
-
.from(tables.follows)
94
-
.where(
95
-
params.cursor
96
-
? and(
97
-
...[
98
-
lt(
99
-
tables.follows.createdAt,
100
-
new Date(Number(params.cursor)),
101
-
),
102
-
eq(tables.follows.subject_did, params.actor),
103
-
params.dids &&
104
-
params.dids?.length > 0 &&
105
-
inArray(tables.follows.follower_did, params.dids),
106
-
],
107
-
)
108
-
: and(
109
-
...[
110
-
eq(tables.follows.subject_did, params.actor),
111
-
params.dids && params.dids?.length > 0
112
-
? inArray(tables.follows.follower_did, params.dids)
113
-
: undefined,
114
-
],
115
-
),
116
-
)
117
-
.orderBy(desc(tables.follows.createdAt))
118
-
.limit(params.limit ?? 50)
119
-
.execute()
120
-
.then((rows) =>
121
-
rows.length > 0
122
-
? rows[rows.length - 1]?.createdAt.getTime().toString(10)
123
-
: undefined,
124
-
),
125
-
ctx.db
126
-
.select({ count: count() })
127
-
.from(tables.follows)
128
-
.where(
129
-
params.dids && params.dids?.length > 0
130
-
? and(
131
-
eq(tables.follows.subject_did, params.actor),
132
-
inArray(tables.follows.follower_did, params.dids),
133
-
)
134
-
: eq(tables.follows.subject_did, params.actor),
135
-
)
136
-
.execute()
137
-
.then((rows) => rows[0]?.count ?? 0),
138
-
]),
139
-
catch: (error) => new Error(`Failed to retrieve user followers: ${error}`),
140
-
});
141
-
};
142
-
143
-
const presentation = ([user, followers, cursor, totalCount]: [
144
-
SelectUser | undefined,
145
-
SelectUser[],
146
-
string | undefined,
147
-
number,
148
-
]): Effect.Effect<
149
-
{
150
-
subject: ProfileViewBasic;
151
-
followers: ProfileViewBasic[];
152
-
cursor?: string;
153
-
count: number;
154
-
},
155
-
never
156
-
> => {
157
-
return Effect.sync(() => ({
158
-
subject: {
159
-
id: user?.id,
160
-
did: user?.did,
161
-
handle: user?.handle,
162
-
displayName: user?.displayName,
163
-
avatar: user?.avatar,
164
-
createdAt: user?.createdAt.toISOString(),
165
-
updatedAt: user?.updatedAt.toISOString(),
166
-
},
167
-
followers: followers.map((follower) => ({
168
-
id: follower.id,
169
-
did: follower.did,
170
-
handle: follower.handle,
171
-
displayName: follower.displayName,
172
-
avatar: follower.avatar,
173
-
createdAt: follower.createdAt.toISOString(),
174
-
updatedAt: follower.updatedAt.toISOString(),
175
-
})),
176
-
cursor,
177
-
count: totalCount,
178
-
}));
179
-
};
-162
apps/api/src/xrpc/app/rocksky/graph/getFollows.ts
-162
apps/api/src/xrpc/app/rocksky/graph/getFollows.ts
···
1
-
import type { Context } from "context";
2
-
import { eq, desc, and, lt, inArray, count } from "drizzle-orm";
3
-
import { Effect, pipe } from "effect";
4
-
import type { Server } from "lexicon";
5
-
import type { QueryParams } from "lexicon/types/app/rocksky/graph/getFollowers";
6
-
import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs";
7
-
import tables from "schema";
8
-
import type { SelectUser } from "schema/users";
9
-
10
-
export default function (server: Server, ctx: Context) {
11
-
const getFollows = (params: QueryParams) =>
12
-
pipe(
13
-
{ params, ctx },
14
-
retrieve,
15
-
Effect.flatMap(([user, follows, cursor, totalCount]) =>
16
-
presentation(user, follows, cursor, totalCount),
17
-
),
18
-
Effect.retry({ times: 3 }),
19
-
Effect.timeout("120 seconds"),
20
-
Effect.catchAll((err) => {
21
-
console.error(err);
22
-
return Effect.succeed({
23
-
subject: undefined,
24
-
follows: [],
25
-
count: 0,
26
-
});
27
-
}),
28
-
);
29
-
server.app.rocksky.graph.getFollows({
30
-
handler: async ({ params }) => {
31
-
const result = await Effect.runPromise(getFollows(params));
32
-
return {
33
-
encoding: "application/json",
34
-
body: result,
35
-
};
36
-
},
37
-
});
38
-
}
39
-
40
-
const retrieve = ({
41
-
params,
42
-
ctx,
43
-
}: {
44
-
params: QueryParams;
45
-
ctx: Context;
46
-
}): Effect.Effect<
47
-
[SelectUser | undefined, SelectUser[], string | undefined, number],
48
-
Error
49
-
> => {
50
-
// Build where conditions dynamically
51
-
const buildWhereConditions = () => {
52
-
const conditions = [eq(tables.follows.follower_did, params.actor)];
53
-
54
-
if (params.cursor) {
55
-
conditions.push(
56
-
lt(tables.follows.createdAt, new Date(Number(params.cursor))),
57
-
);
58
-
}
59
-
60
-
if (params.dids && params.dids.length > 0) {
61
-
conditions.push(inArray(tables.follows.subject_did, params.dids));
62
-
}
63
-
64
-
return conditions.length > 1 ? and(...conditions) : conditions[0];
65
-
};
66
-
67
-
const whereConditions = buildWhereConditions();
68
-
69
-
// Build where conditions for count (without cursor)
70
-
const buildCountWhereConditions = () => {
71
-
const conditions = [eq(tables.follows.follower_did, params.actor)];
72
-
73
-
if (params.dids && params.dids.length > 0) {
74
-
conditions.push(inArray(tables.follows.subject_did, params.dids));
75
-
}
76
-
77
-
return conditions.length > 1 ? and(...conditions) : conditions[0];
78
-
};
79
-
80
-
const countWhereConditions = buildCountWhereConditions();
81
-
82
-
return Effect.tryPromise({
83
-
try: () =>
84
-
Promise.all([
85
-
ctx.db
86
-
.select()
87
-
.from(tables.users)
88
-
.where(eq(tables.users.did, params.actor))
89
-
.execute()
90
-
.then((rows) => rows[0]),
91
-
ctx.db
92
-
.select()
93
-
.from(tables.follows)
94
-
.where(whereConditions)
95
-
.leftJoin(
96
-
tables.users,
97
-
eq(tables.users.did, tables.follows.subject_did),
98
-
)
99
-
.orderBy(desc(tables.follows.createdAt))
100
-
.limit(params.limit ?? 50)
101
-
.execute()
102
-
.then((rows) => rows.map(({ users }) => users)),
103
-
ctx.db
104
-
.select()
105
-
.from(tables.follows)
106
-
.where(whereConditions)
107
-
.orderBy(desc(tables.follows.createdAt))
108
-
.limit(params.limit ?? 50)
109
-
.execute()
110
-
.then((rows) =>
111
-
rows?.length > 0
112
-
? rows[rows.length - 1]?.createdAt.getTime().toString(10)
113
-
: undefined,
114
-
),
115
-
ctx.db
116
-
.select({ count: count() })
117
-
.from(tables.follows)
118
-
.where(countWhereConditions)
119
-
.execute()
120
-
.then((rows) => rows[0]?.count ?? 0),
121
-
]),
122
-
catch: (error) => new Error(`Failed to retrieve user follows: ${error}`),
123
-
});
124
-
};
125
-
126
-
const presentation = (
127
-
user: SelectUser | undefined,
128
-
follows: SelectUser[],
129
-
cursor: string | undefined,
130
-
count: number,
131
-
): Effect.Effect<
132
-
{
133
-
subject: ProfileViewBasic;
134
-
follows: ProfileViewBasic[];
135
-
cursor?: string;
136
-
count: number;
137
-
},
138
-
never
139
-
> => {
140
-
return Effect.sync(() => ({
141
-
subject: {
142
-
id: user?.id,
143
-
did: user?.did,
144
-
handle: user?.handle,
145
-
displayName: user?.displayName,
146
-
avatar: user?.avatar,
147
-
createdAt: user?.createdAt.toISOString(),
148
-
updatedAt: user?.updatedAt.toISOString(),
149
-
},
150
-
follows: follows.map((follow) => ({
151
-
id: follow.id,
152
-
did: follow.did,
153
-
handle: follow.handle,
154
-
displayName: follow.displayName,
155
-
avatar: follow.avatar,
156
-
createdAt: follow.createdAt.toISOString(),
157
-
updatedAt: follow.updatedAt.toISOString(),
158
-
})),
159
-
cursor,
160
-
count,
161
-
}));
162
-
};
-135
apps/api/src/xrpc/app/rocksky/graph/getKnownFollowers.ts
-135
apps/api/src/xrpc/app/rocksky/graph/getKnownFollowers.ts
···
1
-
import type { Context } from "context";
2
-
import { and, eq, sql, desc, lt } from "drizzle-orm";
3
-
import { Effect, pipe } from "effect";
4
-
import type { Server } from "lexicon";
5
-
import type { QueryParams } from "lexicon/types/app/rocksky/graph/getKnownFollowers";
6
-
import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs";
7
-
import tables from "schema";
8
-
import type { HandlerAuth } from "@atproto/xrpc-server";
9
-
import type { SelectUser } from "schema/users";
10
-
11
-
export default function (server: Server, ctx: Context) {
12
-
const getKnownFollowers = (params: QueryParams, auth: HandlerAuth) =>
13
-
pipe(
14
-
{ params, ctx, viewerDid: auth.credentials?.did },
15
-
retrieve,
16
-
Effect.flatMap(presentation),
17
-
Effect.retry({ times: 3 }),
18
-
Effect.timeout("120 seconds"),
19
-
Effect.catchAll((err) => {
20
-
console.error("getKnownFollowers error:", err);
21
-
return Effect.succeed({
22
-
subject: {} satisfies ProfileViewBasic,
23
-
followers: [] as ProfileViewBasic[],
24
-
});
25
-
}),
26
-
);
27
-
28
-
server.app.rocksky.graph.getKnownFollowers({
29
-
auth: ctx.authVerifier,
30
-
handler: async ({ params, auth }) => {
31
-
const result = await Effect.runPromise(getKnownFollowers(params, auth));
32
-
return {
33
-
encoding: "application/json",
34
-
body: result,
35
-
};
36
-
},
37
-
});
38
-
}
39
-
40
-
const retrieve = ({
41
-
params,
42
-
ctx,
43
-
viewerDid,
44
-
}: {
45
-
params: QueryParams;
46
-
ctx: Context;
47
-
viewerDid?: string;
48
-
}): Effect.Effect<
49
-
[SelectUser | undefined, SelectUser[], string | undefined],
50
-
Error
51
-
> => {
52
-
if (!viewerDid) {
53
-
return Effect.succeed([undefined, [], undefined]);
54
-
}
55
-
56
-
return Effect.tryPromise({
57
-
try: async () => {
58
-
const user = await ctx.db
59
-
.select()
60
-
.from(tables.users)
61
-
.where(eq(tables.users.did, params.actor))
62
-
.execute()
63
-
.then((rows) => rows[0]);
64
-
const knownFollowers = await ctx.db
65
-
.select()
66
-
.from(tables.follows)
67
-
.innerJoin(
68
-
tables.users,
69
-
eq(tables.users.did, tables.follows.follower_did),
70
-
)
71
-
.where(
72
-
params.cursor
73
-
? and(
74
-
lt(tables.follows.createdAt, new Date(Number(params.cursor))),
75
-
eq(tables.follows.subject_did, params.actor),
76
-
sql`EXISTS (
77
-
SELECT 1 FROM ${tables.follows} f2
78
-
WHERE f2.subject_did = ${tables.users.did}
79
-
AND f2.follower_did = ${viewerDid}
80
-
)`,
81
-
)
82
-
: and(
83
-
eq(tables.follows.subject_did, params.actor),
84
-
sql`EXISTS (
85
-
SELECT 1 FROM ${tables.follows} f2
86
-
WHERE f2.subject_did = ${tables.users.did}
87
-
AND f2.follower_did = ${viewerDid}
88
-
)`,
89
-
),
90
-
)
91
-
.orderBy(desc(tables.follows.createdAt))
92
-
.limit(params.limit ?? 50)
93
-
.execute();
94
-
const cursor =
95
-
knownFollowers?.length > 0
96
-
? knownFollowers[knownFollowers.length - 1].follows.createdAt
97
-
.getTime()
98
-
.toString(10)
99
-
: undefined;
100
-
return [user, knownFollowers.map((row) => row.users), cursor];
101
-
},
102
-
catch: (error) => new Error(`Failed to retrieve known followers: ${error}`),
103
-
});
104
-
};
105
-
106
-
const presentation = ([user, followers, cursor]: [
107
-
SelectUser | undefined,
108
-
SelectUser[],
109
-
string | undefined,
110
-
]): Effect.Effect<
111
-
{ subject: ProfileViewBasic; followers: ProfileViewBasic[] },
112
-
never
113
-
> => {
114
-
return Effect.sync(() => ({
115
-
subject: {
116
-
id: user?.id,
117
-
did: user?.did,
118
-
handle: user?.handle,
119
-
displayName: user?.displayName,
120
-
avatar: user?.avatar,
121
-
createdAt: user?.createdAt.toISOString(),
122
-
updatedAt: user?.updatedAt.toISOString(),
123
-
},
124
-
followers: followers.map((follower) => ({
125
-
id: follower.id,
126
-
did: follower.did,
127
-
handle: follower.handle,
128
-
displayName: follower.displayName,
129
-
avatar: follower.avatar,
130
-
createdAt: follower.createdAt.toISOString(),
131
-
updatedAt: follower.updatedAt.toISOString(),
132
-
})),
133
-
cursor,
134
-
}));
135
-
};
-176
apps/api/src/xrpc/app/rocksky/graph/unfollowAccount.ts
-176
apps/api/src/xrpc/app/rocksky/graph/unfollowAccount.ts
···
1
-
import type { HandlerAuth } from "@atproto/xrpc-server";
2
-
import type { Context } from "context";
3
-
import { and, eq, desc } from "drizzle-orm";
4
-
import { Effect, pipe } from "effect";
5
-
import type { Server } from "lexicon";
6
-
import type { ProfileViewBasic } from "lexicon/types/app/rocksky/actor/defs";
7
-
import type { QueryParams } from "lexicon/types/app/rocksky/graph/followAccount";
8
-
import { createAgent } from "lib/agent";
9
-
import tables from "schema";
10
-
import type { SelectUser } from "schema/users";
11
-
12
-
export default function (server: Server, ctx: Context) {
13
-
const unfollowAccount = (params: QueryParams, auth: HandlerAuth) =>
14
-
pipe(
15
-
{ params, ctx, did: auth.credentials?.did },
16
-
handleFollow,
17
-
Effect.flatMap(presentation),
18
-
Effect.retry({ times: 3 }),
19
-
Effect.timeout("120 seconds"),
20
-
Effect.catchAll((err) => {
21
-
console.error(err);
22
-
return Effect.succeed({
23
-
subject: {} satisfies ProfileViewBasic,
24
-
followers: [],
25
-
});
26
-
}),
27
-
);
28
-
server.app.rocksky.graph.unfollowAccount({
29
-
auth: ctx.authVerifier,
30
-
handler: async ({ params, auth }) => {
31
-
const result = await Effect.runPromise(unfollowAccount(params, auth));
32
-
return {
33
-
encoding: "application/json",
34
-
body: result,
35
-
};
36
-
},
37
-
});
38
-
}
39
-
40
-
const handleFollow = ({
41
-
params,
42
-
ctx,
43
-
did,
44
-
}: {
45
-
params: QueryParams;
46
-
ctx: Context;
47
-
did?: string;
48
-
}): Effect.Effect<[SelectUser | undefined, SelectUser[]], Error> => {
49
-
return Effect.tryPromise({
50
-
try: async () => {
51
-
if (!did) {
52
-
throw new Error("User is not authenticated");
53
-
}
54
-
if (params.account === did) {
55
-
throw new Error("User cannot follow themselves");
56
-
}
57
-
58
-
if (!(await isFollowing(ctx, did, params.account))) {
59
-
throw new Error("User is not following");
60
-
}
61
-
62
-
const agent = await createAgent(ctx.oauthClient, did);
63
-
if (!agent) {
64
-
throw new Error("Unauthorized");
65
-
}
66
-
67
-
const follow = await ctx.db
68
-
.select()
69
-
.from(tables.follows)
70
-
.where(
71
-
and(
72
-
eq(tables.follows.subject_did, params.account),
73
-
eq(tables.follows.follower_did, did),
74
-
),
75
-
)
76
-
.execute()
77
-
.then((rows) => rows[0]);
78
-
79
-
if (!follow) {
80
-
throw new Error("Follow not found");
81
-
}
82
-
83
-
const rkey = follow.uri.split("/").pop();
84
-
85
-
await agent.com.atproto.repo.deleteRecord({
86
-
repo: agent.assertDid,
87
-
collection: "app.rocksky.graph.follow",
88
-
rkey,
89
-
});
90
-
91
-
await ctx.db
92
-
.delete(tables.follows)
93
-
.where(
94
-
and(
95
-
eq(tables.follows.subject_did, params.account),
96
-
eq(tables.follows.follower_did, did),
97
-
),
98
-
)
99
-
.execute();
100
-
101
-
return Promise.all([
102
-
ctx.db
103
-
.select()
104
-
.from(tables.users)
105
-
.where(eq(tables.users.did, params.account))
106
-
.execute()
107
-
.then((rows) => rows[0]),
108
-
ctx.db
109
-
.select()
110
-
.from(tables.follows)
111
-
.where(eq(tables.follows.subject_did, params.account))
112
-
.leftJoin(
113
-
tables.users,
114
-
eq(tables.users.did, tables.follows.follower_did),
115
-
)
116
-
.orderBy(desc(tables.follows.createdAt))
117
-
.limit(50)
118
-
.execute()
119
-
.then((rows) => rows.map(({ users }) => users)),
120
-
]);
121
-
},
122
-
catch: (error) => new Error(`Failed to retrieve follow: ${error}`),
123
-
});
124
-
};
125
-
126
-
const presentation = ([user, followers]: [
127
-
SelectUser | undefined,
128
-
SelectUser[],
129
-
]): Effect.Effect<
130
-
{ subject: ProfileViewBasic; followers: ProfileViewBasic[] },
131
-
never
132
-
> => {
133
-
return Effect.sync(() => ({
134
-
subject: {
135
-
id: user?.id,
136
-
did: user?.did,
137
-
handle: user?.handle,
138
-
displayName: user?.displayName,
139
-
avatar: user?.avatar,
140
-
createdAt: user?.createdAt.toISOString(),
141
-
updatedAt: user?.updatedAt.toISOString(),
142
-
},
143
-
followers: followers.map((follower) => ({
144
-
id: follower.id,
145
-
did: follower.did,
146
-
handle: follower.handle,
147
-
displayName: follower.displayName,
148
-
avatar: follower.avatar,
149
-
createdAt: follower.createdAt.toISOString(),
150
-
updatedAt: follower.updatedAt.toISOString(),
151
-
})),
152
-
cursor:
153
-
followers.length === 50
154
-
? followers[49].createdAt.getTime().toString(10)
155
-
: undefined,
156
-
}));
157
-
};
158
-
159
-
const isFollowing = async (
160
-
ctx: Context,
161
-
followerDid: string,
162
-
subjectDid: string,
163
-
): Promise<boolean> => {
164
-
const result = await ctx.db
165
-
.select()
166
-
.from(tables.follows)
167
-
.where(
168
-
and(
169
-
eq(tables.follows.follower_did, followerDid),
170
-
eq(tables.follows.subject_did, subjectDid),
171
-
),
172
-
)
173
-
.execute();
174
-
175
-
return result.length > 0;
176
-
};
+6
-30
apps/api/src/xrpc/app/rocksky/scrobble/getScrobbles.ts
+6
-30
apps/api/src/xrpc/app/rocksky/scrobble/getScrobbles.ts
···
1
1
import type { Context } from "context";
2
-
import { and, desc, eq, inArray } from "drizzle-orm";
2
+
import { desc, eq } from "drizzle-orm";
3
3
import { Effect, pipe } from "effect";
4
4
import type { Server } from "lexicon";
5
5
import type { ScrobbleViewBasic } from "lexicon/types/app/rocksky/scrobble/defs";
···
11
11
import type { SelectUser } from "schema/users";
12
12
13
13
export default function (server: Server, ctx: Context) {
14
-
const getScrobbles = (params: QueryParams) =>
14
+
const getScrobbles = (params) =>
15
15
pipe(
16
16
{ params, ctx },
17
17
retrieve,
···
42
42
ctx: Context;
43
43
}): Effect.Effect<Scrobbles | undefined, Error> => {
44
44
return Effect.tryPromise({
45
-
try: async () => {
46
-
const baseQuery = ctx.db
45
+
try: () =>
46
+
ctx.db
47
47
.select()
48
48
.from(tables.scrobbles)
49
49
.leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id))
50
-
.leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id));
51
-
52
-
if (params.did && params.following) {
53
-
const followedUsers = await ctx.db
54
-
.select({ subjectDid: tables.follows.subject_did })
55
-
.from(tables.follows)
56
-
.where(eq(tables.follows.follower_did, params.did))
57
-
.execute();
58
-
59
-
const followedDids = followedUsers.map((f) => f.subjectDid);
60
-
61
-
if (followedDids.length > 0) {
62
-
return baseQuery
63
-
.where(inArray(tables.users.did, followedDids))
64
-
.orderBy(desc(tables.scrobbles.timestamp))
65
-
.offset(params.offset || 0)
66
-
.limit(params.limit || 20)
67
-
.execute();
68
-
} else {
69
-
return [];
70
-
}
71
-
}
72
-
73
-
return baseQuery
50
+
.leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id))
74
51
.orderBy(desc(tables.scrobbles.timestamp))
75
52
.offset(params.offset || 0)
76
53
.limit(params.limit || 20)
77
-
.execute();
78
-
},
54
+
.execute(),
79
55
80
56
catch: (error) => new Error(`Failed to retrieve scrobbles: ${error}`),
81
57
});
-14
apps/api/src/xrpc/index.ts
-14
apps/api/src/xrpc/index.ts
···
75
75
import getFeedGenerators from "./app/rocksky/feed/getFeedGenerators";
76
76
import getFeedGenerator from "./app/rocksky/feed/getFeedGenerator";
77
77
import getFeed from "./app/rocksky/feed/getFeed";
78
-
import followAccount from "./app/rocksky/graph/followAccount";
79
-
import getFollowers from "./app/rocksky/graph/getFollowers";
80
-
import getFollows from "./app/rocksky/graph/getFollows";
81
-
import getKnownFollowers from "./app/rocksky/graph/getKnownFollowers";
82
-
import unfollowAccount from "./app/rocksky/graph/unfollowAccount";
83
-
import getActorNeighbours from "./app/rocksky/actor/getActorNeighbours";
84
-
import getActorCompatibility from "./app/rocksky/actor/getActorCompatibility";
85
78
86
79
export default function (server: Server, ctx: Context) {
87
80
// app.rocksky
···
160
153
getFeedGenerators(server, ctx);
161
154
getFeedGenerator(server, ctx);
162
155
getFeed(server, ctx);
163
-
followAccount(server, ctx);
164
-
getFollowers(server, ctx);
165
-
getFollows(server, ctx);
166
-
getKnownFollowers(server, ctx);
167
-
unfollowAccount(server, ctx);
168
-
getActorNeighbours(server, ctx);
169
-
getActorCompatibility(server, ctx);
170
156
171
157
return server;
172
158
}
+1
-1
apps/feeds/src/algos/afrobeat.ts
+1
-1
apps/feeds/src/algos/afrobeat.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/afrobeats.ts
+1
-1
apps/feeds/src/algos/afrobeats.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/alternative-metal.ts
+4
-2
apps/feeds/src/algos/alternative-metal.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["alternative metal"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["alternative metal"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+4
-2
apps/feeds/src/algos/alternative-rnb.ts
+4
-2
apps/feeds/src/algos/alternative-rnb.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["alternative rnb"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["alternative rnb"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/anime.ts
+1
-1
apps/feeds/src/algos/anime.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+3
-2
apps/feeds/src/algos/art-pop.ts
+3
-2
apps/feeds/src/algos/art-pop.ts
···
1
1
import { Context } from "../context.ts";
2
2
import { Algorithm, feedParams } from "./types.ts";
3
3
import schema from "../schema/mod.ts";
4
-
import { and, arrayContains, desc, eq, lt } from "drizzle-orm";
4
+
import { and, arrayContains, desc, eq } from "drizzle-orm";
5
+
import { lt } from "drizzle-orm/sql";
5
6
6
7
const handler = async (
7
8
ctx: Context,
···
20
21
const scrobbles = await ctx.db
21
22
.select()
22
23
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
25
.where(and(...whereConditions))
25
26
.orderBy(desc(schema.scrobbles.timestamp))
26
27
.limit(limit)
+1
-1
apps/feeds/src/algos/breakcore.ts
+1
-1
apps/feeds/src/algos/breakcore.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/chicago-drill.ts
+4
-2
apps/feeds/src/algos/chicago-drill.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["chicago drill"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["chicago drill"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/chillwave.ts
+1
-1
apps/feeds/src/algos/chillwave.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/country-hip-hop.ts
+4
-2
apps/feeds/src/algos/country-hip-hop.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["country hip hop"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["country hip hop"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/crunk.ts
+1
-1
apps/feeds/src/algos/crunk.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/dance-pop.ts
+1
-1
apps/feeds/src/algos/dance-pop.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/deep-house.ts
+4
-2
apps/feeds/src/algos/deep-house.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["deep house"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["deep house"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/drill.ts
+1
-1
apps/feeds/src/algos/drill.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+3
-3
apps/feeds/src/algos/dubstep.ts
+3
-3
apps/feeds/src/algos/dubstep.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["dupstep"])];
13
+
const whereConditions = [arrayContains(schema.artists.genres, ["dubstep"])];
14
14
15
15
if (cursor) {
16
16
const cursorDate = new Date(parseInt(cursor, 10));
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
···
41
41
};
42
42
43
43
export const publisherDid = "did:plc:vegqomyce4ssoqs7zwqvgqty";
44
-
export const rkey = "dupstep";
44
+
export const rkey = "dubstep";
45
45
46
46
export const info = {
47
47
handler,
+1
-1
apps/feeds/src/algos/emo.ts
+1
-1
apps/feeds/src/algos/emo.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/grunge.ts
+1
-1
apps/feeds/src/algos/grunge.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/hard-rock.ts
+1
-1
apps/feeds/src/algos/hard-rock.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/heavy-metal.ts
+4
-2
apps/feeds/src/algos/heavy-metal.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["heavy metal"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["heavy metal"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/hip-hop.ts
+1
-1
apps/feeds/src/algos/hip-hop.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/house.ts
+1
-1
apps/feeds/src/algos/house.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/hyperpop.ts
+1
-1
apps/feeds/src/algos/hyperpop.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/indie-rock.ts
+4
-2
apps/feeds/src/algos/indie-rock.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["indie rock"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["indie rock"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/indie.ts
+1
-1
apps/feeds/src/algos/indie.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/j-pop.ts
+1
-1
apps/feeds/src/algos/j-pop.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/j-rock.ts
+1
-1
apps/feeds/src/algos/j-rock.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/jazz.ts
+1
-1
apps/feeds/src/algos/jazz.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/k-pop.ts
+1
-1
apps/feeds/src/algos/k-pop.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/lo-fi.ts
+1
-1
apps/feeds/src/algos/lo-fi.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/metal.ts
+1
-1
apps/feeds/src/algos/metal.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/metalcore.ts
+1
-1
apps/feeds/src/algos/metalcore.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/midwest-emo.ts
+4
-2
apps/feeds/src/algos/midwest-emo.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["midwest emo"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["midwest emo"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/nu-metal.ts
+1
-1
apps/feeds/src/algos/nu-metal.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/pop-punk.ts
+1
-1
apps/feeds/src/algos/pop-punk.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/post-grunge.ts
+4
-2
apps/feeds/src/algos/post-grunge.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["post-grunge"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["post-grunge"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/rap-metal.ts
+1
-1
apps/feeds/src/algos/rap-metal.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/rap.ts
+1
-1
apps/feeds/src/algos/rap.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/rnb.ts
+1
-1
apps/feeds/src/algos/rnb.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/rock.ts
+1
-1
apps/feeds/src/algos/rock.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/southern-hip-hop.ts
+4
-2
apps/feeds/src/algos/southern-hip-hop.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["southern hip hop"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["southern hip hop"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/speedcore.ts
+1
-1
apps/feeds/src/algos/speedcore.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/swedish-pop.ts
+4
-2
apps/feeds/src/algos/swedish-pop.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["swedish pop"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["swedish pop"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/synthwave.ts
+1
-1
apps/feeds/src/algos/synthwave.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/thrash-metal.ts
+4
-2
apps/feeds/src/algos/thrash-metal.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["thrash metal"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["thrash metal"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/trap-soul.ts
+1
-1
apps/feeds/src/algos/trap-soul.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+1
-1
apps/feeds/src/algos/trap.ts
+1
-1
apps/feeds/src/algos/trap.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/tropical-house.ts
+4
-2
apps/feeds/src/algos/tropical-house.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["tropical house"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["tropical house"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/vaporwave.ts
+1
-1
apps/feeds/src/algos/vaporwave.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/visual-kei.ts
+4
-2
apps/feeds/src/algos/visual-kei.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["visual kei"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["visual kei"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+1
-1
apps/feeds/src/algos/vocaloid.ts
+1
-1
apps/feeds/src/algos/vocaloid.ts
···
20
20
const scrobbles = await ctx.db
21
21
.select()
22
22
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
23
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
24
.where(and(...whereConditions))
25
25
.orderBy(desc(schema.scrobbles.timestamp))
26
26
.limit(limit)
+4
-2
apps/feeds/src/algos/west-coast-hip-hop.ts
+4
-2
apps/feeds/src/algos/west-coast-hip-hop.ts
···
10
10
) => {
11
11
const { limit = 50, cursor } = params;
12
12
13
-
const whereConditions = [arrayContains(schema.artists.genres, ["west coast hip hop"])];
13
+
const whereConditions = [
14
+
arrayContains(schema.artists.genres, ["west coast hip hop"]),
15
+
];
14
16
15
17
if (cursor) {
16
18
const cursorDate = new Date(parseInt(cursor, 10));
···
20
22
const scrobbles = await ctx.db
21
23
.select()
22
24
.from(schema.scrobbles)
23
-
.innerJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
25
+
.leftJoin(schema.artists, eq(schema.scrobbles.artistId, schema.artists.id))
24
26
.where(and(...whereConditions))
25
27
.orderBy(desc(schema.scrobbles.timestamp))
26
28
.limit(limit)
+2
-69
apps/web/src/api/feed.ts
+2
-69
apps/web/src/api/feed.ts
···
92
92
id: string;
93
93
};
94
94
}[];
95
-
cursor?: string;
96
95
}>("/xrpc/app.rocksky.feed.getFeed", {
97
96
params: {
98
97
feed: uri,
···
107
106
});
108
107
109
108
if (response.status !== 200) {
110
-
return { songs: [], cursor: undefined };
109
+
return [];
111
110
}
112
111
113
-
return {
114
-
songs: response.data.feed.map(({ scrobble }) => scrobble),
115
-
cursor: response.data.cursor,
116
-
};
117
-
};
118
-
119
-
export const getScrobbles = async (
120
-
did: string,
121
-
following: boolean = false,
122
-
offset: number = 0,
123
-
limit: number = 50,
124
-
) => {
125
-
const response = await client.get<{
126
-
scrobbles: {
127
-
title: string;
128
-
artist: string;
129
-
albumArtist: string;
130
-
album: string;
131
-
trackNumber: number;
132
-
duration: number;
133
-
mbId: string | null;
134
-
youtubeLink: string | null;
135
-
spotifyLink: string | null;
136
-
appleMusicLink: string | null;
137
-
tidalLink: string | null;
138
-
sha256: string;
139
-
discNumber: number;
140
-
composer: string | null;
141
-
genre: string | null;
142
-
label: string | null;
143
-
copyrightMessage: string | null;
144
-
uri: string;
145
-
albumUri: string;
146
-
artistUri: string;
147
-
trackUri: string;
148
-
xataVersion: number;
149
-
cover: string;
150
-
date: string;
151
-
user: string;
152
-
userDisplayName: string;
153
-
userAvatar: string;
154
-
tags: string[];
155
-
likesCount: number;
156
-
liked: boolean;
157
-
id: string;
158
-
}[];
159
-
}>("/xrpc/app.rocksky.scrobble.getScrobbles", {
160
-
params: {
161
-
did,
162
-
following,
163
-
offset,
164
-
limit,
165
-
},
166
-
headers: {
167
-
Authorization: localStorage.getItem("token")
168
-
? `Bearer ${localStorage.getItem("token")}`
169
-
: undefined,
170
-
},
171
-
});
172
-
173
-
if (response.status !== 200) {
174
-
return { scrobbles: [] };
175
-
}
176
-
177
-
return {
178
-
scrobbles: response.data.scrobbles,
179
-
};
112
+
return response.data.feed.map(({ scrobble }) => scrobble);
180
113
};
-72
apps/web/src/api/graph.ts
-72
apps/web/src/api/graph.ts
···
1
-
import { client } from ".";
2
-
3
-
export const getFollows = async (
4
-
actor: string,
5
-
limit: number,
6
-
dids?: string[],
7
-
cursor?: string,
8
-
) => {
9
-
const response = await client.get("/xrpc/app.rocksky.graph.getFollows", {
10
-
params: { actor, limit: limit > 0 ? limit : 1, dids, cursor },
11
-
});
12
-
13
-
return response.data;
14
-
};
15
-
16
-
export const getKnownFollowers = async (
17
-
actor: string,
18
-
limit: number,
19
-
cursor?: string,
20
-
) => {
21
-
const response = await client.get(
22
-
"/xrpc/app.rocksky.graph.getKnownFollowers",
23
-
{
24
-
params: { actor, limit: limit > 0 ? limit : 1, cursor },
25
-
},
26
-
);
27
-
28
-
return response.data;
29
-
};
30
-
31
-
export const getFollowers = async (
32
-
actor: string,
33
-
limit: number,
34
-
dids?: string[],
35
-
cursor?: string,
36
-
) => {
37
-
const response = await client.get("/xrpc/app.rocksky.graph.getFollowers", {
38
-
params: { actor, limit: limit > 0 ? limit : 1, dids, cursor },
39
-
});
40
-
41
-
return response.data;
42
-
};
43
-
44
-
export const followAccount = async (account: string) => {
45
-
const response = await client.post(
46
-
"/xrpc/app.rocksky.graph.followAccount",
47
-
undefined,
48
-
{
49
-
params: { account },
50
-
headers: {
51
-
Authorization: `Bearer ${localStorage.getItem("token")}`,
52
-
},
53
-
},
54
-
);
55
-
56
-
return response.data;
57
-
};
58
-
59
-
export const unfollowAccount = async (account: string) => {
60
-
const response = await client.post(
61
-
"/xrpc/app.rocksky.graph.unfollowAccount",
62
-
undefined,
63
-
{
64
-
params: { account },
65
-
headers: {
66
-
Authorization: `Bearer ${localStorage.getItem("token")}`,
67
-
},
68
-
},
69
-
);
70
-
71
-
return response.data;
72
-
};
+3
-32
apps/web/src/api/profile.ts
+3
-32
apps/web/src/api/profile.ts
···
1
1
import { client } from ".";
2
-
import { Compatibility } from "../types/compatibility";
3
-
import { Neighbour } from "../types/neighbour";
4
-
import { Profile } from "../types/profile";
5
2
import { Scrobble } from "../types/scrobble";
6
3
7
4
export const getProfileByDid = async (did: string) => {
8
-
const response = await client.get<Profile>(
9
-
"/xrpc/app.rocksky.actor.getProfile",
10
-
{
11
-
params: { did },
12
-
},
13
-
);
5
+
const response = await client.get("/xrpc/app.rocksky.actor.getProfile", {
6
+
params: { did },
7
+
});
14
8
return response.data;
15
9
};
16
10
···
34
28
);
35
29
return response.data.scrobbles || [];
36
30
};
37
-
38
-
export const getActorNeighbours = async (did: string) => {
39
-
const response = await client.get<{ neighbours: Neighbour[] }>(
40
-
"/xrpc/app.rocksky.actor.getActorNeighbours",
41
-
{
42
-
params: { did },
43
-
},
44
-
);
45
-
return response.data;
46
-
};
47
-
48
-
export const getActorCompatibility = async (did: string) => {
49
-
const response = await client.get<{ compatibility: Compatibility | null }>(
50
-
"/xrpc/app.rocksky.actor.getActorCompatibility",
51
-
{
52
-
params: { did },
53
-
headers: {
54
-
Authorization: `Bearer ${localStorage.getItem("token")}`,
55
-
},
56
-
},
57
-
);
58
-
return response.data;
59
-
};
-3
apps/web/src/atoms/followingFeed.ts
-3
apps/web/src/atoms/followingFeed.ts
-3
apps/web/src/atoms/follows.ts
-3
apps/web/src/atoms/follows.ts
-4
apps/web/src/atoms/tab.ts
-4
apps/web/src/atoms/tab.ts
+44
-180
apps/web/src/components/Handle/Handle.tsx
+44
-180
apps/web/src/components/Handle/Handle.tsx
···
4
4
import { StatefulPopover, TRIGGER_TYPE } from "baseui/popover";
5
5
import { LabelMedium, LabelSmall } from "baseui/typography";
6
6
import { useAtom } from "jotai";
7
-
import { useEffect, useState } from "react";
7
+
import { useEffect } from "react";
8
8
import { profilesAtom } from "../../atoms/profiles";
9
9
import { statsAtom } from "../../atoms/stats";
10
10
import {
···
13
13
} from "../../hooks/useProfile";
14
14
import Stats from "../Stats";
15
15
import NowPlaying from "./NowPlaying";
16
-
import { followsAtom } from "../../atoms/follows";
17
-
import { IconCheck, IconPlus } from "@tabler/icons-react";
18
-
import { Button } from "baseui/button";
19
-
import SignInModal from "../SignInModal";
20
-
import {
21
-
useFollowAccountMutation,
22
-
useFollowersQuery,
23
-
useUnfollowAccountMutation,
24
-
} from "../../hooks/useGraph";
25
16
26
17
export type HandleProps = {
27
18
link: string;
···
29
20
};
30
21
31
22
function Handle(props: HandleProps) {
32
-
const [follows, setFollows] = useAtom(followsAtom);
33
-
const [isSignInOpen, setIsSignInOpen] = useState(false);
34
23
const { link, did } = props;
35
24
const [profiles, setProfiles] = useAtom(profilesAtom);
36
25
const profile = useProfileByDidQuery(did);
37
26
const profileStats = useProfileStatsByDidQuery(did);
38
27
const [stats, setStats] = useAtom(statsAtom);
39
-
const { mutate: followAccount } = useFollowAccountMutation();
40
-
const { mutate: unfollowAccount } = useUnfollowAccountMutation();
41
-
const currentDid = localStorage.getItem("did");
42
-
const { data, isLoading } = useFollowersQuery(
43
-
profile.data?.did,
44
-
1,
45
-
currentDid ? [currentDid] : undefined,
46
-
);
47
-
48
-
const onFollow = () => {
49
-
if (!localStorage.getItem("token")) {
50
-
setIsSignInOpen(true);
51
-
return;
52
-
}
53
-
if (!profile.data?.did) {
54
-
return;
55
-
}
56
-
setFollows((prev) => new Set(prev).add(profile.data?.did));
57
-
followAccount(profile.data?.did);
58
-
};
59
-
60
-
const onUnfollow = () => {
61
-
if (!localStorage.getItem("token")) {
62
-
setIsSignInOpen(true);
63
-
return;
64
-
}
65
-
if (!profile.data?.did) {
66
-
return;
67
-
}
68
-
69
-
setFollows((prev) => {
70
-
if (!profile.data?.did) {
71
-
return prev;
72
-
}
73
-
const newSet = new Set(prev);
74
-
newSet.delete(profile.data?.did);
75
-
return newSet;
76
-
});
77
-
unfollowAccount(profile.data?.did);
78
-
};
79
-
80
-
useEffect(() => {
81
-
if (!data || isLoading) {
82
-
return;
83
-
}
84
-
setFollows((prev) => {
85
-
const newSet = new Set(prev);
86
-
if (!profile.data?.did) {
87
-
return newSet;
88
-
}
89
-
if (
90
-
data.followers.some(
91
-
(follower: { did: string }) => follower.did === currentDid,
92
-
)
93
-
) {
94
-
newSet.add(profile.data?.did);
95
-
} else {
96
-
newSet.delete(profile.data?.did);
97
-
}
98
-
return newSet;
99
-
});
100
-
}, [data, isLoading, currentDid, setFollows, profile.data?.did]);
101
28
102
29
useEffect(() => {
103
30
if (profile.isLoading || profile.isError) {
···
146
73
}, [profileStats.data, profileStats.isLoading, profileStats.isError, did]);
147
74
148
75
return (
149
-
<>
150
-
<StatefulPopover
151
-
content={() => (
152
-
<Block className="!bg-[var(--color-background)] !text-[var(--color-text)] p-[15px] w-[380px] rounded-[6px] border-[1px] border-[var(--color-border)]">
153
-
<div className="flex flex-row items-start justify-between">
154
-
<div className="flex flex-row items-center">
155
-
<Link to={link} className="no-underline">
156
-
<Avatar
157
-
src={profiles[did]?.avatar}
158
-
name={profiles[did]?.displayName}
159
-
size={"60px"}
160
-
/>
161
-
</Link>
162
-
<div className="ml-[16px]">
163
-
<Link to={link} className="no-underline">
164
-
<LabelMedium
165
-
marginTop={"10px"}
166
-
className="!text-[var(--color-text)]"
167
-
>
168
-
{profiles[did]?.displayName}
169
-
</LabelMedium>
170
-
</Link>
171
-
<a
172
-
href={`https://bsky.app/profile/${profiles[did]?.handle}`}
173
-
className="no-underline text-[var(--color-primary)]"
174
-
target="_blank"
175
-
>
176
-
<LabelSmall className="!text-[var(--color-primary)] mt-[3px] mb-[25px]">
177
-
@{did}
178
-
</LabelSmall>
179
-
</a>
180
-
</div>
181
-
</div>
182
-
{(profile.data?.did !== localStorage.getItem("did") ||
183
-
!localStorage.getItem("did")) && (
184
-
<div className="ml-auto mt-[10px]">
185
-
{!follows.has(profile.data?.did || "") && !isLoading && (
186
-
<Button
187
-
shape="pill"
188
-
size="mini"
189
-
startEnhancer={<IconPlus size={16} />}
190
-
onClick={onFollow}
191
-
overrides={{
192
-
BaseButton: {
193
-
style: {
194
-
minWidth: "90px",
195
-
backgroundColor: "#ff2876",
196
-
":hover": {
197
-
backgroundColor: "#ff2876",
198
-
},
199
-
":focus": {
200
-
backgroundColor: "#ff2876",
201
-
},
202
-
},
203
-
},
204
-
}}
205
-
>
206
-
Follow
207
-
</Button>
208
-
)}
209
-
{follows.has(profile.data?.did || "") && !isLoading && (
210
-
<Button
211
-
shape="pill"
212
-
size="mini"
213
-
startEnhancer={<IconCheck size={16} />}
214
-
onClick={onUnfollow}
215
-
overrides={{
216
-
BaseButton: {
217
-
style: {
218
-
backgroundColor: "var(--color-default-button)",
219
-
color: "var(--color-text)",
220
-
":hover": {
221
-
backgroundColor: "var(--color-default-button)",
222
-
},
223
-
":focus": {
224
-
backgroundColor: "var(--color-default-button)",
225
-
},
226
-
},
227
-
},
228
-
}}
229
-
>
230
-
Following
231
-
</Button>
232
-
)}
233
-
</div>
234
-
)}
76
+
<StatefulPopover
77
+
content={() => (
78
+
<Block className="!bg-[var(--color-background)] !text-[var(--color-text)] p-[15px] w-[380px] rounded-[6px] border-[1px] border-[var(--color-border)]">
79
+
<div className="flex flex-row items-center">
80
+
<Link to={link} className="no-underline">
81
+
<Avatar
82
+
src={profiles[did]?.avatar}
83
+
name={profiles[did]?.displayName}
84
+
size={"60px"}
85
+
/>
86
+
</Link>
87
+
<div className="ml-[16px]">
88
+
<Link to={link} className="no-underline">
89
+
<LabelMedium
90
+
marginTop={"10px"}
91
+
className="!text-[var(--color-text)]"
92
+
>
93
+
{profiles[did]?.displayName}
94
+
</LabelMedium>
95
+
</Link>
96
+
<a
97
+
href={`https://bsky.app/profile/${profiles[did]?.handle}`}
98
+
className="no-underline text-[var(--color-primary)]"
99
+
>
100
+
<LabelSmall className="!text-[var(--color-primary)] mt-[3px] mb-[25px]">
101
+
@{did}
102
+
</LabelSmall>
103
+
</a>
235
104
</div>
105
+
</div>
236
106
237
-
{stats[did] && <Stats stats={stats[did]} mb={1} />}
107
+
{stats[did] && <Stats stats={stats[did]} mb={1} />}
238
108
239
-
<NowPlaying did={did} />
240
-
</Block>
241
-
)}
242
-
triggerType={TRIGGER_TYPE.hover}
243
-
autoFocus={false}
244
-
focusLock={false}
245
-
>
246
-
<Link to={link} className="no-underline">
247
-
<LabelMedium className="!text-[var(--color-primary)] !overflow-hidden !text-ellipsis !max-w-[220px] !text-[14px]">
248
-
@{did}
249
-
</LabelMedium>
250
-
</Link>
251
-
</StatefulPopover>
252
-
<SignInModal
253
-
isOpen={isSignInOpen}
254
-
onClose={() => setIsSignInOpen(false)}
255
-
follow
256
-
/>
257
-
</>
109
+
<NowPlaying did={did} />
110
+
</Block>
111
+
)}
112
+
triggerType={TRIGGER_TYPE.hover}
113
+
autoFocus={false}
114
+
focusLock={false}
115
+
>
116
+
<Link to={link} className="no-underline">
117
+
<LabelMedium className="!text-[var(--color-primary)] !overflow-hidden !text-ellipsis !max-w-[220px] !text-[14px]">
118
+
@{did}
119
+
</LabelMedium>
120
+
</Link>
121
+
</StatefulPopover>
258
122
);
259
123
}
260
124
+3
-12
apps/web/src/components/Shout/ShoutList/Shout/ReplyModal/ReplyModal.tsx
+3
-12
apps/web/src/components/Shout/ShoutList/Shout/ReplyModal/ReplyModal.tsx
···
11
11
} from "baseui/modal";
12
12
import { Spinner } from "baseui/spinner";
13
13
import { Textarea } from "baseui/textarea";
14
-
import { LabelMedium, LabelSmall } from "baseui/typography";
14
+
import { LabelMedium } from "baseui/typography";
15
15
import { useAtomValue, useSetAtom } from "jotai";
16
16
import { useState } from "react";
17
17
import { Controller, useForm } from "react-hook-form";
···
19
19
import { profileAtom } from "../../../../../atoms/profile";
20
20
import { shoutsAtom } from "../../../../../atoms/shouts";
21
21
import useShout from "../../../../../hooks/useShout";
22
-
import scrollToTop from "../../../../../lib/scrollToTop";
23
22
24
23
const ShoutSchema = z.object({
25
24
message: z.string().min(1).max(1000),
···
239
238
<div className="ml-[20px] w-full">
240
239
<Header>
241
240
<div>
242
-
<Link
243
-
to={`/profile/${shout.user.handle}`}
244
-
className="flex no-underline"
245
-
style={{ textDecoration: "none" }}
246
-
onClick={() => scrollToTop()}
247
-
>
248
-
<LabelMedium className="!text-[var(--color-text)] no-underline">
241
+
<Link to={`/profile/${shout.user.handle}`} onClick={close}>
242
+
<LabelMedium className="!text-[var(--color-text)]">
249
243
{shout.user.displayName}
250
244
</LabelMedium>
251
-
<LabelSmall className="ml-[5px] mt-[4px] no-underline !text-[var(--color-text-muted)]">
252
-
@{shout.user.handle}
253
-
</LabelSmall>
254
245
</Link>
255
246
</div>
256
247
</Header>
+3
-12
apps/web/src/components/Shout/ShoutList/Shout/Shout.tsx
+3
-12
apps/web/src/components/Shout/ShoutList/Shout/Shout.tsx
···
6
6
import { NestedMenus, StatefulMenu } from "baseui/menu";
7
7
import { PLACEMENT, StatefulPopover } from "baseui/popover";
8
8
import { StatefulTooltip } from "baseui/tooltip";
9
-
import { LabelMedium, LabelSmall } from "baseui/typography";
9
+
import { LabelMedium } from "baseui/typography";
10
10
import dayjs from "dayjs";
11
11
import { useAtomValue } from "jotai";
12
12
import { useState } from "react";
···
17
17
import SignInModal from "../../../SignInModal";
18
18
import DeleteShoutModal from "./DeleteShoutModal";
19
19
import ReplyModal from "./ReplyModal";
20
-
import scrollToTop from "../../../../lib/scrollToTop";
21
20
22
21
const Link = styled(DefaultLink)`
23
22
color: inherit;
···
184
183
<div className="ml-[20px] w-full">
185
184
<Header>
186
185
<div>
187
-
<Link
188
-
to={`/profile/${shout.user.handle}`}
189
-
className="flex no-underline"
190
-
style={{ textDecoration: "none" }}
191
-
onClick={() => scrollToTop()}
192
-
>
193
-
<LabelMedium className="!text-[var(--color-text)] no-underline">
186
+
<Link to={`/profile/${shout.user.handle}`}>
187
+
<LabelMedium className="!text-[var(--color-text)]">
194
188
{shout.user.displayName}
195
189
</LabelMedium>
196
-
<LabelSmall className="ml-[5px] mt-[4px] no-underline !text-[var(--color-text-muted)]">
197
-
@{shout.user.handle}
198
-
</LabelSmall>
199
190
</Link>
200
191
</div>
201
192
<div>
+8
-14
apps/web/src/components/SignInModal/SignInModal.tsx
+8
-14
apps/web/src/components/SignInModal/SignInModal.tsx
···
8
8
isOpen: boolean;
9
9
onClose: () => void;
10
10
like?: boolean;
11
-
follow?: boolean;
12
11
}
13
12
14
13
function SignInModal(props: SignInModalProps) {
15
-
const { isOpen, onClose, like, follow } = props;
14
+
const { isOpen, onClose, like } = props;
16
15
const [handle, setHandle] = useState("");
17
16
18
17
const onLogin = async () => {
···
34
33
overrides={{
35
34
Root: {
36
35
style: {
37
-
zIndex: 50,
36
+
zIndex: 1,
38
37
},
39
38
},
40
39
Dialog: {
···
58
57
<h1 style={{ color: "#ff2876", textAlign: "center" }}>Rocksky</h1>
59
58
<p className="text-[var(--color-text)] text-[18px] mt-[40px] mb-[20px]">
60
59
{!like
61
-
? !follow
62
-
? "Sign in or create your account to join the conversation!"
63
-
: "Sign in or create your account to follow users!"
60
+
? "Sign in or create your account to join the conversation!"
64
61
: "Sign in or create your account to like songs!"}
65
62
</p>
66
63
<div style={{ marginBottom: 20 }}>
67
64
<div style={{ marginBottom: 15 }}>
68
-
<LabelMedium className="!text-[var(--color-text)]">
69
-
Handle
70
-
</LabelMedium>
65
+
<LabelMedium>Bluesky handle</LabelMedium>
71
66
</div>
72
67
<Input
73
68
name="handle"
···
128
123
marginTop={"20px"}
129
124
className="!text-[var(--color-text-muted)] text-center"
130
125
>
131
-
Don't have an atproto handle yet?
126
+
Don't have an account?
132
127
</LabelMedium>
133
-
<div className="text-[var(--color-text-muted)] text-center text-[16px]">
134
-
You can create one at{" "}
128
+
<div className="text-[var(--color-text-muted)] text-center">
135
129
<a
136
130
href="https://bsky.app"
137
131
className="text-[var(--color-primary)] no-underline cursor-pointer text-center"
138
132
target="_blank"
139
133
>
140
-
Bluesky
134
+
Sign up for Bluesky
141
135
</a>{" "}
142
-
or any other AT Protocol service.
136
+
to create one now!
143
137
</div>
144
138
</ModalBody>
145
139
</Modal>
+1
-1
apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx
+1
-1
apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx
+11
-7
apps/web/src/components/Stats/Stats.tsx
+11
-7
apps/web/src/components/Stats/Stats.tsx
···
1
1
import { css } from "@emotion/react";
2
2
import styled from "@emotion/styled";
3
-
import { HeadingSmall } from "baseui/typography";
3
+
import { HeadingSmall, LabelMedium } from "baseui/typography";
4
4
import numeral from "numeral";
5
5
6
6
const Group = styled.div<{ mb?: number }>`
···
27
27
function Stats(props: StatsProps) {
28
28
const { stats, mb } = props;
29
29
return (
30
-
<Group mb={mb} className="!mb-[0px]">
30
+
<Group mb={mb}>
31
31
<div className="mr-[20px]">
32
-
<b className="!text-[var(--color-text-muted)] text-[13px]">SCROBBLES</b>
32
+
<LabelMedium className="!text-[var(--color-text-muted)]">
33
+
SCROBBLES
34
+
</LabelMedium>
33
35
<HeadingSmall margin={0} className="!text-[var(--color-text)]">
34
36
{numeral(stats?.scrobbles).format("0,0")}
35
37
</HeadingSmall>
36
38
</div>
37
39
<div className="mr-[20px]">
38
-
<b className="!text-[var(--color-text-muted)] text-[13px]">ARTISTS</b>
40
+
<LabelMedium className="!text-[var(--color-text-muted)]">
41
+
ARTISTS
42
+
</LabelMedium>
39
43
<HeadingSmall margin={0} className="!text-[var(--color-text)]">
40
44
{numeral(stats?.artists).format("0,0")}
41
45
</HeadingSmall>
42
46
</div>
43
-
<div>
44
-
<b className="!text-[var(--color-text-muted)] text-[13px]">
47
+
<div className="mr-[20px]">
48
+
<LabelMedium className="!text-[var(--color-text-muted)]">
45
49
LOVED TRACKS
46
-
</b>
50
+
</LabelMedium>
47
51
<HeadingSmall margin={0} className="!text-[var(--color-text)]">
48
52
{numeral(stats?.lovedTracks).format("0,0")}
49
53
</HeadingSmall>
+3
-58
apps/web/src/hooks/useFeed.tsx
+3
-58
apps/web/src/hooks/useFeed.tsx
···
1
-
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
2
-
import {
3
-
getFeedByUri,
4
-
getFeedGenerators,
5
-
getFeed,
6
-
getScrobbles,
7
-
} from "../api/feed";
1
+
import { useQuery } from "@tanstack/react-query";
2
+
import { getFeedByUri, getFeedGenerators, getFeed } from "../api/feed";
8
3
9
4
export const useFeedQuery = (feed: string, limit = 114, cursor?: string) =>
10
5
useQuery({
11
6
queryKey: ["feed", feed],
12
-
queryFn: async () => {
13
-
const data = await getFeed(feed, limit, cursor);
14
-
return data.songs;
15
-
},
7
+
queryFn: () => getFeed(feed, limit, cursor),
16
8
});
17
9
18
10
export const useFeedByUriQuery = (uri: string) =>
···
26
18
queryKey: ["feedGenerators"],
27
19
queryFn: () => getFeedGenerators(),
28
20
});
29
-
30
-
export const useFeedInfiniteQuery = (feed: string, limit = 30) =>
31
-
useInfiniteQuery({
32
-
queryKey: ["infiniteFeed", feed],
33
-
queryFn: async ({ pageParam }) => {
34
-
const data = await getFeed(feed, limit, pageParam);
35
-
return {
36
-
feed: data.songs,
37
-
nextCursor: data.cursor,
38
-
};
39
-
},
40
-
getNextPageParam: (lastPage) => lastPage.nextCursor,
41
-
initialPageParam: undefined as string | undefined,
42
-
});
43
-
44
-
export const useScrobblesQuery = (
45
-
did: string,
46
-
following = false,
47
-
offset = 0,
48
-
limit = 50,
49
-
) =>
50
-
useQuery({
51
-
queryKey: ["scrobbles", did, following, offset, limit],
52
-
queryFn: async () => {
53
-
const data = await getScrobbles(did, following, offset, limit);
54
-
return data.scrobbles;
55
-
},
56
-
});
57
-
58
-
export const useScrobbleInfiniteQuery = (
59
-
did: string,
60
-
following = false,
61
-
limit = 50,
62
-
) =>
63
-
useInfiniteQuery({
64
-
queryKey: ["infiniteScrobbles", did, following],
65
-
queryFn: async ({ pageParam }) => {
66
-
const data = await getScrobbles(did, following, pageParam, limit);
67
-
return {
68
-
scrobbles: data.scrobbles,
69
-
nextOffset:
70
-
data.scrobbles.length === limit ? pageParam + limit : undefined,
71
-
};
72
-
},
73
-
getNextPageParam: (lastPage) => lastPage.nextOffset,
74
-
initialPageParam: 0,
75
-
});
-92
apps/web/src/hooks/useGraph.tsx
-92
apps/web/src/hooks/useGraph.tsx
···
1
-
import {
2
-
useInfiniteQuery,
3
-
useMutation,
4
-
useQuery,
5
-
useQueryClient,
6
-
} from "@tanstack/react-query";
7
-
import {
8
-
getFollowers,
9
-
getFollows,
10
-
followAccount,
11
-
unfollowAccount,
12
-
} from "../api/graph";
13
-
14
-
export const useFollowsQuery = (
15
-
actor: string,
16
-
limit: number,
17
-
dids?: string[],
18
-
cursor?: string,
19
-
) =>
20
-
useQuery({
21
-
queryKey: ["follows", actor, limit, dids, cursor],
22
-
queryFn: () => getFollows(actor, limit, dids, cursor),
23
-
});
24
-
25
-
export const useFollowsInfiniteQuery = (
26
-
actor: string,
27
-
limit: number,
28
-
dids?: string[],
29
-
) =>
30
-
useInfiniteQuery({
31
-
queryKey: ["follows-infinite", actor, dids, limit],
32
-
queryFn: ({ pageParam }) => getFollows(actor, limit, dids, pageParam),
33
-
getNextPageParam: (lastPage) => lastPage.cursor,
34
-
initialPageParam: undefined as string | undefined,
35
-
});
36
-
37
-
export const useFollowersQuery = (
38
-
actor: string | undefined,
39
-
limit: number,
40
-
dids?: string[],
41
-
cursor?: string,
42
-
) =>
43
-
useQuery({
44
-
queryKey: ["followers", actor, limit, dids, cursor],
45
-
queryFn: () => getFollowers(actor!, limit, dids, cursor),
46
-
});
47
-
48
-
export const useFollowersInfiniteQuery = (
49
-
actor: string,
50
-
limit: number,
51
-
dids?: string[],
52
-
) =>
53
-
useInfiniteQuery({
54
-
queryKey: ["followers-infinite", actor, limit, dids],
55
-
queryFn: ({ pageParam }) => getFollowers(actor, limit, dids, pageParam),
56
-
getNextPageParam: (lastPage) => lastPage.cursor,
57
-
initialPageParam: undefined as string | undefined,
58
-
});
59
-
60
-
export const useFollowAccountMutation = () => {
61
-
const queryClient = useQueryClient();
62
-
return useMutation({
63
-
mutationFn: followAccount,
64
-
onSuccess: (_data, account) => {
65
-
queryClient.invalidateQueries({ queryKey: ["follows"] });
66
-
queryClient.invalidateQueries({ queryKey: ["followers", account] });
67
-
queryClient.invalidateQueries({
68
-
queryKey: ["follows-infinite"],
69
-
});
70
-
queryClient.invalidateQueries({
71
-
queryKey: ["followers-infinite"],
72
-
});
73
-
},
74
-
});
75
-
};
76
-
77
-
export const useUnfollowAccountMutation = () => {
78
-
const queryClient = useQueryClient();
79
-
return useMutation({
80
-
mutationFn: unfollowAccount,
81
-
onSuccess: (_data, account) => {
82
-
queryClient.invalidateQueries({ queryKey: ["follows"] });
83
-
queryClient.invalidateQueries({ queryKey: ["followers", account] });
84
-
queryClient.invalidateQueries({
85
-
queryKey: ["follows-infinite"],
86
-
});
87
-
queryClient.invalidateQueries({
88
-
queryKey: ["followers-infinite"],
89
-
});
90
-
},
91
-
});
92
-
};
+1
-1
apps/web/src/hooks/useNowPlaying.tsx
+1
-1
apps/web/src/hooks/useNowPlaying.tsx
-16
apps/web/src/hooks/useProfile.tsx
-16
apps/web/src/hooks/useProfile.tsx
···
2
2
import { useSetAtom } from "jotai";
3
3
import { useEffect, useState } from "react";
4
4
import {
5
-
getActorCompatibility,
6
-
getActorNeighbours,
7
5
getProfileByDid,
8
6
getProfileStatsByDid,
9
7
getRecentTracksByDid,
···
29
27
useQuery({
30
28
queryKey: ["profile", "recent-tracks", did, offset, size],
31
29
queryFn: () => getRecentTracksByDid(did, offset, size),
32
-
enabled: !!did,
33
-
});
34
-
35
-
export const useActorNeighboursQuery = (did: string) =>
36
-
useQuery({
37
-
queryKey: ["profile", "neighbours", did],
38
-
queryFn: () => getActorNeighbours(did),
39
-
enabled: !!did,
40
-
});
41
-
42
-
export const useActorCompatibilityQuery = (did: string | undefined) =>
43
-
useQuery({
44
-
queryKey: ["profile", "compatibility", did],
45
-
queryFn: () => getActorCompatibility(did!),
46
30
enabled: !!did,
47
31
});
48
32
+1
-1
apps/web/src/index.css
+1
-1
apps/web/src/index.css
···
103
103
--color-placeholder: #ffffffb4;
104
104
--color-clear-input: #ffffff;
105
105
--color-menu-hover: #1f0d3c;
106
-
--color-default-button: rgba(255, 255, 255, 0.06);
106
+
--color-default-button: rgba(255, 255, 255, 0.05);
107
107
--color-purple: oklch(0.5478 0.201 296.07);
108
108
--color-blue: oklch(0.789 0.154 211.53);
109
109
--color-pink: oklch(0.6372 0.229 346.83);
+7
-116
apps/web/src/layouts/Main.tsx
+7
-116
apps/web/src/layouts/Main.tsx
···
16
16
import Navbar from "./Navbar";
17
17
import Search from "./Search";
18
18
import SpotifyLogin from "./SpotifyLogin";
19
-
import { IconEye, IconEyeOff, IconLock } from "@tabler/icons-react";
20
19
21
20
const Container = styled.div`
22
21
display: flex;
···
60
59
const { children } = props;
61
60
const withRightPane = props.withRightPane ?? true;
62
61
const [handle, setHandle] = useState("");
63
-
const [password, setPassword] = useState("");
64
62
const jwt = localStorage.getItem("token");
65
63
const profile = useAtomValue(profileAtom);
66
64
const [token, setToken] = useState<string | null>(null);
67
65
const { did, cli } = useSearch({ strict: false });
68
-
const [passwordLogin, setPasswordLogin] = useState(false);
69
66
70
67
useEffect(() => {
71
68
if (did && did !== "null") {
···
112
109
return;
113
110
}
114
111
115
-
if (passwordLogin) {
116
-
if (!password.trim()) {
117
-
return;
118
-
}
119
-
120
-
const response = await fetch(`${API_URL}/login`, {
121
-
method: "POST",
122
-
headers: {
123
-
"Content-Type": "application/json",
124
-
},
125
-
body: JSON.stringify({ handle, password }),
126
-
});
127
-
128
-
if (!response.ok) {
129
-
const error = await response.text();
130
-
alert(error);
131
-
return;
132
-
}
133
-
134
-
const data = await response.text();
135
-
const newToken = data.split("jwt:")[1];
136
-
localStorage.setItem("token", newToken);
137
-
setToken(data);
138
-
139
-
if (cli) {
140
-
await fetch("http://localhost:6996/token", {
141
-
method: "POST",
142
-
headers: {
143
-
"Content-Type": "application/json",
144
-
},
145
-
body: JSON.stringify({ token: newToken }),
146
-
});
147
-
}
148
-
149
-
if (!jwt && newToken) {
150
-
window.location.href = "/";
151
-
}
152
-
153
-
return;
154
-
}
155
-
156
112
if (API_URL.includes("localhost")) {
157
113
window.location.href = `${API_URL}/login?handle=${handle}`;
158
114
return;
···
162
118
};
163
119
164
120
return (
165
-
<Container
166
-
id="app-container"
167
-
className="bg-[var(--color-background)] text-[var(--color-text)]"
168
-
>
121
+
<Container className="bg-[var(--color-background)] text-[var(--color-text)]">
169
122
<ToasterContainer
170
123
placement={PLACEMENT.top}
171
124
overrides={{
···
198
151
{!jwt && (
199
152
<div className="mt-[40px]">
200
153
<div className="mb-[20px]">
201
-
<div className="flex flex-row mb-[15px]">
202
-
<LabelMedium className="!text-[var(--color-text)] flex-1">
203
-
Handle
204
-
</LabelMedium>
205
-
<LabelMedium
206
-
className="!text-[var(--color-primary)] cursor-pointer"
207
-
onClick={() => setPasswordLogin(!passwordLogin)}
208
-
>
209
-
{passwordLogin ? "OAuth Login" : "Password Login"}
154
+
<div className="mb-[15px]">
155
+
<LabelMedium className="!text-[var(--color-text)]">
156
+
Bluesky handle
210
157
</LabelMedium>
211
158
</div>
212
159
<Input
···
244
191
},
245
192
}}
246
193
/>
247
-
{passwordLogin && (
248
-
<Input
249
-
name="password"
250
-
startEnhancer={
251
-
<div className="text-[var(--color-text-muted)] bg-[var(--color-input-background)]">
252
-
<IconLock size={19} className="mt-[8px]" />
253
-
</div>
254
-
}
255
-
type="password"
256
-
placeholder="Password"
257
-
value={password}
258
-
onChange={(e) => setPassword(e.target.value)}
259
-
overrides={{
260
-
Root: {
261
-
style: {
262
-
backgroundColor: "var(--color-input-background)",
263
-
borderColor: "var(--color-input-background)",
264
-
marginTop: "1rem",
265
-
},
266
-
},
267
-
StartEnhancer: {
268
-
style: {
269
-
backgroundColor: "var(--color-input-background)",
270
-
},
271
-
},
272
-
InputContainer: {
273
-
style: {
274
-
backgroundColor: "var(--color-input-background)",
275
-
},
276
-
},
277
-
Input: {
278
-
style: {
279
-
color: "var(--color-text)",
280
-
caretColor: "var(--color-text)",
281
-
},
282
-
},
283
-
MaskToggleHideIcon: {
284
-
component: () => (
285
-
<IconEyeOff
286
-
className="text-[var(--color-text-muted)]"
287
-
size={20}
288
-
/>
289
-
),
290
-
},
291
-
MaskToggleShowIcon: {
292
-
component: () => (
293
-
<IconEye
294
-
className="text-[var(--color-text-muted)]"
295
-
size={20}
296
-
/>
297
-
),
298
-
},
299
-
}}
300
-
/>
301
-
)}
302
194
</div>
303
195
<Button
304
196
onClick={onLogin}
···
320
212
Sign In
321
213
</Button>
322
214
<LabelMedium className="text-center mt-[20px] !text-[var(--color-text-muted)]">
323
-
Don't have an atproto handle yet?
215
+
Don't have an account?
324
216
</LabelMedium>
325
217
<div className="text-center text-[var(--color-text-muted)] ">
326
-
You can create one at{" "}
327
218
<a
328
219
href="https://bsky.app"
329
220
className="no-underline cursor-pointer !text-[var(--color-primary)]"
330
221
target="_blank"
331
222
>
332
-
Bluesky
223
+
Sign up for Bluesky
333
224
</a>{" "}
334
-
or any other AT Protocol service.
225
+
to create one now!
335
226
</div>
336
227
</div>
337
228
)}
-6
apps/web/src/lib/scrollToTop.ts
-6
apps/web/src/lib/scrollToTop.ts
+3
-78
apps/web/src/pages/apikeys/ApiKeys.tsx
+3
-78
apps/web/src/pages/apikeys/ApiKeys.tsx
···
305
305
overrides={{
306
306
Root: {
307
307
style: {
308
-
zIndex: 50,
309
-
},
310
-
},
311
-
Dialog: {
312
-
style: {
313
-
backgroundColor: "var(--color-background)",
314
-
},
315
-
},
316
-
Close: {
317
-
style: {
318
-
color: "var(--color-text)",
319
-
":hover": {
320
-
color: "var(--color-text)",
321
-
opacity: 0.8,
322
-
},
308
+
zIndex: 1,
323
309
},
324
310
},
325
311
}}
326
312
>
327
-
<ModalHeader className="!text-[var(--color-text)]">
328
-
Create a new API key
329
-
</ModalHeader>
313
+
<ModalHeader>Create a new API key</ModalHeader>
330
314
<ModalBody>
331
315
<Controller
332
316
name="name"
···
337
321
placeholder="Name"
338
322
clearOnEscape
339
323
error={!!errors.name}
340
-
overrides={{
341
-
Root: {
342
-
style: {
343
-
backgroundColor: "var(--color-input-background)",
344
-
borderColor: "var(--color-input-background)",
345
-
},
346
-
},
347
-
StartEnhancer: {
348
-
style: {
349
-
backgroundColor: "var(--color-input-background)",
350
-
},
351
-
},
352
-
InputContainer: {
353
-
style: {
354
-
backgroundColor: "var(--color-input-background)",
355
-
},
356
-
},
357
-
Input: {
358
-
style: {
359
-
color: "var(--color-text)",
360
-
caretColor: "var(--color-text)",
361
-
},
362
-
},
363
-
}}
364
324
/>
365
325
)}
366
326
/>
···
376
336
overrides={{
377
337
Root: {
378
338
style: {
379
-
backgroundColor: "var(--color-input-background)",
380
-
borderColor: "var(--color-input-background)",
381
339
marginTop: "20px",
382
340
},
383
341
},
384
-
InputContainer: {
385
-
style: {
386
-
backgroundColor: "var(--color-input-background)",
387
-
},
388
-
},
389
-
Input: {
390
-
style: {
391
-
color: "var(--color-text)",
392
-
caretColor: "var(--color-text)",
393
-
},
394
-
},
395
342
}}
396
343
/>
397
344
)}
···
405
352
BaseButton: {
406
353
style: {
407
354
marginRight: "10px",
408
-
backgroundColor: "var(--color-background) !important",
409
-
color: "var(--color-text) !important",
410
-
":hover": {
411
-
backgroundColor: "var(--color-background)",
412
-
},
413
355
},
414
356
},
415
357
}}
416
358
>
417
359
Cancel
418
360
</Button>
419
-
<Button
420
-
onClick={onCreate}
421
-
shape="pill"
422
-
overrides={{
423
-
BaseButton: {
424
-
style: {
425
-
backgroundColor: "var(--color-purple) !important",
426
-
color: "var(--color-button-text) !important",
427
-
":hover": {
428
-
backgroundColor: "var(--color-purple)",
429
-
color: "var(--color-button-text) !important",
430
-
},
431
-
},
432
-
},
433
-
}}
434
-
>
435
-
Create
436
-
</Button>
361
+
<Button onClick={onCreate}>Create</Button>
437
362
</ModalFooter>
438
363
</Modal>
439
364
</Main>
+73
-190
apps/web/src/pages/home/feed/Feed.tsx
+73
-190
apps/web/src/pages/home/feed/Feed.tsx
···
10
10
import ContentLoader from "react-content-loader";
11
11
import Handle from "../../../components/Handle";
12
12
import SongCover from "../../../components/SongCover";
13
-
import {
14
-
useFeedInfiniteQuery,
15
-
useScrobbleInfiniteQuery,
16
-
} from "../../../hooks/useFeed";
13
+
import { useFeedQuery } from "../../../hooks/useFeed";
17
14
import { useEffect, useRef } from "react";
18
15
import { WS_URL } from "../../../consts";
19
16
import { useQueryClient } from "@tanstack/react-query";
20
17
import FeedGenerators from "./FeedGenerators";
21
18
import { useAtomValue } from "jotai";
22
19
import { feedGeneratorUriAtom } from "../../../atoms/feed";
23
-
import { followingFeedAtom } from "../../../atoms/followingFeed";
24
20
25
21
dayjs.extend(relativeTime);
26
22
···
41
37
const queryClient = useQueryClient();
42
38
const socketRef = useRef<WebSocket | null>(null);
43
39
const heartbeatInterval = useRef<number | null>(null);
44
-
const loadMoreRef = useRef<HTMLDivElement | null>(null);
45
40
const feedUri = useAtomValue(feedGeneratorUriAtom);
46
-
const followingFeed = useAtomValue(followingFeedAtom);
47
-
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } =
48
-
useFeedInfiniteQuery(feedUri, 30);
49
-
const {
50
-
data: scrobbleData,
51
-
isLoading: scrobbleLoading,
52
-
fetchNextPage: scrobbleFetchNextPage,
53
-
hasNextPage: scrobbleHasNextPage,
54
-
isFetchingNextPage: scrobbleIsFetchingNextPage,
55
-
} = useScrobbleInfiniteQuery(localStorage.getItem("did")!, true, 30);
56
-
57
-
const allSongs = followingFeed
58
-
? scrobbleData?.pages.flatMap((page) => page.scrobbles) || []
59
-
: data?.pages.flatMap((page) => page.feed) || [];
41
+
const { data, isLoading } = useFeedQuery(feedUri);
60
42
61
43
useEffect(() => {
62
44
const ws = new WebSocket(`${WS_URL.replace("http", "ws")}`);
···
80
62
() => message.scrobblesChart,
81
63
);
82
64
83
-
await queryClient.invalidateQueries({
84
-
queryKey: ["infiniteFeed", feedUri],
85
-
});
65
+
await queryClient.invalidateQueries({ queryKey: ["feed", feedUri] });
86
66
await queryClient.invalidateQueries({ queryKey: ["now-playings"] });
87
67
await queryClient.invalidateQueries({ queryKey: ["scrobblesChart"] });
88
68
};
···
98
78
};
99
79
}, [queryClient, feedUri]);
100
80
101
-
// Intersection Observer for infinite scroll
102
-
useEffect(() => {
103
-
const currentHasNextPage = followingFeed
104
-
? scrobbleHasNextPage
105
-
: hasNextPage;
106
-
const currentIsFetchingNextPage = followingFeed
107
-
? scrobbleIsFetchingNextPage
108
-
: isFetchingNextPage;
109
-
110
-
if (
111
-
!loadMoreRef.current ||
112
-
!currentHasNextPage ||
113
-
currentIsFetchingNextPage
114
-
)
115
-
return;
116
-
117
-
const observer = new IntersectionObserver(
118
-
(entries) => {
119
-
if (
120
-
entries[0].isIntersecting &&
121
-
currentHasNextPage &&
122
-
!currentIsFetchingNextPage
123
-
) {
124
-
if (followingFeed) {
125
-
scrobbleFetchNextPage();
126
-
} else {
127
-
fetchNextPage();
128
-
}
129
-
}
130
-
},
131
-
{ threshold: 0.1 },
132
-
);
133
-
134
-
observer.observe(loadMoreRef.current);
135
-
136
-
return () => observer.disconnect();
137
-
}, [
138
-
followingFeed,
139
-
fetchNextPage,
140
-
hasNextPage,
141
-
isFetchingNextPage,
142
-
scrobbleFetchNextPage,
143
-
scrobbleHasNextPage,
144
-
scrobbleIsFetchingNextPage,
145
-
]);
146
-
147
81
return (
148
82
<Container>
149
83
<FeedGenerators />
150
-
{(isLoading || scrobbleLoading) && (
84
+
{isLoading && (
151
85
<ContentLoader
152
-
width="100%"
153
-
height={800}
154
-
viewBox="0 0 1100 800"
86
+
width={800}
87
+
height={575}
88
+
viewBox="0 0 800 575"
155
89
backgroundColor="var(--color-skeleton-background)"
156
90
foregroundColor="var(--color-skeleton-foreground)"
157
-
style={{ marginTop: "-100px" }}
158
91
>
159
-
{/* First row - 3 items with 24px gap (scale800) */}
160
-
<rect x="0" y="0" rx="2" ry="2" width="349" height="349" />
161
-
<rect x="373" y="0" rx="2" ry="2" width="349" height="349" />
162
-
<rect x="746" y="0" rx="2" ry="2" width="349" height="349" />
163
-
164
-
{/* Second row - 3 items with 32px row gap (scale1000) */}
165
-
<rect x="0" y="381" rx="2" ry="2" width="349" height="349" />
166
-
<rect x="373" y="381" rx="2" ry="2" width="349" height="349" />
167
-
<rect x="746" y="381" rx="2" ry="2" width="349" height="349" />
92
+
<rect x="12" y="9" rx="2" ry="2" width="140" height="10" />
93
+
<rect x="14" y="30" rx="2" ry="2" width="667" height="11" />
94
+
<rect x="12" y="58" rx="2" ry="2" width="211" height="211" />
95
+
<rect x="240" y="57" rx="2" ry="2" width="211" height="211" />
96
+
<rect x="467" y="56" rx="2" ry="2" width="211" height="211" />
97
+
<rect x="12" y="283" rx="2" ry="2" width="211" height="211" />
98
+
<rect x="240" y="281" rx="2" ry="2" width="211" height="211" />
99
+
<rect x="468" y="279" rx="2" ry="2" width="211" height="211" />
100
+
<circle cx="286" cy="536" r="12" />
101
+
<circle cx="319" cy="535" r="12" />
102
+
<circle cx="353" cy="535" r="12" />
103
+
<rect x="378" y="524" rx="0" ry="0" width="52" height="24" />
104
+
<rect x="210" y="523" rx="0" ry="0" width="52" height="24" />
105
+
<circle cx="210" cy="535" r="12" />
106
+
<circle cx="428" cy="536" r="12" />
168
107
</ContentLoader>
169
108
)}
170
109
171
-
{!isLoading && !scrobbleLoading && (
110
+
{!isLoading && (
172
111
<div className="pb-[100px] pt-[20px]">
173
-
{followingFeed && allSongs.length === 0 ? (
174
-
<div className="flex flex-col items-center justify-center py-20 mt-[100px]">
175
-
<LabelSmall className="!text-[var(--color-text-muted)] text-center">
176
-
No scrobbles from people you follow yet.
177
-
<br />
178
-
Start following users to see their music activity here.
179
-
</LabelSmall>
180
-
</div>
181
-
) : (
182
-
<>
183
-
<FlexGrid
184
-
flexGridColumnCount={[1, 2, 3]}
185
-
flexGridColumnGap="scale800"
186
-
flexGridRowGap="scale1000"
187
-
>
188
-
{
189
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
190
-
allSongs.map((song: any) => (
191
-
<FlexGridItem {...itemProps} key={song.id}>
192
-
<Link
193
-
to="/$did/scrobble/$rkey"
194
-
params={{
195
-
did: song.uri?.split("at://")[1]?.split("/")[0] || "",
196
-
rkey: song.uri?.split("/").pop() || "",
197
-
}}
198
-
className="no-underline text-[var(--color-text-primary)]"
199
-
>
200
-
<SongCover
201
-
uri={song.trackUri}
202
-
cover={song.cover}
203
-
artist={song.artist}
204
-
title={song.title}
205
-
liked={song.liked}
206
-
likesCount={song.likesCount}
207
-
withLikeButton
208
-
/>
209
-
</Link>
210
-
<div className="flex">
211
-
<div className="mr-[8px]">
212
-
<Avatar
213
-
src={song.userAvatar}
214
-
name={song.userDisplayName}
215
-
size={"20px"}
216
-
/>
217
-
</div>
218
-
<Handle
219
-
link={`/profile/${song.user}`}
220
-
did={song.user}
221
-
/>{" "}
222
-
</div>
223
-
<LabelSmall className="!text-[var(--color-text-primary)]">
224
-
recently played this song
225
-
</LabelSmall>
226
-
<StatefulTooltip
227
-
content={dayjs(song.date).format(
228
-
"MMMM D, YYYY [at] HH:mm A",
229
-
)}
230
-
returnFocus
231
-
autoFocus
232
-
>
233
-
<LabelSmall className="!text-[var(--color-text-muted)]">
234
-
{dayjs(song.date).fromNow()}
235
-
</LabelSmall>
236
-
</StatefulTooltip>
237
-
</FlexGridItem>
238
-
))
239
-
}
240
-
</FlexGrid>
241
-
242
-
{/* Load more trigger */}
243
-
<div
244
-
ref={loadMoreRef}
245
-
style={{ height: "20px", marginTop: "20px" }}
246
-
>
247
-
{(followingFeed
248
-
? scrobbleIsFetchingNextPage
249
-
: isFetchingNextPage) && (
250
-
<ContentLoader
251
-
width="100%"
252
-
height={360}
253
-
viewBox="0 0 1100 360"
254
-
backgroundColor="var(--color-skeleton-background)"
255
-
foregroundColor="var(--color-skeleton-foreground)"
256
-
>
257
-
{/* 3 items with 24px gap (scale800) */}
258
-
<rect x="0" y="10" rx="2" ry="2" width="349" height="349" />
259
-
<rect
260
-
x="373"
261
-
y="10"
262
-
rx="2"
263
-
ry="2"
264
-
width="349"
265
-
height="349"
112
+
<FlexGrid
113
+
flexGridColumnCount={[1, 2, 3]}
114
+
flexGridColumnGap="scale800"
115
+
flexGridRowGap="scale1000"
116
+
>
117
+
{// eslint-disable-next-line @typescript-eslint/no-explicit-any
118
+
data?.map((song: any) => (
119
+
<FlexGridItem {...itemProps} key={song.id}>
120
+
<Link
121
+
to="/$did/scrobble/$rkey"
122
+
params={{
123
+
did: song.uri?.split("at://")[1]?.split("/")[0] || "",
124
+
rkey: song.uri?.split("/").pop() || "",
125
+
}}
126
+
className="no-underline text-[var(--color-text-primary)]"
127
+
>
128
+
<SongCover
129
+
uri={song.trackUri}
130
+
cover={song.cover}
131
+
artist={song.artist}
132
+
title={song.title}
133
+
liked={song.liked}
134
+
likesCount={song.likesCount}
135
+
withLikeButton
136
+
/>
137
+
</Link>
138
+
<div className="flex">
139
+
<div className="mr-[8px]">
140
+
<Avatar
141
+
src={song.userAvatar}
142
+
name={song.userDisplayName}
143
+
size={"20px"}
266
144
/>
267
-
<rect
268
-
x="746"
269
-
y="10"
270
-
rx="2"
271
-
ry="2"
272
-
width="349"
273
-
height="349"
274
-
/>
275
-
</ContentLoader>
276
-
)}
277
-
</div>
278
-
</>
279
-
)}
145
+
</div>
146
+
<Handle link={`/profile/${song.user}`} did={song.user} />{" "}
147
+
</div>
148
+
<LabelSmall className="!text-[var(--color-text-primary)]">
149
+
recently played this song
150
+
</LabelSmall>
151
+
<StatefulTooltip
152
+
content={dayjs(song.date).format("MMMM D, YYYY [at] HH:mm A")}
153
+
returnFocus
154
+
autoFocus
155
+
>
156
+
<LabelSmall className="!text-[var(--color-text-muted)]">
157
+
{dayjs(song.date).fromNow()}
158
+
</LabelSmall>
159
+
</StatefulTooltip>
160
+
</FlexGridItem>
161
+
))}
162
+
</FlexGrid>
280
163
</div>
281
164
)}
282
165
</Container>
+1
-8
apps/web/src/pages/home/feed/FeedGenerators/FeedGenerators.tsx
+1
-8
apps/web/src/pages/home/feed/FeedGenerators/FeedGenerators.tsx
···
9
9
} from "../../../../atoms/feed";
10
10
import { useFeedGeneratorsQuery } from "../../../../hooks/useFeed";
11
11
import * as R from "ramda";
12
-
import { followingFeedAtom } from "../../../../atoms/followingFeed";
13
12
14
13
function FeedGenerators() {
15
14
const jwt = localStorage.getItem("token");
16
15
const { data: feedGenerators } = useFeedGeneratorsQuery();
17
16
const [feedUris, setFeedUris] = useAtom(feedUrisAtom);
18
17
const [, setFeedUri] = useAtom(feedGeneratorUriAtom);
19
-
const [, setFollowingFeed] = useAtom(followingFeedAtom);
20
18
const [activeCategory, setActiveCategory] = useAtom(feedAtom);
21
19
const [showLeftChevron, setShowLeftChevron] = useState(false);
22
20
const [showRightChevron, setShowRightChevron] = useState(true);
···
76
74
77
75
const handleCategoryClick = (category: string, index: number) => {
78
76
setActiveCategory(category);
79
-
if (category !== "following") {
80
-
setFeedUri(feedUris[category]);
81
-
setFollowingFeed(false);
82
-
} else {
83
-
setFollowingFeed(true);
84
-
}
77
+
setFeedUri(feedUris[category]);
85
78
86
79
const container = scrollContainerRef.current;
87
80
if (container) {
+1
-4
apps/web/src/pages/home/feed/FeedGenerators/constants.ts
+1
-4
apps/web/src/pages/home/feed/FeedGenerators/constants.ts
···
1
1
export const categories = [
2
2
"all",
3
-
"following",
4
3
"afrobeat",
5
4
"afrobeats",
6
5
"alternative metal",
···
51
50
"visual kei",
52
51
"vocaloid",
53
52
"west coast hip hop",
54
-
].filter((category) =>
55
-
localStorage.getItem("did") ? true : category !== "following",
56
-
);
53
+
];
+21
-152
apps/web/src/pages/home/nowplayings/NowPlayings.tsx
+21
-152
apps/web/src/pages/home/nowplayings/NowPlayings.tsx
···
7
7
import dayjs from "dayjs";
8
8
import relativeTime from "dayjs/plugin/relativeTime";
9
9
import utc from "dayjs/plugin/utc";
10
-
import { useEffect, useState, useRef } from "react";
11
-
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
10
+
import { useEffect, useState } from "react";
12
11
import { useNowPlayingsQuery } from "../../../hooks/useNowPlaying";
13
-
import styles, { getModalStyles } from "./styles";
12
+
import styles from "./styles";
14
13
15
14
dayjs.extend(relativeTime);
16
15
dayjs.extend(utc);
17
16
18
17
const Container = styled.div`
18
+
display: flex;
19
+
flex-direction: row;
20
+
align-items: center;
19
21
margin-bottom: 50px;
20
22
`;
21
23
···
34
36
align-items: center;
35
37
margin-right: 20px;
36
38
cursor: pointer;
37
-
flex-shrink: 0;
38
39
`;
39
40
40
41
const Handle = styled.div`
···
105
106
} | null>(null);
106
107
const [currentIndex, setCurrentIndex] = useState(0);
107
108
const [progress, setProgress] = useState(0);
108
-
const [showLeftChevron, setShowLeftChevron] = useState(false);
109
-
const [showRightChevron, setShowRightChevron] = useState(true);
110
-
const [hasOverflow, setHasOverflow] = useState(false);
111
-
const scrollContainerRef = useRef<HTMLDivElement>(null);
112
109
113
110
const onNext = () => {
114
111
const nextIndex = currentIndex + 1;
···
135
132
setProgress(0);
136
133
};
137
134
138
-
// Check scroll position and update chevron visibility
139
-
const handleScroll = () => {
140
-
const container = scrollContainerRef.current;
141
-
if (!container) return;
142
-
143
-
const { scrollLeft, scrollWidth, clientWidth } = container;
144
-
145
-
// Check if content overflows
146
-
const overflow = scrollWidth > clientWidth;
147
-
setHasOverflow(overflow);
148
-
149
-
// Show left chevron if scrolled from the start
150
-
setShowLeftChevron(scrollLeft > 10);
151
-
152
-
// Show right chevron if not scrolled to the end
153
-
setShowRightChevron(scrollLeft < scrollWidth - clientWidth - 10);
154
-
};
155
-
156
-
// Scroll left/right
157
-
const scroll = (direction: "left" | "right") => {
158
-
const container = scrollContainerRef.current;
159
-
if (!container) return;
160
-
161
-
const scrollAmount = 300;
162
-
const newScrollLeft =
163
-
direction === "left"
164
-
? container.scrollLeft - scrollAmount
165
-
: container.scrollLeft + scrollAmount;
166
-
167
-
container.scrollTo({
168
-
left: newScrollLeft,
169
-
behavior: "smooth",
170
-
});
171
-
};
172
-
173
-
// Check overflow on mount and window resize
174
-
useEffect(() => {
175
-
handleScroll();
176
-
177
-
const handleResize = () => {
178
-
handleScroll();
179
-
};
180
-
181
-
window.addEventListener("resize", handleResize);
182
-
return () => window.removeEventListener("resize", handleResize);
183
-
}, [nowPlayings]);
184
-
185
135
useEffect(() => {
186
136
if (!isOpen) {
187
137
setProgress(0);
···
220
170
221
171
return (
222
172
<Container>
223
-
<style>{`
224
-
.no-scrollbar::-webkit-scrollbar {
225
-
display: none;
226
-
}
227
-
.no-scrollbar {
228
-
-ms-overflow-style: none;
229
-
scrollbar-width: none;
230
-
}
231
-
`}</style>
232
-
233
173
{!isLoading && (
234
174
<>
235
-
<div className="relative flex items-center h-[100px]">
236
-
{/* Left chevron */}
237
-
{showLeftChevron && (
238
-
<button
239
-
onClick={() => scroll("left")}
240
-
className="flex-shrink-0 w-8 h-8 min-w-8 min-h-8 p-0 rounded-full bg-transparent hover:bg-[var(--color-input-background)] flex items-center justify-center transition-all outline-none border-none cursor-pointer shadow-md z-0 mt-[-20px]"
241
-
style={{ padding: "5px" }}
242
-
>
243
-
<IconChevronLeft
244
-
size={20}
245
-
className="text-[var(--color-text)] flex-shrink-0"
246
-
/>
247
-
</button>
248
-
)}
249
-
250
-
<div
251
-
className="relative flex-1 overflow-hidden"
252
-
style={
253
-
hasOverflow
254
-
? {
255
-
maskImage:
256
-
showLeftChevron && showRightChevron
257
-
? "linear-gradient(to right, transparent, black 30px, black calc(100% - 30px), transparent)"
258
-
: showLeftChevron
259
-
? "linear-gradient(to right, transparent, black 30px, black 100%)"
260
-
: showRightChevron
261
-
? "linear-gradient(to right, black 0%, black calc(100% - 30px), transparent)"
262
-
: undefined,
263
-
WebkitMaskImage:
264
-
showLeftChevron && showRightChevron
265
-
? "linear-gradient(to right, transparent, black 30px, black calc(100% - 30px), transparent)"
266
-
: showLeftChevron
267
-
? "linear-gradient(to right, transparent, black 30px, black 100%)"
268
-
: showRightChevron
269
-
? "linear-gradient(to right, black 0%, black calc(100% - 30px), transparent)"
270
-
: undefined,
271
-
}
272
-
: undefined
273
-
}
175
+
{(nowPlayings || []).map((item, index) => (
176
+
<StoryContainer
177
+
key={item.id}
178
+
onClick={() => {
179
+
setCurrentlyPlaying(item);
180
+
setCurrentIndex(index);
181
+
setIsOpen(true);
182
+
}}
274
183
>
275
-
<div
276
-
ref={scrollContainerRef}
277
-
onScroll={handleScroll}
278
-
className="flex overflow-x-auto no-scrollbar h-full"
279
-
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
280
-
>
281
-
{(nowPlayings || []).map((item, index) => (
282
-
<StoryContainer
283
-
key={item.id}
284
-
onClick={() => {
285
-
setCurrentlyPlaying(item);
286
-
setCurrentIndex(index);
287
-
setIsOpen(true);
288
-
}}
289
-
>
290
-
<Story src={item.avatar} />
291
-
<StatefulTooltip
292
-
content={item.handle}
293
-
returnFocus
294
-
autoFocus
295
-
>
296
-
<Handle>{item.handle}</Handle>
297
-
</StatefulTooltip>
298
-
</StoryContainer>
299
-
))}
300
-
</div>
301
-
</div>
302
-
303
-
{/* Right chevron */}
304
-
{showRightChevron && (
305
-
<button
306
-
onClick={() => scroll("right")}
307
-
className="flex-shrink-0 w-8 h-8 min-w-8 min-h-8 p-0 rounded-full bg-transparent hover:bg-[var(--color-input-background)] flex items-center justify-center transition-all outline-none border-none cursor-pointer shadow-md z-0 mt-[-20px]"
308
-
style={{ padding: "5px" }}
309
-
>
310
-
<IconChevronRight
311
-
size={20}
312
-
className="text-[var(--color-text)] flex-shrink-0"
313
-
/>
314
-
</button>
315
-
)}
316
-
</div>
317
-
184
+
<Story src={item.avatar} />
185
+
<StatefulTooltip content={item.handle} returnFocus autoFocus>
186
+
<Handle>{item.handle}</Handle>
187
+
</StatefulTooltip>
188
+
</StoryContainer>
189
+
))}
318
190
<Modal
319
191
onClose={() => setIsOpen(false)}
320
192
closeable
···
323
195
autoFocus={false}
324
196
size={"60vw"}
325
197
role={ROLE.dialog}
326
-
overrides={getModalStyles(currentlyPlaying?.albumArt).modal}
198
+
overrides={styles.modal}
327
199
>
328
200
<ModalHeader className="text-[#fff] text-[15px]">
329
201
<div className="w-[380px] mx-auto">
···
338
210
@{currentlyPlaying?.handle}
339
211
</div>
340
212
</Link>
341
-
<span
342
-
className="ml-[10px] text-[15px]"
343
-
style={{ color: "rgba(255, 255, 255, 0.7)" }}
344
-
>
213
+
<span className="ml-[10px] text-[15px] text-[var(--color-text-muted)]">
345
214
{dayjs.utc(currentlyPlaying?.createdAt).local().fromNow()}
346
215
</span>
347
216
</div>
-44
apps/web/src/pages/home/nowplayings/styles.tsx
-44
apps/web/src/pages/home/nowplayings/styles.tsx
···
1
-
export const getModalStyles = (albumArt?: string) => ({
2
-
modal: {
3
-
Root: {
4
-
style: {
5
-
zIndex: 60,
6
-
},
7
-
},
8
-
Dialog: {
9
-
style: {
10
-
backgroundColor: albumArt ? "transparent" : "#000",
11
-
backgroundImage: albumArt
12
-
? `linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url(${albumArt})`
13
-
: "none",
14
-
backgroundSize: "cover",
15
-
backgroundPosition: "center",
16
-
backgroundRepeat: "no-repeat",
17
-
},
18
-
},
19
-
Close: {
20
-
style: {
21
-
color: "#fff",
22
-
},
23
-
},
24
-
},
25
-
progressbar: {
26
-
BarContainer: {
27
-
style: {
28
-
marginLeft: 0,
29
-
marginRight: 0,
30
-
},
31
-
},
32
-
BarProgress: {
33
-
style: () => ({
34
-
backgroundColor: "rgba(255, 255, 255, 0.2)",
35
-
}),
36
-
},
37
-
Bar: {
38
-
style: () => ({
39
-
backgroundColor: "rgba(177, 178, 181, 0.218)",
40
-
}),
41
-
},
42
-
},
43
-
});
44
-
45
1
export default {
46
2
modal: {
47
3
Root: {
+46
-229
apps/web/src/pages/profile/Profile.tsx
+46
-229
apps/web/src/pages/profile/Profile.tsx
···
7
7
import dayjs from "dayjs";
8
8
import { useAtom, useSetAtom } from "jotai";
9
9
import _ from "lodash";
10
-
import { useEffect, useState } from "react";
10
+
import { type Key, useEffect, useState } from "react";
11
11
import { profilesAtom } from "../../atoms/profiles";
12
12
import { userAtom } from "../../atoms/user";
13
13
import Shout from "../../components/Shout/Shout";
···
17
17
import LovedTracks from "./lovedtracks";
18
18
import Overview from "./overview";
19
19
import Playlists from "./playlists";
20
-
import { Button } from "baseui/button";
21
-
import { IconPlus, IconCheck } from "@tabler/icons-react";
22
-
import { followsAtom } from "../../atoms/follows";
23
-
import SignInModal from "../../components/SignInModal";
24
-
import {
25
-
useFollowAccountMutation,
26
-
useFollowersQuery,
27
-
useUnfollowAccountMutation,
28
-
} from "../../hooks/useGraph";
29
-
import Follows from "./follows";
30
-
import Followers from "./followers";
31
-
import { activeTabAtom } from "../../atoms/tab";
32
-
import Circles from "./circles";
33
20
34
21
const Group = styled.div`
35
22
display: flex;
36
23
flex-direction: row;
37
-
justify-content: space-between;
38
-
align-items: flex-start;
39
24
margin-top: 20px;
40
25
margin-bottom: 50px;
41
26
`;
42
27
43
-
const ProfileInfo = styled.div`
44
-
display: flex;
45
-
flex-direction: row;
46
-
flex: 1;
47
-
`;
48
-
49
28
export type ProfileProps = {
50
29
activeKey?: string;
51
30
};
52
31
53
32
function Profile(props: ProfileProps) {
54
-
const [follows, setFollows] = useAtom(followsAtom);
55
-
const [isSignInOpen, setIsSignInOpen] = useState(false);
56
33
const [profiles, setProfiles] = useAtom(profilesAtom);
57
-
const [activeKey, setActiveKey] = useAtom(activeTabAtom);
34
+
const [activeKey, setActiveKey] = useState<Key>(
35
+
_.get(props, "activeKey", "0").split("/")[0],
36
+
);
58
37
const { did } = useParams({ strict: false });
59
38
const profile = useProfileByDidQuery(did!);
60
39
const setUser = useSetAtom(userAtom);
61
40
const { tab } = useSearch({ strict: false });
62
-
const { mutate: followAccount } = useFollowAccountMutation();
63
-
const { mutate: unfollowAccount } = useUnfollowAccountMutation();
64
-
const currentDid = localStorage.getItem("did");
65
-
const { data, isLoading } = useFollowersQuery(
66
-
profile.data?.did,
67
-
1,
68
-
currentDid ? [currentDid] : undefined,
69
-
);
70
-
71
-
const onFollow = () => {
72
-
if (!localStorage.getItem("token")) {
73
-
setIsSignInOpen(true);
74
-
return;
75
-
}
76
-
77
-
if (!profile.data) return;
78
-
79
-
setFollows((prev) => new Set(prev).add(profile.data.did));
80
-
followAccount(profile.data.did);
81
-
};
82
-
83
-
const onUnfollow = () => {
84
-
if (!localStorage.getItem("token")) {
85
-
setIsSignInOpen(true);
86
-
return;
87
-
}
88
-
if (!profile.data) return;
89
-
90
-
setFollows((prev) => {
91
-
const newSet = new Set(prev);
92
-
newSet.delete(profile.data.did);
93
-
return newSet;
94
-
});
95
-
unfollowAccount(profile.data.did);
96
-
};
97
-
98
-
useEffect(() => {
99
-
if (!props.activeKey) {
100
-
setActiveKey("0");
101
-
return;
102
-
}
103
-
setActiveKey(_.get(props, "activeKey", "0").split("/")[0]);
104
-
}, [props.activeKey, setActiveKey, props]);
105
-
106
-
useEffect(() => {
107
-
if (!data || isLoading) {
108
-
return;
109
-
}
110
-
setFollows((prev) => {
111
-
const newSet = new Set(prev);
112
-
if (!profile.data) return newSet;
113
-
if (
114
-
data.followers.some(
115
-
(follower: { did: string }) => follower.did === currentDid,
116
-
)
117
-
) {
118
-
newSet.add(profile.data.did);
119
-
} else {
120
-
newSet.delete(profile.data.did);
121
-
}
122
-
return newSet;
123
-
});
124
-
}, [
125
-
data,
126
-
isLoading,
127
-
currentDid,
128
-
setFollows,
129
-
profile.data?.did,
130
-
profile.data,
131
-
]);
132
41
133
42
useEffect(() => {
134
43
if (tab === undefined) {
···
136
45
}
137
46
138
47
setActiveKey(1);
139
-
}, [tab, setActiveKey]);
48
+
}, [tab]);
140
49
141
50
// biome-ignore lint/correctness/useExhaustiveDependencies: <reason>want to run only on profile.data changes</reason>
142
51
useEffect(() => {
···
181
90
<Main>
182
91
<div className="pb-[100px] pt-[75px]">
183
92
<Group>
184
-
<ProfileInfo>
185
-
<div className="mr-[20px]">
186
-
<Avatar
187
-
name={profiles[did]?.displayName}
188
-
src={profiles[did]?.avatar}
189
-
size="150px"
190
-
/>
191
-
</div>
192
-
<div style={{ marginTop: profiles[did]?.displayName ? 10 : 30 }}>
193
-
<HeadingMedium
194
-
marginTop="0px"
195
-
marginBottom={0}
196
-
className="!text-[var(--color-text)]"
93
+
<div className="mr-[20px]">
94
+
<Avatar
95
+
name={profiles[did]?.displayName}
96
+
src={profiles[did]?.avatar}
97
+
size="150px"
98
+
/>
99
+
</div>
100
+
<div style={{ marginTop: profiles[did]?.displayName ? 10 : 30 }}>
101
+
<HeadingMedium
102
+
marginTop="0px"
103
+
marginBottom={0}
104
+
className="!text-[var(--color-text)]"
105
+
>
106
+
{profiles[did]?.displayName}
107
+
</HeadingMedium>
108
+
<LabelLarge>
109
+
<a
110
+
href={`https://bsky.app/profile/${profiles[did]?.handle}`}
111
+
className="no-underline text-[var(--color-primary)]"
112
+
>
113
+
@{profiles[did]?.handle}
114
+
</a>
115
+
<span className="text-[var(--color-text-muted)] text-[15px]">
116
+
{" "}
117
+
• scrobbling since{" "}
118
+
{dayjs(profiles[did]?.createdAt).format("DD MMM YYYY")}
119
+
</span>
120
+
</LabelLarge>
121
+
<div className="flex-1 mt-[30px] mr-[10px]">
122
+
<a
123
+
href={`https://pdsls.dev/at/${profiles[did]?.did}`}
124
+
target="_blank"
125
+
className="no-underline text-[var(--color-text)] bg-[var(--color-default-button)] p-[16px] rounded-[10px] pl-[25px] pr-[25px]"
197
126
>
198
-
{profiles[did]?.displayName}
199
-
</HeadingMedium>
200
-
<LabelLarge>
201
-
<a
202
-
href={`https://bsky.app/profile/${profiles[did]?.handle}`}
203
-
className="no-underline text-[var(--color-primary)]"
204
-
target="_blank"
205
-
>
206
-
@{profiles[did]?.handle}
207
-
</a>
208
-
<span className="text-[var(--color-text-muted)] text-[15px]">
209
-
{" "}
210
-
• scrobbling since{" "}
211
-
{dayjs(profiles[did]?.createdAt).format("DD MMM YYYY")}
212
-
</span>
213
-
</LabelLarge>
214
-
<div className="flex-1 mt-[30px] mr-[10px]">
215
-
<a
216
-
href={`https://pdsls.dev/at/${profiles[did]?.did}`}
217
-
target="_blank"
218
-
className="no-underline text-[var(--color-text)] bg-[var(--color-default-button)] p-[16px] rounded-[10px] pl-[25px] pr-[25px]"
219
-
>
220
-
<ExternalLink size={24} style={{ marginRight: 10 }} />
221
-
View on PDSls
222
-
</a>
223
-
</div>
127
+
<ExternalLink size={24} style={{ marginRight: 10 }} />
128
+
View on PDSls
129
+
</a>
224
130
</div>
225
-
</ProfileInfo>
226
-
{(profile.data?.did !== localStorage.getItem("did") ||
227
-
!localStorage.getItem("did")) && (
228
-
<>
229
-
{!follows.has(profile.data?.did || "") && !isLoading && (
230
-
<Button
231
-
shape="pill"
232
-
size="compact"
233
-
startEnhancer={<IconPlus size={18} />}
234
-
onClick={onFollow}
235
-
overrides={{
236
-
BaseButton: {
237
-
style: {
238
-
marginTop: "12px",
239
-
minWidth: "120px",
240
-
backgroundColor: "#ff2876",
241
-
":hover": {
242
-
backgroundColor: "#ff2876",
243
-
},
244
-
":focus": {
245
-
backgroundColor: "#ff2876",
246
-
},
247
-
},
248
-
},
249
-
}}
250
-
>
251
-
Follow
252
-
</Button>
253
-
)}
254
-
{follows.has(profile.data?.did || "") && !isLoading && (
255
-
<Button
256
-
shape="pill"
257
-
size="compact"
258
-
startEnhancer={<IconCheck size={18} />}
259
-
onClick={onUnfollow}
260
-
overrides={{
261
-
BaseButton: {
262
-
style: {
263
-
marginTop: "12px",
264
-
minWidth: "120px",
265
-
backgroundColor: "var(--color-default-button)",
266
-
color: "var(--color-text)",
267
-
":hover": {
268
-
backgroundColor: "var(--color-default-button)",
269
-
},
270
-
":focus": {
271
-
backgroundColor: "var(--color-default-button)",
272
-
},
273
-
},
274
-
},
275
-
}}
276
-
>
277
-
Following
278
-
</Button>
279
-
)}
280
-
</>
281
-
)}
131
+
</div>
282
132
</Group>
283
133
284
134
<Tabs
···
329
179
/>
330
180
</Tab>
331
181
<Tab
332
-
title="Followers"
333
-
overrides={{
334
-
Tab: {
335
-
style: {
336
-
color: "var(--color-text)",
337
-
backgroundColor: "var(--color-background) !important",
338
-
},
339
-
},
340
-
}}
341
-
>
342
-
<Followers />
343
-
</Tab>
344
-
<Tab
345
-
title="Following"
346
-
overrides={{
347
-
Tab: {
348
-
style: {
349
-
color: "var(--color-text)",
350
-
backgroundColor: "var(--color-background) !important",
351
-
},
352
-
},
353
-
}}
354
-
>
355
-
<Follows />
356
-
</Tab>
357
-
<Tab
358
-
title="Circles"
182
+
title="Playlists"
359
183
overrides={{
360
184
Tab: {
361
185
style: {
···
365
189
},
366
190
}}
367
191
>
368
-
<Circles />
192
+
<Playlists />
369
193
</Tab>
370
194
<Tab
371
195
title="Loved Tracks"
···
381
205
<LovedTracks />
382
206
</Tab>
383
207
<Tab
384
-
title="Playlists"
208
+
title="Tags"
385
209
overrides={{
386
210
Tab: {
387
211
style: {
···
390
214
},
391
215
},
392
216
}}
393
-
>
394
-
<Playlists />
395
-
</Tab>
217
+
></Tab>
396
218
</Tabs>
397
219
<Shout type="profile" />
398
220
</div>
399
-
<SignInModal
400
-
isOpen={isSignInOpen}
401
-
onClose={() => setIsSignInOpen(false)}
402
-
follow
403
-
/>
404
221
</Main>
405
222
);
406
223
}
-215
apps/web/src/pages/profile/circles/Circles.tsx
-215
apps/web/src/pages/profile/circles/Circles.tsx
···
1
-
import { HeadingSmall, LabelMedium, LabelSmall } from "baseui/typography";
2
-
import {
3
-
useActorNeighboursQuery,
4
-
useProfileByDidQuery,
5
-
} from "../../../hooks/useProfile";
6
-
import { Link, useParams } from "@tanstack/react-router";
7
-
import { Neighbour } from "../../../types/neighbour";
8
-
import { Avatar } from "baseui/avatar";
9
-
import { IconCheck, IconPlus } from "@tabler/icons-react";
10
-
import { Button } from "baseui/button";
11
-
import { useAtom } from "jotai";
12
-
import { followsAtom } from "../../../atoms/follows";
13
-
import {
14
-
useFollowAccountMutation,
15
-
useUnfollowAccountMutation,
16
-
} from "../../../hooks/useGraph";
17
-
import { useState } from "react";
18
-
import SignInModal from "../../../components/SignInModal";
19
-
import { activeTabAtom } from "../../../atoms/tab";
20
-
import scrollToTop from "../../../lib/scrollToTop";
21
-
22
-
function Circles() {
23
-
const [, setActiveKey] = useAtom(activeTabAtom);
24
-
const [follows, setFollows] = useAtom(followsAtom);
25
-
const [isSignInOpen, setIsSignInOpen] = useState(false);
26
-
const { did } = useParams({ strict: false });
27
-
const profile = useProfileByDidQuery(did!);
28
-
const { data, isLoading } = useActorNeighboursQuery(did!);
29
-
const { mutate: followAccount } = useFollowAccountMutation();
30
-
const { mutate: unfollowAccount } = useUnfollowAccountMutation();
31
-
32
-
const onFollow = (followerDid: string) => {
33
-
if (!localStorage.getItem("token")) {
34
-
setIsSignInOpen(true);
35
-
return;
36
-
}
37
-
setFollows((prev) => new Set(prev).add(followerDid));
38
-
followAccount(followerDid);
39
-
};
40
-
41
-
const onUnfollow = (followerDid: string) => {
42
-
if (!localStorage.getItem("token")) {
43
-
setIsSignInOpen(true);
44
-
return;
45
-
}
46
-
setFollows((prev) => {
47
-
const newSet = new Set(prev);
48
-
newSet.delete(followerDid);
49
-
return newSet;
50
-
});
51
-
unfollowAccount(followerDid);
52
-
};
53
-
54
-
return (
55
-
<>
56
-
<HeadingSmall className="!text-[var(--color-text)] mb-[15px]">
57
-
Circles
58
-
</HeadingSmall>
59
-
<p>
60
-
People on Rocksky with similar music taste to @{profile.data?.handle}
61
-
</p>
62
-
{!isLoading && data?.neighbours.length === 0 && (
63
-
<div className="mt-[40px] text-center py-[60px]">
64
-
<LabelMedium className="!text-[var(--color-text-muted)]">
65
-
No circles found yet. Check back later!
66
-
</LabelMedium>
67
-
</div>
68
-
)}
69
-
{!isLoading && (data?.neighbours || []).length > 0 && (
70
-
<div className="mt-[40px]">
71
-
{data?.neighbours.map((neighbour: Neighbour) => (
72
-
<div
73
-
key={neighbour.did}
74
-
className="flex items-start justify-between gap-2 mb-[40px]"
75
-
>
76
-
<div className="flex items-center gap-2">
77
-
<Link
78
-
to={`/profile/${neighbour.handle}` as string}
79
-
className="no-underline mt-[10px]"
80
-
onClick={() => {
81
-
setActiveKey(0);
82
-
scrollToTop();
83
-
}}
84
-
>
85
-
<Avatar
86
-
src={neighbour.avatar}
87
-
name={neighbour.displayName}
88
-
size={"60px"}
89
-
/>
90
-
</Link>
91
-
<div className="ml-[16px]">
92
-
<div className="flex ">
93
-
<Link
94
-
to={`/profile/${neighbour.handle}` as string}
95
-
className="no-underline"
96
-
onClick={() => {
97
-
setActiveKey(0);
98
-
scrollToTop();
99
-
}}
100
-
>
101
-
<LabelMedium
102
-
marginTop={"0px"}
103
-
className="!text-[var(--color-text)] mr-[5px]"
104
-
>
105
-
{neighbour.displayName}
106
-
</LabelMedium>
107
-
</Link>
108
-
<Link
109
-
to={`/profile/${neighbour.handle}` as string}
110
-
className="no-underline text-[var(--color-primary)]"
111
-
onClick={() => {
112
-
setActiveKey(0);
113
-
scrollToTop();
114
-
}}
115
-
>
116
-
<LabelSmall className="!text-[var(--color-text-muted)] mt-[3px] mb-[5px]">
117
-
@{neighbour.handle}
118
-
</LabelSmall>
119
-
</Link>
120
-
</div>
121
-
<p className="mt-[0px] mb-[0px] text-[14px]">
122
-
{localStorage.getItem("did") === profile.data?.did
123
-
? "You"
124
-
: "They"}{" "}
125
-
both listen to{" "}
126
-
{neighbour.topSharedArtistsDetails.map((artist, index) => (
127
-
<div key={artist.id} className="inline">
128
-
<Link
129
-
to={
130
-
`/${artist.uri.split("at://")[1].replace("app.rocksky.", "")}` as string
131
-
}
132
-
className="no-underline"
133
-
>
134
-
<span className="mt-[0px] mb-[0px] text-[14px] !text-[var(--color-primary)]">
135
-
{artist.name}
136
-
</span>
137
-
</Link>
138
-
{index !==
139
-
neighbour.topSharedArtistsDetails.length - 1 &&
140
-
(index ===
141
-
neighbour.topSharedArtistsDetails.length - 2
142
-
? " and "
143
-
: ", ")}
144
-
</div>
145
-
))}
146
-
</p>
147
-
</div>
148
-
</div>
149
-
{(neighbour.did !== localStorage.getItem("did") ||
150
-
!localStorage.getItem("did")) && (
151
-
<div className="ml-[10px] mt-[10px]">
152
-
{!follows.has(neighbour.did) && (
153
-
<Button
154
-
shape="pill"
155
-
size="mini"
156
-
startEnhancer={<IconPlus size={16} />}
157
-
onClick={() => onFollow(neighbour.did)}
158
-
overrides={{
159
-
BaseButton: {
160
-
style: {
161
-
minWidth: "90px",
162
-
backgroundColor: "#ff2876",
163
-
":hover": {
164
-
backgroundColor: "#ff2876",
165
-
},
166
-
":focus": {
167
-
backgroundColor: "#ff2876",
168
-
},
169
-
},
170
-
},
171
-
}}
172
-
>
173
-
Follow
174
-
</Button>
175
-
)}
176
-
{follows.has(neighbour.did) && (
177
-
<Button
178
-
shape="pill"
179
-
size="mini"
180
-
startEnhancer={<IconCheck size={16} />}
181
-
onClick={() => onUnfollow(neighbour.did)}
182
-
overrides={{
183
-
BaseButton: {
184
-
style: {
185
-
backgroundColor: "var(--color-default-button)",
186
-
color: "var(--color-text)",
187
-
":hover": {
188
-
backgroundColor: "var(--color-default-button)",
189
-
},
190
-
":focus": {
191
-
backgroundColor: "var(--color-default-button)",
192
-
},
193
-
},
194
-
},
195
-
}}
196
-
>
197
-
Following
198
-
</Button>
199
-
)}
200
-
</div>
201
-
)}
202
-
</div>
203
-
))}
204
-
</div>
205
-
)}
206
-
<SignInModal
207
-
isOpen={isSignInOpen}
208
-
onClose={() => setIsSignInOpen(false)}
209
-
follow
210
-
/>
211
-
</>
212
-
);
213
-
}
214
-
215
-
export default Circles;
-3
apps/web/src/pages/profile/circles/index.tsx
-3
apps/web/src/pages/profile/circles/index.tsx
-240
apps/web/src/pages/profile/followers/Followers.tsx
-240
apps/web/src/pages/profile/followers/Followers.tsx
···
1
-
import { HeadingSmall, LabelMedium, LabelSmall } from "baseui/typography";
2
-
import {
3
-
useFollowAccountMutation,
4
-
useFollowersInfiniteQuery,
5
-
useFollowsQuery,
6
-
useUnfollowAccountMutation,
7
-
} from "../../../hooks/useGraph";
8
-
import { useProfileByDidQuery } from "../../../hooks/useProfile";
9
-
import { Link, useParams } from "@tanstack/react-router";
10
-
import { Avatar } from "baseui/avatar";
11
-
import { useAtom } from "jotai";
12
-
import { activeTabAtom } from "../../../atoms/tab";
13
-
import { followsAtom } from "../../../atoms/follows";
14
-
import { Button } from "baseui/button";
15
-
import { IconCheck, IconPlus } from "@tabler/icons-react";
16
-
import SignInModal from "../../../components/SignInModal";
17
-
import { useState, useEffect, useRef } from "react";
18
-
import numeral from "numeral";
19
-
import scrollToTop from "../../../lib/scrollToTop";
20
-
21
-
function Followers() {
22
-
const [, setActiveKey] = useAtom(activeTabAtom);
23
-
const [follows, setFollows] = useAtom(followsAtom);
24
-
const [isSignInOpen, setIsSignInOpen] = useState(false);
25
-
const { did } = useParams({ strict: false });
26
-
const profile = useProfileByDidQuery(did!);
27
-
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
28
-
useFollowersInfiniteQuery(profile.data?.did || "", 20);
29
-
const { mutate: followAccount } = useFollowAccountMutation();
30
-
const { mutate: unfollowAccount } = useUnfollowAccountMutation();
31
-
32
-
const loadMoreRef = useRef<HTMLDivElement>(null);
33
-
34
-
// Intersection Observer for infinite scroll
35
-
useEffect(() => {
36
-
if (!loadMoreRef.current || !hasNextPage || isFetchingNextPage) return;
37
-
38
-
const observer = new IntersectionObserver(
39
-
(entries) => {
40
-
if (entries[0]?.isIntersecting) {
41
-
fetchNextPage();
42
-
}
43
-
},
44
-
{ threshold: 0.1 },
45
-
);
46
-
47
-
observer.observe(loadMoreRef.current);
48
-
49
-
return () => observer.disconnect();
50
-
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
51
-
52
-
const allFollowers = data?.pages.flatMap((page) => page.followers) ?? [];
53
-
54
-
const { data: followsData } = useFollowsQuery(
55
-
localStorage.getItem("did")!,
56
-
allFollowers.length,
57
-
allFollowers
58
-
.map((follower) => follower.did)
59
-
.filter((x) => x !== localStorage.getItem("did")),
60
-
);
61
-
62
-
useEffect(() => {
63
-
if (!followsData) return;
64
-
setFollows((prev) => {
65
-
const newSet = new Set(prev);
66
-
followsData.follows.forEach((follow: { did: string }) => {
67
-
newSet.add(follow.did);
68
-
});
69
-
return newSet;
70
-
});
71
-
}, [followsData, setFollows]);
72
-
73
-
const onFollow = (followerDid: string) => {
74
-
if (!localStorage.getItem("token")) {
75
-
setIsSignInOpen(true);
76
-
return;
77
-
}
78
-
setFollows((prev) => new Set(prev).add(followerDid));
79
-
followAccount(followerDid);
80
-
};
81
-
82
-
const onUnfollow = (followerDid: string) => {
83
-
if (!localStorage.getItem("token")) {
84
-
setIsSignInOpen(true);
85
-
return;
86
-
}
87
-
setFollows((prev) => {
88
-
const newSet = new Set(prev);
89
-
newSet.delete(followerDid);
90
-
return newSet;
91
-
});
92
-
unfollowAccount(followerDid);
93
-
};
94
-
95
-
const count = data?.pages?.flatMap((page) => page.count)[0];
96
-
97
-
return (
98
-
<>
99
-
<HeadingSmall className="!text-[var(--color-text)]">
100
-
Followers {count > 0 ? `(${numeral(count).format("0,0")})` : ""}
101
-
</HeadingSmall>
102
-
103
-
{allFollowers.length === 0 && data && (
104
-
<div className="text-center py-8">
105
-
<LabelMedium className="!text-[var(--color-text)] opacity-60">
106
-
No followers yet
107
-
</LabelMedium>
108
-
</div>
109
-
)}
110
-
111
-
{allFollowers.length > 0 && (
112
-
<div>
113
-
{allFollowers.map((follower: any) => (
114
-
<div
115
-
key={follower.did}
116
-
className="flex items-start justify-between gap-2 mb-[20px]"
117
-
>
118
-
<div className="flex items-center gap-2">
119
-
<Link
120
-
to={`/profile/${follower.handle}` as string}
121
-
className="no-underline"
122
-
onClick={() => {
123
-
setActiveKey(0);
124
-
scrollToTop();
125
-
}}
126
-
>
127
-
<Avatar
128
-
src={follower.avatar}
129
-
name={follower.displayName}
130
-
size={"60px"}
131
-
/>
132
-
</Link>
133
-
<div className="ml-[16px]">
134
-
<Link
135
-
to={`/profile/${follower.handle}` as string}
136
-
className="no-underline"
137
-
onClick={() => {
138
-
setActiveKey(0);
139
-
scrollToTop();
140
-
}}
141
-
>
142
-
<LabelMedium
143
-
marginTop={"10px"}
144
-
className="!text-[var(--color-text)]"
145
-
>
146
-
{follower.displayName}
147
-
</LabelMedium>
148
-
</Link>
149
-
<Link
150
-
to={`/profile/${follower.handle}` as string}
151
-
className="no-underline text-[var(--color-primary)]"
152
-
onClick={() => {
153
-
setActiveKey(0);
154
-
scrollToTop();
155
-
}}
156
-
>
157
-
<LabelSmall className="!text-[var(--color-primary)] mt-[3px] mb-[25px]">
158
-
@{follower.handle}
159
-
</LabelSmall>
160
-
</Link>
161
-
</div>
162
-
</div>
163
-
{(follower.did !== localStorage.getItem("did") ||
164
-
!localStorage.getItem("did")) && (
165
-
<div className="ml-auto mt-[10px]">
166
-
{!follows.has(follower.did) && (
167
-
<Button
168
-
shape="pill"
169
-
size="mini"
170
-
startEnhancer={<IconPlus size={16} />}
171
-
onClick={() => onFollow(follower.did)}
172
-
overrides={{
173
-
BaseButton: {
174
-
style: {
175
-
minWidth: "90px",
176
-
backgroundColor: "#ff2876",
177
-
":hover": {
178
-
backgroundColor: "#ff2876",
179
-
},
180
-
":focus": {
181
-
backgroundColor: "#ff2876",
182
-
},
183
-
},
184
-
},
185
-
}}
186
-
>
187
-
Follow
188
-
</Button>
189
-
)}
190
-
{follows.has(follower.did) && (
191
-
<Button
192
-
shape="pill"
193
-
size="mini"
194
-
startEnhancer={<IconCheck size={16} />}
195
-
onClick={() => onUnfollow(follower.did)}
196
-
overrides={{
197
-
BaseButton: {
198
-
style: {
199
-
backgroundColor: "var(--color-default-button)",
200
-
color: "var(--color-text)",
201
-
":hover": {
202
-
backgroundColor: "var(--color-default-button)",
203
-
},
204
-
":focus": {
205
-
backgroundColor: "var(--color-default-button)",
206
-
},
207
-
},
208
-
},
209
-
}}
210
-
>
211
-
Following
212
-
</Button>
213
-
)}
214
-
</div>
215
-
)}
216
-
</div>
217
-
))}
218
-
219
-
{/* Infinite scroll trigger */}
220
-
<div ref={loadMoreRef} className="h-[20px] w-full" />
221
-
222
-
{isFetchingNextPage && (
223
-
<div className="text-center py-4">
224
-
<LabelSmall className="!text-[var(--color-text)]">
225
-
Loading more...
226
-
</LabelSmall>
227
-
</div>
228
-
)}
229
-
</div>
230
-
)}
231
-
<SignInModal
232
-
isOpen={isSignInOpen}
233
-
onClose={() => setIsSignInOpen(false)}
234
-
follow
235
-
/>
236
-
</>
237
-
);
238
-
}
239
-
240
-
export default Followers;
-3
apps/web/src/pages/profile/followers/index.tsx
-3
apps/web/src/pages/profile/followers/index.tsx
-241
apps/web/src/pages/profile/follows/Follows.tsx
-241
apps/web/src/pages/profile/follows/Follows.tsx
···
1
-
import { HeadingSmall, LabelMedium, LabelSmall } from "baseui/typography";
2
-
import { useProfileByDidQuery } from "../../../hooks/useProfile";
3
-
import { Link, useParams } from "@tanstack/react-router";
4
-
import {
5
-
useFollowAccountMutation,
6
-
useFollowsInfiniteQuery,
7
-
useFollowsQuery,
8
-
useUnfollowAccountMutation,
9
-
} from "../../../hooks/useGraph";
10
-
import { Avatar } from "baseui/avatar";
11
-
import { useAtom } from "jotai";
12
-
import { activeTabAtom } from "../../../atoms/tab";
13
-
import { followsAtom } from "../../../atoms/follows";
14
-
import { Button } from "baseui/button";
15
-
import { IconCheck, IconPlus } from "@tabler/icons-react";
16
-
import SignInModal from "../../../components/SignInModal";
17
-
import { useState, useEffect, useRef } from "react";
18
-
import numeral from "numeral";
19
-
import scrollToTop from "../../../lib/scrollToTop";
20
-
21
-
function Follows() {
22
-
const [, setActiveKey] = useAtom(activeTabAtom);
23
-
const [follows, setFollows] = useAtom(followsAtom);
24
-
const [isSignInOpen, setIsSignInOpen] = useState(false);
25
-
const { did } = useParams({ strict: false });
26
-
const profile = useProfileByDidQuery(did!);
27
-
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
28
-
useFollowsInfiniteQuery(profile.data?.did!, 20);
29
-
const { mutate: followAccount } = useFollowAccountMutation();
30
-
const { mutate: unfollowAccount } = useUnfollowAccountMutation();
31
-
32
-
const loadMoreRef = useRef<HTMLDivElement>(null);
33
-
34
-
// Intersection Observer for infinite scroll
35
-
useEffect(() => {
36
-
if (!loadMoreRef.current || !hasNextPage || isFetchingNextPage) return;
37
-
38
-
const observer = new IntersectionObserver(
39
-
(entries) => {
40
-
if (entries[0]?.isIntersecting) {
41
-
fetchNextPage();
42
-
}
43
-
},
44
-
{ threshold: 0.1 },
45
-
);
46
-
47
-
observer.observe(loadMoreRef.current);
48
-
49
-
return () => observer.disconnect();
50
-
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
51
-
52
-
// Flatten all pages into a single array
53
-
const allFollows = data?.pages.flatMap((page) => page.follows) ?? [];
54
-
55
-
const { data: followsData } = useFollowsQuery(
56
-
localStorage.getItem("did")!,
57
-
allFollows.length,
58
-
allFollows
59
-
.map((follow) => follow.did)
60
-
.filter((x) => x !== localStorage.getItem("did")),
61
-
);
62
-
63
-
useEffect(() => {
64
-
if (!followsData) return;
65
-
setFollows((prev) => {
66
-
const newSet = new Set(prev);
67
-
followsData.follows.forEach((follow: { did: string }) => {
68
-
newSet.add(follow.did);
69
-
});
70
-
return newSet;
71
-
});
72
-
}, [followsData, setFollows]);
73
-
74
-
const onFollow = (followDid: string) => {
75
-
if (!localStorage.getItem("token")) {
76
-
setIsSignInOpen(true);
77
-
return;
78
-
}
79
-
setFollows((prev) => new Set(prev).add(followDid));
80
-
followAccount(followDid);
81
-
};
82
-
83
-
const onUnfollow = (followDid: string) => {
84
-
if (!localStorage.getItem("token")) {
85
-
setIsSignInOpen(true);
86
-
return;
87
-
}
88
-
setFollows((prev) => {
89
-
const newSet = new Set(prev);
90
-
newSet.delete(followDid);
91
-
return newSet;
92
-
});
93
-
unfollowAccount(followDid);
94
-
};
95
-
96
-
const count = data?.pages?.flatMap((page) => page.count)[0];
97
-
98
-
return (
99
-
<>
100
-
<HeadingSmall className="!text-[var(--color-text)]">
101
-
Following {count > 0 ? `(${numeral(count).format("0,0")})` : ""}
102
-
</HeadingSmall>
103
-
104
-
{allFollows.length === 0 && data && (
105
-
<div className="text-center py-8">
106
-
<LabelMedium className="!text-[var(--color-text)] opacity-60">
107
-
Not following anyone yet
108
-
</LabelMedium>
109
-
</div>
110
-
)}
111
-
112
-
{allFollows.length > 0 && (
113
-
<div>
114
-
{allFollows.map((follow: any) => (
115
-
<div
116
-
key={follow.did}
117
-
className="flex items-start justify-between gap-2 mb-[20px]"
118
-
>
119
-
<div className="flex items-center gap-2">
120
-
<Link
121
-
to={`/profile/${follow.handle}` as any}
122
-
className="no-underline"
123
-
onClick={() => {
124
-
setActiveKey(0);
125
-
scrollToTop();
126
-
}}
127
-
>
128
-
<Avatar
129
-
src={follow.avatar}
130
-
name={follow.displayName}
131
-
size={"60px"}
132
-
/>
133
-
</Link>
134
-
<div className="ml-[16px]">
135
-
<Link
136
-
to={`/profile/${follow.handle}` as string}
137
-
className="no-underline"
138
-
onClick={() => {
139
-
setActiveKey(0);
140
-
scrollToTop();
141
-
}}
142
-
>
143
-
<LabelMedium
144
-
marginTop={"10px"}
145
-
className="!text-[var(--color-text)]"
146
-
>
147
-
{follow.displayName}
148
-
</LabelMedium>
149
-
</Link>
150
-
<Link
151
-
to={`/profile/${follow.handle}` as string}
152
-
className="no-underline text-[var(--color-primary)]"
153
-
onClick={() => {
154
-
setActiveKey(0);
155
-
scrollToTop();
156
-
}}
157
-
>
158
-
<LabelSmall className="!text-[var(--color-primary)] mt-[3px] mb-[25px]">
159
-
@{follow.handle}
160
-
</LabelSmall>
161
-
</Link>
162
-
</div>
163
-
</div>
164
-
{(follow.did !== localStorage.getItem("did") ||
165
-
!localStorage.getItem("did")) && (
166
-
<div className="ml-auto mt-[10px]">
167
-
{!follows.has(follow.did) && (
168
-
<Button
169
-
shape="pill"
170
-
size="mini"
171
-
startEnhancer={<IconPlus size={16} />}
172
-
onClick={() => onFollow(follow.did)}
173
-
overrides={{
174
-
BaseButton: {
175
-
style: {
176
-
minWidth: "90px",
177
-
backgroundColor: "#ff2876",
178
-
":hover": {
179
-
backgroundColor: "#ff2876",
180
-
},
181
-
":focus": {
182
-
backgroundColor: "#ff2876",
183
-
},
184
-
},
185
-
},
186
-
}}
187
-
>
188
-
Follow
189
-
</Button>
190
-
)}
191
-
{follows.has(follow.did) && (
192
-
<Button
193
-
shape="pill"
194
-
size="mini"
195
-
startEnhancer={<IconCheck size={16} />}
196
-
onClick={() => onUnfollow(follow.did)}
197
-
overrides={{
198
-
BaseButton: {
199
-
style: {
200
-
backgroundColor: "var(--color-default-button)",
201
-
color: "var(--color-text)",
202
-
":hover": {
203
-
backgroundColor: "var(--color-default-button)",
204
-
},
205
-
":focus": {
206
-
backgroundColor: "var(--color-default-button)",
207
-
},
208
-
},
209
-
},
210
-
}}
211
-
>
212
-
Following
213
-
</Button>
214
-
)}
215
-
</div>
216
-
)}
217
-
</div>
218
-
))}
219
-
220
-
{/* Infinite scroll trigger */}
221
-
<div ref={loadMoreRef} className="h-[20px] w-full" />
222
-
223
-
{isFetchingNextPage && (
224
-
<div className="text-center py-4">
225
-
<LabelSmall className="!text-[var(--color-text)]">
226
-
Loading more...
227
-
</LabelSmall>
228
-
</div>
229
-
)}
230
-
</div>
231
-
)}
232
-
<SignInModal
233
-
isOpen={isSignInOpen}
234
-
onClose={() => setIsSignInOpen(false)}
235
-
follow
236
-
/>
237
-
</>
238
-
);
239
-
}
240
-
241
-
export default Follows;
-3
apps/web/src/pages/profile/follows/index.tsx
-3
apps/web/src/pages/profile/follows/index.tsx
+1
-7
apps/web/src/pages/profile/overview/Overview.tsx
+1
-7
apps/web/src/pages/profile/overview/Overview.tsx
···
8
8
import TopAlbums from "./topalbums";
9
9
import TopArtists from "./topartists";
10
10
import TopTracks from "./toptracks";
11
-
import Compatibility from "./compatibility";
12
11
13
12
function Overview() {
14
13
const { did } = useParams({ strict: false });
···
40
39
41
40
return (
42
41
<>
43
-
<div className="flex flex-row mb-[50px]">
44
-
{did && stats[did] && <Stats stats={stats[did]} />}
45
-
<div className="flex-1">
46
-
<Compatibility />
47
-
</div>
48
-
</div>
42
+
{did && stats[did] && <Stats stats={stats[did]} />}
49
43
<div className="mb-20">
50
44
<RecentTracks />
51
45
</div>
-116
apps/web/src/pages/profile/overview/compatibility/Compatibility.tsx
-116
apps/web/src/pages/profile/overview/compatibility/Compatibility.tsx
···
1
-
import { useParams } from "@tanstack/react-router";
2
-
import {
3
-
useActorCompatibilityQuery,
4
-
useProfileByDidQuery,
5
-
} from "../../../../hooks/useProfile";
6
-
import { Avatar } from "baseui/avatar";
7
-
import { Link } from "@tanstack/react-router";
8
-
9
-
function Compatibility() {
10
-
const { did } = useParams({ strict: false });
11
-
const profile = useProfileByDidQuery(did!);
12
-
const currentUser = useProfileByDidQuery(localStorage.getItem("did") || "");
13
-
const { data, isLoading } = useActorCompatibilityQuery(profile.data?.did);
14
-
const currentUserDid = localStorage.getItem("did");
15
-
16
-
const getBorderColor = (percentage: number | undefined) => {
17
-
if (!percentage) return "#666666";
18
-
if (percentage >= 80) return "#19d825"; // Green
19
-
if (percentage >= 60) return "#ffd700"; // Gold
20
-
if (percentage >= 40) return "#ff8c00"; // Orange
21
-
return "#ff4500"; // Red
22
-
};
23
-
24
-
const percentage = data?.compatibility?.compatibilityPercentage || 0;
25
-
const borderColor = getBorderColor(percentage);
26
-
27
-
return (
28
-
<>
29
-
{currentUserDid &&
30
-
data &&
31
-
data.compatibility &&
32
-
currentUserDid != profile.data?.did &&
33
-
!isLoading && (
34
-
<div className="mt-[20px] ml-[19px] flex flex-row items-center">
35
-
<div
36
-
style={{
37
-
position: "relative",
38
-
width: "54px",
39
-
height: "54px",
40
-
flexShrink: 0,
41
-
}}
42
-
>
43
-
<div
44
-
style={{
45
-
position: "absolute",
46
-
inset: 0,
47
-
borderRadius: "50%",
48
-
background: `conic-gradient(${borderColor} 0deg ${(percentage / 100) * 360}deg, transparent ${(percentage / 100) * 360}deg 360deg)`,
49
-
display: "flex",
50
-
alignItems: "center",
51
-
justifyContent: "center",
52
-
}}
53
-
>
54
-
<div
55
-
style={{
56
-
width: "48px",
57
-
height: "48px",
58
-
borderRadius: "50%",
59
-
background: "var(--color-background)",
60
-
display: "flex",
61
-
alignItems: "center",
62
-
justifyContent: "center",
63
-
}}
64
-
>
65
-
<Avatar
66
-
name={currentUser.data?.displayName}
67
-
src={currentUser.data?.avatar}
68
-
size="46px"
69
-
/>
70
-
</div>
71
-
</div>
72
-
</div>
73
-
<div className="ml-[10px] text-[14px]">
74
-
<div className="!text-[var(--color-text)]">
75
-
Your compatibility with <b>@{profile.data?.handle}</b> is{" "}
76
-
<span style={{ color: borderColor }}>
77
-
<b>{data.compatibility?.compatibilityLevel}</b>
78
-
</span>
79
-
</div>
80
-
<div className="!text-[var(--color-text)] mt-[5px]">
81
-
You both listen to{" "}
82
-
{(data.compatibility?.topSharedDetailedArtists || []).map(
83
-
(artist, index) => (
84
-
<div key={artist.id} className="inline">
85
-
<Link
86
-
to={
87
-
`/${artist.uri.split("at://")[1].replace("app.rocksky.", "")}` as string
88
-
}
89
-
className="no-underline"
90
-
>
91
-
<span className="mt-[0px] mb-[0px] text-[14px] !text-[var(--color-primary)]">
92
-
{artist.name}
93
-
</span>
94
-
</Link>
95
-
{index !==
96
-
(data.compatibility?.topSharedDetailedArtists || [])
97
-
.length -
98
-
1 &&
99
-
(index ===
100
-
(data.compatibility?.topSharedDetailedArtists || [])
101
-
.length -
102
-
2
103
-
? " and "
104
-
: ", ")}
105
-
</div>
106
-
),
107
-
)}
108
-
</div>
109
-
</div>
110
-
</div>
111
-
)}
112
-
</>
113
-
);
114
-
}
115
-
116
-
export default Compatibility;
-3
apps/web/src/pages/profile/overview/compatibility/index.tsx
-3
apps/web/src/pages/profile/overview/compatibility/index.tsx
-17
apps/web/src/types/compatibility.ts
-17
apps/web/src/types/compatibility.ts
···
1
-
export type Compatibility = {
2
-
compatibilityLevel: number;
3
-
compatibilityPercentage: number;
4
-
sharedArtists: number;
5
-
topSharedArtists: string[];
6
-
topSharedDetailedArtists: {
7
-
id: string;
8
-
name: string;
9
-
picture: string;
10
-
uri: string;
11
-
user1Rank: number;
12
-
user2Rank: number;
13
-
weight: number;
14
-
}[];
15
-
user1ArtistCount: number;
16
-
user2ArtistCount: number;
17
-
};
-17
apps/web/src/types/neighbour.ts
-17
apps/web/src/types/neighbour.ts
···
1
-
export type Neighbour = {
2
-
id: string;
3
-
avatar: string;
4
-
did: string;
5
-
displayName: string;
6
-
handle: string;
7
-
sharedArtistsCount: number;
8
-
similarityScore: number;
9
-
topSharedArtistNames: string[];
10
-
topSharedArtistsDetails: {
11
-
id: string;
12
-
name: string;
13
-
picture: string;
14
-
uri: string;
15
-
}[];
16
-
userId: string;
17
-
};
-54
apps/web/src/types/profile.ts
-54
apps/web/src/types/profile.ts
···
1
-
export type Profile = {
2
-
id: string;
3
-
did: string;
4
-
handle: string;
5
-
displayName: string;
6
-
avatar: string;
7
-
createdAt: string;
8
-
spotifyUser: {
9
-
id: string;
10
-
xataVersion: number;
11
-
email: string;
12
-
userId: string;
13
-
isBetaUser: boolean;
14
-
spotifyAppId: string;
15
-
createdAt: string;
16
-
updatedAt: string;
17
-
};
18
-
spotifyToken: {
19
-
id: string;
20
-
xataVersion: number;
21
-
userId: string;
22
-
spotifyAppId: string;
23
-
createdAt: string;
24
-
updatedAt: string;
25
-
};
26
-
spotifyConnected: boolean;
27
-
googledrive: {
28
-
id: string;
29
-
email: string;
30
-
isBetaUser: boolean;
31
-
userId: string;
32
-
xataVersion: number;
33
-
createdAt: string;
34
-
updatedAt: string;
35
-
};
36
-
dropbox: {
37
-
id: string;
38
-
email: string;
39
-
isBetaUser: boolean;
40
-
userId: string;
41
-
xataVersion: number;
42
-
createdAt: string;
43
-
updatedAt: string;
44
-
};
45
-
googleDrive: {
46
-
id: string;
47
-
email: string;
48
-
isBetaUser: boolean;
49
-
userId: string;
50
-
xataVersion: number;
51
-
createdAt: string;
52
-
updatedAt: string;
53
-
};
54
-
};
+1
-6
crates/analytics/src/handlers/mod.rs
+1
-6
crates/analytics/src/handlers/mod.rs
···
12
12
};
13
13
use tracks::{get_loved_tracks, get_top_tracks, get_tracks};
14
14
15
-
use crate::handlers::{
16
-
artists::get_artist_listeners,
17
-
stats::{get_compatibility, get_neighbours},
18
-
};
15
+
use crate::handlers::artists::get_artist_listeners;
19
16
20
17
pub mod albums;
21
18
pub mod artists;
···
64
61
"library.getArtistAlbums" => get_artist_albums(payload, req, conn.clone()).await,
65
62
"library.getArtistTracks" => get_artist_tracks(payload, req, conn.clone()).await,
66
63
"library.getArtistListeners" => get_artist_listeners(payload, req, conn.clone()).await,
67
-
"library.getNeighbours" => get_neighbours(payload, req, conn.clone()).await,
68
-
"library.getCompatibility" => get_compatibility(payload, req, conn.clone()).await,
69
64
_ => return Err(anyhow::anyhow!("Method not found")),
70
65
}
71
66
}
+1
-217
crates/analytics/src/handlers/stats.rs
+1
-217
crates/analytics/src/handlers/stats.rs
···
1
1
use std::sync::{Arc, Mutex};
2
2
3
3
use crate::read_payload;
4
-
use crate::types::stats::{
5
-
Compatibility, GetCompatibilityParams, GetNeighboursParams, SharedArtist,
6
-
};
7
4
use crate::types::{
8
5
scrobble::{ScrobblesPerDay, ScrobblesPerMonth, ScrobblesPerYear},
9
6
stats::{
10
7
GetAlbumScrobblesParams, GetArtistScrobblesParams, GetScrobblesPerDayParams,
11
8
GetScrobblesPerMonthParams, GetScrobblesPerYearParams, GetStatsParams,
12
-
GetTrackScrobblesParams, Neighbour,
9
+
GetTrackScrobblesParams,
13
10
},
14
11
};
15
12
use actix_web::{web, HttpRequest, HttpResponse};
···
463
460
let scrobbles: Result<Vec<_>, _> = scrobbles.collect();
464
461
Ok(HttpResponse::Ok().json(scrobbles?))
465
462
}
466
-
467
-
pub async fn get_neighbours(
468
-
payload: &mut web::Payload,
469
-
_req: &HttpRequest,
470
-
conn: Arc<Mutex<Connection>>,
471
-
) -> Result<HttpResponse, Error> {
472
-
let body = read_payload!(payload);
473
-
let params = serde_json::from_slice::<GetNeighboursParams>(&body)?;
474
-
let conn = conn.lock().unwrap();
475
-
tracing::info!(user_id = %params.user_id, "Get neighbours");
476
-
477
-
let mut stmt = conn.prepare(
478
-
r#"
479
-
WITH user_top_artists AS (
480
-
SELECT
481
-
user_id,
482
-
artist_id,
483
-
COUNT(*) as play_count,
484
-
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY COUNT(*) DESC) as artist_rank
485
-
FROM scrobbles s
486
-
INNER JOIN artists a ON a.id = s.artist_id
487
-
WHERE s.artist_id IS NOT NULL
488
-
AND a.name != 'Various Artists'
489
-
GROUP BY user_id, artist_id
490
-
),
491
-
weighted_similarity AS (
492
-
SELECT
493
-
u1.user_id as target_user,
494
-
u2.user_id as neighbor_user,
495
-
SUM(1.0 / (u1.artist_rank + u2.artist_rank)) as similarity_score,
496
-
COUNT(DISTINCT u1.artist_id) as shared_artists,
497
-
ARRAY_AGG(DISTINCT u1.artist_id) FILTER (WHERE u1.artist_rank <= 20) as top_shared_artists
498
-
FROM user_top_artists u1
499
-
JOIN user_top_artists u2
500
-
ON u1.artist_id = u2.artist_id
501
-
AND u1.user_id != u2.user_id
502
-
WHERE u1.user_id = ?
503
-
AND u1.artist_rank <= 50
504
-
AND u2.artist_rank <= 50
505
-
GROUP BY u1.user_id, u2.user_id
506
-
HAVING shared_artists >= 3
507
-
AND top_shared_artists IS NOT NULL
508
-
)
509
-
SELECT
510
-
ws.neighbor_user,
511
-
u.display_name,
512
-
u.handle,
513
-
u.did,
514
-
u.avatar,
515
-
ws.similarity_score,
516
-
ws.shared_artists,
517
-
to_json(LIST(a.name ORDER BY array_position(ws.top_shared_artists, a.id))) as top_shared_artist_names,
518
-
to_json(LIST({'id': a.id, 'name': a.name, 'picture': a.picture, 'uri': a.uri}
519
-
ORDER BY array_position(ws.top_shared_artists, a.id))) as top_shared_artists_details
520
-
FROM weighted_similarity ws
521
-
LEFT JOIN users u ON u.id = ws.neighbor_user
522
-
INNER JOIN UNNEST(ws.top_shared_artists) AS t(artist_id) ON true
523
-
INNER JOIN artists a ON a.id = t.artist_id
524
-
GROUP BY ws.neighbor_user, u.display_name, u.handle, u.did, u.avatar, ws.similarity_score, ws.shared_artists, ws.top_shared_artists
525
-
ORDER BY ws.similarity_score DESC
526
-
LIMIT 20
527
-
"#,
528
-
)?;
529
-
530
-
let neighbours = stmt.query_map([¶ms.user_id], |row| {
531
-
let top_shared_artist_names_json: String = row.get(7)?;
532
-
let top_shared_artists_details_json: String = row.get(8)?;
533
-
534
-
let top_shared_artist_names: Vec<String> =
535
-
serde_json::from_str(&top_shared_artist_names_json).unwrap_or_else(|_| Vec::new());
536
-
let top_shared_artists_details: Vec<crate::types::stats::NeighbourArtist> =
537
-
serde_json::from_str(&top_shared_artists_details_json).unwrap_or_else(|_| Vec::new());
538
-
539
-
Ok(Neighbour {
540
-
user_id: row.get(0)?,
541
-
display_name: row.get(1)?,
542
-
handle: row.get(2)?,
543
-
did: row.get(3)?,
544
-
avatar: row.get(4)?,
545
-
similarity_score: row.get(5)?,
546
-
shared_artists_count: row.get(6)?,
547
-
top_shared_artist_names,
548
-
top_shared_artists_details,
549
-
})
550
-
})?;
551
-
552
-
let neighbours: Result<Vec<_>, _> = neighbours.collect();
553
-
Ok(HttpResponse::Ok().json(neighbours?))
554
-
}
555
-
556
-
pub async fn get_compatibility(
557
-
payload: &mut web::Payload,
558
-
_req: &HttpRequest,
559
-
conn: Arc<Mutex<Connection>>,
560
-
) -> Result<HttpResponse, Error> {
561
-
let body = read_payload!(payload);
562
-
let params = serde_json::from_slice::<GetCompatibilityParams>(&body)?;
563
-
let conn = conn.lock().unwrap();
564
-
tracing::info!(user_id_1 = %params.user_id1, user_id_2 = %params.user_id2, "Get compatibility");
565
-
566
-
let mut stmt = conn.prepare(
567
-
r#"
568
-
WITH user_top_artists AS (
569
-
SELECT
570
-
user_id,
571
-
artist_id,
572
-
COUNT(*) as play_count,
573
-
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY COUNT(*) DESC) as artist_rank
574
-
FROM scrobbles s
575
-
INNER JOIN artists a ON a.id = s.artist_id
576
-
WHERE s.artist_id IS NOT NULL
577
-
AND a.name != 'Various Artists'
578
-
AND user_id IN (?, ?)
579
-
GROUP BY user_id, artist_id
580
-
),
581
-
user_totals AS (
582
-
SELECT
583
-
user_id,
584
-
COUNT(DISTINCT artist_id) as total_artists
585
-
FROM user_top_artists
586
-
WHERE artist_rank <= 50
587
-
GROUP BY user_id
588
-
),
589
-
shared_weighted AS (
590
-
SELECT
591
-
u1.artist_id,
592
-
u1.artist_rank as user1_rank,
593
-
u2.artist_rank as user2_rank,
594
-
(1.0 / u1.artist_rank) * (1.0 / u2.artist_rank) as artist_weight,
595
-
ROW_NUMBER() OVER (ORDER BY (1.0 / u1.artist_rank) * (1.0 / u2.artist_rank) DESC) as weight_rank
596
-
FROM user_top_artists u1
597
-
INNER JOIN user_top_artists u2
598
-
ON u1.artist_id = u2.artist_id
599
-
AND u1.user_id = ?
600
-
AND u2.user_id = ?
601
-
WHERE u1.artist_rank <= 50
602
-
AND u2.artist_rank <= 50
603
-
),
604
-
compatibility_calc AS (
605
-
SELECT
606
-
SUM(sw.artist_weight) as weighted_overlap,
607
-
COUNT(*) as shared_count,
608
-
(SELECT total_artists FROM user_totals WHERE user_id = ?) as user1_total,
609
-
(SELECT total_artists FROM user_totals WHERE user_id = ?) as user2_total
610
-
FROM shared_weighted sw
611
-
)
612
-
SELECT
613
-
ROUND(
614
-
(shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100,
615
-
1
616
-
) as compatibility_percentage,
617
-
CASE
618
-
WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 20 THEN 'Low'
619
-
WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 40 THEN 'Medium'
620
-
WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 60 THEN 'High'
621
-
WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 75 THEN 'Very High'
622
-
WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 90 THEN 'Super'
623
-
ELSE 'ZOMG!1!'
624
-
END as compatibility_level,
625
-
shared_count as shared_artists,
626
-
user1_total as user1_artist_count,
627
-
user2_total as user2_artist_count,
628
-
to_json(LIST(a.name ORDER BY sw.artist_weight DESC) FILTER (WHERE sw.weight_rank <= 10)) as top_shared_artists,
629
-
to_json(LIST({
630
-
'id': a.id,
631
-
'name': a.name,
632
-
'picture': a.picture,
633
-
'uri': a.uri,
634
-
'user1_rank': sw.user1_rank,
635
-
'user2_rank': sw.user2_rank,
636
-
'weight': sw.artist_weight
637
-
} ORDER BY sw.artist_weight DESC) FILTER (WHERE sw.weight_rank <= 10)) as top_shared_detailed_artists
638
-
FROM compatibility_calc
639
-
CROSS JOIN shared_weighted sw
640
-
INNER JOIN artists a ON a.id = sw.artist_id
641
-
GROUP BY weighted_overlap, shared_count, user1_total, user2_total;
642
-
"#,
643
-
)?;
644
-
645
-
let compatibility = stmt.query_map(
646
-
[
647
-
¶ms.user_id1,
648
-
¶ms.user_id2,
649
-
¶ms.user_id1,
650
-
¶ms.user_id2,
651
-
¶ms.user_id1,
652
-
¶ms.user_id2,
653
-
],
654
-
|row| {
655
-
let top_shared_artists_json: String = row.get(5)?;
656
-
let top_shared_artists: Vec<String> =
657
-
serde_json::from_str(&top_shared_artists_json).unwrap_or_else(|_| Vec::new());
658
-
let top_shared_detailed_artists_json: String = row.get(6)?;
659
-
let top_shared_detailed_artists: Vec<SharedArtist> =
660
-
serde_json::from_str(&top_shared_detailed_artists_json)
661
-
.unwrap_or_else(|_| Vec::new());
662
-
Ok(Compatibility {
663
-
compatibility_percentage: row.get(0)?,
664
-
compatibility_level: row.get(1)?,
665
-
shared_artists: row.get(2)?,
666
-
user1_artist_count: row.get(3)?,
667
-
user2_artist_count: row.get(4)?,
668
-
top_shared_artists,
669
-
top_shared_detailed_artists,
670
-
})
671
-
},
672
-
)?;
673
-
674
-
let compatibility = compatibility.collect::<Result<Vec<_>, _>>()?;
675
-
let compatibility = compatibility.into_iter().next();
676
-
677
-
Ok(HttpResponse::Ok().json(compatibility))
678
-
}
-42
crates/analytics/src/subscriber/mod.rs
-42
crates/analytics/src/subscriber/mod.rs
···
679
679
680
680
pub async fn like(conn: Arc<Mutex<Connection>>, payload: LikePayload) -> Result<(), Error> {
681
681
let conn = conn.lock().unwrap();
682
-
683
-
let exists: bool = conn.query_row(
684
-
"SELECT EXISTS(SELECT 1 FROM loved_tracks WHERE user_id = ? AND track_id = ?)",
685
-
params![payload.user_id.xata_id, payload.track_id.xata_id],
686
-
|row| row.get(0),
687
-
)?;
688
-
689
-
if exists {
690
-
tracing::warn!(
691
-
"Like already exists, user_id = {} track_id = {}",
692
-
payload.user_id.xata_id,
693
-
payload.track_id.xata_id
694
-
);
695
-
return Ok(());
696
-
}
697
-
698
682
match conn.execute(
699
683
"INSERT INTO loved_tracks (
700
684
id,
···
953
937
"#;
954
938
955
939
match serde_json::from_str::<types::ScrobblePayload>(data) {
956
-
Err(e) => {
957
-
tracing::error!("Error parsing payload: {}", e);
958
-
tracing::error!("{}", data);
959
-
}
960
-
Ok(_) => {}
961
-
}
962
-
assert!(true);
963
-
}
964
-
965
-
#[test]
966
-
fn test_parse_like() {
967
-
let data = r#"{
968
-
"uri":"at://did:plc:7vdlgi2bflelz7mmuxoqjfcr/app.rocksky.like/3mb6kxku6js2u",
969
-
"user_id": {
970
-
"xata_id": "rec_cug4h6ibhfbm7uq5dte0"
971
-
},
972
-
"track_id": {
973
-
"xata_id":"rec_d11h6cdqrj64hn24639g"
974
-
},
975
-
"xata_createdat": "2025-12-30T04:59:55.203Z",
976
-
"xata_id":"rec_d59loiod60d9sc81mc80",
977
-
"xata_updatedat":"2025-12-30T04:59:55.203Z",
978
-
"xata_version":0
979
-
}"#;
980
-
981
-
match serde_json::from_str::<types::LikePayload>(data) {
982
940
Err(e) => {
983
941
tracing::error!("Error parsing payload: {}", e);
984
942
tracing::error!("{}", data);
-54
crates/analytics/src/types/stats.rs
-54
crates/analytics/src/types/stats.rs
···
128
128
}
129
129
}
130
130
}
131
-
132
-
#[derive(Debug, Serialize, Deserialize)]
133
-
pub struct GetNeighboursParams {
134
-
pub user_id: String,
135
-
}
136
-
137
-
#[derive(Debug, Serialize, Deserialize)]
138
-
pub struct NeighbourArtist {
139
-
pub id: String,
140
-
pub name: String,
141
-
pub picture: Option<String>,
142
-
pub uri: Option<String>,
143
-
}
144
-
145
-
#[derive(Debug, Serialize, Deserialize)]
146
-
pub struct Neighbour {
147
-
pub user_id: String,
148
-
pub display_name: Option<String>,
149
-
pub handle: String,
150
-
pub did: String,
151
-
pub avatar: String,
152
-
pub similarity_score: f64,
153
-
pub shared_artists_count: i64,
154
-
pub top_shared_artist_names: Vec<String>,
155
-
pub top_shared_artists_details: Vec<NeighbourArtist>,
156
-
}
157
-
158
-
#[derive(Debug, Serialize, Deserialize)]
159
-
pub struct GetCompatibilityParams {
160
-
pub user_id1: String,
161
-
pub user_id2: String,
162
-
}
163
-
164
-
#[derive(Debug, Serialize, Deserialize)]
165
-
pub struct SharedArtist {
166
-
pub id: String,
167
-
pub name: String,
168
-
pub picture: Option<String>,
169
-
pub uri: Option<String>,
170
-
pub user1_rank: i64,
171
-
pub user2_rank: i64,
172
-
pub weight: f64,
173
-
}
174
-
175
-
#[derive(Debug, Serialize, Deserialize)]
176
-
pub struct Compatibility {
177
-
pub compatibility_percentage: f64,
178
-
pub compatibility_level: String,
179
-
pub shared_artists: i64,
180
-
pub user1_artist_count: i64,
181
-
pub user2_artist_count: i64,
182
-
pub top_shared_artists: Vec<String>,
183
-
pub top_shared_detailed_artists: Vec<SharedArtist>,
184
-
}
+3
-50
crates/jetstream/src/repo.rs
+3
-50
crates/jetstream/src/repo.rs
···
8
8
9
9
use crate::{
10
10
profile::did_to_profile,
11
-
subscriber::{
12
-
ALBUM_NSID, ARTIST_NSID, FEED_GENERATOR_NSID, FOLLOW_NSID, SCROBBLE_NSID, SONG_NSID,
13
-
},
14
-
types::{
15
-
AlbumRecord, ArtistRecord, Commit, FeedGeneratorRecord, FollowRecord, ScrobbleRecord,
16
-
SongRecord,
17
-
},
11
+
subscriber::{ALBUM_NSID, ARTIST_NSID, FEED_GENERATOR_NSID, SCROBBLE_NSID, SONG_NSID},
12
+
types::{AlbumRecord, ArtistRecord, Commit, FeedGeneratorRecord, ScrobbleRecord, SongRecord},
18
13
webhook::discord::{
19
14
self,
20
15
model::{ScrobbleData, WebhookEnvelope},
···
40
35
ALBUM_NSID,
41
36
SONG_NSID,
42
37
FEED_GENERATOR_NSID,
43
-
FOLLOW_NSID,
44
38
]
45
39
.contains(&commit.collection.as_str())
46
40
{
···
192
186
let uri = format!("at://{}/app.rocksky.feed.generator/{}", did, commit.rkey);
193
187
194
188
let feed_generator_record: FeedGeneratorRecord =
195
-
serde_json::from_value(commit.record.clone())?;
189
+
serde_json::from_value(commit.record)?;
196
190
save_feed_generator(&mut tx, &user_id, feed_generator_record, &uri).await?;
197
-
198
-
tx.commit().await?;
199
-
}
200
-
201
-
if commit.collection == FOLLOW_NSID {
202
-
let mut tx = pool.begin().await?;
203
-
204
-
save_user(&mut tx, did).await?;
205
-
let uri = format!("at://{}/app.rocksky.graph.follow/{}", did, commit.rkey);
206
-
207
-
let follow_record: FollowRecord = serde_json::from_value(commit.record)?;
208
-
save_user(&mut tx, &follow_record.subject).await?;
209
-
save_follow(&mut tx, did, follow_record, &uri).await?;
210
191
211
192
tx.commit().await?;
212
193
}
···
1095
1076
.await?;
1096
1077
Ok(())
1097
1078
}
1098
-
1099
-
pub async fn save_follow(
1100
-
tx: &mut sqlx::Transaction<'_, Postgres>,
1101
-
did: &str,
1102
-
record: FollowRecord,
1103
-
uri: &str,
1104
-
) -> Result<(), Error> {
1105
-
tracing::info!(did = %did, uri = %uri, "Saving follow");
1106
-
1107
-
sqlx::query(
1108
-
r#"
1109
-
INSERT INTO follows (
1110
-
follower_did,
1111
-
subject_did,
1112
-
uri
1113
-
) VALUES (
1114
-
$1, $2, $3
1115
-
)
1116
-
ON CONFLICT (follower_did, subject_did) DO NOTHING
1117
-
"#,
1118
-
)
1119
-
.bind(did)
1120
-
.bind(record.subject)
1121
-
.bind(uri)
1122
-
.execute(&mut **tx)
1123
-
.await?;
1124
-
Ok(())
1125
-
}
-1
crates/jetstream/src/subscriber.rs
-1
crates/jetstream/src/subscriber.rs
···
17
17
pub const LIKE_NSID: &str = "app.rocksky.like";
18
18
pub const SHOUT_NSID: &str = "app.rocksky.shout";
19
19
pub const FEED_GENERATOR_NSID: &str = "app.rocksky.feed.generator";
20
-
pub const FOLLOW_NSID: &str = "app.rocksky.graph.follow";
21
20
22
21
pub struct ScrobbleSubscriber {
23
22
pub service_url: String,
-7
crates/jetstream/src/types.rs
-7
crates/jetstream/src/types.rs
-12
crates/jetstream/src/xata/follow.rs
-12
crates/jetstream/src/xata/follow.rs
···
1
-
use chrono::{DateTime, Utc};
2
-
use serde::Deserialize;
3
-
4
-
#[derive(Debug, sqlx::FromRow, Deserialize, Clone)]
5
-
pub struct Follow {
6
-
pub xata_id: String,
7
-
pub uri: String,
8
-
pub follower_did: String,
9
-
pub subject_did: String,
10
-
#[serde(with = "chrono::serde::ts_seconds")]
11
-
pub xata_createdat: DateTime<Utc>,
12
-
}
-1
crates/jetstream/src/xata/mod.rs
-1
crates/jetstream/src/xata/mod.rs
+1
-1
crates/spotify/src/lib.rs
+1
-1
crates/spotify/src/lib.rs
+1
-2
package.json
+1
-2
package.json
···
27
27
"dev:webscrobbler": "cargo run -p rockskyd --release -- webscrobbler",
28
28
"dev:tracklist": "cargo run -p rockskyd --release -- tracklist",
29
29
"db:pgpull": "cargo run -p rockskyd --release -- pull && rm -f rocksky-analytics.ddb* rocksky-feed.ddb* && curl -o rocksky-analytics.ddb https://backup.rocksky.app/rocksky-analytics.ddb && curl -o rocksky-feed.ddb https://backup.rocksky.app/rocksky-feed.ddb",
30
-
"dev:feeds": "cd apps/feeds && deno task dev",
31
30
"mb": "cd musicbrainz && go run main.go",
32
31
"spotify": "cd apps/api && bun run spotify",
33
32
"build:raichu": "cd crates/raichu && wasm-pack build --release --target web && cp -r pkg ../../apps/web/src",
···
44
43
"apps/uploads",
45
44
"apps/xata-proxy"
46
45
]
47
-
}
46
+
}