rhythia-api 173.0.0 → 175.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,70 @@
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+ import { getUserBySession } from "../utils/getUserBySession";
6
+ import { User } from "@supabase/supabase-js";
7
+
8
+ export const Schema = {
9
+ input: z.strictObject({
10
+ session: z.string(),
11
+ collection: z.number(),
12
+ beatmapPage: z.number(),
13
+ }),
14
+ output: z.object({
15
+ error: z.string().optional(),
16
+ }),
17
+ };
18
+
19
+ export async function POST(request: Request) {
20
+ return protectedApi({
21
+ request,
22
+ schema: Schema,
23
+ authorization: validUser,
24
+ activity: handler,
25
+ });
26
+ }
27
+
28
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
29
+ const user = (await getUserBySession(data.session)) as User;
30
+ let { data: queryUserData, error: userError } = await supabase
31
+ .from("profiles")
32
+ .select("*")
33
+ .eq("uid", user.id)
34
+ .single();
35
+
36
+ if (!queryUserData) {
37
+ return NextResponse.json({ error: "Can't find user" });
38
+ }
39
+
40
+ let { data: queryCollectionData, error: collectionError } = await supabase
41
+ .from("beatmapCollections")
42
+ .select("*")
43
+ .eq("id", data.collection)
44
+ .single();
45
+
46
+ let { data: queryBeatmapData, error: beatmapData } = await supabase
47
+ .from("beatmapPages")
48
+ .select("*")
49
+ .eq("id", data.beatmapPage)
50
+ .single();
51
+
52
+ if (!queryCollectionData) {
53
+ return NextResponse.json({ error: "Can't find collection" });
54
+ }
55
+
56
+ if (!queryBeatmapData) {
57
+ return NextResponse.json({ error: "Can't find beatmap page" });
58
+ }
59
+
60
+ if (queryCollectionData.owner !== queryUserData.id) {
61
+ return NextResponse.json({ error: "You can't update foreign collections" });
62
+ }
63
+
64
+ await supabase.from("collectionRelations").insert({
65
+ collection: data.collection,
66
+ beatmapPage: data.beatmapPage,
67
+ });
68
+
69
+ return NextResponse.json({});
70
+ }
@@ -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
+ }
@@ -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) {
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+ import { getUserBySession } from "../utils/getUserBySession";
6
+ import { User } from "@supabase/supabase-js";
7
+
8
+ export const Schema = {
9
+ input: z.strictObject({
10
+ session: z.string(),
11
+ title: z.string(),
12
+ }),
13
+ output: z.object({
14
+ error: z.string().optional(),
15
+ }),
16
+ };
17
+
18
+ export async function POST(request: Request) {
19
+ return protectedApi({
20
+ request,
21
+ schema: Schema,
22
+ authorization: validUser,
23
+ activity: handler,
24
+ });
25
+ }
26
+
27
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
28
+ const user = (await getUserBySession(data.session)) as User;
29
+ let { data: queryUserData, error: userError } = await supabase
30
+ .from("profiles")
31
+ .select("*")
32
+ .eq("uid", user.id)
33
+ .single();
34
+
35
+ if (!queryUserData) {
36
+ return NextResponse.json({ error: "Can't find user" });
37
+ }
38
+
39
+ await supabase.from("beatmapCollections").insert({
40
+ title: data.title,
41
+ description: "",
42
+ owner: queryUserData.id,
43
+ });
44
+
45
+ return NextResponse.json({});
46
+ }
@@ -0,0 +1,112 @@
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
+ session: z.string(),
9
+ collection: z.number(),
10
+ }),
11
+ output: z.object({
12
+ collection: z.object({
13
+ title: z.string(),
14
+ description: z.string(),
15
+ beatmaps: z.array(
16
+ z.object({
17
+ id: z.number(),
18
+ playcount: z.number().nullable().optional(),
19
+ created_at: z.string().nullable().optional(),
20
+ difficulty: z.number().nullable().optional(),
21
+ length: z.number().nullable().optional(),
22
+ title: z.string().nullable().optional(),
23
+ ranked: z.boolean().nullable().optional(),
24
+ beatmapFile: z.string().nullable().optional(),
25
+ image: z.string().nullable().optional(),
26
+ starRating: z.number().nullable().optional(),
27
+ owner: z.number().nullable().optional(),
28
+ ownerUsername: z.string().nullable().optional(),
29
+ status: z.string().nullable().optional(),
30
+ tags: z.string().nullable().optional(),
31
+ })
32
+ ),
33
+ }),
34
+ error: z.string().optional(),
35
+ }),
36
+ };
37
+
38
+ export async function POST(request: Request) {
39
+ return protectedApi({
40
+ request,
41
+ schema: Schema,
42
+ authorization: () => {},
43
+ activity: handler,
44
+ });
45
+ }
46
+
47
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
48
+ let { data: queryCollectionData, error: collectionError } = await supabase
49
+ .from("beatmapCollections")
50
+ .select("*")
51
+ .eq("id", data.collection)
52
+ .single();
53
+
54
+ if (!queryCollectionData) {
55
+ return NextResponse.json({ error: "Can't find collection" });
56
+ }
57
+
58
+ let { data: queryBeatmaps, error: beatmapsError } = await supabase
59
+ .from("collectionRelations")
60
+ .select(
61
+ `
62
+ *,
63
+ beatmapPages!inner(
64
+ owner,
65
+ created_at,
66
+ id,
67
+ status,
68
+ tags,
69
+ beatmaps!inner(
70
+ playcount,
71
+ ranked,
72
+ beatmapFile,
73
+ image,
74
+ starRating,
75
+ difficulty,
76
+ length,
77
+ title
78
+ ),
79
+ profiles!inner(
80
+ username
81
+ )
82
+ )
83
+ `
84
+ )
85
+ .eq("collection", data.collection);
86
+
87
+ const formattedBeatmaps =
88
+ queryBeatmaps?.map((relation) => ({
89
+ id: relation.beatmapPages.id,
90
+ playcount: relation.beatmapPages.beatmaps.playcount,
91
+ created_at: relation.beatmapPages.created_at,
92
+ difficulty: relation.beatmapPages.beatmaps.difficulty,
93
+ length: relation.beatmapPages.beatmaps.length,
94
+ title: relation.beatmapPages.beatmaps.title,
95
+ ranked: relation.beatmapPages.beatmaps.ranked,
96
+ beatmapFile: relation.beatmapPages.beatmaps.beatmapFile,
97
+ image: relation.beatmapPages.beatmaps.image,
98
+ starRating: relation.beatmapPages.beatmaps.starRating,
99
+ owner: relation.beatmapPages.owner,
100
+ ownerUsername: relation.beatmapPages.profiles.username,
101
+ status: relation.beatmapPages.status,
102
+ tags: relation.beatmapPages.tags,
103
+ })) || [];
104
+
105
+ return NextResponse.json({
106
+ collection: {
107
+ title: queryCollectionData.title,
108
+ description: queryCollectionData.description,
109
+ beatmaps: formattedBeatmaps,
110
+ },
111
+ });
112
+ }
@@ -0,0 +1,72 @@
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
+ session: z.string(),
9
+ }),
10
+ output: z.object({
11
+ collections: z.array(
12
+ z.object({
13
+ id: z.number(),
14
+ title: z.string(),
15
+ description: z.string(),
16
+ beatmapCount: z.number(),
17
+ createdAt: z.string().nullable().optional(),
18
+ updatedAt: z.string().nullable().optional(),
19
+ })
20
+ ),
21
+ error: z.string().optional(),
22
+ }),
23
+ };
24
+
25
+ export async function POST(request: Request) {
26
+ return protectedApi({
27
+ request,
28
+ schema: Schema,
29
+ authorization: () => {},
30
+ activity: handler,
31
+ });
32
+ }
33
+
34
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
35
+ // First get all collections
36
+ let { data: collections, error: collectionsError } = await supabase
37
+ .from("beatmapCollections")
38
+ .select(
39
+ `
40
+ *,
41
+ collectionRelations!left(count)
42
+ `
43
+ )
44
+ .returns<
45
+ Array<{
46
+ id: number;
47
+ title: string;
48
+ description: string;
49
+ created_at: string | null;
50
+ updated_at: string | null;
51
+ collectionRelations: Array<{ count: number }>;
52
+ }>
53
+ >();
54
+
55
+ if (collectionsError) {
56
+ return NextResponse.json({ error: "Error fetching collections" });
57
+ }
58
+
59
+ const formattedCollections =
60
+ collections?.map((collection) => ({
61
+ id: collection.id,
62
+ title: collection.title,
63
+ description: collection.description,
64
+ beatmapCount: collection.collectionRelations?.[0]?.count ?? 0,
65
+ createdAt: collection.created_at,
66
+ updatedAt: collection.updated_at,
67
+ })) || [];
68
+
69
+ return NextResponse.json({
70
+ collections: formattedCollections,
71
+ });
72
+ }
@@ -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,
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
4
  import { rateMapNotes } from "../utils/star-calc";
5
+ import { calculatePerformancePoints } from "./submitScore";
5
6
 
6
7
  export const Schema = {
7
8
  input: z.strictObject({
@@ -41,6 +42,16 @@ export async function handler(
41
42
  return NextResponse.json({
42
43
  beatmap: {
43
44
  starRating,
45
+ rp: {
46
+ "S---": calculatePerformancePoints((starRating * 1) / 1.35, 1),
47
+ "S--": calculatePerformancePoints((starRating * 1) / 1.25, 1),
48
+ "S-": calculatePerformancePoints((starRating * 1) / 1.15, 1),
49
+ S: calculatePerformancePoints(starRating * 1, 1),
50
+ "S+": calculatePerformancePoints(starRating * 1.15, 1),
51
+ "S++": calculatePerformancePoints(starRating * 1.25, 1),
52
+ "S+++": calculatePerformancePoints(starRating * 1.35, 1),
53
+ "S++++": calculatePerformancePoints(starRating * 1.45, 1),
54
+ },
44
55
  },
45
56
  });
46
57
  }
package/index.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  import { handleApi } from "./handleApi"
2
2
 
3
+ // ./api/addCollectionMap.ts API
4
+
5
+ /*
6
+ export const Schema = {
7
+ input: z.strictObject({
8
+ session: z.string(),
9
+ collection: z.number(),
10
+ beatmapPage: z.number(),
11
+ }),
12
+ output: z.object({
13
+ error: z.string().optional(),
14
+ }),
15
+ };*/
16
+ import { Schema as AddCollectionMap } from "./api/addCollectionMap"
17
+ export { Schema as SchemaAddCollectionMap } from "./api/addCollectionMap"
18
+ export const addCollectionMap = handleApi({url:"/api/addCollectionMap",...AddCollectionMap})
19
+
3
20
  // ./api/approveMap.ts API
4
21
 
5
22
  /*
@@ -16,6 +33,17 @@ import { Schema as ApproveMap } from "./api/approveMap"
16
33
  export { Schema as SchemaApproveMap } from "./api/approveMap"
17
34
  export const approveMap = handleApi({url:"/api/approveMap",...ApproveMap})
18
35
 
36
+ // ./api/chartPublicStats.ts API
37
+
38
+ /*
39
+ export const Schema = {
40
+ input: z.strictObject({}),
41
+ output: z.object({}),
42
+ };*/
43
+ import { Schema as ChartPublicStats } from "./api/chartPublicStats"
44
+ export { Schema as SchemaChartPublicStats } from "./api/chartPublicStats"
45
+ export const chartPublicStats = handleApi({url:"/api/chartPublicStats",...ChartPublicStats})
46
+
19
47
  // ./api/createBeatmap.ts API
20
48
 
21
49
  /*
@@ -67,6 +95,22 @@ import { Schema as CreateClan } from "./api/createClan"
67
95
  export { Schema as SchemaCreateClan } from "./api/createClan"
68
96
  export const createClan = handleApi({url:"/api/createClan",...CreateClan})
69
97
 
98
+ // ./api/createCollection.ts API
99
+
100
+ /*
101
+ export const Schema = {
102
+ input: z.strictObject({
103
+ session: z.string(),
104
+ title: z.string(),
105
+ }),
106
+ output: z.object({
107
+ error: z.string().optional(),
108
+ }),
109
+ };*/
110
+ import { Schema as CreateCollection } from "./api/createCollection"
111
+ export { Schema as SchemaCreateCollection } from "./api/createCollection"
112
+ export const createCollection = handleApi({url:"/api/createCollection",...CreateCollection})
113
+
70
114
  // ./api/deleteBeatmapPage.ts API
71
115
 
72
116
  /*
@@ -403,6 +447,69 @@ import { Schema as GetClan } from "./api/getClan"
403
447
  export { Schema as SchemaGetClan } from "./api/getClan"
404
448
  export const getClan = handleApi({url:"/api/getClan",...GetClan})
405
449
 
450
+ // ./api/getCollection.ts API
451
+
452
+ /*
453
+ export const Schema = {
454
+ input: z.strictObject({
455
+ session: z.string(),
456
+ collection: z.number(),
457
+ }),
458
+ output: z.object({
459
+ collection: z.object({
460
+ title: z.string(),
461
+ description: z.string(),
462
+ beatmaps: z.array(
463
+ z.object({
464
+ id: z.number(),
465
+ playcount: z.number().nullable().optional(),
466
+ created_at: z.string().nullable().optional(),
467
+ difficulty: z.number().nullable().optional(),
468
+ length: z.number().nullable().optional(),
469
+ title: z.string().nullable().optional(),
470
+ ranked: z.boolean().nullable().optional(),
471
+ beatmapFile: z.string().nullable().optional(),
472
+ image: z.string().nullable().optional(),
473
+ starRating: z.number().nullable().optional(),
474
+ owner: z.number().nullable().optional(),
475
+ ownerUsername: z.string().nullable().optional(),
476
+ status: z.string().nullable().optional(),
477
+ tags: z.string().nullable().optional(),
478
+ })
479
+ ),
480
+ }),
481
+ error: z.string().optional(),
482
+ }),
483
+ };*/
484
+ import { Schema as GetCollection } from "./api/getCollection"
485
+ export { Schema as SchemaGetCollection } from "./api/getCollection"
486
+ export const getCollection = handleApi({url:"/api/getCollection",...GetCollection})
487
+
488
+ // ./api/getCollections.ts API
489
+
490
+ /*
491
+ export const Schema = {
492
+ input: z.strictObject({
493
+ session: z.string(),
494
+ }),
495
+ output: z.object({
496
+ collections: z.array(
497
+ z.object({
498
+ id: z.number(),
499
+ title: z.string(),
500
+ description: z.string(),
501
+ beatmapCount: z.number(),
502
+ createdAt: z.string().nullable().optional(),
503
+ updatedAt: z.string().nullable().optional(),
504
+ })
505
+ ),
506
+ error: z.string().optional(),
507
+ }),
508
+ };*/
509
+ import { Schema as GetCollections } from "./api/getCollections"
510
+ export { Schema as SchemaGetCollections } from "./api/getCollections"
511
+ export const getCollections = handleApi({url:"/api/getCollections",...GetCollections})
512
+
406
513
  // ./api/getLeaderboard.ts API
407
514
 
408
515
  /*
@@ -540,6 +647,12 @@ export const Schema = {
540
647
  beatmaps: z.number(),
541
648
  scores: z.number(),
542
649
  onlineUsers: z.number(),
650
+ countChart: z.array(
651
+ z.object({
652
+ type: z.string(),
653
+ value: z.number(),
654
+ })
655
+ ),
543
656
  lastBeatmaps: z.array(
544
657
  z.object({
545
658
  id: z.number().nullable().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "173.0.0",
3
+ "version": "175.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors",
6
6
  "scripts": {
package/types/database.ts CHANGED
@@ -9,6 +9,38 @@ export type Json =
9
9
  export type Database = {
10
10
  public: {
11
11
  Tables: {
12
+ beatmapCollections: {
13
+ Row: {
14
+ created_at: string
15
+ description: string
16
+ id: number
17
+ owner: number
18
+ title: string
19
+ }
20
+ Insert: {
21
+ created_at?: string
22
+ description: string
23
+ id?: number
24
+ owner: number
25
+ title: string
26
+ }
27
+ Update: {
28
+ created_at?: string
29
+ description?: string
30
+ id?: number
31
+ owner?: number
32
+ title?: string
33
+ }
34
+ Relationships: [
35
+ {
36
+ foreignKeyName: "beatmapCollections_owner_fkey"
37
+ columns: ["owner"]
38
+ isOneToOne: false
39
+ referencedRelation: "profiles"
40
+ referencedColumns: ["id"]
41
+ },
42
+ ]
43
+ }
12
44
  beatmapPageComments: {
13
45
  Row: {
14
46
  beatmapPage: number
@@ -153,6 +185,27 @@ export type Database = {
153
185
  }
154
186
  Relationships: []
155
187
  }
188
+ chartedValues: {
189
+ Row: {
190
+ created_at: string
191
+ id: number
192
+ type: string | null
193
+ value: number | null
194
+ }
195
+ Insert: {
196
+ created_at?: string
197
+ id?: number
198
+ type?: string | null
199
+ value?: number | null
200
+ }
201
+ Update: {
202
+ created_at?: string
203
+ id?: number
204
+ type?: string | null
205
+ value?: number | null
206
+ }
207
+ Relationships: []
208
+ }
156
209
  clans: {
157
210
  Row: {
158
211
  acronym: string | null
@@ -194,6 +247,42 @@ export type Database = {
194
247
  },
195
248
  ]
196
249
  }
250
+ collectionRelations: {
251
+ Row: {
252
+ beatmapPage: number | null
253
+ collection: number
254
+ created_at: string
255
+ id: number
256
+ }
257
+ Insert: {
258
+ beatmapPage?: number | null
259
+ collection: number
260
+ created_at?: string
261
+ id?: number
262
+ }
263
+ Update: {
264
+ beatmapPage?: number | null
265
+ collection?: number
266
+ created_at?: string
267
+ id?: number
268
+ }
269
+ Relationships: [
270
+ {
271
+ foreignKeyName: "collectionRelations_beatmapPage_fkey"
272
+ columns: ["beatmapPage"]
273
+ isOneToOne: false
274
+ referencedRelation: "beatmapPages"
275
+ referencedColumns: ["id"]
276
+ },
277
+ {
278
+ foreignKeyName: "collectionRelations_collection_fkey"
279
+ columns: ["collection"]
280
+ isOneToOne: false
281
+ referencedRelation: "beatmapCollections"
282
+ referencedColumns: ["id"]
283
+ },
284
+ ]
285
+ }
197
286
  discordWebhooks: {
198
287
  Row: {
199
288
  id: number
@@ -4,19 +4,6 @@ import { sampleMap } from "./osuUtils";
4
4
  import { SSPMParsedMap } from "./sspmParser";
5
5
  import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
6
6
 
7
- function easeInExpoDeq(x: number) {
8
- return x === 0 ? 0 : Math.pow(2, 35 * x - 35);
9
- }
10
-
11
- export function calculatePerformancePoints(
12
- starRating: number,
13
- accuracy: number
14
- ) {
15
- return Math.round(
16
- Math.pow((starRating * easeInExpoDeq(accuracy) * 100) / 2, 2) / 1000
17
- );
18
- }
19
-
20
7
  export function rateMap(map: SSPMParsedMap) {
21
8
  let notes = map.markers
22
9
  .filter((marker) => marker.type === 0)
@@ -31,9 +18,9 @@ export function rateMap(map: SSPMParsedMap) {
31
18
 
32
19
  export function rateMapNotes(map: [number, number, number][]) {
33
20
  let notes = map.map((marker) => ({
34
- time: marker[0],
35
- x: marker[1],
36
- y: marker[2],
21
+ time: marker[2],
22
+ x: marker[0],
23
+ y: marker[1],
37
24
  }));
38
25
 
39
26
  return rate(notes);