rhythia-api 229.0.0 → 231.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/checkQualified.ts +83 -0
- package/api/getBeatmapPage.ts +65 -28
- package/api/getBeatmapPageById.ts +61 -24
- package/api/getUserScores.ts +18 -15
- package/api/{nominateMap.ts → qualifyMap.ts} +86 -82
- package/api/submitScoreInternal.ts +449 -426
- package/api/{approveMap.ts → vetoMap.ts} +94 -78
- package/handleApi.ts +3 -7
- package/index.ts +134 -85
- package/package.json +10 -2
- package/queries/admin_delete_user.sql +39 -0
- package/queries/admin_exclude_user.sql +21 -0
- package/queries/admin_invalidate_ranked_scores.sql +18 -0
- package/queries/admin_log_action.sql +10 -0
- package/queries/admin_profanity_clear.sql +29 -0
- package/queries/admin_remove_all_scores.sql +29 -0
- package/queries/admin_restrict_user.sql +21 -0
- package/queries/admin_search_users.sql +24 -0
- package/queries/admin_silence_user.sql +21 -0
- package/queries/admin_unban_user.sql +21 -0
- package/queries/get_badge_leaderboard.sql +50 -0
- package/queries/get_clan_leaderboard.sql +68 -0
- package/queries/get_collections_v4.sql +109 -0
- package/queries/get_top_scores_for_beatmap.sql +44 -0
- package/queries/get_user_by_email.sql +32 -0
- package/queries/get_user_scores_lastday.sql +47 -0
- package/queries/get_user_scores_reign.sql +31 -0
- package/queries/get_user_scores_top_and_stats.sql +84 -0
- package/queries/grant_special_badges.sql +69 -0
- package/types/database.ts +1224 -1179
- package/utils/getUserBySession.ts +1 -1
- package/utils/requestUtils.ts +127 -88
|
@@ -1,426 +1,449 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
-
import { supabase } from "../utils/supabase";
|
|
5
|
-
import { decryptString } from "../utils/security";
|
|
6
|
-
import { isEqual } from "lodash";
|
|
7
|
-
import { getUserBySession } from "../utils/getUserBySession";
|
|
8
|
-
import { User } from "@supabase/supabase-js";
|
|
9
|
-
import { invalidateCachePrefix } from "../utils/cache";
|
|
10
|
-
import { createClient, RedisClientType } from "redis";
|
|
11
|
-
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
12
|
-
|
|
13
|
-
const s3Client = new S3Client({
|
|
14
|
-
region: "auto",
|
|
15
|
-
endpoint: "https://s3.eu-central-003.backblazeb2.com",
|
|
16
|
-
credentials: {
|
|
17
|
-
secretAccessKey: process.env.SECRET_BUCKET || "",
|
|
18
|
-
accessKeyId: process.env.ACCESS_BUCKET || "",
|
|
19
|
-
},
|
|
20
|
-
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
s3Client.middlewareStack.add(
|
|
24
|
-
(next) =>
|
|
25
|
-
async (args): Promise<any> => {
|
|
26
|
-
const request = args.request as RequestInit;
|
|
27
|
-
const headers = (request.headers || {}) as Record<string, string>;
|
|
28
|
-
|
|
29
|
-
delete headers["x-amz-checksum-crc32"];
|
|
30
|
-
delete headers["x-amz-checksum-crc32c"];
|
|
31
|
-
delete headers["x-amz-checksum-sha1"];
|
|
32
|
-
delete headers["x-amz-checksum-sha256"];
|
|
33
|
-
|
|
34
|
-
request.headers = headers;
|
|
35
|
-
return next(args);
|
|
36
|
-
},
|
|
37
|
-
{ step: "build", name: "stripBackblazeChecksumHeaders" }
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
export const Schema = {
|
|
41
|
-
input: z.strictObject({
|
|
42
|
-
session: z.string(),
|
|
43
|
-
secret: z.string(),
|
|
44
|
-
token: z.string(),
|
|
45
|
-
data: z.strictObject({
|
|
46
|
-
onlineMapId: z.number(),
|
|
47
|
-
misses: z.number(),
|
|
48
|
-
hits: z.number(),
|
|
49
|
-
speed: z.number(),
|
|
50
|
-
mods: z.array(z.string()),
|
|
51
|
-
spin: z.boolean(),
|
|
52
|
-
replayBytes: z.string().nullable().optional(),
|
|
53
|
-
pauses: z.number().nullable().optional(),
|
|
54
|
-
failTime: z.number().nullable().optional(),
|
|
55
|
-
}),
|
|
56
|
-
}),
|
|
57
|
-
output: z.object({
|
|
58
|
-
error: z.string().optional(),
|
|
59
|
-
}),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function easeInExpoDeqHard(x: number, star: number) {
|
|
63
|
-
let exponent = 100 - 12 * star;
|
|
64
|
-
if (exponent < 5) exponent = 5;
|
|
65
|
-
return x === 0 ? 0 : Math.pow(2, exponent * x - exponent);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function calculatePerformancePoints(
|
|
69
|
-
starRating: number,
|
|
70
|
-
accuracy: number
|
|
71
|
-
) {
|
|
72
|
-
return (
|
|
73
|
-
Math.round(
|
|
74
|
-
Math.pow(
|
|
75
|
-
(starRating * easeInExpoDeqHard(accuracy, starRating) * 100) / 2,
|
|
76
|
-
2
|
|
77
|
-
) / 1000
|
|
78
|
-
) * 2
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function POST(request: Request): Promise<NextResponse> {
|
|
83
|
-
console.log("Received
|
|
84
|
-
return protectedApi({
|
|
85
|
-
request,
|
|
86
|
-
schema: Schema,
|
|
87
|
-
authorization: validUser,
|
|
88
|
-
activity: handler,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function handler({
|
|
93
|
-
data,
|
|
94
|
-
token,
|
|
95
|
-
session,
|
|
96
|
-
secret,
|
|
97
|
-
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
98
|
-
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
99
|
-
> {
|
|
100
|
-
if (secret !== "testing-1") {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
let
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
import { decryptString } from "../utils/security";
|
|
6
|
+
import { isEqual } from "lodash";
|
|
7
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
8
|
+
import { User } from "@supabase/supabase-js";
|
|
9
|
+
import { invalidateCachePrefix } from "../utils/cache";
|
|
10
|
+
import { createClient, RedisClientType } from "redis";
|
|
11
|
+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
12
|
+
|
|
13
|
+
const s3Client = new S3Client({
|
|
14
|
+
region: "auto",
|
|
15
|
+
endpoint: "https://s3.eu-central-003.backblazeb2.com",
|
|
16
|
+
credentials: {
|
|
17
|
+
secretAccessKey: process.env.SECRET_BUCKET || "",
|
|
18
|
+
accessKeyId: process.env.ACCESS_BUCKET || "",
|
|
19
|
+
},
|
|
20
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
s3Client.middlewareStack.add(
|
|
24
|
+
(next) =>
|
|
25
|
+
async (args): Promise<any> => {
|
|
26
|
+
const request = args.request as RequestInit;
|
|
27
|
+
const headers = (request.headers || {}) as Record<string, string>;
|
|
28
|
+
|
|
29
|
+
delete headers["x-amz-checksum-crc32"];
|
|
30
|
+
delete headers["x-amz-checksum-crc32c"];
|
|
31
|
+
delete headers["x-amz-checksum-sha1"];
|
|
32
|
+
delete headers["x-amz-checksum-sha256"];
|
|
33
|
+
|
|
34
|
+
request.headers = headers;
|
|
35
|
+
return next(args);
|
|
36
|
+
},
|
|
37
|
+
{ step: "build", name: "stripBackblazeChecksumHeaders" }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export const Schema = {
|
|
41
|
+
input: z.strictObject({
|
|
42
|
+
session: z.string(),
|
|
43
|
+
secret: z.string(),
|
|
44
|
+
token: z.string(),
|
|
45
|
+
data: z.strictObject({
|
|
46
|
+
onlineMapId: z.number(),
|
|
47
|
+
misses: z.number(),
|
|
48
|
+
hits: z.number(),
|
|
49
|
+
speed: z.number(),
|
|
50
|
+
mods: z.array(z.string()),
|
|
51
|
+
spin: z.boolean(),
|
|
52
|
+
replayBytes: z.string().nullable().optional(),
|
|
53
|
+
pauses: z.number().nullable().optional(),
|
|
54
|
+
failTime: z.number().nullable().optional(),
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
output: z.object({
|
|
58
|
+
error: z.string().optional(),
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function easeInExpoDeqHard(x: number, star: number) {
|
|
63
|
+
let exponent = 100 - 12 * star;
|
|
64
|
+
if (exponent < 5) exponent = 5;
|
|
65
|
+
return x === 0 ? 0 : Math.pow(2, exponent * x - exponent);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function calculatePerformancePoints(
|
|
69
|
+
starRating: number,
|
|
70
|
+
accuracy: number
|
|
71
|
+
) {
|
|
72
|
+
return (
|
|
73
|
+
Math.round(
|
|
74
|
+
Math.pow(
|
|
75
|
+
(starRating * easeInExpoDeqHard(accuracy, starRating) * 100) / 2,
|
|
76
|
+
2
|
|
77
|
+
) / 1000
|
|
78
|
+
) * 2
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
83
|
+
console.log("Received submitScoreInternal request");
|
|
84
|
+
return protectedApi({
|
|
85
|
+
request,
|
|
86
|
+
schema: Schema,
|
|
87
|
+
authorization: validUser,
|
|
88
|
+
activity: handler,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function handler({
|
|
93
|
+
data,
|
|
94
|
+
token,
|
|
95
|
+
session,
|
|
96
|
+
secret,
|
|
97
|
+
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
98
|
+
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
99
|
+
> {
|
|
100
|
+
if (secret !== "testing-1") {
|
|
101
|
+
console.log("Internal usage only");
|
|
102
|
+
return NextResponse.json({
|
|
103
|
+
error: "Internal usage only",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const user = (await getUserBySession(session)) as User;
|
|
107
|
+
|
|
108
|
+
if (!user) {
|
|
109
|
+
console.log("token-expired");
|
|
110
|
+
return NextResponse.json(
|
|
111
|
+
{
|
|
112
|
+
error: "token-expired",
|
|
113
|
+
},
|
|
114
|
+
{ status: 401 }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let { data: leversData } = await supabase
|
|
119
|
+
.from("levers")
|
|
120
|
+
.select("*")
|
|
121
|
+
.eq("id", 1)
|
|
122
|
+
.single();
|
|
123
|
+
|
|
124
|
+
if (leversData && leversData.disable_scores) {
|
|
125
|
+
console.log("Lever pulled - blocked score");
|
|
126
|
+
return NextResponse.json({
|
|
127
|
+
error: "Scores are temporarily disabled",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (data.failTime) {
|
|
132
|
+
console.log("Fail Score don't save");
|
|
133
|
+
return NextResponse.json({
|
|
134
|
+
error: "Fail time scores aren't saved for now",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let { data: userData, error: userError } = await supabase
|
|
139
|
+
.from("profiles")
|
|
140
|
+
.select("*")
|
|
141
|
+
.eq("uid", user.id)
|
|
142
|
+
.single();
|
|
143
|
+
|
|
144
|
+
if (!userData) {
|
|
145
|
+
console.log("User doesn't exist");
|
|
146
|
+
return NextResponse.json(
|
|
147
|
+
{
|
|
148
|
+
error: "User doesn't exist",
|
|
149
|
+
},
|
|
150
|
+
{ status: 400 }
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(userData);
|
|
155
|
+
|
|
156
|
+
if (userData.ban == "excluded" || userData.ban == "restricted") {
|
|
157
|
+
console.log(
|
|
158
|
+
"Silenced, restricted or excluded players can't submit scores."
|
|
159
|
+
);
|
|
160
|
+
return NextResponse.json(
|
|
161
|
+
{
|
|
162
|
+
error: "Silenced, restricted or excluded players can't submit scores.",
|
|
163
|
+
},
|
|
164
|
+
{ status: 400 }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let { data: beatmapPages, error: bpError } = await supabase
|
|
169
|
+
.from("beatmapPages")
|
|
170
|
+
.select("*")
|
|
171
|
+
.eq("id", data.onlineMapId)
|
|
172
|
+
.single();
|
|
173
|
+
|
|
174
|
+
let { data: beatmaps, error: bpErr } = await supabase
|
|
175
|
+
.from("beatmaps")
|
|
176
|
+
.select("*")
|
|
177
|
+
.eq("beatmapHash", beatmapPages!.latestBeatmapHash!)
|
|
178
|
+
.single();
|
|
179
|
+
|
|
180
|
+
if (!beatmaps) {
|
|
181
|
+
console.log("Map not submitted");
|
|
182
|
+
return NextResponse.json(
|
|
183
|
+
{
|
|
184
|
+
error: "Map not submitted",
|
|
185
|
+
},
|
|
186
|
+
{ status: 400 }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!beatmapPages) {
|
|
191
|
+
console.log("Map page not submitted");
|
|
192
|
+
return NextResponse.json(
|
|
193
|
+
{
|
|
194
|
+
error: "Map not submitted",
|
|
195
|
+
},
|
|
196
|
+
{ status: 400 }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await supabase.from("beatmaps").upsert({
|
|
201
|
+
beatmapHash: beatmapPages.latestBeatmapHash!,
|
|
202
|
+
playcount: (beatmaps!.playcount || 1) + 1,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
let passed = true;
|
|
206
|
+
|
|
207
|
+
// Pass invalidation
|
|
208
|
+
if (data.misses + data.hits !== beatmaps.noteCount) {
|
|
209
|
+
passed = false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const accurracy = data.hits / beatmaps.noteCount!;
|
|
213
|
+
let awarded_sp = 0;
|
|
214
|
+
|
|
215
|
+
console.log(
|
|
216
|
+
data.misses + data.hits == beatmaps.noteCount,
|
|
217
|
+
data.misses + data.hits,
|
|
218
|
+
beatmaps.noteCount
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
let multiplierMod = 1;
|
|
222
|
+
if (data.mods.includes("mod_hardrock")) {
|
|
223
|
+
multiplierMod *= 1; //todo: change here
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (data.mods.includes("mod_nofail")) {
|
|
227
|
+
multiplierMod *= Math.pow(0.95, data.misses);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
awarded_sp = calculatePerformancePoints(
|
|
231
|
+
data.speed * beatmaps.starRating! * multiplierMod,
|
|
232
|
+
accurracy
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (beatmapPages.status == "UNRANKED") {
|
|
236
|
+
awarded_sp = 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log("p1");
|
|
240
|
+
|
|
241
|
+
let client: RedisClientType | undefined;
|
|
242
|
+
let tokenId = -1;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
if (Buffer.from(token, "base64").length != 4096) {
|
|
246
|
+
console.log("token length check failed");
|
|
247
|
+
return NextResponse.json(
|
|
248
|
+
{
|
|
249
|
+
error: "",
|
|
250
|
+
},
|
|
251
|
+
{ status: 400 }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
client = createClient({
|
|
256
|
+
username: process.env.REDIS_USERNAME,
|
|
257
|
+
password: process.env.REDIS_PASSWORD,
|
|
258
|
+
socket: {
|
|
259
|
+
host: process.env.REDIS_HOST,
|
|
260
|
+
port: process.env.REDIS_PORT as unknown as number,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
client.on("error", (err) => console.log("Redis Client Error", err));
|
|
265
|
+
await client.connect();
|
|
266
|
+
|
|
267
|
+
const { data, error } = await supabase
|
|
268
|
+
.from("tokens")
|
|
269
|
+
.insert({})
|
|
270
|
+
.select()
|
|
271
|
+
.single();
|
|
272
|
+
|
|
273
|
+
if (error !== null) {
|
|
274
|
+
console.log(error);
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
tokenId = data.id;
|
|
279
|
+
|
|
280
|
+
await client.xAdd("score:tokens", "*", {
|
|
281
|
+
token: JSON.stringify({
|
|
282
|
+
userId: userData.id,
|
|
283
|
+
tokenId: tokenId,
|
|
284
|
+
token: token,
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
} catch (exception) {
|
|
288
|
+
console.log(exception);
|
|
289
|
+
console.log("failed to send redis request", {
|
|
290
|
+
token: "<Long>",
|
|
291
|
+
userId: userData.id,
|
|
292
|
+
});
|
|
293
|
+
} finally {
|
|
294
|
+
client?.destroy();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// auto-exclude: if a newly-created account (>600 RP) submits a score
|
|
298
|
+
try {
|
|
299
|
+
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
300
|
+
if (
|
|
301
|
+
awarded_sp > 600 &&
|
|
302
|
+
userData?.created_at &&
|
|
303
|
+
Date.now() - userData.created_at < ONE_WEEK
|
|
304
|
+
) {
|
|
305
|
+
await supabase
|
|
306
|
+
.from("profiles")
|
|
307
|
+
.upsert({ id: userData.id, ban: "excluded", bannedAt: Date.now() });
|
|
308
|
+
|
|
309
|
+
return NextResponse.json(
|
|
310
|
+
{ error: "User excluded due to suspicious activity." },
|
|
311
|
+
{ status: 400 }
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error("safen/ auto-exclude check failed:", e);
|
|
316
|
+
}
|
|
317
|
+
let replayUrl: string | null = null;
|
|
318
|
+
|
|
319
|
+
if (data.replayBytes) {
|
|
320
|
+
try {
|
|
321
|
+
const replayBuffer = Buffer.from(data.replayBytes, "base64");
|
|
322
|
+
if (replayBuffer.length > 0) {
|
|
323
|
+
const replayKey = `score-replay-${Date.now()}-${user.id}.rhr`;
|
|
324
|
+
await s3Client.send(
|
|
325
|
+
new PutObjectCommand({
|
|
326
|
+
Bucket: "rhthia-avatars",
|
|
327
|
+
Key: replayKey,
|
|
328
|
+
Body: replayBuffer,
|
|
329
|
+
ContentType: "application/octet-stream",
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
replayUrl = `https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/${replayKey}`;
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error("Replay upload failed:", error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await supabase.from("scores").upsert({
|
|
340
|
+
beatmapHash: beatmaps.beatmapHash,
|
|
341
|
+
replayHwid: "",
|
|
342
|
+
replay_url: replayUrl,
|
|
343
|
+
songId: beatmaps.beatmapHash,
|
|
344
|
+
userId: userData.id,
|
|
345
|
+
passed,
|
|
346
|
+
misses: data.misses,
|
|
347
|
+
awarded_sp: Math.round(awarded_sp * 100) / 100,
|
|
348
|
+
speed: data.speed,
|
|
349
|
+
mods: data.mods,
|
|
350
|
+
additional_data: "",
|
|
351
|
+
spin: data.spin || false,
|
|
352
|
+
token: tokenId === -1 ? undefined : tokenId,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
console.log("p2");
|
|
356
|
+
|
|
357
|
+
let { data: scores2, error: errorsp } = await supabase
|
|
358
|
+
.from("scores")
|
|
359
|
+
.select(`awarded_sp,beatmapHash,spin`)
|
|
360
|
+
.eq("userId", userData.id)
|
|
361
|
+
.neq("awarded_sp", 0)
|
|
362
|
+
.eq("passed", true)
|
|
363
|
+
.order("awarded_sp", { ascending: false });
|
|
364
|
+
|
|
365
|
+
if (scores2 == null) return NextResponse.json({ error: "No scores" });
|
|
366
|
+
|
|
367
|
+
let allHashMap: Record<string, number> = {};
|
|
368
|
+
let spinHashMap: Record<string, number> = {};
|
|
369
|
+
|
|
370
|
+
for (const score of scores2) {
|
|
371
|
+
const { beatmapHash, awarded_sp } = score;
|
|
372
|
+
|
|
373
|
+
if (!beatmapHash || !awarded_sp) continue;
|
|
374
|
+
|
|
375
|
+
// Normal Scores
|
|
376
|
+
if (!allHashMap[beatmapHash] || allHashMap[beatmapHash] < awarded_sp) {
|
|
377
|
+
allHashMap[beatmapHash] = awarded_sp;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Spin Scores
|
|
381
|
+
if (score.spin) {
|
|
382
|
+
if (!spinHashMap[beatmapHash] || spinHashMap[beatmapHash] < awarded_sp) {
|
|
383
|
+
spinHashMap[beatmapHash] = awarded_sp;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// All scores
|
|
388
|
+
const totalSp = weightCalculate(allHashMap);
|
|
389
|
+
|
|
390
|
+
// Only spin scores
|
|
391
|
+
const spinTotalSp = weightCalculate(spinHashMap);
|
|
392
|
+
|
|
393
|
+
await supabase.from("profiles").upsert({
|
|
394
|
+
id: userData.id,
|
|
395
|
+
play_count: (userData.play_count || 0) + 1,
|
|
396
|
+
skill_points: Math.round(totalSp * 100) / 100,
|
|
397
|
+
spin_skill_points: Math.round(spinTotalSp * 100) / 100,
|
|
398
|
+
squares_hit: (userData.squares_hit || 0) + data.hits,
|
|
399
|
+
});
|
|
400
|
+
console.log("p3");
|
|
401
|
+
|
|
402
|
+
await invalidateCachePrefix(`userscore:${userData.id}`);
|
|
403
|
+
const beatmapIsRanked =
|
|
404
|
+
beatmapPages?.status === "RANKED" || beatmapPages?.status === "APPROVED";
|
|
405
|
+
if (beatmapIsRanked) {
|
|
406
|
+
await invalidateCachePrefix(`beatmap-scores:${beatmaps.beatmapHash}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Grant special badges if applicable
|
|
410
|
+
if (passed && beatmapPages && !data.mods.includes("mod_nofail")) {
|
|
411
|
+
try {
|
|
412
|
+
const { data: badgeResult, error: badgeError } = await supabase.rpc(
|
|
413
|
+
"grant_special_badges",
|
|
414
|
+
{
|
|
415
|
+
p_user_id: userData.id,
|
|
416
|
+
p_beatmap_id: beatmapPages.id,
|
|
417
|
+
p_spin: data.spin || false,
|
|
418
|
+
p_passed: passed,
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const result = badgeResult as { granted: boolean; badge: string | null };
|
|
423
|
+
if (result && result.granted) {
|
|
424
|
+
console.log(`Badge granted: ${result.badge} to user ${userData.id}`);
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error("Error granting badge:", error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return NextResponse.json({});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function weightCalculate(hashMap: Record<string, number>) {
|
|
434
|
+
let totalSp = 0;
|
|
435
|
+
let weight = 100;
|
|
436
|
+
|
|
437
|
+
const values = Object.values(hashMap);
|
|
438
|
+
values.sort((a, b) => b - a);
|
|
439
|
+
|
|
440
|
+
for (const score of values) {
|
|
441
|
+
totalSp += ((score || 0) * weight) / 100;
|
|
442
|
+
weight = weight * 0.97;
|
|
443
|
+
|
|
444
|
+
if (weight < 5) {
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return totalSp;
|
|
449
|
+
}
|