rhythia-api 234.0.0 → 236.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.
@@ -1,166 +1,197 @@
1
- import { NextResponse } from "../utils/response";
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
- textFilter: z.string().optional(),
10
- authorFilter: z.string().optional(),
11
- tagsFilter: z.string().optional(),
12
- page: z.number().default(1),
13
- maxStars: z.number().optional(),
14
- minLength: z.number().optional(),
15
- maxLength: z.number().optional(),
16
- minStars: z.number().optional(),
17
- creator: z.number().optional(),
18
- status: z.string().optional(),
19
- }),
20
- output: z.object({
21
- error: z.string().optional(),
22
- total: z.number(),
23
- viewPerPage: z.number(),
24
- currentPage: z.number(),
25
- beatmaps: z
26
- .array(
27
- z.object({
28
- id: z.number(),
29
- playcount: z.number().nullable().optional(),
30
- created_at: z.string().nullable().optional(),
31
- difficulty: z.number().nullable().optional(),
32
- noteCount: z.number().nullable().optional(),
33
- length: z.number().nullable().optional(),
34
- title: z.string().nullable().optional(),
35
- ranked: z.boolean().nullable().optional(),
36
- beatmapFile: z.string().nullable().optional(),
37
- image: z.string().nullable().optional(),
38
- starRating: z.number().nullable().optional(),
39
- owner: z.number().nullable().optional(),
40
- ownerUsername: z.string().nullable().optional(),
41
- ownerAvatar: z.string().nullable().optional(),
42
- status: z.string().nullable().optional(),
43
- tags: z.string().nullable().optional(),
44
- videoUrl: z.string().nullable().optional(),
45
- })
46
- )
47
- .optional(),
48
- }),
49
- };
50
-
51
- export async function POST(request: Request): Promise<NextResponse> {
52
- return protectedApi({
53
- request,
54
- schema: Schema,
55
- authorization: () => {},
56
- activity: handler,
57
- });
58
- }
59
-
60
- export async function handler(
61
- data: (typeof Schema)["input"]["_type"]
62
- ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
63
- const result = await getBeatmaps(data);
64
- return NextResponse.json(result);
65
- }
66
-
67
- const VIEW_PER_PAGE = 50;
68
-
69
- export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
70
- const startPage = (data.page - 1) * VIEW_PER_PAGE;
71
- const endPage = startPage + VIEW_PER_PAGE - 1;
72
- const countQuery = await supabase
73
- .from("beatmapPages")
74
- .select("id", { count: "exact", head: true });
75
-
76
- let qry = supabase.from("beatmapPages").select(
77
- `
78
- owner,
79
- created_at,
80
- id,
81
- status,
82
- tags,
83
- ranked_at,
84
- video_url,
85
- beatmaps!inner(
86
- playcount,
87
- ranked,
88
- beatmapFile,
89
- image,
90
- starRating,
91
- difficulty,
92
- length,
93
- title
94
- ),
95
- profiles!inner(
96
- username
97
- )`
98
- );
99
-
100
- if (data.status == "RANKED") {
101
- qry = qry.order("ranked_at", { ascending: false });
102
- } else {
103
- qry = qry.order("created_at", { ascending: false });
104
- }
105
-
106
- if (data.textFilter) {
107
- qry = qry.ilike("beatmaps.title", `%${data.textFilter}%`);
108
- }
109
-
110
- if (data.authorFilter) {
111
- qry = qry.ilike("profiles.username", `%${data.authorFilter}%`);
112
- }
113
-
114
- if (data.tagsFilter) {
115
- qry = qry.ilike("tags", `%${data.tagsFilter}%`);
116
- }
117
-
118
- if (data.minStars) {
119
- qry = qry.gt("beatmaps.starRating", data.minStars);
120
- }
121
-
122
- if (data.maxStars) {
123
- qry = qry.lt("beatmaps.starRating", data.maxStars);
124
- }
125
-
126
- if (data.minLength) {
127
- qry = qry.gt("beatmaps.length", data.minLength);
128
- }
129
-
130
- if (data.maxLength) {
131
- qry = qry.lt("beatmaps.length", data.maxLength);
132
- }
133
-
134
- if (data.status) {
135
- qry = qry.eq("status", data.status);
136
- }
137
-
138
- if (data.creator !== undefined) {
139
- qry = qry.eq("owner", data.creator);
140
- }
141
-
142
- let queryData = await qry.range(startPage, endPage);
143
-
144
- return {
145
- total: countQuery.count || 0,
146
- viewPerPage: VIEW_PER_PAGE,
147
- currentPage: data.page,
148
- beatmaps: queryData.data?.map((beatmapPage) => ({
149
- id: beatmapPage.id,
150
- tags: beatmapPage.tags,
151
- playcount: beatmapPage.beatmaps?.playcount,
152
- created_at: beatmapPage.created_at,
153
- difficulty: beatmapPage.beatmaps?.difficulty,
154
- title: beatmapPage.beatmaps?.title,
155
- ranked: beatmapPage.beatmaps?.ranked,
156
- length: beatmapPage.beatmaps?.length,
157
- beatmapFile: beatmapPage.beatmaps?.beatmapFile,
158
- image: beatmapPage.beatmaps?.image,
159
- starRating: beatmapPage.beatmaps?.starRating,
160
- owner: beatmapPage.owner,
161
- status: beatmapPage.status,
162
- ownerUsername: beatmapPage.profiles?.username,
163
- videoUrl: beatmapPage.video_url,
164
- })),
165
- };
166
- }
1
+ import { NextResponse } from "../utils/response";
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
+ textFilter: z.string().optional(),
10
+ authorFilter: z.string().optional(),
11
+ tagsFilter: z.string().optional(),
12
+ page: z.number().default(1),
13
+ maxStars: z.number().optional(),
14
+ minLength: z.number().optional(),
15
+ maxLength: z.number().optional(),
16
+ minStars: z.number().optional(),
17
+ creator: z.number().optional(),
18
+ status: z.string().optional(),
19
+ }),
20
+ output: z.object({
21
+ error: z.string().optional(),
22
+ total: z.number(),
23
+ viewPerPage: z.number(),
24
+ currentPage: z.number(),
25
+ beatmaps: z
26
+ .array(
27
+ z.object({
28
+ id: z.number(),
29
+ playcount: z.number().nullable().optional(),
30
+ created_at: z.string().nullable().optional(),
31
+ difficulty: z.number().nullable().optional(),
32
+ noteCount: z.number().nullable().optional(),
33
+ length: z.number().nullable().optional(),
34
+ title: z.string().nullable().optional(),
35
+ ranked: z.boolean().nullable().optional(),
36
+ beatmapFile: z.string().nullable().optional(),
37
+ image: z.string().nullable().optional(),
38
+ starRating: z.number().nullable().optional(),
39
+ owner: z.number().nullable().optional(),
40
+ ownerUsername: z.string().nullable().optional(),
41
+ ownerAvatar: z.string().nullable().optional(),
42
+ status: z.string().nullable().optional(),
43
+ qualified: z.boolean().nullable().optional(),
44
+ tags: z.string().nullable().optional(),
45
+ videoUrl: z.string().nullable().optional(),
46
+ })
47
+ )
48
+ .optional(),
49
+ }),
50
+ };
51
+
52
+ export async function POST(request: Request): Promise<NextResponse> {
53
+ return protectedApi({
54
+ request,
55
+ schema: Schema,
56
+ authorization: () => {},
57
+ activity: handler,
58
+ });
59
+ }
60
+
61
+ export async function handler(
62
+ data: (typeof Schema)["input"]["_type"]
63
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
64
+ const result = await getBeatmaps(data);
65
+ return NextResponse.json(result);
66
+ }
67
+
68
+ const VIEW_PER_PAGE = 50;
69
+
70
+ export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
71
+ const startPage = (data.page - 1) * VIEW_PER_PAGE;
72
+ const endPage = startPage + VIEW_PER_PAGE - 1;
73
+ // status filter is a bit uncanny, we store qualified as a field, but it acts as a status in the UI.
74
+ const statusFilter = data.status?.toUpperCase();
75
+ let countQry = supabase
76
+ .from("beatmapPages")
77
+ .select(
78
+ `
79
+ id,
80
+ beatmaps!inner(
81
+ title,
82
+ starRating,
83
+ length
84
+ ),
85
+ profiles!inner(
86
+ username
87
+ )
88
+ `,
89
+ { count: "exact", head: true }
90
+ );
91
+
92
+ let qry = supabase.from("beatmapPages").select(
93
+ `
94
+ owner,
95
+ created_at,
96
+ id,
97
+ status,
98
+ qualified,
99
+ tags,
100
+ ranked_at,
101
+ video_url,
102
+ beatmaps!inner(
103
+ playcount,
104
+ ranked,
105
+ beatmapFile,
106
+ image,
107
+ starRating,
108
+ difficulty,
109
+ length,
110
+ title
111
+ ),
112
+ profiles!inner(
113
+ username
114
+ )`
115
+ );
116
+
117
+ if (statusFilter === "RANKED") {
118
+ qry = qry.order("ranked_at", { ascending: false });
119
+ } else {
120
+ qry = qry.order("created_at", { ascending: false });
121
+ }
122
+
123
+ if (data.textFilter) {
124
+ qry = qry.ilike("beatmaps.title", `%${data.textFilter}%`);
125
+ countQry = countQry.ilike("beatmaps.title", `%${data.textFilter}%`);
126
+ }
127
+
128
+ if (data.authorFilter) {
129
+ qry = qry.ilike("profiles.username", `%${data.authorFilter}%`);
130
+ countQry = countQry.ilike("profiles.username", `%${data.authorFilter}%`);
131
+ }
132
+
133
+ if (data.tagsFilter) {
134
+ qry = qry.ilike("tags", `%${data.tagsFilter}%`);
135
+ countQry = countQry.ilike("tags", `%${data.tagsFilter}%`);
136
+ }
137
+
138
+ if (data.minStars) {
139
+ qry = qry.gt("beatmaps.starRating", data.minStars);
140
+ countQry = countQry.gt("beatmaps.starRating", data.minStars);
141
+ }
142
+
143
+ if (data.maxStars) {
144
+ qry = qry.lt("beatmaps.starRating", data.maxStars);
145
+ countQry = countQry.lt("beatmaps.starRating", data.maxStars);
146
+ }
147
+
148
+ if (data.minLength) {
149
+ qry = qry.gt("beatmaps.length", data.minLength);
150
+ countQry = countQry.gt("beatmaps.length", data.minLength);
151
+ }
152
+
153
+ if (data.maxLength) {
154
+ qry = qry.lt("beatmaps.length", data.maxLength);
155
+ countQry = countQry.lt("beatmaps.length", data.maxLength);
156
+ }
157
+
158
+ if (statusFilter === "QUALIFIED") {
159
+ qry = qry.eq("qualified", true);
160
+ countQry = countQry.eq("qualified", true);
161
+ } else if (data.status) {
162
+ qry = qry.eq("status", data.status);
163
+ countQry = countQry.eq("status", data.status);
164
+ }
165
+
166
+ if (data.creator !== undefined) {
167
+ qry = qry.eq("owner", data.creator);
168
+ countQry = countQry.eq("owner", data.creator);
169
+ }
170
+
171
+ const countQuery = await countQry;
172
+ let queryData = await qry.range(startPage, endPage);
173
+
174
+ return {
175
+ total: countQuery.count || 0,
176
+ viewPerPage: VIEW_PER_PAGE,
177
+ currentPage: data.page,
178
+ beatmaps: queryData.data?.map((beatmapPage) => ({
179
+ id: beatmapPage.id,
180
+ tags: beatmapPage.tags,
181
+ playcount: beatmapPage.beatmaps?.playcount,
182
+ created_at: beatmapPage.created_at,
183
+ difficulty: beatmapPage.beatmaps?.difficulty,
184
+ title: beatmapPage.beatmaps?.title,
185
+ ranked: beatmapPage.beatmaps?.ranked,
186
+ length: beatmapPage.beatmaps?.length,
187
+ beatmapFile: beatmapPage.beatmaps?.beatmapFile,
188
+ image: beatmapPage.beatmaps?.image,
189
+ starRating: beatmapPage.beatmaps?.starRating,
190
+ owner: beatmapPage.owner,
191
+ status: beatmapPage.status,
192
+ qualified: beatmapPage.qualified,
193
+ ownerUsername: beatmapPage.profiles?.username,
194
+ videoUrl: beatmapPage.video_url,
195
+ })),
196
+ };
197
+ }
@@ -0,0 +1,94 @@
1
+ import { NextResponse } from "../utils/response";
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
+ const MAX_DESCRIPTION_LENGTH = 1000;
9
+
10
+ export const Schema = {
11
+ input: z.strictObject({
12
+ session: z.string(),
13
+ profileId: z.number(),
14
+ description: z.string(),
15
+ }),
16
+ output: z.strictObject({
17
+ error: z.string().optional(),
18
+ id: z.number().optional(),
19
+ }),
20
+ };
21
+
22
+ export async function POST(request: Request): Promise<NextResponse> {
23
+ return protectedApi({
24
+ request,
25
+ schema: Schema,
26
+ authorization: validUser,
27
+ activity: handler,
28
+ });
29
+ }
30
+
31
+ export async function handler({
32
+ session,
33
+ profileId,
34
+ description,
35
+ }: (typeof Schema)["input"]["_type"]): Promise<
36
+ NextResponse<(typeof Schema)["output"]["_type"]>
37
+ > {
38
+ const trimmedDescription = description.trim();
39
+
40
+ if (!trimmedDescription.length) {
41
+ return NextResponse.json({ error: "Report description is required." });
42
+ }
43
+
44
+ if (trimmedDescription.length > MAX_DESCRIPTION_LENGTH) {
45
+ return NextResponse.json({
46
+ error: `Report description exceeds ${MAX_DESCRIPTION_LENGTH} characters.`,
47
+ });
48
+ }
49
+
50
+ const user = (await getUserBySession(session)) as User;
51
+ const { data: reporterProfile } = await supabase
52
+ .from("profiles")
53
+ .select("id,ban")
54
+ .eq("uid", user.id)
55
+ .single();
56
+
57
+ if (!reporterProfile) {
58
+ return NextResponse.json({ error: "Can't find user" });
59
+ }
60
+
61
+ if (reporterProfile.ban !== "cool") {
62
+ return NextResponse.json({ error: "Error" });
63
+ }
64
+
65
+ if (reporterProfile.id === profileId) {
66
+ return NextResponse.json({ error: "You can't report yourself." });
67
+ }
68
+
69
+ const { data: reportedProfile } = await supabase
70
+ .from("profiles")
71
+ .select("id")
72
+ .eq("id", profileId)
73
+ .single();
74
+
75
+ if (!reportedProfile) {
76
+ return NextResponse.json({ error: "Player not found." });
77
+ }
78
+
79
+ const insertResult = await supabase
80
+ .from("profileReports")
81
+ .insert({
82
+ reporter: reporterProfile.id,
83
+ reported: reportedProfile.id,
84
+ description: trimmedDescription,
85
+ })
86
+ .select("id")
87
+ .single();
88
+
89
+ if (insertResult.error) {
90
+ return NextResponse.json({ error: insertResult.error.message });
91
+ }
92
+
93
+ return NextResponse.json({ id: insertResult.data?.id });
94
+ }
package/index.ts CHANGED
@@ -624,49 +624,50 @@ export const getBeatmapPageById = handleApi({url:"/api/getBeatmapPageById",...Ge
624
624
  // ./api/getBeatmaps.ts API
625
625
 
626
626
  /*
627
- export const Schema = {
628
- input: z.strictObject({
629
- session: z.string(),
630
- textFilter: z.string().optional(),
631
- authorFilter: z.string().optional(),
632
- tagsFilter: z.string().optional(),
633
- page: z.number().default(1),
634
- maxStars: z.number().optional(),
635
- minLength: z.number().optional(),
636
- maxLength: z.number().optional(),
637
- minStars: z.number().optional(),
638
- creator: z.number().optional(),
639
- status: z.string().optional(),
640
- }),
641
- output: z.object({
642
- error: z.string().optional(),
643
- total: z.number(),
644
- viewPerPage: z.number(),
645
- currentPage: z.number(),
646
- beatmaps: z
647
- .array(
648
- z.object({
649
- id: z.number(),
650
- playcount: z.number().nullable().optional(),
651
- created_at: z.string().nullable().optional(),
652
- difficulty: z.number().nullable().optional(),
653
- noteCount: z.number().nullable().optional(),
654
- length: z.number().nullable().optional(),
655
- title: z.string().nullable().optional(),
656
- ranked: z.boolean().nullable().optional(),
657
- beatmapFile: z.string().nullable().optional(),
658
- image: z.string().nullable().optional(),
659
- starRating: z.number().nullable().optional(),
660
- owner: z.number().nullable().optional(),
661
- ownerUsername: z.string().nullable().optional(),
662
- ownerAvatar: z.string().nullable().optional(),
663
- status: z.string().nullable().optional(),
664
- tags: z.string().nullable().optional(),
665
- videoUrl: z.string().nullable().optional(),
666
- })
667
- )
668
- .optional(),
669
- }),
627
+ export const Schema = {
628
+ input: z.strictObject({
629
+ session: z.string(),
630
+ textFilter: z.string().optional(),
631
+ authorFilter: z.string().optional(),
632
+ tagsFilter: z.string().optional(),
633
+ page: z.number().default(1),
634
+ maxStars: z.number().optional(),
635
+ minLength: z.number().optional(),
636
+ maxLength: z.number().optional(),
637
+ minStars: z.number().optional(),
638
+ creator: z.number().optional(),
639
+ status: z.string().optional(),
640
+ }),
641
+ output: z.object({
642
+ error: z.string().optional(),
643
+ total: z.number(),
644
+ viewPerPage: z.number(),
645
+ currentPage: z.number(),
646
+ beatmaps: z
647
+ .array(
648
+ z.object({
649
+ id: z.number(),
650
+ playcount: z.number().nullable().optional(),
651
+ created_at: z.string().nullable().optional(),
652
+ difficulty: z.number().nullable().optional(),
653
+ noteCount: z.number().nullable().optional(),
654
+ length: z.number().nullable().optional(),
655
+ title: z.string().nullable().optional(),
656
+ ranked: z.boolean().nullable().optional(),
657
+ beatmapFile: z.string().nullable().optional(),
658
+ image: z.string().nullable().optional(),
659
+ starRating: z.number().nullable().optional(),
660
+ owner: z.number().nullable().optional(),
661
+ ownerUsername: z.string().nullable().optional(),
662
+ ownerAvatar: z.string().nullable().optional(),
663
+ status: z.string().nullable().optional(),
664
+ qualified: z.boolean().nullable().optional(),
665
+ tags: z.string().nullable().optional(),
666
+ videoUrl: z.string().nullable().optional(),
667
+ })
668
+ )
669
+ .optional(),
670
+ }),
670
671
  };*/
671
672
  import { Schema as GetBeatmaps } from "./api/getBeatmaps"
672
673
  export { Schema as SchemaGetBeatmaps } from "./api/getBeatmaps"
@@ -1347,6 +1348,24 @@ import { Schema as RankMapsArchive } from "./api/rankMapsArchive"
1347
1348
  export { Schema as SchemaRankMapsArchive } from "./api/rankMapsArchive"
1348
1349
  export const rankMapsArchive = handleApi({url:"/api/rankMapsArchive",...RankMapsArchive})
1349
1350
 
1351
+ // ./api/reportProfile.ts API
1352
+
1353
+ /*
1354
+ export const Schema = {
1355
+ input: z.strictObject({
1356
+ session: z.string(),
1357
+ profileId: z.number(),
1358
+ description: z.string(),
1359
+ }),
1360
+ output: z.strictObject({
1361
+ error: z.string().optional(),
1362
+ id: z.number().optional(),
1363
+ }),
1364
+ };*/
1365
+ import { Schema as ReportProfile } from "./api/reportProfile"
1366
+ export { Schema as SchemaReportProfile } from "./api/reportProfile"
1367
+ export const reportProfile = handleApi({url:"/api/reportProfile",...ReportProfile})
1368
+
1350
1369
  // ./api/searchUsers.ts API
1351
1370
 
1352
1371
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "234.0.0",
3
+ "version": "236.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors-cunev",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "test": "tsx ./scripts/test.ts",
11
11
  "cache:clear-user-scores": "bun run scripts/clear-user-score-cache.ts",
12
12
  "db:optimize-user-scores": "bun run scripts/optimize-user-scores-indexes.ts",
13
+ "db:create-profile-reports": "node scripts/create-profile-reports-table.ts",
13
14
  "query-pull": "bun run scripts/pull-queries.ts",
14
15
  "query-push": "bun run scripts/deploy-queries.ts",
15
16
  "queries:pull": "bun run scripts/pull-queries.ts",
package/types/database.ts CHANGED
@@ -519,6 +519,45 @@ export type Database = {
519
519
  }
520
520
  Relationships: []
521
521
  }
522
+ profileReports: {
523
+ Row: {
524
+ created_at: string
525
+ description: string
526
+ id: number
527
+ reported: number
528
+ reporter: number
529
+ }
530
+ Insert: {
531
+ created_at?: string
532
+ description: string
533
+ id?: number
534
+ reported: number
535
+ reporter: number
536
+ }
537
+ Update: {
538
+ created_at?: string
539
+ description?: string
540
+ id?: number
541
+ reported?: number
542
+ reporter?: number
543
+ }
544
+ Relationships: [
545
+ {
546
+ foreignKeyName: "profileReports_reported_fkey"
547
+ columns: ["reported"]
548
+ isOneToOne: false
549
+ referencedRelation: "profiles"
550
+ referencedColumns: ["id"]
551
+ },
552
+ {
553
+ foreignKeyName: "profileReports_reporter_fkey"
554
+ columns: ["reporter"]
555
+ isOneToOne: false
556
+ referencedRelation: "profiles"
557
+ referencedColumns: ["id"]
558
+ },
559
+ ]
560
+ }
522
561
  profiles: {
523
562
  Row: {
524
563
  about_me: string | null
package/worker.ts CHANGED
@@ -46,6 +46,7 @@ import { POST as getVideoUploadUrl } from "./api/getVideoUploadUrl";
46
46
  import { POST as postBeatmapComment } from "./api/postBeatmapComment";
47
47
  import { POST as qualifyMap } from "./api/qualifyMap";
48
48
  import { POST as rankMapsArchive } from "./api/rankMapsArchive";
49
+ import { POST as reportProfile } from "./api/reportProfile";
49
50
  import { POST as searchUsers } from "./api/searchUsers";
50
51
  import { POST as setPasskey } from "./api/setPasskey";
51
52
  import { POST as submitScore } from "./api/submitScore";
@@ -113,6 +114,7 @@ const apiRoutes: Record<string, RouteHandler> = {
113
114
  "/api/postBeatmapComment": postBeatmapComment,
114
115
  "/api/qualifyMap": qualifyMap,
115
116
  "/api/rankMapsArchive": rankMapsArchive,
117
+ "/api/reportProfile": reportProfile,
116
118
  "/api/searchUsers": searchUsers,
117
119
  "/api/setPasskey": setPasskey,
118
120
  "/api/submitScore": submitScore,