├── .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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 | [](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 | [](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 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
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 |
--------------------------------------------------------------------------------