diff --git a/README.md b/README.md index 00b3970..a28a6e9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,34 @@ discordbot A bot that does the discord thing. -`node bot.js --token= --ytapi= [--owner=] [--prefix=] [--game=]` +`node bot.js [--token=] [--ytapi=] [--owner=] [--prefix=] [--game=]` + +The arguments are optional because the token and youtube-api-key that the bot needs to run can also be defined in the config.json in the bot's directory: +```json5 +// config.json +{ + "prefix": "_", + "token": "DISCORD BOT TOKEN", + "ytapikey": "YOUTUBE API KEY", + "presence": "THE DEFAULT GAME IF NO presences.txt IS FOUND IN ./data/", + "presence_duration": 300000, + "owners": [ + "SPECIFY A LIST OF BOT-OWNERS" + ], + "music": { + "timeout": 300000 + } +} +``` + +Features +--- + +At the moment the bot can... +- [x] ...play music (YouTube videos and playlists) +- [x] ...save songs/playlists with a given name +- [x] ...log stuff in a database +- [ ] ...transform into a cow Ideas --- diff --git a/bot.js b/bot.js index e205e44..0a1464d 100644 --- a/bot.js +++ b/bot.js @@ -39,8 +39,6 @@ function main() { client.login(authToken).then(() => { logger.debug("Logged in"); }); - - } function registerCommands() { @@ -87,7 +85,7 @@ function registerCommands() { // returns the time the bot is running cmd.createGlobalCommand(prefix + 'uptime', () => { - return `Uptime: \`${client.uptime/1000} s\`` + return `Uptime: \`${client.uptime / 1000} s\`` }, [], 'Returns the uptime of the bot', 'owner'); // returns the numbe of guilds, the bot has joined diff --git a/commands/servercommands.json b/commands/servercommands.json index 174b942..7fe40c0 100644 --- a/commands/servercommands.json +++ b/commands/servercommands.json @@ -114,15 +114,17 @@ "name": "save", "permission": "dj", "args": [ - "url", - "name" + "url" ], "description": "Saves the YouTube song/playlist with a specific name" }, "saved": { "name": "saved", "permission": "all", - "description": "Prints out all saved playlists." + "description": "Prints out all saved playlists.", + "response": { + "no_saved": "There are no saved songs/playlists :(" + } } } } \ No newline at end of file diff --git a/lib/cmd.js b/lib/cmd.js index fee63c3..642165b 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -103,7 +103,12 @@ module.exports.Servant = class { } let argv = argvars.slice(nLength); logger.debug(`Executing callback for command: ${command}, kwargs: ${kwargs}, argv: ${argv}`); - return cmd.callback(msg, kwargs, argv) || globResult; + try { + return cmd.callback(msg, kwargs, argv) || globResult; + } catch (err) { + logger.error(err.message); + return `The command \`${command}\` has thrown an error.`; + } } }; @@ -189,16 +194,16 @@ function parseGlobalCommand(msg) { /** * @param msg - * @param role {String} + * @param rolePerm {String} * @returns {boolean} */ -function checkPermission(msg, role) { - if (!role || ['all', 'any', 'everyone'].includes(role)) +function checkPermission(msg, rolePerm) { + if (!rolePerm || ['all', 'any', 'everyone'].includes(rolePerm)) return true; if (msg.author.tag === args.owner || config.owners.includes(msg.author.tag)) { return true; } else { - if (msg.member && role && msg.member.roles.some(role => role.name.toLowerCase() === role.toLowerCase())) + if (msg.member && rolePerm && msg.member.roles.some(role => role.name.toLowerCase() === rolePerm.toLowerCase())) return true } return false diff --git a/lib/data.js b/lib/data.js index dd59fe1..6069729 100644 --- a/lib/data.js +++ b/lib/data.js @@ -18,7 +18,10 @@ fs.exists(datapath, (exist) => { }); /* Function Definition */ - +/** + * @deprecated + * @type {DataHandler} + */ exports.DataHandler = class { constructor(name) { this.workingDir = path.join(datapath, name); diff --git a/lib/guilding.js b/lib/guilding.js index f565626..49ae75e 100644 --- a/lib/guilding.js +++ b/lib/guilding.js @@ -1,9 +1,11 @@ const cmd = require('./cmd'), music = require('./music'), - data = require('./data'), + utils = require('./utils.js'), config = require('../config.json'), servercmd = require('../commands/servercommands'), - handlers = {}; + sqlite3 = require('sqlite3'), + handlers = {}, + dbDir = './data/gdb'; let logger = require('winston'); exports.setLogger = function (newLogger) { @@ -11,61 +13,65 @@ exports.setLogger = function (newLogger) { music.setLogger(logger); }; - +/** + * Server-Specific commands, music and more + * @type {GuildHandler} + */ exports.GuildHandler = class { constructor(guild, prefix) { this.guild = guild; - this.dataHandler = new data.DataHandler(guild.name); this.dj = null; this.mention = false; this.prefix = prefix || config.prefix; this.servant = new cmd.Servant(this.prefix); + utils.dirExistence('./data', () => { + utils.dirExistence('./data/gdb', () => { + this.db = new sqlite3.Database(`./data/gdb/${guild}.db`); + this.createTables(); + }) + }); 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 + * Creates all tables needed in the Database. + * These are at the moment: + * messages - logs all messages send on the server + * playlists - save playlists to play them later */ - deleteDataEntry(name, key) { - let data = this.getData(name); - delete data[key]; - this.dataHandler.setData(name, data); + createTables() { + let createCmd = 'CREATE TABLE IF NOT EXISTS'; + let autoIdPK = 'id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL'; + this.db.run(`${createCmd} messages ( + ${autoIdPK}, + creation_timestamp DATETIME NOT NULL, + author VARCHAR(128) NOT NULL, + author_name VARCHAR(128), + content TEXT NOT NULL + )`); + this.db.run(`${createCmd} playlists ( + ${autoIdPK}, + name VARCHAR(32) UNIQUE NOT NULL, + url VARCHAR(255) NOT NULL + )`); } - /** - * registers all music commands and initializes a dj - * @param cmdPrefix - */ - /** * 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.db) { + this.db.run( + 'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)', + [msg.author.id, msg.createdTimestamp, msg.author.username, msg.content], + (err) => { + if (err) + logger.error(err.message); + } + ); + } let answer = this.servant.parseCommand(msg); if (!answer) return; if (this.mention) { @@ -75,59 +81,98 @@ exports.GuildHandler = class { } } + /** + * Connect to a voice-channel if not connected and play the url + * @param vc + * @param url + */ + connectAndPlay(vc, url) { + if (!this.dj.connected) { + this.dj.connect(vc).then(() => { + this.dj.playYouTube(url); + }); + } else { + this.dj.playYouTube(url); + } + } + + /** + * registers all music commands and initializes a dj + * @param cmdPrefix + */ registerMusicCommands(cmdPrefix) { let prefix = cmdPrefix || this.prefix; this.dj = new music.DJ(); // play command - this.servant.createCommand(servercmd.music.play, (msg, argv) => { + this.servant.createCommand(servercmd.music.play, (msg, kwargs, argv) => { let vc = msg.member.voiceChannel; - let url = argv['url']; + let url = kwargs['url']; if (!vc) return 'You are not connected to a VoiceChannel'; if (!url) return servercmd.music.play.response.no_url; if (!url.match(/http/g)) { - if (this.getData('savedplaylists') && this.getData('savedplaylists')[url]) { - url = this.getData('savedplaylists')[url]; - } else { - return servercmd.music.play.response.url_invalid; - } - } - try { - if (!this.dj.connected) { - this.dj.connect(vc).then(() => { - this.dj.playYouTube(url); - }); - } else { - this.dj.playYouTube(url); + if (argv) + url += ' ' + argv.join(' '); + this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => { + if (err) { + console.error(err.message); + } + if (!row) { + return servercmd.music.play.response.url_invalid; + } + url = row.url; + try { + this.connectAndPlay(vc, url); + } catch (err) { + logger.error(err.message); + return servercmd.music.play.response.failure; + } + }); + } else { + try { + this.connectAndPlay(vc, url); + } catch (err) { + logger.error(err.message); + return servercmd.music.play.response.failure; } - } catch (err) { - logger.error(err); - return servercmd.music.play.response.failure; } return servercmd.music.play.response.success; }); // playnext command - this.servant.createCommand(servercmd.music.playnext,(msg, argv) => { + this.servant.createCommand(servercmd.music.playnext,(msg, kwargs, argv) => { let vc = msg.member.voiceChannel; if (!this.dj.connected) this.dj.voiceChannel = vc; - let url = argv['url']; + let url = kwargs['url']; if (!url) return servercmd.music.playnext.response.no_url; if (!url.match(/http/g)) { - if (this.getData('savedplaylists') && this.getData('savedplaylists')[url]) { - url = this.getData('savedplaylists')[url]; - } else { - return servercmd.music.playnext.response.url_invalid; + if (argv) + url += ' ' + argv.join(' '); + this.db.get('SELECT url FROM playlists WHERE name = ?', [url], (err, row) => { + if (err) { + console.error(err.message); + } + if (!row) { + return servercmd.music.play.response.url_invalid; + } + url = row.url; + try { + this.dj.playYouTube(url, true); + } catch (err) { + logger.error(err.message); + return servercmd.music.play.response.failure; + } + }); + } else { + try { + this.dj.playYouTube(url, true); + } catch (err) { + logger.error(err); + return servercmd.music.playnext.response.failure; } } - try { - this.dj.playYouTube(url, true); - } catch (err) { - logger.error(err); - return servercmd.music.playnext.response.failure; - } return servercmd.music.playnext.response.success; }); @@ -206,19 +251,42 @@ exports.GuildHandler = class { }); // saves playlists - this.servant.createCommand(servercmd.music.save, (msg, argv) => { - this.appendData('savedplaylists', argv.name, argv.url); - return `Saved song/playlist as ${argv['name']}` + this.servant.createCommand(servercmd.music.save, (msg, kwargs, argv) => { + let saveName = argv.join(' '); + this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName], (err, row) => { + if(err) { + logger.error(err.message); + } + if (!row || row.count === 0) { + this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url], (err) => { + if (err) + logger.error(err.message); + }); + } else { + this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName], (err) => { + if (err) + logger.error(err.message); + }); + } + }); + return `Saved song/playlist as ${saveName}` }); // saved command - prints out saved playlists - this.servant.createCommand(servercmd.music.saved, () => { + this.servant.createCommand(servercmd.music.saved, (msg) => { let response = '```markdown\nSaved Playlists:\n==\n'; - Object.entries(this.getData('savedplaylists')).forEach(([key, value]) => { - response += `${key.padEnd(10, ' ')} ${value} \n\n`; + this.db.all('SELECT name, url FROM playlists', (err, rows) => { + if (err) + logger.error(err.message); + for (let row of rows) { + response += `${row.name.padEnd(10, ' ')}: ${row.url} \n\n`; + } + response += '```'; + if (rows.length === 0) + msg.channel.send(servercmd.music.saved.response.no_saved); + else + msg.channel.send(response); }); - response += '```'; - return response; }); } }; diff --git a/package.json b/package.json index c6d8656..5fa768c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "ffmpeg-binaries": "4.0.0", "get-youtube-title": "1.0.0", "opusscript": "0.0.6", + "sqlite3": "^4.0.6", "winston": "3.1.0", "winston-daily-rotate-file": "3.5.1", "youtube-playlist-info": "1.1.2",