rhythia-api 242.0.0 → 244.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/createBeatmap.ts +64 -40
- package/api/editProfile.ts +4 -67
- package/api/executeAdminOperation.ts +637 -27
- package/api/getAvatarUploadUrl.ts +90 -85
- package/api/getBeatmapPage.ts +2 -0
- package/api/getBeatmapPageById.ts +2 -0
- package/api/getBeatmaps.ts +110 -197
- package/api/getChangelog.ts +46 -0
- package/api/getCollection.ts +44 -31
- package/api/getMapUploadUrl.ts +90 -93
- package/api/getProfile.ts +297 -297
- package/api/getScore.ts +2 -0
- package/api/getVideoUploadUrl.ts +90 -85
- package/api/submitScoreInternal.ts +506 -461
- package/api/updateBeatmapPage.ts +6 -0
- package/beatmap-file-urls.json +29398 -0
- package/handleApi.ts +7 -4
- package/index.ts +193 -162
- package/package.json +7 -3
- package/queries/admin_delete_user.sql +42 -39
- package/queries/admin_remove_all_scores.sql +6 -3
- package/queries/admin_remove_score.sql +107 -0
- package/queries/admin_update_profile.sql +22 -0
- package/queries/get_beatmaps_v2.sql +48 -0
- package/queries/get_top_scores_for_beatmap3.sql +47 -38
- package/queries/profile_update_guards.sql +66 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/linked-project.json +1 -0
- package/types/database.ts +1702 -1450
- package/utils/beatmapFiles.ts +102 -0
- package/utils/beatmapHash.ts +239 -0
- package/utils/beatmapTopScores.ts +68 -84
- package/utils/getUserBySession.ts +3 -1
- package/utils/moderation.ts +101 -0
- package/utils/profileUpdateValidation.ts +51 -0
- package/utils/redis.ts +24 -0
- package/utils/requestUtils.ts +2 -2
- package/utils/rhrReplay.ts +122 -0
- package/utils/star-calc/formatSingle.ts +107 -0
- package/utils/star-calc/rhmParser.ts +214 -0
- package/utils/star-calc/sspmParser.ts +294 -160
- package/worker.ts +197 -195
- package/.env +0 -1
package/api/createBeatmap.ts
CHANGED
|
@@ -1,38 +1,17 @@
|
|
|
1
1
|
import { NextResponse } from "../utils/response";
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
-
import { SSPMParser } from "../utils/star-calc/sspmParser";
|
|
4
|
+
import { SSPMParser, type SSPMParsedMap } from "../utils/star-calc/sspmParser";
|
|
5
|
+
import {
|
|
6
|
+
isRHM,
|
|
7
|
+
parseRHM,
|
|
8
|
+
rhmToSSPMParsedMap,
|
|
9
|
+
} from "../utils/star-calc/rhmParser";
|
|
5
10
|
import { supabase } from "../utils/supabase";
|
|
6
|
-
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
7
11
|
import { rateMap } from "../utils/star-calc";
|
|
8
12
|
import { getUserBySession } from "../utils/getUserBySession";
|
|
9
13
|
import { User } from "@supabase/supabase-js";
|
|
10
|
-
|
|
11
|
-
const s3Client = new S3Client({
|
|
12
|
-
region: "auto",
|
|
13
|
-
endpoint: "https://s3.eu-central-003.backblazeb2.com",
|
|
14
|
-
credentials: {
|
|
15
|
-
secretAccessKey: process.env.SECRET_BUCKET || "",
|
|
16
|
-
accessKeyId: process.env.ACCESS_BUCKET || "",
|
|
17
|
-
},
|
|
18
|
-
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
s3Client.middlewareStack.add(
|
|
22
|
-
(next) =>
|
|
23
|
-
async (args): Promise<any> => {
|
|
24
|
-
const request = args.request as RequestInit;
|
|
25
|
-
const headers = request.headers as Record<string, string>;
|
|
26
|
-
delete headers["x-amz-checksum-crc32"];
|
|
27
|
-
delete headers["x-amz-checksum-crc32c"];
|
|
28
|
-
delete headers["x-amz-checksum-sha1"];
|
|
29
|
-
delete headers["x-amz-checksum-sha256"];
|
|
30
|
-
request.headers = headers;
|
|
31
|
-
|
|
32
|
-
return next(args);
|
|
33
|
-
},
|
|
34
|
-
{ step: "build", name: "customHeaders" }
|
|
35
|
-
);
|
|
14
|
+
import { putStaticObject, readStaticObject } from "../utils/beatmapFiles";
|
|
36
15
|
|
|
37
16
|
function getCoverContentType(cover: Uint8Array) {
|
|
38
17
|
if (
|
|
@@ -105,11 +84,59 @@ export async function handler({
|
|
|
105
84
|
if (!url.startsWith(`https://static.rhythia.com/`))
|
|
106
85
|
return NextResponse.json({ error: "Invalid url" });
|
|
107
86
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
87
|
+
let parsedData: SSPMParsedMap | undefined;
|
|
88
|
+
let parseError: unknown;
|
|
89
|
+
|
|
90
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
91
|
+
try {
|
|
92
|
+
const { object, objectKey, bytes } = await readStaticObject(url);
|
|
93
|
+
const isSSPM = bytes.subarray(0, 4).toString("hex") === "53532b6d";
|
|
94
|
+
const isBeatmapFile = isSSPM || isRHM(bytes);
|
|
95
|
+
console.log("createBeatmap object fetch result", {
|
|
96
|
+
attempt: attempt + 1,
|
|
97
|
+
url,
|
|
98
|
+
objectKey,
|
|
99
|
+
contentType: object.ContentType,
|
|
100
|
+
contentLength: object.ContentLength,
|
|
101
|
+
byteLength: bytes.length,
|
|
102
|
+
firstBytesHex: bytes.subarray(0, 16).toString("hex"),
|
|
103
|
+
firstBytesText: bytes
|
|
104
|
+
.subarray(0, 16)
|
|
105
|
+
.toString("utf8")
|
|
106
|
+
.replace(/[^\x20-\x7e]/g, "."),
|
|
107
|
+
bodyPreview:
|
|
108
|
+
!isBeatmapFile && bytes.length <= 512
|
|
109
|
+
? bytes.toString("utf8").replace(/[^\x09\x0a\x0d\x20-\x7e]/g, ".")
|
|
110
|
+
: undefined,
|
|
111
|
+
eTag: object.ETag,
|
|
112
|
+
versionId: object.VersionId,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
parsedData = isRHM(bytes)
|
|
116
|
+
? rhmToSSPMParsedMap(parseRHM(bytes))
|
|
117
|
+
: new SSPMParser(bytes).parse();
|
|
118
|
+
console.log("createBeatmap parsed beatmap", {
|
|
119
|
+
attempt: attempt + 1,
|
|
120
|
+
mapID: parsedData.strings.mapID,
|
|
121
|
+
noteCount: parsedData.metadata.noteCount,
|
|
122
|
+
markerCount: parsedData.metadata.markerCount,
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
parseError = error;
|
|
127
|
+
console.log("createBeatmap parse failed", {
|
|
128
|
+
attempt: attempt + 1,
|
|
129
|
+
error: error?.toString?.() || String(error),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (attempt < 3) {
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!parsedData) throw parseError;
|
|
111
139
|
|
|
112
|
-
const parsedData = parser.parse();
|
|
113
140
|
const digested = parsedData.strings.mapID;
|
|
114
141
|
|
|
115
142
|
const user = (await getUserBySession(session)) as User;
|
|
@@ -169,14 +196,11 @@ export async function handler({
|
|
|
169
196
|
const cover = parsedData.cover || Buffer.from([]);
|
|
170
197
|
const coverContentType = getCoverContentType(cover);
|
|
171
198
|
|
|
172
|
-
await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
ContentType: coverContentType,
|
|
178
|
-
})
|
|
179
|
-
);
|
|
199
|
+
await putStaticObject({
|
|
200
|
+
key: imgkey,
|
|
201
|
+
body: cover,
|
|
202
|
+
contentType: coverContentType,
|
|
203
|
+
});
|
|
180
204
|
|
|
181
205
|
const markers = parsedData.markers.sort((a, b) => a.position - b.position);
|
|
182
206
|
|
package/api/editProfile.ts
CHANGED
|
@@ -4,15 +4,12 @@ import { Database } from "../types/database";
|
|
|
4
4
|
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
5
5
|
import { supabase } from "../utils/supabase";
|
|
6
6
|
import { getUserBySession } from "../utils/getUserBySession";
|
|
7
|
-
import { COUNTRY_LIST } from "../utils/countryList";
|
|
8
7
|
import { User } from "@supabase/supabase-js";
|
|
9
|
-
import
|
|
10
|
-
import removeZeroWidth from "zero-width";
|
|
8
|
+
import { normalizeProfileUpdateData } from "../utils/profileUpdateValidation";
|
|
11
9
|
|
|
12
10
|
const USERNAME_CHANGE_COOLDOWN_ERROR =
|
|
13
11
|
"Username can only be changed once every 6 months";
|
|
14
12
|
const FLAG_CHANGE_ERROR = "Flag can only be changed once";
|
|
15
|
-
const countryCodes = new Set(COUNTRY_LIST);
|
|
16
13
|
|
|
17
14
|
function getNextUsernameChangeAt(
|
|
18
15
|
lastChangedAt: string | null | undefined
|
|
@@ -71,70 +68,10 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|
|
71
68
|
export async function handler(
|
|
72
69
|
data: (typeof Schema)["input"]["_type"]
|
|
73
70
|
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
74
|
-
|
|
75
|
-
data.data.username = removeZeroWidth(data.data.username);
|
|
71
|
+
const validationError = normalizeProfileUpdateData(data.data);
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
error: "Username can't start or end with spaces.",
|
|
81
|
-
},
|
|
82
|
-
{ status: 404 }
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (data.data.username.length < 2) {
|
|
87
|
-
return NextResponse.json(
|
|
88
|
-
{
|
|
89
|
-
error: "Username must be at least 2 characters long",
|
|
90
|
-
},
|
|
91
|
-
{ status: 404 }
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (data.data.username.length > 16) {
|
|
96
|
-
return NextResponse.json(
|
|
97
|
-
{
|
|
98
|
-
error: "Username must be 16 characters or fewer.",
|
|
99
|
-
},
|
|
100
|
-
{ status: 404 }
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!/^[A-Za-z0-9._]+$/.test(data.data.username)) {
|
|
105
|
-
return NextResponse.json(
|
|
106
|
-
{
|
|
107
|
-
error:
|
|
108
|
-
"Username can only contain English letters (a-z), numbers (0-9), and up to one '_' or '.'",
|
|
109
|
-
},
|
|
110
|
-
{ status: 404 }
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const specialCharacters = data.data.username.match(/[._]/g) ?? [];
|
|
115
|
-
if (specialCharacters.length > 1) {
|
|
116
|
-
return NextResponse.json(
|
|
117
|
-
{
|
|
118
|
-
error: "Username can contain at most one '_' or one '.'",
|
|
119
|
-
},
|
|
120
|
-
{ status: 404 }
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (data.data.flag !== undefined) {
|
|
126
|
-
const normalizedFlag = validator.trim(data.data.flag).toUpperCase();
|
|
127
|
-
|
|
128
|
-
if (!countryCodes.has(normalizedFlag as (typeof COUNTRY_LIST)[number])) {
|
|
129
|
-
return NextResponse.json(
|
|
130
|
-
{
|
|
131
|
-
error: "Flag must be a valid country code.",
|
|
132
|
-
},
|
|
133
|
-
{ status: 404 }
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
data.data.flag = normalizedFlag;
|
|
73
|
+
if (validationError) {
|
|
74
|
+
return NextResponse.json({ error: validationError }, { status: 404 });
|
|
138
75
|
}
|
|
139
76
|
|
|
140
77
|
const user = (await getUserBySession(data.session)) as User;
|