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/api/getAvatarUploadUrl.ts +1 -0
- package/api/getMapUploadUrl.ts +7 -0
- package/api/getProfile.ts +2 -2
- package/index.ts +2069 -0
- package/package.json +1 -1
- package/utils/test +5 -0
- package/utils/validateToken.ts +5 -5
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})
|