rhythia-api 186.0.0 → 188.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 (60) hide show
  1. package/.prettierrc.json +6 -6
  2. package/api/acceptInvite.ts +79 -0
  3. package/api/addCollectionMap.ts +82 -82
  4. package/api/approveMap.ts +78 -78
  5. package/api/chartPublicStats.ts +32 -32
  6. package/api/createBeatmap.ts +208 -168
  7. package/api/createBeatmapPage.ts +64 -64
  8. package/api/createClan.ts +81 -81
  9. package/api/createCollection.ts +58 -58
  10. package/api/createInvite.ts +66 -0
  11. package/api/deleteBeatmapPage.ts +77 -77
  12. package/api/deleteCollection.ts +59 -59
  13. package/api/deleteCollectionMap.ts +71 -71
  14. package/api/editAboutMe.ts +91 -91
  15. package/api/editClan.ts +90 -90
  16. package/api/editCollection.ts +77 -77
  17. package/api/editProfile.ts +123 -123
  18. package/api/getAvatarUploadUrl.ts +85 -85
  19. package/api/getBadgedUsers.ts +56 -56
  20. package/api/getBeatmapComments.ts +57 -57
  21. package/api/getBeatmapPage.ts +106 -106
  22. package/api/getBeatmapPageById.ts +99 -99
  23. package/api/getBeatmapStarRating.ts +53 -53
  24. package/api/getBeatmaps.ts +159 -159
  25. package/api/getClan.ts +77 -77
  26. package/api/getClans.ts +44 -0
  27. package/api/getCollection.ts +130 -130
  28. package/api/getCollections.ts +132 -132
  29. package/api/getLeaderboard.ts +136 -136
  30. package/api/getMapUploadUrl.ts +93 -93
  31. package/api/getPassToken.ts +55 -55
  32. package/api/getProfile.ts +146 -146
  33. package/api/getPublicStats.ts +180 -180
  34. package/api/getRawStarRating.ts +57 -57
  35. package/api/getScore.ts +85 -85
  36. package/api/getTimestamp.ts +23 -23
  37. package/api/getUserScores.ts +175 -175
  38. package/api/nominateMap.ts +82 -82
  39. package/api/postBeatmapComment.ts +62 -59
  40. package/api/rankMapsArchive.ts +64 -64
  41. package/api/searchUsers.ts +56 -56
  42. package/api/setPasskey.ts +59 -59
  43. package/api/submitScore.ts +433 -433
  44. package/api/updateBeatmapPage.ts +229 -229
  45. package/handleApi.ts +21 -20
  46. package/index.html +2 -2
  47. package/index.ts +914 -863
  48. package/package.json +5 -2
  49. package/types/database.ts +43 -0
  50. package/utils/getUserBySession.ts +48 -48
  51. package/utils/requestUtils.ts +87 -87
  52. package/utils/security.ts +20 -20
  53. package/utils/star-calc/index.ts +72 -72
  54. package/utils/star-calc/osuUtils.ts +53 -53
  55. package/utils/star-calc/sspmParser.ts +398 -398
  56. package/utils/star-calc/sspmv1Parser.ts +165 -165
  57. package/utils/supabase.ts +13 -13
  58. package/utils/test +4 -4
  59. package/utils/validateToken.ts +7 -7
  60. package/vercel.json +12 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "186.0.0",
3
+ "version": "188.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors",
6
6
  "scripts": {
@@ -15,6 +15,7 @@
15
15
  "dependencies": {
16
16
  "@2toad/profanity": "^3.0.0",
17
17
  "@aws-sdk/client-s3": "^3.637.0",
18
+ "@aws-sdk/node-http-handler": "^3.374.0",
18
19
  "@aws-sdk/s3-request-presigner": "^3.637.0",
19
20
  "@netlify/zip-it-and-ship-it": "^9.37.9",
20
21
  "@noble/ciphers": "^1.0.0",
@@ -40,10 +41,12 @@
40
41
  "osu-parsers": "^4.1.7",
41
42
  "osu-standard-stable": "^5.0.0",
42
43
  "sharp": "^0.33.5",
44
+ "short-uuid": "^5.2.0",
43
45
  "simple-git": "^3.25.0",
44
- "supabase": "^2.9.6",
46
+ "supabase": "^2.15.8",
45
47
  "tsx": "^4.17.0",
46
48
  "utf-8-validate": "^6.0.4",
49
+ "uuid": "^11.1.0",
47
50
  "validator": "^13.12.0",
48
51
  "zero-width": "^1.0.29",
49
52
  "zod": "^3.23.8"
package/types/database.ts CHANGED
@@ -307,6 +307,33 @@ export type Database = {
307
307
  }
308
308
  Relationships: []
309
309
  }
310
+ invites: {
311
+ Row: {
312
+ code: string
313
+ created_at: string
314
+ id: number
315
+ resourceId: string
316
+ type: string
317
+ used: boolean
318
+ }
319
+ Insert: {
320
+ code: string
321
+ created_at?: string
322
+ id?: number
323
+ resourceId: string
324
+ type: string
325
+ used?: boolean
326
+ }
327
+ Update: {
328
+ code?: string
329
+ created_at?: string
330
+ id?: number
331
+ resourceId?: string
332
+ type?: string
333
+ used?: boolean
334
+ }
335
+ Relationships: []
336
+ }
310
337
  levers: {
311
338
  Row: {
312
339
  disable_scores: boolean
@@ -511,6 +538,22 @@ export type Database = {
511
538
  [_ in never]: never
512
539
  }
513
540
  Functions: {
541
+ get_clan_leaderboard: {
542
+ Args: {
543
+ page_number?: number
544
+ items_per_page?: number
545
+ }
546
+ Returns: {
547
+ id: number
548
+ name: string
549
+ acronym: string
550
+ avatar_url: string
551
+ description: string
552
+ member_count: number
553
+ total_skill_points: number
554
+ total_pages: number
555
+ }[]
556
+ }
514
557
  get_collections_v1: {
515
558
  Args: {
516
559
  page_number?: number
@@ -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
+ `;