From 8ae81f1cc6ac83087ec4b333a8b0d4bbadda816d Mon Sep 17 00:00:00 2001 From: Lily Wonhalf Date: Sun, 2 May 2021 20:00:36 -0400 Subject: [PATCH] Initial commit --- .gitignore | 6 + bot.js | 105 +++++++++++ config.json.sample | 12 ++ emoji-characters.json | 13 ++ event/bot/message.js | 12 ++ event/bot/ready.js | 19 ++ model/command-category.js | 11 ++ model/command-permission.js | 34 ++++ model/command.js | 108 +++++++++++ model/command/avatar.js | 56 ++++++ model/command/changelog.js | 81 +++++++++ model/command/clean.js | 34 ++++ model/command/custom-front.js | 42 +++++ model/command/eval.js | 55 ++++++ model/command/help.js | 179 ++++++++++++++++++ model/command/kill.js | 30 +++ model/command/reload.js | 29 +++ model/command/set-avatar.js | 34 ++++ model/globals.js | 1 + model/guild.js | 170 +++++++++++++++++ model/jira.js | 37 ++++ model/timer.js | 9 + package-lock.json | 332 ++++++++++++++++++++++++++++++++++ package.json | 16 ++ 24 files changed, 1425 insertions(+) create mode 100644 .gitignore create mode 100644 bot.js create mode 100644 config.json.sample create mode 100644 emoji-characters.json create mode 100644 event/bot/message.js create mode 100644 event/bot/ready.js create mode 100644 model/command-category.js create mode 100644 model/command-permission.js create mode 100644 model/command.js create mode 100644 model/command/avatar.js create mode 100644 model/command/changelog.js create mode 100644 model/command/clean.js create mode 100644 model/command/custom-front.js create mode 100644 model/command/eval.js create mode 100644 model/command/help.js create mode 100644 model/command/kill.js create mode 100644 model/command/reload.js create mode 100644 model/command/set-avatar.js create mode 100644 model/globals.js create mode 100644 model/guild.js create mode 100644 model/jira.js create mode 100644 model/timer.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3bd777 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +*.iml +node_modules +deploy.sh +config.*json* +!config.json.sample \ No newline at end of file diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..e0ac5de --- /dev/null +++ b/bot.js @@ -0,0 +1,105 @@ +const Logger = require('@lilywonhalf/pretty-logger'); + +const mainProcess = () => { + const ChildProcess = require('child_process'); + + process.on('uncaughtException', Logger.exception); + + Logger.info('Spawning bot subprocess...'); + const args = [process.argv[1], 'bot']; + let botProcess = ChildProcess.spawn(process.argv[0], args); + + const stdLog = (callback) => { + return (data) => { + const wantToDie = data.toString().toLowerCase().indexOf('killbotpls') > -1; + const reboot = data.toString().toLowerCase().indexOf('reboot') > -1 + || data.toString().toLowerCase().indexOf('econnreset') > -1 + || data.toString().toLowerCase().indexOf('etimedout') > -1; + + data = data.toString().replace(/\n$/, '').split('\n'); + data.map(datum => callback('|-- ' + datum)); + + if (wantToDie) { + Logger.info('Asked to kill'); + botProcess.kill('SIGHUP'); + process.exit(0); + } + + if (reboot) { + botProcess.kill(); + } + }; + }; + + const bindProcess = (subprocess) => { + subprocess.stdout.on('data', stdLog(console.log)); + subprocess.stderr.on('data', stdLog(console.error)); + subprocess.on('close', (code) => { + Logger.error(`Bot subprocess exited with code ${code}`); + + if (code !== 0) { + botProcess = ChildProcess.spawn( + process.argv[0], + args.concat(['--reboot']) + ); + bindProcess(botProcess); + } + }); + }; + + bindProcess(botProcess); + Logger.info('Bot subprocess spawned'); +}; + +const botProcess = () => { + const { Client } = require('discord.js'); + + global.bot = new Client({ fetchAllMembers: true }); + + const Config = require('./config.json'); + const Command = require('./model/command'); + const fs = require('fs'); + + require('./model/globals'); + require('./model/timer'); + + const crashRecover = (exception) => { + Logger.exception(exception); + Logger.notice('Need reboot'); + }; + + process.on('uncaughtException', crashRecover); + bot.on('error', crashRecover); + + Command.init(); + + bot.on('ready', () => { + fs.readdirSync('./event/bot/') + .filter(filename => filename.endsWith('.js')) + .map(filename => filename.substr(0, filename.length - 3)) + .forEach(filename => { + const event = filename.replace(/([_-][a-z])/gu, character => `${character.substr(1).toUpperCase()}`); + + if (filename !== 'ready') { + bot.on(event, require(`./event/bot/${filename}`)); + } else { + require(`./event/bot/${filename}`)(); + } + }); + }); + + Logger.info('--------'); + + Logger.info('Logging in...'); + bot.login(Config.token); +}; + +switch (process.argv[2]) { + case 'bot': + botProcess(); + break; + + default: + mainProcess(); + break; +} diff --git a/config.json.sample b/config.json.sample new file mode 100644 index 0000000..35f4d8c --- /dev/null +++ b/config.json.sample @@ -0,0 +1,12 @@ +{ + "token": "", + "prefix": ".", + "guild": "", + "mom": "", + "roles": { + "mod": "" + }, + "channels": { + "staff": "" + } +} \ No newline at end of file diff --git a/emoji-characters.json b/emoji-characters.json new file mode 100644 index 0000000..0b85049 --- /dev/null +++ b/emoji-characters.json @@ -0,0 +1,13 @@ +{ + "a": "đŸ‡Ļ", "b": "🇧", "c": "🇨", "d": "🇩", + "e": "đŸ‡Ē", "f": "đŸ‡Ģ", "g": "đŸ‡Ŧ", "h": "🇭", + "i": "🇮", "j": "đŸ‡¯", "k": "🇰", "l": "🇱", + "m": "🇲", "n": "đŸ‡ŗ", "o": "🇴", "p": "đŸ‡ĩ", + "q": "đŸ‡ļ", "r": "🇷", "s": "🇸", "t": "🇹", + "u": "đŸ‡ē", "v": "đŸ‡ģ", "w": "đŸ‡ŧ", "x": "đŸ‡Ŋ", + "y": "🇾", "z": "đŸ‡ŋ", "0": "0âƒŖ", "1": "1âƒŖ", + "2": "2âƒŖ", "3": "3âƒŖ", "4": "4âƒŖ", "5": "5âƒŖ", + "6": "6âƒŖ", "7": "7âƒŖ", "8": "8âƒŖ", "9": "9âƒŖ", + "10": "🔟", "#": "#âƒŖ", "*": "*âƒŖ", + "!": "❗", "?": "❓" +} \ No newline at end of file diff --git a/event/bot/message.js b/event/bot/message.js new file mode 100644 index 0000000..1a0d362 --- /dev/null +++ b/event/bot/message.js @@ -0,0 +1,12 @@ +const Command = require('../../model/command'); + +/** + * @param {Message} message + */ +module.exports = async (message) => { + const user = message.author; + + if (!user.bot) { + await Command.parseMessage(message); + } +}; diff --git a/event/bot/ready.js b/event/bot/ready.js new file mode 100644 index 0000000..9982b60 --- /dev/null +++ b/event/bot/ready.js @@ -0,0 +1,19 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const Config = require('../../config.json'); +const Guild = require('../../model/guild'); + +module.exports = async () => { + Logger.info('Logged in as ' + bot.user.username + '#' + bot.user.discriminator); + + Logger.info('--------'); + + Logger.info('Syncing guilds...'); + await Guild.init(); + Logger.info('Guilds synced. Serving in ' + Guild.discordGuild.name); + + Logger.info('--------'); + + if (process.argv[3] === '--reboot') { + bot.users.cache.get(Config.mom).send(`I'm back :) !`); + } +}; diff --git a/model/command-category.js b/model/command-category.js new file mode 100644 index 0000000..f7091f5 --- /dev/null +++ b/model/command-category.js @@ -0,0 +1,11 @@ +const CommandCategory = { + MODERATION: 'moderation', + ADMINISTRATION: 'administration', + BOT_MANAGEMENT: 'bot_management', + FUN: 'fun', + INFO: 'info', + ROLE: 'role', + RESOURCE: 'resource' +}; + +module.exports = CommandCategory; diff --git a/model/command-permission.js b/model/command-permission.js new file mode 100644 index 0000000..6a3274a --- /dev/null +++ b/model/command-permission.js @@ -0,0 +1,34 @@ +const Config = require('../config.json'); +const Guild = require('./guild'); + +const CommandPermission = { + /** + * @param {Message} message + * @returns {Promise.} + */ + isMommy: async (message) => { + const member = await Guild.getMemberFromMessage(message); + + return member.id === Config.mom; + }, + + /** + * @param {Message} message + * @returns {Promise.} + */ + isMemberMod: async (message) => { + const member = await Guild.getMemberFromMessage(message); + + return member.id === Config.mom || await Guild.isMemberMod(member); + }, + + /** + * @param {Message} message + * @returns {Promise.} + */ + yes: async (message) => { + return true; + } +}; + +module.exports = CommandPermission; diff --git a/model/command.js b/model/command.js new file mode 100644 index 0000000..c61c311 --- /dev/null +++ b/model/command.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const Discord = require('discord.js'); +const Config = require('../config.json'); +const Guild = require('./guild'); + +const cachelessRequire = (path) => { + if (typeof path === 'string') { + delete require.cache[require.resolve(path)]; + } + + return typeof path === 'string' ? require(path) : null; +}; + +const Command = { + commandList: new Discord.Collection(), + commandAliases: {}, + + init: () => { + Command.commandList = new Discord.Collection(); + Command.commandAliases = {}; + + fs.readdirSync('model/command/').forEach(file => { + if (file.substr(file.lastIndexOf('.')).toLowerCase() === '.js') { + const commandPath = `./command/${file}`; + const commandInstance = cachelessRequire(commandPath); + + if (commandInstance !== null) { + const commandName = file.substr(0, file.lastIndexOf('.')).toLowerCase(); + + Command.commandList.set(commandName, commandPath); + + if (commandInstance.aliases !== undefined && commandInstance.aliases !== null) { + commandInstance.aliases.forEach(alias => { + Command.commandAliases[alias.toLowerCase()] = commandName; + }); + } + } + } + }); + }, + + /** + * @param {Message} message + * @returns {boolean} + */ + parseMessage: async (message) => { + let isCommand = false; + + if (message.content.toLowerCase().substr(0, Config.prefix.length) === Config.prefix) { + let content = message.content.substr(Config.prefix.length).trim().split(' '); + const calledCommand = content.shift().toLowerCase(); + + if (await Command.isValid(calledCommand, message)) { + const member = await Guild.getMemberFromMessage(message); + + if (member === null) { + message.reply('sorry, you do not seem to be on the server.'); + } else { + let commandName = calledCommand; + isCommand = true; + + if (Command.commandAliases.hasOwnProperty(calledCommand)) { + commandName = Command.commandAliases[calledCommand]; + } + + const commandInstance = cachelessRequire(Command.commandList.get(commandName)); + + if (commandInstance !== null) { + commandInstance.process(message, content, Command); + } else { + Command.commandList.delete(commandName); + } + } + } + } + + return isCommand; + }, + + /** + * @param {string} command + * @param {Message} message + * @return {Promise.} + */ + isValid: async (command, message) => { + let canonicalCommand = command.toLowerCase(); + let valid = Command.commandList.has(canonicalCommand); + + if (!valid && Command.commandAliases.hasOwnProperty(canonicalCommand)) { + canonicalCommand = Command.commandAliases[command]; + valid = Command.commandList.has(canonicalCommand); + } + + const commandInstance = cachelessRequire(Command.commandList.get(canonicalCommand)); + + if (commandInstance === null) { + Command.commandList.delete(canonicalCommand); + } + + valid = valid + && commandInstance !== null + && await commandInstance.isAllowedForContext(message); + + return valid; + } +}; + +module.exports = Command; diff --git a/model/command/avatar.js b/model/command/avatar.js new file mode 100644 index 0000000..52fb064 --- /dev/null +++ b/model/command/avatar.js @@ -0,0 +1,56 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const Discord = require('discord.js'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); +const Guild = require('../guild'); + +class Avatar +{ + static instance = null; + + constructor() { + if (Avatar.instance !== null) { + return Avatar.instance; + } + + this.aliases = ['av']; + this.category = CommandCategory.FUN; + this.isAllowedForContext = CommandPermission.yes; + this.description = 'Displays the avatar of the specified member.'; + } + + /** + * @param {Message} message + * @param {Array} args + */ + async process(message, args) { + let user = null; + + if (args.length > 0) { + const result = Guild.findDesignatedMemberInMessage(message); + + if (result.foundMembers.length > 0) { + if (result.foundMembers[0].user !== undefined) { + user = result.foundMembers[0].user; + } else { + user = result.foundMembers[0]; + } + } + } else { + user = message.author; + } + + if (user !== null) { + const url = user.displayAvatarURL({ dynamic: true }); + + message.channel.send(new Discord.MessageAttachment( + url + '?size=2048', + user.id + url.substr(url.lastIndexOf('.')) + )).catch(error => Logger.warning(error.toString())); + } else { + message.reply('I... Have no idea who that could be, sorry.'); + } + } +} + +module.exports = new Avatar(); diff --git a/model/command/changelog.js b/model/command/changelog.js new file mode 100644 index 0000000..825db46 --- /dev/null +++ b/model/command/changelog.js @@ -0,0 +1,81 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const Config = require('../../config.json'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); +const { search } = require('../jira'); + +const MAX_CHARACTERS = 1950; + +class Changelog +{ + static instance = null; + + constructor() { + if (Changelog.instance !== null) { + return Changelog.instance; + } + + this.aliases = ['change-log', 'cl']; + this.category = CommandCategory.MODERATION; + this.isAllowedForContext = CommandPermission.isMemberMod; + this.description = 'Builds the changelog for a given version'; + } + + /** + * @param {Message} message + * @param {Array} args + */ + async process(message, args) { + const errorHandler = async (error) => { + if (error) { + Logger.exception(error); + } + + await message.reactions.removeAll(); + await message.react('❌'); + } + + await message.react('âŗ').catch(() => {}); + const issues = args.length > 0 && args[0] ? await search(args[0]).catch(errorHandler) : await search().catch(errorHandler); + + const taskType = Config.jira.issueTypes.task; + const bugType = Config.jira.issueTypes.bug; + const features = issues.filter(issue => parseInt(issue.fields.issuetype.id) === taskType); + const bugs = issues.filter(issue => parseInt(issue.fields.issuetype.id) === bugType); + + if (features.length < 1 && bugs.length < 1) { + await message.channel.send('No issues found.'); + await message.reactions.removeAll().catch(() => {}); + + return; + } + + const output = `${features.map( + issue => `* Feature: ${issue.key} - ${issue.fields.summary}` + ).join('\n')}\n\n${bugs.map( + issue => `* Fixed: ${issue.key} - ${issue.fields.summary}` + ).join('\n')}`.trim(); + const messages = []; + let currentMessage = '```'; + + for (let line of output.split('\n')) { + if (currentMessage.length + line.length >= MAX_CHARACTERS) { + messages.push(`${currentMessage}\`\`\``); + currentMessage = '```'; + } + + currentMessage = `${currentMessage}\n${line}`; + } + + messages.push(`${currentMessage}\`\`\``); + + for (let messageToSend of messages) { + await message.channel.send(messageToSend).catch(() => {}); + } + + await message.reactions.removeAll().catch(() => {}); + await message.react('✔').catch(() => {}); + } +} + +module.exports = new Changelog(); diff --git a/model/command/clean.js b/model/command/clean.js new file mode 100644 index 0000000..956802b --- /dev/null +++ b/model/command/clean.js @@ -0,0 +1,34 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const Config = require('../../config.json'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); + +class Clean +{ + static instance = null; + + constructor() { + if (Clean.instance !== null) { + return Clean.instance; + } + + this.aliases = ['clear', 'purge']; + this.category = CommandCategory.MODERATION; + this.isAllowedForContext = CommandPermission.isMemberMod; + this.description = 'Kills the bot process'; + } + + /** + * @param {Message} message + * @param {Array} args + */ + async process(message, args) { + if (args.length > 0 && parseInt(args[0]) > 0) { + await message.channel.bulkDelete(Math.min(parseInt(args[0]) + 1, 100)); + } else { + message.reply(`You have to tell me how many messages I should clean. \`${Config.prefix}clean 10\` for example.`); + } + } +} + +module.exports = new Clean(); diff --git a/model/command/custom-front.js b/model/command/custom-front.js new file mode 100644 index 0000000..b0d5b3b --- /dev/null +++ b/model/command/custom-front.js @@ -0,0 +1,42 @@ +const { MessageEmbed } = require('discord.js'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); + +class CustomFront +{ + static instance = null; + + constructor() { + if (CustomFront.instance !== null) { + return CustomFront.instance; + } + + this.aliases = ['custom-fronts', 'customfronts', 'customfront', 'cf']; + this.category = CommandCategory.FUN; + this.isAllowedForContext = CommandPermission.yes; + this.description = 'Explains what "custom front" is.'; + } + + /** + * @param {Message} message + * @param {Array} args + */ + async process(message, args) { + const embed = new MessageEmbed(); + + embed.setColor(APP_MAIN_COLOUR); + embed.setAuthor('Custom fronts', bot.user.displayAvatarURL({ dynamic: true })); + embed.setDescription( + 'Custom fronts is a kind of status for fronts, like "blurred", "unknown member", "dissociated", etc... \n' + + '\n' + + 'You don\'t want those to show up as real members in your system list but you still want to be able to ' + + 'set front as one of them — that\'s where custom fronts kick in.\n' + + '\n' + + 'They\'re highly customizable (as per popular request) so you can name them anything you want.' + ); + + return message.channel.send(embed); + } +} + +module.exports = new CustomFront(); diff --git a/model/command/eval.js b/model/command/eval.js new file mode 100644 index 0000000..d99cf72 --- /dev/null +++ b/model/command/eval.js @@ -0,0 +1,55 @@ +const Discord = require('discord.js'); +const Logger = require('@lilywonhalf/pretty-logger'); +const Config = require('../../config.json'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); + +// Including every model here so that it's ready to be used by the command +const Guild = require('../guild'); + +const JAVASCRIPT_LOGO_URL = 'https://i.discord.fr/IEV8.png'; + +class Eval +{ + static instance = null; + + constructor() { + if (Eval.instance !== null) { + return Eval.instance; + } + + this.aliases = []; + this.category = CommandCategory.BOT_MANAGEMENT; + this.isAllowedForContext = CommandPermission.isMommy; + } + + /** + * @param {Message} message + */ + async process(message) { + const code = message.content + .substr(Config.prefix.length + 'eval'.length) + .trim() + .replace(/(`{3})js\n(.+)\n\1/iu, '$2') + .trim(); + + await message.react('✔'); + Logger.notice('Eval: ' + code); + let output = null; + + try { + output = eval(`${code}`); // Spoopy! 🎃 đŸĻ‡ đŸ‘ģ ☠ 🕷 + } catch (exception) { + output = `**${exception.name}: ${exception.message}**\n${exception.stack}`; + } + + const embed = new Discord.MessageEmbed() + .setAuthor('Eval', JAVASCRIPT_LOGO_URL) + .setColor(0x00FF00) + .setDescription(output); + + message.channel.send(embed).catch(error => Logger.warning(error.toString())); + } +} + +module.exports = new Eval(); diff --git a/model/command/help.js b/model/command/help.js new file mode 100644 index 0000000..d7b596f --- /dev/null +++ b/model/command/help.js @@ -0,0 +1,179 @@ +const Discord = require('discord.js'); +const Logger = require('@lilywonhalf/pretty-logger'); +const Config = require('../../config.json'); +const EmojiCharacters = require('../../emoji-characters.json'); +const Guild = require('../guild'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); + +const cachelessRequire = (path) => { + if (typeof path === 'string') { + delete require.cache[require.resolve(path)]; + } + + return typeof path === 'string' ? require(path) : null; +}; + +class HelpDialog +{ + /** + * @param {Message} message + */ + constructor(message) { + this.categoriesMapping = new Discord.Collection(); + this.categoriesEmbed = new Discord.MessageEmbed(); + this.originalMessage = message; + this.categoryCommandMapping = new Discord.Collection(); + this.postedMessage = null; + this.usedEmojis = []; + this.stopAddingReactions = false; + } + + /** + * @param {Command} Command + */ + async init(Command) { + const callableCommands = new Discord.Collection(); + const commandList = Command.commandList.keyArray(); + + for (let i = 0; i < commandList.length; i++) { + const commandName = commandList[i]; + const command = cachelessRequire(`../${Command.commandList.get(commandName)}`); + const isAllowed = await command.isAllowedForContext(this.originalMessage); + + if (isAllowed) { + callableCommands.set(commandName, command); + } + } + + callableCommands.forEach((command, commandName) => { + if (!this.categoryCommandMapping.has(command.category)) { + this.categoryCommandMapping.set(command.category, new Discord.Collection()); + } + + this.categoryCommandMapping.get(command.category).set(commandName, command); + }); + + let i = 1; + + const categories = callableCommands.reduce((accumulator, command) => { + if (!this.categoriesMapping.array().includes(command.category)) { + let commandCategory = command.category.replace('_', ' '); + commandCategory = `${commandCategory.slice(0, 1).toUpperCase()}${commandCategory.slice(1)}`; + + this.categoriesMapping.set(EmojiCharacters[i], command.category); + this.usedEmojis.push(EmojiCharacters[i]); + accumulator += `${EmojiCharacters[i]} ${commandCategory}\n`; + i++; + } + + return accumulator; + }, ''); + + this.categoriesEmbed.setTitle('Help command'); + this.categoriesEmbed.setDescription(categories); + + return this.listCategories(); + } + + /** + * @param {Collection} [collection] + */ + async listCategories(collection) { + this.stopAddingReactions = true; + + if (collection !== undefined && collection.size < 1) { + await this.postedMessage.reactions.removeAll(); + return; + } + + const member = await Guild.getMemberFromMessage(this.originalMessage); + const filter = (reaction, user) => { + const emoji = reaction.emoji.name; + + return this.usedEmojis.includes(emoji) && user.id === member.user.id; + }; + + if (this.postedMessage === null) { + this.postedMessage = await this.originalMessage.channel.send(this.categoriesEmbed).catch(error => Logger.warning(error.toString())); + } else { + await this.postedMessage.reactions.removeAll(); + await this.postedMessage.edit('', this.categoriesEmbed); + } + + // 5 minutes + this.postedMessage.awaitReactions(filter, { time: 300000, max: 1 }).then(this.listCommands.bind(this)).catch(Logger.exception); + this.stopAddingReactions = false; + + for (let i = 0; i < this.usedEmojis.length && !this.stopAddingReactions; i++) { + await this.postedMessage.react(this.usedEmojis[i]); + } + } + + /** + * @param {Collection} collection + */ + async listCommands(collection) { + this.stopAddingReactions = true; + await this.postedMessage.reactions.removeAll(); + + if (collection.size < 1) { + this.postedMessage.edit('Timed out! You may send the command again.'); + return; + } + + const member = await Guild.getMemberFromMessage(this.originalMessage); + const filter = (reaction, user) => { + const emoji = reaction.emoji.name; + + return emoji === 'â†Šī¸' && user.id === member.user.id; + }; + + const category = this.categoriesMapping.get(collection.first().emoji.name); + const commandsEmbed = new Discord.MessageEmbed(); + const commands = this.categoryCommandMapping.get(category).reduce((accumulator, command, commandName) => { + accumulator += `**${Config.prefix}${commandName}** ${command.description}\n\n`; + + return accumulator; + }, ''); + + commandsEmbed.setTitle('Help command'); + commandsEmbed.setDescription(commands); + + this.postedMessage.edit('', commandsEmbed); + + // 5 minutes + this.postedMessage.awaitReactions(filter, { time: 300000, max: 1 }).then(this.listCategories.bind(this)).catch(Logger.exception); + + await this.postedMessage.react('â†Šī¸'); + } +} + +class Help +{ + static instance = null; + + constructor() { + if (Help.instance !== null) { + return Help.instance; + } + + this.aliases = []; + this.category = CommandCategory.INFO; + this.isAllowedForContext = CommandPermission.yes; + this.description = 'Provides the list of commands'; + } + + /** + * @param {Message} message + * @param {Array} args + * @param {Command} Command + */ + async process(message, args, Command) { + const dialog = new HelpDialog(message); + + return dialog.init(Command); + } +} + +module.exports = new Help(); diff --git a/model/command/kill.js b/model/command/kill.js new file mode 100644 index 0000000..45d9662 --- /dev/null +++ b/model/command/kill.js @@ -0,0 +1,30 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const Config = require('../../config.json'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); + +class Kill +{ + static instance = null; + + constructor() { + if (Kill.instance !== null) { + return Kill.instance; + } + + this.aliases = []; + this.category = CommandCategory.BOT_MANAGEMENT; + this.isAllowedForContext = CommandPermission.isMemberMod; + this.description = 'Kills the bot process'; + } + + /** + * @param {Message} message + */ + async process(message) { + await message.react('✔'); + Logger.notice('killbotpls'); + } +} + +module.exports = new Kill(); diff --git a/model/command/reload.js b/model/command/reload.js new file mode 100644 index 0000000..93bc5d1 --- /dev/null +++ b/model/command/reload.js @@ -0,0 +1,29 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); + +class Reload +{ + static instance = null; + + constructor() { + if (Reload.instance !== null) { + return Reload.instance; + } + + this.aliases = ['reboot']; + this.category = CommandCategory.BOT_MANAGEMENT; + this.isAllowedForContext = CommandPermission.isMemberMod; + this.description = 'Reboots the bot'; + } + + /** + * @param {Message} message + */ + async process(message) { + await message.reply(`OK, I'm rebooting now.`); + Logger.notice('Reboot asked'); + } +} + +module.exports = new Reload(); diff --git a/model/command/set-avatar.js b/model/command/set-avatar.js new file mode 100644 index 0000000..3d4959b --- /dev/null +++ b/model/command/set-avatar.js @@ -0,0 +1,34 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const CommandCategory = require('../command-category'); +const CommandPermission = require('../command-permission'); + +class SetAvatar +{ + static instance = null; + + constructor() { + if (SetAvatar.instance !== null) { + return SetAvatar.instance; + } + + this.aliases = ['setavatar']; + this.category = CommandCategory.BOT_MANAGEMENT; + this.isAllowedForContext = CommandPermission.isMemberMod; + this.description = 'Set the bot avatar'; + } + + /** + * @param {Message} message + * @param {Array} args + */ + async process(message, args) { + bot.user.setAvatar(args.join(' ')).then(() => { + message.reply('my avatar has been changed!') + }).catch((error) => { + message.reply('there has been an error changing my avatar. Check the logs for more details.'); + Logger.exception(error); + }); + } +} + +module.exports = new SetAvatar(); diff --git a/model/globals.js b/model/globals.js new file mode 100644 index 0000000..55ec210 --- /dev/null +++ b/model/globals.js @@ -0,0 +1 @@ +global.APP_MAIN_COLOUR = 'A95B44'; \ No newline at end of file diff --git a/model/guild.js b/model/guild.js new file mode 100644 index 0000000..e051939 --- /dev/null +++ b/model/guild.js @@ -0,0 +1,170 @@ +const Logger = require('@lilywonhalf/pretty-logger'); +const Config = require('../config.json'); +const Discord = require('discord.js'); + +const cachelessRequire = (path) => { + if (typeof path === 'string') { + delete require.cache[require.resolve(path)]; + } + + return typeof path === 'string' ? require(path) : null; +}; + +const Guild = { + /** {Guild} */ + discordGuild: null, + + /** {TextChannel} */ + modsReviewChannel: null, + + /** {TextChannel} */ + speakerReviewChannel: null, + + /** {TextChannel} */ + officeChannel: null, + + /** {Collection} */ + messagesCache: new Discord.Collection(), + + init: async () => { + Guild.discordGuild = bot.guilds.cache.find(guild => guild.id === Config.guild); + Guild.modsReviewChannel = Guild.discordGuild.channels.cache.find(channel => channel.id === Config.channels.modsReview); + Guild.speakerReviewChannel = Guild.discordGuild.channels.cache.find(channel => channel.id === Config.channels.speakerReview); + Guild.officeChannel = Guild.discordGuild.channels.cache.find(channel => channel.id === Config.channels.office); + }, + + /** + * @param message + * @returns {Promise.} + */ + getMemberFromMessage: async (message) => { + return await Guild.discordGuild.members.fetch(message.author).catch(exception => { + Logger.error(exception.toString()); + + return null; + }); + }, + + /** + * @param {GuildMember} member + */ + isMemberMod: (member) => { + return member !== undefined && member !== null && member.roles.cache.has(Config.roles.mod); + }, + + /** + * @param {string} roleName + * @returns {Role|null} + */ + getRoleByName: (roleName) => { + return roleName === undefined || roleName === null ? null : Guild.discordGuild.roles.cache.find( + role => role.name.toLowerCase() === roleName.toLowerCase() + ); + }, + + /** + * @param {GuildMember} member + * @param {Snowflake} snowflake - The Role snowflake. + * @returns {boolean} + */ + memberHasRole: (member, snowflake) => { + return member !== undefined && member !== null && member.roles.cache.some(role => role.id === snowflake); + }, + + /** + * @param {Message} message + * @returns {Discord.MessageEmbed} + */ + messageToEmbed: async (message) => { + const member = await Guild.getMemberFromMessage(message); + const suffix = member !== null && member.nickname !== null && member.nickname !== undefined ? ` aka ${member.nickname}` : ''; + const embeds = message.embeds.filter(embed => embed.author.name && embed.author.iconURL); + + let authorName = `${message.author.username}#${message.author.discriminator}${suffix}`; + let authorImage = message.author.displayAvatarURL({ dynamic: true }); + let description = message.content; + let timestamp = message.createdTimestamp; + let image = null; + + if (message.attachments.size > 0) { + image = message.attachments.first().url; + } + + if (description.length < 1 && embeds.length > 0) { + const embed = embeds[0]; + description = embed.description ? embed.description.trim() : ''; + + if (message.author.bot) { + if (embed.author) { + authorName = embed.author.name; + authorImage = embed.author.iconURL; + } + + if (embed.timestamp) { + timestamp = embed.timestamp; + } + + if (embed.image) { + image = embed.image.url; + } + } + } + + return new Discord.MessageEmbed() + .setAuthor(authorName, authorImage) + .setColor(0x00FF00) + .setDescription(description) + .setTimestamp(timestamp) + .setImage(image); + }, + + /** + * @param {Message} message + * @returns {{certain: boolean, foundMembers: Array}} + */ + findDesignatedMemberInMessage: (message) => { + let foundMembers = []; + let certain = true; + const memberList = bot.users.cache.concat(Guild.discordGuild.members.cache); + + if (message.mentions.members !== null && message.mentions.members.size > 0) { + foundMembers = message.mentions.members.array(); + } else if (message.content.match(/[0-9]{18}/u) !== null) { + const ids = message.content.match(/[0-9]{18}/gu); + + ids.map(id => { + if (memberList.has(id)) { + foundMembers.push(memberList.get(id)); + } + }); + } else { + certain = false; + memberList.forEach(member => { + const user = member.user === undefined ? member : member.user; + + const hasNickname = member.nickname !== undefined && member.nickname !== null; + const nickname = hasNickname ? `${member.nickname.toLowerCase()}#${user.discriminator}` : ''; + const username = `${user.username.toLowerCase()}#${user.discriminator}`; + const content = message.cleanContent.toLowerCase().split(' ').splice(1).join(' '); + + if (content.length > 0) { + const contentInNickname = hasNickname ? nickname.indexOf(content) > -1 : false; + const contentInUsername = username.indexOf(content) > -1; + const nicknameInContent = hasNickname ? content.indexOf(nickname) > -1 : false; + const usernameInContent = content.indexOf(username) > -1; + + if (contentInNickname || contentInUsername || nicknameInContent || usernameInContent) { + foundMembers.push(member); + } + } + }); + } + + return { + certain, + foundMembers + }; + } +}; + +module.exports = Guild; \ No newline at end of file diff --git a/model/jira.js b/model/jira.js new file mode 100644 index 0000000..90bf8a6 --- /dev/null +++ b/model/jira.js @@ -0,0 +1,37 @@ +const axios = require('axios'); +const Config = require('../config.json'); + +const ENDPOINTS = { + search: '/search' +}; + +class Jira +{ + async search(version) { + const params = { + 'jql': 'project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20updated%20%3E%3D%20-24h%20ORDER%20BY%20created%20DESC', + 'maxResults': 100, + 'startAt': 0 + }; + + if (version) { + params.jql = `project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20%22Fixed%20In%20Version%5BNumber%5D%22%20%3D%20%22${version}%22%20ORDER%20BY%20created%20DESC`; + } + + const issues = []; + const httpParams = Object.keys(params).map(key => `${key}=${params[key]}`).join('&'); + const url = `${Config.jira.baseUrl}${ENDPOINTS.search}?${httpParams}`; + let response = null; + + do { + response = await axios(url); + + issues.push(...response.data.issues); + params.startAt += 100; + } while (response.data.issues.length > 99 && response.data.total > 100); + + return issues; + } +} + +module.exports = new Jira(); \ No newline at end of file diff --git a/model/timer.js b/model/timer.js new file mode 100644 index 0000000..fce2886 --- /dev/null +++ b/model/timer.js @@ -0,0 +1,9 @@ +/** + * @param {int} milliseconds + * @returns {Promise} + */ +global.sleep = async (milliseconds) => { + return new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..12647af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,332 @@ +{ + "name": "simplypluralbot", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "simplypluralbot", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@lilywonhalf/pretty-logger": "^1.1.3", + "axios": "^0.21.1", + "discord.js": "^12.2.0" + } + }, + "node_modules/@discordjs/collection": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", + "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" + }, + "node_modules/@discordjs/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@lilywonhalf/pretty-logger": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lilywonhalf/pretty-logger/-/pretty-logger-1.1.3.tgz", + "integrity": "sha512-DcEGjCrQV2j6Hl29G4zD5M/84hzrt1xw7uPOZtZLWFzrCB5OWS/64RWl/y5/om0walTIYf7Nv00hCzfRS8gw4A==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/discord.js": { + "version": "12.5.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", + "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", + "dependencies": { + "@discordjs/collection": "^0.1.6", + "@discordjs/form-data": "^3.0.1", + "abort-controller": "^3.0.0", + "node-fetch": "^2.6.1", + "prism-media": "^1.2.9", + "setimmediate": "^1.0.5", + "tweetnacl": "^1.0.3", + "ws": "^7.4.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.0.tgz", + "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/mime-db": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "dependencies": { + "mime-db": "1.47.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/prism-media": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz", + "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==", + "peerDependencies": { + "@discordjs/opus": "^0.5.0", + "ffmpeg-static": "^4.2.7 || ^3.0.0 || ^2.4.0", + "node-opus": "^0.3.3", + "opusscript": "^0.0.8" + }, + "peerDependenciesMeta": { + "@discordjs/opus": { + "optional": true + }, + "ffmpeg-static": { + "optional": true + }, + "node-opus": { + "optional": true + }, + "opusscript": { + "optional": true + } + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "node_modules/ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "@discordjs/collection": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", + "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" + }, + "@discordjs/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "@lilywonhalf/pretty-logger": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lilywonhalf/pretty-logger/-/pretty-logger-1.1.3.tgz", + "integrity": "sha512-DcEGjCrQV2j6Hl29G4zD5M/84hzrt1xw7uPOZtZLWFzrCB5OWS/64RWl/y5/om0walTIYf7Nv00hCzfRS8gw4A==" + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "discord.js": { + "version": "12.5.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", + "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", + "requires": { + "@discordjs/collection": "^0.1.6", + "@discordjs/form-data": "^3.0.1", + "abort-controller": "^3.0.0", + "node-fetch": "^2.6.1", + "prism-media": "^1.2.9", + "setimmediate": "^1.0.5", + "tweetnacl": "^1.0.3", + "ws": "^7.4.4" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "follow-redirects": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.0.tgz", + "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==" + }, + "mime-db": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" + }, + "mime-types": { + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "requires": { + "mime-db": "1.47.0" + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "prism-media": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz", + "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==", + "requires": {} + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "requires": {} + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..530f353 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "simplypluralbot", + "version": "1.0.0", + "description": "Simply Plural bot for Discord", + "main": "bot.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Lily Wonhalf ", + "license": "ISC", + "dependencies": { + "@lilywonhalf/pretty-logger": "^1.1.3", + "axios": "^0.21.1", + "discord.js": "^12.2.0" + } +}