rhythia-api 171.0.0 → 173.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.
@@ -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
  );
@@ -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"];
@@ -28,6 +28,13 @@ export const Schema = {
28
28
  skill_points: z.number().nullable(),
29
29
  spin_skill_points: z.number().nullable(),
30
30
  total_score: z.number().nullable(),
31
+ clans: z
32
+ .object({
33
+ id: z.number(),
34
+ acronym: z.string(),
35
+ })
36
+ .optional()
37
+ .nullable(),
31
38
  })
32
39
  )
33
40
  .optional(),
@@ -92,7 +99,10 @@ export async function getLeaderboard(
92
99
  .select("ban", { count: "exact", head: true })
93
100
  .neq("ban", "excluded");
94
101
 
95
- let query = supabase.from("profiles").select("*").neq("ban", "excluded");
102
+ let query = supabase
103
+ .from("profiles")
104
+ .select("*,clans:clan(id, acronym)")
105
+ .neq("ban", "excluded");
96
106
 
97
107
  if (flag) {
98
108
  query.eq("flag", flag);
@@ -120,6 +130,7 @@ export async function getLeaderboard(
120
130
  spin_skill_points: user.spin_skill_points,
121
131
  total_score: user.total_score,
122
132
  username: user.username,
133
+ clans: user.clans as any,
123
134
  })),
124
135
  };
125
136
  }
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi } from "../utils/requestUtils";
4
+ import { 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
+ },
45
+ });
46
+ }
@@ -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
@@ -429,6 +429,13 @@ export const Schema = {
429
429
  skill_points: z.number().nullable(),
430
430
  spin_skill_points: z.number().nullable(),
431
431
  total_score: z.number().nullable(),
432
+ clans: z
433
+ .object({
434
+ id: z.number(),
435
+ acronym: z.string(),
436
+ })
437
+ .optional()
438
+ .nullable(),
432
439
  })
433
440
  )
434
441
  .optional(),
@@ -576,6 +583,27 @@ import { Schema as GetPublicStats } from "./api/getPublicStats"
576
583
  export { Schema as SchemaGetPublicStats } from "./api/getPublicStats"
577
584
  export const getPublicStats = handleApi({url:"/api/getPublicStats",...GetPublicStats})
578
585
 
586
+ // ./api/getRawStarRating.ts API
587
+
588
+ /*
589
+ export const Schema = {
590
+ input: z.strictObject({
591
+ session: z.string(),
592
+ rawMap: z.string(),
593
+ }),
594
+ output: z.object({
595
+ error: z.string().optional(),
596
+ beatmap: z
597
+ .object({
598
+ starRating: z.number().nullable().optional(),
599
+ })
600
+ .optional(),
601
+ }),
602
+ };*/
603
+ import { Schema as GetRawStarRating } from "./api/getRawStarRating"
604
+ export { Schema as SchemaGetRawStarRating } from "./api/getRawStarRating"
605
+ export const getRawStarRating = handleApi({url:"/api/getRawStarRating",...GetRawStarRating})
606
+
579
607
  // ./api/getScore.ts API
580
608
 
581
609
  /*