rhythia-api 229.0.0 → 231.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.
@@ -0,0 +1,83 @@
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+
6
+ const INTERNAL_SECRET = "testing-1";
7
+ const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
8
+
9
+ export const Schema = {
10
+ input: z.strictObject({
11
+ secret: z.string(),
12
+ }),
13
+ output: z.object({
14
+ error: z.string().optional(),
15
+ updated: z.number(),
16
+ }),
17
+ };
18
+
19
+ export async function POST(request: Request): Promise<NextResponse> {
20
+ return protectedApi({
21
+ request,
22
+ schema: Schema,
23
+ authorization: () => {},
24
+ activity: handler,
25
+ });
26
+ }
27
+
28
+ export async function handler({
29
+ secret,
30
+ }: (typeof Schema)["input"]["_type"]): Promise<
31
+ NextResponse<(typeof Schema)["output"]["_type"]>
32
+ > {
33
+ if (secret !== INTERNAL_SECRET) {
34
+ return NextResponse.json({ error: "Internal usage only", updated: 0 });
35
+ }
36
+
37
+ const { data: qualifiedMaps, error: qualifiedMapsError } = await supabase
38
+ .from("beatmapPages")
39
+ .select("id,status,qualifiedAt")
40
+ .eq("qualified", true)
41
+ .not("qualifiedAt", "is", null);
42
+
43
+ if (qualifiedMapsError) {
44
+ return NextResponse.json({ error: qualifiedMapsError.message, updated: 0 });
45
+ }
46
+
47
+ const cutoffTimestamp = Date.now() - THREE_DAYS_MS;
48
+ const mapsToRank = (qualifiedMaps || []).filter((mapData) => {
49
+ if (mapData.status === "RANKED") {
50
+ return false;
51
+ }
52
+
53
+ const qualifiedAt = Date.parse(mapData.qualifiedAt || "");
54
+ if (!Number.isFinite(qualifiedAt)) {
55
+ return false;
56
+ }
57
+
58
+ return qualifiedAt <= cutoffTimestamp;
59
+ });
60
+
61
+ if (mapsToRank.length === 0) {
62
+ return NextResponse.json({ updated: 0 });
63
+ }
64
+
65
+ const { error: updateError } = await supabase
66
+ .from("beatmapPages")
67
+ .update({
68
+ status: "RANKED",
69
+ ranked_at: Date.now(),
70
+ qualified: false,
71
+ qualifiedAt: null,
72
+ })
73
+ .in(
74
+ "id",
75
+ mapsToRank.map((mapData) => mapData.id)
76
+ );
77
+
78
+ if (updateError) {
79
+ return NextResponse.json({ error: updateError.message, updated: 0 });
80
+ }
81
+
82
+ return NextResponse.json({ updated: mapsToRank.length });
83
+ }
@@ -47,17 +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
- description: z.string().nullable().optional(),
55
- tags: z.string().nullable().optional(),
56
- videoUrl: z.string().nullable().optional(),
57
- })
58
- .optional(),
59
- }),
60
- };
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
+ };
61
75
 
62
76
  export async function POST(request: Request): Promise<NextResponse> {
63
77
  return protectedApi({
@@ -92,12 +106,23 @@ export async function handler(
92
106
  noteCount,
93
107
  title
94
108
  ),
95
- profiles (
96
- username,
97
- avatar_url
98
- )
99
- `
100
- )
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
+ )
101
126
  .eq("id", data.id)
102
127
  .single();
103
128
 
@@ -131,7 +156,16 @@ export async function handler(
131
156
  // }
132
157
  // }
133
158
 
134
- return NextResponse.json({
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({
135
169
  scores: [].map((score: any) => ({
136
170
  id: score.id,
137
171
  awarded_sp: score.awarded_sp,
@@ -162,13 +196,16 @@ export async function handler(
162
196
  starRating: beatmapPage.beatmaps?.starRating,
163
197
  owner: beatmapPage.owner,
164
198
  ownerUsername: beatmapPage.profiles?.username,
165
- ownerAvatar: beatmapPage.profiles?.avatar_url,
166
- id: beatmapPage.id,
167
- status: beatmapPage.status,
168
- nominations: beatmapPage.nominations as number[],
169
- description: beatmapPage.description,
170
- tags: beatmapPage.tags,
171
- videoUrl: beatmapPage.video_url,
172
- },
173
- });
174
- }
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
+ }
@@ -46,15 +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
- videoUrl: z.string().nullable(),
54
- })
55
- .optional(),
56
- }),
57
- };
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
+ };
58
72
 
59
73
  export async function POST(request: Request): Promise<NextResponse> {
60
74
  return protectedApi({
@@ -88,12 +102,23 @@ export async function handler(
88
102
  noteCount,
89
103
  title
90
104
  ),
91
- profiles (
92
- username,
93
- avatar_url
94
- )
95
- `
96
- )
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
+ )
97
122
  .eq("latestBeatmapHash", data.mapId)
98
123
  .single();
99
124
 
@@ -135,7 +160,16 @@ export async function handler(
135
160
  .filter((score) => activeUserIds.has(score.userid))
136
161
  .slice(0, limit);
137
162
 
138
- return NextResponse.json({
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({
139
173
  scores: visibleScores.map((score: any) => ({
140
174
  id: score.id,
141
175
  awarded_sp: score.awarded_sp,
@@ -164,11 +198,14 @@ export async function handler(
164
198
  starRating: beatmapPage.beatmaps?.starRating,
165
199
  owner: beatmapPage.owner,
166
200
  ownerUsername: beatmapPage.profiles?.username,
167
- ownerAvatar: beatmapPage.profiles?.avatar_url,
168
- id: beatmapPage.id,
169
- status: beatmapPage.status,
170
- nominations: beatmapPage.nominations as number[],
171
- videoUrl: beatmapPage.video_url,
172
- },
173
- });
174
- }
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
+ }
@@ -18,11 +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
- songId: z.string().nullable(),
25
- 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(),
26
27
  beatmapDifficulty: z.number().optional().nullable(),
27
28
  beatmapNotes: z.number().optional().nullable(),
28
29
  beatmapTitle: z.string().optional().nullable(),
@@ -37,11 +38,12 @@ export const Schema = {
37
38
  id: z.number(),
38
39
  awarded_sp: z.number().nullable(),
39
40
  created_at: z.string(),
40
- misses: z.number().nullable(),
41
- mods: z.record(z.unknown()),
42
- passed: z.boolean().nullable(),
43
- songId: z.string().nullable(),
44
- 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(),
45
47
  spin: z.boolean(),
46
48
  beatmapHash: z.string().nullable(),
47
49
  beatmapTitle: z.string().nullable(),
@@ -56,11 +58,12 @@ export const Schema = {
56
58
  awarded_sp: z.number().nullable(),
57
59
  beatmapHash: z.string().nullable(),
58
60
  created_at: z.string(),
59
- id: z.number(),
60
- misses: z.number().nullable(),
61
- passed: z.boolean().nullable(),
62
- rank: z.string().nullable(),
63
- 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(),
64
67
  userId: z.number().nullable(),
65
68
  beatmapDifficulty: z.number().optional().nullable(),
66
69
  beatmapNotes: z.number().optional().nullable(),
@@ -1,82 +1,86 @@
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
- export const Schema = {
9
- input: z.strictObject({
10
- session: z.string(),
11
- mapId: z.number(),
12
- }),
13
- output: z.object({
14
- error: z.string().optional(),
15
- }),
16
- };
17
-
18
- export async function POST(request: Request) {
19
- return protectedApi({
20
- request,
21
- schema: Schema,
22
- authorization: validUser,
23
- activity: handler,
24
- });
25
- }
26
-
27
- export async function handler(data: (typeof Schema)["input"]["_type"]) {
28
- const user = (await getUserBySession(data.session)) as User;
29
- let { data: queryUserData, error: userError } = await supabase
30
- .from("profiles")
31
- .select("*")
32
- .eq("uid", user.id)
33
- .single();
34
-
35
- if (!queryUserData) {
36
- return NextResponse.json({ error: "Can't find user" });
37
- }
38
-
39
- const tags = (queryUserData?.badges || []) as string[];
40
-
41
- if (!tags.includes("RCT")) {
42
- return NextResponse.json({ error: "Only RCTs can nominate maps!" });
43
- }
44
-
45
- const { data: mapData, error } = await supabase
46
- .from("beatmapPages")
47
- .select("id,nominations,owner")
48
- .eq("id", data.mapId)
49
- .single();
50
-
51
- if (!mapData) {
52
- return NextResponse.json({ error: "Bad map" });
53
- }
54
-
55
- if (mapData.owner == queryUserData.id) {
56
- return NextResponse.json({ error: "Can't nominate own map" });
57
- }
58
-
59
- if ((mapData.nominations as number[]).includes(queryUserData.id)) {
60
- return NextResponse.json({ error: "Already nominated" });
61
- }
62
-
63
- const newNominations = [
64
- ...(mapData.nominations! as number[]),
65
- queryUserData.id,
66
- ];
67
- if (newNominations.length == 2) {
68
- await supabase.from("beatmapPages").upsert({
69
- id: data.mapId,
70
- nominations: newNominations,
71
- status: "RANKED",
72
- ranked_at: Date.now(),
73
- });
74
- } else {
75
- await supabase.from("beatmapPages").upsert({
76
- id: data.mapId,
77
- nominations: newNominations,
78
- });
79
- }
80
-
81
- return NextResponse.json({});
82
- }
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
+ }