rhythia-api 236.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/getCollection.ts +26 -23
- package/api/reportProfile.ts +12 -3
- package/index.ts +11 -10
- package/package.json +1 -1
- package/utils/profileReportWebhook.ts +180 -0
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
|
},
|
package/api/reportProfile.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { protectedApi, validUser } from "../utils/requestUtils";
|
|
|
4
4
|
import { supabase } from "../utils/supabase";
|
|
5
5
|
import { getUserBySession } from "../utils/getUserBySession";
|
|
6
6
|
import { User } from "@supabase/supabase-js";
|
|
7
|
+
import { postProfileReportWebhook } from "../utils/profileReportWebhook";
|
|
7
8
|
|
|
8
9
|
const MAX_DESCRIPTION_LENGTH = 1000;
|
|
9
10
|
|
|
@@ -50,7 +51,7 @@ export async function handler({
|
|
|
50
51
|
const user = (await getUserBySession(session)) as User;
|
|
51
52
|
const { data: reporterProfile } = await supabase
|
|
52
53
|
.from("profiles")
|
|
53
|
-
.select("id,ban")
|
|
54
|
+
.select("id,ban,username,computedUsername,avatar_url")
|
|
54
55
|
.eq("uid", user.id)
|
|
55
56
|
.single();
|
|
56
57
|
|
|
@@ -68,7 +69,7 @@ export async function handler({
|
|
|
68
69
|
|
|
69
70
|
const { data: reportedProfile } = await supabase
|
|
70
71
|
.from("profiles")
|
|
71
|
-
.select("id")
|
|
72
|
+
.select("id,username,computedUsername,avatar_url")
|
|
72
73
|
.eq("id", profileId)
|
|
73
74
|
.single();
|
|
74
75
|
|
|
@@ -83,12 +84,20 @@ export async function handler({
|
|
|
83
84
|
reported: reportedProfile.id,
|
|
84
85
|
description: trimmedDescription,
|
|
85
86
|
})
|
|
86
|
-
.select("id")
|
|
87
|
+
.select("id,created_at")
|
|
87
88
|
.single();
|
|
88
89
|
|
|
89
90
|
if (insertResult.error) {
|
|
90
91
|
return NextResponse.json({ error: insertResult.error.message });
|
|
91
92
|
}
|
|
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
|
+
|
|
93
102
|
return NextResponse.json({ id: insertResult.data?.id });
|
|
94
103
|
}
|
package/index.ts
CHANGED
|
@@ -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(),
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|