rhythia-api 230.0.0 → 233.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.
- package/api/checkQualified.ts +93 -0
- package/api/enhancedSearch.ts +113 -0
- package/api/getBeatmapPage.ts +65 -28
- package/api/getBeatmapPageById.ts +61 -24
- package/api/{approveMap.ts → qualifyMap.ts} +92 -78
- package/api/rankMapsArchive.ts +19 -12
- package/api/submitScoreInternal.ts +449 -426
- package/api/vetoMap.ts +101 -0
- package/index.ts +157 -70
- package/package.json +4 -3
- package/queries/enhanced_search.sql +217 -0
- package/queries/get_user_scores_lastday.sql +17 -6
- package/queries/get_user_scores_top_and_stats.sql +59 -59
- package/types/database.ts +1248 -1179
- package/utils/getUserBySession.ts +1 -1
- package/utils/mapLifecycleWebhook.ts +277 -0
- package/utils/requestUtils.ts +127 -88
- package/api/nominateMap.ts +0 -82
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
import { postMapLifecycleWebhook } from "../utils/mapLifecycleWebhook";
|
|
6
|
+
|
|
7
|
+
const INTERNAL_SECRET = "testing-1";
|
|
8
|
+
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export const Schema = {
|
|
11
|
+
input: z.strictObject({
|
|
12
|
+
secret: z.string(),
|
|
13
|
+
}),
|
|
14
|
+
output: z.object({
|
|
15
|
+
error: z.string().optional(),
|
|
16
|
+
updated: z.number(),
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
21
|
+
return protectedApi({
|
|
22
|
+
request,
|
|
23
|
+
schema: Schema,
|
|
24
|
+
authorization: () => {},
|
|
25
|
+
activity: handler,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handler({
|
|
30
|
+
secret,
|
|
31
|
+
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
32
|
+
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
33
|
+
> {
|
|
34
|
+
if (secret !== INTERNAL_SECRET) {
|
|
35
|
+
return NextResponse.json({ error: "Internal usage only", updated: 0 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { data: qualifiedMaps, error: qualifiedMapsError } = await supabase
|
|
39
|
+
.from("beatmapPages")
|
|
40
|
+
.select("id,status,qualifiedAt")
|
|
41
|
+
.eq("qualified", true)
|
|
42
|
+
.not("qualifiedAt", "is", null);
|
|
43
|
+
|
|
44
|
+
if (qualifiedMapsError) {
|
|
45
|
+
return NextResponse.json({ error: qualifiedMapsError.message, updated: 0 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cutoffTimestamp = Date.now() - THREE_DAYS_MS;
|
|
49
|
+
const mapsToRank = (qualifiedMaps || []).filter((mapData) => {
|
|
50
|
+
if (mapData.status === "RANKED") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const qualifiedAt = Date.parse(mapData.qualifiedAt || "");
|
|
55
|
+
if (!Number.isFinite(qualifiedAt)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return qualifiedAt <= cutoffTimestamp;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (mapsToRank.length === 0) {
|
|
63
|
+
return NextResponse.json({ updated: 0 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { error: updateError } = await supabase
|
|
67
|
+
.from("beatmapPages")
|
|
68
|
+
.update({
|
|
69
|
+
status: "RANKED",
|
|
70
|
+
ranked_at: Date.now(),
|
|
71
|
+
qualified: false,
|
|
72
|
+
qualifiedAt: null,
|
|
73
|
+
})
|
|
74
|
+
.in(
|
|
75
|
+
"id",
|
|
76
|
+
mapsToRank.map((mapData) => mapData.id)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (updateError) {
|
|
80
|
+
return NextResponse.json({ error: updateError.message, updated: 0 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await Promise.allSettled(
|
|
84
|
+
mapsToRank.map((mapData) =>
|
|
85
|
+
postMapLifecycleWebhook({
|
|
86
|
+
mapId: mapData.id,
|
|
87
|
+
event: "ranked",
|
|
88
|
+
})
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return NextResponse.json({ updated: mapsToRank.length });
|
|
93
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
package/api/getBeatmapPage.ts
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
+
}
|
|
@@ -1,78 +1,92 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
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
|
+
});
|
|
90
|
+
|
|
91
|
+
return NextResponse.json({ qualifiedAt });
|
|
92
|
+
}
|
package/api/rankMapsArchive.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
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";
|
|
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";
|
|
7
8
|
|
|
8
9
|
export const Schema = {
|
|
9
10
|
input: z.strictObject({
|
|
@@ -58,14 +59,20 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
|
58
59
|
return NextResponse.json({ error: "Bad map" });
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
for (const element of mapData) {
|
|
62
|
-
await supabase.from("beatmapPages").upsert({
|
|
63
|
-
id: element.id,
|
|
64
|
-
nominations: [queryUserData.id, queryUserData.id],
|
|
65
|
-
status: "RANKED",
|
|
66
|
-
ranked_at: Date.now(),
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
for (const element of mapData) {
|
|
63
|
+
await supabase.from("beatmapPages").upsert({
|
|
64
|
+
id: element.id,
|
|
65
|
+
nominations: [queryUserData.id, queryUserData.id],
|
|
66
|
+
status: "RANKED",
|
|
67
|
+
ranked_at: Date.now(),
|
|
68
|
+
qualified: false,
|
|
69
|
+
qualifiedAt: null,
|
|
70
|
+
});
|
|
71
|
+
await postMapLifecycleWebhook({
|
|
72
|
+
mapId: element.id,
|
|
73
|
+
event: "ranked",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
69
76
|
|
|
70
77
|
return NextResponse.json({});
|
|
71
78
|
}
|