249 lines
9.3 KiB
TypeScript
249 lines
9.3 KiB
TypeScript
import { Client, ClientOptions, Collection, GuildChannel, GuildMember, Message } from "discord.js";
|
|
import config from "./config.json";
|
|
import strings from "./clientstrings.json";
|
|
import { Sequelize } from "sequelize";
|
|
import { CommandList } from "./commands/commandlist";
|
|
import { Command } from "./commands/command";
|
|
import { BlacklistUser } from "./models/blacklistuser";
|
|
import { CommandBlacklist } from "./models/commandblacklist";
|
|
import { SlashCommand } from "./slash/slash";
|
|
import { SlashCommandList } from "./slash/commandlist";
|
|
|
|
export class CommandClient extends Client {
|
|
constructor (options: ClientOptions, db: Sequelize) {
|
|
super(options);
|
|
|
|
process.on('exit', this.destroy);
|
|
|
|
process.on('SIGINT', this.destroy);
|
|
|
|
this.db = db;
|
|
|
|
for (const command of CommandList) {
|
|
this.commands.set(command.name, command);
|
|
}
|
|
|
|
for (const command of SlashCommandList) {
|
|
this.slashCommands.set(command.name, command);
|
|
}
|
|
|
|
this.on('messageCreate', async (msg: Message) => {
|
|
if (!msg.content.startsWith(config.prefix) || msg.author.bot) return;
|
|
|
|
if (!config.owners.includes(msg.author.id) && (await BlacklistUser.findOne({ where: { user_id: msg.author.id } }))) return;
|
|
|
|
this.parseCommand(msg);
|
|
});
|
|
|
|
this.on('ready', () => {
|
|
console.log(`Logged in as ${this.user!.tag}`);
|
|
});
|
|
|
|
this.on('interactionCreate', async (int) => {
|
|
if (int.isCommand()) {
|
|
if (!int.channel) return;
|
|
|
|
const command = this.slashCommands.get(int.commandName);
|
|
|
|
if (!command) throw new Error('Slash command does not exist');
|
|
|
|
if (!int.guild && command.guildOnly) {
|
|
int.reply({ content: strings.command.guildOnly, ephemeral: true });
|
|
return;
|
|
}
|
|
|
|
if (command.permission && !config.owners.includes(int.user.id)) {
|
|
if (!(int.channel instanceof GuildChannel)) {
|
|
int.reply({ content: strings.command.guildOnly, ephemeral: true });
|
|
return;
|
|
}
|
|
for (const permission of command.permission) {
|
|
if (!(int.member as GuildMember).permissionsIn(int.channel).has(permission)){
|
|
int.reply({ content: strings.command.noPermission, ephemeral: true });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
command.execute(int).catch(error => {
|
|
int.reply({ content: strings.error, ephemeral: true })
|
|
.catch(error => console.error(error));
|
|
return console.error(error);
|
|
});
|
|
return console.info(`${int.user.tag} (${int.user.id}) used ${command.name} in ${'name' in int.channel ? int.channel.name : 'DM CHANNEL'} (${int.channel.id})`);
|
|
}
|
|
else if (int.isAutocomplete()) {
|
|
if (!int.channel) return;
|
|
|
|
const command = this.slashCommands.get(int.commandName);
|
|
|
|
if (!command) throw new Error('Slash command does not exist');
|
|
|
|
command.autocomplete?.(int).catch(error => {
|
|
return console.error(error);
|
|
});
|
|
}
|
|
});
|
|
|
|
this.login(config.token);
|
|
}
|
|
|
|
/**
|
|
* Gives an array of single or multi-word arguments
|
|
* @param msg
|
|
* @returns
|
|
*/
|
|
static formatArgs (msg: string): string[] {
|
|
const args = msg.match(/("[^"]+")|(\S+)/g);
|
|
if (args === null) return [];
|
|
for (let [i, arg] of args.entries()) {
|
|
args[i] = arg.replace(/(^"|"$)/g, '');
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
async parseCommand (msg: Message): Promise<Command | undefined> {
|
|
const args = CommandClient.formatArgs(msg.content.slice(config.prefix.length));
|
|
|
|
if (args.length < 1) return;
|
|
const commandName = args.shift()!.toLowerCase();
|
|
|
|
const command: Command | undefined = this.commands.get(commandName)
|
|
|| this.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
|
|
|
|
if (!command) {
|
|
this.nonCommandParse();
|
|
return;
|
|
};
|
|
|
|
if (command.ownerOnly && !config.owners.includes(msg.author.id)) {
|
|
msg.reply(strings.command.ownerOnly);
|
|
return;
|
|
}
|
|
|
|
if (msg.channel instanceof GuildChannel) {
|
|
if (!msg.member) throw new Error('Command sending member does not exist');
|
|
|
|
if (!config.owners.includes(msg.author.id)) {
|
|
if (command.permission) {
|
|
for (const permission of command.permission) {
|
|
if (!msg.member.permissionsIn(msg.channel).has(permission)) {
|
|
msg.reply(strings.command.noPermission);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const member = (await CommandBlacklist.findOrCreate({ where: { user_id: msg.author.id, guild_id: msg.guild!.id } }))[0];
|
|
if (JSON.parse(member.blacklist)[command.name]) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(command.guildOnly && !(msg.channel instanceof GuildChannel)) {
|
|
msg.reply(strings.command.guildOnly);
|
|
return;
|
|
}
|
|
|
|
if (command.args.length > 0 && !command.args[0].optional && args.length === 0) {
|
|
let reply = strings.command.noArguments;
|
|
|
|
if (command.usage) {
|
|
reply += `\n${strings.command.usagePrefix} \`${config.prefix}${command.name} ${command.usage}\``;
|
|
}
|
|
|
|
msg.channel.send(reply);
|
|
return;
|
|
}
|
|
|
|
if (args.length < command.args.filter(arg => {return !arg.optional}).length) {
|
|
let reply = strings.command.missingArguments;
|
|
|
|
if (command.usage) {
|
|
reply += `\n${strings.command.usagePrefix} \`${config.prefix}${command.name} ${command.usage}\``;
|
|
}
|
|
|
|
msg.channel.send(reply);
|
|
return;
|
|
}
|
|
|
|
if (command.cooldown && !config.owners.includes(msg.author.id)) {
|
|
if (!this.cooldowns.has(command.name)) {
|
|
this.cooldowns.set(command.name, new Collection());
|
|
}
|
|
|
|
const now = Date.now();
|
|
const timestamps = this.cooldowns.get(command.name);
|
|
|
|
if (!timestamps) throw new Error('Command lacks cooldown collection');
|
|
|
|
if (timestamps.has(msg.author.id)) {
|
|
const expirationTime = (timestamps.get(msg.author.id) as number) + command.cooldown;
|
|
const timeLeft = (expirationTime - now) / 1000;
|
|
msg.reply(strings.command.onCooldown.replace('{1}', timeLeft.toFixed(1)).replace('{2}', command.name));
|
|
return;
|
|
}
|
|
|
|
timestamps.set(msg.author.id, now);
|
|
setTimeout(() => timestamps.delete(msg.author.id), command.cooldown);
|
|
}
|
|
|
|
try {
|
|
return this.runCommand(msg, command, args);
|
|
}
|
|
catch (error) {
|
|
console.error(error);
|
|
msg.reply(strings.error);
|
|
}
|
|
}
|
|
|
|
nonCommandParse () {
|
|
return;
|
|
}
|
|
|
|
runCommand (msg: Message, command: Command, args: string[]): Command | undefined {
|
|
if (command.args.length > 0 && !command.args[command.args.length-1].infinite) {
|
|
args[command.args.length-1] = args.slice(command.args.length-1, args.length).join(' ');
|
|
}
|
|
|
|
const parsedArgs: any[] = [];
|
|
for(var i = 0; i < command.args.length; i++) {
|
|
try {
|
|
if (!command.args[i].infinite) {
|
|
if (!command.args[i].type.validate(args[i], msg)) throw new Error('Argument is invalid');
|
|
parsedArgs[i] = command.args[i].type.parse(args[i], msg);
|
|
}
|
|
else {
|
|
const infinite = [];
|
|
for (var j = i; j < args.length; j++) {
|
|
if (!command.args[i].type.validate(args[j], msg)) throw new Error('Argument is invalid');
|
|
infinite.push(command.args[i].type.parse(args[j], msg));
|
|
}
|
|
|
|
parsedArgs[i] = infinite;
|
|
}
|
|
|
|
if (command.args[i].validator && !command.args[i].validator?.(args[i], msg)) throw new Error('Argument is invalid');
|
|
}
|
|
catch {
|
|
msg.reply(strings.command.invalidArgument.replace('{1}', args[i]));
|
|
return;
|
|
}
|
|
}
|
|
const keyedArgs: { [key: string]: any } = {};
|
|
for (var i = 0; i < command.args.length; i++) {
|
|
keyedArgs[command.args[i].key] = parsedArgs[i];
|
|
}
|
|
|
|
console.info(`${msg.author.tag} (${msg.author.id}) used ${command.name} in ${'name' in msg.channel ? msg.channel.name : 'DM CHANNEL'} (${msg.channel.id})`);
|
|
command.execute(msg, keyedArgs);
|
|
return command;
|
|
}
|
|
|
|
readonly commands = new Collection<string, Command>();
|
|
readonly slashCommands = new Collection<string, SlashCommand>();
|
|
readonly cooldowns = new Collection<string, Collection<string, number>>();
|
|
db: Sequelize
|
|
} |