rhythia-api 233.0.0 → 235.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.
Files changed (90) hide show
  1. package/.codex +0 -0
  2. package/.env +1 -12
  3. package/README.md +4 -4
  4. package/api/acceptInvite.ts +1 -1
  5. package/api/addCollectionMap.ts +1 -1
  6. package/api/chartPublicStats.ts +1 -1
  7. package/api/checkQualified.ts +93 -93
  8. package/api/createBeatmap.ts +53 -62
  9. package/api/createBeatmapPage.ts +1 -1
  10. package/api/createClan.ts +1 -1
  11. package/api/createCollection.ts +1 -1
  12. package/api/createInvite.ts +1 -1
  13. package/api/createSupporter.ts +1 -1
  14. package/api/deleteBeatmapPage.ts +2 -5
  15. package/api/deleteCollection.ts +1 -1
  16. package/api/deleteCollectionMap.ts +1 -1
  17. package/api/editAboutMe.ts +1 -1
  18. package/api/editClan.ts +1 -1
  19. package/api/editCollection.ts +1 -2
  20. package/api/editProfile.ts +1 -1
  21. package/api/enhancedSearch.ts +113 -113
  22. package/api/executeAdminOperation.ts +1 -22
  23. package/api/getAvatarUploadUrl.ts +1 -1
  24. package/api/getBadgeLeaders.ts +1 -1
  25. package/api/getBadgedUsers.ts +1 -1
  26. package/api/getBeatmapComments.ts +1 -1
  27. package/api/getBeatmapPage.ts +74 -106
  28. package/api/getBeatmapPageById.ts +70 -109
  29. package/api/getBeatmapStarRating.ts +1 -1
  30. package/api/getBeatmaps.ts +123 -93
  31. package/api/getClan.ts +1 -1
  32. package/api/getClans.ts +1 -1
  33. package/api/getCollection.ts +1 -1
  34. package/api/getCollections.ts +1 -1
  35. package/api/getInventory.ts +1 -1
  36. package/api/getLeaderboard.ts +1 -1
  37. package/api/getMapUploadUrl.ts +2 -2
  38. package/api/getOnlinePlayers.ts +1 -1
  39. package/api/getPassToken.ts +1 -1
  40. package/api/getProfile.ts +51 -31
  41. package/api/getPublicStats.ts +5 -5
  42. package/api/getRawStarRating.ts +1 -1
  43. package/api/getScore.ts +1 -1
  44. package/api/getStoryBeatmaps.ts +1 -1
  45. package/api/getTimestamp.ts +1 -1
  46. package/api/getUserScores.ts +19 -19
  47. package/api/getVerified.ts +1 -1
  48. package/api/getVideoUploadUrl.ts +1 -1
  49. package/api/postBeatmapComment.ts +1 -1
  50. package/api/qualifyMap.ts +97 -92
  51. package/api/rankMapsArchive.ts +20 -20
  52. package/api/searchUsers.ts +1 -1
  53. package/api/setPasskey.ts +1 -1
  54. package/api/submitScore.ts +1 -6
  55. package/api/submitScoreInternal.ts +461 -449
  56. package/api/updateBeatmapPage.ts +1 -1
  57. package/api/vetoMap.ts +101 -101
  58. package/index.ts +180 -167
  59. package/package.json +7 -12
  60. package/queries/admin_delete_user.sql +39 -39
  61. package/queries/admin_exclude_user.sql +21 -21
  62. package/queries/admin_invalidate_ranked_scores.sql +18 -18
  63. package/queries/admin_log_action.sql +10 -10
  64. package/queries/admin_profanity_clear.sql +29 -29
  65. package/queries/admin_remove_all_scores.sql +29 -29
  66. package/queries/admin_restrict_user.sql +21 -21
  67. package/queries/admin_search_users.sql +24 -24
  68. package/queries/admin_silence_user.sql +21 -21
  69. package/queries/admin_unban_user.sql +21 -21
  70. package/queries/enhanced_search.sql +217 -217
  71. package/queries/get_badge_leaderboard.sql +50 -50
  72. package/queries/get_clan_leaderboard.sql +68 -68
  73. package/queries/get_collections_v4.sql +109 -109
  74. package/queries/get_top_scores_for_beatmap.sql +44 -44
  75. package/queries/get_top_scores_for_beatmap3.sql +38 -0
  76. package/queries/get_user_by_email.sql +32 -32
  77. package/queries/get_user_scores_lastday.sql +47 -47
  78. package/queries/get_user_scores_reign.sql +31 -31
  79. package/queries/get_user_scores_top_and_stats.sql +84 -84
  80. package/queries/grant_special_badges.sql +69 -69
  81. package/types/database.ts +1288 -1248
  82. package/utils/beatmapTopScores.ts +84 -0
  83. package/utils/mapLifecycleWebhook.ts +287 -277
  84. package/utils/requestGeo.ts +13 -0
  85. package/utils/requestUtils.ts +127 -127
  86. package/utils/response.ts +11 -0
  87. package/worker.ts +189 -0
  88. package/wrangler.jsonc +10 -0
  89. package/index.html +0 -3
  90. package/vercel.json +0 -13
@@ -1,127 +1,127 @@
1
- import { NextResponse } from "next/server";
2
- import { ZodObject } from "zod";
3
- import { getUserBySession } from "./getUserBySession";
4
- import { supabase } from "./supabase";
5
-
6
- const SENSITIVE_LOG_KEYS = new Set([
7
- "session",
8
- "replayBytes",
9
- "token",
10
- "secret",
11
- "passkey",
12
- "passKey",
13
- ]);
14
- const LONG_LOG_STRING_THRESHOLD = 256;
15
-
16
- function sanitizeForLog(
17
- value: unknown,
18
- key?: string
19
- ):
20
- | string
21
- | number
22
- | boolean
23
- | null
24
- | undefined
25
- | Record<string, unknown>
26
- | unknown[] {
27
- const normalizedKey = (key || "").toLowerCase();
28
- if (
29
- SENSITIVE_LOG_KEYS.has(key || "") ||
30
- SENSITIVE_LOG_KEYS.has(normalizedKey)
31
- ) {
32
- if (value === null || value === undefined) {
33
- return value as null | undefined;
34
- }
35
- return "<Long>";
36
- }
37
-
38
- if (typeof value === "string") {
39
- return value.length > LONG_LOG_STRING_THRESHOLD ? "<Long>" : value;
40
- }
41
-
42
- if (Array.isArray(value)) {
43
- return value.map((item) => sanitizeForLog(item));
44
- }
45
-
46
- if (value && typeof value === "object") {
47
- const sanitizedObject: Record<string, unknown> = {};
48
- Object.entries(value as Record<string, unknown>).forEach(
49
- ([entryKey, entryValue]) => {
50
- sanitizedObject[entryKey] = sanitizeForLog(entryValue, entryKey);
51
- }
52
- );
53
- return sanitizedObject;
54
- }
55
-
56
- return value as string | number | boolean | null | undefined;
57
- }
58
-
59
- interface Props<
60
- K = (...args: any[]) => Promise<NextResponse<any>>,
61
- T = ZodObject<any>,
62
- > {
63
- request: Request;
64
- schema: { input: T; output: T };
65
- authorization?: Function;
66
- activity: K;
67
- }
68
-
69
- export async function protectedApi({
70
- request,
71
- schema,
72
- authorization,
73
- activity,
74
- }: Props) {
75
- try {
76
- const toParse = await request.json();
77
- const data = schema.input.parse(toParse);
78
-
79
- console.log("Request payload:", sanitizeForLog(data));
80
-
81
- setActivity(data);
82
- if (authorization) {
83
- const authorizationResponse = await authorization(data);
84
- if (authorizationResponse) {
85
- return authorizationResponse;
86
- }
87
- }
88
- return await activity(data, request);
89
- } catch (error) {
90
- console.log(`Couldn't parse`, error.toString());
91
- return NextResponse.json({ error: error.toString() }, { status: 400 });
92
- }
93
- }
94
-
95
- export async function setActivity(data: Record<string, any>) {
96
- if (data.session) {
97
- const user = (await supabase.auth.getUser(data.session)).data.user;
98
- if (user) {
99
- await supabase.from("profileActivities").upsert({
100
- uid: user.id,
101
- last_activity: Date.now(),
102
- });
103
- }
104
- }
105
- }
106
-
107
- export async function validUser(data) {
108
- if (!data.session) {
109
- return NextResponse.json(
110
- {
111
- error: "Session is missing",
112
- },
113
- { status: 501 }
114
- );
115
- }
116
-
117
- const user = await getUserBySession(data.session);
118
- if (!user) {
119
- console.log("Invalid user session");
120
- return NextResponse.json(
121
- {
122
- error: "Invalid user session",
123
- },
124
- { status: 401 }
125
- );
126
- }
127
- }
1
+ import { NextResponse } from "./response";
2
+ import { ZodObject } from "zod";
3
+ import { getUserBySession } from "./getUserBySession";
4
+ import { supabase } from "./supabase";
5
+
6
+ const SENSITIVE_LOG_KEYS = new Set([
7
+ "session",
8
+ "replayBytes",
9
+ "token",
10
+ "secret",
11
+ "passkey",
12
+ "passKey",
13
+ ]);
14
+ const LONG_LOG_STRING_THRESHOLD = 256;
15
+
16
+ function sanitizeForLog(
17
+ value: unknown,
18
+ key?: string
19
+ ):
20
+ | string
21
+ | number
22
+ | boolean
23
+ | null
24
+ | undefined
25
+ | Record<string, unknown>
26
+ | unknown[] {
27
+ const normalizedKey = (key || "").toLowerCase();
28
+ if (
29
+ SENSITIVE_LOG_KEYS.has(key || "") ||
30
+ SENSITIVE_LOG_KEYS.has(normalizedKey)
31
+ ) {
32
+ if (value === null || value === undefined) {
33
+ return value as null | undefined;
34
+ }
35
+ return "<Long>";
36
+ }
37
+
38
+ if (typeof value === "string") {
39
+ return value.length > LONG_LOG_STRING_THRESHOLD ? "<Long>" : value;
40
+ }
41
+
42
+ if (Array.isArray(value)) {
43
+ return value.map((item) => sanitizeForLog(item));
44
+ }
45
+
46
+ if (value && typeof value === "object") {
47
+ const sanitizedObject: Record<string, unknown> = {};
48
+ Object.entries(value as Record<string, unknown>).forEach(
49
+ ([entryKey, entryValue]) => {
50
+ sanitizedObject[entryKey] = sanitizeForLog(entryValue, entryKey);
51
+ }
52
+ );
53
+ return sanitizedObject;
54
+ }
55
+
56
+ return value as string | number | boolean | null | undefined;
57
+ }
58
+
59
+ interface Props<
60
+ K = (...args: any[]) => Promise<NextResponse<any>>,
61
+ T = ZodObject<any>,
62
+ > {
63
+ request: Request;
64
+ schema: { input: T; output: T };
65
+ authorization?: Function;
66
+ activity: K;
67
+ }
68
+
69
+ export async function protectedApi({
70
+ request,
71
+ schema,
72
+ authorization,
73
+ activity,
74
+ }: Props) {
75
+ try {
76
+ const toParse = await request.json();
77
+ const data = schema.input.parse(toParse);
78
+
79
+ console.log("Request payload:", sanitizeForLog(data));
80
+
81
+ setActivity(data);
82
+ if (authorization) {
83
+ const authorizationResponse = await authorization(data);
84
+ if (authorizationResponse) {
85
+ return authorizationResponse;
86
+ }
87
+ }
88
+ return await activity(data, request);
89
+ } catch (error) {
90
+ console.log(`Couldn't parse`, error.toString());
91
+ return NextResponse.json({ error: error.toString() }, { status: 400 });
92
+ }
93
+ }
94
+
95
+ export async function setActivity(data: Record<string, any>) {
96
+ if (data.session) {
97
+ const user = (await supabase.auth.getUser(data.session)).data.user;
98
+ if (user) {
99
+ await supabase.from("profileActivities").upsert({
100
+ uid: user.id,
101
+ last_activity: Date.now(),
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ export async function validUser(data) {
108
+ if (!data.session) {
109
+ return NextResponse.json(
110
+ {
111
+ error: "Session is missing",
112
+ },
113
+ { status: 501 }
114
+ );
115
+ }
116
+
117
+ const user = await getUserBySession(data.session);
118
+ if (!user) {
119
+ console.log("Invalid user session");
120
+ return NextResponse.json(
121
+ {
122
+ error: "Invalid user session",
123
+ },
124
+ { status: 401 }
125
+ );
126
+ }
127
+ }
@@ -0,0 +1,11 @@
1
+ export class NextResponse<Body = unknown> extends Response {
2
+ static json<Body>(body: Body, init: ResponseInit = {}) {
3
+ const headers = new Headers(init.headers);
4
+ headers.set("content-type", "application/json; charset=utf-8");
5
+
6
+ return new NextResponse<Body>(JSON.stringify(body), {
7
+ ...init,
8
+ headers,
9
+ });
10
+ }
11
+ }
package/worker.ts ADDED
@@ -0,0 +1,189 @@
1
+ import { POST as acceptInvite } from "./api/acceptInvite";
2
+ import { POST as addCollectionMap } from "./api/addCollectionMap";
3
+ import { POST as chartPublicStats } from "./api/chartPublicStats";
4
+ import { POST as checkQualified } from "./api/checkQualified";
5
+ import { POST as createBeatmap } from "./api/createBeatmap";
6
+ import { POST as createBeatmapPage } from "./api/createBeatmapPage";
7
+ import { POST as createClan } from "./api/createClan";
8
+ import { POST as createCollection } from "./api/createCollection";
9
+ import { POST as createInvite } from "./api/createInvite";
10
+ import { POST as createSupporter } from "./api/createSupporter";
11
+ import { POST as deleteBeatmapPage } from "./api/deleteBeatmapPage";
12
+ import { POST as deleteCollection } from "./api/deleteCollection";
13
+ import { POST as deleteCollectionMap } from "./api/deleteCollectionMap";
14
+ import { POST as editAboutMe } from "./api/editAboutMe";
15
+ import { POST as editClan } from "./api/editClan";
16
+ import { POST as editCollection } from "./api/editCollection";
17
+ import { POST as editProfile } from "./api/editProfile";
18
+ import { POST as enhancedSearch } from "./api/enhancedSearch";
19
+ import { POST as executeAdminOperation } from "./api/executeAdminOperation";
20
+ import { POST as getAvatarUploadUrl } from "./api/getAvatarUploadUrl";
21
+ import { POST as getBadgeLeaders } from "./api/getBadgeLeaders";
22
+ import { POST as getBadgedUsers } from "./api/getBadgedUsers";
23
+ import { POST as getBeatmapComments } from "./api/getBeatmapComments";
24
+ import { POST as getBeatmapPage } from "./api/getBeatmapPage";
25
+ import { POST as getBeatmapPageById } from "./api/getBeatmapPageById";
26
+ import { POST as getBeatmapStarRating } from "./api/getBeatmapStarRating";
27
+ import { POST as getBeatmaps } from "./api/getBeatmaps";
28
+ import { POST as getClan } from "./api/getClan";
29
+ import { POST as getClans } from "./api/getClans";
30
+ import { POST as getCollection } from "./api/getCollection";
31
+ import { POST as getCollections } from "./api/getCollections";
32
+ import { POST as getInventory } from "./api/getInventory";
33
+ import { POST as getLeaderboard } from "./api/getLeaderboard";
34
+ import { POST as getMapUploadUrl } from "./api/getMapUploadUrl";
35
+ import { POST as getOnlinePlayers } from "./api/getOnlinePlayers";
36
+ import { POST as getPassToken } from "./api/getPassToken";
37
+ import { POST as getProfile } from "./api/getProfile";
38
+ import { POST as getPublicStats } from "./api/getPublicStats";
39
+ import { POST as getRawStarRating } from "./api/getRawStarRating";
40
+ import { POST as getScore } from "./api/getScore";
41
+ import { POST as getStoryBeatmaps } from "./api/getStoryBeatmaps";
42
+ import { POST as getTimestamp } from "./api/getTimestamp";
43
+ import { POST as getUserScores } from "./api/getUserScores";
44
+ import { POST as getVerified } from "./api/getVerified";
45
+ import { POST as getVideoUploadUrl } from "./api/getVideoUploadUrl";
46
+ import { POST as postBeatmapComment } from "./api/postBeatmapComment";
47
+ import { POST as qualifyMap } from "./api/qualifyMap";
48
+ import { POST as rankMapsArchive } from "./api/rankMapsArchive";
49
+ import { POST as searchUsers } from "./api/searchUsers";
50
+ import { POST as setPasskey } from "./api/setPasskey";
51
+ import { POST as submitScore } from "./api/submitScore";
52
+ import { POST as submitScoreInternal } from "./api/submitScoreInternal";
53
+ import { POST as updateBeatmapPage } from "./api/updateBeatmapPage";
54
+ import { POST as vetoMap } from "./api/vetoMap";
55
+ import { NextResponse } from "./utils/response";
56
+
57
+ type RouteHandler = (request: Request) => Promise<Response> | Response;
58
+
59
+ const corsHeaders = {
60
+ "Access-Control-Allow-Credentials": "true",
61
+ "Access-Control-Allow-Origin": "*",
62
+ "Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
63
+ "Access-Control-Allow-Headers":
64
+ "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
65
+ };
66
+
67
+ const apiRoutes: Record<string, RouteHandler> = {
68
+ "/api/acceptInvite": acceptInvite,
69
+ "/api/addCollectionMap": addCollectionMap,
70
+ "/api/chartPublicStats": chartPublicStats,
71
+ "/api/checkQualified": checkQualified,
72
+ "/api/createBeatmap": createBeatmap,
73
+ "/api/createBeatmapPage": createBeatmapPage,
74
+ "/api/createClan": createClan,
75
+ "/api/createCollection": createCollection,
76
+ "/api/createInvite": createInvite,
77
+ "/api/createSupporter": createSupporter,
78
+ "/api/deleteBeatmapPage": deleteBeatmapPage,
79
+ "/api/deleteCollection": deleteCollection,
80
+ "/api/deleteCollectionMap": deleteCollectionMap,
81
+ "/api/editAboutMe": editAboutMe,
82
+ "/api/editClan": editClan,
83
+ "/api/editCollection": editCollection,
84
+ "/api/editProfile": editProfile,
85
+ "/api/enhancedSearch": enhancedSearch,
86
+ "/api/executeAdminOperation": executeAdminOperation,
87
+ "/api/getAvatarUploadUrl": getAvatarUploadUrl,
88
+ "/api/getBadgeLeaders": getBadgeLeaders,
89
+ "/api/getBadgedUsers": getBadgedUsers,
90
+ "/api/getBeatmapComments": getBeatmapComments,
91
+ "/api/getBeatmapPage": getBeatmapPage,
92
+ "/api/getBeatmapPageById": getBeatmapPageById,
93
+ "/api/getBeatmapStarRating": getBeatmapStarRating,
94
+ "/api/getBeatmaps": getBeatmaps,
95
+ "/api/getClan": getClan,
96
+ "/api/getClans": getClans,
97
+ "/api/getCollection": getCollection,
98
+ "/api/getCollections": getCollections,
99
+ "/api/getInventory": getInventory,
100
+ "/api/getLeaderboard": getLeaderboard,
101
+ "/api/getMapUploadUrl": getMapUploadUrl,
102
+ "/api/getOnlinePlayers": getOnlinePlayers,
103
+ "/api/getPassToken": getPassToken,
104
+ "/api/getProfile": getProfile,
105
+ "/api/getPublicStats": getPublicStats,
106
+ "/api/getRawStarRating": getRawStarRating,
107
+ "/api/getScore": getScore,
108
+ "/api/getStoryBeatmaps": getStoryBeatmaps,
109
+ "/api/getTimestamp": getTimestamp,
110
+ "/api/getUserScores": getUserScores,
111
+ "/api/getVerified": getVerified,
112
+ "/api/getVideoUploadUrl": getVideoUploadUrl,
113
+ "/api/postBeatmapComment": postBeatmapComment,
114
+ "/api/qualifyMap": qualifyMap,
115
+ "/api/rankMapsArchive": rankMapsArchive,
116
+ "/api/searchUsers": searchUsers,
117
+ "/api/setPasskey": setPasskey,
118
+ "/api/submitScore": submitScore,
119
+ "/api/submitScoreInternal": submitScoreInternal,
120
+ "/api/updateBeatmapPage": updateBeatmapPage,
121
+ "/api/vetoMap": vetoMap,
122
+ };
123
+
124
+ function withCors(response: Response) {
125
+ const headers = new Headers(response.headers);
126
+
127
+ for (const [key, value] of Object.entries(corsHeaders)) {
128
+ headers.set(key, value);
129
+ }
130
+
131
+ return new Response(response.body, {
132
+ status: response.status,
133
+ statusText: response.statusText,
134
+ headers,
135
+ });
136
+ }
137
+
138
+ function notFound() {
139
+ return NextResponse.json({ error: "Not Found" }, { status: 404 });
140
+ }
141
+
142
+ export default {
143
+ async fetch(request: Request) {
144
+ const url = new URL(request.url);
145
+ const pathname =
146
+ url.pathname.endsWith("/") && url.pathname.length > 1
147
+ ? url.pathname.slice(0, -1)
148
+ : url.pathname;
149
+
150
+ if (request.method === "OPTIONS") {
151
+ return withCors(new Response(null, { status: 204 }));
152
+ }
153
+
154
+ if (pathname === "/") {
155
+ return withCors(
156
+ new Response("<html><div>Rhythia API</div></html>", {
157
+ headers: {
158
+ "content-type": "text/html; charset=utf-8",
159
+ },
160
+ })
161
+ );
162
+ }
163
+
164
+ const handler = apiRoutes[pathname];
165
+ if (!handler) {
166
+ return withCors(notFound());
167
+ }
168
+
169
+ if (request.method !== "POST") {
170
+ return withCors(
171
+ NextResponse.json({ error: "Method Not Allowed" }, { status: 405 })
172
+ );
173
+ }
174
+
175
+ try {
176
+ return withCors(await handler(request));
177
+ } catch (error) {
178
+ console.error(error);
179
+ return withCors(
180
+ NextResponse.json(
181
+ {
182
+ error: "Internal Server Error",
183
+ },
184
+ { status: 500 }
185
+ )
186
+ );
187
+ }
188
+ },
189
+ };
package/wrangler.jsonc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "./node_modules/wrangler/config-schema.json",
3
+ "name": "rhythia-online-api",
4
+ "main": "worker.ts",
5
+ "compatibility_date": "2026-03-11",
6
+ "compatibility_flags": ["nodejs_compat"],
7
+ "observability": {
8
+ "enabled": true
9
+ }
10
+ }
package/index.html DELETED
@@ -1,3 +0,0 @@
1
- <html>
2
- <div>Rhythia API</div>
3
- </html>
package/vercel.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "headers": [
3
- {
4
- "source": "/api/(.*)",
5
- "headers": [
6
- { "key": "Access-Control-Allow-Credentials", "value": "true" },
7
- { "key": "Access-Control-Allow-Origin", "value": "*" },
8
- { "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" },
9
- { "key": "Access-Control-Allow-Headers", "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" }
10
- ]
11
- }
12
- ]
13
- }