rhythia-api 231.0.0 → 234.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 -83
  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 -0
  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 +1 -1
  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 -86
  51. package/api/rankMapsArchive.ts +8 -1
  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 -94
  58. package/index.ts +173 -120
  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 -0
  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 -1224
  82. package/utils/beatmapTopScores.ts +84 -0
  83. package/utils/mapLifecycleWebhook.ts +287 -0
  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,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
+ }
@@ -1,9 +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";
6
- import { getActiveProfileIdSet } from "../utils/activityStatus";
5
+ import { getVisibleTopScoresForBeatmap } from "../utils/beatmapTopScores";
7
6
 
8
7
  export const Schema = {
9
8
  input: z.strictObject({
@@ -28,6 +27,7 @@ export const Schema = {
28
27
  userId: z.number().nullable(),
29
28
  username: z.string().nullable(),
30
29
  avatar_url: z.string().nullable(),
30
+ accuracy: z.number().nullable(),
31
31
  })
32
32
  )
33
33
  .optional(),
@@ -46,29 +46,29 @@ export const Schema = {
46
46
  beatmapFile: z.string().nullable().optional(),
47
47
  image: z.string().nullable().optional(),
48
48
  starRating: z.number().nullable().optional(),
49
- owner: z.number().nullable().optional(),
50
- ownerUsername: z.string().nullable().optional(),
51
- ownerAvatar: z.string().nullable().optional(),
52
- status: z.string().nullable().optional(),
53
- qualified: z.boolean().nullable().optional(),
54
- qualifiedAt: z.string().nullable().optional(),
55
- videoUrl: z.string().nullable(),
56
- vetos: z
57
- .array(
58
- z.object({
59
- id: z.number(),
60
- userId: z.number().nullable().optional(),
61
- username: z.string().nullable().optional(),
62
- avatar_url: z.string().nullable().optional(),
63
- veto_reason: z.string().nullable().optional(),
64
- created_at: z.string().nullable().optional(),
65
- })
66
- )
67
- .optional(),
68
- })
69
- .optional(),
70
- }),
71
- };
49
+ owner: z.number().nullable().optional(),
50
+ ownerUsername: z.string().nullable().optional(),
51
+ ownerAvatar: z.string().nullable().optional(),
52
+ status: z.string().nullable().optional(),
53
+ qualified: z.boolean().nullable().optional(),
54
+ qualifiedAt: z.string().nullable().optional(),
55
+ videoUrl: z.string().nullable(),
56
+ vetos: z
57
+ .array(
58
+ z.object({
59
+ id: z.number(),
60
+ userId: z.number().nullable().optional(),
61
+ username: z.string().nullable().optional(),
62
+ avatar_url: z.string().nullable().optional(),
63
+ veto_reason: z.string().nullable().optional(),
64
+ created_at: z.string().nullable().optional(),
65
+ })
66
+ )
67
+ .optional(),
68
+ })
69
+ .optional(),
70
+ }),
71
+ };
72
72
 
73
73
  export async function POST(request: Request): Promise<NextResponse> {
74
74
  return protectedApi({
@@ -102,88 +102,49 @@ export async function handler(
102
102
  noteCount,
103
103
  title
104
104
  ),
105
- profiles (
106
- username,
107
- avatar_url
108
- ),
109
- vetos (
110
- id,
111
- user,
112
- veto_reason,
113
- created_at,
114
- profiles (
115
- id,
116
- username,
117
- avatar_url
118
- )
119
- )
120
- `
121
- )
105
+ profiles (
106
+ username,
107
+ avatar_url
108
+ ),
109
+ vetos (
110
+ id,
111
+ user,
112
+ veto_reason,
113
+ created_at,
114
+ profiles (
115
+ id,
116
+ username,
117
+ avatar_url
118
+ )
119
+ )
120
+ `
121
+ )
122
122
  .eq("latestBeatmapHash", data.mapId)
123
123
  .single();
124
124
 
125
125
  if (!beatmapPage) return NextResponse.json({});
126
126
 
127
127
  const beatmapHash = beatmapPage?.latestBeatmapHash || "";
128
- const isCacheable =
129
- beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
130
- const cacheKey = `beatmap-scores:${beatmapHash}`;
131
-
132
- let scoreData: any[] | null = null;
133
-
134
- if (isCacheable && beatmapHash) {
135
- scoreData = await getCacheValue<any[]>(cacheKey);
136
- }
137
-
138
- if (!scoreData) {
139
- const { data: rpcScores, error } = await supabase.rpc(
140
- "get_top_scores_for_beatmap",
141
- { beatmap_hash: beatmapHash }
142
- );
143
-
144
- if (error) {
145
- return NextResponse.json({ error: JSON.stringify(error) });
146
- }
147
-
148
- scoreData = (rpcScores || []).slice(0, 200);
128
+ const { scores, error: scoreError } = await getVisibleTopScoresForBeatmap(
129
+ beatmapHash,
130
+ limit
131
+ );
149
132
 
150
- if (isCacheable && beatmapHash) {
151
- await setCacheValue(cacheKey, scoreData);
152
- }
133
+ if (scoreError) {
134
+ return NextResponse.json({ error: scoreError });
153
135
  }
154
136
 
155
- const userIds = Array.from(
156
- new Set((scoreData || []).map((score) => score.userid).filter(Boolean))
157
- );
158
- const activeUserIds = await getActiveProfileIdSet(userIds);
159
- const visibleScores = (scoreData || [])
160
- .filter((score) => activeUserIds.has(score.userid))
161
- .slice(0, limit);
137
+ const mappedVetos = ((beatmapPage as any).vetos || []).map((veto: any) => ({
138
+ id: veto.id,
139
+ userId: veto.user,
140
+ username: veto.profiles?.username,
141
+ avatar_url: veto.profiles?.avatar_url,
142
+ veto_reason: veto.veto_reason,
143
+ created_at: veto.created_at,
144
+ }));
162
145
 
163
- const mappedVetos = ((beatmapPage as any).vetos || []).map((veto: any) => ({
164
- id: veto.id,
165
- userId: veto.user,
166
- username: veto.profiles?.username,
167
- avatar_url: veto.profiles?.avatar_url,
168
- veto_reason: veto.veto_reason,
169
- created_at: veto.created_at,
170
- }));
171
-
172
- return NextResponse.json({
173
- scores: visibleScores.map((score: any) => ({
174
- id: score.id,
175
- awarded_sp: score.awarded_sp,
176
- created_at: score.created_at,
177
- misses: score.misses,
178
- mods: score.mods,
179
- passed: score.passed,
180
- songId: score.songid,
181
- speed: score.speed,
182
- spin: score.spin,
183
- userId: score.userid,
184
- username: score.username,
185
- avatar_url: score.avatar_url,
186
- })),
146
+ return NextResponse.json({
147
+ scores,
187
148
  beatmap: {
188
149
  playcount: beatmapPage.beatmaps?.playcount,
189
150
  created_at: beatmapPage.created_at,
@@ -198,14 +159,14 @@ export async function handler(
198
159
  starRating: beatmapPage.beatmaps?.starRating,
199
160
  owner: beatmapPage.owner,
200
161
  ownerUsername: beatmapPage.profiles?.username,
201
- ownerAvatar: beatmapPage.profiles?.avatar_url,
202
- id: beatmapPage.id,
203
- status: beatmapPage.status,
204
- qualified: beatmapPage.qualified,
205
- qualifiedAt: beatmapPage.qualifiedAt,
206
- nominations: beatmapPage.nominations as number[],
207
- videoUrl: beatmapPage.video_url,
208
- vetos: mappedVetos.length > 0 ? mappedVetos : undefined,
209
- },
210
- });
211
- }
162
+ ownerAvatar: beatmapPage.profiles?.avatar_url,
163
+ id: beatmapPage.id,
164
+ status: beatmapPage.status,
165
+ qualified: beatmapPage.qualified,
166
+ qualifiedAt: beatmapPage.qualifiedAt,
167
+ nominations: beatmapPage.nominations as number[],
168
+ videoUrl: beatmapPage.video_url,
169
+ vetos: mappedVetos.length > 0 ? mappedVetos : undefined,
170
+ },
171
+ });
172
+ }
@@ -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 } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
package/api/getClan.ts CHANGED
@@ -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";
package/api/getClans.ts CHANGED
@@ -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 { 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 } 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 { 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 } 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 { supabase } from "../utils/supabase";
@@ -63,7 +63,7 @@ export async function handler({
63
63
  });
64
64
  }
65
65
 
66
- if (contentLength > 50000000) {
66
+ if (contentLength > 100000000) {
67
67
  return NextResponse.json({
68
68
  error: "Max content length exceeded.",
69
69
  });
@@ -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 { Database } from "../types/database";
4
4
  import { protectedApi, validUser } from "../utils/requestUtils";
package/api/getProfile.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { geolocation } from "@vercel/edge";
2
- import { NextResponse } from "next/server";
1
+ import { geolocation } from "../utils/requestGeo";
2
+ import { NextResponse } from "../utils/response";
3
3
  import z from "zod";
4
4
  import { Database } from "../types/database";
5
5
  import { protectedApi } from "../utils/requestUtils";
@@ -33,15 +33,16 @@ export const Schema = {
33
33
  verified: z.boolean().nullable(),
34
34
  verificationDeadline: z.number().nullable(),
35
35
  play_count: z.number().nullable(),
36
- skill_points: z.number().nullable(),
37
- squares_hit: z.number().nullable(),
38
- total_score: z.number().nullable(),
39
- position: z.number().nullable(),
40
- activity_status: z.enum(["active", "inactive"]),
41
- is_online: z.boolean(),
42
- clans: z
43
- .object({
44
- id: z.number(),
36
+ skill_points: z.number().nullable(),
37
+ squares_hit: z.number().nullable(),
38
+ total_score: z.number().nullable(),
39
+ position: z.number().nullable(),
40
+ country_position: z.number().nullable(),
41
+ activity_status: z.enum(["active", "inactive"]),
42
+ is_online: z.boolean(),
43
+ clans: z
44
+ .object({
45
+ id: z.number(),
45
46
  acronym: z.string(),
46
47
  })
47
48
  .optional()
@@ -131,19 +132,37 @@ export async function handler(
131
132
  if (activityData && activityData.last_activity) {
132
133
  isOnline = Date.now() - activityData.last_activity < 1800000;
133
134
  }
134
-
135
- let position: number | null = null;
136
- if (activityStatus === "active") {
137
- const cutoffIso = getScoreActivityCutoffIso();
138
- const { count: playersWithMorePoints } = await supabase
139
- .from("profiles")
140
- .select("id,scores!inner(id)", { count: "exact", head: true })
141
- .neq("ban", "excluded")
142
- .gte("scores.created_at", cutoffIso)
143
- .gt("skill_points", user.skill_points);
144
-
145
- position = (playersWithMorePoints || 0) + 1;
146
- }
135
+
136
+ let position: number | null = null;
137
+ let countryPosition: number | null = null;
138
+ if (activityStatus === "active") {
139
+ const cutoffIso = getScoreActivityCutoffIso();
140
+ const globalPositionQuery = supabase
141
+ .from("profiles")
142
+ .select("id,scores!inner(id)", { count: "exact", head: true })
143
+ .neq("ban", "excluded")
144
+ .gte("scores.created_at", cutoffIso)
145
+ .gt("skill_points", user.skill_points);
146
+
147
+ const countryPositionQuery = user.flag
148
+ ? supabase
149
+ .from("profiles")
150
+ .select("id,scores!inner(id)", { count: "exact", head: true })
151
+ .neq("ban", "excluded")
152
+ .eq("flag", user.flag)
153
+ .gte("scores.created_at", cutoffIso)
154
+ .gt("skill_points", user.skill_points)
155
+ : Promise.resolve({ count: null });
156
+
157
+ const [{ count: playersWithMorePoints }, { count: countryPlayersWithMorePoints }] =
158
+ await Promise.all([globalPositionQuery, countryPositionQuery]);
159
+
160
+ position = (playersWithMorePoints || 0) + 1;
161
+ countryPosition =
162
+ user.flag && countryPlayersWithMorePoints !== null
163
+ ? (countryPlayersWithMorePoints || 0) + 1
164
+ : null;
165
+ }
147
166
 
148
167
  if (user.verificationDeadline < Date.now()) {
149
168
  await supabase
@@ -157,10 +176,11 @@ export async function handler(
157
176
 
158
177
  return NextResponse.json({
159
178
  user: {
160
- ...user,
161
- position,
162
- activity_status: activityStatus,
163
- is_online: isOnline,
164
- },
165
- });
166
- }
179
+ ...user,
180
+ position,
181
+ country_position: countryPosition,
182
+ activity_status: activityStatus,
183
+ is_online: isOnline,
184
+ },
185
+ });
186
+ }