rhythia-api 183.0.0 → 184.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 -168
  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 -77
  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 -77
  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 -130
  25. package/api/getCollections.ts +126 -128
  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 -82
  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 +22 -20
  43. package/index.html +2 -2
  44. package/index.ts +864 -865
  45. package/package-lock.json +8913 -0
  46. package/package.json +2 -2
  47. package/types/database.ts +0 -39
  48. package/utils/getUserBySession.ts +48 -48
  49. package/utils/requestUtils.ts +87 -87
  50. package/utils/security.ts +20 -20
  51. package/utils/star-calc/index.ts +72 -72
  52. package/utils/star-calc/osuUtils.ts +53 -53
  53. package/utils/star-calc/sspmParser.ts +398 -398
  54. package/utils/star-calc/sspmv1Parser.ts +165 -165
  55. package/utils/supabase.ts +13 -13
  56. package/utils/test +4 -4
  57. package/utils/validateToken.ts +7 -7
  58. package/vercel.json +12 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "183.0.0",
3
+ "version": "184.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.9.6",
44
+ "supabase": "^2.1.1",
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
@@ -256,21 +256,18 @@ export type Database = {
256
256
  collection: number
257
257
  created_at: string
258
258
  id: number
259
- sort: number
260
259
  }
261
260
  Insert: {
262
261
  beatmapPage?: number | null
263
262
  collection: number
264
263
  created_at?: string
265
264
  id?: number
266
- sort?: number
267
265
  }
268
266
  Update: {
269
267
  beatmapPage?: number | null
270
268
  collection?: number
271
269
  created_at?: string
272
270
  id?: number
273
- sort?: number
274
271
  }
275
272
  Relationships: [
276
273
  {
@@ -579,42 +576,6 @@ export type Database = {
579
576
  total_pages: number
580
577
  }[]
581
578
  }
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
- }
618
579
  }
619
580
  Enums: {
620
581
  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
+ `;