diff --git a/doorman.js b/doorman.js index df6bc97..e91481c 100644 --- a/doorman.js +++ b/doorman.js @@ -1,5 +1,7 @@ 'use strict'; +require('debug-trace')({ always: true }); + const config = require('./config'); const Doorman = require('./lib/doorman'); diff --git a/lib/discord/index.js b/lib/discord/index.js deleted file mode 100644 index 4c4ae84..0000000 --- a/lib/discord/index.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const path = require('path'); -const util = require('util'); -const chalk = require('chalk'); - -const DiscordJS = require('discord.js'); -const Plugin = require('../plugin'); - -const helpers = require('../helpers'); -const commandFiles = helpers.getFileArray('./plugins'); - -function Discord (config) { - doorman.discord = new DiscordJS.Client(); - - console.log(chalk.magenta(`Discord Enabled... Starting.\nDiscord.js version: ${Discord.version}`)); - if (doorman.auth.discord && doorman.auth.discord.bot_token) { - console.log('Logging in to Discord...'); - doorman.discord.login(doorman.auth.discord.bot_token); - - // TODO: make these configurable... - require('./onEvent/disconnected')(doorman); - require('./onEvent/guild-member-add')(doorman); - require('./onEvent/guild-member-leave')(doorman); - require('./onEvent/message')(doorman); - require('./onEvent/ready')(doorman); - } else { - console.log(chalk.red('ERROR: doorman must have a Discord bot token...')); - return; - } - - doorman.setupDiscordCommands = function () { - if (commandFiles) { - doorman.discordCommands = {}; - commandFiles.forEach(commandFile => { - try { - commandFile = doorman.require(`${path.join(commandDirectory, commandFile)}`); - } catch (err) { - console.log(chalk.red(`Improper setup of the '${commandFile}' command file. : ${err}`)); - } - - if (commandFile) { - if (commandFile.commands) { - commandFile.commands.forEach(command => { - if (command in commandFile) { - doorman.discordCommands[command] = module[command]; - } - }); - } - } - }); - } - }; - doorman.setupDiscordCommands(); - - console.log(`Loaded ${doorman.commandCount()} base commands`); - console.log(`Loaded ${Object.keys(doorman.discordCommands).length} Discord commands`); -}; - -util.inherits(Discord, Plugin); - -module.exports = Discord; diff --git a/lib/discord/onEvent/disconnected.js b/lib/discord/onEvent/disconnected.js deleted file mode 100644 index 48d3023..0000000 --- a/lib/discord/onEvent/disconnected.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = Doorman => { - Doorman.Discord.on('disconnected', () => { - console.log('Disconnected from Discord!'); - }); -}; diff --git a/lib/discord/onEvent/guild-member-add.js b/lib/discord/onEvent/guild-member-add.js deleted file mode 100644 index 3c6b1d5..0000000 --- a/lib/discord/onEvent/guild-member-add.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = Doorman => { - Doorman.Discord.on('guildMemberAdd', member => { - const guildId = member.guild.id; - const guild = Doorman.Discord.guilds.find('id', guildId); - const defaultChannel = guild.defaultChannel; - - member.send(`Welcome, ${member.user.username}, to the Community!\nBe sure to read our <#${Doorman.config.discord.welcomeChannel}> channel for an overview of our rules and features.`); - defaultChannel.send(`@here, please Welcome ${member.user.username} to ${member.guild.name}!`); - }); -}; diff --git a/lib/discord/onEvent/guild-member-leave.js b/lib/discord/onEvent/guild-member-leave.js deleted file mode 100644 index 0292601..0000000 --- a/lib/discord/onEvent/guild-member-leave.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = Doorman => { - Doorman.Discord.on('guildMemberLeave', member => { - const guild = member.guild.id; - if (guild.defaultChannel) { - guild.defaultChannel.send(`${member.user.username} has left the server.`); - } - }); -}; diff --git a/lib/discord/onEvent/message.js b/lib/discord/onEvent/message.js deleted file mode 100644 index e8ab3f2..0000000 --- a/lib/discord/onEvent/message.js +++ /dev/null @@ -1,147 +0,0 @@ -module.exports = Doorman => { - function discordMessageCb (output, msg, expires, delCalling) { - if (expires) { - return msg.channel.send(output).then(message => message.delete(5000)); - } - if (delCalling) { - return msg.channel.send(output).then(() => msg.delete()); - } - msg.channel.send(output); - } - - function checkMessageForCommand (msg, isEdit) { - // Drop our own messages to prevent feedback loops - if (msg.author === Doorman.Discord.user) { - return; - } - if (msg.channel.type === 'dm') { - msg.channel.send(`I don't respond to direct messages.`); - return; - } - if (msg.isMentioned(Doorman.Discord.user)) { - msg.channel.send('Yes?'); - return; - } - - // Check if message is a command - if (msg.content.startsWith(Doorman.config.commandPrefix)) { - const allCommands = Object.assign(Doorman.Commands, Doorman.discordCommands); - const cmdTxt = msg.content.split(' ')[0].substring(Doorman.config.commandPrefix.length).toLowerCase(); - const suffix = msg.content.substring(cmdTxt.length + Doorman.config.commandPrefix.length + 1); // Add one for the ! and one for the space - const cmd = allCommands[cmdTxt]; - - if (cmdTxt === 'help') { - // Help is special since it iterates over the other commands - if (suffix) { - const cmds = suffix.split(' ').filter(cmd => { - return allCommands[cmd]; - }); - - let info = ''; - if (cmds.length > 0) { - cmds.forEach(cmd => { - if (Doorman.Permissions.checkPermission(msg.guild.id, cmd)) { - info += `**${Doorman.config.commandPrefix + cmd}**`; - const usage = allCommands[cmd].usage; - if (usage) { - info += ` ${usage}`; - } - - let description = allCommands[cmd].description; - - if (description instanceof Function) { - description = description(); - } - - if (description) { - info += `\n\t${description}`; - } - info += '\n'; - } - }); - msg.channel.send(info); - return; - } - msg.channel.send('I can\'t describe a command that doesn\'t exist'); - } else { - msg.author.send('**Available Commands:**').then(() => { - let batch = ''; - const sortedCommands = Object.keys(allCommands).sort(); - for (const i in sortedCommands) { - const cmd = sortedCommands[i]; - let info = `**${Doorman.config.commandPrefix + cmd}**`; - const usage = allCommands[cmd].usage; - - if (usage) { - info += ` ${usage}`; - } - - let description = allCommands[cmd].description; - - if (description instanceof Function) { - description = description(); - } - - if (description) { - info += `\n\t${description}`; - } - - const newBatch = `${batch}\n${info}`; - - if (newBatch.length > (1024 - 8)) { // Limit message length - msg.author.send(batch); - batch = info; - } else { - batch = newBatch; - } - } - - if (batch.length > 0) { - msg.author.send(batch); - } - }); - } - } else if (cmdTxt === 'reload') { - if (msg.member.hasPermission('ADMINISTRATOR')) { - msg.channel.send({ embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: 'Reloading commands...' - } }).then(response => { - Doorman.setupCommands(); - Doorman.setupDiscordCommands(); - response.delete(); - msg.channel.send({ embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Reloaded:\n* ${Doorman.commandCount()} Base Commands\n* ${Object.keys(Doorman.discordCommands).length} Discord Commands` - } }); - }); - } else { - msg.channel.send({ embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `You can't do that Dave...` - } }); - } - } else if (cmd) { - try { - if (Doorman.Permissions.checkPermission(msg.guild.id, cmdTxt)) { - console.log(`Treating ${msg.content} from ${msg.guild.id}:${msg.author} as command`); - cmd.process(msg, suffix, isEdit, discordMessageCb); - } - } catch (err) { - let msgTxt = `Command ${cmdTxt} failed :disappointed_relieved:`; - if (Doorman.config.debug) { - msgTxt += `\n${err.stack}`; - } - msg.channel.send(msgTxt); - } - } else { - msg.channel.send(`${cmdTxt} not recognized as a command!`).then(message => message.delete(5000)); - } - } - } - - Doorman.Discord.on('message', msg => checkMessageForCommand(msg, false)); - Doorman.Discord.on('messageUpdate', (oldMessage, newMessage) => { - checkMessageForCommand(newMessage, true); - }); -}; diff --git a/lib/discord/onEvent/ready.js b/lib/discord/onEvent/ready.js deleted file mode 100644 index 8bd471b..0000000 --- a/lib/discord/onEvent/ready.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = Doorman => { - Doorman.Discord.once('ready', () => { - console.log(`Logged into Discord! Serving in ${Doorman.Discord.guilds.array().length} Discord servers`); - }); - - Doorman.Discord.on('ready', () => { - Doorman.Discord.user.setPresence({ game: { name: `${Doorman.config.commandPrefix}help | ${Doorman.Discord.guilds.array().length} Servers`, type: 0 } }); - }); -}; diff --git a/lib/discord/plugins/moderation.js b/lib/discord/plugins/moderation.js deleted file mode 100644 index f0fd098..0000000 --- a/lib/discord/plugins/moderation.js +++ /dev/null @@ -1,319 +0,0 @@ -module.exports = Doorman => { - let lastPruned = new Date().getTime() - (Doorman.config.discord.pruneInterval * 1000); - - return { - commands: [ - 'ban', - 'kick', - 'prune', - 'topic', - 'servers' - ], - ban: { - usage: ' [days of messages to delete] [reason]', - description: 'bans the user, optionally deleting messages from them in the last x days', - process: (msg, suffix) => { - const args = suffix.split(' '); - if (args.length > 0 && args[0]) { - if (!msg.guild.me.hasPermission('BAN_MEMBERS')) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `I don't have permission to ban people!` - } - }).then(message => message.delete(5000)); - - return; - } - - if (!msg.member.hasPermission('BAN_MEMBERS')) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `You don't have permission to ban people, ${msg.member}!` - } - }).then(message => message.delete(5000)); - - return; - } - - Doorman.Discord.fetchUser(Doorman.resolveMention(args[0])).then(member => { - if (member !== undefined) { - if (msg.mentions.members.first() === member && !member.bannable) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `I can't ban ${member}. Do they have the same or a higher role than me?` - } - }).then(message => message.delete(5000)); - return; - } - if (args.length > 1) { - if (!isNaN(parseInt(args[1], 10))) { - if (args.length > 2) { - const days = args[1]; - const reason = args.slice(2).join(' '); - msg.guild.ban(member, { days: parseFloat(days), reason }).then(() => { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Banning ${member} from ${msg.guild} for ${reason}!` - } - }); - }).catch(() => msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Banning ${member} from ${msg.guild} failed!` - } - })); - } else { - const days = args[1]; - msg.guild.ban(member, { days: parseFloat(days) }).then(() => { - msg.channel.send(`Banning ${member} from ${msg.guild}!`); - }).catch(() => msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Banning ${member} from ${msg.guild} failed!` - } - })); - } - } else { - const reason = args.slice(1).join(' '); - msg.guild.ban(member, { reason }).then(() => { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Banning ${member} from ${msg.guild} for ${reason}!` - } - }); - }).catch(() => msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Banning ${member} from ${msg.guild} failed!` - } - })); - } - } else { - msg.guild.ban(member).then(() => { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Banning ${member} from ${msg.guild}!` - } - }); - }).catch(() => msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Banning ${member} from ${msg.guild} failed!` - } - })); - } - } - }).catch(() => { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Cannot find a user by the nickname of ${args[0]}. Try using their snowflake.` - } - }); - }); - } else { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: 'You must specify a user to ban.' - } - }); - } - } - }, - kick: { - usage: ' [reason]', - description: 'Kick a user with an optional reason. Requires both the command user and the bot to have kick permission', - process: (msg, suffix) => { - const args = suffix.split(' '); - - if (args.length > 0 && args[0]) { - if (!msg.guild.me.hasPermission('KICK_MEMBERS')) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `I don't have permission to kick people!` - } - }).then(message => message.delete(5000)); - - return; - } - - if (!msg.member.hasPermission('KICK_MEMBERS')) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `I don't have permission to kick people, ${msg.member}!` - } - }).then(message => message.delete(5000)); - - return; - } - - const member = msg.mentions.members.first(); - - if (member !== undefined) { - if (!member.kickable) { - msg.channel.send(`I can't kick ${member}. Do they have the same or a higher role than me?`); - return; - } - if (args.length > 1) { - const reason = args.slice(1).join(' '); - member.kick(reason).then(() => { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Kicking ${member} from ${msg.guild} for ${reason}!` - } - }); - }).catch(() => msg.channel.send(`Kicking ${member} failed!`)); - } else { - member.kick().then(() => { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Kicking ${member} from ${msg.guild}!` - } - }); - }).catch(() => msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Kicking ${member} failed!` - } - }).then(message => message.delete(5000))); - } - } else { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `I couldn't find a user ${args[0]}` - } - }); - } - } else { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: 'You must specify a user to kick.' - } - }); - } - } - }, - prune: { - usage: '', - process: (msg, suffix) => { - if (!suffix) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: 'You must specify a number of messages to prune.' - } - }).then(message => message.delete(5000)); - - return; - } - - if (!msg.guild.me.hasPermission('MANAGE_MESSAGES')) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `I can't prune messages, ${msg.member}...` - } - }).then(message => message.delete(5000)); - - return; - } - - if (!msg.member.hasPermission('MANAGE_MESSAGES')) { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `You can't prune messages, ${msg.member}...` - } - }).then(message => message.delete(5000)); - - return; - } - - const timeSinceLastPrune = Math.floor(new Date().getTime() - lastPruned); - - if (timeSinceLastPrune > (Doorman.config.discord.pruneInterval * 1000)) { - if (!isNaN(parseInt(suffix, 10))) { - let count = parseInt(suffix, 10); - - count++; - - if (count > Doorman.config.discord.pruneMax) { - count = Doorman.config.discord.pruneMax; - } - - msg.channel.fetchMessages({ limit: count }) - .then(messages => messages.map(m => m.delete().catch(() => { }))) - .then(() => { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `Pruning ${(count === 100) ? 100 : count - 1} messages...` - } - }).then(message => message.delete(5000).catch(() => { })); - }); - - lastPruned = new Date().getTime(); - } else { - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `I need a numerical number...` - } - }).then(message => message.delete(5000)); - } - } else { - const wait = Math.floor(Doorman.config.discord.pruneInterval - (timeSinceLastPrune / 1000)); - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - description: `You can't do that yet, please wait ${wait} second${wait > 1 ? 's' : ''}` - } - }).then(message => message.delete(5000)); - } - } - }, - topic: { - description: 'Shows the purpose of the chat channel', - process: msg => { - let response = msg.channel.topic; - if (msg.channel.topic.trim() === '') { - response = `There doesn't seem to be a topic for this channel. Maybe ask the mods?`; - } - - msg.channel.send({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - title: msg.channel.name, - description: response - } - }); - } - }, - servers: { - usage: '', - description: 'Returns a list of servers the bot is connected to', - process: (msg, suffix, isEdit, cb) => { - cb({ - embed: { - color: Doorman.config.discord.defaultEmbedColor, - title: Doorman.Discord.user.username, - description: `Currently on the following servers:\n\n${Doorman.Discord.guilds.map(g => `${g.name} - **${g.memberCount} Members**`).join(`\n`)}` - } - }, msg); - } - } - }; -}; diff --git a/lib/discord/plugins/musicplayer.js b/lib/discord/plugins/musicplayer.js deleted file mode 100644 index dea81b6..0000000 --- a/lib/discord/plugins/musicplayer.js +++ /dev/null @@ -1,373 +0,0 @@ -const YoutubeDL = require('youtube-dl'); -const request = require('request'); - -module.exports = Doorman => { - const options = false; - const GLOBAL_QUEUE = (options && options.global) || false; - const MAX_QUEUE_SIZE = (options && options.maxQueueSize) || 20; - const queues = {}; - - function getQueue (server) { - // Check if global queues are enabled. - if (GLOBAL_QUEUE) { - server = '_'; // Change to global queue. - } - - // Return the queue. - if (!queues[server]) { - queues[server] = []; - } - - return queues[server]; - } - - function executeQueue (msg, queue) { - // If the queue is empty, finish. - if (queue.length === 0) { - msg.channel.send(createEmbed('Playback finished.')); - - // Leave the voice channel. - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - if (voiceConnection !== null) { - if (voiceConnection.player.dispatcher) { - voiceConnection.player.dispatcher.end(); - } - - voiceConnection.channel.leave(); - return; - } - } - - new Promise((resolve, reject) => { - // Join the voice channel if not already in one. - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - - if (voiceConnection == null) { - // Check if the user is in a voice channel. - const voiceChannel = getAuthorVoiceChannel(msg); - - if (voiceChannel != null) { - voiceChannel.join().then(connection => { - resolve(connection); - }).catch(console.error); - } else { - // Otherwise, clear the queue and do nothing. - queue.splice(0, queue.length); - reject(); - } - } else { - resolve(voiceConnection); - } - }).then(connection => { - // Get the first item in the queue. - const video = queue[0]; - - // Play the video. - msg.channel.send(createEmbed(`Now Playing: ${video.title}`)).then(() => { - const dispatcher = connection.playStream(request(video.url)); - - dispatcher.on('debug', i => console.log(`debug: ${i}`)); - // Catch errors in the connection. - dispatcher.on('error', err => { - msg.channel.send(`fail: ${err}`); - // Skip to the next song. - queue.shift(); - executeQueue(msg, queue); - }); - - // Catch the end event. - dispatcher.on('end', () => { - // Wait a second. - setTimeout(() => { - // Remove the song from the queue. - queue.shift(); - - // Play the next song in the queue. - executeQueue(msg, queue); - }, 1000); - }); - }).catch(console.error); - }).catch(console.error); - } - - function getAuthorVoiceChannel (msg) { - const voiceChannelArray = msg.guild.channels.filter(v => v.type.toLowerCase() === 'voice').filter(v => v.members.has(msg.author.id)).array(); - - if (voiceChannelArray.length === 0) { - return null; - } - - return voiceChannelArray[0]; - } - - function createEmbed (text) { - return { - embed: { - color: Doorman.config.discord.defaultEmbedColor, - author: { - name: 'Music Player', - icon_url: 'https://emojipedia-us.s3.amazonaws.com/thumbs/120/twitter/103/multiple-musical-notes_1f3b6.png' - }, - footer: { - text: 'powered by youtube-dl' - }, - description: text - } - }; - } - - return { - commands: [ - 'play', - 'skip', - 'queue', - 'dequeue', - 'pause', - 'resume', - 'stop', - 'volume' - ], - play: { - usage: '', - description: `Plays the given video in the user's voice channel. Supports YouTube and many others: `, - process: (msg, suffix, isEdit) => { - if (isEdit) { - return; - } - - const arr = msg.guild.channels.filter(v => v.type === 'voice').filter(v => v.members.has(msg.author.id)); - // Make sure the user is in a voice channel. - if (arr.length === 0) { - return msg.channel.send(createEmbed(`You're not in a voice channel.`)); - } - - // Make sure the suffix exists. - if (!suffix) { - return msg.channel.send(createEmbed('No video specified!')); - } - - // Get the queue. - const queue = getQueue(msg.guild.id); - - // Check if the queue has reached its maximum size. - if (queue.length >= MAX_QUEUE_SIZE) { - return msg.channel.send(createEmbed('Maximum queue size reached!')); - } - - // Get the video information. - msg.channel.send(createEmbed('Searching...')).then(response => { - // If the suffix doesn't start with 'http', assume it's a search. - if (!suffix.toLowerCase().startsWith('http')) { - suffix = `gvsearch1:${suffix}`; - } - - // Get the video info from youtube-dl. - YoutubeDL.getInfo(suffix, ['-q', '--no-warnings', '--force-ipv4'], (err, info) => { - // Verify the info. - if (err || info.format_id === undefined || info.format_id.startsWith('0')) { - return response.edit(createEmbed('Invalid video!')); - } - - // Queue the video. - response.edit(createEmbed(`Queued: ${info.title}`)).then(resp => { - queue.push(info); - - // Play if only one element in the queue. - if (queue.length === 1) { - executeQueue(msg, queue); - resp.delete(1000); - } - }).catch(() => { }); - }); - }).catch(() => { }); - } - }, - skip: { - description: 'skips to the next song in the playback queue', - process: (msg, suffix) => { - // Get the voice connection. - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - if (voiceConnection === null) { - return msg.channel.send(createEmbed('No music being played.')); - } - - // Get the queue. - const queue = getQueue(msg.guild.id); - - // Get the number to skip. - let toSkip = 1; // Default 1. - if (!isNaN(suffix) && parseInt(suffix, 10) > 0) { - toSkip = parseInt(suffix, 10); - } - toSkip = Math.min(toSkip, queue.length); - - // Skip. - queue.splice(0, toSkip - 1); - - // Resume and stop playing. - if (voiceConnection.player.dispatcher) { - voiceConnection.player.dispatcher.resume(); - } - - voiceConnection.player.dispatcher.end(); - - msg.channel.send(createEmbed(`Skipped ${toSkip}!`)); - } - }, - queue: { - description: 'prints the current music queue for this server', - process: msg => { - // Get the queue. - const queue = getQueue(msg.guild.id); - - // Get the queue text. - const text = queue.map((video, index) => (`${(index + 1)}: ${video.title}`)).join('\n'); - - // Get the status of the queue. - let queueStatus = 'Stopped'; - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - if (voiceConnection !== null && voiceConnection !== undefined) { - queueStatus = voiceConnection.paused ? 'Paused' : 'Playing'; - } - - // Send the queue and status. - msg.channel.send(createEmbed(`Queue (${queueStatus}):\n${text}`)); - } - }, - dequeue: { - description: 'Dequeues the given song index from the song queue. Use the queue command to get the list of songs in the queue.', - process: (msg, suffix) => { - // Define a usage string to print out on errors - const usageString = `The format is "${Doorman.config.commandPrefix}dequeue ". Use ${Doorman.config.commandPrefix}queue to find the indices of each song in the queue.`; - - // Get the queue. - const queue = getQueue(msg.guild.id); - - // Make sure the suffix exists. - if (!suffix) { - return msg.channel.send(createEmbed(`You need to specify an index to remove from the queue. ${usageString}`)); - } - - // Get the arguments - const split = suffix.split(/(\s+)/); - - // Make sure there's only 1 index - if (split.length > 1) { - return msg.channel.send(createEmbed(`There are too many arguments. ${usageString}`)); - } - - // Remove the index - let index = parseInt(split[0], 10); - let songRemoved = ''; // To be filled out below - if (!isNaN(index)) { - index--; - - if (index >= 0 && index < queue.length) { - songRemoved = queue[index].title; - - if (index === 0) { - // If it was the first one, skip it - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - if (voiceConnection.player.dispatcher) { - voiceConnection.player.dispatcher.resume(); - } - - voiceConnection.player.dispatcher.end(); - } else { - // Otherwise, just remove it from the queue - queue.splice(index, 1); - } - } else { - return msg.channel.send(createEmbed(`The index is out of range. ${usageString}`)); - } - } else { - return msg.channel.send(createEmbed(`That index isn't a number. ${usageString}`)); - } - - // Send the queue and status. - msg.channel.send(createEmbed(`Removed '${songRemoved}' (index ${split[0]}) from the queue.`)); - } - }, - pause: { - description: 'pauses music playback', - process: msg => { - // Get the voice connection. - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - if (voiceConnection === null) { - return msg.channel.send(createEmbed('No music being played.')); - } - - // Pause. - msg.channel.send(createEmbed('Playback paused.')); - - if (voiceConnection.player.dispatcher) { - voiceConnection.player.dispatcher.pause(); - } - } - }, - resume: { - description: 'resumes music playback', - process: msg => { - // Get the voice connection. - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - if (voiceConnection === null) { - return msg.channel.send(createEmbed('No music being played.')); - } - - // Resume. - msg.channel.send(createEmbed('Playback resumed.')); - if (voiceConnection.player.dispatcher) { - voiceConnection.player.dispatcher.resume(); - } - } - }, - stop: { - description: 'stops playback and removes everything from queue', - process: msg => { - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - const queue = getQueue(msg.guild.id); - // Clear Queue - queue.splice(0, queue.length); - // Resume and stop playing. - if (voiceConnection.player.dispatcher) { - voiceConnection.player.dispatcher.resume(); - } - voiceConnection.player.dispatcher.end(); - } - }, - volume: { - usage: '', - description: 'set music playback volume as a fraction, a percent, or in dB', - process: (msg, suffix) => { - // Get the voice connection. - const voiceConnection = Doorman.Discord.voiceConnections.get(msg.guild.id); - - if (voiceConnection === null) { - return msg.channel.send(createEmbed('No music being played.')); - } - - // Set the volume - if (voiceConnection.player.dispatcher) { - if (suffix === '') { - const displayVolume = Math.pow(voiceConnection.player.dispatcher.volume, 0.6020600085251697) * 100.0; - msg.channel.send(createEmbed(`volume: ${displayVolume}%`)); - } else if (suffix.toLowerCase().indexOf('db') === -1) { - if (suffix.indexOf('%') === -1) { - if (suffix > 1) { - suffix /= 100.0; - } - - voiceConnection.player.dispatcher.setVolumeLogarithmic(suffix); - } else { - const num = suffix.split('%')[0]; - voiceConnection.player.dispatcher.setVolumeLogarithmic(num / 100.0); - } - } else { - const value = suffix.toLowerCase().split('db')[0]; - voiceConnection.player.dispatcher.setVolumeDecibels(value); - } - } - } - } - }; -}; diff --git a/lib/disk.js b/lib/disk.js index e0c432c..c26bd01 100644 --- a/lib/disk.js +++ b/lib/disk.js @@ -2,12 +2,19 @@ const fs = require('fs'); -function Disk () { +function Disk (root) { this.type = 'Disk'; + this.root = root || process.env.PWD; } Disk.prototype.exists = function (path) { - return fs.existsSync(path); + let full = [this.root, path].join('/'); + return fs.existsSync(full); +}; + +Disk.prototype.get = function (path) { + let full = [this.root, path].join('/'); + return require(full); }; module.exports = Disk; diff --git a/lib/doorman.js b/lib/doorman.js index 0fa77b6..890cfa0 100644 --- a/lib/doorman.js +++ b/lib/doorman.js @@ -16,10 +16,10 @@ function Doorman (config) { self.services = {}; self.triggers = {}; - self.router = new Router(); - self.scribe = new Scribe({ - namespace: 'doorman' - }); + self.router = new Router({ trigger: config.trigger }); + self.scribe = new Scribe({ namespace: 'doorman' }); + + return self; } require('util').inherits(Doorman, require('events').EventEmitter); @@ -30,7 +30,7 @@ Doorman.prototype.start = function configure () { if (self.config.triggers) { Object.keys(self.config.triggers).forEach(name => { let route = { - name: name, + name: self.config.trigger + name, value: self.config.triggers[name] }; @@ -60,13 +60,18 @@ Doorman.prototype.start = function configure () { }; Doorman.prototype.enable = function enable (name) { + let self = this; + let Service = require(`../services/${name}`); let service = new Service(this.config[name]); - - console.log('dat service:', service); - service.on('message', function (msg) { - console.log('magic handler, incoming magic message:', msg); + service.on('message', async function (msg) { + console.log(`magic handler ${name}, incoming magic message:`, msg); + let response = await self.parse(msg.object); + + self.scribe.log('response:', response); + + service.send(msg.target, response); }); this.services[name] = service; @@ -76,17 +81,14 @@ Doorman.prototype.enable = function enable (name) { }; Doorman.prototype.use = function assemble (plugin) { - this.scribe.log(`importing ${plugin}...`); - + let self = this; let handler = Plugin.fromName(plugin); - this.scribe.log('handler:', handler); if (handler) { - let trigger = this.register(handler); - - if (!trigger) { - this.scribe.warn(`Could not successfully configure ${plugin}. Plugin disabled.`); - } + Object.keys(handler).forEach(name => { + let value = handler[name]; + self.register({ name, value }); + }); } return this; @@ -96,13 +98,18 @@ Doorman.prototype.register = function configure (handler) { if (!handler.name) return false; if (!handler.value) return false; + this.scribe.log('handler registered:', handler); + this.triggers[handler.name] = handler.value; + this.router.use(handler); + + this.emit('trigger', handler); return this; }; -Doorman.prototype.parse = function interpret (msg) { - let answers = this.router.route(msg); +Doorman.prototype.parse = async function interpret (msg) { + let answers = await this.router.route(msg); let message = null; if (answers.length) { diff --git a/lib/plugin.js b/lib/plugin.js index 4cad82b..c69f46f 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -10,12 +10,12 @@ function Plugin (doorman) { util.inherits(Plugin, require('events').EventEmitter); Plugin.fromName = function (name) { - let disk = new Disk('../'); + let disk = new Disk(); let path = `plugins/${name}`; let plugin = null; - if (disk.exists(path)) { - plugin = require(path); + if (disk.exists(path) + '.js' || disk.exists(path)) { + plugin = disk.get(path); } else { try { plugin = require(`doorman-${name}`); diff --git a/lib/router.js b/lib/router.js index 357d4e6..a62ca72 100644 --- a/lib/router.js +++ b/lib/router.js @@ -5,8 +5,9 @@ * @param {Object} map Map of command names => behaviors. * @constructor */ -function Router (map) { - this.handlers = map || {}; +function Router (config) { + this.config = config || {}; + this.handlers = {}; } /** @@ -16,13 +17,28 @@ function Router (map) { */ Router.prototype.route = async function handle (msg) { if (typeof msg !== 'string') return; - let parts = msg.split(/\s+/g); + let parts = msg + .split(/\s+/g) + .filter(x => x.charAt(0) === this.config.trigger) + .map(x => x.substr(1)); let output = []; - for (var token in parts) { + for (var i in parts) { + let token = parts[i]; let command = token.toLowerCase(); - if (this.handlers[command]) { - let result = await this.handlers[command].apply({}, msg); + let handler = this.handlers[command]; + let result = null; + + if (handler) { + switch (typeof handler.value) { + case 'string': + result = handler.value; + break; + default: + result = await handler.value.apply({}, msg); + break; + } + if (result) { output.push(result); } diff --git a/lib/scribe.js b/lib/scribe.js index a15aea0..3807dc0 100644 --- a/lib/scribe.js +++ b/lib/scribe.js @@ -3,21 +3,26 @@ function Scribe (config) { this.type = 'Scribe'; this.config = config || { namespace: 'scribe' }; + this.stack = []; } +Scribe.prototype.inherits = function inherit (scribe) { + this.stack.push(scribe.config.namespace); +}; + Scribe.prototype.log = function append (...inputs) { inputs.unshift(['[', this.config.namespace.toUpperCase(), ']'].join('')); - console.log.apply(null, inputs); + console.log.apply(null, this.stack.concat(inputs)); }; Scribe.prototype.throw = function exit (...inputs) { inputs.unshift(['[', this.config.namespace.toUpperCase(), ']'].join('')); - console.error.apply(null, inputs); + console.error.apply(null, this.stack.concat(inputs)); }; Scribe.prototype.warn = function exit (...inputs) { inputs.unshift(['[', this.config.namespace.toUpperCase(), ']'].join('')); - console.warn.apply(null, inputs); + console.warn.apply(null, this.stack.concat(inputs)); }; module.exports = Scribe; diff --git a/lib/service.js b/lib/service.js index b0eb241..35a91ed 100644 --- a/lib/service.js +++ b/lib/service.js @@ -16,10 +16,22 @@ Service.prototype.connect = function initialize () { this.connection = { status: 'active' }; + + this.bus = new stream.Transform(); }; +/** + * Default route handler for an incoming message. Follows the Activity Streams + * 2.0 spec: https://www.w3.org/TR/activitystreams-core/ + * @param {Object} message Message object. + * @return {Boolean} Message handled! + */ Service.prototype.handler = function route (message) { - this.emit('message', message.text); + this.emit('message', { + actor: message.actor, + target: message.target, + object: message.object + }); }; Service.prototype.send = function send (channel, message) { diff --git a/lib/slack/index.js b/lib/slack/index.js deleted file mode 100644 index 7163649..0000000 --- a/lib/slack/index.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const chalk = require('chalk'); -const Slack = require('@slack/client'); -const RTMClient = Slack.RTMClient; - -module.exports = doorman => { - console.log(chalk.magenta(`Slack enabled. Starting...`)); - if (doorman.auth.slack && doorman.auth.slack.bot_token) { - console.log('Logging in to Slack...'); - - // initialize Slack client - doorman.slack = new RTMClient(doorman.auth.slack.bot_token); - doorman.slack.c_events = Slack.CLIENT_EVENTS; - doorman.slack.rtm_events = Slack.RTM_EVENTS; - - // TODO: ask naterchrdsn why this is before plugins in the control flow? - doorman.slack.start(); - - // load event handlers? - require('./onEvent/auth-and-connect')(doorman); - require('./onEvent/message')(doorman); - require('./onEvent/team-join')(doorman); - } -}; diff --git a/lib/slack/onEvent/auth-and-connect.js b/lib/slack/onEvent/auth-and-connect.js deleted file mode 100644 index fdece9c..0000000 --- a/lib/slack/onEvent/auth-and-connect.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function (doorman) { - doorman.slack.on('ready', rtmStartData => { - //console.log(`Logged into Slack! Name: ${rtmStartData.self.name}, Team:${rtmStartData.team.name}`); - console.log(`Logged into Slack!`); - }); - doorman.slack.on('connected', () => { - console.log(`Connection to Slack Successful!`); - }); -}; diff --git a/lib/slack/onEvent/message.js b/lib/slack/onEvent/message.js deleted file mode 100644 index 072e3a7..0000000 --- a/lib/slack/onEvent/message.js +++ /dev/null @@ -1,206 +0,0 @@ -module.exports = function (doorman) { - function currentChannelHasHook (channelId) { - if (doorman.Auth.slack.webhooks[doorman.slack.dataStore.getChannelById(channelId).name]) { - return true; - } - return false; - } - - function slackMessageCb (output, msg) { - // Format the message output for the Slack API - if (typeof output === 'object') { - const reformatted = { - attachments: [ - { fallback: 'There was an error' } - ] - }; - if (output.embed.color) { - reformatted.attachments[0].color = `#${output.embed.color}`; - } - if (output.embed.title) { - reformatted.attachments[0].title = output.embed.title; - } - if (output.embed.description) { - reformatted.attachments[0].text = output.embed.description; - } - if (typeof output.embed.image === 'object') { - reformatted.attachments[0].image_url = output.embed.image.url; - } - if (typeof output.embed.thumbnail === 'object') { - reformatted.attachments[0].thumb_url = output.embed.thumbnail.url; - } - if (output.embed.footer) { - reformatted.attachments[0].footer = output.embed.footer; - } - if (typeof output.embed.author === 'object') { - if (output.embed.author.name) { - reformatted.attachments[0].author_name = output.embed.author.name; - } - if (output.embed.author.url) { - reformatted.attachments[0].author_link = output.embed.author.url; - } - if (output.embed.author.icon_url) { - reformatted.attachments[0].author_icon = output.embed.author.icon_url; - } - } - if (typeof output.embed.fields === 'object') { - reformatted.attachments[0].fields = []; - output.embed.fields.forEach(field => { - reformatted.attachments[0].fields.push({ - title: field.name, - value: field.value, - short: field.inline - }); - }); - } - if (output.reply) { - reformatted.attachments[0].pretext = `<@${output.reply}>`; - } - if (currentChannelHasHook(msg.channel)) { - const url = doorman.Auth.slack.webhooks[doorman.slack.dataStore.getChannelById(msg.channel).name]; - require('request').post({ - uri: url, - json: true, - body: reformatted - }); - } - return; - } - - // TODO: add expiring and maybe delCalling pieces - // Then send it and interact with it based on the supplied flags... - /* if (expires) { - return msg.channel.send(output).then(message => message.delete(5000)); - } - if (delCalling) { - return msg.channel.send(output).then(() => msg.delete()); - } */ - doorman.slack.sendMessage(output, msg.channel); - } - - function checkMessageForCommand (msg, isEdit) { - // TODO: re-bind doorman.config to doorman.config - //const bot = doorman.slack.dataStore.getBotByName(doorman.config.name); - msg.author = `<@${msg.user}>`; - - // Drop our own messages to prevent feedback loops - if (msg.subtype && msg.subtype === 'bot_message') { - return; - } - - if (doorman.config.debug) { - console.log('message received:', msg.type, msg.subtype, 'interpreting...'); - } - - // Check for mention - /* if (msg.text.split(' ')[0] === `<@${bot.id}>`) { - if (doorman.config.elizaEnabled) { - // If Eliza AI is enabled, respond to @mention - const message = msg.text.replace(`<@${bot.id}> `, ''); - doorman.slack.sendMessage(doorman.Eliza.transform(message), msg.channel); - return; - } - doorman.slack.sendMessage('Yes?', msg.channel); - return; - }*/ - - // Check for IM - /*if (doorman.slack.dataStore.getDMById(msg.channel)) { - if (msg.text.startsWith(doorman.config.commandPrefix) && msg.text.split(' ')[0].substring(doorman.config.commandPrefix.length).toLowerCase() === 'reload') { - doorman.setupCommands(); - doorman.setupSlackCommands(); - doorman.slack.sendMessage(`Reloaded ${doorman.commandCount()} Base Commands`, msg.channel); - doorman.slack.sendMessage(`Reloaded ${Object.keys(doorman.slackCommands).length} Slack Commands`, msg.channel); - return; - } - doorman.slack.sendMessage(`I don't respond to direct messages.`, msg.channel); - return; - }*/ - - // Check if message is a command - if (msg.text.startsWith(doorman.config.commandPrefix)) { - const allCommands = Object.assign(doorman.Commands, doorman.slackCommands); - const cmdTxt = msg.text.split(' ')[0].substring(doorman.config.commandPrefix.length).toLowerCase(); - const suffix = msg.text.substring(cmdTxt.length + doorman.config.commandPrefix.length + 1); // Add one for the ! and one for the space - const cmd = allCommands[cmdTxt]; - - console.log('MESSAGE:', msg); - - if (cmdTxt === 'help') { - const DM = doorman.slack.dataStore.getDMByUserId(msg.user).id; - if (suffix) { - const cmds = suffix.split(' ').filter(cmd => { - return allCommands[cmd]; - }); - let info = ''; - if (cmds.length > 0) { - cmds.forEach(cmd => { - // TODO: add permissions check back here - info += `**${doorman.config.commandPrefix + cmd}**`; - const usage = allCommands[cmd].usage; - if (usage) { - info += ` ${usage}`; - } - let description = allCommands[cmd].description; - if (description instanceof Function) { - description = description(); - } - if (description) { - info += `\n\t${description}`; - } - info += '\n'; - }); - doorman.slack.sendMessage(info, DM); - return; - } - doorman.slack.sendMessage('I can\'t describe a command that doesn\'t exist', msg.channel); - } else { - doorman.slack.sendMessage('**Available Commands:**', DM); - let batch = ''; - const sortedCommands = Object.keys(allCommands).sort(); - for (const i in sortedCommands) { - const cmd = sortedCommands[i]; - let info = `**${doorman.config.commandPrefix + cmd}**`; - const usage = allCommands[cmd].usage; - if (usage) { - info += ` ${usage}`; - } - let description = allCommands[cmd].description; - if (description instanceof Function) { - description = description(); - } - if (description) { - info += `\n\t${description}`; - } - const newBatch = `${batch}\n${info}`; - if (newBatch.length > (1024 - 8)) { // Limit message length - doorman.slack.sendMessage(batch, DM); - batch = info; - } else { - batch = newBatch; - } - } - if (batch.length > 0) { - doorman.slack.sendMessage(batch, DM); - } - } - } else if (cmd) { - try { - // Add permissions check back here, too - console.log(`Treating ${msg.text} from ${msg.team}:${msg.user} as command`); - cmd.process(msg, suffix, isEdit, slackMessageCb); - } catch (err) { - let msgTxt = `Command ${cmdTxt} failed :disappointed_relieved:`; - if (doorman.config.debug) { - msgTxt += `\n${err.stack}`; - } - doorman.slack.sendMessage(msgTxt, msg.channel); - } - } else { - doorman.slack.sendMessage(`${cmdTxt} not recognized as a command!`, msg.channel); - } - } - } - - doorman.slack.on('message', msg => checkMessageForCommand(msg, false)); -}; diff --git a/lib/slack/onEvent/team-join.js b/lib/slack/onEvent/team-join.js deleted file mode 100644 index ccb0879..0000000 --- a/lib/slack/onEvent/team-join.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function (doorman) { - doorman.slack.on('team_join', event => { - doorman.slack.sendMessage(`@here, please Welcome ${event.user.name} to ${doorman.config.slack.teamName}!`, doorman.slack.dataStore.getChannelByName(doorman.config.slack.welcomeChannel).id); - }); -}; diff --git a/package.json b/package.json index 8e2f978..972ba64 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node doorman.js", "test": "NODE_ENV=test mocha --recursive", - "coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive" + "coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive", + "make:docs": "jsdoc doorman.js -d docs" }, "repository": { "type": "git", @@ -32,27 +33,7 @@ "homepage": "https://github.com/FabricLabs/doorman#readme", "dependencies": { "@slack/client": "^4.1.0", - "bufferutil": "^3.0.1", - "chalk": "^2.0.1", - "discord.js": "^11.1.0", - "doorman-admin": "FabricLabs/doorman-admin", - "doorman-beer-lookup": "FabricLabs/doorman-beer-lookup", - "doorman-catfact": "FabricLabs/doorman-catfact", - "doorman-cocktail-lookup": "FabricLabs/doorman-cocktail-lookup", - "doorman-dice": "FabricLabs/doorman-dice", - "doorman-dictionary": "FabricLabs/doorman-dictionary", - "doorman-misc": "FabricLabs/doorman-misc", - "doorman-translator": "FabricLabs/doorman-translator", - "doorman-urbandictionary": "FabricLabs/doorman-urbandictionary", - "doorman-wikipedia": "FabricLabs/doorman-wikipedia", - "doorman-xkcd": "FabricLabs/doorman-xkcd", - "erlpack": "hammerandchisel/erlpack", - "libsodium-wrappers": "^0.5.2", - "maki": "martindale/maki#0.3", - "maki-client-level": "^0.1.0", - "node-opus": "^0.2.6", - "sodium": "^2.0.1", - "youtube-dl": "^1.11.1" + "discord.js": "^11.1.0" }, "devDependencies": { "chai": "^4.1.2", diff --git a/plugins/debug.js b/plugins/debug.js new file mode 100644 index 0000000..24def65 --- /dev/null +++ b/plugins/debug.js @@ -0,0 +1,4 @@ +module.exports = { + test: 'This is a test.', + debug: `Here's some debug data (current context):\n\`\`\`\n${JSON.stringify(this)}\`\`\`` +}; diff --git a/services/slack.js b/services/slack.js index ce13871..8b63d6f 100644 --- a/services/slack.js +++ b/services/slack.js @@ -15,23 +15,35 @@ util.inherits(Slack, Service); Slack.prototype.connect = function initialize () { if (this.config.token) { this.connection = new SlackSDK.RTMClient(this.config.token); - this.connection.on('ready', this.ready); + // TODO: this event is bound twice, please fix + this.connection.on('ready', this.ready.bind(this)); this.connection.on('message', this.handler.bind(this)); this.connection.start(); } }; -Slack.prototype.ready = function (data) { - console.log('le data:', data); +Slack.prototype.ready = async function (data) { + let self = this; + let slack = new SlackSDK.WebClient(this.config.token); + let result = await slack.channels.list(); + + result.channels.forEach(channel => { + self.map[`/topics/${channel.name}`] = channel.id; + }); + + self.emit('ready'); }; Slack.prototype.handler = function route (message) { - this.emit('message', message.text); + this.emit('message', { + actor: message.user, + target: message.channel, + object: message.text + }); }; Slack.prototype.send = function send (channel, message) { - console.log('[SLACK]', 'send:', channel, message); - this.connection.sendMessage(message, this.map[channel]); + this.connection.sendMessage(message, channel); }; module.exports = Slack;