aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author8cy <[email protected]>2020-07-19 02:10:00 -0700
committer8cy <[email protected]>2020-07-19 02:10:00 -0700
commit4014ac46b4e5e515f133ca2c04859824cd43738c (patch)
tree5842824afd615f70e8a4959704f9cf3492fa54f2
downloadwater-waifu-4014ac46b4e5e515f133ca2c04859824cd43738c.tar.xz
water-waifu-4014ac46b4e5e515f133ca2c04859824cd43738c.zip
:star:
-rw-r--r--.gitattributes2
-rw-r--r--.gitignore66
-rw-r--r--.travis.yml7
-rw-r--r--LICENSE21
-rw-r--r--Procfile1
-rw-r--r--README.md45
-rw-r--r--package.json23
-rw-r--r--role.js187
-rw-r--r--tests.js6
-rw-r--r--yarn.lock46
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7771da8
--- /dev/null
+++ b/LICENSE
@@ -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"
+ }
+}
diff --git a/role.js b/role.js
new file mode 100644
index 0000000..5ca2885
--- /dev/null
+++ b/role.js
@@ -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"