rhythia-api 215.0.0 → 217.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/deleteBeatmapPage.ts +26 -19
- package/api/executeAdminOperation.ts +113 -77
- package/api/getBeatmapComments.ts +32 -18
- package/api/getBeatmapPage.ts +67 -44
- package/api/getBeatmapPageById.ts +63 -40
- package/api/getBeatmaps.ts +3 -0
- package/api/getOnlinePlayers.ts +78 -0
- package/api/getUserScores.ts +59 -30
- package/api/postBeatmapComment.ts +23 -19
- package/api/submitScore.ts +47 -17
- package/api/updateBeatmapPage.ts +12 -10
- package/index.ts +41 -18
- package/package.json +4 -2
- package/types/database.ts +149 -88
- package/utils/cache.ts +60 -0
|
@@ -0,0 +1,78 @@
|
|
|
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 ONLINE_WINDOW_MS = 30 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
export const Schema = {
|
|
9
|
+
input: z.strictObject({}),
|
|
10
|
+
output: z.object({
|
|
11
|
+
error: z.string().optional(),
|
|
12
|
+
players: z.array(
|
|
13
|
+
z.object({
|
|
14
|
+
id: z.number(),
|
|
15
|
+
name: z.string().nullable(),
|
|
16
|
+
profilePictureUrl: z.string().nullable(),
|
|
17
|
+
})
|
|
18
|
+
),
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
23
|
+
return protectedApi({
|
|
24
|
+
request,
|
|
25
|
+
schema: Schema,
|
|
26
|
+
authorization: () => {},
|
|
27
|
+
activity: handler,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function handler(
|
|
32
|
+
_data: (typeof Schema)["input"]["_type"]
|
|
33
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
34
|
+
const cutoff = Date.now() - ONLINE_WINDOW_MS;
|
|
35
|
+
|
|
36
|
+
const { data: activityRows, error: activityError } = await supabase
|
|
37
|
+
.from("profileActivities")
|
|
38
|
+
.select("uid")
|
|
39
|
+
.gt("last_activity", cutoff);
|
|
40
|
+
|
|
41
|
+
if (activityError) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ players: [], error: "Failed to load online players" },
|
|
44
|
+
{ status: 500 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const onlineUids = Array.from(
|
|
49
|
+
new Set((activityRows || []).map((row) => row.uid).filter(Boolean))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!onlineUids.length) {
|
|
53
|
+
return NextResponse.json({ players: [] });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { data: profiles, error: profilesError } = await supabase
|
|
57
|
+
.from("profiles")
|
|
58
|
+
.select("id,username,avatar_url,profile_image,skill_points")
|
|
59
|
+
.in("uid", onlineUids)
|
|
60
|
+
.neq("ban", "excluded")
|
|
61
|
+
.order("skill_points", { ascending: false });
|
|
62
|
+
|
|
63
|
+
if (profilesError) {
|
|
64
|
+
return NextResponse.json(
|
|
65
|
+
{ players: [], error: "Failed to load online players" },
|
|
66
|
+
{ status: 500 }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const players =
|
|
71
|
+
profiles?.map((profile) => ({
|
|
72
|
+
id: profile.id,
|
|
73
|
+
name: profile.username,
|
|
74
|
+
profilePictureUrl: profile.profile_image || profile.avatar_url,
|
|
75
|
+
})) || [];
|
|
76
|
+
|
|
77
|
+
return NextResponse.json({ players });
|
|
78
|
+
}
|
package/api/getUserScores.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { getCacheValue, setCacheValue } from "../utils/cache";
|
|
4
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
5
|
+
import { supabase } from "../utils/supabase";
|
|
5
6
|
|
|
6
7
|
export const Schema = {
|
|
7
8
|
input: z.strictObject({
|
|
@@ -87,33 +88,61 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
export async function handler(
|
|
91
|
-
data: (typeof Schema)["input"]["_type"],
|
|
92
|
-
req: Request
|
|
93
|
-
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
94
|
-
|
|
95
|
-
const {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
export async function handler(
|
|
92
|
+
data: (typeof Schema)["input"]["_type"],
|
|
93
|
+
req: Request
|
|
94
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
95
|
+
const limit = data.limit ?? 10;
|
|
96
|
+
const cacheKey = `userscore:${data.id}:limit=${limit}`;
|
|
97
|
+
const cachedValue = await getCacheValue<(typeof Schema)["output"]["_type"]>(
|
|
98
|
+
cacheKey
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (cachedValue !== null) {
|
|
102
|
+
return NextResponse.json(cachedValue);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// parallel RPCs
|
|
106
|
+
const [
|
|
107
|
+
{ data: lastDay, error: err1 },
|
|
108
|
+
{ data: topAndStats, error: err2 },
|
|
109
|
+
{ data: reign, error: err3 },
|
|
110
|
+
] = await Promise.all([
|
|
111
|
+
supabase.rpc("get_user_scores_lastday", {
|
|
112
|
+
userid: data.id,
|
|
113
|
+
limit_param: limit,
|
|
114
|
+
}),
|
|
115
|
+
supabase.rpc("get_user_scores_top_and_stats", {
|
|
116
|
+
userid: data.id,
|
|
117
|
+
limit_param: limit,
|
|
118
|
+
}),
|
|
119
|
+
supabase.rpc("get_user_scores_reign", {
|
|
120
|
+
userid: data.id,
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
const err = err1 || err2 || err3;
|
|
125
|
+
if (err) {
|
|
126
|
+
return NextResponse.json({ error: JSON.stringify(err) });
|
|
105
127
|
}
|
|
106
128
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// The RPC may return { error: "..." } as a JSON object
|
|
112
|
-
if (typeof result === "object" && result !== null && "error" in result) {
|
|
113
|
-
return NextResponse.json({ error: String((result as any).error) });
|
|
114
|
-
}
|
|
129
|
+
// Extract pieces from the { top, stats } object
|
|
130
|
+
let top: unknown[] | undefined;
|
|
131
|
+
let stats: { totalScores: number; spinScores: number } | undefined;
|
|
115
132
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
133
|
+
if (topAndStats && typeof topAndStats === "object") {
|
|
134
|
+
top = (topAndStats as any).top ?? [];
|
|
135
|
+
stats = (topAndStats as any).stats;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const responseBody = {
|
|
139
|
+
lastDay: (lastDay as any) ?? [],
|
|
140
|
+
top: (top as any) ?? [],
|
|
141
|
+
stats,
|
|
142
|
+
reign: (reign as any) ?? [],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await setCacheValue(cacheKey, responseBody);
|
|
146
|
+
|
|
147
|
+
return NextResponse.json(responseBody);
|
|
148
|
+
}
|
|
@@ -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 { User } from "@supabase/supabase-js";
|
|
6
|
-
import { getUserBySession } from "../utils/getUserBySession";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
import { User } from "@supabase/supabase-js";
|
|
6
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
7
|
+
import { invalidateCache } from "../utils/cache";
|
|
7
8
|
|
|
8
9
|
export const Schema = {
|
|
9
10
|
input: z.strictObject({
|
|
@@ -45,18 +46,21 @@ export async function handler({
|
|
|
45
46
|
if (content.length > 256)
|
|
46
47
|
return NextResponse.json({ error: "Comment exceeds length." });
|
|
47
48
|
|
|
48
|
-
const upserted = await supabase
|
|
49
|
-
.from("beatmapPageComments")
|
|
50
|
-
.upsert({
|
|
51
|
-
beatmapPage: page,
|
|
52
|
-
owner: userData.id,
|
|
53
|
-
content,
|
|
54
|
-
})
|
|
55
|
-
.select("*")
|
|
56
|
-
.single();
|
|
57
|
-
|
|
58
|
-
if (upserted.error?.message.length) {
|
|
59
|
-
return NextResponse.json({ error: upserted.error.message });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
}
|
|
49
|
+
const upserted = await supabase
|
|
50
|
+
.from("beatmapPageComments")
|
|
51
|
+
.upsert({
|
|
52
|
+
beatmapPage: page,
|
|
53
|
+
owner: userData.id,
|
|
54
|
+
content,
|
|
55
|
+
})
|
|
56
|
+
.select("*")
|
|
57
|
+
.single();
|
|
58
|
+
|
|
59
|
+
if (upserted.error?.message.length) {
|
|
60
|
+
return NextResponse.json({ error: upserted.error.message });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await invalidateCache(`beatmap-comments:${page}`);
|
|
64
|
+
|
|
65
|
+
return NextResponse.json({});
|
|
66
|
+
}
|
package/api/submitScore.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
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 { decryptString } from "../utils/security";
|
|
6
|
-
import { isEqual } from "lodash";
|
|
7
|
-
import { getUserBySession } from "../utils/getUserBySession";
|
|
8
|
-
import { User } from "@supabase/supabase-js";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
import { supabase } from "../utils/supabase";
|
|
5
|
+
import { decryptString } from "../utils/security";
|
|
6
|
+
import { isEqual } from "lodash";
|
|
7
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
8
|
+
import { User } from "@supabase/supabase-js";
|
|
9
|
+
import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
|
|
9
10
|
|
|
10
11
|
export const Schema = {
|
|
11
12
|
input: z.strictObject({
|
|
@@ -214,6 +215,27 @@ export async function handler({
|
|
|
214
215
|
|
|
215
216
|
console.log("p1");
|
|
216
217
|
|
|
218
|
+
// auto-exclude: if a newly-created account (>600 RP) submits a score
|
|
219
|
+
try {
|
|
220
|
+
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
221
|
+
if (
|
|
222
|
+
awarded_sp > 600 &&
|
|
223
|
+
userData?.created_at &&
|
|
224
|
+
Date.now() - userData.created_at < ONE_WEEK
|
|
225
|
+
) {
|
|
226
|
+
await supabase
|
|
227
|
+
.from("profiles")
|
|
228
|
+
.upsert({ id: userData.id, ban: "excluded", bannedAt: Date.now() });
|
|
229
|
+
|
|
230
|
+
return NextResponse.json(
|
|
231
|
+
{ error: "User excluded due to suspicious activity." },
|
|
232
|
+
{ status: 400 }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error("safen/ auto-exclude check failed:", e);
|
|
237
|
+
}
|
|
238
|
+
|
|
217
239
|
let parsed = [];
|
|
218
240
|
|
|
219
241
|
try {
|
|
@@ -271,16 +293,24 @@ export async function handler({
|
|
|
271
293
|
const spinTotalSp = weightCalculate(spinHashMap);
|
|
272
294
|
|
|
273
295
|
console.log("VERSION: " + version);
|
|
274
|
-
await supabase.from("profiles").upsert({
|
|
275
|
-
id: userData.id,
|
|
276
|
-
play_count: (userData.play_count || 0) + 1,
|
|
277
|
-
skill_points: Math.round(totalSp * 100) / 100,
|
|
278
|
-
spin_skill_points: Math.round(spinTotalSp * 100) / 100,
|
|
279
|
-
squares_hit: (userData.squares_hit || 0) + data.hits,
|
|
280
|
-
});
|
|
281
|
-
console.log("p3");
|
|
282
|
-
|
|
283
|
-
|
|
296
|
+
await supabase.from("profiles").upsert({
|
|
297
|
+
id: userData.id,
|
|
298
|
+
play_count: (userData.play_count || 0) + 1,
|
|
299
|
+
skill_points: Math.round(totalSp * 100) / 100,
|
|
300
|
+
spin_skill_points: Math.round(spinTotalSp * 100) / 100,
|
|
301
|
+
squares_hit: (userData.squares_hit || 0) + data.hits,
|
|
302
|
+
});
|
|
303
|
+
console.log("p3");
|
|
304
|
+
|
|
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
|
+
|
|
312
|
+
// Grant special badges if applicable
|
|
313
|
+
if (passed && beatmapPages && !data.mods.includes("mod_nofail")) {
|
|
284
314
|
try {
|
|
285
315
|
const { data: badgeResult, error: badgeError } = await supabase.rpc(
|
|
286
316
|
"grant_special_badges",
|
package/api/updateBeatmapPage.ts
CHANGED
|
@@ -74,21 +74,23 @@ export async function handler({
|
|
|
74
74
|
if (userData.id !== pageData?.owner)
|
|
75
75
|
return NextResponse.json({ error: "Non-authz user." });
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
return NextResponse.json({ error: "Only unranked maps can be updated" });
|
|
79
|
-
|
|
80
|
-
const upsertPayload = {
|
|
77
|
+
const upsertPayload: any = {
|
|
81
78
|
id,
|
|
82
|
-
genre: "",
|
|
83
|
-
status: "UNRANKED",
|
|
84
79
|
owner: userData.id,
|
|
85
|
-
description: description ? description : pageData.description,
|
|
86
|
-
tags: tags ? tags : pageData.tags,
|
|
87
|
-
video_url: videoUrl ? videoUrl : pageData.video_url,
|
|
88
80
|
updated_at: Date.now(),
|
|
89
|
-
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (typeof description !== "undefined")
|
|
84
|
+
upsertPayload.description = description;
|
|
85
|
+
if (typeof tags !== "undefined") upsertPayload.tags = tags;
|
|
86
|
+
if (typeof videoUrl !== "undefined") upsertPayload.video_url = videoUrl;
|
|
90
87
|
|
|
91
88
|
if (beatmapHash && beatmapData) {
|
|
89
|
+
if (pageData?.status !== "UNRANKED") {
|
|
90
|
+
return NextResponse.json({
|
|
91
|
+
error: "Only unranked maps can be updated",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
92
94
|
upsertPayload["title"] = beatmapData.title;
|
|
93
95
|
upsertPayload["latestBeatmapHash"] = beatmapHash;
|
|
94
96
|
upsertPayload["nominations"] = [];
|
package/index.ts
CHANGED
|
@@ -425,15 +425,16 @@ export const getBeatmapComments = handleApi({url:"/api/getBeatmapComments",...Ge
|
|
|
425
425
|
// ./api/getBeatmapPage.ts API
|
|
426
426
|
|
|
427
427
|
/*
|
|
428
|
-
export const Schema = {
|
|
429
|
-
input: z.strictObject({
|
|
430
|
-
session: z.string(),
|
|
431
|
-
id: z.number(),
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
428
|
+
export const Schema = {
|
|
429
|
+
input: z.strictObject({
|
|
430
|
+
session: z.string(),
|
|
431
|
+
id: z.number(),
|
|
432
|
+
limit: z.number().min(1).max(200).default(50),
|
|
433
|
+
}),
|
|
434
|
+
output: z.object({
|
|
435
|
+
error: z.string().optional(),
|
|
436
|
+
scores: z
|
|
437
|
+
.array(
|
|
437
438
|
z.object({
|
|
438
439
|
id: z.number(),
|
|
439
440
|
awarded_sp: z.number().nullable(),
|
|
@@ -485,15 +486,16 @@ export const getBeatmapPage = handleApi({url:"/api/getBeatmapPage",...GetBeatmap
|
|
|
485
486
|
// ./api/getBeatmapPageById.ts API
|
|
486
487
|
|
|
487
488
|
/*
|
|
488
|
-
export const Schema = {
|
|
489
|
-
input: z.strictObject({
|
|
490
|
-
session: z.string(),
|
|
491
|
-
mapId: z.string(),
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
489
|
+
export const Schema = {
|
|
490
|
+
input: z.strictObject({
|
|
491
|
+
session: z.string(),
|
|
492
|
+
mapId: z.string(),
|
|
493
|
+
limit: z.number().min(1).max(200).default(50),
|
|
494
|
+
}),
|
|
495
|
+
output: z.object({
|
|
496
|
+
error: z.string().optional(),
|
|
497
|
+
scores: z
|
|
498
|
+
.array(
|
|
497
499
|
z.object({
|
|
498
500
|
id: z.number(),
|
|
499
501
|
awarded_sp: z.number().nullable(),
|
|
@@ -579,6 +581,7 @@ export const Schema = {
|
|
|
579
581
|
ownerAvatar: z.string().nullable().optional(),
|
|
580
582
|
status: z.string().nullable().optional(),
|
|
581
583
|
tags: z.string().nullable().optional(),
|
|
584
|
+
videoUrl: z.string().nullable().optional(),
|
|
582
585
|
})
|
|
583
586
|
)
|
|
584
587
|
.optional(),
|
|
@@ -841,6 +844,26 @@ import { Schema as GetMapUploadUrl } from "./api/getMapUploadUrl"
|
|
|
841
844
|
export { Schema as SchemaGetMapUploadUrl } from "./api/getMapUploadUrl"
|
|
842
845
|
export const getMapUploadUrl = handleApi({url:"/api/getMapUploadUrl",...GetMapUploadUrl})
|
|
843
846
|
|
|
847
|
+
// ./api/getOnlinePlayers.ts API
|
|
848
|
+
|
|
849
|
+
/*
|
|
850
|
+
export const Schema = {
|
|
851
|
+
input: z.strictObject({}),
|
|
852
|
+
output: z.object({
|
|
853
|
+
error: z.string().optional(),
|
|
854
|
+
players: z.array(
|
|
855
|
+
z.object({
|
|
856
|
+
id: z.number(),
|
|
857
|
+
name: z.string().nullable(),
|
|
858
|
+
profilePictureUrl: z.string().nullable(),
|
|
859
|
+
})
|
|
860
|
+
),
|
|
861
|
+
}),
|
|
862
|
+
};*/
|
|
863
|
+
import { Schema as GetOnlinePlayers } from "./api/getOnlinePlayers"
|
|
864
|
+
export { Schema as SchemaGetOnlinePlayers } from "./api/getOnlinePlayers"
|
|
865
|
+
export const getOnlinePlayers = handleApi({url:"/api/getOnlinePlayers",...GetOnlinePlayers})
|
|
866
|
+
|
|
844
867
|
// ./api/getPassToken.ts API
|
|
845
868
|
|
|
846
869
|
/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhythia-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "217.0.0",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"author": "online-contributors-cunev",
|
|
6
6
|
"scripts": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"osu-classes": "^3.1.0",
|
|
41
41
|
"osu-parsers": "^4.1.7",
|
|
42
42
|
"osu-standard-stable": "^5.0.0",
|
|
43
|
+
"remote-cloudflare-kv": "^1.0.1",
|
|
43
44
|
"sharp": "^0.33.5",
|
|
44
45
|
"short-uuid": "^5.2.0",
|
|
45
46
|
"simple-git": "^3.25.0",
|
|
@@ -50,5 +51,6 @@
|
|
|
50
51
|
"validator": "^13.12.0",
|
|
51
52
|
"zero-width": "^1.0.29",
|
|
52
53
|
"zod": "^3.24.2"
|
|
53
|
-
}
|
|
54
|
+
},
|
|
55
|
+
"packageManager": "yarn@1.22.22"
|
|
54
56
|
}
|