├── .vscode └── settings.json ├── .eslintrc.js ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── misc.xml ├── vcs.xml ├── jsLibraryMappings.xml ├── markdown-exported-files.xml ├── modules.xml ├── devmod.iml ├── inspectionProfiles │ └── Project_Default.xml └── markdown-navigator.xml ├── babel.config.js ├── src ├── esm.js ├── utils │ ├── verify │ │ ├── esm.js │ │ └── verify.js │ ├── capitalize.js │ ├── colours.js │ ├── tests │ │ ├── dbTest.js │ │ └── test.js │ ├── config.js │ ├── sendErrorMessage.js │ ├── expandCommands.js │ ├── user.js │ ├── testChannelsAndRoles.js │ ├── default.config.js │ └── log.js ├── commands │ ├── kill.js │ ├── users.js │ ├── unlock.js │ ├── lock.js │ ├── prune.js │ ├── about.js │ ├── ping.js │ ├── index.js │ ├── tag.js │ ├── stats.js │ ├── mdn.js │ ├── tags.js │ ├── clearWarns.js │ ├── lmgtfy.js │ ├── unmute.js │ ├── mute.js │ ├── reputation.js │ ├── roles.js │ ├── unban.js │ ├── warns.js │ ├── report.js │ ├── buildInfo.js │ ├── help.js │ ├── move.js │ ├── ban.js │ ├── buildRoles.js │ ├── yeet.js │ ├── role.js │ └── warn.js ├── main.js ├── processes │ ├── activityChanger.js │ ├── torielsAntiBotCrusade.js │ ├── commandListener.js │ ├── thanksListener.js │ ├── infoReactionListener.js │ └── roleReactionListener.js ├── devmod.js └── db.js ├── docs ├── kill.md ├── ping.md ├── about.md ├── mdn.md ├── tags.md ├── roles.md ├── prune.md ├── users.md ├── role.md ├── unban.md ├── report.md ├── stats.md ├── tag.md ├── reputation.md ├── lock.md ├── warns.md ├── unlock.md ├── clearwarns.md ├── move.md ├── mute.md ├── unmute.md ├── help.md ├── buildinfo.md ├── lmgtfy.md ├── warn.md ├── ban.md ├── buildroles.md └── usage.md ├── LICENSE.md ├── package.json ├── env.json ├── .gitignore ├── config ├── approvedRoles.js └── tags.js └── readme.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard' 3 | } 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | '@babel/env', 4 | { 5 | targets: { 6 | node: 'current' 7 | } 8 | } 9 | ] 10 | ] 11 | 12 | module.exports = { presets } 13 | -------------------------------------------------------------------------------- /src/esm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * File to run the main bot through ESM compatibility. 4 | */ 5 | 6 | const r = require('esm')(module) 7 | 8 | // Import the main module. 9 | module.exports = r('./main.js') 10 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/markdown-exported-files.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/utils/verify/esm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * File to run the verification bot through ESM compatibility. 4 | */ 5 | 6 | const r = require('esm')(module) 7 | 8 | // Import the main module. 9 | module.exports = r('./verify.js') 10 | -------------------------------------------------------------------------------- /docs/kill.md: -------------------------------------------------------------------------------- 1 | # Kill 2 | 3 | ## Usage 4 | `.kill` 5 | 6 | Kills the bot's process. 7 | 8 | ## Permission Requirements 9 | `['ADMINISTRATOR']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /docs/ping.md: -------------------------------------------------------------------------------- 1 | # Ping 2 | 3 | ## Usage 4 | `.ping` 5 | 6 | Shows ping and round trip time for the bot. 7 | 8 | ## Permission Requirements 9 | `['SEND_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ## Usage 4 | `.about` 5 | 6 | Sends a message with information about the bot. 7 | 8 | ## Permission Requirements 9 | `['SEND_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /docs/mdn.md: -------------------------------------------------------------------------------- 1 | # MDN 2 | 3 | ## Usage 4 | `.mdn ` 5 | 6 | Searches MDN and returns the first result. 7 | 8 | ## Permission Requirements 9 | `['SEND_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /docs/tags.md: -------------------------------------------------------------------------------- 1 | # Tags 2 | 3 | ## Usage 4 | `.tags` 5 | 6 | Shows all tags available to be used, specified in `tags`. 7 | 8 | ## Permission Requirements 9 | `['SEND_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /src/utils/capitalize.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Export a function to capitalize the first letter of a word. 4 | */ 5 | 6 | // Return a string with the first character.toUpperCase() and the rest the word sliced from index 1 onward. 7 | export const capitalize = word => `${word[0].toUpperCase()}${word.slice(1)}` 8 | -------------------------------------------------------------------------------- /docs/roles.md: -------------------------------------------------------------------------------- 1 | # Roles 2 | 3 | ## Usage 4 | `.roles` 5 | 6 | Shows all roles available to be added, specified in `approvedRoles`. 7 | 8 | ## Permission Requirements 9 | `['SEND_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /docs/prune.md: -------------------------------------------------------------------------------- 1 | # Prune 2 | 3 | ## Usage 4 | `.prune []` 5 | 6 | Removes specified number of messages from the channel. 7 | 8 | ## Permission Requirements 9 | `['MANAGE_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /docs/users.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | ## Usage 4 | `.users` 5 | 6 | Shows the amount of users in the server along with the number currently online. 7 | 8 | ## Permission Requirements 9 | `['SEND_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /docs/role.md: -------------------------------------------------------------------------------- 1 | # Role 2 | 3 | ## Usage 4 | `.role ` 5 | 6 | Adds or removed the specified role, if it is listed in `approvedRoles`. 7 | 8 | ## Permission Requirements 9 | `['SEND_MESSAGES']` 10 | 11 | ## Author 12 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 13 | -------------------------------------------------------------------------------- /docs/unban.md: -------------------------------------------------------------------------------- 1 | # Unban 2 | 3 | ## Usage 4 | `.unban ` 5 | 6 | Unbans a user and logs it to `channel.ban`. 7 | 8 | You can ban a user with the [ban](./ban.md) command. 9 | 10 | ## Permission Requirements 11 | `['BAN_MEMBERS']` 12 | 13 | ## Author 14 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 15 | -------------------------------------------------------------------------------- /src/utils/colours.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Constants for colours used in discord embedded messages. 4 | */ 5 | 6 | export const red = 0xE74C3C 7 | export const orange = 0xF39C12 8 | export const yellow = 0xF1C40F 9 | export const green = 0x2ECC71 10 | export const blue = 0x3498DB 11 | export const grey = 0x34495E 12 | export const purple = 0x9932CC 13 | -------------------------------------------------------------------------------- /docs/report.md: -------------------------------------------------------------------------------- 1 | # Report 2 | 3 | ## Usage 4 | `.report ` 5 | 6 | Reports a user to the staff, logged to the channel specified by `channels.reports` 7 | in the config. 8 | 9 | ## Permission Requirements 10 | `['SEND_MESSAGES']` 11 | 12 | ## Author 13 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 14 | -------------------------------------------------------------------------------- /docs/stats.md: -------------------------------------------------------------------------------- 1 | # Stats 2 | 3 | ## Usage 4 | `.stats` 5 | 6 | Sends some stats about the bot and server, such as member count, server creation date, 7 | number of channels, server regions, and bot uptime. 8 | 9 | ## Permission Requirements 10 | `['SEND_MESSAGES']` 11 | 12 | ## Author 13 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 14 | -------------------------------------------------------------------------------- /docs/tag.md: -------------------------------------------------------------------------------- 1 | # Tag 2 | 3 | ## Usage 4 | `.tag []` 5 | 6 | Sends a discord embed with the name specified by the command, from the config option `tags`. 7 | 8 | If a user is specified, the bot will tag them. 9 | 10 | ## Permission Requirements 11 | `['SEND_MESSAGES']` 12 | 13 | ## Author 14 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 15 | -------------------------------------------------------------------------------- /docs/reputation.md: -------------------------------------------------------------------------------- 1 | # Reputation 2 | 3 | ## Usage 4 | `.rep []` 5 | 6 | If a user is specified, show that user's reputation. If not, show the leaderboard. 7 | 8 | You can add reputation to a user with the by saying `thanks @user`. 9 | 10 | ## Permission Requirements 11 | `['SEND_MESSAGES']` 12 | 13 | ## Author 14 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 15 | -------------------------------------------------------------------------------- /docs/lock.md: -------------------------------------------------------------------------------- 1 | # Lock 2 | 3 | ## Usage 4 | `.lock` 5 | 6 | Locks a channel. Sets permissions for the `@everyone` and `verified` role in the current channel to `{ 'SEND_MESSAGES': false }` 7 | 8 | You can unlock a channel with the [unlock](./unlock.md) command. 9 | 10 | ## Permission Requirements 11 | `['MANAGE_CHANNELS']` 12 | 13 | ## Author 14 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 15 | -------------------------------------------------------------------------------- /docs/warns.md: -------------------------------------------------------------------------------- 1 | # Warns 2 | 3 | ## Usage 4 | `.warns ` 5 | 6 | Lists a users warnings. 7 | 8 | You can add warnings to a user with the [warn](./warn.md) command. 9 | 10 | You can clear a user's warnings with the [clear warns](./clearwarns.md) command. 11 | 12 | ## Permission Requirements 13 | `['KICK_MEMBERS']` 14 | 15 | ## Author 16 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 17 | -------------------------------------------------------------------------------- /docs/unlock.md: -------------------------------------------------------------------------------- 1 | # Unlock 2 | 3 | ## Usage 4 | `.unlock` 5 | 6 | Unlocks a channel. Sets permissions for the `@everyone` and `verified` role in the current channel to `{ 'SEND_MESSAGES': null }` 7 | 8 | You can lock a channel with the [lock](./lock.md) command. 9 | 10 | ## Permission Requirements 11 | `['MANAGE_CHANNELS']` 12 | 13 | ## Author 14 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 15 | -------------------------------------------------------------------------------- /docs/clearwarns.md: -------------------------------------------------------------------------------- 1 | # Clear Warns 2 | 3 | ## Usage 4 | `.clearwarns ` 5 | 6 | Removes all of a user's warnings. 7 | 8 | You can add warnings to a user with the [warn](./warn.md) command. 9 | 10 | You can list a user's warnings with the [warns](./warns.md) command. 11 | 12 | ## Permission Requirements 13 | `['KICK_MEMBERS']` 14 | 15 | ## Author 16 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 17 | -------------------------------------------------------------------------------- /docs/move.md: -------------------------------------------------------------------------------- 1 | # Move 2 | 3 | ## Usage 4 | `.move ` 5 | 6 | Looks at the last 20 messages in the channel the command was sent, selects the most recent 7 | set of consecutive message, quotes them in the specified channel, and deletes the initial 8 | messages. 9 | 10 | ## Permission Requirements 11 | `['MANAGE_MESSAGES']` 12 | 13 | ## Author 14 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 15 | -------------------------------------------------------------------------------- /docs/mute.md: -------------------------------------------------------------------------------- 1 | # Mute 2 | 3 | ## Usage 4 | `.mute ` 5 | 6 | Adds the role specified in the config's `roles.muted` to the user, and logs it to the 7 | channel specified in the config's `channels.warn`. 8 | 9 | You can unmute a user with the [unmute](./unmute.md) command. 10 | 11 | ## Permission Requirements 12 | `['KICK_MEMBERS']` 13 | 14 | ## Author 15 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 16 | -------------------------------------------------------------------------------- /docs/unmute.md: -------------------------------------------------------------------------------- 1 | # Unmute 2 | 3 | ## Usage 4 | `.unmute ` 5 | 6 | Removes the role specified in the config's `roles.muted` to the user, and logs it to the 7 | channel specified in the config's `channels.warn`. 8 | 9 | You can mute a user with the [mute](./mute.md) command. 10 | 11 | ## Permission Requirements 12 | `['KICK_MEMBERS']` 13 | 14 | ## Author 15 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 16 | -------------------------------------------------------------------------------- /.idea/devmod.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /src/utils/tests/dbTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * File to run to confirm database is working fine. 4 | */ 5 | 6 | import { getSetting, setSetting } from '../../db' 7 | import { log } from '../log' 8 | 9 | // Main function to utilize await for db functions. 10 | const test = async () => { 11 | // Set the value of key 'test' to something. 12 | await setSetting('test', 'hey you') 13 | 14 | // Retrieve the value of key 'test' and log it to confirm it is accurate. 15 | const val = await getSetting('test') 16 | 17 | log('DBTest', val) 18 | } 19 | 20 | test() 21 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * File to compile and export configuration. 4 | */ 5 | 6 | import defaultConfig from './default.config' 7 | import { existsSync, realpathSync } from 'fs' 8 | 9 | const configFile = realpathSync('devmod.config.js') 10 | 11 | const userConfig = existsSync(configFile) 12 | ? require(configFile).default 13 | : {} 14 | 15 | export default { 16 | ...defaultConfig, 17 | ...userConfig, 18 | channels: { 19 | ...defaultConfig.channels, 20 | ...userConfig.channels 21 | }, 22 | roles: { 23 | ...defaultConfig.roles, 24 | ...userConfig.roles 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/help.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | ## Usage 4 | `.help [ ]` 5 | 6 | Looks at all of the commands loaded, extract the name & usage information, and send it in 7 | a message. The message will delete after `msgDeleteTime` seconds. 8 | 9 | Only commands that the user has permission to use will be shown. 10 | 11 | If a user is specified, the bot will tag that used in the message. 12 | 13 | If `'true'` is included anywhere in the arguments, the message will not be deleted. 14 | 15 | ## Permission Requirements 16 | `['SEND_MESSAGES']` 17 | 18 | ## Author 19 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 20 | -------------------------------------------------------------------------------- /docs/buildinfo.md: -------------------------------------------------------------------------------- 1 | # Build Info 2 | 3 | ## Usage 4 | `.buildinfo` 5 | 6 | Sends an info message to the info channel. Reads up to 10 messages from the channel the 7 | command was sent and copies them to the channel specified in the config's `channels.info` 8 | 9 | If there's a message that contains the string "You agree to this" a checkmark reaction is 10 | added to it's copy, the message ID is saved to the database, and the info reaction listener 11 | listens to reactions to it. 12 | 13 | ## Permission Requirements 14 | `['ADMINISTRATOR']` 15 | 16 | ## Author 17 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 18 | -------------------------------------------------------------------------------- /src/utils/tests/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * File to run to test various js methods and etc. 4 | */ 5 | 6 | // Main function to utilize await for db functions. 7 | import { log } from '../log' 8 | 9 | const test = async () => { 10 | const array = [ 11 | { 12 | array: [1, 2, 3, 4] 13 | }, 14 | { 15 | array: [5, 6, 7, 8] 16 | } 17 | ] 18 | const newArray = array.reduce((previousValue, currentValue) => { 19 | const previousArray = previousValue 20 | for (const item of currentValue.array) { 21 | previousArray.push(item) 22 | } 23 | return previousArray 24 | }, []) 25 | 26 | log('Test', newArray) 27 | } 28 | 29 | test() 30 | -------------------------------------------------------------------------------- /docs/lmgtfy.md: -------------------------------------------------------------------------------- 1 | # LMGTFY 2 | 3 | ## Usage 4 | `.lmgtfy [ ]` 5 | 6 | Sends a 'let me google that for you link'. 7 | 8 | `` can be one of these options (the key - with the hyphen) 9 | ```js 10 | const sites = { 11 | '-g': 'g', // google 12 | '-y': 'y', //yahoo 13 | '-b': 'b', // bing 14 | '-k': 'k', // ask 15 | '-a': 'a', // aol 16 | '-d': 'd' // duckduckgo 17 | } 18 | ``` 19 | The same applies to the type of query: 20 | ```js 21 | const types = { 22 | '-w': 'w', // web 23 | '-i': 'i' // image 24 | } 25 | ``` 26 | 27 | 28 | ## Permission Requirements 29 | `['SEND_MESSAGES']` 30 | 31 | ## Author 32 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 33 | -------------------------------------------------------------------------------- /docs/warn.md: -------------------------------------------------------------------------------- 1 | # Warn 2 | 3 | ## Usage 4 | `.warn []` 5 | 6 | Adds a warning to the user. If no reason is specified, `'warned by devmod'` is used. 7 | The warning is logged in `channels.warn`. 8 | 9 | If `autoBan` is true and the user's warnings after this one are equal to `autoBanWarns`, 10 | the user will be banned, their messages from the path `banMsgDelete` days, and the ban 11 | will be logged to `channels.ban`. 12 | 13 | You can clear a user's warnings with the [clear warns](./clearwarns.md) command. 14 | 15 | You can list a user's warnings with the [warns](./warns.md) command. 16 | 17 | ## Permission Requirements 18 | `['KICK_MEMBERS']` 19 | 20 | ## Author 21 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 22 | -------------------------------------------------------------------------------- /docs/ban.md: -------------------------------------------------------------------------------- 1 | # Ban 2 | 3 | ## Usage 4 | `.ban [ <--rm days>]` 5 | 6 | Bans a user and removes their messages from a specified number of days previous, 7 | and log it to the channel specified in `channels.bans`. 8 | 9 | To specify a number of days, pass the argument `--rm` followed immediately by 10 | the number of days you want to specify, for example `--rm 7` to delete messages 11 | from the past week. This can be included anywhere in the arguments after the 12 | user is tagged. 13 | 14 | If there isn't a number of days specified, use the in `banMsgDelete` field from 15 | the config. 16 | 17 | You can unban a user with the [unban](./unban.md) command. 18 | 19 | ## Permission Requirements 20 | `['BAN_MEMBERS']` 21 | 22 | ## Author 23 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 24 | -------------------------------------------------------------------------------- /src/utils/sendErrorMessage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Export a function to create consistent Discord embedded error messages. 4 | */ 5 | 6 | import { red } from './colours' 7 | import { logError } from './log' 8 | 9 | // Given a title and a description, return the object for a Discord embedded error message. 10 | export const sendErrorMessage = async (title, description, message) => { 11 | try { 12 | // noinspection JSUnresolvedVariable 13 | if (!message.deleted) { 14 | // React to the message with an X emoji. 15 | await message.react('❌') 16 | } 17 | } catch (err) { 18 | await logError('Function', 'Failed to react to message', err) 19 | } 20 | 21 | try { 22 | // Send the error message. 23 | return message.channel.send({ 24 | embed: { 25 | title, 26 | color: red, 27 | description 28 | } 29 | }) 30 | } catch (err) { 31 | await logError('Function', 'Failed to send error message', err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/expandCommands.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Export a function that expands the commands array into an object with all aliases. 4 | */ 5 | 6 | // Given an array of commands, returns an object with all aliases as keys. 7 | import { 8 | logError 9 | } from './log' 10 | 11 | export const expandCommands = commands => { 12 | try { 13 | // Return a reduced array. 14 | return commands.reduce((previous, current) => { 15 | // Clone previous object. 16 | const newCommands = previous 17 | // For each alias in the current command, add a key to the expanded object. 18 | for (const alias of current.aliases) { 19 | // If the alias isn't already used, add it to the object. 20 | if (!Object.prototype.hasOwnProperty.call(newCommands, alias)) { 21 | newCommands[alias] = current 22 | } 23 | } 24 | return previous 25 | }, {}) 26 | } catch (err) { 27 | // noinspection JSIgnoredPromiseFromCall 28 | logError('Function', 'expandCommand failed', err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/buildroles.md: -------------------------------------------------------------------------------- 1 | # Build Roles 2 | 3 | ## Usage 4 | `.buildroles` 5 | 6 | For each item in the `approvedRoles` field in the config, sends a message to the channel 7 | specified in `channels.roles`, saves the IDs to the database, and lets the role reaction 8 | listener add and removes roles based on the reaction. 9 | 10 | ## Specification 11 | Each element of the `approvedRoles` array should look like this: 12 | ```js 13 | { 14 | 'general', // unique ID 15 | name: 'General Roles', // name for roles group 16 | message: 'These are some general roles that we have for you to add.', // message for the roles group 17 | roles: { // object with the role name as keys 18 | { // role name (same as in server settings) 19 | 'Developer', // the name that should be shown in the message 20 | emoji: '☕' // the emoji that will be used as the reaction 21 | } 22 | } 23 | } 24 | ``` 25 | 26 | ## Permission Requirements 27 | `['ADMINISTRATOR']` 28 | 29 | ## Author 30 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devmod", 3 | "version": "5.1.0", 4 | "description": "A bot for moderating discord servers. Written by and for developers with modularity in mind.", 5 | "main": "src/main.js", 6 | "types": "module", 7 | "license": "MIT", 8 | "author": { 9 | "name": "Gabe Dunn", 10 | "email": "gabe@gabedunn.dev", 11 | "url": "https://gabedunn.dev" 12 | }, 13 | "scripts": { 14 | "dev": "nodemon src/esm.js", 15 | "lint": "eslint --ext .js src", 16 | "start": "node src/esm.js", 17 | "verify": "node src/utils/verify/esm.js" 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "npm run lint", 22 | "pre-push": "npm run lint --fix" 23 | } 24 | }, 25 | "dependencies": { 26 | "chalk": "^4.0.0", 27 | "discord.js": "^12.2.0", 28 | "esm": "^3.2.25", 29 | "husky": "^4.2.5", 30 | "moment": "^2.26.0", 31 | "nedb-async-await": "^0.1.2" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^6.8.0", 35 | "eslint-config-standard": "^14.1.1", 36 | "eslint-plugin-import": "^2.20.2", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-standard": "^4.0.1", 40 | "nodemon": "^2.0.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * A few functions relating to member & getName objects. 4 | */ 5 | 6 | // Given a member or getName, return a string with the name of the member. 7 | export const getName = (user, id) => { 8 | if (user !== undefined) { 9 | // If he the user has a 'user' field (read: is a member), return the nickname or user.username. Otherwise, return the user.username. 10 | return Object.prototype.hasOwnProperty.call(user, 'user') 11 | ? user.nickname 12 | ? user.nickname 13 | : user.user.username 14 | : user.username 15 | } else { 16 | return `<@${id}>` 17 | } 18 | } 19 | 20 | // Given a member or getName, return an object of the author field for a message. 21 | export const getAuthor = user => { 22 | // Create an initial author object with the name of the user. 23 | const author = { 24 | name: getName(user, user.id) 25 | } 26 | 27 | // If the user has a 'user' field (read: is a member), set the icon_url to the user.avatarURL(). Otherwise, set it to the avatarURL. 28 | if (Object.prototype.hasOwnProperty.call(user, 'user')) { 29 | author.icon_url = user.user.avatarURL() 30 | } else { 31 | author.icon_url = user.avatarURL() 32 | } 33 | 34 | // Return the author object. 35 | return author 36 | } 37 | -------------------------------------------------------------------------------- /env.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "BOT_TOKEN" 4 | }, 5 | { 6 | "name": "BOT_OWNER" 7 | }, 8 | { 9 | "name": "PREFIX", 10 | "defaultOption": "." 11 | }, 12 | { 13 | "name": "MSG_DELETE_TIME", 14 | "defaultOption": "20" 15 | }, 16 | { 17 | "name": "DB_FILE", 18 | "defaultOption": "devmod.db" 19 | }, 20 | { 21 | "name": "AUTOBAN", 22 | "defaultOption": "true" 23 | }, 24 | { 25 | "name": "AUTOBAN_WARNS", 26 | "defaultOption": "3" 27 | }, 28 | { 29 | "name": "BAN_MSG_DELETE", 30 | "defaultOption": "0" 31 | }, 32 | { 33 | "name": "CHANNEL_WARN", 34 | "defaultOption": "warnings" 35 | }, 36 | { 37 | "name": "CHANNEL_BAN", 38 | "defaultOption": "bans" 39 | }, 40 | { 41 | "name": "CHANNEL_REPORT", 42 | "defaultOption": "reports" 43 | }, 44 | { 45 | "name": "CHANNEL_ROLES", 46 | "defaultOption": "roles" 47 | }, 48 | { 49 | "name": "CHANNEL_INFO", 50 | "defaultOption": "info" 51 | }, 52 | { 53 | "name": "CHANNEL_CRUSADE", 54 | "defaultOption": "crusade" 55 | }, 56 | { 57 | "name": "CHANNEL_ERRORS", 58 | "defaultOption": "errors" 59 | }, 60 | { 61 | "name": "ROLE_MUTED", 62 | "defaultOption": "muted" 63 | }, 64 | { 65 | "name": "ROLE_VERIFIED", 66 | "defaultOption": "verified" 67 | } 68 | ] 69 | -------------------------------------------------------------------------------- /src/utils/testChannelsAndRoles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Function to test that all the channels and roles specified in the config exist. 4 | */ 5 | 6 | import { logError } from './log' 7 | 8 | const { channels, guildID, roles } = require('./config').default 9 | 10 | export const testChannelsAndRoles = async client => { 11 | const guild = client.guilds.cache.find(g => g.id === guildID) 12 | 13 | const guildChannels = guild.channels 14 | const guildRoles = guild.roles 15 | 16 | const nullChannels = Object.values(channels).map(c => { 17 | return { 18 | name: c, 19 | channel: guildChannels.cache.find(gc => gc.name === c) 20 | } 21 | } 22 | ).filter(c => c.channel === null) 23 | 24 | const nullRoles = Object.values(roles).map(r => { 25 | return { 26 | name: r, 27 | role: guildRoles.cache.find(gr => gr.name === r) 28 | } 29 | } 30 | ).filter(r => r.role === null) 31 | 32 | if (nullChannels.length !== 0) { 33 | await logError('Test CnR', `Channels Don't Exist: ${nullChannels.map(c => `${c.name}`).join(', ')}`, false) 34 | } 35 | if (nullRoles.length !== 0) { 36 | await logError('Test CnR', `Roles Don't Exist: ${nullRoles.map(c => `${c.name}`).join(', ')}`, false) 37 | } 38 | if (nullChannels.length !== 0 || nullRoles.length !== 0) { 39 | throw new Error('channels or roles don\'t exist.') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/kill.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that kills the bot. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { log, logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | // Export an object with command info and the function to execute. 11 | export const killCommand = { 12 | name: 'Kill', 13 | aliases: ['kill', 'die'], 14 | category: 'utils', 15 | description: 'Kills the bot\'s process.', 16 | permissions: ['ADMINISTRATOR'], 17 | exec: async (args, message) => { 18 | try { 19 | try { 20 | // Remove the user's message. 21 | await message.delete() 22 | } catch (err) { 23 | await logError('Kill', 'Failed to delete message', err, message) 24 | } 25 | 26 | try { 27 | // Send the kill embed. 28 | await message.channel.send({ 29 | embed: { 30 | title: 'Killing Bot...', 31 | color: blue, 32 | author: getAuthor(message.member), 33 | timestamp: new Date() 34 | } 35 | }) 36 | log('Kill', 'Killing the bot...') 37 | process.exit(0) 38 | } catch (err) { 39 | await logError('Kill', 'Failed to kill process.', err, message) 40 | } 41 | } catch (err) { 42 | await logError('Kill', 'Failed to run command', err, message) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * File to import all main functions and run them. 4 | * TODO: go through entire project and user proper naming for user & member 5 | * TODO: test every case of each command 6 | * TODO: more extensively check for non-existent instances of classes 7 | */ 8 | 9 | import { devmod } from './devmod' 10 | import { log, logError } from './utils/log' 11 | 12 | const { botToken, guildID } = require('./utils/config').default 13 | 14 | const main = async () => { 15 | try { 16 | // If an unhandled rejection occurs log it. 17 | process.on('unhandledRejection', async err => { 18 | await logError('Main', 'Unhandled Rejection', err) 19 | }) 20 | // Same thing but for uncaught exception. 21 | process.on('uncaughtException', async err => { 22 | await logError('Main', 'Uncaught Exception', err) 23 | }) 24 | } catch (err) { 25 | await logError('Main', 'Failed to add process error listeners', err) 26 | } 27 | 28 | try { 29 | if (botToken) { 30 | if (guildID) { 31 | await devmod() 32 | } else { 33 | log('Main', 'NO GUILD ID!') 34 | } 35 | } else { 36 | log('Main', 'NO BOT TOKEN!') 37 | } 38 | } catch (err) { 39 | await logError('Main', 'Something has failed', err) 40 | } 41 | } 42 | 43 | // Run the main function. 44 | main().then(() => log('Main', 'Bot Initialized!')) 45 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 15 | 16 | 29 | 30 | -------------------------------------------------------------------------------- /src/commands/users.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that shows how many users are on the server. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | // Export an object with command info and the function to execute. 11 | export const usersCommand = { 12 | name: 'Users', 13 | aliases: ['users', 'usercount'], 14 | category: 'utils', 15 | description: 'Shows how many users are on the server.', 16 | permissions: ['SEND_MESSAGES'], 17 | exec: async (args, message) => { 18 | try { 19 | try { 20 | // Remove the user's message. 21 | await message.delete() 22 | } catch (err) { 23 | await logError('Users', 'Failed to delete message', err, message) 24 | } 25 | 26 | // Save some info about the server and bot. 27 | const guild = message.guild 28 | 29 | try { 30 | // Send the stats message. 31 | // noinspection JSUnresolvedFunction 32 | return message.channel.send({ 33 | embed: { 34 | title: 'Users', 35 | color: blue, 36 | description: `There are currently ${guild.memberCount} users in this discord server (${guild.members.cache.array().filter( 37 | m => m.presence.status !== 'offline').length} currently online).`, 38 | author: getAuthor(message.member) 39 | } 40 | }) 41 | } catch (err) { 42 | await logError('Users', 'Failed to send message', err, message) 43 | } 44 | } catch (err) { 45 | await logError('Users', 'Failed to run command', err, message) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/processes/activityChanger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Functionality relating to changing the bot's activity in the sidebar. 4 | */ 5 | 6 | import { log, logError } from '../utils/log' 7 | 8 | const { activities, prefix } = require('../utils/config').default 9 | 10 | // Changes the status of the bot to the specified activities on an interval based on the config value 'statusInterval'. 11 | export const initActivityChanger = async client => { 12 | // Initially set the status message to `${prefix}help`. 13 | try { 14 | await client.user.setActivity(`${prefix}help`) 15 | } catch (err) { 16 | await logError('Activity', `Failed to set activity to ${prefix}help:`, err) 17 | } 18 | // Set an interval to run a function every 5 minutes (5 mins * 60 secs * 1000 ms). 19 | setInterval(async () => { 20 | try { 21 | // Choose a random activity from the activities file. 22 | const activity = activities[Math.floor(Math.random() * activities.length)] 23 | 24 | // Wait for the activity to be set. 25 | await client.user.setActivity(activity) 26 | } catch (err) { 27 | await logError('Activity', 'Failed to set activity', err) 28 | } 29 | 30 | // Set a timeout to switch the activity back to '.help' after 1 minute. (60 secs * 1000 ms). 31 | setTimeout(async () => { 32 | try { 33 | // Set the activity back to '.help'. 34 | await client.user.setActivity(`${prefix}help`) 35 | } catch (err) { 36 | await logError('Activity', `Failed to set activity back to ${prefix}help:`, err) 37 | } 38 | }, 60 * 1000) 39 | }, 5 * 60 * 1000) 40 | log('Init', 'Activity changer initialized!') 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Manual 2 | .cache 3 | dist 4 | devmod.db 5 | devmod.config.js 6 | 7 | # Created by .ignore support plugin (hsz.mobi) 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | Icon 12 | ._* 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | .AppleDB 21 | .AppleDesktop 22 | Network Trash Folder 23 | Temporary Items 24 | .apdisk 25 | Thumbs.db 26 | ehthumbs.db 27 | ehthumbs_vista.db 28 | *.stackdump 29 | [Dd]esktop.ini 30 | $RECYCLE.BIN/ 31 | *.cab 32 | *.msi 33 | *.msix 34 | *.msm 35 | *.msp 36 | *.lnk 37 | .idea/**/workspace.xml 38 | .idea/**/tasks.xml 39 | .idea/**/dictionaries 40 | .idea/**/shelf 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | .idea/**/gradle.xml 49 | .idea/**/libraries 50 | cmake-build-debug/ 51 | cmake-build-release/ 52 | .idea/**/mongoSettings.xml 53 | *.iws 54 | out/ 55 | .idea_modules/ 56 | atlassian-ide-plugin.xml 57 | .idea/replstate.xml 58 | com_crashlytics_export_strings.xml 59 | crashlytics.properties 60 | crashlytics-build.properties 61 | fabric.properties 62 | .idea/httpRequests 63 | *~ 64 | .fuse_hidden* 65 | .directory 66 | .Trash-* 67 | .nfs* 68 | logs 69 | *.log 70 | npm-debug.log* 71 | yarn-debug.log* 72 | yarn-error.log* 73 | pids 74 | *.pid 75 | *.seed 76 | *.pid.lock 77 | lib-cov 78 | coverage 79 | .nyc_output 80 | .grunt 81 | bower_components 82 | .lock-wscript 83 | build/Release 84 | node_modules/ 85 | jspm_packages/ 86 | typings/ 87 | .npm 88 | .eslintcache 89 | .node_repl_history 90 | *.tgz 91 | .yarn-integrity 92 | .env 93 | .next 94 | -------------------------------------------------------------------------------- /config/approvedRoles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * List and gather all roles approved for use with automatic role assignment. 4 | */ 5 | 6 | // For each category of roles, make an object with a unique ID and name, a message, and an object consisting of 7 | // {role: {name, emoji}} groups. 8 | 9 | // const generalRoles = { 10 | // id: 'general', 11 | // name: 'General Roles', 12 | // message: 'These are some general roles that we have for you to add.', 13 | // roles: { 14 | // developer: { name: 'Developer', emoji: '💻' }, 15 | // challenges: { name: 'Challenges', emoji: '👌' } 16 | // } 17 | // } 18 | 19 | const helpRoles = { 20 | id: 'help', 21 | name: 'Help Roles', 22 | message: 'These are **pingable roles** that you can add to yourself to say that you\'re able and willing to help in the specified area.', 23 | roles: { 24 | helper: { name: 'Helper', emoji: '🚁' }, 25 | 'help-javascript': { name: 'JavaScript Helper', emoji: '🖥' }, 26 | 'help-frontend': { name: 'Frontend Helper', emoji: '📰' }, 27 | 'help-design': { name: 'Design Helper', emoji: '📱' }, 28 | 'help-ux': { name: 'UX Helper', emoji: '⚙' }, 29 | 'help-php': { name: 'PHP Helper', emoji: '🐘' }, 30 | 'help-python': { name: 'Python Helper', emoji: '🐍' }, 31 | 'help-java': { name: 'Java Helper', emoji: '☕' }, 32 | 'help-ruby': { name: 'Ruby Helper', emoji: '💎' }, 33 | 'help-rust': { name: 'Rust Helper', emoji: '⚓' }, 34 | 'help-cpp': { name: 'C++ Helper', emoji: '💪' }, 35 | 'help-csharp': { name: 'C# Helper', emoji: '🎼' }, 36 | 'help-devops': { name: 'DevOps Helper', emoji: '🚄' } 37 | } 38 | } 39 | 40 | // Export a list of all available roles. 41 | export const approvedRoles = [ 42 | // generalRoles, 43 | helpRoles 44 | ] 45 | -------------------------------------------------------------------------------- /src/commands/unlock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that unlocks a channel. 4 | */ 5 | 6 | import { green } from '../utils/colours' 7 | import { logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | const { roles: { verified } } = require('../utils/config').default 11 | 12 | // Export an object with command info and the function to execute. 13 | export const unlockCommand = { 14 | name: 'Unlock', 15 | aliases: ['unlock'], 16 | category: 'utils', 17 | description: 'Unlocks the current channel.', 18 | permissions: ['MANAGE_CHANNELS'], 19 | exec: async (args, message) => { 20 | try { 21 | try { 22 | // Remove the user's message. 23 | await message.delete() 24 | } catch (err) { 25 | await logError('Unlock', 'Failed to delete message', err, message) 26 | } 27 | 28 | const channel = message.channel 29 | const guild = message.guild 30 | const verifiedRole = guild.roles.cache.find(r => r.name === verified) 31 | 32 | for (const role of [verifiedRole, guild.defaultRole]) { 33 | await channel.overwritePermissions(role, { 34 | SEND_MESSAGES: null 35 | }, 'Unlocking the channel') 36 | } 37 | 38 | try { 39 | // Send the lock embed. 40 | return message.channel.send({ 41 | embed: { 42 | title: 'Channel Unlocked', 43 | color: green, 44 | author: getAuthor(message.member), 45 | timestamp: new Date() 46 | } 47 | }) 48 | } catch (err) { 49 | await logError('Lock', 'Failed to send message.', err, message) 50 | } 51 | } catch (err) { 52 | await logError('Lock', 'Failed to run command', err, message) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/verify/verify.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Verification bot. 4 | */ 5 | 6 | import { log, logError } from '../log' 7 | import { initInfoReactionListener } from '../../processes/infoReactionListener' 8 | import { testChannelsAndRoles } from '../testChannelsAndRoles' 9 | import discord from 'discord.js' 10 | 11 | const { botToken } = require('../config').default 12 | 13 | const main = async () => { 14 | try { 15 | // If an unhandled rejection occurs, log it and exit the program. 16 | process.on('unhandledRejection', async err => { 17 | await logError('Verify', 'Unhandled Rejection', err) 18 | }) 19 | // Same thing but for uncaught exception. 20 | process.on('uncaughtException', async err => { 21 | await logError('Verify', 'Uncaught Exception', err) 22 | }) 23 | } catch (err) { 24 | await logError('Verify', 'Failed to add process error listeners', err) 25 | } 26 | 27 | try { 28 | // Initialize the client. 29 | const client = new discord.Client() 30 | 31 | // Set a listener for the ready event to log that the bot is ready. 32 | client.on('ready', () => { 33 | log('Init', `Logged in as ${client.user.tag}.`) 34 | }) 35 | 36 | // Log the bot in. 37 | try { 38 | await client.login(botToken) 39 | } catch (err) { 40 | await logError('Init', 'Bot failed to log in', err) 41 | } 42 | 43 | // Test that all the channels and roles specified in the config exist. 44 | // noinspection ES6MissingAwait 45 | testChannelsAndRoles(client) 46 | 47 | await initInfoReactionListener(client) 48 | } catch (err) { 49 | await logError('Verify', 'Something has failed', err) 50 | } 51 | } 52 | 53 | // Run the main function. 54 | main().then(() => log('Verify', 'Bot Initialized!')) 55 | -------------------------------------------------------------------------------- /src/commands/lock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that locks a channel. 4 | */ 5 | 6 | import { red } from '../utils/colours' 7 | import { logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | const { roles: { verified } } = require('../utils/config').default 11 | 12 | // Export an object with command info and the function to execute. 13 | export const lockCommand = { 14 | name: 'Lock', 15 | aliases: ['lock'], 16 | category: 'utils', 17 | description: 'Locks the current channel.', 18 | permissions: ['MANAGE_CHANNELS'], 19 | exec: async (args, message) => { 20 | try { 21 | try { 22 | // Remove the user's message. 23 | await message.delete() 24 | } catch (err) { 25 | await logError('Lock', 'Failed to delete message', err, message) 26 | } 27 | 28 | const channel = message.channel 29 | const guild = message.guild 30 | const verifiedRole = guild.roles.cache.find(r => r.name === verified) 31 | 32 | // BUG: Overwrite Permissions neds to be an array or permission 33 | for (const role of [verifiedRole, guild.defaultRole]) { 34 | await channel.overwritePermissions(role, { 35 | SEND_MESSAGES: false 36 | }, 'Locking the channel') 37 | } 38 | 39 | try { 40 | // Send the lock embed. 41 | return message.channel.send({ 42 | embed: { 43 | title: 'Channel Locked', 44 | color: red, 45 | author: getAuthor(message.member), 46 | timestamp: new Date() 47 | } 48 | }) 49 | } catch (err) { 50 | await logError('Lock', 'Failed to send message.', err, message) 51 | } 52 | } catch (err) { 53 | await logError('Lock', 'Failed to run command', err, message) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/devmod.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Main function to initialize the bot's operation. 4 | */ 5 | 6 | import discord from 'discord.js' 7 | 8 | import { log, logError } from './utils/log' 9 | import { initCommandListener } from './processes/commandListener' 10 | import { initActivityChanger } from './processes/activityChanger' 11 | import { initReactionListener } from './processes/roleReactionListener' 12 | import { initTorielsAntiBotCrusade } from './processes/torielsAntiBotCrusade' 13 | import { initThanksListener } from './processes/thanksListener' 14 | import { testChannelsAndRoles } from './utils/testChannelsAndRoles' 15 | 16 | const { botToken } = require('./utils/config').default 17 | 18 | export const devmod = async () => { 19 | // Initialize the client. 20 | const client = new discord.Client() 21 | 22 | // Set a listener for the ready event to log that the bot is ready. 23 | client.on('ready', () => { 24 | log('Init', `Logged in as ${client.user.tag}.`) 25 | }) 26 | 27 | // Log the bot in. 28 | try { 29 | await client.login(botToken) 30 | } catch (err) { 31 | await logError('Init', 'Bot failed to log in', err) 32 | } 33 | 34 | // Test that all the channels and roles specified in the config exist. 35 | // noinspection ES6MissingAwait 36 | testChannelsAndRoles(client) 37 | 38 | // Save all the processes to an array. 39 | const processes = [ 40 | initCommandListener, 41 | initReactionListener, 42 | initActivityChanger, 43 | initTorielsAntiBotCrusade, 44 | initThanksListener 45 | ] 46 | 47 | // For each process, run it asynchronously. 48 | for (const process of processes) { 49 | try { 50 | // noinspection ES6MissingAwait 51 | process(client) 52 | } catch (err) { 53 | await logError('Init', `Failed to initialize process ${process.name}`, err) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/prune.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that prunes the last x messages from the channel. 4 | * TODO: fix 5 | */ 6 | 7 | // Export an object with command info and the function to execute. 8 | import { logError } from '../utils/log' 9 | 10 | export const pruneCommand = { 11 | name: 'Prune', 12 | aliases: ['prune', 'purge'], 13 | category: 'utils', 14 | description: 'Removes specified number of messages from the channel.', 15 | permissions: ['MANAGE_MESSAGES'], 16 | usage: '[]', 17 | exec: async (args, message) => { 18 | try { 19 | // Save the amount arg. If it doesn't exist, default to 5. 20 | const amount = args.length > 0 21 | ? isNaN(parseInt(args[0])) 22 | ? 0 23 | : parseInt(args[0]) 24 | : 5 25 | 26 | try { 27 | // Remove the user's message. 28 | await message.delete() 29 | } catch (err) { 30 | await logError('Prune', 'Failed to delete message', err, message) 31 | } 32 | 33 | try { 34 | // Limit amount of messages to delete to 50. 35 | const actualAmount = amount > 50 ? 50 : amount 36 | 37 | // Fetch the last 'amount' of messages form the current channel. 38 | const messages = await message.channel.messages.fetch({ limit: actualAmount }) 39 | 40 | try { 41 | // Delete all of the messages selected with the previous 42 | // command. 43 | // TEMP FIX: Itterate over messages and remove. 44 | messages.array().forEach(m => { 45 | m.delete() 46 | }) 47 | // await messages.clear() 48 | } catch (err) { 49 | await logError('Prune', 'Failed to delete messages', err, message) 50 | } 51 | } catch (err) { 52 | await logError('Prune', 'Failed to fetch messages', err, message) 53 | } 54 | } catch (err) { 55 | await logError('Prune', 'Failed to run command', err, message) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/about.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends some info about the bot. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | // Export an object with command info and the function to execute. 11 | export const aboutCommand = { 12 | name: 'About', 13 | aliases: ['about', 'info'], 14 | category: 'utils', 15 | description: 'Tells a little bit about the bot.', 16 | permissions: ['SEND_MESSAGES'], 17 | exec: async (args, message) => { 18 | try { 19 | try { 20 | // Remove the user's message. 21 | await message.delete() 22 | } catch (err) { 23 | await logError('About', 'Failed to delete message', err, message) 24 | } 25 | 26 | try { 27 | // Send the about message embed. 28 | // noinspection JSUnresolvedFunction 29 | return message.channel.send({ 30 | embed: { 31 | title: 'devmod - about the bot', 32 | color: blue, 33 | url: 'https://github.com/redxtech/devmod', 34 | description: 'devmod is a bot made for the DevCord community, but is applicable to any server that needs ' + 35 | 'moderating. It is written with discord.js. To use it on your own server, follow the steps in the ' + 36 | 'GitHub repo.', 37 | fields: [ 38 | { 39 | name: 'Author:', 40 | value: '<@170451883134156800>', 41 | inline: true 42 | }, 43 | { 44 | name: 'GitHub Repo:', 45 | value: 'https://github.com/redxtech/devmod', 46 | inline: true 47 | } 48 | ], 49 | author: getAuthor(message.member), 50 | timestamp: new Date() 51 | } 52 | }) 53 | } catch (err) { 54 | await logError('About', 'Failed to send message', err, message) 55 | } 56 | } catch (err) { 57 | await logError('About', 'Failed to run command', err, message) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/ping.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that shows ping and round trip time for the bot. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | // Export an object with command info and the function to execute. 11 | export const pingCommand = { 12 | name: 'Ping', 13 | aliases: ['ping'], 14 | category: 'utils', 15 | description: 'Shows ping and round trip time for the bot.', 16 | permissions: ['SEND_MESSAGES'], 17 | exec: async (args, message) => { 18 | try { 19 | // Create the initial embed to send. 20 | const embed = { 21 | title: 'Pong!', 22 | color: blue, 23 | author: getAuthor(message.member), 24 | fields: [{ 25 | name: 'Ping:', 26 | value: `${Math.round(message.client.ping)}ms.` 27 | }] 28 | } 29 | 30 | try { 31 | // noinspection JSUnresolvedFunction,JSCheckFunctionSignatures 32 | const sent = await message.channel.send({ embed }) 33 | try { 34 | // Calculate difference in time between when message was send & when it was edited. 35 | const timeDiff = (sent.createdAt) - (message.createdAt) 36 | // Create fields for the embed. 37 | embed.fields.push({ 38 | name: 'Round Trip Time:', 39 | value: `${timeDiff}ms.` 40 | }) 41 | // Edit the message. 42 | return await sent.edit({ embed }) 43 | } catch (err) { 44 | await logError('Ping', 'Error updating message', err, message) 45 | return await new Promise(resolve => resolve) 46 | } 47 | } catch (err) { 48 | await logError('Ping', 'Failed to send message', err, message) 49 | } 50 | try { 51 | // Remove the user's message. 52 | await message.delete() 53 | } catch (err) { 54 | await logError('Ping', 'Failed to delete message', err, message) 55 | } 56 | } catch (err) { 57 | await logError('Ping', 'Failed to run command', err, message) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Object containing all commands and their functions. 4 | */ 5 | 6 | import { expandCommands } from '../utils/expandCommands' 7 | 8 | import { aboutCommand } from './about' 9 | import { banCommand } from './ban' 10 | import { buildInfoCommand } from './buildInfo' 11 | import { buildRolesCommand } from './buildRoles' 12 | import { clearWarnsCommand } from './clearWarns' 13 | import { helpCommand } from './help' 14 | import { killCommand } from './kill' 15 | import { lmgtfyCommand } from './lmgtfy' 16 | import { lockCommand } from './lock' 17 | import { mdnCommand } from './mdn' 18 | import { moveCommand } from './move' 19 | import { muteCommand } from './mute' 20 | import { pingCommand } from './ping' 21 | import { pruneCommand } from './prune' 22 | import { reportCommand } from './report' 23 | import { reputationCommand } from './reputation' 24 | import { roleCommand } from './role' 25 | import { rolesCommand } from './roles' 26 | import { statsCommand } from './stats' 27 | import { tagCommand } from './tag' 28 | import { tagsCommand } from './tags' 29 | import { unbanCommand } from './unban' 30 | import { unlockCommand } from './unlock' 31 | import { unmuteCommand } from './unmute' 32 | import { usersCommand } from './users' 33 | import { warnCommand } from './warn' 34 | import { warnsCommand } from './warns' 35 | import { yeetCommand } from './yeet' 36 | 37 | // Export an array with all of the commands. 38 | export const commandsArray = [ 39 | aboutCommand, 40 | banCommand, 41 | buildInfoCommand, 42 | buildRolesCommand, 43 | clearWarnsCommand, 44 | helpCommand, 45 | killCommand, 46 | lmgtfyCommand, 47 | lockCommand, 48 | mdnCommand, 49 | moveCommand, 50 | muteCommand, 51 | pingCommand, 52 | pruneCommand, 53 | reportCommand, 54 | reputationCommand, 55 | roleCommand, 56 | rolesCommand, 57 | statsCommand, 58 | tagCommand, 59 | tagsCommand, 60 | unbanCommand, 61 | unlockCommand, 62 | unmuteCommand, 63 | usersCommand, 64 | warnCommand, 65 | warnsCommand, 66 | yeetCommand 67 | ] 68 | 69 | // Export an array with all of the commands' aliases as keys. 70 | export const commands = expandCommands(commandsArray) 71 | -------------------------------------------------------------------------------- /src/commands/tag.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends a specified tag. 4 | */ 5 | 6 | import { sendErrorMessage } from '../utils/sendErrorMessage' 7 | import { logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | const { tags } = require('../utils/config').default 11 | 12 | // Export an object with command info and the function to execute. 13 | export const tagCommand = { 14 | name: 'Tag', 15 | aliases: ['tag', 't'], 16 | category: 'utils', 17 | description: 'Sends a specified tag.', 18 | permissions: ['SEND_MESSAGES'], 19 | usage: ' [ { 21 | try { 22 | // If a tag isn't specified send an error message and terminate the command. 23 | if (args.length < 1) { 24 | return await sendErrorMessage('Command Not Specified', 'You didn\'t specify whether to add or remove a role.', message) 25 | } 26 | 27 | // Save the tag. 28 | const tag = args[0] 29 | 30 | // If a member is specified, save it. Otherwise return undefined. 31 | const taggedMember = message.mentions.members.first() 32 | 33 | // If the tag doesn't exist, send an error message and terminate the command. 34 | if (!Object.prototype.hasOwnProperty.call(tags, tag)) { 35 | return await sendErrorMessage('Tag Not Found', 'No tag with that name exists.', message) 36 | } 37 | 38 | // Save the embed from the tags. 39 | const embed = tags[tag] 40 | 41 | // Add the author & timestamp to the embed. 42 | embed.author = getAuthor(message.member) 43 | embed.timestamp = new Date() 44 | 45 | try { 46 | // Remove the user's message. 47 | await message.delete() 48 | } catch (err) { 49 | await logError('Tag', 'Failed to delete message', err, message) 50 | } 51 | 52 | try { 53 | // noinspection JSUnresolvedFunction 54 | return message.channel.send(taggedMember, { embed }) 55 | } catch (err) { 56 | await logError('Tag', 'Failed to send message', err, message) 57 | } 58 | } catch (err) { 59 | await logError('Tag', 'Failed to run command', err, message) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/stats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends some stats about the bot and server. 4 | */ 5 | 6 | import moment from 'moment' 7 | 8 | import { blue } from '../utils/colours' 9 | import { logError } from '../utils/log' 10 | import { getAuthor } from '../utils/user' 11 | 12 | // Export an object with command info and the function to execute. 13 | export const statsCommand = { 14 | name: 'Stats', 15 | aliases: ['stats', 'statistics'], 16 | category: 'utils', 17 | description: 'Sends some stats about the bot and server.', 18 | permissions: ['SEND_MESSAGES'], 19 | exec: async (args, message) => { 20 | try { 21 | // Save some info about the server and bot. 22 | const guild = message.guild 23 | const client = message.client 24 | const uptime = moment.duration(client.uptime) 25 | 26 | try { 27 | // Remove the user's message. 28 | await message.delete() 29 | } catch (err) { 30 | await logError('Stats', 'Failed to delete message', err, message) 31 | } 32 | 33 | try { 34 | // Send the stats message. 35 | // noinspection JSUnresolvedFunction,JSCheckFunctionSignatures 36 | return message.channel.send({ 37 | embed: { 38 | title: 'Server Stats', 39 | color: blue, 40 | author: getAuthor(message.member), 41 | fields: [ 42 | { 43 | name: `${guild.name}:`, 44 | value: `Members: ${guild.memberCount}\n` + 45 | `Server was created at: ${moment(guild.createdAt).format('YYYY/M/D')}\n` + 46 | `Num. of channels: ${guild.channels.cache.array().filter(channel => channel.type !== 'category').length}\n` + 47 | `Region: ${guild.region}\n` + 48 | `AFK Timeout: ${guild.afkTimeout}s\n` 49 | }, 50 | { 51 | name: 'Bot Information:', 52 | value: `Uptime: ${uptime.hours()} hours, ${uptime.minutes()} mins, ${uptime.seconds()}s` 53 | } 54 | ] 55 | } 56 | }) 57 | } catch (err) { 58 | await logError('Stat', 'Failed to send message', err, message) 59 | } 60 | } catch (err) { 61 | await logError('Stats', 'Failed to run command', err, message) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/processes/torielsAntiBotCrusade.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Process to catch spam bots. Adapted from https://gist.github.com/Dracovian/cb923c2b2fff7bad07ff7da4cdb6a3f8 4 | */ 5 | 6 | // Export a function to initialize the anti bot process. 7 | import { orange } from '../utils/colours' 8 | import { log, logError } from '../utils/log' 9 | 10 | const { channels: { crusade } } = require('../utils/config').default 11 | 12 | export const initTorielsAntiBotCrusade = async client => { 13 | try { 14 | // Make a list of spam bot urls. 15 | const spamBotUrls = [ 16 | 'privatepage.vip', 17 | 'nakedphotos.club' 18 | ] 19 | 20 | // On each message check to see if it contains the link. 21 | client.on('message', async message => { 22 | try { 23 | // If the message content includes one of the spam bot urls. 24 | if (spamBotUrls.some(spam => message.content.includes(spam))) { 25 | // If the message is from a bot, a staff/trusted member, ignore it. 26 | if (!message.author.bot && !message.member.roles.some(role => ['Staff', 'MVP'].includes(role.name))) { 27 | try { 28 | // Send a message to the reports channel detailing the removal. 29 | await client.channels.find(c => c.name === crusade).send({ 30 | embed: { 31 | color: orange, 32 | author: { 33 | name: `${client.user.username}'s (${client.user.tag})`, 34 | icon_url: client.user.avatarURL() 35 | }, 36 | fields: [{ 37 | name: `**Deleted message from ${message.author.username}#${message.author.discriminator}** *(ID ${message.author.id})*`, 38 | value: `**Message:** ${message.content}`, 39 | inline: false 40 | }] 41 | } 42 | }) 43 | // Delete the message. 44 | await message.delete() 45 | } catch (err) { 46 | await logError('AntiBot', `Failed to delete spam message from ${message.author.username}`, err) 47 | } 48 | } 49 | } 50 | } catch (err) { 51 | await logError('AntiBot', 'Failed to handle the message', err) 52 | } 53 | }) 54 | log('Init', 'Toriel\'s anti-bot crusade initialized!') 55 | } catch (err) { 56 | await logError('AntiBot', 'Failed to initialize the anti bot crusade', err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | > This overview assumes that all config options are set to their defaults. 3 | 4 | ## Commands 5 | > You can run commands with `.`. Each one has a page describing it's usage. 6 | - [About](./about.md) 7 | - [Ban](./ban.md) 8 | - [Build Info](./buildinfo.md) 9 | - [Build Roles](./buildroles.md) 10 | - [Clear Warns](./clearwarns.md) 11 | - [Help](./help.md) 12 | - [LMGTFY](./lmgtfy.md) 13 | - [Lock](./lock.md) 14 | - [MDN](./mdn.md) 15 | - [Move](./move.md) 16 | - [Mute](./mute.md) 17 | - [Ping](./ping.md) 18 | - [Prune](./prune.md) 19 | - [Report](./report.md) 20 | - [Reputation](./reputation.md) 21 | - [Role](./role.md) 22 | - [Roles](./roles.md) 23 | - [Stats](./stats.md) 24 | - [Tag](./tag.md) 25 | - [Tags](./tags.md) 26 | - [Unban](./unban.md) 27 | - [Unlock](./unlock.md) 28 | - [Unmute](./unmute.md) 29 | - [Users](./users.md) 30 | - [Warn](./warn.md) 31 | - [Warns](./warns.md) 32 | 33 | ## Processes 34 | > Each of these processes is run as an `async` function when the bot starts up. 35 | 36 | ### Activity Changer 37 | Every five minutes, the bot's status is set to a random item from the `activities` 38 | element of the config object. After one minute, it's set back to `.help`. 39 | 40 | ### Command Listener 41 | Every time a message is sent, if it isn't a dm or sent by a bot and begins with `.`, 42 | the command is checked against the commands object from `src/commands/index.js`, and 43 | if it exists, the user is checked to make sure it has the proper permissions, and if 44 | so, the command is run with the arguments passed in. 45 | 46 | ### Info Reaction Listener 47 | This listens for reactions to the info message, and if a user reacts with the checkmark, 48 | the verified role is added to them, and removed if they remove the reaction. 49 | 50 | ### Role Reaction Listener 51 | This listens to reactions to the role messages and if they match up with approved roles, 52 | the role is added to the user, and removed if they remove the reaction. 53 | 54 | ### Thanks Listener 55 | For every message, if it isn't a dm or sent by a bot, and if it contains the words `['thank', 'kudos']`, 56 | the user who was thanked (tagged) gets a reputation point added to them. 57 | 58 | ### Toriel's Anti Bot Crusade 59 | Adapted from https://gist.github.com/Dracovian/cb923c2b2fff7bad07ff7da4cdb6a3f8 60 | 61 | If a message contains a blacklisted URL, remove it and log it. 62 | 63 | ## Author 64 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](../LICENSE.md) License. 65 | -------------------------------------------------------------------------------- /src/processes/commandListener.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Functionality relating to listening to messages and running commands. 4 | */ 5 | 6 | import { 7 | commands 8 | } from '../commands' 9 | import { 10 | sendErrorMessage 11 | } from '../utils/sendErrorMessage' 12 | import { 13 | log, 14 | logError 15 | } from '../utils/log' 16 | 17 | const { 18 | prefix 19 | } = require('../utils/config').default 20 | 21 | export const initCommandListener = async client => { 22 | try { 23 | // For each message run a function. 24 | client.on('message', async msg => { 25 | // If the message isn't a dm, the first character is the prefix, and the author isn't a bot, continue. 26 | if (msg.channel.type !== 'dm' && msg.content[0] === prefix && !msg.author.bot) { 27 | // Separate the entire command after the prefix into args. 28 | const args = msg.content.substr(1).split(' ') 29 | 30 | // Set the command to the first argument and remove it from the args array. 31 | const command = args.shift() 32 | 33 | // If the command exists, test for permissions and run the command function. 34 | if (Object.prototype.hasOwnProperty.call(commands, command)) { 35 | // Save the command to a variable. 36 | const cmd = commands[command] 37 | try { 38 | // Test that the users has the proper permissions to run the command. 39 | if (msg.member.permissions.has(cmd.permissions)) { 40 | try { 41 | // Run the command. 42 | cmd.exec(args, msg) 43 | } catch (err) { 44 | await logError('CommandListener', 'Failed to execute command', err) 45 | } 46 | } else { 47 | try { 48 | // Send error message. 49 | return await sendErrorMessage( 50 | 'Insufficient Permissions', 51 | 'You do not have permission to use that command.', 52 | msg 53 | ) 54 | } catch (err) { 55 | await logError('CommandListener', 'Failed to send error message', err) 56 | } 57 | } 58 | } catch (err) { 59 | await logError('CommandListener', 'Failed to test for permissions', err) 60 | } 61 | } 62 | } 63 | }) 64 | log('Init', 'Command listener initialized!') 65 | } catch (err) { 66 | await logError('CommandListener', 'Failed to initialize the command listener', err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/mdn.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that queries MDN. 4 | */ 5 | 6 | import https from 'https' 7 | 8 | import { blue } from '../utils/colours' 9 | import { logError } from '../utils/log' 10 | import { getAuthor } from '../utils/user' 11 | import { sendErrorMessage } from '../utils/sendErrorMessage' 12 | 13 | // Function to query mdn and return the result 14 | const queryMDN = query => new Promise((resolve, reject) => { 15 | https.get( 16 | `https://developer.mozilla.org/api/v1/${query}`, 17 | 18 | res => { 19 | const chunks = [] 20 | 21 | res.on('data', chunk => chunks.push(chunk)) 22 | 23 | res.on('end', () => { 24 | try { 25 | resolve(JSON.parse(chunks.join(''))) 26 | } catch (error) { 27 | reject(error) 28 | } 29 | }) 30 | } 31 | ).on('error', reject) 32 | }) 33 | 34 | // Export an object with command info and the function to execute. 35 | export const mdnCommand = { 36 | name: 'MDN', 37 | aliases: ['mdn'], 38 | category: 'utils', 39 | description: 'Searches MDN and returns the first result.', 40 | permissions: ['SEND_MESSAGES'], 41 | 42 | exec: async (args, message) => { 43 | // If a query isn't specified send an error message and terminate the command. 44 | if (args.length < 1) { 45 | return sendErrorMessage('Query Not Specified', 'You need to specify a query.', message) 46 | } 47 | 48 | const query = encodeURIComponent(args.join(' ')) 49 | 50 | try { 51 | // Query the MDN search API 52 | const { documents } = await queryMDN(`search/en-US?q=${query}&highlight=false`) 53 | const [result] = documents 54 | 55 | try { 56 | // Remove the user's message. 57 | await message.delete() 58 | } catch (err) { 59 | await logError('MDN', 'Failed to delete message', err, message) 60 | } 61 | 62 | try { 63 | // Send the MDN result. 64 | // noinspection JSUnresolvedFunction 65 | return message.channel.send({ 66 | embed: { 67 | title: result.title, 68 | color: blue, 69 | description: 70 | `...${result.excerpt}... 71 | 72 | [Search on MDN](https://developer.mozilla.org/en-US/search?q=${query})`, 73 | author: getAuthor(message.member), 74 | url: `https://developer.mozilla.org/en-US/${result.slug}` 75 | } 76 | }) 77 | } catch (err) { 78 | await logError('MDN', 'Failed to send message', err, message) 79 | } 80 | } catch (err) { 81 | await logError('MDN', `Failed to search MDN for query "${query}"`, err, message) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/default.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Default configuration values. 4 | */ 5 | 6 | import { join } from 'path' 7 | 8 | export default { 9 | botToken: undefined, // Discord API token for the bot. 10 | guildID: undefined, // Discord ID of the server your bot is running on. 11 | prefix: '.', // Prefix for bot commands. 12 | msgDeleteTime: 30, // Amount of time in seconds to wait before deleting large help messages. 13 | dbFile: join(__dirname, '..', '..', 'devmod.db'), // Absolute path for the database file. 14 | autoBan: true, // Whether or not to enforce auto-banning after a specified number of warnings. 15 | autoBanWarns: 3, // Amount of warnings to warrant an auto-ban if enabled. 16 | banMsgDelete: 0, // Number of days of messages to delete when user is banned. 17 | thanks: ['thank', 'kudos'], // List of triggers for thanking users. 18 | repCoin: undefined, // The emoji to prefix the thanks received message with. 19 | channels: { 20 | warn: 'warnings', // Channel to forward all warning confirmation messages. 21 | ban: 'bans', // Channel to forward all ban confirmation messages. 22 | reports: 'reports', // Channel to forward all user report messages. 23 | roles: 'roles', // Channel to send and listen to reactions for roles. 24 | info: 'info', // Channel to send the info to. 25 | crusade: 'crusade', // Channel to send notifications that the anti bot crusade has deleted a message. 26 | errors: 'errors' // Channel to log errors to. 27 | }, 28 | roles: { 29 | muted: 'muted', // Name of the role to apply to muted users. 30 | verified: 'verified' // Name of the role to apply to verified users. 31 | }, 32 | activities: [ 33 | 'Counting your good boye points (rip)...', 34 | 'Trying to unban SWAT SEAL...', 35 | 'Spamming the DMs of a random user...', 36 | 'Compiling...', 37 | 'Having a snack.', 38 | 'uncaughtException', 39 | 'Shitposting in #offtopic.', 40 | 'Admiring egg.', 41 | 'Trying to exit vim...', 42 | 'BEING A HUMAN.', 43 | '10011001101111100000011011110', 44 | 'Hacking the FBI...', 45 | 'Serving NaN users.', 46 | 'on a Christian Server.', 47 | 'et ur brokli', 48 | 'this chat gets weird fasy', 49 | 'https://i.imgur.com/BVRHZzg.png', 50 | 'Complaining about the logo.', 51 | 'REEEEEEEEEEEEEEEEEEEEEE', 52 | 'Promoting VUE!' 53 | ], // List of activities for the bot to show as a status. 54 | tags: [], // List of tags for the .tag command. Each one is a discord embed object. Can be imported from a different file. 55 | approvedRoles: [] // List of lists of roles the reaction roles channel. Can be imported from a different file. 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/tags.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends a list of available tags. 4 | */ 5 | 6 | import { blue, red } from '../utils/colours' 7 | import { capitalize } from '../utils/capitalize' 8 | import { logError } from '../utils/log' 9 | import { getAuthor } from '../utils/user' 10 | 11 | const { msgDeleteTime, prefix, tags } = require('../utils/config').default 12 | 13 | // Export an object with command info and the function to execute. 14 | export const tagsCommand = { 15 | name: 'Tags', 16 | aliases: ['tags', 'taglist'], 17 | category: 'utils', 18 | description: 'Sends a list of tags.', 19 | permissions: ['SEND_MESSAGES'], 20 | exec: async (args, message) => { 21 | try { 22 | try { 23 | // Create the initial embed. 24 | const embed = { 25 | title: 'Available Tags', 26 | color: blue, 27 | author: getAuthor(message.member), 28 | fields: [], 29 | timestamp: new Date() 30 | } 31 | 32 | // For each tag of tags, add the tag to the embed. 33 | for (const tag of Object.keys(tags)) { 34 | // Save the tag name. 35 | const name = tags[tag].title ? tags[tag].title : capitalize(tag) 36 | 37 | embed.fields.push({ 38 | name, 39 | value: `\`${prefix}tag ${tag} []\`` 40 | }) 41 | } 42 | 43 | // If there aren't any roles added, change the colour to red and make the embed say empty. 44 | if (embed.fields.length <= 0) { 45 | embed.color = red 46 | embed.fields = [ 47 | { 48 | name: 'No Tags', 49 | value: 'There are currently no tags specified for the bot.' 50 | } 51 | ] 52 | } 53 | 54 | // Save the message to a variable for later deletion. 55 | // noinspection JSCheckFunctionSignatures 56 | const sent = await message.channel.send({ embed }) 57 | 58 | try { 59 | // Remove the user's message. 60 | await message.delete() 61 | } catch (err) { 62 | await logError('Tags', 'Failed to delete message', err, message) 63 | } 64 | 65 | // Return a timeout that deletes the message after x seconds (x seconds * 1000 ms where x = msgDeleteTime). 66 | return setTimeout(async () => { 67 | try { 68 | // Delete the message. 69 | sent.delete(1) 70 | } catch (err) { 71 | await logError('Tags', 'Failed to delete command', err, message) 72 | } 73 | }, msgDeleteTime * 1000) 74 | } catch (err) { 75 | await logError('Tags', 'Failed to send message', err, message) 76 | } 77 | } catch (err) { 78 | await logError('Tags', 'Failed to run command', err, message) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/clearWarns.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that clears a user's warnings. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { clearWarnings } from '../db' 9 | import { logError } from '../utils/log' 10 | import { getAuthor, getName } from '../utils/user' 11 | 12 | // Export an object with command info and the function to execute. 13 | export const clearWarnsCommand = { 14 | name: 'Clear Warns', 15 | aliases: ['clearwarns', 'clearWarns', 'clear-warns', 'cWarns', 'cwarns', 'c-warns'], 16 | category: 'moderation', 17 | description: 'Clears a user\'s warnings.', 18 | permissions: ['KICK_MEMBERS'], 19 | usage: '', 20 | exec: async (args, message) => { 21 | try { 22 | // If a user isn't specified send an error message and terminate the command. 23 | if (args.length < 1) { 24 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to clear their warnings.', message) 25 | } 26 | 27 | // Save the user object of the member to clear their warnings. 28 | // noinspection DuplicatedCode 29 | const member = message.mentions.members.first() 30 | 31 | // If the user doesn't exist send an error message and terminate the command. 32 | if (member === undefined) { 33 | return await sendErrorMessage('Not a User', 'The user you specified either doesn\'t exist or isn\'t a user.', message) 34 | } 35 | 36 | try { 37 | // Clear the warnings from the database. 38 | await clearWarnings(member.user.id) 39 | } catch (err) { 40 | await logError('ClearWarns', 'Failed to clear warnings', err, message) 41 | } 42 | 43 | try { 44 | // Remove the user's message. 45 | await message.delete() 46 | } catch (err) { 47 | await logError('ClearWarns', 'Failed to delete message', err, message) 48 | } 49 | 50 | // Save some info about the staff member. 51 | const staffMember = message.member 52 | 53 | try { 54 | // Send a confirmation message. 55 | // noinspection JSUnresolvedFunction,JSCheckFunctionSignatures 56 | return message.channel.send({ 57 | embed: { 58 | color: blue, 59 | title: 'Warnings Cleared', 60 | description: `${getName(member, member.id)}'s (${member.user.tag}'s) warnings have been cleared.`, 61 | author: getAuthor(staffMember), 62 | footer: { 63 | icon_url: member.user.avatarURL(), 64 | text: `${getName(member, member.id)}'s (${member.user.tag}'s) warnings have been cleared.` 65 | }, 66 | timestamp: new Date() 67 | } 68 | }) 69 | } catch (err) { 70 | await logError('ClearWarns', 'Failed to send message', err, message) 71 | } 72 | } catch (err) { 73 | await logError('ClearWarns', 'Failed to run command', err, message) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/lmgtfy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends a 'let me google that for you link'. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { logError } from '../utils/log' 9 | import { getAuthor } from '../utils/user' 10 | 11 | // Export an object with command info and the function to execute. 12 | export const lmgtfyCommand = { 13 | name: 'LMGTFY', 14 | aliases: ['lmgtfy'], 15 | category: 'fun', 16 | description: 'Sends a \'let me google that for you link\'.', 17 | permissions: ['SEND_MESSAGES'], 18 | usage: ' [ ]', 19 | exec: async (args, message) => { 20 | // Set up type and site options. 21 | const types = { 22 | '-w': 'w', // web 23 | '-i': 'i' // image 24 | } 25 | const sites = { 26 | '-g': 'g', // google 27 | '-y': 'y', // yahoo 28 | '-b': 'b', // bing 29 | '-k': 'k', // ask 30 | '-a': 'a', // aol 31 | '-d': 'd' // duckduckgo 32 | } 33 | try { 34 | // Set default options to web & google. 35 | let type = types['-w'] 36 | let site = sites['-g'] 37 | 38 | // If a query isn't specified, send an error message and terminate the command. 39 | if (args.length < 1) { 40 | return await sendErrorMessage( 41 | 'No Query Specified', 'You need to specify a query.', message 42 | ) 43 | } 44 | 45 | const query = args.filter(a => a[0] !== '-') 46 | const options = args.filter(args => args[0] === '-') 47 | 48 | // If the specified options exists, set them. 49 | for (const option of options) { 50 | if (Object.keys(sites).includes(option.toLowerCase())) { 51 | site = sites[option] 52 | } 53 | } 54 | 55 | // LMGTFY only supports image for google searches 56 | if (site === 'g') { 57 | for (const option of options) { 58 | if (Object.keys(types).includes(option.toLowerCase())) { 59 | type = types[option] 60 | } 61 | } 62 | } 63 | 64 | try { 65 | // Remove the user's message. 66 | await message.delete() 67 | } catch (err) { 68 | await logError('LMGTFY', 'Failed to delete message', err, message) 69 | } 70 | 71 | try { 72 | // Send a let me google that for you link in an embedded message. 73 | // noinspection JSUnresolvedFunction,JSCheckFunctionSignatures 74 | return message.channel.send({ 75 | embed: { 76 | title: query.join(' '), 77 | color: blue, 78 | url: `https://lmgtfy.com/?q=${query.join('+')}&s=${site}&t=${type}`, 79 | description: 'Here you go!', 80 | author: getAuthor(message.member) 81 | } 82 | }) 83 | } catch (err) { 84 | await logError('LMGTFY', 'Failed to send message', err, message) 85 | } 86 | } catch (err) { 87 | await logError('LMGTFY', 'Failed to run command', err, message) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/commands/unmute.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that unmutes a user. 4 | */ 5 | 6 | import { green } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { logError } from '../utils/log' 9 | import { getAuthor, getName } from '../utils/user' 10 | 11 | const { channels: { warn }, roles: { muted } } = require('../utils/config').default 12 | 13 | // Export an object with command info and the function to execute. 14 | export const unmuteCommand = { 15 | name: 'Unmute', 16 | aliases: ['unmute'], 17 | category: 'moderation', 18 | description: 'Remove a muted role from a user.', 19 | permissions: ['KICK_MEMBERS'], 20 | usage: '', 21 | exec: async (args, message) => { 22 | try { 23 | // If a user isn't specified send an error message and terminate the command. 24 | if (args.length < 1) { 25 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to unmute.', message) 26 | } 27 | 28 | // Save the user object of the member to be unmuted. 29 | const member = message.mentions.members.first() 30 | 31 | // If the user doesn't exist send an error message and terminate the command. 32 | if (member === undefined) { 33 | return await sendErrorMessage('Not a User', 'The user you specified either doesn\'t exist or isn\'t a user.', message) 34 | } 35 | 36 | // Save the server to a variable. 37 | const guild = message.guild 38 | 39 | // Fetch the muted role from the server. 40 | const mutedRole = guild.roles.cache.find(r => r.name === muted) 41 | 42 | // If the muted role doesn't exist, send an error message and terminate the command. 43 | if (mutedRole === undefined) { 44 | return await sendErrorMessage('Muted Role Doesn\'t Exist', 'The muted role specified in the config does not exist.', message) 45 | } 46 | 47 | try { 48 | // Remove the muted role from the member. 49 | await member.roles.remove(mutedRole) 50 | 51 | // Save the warnings channel. 52 | const channel = guild.channels.cache.find(c => c.name === warn) 53 | 54 | try { 55 | // Remove the user's message. 56 | await message.delete() 57 | } catch (err) { 58 | await logError('Unmute', 'Failed to delete message', err, message) 59 | } 60 | 61 | try { 62 | // Log the unmute to the warnings channel. 63 | // noinspection JSUnresolvedFunction 64 | return channel.send({ 65 | embed: { 66 | color: green, 67 | title: 'Unmute', 68 | description: `${getName(member)} (${member.user.tag} - ${member}) has been unmuted.`, 69 | author: getAuthor(message.member), 70 | timestamp: new Date() 71 | } 72 | }) 73 | } catch (err) { 74 | await logError('Unmute', 'Failed to send message', err, message) 75 | } 76 | } catch (err) { 77 | await logError('Unmute', 'Failed to remove role', err, message) 78 | } 79 | } catch (err) { 80 | await logError('Unmute', 'Failed to run command', err, message) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/mute.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that mutes a user. 4 | */ 5 | 6 | import { orange } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { logError } from '../utils/log' 9 | import { getAuthor, getName } from '../utils/user' 10 | 11 | const { channels: { warn }, roles: { muted } } = require('../utils/config').default 12 | 13 | // Export an object with command info and the function to execute. 14 | export const muteCommand = { 15 | name: 'Mute', 16 | aliases: ['mute', 'silence'], 17 | category: 'moderation', 18 | description: 'Applied a muted role to a user.', 19 | permissions: ['KICK_MEMBERS'], 20 | usage: '', 21 | exec: async (args, message) => { 22 | try { 23 | // If a user isn't specified send an error message and terminate the command. 24 | if (args.length < 1) { 25 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to mute.', message) 26 | } 27 | 28 | // Save the user object of the member to be muted. 29 | const member = message.mentions.members.first() 30 | 31 | // If the user doesn't exist send an error message and terminate the command. 32 | if (member === undefined) { 33 | return await sendErrorMessage('Not a User', 'The user you specified either doesn\'t exist or isn\'t a user.', message) 34 | } 35 | 36 | // Save the server to a variable. 37 | const guild = message.guild 38 | 39 | // Fetch the muted role from the server. 40 | const mutedRole = guild.roles.cache.find(r => r.name === muted) 41 | 42 | // If the muted role doesn't exist, send an error message and terminate the command. 43 | if (mutedRole === undefined) { 44 | return await sendErrorMessage('Muted Role Doesn\'t Exist', 'The muted role specified in the config does not exist.', message) 45 | } 46 | 47 | try { 48 | // Remove the user's message. 49 | await message.delete() 50 | } catch (err) { 51 | await logError('Mute', 'Failed to delete message', err, message) 52 | } 53 | 54 | try { 55 | // Add the muted role to the member. 56 | await member.roles.add(mutedRole) 57 | } catch (err) { 58 | await logError('Mute', 'Failed to add muted role', err, message) 59 | } 60 | 61 | // Save some info about the staff member. 62 | const staffMember = message.member 63 | 64 | // Save the warnings channel. 65 | const warnChannel = guild.channels.cache.find(c => c.name === warn) 66 | 67 | try { 68 | // Log the mute to the warnings channel. 69 | return warnChannel.send({ 70 | embed: { 71 | color: orange, 72 | title: 'Mute', 73 | description: `${getName(member, member.id)} (${member.user.tag} - ${member}) has been muted.`, 74 | author: getAuthor(staffMember), 75 | timestamp: new Date() 76 | } 77 | }) 78 | } catch (err) { 79 | await logError('Mute', 'Failed to send message', err, message) 80 | } 81 | } catch (err) { 82 | await logError('Mute', 'Failed to run command', err, message) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/commands/reputation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends the number of reputation points for either a user or the top users. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { getThanks, getTopThanks } from '../db' 9 | import { logError } from '../utils/log' 10 | import { getAuthor, getName } from '../utils/user' 11 | 12 | const { repCoin } = require('../utils/config').default 13 | 14 | // Export an object with command info and the function to execute. 15 | export const reputationCommand = { 16 | name: 'Reputation', 17 | aliases: ['reputation', 'reps', 'rep'], 18 | category: 'utils', 19 | description: 'Sends a count of reputation points for a user or show a leaderboard.', 20 | permissions: ['SEND_MESSAGES'], 21 | usage: '[]', 22 | exec: async (args, message) => { 23 | try { 24 | // Create the initial embed. 25 | const embed = { 26 | color: blue, 27 | author: getAuthor(message.member) 28 | } 29 | 30 | // If a user isn't specified send the leaderboard. 31 | if (args.length < 1) { 32 | // Pull current thanks count from the database. 33 | const topReputation = await getTopThanks() 34 | 35 | // Create the initial embed. 36 | embed.title = `${repCoin ? `${repCoin} ` : ''}Top ${topReputation.length} Thanked Users` 37 | 38 | // Save the server. 39 | const guild = message.guild 40 | 41 | // Map the array of users to each be a string and join it with 42 | // a new line. 43 | embed.description = topReputation 44 | .map((user, i) => `${i + 1}) **${getName(guild.members.cache.find(m => m.id === user[0]), user[0])}** has ${user[1].length} reputation.`) 45 | .join('\n') 46 | } else { 47 | // Save the user object of the member to show reputation for. 48 | const member = message.mentions.members.first() 49 | 50 | // If the user doesn't exist send an error message and terminate the command. 51 | if (member === undefined) { 52 | return await sendErrorMessage('Not a User', 'The user you specified either doesn\'t exist or isn\'t a user.', message) 53 | } 54 | 55 | // Pull current thanks count from the database. 56 | const reputation = (await getThanks(member.user.id)).length 57 | 58 | // Create the initial embed. 59 | embed.title = `${repCoin ? `${repCoin} ` : ''}${getName(member, member.id)} has ${reputation} reputation.` 60 | embed.footer = { 61 | text: 'Use "thanks @user" to give someone rep!' 62 | } 63 | } 64 | 65 | try { 66 | // Remove the user's message. 67 | await message.delete() 68 | } catch (err) { 69 | await logError('Reputation', 'Failed to delete message', err, message) 70 | } 71 | 72 | try { 73 | // Send the embed. 74 | return await message.channel.send({ embed }) 75 | } catch (err) { 76 | await logError('Reputation', 'Failed to send message', err, message) 77 | } 78 | } catch (err) { 79 | await logError('Reputation', 'Failed to run command', err, message) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/commands/roles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends a list of roles available to self-assign. 4 | */ 5 | 6 | import { blue, red } from '../utils/colours' 7 | import { logError } from '../utils/log' 8 | import { getAuthor } from '../utils/user' 9 | 10 | const { approvedRoles, msgDeleteTime, prefix } = require('../utils/config').default 11 | 12 | // Export an object with command info and the function to execute. 13 | export const rolesCommand = { 14 | name: 'Roles', 15 | aliases: ['roles', 'rolelist'], 16 | category: 'utils', 17 | description: 'Sends a list of roles.', 18 | permissions: ['SEND_MESSAGES'], 19 | exec: async (args, message) => { 20 | try { 21 | try { 22 | // Create the initial embed. 23 | const embed = { 24 | title: 'Available Roles', 25 | color: blue, 26 | author: getAuthor(message.member), 27 | fields: [], 28 | timestamp: new Date() 29 | } 30 | 31 | // For each roleGroup of approvedRoles, loop through the roleGroup and add each role to the embed. 32 | for (const roleGroup of approvedRoles) { 33 | // For each role in the roleGroup's roles property, push an entry to the embed with it's information. 34 | for (const role of Object.keys(roleGroup.roles)) { 35 | embed.fields.push({ 36 | name: `${roleGroup.roles[role].name} | ${roleGroup.roles[role].emoji}`, 37 | value: `\`${prefix}role add ${role}\` | \`${prefix}role rm ${role}\`` 38 | }) 39 | } 40 | } 41 | 42 | // If there aren't any roles added, change the colour to red and make the embed say empty. 43 | if (embed.fields.length <= 0) { 44 | embed.color = red 45 | embed.fields = [ 46 | { 47 | name: 'No (Approved) Roles', 48 | value: 'There are currently either no approved roles or no roles' + 49 | ' at all on this server.' 50 | } 51 | ] 52 | } 53 | 54 | try { 55 | // Save the message to a variable for later deletion. 56 | // noinspection JSCheckFunctionSignatures 57 | const sent = await message.channel.send({ embed }) 58 | 59 | try { 60 | // Remove the user's message. 61 | await message.delete() 62 | } catch (err) { 63 | await logError('Roles', 'Failed to delete message', err, message) 64 | } 65 | 66 | // Return a timeout that deletes the message after x seconds (x seconds * 1000 ms where x = msgDeleteTime). 67 | return setTimeout(async () => { 68 | try { 69 | // Delete the message. 70 | sent.delete(1) 71 | } catch (err) { 72 | await logError('Roles', 'Failed to delete message', err, message) 73 | } 74 | }, msgDeleteTime * 1000) 75 | } catch (err) { 76 | await logError('Roles', 'Failed to send message', err, message) 77 | } 78 | } catch (err) { 79 | await logError('Roles', 'Failed to send message', err, message) 80 | } 81 | } catch (err) { 82 | await logError('Roles', 'Failed to run command', err, message) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/commands/unban.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that unbans a member. 4 | */ 5 | 6 | import { sendErrorMessage } from '../utils/sendErrorMessage' 7 | import { green } from '../utils/colours' 8 | import { logError } from '../utils/log' 9 | import { getAuthor, getName } from '../utils/user' 10 | 11 | const { channels: { ban } } = require('../utils/config').default 12 | 13 | // Export an object with command info and the function to execute. 14 | export const unbanCommand = { 15 | name: 'Unban', 16 | aliases: ['unban'], 17 | category: 'moderation', 18 | description: 'Unbans a user.', 19 | permissions: ['BAN_MEMBERS'], 20 | usage: '', 21 | exec: async (args, message) => { 22 | try { 23 | // If there aren't any args, send an error message stating a member wasn't specified and terminate the command. 24 | if (args.length < 1) { 25 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to unban.', message) 26 | } 27 | 28 | const tagged = message.mentions.members.first() 29 | 30 | // Get the member tagged in the args. 31 | const member = tagged || args[0] 32 | 33 | // Save the server. 34 | const guild = message.guild 35 | 36 | // Grab the banned users from the guild. 37 | const bans = await guild.fetchBans(true) 38 | 39 | // Save the member who is to be unbanned. 40 | // noinspection JSUnresolvedVariable 41 | const memberToUnban = bans.find(m => `${m.user.username}#${m.user.discriminator}` === member || m.user.id === member) 42 | 43 | // If there isn't a banned member with that name, send an error message and terminate the command. 44 | if (memberToUnban === undefined) { 45 | return await sendErrorMessage('User Not Banned', 'The specified user either doesn\'t exist or isn\'t banned.', message) 46 | } 47 | 48 | // Save the banned reason. 49 | const reason = memberToUnban.reason 50 | 51 | // Save the ban channel. 52 | const channel = guild.channels.cache.find(c => c.name === ban) 53 | 54 | try { 55 | // Log the unban to the current channel. 56 | // noinspection JSCheckFunctionSignatures 57 | await channel.send({ 58 | embed: { 59 | color: green, 60 | title: 'Unban', 61 | description: `${getName(memberToUnban, memberToUnban.id)} has been unbanned.\nThey were previously banned for reason: ${reason}`, 62 | author: getAuthor(message.member), 63 | footer: { 64 | icon_url: memberToUnban.user.avatarURL(), 65 | text: `${getName(memberToUnban, memberToUnban.id)} has been unbanned.` 66 | }, 67 | timestamp: new Date() 68 | } 69 | }) 70 | } catch (err) { 71 | await logError('Unban', 'Failed to log unban', err, message) 72 | } 73 | 74 | try { 75 | // Remove the user's message. 76 | await message.delete() 77 | } catch (err) { 78 | await logError('Unban', 'Failed to delete message', err, message) 79 | } 80 | 81 | try { 82 | // Unban the user 83 | await guild.unban(memberToUnban.user, 'Unbanned by devmod.') 84 | } catch (err) { 85 | await logError('Unban', 'Failed to unban member', err, message) 86 | } 87 | } catch (err) { 88 | await logError('Unban', 'Failed to run command', err, message) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Function to make logging look good. 4 | */ 5 | 6 | import chalk from 'chalk' 7 | import discord from 'discord.js' 8 | import { red } from './colours' 9 | import { capitalize } from './capitalize' 10 | import { sendErrorMessage } from './sendErrorMessage' 11 | 12 | const { botToken, channels: { errors } } = require('./config').default 13 | 14 | // Given an area and a message, log a nice looking message to the console. 15 | export const log = (area, message) => { 16 | console.log(`${chalk.greenBright(`[${area}]`)} ${chalk.blue(message)}`) 17 | } 18 | 19 | // Given an area, message, and error, log a nice looking error to the console. 20 | export const logError = async (area, message, err, msg = false) => { 21 | // Log the error to the console. 22 | console.error(`${chalk.greenBright(`[${area}]`)} ${chalk.redBright(`${message}${err === false ? '' : ':'}`)}`, err === false ? '' : err) 23 | 24 | // Log the error to the error channel. 25 | logErrorToChannel(area, message, err) 26 | 27 | // If a msg was passed, try to add the failed reaction. 28 | if (msg) { 29 | try { 30 | // If the message wasn't deleted, add the reaction. 31 | // noinspection JSUnresolvedVariable 32 | if (!msg.deleted) { 33 | await msg.react('❌') 34 | } 35 | } catch (err) { 36 | console.error(`${chalk.greenBright('[Log]')} ${chalk.redBright('Failed to add failure reaction:')}`, err) 37 | } 38 | } 39 | } 40 | 41 | const logErrorToChannel = (area, message, err) => { 42 | try { 43 | // Create the discord client. 44 | const client = new discord.Client() 45 | 46 | // Add a listener to run when the client is ready. 47 | client.on('ready', async () => { 48 | // Grab the guild object. 49 | const guild = client.guilds.cache.first() 50 | 51 | // Save the errors channel 52 | const errorChannel = guild.channels.cache.find(c => c.name === errors) 53 | 54 | if (errorChannel === undefined) { 55 | return sendErrorMessage('No Error Channel', 'The errors channel either isn\'t set or doesn\'t exist.') 56 | } 57 | 58 | // Convert the error to an iterable object using a custom replacer function. 59 | const errorObject = JSON.parse(JSON.stringify(err, errorReplacer)) 60 | 61 | // Send the error message. 62 | await errorChannel.send({ 63 | embed: { 64 | title: `Error [${area}]:`, 65 | color: red, 66 | description: `${message}.`, 67 | fields: Object.entries(errorObject).map(field => { 68 | return { 69 | name: `${capitalize(field[0])}:`, 70 | value: field[1] 71 | } 72 | }) 73 | } 74 | }) 75 | 76 | // Destroy the client after sending the message. 77 | return client.destroy() 78 | }) 79 | 80 | // Log the bot in. 81 | return client.login(botToken) 82 | } catch (err) { 83 | console.error(`${chalk.greenBright('[Log]')} ${chalk.redBright('Failed to send error message to channel:')}`, err) 84 | } 85 | } 86 | 87 | // Used in JSON.stringify to parse all entries in an error object. 88 | const errorReplacer = (key, value) => { 89 | // If it's an error, treat it as such. 90 | if (value instanceof Error) { 91 | const errorObject = { 92 | // Pull all enumerable properties, supporting properties on custom Errors 93 | ...value, 94 | // Explicitly pull Error's non-enumerable properties 95 | name: value.name, 96 | message: value.message 97 | } 98 | delete errorObject.stack 99 | return errorObject 100 | } 101 | return value 102 | } 103 | -------------------------------------------------------------------------------- /src/commands/warns.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends a list of warnings for a user. 4 | */ 5 | 6 | import { green, orange, red, yellow } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { getWarnings } from '../db' 9 | import { logError } from '../utils/log' 10 | import { getAuthor, getName } from '../utils/user' 11 | 12 | const { autoBanWarns } = require('../utils/config').default 13 | 14 | // Export an object with command info and the function to execute. 15 | export const warnsCommand = { 16 | name: 'Warns', 17 | aliases: ['warns', 'warnlist'], 18 | category: 'moderation', 19 | description: 'Sends a list of warnings for a user.', 20 | permissions: ['KICK_MEMBERS'], 21 | usage: '', 22 | exec: async (args, message) => { 23 | try { 24 | // If a user isn't specified send an error message and terminate the command. 25 | if (args.length < 1) { 26 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to list the warnings for.', message) 27 | } 28 | 29 | // Save the user object of the member to be warned. 30 | // noinspection DuplicatedCode 31 | const member = message.mentions.members.first() 32 | 33 | // If the user doesn't exist send an error message and terminate the command. 34 | if (member === undefined) { 35 | return await sendErrorMessage('Not a User', 'The user you specified either doesn\'t exist or isn\'t a user.', message) 36 | } 37 | 38 | // Pull current warnings from the database. 39 | const currentWarnings = await getWarnings(member.user.id) 40 | 41 | // Select the colour based on the number of previous warnings. 42 | const colour = currentWarnings.length === 1 43 | ? yellow 44 | : currentWarnings.length < autoBanWarns 45 | ? orange 46 | : red 47 | 48 | // Create the initial embed. 49 | const embed = { 50 | title: `Warnings for ${getName(member, member.id)} (${member.user.tag})`, 51 | color: colour, 52 | author: getAuthor(member), 53 | fields: [], 54 | footer: { 55 | icon_url: member.user.avatarURL(), 56 | text: `${getName(member, member.id)}'s (${member.user.tag}'s) warnings.` 57 | }, 58 | timestamp: new Date() 59 | } 60 | 61 | // Start the warning counter. 62 | let count = 0 63 | 64 | // For each tag of tags, add the tag to the embed. 65 | for (const warning of currentWarnings) { 66 | // Push a field to the embed for each warning. 67 | embed.fields.push({ 68 | name: `Warning #${++count}`, 69 | value: `Reason: ${warning.reason}.` 70 | }) 71 | } 72 | 73 | // If there aren't any warnings added, change the colour to red and make the embed say empty. 74 | if (embed.fields.length <= 0) { 75 | embed.color = green 76 | embed.fields = [ 77 | { 78 | name: 'No Warnings', 79 | value: 'The specified member doesn\'t have any warnings.' 80 | } 81 | ] 82 | } 83 | 84 | try { 85 | // Remove the user's message. 86 | await message.delete() 87 | } catch (err) { 88 | await logError('Warns', 'Failed to delete message', err, message) 89 | } 90 | 91 | try { 92 | // Send the embed. 93 | // noinspection JSCheckFunctionSignatures 94 | return await message.channel.send({ embed }) 95 | } catch (err) { 96 | await logError('Warns', 'Failed to send message', err, message) 97 | } 98 | } catch (err) { 99 | await logError('Warns', 'Failed to run command', err, message) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/commands/report.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that reports a member to the staff. 4 | */ 5 | 6 | import { blue, orange } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { logError } from '../utils/log' 9 | import { getAuthor } from '../utils/user' 10 | 11 | const { channels: { reports } } = require('../utils/config').default 12 | 13 | // Export an object with command info and the function to execute. 14 | export const reportCommand = { 15 | name: 'Report', 16 | aliases: ['report', 'snitch'], 17 | category: 'moderation', 18 | description: 'Reports a user to the staff.', 19 | permissions: ['SEND_MESSAGES'], 20 | usage: ' ', 21 | exec: async (args, message) => { 22 | try { 23 | // If there aren't any args, send an error message stating a member wasn't specified and terminate the command. 24 | if (args.length < 1) { 25 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to report.', message) 26 | } 27 | 28 | // Get the member tagged in the args. 29 | const memberReported = message.mentions.members.first() 30 | 31 | if (args.length < 1) { 32 | return await sendErrorMessage('Reason Not Specified', 'You didn\'t specify a reason for reporting the user.', message) 33 | } 34 | 35 | // Save the args remaining after the first one. 36 | const reason = args.slice(1).join(' ') 37 | 38 | try { 39 | // Create a DM channel to send a message to. 40 | const dmChannel = await message.member.user.createDM() 41 | 42 | try { 43 | // Send the user a DM thanking them for reporting the user. 44 | await dmChannel.send({ 45 | embed: { 46 | title: 'Thanks for the Report!', 47 | color: blue, 48 | description: `Thanks for reporting ${memberReported} for reason: \`${reason}\`.\nThe staff have been notified.`, 49 | author: getAuthor(message.client.user), 50 | timestamp: new Date() 51 | } 52 | }) 53 | } catch (err) { 54 | await logError('Report', 'Failed to send thanks message', err, message) 55 | } 56 | } catch (err) { 57 | await logError('Report', 'Failed to create DM channel', err, message) 58 | } 59 | 60 | // Save the current server. 61 | const guild = message.guild 62 | 63 | // Save the reports channel. 64 | const reportsChannel = guild.channels.cache.find(c => c.name === reports) 65 | 66 | try { 67 | // Remove the user's message. 68 | await message.delete() 69 | } catch (err) { 70 | await logError('Report', 'Failed to delete message', err, message) 71 | } 72 | 73 | try { 74 | // Send the report message to the proper channel. 75 | // noinspection JSUnresolvedFunction 76 | return reportsChannel.send({ 77 | embed: { 78 | title: 'New Report', 79 | color: orange, 80 | description: reason, 81 | fields: [ 82 | { 83 | name: 'Member Reported:', 84 | value: `${memberReported}`, 85 | inline: true 86 | }, 87 | { 88 | name: 'Channel:', 89 | value: `${message.channel}`, 90 | inline: true 91 | } 92 | ], 93 | author: getAuthor(message.member), 94 | footer: { 95 | icon_url: memberReported.user.avatarURL(), 96 | text: `${memberReported.user.tag} reported from #${message.channel.name} by ${message.member.user.tag}.` 97 | }, 98 | timestamp: new Date() 99 | } 100 | }) 101 | } catch (err) { 102 | await logError('Report', 'Failed to send message', err, message) 103 | } 104 | } catch (err) { 105 | await logError('Report', 'Failed to run command', err, message) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/processes/thanksListener.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Functionality relating to listening and logging thanks. 4 | * TODO: the thing 5 | */ 6 | 7 | import { log, logError } from '../utils/log' 8 | import { incrementThanks } from '../db' 9 | import { green } from '../utils/colours' 10 | import { sendErrorMessage } from '../utils/sendErrorMessage' 11 | import { getName } from '../utils/user' 12 | 13 | const { repCoin } = require('../utils/config').default 14 | 15 | export const initThanksListener = async client => { 16 | try { 17 | // For each message run a function. 18 | client.on('message', async message => { 19 | try { 20 | // If the message isn't a dm, the author isn't a bot, and it contains the word 'thank' or 'kudos', continue. 21 | if (message.channel.type !== 'dm' && !message.author.bot && ['thank', 'kudos'].some(t => message.content.toLowerCase().includes(t))) { 22 | // Get the member thanked and filter for undefined members. 23 | const thankees = message.mentions.members.filter(thankee => thankee !== undefined) 24 | 25 | // If the number of members tagged isn't zero, continue. 26 | if (thankees.size !== 0) { 27 | // Save the thanker. 28 | const thanker = message.member 29 | 30 | // If the thanker is in the list of thankees, send an error message. 31 | if (thankees.map(thankee => thankee.user.id).includes(thanker.user.id)) { 32 | return await sendErrorMessage(`You Can't Thank Yourself, ${getName(thanker, thanker.id)}!`, 'You can see how that would be an issue, yes?', message) 33 | } 34 | 35 | // For each person thanked, increment their thanks counter. 36 | for (const thankee of thankees) { 37 | try { 38 | // Increment the thanks for the user. 39 | await incrementThanks(thankee[1].user.id, thanker.user.id) 40 | } catch (err) { 41 | await logError('Thanks', 'Failed to log the thanks', err) 42 | } 43 | } 44 | 45 | // Create array from thankees for easier looping. 46 | const thankeesArray = thankees.array() 47 | 48 | // Create a string from the list of thankees. 49 | let thankeesString = '' 50 | 51 | // If there are one, two or more thankees, add them to the string in the proper formatting. 52 | if (thankeesArray.length === 1) { 53 | thankeesString = `${thankeesArray[0]} ` 54 | } else if (thankeesArray.length === 2) { 55 | thankeesString = `${thankeesArray[0]} & ${thankeesArray[1]} ` 56 | } else { 57 | // Loop through the thankees and depending on what position, add it with the proper separator. 58 | let iterator = 0 59 | for (const thankee of thankeesArray) { 60 | thankeesString += thankeesArray.length === ++iterator 61 | ? `& ${thankee}` 62 | : `${thankee},` 63 | } 64 | } 65 | 66 | try { 67 | // Send a confirmation message. 68 | return message.channel.send({ 69 | embed: { 70 | title: `${repCoin ? `${repCoin} ` : ''}Thanks received!`, 71 | color: green, 72 | description: `${thankeesString} ${thankeesArray.length === 1 ? 'has' : 'have'} been thanked by ${thanker}!`, 73 | footer: { 74 | text: 'Use "thanks @user" to give someone rep, and ".rep @user" to see how much they have!' 75 | } 76 | } 77 | }) 78 | } catch (err) { 79 | await logError('Thanks', 'Failed to send message', err) 80 | } 81 | 82 | log('Thanks', thanker.id) 83 | } 84 | } 85 | } catch (err) { 86 | await logError('Thanks', 'Failed to run listener', err) 87 | } 88 | }) 89 | log('Init', 'Thanks listener initialized!') 90 | } catch (err) { 91 | // noinspection ES6MissingAwait 92 | logError('CommandListener', 'Failed to initialize the command listener', err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/commands/buildInfo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends an info message to the info channel. 4 | */ 5 | 6 | import { getSetting, setSetting } from '../db' 7 | import chalk from 'chalk' 8 | import { logError } from '../utils/log' 9 | import { blue } from '../utils/colours' 10 | import { getAuthor } from '../utils/user' 11 | 12 | const { channels: { info } } = require('../utils/config').default 13 | 14 | // Export an object with command info and the function to execute. 15 | export const buildInfoCommand = { 16 | name: 'Build Info', 17 | aliases: ['buildinfo', 'buildInfo', 'build-info'], 18 | category: 'admin', 19 | description: 'Sends an info message to the info channel.', 20 | permissions: ['ADMINISTRATOR'], 21 | exec: async (args, message) => { 22 | try { 23 | // Save the message's channel. 24 | const channel = message.channel 25 | 26 | try { 27 | // Remove the user's message. 28 | await message.delete() 29 | } catch (err) { 30 | await logError('BuildInfo', 'Failed to delete message', err, message) 31 | } 32 | 33 | try { 34 | // Fetch the most recent messages from the channel. 35 | const infoMessages = (await channel.messages.fetch({ limit: 10 })) 36 | .map(message => message.content) 37 | .reverse() 38 | 39 | // Save the server. 40 | const guild = message.guild 41 | 42 | // Find the info channel. 43 | // console.log(guild) 44 | const infoChannel = guild.channels.cache.find(c => c.name === info) 45 | 46 | // Save the previous info messages. 47 | const previousInfoMessages = await getSetting('info_message_ids') 48 | 49 | // Delete the previous info messages if they exist. 50 | for (const messageID of Object.values(previousInfoMessages)) { 51 | try { 52 | // noinspection JSCheckFunctionSignatures 53 | const message = await infoChannel.messages.fetch(messageID) 54 | await message.delete() 55 | } catch (err) { 56 | await logError('BuildInfo', 'Failed to delete info message(s)', err, message) 57 | } 58 | } 59 | 60 | // Initialize an array for message IDs. 61 | const infoMessageIDs = [] 62 | let verifyMessage 63 | 64 | // For each message, send it to the info channel. 65 | for (const message of infoMessages) { 66 | try { 67 | // Sent the message and save it. 68 | const sentMessage = await infoChannel.send(message) 69 | 70 | // Push the message ID to the array of message IDs. 71 | infoMessageIDs.push(sentMessage.id) 72 | 73 | // If the message contains the string, save its ID and react with a checkmark. 74 | if (message.includes('You agree to this')) { 75 | try { 76 | verifyMessage = sentMessage.id 77 | await sentMessage.react('✅') 78 | } catch (err) { 79 | await logError('BuildInfo', 'Failed to add reaction', err, message) 80 | } 81 | } 82 | } catch (err) { 83 | await logError('BuildInfo', 'Failed to send message', err, message) 84 | } 85 | } 86 | 87 | try { 88 | // Save the infoMessages to the database. 89 | await setSetting('info_message_ids', infoMessageIDs) 90 | await setSetting('verify_message_id', verifyMessage) 91 | console.log(`${chalk.greenBright('[BuildInfo]')} ${chalk.blue('Successfully built info message(s)!')}`) 92 | } catch (err) { 93 | await logError('BuildInfo', 'Error pushing messageIDs to database', err, message) 94 | } 95 | 96 | try { 97 | // Send the confirmation message. 98 | // noinspection JSCheckFunctionSignatures 99 | return message.channel.send({ 100 | embed: { 101 | title: 'Built Info Message', 102 | color: blue, 103 | description: `The info message has been built and is live in ${infoChannel}.`, 104 | author: getAuthor(message.member), 105 | timestamp: new Date() 106 | } 107 | }) 108 | } catch (err) { 109 | await logError('BuildInfo', 'Failed to send message', err, message) 110 | } 111 | } catch (err) { 112 | await logError('BuildInfo', 'Failed to fetch messages', err, message) 113 | } 114 | } catch (err) { 115 | await logError('BuildInfo', 'BuildInfo command failed', err, message) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/commands/help.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that sends some information on how to use the bot. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { commandsArray } from './index' 8 | import { capitalize } from '../utils/capitalize' 9 | import { logError } from '../utils/log' 10 | import { getAuthor } from '../utils/user' 11 | 12 | const { msgDeleteTime, prefix } = require('../utils/config').default 13 | 14 | // Export an object with command info and the function to execute. 15 | export const helpCommand = { 16 | name: 'Help', 17 | aliases: ['help', 'commands'], 18 | category: 'utils', 19 | description: 'Sends a list of commands that can be used with the bot.', 20 | permissions: ['SEND_MESSAGES'], 21 | usage: '[ ]', 22 | exec: async (args, message) => { 23 | try { 24 | // Set categories to an empty object to have all the commands added into. 25 | const categories = {} 26 | 27 | const deleteMessages = msgDeleteTime !== 0 && !args.includes('true') 28 | 29 | // Loop through all commands and add them to their proper place in the categories object. 30 | for (const command of commandsArray) { 31 | // Format usage 32 | const usage = command.usage ? `${command.aliases[0]} ${command.usage}` : command.aliases[0] 33 | // Save the embed field. 34 | const field = { 35 | name: command.name, 36 | value: `Usage: \`${prefix}${usage}\`\n${command.description}` 37 | } 38 | 39 | try { 40 | // If the user has the permissions to run the command, add it to the array. 41 | if (await message.member.hasPermission(command.permissions)) { 42 | // If the category exists, push the field. Otherwise, initialize the category with the field as it's first element. 43 | if (Object.prototype.hasOwnProperty.call(categories, command.category)) { 44 | categories[command.category].push(field) 45 | } else { 46 | categories[command.category] = [field] 47 | } 48 | } 49 | } catch (err) { 50 | await logError('Help', 'Failed to test for user permissions', err, message) 51 | } 52 | } 53 | 54 | try { 55 | // If a member is tagged, tag them. 56 | if (message.mentions.members.array().length > 0) { 57 | // Save the user object of the member 58 | const taggedUserID = message.mentions.members.first().id 59 | 60 | // If the user exists, send a message tagging them. 61 | if (taggedUserID !== undefined) { 62 | // Sent the message tagging the user. 63 | const sent = await message.channel.send(`<@${taggedUserID}>`) 64 | 65 | // If msgDeleteTime doesn't equal 0, set a timeout to delete the message after x seconds. (x secs * 1000 ms). 66 | if (deleteMessages) { 67 | setTimeout(() => { 68 | // Delete the message. 69 | sent.delete(1) 70 | }, msgDeleteTime * 1000) 71 | } 72 | } 73 | } 74 | } catch (err) { 75 | await logError('Help', 'Failed to tag user', err, message) 76 | } 77 | 78 | // For each category, send a message with each of the commands. 79 | for (const category of Object.keys(categories)) { 80 | try { 81 | // Send the message. 82 | // noinspection JSUnresolvedFunction,JSCheckFunctionSignatures 83 | const sent = await message.channel.send({ 84 | embed: { 85 | title: capitalize(category), 86 | color: blue, 87 | fields: categories[category], 88 | author: getAuthor(message.member) 89 | } 90 | }) 91 | 92 | // If msgDeleteTime doesn't equal 0, set a timeout to delete the message after x seconds. (x secs * 1000 ms). 93 | if (deleteMessages) { 94 | setTimeout(async () => { 95 | try { 96 | // Delete the message. 97 | // BUG: Not deleting 98 | sent.delete() 99 | } catch (err) { 100 | await logError('Tags', 'Failed to delete message', err, message) 101 | } 102 | }, msgDeleteTime * 1000) 103 | } else { 104 | return sent 105 | } 106 | } catch (err) { 107 | await logError('Help', 'Failed to send message', err, message) 108 | } 109 | } 110 | try { 111 | // Remove the user's message. 112 | await message.delete() 113 | } catch (err) { 114 | await logError('Help', 'Failed to delete message', err, message) 115 | } 116 | } catch (err) { 117 | await logError('Help', 'Failed to run command', err, message) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/processes/infoReactionListener.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Functionality relating to listening for a reaction on the info message and applying roles to users. 4 | */ 5 | 6 | import { getSetting } from '../db' 7 | import { log, logError } from '../utils/log' 8 | 9 | const { roles: { verified } } = require('../utils/config').default 10 | 11 | // Applied an action to either add a remove a role from a user based on the action provided. 12 | const roleAction = async ({ client, guildId, messageId, userId, emojiName }, remove = false) => { 13 | try { 14 | // Retrieve the list of messages to listen to from the database. 15 | const infoMessageID = await getSetting('verify_message_id') 16 | 17 | // Save some details about the reaction event to constants. 18 | const guild = client.guilds.get(guildId) 19 | 20 | if (guild === undefined || guild === null) { 21 | return await logError('InfoListener', 'The guild is invalid') 22 | } 23 | 24 | const member = await guild.fetchMember(userId) 25 | 26 | if (member === undefined || member === null) { 27 | return await logError('InfoListener', 'The member is invalid') 28 | } 29 | 30 | const guildRoles = guild.roles.cache 31 | 32 | // Grab the verified role from the server. 33 | const role = guildRoles.find(r => r.name === verified) 34 | 35 | try { 36 | // If the role exists, continue. 37 | if (role !== null) { 38 | // If the message ID is equal the info message ID, continue. 39 | if (infoMessageID === messageId) { 40 | // If the emoji is the right one, continue. 41 | if (emojiName === '✅') { 42 | // Add or remove the role. 43 | remove ? await member.roles.remove(role) : await member.roles.add(role) 44 | } 45 | } 46 | } 47 | } catch (err) { 48 | await logError('InfoListener', 'Failed to add or remove the role', err) 49 | } 50 | } catch (err) { 51 | await logError('InfoListener', 'Failed to execute the verified role action', err) 52 | } 53 | } 54 | 55 | // Given some information about a reaction addition, pass the info to roleAction to decide whether to add the role or not. 56 | const roleAdd = async (client, guildId, messageId, userId, emojiName) => { 57 | try { 58 | // Create a context object with all of the params to pass to roleAction. 59 | const context = { 60 | client, 61 | guildId, 62 | messageId, 63 | userId, 64 | emojiName 65 | } 66 | // Run the roleAction function with the context passed in. 67 | await roleAction(context) 68 | } catch (err) { 69 | await logError('InfoListener', `Failed to add role: ${err}`) 70 | } 71 | } 72 | 73 | // Given some information about a reaction removal, pass the info to roleAction to decide whether to remove the role or not. 74 | const roleRm = async (client, guildId, messageId, userId, emojiName) => { 75 | try { 76 | // Create a context object with all of the params to pass to roleAction. 77 | const context = { 78 | client, 79 | guildId, 80 | messageId, 81 | userId, 82 | emojiName 83 | } 84 | // Run the roleAction function with the context passed in and remove set to true. 85 | await roleAction(context, true) 86 | } catch (err) { 87 | await logError('InfoListener', 'Failed to remove role', err) 88 | } 89 | } 90 | 91 | // Given a client, add a listener for message reactions that calls the proper functions for role listening. 92 | export const initInfoReactionListener = async client => { 93 | try { 94 | // Add a listener for all events ('raw' type). 95 | client.on('raw', async event => { 96 | try { 97 | // Save event data. 98 | const { d: data } = event 99 | // If the event type is a reaction addition, run the roleAdd function. 100 | if (event.t === 'MESSAGE_REACTION_ADD') { 101 | // noinspection JSUnresolvedVariable 102 | await roleAdd( 103 | client, 104 | data.guild_id, 105 | data.message_id, 106 | data.user_id, 107 | data.emoji.name 108 | ) 109 | // Otherwise, if the type is a reaction removal, run the roleRm function. 110 | } else if (event.t === 'MESSAGE_REACTION_REMOVE') { 111 | // noinspection JSUnresolvedVariable 112 | await roleRm( 113 | client, 114 | data.guild_id, 115 | data.message_id, 116 | data.user_id, 117 | data.emoji.name 118 | ) 119 | } 120 | } catch (err) { 121 | await logError('InfoListener', 'Failed handling reaction', err) 122 | } 123 | }) 124 | log('Init', 'Info reaction listener initialized!') 125 | } catch (err) { 126 | await logError('InitListener', 'Failed to add raw listener', err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/commands/move.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that quotes and pings a user in the specified channel. 4 | */ 5 | 6 | import { orange } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { logError } from '../utils/log' 9 | import { getAuthor, getName } from '../utils/user' 10 | 11 | // Export an object with command info and the function to execute. 12 | export const moveCommand = { 13 | name: 'Move', 14 | aliases: ['move'], 15 | category: 'moderation', 16 | description: 'Move\'s a user\'s message(s) to a different channel.', 17 | permissions: ['MANAGE_MESSAGES'], 18 | usage: ' ', 19 | exec: async (args, message) => { 20 | try { 21 | // If there aren't any args, send an error message stating a member wasn't specified and terminate the command. 22 | if (args.length < 1) { 23 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to move.', message) 24 | } 25 | 26 | // Get the member tagged in the args. 27 | const memberTagged = message.mentions.members.first() 28 | 29 | // Check that the user is valid. 30 | if (memberTagged === undefined) { 31 | return await sendErrorMessage('User Not Valid', 'That user isn\'t a valid user.', message) 32 | } 33 | 34 | // Send error message. 35 | if (args.length < 2) { 36 | return await sendErrorMessage('Channel Not Specified', 'You didn\'t specify a channel to move the message to.', message) 37 | } 38 | 39 | // Save the args remaining after the first two. If there aren't more than two args, default to 'Banned by devmod.'. 40 | const channel = message.mentions.channels.first() 41 | 42 | // Check to make sure the specified channel exists. 43 | if (channel === undefined) { 44 | return await sendErrorMessage('Channel Not Valid', 'You didn\'t specify a valid channel.', message) 45 | } 46 | 47 | // Fetch the last 20 of messages form the current channel. 48 | const messages = await message.channel.messages.fetch({ limit: 20 }) 49 | 50 | // Filter through the grabbed IDs for message by the tagged used and grab the most recent. 51 | const recentMessage = messages.filter(m => m.member.user.id === memberTagged.user.id).first() 52 | 53 | // It the most recent message doesn't exist, send an error message. 54 | if (recentMessage === undefined) { 55 | return await sendErrorMessage('User Not Found', 'That user hasn\'t sent a message recently.', message) 56 | } 57 | 58 | // Select the first 10 messages before the most recent message. 59 | const messagesBefore = await message.channel.messages.fetch({ 60 | before: recentMessage.id, 61 | limit: 10 62 | }) 63 | 64 | // Initialize the array of messages with the original message. 65 | const messagesToQuote = [recentMessage] 66 | 67 | // For each message before the original, if it is sent by the original member, add it to the messages to quote, 68 | // otherwise break the loop. 69 | for (const beforeMessage of messagesBefore) { 70 | if (beforeMessage[1].member.user.id === memberTagged.user.id) { 71 | messagesToQuote.push(beforeMessage[1]) 72 | } else { 73 | break 74 | } 75 | } 76 | 77 | // Fetch all messages again. 78 | const messagesToDelete = await message.channel.messages.fetch({ 79 | before: recentMessage.id, 80 | limit: messagesToQuote.length - 1 81 | }) 82 | 83 | try { 84 | // Delete all of the users quoted messages. 85 | // BUG: Does not work 86 | await messagesToDelete.clear() 87 | } catch (err) { 88 | await logError('Move', 'Failed to delete quoted messages', err, message) 89 | } 90 | 91 | try { 92 | // Remove the user's message. 93 | await message.delete() 94 | } catch (err) { 95 | await logError('Move', 'Failed to delete message', err, message) 96 | } 97 | 98 | try { 99 | // Send the quoted message to the proper channel. 100 | // noinspection JSUnresolvedFunction 101 | return channel.send(memberTagged, { 102 | embed: { 103 | title: `Message${messagesToQuote.length === 1 ? '' : 's'} Moved`, 104 | color: orange, 105 | description: messagesToQuote.map(m => m.content).reverse().join('\n'), 106 | author: getAuthor(memberTagged), 107 | footer: { 108 | icon_url: message.member.user.avatarURL(), 109 | text: `${getName(message.member, message.member.id)} has moved your message${messagesToQuote.length === 1 ? '' : 's'} to the proper channel.` 110 | }, 111 | timestamp: new Date() 112 | } 113 | }) 114 | } catch (err) { 115 | await logError('Move', 'Failed to send message', err, message) 116 | } 117 | } catch (err) { 118 | await logError('Move', 'Failed to run command', err, message) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/commands/ban.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that bans a member. 4 | */ 5 | 6 | import { sendErrorMessage } from '../utils/sendErrorMessage' 7 | import { red } from '../utils/colours' 8 | import { logError } from '../utils/log' 9 | import { getAuthor, getName } from '../utils/user' 10 | 11 | const { banMsgDelete, channels: { ban } } = require('../utils/config').default 12 | 13 | // Export an object with command info and the function to execute. 14 | export const banCommand = { 15 | name: 'Ban', 16 | aliases: ['ban'], 17 | category: 'moderation', 18 | description: 'Bans a user and removes their messages from a specified number of days previous.', 19 | permissions: ['BAN_MEMBERS'], 20 | usage: ' [ ]', 21 | exec: async (args, message) => { 22 | try { 23 | // If there aren't any args, send an error message stating a member wasn't specified and terminate the command. 24 | if (args.length < 1) { 25 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to ban.', message) 26 | } 27 | 28 | // Get the member tagged in the args. 29 | const member = message.mentions.members.first() 30 | 31 | // If there isn't a member with that name, send an error message and terminate the command. 32 | if (member === undefined) { 33 | return await sendErrorMessage('User Doesn\'t Exist', 'The specified user doesn\'t exist.', message) 34 | } 35 | 36 | // If the specified user is able to kick members (read: moderator) terminate the command. 37 | if (member.hasPermission('KICK_MEMBERS')) { 38 | return await sendErrorMessage( 39 | 'Can\'t Ban Member', 40 | `${member} cannot be banned.`, 41 | message 42 | ) 43 | } 44 | 45 | const rmIndex = args.indexOf('--rm') 46 | 47 | let days 48 | 49 | // Save the days arg. If it doesn't exist, default to 0. If it isn't an int, default to the amount specified in the config. 50 | if ((args.length > (rmIndex + 1)) && !isNaN(parseInt(args[rmIndex + 1]))) { 51 | days = parseInt(args[rmIndex + 1]) 52 | args.splice(rmIndex, 2) 53 | } else { 54 | days = banMsgDelete 55 | } 56 | 57 | // Save the args remaining after the first. If there isn't more than one arg, default to 'banned by devmod.'. 58 | const reason = args.length > 1 ? args.slice(1).join(' ') : 'banned by devmod.' 59 | 60 | try { 61 | // Send the user a DM letting them know they've been banned. 62 | // noinspection JSUnresolvedFunction 63 | await member.user.send({ 64 | embed: { 65 | title: `You have been banned from ${message.guild.name}.`, 66 | color: red, 67 | thumbnail: { 68 | url: message.guild.iconURL() 69 | }, 70 | fields: [ 71 | { 72 | name: 'Reason:', 73 | value: reason 74 | } 75 | ], 76 | footer: { 77 | text: `Your messages from the past ${days === 1 ? ' day' : `${days} days`} have been deleted.` 78 | } 79 | } 80 | }) 81 | } catch (err) { 82 | await logError('Ban', 'Failed to send member message', err, message) 83 | } 84 | 85 | // Save some info about the staff member. 86 | const staffMember = message.member 87 | 88 | try { 89 | // Log the ban to the bans channel. 90 | await message.guild.channels.cache 91 | .find(c => c.name === ban) 92 | .send({ 93 | embed: { 94 | color: red, 95 | title: 'Ban', 96 | description: `${getName(member, member.id)} (${member.user.tag} - ${member}) has been banned.`, 97 | author: getAuthor(staffMember), 98 | fields: [ 99 | { 100 | name: 'Reason:', 101 | value: reason 102 | } 103 | ], 104 | footer: { 105 | icon_url: member.user.avatarURL(), 106 | text: `${getName(member, member.id)}'s (${member.user.tag}'s) messages from the past ${days === 1 ? ' day' : `${days} days`} have been deleted.` 107 | }, 108 | timestamp: new Date() 109 | } 110 | }) 111 | } catch (err) { 112 | await logError('Ban', 'Failed to log ban to channel', err, message) 113 | } 114 | 115 | try { 116 | // Ban the member. 117 | await member.ban({ days, reason }) 118 | } catch (err) { 119 | await logError('Ban', 'Failed to ban the member', err, message) 120 | } 121 | 122 | try { 123 | // Remove the user's message. 124 | await message.delete() 125 | } catch (err) { 126 | if (err.message !== 'Unknown Message') { 127 | await logError('Ban', 'Failed to delete message', err, message) 128 | } 129 | } 130 | } catch (err) { 131 | await logError('Ban', 'Failed to run command', err, message) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/commands/buildRoles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that rebuilds the roles channel messages. 4 | */ 5 | 6 | import { blue } from '../utils/colours' 7 | import { getSetting, setSetting } from '../db' 8 | import { log, logError } from '../utils/log' 9 | import { getAuthor } from '../utils/user' 10 | import { sendErrorMessage } from '../utils/sendErrorMessage' 11 | 12 | const { approvedRoles, channels: { roles } } = require('../utils/config').default 13 | 14 | // Export an object with command info and the function to execute. 15 | export const buildRolesCommand = { 16 | name: 'Build Roles', 17 | aliases: ['buildroles', 'buildRoles', 'build-roles'], 18 | category: 'admin', 19 | description: 'Builds the messages in the roles channel.', 20 | permissions: ['ADMINISTRATOR'], 21 | exec: async (args, message) => { 22 | try { 23 | // Save the previous roles messages. 24 | const previousRolesMessages = await getSetting('reactions_message_ids') 25 | 26 | // Save the roles channel 27 | const rolesChannel = message.guild.channels.cache.find(c => c.name === roles) 28 | 29 | if (rolesChannel === undefined) { 30 | return await sendErrorMessage('No Role Channel', 'The roles channel either isn\'t set or doesn\'t exist.') 31 | } 32 | 33 | // Find and delete the previous messages. 34 | // Delete the previous roles messages if they exist. 35 | for (const messageID of Object.values(previousRolesMessages)) { 36 | try { 37 | // noinspection JSCheckFunctionSignatures 38 | const roleMessage = await rolesChannel.fetchMessage(messageID) 39 | await roleMessage.delete() 40 | } catch (err) { 41 | await logError('BuildRoles', 'Failed to delete roles message', err, message) 42 | } 43 | } 44 | 45 | // Initialize empty variables. 46 | const messageIDs = {} 47 | let count = 0 48 | 49 | // For each group of roles in approvedRoles, send a message. 50 | for (const roleGroup of approvedRoles) { 51 | // Create an array, and for each approved role push a string with 'name: emoji' to it. 52 | const roleArray = [] 53 | for (const role of Object.values(roleGroup.roles)) { 54 | roleArray.push(`${role.name}: ${role.emoji}`) 55 | } 56 | 57 | try { 58 | // Find the roles channel, send the roles message, and save the ID. 59 | const roleMessage = await rolesChannel.send({ 60 | // Create the discord embed to send to the channel. 61 | embed: { 62 | title: roleGroup.name, 63 | color: blue, 64 | description: `${roleGroup.message}\n\n${roleArray.join('\n')}` 65 | } 66 | }) 67 | 68 | // Save the message ID. 69 | messageIDs[roleGroup.id] = roleMessage.id 70 | 71 | // Add each reaction to the roles message. 72 | for (const reaction of Object.values(roleGroup.roles)) { 73 | // Wait for the reaction to be added. 74 | try { 75 | await roleMessage.react(reaction.emoji) 76 | } catch (err) { 77 | await logError('BuildRoles', 'Failed to add reaction to roles message:', err, message) 78 | } 79 | } 80 | } catch (err) { 81 | await logError('BuildRoles', 'Failed to send message to role channel', err, message) 82 | } 83 | 84 | // Increment count, and if it's equal to the number of role groups, call the finish function with messageIDs. 85 | if (++count === approvedRoles.length) { 86 | // Log the IDs to the console joined by ', ' 87 | log('BuildRoles', `Role Message IDs: ${Object.values(messageIDs).join(', ')}.`) 88 | 89 | try { 90 | // Save the messageIDs to the database. 91 | await setSetting('reactions_message_ids', messageIDs) 92 | log('BuildRoles', 'Successfully sent roles message!') 93 | } catch (err) { 94 | await logError('BuildRoles', 'Error pushing messageIDs to database', err, message) 95 | } 96 | } 97 | } 98 | 99 | try { 100 | // Remove the user's message. 101 | await message.delete() 102 | } catch (err) { 103 | await logError('BuildRoles', 'Failed to delete message:', err, message) 104 | } 105 | 106 | try { 107 | // Send the confirmation message. 108 | // noinspection JSCheckFunctionSignatures 109 | return message.channel.send({ 110 | embed: { 111 | title: 'Built Roles Message', 112 | color: blue, 113 | description: `The roles message has been built and is live in ${rolesChannel}.`, 114 | author: getAuthor(message.member), 115 | timestamp: new Date() 116 | } 117 | }) 118 | } catch (err) { 119 | await logError('BuildRoles', 'Failed to send message', err, message) 120 | } 121 | } catch (err) { 122 | await logError('BuildRoles', 'Failed to run command', err, message) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/commands/yeet.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that yeets a member. 4 | */ 5 | 6 | import { sendErrorMessage } from '../utils/sendErrorMessage' 7 | import { red } from '../utils/colours' 8 | import { logError } from '../utils/log' 9 | import { getAuthor, getName } from '../utils/user' 10 | 11 | const { banMsgDelete, channels: { ban } } = require('../utils/config').default 12 | 13 | const yeetIds = [ 14 | 'J1ABRhlfvQNwIOiAas' 15 | 'YnBthdanxDqhB99BGU', 16 | 'M9aM4NXS8q29W6Ia6S', 17 | '11HkufsiNrBXK8', 18 | '5PhDdJQd2yG1MvHzJ6', 19 | 'Izi543BvWEbAVXZLG6', 20 | '4EEIsDmNJCiNcvAERef', 21 | 'KzoZUrq40MaazLgHsg', 22 | 'DvMHwFYLVHlZe', 23 | ] 24 | 25 | 26 | // Export an object with command info and the function to execute. 27 | export const yeetCommand = { 28 | name: 'Yeet', 29 | aliases: ['yeet', 'Yeet', 'yeeted'], 30 | category: 'moderation', 31 | description: 'Yeets a user and removes their messages from a specified number of days previous.', 32 | permissions: ['BAN_MEMBERS'], 33 | usage: ' [ ]', 34 | exec: async (args, message) => { 35 | try { 36 | // If there aren't any args, send a random yeet gif and terminate the command. 37 | if (args.length < 1) { 38 | return await message.channel.send('https://media.giphy.com/media/'+yeetIds[Math.floor(Math.random()*yeetIds.length)]+'/giphy.gif') 39 | } 40 | 41 | // Get the member tagged in the args. 42 | const member = message.mentions.members.first() 43 | 44 | // If there isn't a member with that name, send an error message and terminate the command. 45 | if (member === undefined) { 46 | return await sendErrorMessage('User Doesn\'t Exist', 'The specified user doesn\'t exist.', message) 47 | } 48 | 49 | // If the specified user is able to kick members (read: moderator) terminate the command. 50 | if (member.hasPermission('KICK_MEMBERS')) { 51 | return await sendErrorMessage( 52 | 'Can\'t Yeet Member', 53 | `${member} cannot be yeeted.`, 54 | message 55 | ) 56 | } 57 | 58 | const rmIndex = args.indexOf('--rm') 59 | 60 | let days 61 | 62 | // Save the days arg. If it doesn't exist, default to 0. If it isn't an int, default to the amount specified in the config. 63 | if ((args.length > (rmIndex + 1)) && !isNaN(parseInt(args[rmIndex + 1]))) { 64 | days = parseInt(args[rmIndex + 1]) 65 | args.splice(rmIndex, 2) 66 | } else { 67 | days = banMsgDelete 68 | } 69 | 70 | // Save the args remaining after the first. If there isn't more than one arg, default to 'banned by devmod.'. 71 | const reason = args.length > 1 ? args.slice(1).join(' ') : 'yeeted by devmod.' 72 | 73 | try { 74 | // Send the user a DM letting them know they've been banned. 75 | // noinspection JSUnresolvedFunction 76 | await member.user.send({ 77 | embed: { 78 | title: `You have been yeeted from ${message.guild.name}.`, 79 | color: red, 80 | thumbnail: { 81 | url: message.guild.iconURL() 82 | }, 83 | fields: [ 84 | { 85 | name: 'Reason:', 86 | value: reason 87 | } 88 | ], 89 | footer: { 90 | text: `Your messages from the past ${days === 1 ? ' day' : `${days} days`} have been deleted.` 91 | } 92 | } 93 | }) 94 | } catch (err) { 95 | await logError('Yeet', 'Failed to send member message', err, message) 96 | } 97 | 98 | // Save some info about the staff member. 99 | const staffMember = message.member 100 | 101 | try { 102 | // Log the ban to the bans channel. 103 | await message.guild.channels.cache 104 | .find(c => c.name === ban) 105 | .send({ 106 | embed: { 107 | color: red, 108 | title: 'Yeet', 109 | description: `${getName(member, member.id)} (${member.user.tag} - ${member}) has been yeeted.`, 110 | author: getAuthor(staffMember), 111 | fields: [ 112 | { 113 | name: 'Reason:', 114 | value: reason 115 | } 116 | ], 117 | footer: { 118 | icon_url: member.user.avatarURL(), 119 | text: `${getName(member, member.id)}'s (${member.user.tag}'s) messages from the past ${days === 1 ? ' day' : `${days} days`} have been deleted.` 120 | }, 121 | timestamp: new Date() 122 | } 123 | }) 124 | } catch (err) { 125 | await logError('Yeet', 'Failed to log yeet to channel', err, message) 126 | } 127 | 128 | try { 129 | // Ban the member. 130 | await member.ban({ days, reason }) 131 | } catch (err) { 132 | await logError('Yeet', 'Failed to yeet the member', err, message) 133 | } 134 | 135 | try { 136 | // Remove the user's message. 137 | await message.delete() 138 | } catch (err) { 139 | if (err.message !== 'Unknown Message') { 140 | await logError('Yeet', 'Failed to delete message', err, message) 141 | } 142 | } 143 | } catch (err) { 144 | await logError('Yeet', 'Failed to run command', err, message) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/commands/role.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that adds or removes a role from a user. 4 | */ 5 | 6 | import { green, red } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { logError } from '../utils/log' 9 | import { getAuthor } from '../utils/user' 10 | 11 | const { approvedRoles } = require('../utils/config').default 12 | 13 | // Export an object with command info and the function to execute. 14 | export const roleCommand = { 15 | name: 'Role', 16 | aliases: ['role'], 17 | category: 'utils', 18 | description: 'Adds or removes a role from a user.', 19 | permissions: ['SEND_MESSAGES'], 20 | usage: ' ', 21 | exec: async (args, message) => { 22 | try { 23 | // If a command isn't specified send an error message and terminate the command. 24 | if (args.length < 1) { 25 | return await sendErrorMessage('Command Not Specified', 'You didn\'t specify whether to add or remove a role.', message) 26 | } 27 | 28 | // Save the command. 29 | const command = args[0] 30 | 31 | // If a role isn't specified send an error message and terminate the command. 32 | if (args.length < 2) { 33 | return await sendErrorMessage('Role Not Specified', 'You didn\'t specify a role to add or remove.', message) 34 | } 35 | 36 | // Save the role after converting it to lower case. 37 | const role = args[1].toLowerCase() 38 | 39 | // Reduce the approvedRoles list to an array of all of the keys of each roleGroup.roles. 40 | const availableRoles = approvedRoles.reduce((previousValue, currentValue) => { 41 | const previousArray = previousValue 42 | for (const role of Object.keys(currentValue.roles)) { 43 | previousArray.push(role) 44 | } 45 | return previousArray 46 | }, []) 47 | 48 | // If the specified role isn't in availableRoles, send an error message and terminate the command. 49 | if (!availableRoles.some(r => r === role)) { 50 | return await sendErrorMessage('Invalid Role', 'That role isn\'t allowed or doesn\'t exist.', message) 51 | } 52 | 53 | // Save the server. 54 | const guild = message.guild 55 | 56 | // Grab the role from the server. 57 | const guildRole = guild.roles.cache.find(r => r.name === role) 58 | 59 | // If the role doesn't exist, send an error message and terminate the command. 60 | if (guildRole === undefined) { 61 | return await sendErrorMessage('Role Doesn\'t Exist', 'The specified role doesn\'t exist.', message) 62 | } 63 | 64 | // Save the member. 65 | const member = message.member 66 | 67 | // Add or remove the role, or reject the command based on the command specified. 68 | switch (command) { 69 | case 'add': 70 | try { 71 | // Add the role to the member. 72 | await member.addRole(guildRole) 73 | 74 | try { 75 | // Delete the user's message. 76 | await message.delete() 77 | } catch (err) { 78 | await logError('Role', 'Failed to delete message', err, message) 79 | } 80 | 81 | try { 82 | // Send a confirmation message. 83 | return message.channel.send({ 84 | embed: { 85 | title: 'Role Added', 86 | color: green, 87 | description: `Added ${guildRole} to ${member}.`, 88 | author: getAuthor(member) 89 | } 90 | }) 91 | } catch (err) { 92 | await logError('Role', 'Failed to send message', err, message) 93 | } 94 | } catch (err) { 95 | await logError('Role', 'Failed to add role', err, message) 96 | return await sendErrorMessage('Couldn\'t Add Role', 'Couldn\'t add the specified role.', message) 97 | } 98 | break 99 | case 'rm': 100 | try { 101 | // Remove the role from the member. 102 | await member.roles.remove(guildRole) 103 | 104 | try { 105 | // Delete the user's message. 106 | await message.delete() 107 | } catch (err) { 108 | await logError('Role', 'Failed to delete message', err, message) 109 | } 110 | 111 | try { 112 | // Send a confirmation message. 113 | return message.channel.send({ 114 | embed: { 115 | title: 'Role Removed', 116 | color: red, 117 | description: `Removed ${guildRole} from ${member}.`, 118 | author: getAuthor(member) 119 | } 120 | }) 121 | } catch (err) { 122 | await logError('Role', 'Failed to send message', err, message) 123 | } 124 | } catch (err) { 125 | await logError('Role', 'Failed to add role', err, message) 126 | return await sendErrorMessage('Couldn\'t Remove Role', 'Couldn\'t remove the specified role.', message) 127 | } 128 | break 129 | default: 130 | return await sendErrorMessage('Invalid Command', 'The command wasn\'t one of add or rm.', message) 131 | } 132 | } catch (err) { 133 | await logError('Role', 'Failed to run command', err, message) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/commands/warn.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Command that warns a user. 4 | */ 5 | 6 | import { orange, red, yellow } from '../utils/colours' 7 | import { sendErrorMessage } from '../utils/sendErrorMessage' 8 | import { addWarning, getWarnings } from '../db' 9 | import { banCommand } from './ban' 10 | import { logError } from '../utils/log' 11 | import { getAuthor, getName } from '../utils/user' 12 | 13 | const { autoBan, autoBanWarns, banMsgDelete, channels: { warn } } = require('../utils/config').default 14 | 15 | // Export an object with command info and the function to execute. 16 | export const warnCommand = { 17 | name: 'Warn', 18 | aliases: ['warn', 'addwarn'], 19 | category: 'moderation', 20 | description: 'Warns a user.', 21 | permissions: ['KICK_MEMBERS'], 22 | usage: ' []', 23 | exec: async (args, message) => { 24 | try { 25 | // If a user isn't specified send an error message and terminate the command. 26 | if (args.length < 1) { 27 | return await sendErrorMessage('User Not Specified', 'You didn\'t specify a user to warn.', message) 28 | } 29 | 30 | // Save the user object of the member to be warned. 31 | // noinspection DuplicatedCode 32 | const member = message.mentions.members.first() 33 | 34 | // If the user doesn't exist send an error message and terminate the command. 35 | if (member === undefined) { 36 | return await sendErrorMessage('Not a User', 'The user you specified either doesn\'t exist or isn\'t a user.', message) 37 | } 38 | 39 | try { 40 | // If the specified user is able to kick members (read: moderator) terminate the command. 41 | if (member.hasPermission('KICK_MEMBERS')) { 42 | return await sendErrorMessage( 43 | 'Can\'t Warn Member', 44 | 'You are not allowed to warn that member.', 45 | message 46 | ) 47 | } 48 | } catch (err) { 49 | await logError('Warn', 'Failed to check user permissions', err, message) 50 | } 51 | 52 | const reason = args.length > 1 ? args.slice(1).join(' ') : 'warned by devmod' 53 | 54 | // Save some info about the staff member. 55 | const staffMember = message.member 56 | 57 | // Pull current warnings from the database. 58 | const currentWarnings = await getWarnings(member.user.id) 59 | 60 | try { 61 | // Log the warning to the database. 62 | await addWarning(member.user.id, reason, staffMember.user.id) 63 | } catch (err) { 64 | await logError('Warn', 'Failed to log warning', err, message) 65 | } 66 | 67 | // Select the colour based on the number of previous warnings. 68 | const colour = currentWarnings.length < 1 69 | ? yellow 70 | : currentWarnings.length < 2 71 | ? orange 72 | : red 73 | 74 | try { 75 | // Log the warn to the current channel. 76 | // noinspection JSUnresolvedFunction 77 | await message.guild.channels.cache 78 | .find(c => c.name === warn) 79 | .send({ 80 | embed: { 81 | color: colour, 82 | title: `Warning #${currentWarnings.length + 1}`, 83 | description: `${getName(member, member.id)} (${member.user.tag} - ${member}) has been warned for: ${reason}.`, 84 | author: getAuthor(staffMember), 85 | footer: { 86 | icon_url: member.user.avatarURL(), 87 | text: `${getName(member, member.id)}'s (${member.user.tag}'s) has been warned.` 88 | }, 89 | timestamp: new Date() 90 | } 91 | }) 92 | } catch (err) { 93 | await logError('Warn', 'Failed to log warn', err, message) 94 | } 95 | 96 | try { 97 | // Create a dm channel to the user. 98 | const dm = await member.user.createDM() 99 | 100 | // Send a dm to the user letting them know they've been warned. 101 | await dm.send({ 102 | embed: { 103 | title: `You have received a warning on ${message.guild.name}.`, 104 | color: colour, 105 | author: getAuthor(member.client.user), 106 | thumbnail: { 107 | url: message.guild.iconURL() 108 | }, 109 | fields: [ 110 | { 111 | name: 'Reason:', 112 | value: reason 113 | } 114 | ] 115 | } 116 | }) 117 | } catch (err) { 118 | await logError('Warn', 'Failed to DM user', err, message) 119 | } 120 | 121 | // If autoban is enabled, see if the user should be banned. 122 | if (autoBan) { 123 | // If the number of warnings passes the specified threshold, ban the user. 124 | if (currentWarnings.length + 1 >= autoBanWarns) { 125 | try { 126 | // Run the ban command. 127 | await banCommand.exec([args[0], banMsgDelete, ...'Exceeded maximum warnings'.split(' ')], message) 128 | } catch (err) { 129 | await logError('Warn', 'Failed to autoban user', err, message) 130 | } 131 | } 132 | } 133 | 134 | try { 135 | // Remove the user's message. 136 | await message.delete() 137 | } catch (err) { 138 | await logError('Warn', 'Failed to delete message', err, message) 139 | } 140 | } catch (err) { 141 | await logError('Warn', 'Failed to run command', err, message) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/processes/roleReactionListener.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Functionality relating to listening for reactions on the roles message(s) and applying roles to users. 4 | */ 5 | 6 | import { getSetting } from '../db' 7 | import { log, logError } from '../utils/log' 8 | 9 | const { approvedRoles } = require('../utils/config').default 10 | 11 | // Applied an action to either add a remove a role from a user based on the action provided and the approved roles list. 12 | const roleAction = async ({ client, guildId, messageId, userId, emojiName }, remove = false) => { 13 | try { 14 | // Retrieve the list of messages to listen to from the database. 15 | const reactionMessageIDs = await getSetting('reactions_message_ids') 16 | 17 | // Save some details about the reaction event to constants. 18 | const guild = client.guilds.cache.get(guildId) 19 | 20 | if (guild === undefined || guild === null) { 21 | return await logError('InfoListener', 'The guild is invalid') 22 | } 23 | 24 | const member = await guild.members.fetch(userId) 25 | 26 | if (member === undefined || member === null) { 27 | return await logError('InfoListener', 'The member is invalid') 28 | } 29 | 30 | const roles = guild.roles.cache 31 | 32 | // Run a function for each message ID in the list of reaction role messages. 33 | for (const key of Object.keys(reactionMessageIDs)) { 34 | // If the message ID is equal the ID from the loop, continue. 35 | if (reactionMessageIDs[key] === messageId) { 36 | // For each group of roles in the approved roles list, run a function. 37 | for (const roleGroup of approvedRoles) { 38 | // If the current key (from the message) is equal to a role group ID, continue. 39 | if (roleGroup.id === key) { 40 | // For each role in the current role group, run a function. 41 | for (const roleEntry of Object.keys(roleGroup.roles)) { 42 | // If the reaction emoji is equal to the emoji of the current role, continue. 43 | if (roleGroup.roles[roleEntry].emoji === emojiName) { 44 | // If the role exists, find the role and either add or remove the role from the user based on the params. 45 | if (roleGroup.roles[roleEntry] !== null && roleGroup.roles[roleEntry] !== undefined) { 46 | // Find the role from the guild's list of roles. 47 | const role = roles.find(r => r.name === roleEntry) 48 | try { 49 | // If remove is true, remove the role from the user. Otherwise, add the role to the user. 50 | remove ? await member.roles.remove(role) : await member.roles.add(role) 51 | } catch (err) { 52 | await logError('RoleListener', 'Failed to add or remove role', err) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } catch (err) { 62 | await logError('RoleListener', 'Failed to execute the role action', err) 63 | } 64 | } 65 | 66 | // Given some information about a reaction addition, pass the info to roleAction to decide whether to add the role or not. 67 | const roleAdd = async (client, guildId, messageId, userId, emojiName) => { 68 | try { 69 | // Create a context object with all of the params to pass to roleAction. 70 | const context = { 71 | client, 72 | guildId, 73 | messageId, 74 | userId, 75 | emojiName 76 | } 77 | // Run the roleAction function with the context passed in. 78 | await roleAction(context) 79 | } catch (err) { 80 | await logError('RoleListener', 'Failed to add role', err) 81 | } 82 | } 83 | 84 | // Given some information about a reaction removal, pass the info to roleAction to decide whether to remove the role or not. 85 | const roleRm = async (client, guildId, messageId, userId, emojiName) => { 86 | try { 87 | // Create a context object with all of the params to pass to roleAction. 88 | const context = { 89 | client, 90 | guildId, 91 | messageId, 92 | userId, 93 | emojiName 94 | } 95 | // Run the roleAction function with the context passed in and remove set to true. 96 | await roleAction(context, true) 97 | } catch (err) { 98 | await logError('RoleListener', 'Failed to remove role', err) 99 | } 100 | } 101 | 102 | // Given a client, add a listener for message reactions that calls the proper functions for role listening. 103 | export const initReactionListener = async client => { 104 | try { 105 | // Add a listener for all events ('raw' type). 106 | client.on('raw', async event => { 107 | try { 108 | // Save event data. 109 | const { d: data } = event 110 | // If the event type is a reaction addition, run the roleAdd function. 111 | if (event.t === 'MESSAGE_REACTION_ADD') { 112 | // noinspection JSUnresolvedVariable 113 | await roleAdd( 114 | client, 115 | data.guild_id, 116 | data.message_id, 117 | data.user_id, 118 | data.emoji.name 119 | ) 120 | // Otherwise, if the type is a reaction removal, run the roleRm function. 121 | } else if (event.t === 'MESSAGE_REACTION_REMOVE') { 122 | // noinspection JSUnresolvedVariable 123 | await roleRm( 124 | client, 125 | data.guild_id, 126 | data.message_id, 127 | data.user_id, 128 | data.emoji.name 129 | ) 130 | } 131 | } catch (err) { 132 | await logError('RoleListener', 'Failed to handle reaction', err) 133 | } 134 | }) 135 | log('Init', 'Role reaction listener initialized!') 136 | } catch (err) { 137 | await logError('RoleListener', 'Failed to add raw listener', err) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # devmod 2 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fredxtech%2Fdevmod.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fredxtech%2Fdevmod?ref=badge_shield) 3 | 4 | > v5.1.0 5 | 6 | > A bot for moderating discord servers. Written by and for developers with modularity in mind. 7 | 8 | This was originally made to moderate the Devcord community on Discord, but you don't need to be a developer to use it. 9 | 10 | ## Setup 11 | ### Step 1 - Creating the Bot User 12 | First, go to the 13 | [Discord Developers](https://discordapp.com/developers/applications/me) 14 | page and create a new app. Name the app and give it a profile picture as you 15 | please. Afterwards, navigate to the "Bot" section. Click on "Add Bot" under the 16 | "Build-a-Bot" subsection. A popup will appear asking you to confirm the action. 17 | Once you confirm the action, the bot section will expand and you will be able to 18 | obtain your token under the "Token" section. This token will allow your bot to 19 | log in to Discord. 20 | 21 | ### Step 2 - Inviting the Bot to your Server 22 | Navigate to the "OAuth2" section and head to the "OAuth2 URL Generator" 23 | subsection. Under the "Scopes" section, click on "bot". A new section, titled 24 | "Bot Permissions", will appear. Select "Administrator. This will allow the bot 25 | to see all of the channels, ban users, etc. Afterwards, look at the "Scopes" 26 | section. At the bottom, there is a URL contained within a field. Copy and paste 27 | the URL into a new browser tab. A list of servers in which you have 28 | administrator permissions will appear. After choosing a server and clicking 29 | "Authorize", the bot will join the selected server. 30 | 31 | ### Step 3 - Setting Up the Host Machine 32 | Because the bot runs on [node](https://nodejs.org), you must have it installed 33 | on your host machine. 34 | 35 | If you are familiar with git, clone the devmod repo using `git clone`. 36 | Otherwise, download the repo as a zip and unpack it to any folder of 37 | your choosing. Open up the command line and `cd` into the folder that contains 38 | the cloned/unzipped code. 39 | 40 | Run `yarn` (or `npm install`) to install all of the bot's dependencies and then `yarn global add pm2` 41 | (or `npm install -g pm2`) to be able to run the bot in the background. The host machine 42 | is now configured to run the bot. All you need to do now is set up the config. 43 | 44 | Of course, you could also run the bot with other solutions, such as `forever`, `tmux`, or `screen`. 45 | 46 | ### Step 4 - Configuring the bot 47 | To configure the bot, edit `devmod.config.js`. The supported values are in the table below. 48 | 49 | Option | Default | Description 50 | ---|---|--- 51 | `botToken` | `undefined` | Discord API token for the bot. 52 | `guildID` | `undefined` | Discord ID of the server your bot is running on. 53 | `prefix` | `.` | Prefix for bot commands. 54 | `msgDeleteTime` | `join(__dirname, '..', '..', 'devmod.db')` | Amount of time in seconds to wait before deleting large help messages. 55 | `dbFile` | `devmod.db` | Absolute path for the database file. 56 | `autoBan` | `true` | Whether or not to enforce auto-banning after a specified number of warnings. 57 | `autoBanWarns` | `3` | Amount of warnings to warrant an auto-ban if enabled. 58 | `banMsgDelete` | `0` | Number of days of messages to delete when user is banned. 59 | `thanks` | `['thank', 'kudos']` | List of triggers for thanking users. 60 | `repCoin` | `undefined` | The emoji to prefix the thanks received message. `<:name:id>`. 61 | `channels.warn` | `'warnings'` | Channel to forward all warning confirmation messages. 62 | `channels.bans` | `'bans'` | Channel to forward all ban confirmation messages. 63 | `channels.reports` | `'reports'` | Channel to forward all user report messages. 64 | `channels.roles` | `'roles'` | Channel to send and listen to reactions for roles. 65 | `channels.info` | `'info'` | Channel to send the info to. 66 | `channels.crusade` | `'crusade'` | Channel to send notifications that the anti bot crusade has deleted a message. 67 | `channels.errors` | `'errors'` | Channel to log errors to. 68 | `roles.muted` | `'muted'` | Name of the role to apply to muted users. 69 | `roles.verified` | `'verified'` | Name of the role to apply to verified users. 70 | `Activities` | `[...]` | List of activities for the bot's status message. 71 | `tags` | `[...]` | List of tags for the `.tag` command. Each one is a discord embed object. Can be imported from a different file (config/tags.js). 72 | `approvedRoles` | `[...]` | List of lists of roles the reaction roles channel. Can be imported from a different file (config/approvedRoles.js). 73 | 74 | ### Step 5 - Running the Bot 75 | Now that you have all of the options set up, you can run the bot. To run it normally, use 76 | `yarn start` (`npm run start`). It can also be run with `node ./src/esm.js`. 77 | 78 | #### Verification bot 79 | There is also a verification bot that requires users to click a reaction on a message to agree to 80 | some rules in order to get a role added which you can configure to give them permission participate. 81 | 82 | You can start this bot with `yarn verify` (or `npm run verify`) or with `node ./src/utils/verify.js`. 83 | 84 | Now the bot is ready for use! 85 | 86 | > Just a quick note - the default tags in `config/tags.js`, the activities in 87 | `src/utils/default.config.js/activities.js`, and the roles in `config/approvedRoles.js` 88 | may not be specific to all servers, so make sure to check those out 89 | before running the bot. 90 | 91 | ## Usage 92 | The usage of this bot is described and documented on the [usage page](docs/usage.md). 93 | 94 | ## Future Ideas 95 | - Add configuration for whether to delete warns on a ban. 96 | - Add usage statistics to DB. 97 | 98 | ## Author 99 | **devmod** © [RedXTech](https://github.com/redxtech), Released under the [MIT](./LICENSE.md) License. 100 | 101 | 102 | ## License 103 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fredxtech%2Fdevmod.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fredxtech%2Fdevmod?ref=badge_large) -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2019 3 | * Contains all functions that interact with the database. 4 | */ 5 | 6 | import { 7 | Datastore 8 | } from 'nedb-async-await' 9 | import { 10 | logError 11 | } from './utils/log' 12 | 13 | const { 14 | dbFile 15 | } = require('./utils/config').default 16 | 17 | // Create and initialize the database using auto-loading and the configured filename. 18 | const db = new Datastore({ 19 | autoload: true, 20 | filename: dbFile 21 | }) 22 | 23 | // Object containing default values to return if there isn't an entry in the database. 24 | const defaultDBValues = { 25 | owner: 'RedXTech#3076', 26 | test: 'default value', 27 | reactions_message_ids: {}, 28 | info_message_ids: [] 29 | } 30 | 31 | // Given a key and a value, sets the 'key' document in the database to have a value of 'value'. 32 | export const setSetting = async (key, value) => { 33 | try { 34 | // Update database entries with a key of 'key' with the new values. Upsert: create new document if one doesn't already exist. 35 | await db.update({ 36 | key 37 | }, { 38 | key, 39 | value 40 | }, { 41 | upsert: true 42 | }) 43 | } catch (err) { 44 | await logError('DB', `setSetting: ${key}:${value} failed`, err) 45 | } 46 | } 47 | 48 | // Returns the value in the database for the given key if it exists, otherwise returns the default value from defaultDBValues. 49 | export const getSetting = async key => { 50 | try { 51 | // Search the database for any one document with a key of 'key' and save it to setting. 52 | const setting = await db.findOne({ 53 | key 54 | }) 55 | // If 'key' exists (setting !== null), setting has more than 0 entries, and has a value property return the value. Otherwise return the default value. 56 | if (setting !== null && Object.entries(setting).length > 0 && Object.prototype.hasOwnProperty.call(setting, 'value')) { 57 | return setting.value 58 | } else { 59 | return defaultDBValues[key] 60 | } 61 | } catch (err) { 62 | await logError('DB', `getSetting: ${key} failed`, err) 63 | } 64 | } 65 | 66 | // Given a user, reason, and staff member, pushes a warning into the database. 67 | export const addWarning = async (user, reason, staff) => { 68 | try { 69 | // Create the warning object. 70 | const warning = { 71 | reason, 72 | staff, 73 | timestamp: new Date() 74 | } 75 | 76 | // Create the push object and add the warning to it. 77 | const $push = {} 78 | $push[user] = warning 79 | 80 | // Update the database by pushing the warning to the user. 81 | await db.update({ 82 | key: 'warnings' 83 | }, { 84 | $push 85 | }, { 86 | upsert: true 87 | }) 88 | } catch (err) { 89 | await logError('DB', 'addWarning failed', err) 90 | } 91 | } 92 | 93 | // Given a user, returns a list of warnings from the database. 94 | export const getWarnings = async user => { 95 | try { 96 | // Pull the warnings from the database. 97 | const warnings = await db.findOne({ 98 | key: 'warnings' 99 | }) 100 | // If warnings isn't null, continue. 101 | if (warnings !== null) { 102 | // If warnings has the property 'user', return the property. 103 | if (Object.prototype.hasOwnProperty.call(warnings, user)) { 104 | return warnings[user] 105 | } else { 106 | return [] 107 | } 108 | } else { 109 | return [] 110 | } 111 | } catch (err) { 112 | await logError('DB', 'getWarning failed', err) 113 | } 114 | } 115 | 116 | // Given a user, removes all warnings from the database. 117 | export const clearWarnings = async user => { 118 | try { 119 | // Set the set object to an empty array. 120 | const $set = {} 121 | $set[user] = [] 122 | 123 | // Update the database by setting the user's warnings to an empty array. 124 | await db.update({ 125 | key: 'warnings' 126 | }, { 127 | $set 128 | }, { 129 | upsert: true 130 | }) 131 | } catch (err) { 132 | await logError('DB', 'clearWarning failed', err) 133 | } 134 | } 135 | 136 | // Given two users, add the second user to the list of thanks on the first. 137 | export const incrementThanks = async (thankee, thanker) => { 138 | try { 139 | // Create the push object and add the thanker ID to it. 140 | const $push = {} 141 | $push[thankee] = thanker 142 | 143 | // Update the database by pushing the thanks to the user. 144 | await db.update({ 145 | key: 'thanks' 146 | }, { 147 | $push 148 | }, { 149 | upsert: true 150 | }) 151 | 152 | try { 153 | // Return the number of thanks. 154 | return (await getThanks(thankee)).length 155 | } catch (err) { 156 | await logError('DB', 'Failed to get number of thanks', err) 157 | } 158 | } catch (err) { 159 | await logError('DB', 'incrementThanks failed', err) 160 | } 161 | } 162 | 163 | // Given a user, returns a list of thanks from the database. 164 | export const getThanks = async user => { 165 | try { 166 | // Pull the thanks from the database. 167 | const thanks = await db.findOne({ 168 | key: 'thanks' 169 | }) 170 | // If thanks isn't null, continue. 171 | if (thanks !== null) { 172 | // If thanks has the property 'user', return the property. 173 | if (Object.prototype.hasOwnProperty.call(thanks, user)) { 174 | return thanks[user] 175 | } else { 176 | return [] 177 | } 178 | } else { 179 | return [] 180 | } 181 | } catch (err) { 182 | await logError('DB', 'getThanks failed', err) 183 | } 184 | } 185 | 186 | // Returns the 10 most thanked users from the database. 187 | export const getTopThanks = async () => { 188 | try { 189 | // Pull the thanks from the database. 190 | const thanks = await db.findOne({ 191 | key: 'thanks' 192 | }) 193 | // If thanks isn't null, continue. 194 | if (thanks !== null) { 195 | // Filter out database fields, sort by amount of thanks (length of thanks 196 | // array), and take the first 10 elements. 197 | return [...Object.entries(thanks)] 198 | .filter(user => !['key', '_id'].includes(user[0])) 199 | .sort((a, b) => (a[1].length > b[1].length) ? -1 : ((b[1].length > a[1].length) ? 1 : 0)) 200 | .slice(0, 10) 201 | } else { 202 | return [] 203 | } 204 | } catch (err) { 205 | await logError('DB', 'getThanks failed', err) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /config/tags.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gabe Dunn 2018 3 | * Definitions of available tags. 4 | */ 5 | 6 | import { blue, green, orange, purple, red, yellow } from '../src/utils/colours' 7 | 8 | export const tags = { 9 | lighthouse: { 10 | title: 'Website Performance and Accessibility Test', 11 | color: red, 12 | description: 'A Lighthouse Audit is an industry standard performance and accessibility test developed by Google.' + 13 | 'https://web.dev/measure' 14 | }, 15 | oss: { 16 | title: 'First Time Contributors', 17 | color: blue, 18 | description: [ 19 | 'Contributing to reputable open-source projects is a great way to build experience in the tech industry without doing free work for clients.', 20 | 'https://yourfirstpr.github.io/', 21 | 'https://www.firsttimersonly.com/' 22 | ].join('\n') 23 | }, 24 | bot: { 25 | title: 'Bot Channel', 26 | color: blue, 27 | description: 'Please use commands related to the bot in the' + 28 | ' <#271293371438333952> channel. Thanks.' 29 | }, 30 | ask: { 31 | title: 'Asking to Ask', 32 | color: red, 33 | description: 'Instead of asking to ask, ask your question instead.' + 34 | ' People can help you better if they know your question.\n\n' + 35 | 'Example: "Hey can anyone help me with some JS?" & "Anyone good with' + 36 | ' JS?" => "I\'m having trouble adding a class to a div using JS. Can I' + 37 | ' have some help?"\n\n' + 38 | '[How do I ask a good question?](https://stackoverflow.com/help/how-to-ask)' 39 | }, 40 | ideas: { 41 | title: 'Project Ideas', 42 | color: blue, 43 | description: [ 44 | 'Personal Blog', 45 | 'IRC bot', 46 | 'Echo Server', 47 | 'Network proxy', 48 | 'Read CPU fan sensors', 49 | 'Make a Hackernews/reddit client (they have a nice JSON API)', 50 | 'Rendering something in a window (Animated Sprite for example)', 51 | 'Making a desktop app with GTK or QT', 52 | 'Make a JSON editor GUI', 53 | 'Create a pokedev with the Pokemon API', 54 | 'Headless CMS', 55 | 'Build an Interpreter', 56 | 'Weather App' 57 | ].map(idea => '- ' + idea).join('\n') 58 | }, 59 | jobs: { 60 | title: 'Jobs', 61 | color: blue, 62 | description: 'No job offers allowed on this server please. Free work is' + 63 | ' allowed, but not many people will want to work for free.' 64 | }, 65 | whynojobs: { 66 | title: 'Why there are no Jobs', 67 | color: blue, 68 | description: 'In the past, there have been multiple cases of people' + 69 | ' being scammed when taking a job with someone else on this server. We' + 70 | ' do not want to be associated with that, so we have removed jobs' + 71 | ' entirely.\n\nThanks for understanding.' 72 | }, 73 | sfw: { 74 | title: 'SFW (Safe for Work)', 75 | color: yellow, 76 | description: 'Please keep the messages safe for work here.' 77 | }, 78 | channels: { 79 | title: 'Posting in Multiple Channels', 80 | color: orange, 81 | description: 'It would be greatly appreciated if you kept your posting' + 82 | ' to the channel designated for that topic. Additionally, please refrain' + 83 | ' from posting the same question in multiple channels.' 84 | }, 85 | cs: { 86 | title: 'Christian Server', 87 | color: red, 88 | image: { url: 'https://cdn.discordapp.com/attachments/174075418410876928/377425219872096256/maxresdefault.png' } 89 | }, 90 | canudont: { 91 | title: 'Can U Dont', 92 | color: blue, 93 | image: { url: 'https://cdn.discordapp.com/attachments/174075418410876928/428989988286365696/can_u_dont.jpg' } 94 | }, 95 | code: { 96 | title: 'Use Code Blocks', 97 | color: blue, 98 | fields: [ 99 | { 100 | name: 'To directly post code into Discord, type:', 101 | value: '\\`\\`\\`\n// code\nconsole.log(\'I have no' + 102 | ' language.\')\n\\`\\`\\`' 103 | }, { 104 | name: 'For syntax highlighting replace lang with the language (js,' + 105 | ' css, html, etc.):', 106 | value: '\\`\\`\\`js\nconsole.log(\'hello this is js\')\n\\`\\`\\`' 107 | }, { 108 | name: 'How the first will look:', 109 | value: '```LANGUAGE\n// code\nconsole.log(\'I have no language.\')\n```' 110 | }, { 111 | name: 'How it will look with highlighting:', 112 | value: '```js\nconsole.log(\'hello this is js\')\n```' 113 | }, { 114 | name: 'More Information', 115 | value: 'You can find more information on code blocks from [Discord Support](https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-)' 116 | } 117 | ] 118 | }, 119 | pen: { 120 | title: 'Post Your Code', 121 | color: blue, 122 | description: 'Post your code online. Here are some options:', 123 | fields: [ 124 | { 125 | name: 'CodePen (https://codepen.io/)', 126 | value: 'CodePen is a nice place to post your code online.', 127 | inline: true 128 | }, { 129 | name: 'JSFiddle (https://jsfiddle.net/)', 130 | value: 'JSFiddle is also a nice place to post your code online.', 131 | inline: true 132 | }, 133 | { 134 | name: 'GitHub (https://github.com)', 135 | value: 'GitHub is a great place to post the code for full projects.', 136 | inline: true 137 | }, 138 | { 139 | name: 'CodeSandbox (https://codesandbox.io/)', 140 | value: 'CodeSandbox is a great place to play around with a full node' + 141 | ' environment without having it locally.', 142 | inline: true 143 | }] 144 | }, 145 | gbp: { 146 | title: 'Good Boye Points', 147 | color: green, 148 | description: 'Good Boye Points (GBP) are the best way to tell if a user' + 149 | ' has been a good addition to the server. You can get GBP only by the' + 150 | ' generosity of the overlords (Admins & Mods). See how many you have with' + 151 | ' `.gbp show`. Find the best people on the server with `.gbp top`' 152 | }, 153 | roadmap: { 154 | title: 'Developer Roadmap', 155 | url: 'https://github.com/kamranahmedse/developer-roadmap', 156 | color: blue, 157 | description: 'This is a very nice outline on the general technologies' + 158 | ' recommended for the path of your choosing. Use the as an outline, and' + 159 | ' not as the sole source of authority. Make your own decisions as well.' 160 | }, 161 | nice: { 162 | title: 'Nice', 163 | color: blue, 164 | image: { url: 'https://cdn.discordapp.com/attachments/287200463382642689/433326230495035412/1447912710960.jpg' } 165 | }, 166 | editor: { 167 | title: 'IDEs & Text Editors', 168 | color: blue, 169 | description: 'There are many different ways to edit code, from code' + 170 | ' editors to Integrated Development Environments ("IDEs"). Here are some' + 171 | ' differences between the two and some examples of each:', 172 | fields: [ 173 | { 174 | name: 'IDEs', 175 | value: 'IDEs (Integrated Development Environment) are programs that' + 176 | ' include a code editor, but also integrations with various other' + 177 | ' development tools (linters, version control,' + 178 | ' intellisense/autocomplete, automatic refactoring, database' + 179 | ' management, etc.).' 180 | }, 181 | { 182 | name: 'Code Editors', 183 | value: 'Code editors are text editors that usually include syntax' + 184 | ' highlighting, simple project management, and other helpful code' + 185 | ' editing tools.' 186 | }, 187 | { 188 | name: 'WebStorm/PHPStorm (or any other JetBrains Product)', 189 | value: 'These IDEs, as they have a full suite of tools for' + 190 | ' development. Additionally they have a plugin system for anything' + 191 | ' that they do not automatically include. [Webstorm Download](https://www.jetbrains.com/webstorm/), [PHPStorm Download](https://www.jetbrains.com/phpstorm/)' 192 | }, 193 | { 194 | name: 'Visual Studio', 195 | value: 'Visual studio is a full IDE made by microsoft. It works well' + 196 | ' with .NET based languages, as they are made by the same people.' + 197 | ' They also include a plugin system. [Download](https://visualstudio.microsoft.com/)' 198 | }, 199 | { 200 | name: 'NetBeans', 201 | value: 'I honestly don\'t know much about NetBeans, having never' + 202 | ' used it. If you know more make a PR on the DevMod repo. I do know' + 203 | ' that it is a Java IDE. [Download](https://netbeans.org/)' 204 | }, 205 | { 206 | name: 'Atom', 207 | value: 'Atom is a code editor based on web technology. It\'s made by' + 208 | ' GitHub, and has a massive community, with plugins for everything. [Download](https://atom.io/)' 209 | }, 210 | { 211 | name: 'VS Code', 212 | value: 'VS Code is another editor based off of web technology, but' + 213 | ' is better optimized and runs faster. This is built by microsoft' + 214 | ' and has a large set of plugins as well. [Download](https://code.visualstudio.com/)' 215 | }, 216 | { 217 | name: 'Sublime Text', 218 | value: 'Sublime text starts off as a nice small and fast editor.' + 219 | ' It\'s the fastest text editor that I\'ve seen. There is also a' + 220 | ' wide selection of plugins. [Download](https://www.sublimetext.com/)' 221 | }, 222 | { 223 | name: 'Vim', 224 | value: 'Vim is a command line text editor with plugins that can do' + 225 | ' pretty much anything. It is largely popular, but has a learning' + 226 | ' curve before you can be productive in it. [Download](https://www.vim.org/)' 227 | }, 228 | { 229 | name: 'Brackets', 230 | value: 'Brackets is also based on web tech, and has a live reload' + 231 | ' feature that allows you to view your website live. Many other' + 232 | ' editors have this feature, but few work as smoothly as this one. [Download](http://brackets.io/)' 233 | }, 234 | { 235 | name: 'Notepad++', 236 | value: 'Notepad++ is a very lightweight code editor, with a lot ' + 237 | 'of plugins for everything you can think of. It is beyond excellent ' + 238 | 'for quick edit or doodle work. [Download](https://notepad-plus-plus.org/)' 239 | } 240 | ] 241 | }, 242 | framework: { 243 | title: 'Frameworks (& Libraries)', 244 | color: green, 245 | description: 'There is a large debate as to which framework is the best' + 246 | ' for your webapp, and this is just an overview of the top contenders.', 247 | fields: [ 248 | { 249 | name: 'Vue', 250 | value: 'Vue is a web framework that is easy to learn and use while' + 251 | ' being quite powerful. It has been described as taking the best' + 252 | ' from both React and Angular, and combining them. They have a large' + 253 | ' community, and it\'s quite fun to use. It has separation of' + 254 | ' concerns while being all in the same file, and has a large' + 255 | ' community of people and plugins. There are projects like ream and' + 256 | ' nuxt for SSR, and it\'s lightweight (smaller file than jQuery).' + 257 | ' Vue also has nativescript-vue and weex to write mobile apps using' + 258 | ' the same Vue syntax.' 259 | }, 260 | { 261 | name: 'React', 262 | value: 'React is not a framework, and is rather a library. It is' + 263 | ' backed by Facebook, and has a lot of useful features. It uses JSX,' + 264 | ' which is writing your HTML (or XML, actually) in javascript, and' + 265 | ' makes for a bit of a learning curve, but in the short time it' + 266 | ' takes to learn it\'s interesting to use. React also has React' + 267 | ' native which allows mobile development using the same syntax and' + 268 | ' application logic as your rect webapp.' 269 | }, 270 | { 271 | name: 'Angular 5', 272 | value: 'I do not know as much about Angular as I do Vue and React,' + 273 | ' (please make a PR if you have more knowledge), I do know that to' + 274 | ' use Angular, it\'s almost required to use typescript. Angular is a' + 275 | ' full MVC, so it provided the entire suite of tools including' + 276 | ' routing, state management, etc. This does lead to a more' + 277 | ' opinionated way of doing things, but makes making decisions a lot' + 278 | ' easier.' 279 | } 280 | ], 281 | footer: { 282 | text: 'My personal recommendation is Vue but definitely try out the' + 283 | ' others and use what you prefer.' 284 | } 285 | }, 286 | invite: { 287 | title: 'Invite Link', 288 | color: blue, 289 | url: 'https://discord.me/devcord', 290 | description: 'You can invite people with' + 291 | ' this link: [https://discord.me/devcord](https://discord.me/devcord).' 292 | }, 293 | doubt: { 294 | title: '[x] Doubt', 295 | color: blue, 296 | image: { url: 'https://media.discordapp.net/attachments/174075418410876928/435482310612221993/doubt.jpg?width=400&height=228' } 297 | }, 298 | fasy: { 299 | color: blue, 300 | image: { url: 'https://media.discordapp.net/attachments/174075418410876928/435887256843321354/loamy.jpg?width=401&height=84' } 301 | }, 302 | dog: { 303 | color: blue, 304 | image: { url: 'https://cdn.discordapp.com/attachments/174075418410876928/436958508039012379/unknown.png' } 305 | }, 306 | sqlinjection: { 307 | title: 'Bind your parameters to prevent SQL injection', 308 | color: blue, 309 | description: 'Don\'t get hacked, use prepared statements as explained here:', 310 | fields: [ 311 | { 312 | name: 'PDO', 313 | value: '[Prepared statements with PDO](https://secure.php.net/manual/en/pdo.prepared-statements.php).' 314 | }, { 315 | name: 'Mysqli', 316 | value: '[Prepared statements with mysqli](https://secure.php.net/manual/en/mysqli.quickstart.prepared-statements.php).' 317 | } 318 | ] 319 | }, 320 | template: { 321 | title: 'Template', 322 | color: blue, 323 | description: 'The information needed to post your project in this channel.', 324 | fields: [ 325 | { 326 | name: 'Project Description', 327 | value: 'A short description of the project.' 328 | }, 329 | { 330 | name: 'Time Commitment', 331 | value: 'How long will this project take? How much time can someone expect to commit to this project? Is there a deadline or desired date of completion?' 332 | }, 333 | { 334 | name: 'Languages', 335 | value: 'List all relevant languages.' 336 | }, 337 | { 338 | name: 'Skill Level', 339 | value: 'What skill level are you looking for? Are you willing to work with junior developers or prefer middle/senior level developers?' 340 | }, 341 | { 342 | name: 'Communication', 343 | value: 'What is the preferred method of communication? If a developer is interested, how should they contact you? Do NOT give out email addresses or other personally identifiable information.' 344 | } 345 | ] 346 | }, 347 | flex: { 348 | title: 'Flexbox', 349 | color: purple, 350 | description: 'The Flexible Box Module, usually referred to as flexbox, was' + 351 | ' designed as a one-dimensional layout model, and as a method that could' + 352 | ' offer space distribution between items in an interface and powerful alignment capabilities.', 353 | fields: [ 354 | { 355 | name: 'MDN web docs', 356 | value: '[Flexbox documentation.](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox)' 357 | }, 358 | { 359 | name: 'Flexbox quickstart guide', 360 | value: '[Learn flexbox in minutes! Easy, and powerful!](https://css-tricks.com/snippets/css/a-guide-to-flexbox/)' 361 | }, 362 | { 363 | name: 'Flexbox Froggy', 364 | value: '[Get frogs to where they need to be with the magic of flexbox!](https://flexboxfroggy.com)' 365 | }, 366 | { 367 | name: 'Flexbox Defense', 368 | value: '[Blast enemies into nothingness with defensive towers positioned by flexbox!](http://www.flexboxdefense.com/)' 369 | } 370 | ] 371 | }, 372 | fetch: { 373 | title: 'JavaScript Fetch API', 374 | color: purple, 375 | description: 'The Fetch API provides an interface for fetching resources' + 376 | ' (including across the network). It will seem familiar to anyone who has used' + 377 | ' XMLHttpRequest, but the new API provides a more powerful and flexible feature set.', 378 | fields: [ 379 | { 380 | name: 'MDN web docs', 381 | value: '[Fetch API documentation.](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)' 382 | }, 383 | { 384 | name: 'Fetch HTTPlib', 385 | value: '[A Fetch library that assists with learning how to use fetch.](https://codepen.io/papaamazon-the-flexboxer/project/editor/DWwjNM)' 386 | }, 387 | { 388 | name: 'Fetch tutorial', 389 | value: '[A tutorial that shows how to use fetch to get data from an API.](https://scotch.io/tutorials/how-to-use-the-javascript-fetch-api-to-get-data)' 390 | }, 391 | { 392 | name: 'MDN fetch usage example', 393 | value: '[A very in depth write up write-up on how to use fetch.](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch_)' 394 | } 395 | ] 396 | }, 397 | grid: { 398 | title: 'CSS Grid', 399 | color: purple, 400 | description: 'CSS Grid Layout excels at dividing a page into major' + 401 | 'regions or defining the relationship in terms of size, position, and' + 402 | 'layer, between parts of a control built from HTML primitives. \n' + 403 | 'Like tables, grid layout enables an author to align elements into' + 404 | 'columns and rows. However, many more layouts are either possible or' + 405 | 'easier with CSS grid than they were with tables. For example, a grid' + 406 | 'container\'s child elements could position themselves so they actually' + 407 | 'overlap and layer, similar to CSS positioned elements.', 408 | fields: [ 409 | { 410 | name: 'MDN Web Docs', 411 | value: '[CSS Grid Layout Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout)' 412 | }, 413 | { 414 | name: 'CSS Grid Quickstart Guide', 415 | value: '[A quick and dirty guide to CSS Grid.](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout)' 416 | }, 417 | { 418 | name: 'CSS Grid Garden', 419 | value: '[A game for learning CSS Grid.](http://cssgridgarden.com/)' 420 | }, 421 | { 422 | name: 'CSS Grid Video Series', 423 | value: '[A CSS Grid video series by Wes Bos.](https://cssgrid.io/)' 424 | } 425 | ] 426 | }, 427 | markdown: { 428 | title: 'Markdown Formatting', 429 | color: blue, 430 | description: 'A guide on markdown formatting in discord.', 431 | url: 'https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-' 432 | }, 433 | thanks: { 434 | title: 'Thanking people', 435 | color: blue, 436 | description: 'Thanking someone is a nice way to appreciate the help they have given you' + 437 | ' for solving a problem or explaining something. ' + 438 | ' You can thank someone by typing ``thanks @user#id``.' + 439 | ' Additionally any sentence with the word "thank" and a user tag will thank the user.' 440 | }, 441 | design: { 442 | title: 'Design Articles', 443 | color: purple, 444 | description: 'Looking to improve your design skills? Here are some helpful' + 445 | 'articles.', 446 | fields: [ 447 | { 448 | name: '[7 Tips for Cheating at Design](https://medium.com/refactoring-ui/7-practical-tips-for-cheating-at-design-40c736799886)' 449 | }, 450 | { 451 | name: '[10 Design Rules for Programmers](https://thoughtspile.github.io/2018/12/17/design-crash-course/)' 452 | }, 453 | { 454 | name: '[The Secret Heroes of UX Design](https://link.medium.com/XrKhnmTAsT)' 455 | }, 456 | { 457 | name: '[Color Pallete](https://refactoringui.com/previews/building-your-color-palette/)' 458 | }, 459 | { 460 | name: '[Color Usage](https://www.smashingmagazine.com/2017/09/vibrant-colors-apps-websites/)' 461 | }, 462 | { 463 | name: '[Using Gradients](https://www.smashingmagazine.com/2018/01/gradients-user-experience-design/)' 464 | } 465 | ] 466 | }, 467 | research: { 468 | color: red, 469 | title: 'Research before asking', 470 | description: 'Search, research, and keep track of what you find. Even if you ' + 471 | 'don\'t find a useful answer elsewhere, including links to related questions ' + 472 | 'that haven\'t helped can help others better understand your question.' 473 | } 474 | } 475 | --------------------------------------------------------------------------------