/[sudobot]/branches/7.x/src/utils/EmbedSchemaParser.ts
ViewVC logotype

Contents of /branches/7.x/src/utils/EmbedSchemaParser.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 7846 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 /**
2 * This file is part of SudoBot.
3 *
4 * Copyright (C) 2021-2023 OSN Developers.
5 *
6 * SudoBot is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU Affero General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * SudoBot is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
15 *
16 * You should have received a copy of the GNU Affero General Public License
17 * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20 import {
21 APIEmbed,
22 APIMessage,
23 Embed,
24 EmbedBuilder,
25 GuildMember,
26 JSONEncodable,
27 Message,
28 MessageCreateOptions,
29 MessageEditOptions,
30 TextBasedChannel,
31 User
32 } from "discord.js";
33 import JSON5 from "json5";
34 import { z } from "zod";
35 import { log } from "./logger";
36
37 type EmbedType = Embed | APIEmbed;
38 type GetMessageOptions = MessageCreateOptions | APIMessage | MessageEditOptions;
39
40 export default class EmbedSchemaParser {
41 private static readonly embedZodSchema = z.object({
42 title: z.string().max(256).optional(),
43 description: z.string().max(4096).optional(),
44 url: z.string().url().optional(),
45 timestamp: z.string().datetime().optional(),
46 color: z.number().min(0).max(0xffffff).optional(),
47 provider: z
48 .union([
49 z.object({
50 name: z.string(),
51 url: z.string().url()
52 }),
53 z.object({
54 name: z.string().optional(),
55 url: z.string().url()
56 }),
57 z.object({
58 name: z.string(),
59 url: z.string().url().optional()
60 })
61 ])
62 .optional(),
63 author: z
64 .object({
65 name: z.string().max(256),
66 icon_url: z.string().url().optional(),
67 url: z.string().url().optional(),
68 proxy_icon_url: z.string().url().optional()
69 })
70 .optional(),
71 footer: z
72 .object({
73 text: z.string().max(2048),
74 icon_url: z.string().url().optional(),
75 proxy_icon_url: z.string().url().optional()
76 })
77 .optional(),
78 image: z
79 .object({
80 url: z.string().url(),
81 proxy_url: z.string().url().optional(),
82 height: z.number().int().optional(),
83 width: z.number().int().optional()
84 })
85 .optional(),
86 thumbnail: z
87 .object({
88 url: z.string().url(),
89 proxy_url: z.string().url().optional(),
90 height: z.number().int().optional(),
91 width: z.number().int().optional()
92 })
93 .optional(),
94 video: z
95 .object({
96 url: z.string().url(),
97 proxy_url: z.string().url().optional(),
98 height: z.number().int().optional(),
99 width: z.number().int().optional()
100 })
101 .optional(),
102 fields: z
103 .array(
104 z.object({
105 name: z.string().max(256),
106 value: z.string().max(1024),
107 inline: z.boolean().optional()
108 })
109 )
110 .optional()
111 });
112
113 private static readonly embedRequiredFieldNames = [
114 "title",
115 "description",
116 "author",
117 "footer",
118 "image",
119 "video",
120 "thumbnail",
121 "fields"
122 ];
123
124 static parseString(string: string): [EmbedBuilder[], string] {
125 const length = string.length;
126 const embeds = [];
127 let outString = string;
128
129 for (let i = 0; i < length; i++) {
130 if (i + 8 < length && (i === 0 || [" ", "\n"].includes(string[i - 1])) && string.substring(i, i + 8) === "embed::{") {
131 const pos = i;
132 i += 7;
133
134 let jsonStream = "";
135
136 while (string.substring(i, i + 3) !== "}::") {
137 if (string[i] === "\n") {
138 i = pos;
139 break;
140 }
141
142 jsonStream += string[i];
143 i++;
144 }
145
146 jsonStream += "}";
147
148 if (i !== pos) {
149 try {
150 const parsedJSON = JSON5.parse(jsonStream);
151
152 if (typeof parsedJSON.color === "string") {
153 parsedJSON.color = parsedJSON.color.startsWith("#")
154 ? parseInt(parsedJSON.color.substring(1), 16)
155 : parseInt(parsedJSON.color);
156 }
157
158 if (!this.validate(parsedJSON)) {
159 continue;
160 }
161
162 embeds.push(new EmbedBuilder(parsedJSON));
163 outString = outString.replace(new RegExp(`(\\s*)embed::(.{${jsonStream.length}})::(\\s*)`, "gm"), "");
164 } catch (e) {
165 console.error(e);
166 continue;
167 }
168 }
169 }
170 }
171
172 return [embeds, outString];
173 }
174
175 private static validate(parsedJSON: object) {
176 log(parsedJSON);
177
178 const { success } = this.embedZodSchema.safeParse(parsedJSON);
179
180 if (!success) {
181 log("Embed validation failed");
182 return false;
183 }
184
185 for (const key of this.embedRequiredFieldNames) {
186 if (key in parsedJSON) {
187 return true;
188 }
189 }
190
191 log("Embed required key validation failed");
192
193 return false;
194 }
195
196 private static toSchemaStringSingle(embed: EmbedType) {
197 return `embed::${JSON.stringify(embed)}::`;
198 }
199
200 static toSchemaString(embed: EmbedType): string;
201 static toSchemaString(embeds: EmbedType[]): string;
202
203 static toSchemaString(embed: EmbedType | EmbedType[]) {
204 if (embed instanceof Array) {
205 return embed.map(this.toSchemaStringSingle.bind(this));
206 }
207
208 return this.toSchemaStringSingle(embed);
209 }
210
211 static getMessageOptions<T extends GetMessageOptions>(payload: T, withContent = true) {
212 const { content, embeds = [], ...options } = payload;
213
214 type GetMessageOptionsResult = (T extends MessageCreateOptions ? MessageCreateOptions : MessageEditOptions) & {
215 embeds: (APIEmbed | JSONEncodable<APIEmbed>)[];
216 };
217
218 if (!content) {
219 return {
220 content,
221 embeds,
222 ...options
223 } as unknown as GetMessageOptionsResult;
224 }
225
226 const [parsedEmbeds, strippedContent] = EmbedSchemaParser.parseString(content);
227
228 return {
229 ...options,
230 embeds: [...embeds, ...parsedEmbeds.slice(0, 10)],
231 content: withContent ? strippedContent : undefined
232 } as unknown as (T extends MessageCreateOptions ? MessageCreateOptions : MessageEditOptions) & {
233 embeds: (APIEmbed | JSONEncodable<APIEmbed>)[];
234 };
235 }
236
237 static sendMessage(sendable: TextBasedChannel | User | GuildMember, options: MessageCreateOptions) {
238 return sendable.send(EmbedSchemaParser.getMessageOptions(options));
239 }
240
241 static editMessage(message: Message, options: MessageEditOptions) {
242 return message.edit(EmbedSchemaParser.getMessageOptions(options));
243 }
244 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26