rhythia-api 172.0.0 → 174.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/chartPublicStats.ts +32 -0
- package/api/createBeatmap.ts +4 -1
- package/api/createBeatmapPage.ts +2 -1
- package/api/editProfile.ts +14 -0
- package/api/getPublicStats.ts +13 -0
- package/api/getRawStarRating.ts +56 -0
- package/api/searchUsers.ts +15 -1
- package/api/submitScore.ts +14 -1
- package/api/updateBeatmapPage.ts +134 -1
- package/index.ts +38 -0
- package/package-lock.json +8913 -8814
- package/package.json +4 -1
- package/types/database.ts +25 -0
- package/utils/star-calc/index.ts +10 -0
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
export const Schema = {
|
|
7
|
+
input: z.strictObject({}),
|
|
8
|
+
output: z.object({}),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function POST(request: Request) {
|
|
12
|
+
return protectedApi({
|
|
13
|
+
request,
|
|
14
|
+
schema: Schema,
|
|
15
|
+
authorization: () => {},
|
|
16
|
+
activity: handler,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
21
|
+
// 30 minutes activity
|
|
22
|
+
const countOnline = await supabase
|
|
23
|
+
.from("profileActivities")
|
|
24
|
+
.select("*", { count: "exact", head: true })
|
|
25
|
+
.gt("last_activity", Date.now() - 1800000);
|
|
26
|
+
|
|
27
|
+
await supabase.from("chartedValues").insert({
|
|
28
|
+
type: "online_players",
|
|
29
|
+
value: countOnline.count,
|
|
30
|
+
});
|
|
31
|
+
return NextResponse.json({});
|
|
32
|
+
}
|
package/api/createBeatmap.ts
CHANGED
|
@@ -73,7 +73,7 @@ export async function handler({
|
|
|
73
73
|
return NextResponse.json(
|
|
74
74
|
{
|
|
75
75
|
error:
|
|
76
|
-
"Silenced, restricted or excluded players can't
|
|
76
|
+
"Silenced, restricted or excluded players can't create beatmaps their profile.",
|
|
77
77
|
},
|
|
78
78
|
{ status: 404 }
|
|
79
79
|
);
|
|
@@ -86,6 +86,9 @@ export async function handler({
|
|
|
86
86
|
.single();
|
|
87
87
|
|
|
88
88
|
if (beatmapPage) {
|
|
89
|
+
if (beatmapPage?.status !== "UNRANKED")
|
|
90
|
+
return NextResponse.json({ error: "Only unranked maps can be updated" });
|
|
91
|
+
|
|
89
92
|
if (!updateFlag) {
|
|
90
93
|
return NextResponse.json({ error: "Already Exists" });
|
|
91
94
|
} else if (beatmapPage.owner !== userData.id) {
|
package/api/createBeatmapPage.ts
CHANGED
|
@@ -46,7 +46,7 @@ export async function handler({
|
|
|
46
46
|
return NextResponse.json(
|
|
47
47
|
{
|
|
48
48
|
error:
|
|
49
|
-
"Silenced, restricted or excluded players can't
|
|
49
|
+
"Silenced, restricted or excluded players can't create beatmap pages.",
|
|
50
50
|
},
|
|
51
51
|
{ status: 404 }
|
|
52
52
|
);
|
|
@@ -59,5 +59,6 @@ export async function handler({
|
|
|
59
59
|
})
|
|
60
60
|
.select("*")
|
|
61
61
|
.single();
|
|
62
|
+
|
|
62
63
|
return NextResponse.json({ id: upserted.data?.id });
|
|
63
64
|
}
|
package/api/editProfile.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { protectedApi, validUser } from "../utils/requestUtils";
|
|
|
5
5
|
import { supabase } from "../utils/supabase";
|
|
6
6
|
import { getUserBySession } from "../utils/getUserBySession";
|
|
7
7
|
import { User } from "@supabase/supabase-js";
|
|
8
|
+
import validator from "validator";
|
|
9
|
+
import removeZeroWidth from "zero-width";
|
|
10
|
+
|
|
8
11
|
export const Schema = {
|
|
9
12
|
input: z.strictObject({
|
|
10
13
|
session: z.string(),
|
|
@@ -49,6 +52,17 @@ export async function handler(
|
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
if (validator.trim(data.data.username || "") !== (data.data.username || "")) {
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{
|
|
58
|
+
error: "Username can't start or end with spaces.",
|
|
59
|
+
},
|
|
60
|
+
{ status: 404 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
data.data.username = removeZeroWidth(data.data.username || "");
|
|
65
|
+
|
|
52
66
|
const user = (await getUserBySession(data.session)) as User;
|
|
53
67
|
|
|
54
68
|
let userData: Database["public"]["Tables"]["profiles"]["Update"];
|
package/api/getPublicStats.ts
CHANGED
|
@@ -10,6 +10,12 @@ export const Schema = {
|
|
|
10
10
|
beatmaps: z.number(),
|
|
11
11
|
scores: z.number(),
|
|
12
12
|
onlineUsers: z.number(),
|
|
13
|
+
countChart: z.array(
|
|
14
|
+
z.object({
|
|
15
|
+
type: z.string(),
|
|
16
|
+
value: z.number(),
|
|
17
|
+
})
|
|
18
|
+
),
|
|
13
19
|
lastBeatmaps: z.array(
|
|
14
20
|
z.object({
|
|
15
21
|
id: z.number().nullable().optional(),
|
|
@@ -127,11 +133,18 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
|
127
133
|
.select("*", { count: "exact", head: true })
|
|
128
134
|
.gt("last_activity", Date.now() - 1800000);
|
|
129
135
|
|
|
136
|
+
const countChart = await supabase
|
|
137
|
+
.from("chartedValues")
|
|
138
|
+
.select("*")
|
|
139
|
+
.eq("type", "online_players")
|
|
140
|
+
.gt("created_at", new Date(Date.now() - 86400000).toISOString());
|
|
141
|
+
|
|
130
142
|
return NextResponse.json({
|
|
131
143
|
beatmaps: countBeatmapsQuery.count,
|
|
132
144
|
profiles: countProfilesQuery.count,
|
|
133
145
|
scores: countScoresQuery.count,
|
|
134
146
|
onlineUsers: countOnline.count,
|
|
147
|
+
countChart: countChart.data,
|
|
135
148
|
lastBeatmaps: beatmapPage?.map((e) => ({
|
|
136
149
|
playcount: e.beatmaps?.playcount,
|
|
137
150
|
created_at: e.created_at,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
4
|
+
import { calculatePerformancePoints, rateMapNotes } from "../utils/star-calc";
|
|
5
|
+
|
|
6
|
+
export const Schema = {
|
|
7
|
+
input: z.strictObject({
|
|
8
|
+
session: z.string(),
|
|
9
|
+
rawMap: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
output: z.object({
|
|
12
|
+
error: z.string().optional(),
|
|
13
|
+
beatmap: z
|
|
14
|
+
.object({
|
|
15
|
+
starRating: z.number().nullable().optional(),
|
|
16
|
+
})
|
|
17
|
+
.optional(),
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
22
|
+
return protectedApi({
|
|
23
|
+
request,
|
|
24
|
+
schema: Schema,
|
|
25
|
+
authorization: () => {},
|
|
26
|
+
activity: handler,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handler(
|
|
31
|
+
data: (typeof Schema)["input"]["_type"],
|
|
32
|
+
req: Request
|
|
33
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
34
|
+
const notes = data.rawMap.split(",");
|
|
35
|
+
notes.shift();
|
|
36
|
+
|
|
37
|
+
const rawNotes = notes.map(
|
|
38
|
+
(e) => e.split("|").map((e) => Number(e)) as [number, number, number]
|
|
39
|
+
);
|
|
40
|
+
const starRating = rateMapNotes(rawNotes);
|
|
41
|
+
return NextResponse.json({
|
|
42
|
+
beatmap: {
|
|
43
|
+
starRating,
|
|
44
|
+
rp: {
|
|
45
|
+
"S---": calculatePerformancePoints((starRating * 1) / 1.35, 1),
|
|
46
|
+
"S--": calculatePerformancePoints((starRating * 1) / 1.25, 1),
|
|
47
|
+
"S-": calculatePerformancePoints((starRating * 1) / 1.15, 1),
|
|
48
|
+
S: calculatePerformancePoints(starRating * 1, 1),
|
|
49
|
+
"S+": calculatePerformancePoints(starRating * 1.15, 1),
|
|
50
|
+
"S++": calculatePerformancePoints(starRating * 1.25, 1),
|
|
51
|
+
"S+++": calculatePerformancePoints(starRating * 1.35, 1),
|
|
52
|
+
"S++++": calculatePerformancePoints(starRating * 1.45, 1),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
package/api/searchUsers.ts
CHANGED
|
@@ -30,13 +30,27 @@ export async function POST(request: Request) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
33
|
+
const { data: exactUser } = await supabase
|
|
34
|
+
.from("profiles")
|
|
35
|
+
.select("id,username")
|
|
36
|
+
.neq("ban", "excluded")
|
|
37
|
+
.eq("computedUsername", data.text.toLocaleLowerCase())
|
|
38
|
+
.single();
|
|
39
|
+
|
|
33
40
|
const { data: searchData, error } = await supabase
|
|
34
41
|
.from("profiles")
|
|
35
42
|
.select("id,username")
|
|
36
43
|
.neq("ban", "excluded")
|
|
37
44
|
.ilike("username", `%${data.text}%`)
|
|
38
45
|
.limit(10);
|
|
46
|
+
|
|
47
|
+
const conjoined = searchData || [];
|
|
48
|
+
|
|
49
|
+
if (exactUser && !conjoined.some((user) => user.id === exactUser?.id)) {
|
|
50
|
+
conjoined.unshift(exactUser);
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
return NextResponse.json({
|
|
40
|
-
results:
|
|
54
|
+
results: conjoined || [],
|
|
41
55
|
});
|
|
42
56
|
}
|
package/api/submitScore.ts
CHANGED
|
@@ -116,6 +116,16 @@ export async function handler({
|
|
|
116
116
|
);
|
|
117
117
|
|
|
118
118
|
console.log(userData);
|
|
119
|
+
|
|
120
|
+
if (userData.ban == "excluded" || userData.ban == "restricted") {
|
|
121
|
+
return NextResponse.json(
|
|
122
|
+
{
|
|
123
|
+
error: "Silenced, restricted or excluded players can't submit scores.",
|
|
124
|
+
},
|
|
125
|
+
{ status: 400 }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
let { data: beatmaps, error } = await supabase
|
|
120
130
|
.from("beatmaps")
|
|
121
131
|
.select("*")
|
|
@@ -386,7 +396,10 @@ export async function postToWebhooks({
|
|
|
386
396
|
mapid: number;
|
|
387
397
|
misses: number;
|
|
388
398
|
}) {
|
|
389
|
-
const webHooks = await supabase
|
|
399
|
+
const webHooks = await supabase
|
|
400
|
+
.from("discordWebhooks")
|
|
401
|
+
.select("*")
|
|
402
|
+
.eq("type", "scores");
|
|
390
403
|
|
|
391
404
|
if (!webHooks.data) return;
|
|
392
405
|
|
package/api/updateBeatmapPage.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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 { calculatePerformancePoints } from "./submitScore";
|
|
7
8
|
|
|
8
9
|
export const Schema = {
|
|
9
10
|
input: z.strictObject({
|
|
@@ -26,7 +27,14 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|
|
26
27
|
activity: handler,
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
export function formatTime(milliseconds: number): string {
|
|
31
|
+
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
32
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
33
|
+
const seconds = totalSeconds % 60;
|
|
34
|
+
return `${minutes.toString().padStart(2, "0")}:${seconds
|
|
35
|
+
.toString()
|
|
36
|
+
.padStart(2, "0")}`;
|
|
37
|
+
}
|
|
30
38
|
export async function handler({
|
|
31
39
|
session,
|
|
32
40
|
beatmapHash,
|
|
@@ -92,5 +100,130 @@ export async function handler({
|
|
|
92
100
|
if (upserted.error?.message.length) {
|
|
93
101
|
return NextResponse.json({ error: upserted.error.message });
|
|
94
102
|
}
|
|
103
|
+
|
|
104
|
+
if (beatmapData?.starRating) {
|
|
105
|
+
postBeatmapToWebhooks({
|
|
106
|
+
username: userData.username || "",
|
|
107
|
+
userid: userData.id,
|
|
108
|
+
avatar: userData.avatar_url || "",
|
|
109
|
+
mapimage: beatmapData?.image || "",
|
|
110
|
+
mapname: beatmapData?.title || "",
|
|
111
|
+
mapid: upserted.data?.id || 0,
|
|
112
|
+
mapDownload: beatmapData?.beatmapFile || "",
|
|
113
|
+
starRating: beatmapData?.starRating || 0,
|
|
114
|
+
length: beatmapData?.length || 0,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
return NextResponse.json({});
|
|
96
119
|
}
|
|
120
|
+
|
|
121
|
+
const beatmapWebhookTemplate: any = {
|
|
122
|
+
content: null,
|
|
123
|
+
embeds: [
|
|
124
|
+
{
|
|
125
|
+
title: "Captain Lou Albano - Do the Mario",
|
|
126
|
+
description: "Beatmap Created",
|
|
127
|
+
url: "https://www.rhythia.com/maps/4469",
|
|
128
|
+
color: 16775930,
|
|
129
|
+
fields: [
|
|
130
|
+
{
|
|
131
|
+
name: "Star Rating",
|
|
132
|
+
value: "12.4*",
|
|
133
|
+
inline: true,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "Length",
|
|
137
|
+
value: "4:24 minutes",
|
|
138
|
+
inline: true,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "Max RP",
|
|
142
|
+
value: "100 RP",
|
|
143
|
+
inline: true,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
author: {
|
|
147
|
+
name: "cunev",
|
|
148
|
+
url: "https://www.rhythia.com/player/0",
|
|
149
|
+
icon_url:
|
|
150
|
+
"https://static.rhythia.com/user-avatar-1735149648551-a2a8cfbe-af5d-46e8-a19a-be2339c1679a",
|
|
151
|
+
},
|
|
152
|
+
footer: {
|
|
153
|
+
text: "Sun, 22 Dec 2024 22:40:17 GMT",
|
|
154
|
+
},
|
|
155
|
+
thumbnail: {
|
|
156
|
+
url: "https://static.rhythia.com/beatmap-img-1735995790136-gen_-_frozy_tomo_-_islands_(kompa_pasi%C3%B3n)large",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
title: "Direct download",
|
|
161
|
+
url: "https://www.rhythia.com/maps/4469",
|
|
162
|
+
color: null,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
attachments: [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export async function postBeatmapToWebhooks({
|
|
169
|
+
username,
|
|
170
|
+
userid,
|
|
171
|
+
avatar,
|
|
172
|
+
mapimage,
|
|
173
|
+
mapname,
|
|
174
|
+
mapid,
|
|
175
|
+
starRating,
|
|
176
|
+
length,
|
|
177
|
+
mapDownload,
|
|
178
|
+
}: {
|
|
179
|
+
username: string;
|
|
180
|
+
userid: number;
|
|
181
|
+
avatar: string;
|
|
182
|
+
mapimage: string;
|
|
183
|
+
mapname: string;
|
|
184
|
+
mapid: number;
|
|
185
|
+
starRating: number;
|
|
186
|
+
length: number;
|
|
187
|
+
mapDownload: string;
|
|
188
|
+
}) {
|
|
189
|
+
// format length in MM:SS with padding 0
|
|
190
|
+
|
|
191
|
+
const webHooks = await supabase
|
|
192
|
+
.from("discordWebhooks")
|
|
193
|
+
.select("*")
|
|
194
|
+
.eq("type", "maps");
|
|
195
|
+
|
|
196
|
+
if (!webHooks.data) return;
|
|
197
|
+
|
|
198
|
+
for (const webhook of webHooks.data) {
|
|
199
|
+
const webhookUrl = webhook.webhook_link;
|
|
200
|
+
|
|
201
|
+
const mainEmbed = beatmapWebhookTemplate.embeds[0];
|
|
202
|
+
const downloadEmbed = beatmapWebhookTemplate.embeds[1];
|
|
203
|
+
|
|
204
|
+
mainEmbed.title = mapname;
|
|
205
|
+
mainEmbed.url = `https://www.rhythia.com/maps/${mapid}`;
|
|
206
|
+
mainEmbed.fields[0].value = `${Math.round(starRating * 100) / 100}*`;
|
|
207
|
+
mainEmbed.fields[1].value = `${formatTime(length)} minutes`;
|
|
208
|
+
mainEmbed.fields[2].value =
|
|
209
|
+
calculatePerformancePoints(starRating, 1) + " RP";
|
|
210
|
+
mainEmbed.author.name = username;
|
|
211
|
+
mainEmbed.author.url = `https://www.rhythia.com/player/${userid}`;
|
|
212
|
+
mainEmbed.author.icon_url = avatar;
|
|
213
|
+
mainEmbed.thumbnail.url = mapimage;
|
|
214
|
+
if (mapimage.includes("backfill")) {
|
|
215
|
+
mainEmbed.thumbnail.url = "https://www.rhythia.com/unkimg.png";
|
|
216
|
+
}
|
|
217
|
+
mainEmbed.footer.text = new Date().toUTCString();
|
|
218
|
+
|
|
219
|
+
downloadEmbed.url = mapDownload;
|
|
220
|
+
|
|
221
|
+
await fetch(webhookUrl, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify(beatmapWebhookTemplate),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
package/index.ts
CHANGED
|
@@ -16,6 +16,17 @@ import { Schema as ApproveMap } from "./api/approveMap"
|
|
|
16
16
|
export { Schema as SchemaApproveMap } from "./api/approveMap"
|
|
17
17
|
export const approveMap = handleApi({url:"/api/approveMap",...ApproveMap})
|
|
18
18
|
|
|
19
|
+
// ./api/chartPublicStats.ts API
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
export const Schema = {
|
|
23
|
+
input: z.strictObject({}),
|
|
24
|
+
output: z.object({}),
|
|
25
|
+
};*/
|
|
26
|
+
import { Schema as ChartPublicStats } from "./api/chartPublicStats"
|
|
27
|
+
export { Schema as SchemaChartPublicStats } from "./api/chartPublicStats"
|
|
28
|
+
export const chartPublicStats = handleApi({url:"/api/chartPublicStats",...ChartPublicStats})
|
|
29
|
+
|
|
19
30
|
// ./api/createBeatmap.ts API
|
|
20
31
|
|
|
21
32
|
/*
|
|
@@ -540,6 +551,12 @@ export const Schema = {
|
|
|
540
551
|
beatmaps: z.number(),
|
|
541
552
|
scores: z.number(),
|
|
542
553
|
onlineUsers: z.number(),
|
|
554
|
+
countChart: z.array(
|
|
555
|
+
z.object({
|
|
556
|
+
type: z.string(),
|
|
557
|
+
value: z.number(),
|
|
558
|
+
})
|
|
559
|
+
),
|
|
543
560
|
lastBeatmaps: z.array(
|
|
544
561
|
z.object({
|
|
545
562
|
id: z.number().nullable().optional(),
|
|
@@ -583,6 +600,27 @@ import { Schema as GetPublicStats } from "./api/getPublicStats"
|
|
|
583
600
|
export { Schema as SchemaGetPublicStats } from "./api/getPublicStats"
|
|
584
601
|
export const getPublicStats = handleApi({url:"/api/getPublicStats",...GetPublicStats})
|
|
585
602
|
|
|
603
|
+
// ./api/getRawStarRating.ts API
|
|
604
|
+
|
|
605
|
+
/*
|
|
606
|
+
export const Schema = {
|
|
607
|
+
input: z.strictObject({
|
|
608
|
+
session: z.string(),
|
|
609
|
+
rawMap: z.string(),
|
|
610
|
+
}),
|
|
611
|
+
output: z.object({
|
|
612
|
+
error: z.string().optional(),
|
|
613
|
+
beatmap: z
|
|
614
|
+
.object({
|
|
615
|
+
starRating: z.number().nullable().optional(),
|
|
616
|
+
})
|
|
617
|
+
.optional(),
|
|
618
|
+
}),
|
|
619
|
+
};*/
|
|
620
|
+
import { Schema as GetRawStarRating } from "./api/getRawStarRating"
|
|
621
|
+
export { Schema as SchemaGetRawStarRating } from "./api/getRawStarRating"
|
|
622
|
+
export const getRawStarRating = handleApi({url:"/api/getRawStarRating",...GetRawStarRating})
|
|
623
|
+
|
|
586
624
|
// ./api/getScore.ts API
|
|
587
625
|
|
|
588
626
|
/*
|