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.
@@ -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
+ }
@@ -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 update their profile.",
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) {
@@ -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 update their profile.",
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
  }
@@ -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"];
@@ -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
+ }
@@ -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: searchData || [],
54
+ results: conjoined || [],
41
55
  });
42
56
  }
@@ -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.from("discordWebhooks").select("*");
399
+ const webHooks = await supabase
400
+ .from("discordWebhooks")
401
+ .select("*")
402
+ .eq("type", "scores");
390
403
 
391
404
  if (!webHooks.data) return;
392
405
 
@@ -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
  /*