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