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 } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
@@ -69,11 +69,11 @@ export async function POST(request: Request) {
69
69
  export async function handler(data: (typeof Schema)["input"]["_type"]) {
70
70
  const countProfilesQuery = await supabase
71
71
  .from("profiles")
72
- .select("*", { count: "exact", head: true });
72
+ .select("*", { count: "estimated", head: true });
73
73
 
74
74
  const countBeatmapsQuery = await supabase
75
75
  .from("beatmaps")
76
- .select("*", { count: "exact", head: true });
76
+ .select("*", { count: "estimated", head: true });
77
77
 
78
78
  let { data: beatmapPage, error: errorlast } = await supabase
79
79
  .from("beatmapPages")
@@ -128,12 +128,12 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
128
128
 
129
129
  const countScoresQuery = await supabase
130
130
  .from("scores")
131
- .select("id", { count: "exact", head: true });
131
+ .select("id", { count: "estimated", head: true });
132
132
 
133
133
  // 30 minutes activity
134
134
  const countOnline = await supabase
135
135
  .from("profileActivities")
136
- .select("*", { count: "exact", head: true })
136
+ .select("*", { count: "estimated", head: true })
137
137
  .gt("last_activity", Date.now() - 1800000);
138
138
 
139
139
  const countChart = await 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 { rateMapNotes } from "../utils/star-calc";
package/api/getScore.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 } 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
 
@@ -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 { getCacheValue, setCacheValue } from "../utils/cache";
4
4
  import { protectedApi } from "../utils/requestUtils";
@@ -18,12 +18,12 @@ export const Schema = {
18
18
  awarded_sp: z.number().nullable(),
19
19
  beatmapHash: z.string().nullable(),
20
20
  created_at: z.string(),
21
- id: z.number(),
22
- misses: z.number().nullable(),
23
- passed: z.boolean().nullable(),
24
- replay_url: z.string().nullable().optional(),
25
- songId: z.string().nullable(),
26
- userId: z.number().nullable(),
21
+ id: z.number(),
22
+ misses: z.number().nullable(),
23
+ passed: z.boolean().nullable(),
24
+ replay_url: z.string().nullable().optional(),
25
+ songId: z.string().nullable(),
26
+ userId: z.number().nullable(),
27
27
  beatmapDifficulty: z.number().optional().nullable(),
28
28
  beatmapNotes: z.number().optional().nullable(),
29
29
  beatmapTitle: z.string().optional().nullable(),
@@ -38,12 +38,12 @@ export const Schema = {
38
38
  id: z.number(),
39
39
  awarded_sp: z.number().nullable(),
40
40
  created_at: z.string(),
41
- misses: z.number().nullable(),
42
- mods: z.record(z.unknown()),
43
- passed: z.boolean().nullable(),
44
- replay_url: z.string().nullable().optional(),
45
- songId: z.string().nullable(),
46
- speed: z.number().nullable(),
41
+ misses: z.number().nullable(),
42
+ mods: z.record(z.unknown()),
43
+ passed: z.boolean().nullable(),
44
+ replay_url: z.string().nullable().optional(),
45
+ songId: z.string().nullable(),
46
+ speed: z.number().nullable(),
47
47
  spin: z.boolean(),
48
48
  beatmapHash: z.string().nullable(),
49
49
  beatmapTitle: z.string().nullable(),
@@ -58,12 +58,12 @@ export const Schema = {
58
58
  awarded_sp: z.number().nullable(),
59
59
  beatmapHash: z.string().nullable(),
60
60
  created_at: z.string(),
61
- id: z.number(),
62
- misses: z.number().nullable(),
63
- passed: z.boolean().nullable(),
64
- replay_url: z.string().nullable().optional(),
65
- rank: z.string().nullable(),
66
- songId: z.string().nullable(),
61
+ id: z.number(),
62
+ misses: z.number().nullable(),
63
+ passed: z.boolean().nullable(),
64
+ replay_url: z.string().nullable().optional(),
65
+ rank: z.string().nullable(),
66
+ songId: z.string().nullable(),
67
67
  userId: z.number().nullable(),
68
68
  beatmapDifficulty: z.number().optional().nullable(),
69
69
  beatmapNotes: z.number().optional().nullable(),
@@ -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, 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, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
package/api/qualifyMap.ts CHANGED
@@ -1,86 +1,97 @@
1
- import { NextResponse } from "next/server";
2
- import z from "zod";
3
- import { protectedApi, validUser } from "../utils/requestUtils";
4
- import { supabase } from "../utils/supabase";
5
- import { getUserBySession } from "../utils/getUserBySession";
6
- import { User } from "@supabase/supabase-js";
7
-
8
- const QUALIFY_BADGES = ["Team Ranked"];
9
-
10
- export const Schema = {
11
- input: z.strictObject({
12
- session: z.string(),
13
- mapId: z.number(),
14
- }),
15
- output: z.object({
16
- error: z.string().optional(),
17
- qualifiedAt: z.string().optional(),
18
- }),
19
- };
20
-
21
- export async function POST(request: Request) {
22
- return protectedApi({
23
- request,
24
- schema: Schema,
25
- authorization: validUser,
26
- activity: handler,
27
- });
28
- }
29
-
30
- export async function handler(data: (typeof Schema)["input"]["_type"]) {
31
- const user = (await getUserBySession(data.session)) as User;
32
- const { data: queryUserData } = await supabase
33
- .from("profiles")
34
- .select("*")
35
- .eq("uid", user.id)
36
- .single();
37
-
38
- if (!queryUserData) {
39
- return NextResponse.json({ error: "Can't find user" });
40
- }
41
-
42
- const tags = (queryUserData?.badges || []) as string[];
43
- const hasQualifyAccess = QUALIFY_BADGES.some((badge) => tags.includes(badge));
44
-
45
- if (!hasQualifyAccess) {
46
- return NextResponse.json({ error: "Only management can qualify maps!" });
47
- }
48
-
49
- const { data: mapData } = await supabase
50
- .from("beatmapPages")
51
- .select("id,owner,status,qualified,qualifiedAt")
52
- .eq("id", data.mapId)
53
- .single();
54
-
55
- if (!mapData) {
56
- return NextResponse.json({ error: "Bad map" });
57
- }
58
-
59
- if (mapData.owner === queryUserData.id) {
60
- return NextResponse.json({ error: "Can't qualify own map" });
61
- }
62
-
63
- if (mapData.status === "RANKED") {
64
- return NextResponse.json({ error: "Map is already ranked" });
65
- }
66
-
67
- if (mapData.qualified) {
68
- return NextResponse.json({
69
- error: "Map is already qualified",
70
- qualifiedAt: mapData.qualifiedAt || undefined,
71
- });
72
- }
73
-
74
- const qualifiedAt = new Date().toISOString();
75
- const { error: updateError } = await supabase.from("beatmapPages").upsert({
76
- id: data.mapId,
77
- qualified: true,
78
- qualifiedAt,
79
- });
80
-
81
- if (updateError) {
82
- return NextResponse.json({ error: updateError.message });
83
- }
84
-
85
- return NextResponse.json({ qualifiedAt });
86
- }
1
+ import { NextResponse } from "../utils/response";
2
+ import z from "zod";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+ import { getUserBySession } from "../utils/getUserBySession";
6
+ import { User } from "@supabase/supabase-js";
7
+ import { postMapLifecycleWebhook } from "../utils/mapLifecycleWebhook";
8
+
9
+ const QUALIFY_BADGES = ["Team Ranked"];
10
+
11
+ export const Schema = {
12
+ input: z.strictObject({
13
+ session: z.string(),
14
+ mapId: z.number(),
15
+ }),
16
+ output: z.object({
17
+ error: z.string().optional(),
18
+ qualifiedAt: z.string().optional(),
19
+ }),
20
+ };
21
+
22
+ export async function POST(request: Request) {
23
+ return protectedApi({
24
+ request,
25
+ schema: Schema,
26
+ authorization: validUser,
27
+ activity: handler,
28
+ });
29
+ }
30
+
31
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
32
+ const user = (await getUserBySession(data.session)) as User;
33
+ const { data: queryUserData } = await supabase
34
+ .from("profiles")
35
+ .select("*")
36
+ .eq("uid", user.id)
37
+ .single();
38
+
39
+ if (!queryUserData) {
40
+ return NextResponse.json({ error: "Can't find user" });
41
+ }
42
+
43
+ const tags = (queryUserData?.badges || []) as string[];
44
+ const hasQualifyAccess = QUALIFY_BADGES.some((badge) => tags.includes(badge));
45
+
46
+ if (!hasQualifyAccess) {
47
+ return NextResponse.json({ error: "Only management can qualify maps!" });
48
+ }
49
+
50
+ const { data: mapData } = await supabase
51
+ .from("beatmapPages")
52
+ .select("id,owner,status,qualified,qualifiedAt")
53
+ .eq("id", data.mapId)
54
+ .single();
55
+
56
+ if (!mapData) {
57
+ return NextResponse.json({ error: "Bad map" });
58
+ }
59
+
60
+ if (mapData.owner === queryUserData.id) {
61
+ return NextResponse.json({ error: "Can't qualify own map" });
62
+ }
63
+
64
+ if (mapData.status === "RANKED") {
65
+ return NextResponse.json({ error: "Map is already ranked" });
66
+ }
67
+
68
+ if (mapData.qualified) {
69
+ return NextResponse.json({
70
+ error: "Map is already qualified",
71
+ qualifiedAt: mapData.qualifiedAt || undefined,
72
+ });
73
+ }
74
+
75
+ const qualifiedAt = new Date().toISOString();
76
+ const { error: updateError } = await supabase.from("beatmapPages").upsert({
77
+ id: data.mapId,
78
+ qualified: true,
79
+ qualifiedAt,
80
+ });
81
+
82
+ if (updateError) {
83
+ return NextResponse.json({ error: updateError.message });
84
+ }
85
+
86
+ await postMapLifecycleWebhook({
87
+ mapId: data.mapId,
88
+ event: "qualified",
89
+ candidateQualifierUsername: tags.includes("Candidate")
90
+ ? queryUserData.username ||
91
+ queryUserData.computedUsername ||
92
+ `User #${queryUserData.id}`
93
+ : undefined,
94
+ });
95
+
96
+ return NextResponse.json({ qualifiedAt });
97
+ }
@@ -1,9 +1,10 @@
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";
5
5
  import { getUserBySession } from "../utils/getUserBySession";
6
6
  import { User } from "@supabase/supabase-js";
7
+ import { postMapLifecycleWebhook } from "../utils/mapLifecycleWebhook";
7
8
 
8
9
  export const Schema = {
9
10
  input: z.strictObject({
@@ -64,6 +65,12 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
64
65
  nominations: [queryUserData.id, queryUserData.id],
65
66
  status: "RANKED",
66
67
  ranked_at: Date.now(),
68
+ qualified: false,
69
+ qualifiedAt: null,
70
+ });
71
+ await postMapLifecycleWebhook({
72
+ mapId: element.id,
73
+ event: "ranked",
67
74
  });
68
75
  }
69
76
 
@@ -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/setPasskey.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 { Database } from "../types/database";
4
4
  import { protectedApi, validUser } from "../utils/requestUtils";
@@ -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";
@@ -303,11 +303,6 @@ export async function handler({
303
303
  console.log("p3");
304
304
 
305
305
  await invalidateCachePrefix(`userscore:${userData.id}`);
306
- const beatmapIsRanked =
307
- beatmapPages?.status === "RANKED" || beatmapPages?.status === "APPROVED";
308
- if (beatmapIsRanked) {
309
- await invalidateCachePrefix(`beatmap-scores:${data.mapHash}`);
310
- }
311
306
 
312
307
  // Grant special badges if applicable
313
308
  if (passed && beatmapPages && !data.mods.includes("mod_nofail")) {