diff --git a/README.md b/README.md index 0ca8070..00b3970 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,11 @@ discordbot A bot that does the discord thing. -`node bot.js --token=DICORDBOTTOKEN --ytapi=YOUTUBEAPIKEY` +`node bot.js --token= --ytapi= [--owner=] [--prefix=] [--game=]` Ideas --- -- an instance (class) of the bot for each server - - planned with datahandler, could be for the whole server in the future - - save data for each server (custom commands/playlists and so on) - - custom scripts for each server (?) <- not that hard to code (with the right api) - - status rotator - - +- command replies saved in file (server specific file and global file) - reddit api - anilist api - othercoolstuff api \ No newline at end of file diff --git a/bot.js b/bot.js index d1e6326..59a9054 100644 --- a/bot.js +++ b/bot.js @@ -3,30 +3,88 @@ 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(); - client.login(authToken).then(()=> { + 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() { cmd.createGlobalCommand(prefix + 'ping', () => { - return 'Pong!'; + return 'Pong!'; }, [], "Try it yourself."); cmd.createGlobalCommand(prefix + 'repeatafterme', (msg, argv, args) => { return args.join(' '); - },[], "Repeats what you say"); + }, [], "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 +98,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 b5e1d41..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 */ @@ -12,13 +14,24 @@ let logger = require('winston'), exports.Servant = class { constructor(prefix) { this.commands = {}; - this.createCommand(((prefix || '~')+'help') || "~help", () => { + this.createCommand(((prefix || '~') + 'help') || "~help", () => { let helpstr = "```markdown\n"; - helpstr += "Commands\n---\n"; - 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; @@ -32,11 +45,12 @@ exports.Servant = class { * @param args * @param description */ - createCommand(command, call, args, description) { - this.commands[command] = { + createCommand(command, call, args, description, role) { + this.commands[command] = { 'args': args, 'description': description, - 'callback': call + 'callback': call, + 'role': role }; } @@ -60,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); @@ -77,7 +93,7 @@ exports.Servant = class { * Getting the logger * @param {Object} newLogger */ -exports.setLogger = function(newLogger) { +exports.setLogger = function (newLogger) { logger = newLogger; }; @@ -87,12 +103,14 @@ exports.setLogger = function(newLogger) { * @param call * @param args * @param description + * @param role */ -exports.createGlobalCommand = function(command, call, args, description) { - globCommands[command] = { +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}`); }; @@ -103,22 +121,24 @@ exports.createGlobalCommand = function(command, call, args, description) { * @param msg * @returns {boolean|*} */ -exports.parseMessage = function(msg) { +exports.parseMessage = function (msg) { return parseGlobalCommand(msg); -} +}; /** * Initializes the module by creating a help command */ -exports.init = function(prefix) { +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; @@ -134,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); @@ -143,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/data.js b/lib/data.js index 7042bfc..dd59fe1 100644 --- a/lib/data.js +++ b/lib/data.js @@ -41,23 +41,47 @@ exports.DataHandler = class { } } + /** + * adds an entry to the fileEntries. refreshes the entrie file + * @param name + * @param path + */ addEntry(name, path) { this.fileEntries.name = path; this.refreshEntries(); } + /** + * shortcut function to join the path with the working directory + * @param file + * @returns {Promise | string} + */ getfp(file) { return path.join(this.workingDir, file); } + /** + * shortcut function that evokes the callback after reading the file. the files path is the name + * joined with the working directory + * @param file + * @param callback + */ getcont(file, callback) { fs.readFile(this.getfp, 'utf-8', callback); } + /** + * returns the JSON content of a file in the working directory + * @param file + * @returns {any} + */ getJSONSync(file) { return JSON.parse(fs.readFileSync(this.getfp(file), 'utf-8')); } + /** + * writes all entris of the fileEntries variable into the fileEntries file. + */ refreshEntries() { fs.writeFile(this.getfp(entryfile), JSON.stringify(this.fileEntries), (err) => { if (err) @@ -65,6 +89,11 @@ exports.DataHandler = class { }); } + /** + * returns the data for the entry + * @param name + * @returns {*} + */ getData(name) { try { if (this.fileData[name]) @@ -79,7 +108,11 @@ exports.DataHandler = class { } } - + /** + * sets the entry to data + * @param name + * @param data + */ setData(name, data) { this.fileData[name] = data; if (!this.fileEntries[name]) { diff --git a/lib/guilding.js b/lib/guilding.js index c396fb2..1afe3f6 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -9,6 +9,7 @@ exports.setLogger = function (newLogger) { music.setLogger(logger); }; + exports.GuildHandler = class { constructor(guild, prefix) { this.guild = guild; @@ -20,22 +21,42 @@ exports.GuildHandler = class { this.registerMusicCommands(); } + /** + * function shortcut returns the data from the dataHandler + * @param name + * @returns {{}} + */ getData(name) { return this.dataHandler.getData(name); } + /** + * appends data to the data handler + * @param name + * @param key + * @param value + */ appendData(name, key, value) { let data = this.getData(name); data[key] = value; this.dataHandler.setData(name, data); } + /** + * deletes an entry from the data handler + * @param name + * @param key + */ deleteDataEntry(name, key) { let data = this.getData(name); delete data[key]; this.dataHandler.setData(name, data); } + /** + * registers all music commands and initializes a dj + * @param cmdPrefix + */ registerMusicCommands(cmdPrefix) { let prefix = cmdPrefix || this.prefix; this.dj = new music.DJ(); @@ -95,7 +116,7 @@ exports.GuildHandler = class { if (msg.member.voiceChannel) { this.dj.connect(msg.member.voiceChannel); } else { - msg.reply("You are not connected to a voicechannel."); + return "You are not connected to a voicechannel."; } }, [], "Joins the VC you are in."); @@ -161,20 +182,32 @@ exports.GuildHandler = class { // saved command - prints out saved playlists this.createCommand(prefix + 'saved', () => { - let response = '```markdown\nSaved Playlists:\n==\n' + let response = '```markdown\nSaved Playlists:\n==\n'; Object.entries(this.getData('savedplaylists')).forEach(([key, value]) => { response += `${key.padEnd(10, ' ')} ${value} \n\n`; }); - response += '```' + response += '```'; return response; }, [], "Prints out all saved playlists."); } + /** + * creates a servant if not set and lets the servant create a command + * @param command + * @param call + * @param args + * @param description + */ createCommand(command, call, args, description) { if (!this.servant) this.servant = new cmd.Servant(this.prefix); this.servant.createCommand(command, call, args, description); } + /** + * handles the message by letting the servant parse the command. Depending on the message setting it + * replies or just sends the answer. + * @param msg + */ handleMessage(msg) { if (!this.servant) this.servant = new cmd.Servant(this.prefix); let answer = this.servant.parseCommand(msg); diff --git a/lib/logging.js b/lib/logging.js index de310cc..d73087c 100644 --- a/lib/logging.js +++ b/lib/logging.js @@ -50,7 +50,7 @@ const winston = require('winston'), * A function to return the logger that has been created after appending an exception handler * @returns {Object} */ -exports.getLogger = function() { +exports.getLogger = function () { logger.exceptions.handle( new winston.transports.File({ filename: './.log/exceptions.log' diff --git a/lib/music.js b/lib/music.js index 7479b57..a26a175 100644 --- a/lib/music.js +++ b/lib/music.js @@ -11,7 +11,7 @@ let connections = {}; /* Function Definition */ -exports.DJ = class{ +exports.DJ = class { constructor(voiceChannel) { this.conn = null; this.disp = null; @@ -80,7 +80,7 @@ exports.DJ = class{ setTimeout(() => { if (this.voiceChannel && this.voiceChannel.members.size === 1) logger.verbose(`Exiting ${this.voiceChannel.name}`); - this.stop(); + this.stop(); }, 300000); } else if (this.connected) setTimeout(() => this.checkListeners(), 10000); @@ -226,7 +226,7 @@ exports.DJ = class{ this.voiceChannel.leave(); logger.debug("Left VoiceChannel"); } - } catch(error) { + } catch (error) { logger.verbose(JSON.stringify(error)); } } @@ -236,7 +236,7 @@ exports.DJ = class{ * end event of the dispatcher that automatically plays the next song. If no dispatcher is found * It tries to play the next song with playYouTube */ - skip () { + skip() { logger.debug("Skipping song"); if (this.disp !== null) { this.disp.end(); @@ -284,7 +284,7 @@ exports.DJ = class{ clear() { this.queue = []; } -} +}; /** * Getting the logger; @@ -298,7 +298,7 @@ exports.setLogger = function (newLogger) { * Connects to a voicechannel * @param voiceChannel */ -exports.connect = function(voiceChannel) { +exports.connect = function (voiceChannel) { let gid = voiceChannel.guild.id; let voiceDJ = new this.DJ(voiceChannel); djs[gid] = voiceDJ; @@ -310,7 +310,7 @@ exports.connect = function(voiceChannel) { * @param filename * @param guildId */ -exports.playFile = function(guildId, filename) { +exports.playFile = function (guildId, filename) { djs[guildId].playFile(filename); }; @@ -319,7 +319,7 @@ exports.playFile = function(guildId, filename) { * @param voiceChannel * @param url */ -exports.play = function(voiceChannel, url) { +exports.play = function (voiceChannel, url) { let guildId = voiceChannel.guild.id; if (!djs[guildId]) { this.connect(voiceChannel).then(() => { @@ -335,7 +335,7 @@ exports.play = function(voiceChannel, url) { * @param voiceChannel * @param url */ -exports.playnext = function(voiceChannel, url) { +exports.playnext = function (voiceChannel, url) { let guildId = voiceChannel.guild.id; if (!djs[guildId]) { this.connect(voiceChannel).then(() => { @@ -351,14 +351,14 @@ exports.playnext = function(voiceChannel, url) { * @param percentage * @param guildId */ -exports.setVolume = function(guildId, percentage) { +exports.setVolume = function (guildId, percentage) { djs[guildId].setVolume(percentage); }; /** * pauses the music */ -exports.pause = function(guildId) { +exports.pause = function (guildId) { djs[guildId].pause(); }; @@ -366,7 +366,7 @@ exports.pause = function(guildId) { * Resumes the music * @param guildId */ -exports.resume = function(guildId) { +exports.resume = function (guildId) { djs[guildId].resume(); }; @@ -374,7 +374,7 @@ exports.resume = function(guildId) { * Stops the music * @param guildId */ -exports.stop = function(guildId) { +exports.stop = function (guildId) { djs[guildId].stop(); delete djs[guildId]; }; @@ -383,7 +383,7 @@ exports.stop = function(guildId) { * Skips the song * @param guildId */ -exports.skip = function(guildId) { +exports.skip = function (guildId) { djs[guildId].skip(); }; @@ -391,7 +391,7 @@ exports.skip = function(guildId) { * Clears the playlist * @param guildId */ -exports.clearQueue = function(guildId) { +exports.clearQueue = function (guildId) { djs[guildId].clear(); }; @@ -399,7 +399,7 @@ exports.clearQueue = function(guildId) { * Returns the queue * @param guildId */ -exports.getQueue = function(guildId) { +exports.getQueue = function (guildId) { return djs[guildId].playlist; }; @@ -407,7 +407,7 @@ exports.getQueue = function(guildId) { * evokes the callback function with the title of the current song * @param guildId */ -exports.nowPlaying = function(guildId) { +exports.nowPlaying = function (guildId) { return djs[guildId].song; }; @@ -415,7 +415,7 @@ exports.nowPlaying = function(guildId) { * shuffles the queue * @param guildId */ -exports.shuffle = function(guildId) { +exports.shuffle = function (guildId) { djs[guildId].shuffle(); }; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..a28e04b --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,74 @@ +/** + * 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