rhythia-api 181.0.0 → 183.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.
Files changed (58) hide show
  1. package/.prettierrc.json +6 -6
  2. package/api/addCollectionMap.ts +82 -82
  3. package/api/approveMap.ts +78 -78
  4. package/api/chartPublicStats.ts +32 -32
  5. package/api/createBeatmap.ts +168 -156
  6. package/api/createBeatmapPage.ts +64 -64
  7. package/api/createClan.ts +81 -81
  8. package/api/createCollection.ts +58 -58
  9. package/api/deleteBeatmapPage.ts +77 -72
  10. package/api/deleteCollection.ts +59 -59
  11. package/api/deleteCollectionMap.ts +71 -71
  12. package/api/editAboutMe.ts +91 -91
  13. package/api/editClan.ts +90 -90
  14. package/api/editCollection.ts +77 -75
  15. package/api/editProfile.ts +123 -123
  16. package/api/getAvatarUploadUrl.ts +85 -85
  17. package/api/getBadgedUsers.ts +56 -56
  18. package/api/getBeatmapComments.ts +57 -57
  19. package/api/getBeatmapPage.ts +106 -106
  20. package/api/getBeatmapPageById.ts +99 -99
  21. package/api/getBeatmapStarRating.ts +53 -53
  22. package/api/getBeatmaps.ts +159 -159
  23. package/api/getClan.ts +77 -77
  24. package/api/getCollection.ts +130 -128
  25. package/api/getCollections.ts +128 -126
  26. package/api/getLeaderboard.ts +136 -136
  27. package/api/getMapUploadUrl.ts +93 -93
  28. package/api/getPassToken.ts +55 -55
  29. package/api/getProfile.ts +146 -146
  30. package/api/getPublicStats.ts +180 -180
  31. package/api/getRawStarRating.ts +57 -57
  32. package/api/getScore.ts +85 -85
  33. package/api/getTimestamp.ts +23 -23
  34. package/api/getUserScores.ts +175 -175
  35. package/api/nominateMap.ts +82 -69
  36. package/api/postBeatmapComment.ts +59 -59
  37. package/api/rankMapsArchive.ts +64 -64
  38. package/api/searchUsers.ts +56 -56
  39. package/api/setPasskey.ts +59 -59
  40. package/api/submitScore.ts +433 -433
  41. package/api/updateBeatmapPage.ts +229 -229
  42. package/handleApi.ts +20 -20
  43. package/index.html +2 -2
  44. package/index.ts +865 -862
  45. package/package.json +2 -2
  46. package/types/database.ts +42 -0
  47. package/utils/getUserBySession.ts +48 -48
  48. package/utils/requestUtils.ts +87 -87
  49. package/utils/security.ts +20 -20
  50. package/utils/star-calc/index.ts +72 -72
  51. package/utils/star-calc/osuUtils.ts +53 -53
  52. package/utils/star-calc/sspmParser.ts +398 -398
  53. package/utils/star-calc/sspmv1Parser.ts +165 -165
  54. package/utils/supabase.ts +13 -13
  55. package/utils/test +4 -4
  56. package/utils/validateToken.ts +7 -7
  57. package/vercel.json +12 -12
  58. package/package-lock.json +0 -8913
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "181.0.0",
3
+ "version": "183.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors",
6
6
  "scripts": {
@@ -41,7 +41,7 @@
41
41
  "osu-standard-stable": "^5.0.0",
42
42
  "sharp": "^0.33.5",
43
43
  "simple-git": "^3.25.0",
44
- "supabase": "^2.1.1",
44
+ "supabase": "^2.9.6",
45
45
  "tsx": "^4.17.0",
46
46
  "utf-8-validate": "^6.0.4",
47
47
  "validator": "^13.12.0",
package/types/database.ts CHANGED
@@ -14,6 +14,7 @@ export type Database = {
14
14
  created_at: string
15
15
  description: string
16
16
  id: number
17
+ is_list: boolean
17
18
  owner: number
18
19
  title: string
19
20
  }
@@ -21,6 +22,7 @@ export type Database = {
21
22
  created_at?: string
22
23
  description: string
23
24
  id?: number
25
+ is_list?: boolean
24
26
  owner: number
25
27
  title: string
26
28
  }
@@ -28,6 +30,7 @@ export type Database = {
28
30
  created_at?: string
29
31
  description?: string
30
32
  id?: number
33
+ is_list?: boolean
31
34
  owner?: number
32
35
  title?: string
33
36
  }
@@ -253,18 +256,21 @@ export type Database = {
253
256
  collection: number
254
257
  created_at: string
255
258
  id: number
259
+ sort: number
256
260
  }
257
261
  Insert: {
258
262
  beatmapPage?: number | null
259
263
  collection: number
260
264
  created_at?: string
261
265
  id?: number
266
+ sort?: number
262
267
  }
263
268
  Update: {
264
269
  beatmapPage?: number | null
265
270
  collection?: number
266
271
  created_at?: string
267
272
  id?: number
273
+ sort?: number
268
274
  }
269
275
  Relationships: [
270
276
  {
@@ -573,6 +579,42 @@ export type Database = {
573
579
  total_pages: number
574
580
  }[]
575
581
  }
582
+ get_collections_v3: {
583
+ Args: {
584
+ page_number?: number
585
+ items_per_page?: number
586
+ owner_filter?: number
587
+ }
588
+ Returns: {
589
+ id: number
590
+ title: string
591
+ description: string
592
+ created_at: string
593
+ owner: number
594
+ owner_username: string
595
+ owner_avatar_url: string
596
+ beatmap_count: number
597
+ star1: number
598
+ star2: number
599
+ star3: number
600
+ star4: number
601
+ star5: number
602
+ star6: number
603
+ star7: number
604
+ star8: number
605
+ star9: number
606
+ star10: number
607
+ star11: number
608
+ star12: number
609
+ star13: number
610
+ star14: number
611
+ star15: number
612
+ star16: number
613
+ star17: number
614
+ star18: number
615
+ total_pages: number
616
+ }[]
617
+ }
576
618
  }
577
619
  Enums: {
578
620
  banTypes: "cool" | "silenced" | "restricted" | "excluded"
@@ -1,48 +1,48 @@
1
- import { User } from "@supabase/supabase-js";
2
- import { decryptString } from "./security";
3
- import { supabase } from "./supabase";
4
-
5
- export async function getUserBySession(session: string): Promise<User | null> {
6
- const user = (await supabase.auth.getUser(session)).data.user;
7
-
8
- if (user) {
9
- return user;
10
- }
11
-
12
- try {
13
- console.log("trying legacy token");
14
- const decryptedToken = JSON.parse(decryptString(session)) as {
15
- userId: number;
16
- email: string;
17
- passKey: string;
18
- computerName: string;
19
- };
20
-
21
- for (const key of Object.keys(decryptedToken)) {
22
- if (decryptedToken[key] === undefined || decryptedToken[key] === null) {
23
- return null;
24
- }
25
- }
26
-
27
- console.log(decryptedToken);
28
-
29
- let { data: queryPasskey, error } = await supabase
30
- .from("passkeys")
31
- .select("*,profiles(uid)")
32
- .eq("id", decryptedToken.userId)
33
- .eq("email", decryptedToken.email)
34
- .eq("passkey", decryptedToken.passKey)
35
- .single();
36
-
37
- console.log(queryPasskey);
38
-
39
- if (!queryPasskey) {
40
- return null;
41
- }
42
-
43
- return (await supabase.auth.admin.getUserById(queryPasskey.profiles?.uid!))
44
- .data.user;
45
- } catch (error) {}
46
-
47
- return null;
48
- }
1
+ import { User } from "@supabase/supabase-js";
2
+ import { decryptString } from "./security";
3
+ import { supabase } from "./supabase";
4
+
5
+ export async function getUserBySession(session: string): Promise<User | null> {
6
+ const user = (await supabase.auth.getUser(session)).data.user;
7
+
8
+ if (user) {
9
+ return user;
10
+ }
11
+
12
+ try {
13
+ console.log("trying legacy token");
14
+ const decryptedToken = JSON.parse(decryptString(session)) as {
15
+ userId: number;
16
+ email: string;
17
+ passKey: string;
18
+ computerName: string;
19
+ };
20
+
21
+ for (const key of Object.keys(decryptedToken)) {
22
+ if (decryptedToken[key] === undefined || decryptedToken[key] === null) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ console.log(decryptedToken);
28
+
29
+ let { data: queryPasskey, error } = await supabase
30
+ .from("passkeys")
31
+ .select("*,profiles(uid)")
32
+ .eq("id", decryptedToken.userId)
33
+ .eq("email", decryptedToken.email)
34
+ .eq("passkey", decryptedToken.passKey)
35
+ .single();
36
+
37
+ console.log(queryPasskey);
38
+
39
+ if (!queryPasskey) {
40
+ return null;
41
+ }
42
+
43
+ return (await supabase.auth.admin.getUserById(queryPasskey.profiles?.uid!))
44
+ .data.user;
45
+ } catch (error) {}
46
+
47
+ return null;
48
+ }
@@ -1,87 +1,87 @@
1
- import { NextResponse } from "next/server";
2
- import { set, ZodObject } from "zod";
3
- import { getUserBySession } from "./getUserBySession";
4
- import { supabase } from "./supabase";
5
-
6
- interface Props<
7
- K = (...args: any[]) => Promise<NextResponse<any>>,
8
- T = ZodObject<any>
9
- > {
10
- request: Request;
11
- schema: { input: T; output: T };
12
- authorization?: Function;
13
- activity: K;
14
- }
15
-
16
- export async function protectedApi({
17
- request,
18
- schema,
19
- authorization,
20
- activity,
21
- }: Props) {
22
- try {
23
- const toParse = await request.json();
24
- const data = schema.input.parse(toParse);
25
-
26
- const dataClone = structuredClone(data);
27
- if (dataClone) {
28
- if (dataClone["token"]) {
29
- dataClone["token"] = "********";
30
- }
31
- Object.keys(dataClone).forEach((key) => {
32
- console.log("KEY: ", key, dataClone[key]);
33
- if (key == "data") {
34
- try {
35
- Object.keys(dataClone[key]).forEach((key2) => {
36
- console.log("KEY2: ", key2, dataClone[key][key2]);
37
- });
38
- } catch (error) {}
39
- }
40
- });
41
- }
42
-
43
- setActivity(data);
44
- if (authorization) {
45
- const authorizationResponse = await authorization(data);
46
- if (authorizationResponse) {
47
- return authorizationResponse;
48
- }
49
- }
50
- return await activity(data, request);
51
- } catch (error) {
52
- return NextResponse.json({ error: error.toString() }, { status: 400 });
53
- }
54
- }
55
-
56
- export async function setActivity(data: Record<string, any>) {
57
- if (data.session) {
58
- const user = (await supabase.auth.getUser(data.session)).data.user;
59
- if (user) {
60
- await supabase.from("profileActivities").upsert({
61
- uid: user.id,
62
- last_activity: Date.now(),
63
- });
64
- }
65
- }
66
- }
67
-
68
- export async function validUser(data) {
69
- if (!data.session) {
70
- return NextResponse.json(
71
- {
72
- error: "Session is missing",
73
- },
74
- { status: 501 }
75
- );
76
- }
77
-
78
- const user = await getUserBySession(data.session);
79
- if (!user) {
80
- return NextResponse.json(
81
- {
82
- error: "Invalid user session",
83
- },
84
- { status: 400 }
85
- );
86
- }
87
- }
1
+ import { NextResponse } from "next/server";
2
+ import { set, ZodObject } from "zod";
3
+ import { getUserBySession } from "./getUserBySession";
4
+ import { supabase } from "./supabase";
5
+
6
+ interface Props<
7
+ K = (...args: any[]) => Promise<NextResponse<any>>,
8
+ T = ZodObject<any>
9
+ > {
10
+ request: Request;
11
+ schema: { input: T; output: T };
12
+ authorization?: Function;
13
+ activity: K;
14
+ }
15
+
16
+ export async function protectedApi({
17
+ request,
18
+ schema,
19
+ authorization,
20
+ activity,
21
+ }: Props) {
22
+ try {
23
+ const toParse = await request.json();
24
+ const data = schema.input.parse(toParse);
25
+
26
+ const dataClone = structuredClone(data);
27
+ if (dataClone) {
28
+ if (dataClone["token"]) {
29
+ dataClone["token"] = "********";
30
+ }
31
+ Object.keys(dataClone).forEach((key) => {
32
+ console.log("KEY: ", key, dataClone[key]);
33
+ if (key == "data") {
34
+ try {
35
+ Object.keys(dataClone[key]).forEach((key2) => {
36
+ console.log("KEY2: ", key2, dataClone[key][key2]);
37
+ });
38
+ } catch (error) {}
39
+ }
40
+ });
41
+ }
42
+
43
+ setActivity(data);
44
+ if (authorization) {
45
+ const authorizationResponse = await authorization(data);
46
+ if (authorizationResponse) {
47
+ return authorizationResponse;
48
+ }
49
+ }
50
+ return await activity(data, request);
51
+ } catch (error) {
52
+ return NextResponse.json({ error: error.toString() }, { status: 400 });
53
+ }
54
+ }
55
+
56
+ export async function setActivity(data: Record<string, any>) {
57
+ if (data.session) {
58
+ const user = (await supabase.auth.getUser(data.session)).data.user;
59
+ if (user) {
60
+ await supabase.from("profileActivities").upsert({
61
+ uid: user.id,
62
+ last_activity: Date.now(),
63
+ });
64
+ }
65
+ }
66
+ }
67
+
68
+ export async function validUser(data) {
69
+ if (!data.session) {
70
+ return NextResponse.json(
71
+ {
72
+ error: "Session is missing",
73
+ },
74
+ { status: 501 }
75
+ );
76
+ }
77
+
78
+ const user = await getUserBySession(data.session);
79
+ if (!user) {
80
+ return NextResponse.json(
81
+ {
82
+ error: "Invalid user session",
83
+ },
84
+ { status: 400 }
85
+ );
86
+ }
87
+ }
package/utils/security.ts CHANGED
@@ -1,20 +1,20 @@
1
- import { xchacha20poly1305 } from "@noble/ciphers/chacha";
2
- import { managedNonce } from "@noble/ciphers/webcrypto";
3
- import {
4
- bytesToHex,
5
- bytesToUtf8,
6
- hexToBytes,
7
- utf8ToBytes,
8
- } from "@noble/ciphers/utils";
9
- const key = hexToBytes(process.env.TOKEN_SECRET || "");
10
- const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
11
-
12
- export function encryptString(str: string) {
13
- const data = utf8ToBytes(str);
14
- return bytesToHex(chacha.encrypt(data));
15
- }
16
-
17
- export function decryptString(str: string) {
18
- const data = hexToBytes(str);
19
- return bytesToUtf8(chacha.decrypt(data));
20
- }
1
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
2
+ import { managedNonce } from "@noble/ciphers/webcrypto";
3
+ import {
4
+ bytesToHex,
5
+ bytesToUtf8,
6
+ hexToBytes,
7
+ utf8ToBytes,
8
+ } from "@noble/ciphers/utils";
9
+ const key = hexToBytes(process.env.TOKEN_SECRET || "");
10
+ const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
11
+
12
+ export function encryptString(str: string) {
13
+ const data = utf8ToBytes(str);
14
+ return bytesToHex(chacha.encrypt(data));
15
+ }
16
+
17
+ export function decryptString(str: string) {
18
+ const data = hexToBytes(str);
19
+ return bytesToUtf8(chacha.decrypt(data));
20
+ }
@@ -1,72 +1,72 @@
1
- import { BeatmapDecoder, BeatmapEncoder } from "osu-parsers";
2
- import { StandardRuleset } from "osu-standard-stable";
3
- import { sampleMap } from "./osuUtils";
4
- import { SSPMParsedMap } from "./sspmParser";
5
- import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
6
-
7
- export function rateMap(map: SSPMParsedMap) {
8
- let notes = map.markers
9
- .filter((marker) => marker.type === 0)
10
- .map((marker) => ({
11
- time: marker.position,
12
- x: marker.data["field0"].x,
13
- y: marker.data["field0"].y,
14
- }));
15
-
16
- return rate(notes);
17
- }
18
-
19
- export function rateMapNotes(map: [number, number, number][]) {
20
- let notes = map.map((marker) => ({
21
- time: marker[2],
22
- x: marker[0],
23
- y: marker[1],
24
- }));
25
-
26
- return rate(notes);
27
- }
28
-
29
- export function rateMapOld(map: SSPMMap) {
30
- let notes = map.notes.map((marker) => ({
31
- time: marker.position,
32
- x: marker.x,
33
- y: marker.y,
34
- }));
35
-
36
- return rate(notes);
37
- }
38
-
39
- export function rate(
40
- notes: {
41
- time: number;
42
- x: number;
43
- y: number;
44
- }[]
45
- ) {
46
- notes = notes.sort((a, b) => a.time - b.time);
47
- const decoder = new BeatmapDecoder();
48
- const beatmap1 = decoder.decodeFromString(sampleMap);
49
- let i = 0;
50
- while (i < notes.length - 1) {
51
- const note = notes[i];
52
- const nextNote = notes[i + 1];
53
- if (distance(note.x, note.y, nextNote.x, nextNote.y) < 1.25) {
54
- notes.splice(i + 1, 1);
55
- continue;
56
- }
57
-
58
- const hittable = beatmap1.hitObjects[0].clone();
59
- hittable.startX = Math.round((note.x / 2) * 100);
60
- hittable.startY = Math.round((note.y / 2) * 100);
61
- hittable.startTime = note.time;
62
- beatmap1.hitObjects.push(hittable);
63
- i++;
64
- }
65
- const ruleset = new StandardRuleset();
66
- const mods = ruleset.createModCombination("RX");
67
- const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
68
- const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
69
- return difficultyAttributes.starRating;
70
- }
71
-
72
- const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
1
+ import { BeatmapDecoder, BeatmapEncoder } from "osu-parsers";
2
+ import { StandardRuleset } from "osu-standard-stable";
3
+ import { sampleMap } from "./osuUtils";
4
+ import { SSPMParsedMap } from "./sspmParser";
5
+ import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
6
+
7
+ export function rateMap(map: SSPMParsedMap) {
8
+ let notes = map.markers
9
+ .filter((marker) => marker.type === 0)
10
+ .map((marker) => ({
11
+ time: marker.position,
12
+ x: marker.data["field0"].x,
13
+ y: marker.data["field0"].y,
14
+ }));
15
+
16
+ return rate(notes);
17
+ }
18
+
19
+ export function rateMapNotes(map: [number, number, number][]) {
20
+ let notes = map.map((marker) => ({
21
+ time: marker[2],
22
+ x: marker[0],
23
+ y: marker[1],
24
+ }));
25
+
26
+ return rate(notes);
27
+ }
28
+
29
+ export function rateMapOld(map: SSPMMap) {
30
+ let notes = map.notes.map((marker) => ({
31
+ time: marker.position,
32
+ x: marker.x,
33
+ y: marker.y,
34
+ }));
35
+
36
+ return rate(notes);
37
+ }
38
+
39
+ export function rate(
40
+ notes: {
41
+ time: number;
42
+ x: number;
43
+ y: number;
44
+ }[]
45
+ ) {
46
+ notes = notes.sort((a, b) => a.time - b.time);
47
+ const decoder = new BeatmapDecoder();
48
+ const beatmap1 = decoder.decodeFromString(sampleMap);
49
+ let i = 0;
50
+ while (i < notes.length - 1) {
51
+ const note = notes[i];
52
+ const nextNote = notes[i + 1];
53
+ if (distance(note.x, note.y, nextNote.x, nextNote.y) < 1.25) {
54
+ notes.splice(i + 1, 1);
55
+ continue;
56
+ }
57
+
58
+ const hittable = beatmap1.hitObjects[0].clone();
59
+ hittable.startX = Math.round((note.x / 2) * 100);
60
+ hittable.startY = Math.round((note.y / 2) * 100);
61
+ hittable.startTime = note.time;
62
+ beatmap1.hitObjects.push(hittable);
63
+ i++;
64
+ }
65
+ const ruleset = new StandardRuleset();
66
+ const mods = ruleset.createModCombination("RX");
67
+ const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
68
+ const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
69
+ return difficultyAttributes.starRating;
70
+ }
71
+
72
+ const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
@@ -1,53 +1,53 @@
1
- export const sampleMap = `osu file format v14
2
-
3
- [General]
4
- AudioFilename: audio.mp3
5
- AudioLeadIn: 0
6
- PreviewTime: 99664
7
- Countdown: 0
8
- SampleSet: Normal
9
- StackLeniency: 0
10
- Mode: 0
11
- LetterboxInBreaks: 0
12
- UseSkinSprites: 1
13
- SkinPreference:Default
14
- WidescreenStoryboard: 1
15
- SamplesMatchPlaybackRate: 1
16
-
17
- [Editor]
18
-
19
-
20
- [Metadata]
21
- Title:new beginnings
22
- TitleUnicode:new beginnings
23
- Artist:nekodex
24
- ArtistUnicode:nekodex
25
- Creator:pishifat
26
- Version:tutorial
27
- Source:
28
- Tags:
29
- BeatmapID:2116202
30
- BeatmapSetID:1011011
31
-
32
- [Difficulty]
33
- HPDrainRate:5
34
- CircleSize:2
35
- OverallDifficulty:8
36
- ApproachRate:8
37
- SliderMultiplier:1
38
- SliderTickRate:1
39
-
40
- [Events]
41
- //Background and Video events
42
- 0,0,"new-beginnings.jpg",0,0
43
-
44
- [TimingPoints]
45
- -28,461.538461538462,4,1,0,100,1,0
46
-
47
-
48
- [Colours]
49
-
50
-
51
- [HitObjects]
52
- 256,192,24202,1,0,0:0:0:0:
53
- `;
1
+ export const sampleMap = `osu file format v14
2
+
3
+ [General]
4
+ AudioFilename: audio.mp3
5
+ AudioLeadIn: 0
6
+ PreviewTime: 99664
7
+ Countdown: 0
8
+ SampleSet: Normal
9
+ StackLeniency: 0
10
+ Mode: 0
11
+ LetterboxInBreaks: 0
12
+ UseSkinSprites: 1
13
+ SkinPreference:Default
14
+ WidescreenStoryboard: 1
15
+ SamplesMatchPlaybackRate: 1
16
+
17
+ [Editor]
18
+
19
+
20
+ [Metadata]
21
+ Title:new beginnings
22
+ TitleUnicode:new beginnings
23
+ Artist:nekodex
24
+ ArtistUnicode:nekodex
25
+ Creator:pishifat
26
+ Version:tutorial
27
+ Source:
28
+ Tags:
29
+ BeatmapID:2116202
30
+ BeatmapSetID:1011011
31
+
32
+ [Difficulty]
33
+ HPDrainRate:5
34
+ CircleSize:2
35
+ OverallDifficulty:8
36
+ ApproachRate:8
37
+ SliderMultiplier:1
38
+ SliderTickRate:1
39
+
40
+ [Events]
41
+ //Background and Video events
42
+ 0,0,"new-beginnings.jpg",0,0
43
+
44
+ [TimingPoints]
45
+ -28,461.538461538462,4,1,0,100,1,0
46
+
47
+
48
+ [Colours]
49
+
50
+
51
+ [HitObjects]
52
+ 256,192,24202,1,0,0:0:0:0:
53
+ `;