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.
@@ -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
- isList: z.boolean(),
20
- beatmaps: z.array(
21
- 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
+ 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
- isList: queryCollectionData.is_list,
125
- title: queryCollectionData.title,
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
  },
@@ -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
- isList: z.boolean(),
784
- beatmaps: z.array(
785
- 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
+ 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "236.0.0",
3
+ "version": "237.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors-cunev",
6
6
  "scripts": {
@@ -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
+ }