rhythia-api 235.0.0 → 237.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.
- package/api/getBeatmaps.ts +75 -74
- package/api/getCollection.ts +26 -23
- package/api/reportProfile.ts +103 -0
- package/index.ts +58 -39
- package/package.json +2 -1
- package/types/database.ts +39 -0
- package/utils/profileReportWebhook.ts +180 -0
- package/worker.ts +2 -0
package/api/getBeatmaps.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
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
|
|
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
26
|
.array(
|
|
27
27
|
z.object({
|
|
28
28
|
id: z.number(),
|
|
29
29
|
playcount: z.number().nullable().optional(),
|
|
30
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(),
|
|
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
39
|
owner: z.number().nullable().optional(),
|
|
40
40
|
ownerUsername: z.string().nullable().optional(),
|
|
41
41
|
ownerAvatar: z.string().nullable().optional(),
|
|
@@ -46,30 +46,31 @@ export const Schema = {
|
|
|
46
46
|
})
|
|
47
47
|
)
|
|
48
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
|
-
|
|
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
70
|
export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
|
|
71
71
|
const startPage = (data.page - 1) * VIEW_PER_PAGE;
|
|
72
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.
|
|
73
74
|
const statusFilter = data.status?.toUpperCase();
|
|
74
75
|
let countQry = supabase
|
|
75
76
|
.from("beatmapPages")
|
|
@@ -100,14 +101,14 @@ export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
|
|
|
100
101
|
video_url,
|
|
101
102
|
beatmaps!inner(
|
|
102
103
|
playcount,
|
|
103
|
-
ranked,
|
|
104
|
-
beatmapFile,
|
|
105
|
-
image,
|
|
106
|
-
starRating,
|
|
107
|
-
difficulty,
|
|
108
|
-
length,
|
|
109
|
-
title
|
|
110
|
-
),
|
|
104
|
+
ranked,
|
|
105
|
+
beatmapFile,
|
|
106
|
+
image,
|
|
107
|
+
starRating,
|
|
108
|
+
difficulty,
|
|
109
|
+
length,
|
|
110
|
+
title
|
|
111
|
+
),
|
|
111
112
|
profiles!inner(
|
|
112
113
|
username
|
|
113
114
|
)`
|
|
@@ -173,18 +174,18 @@ export async function getBeatmaps(data: (typeof Schema)["input"]["_type"]) {
|
|
|
173
174
|
return {
|
|
174
175
|
total: countQuery.count || 0,
|
|
175
176
|
viewPerPage: VIEW_PER_PAGE,
|
|
176
|
-
currentPage: data.page,
|
|
177
|
-
beatmaps: queryData.data?.map((beatmapPage) => ({
|
|
178
|
-
id: beatmapPage.id,
|
|
179
|
-
tags: beatmapPage.tags,
|
|
180
|
-
playcount: beatmapPage.beatmaps?.playcount,
|
|
181
|
-
created_at: beatmapPage.created_at,
|
|
182
|
-
difficulty: beatmapPage.beatmaps?.difficulty,
|
|
183
|
-
title: beatmapPage.beatmaps?.title,
|
|
184
|
-
ranked: beatmapPage.beatmaps?.ranked,
|
|
185
|
-
length: beatmapPage.beatmaps?.length,
|
|
186
|
-
beatmapFile: beatmapPage.beatmaps?.beatmapFile,
|
|
187
|
-
image: beatmapPage.beatmaps?.image,
|
|
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,
|
|
188
189
|
starRating: beatmapPage.beatmaps?.starRating,
|
|
189
190
|
owner: beatmapPage.owner,
|
|
190
191
|
status: beatmapPage.status,
|
package/api/getCollection.ts
CHANGED
|
@@ -9,16 +9,17 @@ export const Schema = {
|
|
|
9
9
|
collection: z.number(),
|
|
10
10
|
}),
|
|
11
11
|
output: z.object({
|
|
12
|
-
collection: z.object({
|
|
13
|
-
title: z.string(),
|
|
14
|
-
description: z.string(),
|
|
15
|
-
owner: z.object({
|
|
16
|
-
id: z.number(),
|
|
17
|
-
username: z.string(),
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
collection: z.object({
|
|
13
|
+
title: z.string(),
|
|
14
|
+
description: z.string(),
|
|
15
|
+
owner: z.object({
|
|
16
|
+
id: z.number(),
|
|
17
|
+
username: z.string(),
|
|
18
|
+
avatar_url: z.string().nullable(),
|
|
19
|
+
}),
|
|
20
|
+
isList: z.boolean(),
|
|
21
|
+
beatmaps: z.array(
|
|
22
|
+
z.object({
|
|
22
23
|
id: z.number(),
|
|
23
24
|
playcount: z.number().nullable().optional(),
|
|
24
25
|
created_at: z.string().nullable().optional(),
|
|
@@ -54,13 +55,14 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
|
54
55
|
.from("beatmapCollections")
|
|
55
56
|
.select(
|
|
56
57
|
`
|
|
57
|
-
*,
|
|
58
|
-
profiles!inner(
|
|
59
|
-
id,
|
|
60
|
-
username
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
*,
|
|
59
|
+
profiles!inner(
|
|
60
|
+
id,
|
|
61
|
+
username,
|
|
62
|
+
avatar_url
|
|
63
|
+
)
|
|
64
|
+
`
|
|
65
|
+
)
|
|
64
66
|
.eq("id", data.collection)
|
|
65
67
|
.single();
|
|
66
68
|
|
|
@@ -117,12 +119,13 @@ export async function handler(data: (typeof Schema)["input"]["_type"]) {
|
|
|
117
119
|
|
|
118
120
|
return NextResponse.json({
|
|
119
121
|
collection: {
|
|
120
|
-
owner: {
|
|
121
|
-
username: queryCollectionData.profiles.username,
|
|
122
|
-
id: queryCollectionData.profiles.id,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
owner: {
|
|
123
|
+
username: queryCollectionData.profiles.username,
|
|
124
|
+
id: queryCollectionData.profiles.id,
|
|
125
|
+
avatar_url: queryCollectionData.profiles.avatar_url,
|
|
126
|
+
},
|
|
127
|
+
isList: queryCollectionData.is_list,
|
|
128
|
+
title: queryCollectionData.title,
|
|
126
129
|
description: queryCollectionData.description,
|
|
127
130
|
beatmaps: formattedBeatmaps,
|
|
128
131
|
},
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
import { postProfileReportWebhook } from "../utils/profileReportWebhook";
|
|
8
|
+
|
|
9
|
+
const MAX_DESCRIPTION_LENGTH = 1000;
|
|
10
|
+
|
|
11
|
+
export const Schema = {
|
|
12
|
+
input: z.strictObject({
|
|
13
|
+
session: z.string(),
|
|
14
|
+
profileId: z.number(),
|
|
15
|
+
description: z.string(),
|
|
16
|
+
}),
|
|
17
|
+
output: z.strictObject({
|
|
18
|
+
error: z.string().optional(),
|
|
19
|
+
id: z.number().optional(),
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
24
|
+
return protectedApi({
|
|
25
|
+
request,
|
|
26
|
+
schema: Schema,
|
|
27
|
+
authorization: validUser,
|
|
28
|
+
activity: handler,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function handler({
|
|
33
|
+
session,
|
|
34
|
+
profileId,
|
|
35
|
+
description,
|
|
36
|
+
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
37
|
+
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
38
|
+
> {
|
|
39
|
+
const trimmedDescription = description.trim();
|
|
40
|
+
|
|
41
|
+
if (!trimmedDescription.length) {
|
|
42
|
+
return NextResponse.json({ error: "Report description is required." });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (trimmedDescription.length > MAX_DESCRIPTION_LENGTH) {
|
|
46
|
+
return NextResponse.json({
|
|
47
|
+
error: `Report description exceeds ${MAX_DESCRIPTION_LENGTH} characters.`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const user = (await getUserBySession(session)) as User;
|
|
52
|
+
const { data: reporterProfile } = await supabase
|
|
53
|
+
.from("profiles")
|
|
54
|
+
.select("id,ban,username,computedUsername,avatar_url")
|
|
55
|
+
.eq("uid", user.id)
|
|
56
|
+
.single();
|
|
57
|
+
|
|
58
|
+
if (!reporterProfile) {
|
|
59
|
+
return NextResponse.json({ error: "Can't find user" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (reporterProfile.ban !== "cool") {
|
|
63
|
+
return NextResponse.json({ error: "Error" });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (reporterProfile.id === profileId) {
|
|
67
|
+
return NextResponse.json({ error: "You can't report yourself." });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { data: reportedProfile } = await supabase
|
|
71
|
+
.from("profiles")
|
|
72
|
+
.select("id,username,computedUsername,avatar_url")
|
|
73
|
+
.eq("id", profileId)
|
|
74
|
+
.single();
|
|
75
|
+
|
|
76
|
+
if (!reportedProfile) {
|
|
77
|
+
return NextResponse.json({ error: "Player not found." });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const insertResult = await supabase
|
|
81
|
+
.from("profileReports")
|
|
82
|
+
.insert({
|
|
83
|
+
reporter: reporterProfile.id,
|
|
84
|
+
reported: reportedProfile.id,
|
|
85
|
+
description: trimmedDescription,
|
|
86
|
+
})
|
|
87
|
+
.select("id,created_at")
|
|
88
|
+
.single();
|
|
89
|
+
|
|
90
|
+
if (insertResult.error) {
|
|
91
|
+
return NextResponse.json({ error: insertResult.error.message });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await postProfileReportWebhook({
|
|
95
|
+
reportId: insertResult.data.id,
|
|
96
|
+
reporter: reporterProfile,
|
|
97
|
+
reported: reportedProfile,
|
|
98
|
+
description: trimmedDescription,
|
|
99
|
+
createdAt: insertResult.data.created_at,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return NextResponse.json({ id: insertResult.data?.id });
|
|
103
|
+
}
|
package/index.ts
CHANGED
|
@@ -624,39 +624,39 @@ 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
|
|
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
647
|
.array(
|
|
648
648
|
z.object({
|
|
649
649
|
id: z.number(),
|
|
650
650
|
playcount: z.number().nullable().optional(),
|
|
651
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(),
|
|
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
660
|
owner: z.number().nullable().optional(),
|
|
661
661
|
ownerUsername: z.string().nullable().optional(),
|
|
662
662
|
ownerAvatar: z.string().nullable().optional(),
|
|
@@ -667,7 +667,7 @@ export const Schema = {
|
|
|
667
667
|
})
|
|
668
668
|
)
|
|
669
669
|
.optional(),
|
|
670
|
-
}),
|
|
670
|
+
}),
|
|
671
671
|
};*/
|
|
672
672
|
import { Schema as GetBeatmaps } from "./api/getBeatmaps"
|
|
673
673
|
export { Schema as SchemaGetBeatmaps } from "./api/getBeatmaps"
|
|
@@ -773,16 +773,17 @@ export const Schema = {
|
|
|
773
773
|
collection: z.number(),
|
|
774
774
|
}),
|
|
775
775
|
output: z.object({
|
|
776
|
-
collection: z.object({
|
|
777
|
-
title: z.string(),
|
|
778
|
-
description: z.string(),
|
|
779
|
-
owner: z.object({
|
|
780
|
-
id: z.number(),
|
|
781
|
-
username: z.string(),
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
776
|
+
collection: z.object({
|
|
777
|
+
title: z.string(),
|
|
778
|
+
description: z.string(),
|
|
779
|
+
owner: z.object({
|
|
780
|
+
id: z.number(),
|
|
781
|
+
username: z.string(),
|
|
782
|
+
avatar_url: z.string().nullable(),
|
|
783
|
+
}),
|
|
784
|
+
isList: z.boolean(),
|
|
785
|
+
beatmaps: z.array(
|
|
786
|
+
z.object({
|
|
786
787
|
id: z.number(),
|
|
787
788
|
playcount: z.number().nullable().optional(),
|
|
788
789
|
created_at: z.string().nullable().optional(),
|
|
@@ -1348,6 +1349,24 @@ import { Schema as RankMapsArchive } from "./api/rankMapsArchive"
|
|
|
1348
1349
|
export { Schema as SchemaRankMapsArchive } from "./api/rankMapsArchive"
|
|
1349
1350
|
export const rankMapsArchive = handleApi({url:"/api/rankMapsArchive",...RankMapsArchive})
|
|
1350
1351
|
|
|
1352
|
+
// ./api/reportProfile.ts API
|
|
1353
|
+
|
|
1354
|
+
/*
|
|
1355
|
+
export const Schema = {
|
|
1356
|
+
input: z.strictObject({
|
|
1357
|
+
session: z.string(),
|
|
1358
|
+
profileId: z.number(),
|
|
1359
|
+
description: z.string(),
|
|
1360
|
+
}),
|
|
1361
|
+
output: z.strictObject({
|
|
1362
|
+
error: z.string().optional(),
|
|
1363
|
+
id: z.number().optional(),
|
|
1364
|
+
}),
|
|
1365
|
+
};*/
|
|
1366
|
+
import { Schema as ReportProfile } from "./api/reportProfile"
|
|
1367
|
+
export { Schema as SchemaReportProfile } from "./api/reportProfile"
|
|
1368
|
+
export const reportProfile = handleApi({url:"/api/reportProfile",...ReportProfile})
|
|
1369
|
+
|
|
1351
1370
|
// ./api/searchUsers.ts API
|
|
1352
1371
|
|
|
1353
1372
|
/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhythia-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "237.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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
type ProfileReportWebhookProfile = {
|
|
2
|
+
id: number;
|
|
3
|
+
username: string | null;
|
|
4
|
+
computedUsername: string | null;
|
|
5
|
+
avatar_url: string | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function clampText(value: string, maxLength: number) {
|
|
9
|
+
const sanitized = value.replace(
|
|
10
|
+
/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,
|
|
11
|
+
""
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (sanitized.length <= maxLength) {
|
|
15
|
+
return sanitized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (maxLength <= 3) {
|
|
19
|
+
return sanitized.slice(0, maxLength);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `${sanitized.slice(0, maxLength - 3)}...`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getSafeHttpUrl(value: string | null | undefined) {
|
|
26
|
+
if (!value) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(value.trim());
|
|
32
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const serialized = parsed.toString();
|
|
37
|
+
if (serialized.length > 2048) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return serialized;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDisplayName(profile: ProfileReportWebhookProfile) {
|
|
48
|
+
return (
|
|
49
|
+
profile.username ||
|
|
50
|
+
profile.computedUsername ||
|
|
51
|
+
`User #${profile.id}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPlayerUrl(profileId: number) {
|
|
56
|
+
return `https://www.rhythia.com/player/${profileId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function postProfileReportWebhook({
|
|
60
|
+
reportId,
|
|
61
|
+
reporter,
|
|
62
|
+
reported,
|
|
63
|
+
description,
|
|
64
|
+
createdAt,
|
|
65
|
+
}: {
|
|
66
|
+
reportId: number;
|
|
67
|
+
reporter: ProfileReportWebhookProfile;
|
|
68
|
+
reported: ProfileReportWebhookProfile;
|
|
69
|
+
description: string;
|
|
70
|
+
createdAt?: string;
|
|
71
|
+
}) {
|
|
72
|
+
const webhookUrl = process.env.DISCORD_REPORT_WEBHOOK_URL;
|
|
73
|
+
if (!webhookUrl) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const reporterName = getDisplayName(reporter);
|
|
79
|
+
const reportedName = getDisplayName(reported);
|
|
80
|
+
const safeReporterAvatarUrl = getSafeHttpUrl(reporter.avatar_url);
|
|
81
|
+
const safeReportedAvatarUrl = getSafeHttpUrl(reported.avatar_url);
|
|
82
|
+
|
|
83
|
+
const embed: Record<string, any> = {
|
|
84
|
+
title: clampText(`Profile Report #${reportId}`, 256),
|
|
85
|
+
description: clampText(description, 4096),
|
|
86
|
+
color: 0xe67e22,
|
|
87
|
+
fields: [
|
|
88
|
+
{
|
|
89
|
+
name: "Reported Player",
|
|
90
|
+
value: clampText(
|
|
91
|
+
`${reportedName} (#${reported.id})\n${getPlayerUrl(reported.id)}`,
|
|
92
|
+
1024
|
|
93
|
+
),
|
|
94
|
+
inline: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "Reporter",
|
|
98
|
+
value: clampText(
|
|
99
|
+
`${reporterName} (#${reporter.id})\n${getPlayerUrl(reporter.id)}`,
|
|
100
|
+
1024
|
|
101
|
+
),
|
|
102
|
+
inline: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "Submitted At",
|
|
106
|
+
value: clampText(
|
|
107
|
+
new Date(createdAt || Date.now()).toUTCString(),
|
|
108
|
+
1024
|
|
109
|
+
),
|
|
110
|
+
inline: true,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
author: {
|
|
114
|
+
name: clampText(reporterName, 256),
|
|
115
|
+
url: getPlayerUrl(reporter.id),
|
|
116
|
+
icon_url: safeReporterAvatarUrl || "https://www.rhythia.com/unkimg.png",
|
|
117
|
+
},
|
|
118
|
+
footer: {
|
|
119
|
+
text: clampText(
|
|
120
|
+
`Reported player: ${reportedName} | Report ID: ${reportId}`,
|
|
121
|
+
2048
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (safeReportedAvatarUrl) {
|
|
127
|
+
embed.thumbnail = {
|
|
128
|
+
url: safeReportedAvatarUrl,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const payload = {
|
|
133
|
+
content: "New player report submitted.",
|
|
134
|
+
embeds: [embed],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
let response = await fetch(webhookUrl, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify(payload),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
!response.ok &&
|
|
147
|
+
response.status === 400 &&
|
|
148
|
+
payload.embeds?.[0]?.thumbnail?.url
|
|
149
|
+
) {
|
|
150
|
+
const retryPayload = {
|
|
151
|
+
...payload,
|
|
152
|
+
embeds: payload.embeds.map((embed: any) => {
|
|
153
|
+
const clone = { ...embed };
|
|
154
|
+
delete clone.thumbnail;
|
|
155
|
+
return clone;
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
response = await fetch(webhookUrl, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify(retryPayload),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const responseBody = await response.text();
|
|
170
|
+
console.log("Discord report webhook failed", {
|
|
171
|
+
reportId,
|
|
172
|
+
status: response.status,
|
|
173
|
+
statusText: response.statusText,
|
|
174
|
+
responseBody: clampText(responseBody || "-", 4000),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.log("Failed to post profile report webhook", error);
|
|
179
|
+
}
|
|
180
|
+
}
|
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,
|