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,113 +1,113 @@
1
- import { NextResponse } from "next/server";
2
- import z from "zod";
3
- import { Database } from "../types/database";
4
- import { protectedApi } from "../utils/requestUtils";
5
- import { supabase } from "../utils/supabase";
6
-
7
- type EnhancedSearchRow =
8
- Database["public"]["Functions"]["enhanced_search"]["Returns"][number];
9
-
10
- export const Schema = {
11
- input: z.strictObject({
12
- text: z.string().trim().min(1),
13
- limit: z.number().int().min(1).max(25).default(10),
14
- }),
15
- output: z.object({
16
- error: z.string().optional(),
17
- users: z.array(
18
- z.object({
19
- id: z.number(),
20
- username: z.string().nullable(),
21
- avatar_url: z.string().nullable(),
22
- about_me: z.string().nullable(),
23
- flag: z.string().nullable(),
24
- })
25
- ),
26
- beatmaps: z.array(
27
- z.object({
28
- id: z.number(),
29
- mapId: z.string().nullable(),
30
- title: z.string().nullable(),
31
- description: z.string().nullable(),
32
- image: z.string().nullable(),
33
- starRating: z.number().nullable(),
34
- length: z.number().nullable(),
35
- status: z.string().nullable(),
36
- tags: z.string().nullable(),
37
- owner: z.number().nullable(),
38
- ownerUsername: z.string().nullable(),
39
- ownerAvatar: z.string().nullable(),
40
- })
41
- ),
42
- }),
43
- };
44
-
45
- export async function POST(request: Request): Promise<NextResponse> {
46
- return protectedApi({
47
- request,
48
- schema: Schema,
49
- authorization: () => {},
50
- activity: handler,
51
- });
52
- }
53
-
54
- export async function handler(
55
- data: (typeof Schema)["input"]["_type"]
56
- ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
57
- const { data: results, error } = await supabase.rpc("enhanced_search", {
58
- search_text: data.text,
59
- result_limit: data.limit,
60
- });
61
-
62
- if (error) {
63
- return NextResponse.json(
64
- {
65
- error: JSON.stringify(error),
66
- users: [],
67
- beatmaps: [],
68
- },
69
- { status: 500 }
70
- );
71
- }
72
-
73
- const users: (typeof Schema)["output"]["_type"]["users"] = [];
74
- const beatmaps: (typeof Schema)["output"]["_type"]["beatmaps"] = [];
75
-
76
- for (const result of (results || []) as EnhancedSearchRow[]) {
77
- if (result.result_type === "user" && result.user_id !== null) {
78
- users.push({
79
- id: result.user_id,
80
- username: result.user_username,
81
- avatar_url: result.user_avatar_url,
82
- about_me: result.user_about_me,
83
- flag: result.user_flag,
84
- });
85
- continue;
86
- }
87
-
88
- if (
89
- result.result_type === "beatmap" &&
90
- result.beatmap_page_id !== null
91
- ) {
92
- beatmaps.push({
93
- id: result.beatmap_page_id,
94
- mapId: result.beatmap_map_id,
95
- title: result.beatmap_title,
96
- description: result.beatmap_description,
97
- image: result.beatmap_image,
98
- starRating: result.beatmap_star_rating,
99
- length: result.beatmap_length,
100
- status: result.beatmap_status,
101
- tags: result.beatmap_tags,
102
- owner: result.beatmap_owner,
103
- ownerUsername: result.beatmap_owner_username,
104
- ownerAvatar: result.beatmap_owner_avatar,
105
- });
106
- }
107
- }
108
-
109
- return NextResponse.json({
110
- users,
111
- beatmaps,
112
- });
113
- }
1
+ import { NextResponse } from "../utils/response";
2
+ import z from "zod";
3
+ import { Database } from "../types/database";
4
+ import { protectedApi } from "../utils/requestUtils";
5
+ import { supabase } from "../utils/supabase";
6
+
7
+ type EnhancedSearchRow =
8
+ Database["public"]["Functions"]["enhanced_search"]["Returns"][number];
9
+
10
+ export const Schema = {
11
+ input: z.strictObject({
12
+ text: z.string().trim().min(1),
13
+ limit: z.number().int().min(1).max(25).default(10),
14
+ }),
15
+ output: z.object({
16
+ error: z.string().optional(),
17
+ users: z.array(
18
+ z.object({
19
+ id: z.number(),
20
+ username: z.string().nullable(),
21
+ avatar_url: z.string().nullable(),
22
+ about_me: z.string().nullable(),
23
+ flag: z.string().nullable(),
24
+ })
25
+ ),
26
+ beatmaps: z.array(
27
+ z.object({
28
+ id: z.number(),
29
+ mapId: z.string().nullable(),
30
+ title: z.string().nullable(),
31
+ description: z.string().nullable(),
32
+ image: z.string().nullable(),
33
+ starRating: z.number().nullable(),
34
+ length: z.number().nullable(),
35
+ status: z.string().nullable(),
36
+ tags: z.string().nullable(),
37
+ owner: z.number().nullable(),
38
+ ownerUsername: z.string().nullable(),
39
+ ownerAvatar: z.string().nullable(),
40
+ })
41
+ ),
42
+ }),
43
+ };
44
+
45
+ export async function POST(request: Request): Promise<NextResponse> {
46
+ return protectedApi({
47
+ request,
48
+ schema: Schema,
49
+ authorization: () => {},
50
+ activity: handler,
51
+ });
52
+ }
53
+
54
+ export async function handler(
55
+ data: (typeof Schema)["input"]["_type"]
56
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
57
+ const { data: results, error } = await supabase.rpc("enhanced_search", {
58
+ search_text: data.text,
59
+ result_limit: data.limit,
60
+ });
61
+
62
+ if (error) {
63
+ return NextResponse.json(
64
+ {
65
+ error: JSON.stringify(error),
66
+ users: [],
67
+ beatmaps: [],
68
+ },
69
+ { status: 500 }
70
+ );
71
+ }
72
+
73
+ const users: (typeof Schema)["output"]["_type"]["users"] = [];
74
+ const beatmaps: (typeof Schema)["output"]["_type"]["beatmaps"] = [];
75
+
76
+ for (const result of (results || []) as EnhancedSearchRow[]) {
77
+ if (result.result_type === "user" && result.user_id !== null) {
78
+ users.push({
79
+ id: result.user_id,
80
+ username: result.user_username,
81
+ avatar_url: result.user_avatar_url,
82
+ about_me: result.user_about_me,
83
+ flag: result.user_flag,
84
+ });
85
+ continue;
86
+ }
87
+
88
+ if (
89
+ result.result_type === "beatmap" &&
90
+ result.beatmap_page_id !== null
91
+ ) {
92
+ beatmaps.push({
93
+ id: result.beatmap_page_id,
94
+ mapId: result.beatmap_map_id,
95
+ title: result.beatmap_title,
96
+ description: result.beatmap_description,
97
+ image: result.beatmap_image,
98
+ starRating: result.beatmap_star_rating,
99
+ length: result.beatmap_length,
100
+ status: result.beatmap_status,
101
+ tags: result.beatmap_tags,
102
+ owner: result.beatmap_owner,
103
+ ownerUsername: result.beatmap_owner_username,
104
+ ownerAvatar: result.beatmap_owner_avatar,
105
+ });
106
+ }
107
+ }
108
+
109
+ return NextResponse.json({
110
+ users,
111
+ beatmaps,
112
+ });
113
+ }
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { Database } from "../types/database";
4
4
  import { protectedApi, validUser } from "../utils/requestUtils";
@@ -368,27 +368,6 @@ export async function handler(
368
368
 
369
369
  if (targetUserId !== null && !result?.error) {
370
370
  await invalidateCachePrefix(`userscore:${targetUserId}`);
371
-
372
- if (
373
- operation === "removeAllScores" ||
374
- operation === "invalidateRankedScores" ||
375
- operation === "deleteUser"
376
- ) {
377
- const { data: beatmapHashes } = await supabase
378
- .from("scores")
379
- .select("beatmapHash")
380
- .eq("userId", targetUserId);
381
-
382
- const uniqueHashes = new Set(
383
- (beatmapHashes || [])
384
- .map((row) => row.beatmapHash)
385
- .filter((hash): hash is string => Boolean(hash))
386
- );
387
-
388
- for (const hash of uniqueHashes) {
389
- await invalidateCachePrefix(`beatmap-scores:${hash}`);
390
- }
391
- }
392
371
  }
393
372
 
394
373
  return NextResponse.json({
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { supabase } from "../utils/supabase";
4
4
  import { getActiveProfileIdSet } from "../utils/activityStatus";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -1,4 +1,4 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { getCacheValue, setCacheValue } from "../utils/cache";
@@ -1,8 +1,8 @@
1
- import { NextResponse } from "next/server";
1
+ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
- import { getCacheValue, setCacheValue } from "../utils/cache";
5
4
  import { supabase } from "../utils/supabase";
5
+ import { getVisibleTopScoresForBeatmap } from "../utils/beatmapTopScores";
6
6
 
7
7
  export const Schema = {
8
8
  input: z.strictObject({
@@ -47,31 +47,31 @@ export const Schema = {
47
47
  image: z.string().nullable().optional(),
48
48
  imageLarge: z.string().nullable().optional(),
49
49
  starRating: z.number().nullable().optional(),
50
- owner: z.number().nullable().optional(),
51
- ownerUsername: z.string().nullable().optional(),
52
- ownerAvatar: z.string().nullable().optional(),
53
- status: z.string().nullable().optional(),
54
- qualified: z.boolean().nullable().optional(),
55
- qualifiedAt: z.string().nullable().optional(),
56
- description: z.string().nullable().optional(),
57
- tags: z.string().nullable().optional(),
58
- videoUrl: z.string().nullable().optional(),
59
- vetos: z
60
- .array(
61
- z.object({
62
- id: z.number(),
63
- userId: z.number().nullable().optional(),
64
- username: z.string().nullable().optional(),
65
- avatar_url: z.string().nullable().optional(),
66
- veto_reason: z.string().nullable().optional(),
67
- created_at: z.string().nullable().optional(),
68
- })
69
- )
70
- .optional(),
71
- })
72
- .optional(),
73
- }),
74
- };
50
+ owner: z.number().nullable().optional(),
51
+ ownerUsername: z.string().nullable().optional(),
52
+ ownerAvatar: z.string().nullable().optional(),
53
+ status: z.string().nullable().optional(),
54
+ qualified: z.boolean().nullable().optional(),
55
+ qualifiedAt: z.string().nullable().optional(),
56
+ description: z.string().nullable().optional(),
57
+ tags: z.string().nullable().optional(),
58
+ videoUrl: z.string().nullable().optional(),
59
+ vetos: z
60
+ .array(
61
+ z.object({
62
+ id: z.number(),
63
+ userId: z.number().nullable().optional(),
64
+ username: z.string().nullable().optional(),
65
+ avatar_url: z.string().nullable().optional(),
66
+ veto_reason: z.string().nullable().optional(),
67
+ created_at: z.string().nullable().optional(),
68
+ })
69
+ )
70
+ .optional(),
71
+ })
72
+ .optional(),
73
+ }),
74
+ };
75
75
 
76
76
  export async function POST(request: Request): Promise<NextResponse> {
77
77
  return protectedApi({
@@ -106,81 +106,49 @@ export async function handler(
106
106
  noteCount,
107
107
  title
108
108
  ),
109
- profiles (
110
- username,
111
- avatar_url
112
- ),
113
- vetos (
114
- id,
115
- user,
116
- veto_reason,
117
- created_at,
118
- profiles (
119
- id,
120
- username,
121
- avatar_url
122
- )
123
- )
124
- `
125
- )
109
+ profiles (
110
+ username,
111
+ avatar_url
112
+ ),
113
+ vetos (
114
+ id,
115
+ user,
116
+ veto_reason,
117
+ created_at,
118
+ profiles (
119
+ id,
120
+ username,
121
+ avatar_url
122
+ )
123
+ )
124
+ `
125
+ )
126
126
  .eq("id", data.id)
127
127
  .single();
128
128
 
129
129
  if (!beatmapPage) return NextResponse.json({});
130
130
 
131
131
  const beatmapHash = beatmapPage?.latestBeatmapHash || "";
132
- const isCacheable =
133
- beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
134
- const cacheKey = `beatmap-scores:${beatmapHash}:limit=${limit}`;
135
-
136
- // let scoreData: any[] | null = null;
132
+ const { scores, error: scoreError } = await getVisibleTopScoresForBeatmap(
133
+ beatmapHash,
134
+ limit
135
+ );
137
136
 
138
- // if (isCacheable && beatmapHash) {
139
- // scoreData = await getCacheValue<any[]>(cacheKey);
140
- // }
137
+ if (scoreError) {
138
+ return NextResponse.json({ error: scoreError });
139
+ }
141
140
 
142
- // if (!scoreData) {
143
- // const { data: rpcScores, error } = await supabase.rpc(
144
- // "get_top_scores_for_beatmap",
145
- // { beatmap_hash: beatmapHash }
146
- // );
141
+ const mappedVetos = ((beatmapPage as any).vetos || []).map((veto: any) => ({
142
+ id: veto.id,
143
+ userId: veto.user,
144
+ username: veto.profiles?.username,
145
+ avatar_url: veto.profiles?.avatar_url,
146
+ veto_reason: veto.veto_reason,
147
+ created_at: veto.created_at,
148
+ }));
147
149
 
148
- // if (error) {
149
- // return NextResponse.json({ error: JSON.stringify(error) });
150
- // }
151
-
152
- // scoreData = (rpcScores || []).slice(0, limit);
153
-
154
- // if (isCacheable && beatmapHash) {
155
- // await setCacheValue(cacheKey, scoreData);
156
- // }
157
- // }
158
-
159
- const mappedVetos = ((beatmapPage as any).vetos || []).map((veto: any) => ({
160
- id: veto.id,
161
- userId: veto.user,
162
- username: veto.profiles?.username,
163
- avatar_url: veto.profiles?.avatar_url,
164
- veto_reason: veto.veto_reason,
165
- created_at: veto.created_at,
166
- }));
167
-
168
- return NextResponse.json({
169
- scores: [].map((score: any) => ({
170
- id: score.id,
171
- awarded_sp: score.awarded_sp,
172
- created_at: score.created_at,
173
- misses: score.misses,
174
- mods: score.mods,
175
- passed: score.passed,
176
- songId: score.songid,
177
- speed: score.speed,
178
- spin: score.spin,
179
- userId: score.userid,
180
- username: score.username,
181
- avatar_url: score.avatar_url,
182
- accuracy: score.accuracy,
183
- })),
150
+ return NextResponse.json({
151
+ scores,
184
152
  beatmap: {
185
153
  playcount: beatmapPage.beatmaps?.playcount,
186
154
  created_at: beatmapPage.created_at,
@@ -196,16 +164,16 @@ export async function handler(
196
164
  starRating: beatmapPage.beatmaps?.starRating,
197
165
  owner: beatmapPage.owner,
198
166
  ownerUsername: beatmapPage.profiles?.username,
199
- ownerAvatar: beatmapPage.profiles?.avatar_url,
200
- id: beatmapPage.id,
201
- status: beatmapPage.status,
202
- qualified: beatmapPage.qualified,
203
- qualifiedAt: beatmapPage.qualifiedAt,
204
- nominations: beatmapPage.nominations as number[],
205
- description: beatmapPage.description,
206
- tags: beatmapPage.tags,
207
- videoUrl: beatmapPage.video_url,
208
- vetos: mappedVetos.length > 0 ? mappedVetos : undefined,
209
- },
210
- });
211
- }
167
+ ownerAvatar: beatmapPage.profiles?.avatar_url,
168
+ id: beatmapPage.id,
169
+ status: beatmapPage.status,
170
+ qualified: beatmapPage.qualified,
171
+ qualifiedAt: beatmapPage.qualifiedAt,
172
+ nominations: beatmapPage.nominations as number[],
173
+ description: beatmapPage.description,
174
+ tags: beatmapPage.tags,
175
+ videoUrl: beatmapPage.video_url,
176
+ vetos: mappedVetos.length > 0 ? mappedVetos : undefined,
177
+ },
178
+ });
179
+ }