diff --git a/discord-bot-core/.gitrepo b/discord-bot-core/.gitrepo index dbfcf16..e920257 100644 --- a/discord-bot-core/.gitrepo +++ b/discord-bot-core/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:benji7425/discord-bot-core.git branch = master - commit = 81a8c678cad14ed8162dd52475bff7ed02b6bc4e - parent = 052a0bf7e6a782290e50aa54f504be670dff776d + commit = 180d069b012d38d6d3c539df5a04475683139b01 + parent = 176b7b8ad9ca41d1b9f7a15869ed5ed95ccad25d method = merge cmdver = 0.3.1 diff --git a/discord-bot-core/BaseGuildData.js b/discord-bot-core/BaseGuildData.js new file mode 100644 index 0000000..2db73d2 --- /dev/null +++ b/discord-bot-core/BaseGuildData.js @@ -0,0 +1,6 @@ +module.exports = class BaseGuildData { + /**@param param */ + constructor(id) { + this.id = id; + } +}; \ No newline at end of file diff --git a/discord-bot-core/HandleMessage.js b/discord-bot-core/HandleMessage.js new file mode 100644 index 0000000..9202684 --- /dev/null +++ b/discord-bot-core/HandleMessage.js @@ -0,0 +1,35 @@ +// @ts-ignore +const ParentPackageJSON = require("../package.json"); + +/**@param param*/ +function handleMessage(message, commands, guildData) { + if (!message.content.startsWith(message.guild.me.toString())) //criteria for a command is the bot being tagged + return; + + const botName = "@" + (message.guild.me.nickname || message.guild.me.user.username), + isMemberAdmin = message.member.permissions.has("ADMINISTRATOR"), + split = message.content.split(/ +/), + params = split.slice(2, split.length), + command = commands[Object.keys(commands).find(x => commands[x].name.toLowerCase() === (split[1] || "").toLowerCase())]; + + if (!command) + handleInternalCommand(message, params); + else if (params.length < command.expectedParamCount) + message.reply(`Incorrect syntax!\n**Expected:** *${botName} ${command.syntax}*\n**Need help?** *${botName} help*`); + else if(isMemberAdmin || !command.admin) + command.invoke({ message, params, guildData }); +} + +/**@param param*/ +function handleInternalCommand(message, split) { + if (split[1].toLowerCase() === "version") + message.reply(`${ParentPackageJSON.name} v${ParentPackageJSON.version}`); + else if(split[1].toLowerCase() === "help") + message.reply(createHelpEmbed()); +} + +function createHelpEmbed() { + return "not yet implemented"; +} + +module.exports = handleMessage; \ No newline at end of file diff --git a/discord-bot-core/client.js b/discord-bot-core/client.js index da40094..464539e 100644 --- a/discord-bot-core/client.js +++ b/discord-bot-core/client.js @@ -1,111 +1,84 @@ -//node imports -const FileSystem = require("fs"); //checking if files exist +const FileSystem = require("fs"); +const Discord = require("discord.js"); +const JsonFile = require("jsonfile"); +const RequireAll = require("require-all"); +const CoreUtil = require("./util.js"); +const HandleMessage = require("./HandleMessage.js"); +// @ts-ignore +const InternalConfig = require("./internal-config.json"); -//external lib imports -const Discord = require("discord.js"); //discord interaction -const JsonFile = require("jsonfile"); //saving to/reading from json - -//component imports -const DiscordUtil = require("./util.js"); //some helper methods -const MessageHandler = require("./message-handler.js"); //message handling -const Config = require("./internal-config.json"); //some configuration values - -class CoreClient { +module.exports = class Client extends Discord.Client { /** - * @param {string} token - * @param {string} dataFile - * @param {object} guildDataModel - * @param {object[]} commands + * @param {*} token + * @param {*} dataFile + * @param {*} commandsDir + * @param {*} guildDataModel */ - constructor(token, dataFile, commands, implementations, guildDataModel) { - this.actual = new Discord.Client(); + constructor(token, dataFile, commandsDir, guildDataModel) { + super(); - this.token = token; + this._token = token; this.dataFile = dataFile; - this.commands = commands; - this.implementations = implementations; + this.commandsDir = commandsDir; this.guildDataModel = guildDataModel; - this.guildsData = FileSystem.existsSync(this.dataFile) ? - fromJSON(JsonFile.readFileSync(this.dataFile), this.guildDataModel) : {}; - process.on("uncaughtException", err => onUncaughtException(this, err)); + this.commands = RequireAll(this.commandsDir); + this.guildsData = FileSystem.existsSync(this.dataFile) ? this.fromJSON(JsonFile.readFileSync(this.dataFile)) : {}; + + this.on("ready", this.onReady); + this.on("message", this.onMessage); + this.on("debug", this.onDebug); + process.on("uncaughtException", err => this.onUnhandledException(this, err)); + } + + bootstrap() { + this.beforeLogin(); + this.login(this._token); + } + + beforeLogin() { + setInterval(() => this.writeFile(), InternalConfig.saveIntervalSec * 1000); + this.emit("beforeLogin"); + } + + onReady() { + this.user.setGame(InternalConfig.website.replace("http://", "")); + CoreUtil.dateLog(`Registered bot ${this.user.username}`); + } + + onMessage(message) { + if (message.channel.type === "text" && message.member) + HandleMessage(message, this.commands, this.guildsData[message.guild.id] || new this.guildDataModel(message.guild.id)); + } + + onDebug(info) { + if (!InternalConfig.debugIgnores.some(x => info.startsWith(x))) + CoreUtil.dateLog(info); + } + + onUnhandledException(client, err) { + CoreUtil.dateError(err.message || err); + CoreUtil.dateLog("Destroying existing client..."); + client.destroy().then(() => { + CoreUtil.dateLog("Client destroyed, recreating..."); + client.login(client._token); + }); } writeFile() { JsonFile.writeFile( this.dataFile, this.guildsData, - err => { - if (err) DiscordUtil.dateError("Error writing file", err); - }); + err => { if (err) CoreUtil.dateError(`Error writing data file! ${err.message || err}`); }); } - bootstrap() { - this.actual.on("ready", () => onReady(this)); - - this.actual.on("disconnect", eventData => DiscordUtil.dateError("Disconnect!", eventData.code, eventData.reason)); - - this.actual.on("message", message => { - if (message.author.id === this.actual.user.id) - return; - if (message.channel.type === "dm") - MessageHandler.handleDirectMessage(this, message); - else if (message.channel.type === "text" && message.member) - MessageHandler.handleTextMessage(this, message, this.guildsData) - .then(msg => { - if (msg) message.reply(msg); - }) - .catch(err => { - if (err) { - message.reply(err); - DiscordUtil.dateError(`Command error in guild ${message.guild.name}\n`, err.message || err); - } - }) - .then(() => this.writeFile()); - }); - - this.actual.login(this.token); + /** + * @param {*} json + * @param {*} guildDataModel + */ + fromJSON(json) { + const guildsData = Object.keys(json); + guildsData.forEach(guildID => { json[guildID] = new this.guildDataModel(json[guildID]); }); + return json; } -} - -/** - * @param {*} coreClient - */ -function onReady(coreClient) { - coreClient.actual.user.setGame("benji7425.github.io"); - DiscordUtil.dateLog("Registered bot " + coreClient.actual.user.username); - - setInterval(() => coreClient.writeFile(), Config.saveIntervalSec * 1000); - - if (coreClient.implementations.onReady) - coreClient.implementations.onReady(coreClient) - .then(() => coreClient.writeFile()) - .catch(err => DiscordUtil.dateError(err)); -} - -/** - * @param {*} coreClient - * @param {*} err - */ -function onUncaughtException(coreClient, err) { - DiscordUtil.dateError(err.message || err); - DiscordUtil.dateLog("Destroying existing client..."); - coreClient.actual.destroy().then(() => { - DiscordUtil.dateLog("Client destroyed, recreating..."); - coreClient.actual = new Discord.Client(); - coreClient.bootstrap(); - }); -} - -/** - * Convert json from file to a usable format - * @param {object} json json from file - * @param {*} guildDataModel - */ -function fromJSON(json, guildDataModel) { - const guildsData = Object.keys(json); - guildsData.forEach(guildID => { json[guildID] = new guildDataModel(json[guildID]); }); - return json; -} - -module.exports = CoreClient; \ No newline at end of file +}; \ No newline at end of file diff --git a/discord-bot-core/command.js b/discord-bot-core/command.js new file mode 100644 index 0000000..0dbe88b --- /dev/null +++ b/discord-bot-core/command.js @@ -0,0 +1,12 @@ +module.exports = class Command { + /**@param param */ + constructor({ name, description, syntax, admin, invoke }) { + this.name = name; + this.description = description; + this.syntax = syntax; + this.admin = admin; + this.invoke = invoke; + + this.expectedParamCount = this.syntax.split(/ +/).length - 1; + } +}; \ No newline at end of file diff --git a/discord-bot-core/index.js b/discord-bot-core/index.js index 826b29b..4691597 100644 --- a/discord-bot-core/index.js +++ b/discord-bot-core/index.js @@ -1,10 +1,13 @@ -const Config = require("./internal-config.json"); +// @ts-ignore +const InternalConfig = require("./internal-config.json"); module.exports = { Client: require("./client.js"), - util: require("./util.js"), + BaseGuildData: require("./BaseGuildData.js"), + Command: require("./Command.js"), + util: require("./Util.js"), details: { - website: Config.website, - discordInvite: Config.discordInvite + website: InternalConfig.website, + discordInvite: InternalConfig.discordInvite } }; diff --git a/discord-bot-core/internal-config.json b/discord-bot-core/internal-config.json index 542a675..86adf93 100644 --- a/discord-bot-core/internal-config.json +++ b/discord-bot-core/internal-config.json @@ -1,21 +1,11 @@ { - "saveFile": "./%s-data.json", "saveIntervalSec": 60, "website": "https://benji7425.github.io", "discordInvite": "https://discord.gg/SSkbwSJ", "defaultDMResponse": "This bot does not have any handling for direct messages. To learn more or get help please visit %s, or join my Discord server here: %s", - "commands": { - "version": { - "command": "version", - "description": "Returns the bot version", - "syntax": "version", - "admin": false - }, - "help": { - "command": "help", - "description": "Display information about commands available to you", - "syntax": "help", - "admin": false - } - } + "debugIgnores":[ + "[ws] [connection] Sending a heartbeat", + "[ws] [connection] Heartbeat acknowledged", + "Authenticated using token" + ] } \ No newline at end of file diff --git a/discord-bot-core/message-handler.js b/discord-bot-core/message-handler.js deleted file mode 100644 index b9bec94..0000000 --- a/discord-bot-core/message-handler.js +++ /dev/null @@ -1,112 +0,0 @@ -//node imports -const Util = require("util"); - -//external lib imports -const Discord = require("discord.js"); - -//component imports -const Config = require("./internal-config.json"); //generic lib configuration -const ParentPackageJSON = require("../package.json"); //used to provide some info about the bot - -/** - * Handle a direct message to the bot - * @param {*} coreClient Core.Client - * @param {*} message Discord.Message - */ -function handleDirectMessage(coreClient, message) { - message.reply(Util.format(Config.defaultDMResponse, Config.website, Config.discordInvite)); -} - -/** - * - * @param {*} coreClient Core.Client - * @param {*} message Discord.Message - * @param {*[]} guildsData GuildData[] - */ -function handleTextMessage(coreClient, message, guildsData) { - return new Promise((resolve, reject) => { - const isCommand = message.content.startsWith(message.guild.me.toString()); - let guildData = guildsData[message.guild.id]; - - if (!guildData) - guildData = guildsData[message.guild.id] = new coreClient.guildDataModel({ id: message.guild.id }); - - if (!isCommand) - return coreClient.implementations.onTextMessage(message, guildData).then(msg => resolve(msg)); - - Object.assign(coreClient.commands, Config.commands); - const userIsAdmin = message.member.permissions.has("ADMINISTRATOR"); - const botName = "@" + (message.guild.me.nickname || coreClient.actual.user.username); - const { command, commandProp, params, expectedParamCount } = getCommandDetails(message, coreClient.commands, userIsAdmin) || { command: null, commandProp: null, params: null, expectedParamCount: null }; - const invoke = coreClient.implementations[commandProp]; - - if (!command || !params || isNaN(expectedParamCount)) - return reject(); - - if (command === Config.commands.version) - resolve(`${ParentPackageJSON.name} v${ParentPackageJSON.version}`); - else if (command === Config.commands.help) - message.channel.send(createHelpEmbed(botName, coreClient.commands, userIsAdmin)); - else { - if (invoke && params.length >= expectedParamCount) - invoke({ command, params: params, guildData, botName, message, coreClient }) - .then(msg => - resolve(msg)) - .catch(err => reject(err)); - else - reject(`Incorrect syntax!\n**Expected:** *${botName} ${command.syntax}*\n**Need help?** *${botName} ${coreClient.commands.help.command}*`); - } - }); -} - -/** - * Determine details about a command invoked via a message - * @param {*} message Discord.Message - * @param {*[]} commands commands array (probably from commands.json) - * @param {boolean} userIsAdmin whether the user is an admin - */ -function getCommandDetails(message, commands, userIsAdmin) { - const splitMessage = message.content.toLowerCase().split(/ +/); - const commandStr = splitMessage[1]; - const commandProp = Object.keys(commands).find(x => commands[x].command.toLowerCase() === commandStr); - const command = commands[commandProp]; - - if (!command || (command.admin && !userIsAdmin)) - return; - - const params = splitMessage.slice(2, splitMessage.length); - const expectedParamCount = command.syntax.split(/ +/).length - 1; - - let finalisedParams; - if (params.length > expectedParamCount) - finalisedParams = params.slice(0, expectedParamCount - 1).concat([params.slice(expectedParamCount - 1, params.length).join(" ")]); - else - finalisedParams = params; - - return { command, commandProp, params: finalisedParams, expectedParamCount }; -} - -/** - * Create a help embed for available commands - * @param {string} name name of the bot - * @param {*[]} commands commands array - * @param {boolean} userIsAdmin whether the user is admin - */ -function createHelpEmbed(name, commands, userIsAdmin) { - const commandsArr = Object.keys(commands).map(x => commands[x]).filter(x => userIsAdmin || !x.admin); - - const embed = new Discord.RichEmbed().setTitle(`__Help__ for ${(ParentPackageJSON.name + "").replace("discord-bot-", "")}`); - - commandsArr.forEach(command => { - embed.addField(command.command, `${command.description}\n**Usage:** *${name} ${command.syntax}*${userIsAdmin && command.admin ? "\n***Admin only***" : ""}`); - }); - - embed.addField("__Need more help?__", `[Visit my website](${Config.website}) or [Join my Discord](${Config.discordInvite})`, true); - - return { embed }; -} - -module.exports = { - handleDirectMessage, - handleTextMessage -}; \ No newline at end of file