diff --git a/README.md b/README.md index 5cdbf1b..2d85377 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -2b-bot-2 +# 2B Bot 2 + +A typescript rewrite of my discordbot 2b. diff --git a/src/Bot.ts b/src/Bot.ts index 20f288f..b09e160 100644 --- a/src/Bot.ts +++ b/src/Bot.ts @@ -1,12 +1,18 @@ import {Client, Guild} from "discord.js"; -import { Config } from "./lib/utils/Config"; -import { DefaultConfig } from "./lib/utils/DefaultConfig"; +import {Config} from "./lib/utils/Config"; +import {DefaultConfig} from "./lib/utils/DefaultConfig"; import * as path from "path"; import * as fsx from "fs-extra"; import * as yaml from "js-yaml"; import {BotLogger} from "./lib/utils/BotLogger"; import {DataHandler} from "./lib/DataHandler"; import {GuildHandler} from "./lib/GuildHandler"; +import {CommandCollection} from "./lib/CommandCollection"; +import {Command} from "./lib/Command"; +import {globalCommands} from "./commands/global"; +import {privateCommands} from "./commands/private"; +import {parseMessage} from "./lib/utils"; +import {CommandPermission} from "./lib/CommandPermission"; const configFile = "config.yaml"; @@ -18,6 +24,7 @@ export class Bot { public readonly config: Config; public readonly logger: BotLogger; public readonly dataHandler: DataHandler; + private commandCollection: CommandCollection; private guildHandlers: any = {}; /** @@ -28,6 +35,9 @@ export class Bot { this.logger = new BotLogger(this.config.logging.directory, this.config.logging.level); this.client = new Client(); this.dataHandler = new DataHandler(this); + this.commandCollection = new CommandCollection(); + this.commandCollection.include(globalCommands); + this.commandCollection.include(privateCommands); } /** @@ -69,6 +79,16 @@ export class Bot { if (message.guild) { const handler = await this.getGuildHandler(message.guild); await handler.onMessage(message); + } else { + this.logger.debug(` ${message.content}`); + const userPermission = this.config.owners.includes(message.author.tag) ? + CommandPermission.OWNER : CommandPermission.REGULAR; + const CommandClass = parseMessage(message, this.commandCollection, + this.config.prefix, userPermission); + if (CommandClass) { + const command = new CommandClass(this); + command.invoke(message); + } } } }); diff --git a/src/commands/global/index.ts b/src/commands/global/index.ts index 47bc2ca..9476fdb 100644 --- a/src/commands/global/index.ts +++ b/src/commands/global/index.ts @@ -1,4 +1,6 @@ import {CommandCollection} from "../../lib/CommandCollection"; +import {Ping} from "./utility/Ping"; export const globalCommands = new CommandCollection([ + Ping, ]); diff --git a/src/commands/global/utility/Ping.ts b/src/commands/global/utility/Ping.ts new file mode 100644 index 0000000..96c4f6a --- /dev/null +++ b/src/commands/global/utility/Ping.ts @@ -0,0 +1,17 @@ +import {Command} from "../../../lib/Command"; +import {CommandPermission} from "../../../lib/CommandPermission"; +import {Message} from "discord.js"; + +export class Ping extends Command { + public static commandName = "ping"; + public static permission = CommandPermission.REGULAR; + public static description = "Replies with the bots ping."; + + /** + * Replies with the current ping. + * @param msg + */ + public invoke(msg: Message): void { + msg.channel.send(`My latency is **${Math.round(this.bot.client.ping)}** ms.`); + } +} diff --git a/src/commands/guild/index.ts b/src/commands/guild/index.ts index 600d504..27d10c8 100644 --- a/src/commands/guild/index.ts +++ b/src/commands/guild/index.ts @@ -1,6 +1,10 @@ import {CommandCollection} from "../../lib/CommandCollection"; import {AddAdminRoles} from "./utility/AddAdminRoles"; +import {SetPrefix} from "./utility/SetPrefix"; +import {RemoveAdminRoles} from "./utility/RemoveAdminRoles"; export const guildCommands = new CommandCollection([ AddAdminRoles, + SetPrefix, + RemoveAdminRoles, ]); diff --git a/src/commands/guild/utility/AddAdminRoles.ts b/src/commands/guild/utility/AddAdminRoles.ts index 8b09856..0cf5571 100644 --- a/src/commands/guild/utility/AddAdminRoles.ts +++ b/src/commands/guild/utility/AddAdminRoles.ts @@ -12,12 +12,15 @@ export class AddAdminRoles extends GuildCommand { */ public async invoke(msg: Message) { const args = AddAdminRoles.getArgs(msg.content); - this.bot.logger.debug(args[0]); if (args.length < 2) { msg.channel.send("No argument for role names provided."); } else { const roles = args.splice(1); - this.guildHandler.settings.adminRoles.push(...roles); + for (const role of roles) { + if (this.guildHandler.settings.adminRoles.includes(role)) { + this.guildHandler.settings.adminRoles.push(role); + } + } msg.channel.send(`Added **${roles.join("**, **")}** to the admin roles.`); } } diff --git a/src/commands/guild/utility/RemoveAdminRoles.ts b/src/commands/guild/utility/RemoveAdminRoles.ts new file mode 100644 index 0000000..5e58faa --- /dev/null +++ b/src/commands/guild/utility/RemoveAdminRoles.ts @@ -0,0 +1,24 @@ +import {CommandPermission} from "../../../lib/CommandPermission"; +import {GuildCommand} from "../../../lib/GuildCommand"; +import {Message} from "discord.js"; + +export class RemoveAdminRoles extends GuildCommand { + public static commandName = "removeAdminRoles"; + public static description = "Removes one or more roles from the configured roles"; + public static permission = CommandPermission.ADMIN; + + /** + * Removes all specified roles from the admin roles. + */ + public invoke(msg: Message): Promise | void { + const args = RemoveAdminRoles.getArgs(msg.content); + if (args.length < 2) { + msg.channel.send("No argument for role names provided."); + } else { + const roles = args.splice(1); + const adminRoles = this.guildHandler.settings.adminRoles; + this.guildHandler.settings.adminRoles = adminRoles.filter((role) => !roles.includes(role)); + msg.channel.send(`Removed **${roles.join("**, **")}** from the admin roles.`); + } + } +} diff --git a/src/commands/guild/utility/SetPrefix.ts b/src/commands/guild/utility/SetPrefix.ts new file mode 100644 index 0000000..9aed402 --- /dev/null +++ b/src/commands/guild/utility/SetPrefix.ts @@ -0,0 +1,27 @@ +import {GuildCommand} from "../../../lib/GuildCommand"; +import {CommandPermission} from "../../../lib/CommandPermission"; +import {Message} from "discord.js"; + +export class SetPrefix extends GuildCommand { + public static commandName = "setPrefix"; + public static permission = CommandPermission.ADMIN; + + /** + * Sets the prefix for a guild. + * @param msg + */ + public invoke(msg: Message): Promise | void { + const args = SetPrefix.getArgs(msg.content); + if (args.length > 1) { + const prefix: string = args[1]; + if (/^\S+$/.test(prefix)) { + this.guildHandler.settings.prefix = prefix; + msg.channel.send(`Changed the command prefix to **${prefix}**`); + } else { + msg.channel.send(`**${prefix}** Is not a valid prefix. \nA prefix must be a non-whitespace sequence of characters.`); + } + } else { + msg.channel.send("You need to provide a prefix as commadn argument."); + } + } +} diff --git a/src/lib/Command.ts b/src/lib/Command.ts index 5105f53..89f8c99 100644 --- a/src/lib/Command.ts +++ b/src/lib/Command.ts @@ -11,6 +11,11 @@ export abstract class Command { */ public static commandName: string; + /** + * The description of the command + */ + public static description: string; + /** * The time to live in seconds before the instance is deleted. */ @@ -31,17 +36,17 @@ export abstract class Command { this.createdAt = Date.now(); } - public invoke?(msg: Message): Promise; + public invoke?(msg: Message): Promise|void; /** * A function that is executed when the answer to the command was sent. */ - public onSent?(answer: Message): Promise; + public onSent?(answer: Message): Promise|void; /** * A function that is executed when a reaction is added to the command answer */ - public onReaction?(reaction: MessageReaction): Promise; + public onReaction?(reaction: MessageReaction): Promise|void; /** * returns the name of the command to make it accessible. diff --git a/src/lib/GuildHandler.ts b/src/lib/GuildHandler.ts index 295e399..6207a59 100644 --- a/src/lib/GuildHandler.ts +++ b/src/lib/GuildHandler.ts @@ -9,13 +9,14 @@ import {globalCommands} from "../commands/global"; import {Command} from "./Command"; import {BotLogger} from "./utils/BotLogger"; import {ProxyEventEmitter} from "./utils/ProxyEventEmitter"; +import {parseMessage} from "./utils"; /** * Handles all guild related tasks. */ export class GuildHandler { private guild: Guild; - private bot: Bot; + private readonly bot: Bot; private guildData: GuildData; private guildSettings: GuildSettings; private guildSettingsProxy: ProxyEventEmitter; @@ -61,26 +62,14 @@ export class GuildHandler { * @param message */ public async onMessage(message: Message): Promise { - const commandPattern = new RegExp( `\\s*${this.guildSettings.prefix}(\\w+)`); - - this.logger.debug(`Command Pattern is: ${commandPattern}`); this.logger.debug(`<${this.guild.name}:${message.author.tag}>"${message.content}"`); - if (commandPattern.test(message.content)) { - this.logger.debug("Message matches command syntax."); - const commandString = commandPattern.exec(message.content)[1]; - const CommandClass = this.commandCollection.findByName(commandString); + const CommandClass = parseMessage(message, this.commandCollection, + this.settings.prefix, this.getMembersHighestRole(message.member)); - if (CommandClass) { - this.logger.debug(`${commandString} -> ${CommandClass.name}`); - this.logger.debug(`Member Permission: ${this.getMembersHighestRole(message.member)}, command permission: ${CommandClass.permission}`); - if (CommandClass.permission <= this.getMembersHighestRole(message.member)) { - const command = new CommandClass(this.bot, this); - await command.invoke(message); - } else { - message.channel.send("You don't have permission for that command."); - } - } + if (CommandClass) { + const command = new CommandClass(this.bot, this); + await command.invoke(message); } } @@ -99,9 +88,9 @@ export class GuildHandler { private getMembersHighestRole(guildMember: GuildMember) { const adminRoles = this.guildSettings.adminRoles; const djRoles = this.guildSettings.djRoles; - if (adminRoles.find((role) => GuildHandler.memberHasRole(guildMember, role)).length > 0) { + if (adminRoles.find((role) => GuildHandler.memberHasRole(guildMember, role))) { return CommandPermission.ADMIN; - } else if (djRoles.find((role) => GuildHandler.memberHasRole(guildMember, role)).length > 0) { + } else if (djRoles.find((role) => GuildHandler.memberHasRole(guildMember, role))) { return CommandPermission.DJ; } else { return CommandPermission.REGULAR; @@ -114,6 +103,6 @@ export class GuildHandler { * @param roleName */ private static memberHasRole(guildMember: GuildMember, roleName: string) { - return guildMember.roles.filter((role, key) => role.name === roleName).size > 0; + return guildMember.roles.filter((role) => role.name === roleName).size > 0; } } diff --git a/src/lib/GuildSettings.ts b/src/lib/GuildSettings.ts index 92ec65a..72040be 100644 --- a/src/lib/GuildSettings.ts +++ b/src/lib/GuildSettings.ts @@ -1,5 +1,15 @@ +import {Config} from "./utils/Config"; + export class GuildSettings { + /** + * Constructor with config that assigns some config specific stuff. + * @param config + */ + constructor(config: Config) { + this.prefix = config.prefix; + } + /** * The prefix of the bot. */ diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index 9dcffe1..3a3fcbc 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -1,6 +1,7 @@ import {Table, Column, Model, NotNull} from "sequelize-typescript"; import {JSON as SQJSON} from "sequelize"; import {GuildSettings} from "../GuildSettings"; +import {DefaultConfig} from "../utils/DefaultConfig"; @Table({underscored: true}) export class Guild extends Model { @@ -10,6 +11,7 @@ export class Guild extends Model { public guildId: string; @NotNull - @Column({allowNull: false, type: SQJSON, defaultValue: new GuildSettings()}) + // @ts-ignore + @Column({allowNull: false, type: SQJSON, defaultValue: new GuildSettings(new DefaultConfig())}) public settings: GuildSettings; } diff --git a/src/lib/utils/Config.ts b/src/lib/utils/Config.ts index 7689b6f..119398b 100644 --- a/src/lib/utils/Config.ts +++ b/src/lib/utils/Config.ts @@ -6,11 +6,6 @@ export abstract class Config { */ public presenceDuration: number = 300000; - /** - * The maximum number of commands in a sequence. - */ - public maxCommandSequenceLength: number = 10; - /** * The number of commands that are allowed in a minute by one user. */ @@ -38,4 +33,9 @@ export abstract class Config { * The owners of the bot that have elevated privileges */ public owners: string[]; + + /** + * The prefix of the bot. + */ + public prefix: string; } diff --git a/src/lib/utils/DefaultConfig.ts b/src/lib/utils/DefaultConfig.ts index c36eeac..1e0a70e 100644 --- a/src/lib/utils/DefaultConfig.ts +++ b/src/lib/utils/DefaultConfig.ts @@ -6,11 +6,6 @@ export class DefaultConfig extends Config { */ public presenceDuration: number = 300000; - /** - * The maximum number of commands in a sequence. - */ - public maxCommandSequenceLength: number = 10; - /** * The number of commands a user is allowed to execute in one minute. */ @@ -36,4 +31,9 @@ export class DefaultConfig extends Config { * The owners of the bot that have elevated privileges */ public owners: string[] = []; + + /** + * The prefix of the bot. + */ + public prefix: string = "~"; } diff --git a/src/lib/utils/ProxyEventEmitter.ts b/src/lib/utils/ProxyEventEmitter.ts index 254653d..cbc900d 100644 --- a/src/lib/utils/ProxyEventEmitter.ts +++ b/src/lib/utils/ProxyEventEmitter.ts @@ -1,6 +1,8 @@ import {EventEmitter} from "events"; -import {type} from "os"; +/** + * An event emitter that can be used to listen to events regarding object properties. + */ export class ProxyEventEmitter extends EventEmitter implements ProxyHandler { /** diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..56ccef8 --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1,31 @@ +import {Message} from "discord.js"; +import {CommandPermission} from "../CommandPermission"; +import {CommandCollection} from "../CommandCollection"; +import {Command} from "../Command"; + +/** + * Parses a message and returns the corresponding command. + * @param message + * @param commandCollection + * @param prefix + * @param userPermission + */ +export function parseMessage(message: Message, commandCollection: CommandCollection, + prefix: string, userPermission: CommandPermission = 0): any { + + const commandPattern = new RegExp( `\\s*${prefix}(\\w+)`); + + if (commandPattern.test(message.content)) { + const commandString = commandPattern.exec(message.content)[1]; + const CommandClass = commandCollection.findByName(commandString); + + if (CommandClass) { + if (CommandClass.permission <= userPermission) { + return CommandClass; + } else { + message.channel.send("You don't have permission for that command."); + } + } + } + return false; +}