Skip to content

Instantly share code, notes, and snippets.

@Floffah
Created August 17, 2021 14:19
Show Gist options
  • Save Floffah/f4d061a121f3020277094d4f017c4076 to your computer and use it in GitHub Desktop.
Save Floffah/f4d061a121f3020277094d4f017c4076 to your computer and use it in GitHub Desktop.
// ...
export default class IncomingCommand {
// ...
/**
* Parses a commands args into valid interaction options (for compatibility and efficiency)
*/
async parseCommand() {
if (
typeof this.content !== "string" ||
typeof this.message === "undefined"
)
throw new Error("Must be a message based command");
let finallist: CommandInteractionOption[] = [];
// options to use
let cmdopts = this.command.options ?? [];
// how many args are required
let argsneeded = 0;
// on demand argsneeded calculation (to support groups more efficiently)
const calcNeeded = () => {
argsneeded = 0;
for (const opt of cmdopts) {
if (opt.required || opt.type === "SUB_COMMAND") argsneeded += 1;
}
};
calcNeeded();
// if the message contains arguments
if (
this.content !== "" &&
!/^\s+$/.test(this.content) &&
this.command.options
) {
// commands groups mapped to indexes in the cmdopts variable
const groups: { [name: string]: number } = {};
// commands groups mapped to what options they take
const groupopts: {
[name: string]: ApplicationCommandOption[] | undefined;
} = {};
// the current group being parsed
let group: string | undefined = undefined;
// for each opt, if the opt is group relating, populate groups and groupopts
for (let i = 0; i < cmdopts.length; i++) {
const opt = cmdopts[i];
if (opt.type === "SUB_COMMAND") {
groups[opt.name.toLowerCase()] = i;
groupopts[opt.name.toLowerCase()] = opt.options;
}
}
// the command args
const args = this.content.split(" ");
// index of cmdopts
let optindex = 0;
// if a multi arg ("some sentence" rather than aSingleArgument) is being parsed, the option name, and the content found already
let stringing: { str: string; name: string } | undefined =
undefined;
// if the loop ran into the last option that has a value, keep adding content until the end of the args, and the last option's name
let restofstring: { str: string; name: string } | undefined =
undefined;
// for each argument
for (let argindex = 0; argindex < args.length; argindex++) {
const arg = args[argindex];
// if the argument is just whitespace
if (arg === "" || /^\s+$/.test(arg)) continue;
// if parsing the last option and it maps to multiple arguments
if (restofstring) {
if (argindex + 1 >= args.length) {
// found the end
finallist.push({
name: restofstring.name,
type: "STRING",
value: `${restofstring.str} ${arg}`,
});
optindex += 1;
} else {
// still parsing
restofstring.str += ` ${arg}`;
}
// if found a multi arg string ("some sentence" rather than aSingleArgument)
} else if (stringing) {
if (arg.endsWith('"')) {
// found the end
finallist.push({
name: stringing.name,
type: "STRING",
value: `${stringing.str} ${arg.replace(/"$/, "")}`,
});
stringing = undefined;
optindex += 1;
} else {
// still parsing
stringing.str += ` ${arg}`;
}
// if the argument is a group and groups are available
} else if (
optindex <= 0 &&
typeof groups[arg.toLowerCase()] !== "undefined"
) {
group = arg.toLowerCase();
cmdopts = groupopts[group] ?? [];
// re-calculate args needed based on opts accepted by the command group
calcNeeded();
// if there is still opts available
} else if (cmdopts[optindex]) {
const opt = cmdopts[optindex];
// if the arg is a mention
if (/<@!?[0-9]+>/.test(arg) && opt.type === "USER") {
const id = arg.replace(/(^<@!?|>$)/g, "");
// fetch it and set option if exists
let fetched: GuildMember | undefined = undefined;
let fetcheduser: User | undefined = undefined;
try {
fetched = await this.message.guild?.members.fetch(
id as Snowflake,
);
} catch (e) {
fetched = undefined;
try {
fetcheduser = await this.bot.users.fetch(
id as Snowflake,
);
} catch (e) {
fetcheduser = undefined;
}
}
if (!fetched && !fetcheduser)
throw `Could not find user ${arg} for argument ${
opt.name
}\n\n${this.getUsage(cmdopts, group)}`;
finallist.push({
name: opt.name,
type: "USER",
member: fetched,
user: fetched?.user ?? fetcheduser,
});
optindex += 1;
// if the arg is a role mention
} else if (/<@&!?[0-9]+>/.test(arg)) {
const id = arg.replace(/(^<@&!?|>$)/g, "");
// fetch role and set option if exists
const fetched = await this.message.guild?.roles.fetch(
id as Snowflake,
);
if (!fetched)
throw `Could not find role ${arg} for argument ${
opt.name
}\n\n${this.getUsage(cmdopts, group)}`;
finallist.push({
name: opt.name,
type: "ROLE",
role: fetched,
});
optindex += 1;
// if the arg is a channel mention
} else if (/<#!?[0-9]+>/.test(arg)) {
const id = arg.replace(/(^<#!?|>$)/g, "");
// fetch channel and set option if exists
const fetched =
await this.message.guild?.channels.fetch(
id as Snowflake,
);
if (!fetched)
throw `Could not find channel ${arg} for argument ${
opt.name
}\n\n${this.getUsage(cmdopts, group)}`;
finallist.push({
name: opt.name,
type: "CHANNEL",
channel: fetched,
});
optindex += 1;
// if the arg is a string id and the next option is a user, role, or channel
} else if (
/[0-9]+/.test(arg) &&
["USER", "CHANNEL", "ROLE"].includes(opt.type)
) {
// if the next option is a user
if (opt.type === "USER") {
// fetch it and set option if exists
let fetched: GuildMember | undefined = undefined;
let fetcheduser: User | undefined = undefined;
try {
fetched =
await this.message.guild?.members.fetch(
arg as Snowflake,
);
} catch (e) {
fetched = undefined;
try {
fetcheduser = await this.bot.users.fetch(
arg as Snowflake,
);
} catch (e) {
fetcheduser = undefined;
}
}
if (!fetched && !fetcheduser)
throw `Could not find user ${arg} for argument ${
opt.name
}\n\n${this.getUsage(cmdopts, group)}`;
finallist.push({
name: opt.name,
type: "USER",
member: fetched,
user: fetched?.user ?? fetcheduser,
});
optindex += 1;
// if the next option is a role
} else if (opt.type === "ROLE") {
// fetch the role and set option if exists
const fetched =
await this.message.guild?.roles.fetch(
arg as Snowflake,
);
if (!fetched)
throw `Could not find role ${arg} for argument ${
opt.name
}\n\n${this.getUsage(cmdopts, group)}`;
finallist.push({
name: opt.name,
type: "ROLE",
role: fetched,
});
optindex += 1;
// if the next option is a channel
} else if (opt.type === "CHANNEL") {
// fetch channel and set option if xists
const fetched =
await this.message.guild?.channels.fetch(
arg as Snowflake,
);
if (!fetched)
throw `Could not find channel ${arg} for argument ${
opt.name
}\n\n${this.getUsage(cmdopts, group)}`;
finallist.push({
name: opt.name,
type: "CHANNEL",
channel: fetched,
});
optindex += 1;
// this else should never be reached, but just in case, throw an error
} else {
throw `Incorrect value type for ${
opt.type
}\n\n${this.getUsage(cmdopts, group)}`;
}
// if the next option is an integer and the arg is a number like 1234 or decimal like 1234.4321
} else if (
/[0-9]+(\.[0-9]+)?/.test(arg) &&
opt.type === "INTEGER"
) {
// set the option (allow NaN, command should independently recognise this and fail)
finallist.push({
name: opt.name,
type: "INTEGER",
value: parseInt(arg),
});
optindex += 1;
// if the option is anything else (any character excluding whitespace characters (tab, space, carriage return, line feed, etc)
} else if (/^\S+/.test(arg)) {
if (opt.type === "STRING") {
// if the there are no more options but there are more arguments
if (
optindex + 1 >= cmdopts.length &&
argindex + 1 < args.length
) {
// start rest of string tracking
restofstring = {
str: arg,
name: opt.name,
};
// if the arg starts with a ", it must be a stringing multi-arg
} else if (arg.startsWith('"')) {
// start stringing tracking
stringing = {
str: arg.replace(/^"/, ""),
name: opt.name,
};
// if its not a restofstring or stringing arg
} else {
// make sure its allowed
if (opt.choices) {
const raw = opt.choices.map((c) =>
c.name.toLowerCase(),
);
if (!raw.includes(arg.toLowerCase()))
throw `${arg.toLowerCase()} does not exist in choices of ${raw.join(
", ",
)}`;
else {
// safe to cast as the statement above determines it will exist if we reach this
const choice = opt.choices.find(
(c) =>
c.name.toLowerCase() ===
arg.toLowerCase(),
) as ApplicationCommandOptionChoice;
// set the option as the choice's set value
finallist.push({
name: opt.name,
type: "STRING",
value: choice.value,
});
}
} else {
// set the option as the raw arg
finallist.push({
name: opt.name,
type: "STRING",
value: arg,
});
}
optindex += 1;
}
// if the next option is a boolean
} else if (opt.type === "BOOLEAN") {
// set the boolean acceptiong yes/no true/false y/n without being case sensitive
finallist.push({
name: opt.name,
type: "STRING",
value:
arg.toLowerCase() === "yes" ||
arg.toLowerCase() === "true" ||
arg.toLowerCase() === "y",
});
optindex += 1;
// this probably wont be reached, but just in case, throw an error
} else {
throw `Incorrect value type for ${
opt.type
}\n\n${this.getUsage(cmdopts, group)}`;
}
// this also probably wont be reached
} else {
throw `Incorrect value type for ${
opt.type
}\n\n${this.getUsage(cmdopts, group)}`;
}
// somehow there are too many arguments
} else {
throw `Too many arguments\n\n${this.getUsage(
cmdopts,
group,
)}`;
}
}
// tell the user if they didnt provide enough arguments
if (finallist.length < argsneeded)
throw `Not enough arguments\n\n${this.getUsage(
cmdopts,
group,
)}`;
// if there is a group, set the final option collection to wrap the group options
if (group) {
finallist = [
{
type: "SUB_COMMAND",
name: group,
options: finallist,
},
];
}
} else {
// there was no arguments or nothing to parse, Efficiency™
this.bot.logger.debug("Nothing to parse");
}
// just to make sure, the block inside this statement will probably never be executed
if (finallist.length < argsneeded)
throw `Not enough arguments\n\n${this.getUsage(cmdopts)}`;
this.options = new CommandInteractionOptionResolver(
this.bot,
finallist,
);
return;
}
// ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment