diff options
| author | 8cy <[email protected]> | 2020-07-19 02:10:00 -0700 |
|---|---|---|
| committer | 8cy <[email protected]> | 2020-07-19 02:10:00 -0700 |
| commit | 4014ac46b4e5e515f133ca2c04859824cd43738c (patch) | |
| tree | 5842824afd615f70e8a4959704f9cf3492fa54f2 | |
| download | water-waifu-4014ac46b4e5e515f133ca2c04859824cd43738c.tar.xz water-waifu-4014ac46b4e5e515f133ca2c04859824cd43738c.zip | |
:star:
| -rw-r--r-- | .gitattributes | 2 | ||||
| -rw-r--r-- | .gitignore | 66 | ||||
| -rw-r--r-- | .travis.yml | 7 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | Procfile | 1 | ||||
| -rw-r--r-- | README.md | 45 | ||||
| -rw-r--r-- | package.json | 23 | ||||
| -rw-r--r-- | role.js | 187 | ||||
| -rw-r--r-- | tests.js | 6 | ||||
| -rw-r--r-- | yarn.lock | 46 |
10 files changed, 404 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..098ecb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# Other +data/ +package-lock.json +config.js diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f0ba990 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - "8" + - "10" + +script: + - npm run test
\ No newline at end of file @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Sin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..cf68c4e --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +worker: node .
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..275c25f --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +<div align="center"> +<h1>Water Waifu</h1> +<br> +<strong><i>A bot that automatically assigns roles based on message reactions.</i></strong> +<br> +<br> +<hr> + +<a href="https://travis-ci.com/8cy/water-waifu"> + <img src="https://img.shields.io/travis/com/8cy/water-waifu.svg?style=for-the-badge" alt="Build"> +</a> + +<a href="https://github.com/8cy/water-waifu"> + <img src="https://img.shields.io/github/languages/top/8cy/water-waifu.svg?colorB=f0db4f&style=for-the-badge" alt="Languages"> +</a> + +<br> + +<a href="https://github.com/8cy/water-waifu"> + <img src="https://img.shields.io/github/package-json/v/8cy/water-waifu.svg?colorB=Orange&style=for-the-badge" alt="Version"> +</a> + +<a href="https://github.com/8cy/water-waifu/issues"> + <img src="https://img.shields.io/github/issues/8cy/water-waifu.svg?style=for-the-badge&colorB=37f149" alt="Issues"> +</a> + +<a href="https://github.com/8cy/water-waifu/pulls"> + <img src="https://img.shields.io/github/issues-pr/8cy/water-waifu.svg?style=for-the-badge&colorB=37f149" alt="Pull Request"> +</a> + +<br> +<br> +</div> +<hr> +<br> + +## Features + +- Customizable messages, reactions, command, and roles + +- Auto-removes role when user removes a reaction + +- Option to use an embed to have all options in one message + +- Error handling to let you know if something is wrong diff --git a/package.json b/package.json new file mode 100644 index 0000000..96bc80e --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "water-waifu", + "version": "1.0.0", + "description": "A bot that automatically assigns roles based on message reactions.", + "main": "role.js", + "scripts": { + "start": "node .", + "test": "node tests.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/8cy/water-waifu.git" + }, + "author": "Sin", + "license": "MIT", + "bugs": { + "url": "https://github.com/8cy/water-waifu/issues" + }, + "homepage": "https://github.com/8cy/water-waifu#readme", + "dependencies": { + "discord.js": "^11.5.1" + } +} @@ -0,0 +1,187 @@ +// Import constructors, configuration and login the client +const { Client, RichEmbed, Emoji, MessageReaction } = require('discord.js'); +const CONFIG = require('./config'); + +const client = new Client({ disableEveryone: true }); +if (CONFIG.botToken === '') + throw new Error("The 'botToken' property is not set in the config.js file. Please do this!"); + +client.login(CONFIG.botToken); + +// If there isn't a reaction for every role, alert the user +if (CONFIG.roles.length !== CONFIG.reactions.length) + throw "Roles list and reactions list are not the same length! Please double check this in the config.js file"; + +// Function to generate the role messages, based on your settings +function generateMessages() { + return CONFIG.roles.map((r, e) => { + return { + role: r, + message: `React below to get the **"${r}"** role!`, // DON'T CHANGE THIS + emoji: CONFIG.reactions[e] + }; + }); +} + +// Function to generate the embed fields, based on your settings and if you set "const embed = true;" +function generateEmbedFields() { + return CONFIG.roles.map((r, e) => { + return { + emoji: CONFIG.reactions[e], + role: r + }; + }); +} + +// Client events to let you know if the bot is online and to handle any Discord.js errors +client.on("ready", () => console.log("Role Reactions is online!")); +client.on('error', console.error); + +// Handles the creation of the role reactions. Will either send the role messages separately or in an embed +client.on("message", message => { + // Make sure bots can't run this command + if (message.author.bot) return; + + // Make sure the command can only be ran in a server + if (!message.guild) return; + + // We don't want the bot to do anything further if it can't send messages in the channel + if (message.guild && !message.channel.permissionsFor(message.guild.me).missing('SEND_MESSAGES')) return; + + if ((CONFIG.validIDs.includes(message.author.id)) || (message.content.toLowerCase() !== CONFIG.setupCMD)) return; // message.author.id !== CONFIG.yourID + + if (CONFIG.deleteSetupCMD) { + const missing = message.channel.permissionsFor(message.guild.me).missing('MANAGE_MESSAGES'); + // Here we check if the bot can actually delete messages in the channel the command is being ran in + if (missing.includes('MANAGE_MESSAGES')) + throw new Error("I need permission to delete your command message! Please assign the 'Manage Messages' permission to me in this channel!"); + message.delete().catch(O_o=>{}); + } + + const missing = message.channel.permissionsFor(message.guild.me).missing('MANAGE_MESSAGES'); + // Here we check if the bot can actually add recations in the channel the command is being ran in + if (missing.includes('ADD_REACTIONS')) + throw new Error("I need permission to add reactions to these messages! Please assign the 'Add Reactions' permission to me in this channel!"); + + if (!CONFIG.embed) { + if (!CONFIG.initialMessage || (CONFIG.initialMessage === '')) + throw "The 'initialMessage' property is not set in the config.js file. Please do this!"; + + message.channel.send(CONFIG.initialMessage); + + const messages = generateMessages(); + for (const { role, message: msg, emoji } of messages) { + if (!message.guild.roles.find(r => r.name === role)) + throw `The role '${role}' does not exist!`; + + message.channel.send(msg).then(async m => { + const customCheck = message.guild.emojis.find(e => e.name === emoji); + if (!customCheck) await m.react(emoji); + else await m.react(customCheck.id); + }).catch(console.error); + } + } else { + if (!CONFIG.embedMessage || (CONFIG.embedMessage === '')) + throw "The 'embedMessage' property is not set in the config.js file. Please do this!"; + if (!CONFIG.embedFooter || (CONFIG.embedMessage === '')) + throw "The 'embedFooter' property is not set in the config.js file. Please do this!"; + + const roleEmbed = new RichEmbed() + .setDescription(CONFIG.embedMessage) + .setFooter(CONFIG.embedFooter); + + if (CONFIG.embedColor) roleEmbed.setColor(CONFIG.embedColor); + + if (CONFIG.embedThumbnail && (CONFIG.embedThumbnailLink !== '')) + roleEmbed.setThumbnail(CONFIG.embedThumbnailLink); + else if (CONFIG.embedThumbnail && message.guild.icon) + roleEmbed.setThumbnail(message.guild.iconURL); + + const fields = generateEmbedFields(); + if (fields.length > 25) throw "That maximum roles that can be set for an embed is 25!"; + + for (const { emoji, role } of fields) { + if (!message.guild.roles.find(r => r.name === role)) + throw `The role '${role}' does not exist!`; + + const customEmote = client.emojis.find(e => e.name === emoji); + + if (!customEmote) roleEmbed.addField(emoji, role, false); + else roleEmbed.addField(customEmote, role, false); // true + } + + message.channel.send(roleEmbed).then(async m => { + for (const r of CONFIG.reactions) { + const emoji = r; + const customCheck = client.emojis.find(e => e.name === emoji); + + if (!customCheck) await m.react(emoji); + else await m.react(customCheck.id); + } + }); + } +}); + +// This makes the events used a bit more readable +const events = { + MESSAGE_REACTION_ADD: 'messageReactionAdd', + MESSAGE_REACTION_REMOVE: 'messageReactionRemove', +}; + +// This event handles adding/removing users from the role(s) they chose based on message reactions +client.on('raw', async event => { + if (!events.hasOwnProperty(event.t)) return; + + const { d: data } = event; + const user = client.users.get(data.user_id); + const channel = client.channels.get(data.channel_id); + + const message = await channel.fetchMessage(data.message_id); + const member = message.guild.members.get(user.id); + + const emojiKey = (data.emoji.id) ? `${data.emoji.name}:${data.emoji.id}` : data.emoji.name; + let reaction = message.reactions.get(emojiKey); + + if (!reaction) { + // Create an object that can be passed through the event like normal + const emoji = new Emoji(client.guilds.get(data.guild_id), data.emoji); + reaction = new MessageReaction(message, emoji, 1, data.user_id === client.user.id); + } + + let embedFooterText; + if (message.embeds[0]) embedFooterText = message.embeds[0].footer.text; + + if ( + (message.author.id === client.user.id) && (message.content !== CONFIG.initialMessage || + (message.embeds[0] && (embedFooterText !== CONFIG.embedFooter))) + ) { + + if (!CONFIG.embed && (message.embeds.length < 1)) { + const re = `\\*\\*"(.+)?(?="\\*\\*)`; + const role = message.content.match(re)[1]; + + if (member.id !== client.user.id) { + const guildRole = message.guild.roles.find(r => r.name === role); + if (event.t === "MESSAGE_REACTION_ADD") member.addRole(guildRole.id); + else if (event.t === "MESSAGE_REACTION_REMOVE") member.removeRole(guildRole.id); + } + } else if (CONFIG.embed && (message.embeds.length >= 1)) { + const fields = message.embeds[0].fields; + + for (const { name, value } of fields) { + if (member.id !== client.user.id) { + const guildRole = message.guild.roles.find(r => r.name === value); + if ((name === reaction.emoji.name) || (name === reaction.emoji.toString())) { + if (event.t === "MESSAGE_REACTION_ADD") member.addRole(guildRole.id); + else if (event.t === "MESSAGE_REACTION_REMOVE") member.removeRole(guildRole.id); + } + } + } + } + } +}); + +process.on('unhandledRejection', err => { + const msg = err.stack.replace(new RegExp(`${__dirname}/`, 'g'), './'); + console.error("Unhandled Rejection", msg); +}); diff --git a/tests.js b/tests.js new file mode 100644 index 0000000..e8a9837 --- /dev/null +++ b/tests.js @@ -0,0 +1,6 @@ +const CONFIG = require('./config.example'); + +if (CONFIG.botToken !== '') + throw new Error("Please remove the Discord bot token from 'botToken' in the config.js file."); +if (CONFIG.yourID !== '') + throw new Error("Please remove the user ID from 'yourID' in the config.js file."); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..d02a1d6 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,46 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +discord.js@^11.5.1: + version "11.6.4" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-11.6.4.tgz#76bab98de08d7586ecde44c063ef310e6b9a2700" + integrity sha512-cK6rH1PuGjSjpmEQbnpuTxq1Yv8B89SotyKUFcr4RhnsiZnfBfDOev7DD7v5vhtEyyj51NuMWFoRJzgy/m08Uw== + dependencies: + long "^4.0.0" + prism-media "^0.0.4" + snekfetch "^3.6.4" + tweetnacl "^1.0.0" + ws "^6.0.0" + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +prism-media@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-0.0.4.tgz#df5ddc6463670c97ff0e9cbac3c3e0db18df326f" + integrity sha512-dG2w7WtovUa4SiYTdWn9H8Bd4JNdei2djtkP/Bk9fXq81j5Q15ZPHYSwhUVvBRbp5zMkGtu0Yk62HuMcly0pRw== + +snekfetch@^3.6.4: + version "3.6.4" + resolved "https://registry.yarnpkg.com/snekfetch/-/snekfetch-3.6.4.tgz#d13e80a616d892f3d38daae4289f4d258a645120" + integrity sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw== + +tweetnacl@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +ws@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" |