diff --git a/README.md b/README.md index ab239ef..00b3970 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,10 @@ discordbot A bot that does the discord thing. -`node bot.js --token=DISCORDBOTTOKEN --ytapi=YOUTUBEAPIKEY` +`node bot.js --token= --ytapi= [--owner=] [--prefix=] [--game=]` Ideas --- -- an instance (class) of the bot for each server - - save data for each server (custom commands/playlists and so on) - - status rotator - command replies saved in file (server specific file and global file) - reddit api - anilist api diff --git a/bot.js b/bot.js index d1e6326..f9d33dd 100644 --- a/bot.js +++ b/bot.js @@ -3,20 +3,43 @@ const Discord = require("discord.js"), logger = require('./lib/logging').getLogger(), cmd = require("./lib/cmd"), guilding = require('./lib/guilding'), + utils = require('./lib/utils'), client = new Discord.Client(), args = require('args-parser')(process.argv), authToken = args.token, prefix = args.prefix || '~', - gamepresence = args.game || 'NieR:Automata'; + gamepresence = args.game || prefix + 'help'; + +let presences = [], + rotator = null; function main() { + utils.Cleanup(() => { + client.destroy(); + }); cmd.setLogger(logger); guilding.setLogger(logger); cmd.init(prefix); registerCommands(); + utils.dirExistence('./data', () => { + fs.exists('./data/presences.txt', (exist) => { + if (exist) { + logger.debug('Loading presences from file...'); + let lineReader = require('readline').createInterface({ + input: require('fs').createReadStream('./data/presences.txt') + }); + lineReader.on('line', (line) => { + presences.push(line); + }); + rotator = setInterval(() => rotatePresence(), 60000); + } + }) + }); client.login(authToken).then(()=> { logger.debug("Logged in"); }); + + } function registerCommands() { @@ -27,6 +50,40 @@ function registerCommands() { cmd.createGlobalCommand(prefix + 'repeatafterme', (msg, argv, args) => { return args.join(' '); },[], "Repeats what you say"); + + cmd.createGlobalCommand(prefix + 'addpresence', (msg, argv, args) => { + let p = args.join(' '); + presences.push(p); + fs.writeFile('./data/presences.txt', presences.join('\n'), (err) => {}); + return `Added Presence \`${p}\``; + },[], "Adds a presence to the rotation.", 'owner'); + + cmd.createGlobalCommand(prefix + 'shutdown', (msg) => { + msg.reply('Shutting down...').finally(() => { + logger.debug('Destroying client...'); + client.destroy().finally(() => { + logger.debug(`Exiting Process...`); + process.exit(0); + }); + }); + },[], "Shuts the bot down.", 'owner'); + + cmd.createGlobalCommand(prefix + 'rotate', () => { + try { + clearInterval(rotator); + rotatePresence(); + rotator = setInterval(() => rotatePresence(), 60000); + } catch (error) { + logger.warn(JSON.stringify(error)); + } + }, [], 'Force presence rotation', 'owner'); +} + +function rotatePresence() { + let pr = presences.shift(); + presences.push(pr); + client.user.setPresence({game: {name: `${gamepresence} | ${pr}`, type: "PLAYING"}, status: 'online'}); + logger.debug(`Presence rotation to ${pr}`); } client.on('ready', () => { @@ -40,7 +97,7 @@ client.on('message', msg => { logger.verbose(`ME: ${msg.content}`); return; } - logger.verbose(`<${msg.author.username}>: ${msg.content}`); + logger.verbose(`<${msg.author.tag}>: ${msg.content}`); if (!msg.guild) { let reply = cmd.parseMessage(msg); if (reply) msg.channel.send(reply); diff --git a/lib/cmd.js b/lib/cmd.js index b2e3aa8..d276f0b 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -2,7 +2,9 @@ /* Variable Definition */ let logger = require('winston'), - globCommands = {}; + globCommands = {}, + ownerCommands = {}, + args = require('args-parser')(process.argv); /* Function Definition */ @@ -14,12 +16,22 @@ exports.Servant = class { this.commands = {}; this.createCommand(((prefix || '~') + 'help') || "~help", () => { let helpstr = "```markdown\n"; - helpstr += "Commands\n---\n"; - // TODO: Duplicate commands should not appear or make two sections, one with global commands, one with server commands - Object.entries(globCommands).concat(Object.entries(this.commands)).forEach(([key, value]) => { - let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); - cmdhelp += value.description || ''; - helpstr += `\n${cmdhelp}\n`; + helpstr += "Commands\n===\n"; + helpstr += 'Global Commands\n---\n'; + Object.entries(globCommands).sort().forEach(([key, value]) => { + if (value.role !== 'owner' || msg.author.tag === args.owner) { + let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); + cmdhelp += value.description || ''; + helpstr += `\n${cmdhelp}\n`; + } + }); + helpstr += '\nServer Commands\n---\n'; + Object.entries(this.commands).sort().forEach(([key, value]) => { + if (value.role !== 'owner' || msg.author.tag === args.owner) { + let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); + cmdhelp += value.description || ''; + helpstr += `\n${cmdhelp}\n`; + } }); helpstr += "```"; return helpstr; @@ -33,11 +45,12 @@ exports.Servant = class { * @param args * @param description */ - createCommand(command, call, args, description) { + createCommand(command, call, args, description, role) { this.commands[command] = { 'args': args, 'description': description, - 'callback': call + 'callback': call, + 'role': role }; } @@ -61,6 +74,8 @@ exports.Servant = class { let command = (content.match(/^.\w+/) || [])[0]; if (!command || !this.commands[command]) return globResult; let cmd = this.commands[command]; + if (!checkPermission(msg, cmd.role)) return 'No Permission'; + logger.debug(`Permission <${cmd.role || 'all'}> granted for command ${command} for user <${msg.author.tag}>`); let argvars = content.match(/(?<= )\S+/g) || []; let kwargs = {}; let nLength = Math.min(cmd.args.length, argvars.length); @@ -88,12 +103,14 @@ exports.setLogger = function (newLogger) { * @param call * @param args * @param description + * @param role */ -exports.createGlobalCommand = function (command, call, args, description) { +exports.createGlobalCommand = function (command, call, args, description, role) { globCommands[command] = { 'args': args || [], 'description': description, - 'callback': call + 'callback': call, + 'role': role }; logger.debug(`Created command: ${command}, args: ${args}`); }; @@ -113,13 +130,15 @@ exports.parseMessage = function (msg) { */ exports.init = function (prefix) { logger.verbose("Created help command"); - this.createGlobalCommand((prefix + 'help') || "~help", () => { + this.createGlobalCommand((prefix + 'help') || "~help", (msg) => { let helpstr = "```markdown\n"; helpstr += "Commands\n---\n"; - Object.entries(globCommands).forEach(([key, value]) => { - let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); - cmdhelp += value.description || ''; - helpstr += `\n${cmdhelp}\n`; + Object.entries(globCommands).sort().forEach(([key, value]) => { + if (value.role !== 'owner' || msg.author.tag === args.owner) { + let cmdhelp = `${key} [${value.args.join('] [')}]`.replace('[]', '').padEnd(25, ' '); + cmdhelp += value.description || ''; + helpstr += `\n${cmdhelp}\n`; + } }); helpstr += "```"; return helpstr; @@ -135,6 +154,8 @@ function parseGlobalCommand(msg) { let command = (content.match(/^.\w+/) || [])[0]; if (!command || !globCommands[command]) return false; let cmd = globCommands[command]; + if (!checkPermission(msg, cmd.role)) return false; + logger.debug(`Permission <${cmd.role}> granted for command ${command} for user <${msg.author.tag}>`); let argvars = content.match(/(?<= )\S+/g) || []; let kwargs = {}; let nLength = Math.min(cmd.args.length, argvars.length); @@ -144,4 +165,16 @@ function parseGlobalCommand(msg) { let argv = argvars.slice(nLength); logger.debug(`Executing callback for command: ${command}, kwargs: ${JSON.stringify(kwargs)}, argv: ${argv}`); return cmd.callback(msg, kwargs, argv); +} + +function checkPermission(msg, role) { + if (!role || ['all', 'any', 'everyone'].includes(role)) + return true; + if (msg.author.tag === args.owner) { + return true; + } else { + if (msg.member && role && msg.member.roles.find("id", role.id)) + return true + } + return false } \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..41adc05 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,72 @@ +/** + * A Series of utility functions + */ +const fs = require('fs'); + +function noOp() {} +let sysdataPath = './res/data/sys.json'; +let sysData = {}; + +/** + * returns the extension of a file for the given filename. + * @param {String} filename The name of the file. + * @return {String} A string that represents the file-extension. + */ +exports.getExtension = function (filename) { + if (!filename) return null; + try { + let exts = filename.match(/\.[a-z]+/g); // get the extension by using regex + if (exts) return exts[exts.length - 1]; // return the found extension + else return null; // return null if no extension could be found + } catch (error) { + console.error(error); + return null; + } +}; + +/** + * lets you define a cleanup for your program exit + * @param {Function} callback the cleanup function + * @constructor + * @author CanyonCasa & Pier-Luc Gendreau on StackOverflow + */ +exports.Cleanup = function Cleanup(callback) { + + // attach user callback to the process event emitter + // if no callback, it will still exit gracefully on Ctrl-C + callback = callback || noOp; + process.on('cleanup',callback); + + // do app specific cleaning before exiting + process.on('exit', function () { + process.emit('cleanup'); + }); + + // catch ctrl+c event and exit normally + process.on('SIGINT', function () { + console.log('Ctrl-C...'); + process.exit(2); + }); + + //catch uncaught exceptions, trace, then exit normally + process.on('uncaughtException', function(e) { + console.log('Uncaught Exception...'); + console.log(e.stack); + process.exit(99); + }); +}; + +/* FS */ + +exports.dirExistence = function(path, callback) { + fs.exists(path, (exist) => { + if (!exist) { + fs.mkdir(path, (err) => { + if (!err) + callback(); + }); + } else { + callback(); + } + }) +}; \ No newline at end of file