diff --git a/README.md b/README.md index 4207d63..108a8c8 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,7 @@ 6. Take all of the information from the page and enter it into the `config/keys.js` file, replacing the placeholders. 7. Navigate to the `config/plex.js` file and replace the placeholders with your Plex Server information 1. To get your token, following the instructions here: https://support.plex.tv/hc/en-us/articles/204059436-Finding-an-authentication-token-X-Plex-Token - 2. To get your machineId or "machineIdentifier", follow the instructions here: https://support.plex.tv/hc/en-us/articles/201638786-Plex-Media-Server-URL-Commands - * In the first example under "Base Server Capabilities", you can see the information returned when you type `http://[PMS_IP_Address]:32400/?X-Plex-Token=YourTokenGoesHere` into your address bar of a web browser. Copy everything between the quotes for the parameter "machineIdentifier" and paste it into the "machineId" property in `config/plex.js` - 3. The identifier, product, version, and deviceName can be anything you want + 2. The identifier, product, version, and deviceName can be anything you want 8. Once you have the configs set up correctly, you'll need to authorize your bot on a server you have administrative access to. For documentation, you can read: https://discordapp.com/developers/docs/topics/oauth2#bots. The steps are as follows: 1. Go to `https://discordapp.com/api/oauth2/authorize?client_id=[CLIENT_ID]&scope=bot&permissions=1` where [CLIENT_ID] is the Discord App Client ID 2. Select **Add a bot to a server** and select the server to add it to @@ -53,6 +51,7 @@ If I am missing any steps, feel free to reach out or open an issue/bug in the I * `!skip` : skips the current song if one is playing and plays the next song in queue if it exists * `!stop` : stops song if one is playing * `!viewqueue` : displays current song queue +* `!bot prefix ` : changes the bot prefix for the guild *** ## Customization @@ -61,10 +60,8 @@ Update the `config\keys.js` file with your information: ```javascript module.exports = { - 'clientId' : 'DISCORD_CLIENT_ID', - 'clientSecret' : 'DISCORD_CLIENT_SECRET', - 'username' : 'DISCORD_BOT_USERNAME', 'botToken' : 'DISCORD_BOT_TOKEN', + 'defaultPrefix' : '!' }; ``` @@ -73,17 +70,15 @@ And update the `config\plex.js` file with your Plex information: ```javascript module.exports= { 'hostname' : 'PLEX_LOCAL_IP', - 'port' : 'PLEX_LOCAL_PORT' + 'port' : 'PLEX_LOCAL_PORT', 'username' : 'PLEX_USERNAME', 'password' : 'PLEX_PASSWORD', 'token' : 'PLEX_TOKEN', - 'machineId' : 'PLEX_MACHINEID', - 'managedUser' : 'PLEX_MANAGED_USERNAME', 'options' : { - 'identifier': 'APP_IDENTIFIER', - 'product' : 'APP_PRODUCT_NAME', - 'version' : 'APP_VERSION_NUMBER', - 'deviceName': 'APP_DEVICE_NAME', + 'identifier': 'Plex-Discord-Bot', + 'product' : 'Node.js App', + 'version' : '1.0.0', + 'deviceName': 'Node.js App', 'platform' : 'Discord', 'device' : 'Discord' } diff --git a/app/music.js b/app/music.js index 809b9a6..bbbc792 100644 --- a/app/music.js +++ b/app/music.js @@ -1,35 +1,202 @@ module.exports = function(client) { // plex commands ------------------------------------------------------------- - var plexCommands = require('../commands/plex'); + var plexCommands = require('../commands/plex.js'); + var keys = require('../config/keys.js'); + + // Database for individual server settings as well as saving bot changes. This can be used later for even more advanced customizations like mod roles, log channels, etc. + const SQLite = require("better-sqlite3"); + const sql = new SQLite('./config/database.sqlite'); // when bot is ready - client.on('ready', function() { + client.on('ready', async message => { console.log('bot ready'); console.log('logged in as: ' + client.user.tag); + client.user.setActivity('music | ' + keys.defaultPrefix + 'help', { type: 'PLAYING' }); + + // Check if the table "guildSettings" exists. + const tableGuildSettings = sql.prepare("SELECT count(*) FROM sqlite_master WHERE type='table' AND name = 'guildSettings';").get(); + if (!tableGuildSettings['count(*)']) { + // If the table isn't there, create it and setup the database correctly. + sql.prepare("CREATE TABLE guildSettings (id TEXT PRIMARY KEY, guild TEXT, prefix TEXT);").run(); + // Ensure that the "id" row is always unique and indexed. + sql.prepare("CREATE UNIQUE INDEX idx_guildSettings_id ON guildSettings (id);").run(); + sql.pragma("synchronous = 1"); + sql.pragma("journal_mode = wal"); + } + + // And then we have prepared statements to get and set guildSettings data. + client.getGuildSettings = sql.prepare("SELECT * FROM guildSettings WHERE guild = ?"); + client.setGuildSettings = sql.prepare("INSERT OR REPLACE INTO guildSettings (id, guild, prefix) VALUES (@id, @guild, @prefix);"); + plexCommands['plexTest'].process(); }); // when message is sent to discord - client.on('message', function(message){ + client.on('message', async message => { + if (message.author.bot) return; // If a bot sends a message, ignore it. + let guildSettings; // used for discord server settings + + if (message.guild) { + // Sets default server settings if message occurs in a guild (not a dm) + guildSettings = client.getGuildSettings.get(message.guild.id); + if (!guildSettings) { + guildSettings = { id: `${message.guild.id}-${client.user.id}`, guild: message.guild.id, prefix: keys.defaultPrefix }; + client.setGuildSettings.run(guildSettings); + guildSettings = client.getGuildSettings.get(message.guild.id); + } + } + var prefix = guildSettings.prefix; + var msg = message.content.toLowerCase(); - if (msg.startsWith('!')){ - var cmdTxt = msg.split(" ")[0].substring("-".length, msg.length); + if (msg.startsWith(prefix)){ + // Used for bot settings + var args = message.content.slice(prefix.length).trim().split(/ +/g); + var command = args.shift().toLowerCase(); + + var cmdTxt = msg.split(" ")[0].substring(prefix.length, msg.length); var query = msg.substring(msg.indexOf(' ')+1); var cmd = plexCommands[cmdTxt]; - if (cmd){ + if (command === "bot") { + // This is where we change bot information + if (args.length > 0) { + command = args.shift().toLowerCase(); + } else { + command = "help"; + } + + if (command === "prefix") { + if (args.length > 0) { + if (message.channel.guild.member(message.author).hasPermission('ADMINISTRATOR')) { + command = args.shift().toLowerCase(); + guildSettings.prefix = command; + client.setGuildSettings.run(guildSettings); + guildSettings = client.getGuildSettings.get(message.guild.id); + message.channel.send("Prefix changed to `" + guildSettings.prefix + "`"); + } + else { + return message.channel.send('You do not have permissions to use `' + prefix + 'bot prefix` in <#' + message.channel.id + '>!'); + } + } else { + return message.channel.send("The current prefix is `" + guildSettings.prefix + "`\nTo change it type: `" + guildSettings.prefix + "bot prefix <" + keys.defaultPrefix + ">` (where **<" + keys.defaultPrefix + ">** is the prefix)"); + } + } + else if (command === "help") { + // Help message for bot settings goes here + const help = { + "title": "The following are available for the command " + prefix + "bot ", + "description": "\n\u200b", + "color": 4025171, + "timestamp": new Date(), + "footer": { + "icon_url": client.user.avatarURL, + "text": "Fetched" + }, + "thumbnail": { + "url": client.user.avatarURL + }, + "author": { + "name": client.user.username, + "icon_url": client.user.avatarURL + }, + "fields": [ + { + "name": prefix + "bot prefix :", + "value": "changes the bot prefix for the guild" + } + ] + } + + return message.channel.send({ embed: help }); + } + else { + return message.channel.send("**Command not recognized!** Type `" + prefix + "bot help` for a list of bot settings."); + } + } + else if (command === "help") { + // Help message for available bot commands goes here + const help = { + "title": "The following commands are available for this bot:", + "description": "\n\u200b", + "color": 4025171, + "timestamp": new Date(), + "footer": { + "icon_url": client.user.avatarURL, + "text": "Fetched" + }, + "thumbnail": { + "url": client.user.avatarURL + }, + "author": { + "name": client.user.username, + "icon_url": client.user.avatarURL + }, + "fields": [ + { + "name": prefix + "plexTest :", + "value": "a test to see make sure your Plex server is connected properly" + }, + { + "name": prefix + "clearqueue : ", + "value": "clears all songs in queue" + }, + { + "name": prefix + "nextpage :", + "value": "get next page of songs if desired song is not listed" + }, + { + "name": prefix + "pause : ", + "value": "pauses current song if one is playing" + }, + { + "name": prefix + "play :", + "value": "bot will join voice channel and play song if one song available. if more than one, bot will return a list to choose from" + }, + { + "name": prefix + "playsong : ", + "value": "plays a song from the generated song list" + }, + { + "name": prefix + "removesong :", + "value": "removes song by index from the song queue" + }, + { + "name": prefix + "resume :", + "value": "resumes song if previously paused" + }, + { + "name": prefix + "skip :", + "value": "skips the current song if one is playing and plays the next song in queue if it exists" + }, + { + "name": prefix + "stop :", + "value": "stops song if one is playing" + }, + { + "name": prefix + "viewqueue :", + "value": "displays current song queue" + }, + { + "name": prefix + "bot prefix :", + "value": "changes the bot prefix for the guild" + } + ] + } + + return message.channel.send({ embed: help }); + } + else if (cmd){ try { - cmd.process(client, message, query); + cmd.process(client, message, query, prefix); } catch (e) { console.log(e); } } else { - message.reply('**Sorry, that\'s not a command.**'); + return message.channel.send('I\'m sorry **' + message.author.username + '**, that\'s not a command!\nIf you need help, please type `' + prefix + "help` for a list of available commands."); } - } }); }; diff --git a/commands/plex.js b/commands/plex.js index 29a475a..5999525 100644 --- a/commands/plex.js +++ b/commands/plex.js @@ -15,7 +15,7 @@ var plex = new PlexAPI({ password: plexConfig.password, token: plexConfig.token, options: { - identifier: 'PlexBot', + identifier: plexConfig.options.identifier, product: plexConfig.options.identifier, version: plexConfig.options.version, deviceName: plexConfig.options.deviceName, @@ -45,7 +45,7 @@ var conn = null; // plex functions ------------------------------------------------------------ // find song when provided with query string, offset, pagesize, and message -function findSong(query, offset, pageSize, message) { +function findSong(query, offset, pageSize, message, prefix) { plex.query('/search/?type=10&query=' + query + '&X-Plex-Container-Start=' + offset + '&X-Plex-Container-Size=' + pageSize).then(function(res) { tracks = res.MediaContainer.Metadata; @@ -59,7 +59,7 @@ function findSong(query, offset, pageSize, message) { if (resultSize == 1 && offset == 0) { songKey = 0; // add song to queue - addToQueue(songKey, tracks, message); + addToQueue(songKey, tracks, message, prefix); } else if (resultSize > 1) { for (var t = 0; t < tracks.length; t++) { @@ -71,8 +71,8 @@ function findSong(query, offset, pageSize, message) { } messageLines += (t+1) + ' - ' + artist + ' - ' + tracks[t].title + '\n'; } - messageLines += '\n***!playsong (number)** to play your song.*'; - messageLines += '\n***!nextpage** if the song you want isn\'t listed*'; + messageLines += '\n***' + prefix + 'playsong (number)** to play your song.*'; + messageLines += '\n***' + prefix + 'nextpage** if the song you want isn\'t listed*'; message.reply(messageLines); } else { @@ -84,7 +84,7 @@ function findSong(query, offset, pageSize, message) { } // not sure if ill need this -function addToQueue(songNumber, tracks, message) { +function addToQueue(songNumber, tracks, message, prefix) { if (songNumber > -1){ var key = tracks[songNumber].Media[0].Part[0].key; var artist = ''; @@ -98,7 +98,7 @@ function addToQueue(songNumber, tracks, message) { songQueue.push({'artist' : artist, 'title': title, 'key': key}); if (songQueue.length > 1) { - message.reply('You have added **' + artist + ' - ' + title + '** to the queue.\n\n***!viewqueue** to view the queue.*'); + message.reply('You have added **' + artist + ' - ' + title + '** to the queue.\n\n***' + prefix + 'viewqueue** to view the queue.*'); } if (!isPlaying) { @@ -204,8 +204,8 @@ var commands = { 'nextpage' : { usage: '', description: 'get next page of songs if desired song not listed', - process: function(client, message, query) { - findSong(plexQuery, plexOffset, plexPageSize, message); + process: function(client, message, query, prefix) { + findSong(plexQuery, plexOffset, plexPageSize, message, prefix); } }, 'pause' : { @@ -231,13 +231,13 @@ var commands = { 'play' : { usage: '', description: 'bot will join voice channel and play song if one song available. if more than one, bot will return a list to choose from', - process: function(client, message, query) { + process: function(client, message, query, prefix) { // if song request exists if (query.length > 0) { plexOffset = 0; // reset paging plexQuery = null; // reset query for !nextpage - findSong(query, plexOffset, plexPageSize, message); + findSong(query, plexOffset, plexPageSize, message, prefix); } else { message.reply('**Please enter a song title**'); @@ -247,12 +247,12 @@ var commands = { 'playsong' : { usage: '', description: 'play a song from the generated song list', - process: function(client, message, query) { + process: function(client, message, query, prefix) { var songNumber = query; songNumber = parseInt(songNumber); songNumber = songNumber - 1; - addToQueue(songNumber, tracks, message); + addToQueue(songNumber, tracks, message, prefix); } }, 'removesong' : { @@ -336,7 +336,7 @@ var commands = { 'viewqueue' : { usage: '', description: 'displays current song queue', - process: function(client, message) { + process: function(client, message, prefix) { //var messageLines = '\n**Song Queue:**\n\n'; var messageLines = ''; @@ -346,8 +346,8 @@ var commands = { messageLines += (t+1) + ' - ' + songQueue[t].artist + ' - ' + songQueue[t].title + '\n'; } - messageLines += '\n***!removesong (number)** to remove a song*'; - messageLines += '\n***!skip** to skip the current song*'; + messageLines += '\n***' + prefix + 'removesong (number)** to remove a song*'; + messageLines += '\n***' + prefix + 'skip** to skip the current song*'; var embedObj = { embed: { diff --git a/config/keys.js b/config/keys.js index a367ca3..d2953af 100644 --- a/config/keys.js +++ b/config/keys.js @@ -1,6 +1,4 @@ module.exports = { - 'clientId' : 'DISCORD_CLIENT_ID', - 'clientSecret' : 'DISCORD_CLIENT_SECRET', - 'username' : 'DISCORD_BOT_USERNAME', 'botToken' : 'DISCORD_BOT_TOKEN', + 'defaultPrefix' : '!' }; diff --git a/config/plex.js b/config/plex.js index b712bb6..09fadeb 100644 --- a/config/plex.js +++ b/config/plex.js @@ -4,13 +4,11 @@ module.exports = { 'username' : 'PLEX_USERNAME', 'password' : 'PLEX_PASSWORD', 'token' : 'PLEX_TOKEN', - 'machineId' : 'PLEX_MACHINEID', - 'managedUser' : 'PLEX_MANAGED_USERNAME', 'options' : { - 'identifier': 'APP_IDENTIFIER', - 'product' : 'APP_PRODUCT_NAME', - 'version' : 'APP_VERSION_NUMBER', - 'deviceName': 'APP_DEVICE_NAME', + 'identifier': 'Plex-Discord-Bot', + 'product' : 'Node.js App', + 'version' : '1.0.0', + 'deviceName': 'Node.js App', 'platform' : 'Discord', 'device' : 'Discord' } diff --git a/package.json b/package.json index 2ecb70a..2b72436 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,19 @@ "author": "danxfisher", "license": "ISC", "dependencies": { - "bufferutil": "^3.0.2", - "discord.js": "^11.1.0", - "erlpack": "github:hammerandchisel/erlpack", - "ffmpeg-binaries": "^3.2.2-3", - "libsodium-wrappers": "^0.5.2", - "node-opus": "^0.2.6", - "opusscript": "0.0.3", - "plex-api": "^5.1.0", - "request": "^2.81.0", - "uws": "^8.14.1" + "bufferutil": "^4.0.1", + "erlpack": "^0.1.3", + "ffmpeg-static": "^2.7.0", + "libsodium-wrappers": "^0.7.6", + "node-opus": "^0.3.3", + "opusscript": "^0.0.7", + "plex-api": "^5.2.5", + "request": "^2.88.0", + "discord.js": "^11.5.1", + "better-sqlite3": "^5.4.3" + }, + "repository": { + "type": "git", + "url": "https://github.com/danxfisher/Plex-Discord-Bot" } }