diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index acb4b4a..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Javascript Node CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-javascript/ for more details -# -version: 2 -jobs: - build: - docker: - # specify the version you desire here - - image: circleci/node:10.16.3 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - # - image: circleci/mongo:3.4.4 - - working_directory: ~/repo - - steps: - - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "package.json" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: yarn install - - - save_cache: - paths: - - node_modules - key: v1-dependencies-{{ checksum "package.json" }} - - # run tests! - - run: yarn test diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 3c3629e..0000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ba990c2..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:10.16.3 - -MAINTAINER Chapman@Apextion.com - -# Replace shell with bash so we can source files -# RUN rm /bin/sh && ln -sf /bin/bash /bin/sh - -WORKDIR /var/app -COPY package.json . - -RUN npm install --silent - -# Global Installs -RUN npm install -g pm2 sequelize-cli gulp-cli - -COPY . . diff --git a/bot/client.js b/bot/client.js index 18dfef0..208c2d5 100644 --- a/bot/client.js +++ b/bot/client.js @@ -34,7 +34,7 @@ client.on('message', (message) => { util.log('Command message received', message.content, 0); // Build basic help string - let helpString = 'v1.6.0 Discovered Commands:\n\n\t**<> - Required Item\t\t[] - Optional Item**'; + let helpString = 'v1.7.0 Discovered Commands:\n\n\t**<> - Required Item\t\t[] - Optional Item**'; // Process message against every controller Object.keys(controllers).forEach((key) => { diff --git a/bot/controllers/roles.js b/bot/controllers/roles.js index 09a5542..3655561 100644 --- a/bot/controllers/roles.js +++ b/bot/controllers/roles.js @@ -59,20 +59,7 @@ class RoleController extends BaseController { // User roles commands cannot change this.disallowedRoles = [ - 'Admin', 'Armada Officers', 'Armada Officer', 'Fleet Officer', - 'Moderator', 'Tester', 'Team Captain', 'Full Sail Staff', 'Privateers', - 'Team Liaison', 'Armada Athlete', '@everyone', 'Crew', - 'Overwatch_V', 'Overwatch_JV', - 'CS:GO_V', 'CS:GO_JV', - 'Smite_V', 'Smite_JV', - 'Fortnite_V', 'Fortnite_JV', - 'Madden_V', 'Madden_JV', - 'LoL_V', 'LoL_JV', - 'SuperSmashBros_V', 'SuperSmashBros_JV', - 'HeroesOfTheStorm_V', 'HeroesOfTheStorm_JV', - 'RocketLeague_V', 'RocketLeague_JV', - 'DragonBall_V', 'DragonBall_JV', - 'Hearthstone_V', 'Hearthstone_JV', + 'Admin', '@everyone', ]; } @@ -80,10 +67,13 @@ class RoleController extends BaseController { rolesAction() { const { message, disallowedRoles } = this; const roles = []; + + + const dividerRoleName = 'MAX_SELF_ASSIGN_ROLE'; + const maxRole = message.guild.roles.find(role => role.name === dividerRoleName); message.guild.roles.map((role) => { - if (!disallowedRoles.includes(role.name)) { + if(role.position < maxRole.position && !disallowedRoles.includes(role.name)) return roles.push(role.name); - } return role.name; }); return 'List of all Armada Roles: \n\n' + roles.join('\n'); @@ -92,11 +82,14 @@ class RoleController extends BaseController { // Adds a role to the user addRoleAction() { const { message, disallowedRoles } = this; + + const dividerRoleName = 'MAX_SELF_ASSIGN_ROLE'; + const maxRole = message.guild.roles.find(role => role.name === dividerRoleName); const targetRole = message.guild.roles.find('name', message.parsed[1]); if (targetRole === null) { util.log('No role matched', message.parsed[1], 2); return '"' + message.parsed[1] + '" is not a known role. Try `!roles` to get a list of all Roles (They are case-sensitive)'; - } else if (disallowedRoles.includes(targetRole.name)) { + } else if (disallowedRoles.includes(targetRole.name) || targetRole.position >= maxRole.position) { util.log('User Tried to add a restricted/dissalowed role', targetRole.name, 2); return 'You are not worthy of the role ' + message.parsed[1] + '.'; } else { @@ -109,6 +102,9 @@ class RoleController extends BaseController { // Adds multiple roles to the user addRolesAction() { const { message, disallowedRoles } = this; + + const dividerRoleName = 'MAX_SELF_ASSIGN_ROLE'; + const maxRole = message.guild.roles.find(role => role.name === dividerRoleName); const roles = message.parsed[1].split(','); util.log('Multiple Roles Parsing', roles, 4); @@ -116,8 +112,8 @@ class RoleController extends BaseController { if (!disallowedRoles.includes(role)) { const targetRole = message.guild.roles.find('name', role); util.log('Asking API for Role', targetRole, 4); - - if (targetRole === null) { + + if (targetRole === null || targetRole.position >= maxRole.position) { return '"' + role + '" is not a known role. Try `!roles` to get a list of all Roles (They are case-sensitive)'; } return message.member.addRole(targetRole).catch(util.log); @@ -131,12 +127,14 @@ class RoleController extends BaseController { // Removes role from user removeRoleAction() { const { message, disallowedRoles } = this; + const dividerRoleName = 'MAX_SELF_ASSIGN_ROLE'; + const maxRole = message.guild.roles.find(role => role.name === dividerRoleName); const targetRole = message.guild.roles.find('name', message.parsed[1]); if (targetRole === null) { util.log('No role matched', message.parsed[1], 2); return '"' + message.parsed[1] + '" is not a known role. Try `!roles` to get a list of all Roles (They are case-sensitive)'; } - if (disallowedRoles.includes(targetRole.name)) { + if (disallowedRoles.includes(targetRole.name) || targetRole.position >= maxRole.position) { util.log('User Tried to add a restricted/dissalowed role', targetRole.name, 2); return 'You have not the power or the will to control this power.'; } @@ -149,8 +147,11 @@ class RoleController extends BaseController { // Adds all roles to user addAllRolesAction() { const { message, disallowedRoles } = this; + const dividerRoleName = 'MAX_SELF_ASSIGN_ROLE'; + const maxRole = message.guild.roles.find(role => role.name === dividerRoleName); + message.guild.roles.map((role) => { - if (!disallowedRoles.includes(role.name)) { + if (!disallowedRoles.includes(role.name) && role.position < maxRole.position) { return message.member.addRole(role).catch(util.log); } return role.name; @@ -162,8 +163,10 @@ class RoleController extends BaseController { // Removes all roles from user removeAllRolesAction() { const { message, disallowedRoles } = this; + const dividerRoleName = 'MAX_SELF_ASSIGN_ROLE'; + const maxRole = message.guild.roles.find(role => role.name === dividerRoleName); message.guild.roles.map((role) => { - if (!disallowedRoles.includes(role.name)) { + if (!disallowedRoles.includes(role.name) && role.position < maxRole.position) { return message.member.removeRole(role).catch(util.log); } return role.name; diff --git a/changelog.md b/changelog.md index 778a5ea..b11a91c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +## [v1.7.0](https://github.com/reactivepixel/Max-Bot/releases/tag/v1.7.0) + +* Roles are now dynamically blocked by the role `MAX_SELF_ASSIGN_ROLE`. Only roles below this are valid for self assignment. + +## [v1.6.0](https://github.com/reactivepixel/Max-Bot/releases/tag/v1.6.0) + +Max has been moved from a VPS on Digital Ocean running on Docker and a Docker Maria DB Container onto Heroku using a JawsMariaDB resource. Additional enhancements have been made to the fs.armada.bot@gmail.com gmail account to enable 2-factor Auth and a bypass key for Max has been generated. + ## [v1.2.2](https://github.com/reactivepixel/Max-Bot/releases/tag/v1.2.2) Initial feedback from Crewmates prompted some quality of life features to be released early. diff --git a/db/first_run.sh b/db/first_run.sh deleted file mode 100755 index b9b0ac7..0000000 --- a/db/first_run.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -echo "Generating Sequelize config from .env file" -gulp db -echo "Generating PM2 config from .env file" -gulp pm2 -echo "Migrating" -sequelize db:migrate --config db/config/config.json --migrations-path db/migrations/ -echo "Seeding" -# TODO: Seed based upon Environment -# sequelize db:seed:undo:all --config db/config/config.json --migrations-path db/migrations/ --seeders-path db/seeders -# sequelize db:seed:all --config db/config/config.json --migrations-path db/migrations/ --seeders-path db/seeders -echo "Running PM2" -pm2 start max.config.js --no-daemon diff --git a/db/test_data.js b/db/test_data.js deleted file mode 100644 index 8766c72..0000000 --- a/db/test_data.js +++ /dev/null @@ -1,9 +0,0 @@ -const models = require('../models'); -const uuidv4 = require('uuid/v4'); - -models.Member.create({ - discorduser: '@fsafas', - email: 'fasfasaf@apextion.com', - uuid: uuidv4(), - verified: 1 -}).then(console.log).catch(console.error); diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ced8bd9..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: "3" -services: - bot: - build: . - volumes: - - /usr/src/app/node_modules - restart: always - ports: - - "$BOT_PORT_HOST:$BOT_PORT_GUEST" - command: ["./wait-for-it.sh", "mysql:3306", "--", "db/first_run.sh"] - - mysql: - container_name: mariadb - restart: always - image: mariadb:latest - environment: - MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD - MYSQL_USER: $MYSQL_USER - MYSQL_PASS: $MYSQL_PASS - MYSQL_DATABASE: $MYSQL_DATABASE - volumes: - - my-datavolume:/var/lib/mysql - ports: - - "$DB_PORT_HOST:$DB_PORT_GUEST" -volumes: - my-datavolume: diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 35ad2ac..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,34 +0,0 @@ -const gulp = require('gulp'); -const fs = require('fs'); - -gulp.task('db', () => { - require('dotenv').config(); - const config = { - development: { - username: 'root', - password: process.env.MYSQL_ROOT_PASSWORD, - database: process.env.MYSQL_DATABASE, - host: process.env.MYSQL_HOST, - dialect: 'mysql', - }, - }; - fs.writeFileSync('./db/config/config.json', JSON.stringify(config)); -}); - -gulp.task('pm2', () => { - require('dotenv').config(); - const config = { - apps: [{ - name: 'max', - script: './bot/client.js', - env: { - TOKEN: process.env.DISCORD_BOT_TOKEN, - DEBUG_MODE: process.env.DEBUG_MODE, - EMAIL_USERNAME: process.env.EMAIL_USERNAME, - EMAIL_PASS: process.env.EMAIL_PASS, - NODE_ENV: process.env.NODE_ENV, - }, - }], - }; - fs.writeFileSync('./max.config.js', 'module.exports = ' + JSON.stringify(config)); -}); diff --git a/package.json b/package.json index 4517df3..0e85358 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "max", - "version": "1.6.0", + "version": "1.7.0", "description": "", "main": "bot/client.js", "engines": { @@ -8,8 +8,7 @@ "npm": "6.9.0" }, "scripts": { - "start": "node bot/client.js", - "test": "mocha" + "start": "node bot/client.js" }, "keywords": [], "author": "Chapman - Chapman@Apextion.com", @@ -18,7 +17,7 @@ "discord.js": "^11.5.1", "mysql2": "^1.6.5", "nodemailer": "^6.3.1", - "sequelize": "^5.15.2", + "sequelize": "^5.21.2", "uuid": "^3.3.3" } } diff --git a/readme.md b/readme.md index 438ebcb..6c9fc16 100644 --- a/readme.md +++ b/readme.md @@ -7,7 +7,8 @@ Interested in learning how to use Max? See the [Usage documentation here](usage. ### Prerequisites * [Git](https://git-scm.com/downloads) -* [Docker (Stable)](https://docs.docker.com/docker-for-mac/install/) +* [Node JS](https://github.com/nvm-sh/nvm#installation-and-update) +* [MariaDB](https://mariadb.com/kb/en/library/installing-mariadb-on-macos-using-homebrew/) ### Clone the Codebase. @@ -20,38 +21,33 @@ git clone git@github.com:reactivepixel/Max-Bot.git Create an ```.env``` file with the following sensitive information. Replace the "xxx"'s with some unique information for your local Environment. +> ```!verify``` will not work unless you additionally configure *your own* Gmail account to work with this bot. The Official Max email info will not be distributed. + ``` NODE_ENV=development DEBUG_MODE=3 +DISCORD_BOT_TOKEN=xxx_bot_token_from_next_step_xxx + MYSQL_ROOT_PASSWORD=xxx MYSQL_USER=xxx MYSQL_PASS=xxx MYSQL_DATABASE=max -MYSQL_HOST=mysql -DB_PORT_HOST=3306 -DB_PORT_GUEST=3306 - -DISCORD_BOT_TOKEN=xxx_bot_token_from_next_step_xxx - -BOT_PORT_HOST=80 -BOT_PORT_GUEST=3000 +MYSQL_HOST=localhost EMAIL_USERNAME=xxx EMAIL_PASS=xxx +GOOGLE_APP_PASSWORD=xxx ``` > Update ```DISCORD_BOT_TOKEN``` with the token you receive from the next step. - - ### Bot Token Add a new App and create a Bot User then obtain the [Discord App Bot Token](https://discordapp.com/developers/applications/me) from the created Bot User or contact a Release Manager for Max's Dev Bot token. - Update the ```DISCORD_BOT_TOKEN``` in the .env file with the token provided to you. Run ``$ gulp pm2`` to create a max.config.js file within your root directory. @@ -61,37 +57,27 @@ Run ``$ gulp pm2`` to create a max.config.js file within your root directory. As an authorized user of the bot you will need to add it to a server. 1. Go to the Discord developer pages (login if you haven't). -1. Go to the application with the bot you want to add to your channel. -1. Copy the Client/Application ID. -1. Go to https://discordapp.com/oauth2/authorize?client_id=```CLIENT_ID_GOES_HERE```&scope=bot&permissions=0 -1. Select server and click authorize. +2. Go to the application with the bot you want to add to your channel. +3. Copy the Client/Application ID. +4. Go to https://discordapp.com/oauth2/authorize?client_id=```CLIENT_ID_GOES_HERE```&scope=bot&permissions=0 +5. Select server and click authorize. > [Source](https://stackoverflow.com/questions/37689289/joining-a-server-with-the-discord-python-api) -### Configure Email Functionality +### Optional Configure Email Functionality Max uses [Nodemailer](https://nodemailer.com/about/) to send verification emails to users joining the Armada server. to work proper locally please follow these steps to create a new gmail account to locally test with: 1. Create a new gmail.com account 2. Once logged in with this account enable [less secure application access](https://myaccount.google.com/u/1/lesssecureapps?pageId=none&pli=1). 3. fill in the ```EMAIL_USERNAME``` & ```EMAIL_PASS``` values on the ```.env``` file with this new gmail account's info. - -# Running the Bot - -## Local - -To run the bot locally ensure that you have followed the installation instructions above and have docker running. - -Use docker-compose to start the container with the bot client. +# Running the Bot Locally ``` -docker-compose up +npm install +npm start ``` -If all went well, and your **DEBUG_MODE** is set properly you will see a logged message of ```Bot Online and Ready!:``` - -Hop onto your discord server and find a room with the bot and run the command ```!help``` to see a display of optional commands. - # Style Guide The AirBnB javascript style guide has been put in place and will be enforced through active and passive linting. @@ -146,24 +132,3 @@ of this particular output should be. Reference the **Debug Level Chart** 2. General Debug Information for Development 3. Very Detailed and in-depth Output. 4. Highly Fine Grained Detailed and in-depth Output. - - -# Modules & integrations - -## Database -### How to connect using Sequel Pro - -You can find more information about the config file `db/config/config.json` - -![](https://preview.ibb.co/d07YOG/Screen_Shot_2017_11_10_at_2_49_46_PM.png) - - -# Container Information - -## Node Container - -[Official Node](https://hub.docker.com/_/node/) Container used. - -# Other Information - -## [Change Log](changelog.md) diff --git a/wait-for-it.sh b/wait-for-it.sh deleted file mode 100755 index bbe4043..0000000 --- a/wait-for-it.sh +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env bash -# Use this script to test if a given TCP host/port are available - -cmdname=$(basename $0) - -echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } - -usage() -{ - cat << USAGE >&2 -Usage: - $cmdname host:port [-s] [-t timeout] [-- command args] - -h HOST | --host=HOST Host or IP under test - -p PORT | --port=PORT TCP port under test - Alternatively, you specify the host and port as host:port - -s | --strict Only execute subcommand if the test succeeds - -q | --quiet Don't output any status messages - -t TIMEOUT | --timeout=TIMEOUT - Timeout in seconds, zero for no timeout - -- COMMAND ARGS Execute command with args after the test finishes -USAGE - exit 1 -} - -wait_for() -{ - if [[ $TIMEOUT -gt 0 ]]; then - echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" - else - echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" - fi - start_ts=$(date +%s) - while : - do - if [[ $ISBUSY -eq 1 ]]; then - nc -z $HOST $PORT - result=$? - else - (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 - result=$? - fi - if [[ $result -eq 0 ]]; then - end_ts=$(date +%s) - echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" - break - fi - sleep 1 - done - return $result -} - -wait_for_wrapper() -{ - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - if [[ $QUIET -eq 1 ]]; then - timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & - else - timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & - fi - PID=$! - trap "kill -INT -$PID" INT - wait $PID - RESULT=$? - if [[ $RESULT -ne 0 ]]; then - echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" - fi - return $RESULT -} - -# process arguments -while [[ $# -gt 0 ]] -do - case "$1" in - *:* ) - hostport=(${1//:/ }) - HOST=${hostport[0]} - PORT=${hostport[1]} - shift 1 - ;; - --child) - CHILD=1 - shift 1 - ;; - -q | --quiet) - QUIET=1 - shift 1 - ;; - -s | --strict) - STRICT=1 - shift 1 - ;; - -h) - HOST="$2" - if [[ $HOST == "" ]]; then break; fi - shift 2 - ;; - --host=*) - HOST="${1#*=}" - shift 1 - ;; - -p) - PORT="$2" - if [[ $PORT == "" ]]; then break; fi - shift 2 - ;; - --port=*) - PORT="${1#*=}" - shift 1 - ;; - -t) - TIMEOUT="$2" - if [[ $TIMEOUT == "" ]]; then break; fi - shift 2 - ;; - --timeout=*) - TIMEOUT="${1#*=}" - shift 1 - ;; - --) - shift - CLI=("$@") - break - ;; - --help) - usage - ;; - *) - echoerr "Unknown argument: $1" - usage - ;; - esac -done - -if [[ "$HOST" == "" || "$PORT" == "" ]]; then - echoerr "Error: you need to provide a host and port to test." - usage -fi - -TIMEOUT=${TIMEOUT:-15} -STRICT=${STRICT:-0} -CHILD=${CHILD:-0} -QUIET=${QUIET:-0} - -# check to see if timeout is from busybox? -# check to see if timeout is from busybox? -TIMEOUT_PATH=$(realpath $(which timeout)) -if [[ $TIMEOUT_PATH =~ "busybox" ]]; then - ISBUSY=1 - BUSYTIMEFLAG="-t" -else - ISBUSY=0 - BUSYTIMEFLAG="" -fi - -if [[ $CHILD -gt 0 ]]; then - wait_for - RESULT=$? - exit $RESULT -else - if [[ $TIMEOUT -gt 0 ]]; then - wait_for_wrapper - RESULT=$? - else - wait_for - RESULT=$? - fi -fi - -if [[ $CLI != "" ]]; then - if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then - echoerr "$cmdname: strict mode, refusing to execute subprocess" - exit $RESULT - fi - exec "${CLI[@]}" -else - exit $RESULT -fi