/[sudobot]/branches/7.x/src/core/CommandArgumentParser.ts
ViewVC logotype

Contents of /branches/7.x/src/core/CommandArgumentParser.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: 11912 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 { Awaitable, Client, GuildBasedChannel, GuildMember, Role, SnowflakeUtil, User } from "discord.js";
21 import { stringToTimeInterval } from "../utils/datetime";
22 import { logWarn } from "../utils/logger";
23 import CommandArgumentParserInterface, {
24 ArgumentType,
25 ParseOptions,
26 ParseResult,
27 ParserJump,
28 ParsingState,
29 ValidationErrorType
30 } from "./CommandArgumentParserInterface";
31
32 class ArgumentParseError extends Error {
33 constructor(message: string, public readonly type: ValidationErrorType | ValidationErrorType[]) {
34 super(`[${type}]: ${message}`);
35 }
36 }
37
38 export default class CommandArgumentParser implements CommandArgumentParserInterface {
39 protected readonly parsers: Record<ArgumentType, Extract<keyof CommandArgumentParser, `parse${string}Type`>> = {
40 [ArgumentType.String]: "parseStringType",
41 [ArgumentType.Snowflake]: "parseSnowflakeType",
42 [ArgumentType.StringRest]: "parseStringRestType",
43 [ArgumentType.Channel]: "parseEntityType",
44 [ArgumentType.User]: "parseEntityType",
45 [ArgumentType.Role]: "parseEntityType",
46 [ArgumentType.Member]: "parseEntityType",
47 [ArgumentType.Float]: "parseNumericType",
48 [ArgumentType.Integer]: "parseNumericType",
49 [ArgumentType.Number]: "parseNumericType",
50 [ArgumentType.Link]: "parseLinkType",
51 [ArgumentType.TimeInterval]: "parseTimeIntervalType"
52 };
53
54 constructor(protected readonly client: Client) {}
55
56 parseTimeIntervalType(state: ParsingState): Awaitable<ParseResult<number>> {
57 const { result, error } = stringToTimeInterval(state.currentArg!, {
58 milliseconds: state.rule.time?.unit === "ms"
59 });
60
61 if (error) {
62 throw new ArgumentParseError(`Error occurred while parsing time interval: ${error}`, "type:invalid");
63 }
64
65 const max = state.rule.time?.max;
66 const min = state.rule.time?.min;
67
68 if (min !== undefined && result < min) {
69 throw new ArgumentParseError("Time interval is less than the minimum limit", ["time:range:min", "time:range"]);
70 } else if (max !== undefined && result > max) {
71 throw new ArgumentParseError("Time interval has exceeded the maximum limit", ["time:range:max", "time:range"]);
72 }
73
74 return {
75 result
76 };
77 }
78
79 parseLinkType(state: ParsingState): Awaitable<ParseResult<string | URL>> {
80 try {
81 const url = new URL(state.currentArg!);
82
83 return {
84 result: state.rule.link?.urlObject ? url : state.currentArg!
85 };
86 } catch (error) {
87 throw new ArgumentParseError("Invalid URL", "type:invalid");
88 }
89 }
90
91 parseNumericType(state: ParsingState): Awaitable<ParseResult<number>> {
92 const number =
93 state.type === ArgumentType.Float || state.currentArg!.includes(".")
94 ? parseFloat(state.currentArg!)
95 : parseInt(state.currentArg!);
96
97 if (isNaN(number)) {
98 throw new ArgumentParseError("Invalid numeric value", "type:invalid");
99 }
100
101 const max = state.rule.number?.max;
102 const min = state.rule.number?.min;
103
104 if (min !== undefined && number < min) {
105 throw new ArgumentParseError("Numeric value is less than the minimum limit", ["number:range:min", "number:range"]);
106 } else if (max !== undefined && number > max) {
107 throw new ArgumentParseError("Numeric value exceeded the maximum limit", ["number:range:max", "number:range"]);
108 }
109
110 return {
111 result: number
112 };
113 }
114
115 async parseEntityType(state: ParsingState): Promise<ParseResult<GuildBasedChannel | User | Role | GuildMember | null>> {
116 let id = state.currentArg!;
117
118 if (!id.startsWith("<") && !id.endsWith(">") && !/^\d+$/.test(id)) {
119 throw new ArgumentParseError("Invalid entity ID", ["type:invalid"]);
120 }
121
122 switch (state.type) {
123 case ArgumentType.Role:
124 id = id.startsWith("<@&") && id.endsWith(">") ? id.substring(3, id.length - 1) : id;
125 break;
126
127 case ArgumentType.Member:
128 case ArgumentType.User:
129 id = id.startsWith("<@") && id.endsWith(">") ? id.substring(id.includes("!") ? 3 : 2, id.length - 1) : id;
130 break;
131
132 case ArgumentType.Channel:
133 id = id.startsWith("<#") && id.endsWith(">") ? id.substring(2, id.length - 1) : id;
134 break;
135
136 default:
137 logWarn("parseEntityType logic error: used as unsupported argument type handler");
138 }
139
140 try {
141 const entity = await (state.type === ArgumentType.Channel
142 ? state.parseOptions.message?.guild?.channels.fetch(id)
143 : state.type === ArgumentType.Member
144 ? state.parseOptions.message?.guild?.members.fetch(id)
145 : state.type === ArgumentType.Role
146 ? state.parseOptions.message?.guild?.roles.fetch(id)
147 : state.type === ArgumentType.User
148 ? this.client.users.fetch(id)
149 : null);
150
151 if (!entity) {
152 throw new Error();
153 }
154
155 return {
156 result: entity
157 };
158 } catch (error) {
159 if (state.rule.entity === true || (typeof state.rule.entity === "object" && state.rule.entity?.notNull)) {
160 throw new ArgumentParseError("Failed to fetch entity", "entity:null");
161 }
162
163 return {
164 result: null
165 };
166 }
167 }
168
169 parseStringType(state: ParsingState): Awaitable<ParseResult<string | null>> {
170 this.validateStringType(state);
171
172 return {
173 result: state.currentArg == "" ? null : state.currentArg
174 };
175 }
176
177 parseSnowflakeType(state: ParsingState): Awaitable<ParseResult<string>> {
178 try {
179 SnowflakeUtil.decode(state.currentArg!);
180 } catch (error) {
181 throw new ArgumentParseError("The snowflake argument is invalid", "type:invalid");
182 }
183
184 return {
185 result: state.currentArg
186 };
187 }
188
189 parseStringRestType(state: ParsingState): Awaitable<ParseResult<string>> {
190 this.validateStringType(state, ["string:rest", "string"]);
191
192 let string = state.parseOptions.input.trim().substring(state.parseOptions.prefix.length).trim();
193
194 for (let i = 0; i < Object.keys(state.parsedArgs).length + 1; i++) {
195 string = string.trim().substring(state.argv[i].length);
196 }
197
198 string = string.trim();
199
200 return {
201 result: string,
202 jump: ParserJump.Break
203 };
204 }
205
206 private validateStringType(state: ParsingState, prefixes: ("string" | "string:rest")[] = ["string"]) {
207 if (
208 (state.rule.string?.notEmpty || !state.rule.optional) &&
209 !state.currentArg?.trim() &&
210 prefixes.length === 1 &&
211 prefixes.includes("string")
212 )
213 throw new ArgumentParseError(
214 "The string must not be empty",
215 !state.rule.optional ? `required` : `${prefixes[0] as "string"}:empty`
216 );
217 else if (state.rule.string?.minLength !== undefined && (state.currentArg?.length ?? 0) < state.rule.string.minLength)
218 throw new ArgumentParseError(
219 "The string is too short",
220 prefixes.map(prefix => `${prefix}:length:min` as const)
221 );
222 else if (state.rule.string?.maxLength !== undefined && (state.currentArg?.length ?? 0) > state.rule.string.maxLength)
223 throw new ArgumentParseError(
224 "The string is too long",
225 prefixes.map(prefix => `${prefix}:length:max` as const)
226 );
227 }
228
229 async parse(parseOptions: ParseOptions) {
230 const { input, rules, prefix } = parseOptions;
231 const parsedArgs: Record<string | number, any> = {};
232 const argv = input.trim().substring(prefix.length).trim().split(/\s+/);
233 const args = [...argv];
234
235 args.shift();
236
237 const state = {
238 argv,
239 args,
240 parsedArgs,
241 index: 0,
242 currentArg: args[0],
243 rule: rules[0],
244 parseOptions
245 } as ParsingState;
246
247 let counter = 0;
248
249 ruleLoop: for (state.index = 0; state.index < rules.length; state.index++) {
250 const rule = rules[state.index];
251 state.currentArg = state.args[state.index];
252 state.rule = rules[state.index];
253
254 let result = null,
255 lastError: ArgumentParseError | null = null;
256
257 if (!state.currentArg) {
258 if (!rule.optional) {
259 return { error: rule.errors?.["required"] ?? `Argument #${state.index} is required` };
260 } else if (rule.default !== undefined) {
261 result = {
262 result: rule.default
263 } satisfies ParseResult;
264 }
265
266 state.parsedArgs[rule.name ?? counter++] = result?.result ?? null;
267 continue;
268 }
269
270 for (const type of rule.types) {
271 const parser = this.parsers[type];
272 const handler = this[parser] as Function;
273
274 if (!parser || !handler) {
275 throw new Error(`Parser for type "${ArgumentType[type]}" is not implemented.`);
276 }
277
278 state.type = type;
279
280 try {
281 result = await handler.call(this, state);
282 lastError = null;
283 } catch (error) {
284 if (error instanceof ArgumentParseError) {
285 lastError = error;
286 }
287 }
288
289 if (!lastError) {
290 break;
291 }
292
293 if (result?.jump === ParserJump.Break) break ruleLoop;
294 else if (result?.jump === ParserJump.Next) continue ruleLoop;
295 else if (result?.jump === ParserJump.Steps) {
296 state.index += result?.steps ?? 1;
297 break;
298 }
299 }
300
301 if (lastError) {
302 let errorMessage: string | undefined;
303
304 if (typeof lastError.type === "string") {
305 errorMessage = rule.errors?.[lastError.type];
306 } else {
307 for (const type of lastError.type) {
308 errorMessage = rule.errors?.[type];
309
310 if (errorMessage) {
311 break;
312 }
313 }
314 }
315
316 return { error: errorMessage ?? lastError.message };
317 }
318
319 if (!result) {
320 return { error: `Failed to parse argument #${state.index}` };
321 }
322
323 state.parsedArgs[rule.name ?? counter++] = result.result;
324 }
325
326 return {
327 parsedArgs
328 };
329 }
330 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26