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,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";
@@ -23,11 +23,11 @@ export const Schema = {
23
23
  viewPerPage: z.number(),
24
24
  currentPage: z.number(),
25
25
  beatmaps: z
26
- .array(
27
- z.object({
28
- id: z.number(),
29
- playcount: z.number().nullable().optional(),
30
- created_at: z.string().nullable().optional(),
26
+ .array(
27
+ z.object({
28
+ id: z.number(),
29
+ playcount: z.number().nullable().optional(),
30
+ created_at: z.string().nullable().optional(),
31
31
  difficulty: z.number().nullable().optional(),
32
32
  noteCount: z.number().nullable().optional(),
33
33
  length: z.number().nullable().optional(),
@@ -36,15 +36,16 @@ export const Schema = {
36
36
  beatmapFile: z.string().nullable().optional(),
37
37
  image: z.string().nullable().optional(),
38
38
  starRating: z.number().nullable().optional(),
39
- owner: z.number().nullable().optional(),
40
- ownerUsername: z.string().nullable().optional(),
41
- ownerAvatar: z.string().nullable().optional(),
42
- status: z.string().nullable().optional(),
43
- tags: z.string().nullable().optional(),
44
- videoUrl: z.string().nullable().optional(),
45
- })
46
- )
47
- .optional(),
39
+ owner: z.number().nullable().optional(),
40
+ ownerUsername: z.string().nullable().optional(),
41
+ ownerAvatar: z.string().nullable().optional(),
42
+ status: z.string().nullable().optional(),
43
+ qualified: z.boolean().nullable().optional(),
44
+ tags: z.string().nullable().optional(),
45
+ videoUrl: z.string().nullable().optional(),
46
+ })
47
+ )
48
+ .optional(),
48
49
  }),
49
50
  };
50
51
 
@@ -66,24 +67,39 @@ export async function handler(
66
67
 
67
68
  const VIEW_PER_PAGE = 50;
68
69
 
69
- export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
70
- const startPage = (data.page - 1) * VIEW_PER_PAGE;
71
- const endPage = startPage + VIEW_PER_PAGE - 1;
72
- const countQuery = await supabase
73
- .from("beatmapPages")
74
- .select("id", { count: "exact", head: true });
75
-
76
- let qry = supabase.from("beatmapPages").select(
77
- `
78
- owner,
79
- created_at,
80
- id,
81
- status,
82
- tags,
83
- ranked_at,
84
- video_url,
85
- beatmaps!inner(
86
- playcount,
70
+ export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
71
+ const startPage = (data.page - 1) * VIEW_PER_PAGE;
72
+ const endPage = startPage + VIEW_PER_PAGE - 1;
73
+ const statusFilter = data.status?.toUpperCase();
74
+ let countQry = supabase
75
+ .from("beatmapPages")
76
+ .select(
77
+ `
78
+ id,
79
+ beatmaps!inner(
80
+ title,
81
+ starRating,
82
+ length
83
+ ),
84
+ profiles!inner(
85
+ username
86
+ )
87
+ `,
88
+ { count: "exact", head: true }
89
+ );
90
+
91
+ let qry = supabase.from("beatmapPages").select(
92
+ `
93
+ owner,
94
+ created_at,
95
+ id,
96
+ status,
97
+ qualified,
98
+ tags,
99
+ ranked_at,
100
+ video_url,
101
+ beatmaps!inner(
102
+ playcount,
87
103
  ranked,
88
104
  beatmapFile,
89
105
  image,
@@ -92,58 +108,71 @@ export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
92
108
  length,
93
109
  title
94
110
  ),
95
- profiles!inner(
96
- username
97
- )`
98
- );
99
-
100
- if (data.status == "RANKED") {
101
- qry = qry.order("ranked_at", { ascending: false });
102
- } else {
103
- qry = qry.order("created_at", { ascending: false });
104
- }
105
-
106
- if (data.textFilter) {
107
- qry = qry.ilike("beatmaps.title", `%${data.textFilter}%`);
108
- }
109
-
110
- if (data.authorFilter) {
111
- qry = qry.ilike("profiles.username", `%${data.authorFilter}%`);
112
- }
113
-
114
- if (data.tagsFilter) {
115
- qry = qry.ilike("tags", `%${data.tagsFilter}%`);
116
- }
117
-
118
- if (data.minStars) {
119
- qry = qry.gt("beatmaps.starRating", data.minStars);
120
- }
121
-
122
- if (data.maxStars) {
123
- qry = qry.lt("beatmaps.starRating", data.maxStars);
124
- }
125
-
126
- if (data.minLength) {
127
- qry = qry.gt("beatmaps.length", data.minLength);
128
- }
129
-
130
- if (data.maxLength) {
131
- qry = qry.lt("beatmaps.length", data.maxLength);
132
- }
133
-
134
- if (data.status) {
135
- qry = qry.eq("status", data.status);
136
- }
137
-
138
- if (data.creator !== undefined) {
139
- qry = qry.eq("owner", data.creator);
140
- }
141
-
142
- let queryData = await qry.range(startPage, endPage);
143
-
144
- return {
145
- total: countQuery.count || 0,
146
- viewPerPage: VIEW_PER_PAGE,
111
+ profiles!inner(
112
+ username
113
+ )`
114
+ );
115
+
116
+ if (statusFilter === "RANKED") {
117
+ qry = qry.order("ranked_at", { ascending: false });
118
+ } else {
119
+ qry = qry.order("created_at", { ascending: false });
120
+ }
121
+
122
+ if (data.textFilter) {
123
+ qry = qry.ilike("beatmaps.title", `%${data.textFilter}%`);
124
+ countQry = countQry.ilike("beatmaps.title", `%${data.textFilter}%`);
125
+ }
126
+
127
+ if (data.authorFilter) {
128
+ qry = qry.ilike("profiles.username", `%${data.authorFilter}%`);
129
+ countQry = countQry.ilike("profiles.username", `%${data.authorFilter}%`);
130
+ }
131
+
132
+ if (data.tagsFilter) {
133
+ qry = qry.ilike("tags", `%${data.tagsFilter}%`);
134
+ countQry = countQry.ilike("tags", `%${data.tagsFilter}%`);
135
+ }
136
+
137
+ if (data.minStars) {
138
+ qry = qry.gt("beatmaps.starRating", data.minStars);
139
+ countQry = countQry.gt("beatmaps.starRating", data.minStars);
140
+ }
141
+
142
+ if (data.maxStars) {
143
+ qry = qry.lt("beatmaps.starRating", data.maxStars);
144
+ countQry = countQry.lt("beatmaps.starRating", data.maxStars);
145
+ }
146
+
147
+ if (data.minLength) {
148
+ qry = qry.gt("beatmaps.length", data.minLength);
149
+ countQry = countQry.gt("beatmaps.length", data.minLength);
150
+ }
151
+
152
+ if (data.maxLength) {
153
+ qry = qry.lt("beatmaps.length", data.maxLength);
154
+ countQry = countQry.lt("beatmaps.length", data.maxLength);
155
+ }
156
+
157
+ if (statusFilter === "QUALIFIED") {
158
+ qry = qry.eq("qualified", true);
159
+ countQry = countQry.eq("qualified", true);
160
+ } else if (data.status) {
161
+ qry = qry.eq("status", data.status);
162
+ countQry = countQry.eq("status", data.status);
163
+ }
164
+
165
+ if (data.creator !== undefined) {
166
+ qry = qry.eq("owner", data.creator);
167
+ countQry = countQry.eq("owner", data.creator);
168
+ }
169
+
170
+ const countQuery = await countQry;
171
+ let queryData = await qry.range(startPage, endPage);
172
+
173
+ return {
174
+ total: countQuery.count || 0,
175
+ viewPerPage: VIEW_PER_PAGE,
147
176
  currentPage: data.page,
148
177
  beatmaps: queryData.data?.map((beatmapPage) => ({
149
178
  id: beatmapPage.id,
@@ -156,11 +185,12 @@ export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
156
185
  length: beatmapPage.beatmaps?.length,
157
186
  beatmapFile: beatmapPage.beatmaps?.beatmapFile,
158
187
  image: beatmapPage.beatmaps?.image,
159
- starRating: beatmapPage.beatmaps?.starRating,
160
- owner: beatmapPage.owner,
161
- status: beatmapPage.status,
162
- ownerUsername: beatmapPage.profiles?.username,
163
- videoUrl: beatmapPage.video_url,
164
- })),
165
- };
166
- }
188
+ starRating: beatmapPage.beatmaps?.starRating,
189
+ owner: beatmapPage.owner,
190
+ status: beatmapPage.status,
191
+ qualified: beatmapPage.qualified,
192
+ ownerUsername: beatmapPage.profiles?.username,
193
+ videoUrl: beatmapPage.video_url,
194
+ })),
195
+ };
196
+ }
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
+ }