rhythia-api 105.0.0 → 107.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,47 @@
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 {
6
+ SSPMParsedMap,
7
+ SSPMParser,
8
+ } from "rhythia-star-calculator/src/sspmParser";
9
+
10
+ export const Schema = {
11
+ input: z.strictObject({
12
+ url: z.string(),
13
+ }),
14
+ output: z.strictObject({
15
+ hash: z.string().optional(),
16
+ error: z.string().optional(),
17
+ }),
18
+ };
19
+
20
+ export async function POST(request: Request): Promise<NextResponse> {
21
+ return protectedApi({
22
+ request,
23
+ schema: Schema,
24
+ authorization: validUser,
25
+ activity: handler,
26
+ });
27
+ }
28
+
29
+ export async function handler({
30
+ url,
31
+ }: (typeof Schema)["input"]["_type"]): Promise<
32
+ NextResponse<(typeof Schema)["output"]["_type"]>
33
+ > {
34
+ if (
35
+ !url.startsWith(`https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/`)
36
+ )
37
+ return NextResponse.json({ error: "Invalid url" });
38
+
39
+ const request = await fetch(url);
40
+ const bytes = await request.arrayBuffer();
41
+ const parser = new SSPMParser(Buffer.from(bytes));
42
+
43
+ const parsedData = parser.parse();
44
+
45
+ console.log(parsedData);
46
+ return NextResponse.json({});
47
+ }
@@ -0,0 +1,47 @@
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
+
6
+ export const Schema = {
7
+ input: z.strictObject({
8
+ session: z.string(),
9
+ }),
10
+ output: z.strictObject({
11
+ error: z.string().optional(),
12
+ id: z.number().optional(),
13
+ }),
14
+ };
15
+
16
+ export async function POST(request: Request): Promise<NextResponse> {
17
+ return protectedApi({
18
+ request,
19
+ schema: Schema,
20
+ authorization: validUser,
21
+ activity: handler,
22
+ });
23
+ }
24
+
25
+ export async function handler({
26
+ session,
27
+ }: (typeof Schema)["input"]["_type"]): Promise<
28
+ NextResponse<(typeof Schema)["output"]["_type"]>
29
+ > {
30
+ const user = (await supabase.auth.getUser(session)).data.user!;
31
+ let { data: userData, error: userError } = await supabase
32
+ .from("profiles")
33
+ .select("*")
34
+ .eq("uid", user.id)
35
+ .single();
36
+
37
+ if (!userData) return NextResponse.json({ error: "No user." });
38
+
39
+ const upserted = await supabase
40
+ .from("beatmapPages")
41
+ .upsert({
42
+ owner: userData.id,
43
+ })
44
+ .select("*")
45
+ .single();
46
+ return NextResponse.json({ id: upserted.data?.id });
47
+ }
@@ -47,6 +47,7 @@ export async function handler(
47
47
  }
48
48
 
49
49
  const user = (await supabase.auth.getUser(data.session)).data.user!;
50
+
50
51
  let userData: Database["public"]["Tables"]["profiles"]["Update"];
51
52
 
52
53
  // Find user's entry
@@ -67,6 +68,20 @@ export async function handler(
67
68
  userData = queryUserData[0];
68
69
  }
69
70
 
71
+ if (
72
+ userData.ban == "excluded" ||
73
+ userData.ban == "restricted" ||
74
+ userData.ban == "silenced"
75
+ ) {
76
+ return NextResponse.json(
77
+ {
78
+ error:
79
+ "Silenced, restricted or excluded players can't update their profile.",
80
+ },
81
+ { status: 404 }
82
+ );
83
+ }
84
+
70
85
  const upsertPayload: Database["public"]["Tables"]["profiles"]["Update"] = {
71
86
  id: userData.id,
72
87
  computedUsername: data.data.username?.toLowerCase(),
@@ -74,11 +74,13 @@ export async function getLeaderboard(page = 1, session: string) {
74
74
  console.log(startPage, endPage);
75
75
  const countQuery = await supabase
76
76
  .from("profiles")
77
- .select("*", { count: "exact", head: true });
77
+ .select("*", { count: "exact", head: true })
78
+ .neq("ban", "excluded");
78
79
 
79
80
  let { data: queryData, error } = await supabase
80
81
  .from("profiles")
81
82
  .select("*")
83
+ .neq("ban", "excluded")
82
84
  .order("skill_points", { ascending: false })
83
85
  .range(startPage, endPage);
84
86
 
@@ -0,0 +1,74 @@
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
+
6
+ import {
7
+ PutBucketCorsCommand,
8
+ PutObjectCommand,
9
+ S3Client,
10
+ } from "@aws-sdk/client-s3";
11
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
12
+
13
+ const s3Client = new S3Client({
14
+ region: "auto",
15
+ endpoint: "https://s3.eu-central-003.backblazeb2.com",
16
+ credentials: {
17
+ secretAccessKey: "K0039mm4iKsteQOXpZSzf0+VDzuH89U",
18
+ accessKeyId: "003c245e893e8060000000001",
19
+ },
20
+ });
21
+
22
+ export const Schema = {
23
+ input: z.strictObject({
24
+ session: z.string(),
25
+ contentLength: z.number(),
26
+ contentType: z.string(),
27
+ }),
28
+ output: z.strictObject({
29
+ error: z.string().optional(),
30
+ url: z.string().optional(),
31
+ objectKey: z.string().optional(),
32
+ }),
33
+ };
34
+
35
+ export async function POST(request: Request): Promise<NextResponse> {
36
+ return protectedApi({
37
+ request,
38
+ schema: Schema,
39
+ authorization: validUser,
40
+ activity: handler,
41
+ });
42
+ }
43
+
44
+ export async function handler({
45
+ session,
46
+ contentLength,
47
+ contentType,
48
+ }: (typeof Schema)["input"]["_type"]): Promise<
49
+ NextResponse<(typeof Schema)["output"]["_type"]>
50
+ > {
51
+ const user = (await supabase.auth.getUser(session)).data.user!;
52
+
53
+ if (contentLength > 50000000) {
54
+ return NextResponse.json({
55
+ error: "Max content length exceeded.",
56
+ });
57
+ }
58
+
59
+ const key = `beatmap-${Date.now()}-${user.id}`;
60
+ const command = new PutObjectCommand({
61
+ Bucket: "rhthia-avatars",
62
+ Key: key,
63
+ ContentLength: contentLength,
64
+ ContentType: contentType,
65
+ });
66
+
67
+ const presigned = await getSignedUrl(s3Client, command, {
68
+ expiresIn: 3600,
69
+ });
70
+ return NextResponse.json({
71
+ url: presigned,
72
+ objectKey: key,
73
+ });
74
+ }
package/api/getProfile.ts CHANGED
@@ -21,6 +21,7 @@ export const Schema = {
21
21
  flag: z.string().nullable(),
22
22
  id: z.number(),
23
23
  uid: z.string().nullable(),
24
+ ban: z.string().nullable(),
24
25
  username: z.string().nullable(),
25
26
  verified: z.boolean().nullable(),
26
27
  play_count: z.number().nullable(),
@@ -86,7 +87,7 @@ export async function handler(
86
87
  about_me: "",
87
88
  avatar_url:
88
89
  "https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
89
- badges: ["Early Bird"],
90
+ badges: [],
90
91
  username: `${user.user_metadata.full_name.slice(0, 20)}${Math.round(
91
92
  Math.random() * 900000 + 100000
92
93
  )}`,
@@ -112,6 +113,7 @@ export async function handler(
112
113
  const { count: playersWithMorePoints, error: rankError } = await supabase
113
114
  .from("profiles")
114
115
  .select("*", { count: "exact", head: true })
116
+ .neq("ban", "excluded")
115
117
  .gt("skill_points", user.skill_points);
116
118
 
117
119
  return NextResponse.json({
@@ -33,6 +33,7 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
33
33
  const { data: searchData, error } = await supabase
34
34
  .from("profiles")
35
35
  .select("id,username")
36
+ .neq("ban", "excluded")
36
37
  .ilike("username", `%${data.text}%`)
37
38
  .limit(10);
38
39
  return NextResponse.json({
@@ -0,0 +1,64 @@
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
+
6
+ export const Schema = {
7
+ input: z.strictObject({
8
+ session: z.string(),
9
+ id: z.number(),
10
+ beatmapHash: z.string(),
11
+ }),
12
+ output: z.strictObject({
13
+ error: z.string().optional(),
14
+ }),
15
+ };
16
+
17
+ export async function POST(request: Request): Promise<NextResponse> {
18
+ return protectedApi({
19
+ request,
20
+ schema: Schema,
21
+ authorization: validUser,
22
+ activity: handler,
23
+ });
24
+ }
25
+
26
+ export async function handler({
27
+ session,
28
+ beatmapHash,
29
+ id,
30
+ }: (typeof Schema)["input"]["_type"]): Promise<
31
+ NextResponse<(typeof Schema)["output"]["_type"]>
32
+ > {
33
+ const user = (await supabase.auth.getUser(session)).data.user!;
34
+ let { data: userData, error: userError } = await supabase
35
+ .from("profiles")
36
+ .select("*")
37
+ .eq("uid", user.id)
38
+ .single();
39
+
40
+ let { data: pageData, error: pageError } = await supabase
41
+ .from("beatmapPages")
42
+ .select("*")
43
+ .eq("id", id)
44
+ .single();
45
+
46
+ if (!userData) return NextResponse.json({ error: "No user." });
47
+ if (userData.id !== pageData?.owner)
48
+ return NextResponse.json({ error: "Non-authz user." });
49
+
50
+ const upserted = await supabase
51
+ .from("beatmapPages")
52
+ .upsert({
53
+ id,
54
+ latestBeatmapHash: beatmapHash,
55
+ owner: userData.id,
56
+ })
57
+ .select("*")
58
+ .single();
59
+
60
+ if (upserted.error) {
61
+ return NextResponse.json({ error: upserted.error.message });
62
+ }
63
+ return NextResponse.json({});
64
+ }
package/index.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { handleApi } from "./handleApi"
2
2
 
3
+ // ./api/createBeatmap.ts API
4
+ import { Schema as CreateBeatmap } from "./api/createBeatmap"
5
+ export { Schema as SchemaCreateBeatmap } from "./api/createBeatmap"
6
+ export const createBeatmap = handleApi({url:"/api/createBeatmap",...CreateBeatmap})
7
+
8
+ // ./api/createBeatmapPage.ts API
9
+ import { Schema as CreateBeatmapPage } from "./api/createBeatmapPage"
10
+ export { Schema as SchemaCreateBeatmapPage } from "./api/createBeatmapPage"
11
+ export const createBeatmapPage = handleApi({url:"/api/createBeatmapPage",...CreateBeatmapPage})
12
+
3
13
  // ./api/editAboutMe.ts API
4
14
  import { Schema as EditAboutMe } from "./api/editAboutMe"
5
15
  export { Schema as SchemaEditAboutMe } from "./api/editAboutMe"
@@ -20,6 +30,11 @@ import { Schema as GetLeaderboard } from "./api/getLeaderboard"
20
30
  export { Schema as SchemaGetLeaderboard } from "./api/getLeaderboard"
21
31
  export const getLeaderboard = handleApi({url:"/api/getLeaderboard",...GetLeaderboard})
22
32
 
33
+ // ./api/getMapUploadUrl.ts API
34
+ import { Schema as GetMapUploadUrl } from "./api/getMapUploadUrl"
35
+ export { Schema as SchemaGetMapUploadUrl } from "./api/getMapUploadUrl"
36
+ export const getMapUploadUrl = handleApi({url:"/api/getMapUploadUrl",...GetMapUploadUrl})
37
+
23
38
  // ./api/getProfile.ts API
24
39
  import { Schema as GetProfile } from "./api/getProfile"
25
40
  export { Schema as SchemaGetProfile } from "./api/getProfile"
@@ -49,4 +64,9 @@ export const searchUsers = handleApi({url:"/api/searchUsers",...SearchUsers})
49
64
  import { Schema as SubmitScore } from "./api/submitScore"
50
65
  export { Schema as SchemaSubmitScore } from "./api/submitScore"
51
66
  export const submitScore = handleApi({url:"/api/submitScore",...SubmitScore})
67
+
68
+ // ./api/updateBeatmapPage.ts API
69
+ import { Schema as UpdateBeatmapPage } from "./api/updateBeatmapPage"
70
+ export { Schema as SchemaUpdateBeatmapPage } from "./api/updateBeatmapPage"
71
+ export const updateBeatmapPage = handleApi({url:"/api/updateBeatmapPage",...UpdateBeatmapPage})
52
72
  export { handleApi } from "./handleApi"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "105.0.0",
3
+ "version": "107.0.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "update": "bun ./scripts/update.ts",
@@ -30,8 +30,9 @@
30
30
  "isomorphic-git": "^1.27.1",
31
31
  "lodash": "^4.17.21",
32
32
  "next": "^14.2.5",
33
+ "rhythia-star-calculator": "^1.0.4",
33
34
  "simple-git": "^3.25.0",
34
- "supabase": "^1.191.3",
35
+ "supabase": "^1.192.5",
35
36
  "tsx": "^4.17.0",
36
37
  "utf-8-validate": "^6.0.4",
37
38
  "zod": "^3.23.8"
@@ -0,0 +1,31 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+ import { Database } from "../types/database";
3
+
4
+ export const supabase = createClient<Database>(
5
+ `https://pfkajngbllcbdzoylrvp.supabase.co`,
6
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBma2FqbmdibGxjYmR6b3lscnZwIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcxODU3NjA3MCwiZXhwIjoyMDM0MTUyMDcwfQ.XKUlQWvzmcYyirM-Zi4nwhiEKcpx1xLS97QUyuR3MoY",
7
+ {
8
+ auth: {
9
+ autoRefreshToken: false,
10
+ persistSession: false,
11
+ },
12
+ }
13
+ );
14
+
15
+ async function main() {
16
+ const countQuery = await supabase
17
+ .from("profiles")
18
+ .select("*", { count: "exact", head: true })
19
+ .neq("ban", "excluded");
20
+
21
+ let { data: queryData, error } = await supabase
22
+ .from("profiles")
23
+ .select("*")
24
+ .neq("ban", "excluded")
25
+ .order("skill_points", { ascending: false })
26
+ .range(0, 50);
27
+
28
+ console.log(queryData);
29
+ }
30
+
31
+ main();
package/types/database.ts CHANGED
@@ -9,11 +9,49 @@ export type Json =
9
9
  export type Database = {
10
10
  public: {
11
11
  Tables: {
12
+ beatmapPages: {
13
+ Row: {
14
+ created_at: string
15
+ id: number
16
+ latestBeatmapHash: string | null
17
+ owner: number | null
18
+ }
19
+ Insert: {
20
+ created_at?: string
21
+ id?: number
22
+ latestBeatmapHash?: string | null
23
+ owner?: number | null
24
+ }
25
+ Update: {
26
+ created_at?: string
27
+ id?: number
28
+ latestBeatmapHash?: string | null
29
+ owner?: number | null
30
+ }
31
+ Relationships: [
32
+ {
33
+ foreignKeyName: "beatmapPages_latestBeatmapHash_fkey"
34
+ columns: ["latestBeatmapHash"]
35
+ isOneToOne: false
36
+ referencedRelation: "beatmaps"
37
+ referencedColumns: ["beatmapHash"]
38
+ },
39
+ {
40
+ foreignKeyName: "beatmapPages_owner_fkey"
41
+ columns: ["owner"]
42
+ isOneToOne: false
43
+ referencedRelation: "profiles"
44
+ referencedColumns: ["id"]
45
+ },
46
+ ]
47
+ }
12
48
  beatmaps: {
13
49
  Row: {
50
+ beatmapFile: string | null
14
51
  beatmapHash: string
15
52
  created_at: string
16
53
  difficulty: number | null
54
+ image: string | null
17
55
  length: number | null
18
56
  noteCount: number | null
19
57
  playcount: number | null
@@ -21,9 +59,11 @@ export type Database = {
21
59
  title: string | null
22
60
  }
23
61
  Insert: {
62
+ beatmapFile?: string | null
24
63
  beatmapHash: string
25
64
  created_at?: string
26
65
  difficulty?: number | null
66
+ image?: string | null
27
67
  length?: number | null
28
68
  noteCount?: number | null
29
69
  playcount?: number | null
@@ -31,9 +71,11 @@ export type Database = {
31
71
  title?: string | null
32
72
  }
33
73
  Update: {
74
+ beatmapFile?: string | null
34
75
  beatmapHash?: string
35
76
  created_at?: string
36
77
  difficulty?: number | null
78
+ image?: string | null
37
79
  length?: number | null
38
80
  noteCount?: number | null
39
81
  playcount?: number | null
@@ -47,6 +89,8 @@ export type Database = {
47
89
  about_me: string | null
48
90
  avatar_url: string | null
49
91
  badges: Json | null
92
+ ban: Database["public"]["Enums"]["banTypes"] | null
93
+ bannedAt: number | null
50
94
  computedUsername: string | null
51
95
  created_at: number | null
52
96
  flag: string | null
@@ -63,6 +107,8 @@ export type Database = {
63
107
  about_me?: string | null
64
108
  avatar_url?: string | null
65
109
  badges?: Json | null
110
+ ban?: Database["public"]["Enums"]["banTypes"] | null
111
+ bannedAt?: number | null
66
112
  computedUsername?: string | null
67
113
  created_at?: number | null
68
114
  flag?: string | null
@@ -79,6 +125,8 @@ export type Database = {
79
125
  about_me?: string | null
80
126
  avatar_url?: string | null
81
127
  badges?: Json | null
128
+ ban?: Database["public"]["Enums"]["banTypes"] | null
129
+ bannedAt?: number | null
82
130
  computedUsername?: string | null
83
131
  created_at?: number | null
84
132
  flag?: string | null
@@ -160,7 +208,7 @@ export type Database = {
160
208
  [_ in never]: never
161
209
  }
162
210
  Enums: {
163
- [_ in never]: never
211
+ banTypes: "cool" | "silenced" | "restricted" | "excluded"
164
212
  }
165
213
  CompositeTypes: {
166
214
  [_ in never]: never