rhythia-api 144.0.0 → 145.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,121 +1,2190 @@
1
1
  import { handleApi } from "./handleApi"
2
2
 
3
3
  // ./api/approveMap.ts API
4
+
5
+ /*
6
+ export const Schema = {
7
+ input: z.strictObject({
8
+ session: z.string(),
9
+ mapId: z.number(),
10
+ }),
11
+ output: z.object({
12
+ error: z.string().optional(),
13
+ }),
14
+ };
15
+
16
+ export async function POST(request: Request) {
17
+ return protectedApi({
18
+ request,
19
+ schema: Schema,
20
+ authorization: validUser,
21
+ activity: handler,
22
+ });
23
+ }
24
+
25
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
26
+ const user = (await supabase.auth.getUser(data.session)).data.user!;
27
+ let { data: queryUserData, error: userError } = await supabase
28
+ .from("profiles")
29
+ .select("*")
30
+ .eq("uid", user.id)
31
+ .single();
32
+
33
+ if (!queryUserData) {
34
+ return NextResponse.json({ error: "Can't find user" });
35
+ }
36
+
37
+ const tags = (queryUserData?.badges || []) as string[];
38
+
39
+ if (!tags.includes("MMT")) {
40
+ return NextResponse.json({ error: "Only MMTs can approve maps!" });
41
+ }
42
+
43
+ const { data: mapData, error } = await supabase
44
+ .from("beatmapPages")
45
+ .select("id,nominations,owner")
46
+ .eq("id", data.mapId)
47
+ .single();
48
+
49
+ if (!mapData) {
50
+ return NextResponse.json({ error: "Bad map" });
51
+ }
52
+
53
+ if (mapData.owner == queryUserData.id) {
54
+ return NextResponse.json({ error: "Can't approve own map" });
55
+ }
56
+
57
+ if ((mapData.nominations as number[])!.length < 2) {
58
+ return NextResponse.json({
59
+ error: "Maps can get approved only if they have 2 nominations",
60
+ });
61
+ }
62
+
63
+ if ((mapData.nominations as number[]).includes(queryUserData.id)) {
64
+ return NextResponse.json({
65
+ error: "Can't nominate and approve",
66
+ });
67
+ }
68
+
69
+ await supabase.from("beatmapPages").upsert({
70
+ id: data.mapId,
71
+ status: "RANKED",
72
+ });
73
+
74
+ return NextResponse.json({});
75
+ }
4
76
  import { Schema as ApproveMap } from "./api/approveMap"
5
77
  export { Schema as SchemaApproveMap } from "./api/approveMap"
6
78
  export const approveMap = handleApi({url:"/api/approveMap",...ApproveMap})
7
79
 
8
80
  // ./api/createBeatmap.ts API
81
+
82
+ /*
83
+ export const Schema = {
84
+ input: z.strictObject({
85
+ url: z.string(),
86
+ session: z.string(),
87
+ updateFlag: z.boolean().optional(),
88
+ }),
89
+ output: z.strictObject({
90
+ hash: z.string().optional(),
91
+ error: z.string().optional(),
92
+ }),
93
+ };
94
+
95
+ export async function POST(request: Request): Promise<NextResponse> {
96
+ return protectedApi({
97
+ request,
98
+ schema: Schema,
99
+ authorization: validUser,
100
+ activity: handler,
101
+ });
102
+ }
103
+
104
+ export async function handler({
105
+ url,
106
+ session,
107
+ updateFlag,
108
+ }: (typeof Schema)["input"]["_type"]): Promise<
109
+ NextResponse<(typeof Schema)["output"]["_type"]>
110
+ > {
111
+ if (!url.startsWith(`https://static.rhythia.com/`))
112
+ return NextResponse.json({ error: "Invalid url" });
113
+
114
+ const request = await fetch(url);
115
+ const bytes = await request.arrayBuffer();
116
+ const parser = new SSPMParser(Buffer.from(bytes));
117
+
118
+ const parsedData = parser.parse();
119
+ const digested = parsedData.strings.mapID;
120
+
121
+ const user = (await supabase.auth.getUser(session)).data.user!;
122
+ let { data: userData, error: userError } = await supabase
123
+ .from("profiles")
124
+ .select("*")
125
+ .eq("uid", user.id)
126
+ .single();
127
+
128
+ if (!userData) {
129
+ return NextResponse.json({ error: "Bad user" });
130
+ }
131
+
132
+ if (
133
+ userData.ban == "excluded" ||
134
+ userData.ban == "restricted" ||
135
+ userData.ban == "silenced"
136
+ ) {
137
+ return NextResponse.json(
138
+ {
139
+ error:
140
+ "Silenced, restricted or excluded players can't update their profile.",
141
+ },
142
+ { status: 404 }
143
+ );
144
+ }
145
+
146
+ let { data: beatmapPage, error: errorlast } = await supabase
147
+ .from("beatmapPages")
148
+ .select(`*`)
149
+ .eq("latestBeatmapHash", digested)
150
+ .single();
151
+
152
+ if (beatmapPage) {
153
+ if (!updateFlag) {
154
+ return NextResponse.json({ error: "Already Exists" });
155
+ } else if (beatmapPage.owner !== userData.id) {
156
+ return NextResponse.json({ error: "Already Exists" });
157
+ }
158
+ }
159
+
160
+ const imgkey = `beatmap-img-${Date.now()}-${digested}`;
161
+
162
+ let buffer = Buffer.from([]);
163
+ try {
164
+ buffer = await require("sharp")(parsedData.cover)
165
+ .resize(250)
166
+ .jpeg({ mozjpeg: true })
167
+ .toBuffer();
168
+ } catch (error) {}
169
+
170
+ const command = new PutObjectCommand({
171
+ Bucket: "rhthia-avatars",
172
+ Key: imgkey,
173
+ Body: buffer,
174
+ ContentType: "image/jpeg",
175
+ });
176
+
177
+ await s3Client.send(command);
178
+ const markers = parsedData.markers.sort((a, b) => a.position - b.position);
179
+
180
+ const upserted = await supabase.from("beatmaps").upsert({
181
+ beatmapHash: digested,
182
+ title: parsedData.strings.mapName,
183
+ playcount: 0,
184
+ difficulty: parsedData.metadata.difficulty,
185
+ noteCount: parsedData.metadata.noteCount,
186
+ length: markers[markers.length - 1].position,
187
+ beatmapFile: url,
188
+ image: `https://static.rhythia.com/${imgkey}`,
189
+ starRating: rateMap(parsedData),
190
+ });
191
+
192
+ if (upserted.error?.message.length) {
193
+ return NextResponse.json({ error: upserted.error.message });
194
+ }
195
+ return NextResponse.json({ hash: digested });
196
+ }
9
197
  import { Schema as CreateBeatmap } from "./api/createBeatmap"
10
198
  export { Schema as SchemaCreateBeatmap } from "./api/createBeatmap"
11
199
  export const createBeatmap = handleApi({url:"/api/createBeatmap",...CreateBeatmap})
12
200
 
13
201
  // ./api/createBeatmapPage.ts API
202
+
203
+ /*
204
+ export const Schema = {
205
+ input: z.strictObject({
206
+ session: z.string(),
207
+ }),
208
+ output: z.strictObject({
209
+ error: z.string().optional(),
210
+ id: z.number().optional(),
211
+ }),
212
+ };
213
+
214
+ export async function POST(request: Request): Promise<NextResponse> {
215
+ return protectedApi({
216
+ request,
217
+ schema: Schema,
218
+ authorization: validUser,
219
+ activity: handler,
220
+ });
221
+ }
222
+
223
+ export async function handler({
224
+ session,
225
+ }: (typeof Schema)["input"]["_type"]): Promise<
226
+ NextResponse<(typeof Schema)["output"]["_type"]>
227
+ > {
228
+ const user = (await supabase.auth.getUser(session)).data.user!;
229
+ let { data: userData, error: userError } = await supabase
230
+ .from("profiles")
231
+ .select("*")
232
+ .eq("uid", user.id)
233
+ .single();
234
+
235
+ if (!userData) return NextResponse.json({ error: "No user." });
236
+
237
+ if (
238
+ userData.ban == "excluded" ||
239
+ userData.ban == "restricted" ||
240
+ userData.ban == "silenced"
241
+ ) {
242
+ return NextResponse.json(
243
+ {
244
+ error:
245
+ "Silenced, restricted or excluded players can't update their profile.",
246
+ },
247
+ { status: 404 }
248
+ );
249
+ }
250
+
251
+ const upserted = await supabase
252
+ .from("beatmapPages")
253
+ .upsert({
254
+ owner: userData.id,
255
+ })
256
+ .select("*")
257
+ .single();
258
+ return NextResponse.json({ id: upserted.data?.id });
259
+ }
14
260
  import { Schema as CreateBeatmapPage } from "./api/createBeatmapPage"
15
261
  export { Schema as SchemaCreateBeatmapPage } from "./api/createBeatmapPage"
16
262
  export const createBeatmapPage = handleApi({url:"/api/createBeatmapPage",...CreateBeatmapPage})
17
263
 
18
264
  // ./api/deleteBeatmapPage.ts API
265
+
266
+ /*
267
+ export const Schema = {
268
+ input: z.strictObject({
269
+ session: z.string(),
270
+ id: z.number(),
271
+ }),
272
+ output: z.strictObject({
273
+ error: z.string().optional(),
274
+ }),
275
+ };
276
+
277
+ export async function POST(request: Request): Promise<NextResponse> {
278
+ return protectedApi({
279
+ request,
280
+ schema: Schema,
281
+ authorization: validUser,
282
+ activity: handler,
283
+ });
284
+ }
285
+
286
+ export async function handler({
287
+ session,
288
+ id,
289
+ }: (typeof Schema)["input"]["_type"]): Promise<
290
+ NextResponse<(typeof Schema)["output"]["_type"]>
291
+ > {
292
+ const user = (await supabase.auth.getUser(session)).data.user!;
293
+ let { data: userData, error: userError } = await supabase
294
+ .from("profiles")
295
+ .select("*")
296
+ .eq("uid", user.id)
297
+ .single();
298
+
299
+ let { data: pageData, error: pageError } = await supabase
300
+ .from("beatmapPages")
301
+ .select("*")
302
+ .eq("id", id)
303
+ .single();
304
+
305
+ if (!pageData) return NextResponse.json({ error: "No beatmap." });
306
+
307
+ let { data: beatmapData, error: bmPageError } = await supabase
308
+ .from("beatmaps")
309
+ .select("*")
310
+ .eq("beatmapHash", pageData.latestBeatmapHash || "-1-1-1-1")
311
+ .single();
312
+
313
+ if (!userData) return NextResponse.json({ error: "No user." });
314
+ if (!beatmapData) return NextResponse.json({ error: "No beatmap." });
315
+
316
+ if (userData.id !== pageData.owner)
317
+ return NextResponse.json({ error: "Non-authz user." });
318
+
319
+ if (pageData.status !== "UNRANKED")
320
+ return NextResponse.json({ error: "Only unranked maps can be updated" });
321
+
322
+ await supabase.from("beatmapPageComments").delete().eq("beatmapPage", id);
323
+ await supabase.from("beatmapPages").delete().eq("id", id);
324
+ await supabase
325
+ .from("beatmaps")
326
+ .delete()
327
+ .eq("beatmapHash", beatmapData.beatmapHash);
328
+
329
+ return NextResponse.json({});
330
+ }
19
331
  import { Schema as DeleteBeatmapPage } from "./api/deleteBeatmapPage"
20
332
  export { Schema as SchemaDeleteBeatmapPage } from "./api/deleteBeatmapPage"
21
333
  export const deleteBeatmapPage = handleApi({url:"/api/deleteBeatmapPage",...DeleteBeatmapPage})
22
334
 
23
335
  // ./api/editAboutMe.ts API
336
+
337
+ /*
338
+ export const Schema = {
339
+ input: z.strictObject({
340
+ session: z.string(),
341
+ data: z.object({
342
+ about_me: z.string().optional(),
343
+ }),
344
+ }),
345
+ output: z.object({
346
+ error: z.string().optional(),
347
+ }),
348
+ };
349
+
350
+ export async function POST(request: Request): Promise<NextResponse> {
351
+ return protectedApi({
352
+ request,
353
+ schema: Schema,
354
+ authorization: validUser,
355
+ activity: handler,
356
+ });
357
+ }
358
+
359
+ export async function handler(
360
+ data: (typeof Schema)["input"]["_type"]
361
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
362
+ if (!data.data.about_me) {
363
+ return NextResponse.json(
364
+ {
365
+ error: "Missing body.",
366
+ },
367
+ { status: 404 }
368
+ );
369
+ }
370
+
371
+ if (data.data.about_me.length > 10000) {
372
+ return NextResponse.json(
373
+ {
374
+ error: "Too long.",
375
+ },
376
+ { status: 404 }
377
+ );
378
+ }
379
+
380
+ const user = (await supabase.auth.getUser(data.session)).data.user!;
381
+ let userData: Database["public"]["Tables"]["profiles"]["Update"];
382
+
383
+ // Find user's entry
384
+ {
385
+ let { data: queryUserData, error } = await supabase
386
+ .from("profiles")
387
+ .select("*")
388
+ .eq("uid", user.id);
389
+
390
+ if (!queryUserData?.length) {
391
+ return NextResponse.json(
392
+ {
393
+ error: "User cannot be retrieved from session",
394
+ },
395
+ { status: 404 }
396
+ );
397
+ }
398
+ userData = queryUserData[0];
399
+ }
400
+
401
+ const upsertPayload: Database["public"]["Tables"]["profiles"]["Update"] = {
402
+ id: userData.id,
403
+ about_me: data.data.about_me,
404
+ };
405
+
406
+ const upsertResult = await supabase
407
+ .from("profiles")
408
+ .upsert(upsertPayload)
409
+ .select();
410
+
411
+ if (upsertResult.error) {
412
+ return NextResponse.json(
413
+ {
414
+ error: "Can't update..",
415
+ },
416
+ { status: 404 }
417
+ );
418
+ }
419
+
420
+ return NextResponse.json({});
421
+ }
24
422
  import { Schema as EditAboutMe } from "./api/editAboutMe"
25
423
  export { Schema as SchemaEditAboutMe } from "./api/editAboutMe"
26
424
  export const editAboutMe = handleApi({url:"/api/editAboutMe",...EditAboutMe})
27
425
 
28
426
  // ./api/editProfile.ts API
427
+
428
+ /*
429
+ export const Schema = {
430
+ input: z.strictObject({
431
+ session: z.string(),
432
+ data: z.object({
433
+ avatar_url: z.string().optional(),
434
+ username: z.string().optional(),
435
+ }),
436
+ }),
437
+ output: z.object({
438
+ error: z.string().optional(),
439
+ }),
440
+ };
441
+
442
+ export async function POST(request: Request): Promise<NextResponse> {
443
+ return protectedApi({
444
+ request,
445
+ schema: Schema,
446
+ authorization: validUser,
447
+ activity: handler,
448
+ });
449
+ }
450
+
451
+ export async function handler(
452
+ data: (typeof Schema)["input"]["_type"]
453
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
454
+ if (data.data.username !== undefined && data.data.username.length === 0) {
455
+ return NextResponse.json(
456
+ {
457
+ error: "Username can't be empty",
458
+ },
459
+ { status: 404 }
460
+ );
461
+ }
462
+
463
+ if (data.data.username && data.data.username.length > 20) {
464
+ return NextResponse.json(
465
+ {
466
+ error: "Username too long.",
467
+ },
468
+ { status: 404 }
469
+ );
470
+ }
471
+
472
+ const user = (await supabase.auth.getUser(data.session)).data.user!;
473
+
474
+ let userData: Database["public"]["Tables"]["profiles"]["Update"];
475
+
476
+ // Find user's entry
477
+ {
478
+ let { data: queryUserData, error } = await supabase
479
+ .from("profiles")
480
+ .select("*")
481
+ .eq("uid", user.id);
482
+
483
+ if (!queryUserData?.length) {
484
+ return NextResponse.json(
485
+ {
486
+ error: "User cannot be retrieved from session",
487
+ },
488
+ { status: 404 }
489
+ );
490
+ }
491
+ userData = queryUserData[0];
492
+ }
493
+
494
+ if (
495
+ userData.ban == "excluded" ||
496
+ userData.ban == "restricted" ||
497
+ userData.ban == "silenced"
498
+ ) {
499
+ return NextResponse.json(
500
+ {
501
+ error:
502
+ "Silenced, restricted or excluded players can't update their profile.",
503
+ },
504
+ { status: 404 }
505
+ );
506
+ }
507
+
508
+ const upsertPayload: Database["public"]["Tables"]["profiles"]["Update"] = {
509
+ id: userData.id,
510
+ computedUsername: data.data.username?.toLowerCase(),
511
+ ...data.data,
512
+ };
513
+
514
+ const upsertResult = await supabase
515
+ .from("profiles")
516
+ .upsert(upsertPayload)
517
+ .select();
518
+
519
+ if (upsertResult.error) {
520
+ return NextResponse.json(
521
+ {
522
+ error: "Can't update, username might be used by someone else!",
523
+ },
524
+ { status: 404 }
525
+ );
526
+ }
527
+
528
+ return NextResponse.json({});
529
+ }
29
530
  import { Schema as EditProfile } from "./api/editProfile"
30
531
  export { Schema as SchemaEditProfile } from "./api/editProfile"
31
532
  export const editProfile = handleApi({url:"/api/editProfile",...EditProfile})
32
533
 
33
534
  // ./api/getAvatarUploadUrl.ts API
535
+
536
+ /*
537
+ export const Schema = {
538
+ input: z.strictObject({
539
+ session: z.string(),
540
+ contentLength: z.number(),
541
+ contentType: z.string(),
542
+ intrinsicToken: z.string(),
543
+ }),
544
+ output: z.strictObject({
545
+ error: z.string().optional(),
546
+ url: z.string().optional(),
547
+ objectKey: z.string().optional(),
548
+ }),
549
+ };
550
+
551
+ export async function POST(request: Request): Promise<NextResponse> {
552
+ return protectedApi({
553
+ request,
554
+ schema: Schema,
555
+ authorization: validUser,
556
+ activity: handler,
557
+ });
558
+ }
559
+
560
+ export async function handler({
561
+ session,
562
+ contentLength,
563
+ contentType,
564
+ intrinsicToken,
565
+ }: (typeof Schema)["input"]["_type"]): Promise<
566
+ NextResponse<(typeof Schema)["output"]["_type"]>
567
+ > {
568
+ const user = (await supabase.auth.getUser(session)).data.user!;
569
+
570
+ if (!validateIntrinsicToken(intrinsicToken)) {
571
+ return NextResponse.json({
572
+ error: "Invalid intrinsic token",
573
+ });
574
+ }
575
+
576
+ if (contentLength > 5000000) {
577
+ return NextResponse.json({
578
+ error: "Max content length exceeded.",
579
+ });
580
+ }
581
+
582
+ const key = `user-avatar-${Date.now()}-${user.id}`;
583
+ const command = new PutObjectCommand({
584
+ Bucket: "rhthia-avatars",
585
+ Key: key,
586
+ ContentLength: contentLength,
587
+ ContentType: contentType,
588
+ });
589
+
590
+ const presigned = await getSignedUrl(s3Client, command, {
591
+ expiresIn: 3600,
592
+ });
593
+ return NextResponse.json({
594
+ url: presigned,
595
+ objectKey: key,
596
+ });
597
+ }
34
598
  import { Schema as GetAvatarUploadUrl } from "./api/getAvatarUploadUrl"
35
599
  export { Schema as SchemaGetAvatarUploadUrl } from "./api/getAvatarUploadUrl"
36
600
  export const getAvatarUploadUrl = handleApi({url:"/api/getAvatarUploadUrl",...GetAvatarUploadUrl})
37
601
 
38
602
  // ./api/getBadgedUsers.ts API
603
+
604
+ /*
605
+ export const Schema = {
606
+ input: z.strictObject({
607
+ badge: z.string(),
608
+ }),
609
+ output: z.object({
610
+ error: z.string().optional(),
611
+ leaderboard: z
612
+ .array(
613
+ z.object({
614
+ flag: z.string().nullable(),
615
+ id: z.number(),
616
+ username: z.string().nullable(),
617
+ })
618
+ )
619
+ .optional(),
620
+ }),
621
+ };
622
+
623
+ export async function POST(request: Request): Promise<NextResponse> {
624
+ return protectedApi({
625
+ request,
626
+ schema: Schema,
627
+ authorization: () => {},
628
+ activity: handler,
629
+ });
630
+ }
631
+
632
+ export async function handler(
633
+ data: (typeof Schema)["input"]["_type"]
634
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
635
+ const result = await getLeaderboard(data.badge);
636
+ return NextResponse.json(result);
637
+ }
638
+
639
+ export async function getLeaderboard(badge: string) {
640
+ let { data: queryData, error } = await supabase
641
+ .from("profiles")
642
+ .select("flag,id,username,badges");
643
+
644
+ const users = queryData?.filter((e) =>
645
+ ((e.badges || []) as string[]).includes(badge)
646
+ );
647
+
648
+ return {
649
+ leaderboard: users?.map((user) => ({
650
+ flag: user.flag,
651
+ id: user.id,
652
+ username: user.username,
653
+ })),
654
+ };
655
+ }
39
656
  import { Schema as GetBadgedUsers } from "./api/getBadgedUsers"
40
657
  export { Schema as SchemaGetBadgedUsers } from "./api/getBadgedUsers"
41
658
  export const getBadgedUsers = handleApi({url:"/api/getBadgedUsers",...GetBadgedUsers})
42
659
 
43
660
  // ./api/getBeatmapComments.ts API
661
+
662
+ /*
663
+ export const Schema = {
664
+ input: z.strictObject({
665
+ page: z.number(),
666
+ }),
667
+ output: z.strictObject({
668
+ error: z.string().optional(),
669
+ comments: z.array(
670
+ z.object({
671
+ beatmapPage: z.number(),
672
+ content: z.string().nullable(),
673
+ owner: z.number(),
674
+ created_at: z.string(),
675
+ profiles: z.object({
676
+ avatar_url: z.string().nullable(),
677
+ username: z.string().nullable(),
678
+ badges: z.any().nullable(),
679
+ }),
680
+ })
681
+ ),
682
+ }),
683
+ };
684
+
685
+ export async function POST(request: Request): Promise<NextResponse> {
686
+ return protectedApi({
687
+ request,
688
+ schema: Schema,
689
+ authorization: () => {},
690
+ activity: handler,
691
+ });
692
+ }
693
+
694
+ export async function handler({
695
+ page,
696
+ }: (typeof Schema)["input"]["_type"]): Promise<
697
+ NextResponse<(typeof Schema)["output"]["_type"]>
698
+ > {
699
+ let { data: userData, error: userError } = await supabase
700
+ .from("beatmapPageComments")
701
+ .select(
702
+ `
703
+ *,
704
+ profiles!inner(
705
+ username,
706
+ avatar_url,
707
+ badges
708
+ )
709
+ `
710
+ )
711
+ .eq("beatmapPage", page);
712
+
713
+ return NextResponse.json({ comments: userData! });
714
+ }
44
715
  import { Schema as GetBeatmapComments } from "./api/getBeatmapComments"
45
716
  export { Schema as SchemaGetBeatmapComments } from "./api/getBeatmapComments"
46
717
  export const getBeatmapComments = handleApi({url:"/api/getBeatmapComments",...GetBeatmapComments})
47
718
 
48
719
  // ./api/getBeatmapPage.ts API
720
+
721
+ /*
722
+ export const Schema = {
723
+ input: z.strictObject({
724
+ session: z.string(),
725
+ id: z.number(),
726
+ }),
727
+ output: z.object({
728
+ error: z.string().optional(),
729
+ beatmap: z
730
+ .object({
731
+ id: z.number().nullable().optional(),
732
+ nominations: z.array(z.number()).nullable().optional(),
733
+ playcount: z.number().nullable().optional(),
734
+ created_at: z.string().nullable().optional(),
735
+ difficulty: z.number().nullable().optional(),
736
+ noteCount: z.number().nullable().optional(),
737
+ length: z.number().nullable().optional(),
738
+ title: z.string().nullable().optional(),
739
+ ranked: z.boolean().nullable().optional(),
740
+ beatmapFile: z.string().nullable().optional(),
741
+ image: z.string().nullable().optional(),
742
+ starRating: z.number().nullable().optional(),
743
+ owner: z.number().nullable().optional(),
744
+ ownerUsername: z.string().nullable().optional(),
745
+ ownerAvatar: z.string().nullable().optional(),
746
+ status: z.string().nullable().optional(),
747
+ })
748
+ .optional(),
749
+ }),
750
+ };
751
+
752
+ export async function POST(request: Request): Promise<NextResponse> {
753
+ return protectedApi({
754
+ request,
755
+ schema: Schema,
756
+ authorization: () => {},
757
+ activity: handler,
758
+ });
759
+ }
760
+
761
+ export async function handler(
762
+ data: (typeof Schema)["input"]["_type"],
763
+ req: Request
764
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
765
+ let { data: beatmapPage, error: errorlast } = await supabase
766
+ .from("beatmapPages")
767
+ .select(
768
+ `
769
+ *,
770
+ beatmaps (
771
+ created_at,
772
+ playcount,
773
+ length,
774
+ ranked,
775
+ beatmapFile,
776
+ image,
777
+ starRating,
778
+ difficulty,
779
+ noteCount,
780
+ title
781
+ ),
782
+ profiles (
783
+ username,
784
+ avatar_url
785
+ )
786
+ `
787
+ )
788
+ .eq("id", data.id)
789
+ .single();
790
+
791
+ if (!beatmapPage) return NextResponse.json({});
792
+
793
+ return NextResponse.json({
794
+ beatmap: {
795
+ playcount: beatmapPage.beatmaps?.playcount,
796
+ created_at: beatmapPage.created_at,
797
+ difficulty: beatmapPage.beatmaps?.difficulty,
798
+ noteCount: beatmapPage.beatmaps?.noteCount,
799
+ length: beatmapPage.beatmaps?.length,
800
+ title: beatmapPage.beatmaps?.title,
801
+ ranked: beatmapPage.beatmaps?.ranked,
802
+ beatmapFile: beatmapPage.beatmaps?.beatmapFile,
803
+ image: beatmapPage.beatmaps?.image,
804
+ starRating: beatmapPage.beatmaps?.starRating,
805
+ owner: beatmapPage.owner,
806
+ ownerUsername: beatmapPage.profiles?.username,
807
+ ownerAvatar: beatmapPage.profiles?.avatar_url,
808
+ id: beatmapPage.id,
809
+ status: beatmapPage.status,
810
+ nominations: beatmapPage.nominations as number[],
811
+ },
812
+ });
813
+ }
49
814
  import { Schema as GetBeatmapPage } from "./api/getBeatmapPage"
50
815
  export { Schema as SchemaGetBeatmapPage } from "./api/getBeatmapPage"
51
816
  export const getBeatmapPage = handleApi({url:"/api/getBeatmapPage",...GetBeatmapPage})
52
817
 
53
818
  // ./api/getBeatmapPageById.ts API
819
+
820
+ /*
821
+ export const Schema = {
822
+ input: z.strictObject({
823
+ session: z.string(),
824
+ mapId: z.string(),
825
+ }),
826
+ output: z.object({
827
+ error: z.string().optional(),
828
+ beatmap: z
829
+ .object({
830
+ id: z.number().nullable().optional(),
831
+ nominations: z.array(z.number()).nullable().optional(),
832
+ playcount: z.number().nullable().optional(),
833
+ created_at: z.string().nullable().optional(),
834
+ difficulty: z.number().nullable().optional(),
835
+ noteCount: z.number().nullable().optional(),
836
+ length: z.number().nullable().optional(),
837
+ title: z.string().nullable().optional(),
838
+ ranked: z.boolean().nullable().optional(),
839
+ beatmapFile: z.string().nullable().optional(),
840
+ image: z.string().nullable().optional(),
841
+ starRating: z.number().nullable().optional(),
842
+ owner: z.number().nullable().optional(),
843
+ ownerUsername: z.string().nullable().optional(),
844
+ ownerAvatar: z.string().nullable().optional(),
845
+ status: z.string().nullable().optional(),
846
+ })
847
+ .optional(),
848
+ }),
849
+ };
850
+
851
+ export async function POST(request: Request): Promise<NextResponse> {
852
+ return protectedApi({
853
+ request,
854
+ schema: Schema,
855
+ authorization: () => {},
856
+ activity: handler,
857
+ });
858
+ }
859
+
860
+ export async function handler(
861
+ data: (typeof Schema)["input"]["_type"],
862
+ req: Request
863
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
864
+ let { data: beatmapPage, error: errorlast } = await supabase
865
+ .from("beatmapPages")
866
+ .select(
867
+ `
868
+ *,
869
+ beatmaps (
870
+ created_at,
871
+ playcount,
872
+ length,
873
+ ranked,
874
+ beatmapFile,
875
+ image,
876
+ starRating,
877
+ difficulty,
878
+ noteCount,
879
+ title
880
+ ),
881
+ profiles (
882
+ username,
883
+ avatar_url
884
+ )
885
+ `
886
+ )
887
+ .eq("latestBeatmapHash", data.mapId)
888
+ .single();
889
+
890
+ if (!beatmapPage) return NextResponse.json({});
891
+
892
+ return NextResponse.json({
893
+ beatmap: {
894
+ playcount: beatmapPage.beatmaps?.playcount,
895
+ created_at: beatmapPage.created_at,
896
+ difficulty: beatmapPage.beatmaps?.difficulty,
897
+ noteCount: beatmapPage.beatmaps?.noteCount,
898
+ length: beatmapPage.beatmaps?.length,
899
+ title: beatmapPage.beatmaps?.title,
900
+ ranked: beatmapPage.beatmaps?.ranked,
901
+ beatmapFile: beatmapPage.beatmaps?.beatmapFile,
902
+ image: beatmapPage.beatmaps?.image,
903
+ starRating: beatmapPage.beatmaps?.starRating,
904
+ owner: beatmapPage.owner,
905
+ ownerUsername: beatmapPage.profiles?.username,
906
+ ownerAvatar: beatmapPage.profiles?.avatar_url,
907
+ id: beatmapPage.id,
908
+ status: beatmapPage.status,
909
+ nominations: beatmapPage.nominations as number[],
910
+ },
911
+ });
912
+ }
54
913
  import { Schema as GetBeatmapPageById } from "./api/getBeatmapPageById"
55
914
  export { Schema as SchemaGetBeatmapPageById } from "./api/getBeatmapPageById"
56
915
  export const getBeatmapPageById = handleApi({url:"/api/getBeatmapPageById",...GetBeatmapPageById})
57
916
 
58
917
  // ./api/getBeatmaps.ts API
918
+
919
+ /*
920
+ export const Schema = {
921
+ input: z.strictObject({
922
+ session: z.string(),
923
+ textFilter: z.string().optional(),
924
+ authorFilter: z.string().optional(),
925
+ tagsFilter: z.string().optional(),
926
+ page: z.number().default(1),
927
+ maxStars: z.number().optional(),
928
+ minLength: z.number().optional(),
929
+ maxLength: z.number().optional(),
930
+ minStars: z.number().optional(),
931
+ creator: z.number().optional(),
932
+ status: z.string().optional(),
933
+ }),
934
+ output: z.object({
935
+ error: z.string().optional(),
936
+ total: z.number(),
937
+ viewPerPage: z.number(),
938
+ currentPage: z.number(),
939
+ beatmaps: z
940
+ .array(
941
+ z.object({
942
+ id: z.number(),
943
+ playcount: z.number().nullable().optional(),
944
+ created_at: z.string().nullable().optional(),
945
+ difficulty: z.number().nullable().optional(),
946
+ noteCount: z.number().nullable().optional(),
947
+ length: z.number().nullable().optional(),
948
+ title: z.string().nullable().optional(),
949
+ ranked: z.boolean().nullable().optional(),
950
+ beatmapFile: z.string().nullable().optional(),
951
+ image: z.string().nullable().optional(),
952
+ starRating: z.number().nullable().optional(),
953
+ owner: z.number().nullable().optional(),
954
+ ownerUsername: z.string().nullable().optional(),
955
+ ownerAvatar: z.string().nullable().optional(),
956
+ status: z.string().nullable().optional(),
957
+ tags: z.string().nullable().optional(),
958
+ })
959
+ )
960
+ .optional(),
961
+ }),
962
+ };
963
+
964
+ export async function POST(request: Request): Promise<NextResponse> {
965
+ return protectedApi({
966
+ request,
967
+ schema: Schema,
968
+ authorization: () => {},
969
+ activity: handler,
970
+ });
971
+ }
972
+
973
+ export async function handler(
974
+ data: (typeof Schema)["input"]["_type"]
975
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
976
+ const result = await getBeatmaps(data);
977
+ return NextResponse.json(result);
978
+ }
979
+
980
+ const VIEW_PER_PAGE = 50;
981
+
982
+ export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
983
+ const startPage = (data.page - 1) * VIEW_PER_PAGE;
984
+ const endPage = startPage + VIEW_PER_PAGE - 1;
985
+ const countQuery = await supabase
986
+ .from("beatmapPages")
987
+ .select("id", { count: "exact", head: true });
988
+
989
+ let qry = supabase
990
+ .from("beatmapPages")
991
+ .select(
992
+ `
993
+ owner,
994
+ created_at,
995
+ id,
996
+ status,
997
+ tags,
998
+ beatmaps!inner(
999
+ playcount,
1000
+ ranked,
1001
+ beatmapFile,
1002
+ image,
1003
+ starRating,
1004
+ difficulty,
1005
+ length,
1006
+ title
1007
+ ),
1008
+ profiles!inner(
1009
+ username
1010
+ )`
1011
+ )
1012
+ .order("created_at", { ascending: false });
1013
+
1014
+ if (data.textFilter) {
1015
+ qry = qry.ilike("beatmaps.title", `%${data.textFilter}%`);
1016
+ }
1017
+
1018
+ if (data.authorFilter) {
1019
+ qry = qry.ilike("profiles.username", `%${data.authorFilter}%`);
1020
+ }
1021
+
1022
+ if (data.tagsFilter) {
1023
+ qry = qry.ilike("tags", `%${data.tagsFilter}%`);
1024
+ }
1025
+
1026
+ if (data.minStars) {
1027
+ qry = qry.gt("beatmaps.starRating", data.minStars);
1028
+ }
1029
+
1030
+ if (data.maxStars) {
1031
+ qry = qry.lt("beatmaps.starRating", data.maxStars);
1032
+ }
1033
+
1034
+ if (data.minLength) {
1035
+ qry = qry.gt("beatmaps.length", data.minLength);
1036
+ }
1037
+
1038
+ if (data.maxLength) {
1039
+ qry = qry.lt("beatmaps.length", data.maxLength);
1040
+ }
1041
+
1042
+ if (data.status) {
1043
+ qry = qry.eq("status", data.status);
1044
+ }
1045
+
1046
+ if (data.creator !== undefined) {
1047
+ qry = qry.eq("owner", data.creator);
1048
+ }
1049
+
1050
+ let queryData = await qry.range(startPage, endPage);
1051
+
1052
+ return {
1053
+ total: countQuery.count || 0,
1054
+ viewPerPage: VIEW_PER_PAGE,
1055
+ currentPage: data.page,
1056
+ beatmaps: queryData.data?.map((beatmapPage) => ({
1057
+ id: beatmapPage.id,
1058
+ tags: beatmapPage.tags,
1059
+ playcount: beatmapPage.beatmaps?.playcount,
1060
+ created_at: beatmapPage.created_at,
1061
+ difficulty: beatmapPage.beatmaps?.difficulty,
1062
+ title: beatmapPage.beatmaps?.title,
1063
+ ranked: beatmapPage.beatmaps?.ranked,
1064
+ length: beatmapPage.beatmaps?.length,
1065
+ beatmapFile: beatmapPage.beatmaps?.beatmapFile,
1066
+ image: beatmapPage.beatmaps?.image,
1067
+ starRating: beatmapPage.beatmaps?.starRating,
1068
+ owner: beatmapPage.owner,
1069
+ status: beatmapPage.status,
1070
+ ownerUsername: beatmapPage.profiles?.username,
1071
+ })),
1072
+ };
1073
+ }
59
1074
  import { Schema as GetBeatmaps } from "./api/getBeatmaps"
60
1075
  export { Schema as SchemaGetBeatmaps } from "./api/getBeatmaps"
61
1076
  export const getBeatmaps = handleApi({url:"/api/getBeatmaps",...GetBeatmaps})
62
1077
 
63
1078
  // ./api/getLeaderboard.ts API
1079
+
1080
+ /*
1081
+ export const Schema = {
1082
+ input: z.strictObject({
1083
+ session: z.string(),
1084
+ page: z.number().default(1),
1085
+ }),
1086
+ output: z.object({
1087
+ error: z.string().optional(),
1088
+ total: z.number(),
1089
+ viewPerPage: z.number(),
1090
+ currentPage: z.number(),
1091
+ userPosition: z.number(),
1092
+ leaderboard: z
1093
+ .array(
1094
+ z.object({
1095
+ flag: z.string().nullable(),
1096
+ id: z.number(),
1097
+ username: z.string().nullable(),
1098
+ play_count: z.number().nullable(),
1099
+ skill_points: z.number().nullable(),
1100
+ total_score: z.number().nullable(),
1101
+ })
1102
+ )
1103
+ .optional(),
1104
+ }),
1105
+ };
1106
+
1107
+ export async function POST(request: Request): Promise<NextResponse> {
1108
+ return protectedApi({
1109
+ request,
1110
+ schema: Schema,
1111
+ authorization: () => {},
1112
+ activity: handler,
1113
+ });
1114
+ }
1115
+
1116
+ export async function handler(
1117
+ data: (typeof Schema)["input"]["_type"]
1118
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
1119
+ const result = await getLeaderboard(data.page, data.session);
1120
+ return NextResponse.json(result);
1121
+ }
1122
+
1123
+ const VIEW_PER_PAGE = 50;
1124
+
1125
+ export async function getLeaderboard(page = 1, session: string) {
1126
+ const getUserData = await getUser({ session });
1127
+
1128
+ let leaderPosition = 0;
1129
+
1130
+ if (getUserData) {
1131
+ let { data: queryData, error } = await supabase
1132
+ .from("profiles")
1133
+ .select("*")
1134
+ .eq("uid", getUserData.data.user.id)
1135
+ .single();
1136
+
1137
+ if (queryData) {
1138
+ const { count: playersWithMorePoints, error: rankError } = await supabase
1139
+ .from("profiles")
1140
+ .select("*", { count: "exact", head: true })
1141
+ .gt("skill_points", queryData.skill_points);
1142
+
1143
+ leaderPosition = (playersWithMorePoints || 0) + 1;
1144
+ }
1145
+ }
1146
+
1147
+ const startPage = (page - 1) * VIEW_PER_PAGE;
1148
+ const endPage = startPage + VIEW_PER_PAGE - 1;
1149
+ const countQuery = await supabase
1150
+ .from("profiles")
1151
+ .select("ban", { count: "exact", head: true })
1152
+ .neq("ban", "excluded");
1153
+
1154
+ let { data: queryData, error } = await supabase
1155
+ .from("profiles")
1156
+ .select("*")
1157
+ .neq("ban", "excluded")
1158
+ .order("skill_points", { ascending: false })
1159
+ .range(startPage, endPage);
1160
+
1161
+ return {
1162
+ total: countQuery.count || 0,
1163
+ viewPerPage: VIEW_PER_PAGE,
1164
+ currentPage: page,
1165
+ userPosition: leaderPosition,
1166
+ leaderboard: queryData?.map((user) => ({
1167
+ flag: user.flag,
1168
+ id: user.id,
1169
+ play_count: user.play_count,
1170
+ skill_points: user.skill_points,
1171
+ total_score: user.total_score,
1172
+ username: user.username,
1173
+ })),
1174
+ };
1175
+ }
64
1176
  import { Schema as GetLeaderboard } from "./api/getLeaderboard"
65
1177
  export { Schema as SchemaGetLeaderboard } from "./api/getLeaderboard"
66
1178
  export const getLeaderboard = handleApi({url:"/api/getLeaderboard",...GetLeaderboard})
67
1179
 
68
1180
  // ./api/getMapUploadUrl.ts API
1181
+
1182
+ /*
1183
+ export const Schema = {
1184
+ input: z.strictObject({
1185
+ mapName: z.string().optional(),
1186
+ session: z.string(),
1187
+ contentLength: z.number(),
1188
+ contentType: z.string(),
1189
+ intrinsicToken: z.string(),
1190
+ }),
1191
+ output: z.strictObject({
1192
+ error: z.string().optional(),
1193
+ url: z.string().optional(),
1194
+ objectKey: z.string().optional(),
1195
+ }),
1196
+ };
1197
+
1198
+ export async function POST(request: Request): Promise<NextResponse> {
1199
+ return protectedApi({
1200
+ request,
1201
+ schema: Schema,
1202
+ authorization: validUser,
1203
+ activity: handler,
1204
+ });
1205
+ }
1206
+
1207
+ export async function handler({
1208
+ mapName,
1209
+ session,
1210
+ contentLength,
1211
+ contentType,
1212
+ intrinsicToken,
1213
+ }: (typeof Schema)["input"]["_type"]): Promise<
1214
+ NextResponse<(typeof Schema)["output"]["_type"]>
1215
+ > {
1216
+ const user = (await supabase.auth.getUser(session)).data.user!;
1217
+
1218
+ if (!validateIntrinsicToken(intrinsicToken)) {
1219
+ return NextResponse.json({
1220
+ error: "Invalid intrinsic token",
1221
+ });
1222
+ }
1223
+
1224
+ if (contentLength > 50000000) {
1225
+ return NextResponse.json({
1226
+ error: "Max content length exceeded.",
1227
+ });
1228
+ }
1229
+
1230
+ if (contentType !== "application/octet-stream") {
1231
+ return NextResponse.json({
1232
+ error: "Unnacceptable format",
1233
+ });
1234
+ }
1235
+
1236
+ const key = `rhythia-${mapName ? mapName : user.id}-${Date.now()}.sspm`;
1237
+ const command = new PutObjectCommand({
1238
+ Bucket: "rhthia-avatars",
1239
+ Key: key,
1240
+ ContentLength: contentLength,
1241
+ ContentType: contentType,
1242
+ });
1243
+
1244
+ const presigned = await getSignedUrl(s3Client, command, {
1245
+ expiresIn: 3600,
1246
+ });
1247
+ return NextResponse.json({
1248
+ url: presigned,
1249
+ objectKey: key,
1250
+ });
1251
+ }
69
1252
  import { Schema as GetMapUploadUrl } from "./api/getMapUploadUrl"
70
1253
  export { Schema as SchemaGetMapUploadUrl } from "./api/getMapUploadUrl"
71
1254
  export const getMapUploadUrl = handleApi({url:"/api/getMapUploadUrl",...GetMapUploadUrl})
72
1255
 
73
1256
  // ./api/getProfile.ts API
1257
+
1258
+ /*
1259
+ export const Schema = {
1260
+ input: z.strictObject({
1261
+ session: z.string(),
1262
+ id: z.number().nullable().optional(),
1263
+ }),
1264
+ output: z.object({
1265
+ error: z.string().optional(),
1266
+ user: z
1267
+ .object({
1268
+ about_me: z.string().nullable(),
1269
+ avatar_url: z.string().nullable(),
1270
+ profile_image: z.string().nullable(),
1271
+ badges: z.any().nullable(),
1272
+ created_at: z.number().nullable(),
1273
+ flag: z.string().nullable(),
1274
+ id: z.number(),
1275
+ uid: z.string().nullable(),
1276
+ ban: z.string().nullable(),
1277
+ username: z.string().nullable(),
1278
+ verified: z.boolean().nullable(),
1279
+ play_count: z.number().nullable(),
1280
+ skill_points: z.number().nullable(),
1281
+ squares_hit: z.number().nullable(),
1282
+ total_score: z.number().nullable(),
1283
+ position: z.number().nullable(),
1284
+ })
1285
+ .optional(),
1286
+ }),
1287
+ };
1288
+
1289
+ export async function POST(request: Request): Promise<NextResponse> {
1290
+ return protectedApi({
1291
+ request,
1292
+ schema: Schema,
1293
+ authorization: () => {},
1294
+ activity: handler,
1295
+ });
1296
+ }
1297
+
1298
+ export async function handler(
1299
+ data: (typeof Schema)["input"]["_type"],
1300
+ req: Request
1301
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
1302
+ let profiles: Database["public"]["Tables"]["profiles"]["Row"][] = [];
1303
+
1304
+ // Fetch by id
1305
+ if (data.id !== undefined && data.id !== null) {
1306
+ let { data: queryData, error } = await supabase
1307
+ .from("profiles")
1308
+ .select("*")
1309
+ .eq("id", data.id);
1310
+
1311
+ console.log(profiles, error);
1312
+
1313
+ if (!queryData?.length) {
1314
+ return NextResponse.json(
1315
+ {
1316
+ error: "User not found",
1317
+ },
1318
+ { status: 404 }
1319
+ );
1320
+ }
1321
+
1322
+ profiles = queryData;
1323
+ } else {
1324
+ // Fetch by session id
1325
+ const user = (await supabase.auth.getUser(data.session)).data.user;
1326
+
1327
+ if (user) {
1328
+ let { data: queryData, error } = await supabase
1329
+ .from("profiles")
1330
+ .select("*")
1331
+ .eq("uid", user.id);
1332
+
1333
+ if (!queryData?.length) {
1334
+ const geo = geolocation(req);
1335
+ const data = await supabase
1336
+ .from("profiles")
1337
+ .upsert({
1338
+ uid: user.id,
1339
+ about_me: "",
1340
+ avatar_url:
1341
+ "https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
1342
+ badges: [],
1343
+ username: `${user.user_metadata.full_name.slice(0, 20)}${Math.round(
1344
+ Math.random() * 900000 + 100000
1345
+ )}`,
1346
+ computedUsername: `${user.user_metadata.full_name.slice(
1347
+ 0,
1348
+ 20
1349
+ )}${Math.round(Math.random() * 900000 + 100000)}`.toLowerCase(),
1350
+ flag: (geo.country || "US").toUpperCase(),
1351
+ created_at: Date.now(),
1352
+ })
1353
+ .select();
1354
+
1355
+ profiles = data.data!;
1356
+ } else {
1357
+ profiles = queryData;
1358
+ }
1359
+ }
1360
+ }
1361
+
1362
+ const user = profiles[0];
1363
+
1364
+ // Query to count how many players have more skill points than the specific player
1365
+ const { count: playersWithMorePoints, error: rankError } = await supabase
1366
+ .from("profiles")
1367
+ .select("*", { count: "exact", head: true })
1368
+ .neq("ban", "excluded")
1369
+ .gt("skill_points", user.skill_points);
1370
+
1371
+ return NextResponse.json({
1372
+ user: {
1373
+ ...user,
1374
+ position: (playersWithMorePoints || 0) + 1,
1375
+ },
1376
+ });
1377
+ }
74
1378
  import { Schema as GetProfile } from "./api/getProfile"
75
1379
  export { Schema as SchemaGetProfile } from "./api/getProfile"
76
1380
  export const getProfile = handleApi({url:"/api/getProfile",...GetProfile})
77
1381
 
78
1382
  // ./api/getPublicStats.ts API
1383
+
1384
+ /*
1385
+ export const Schema = {
1386
+ input: z.strictObject({}),
1387
+ output: z.object({
1388
+ profiles: z.number(),
1389
+ beatmaps: z.number(),
1390
+ scores: z.number(),
1391
+ }),
1392
+ };
1393
+
1394
+ export async function POST(request: Request) {
1395
+ return protectedApi({
1396
+ request,
1397
+ schema: Schema,
1398
+ authorization: () => {},
1399
+ activity: handler,
1400
+ });
1401
+ }
1402
+
1403
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
1404
+ const countProfilesQuery = await supabase
1405
+ .from("profiles")
1406
+ .select("*", { count: "exact", head: true });
1407
+
1408
+ const countBeatmapsQuery = await supabase
1409
+ .from("beatmaps")
1410
+ .select("*", { count: "exact", head: true });
1411
+
1412
+ const countScoresQuery = await supabase
1413
+ .from("scores")
1414
+ .select("*", { count: "exact", head: true });
1415
+
1416
+ return NextResponse.json({
1417
+ beatmaps: countBeatmapsQuery.count,
1418
+ profiles: countProfilesQuery.count,
1419
+ scores: countScoresQuery.count,
1420
+ });
1421
+ }
79
1422
  import { Schema as GetPublicStats } from "./api/getPublicStats"
80
1423
  export { Schema as SchemaGetPublicStats } from "./api/getPublicStats"
81
1424
  export const getPublicStats = handleApi({url:"/api/getPublicStats",...GetPublicStats})
82
1425
 
83
1426
  // ./api/getScore.ts API
1427
+
1428
+ /*
1429
+ export const Schema = {
1430
+ input: z.strictObject({
1431
+ session: z.string(),
1432
+ id: z.number(),
1433
+ }),
1434
+ output: z.object({
1435
+ error: z.string().optional(),
1436
+ score: z
1437
+ .object({
1438
+ awarded_sp: z.number().nullable(),
1439
+ beatmapHash: z.string().nullable(),
1440
+ created_at: z.string(),
1441
+ id: z.number(),
1442
+ misses: z.number().nullable(),
1443
+ passed: z.boolean().nullable(),
1444
+ songId: z.string().nullable(),
1445
+ userId: z.number().nullable(),
1446
+ beatmapDifficulty: z.number().optional().nullable(),
1447
+ beatmapNotes: z.number().optional().nullable(),
1448
+ beatmapTitle: z.string().optional().nullable(),
1449
+ username: z.string().optional().nullable(),
1450
+ speed: z.number().optional().nullable(),
1451
+ })
1452
+ .optional(),
1453
+ }),
1454
+ };
1455
+
1456
+ export async function POST(request: Request): Promise<NextResponse> {
1457
+ return protectedApi({
1458
+ request,
1459
+ schema: Schema,
1460
+ authorization: () => {},
1461
+ activity: handler,
1462
+ });
1463
+ }
1464
+
1465
+ export async function handler(
1466
+ data: (typeof Schema)["input"]["_type"],
1467
+ req: Request
1468
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
1469
+ let { data: score, error: errorlast } = await supabase
1470
+ .from("scores")
1471
+ .select(
1472
+ `
1473
+ *,
1474
+ beatmaps (
1475
+ difficulty,
1476
+ noteCount,
1477
+ title
1478
+ ),
1479
+ profiles (
1480
+ username
1481
+ )
1482
+ `
1483
+ )
1484
+ .eq("id", data.id)
1485
+ .single();
1486
+
1487
+ if (!score) return NextResponse.json({});
1488
+
1489
+ return NextResponse.json({
1490
+ score: {
1491
+ created_at: score.created_at,
1492
+ id: score.id,
1493
+ passed: score.passed,
1494
+ userId: score.userId,
1495
+ awarded_sp: score.awarded_sp,
1496
+ beatmapHash: score.beatmapHash,
1497
+ misses: score.misses,
1498
+ songId: score.songId,
1499
+ beatmapDifficulty: score.beatmaps?.difficulty,
1500
+ beatmapNotes: score.beatmaps?.noteCount,
1501
+ beatmapTitle: score.beatmaps?.title,
1502
+ username: score.profiles?.username,
1503
+ speed: score.speed,
1504
+ },
1505
+ });
1506
+ }
84
1507
  import { Schema as GetScore } from "./api/getScore"
85
1508
  export { Schema as SchemaGetScore } from "./api/getScore"
86
1509
  export const getScore = handleApi({url:"/api/getScore",...GetScore})
87
1510
 
88
1511
  // ./api/getUserScores.ts API
1512
+
1513
+ /*
1514
+ export const Schema = {
1515
+ input: z.strictObject({
1516
+ session: z.string(),
1517
+ id: z.number(),
1518
+ }),
1519
+ output: z.object({
1520
+ error: z.string().optional(),
1521
+ lastDay: z
1522
+ .array(
1523
+ z.object({
1524
+ awarded_sp: z.number().nullable(),
1525
+ beatmapHash: z.string().nullable(),
1526
+ created_at: z.string(),
1527
+ id: z.number(),
1528
+ misses: z.number().nullable(),
1529
+ passed: z.boolean().nullable(),
1530
+ songId: z.string().nullable(),
1531
+ userId: z.number().nullable(),
1532
+ beatmapDifficulty: z.number().optional().nullable(),
1533
+ beatmapNotes: z.number().optional().nullable(),
1534
+ beatmapTitle: z.string().optional().nullable(),
1535
+ speed: z.number().optional().nullable(),
1536
+ })
1537
+ )
1538
+ .optional(),
1539
+ top: z
1540
+ .array(
1541
+ z.object({
1542
+ awarded_sp: z.number().nullable(),
1543
+ beatmapHash: z.string().nullable(),
1544
+ created_at: z.string(),
1545
+ id: z.number(),
1546
+ misses: z.number().nullable(),
1547
+ passed: z.boolean().nullable(),
1548
+ rank: z.string().nullable(),
1549
+ songId: z.string().nullable(),
1550
+ userId: z.number().nullable(),
1551
+ beatmapDifficulty: z.number().optional().nullable(),
1552
+ beatmapNotes: z.number().optional().nullable(),
1553
+ beatmapTitle: z.string().optional().nullable(),
1554
+ speed: z.number().optional().nullable(),
1555
+ })
1556
+ )
1557
+ .optional(),
1558
+ }),
1559
+ };
1560
+
1561
+ export async function POST(request: Request): Promise<NextResponse> {
1562
+ return protectedApi({
1563
+ request,
1564
+ schema: Schema,
1565
+ authorization: () => {},
1566
+ activity: handler,
1567
+ });
1568
+ }
1569
+
1570
+ export async function handler(
1571
+ data: (typeof Schema)["input"]["_type"],
1572
+ req: Request
1573
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
1574
+ let { data: scores1, error: errorlast } = await supabase
1575
+ .from("scores")
1576
+ .select(
1577
+ `
1578
+ *,
1579
+ beatmaps (
1580
+ difficulty,
1581
+ noteCount,
1582
+ title
1583
+ )
1584
+ `
1585
+ )
1586
+ .eq("userId", data.id)
1587
+ .eq("passed", true)
1588
+ .order("created_at", { ascending: false })
1589
+ .limit(10);
1590
+
1591
+ let { data: scores2, error: errorsp } = await supabase
1592
+ .from("scores")
1593
+ .select(
1594
+ `
1595
+ *,
1596
+ beatmaps (
1597
+ difficulty,
1598
+ noteCount,
1599
+ title
1600
+ )
1601
+ `
1602
+ )
1603
+ .eq("userId", data.id)
1604
+ .neq("awarded_sp", 0)
1605
+ .eq("passed", true)
1606
+ .order("awarded_sp", { ascending: false });
1607
+
1608
+ if (scores2 == null) return NextResponse.json({ error: "No scores" });
1609
+
1610
+ let hashMap: Record<string, { awarded_sp: number; score: any }> = {};
1611
+
1612
+ for (const score of scores2) {
1613
+ const { beatmapHash, awarded_sp } = score;
1614
+
1615
+ if (!beatmapHash || !awarded_sp) continue;
1616
+
1617
+ if (!hashMap[beatmapHash] || hashMap[beatmapHash].awarded_sp < awarded_sp) {
1618
+ hashMap[beatmapHash] = { awarded_sp, score };
1619
+ }
1620
+ }
1621
+
1622
+ const values = Object.values(hashMap);
1623
+ let vals = values
1624
+ .sort((a, b) => b.awarded_sp - a.awarded_sp)
1625
+ .slice(0, 10)
1626
+ .map((e) => e.score);
1627
+
1628
+ return NextResponse.json({
1629
+ lastDay: scores1?.map((s) => ({
1630
+ created_at: s.created_at,
1631
+ id: s.id,
1632
+ passed: s.passed,
1633
+ userId: s.userId,
1634
+ awarded_sp: s.awarded_sp,
1635
+ beatmapHash: s.beatmapHash,
1636
+ misses: s.misses,
1637
+ songId: s.songId,
1638
+ beatmapDifficulty: s.beatmaps?.difficulty,
1639
+ beatmapNotes: s.beatmaps?.noteCount,
1640
+ beatmapTitle: s.beatmaps?.title,
1641
+ speed: s.speed,
1642
+ })),
1643
+ top: vals?.map((s) => ({
1644
+ created_at: s.created_at,
1645
+ id: s.id,
1646
+ passed: s.passed,
1647
+ userId: s.userId,
1648
+ awarded_sp: s.awarded_sp,
1649
+ beatmapHash: s.beatmapHash,
1650
+ misses: s.misses,
1651
+ rank: s.rank,
1652
+ songId: s.songId,
1653
+ beatmapDifficulty: s.beatmaps?.difficulty,
1654
+ beatmapNotes: s.beatmaps?.noteCount,
1655
+ beatmapTitle: s.beatmaps?.title,
1656
+ speed: s.speed,
1657
+ })),
1658
+ });
1659
+ }
89
1660
  import { Schema as GetUserScores } from "./api/getUserScores"
90
1661
  export { Schema as SchemaGetUserScores } from "./api/getUserScores"
91
1662
  export const getUserScores = handleApi({url:"/api/getUserScores",...GetUserScores})
92
1663
 
93
1664
  // ./api/nominateMap.ts API
1665
+
1666
+ /*
1667
+ export const Schema = {
1668
+ input: z.strictObject({
1669
+ session: z.string(),
1670
+ mapId: z.number(),
1671
+ }),
1672
+ output: z.object({
1673
+ error: z.string().optional(),
1674
+ }),
1675
+ };
1676
+
1677
+ export async function POST(request: Request) {
1678
+ return protectedApi({
1679
+ request,
1680
+ schema: Schema,
1681
+ authorization: validUser,
1682
+ activity: handler,
1683
+ });
1684
+ }
1685
+
1686
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
1687
+ const user = (await supabase.auth.getUser(data.session)).data.user!;
1688
+ let { data: queryUserData, error: userError } = await supabase
1689
+ .from("profiles")
1690
+ .select("*")
1691
+ .eq("uid", user.id)
1692
+ .single();
1693
+
1694
+ if (!queryUserData) {
1695
+ return NextResponse.json({ error: "Can't find user" });
1696
+ }
1697
+
1698
+ const tags = (queryUserData?.badges || []) as string[];
1699
+
1700
+ if (!tags.includes("RCT")) {
1701
+ return NextResponse.json({ error: "Only RCTs can nominate maps!" });
1702
+ }
1703
+
1704
+ const { data: mapData, error } = await supabase
1705
+ .from("beatmapPages")
1706
+ .select("id,nominations,owner")
1707
+ .eq("id", data.mapId)
1708
+ .single();
1709
+
1710
+ if (!mapData) {
1711
+ return NextResponse.json({ error: "Bad map" });
1712
+ }
1713
+
1714
+ if (mapData.owner == queryUserData.id) {
1715
+ return NextResponse.json({ error: "Can't nominate own map" });
1716
+ }
1717
+
1718
+ if ((mapData.nominations as number[]).includes(queryUserData.id)) {
1719
+ return NextResponse.json({ error: "Already nominated" });
1720
+ }
1721
+
1722
+ await supabase.from("beatmapPages").upsert({
1723
+ id: data.mapId,
1724
+ nominations: [...(mapData.nominations! as number[]), queryUserData.id],
1725
+ });
1726
+
1727
+ return NextResponse.json({});
1728
+ }
94
1729
  import { Schema as NominateMap } from "./api/nominateMap"
95
1730
  export { Schema as SchemaNominateMap } from "./api/nominateMap"
96
1731
  export const nominateMap = handleApi({url:"/api/nominateMap",...NominateMap})
97
1732
 
98
1733
  // ./api/postBeatmapComment.ts API
1734
+
1735
+ /*
1736
+ export const Schema = {
1737
+ input: z.strictObject({
1738
+ session: z.string(),
1739
+ page: z.number(),
1740
+ content: z.string(),
1741
+ }),
1742
+ output: z.strictObject({
1743
+ error: z.string().optional(),
1744
+ }),
1745
+ };
1746
+
1747
+ export async function POST(request: Request): Promise<NextResponse> {
1748
+ return protectedApi({
1749
+ request,
1750
+ schema: Schema,
1751
+ authorization: validUser,
1752
+ activity: handler,
1753
+ });
1754
+ }
1755
+
1756
+ export async function handler({
1757
+ session,
1758
+ page,
1759
+ content,
1760
+ }: (typeof Schema)["input"]["_type"]): Promise<
1761
+ NextResponse<(typeof Schema)["output"]["_type"]>
1762
+ > {
1763
+ const user = (await supabase.auth.getUser(session)).data.user!;
1764
+ let { data: userData, error: userError } = await supabase
1765
+ .from("profiles")
1766
+ .select("*")
1767
+ .eq("uid", user.id)
1768
+ .single();
1769
+
1770
+ if (!userData) return NextResponse.json({ error: "No user." });
1771
+
1772
+ const upserted = await supabase
1773
+ .from("beatmapPageComments")
1774
+ .upsert({
1775
+ beatmapPage: page,
1776
+ owner: userData.id,
1777
+ content,
1778
+ })
1779
+ .select("*")
1780
+ .single();
1781
+
1782
+ if (upserted.error?.message.length) {
1783
+ return NextResponse.json({ error: upserted.error.message });
1784
+ }
1785
+ return NextResponse.json({});
1786
+ }
99
1787
  import { Schema as PostBeatmapComment } from "./api/postBeatmapComment"
100
1788
  export { Schema as SchemaPostBeatmapComment } from "./api/postBeatmapComment"
101
1789
  export const postBeatmapComment = handleApi({url:"/api/postBeatmapComment",...PostBeatmapComment})
102
1790
 
103
1791
  // ./api/rankMapsArchive.ts API
1792
+
1793
+ /*
1794
+ export const Schema = {
1795
+ input: z.strictObject({
1796
+ session: z.string(),
1797
+ mapId: z.number(),
1798
+ }),
1799
+ output: z.object({
1800
+ error: z.string().optional(),
1801
+ }),
1802
+ };
1803
+
1804
+ export async function POST(request: Request) {
1805
+ return protectedApi({
1806
+ request,
1807
+ schema: Schema,
1808
+ authorization: validUser,
1809
+ activity: handler,
1810
+ });
1811
+ }
1812
+
1813
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
1814
+ const user = (await supabase.auth.getUser(data.session)).data.user!;
1815
+ let { data: queryUserData, error: userError } = await supabase
1816
+ .from("profiles")
1817
+ .select("*")
1818
+ .eq("uid", user.id)
1819
+ .single();
1820
+
1821
+ if (!queryUserData) {
1822
+ return NextResponse.json({ error: "Can't find user" });
1823
+ }
1824
+
1825
+ const tags = (queryUserData?.badges || []) as string[];
1826
+
1827
+ if (!tags.includes("Bot")) {
1828
+ return NextResponse.json({ error: "Only Bots can force-rank maps!" });
1829
+ }
1830
+
1831
+ const { data: mapData, error } = await supabase
1832
+ .from("beatmapPages")
1833
+ .select("id,nominations,owner,status")
1834
+ .eq("owner", user.id)
1835
+ .eq("status", "UNRANKED");
1836
+
1837
+ if (!mapData) {
1838
+ return NextResponse.json({ error: "Bad map" });
1839
+ }
1840
+
1841
+ for (const element of mapData) {
1842
+ await supabase.from("beatmapPages").upsert({
1843
+ id: element.id,
1844
+ nominations: [queryUserData.id, queryUserData.id],
1845
+ status: "RANKED",
1846
+ });
1847
+ }
1848
+
1849
+ return NextResponse.json({});
1850
+ }
104
1851
  import { Schema as RankMapsArchive } from "./api/rankMapsArchive"
105
1852
  export { Schema as SchemaRankMapsArchive } from "./api/rankMapsArchive"
106
1853
  export const rankMapsArchive = handleApi({url:"/api/rankMapsArchive",...RankMapsArchive})
107
1854
 
108
1855
  // ./api/searchUsers.ts API
1856
+
1857
+ /*
1858
+ export const Schema = {
1859
+ input: z.strictObject({
1860
+ text: z.string(),
1861
+ }),
1862
+ output: z.object({
1863
+ error: z.string().optional(),
1864
+ results: z
1865
+ .array(
1866
+ z.object({
1867
+ id: z.number(),
1868
+ username: z.string().nullable(),
1869
+ })
1870
+ )
1871
+ .optional(),
1872
+ }),
1873
+ };
1874
+
1875
+ export async function POST(request: Request) {
1876
+ return protectedApi({
1877
+ request,
1878
+ schema: Schema,
1879
+ authorization: () => {},
1880
+ activity: handler,
1881
+ });
1882
+ }
1883
+
1884
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
1885
+ const { data: searchData, error } = await supabase
1886
+ .from("profiles")
1887
+ .select("id,username")
1888
+ .neq("ban", "excluded")
1889
+ .ilike("username", `%${data.text}%`)
1890
+ .limit(10);
1891
+ return NextResponse.json({
1892
+ results: searchData || [],
1893
+ });
1894
+ }
109
1895
  import { Schema as SearchUsers } from "./api/searchUsers"
110
1896
  export { Schema as SchemaSearchUsers } from "./api/searchUsers"
111
1897
  export const searchUsers = handleApi({url:"/api/searchUsers",...SearchUsers})
112
1898
 
113
1899
  // ./api/submitScore.ts API
1900
+
1901
+ /*
1902
+ export const Schema = {
1903
+ input: z.strictObject({
1904
+ session: z.string(),
1905
+ data: z.strictObject({
1906
+ token: z.string(),
1907
+ relayHwid: z.string(),
1908
+ songId: z.string(),
1909
+ misses: z.number(),
1910
+ hits: z.number(),
1911
+ mapHash: z.string(),
1912
+ mapNoteCount: z.number(),
1913
+ speed: z.number(),
1914
+ }),
1915
+ }),
1916
+ output: z.object({
1917
+ error: z.string().optional(),
1918
+ }),
1919
+ };
1920
+
1921
+ function easeInExpoDeq(x: number) {
1922
+ return x === 0 ? 0 : Math.pow(2, 50 * x - 50);
1923
+ }
1924
+
1925
+ export function calculatePerformancePoints(
1926
+ starRating: number,
1927
+ accuracy: number
1928
+ ) {
1929
+ return Math.round(
1930
+ Math.pow((starRating * easeInExpoDeq(accuracy) * 100) / 2, 2) / 1000
1931
+ );
1932
+ }
1933
+
1934
+ export async function POST(request: Request): Promise<NextResponse> {
1935
+ return protectedApi({
1936
+ request,
1937
+ schema: Schema,
1938
+ authorization: validUser,
1939
+ activity: handler,
1940
+ });
1941
+ }
1942
+
1943
+ export async function handler({
1944
+ data,
1945
+ session,
1946
+ }: (typeof Schema)["input"]["_type"]): Promise<
1947
+ NextResponse<(typeof Schema)["output"]["_type"]>
1948
+ > {
1949
+ return NextResponse.json(
1950
+ {
1951
+ error: "Disabled",
1952
+ },
1953
+ { status: 500 }
1954
+ );
1955
+ const user = (await supabase.auth.getUser(session)).data.user!;
1956
+
1957
+ let { data: userData, error: userError } = await supabase
1958
+ .from("profiles")
1959
+ .select("*")
1960
+ .eq("uid", user.id)
1961
+ .single();
1962
+
1963
+ if (!userData)
1964
+ return NextResponse.json(
1965
+ {
1966
+ error: "User doesn't exist",
1967
+ },
1968
+ { status: 500 }
1969
+ );
1970
+
1971
+ console.log(userData);
1972
+ let { data: beatmaps, error } = await supabase
1973
+ .from("beatmaps")
1974
+ .select("*")
1975
+ .eq("beatmapHash", data.mapHash)
1976
+ .single();
1977
+
1978
+ let { data: beatmapPages, error: bpError } = await supabase
1979
+ .from("beatmapPages")
1980
+ .select("*")
1981
+ .eq("latestBeatmapHash", data.mapHash)
1982
+ .single();
1983
+
1984
+ if (!beatmapPages) {
1985
+ return NextResponse.json(
1986
+ {
1987
+ error: "Map not submitted",
1988
+ },
1989
+ { status: 500 }
1990
+ );
1991
+ }
1992
+
1993
+ if (!beatmaps) {
1994
+ return NextResponse.json(
1995
+ {
1996
+ error: "Map not submitted",
1997
+ },
1998
+ { status: 500 }
1999
+ );
2000
+ }
2001
+
2002
+ if (beatmaps.noteCount !== data.mapNoteCount) {
2003
+ return NextResponse.json(
2004
+ {
2005
+ error: "Wrong map",
2006
+ },
2007
+ { status: 500 }
2008
+ );
2009
+ }
2010
+
2011
+ await supabase.from("beatmaps").upsert({
2012
+ beatmapHash: data.mapHash,
2013
+ playcount: (beatmaps.playcount || 1) + 1,
2014
+ });
2015
+
2016
+ let passed = true;
2017
+
2018
+ // Pass invalidation
2019
+ if (data.misses + data.hits !== beatmaps.noteCount) {
2020
+ passed = false;
2021
+ }
2022
+
2023
+ const accurracy = data.hits / beatmaps.noteCount;
2024
+ let awarded_sp = 0;
2025
+
2026
+ console.log(
2027
+ data.misses + data.hits == beatmaps.noteCount,
2028
+ data.misses + data.hits,
2029
+ beatmaps.noteCount
2030
+ );
2031
+
2032
+ if (beatmaps.starRating) {
2033
+ awarded_sp = calculatePerformancePoints(
2034
+ data.speed * beatmaps.starRating,
2035
+ accurracy
2036
+ );
2037
+ }
2038
+
2039
+ if (beatmapPages.status == "UNRANKED") {
2040
+ awarded_sp = 0;
2041
+ }
2042
+
2043
+ console.log("p1");
2044
+ await supabase.from("scores").upsert({
2045
+ beatmapHash: data.mapHash,
2046
+ replayHwid: data.relayHwid,
2047
+ songId: data.songId,
2048
+ userId: userData.id,
2049
+ passed,
2050
+ misses: data.misses,
2051
+ awarded_sp: Math.round(awarded_sp * 100) / 100,
2052
+ speed: data.speed,
2053
+ });
2054
+ console.log("p2");
2055
+
2056
+ let totalSp = 0;
2057
+ let { data: scores2, error: errorsp } = await supabase
2058
+ .from("scores")
2059
+ .select(`awarded_sp,beatmapHash`)
2060
+ .eq("userId", userData.id)
2061
+ .neq("awarded_sp", 0)
2062
+ .eq("passed", true)
2063
+ .order("awarded_sp", { ascending: false });
2064
+
2065
+ if (scores2 == null) return NextResponse.json({ error: "No scores" });
2066
+
2067
+ let hashMap: Record<string, number> = {};
2068
+
2069
+ for (const score of scores2) {
2070
+ const { beatmapHash, awarded_sp } = score;
2071
+
2072
+ if (!beatmapHash || !awarded_sp) continue;
2073
+
2074
+ if (!hashMap[beatmapHash] || hashMap[beatmapHash] < awarded_sp) {
2075
+ hashMap[beatmapHash] = awarded_sp;
2076
+ }
2077
+ }
2078
+ let weight = 100;
2079
+ const values = Object.values(hashMap);
2080
+ values.sort((a, b) => b - a);
2081
+
2082
+ for (const score of values) {
2083
+ totalSp += ((score || 0) * weight) / 100;
2084
+ weight -= 1;
2085
+
2086
+ if (weight == 0) {
2087
+ break;
2088
+ }
2089
+ }
2090
+
2091
+ await supabase.from("profiles").upsert({
2092
+ id: userData.id,
2093
+ play_count: (userData.play_count || 0) + 1,
2094
+ skill_points: Math.round(totalSp * 100) / 100,
2095
+ squares_hit: (userData.squares_hit || 0) + data.hits,
2096
+ });
2097
+ console.log("p3");
2098
+
2099
+ return NextResponse.json({});
2100
+ }
114
2101
  import { Schema as SubmitScore } from "./api/submitScore"
115
2102
  export { Schema as SchemaSubmitScore } from "./api/submitScore"
116
2103
  export const submitScore = handleApi({url:"/api/submitScore",...SubmitScore})
117
2104
 
118
2105
  // ./api/updateBeatmapPage.ts API
2106
+
2107
+ /*
2108
+ export const Schema = {
2109
+ input: z.strictObject({
2110
+ session: z.string(),
2111
+ id: z.number(),
2112
+ beatmapHash: z.string(),
2113
+ tags: z.string(),
2114
+ description: z.string(),
2115
+ }),
2116
+ output: z.strictObject({
2117
+ error: z.string().optional(),
2118
+ }),
2119
+ };
2120
+
2121
+ export async function POST(request: Request): Promise<NextResponse> {
2122
+ return protectedApi({
2123
+ request,
2124
+ schema: Schema,
2125
+ authorization: validUser,
2126
+ activity: handler,
2127
+ });
2128
+ }
2129
+
2130
+ export async function handler({
2131
+ session,
2132
+ beatmapHash,
2133
+ id,
2134
+ description,
2135
+ tags,
2136
+ }: (typeof Schema)["input"]["_type"]): Promise<
2137
+ NextResponse<(typeof Schema)["output"]["_type"]>
2138
+ > {
2139
+ const user = (await supabase.auth.getUser(session)).data.user!;
2140
+ let { data: userData, error: userError } = await supabase
2141
+ .from("profiles")
2142
+ .select("*")
2143
+ .eq("uid", user.id)
2144
+ .single();
2145
+
2146
+ let { data: pageData, error: pageError } = await supabase
2147
+ .from("beatmapPages")
2148
+ .select("*")
2149
+ .eq("id", id)
2150
+ .single();
2151
+
2152
+ let { data: beatmapData, error: bmPageError } = await supabase
2153
+ .from("beatmaps")
2154
+ .select("*")
2155
+ .eq("beatmapHash", beatmapHash)
2156
+ .single();
2157
+
2158
+ if (!userData) return NextResponse.json({ error: "No user." });
2159
+ if (!beatmapData) return NextResponse.json({ error: "No beatmap." });
2160
+
2161
+ if (userData.id !== pageData?.owner)
2162
+ return NextResponse.json({ error: "Non-authz user." });
2163
+
2164
+ if (pageData?.status !== "UNRANKED")
2165
+ return NextResponse.json({ error: "Only unranked maps can be updated" });
2166
+
2167
+ const upserted = await supabase
2168
+ .from("beatmapPages")
2169
+ .upsert({
2170
+ id,
2171
+ latestBeatmapHash: beatmapHash,
2172
+ genre: "",
2173
+ title: beatmapData.title,
2174
+ status: "UNRANKED",
2175
+ owner: userData.id,
2176
+ description,
2177
+ tags,
2178
+ nominations: [],
2179
+ })
2180
+ .select("*")
2181
+ .single();
2182
+
2183
+ if (upserted.error?.message.length) {
2184
+ return NextResponse.json({ error: upserted.error.message });
2185
+ }
2186
+ return NextResponse.json({});
2187
+ }
119
2188
  import { Schema as UpdateBeatmapPage } from "./api/updateBeatmapPage"
120
2189
  export { Schema as SchemaUpdateBeatmapPage } from "./api/updateBeatmapPage"
121
2190
  export const updateBeatmapPage = handleApi({url:"/api/updateBeatmapPage",...UpdateBeatmapPage})