├── .env.sample ├── .eslintrc.json ├── .gitattributes ├── .github ├── dependabot.yml ├── greetings.yml ├── linters │ └── .eslintrc.yml ├── stale.yml └── workflows │ ├── codeql.yml │ ├── docker-develop.yml │ ├── docker.yml │ └── linter.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── package.json └── src ├── bot.js ├── commands ├── debug │ ├── args-info.js │ ├── eval.js │ └── reload.js ├── fun │ └── dog.js ├── general │ ├── avatar.js │ ├── help.js │ ├── invite.js │ ├── ping.js │ └── stats.js └── moderation │ ├── kick.js │ ├── prune.js │ └── server.js ├── dashboard ├── index.js ├── public │ ├── css │ │ └── dashboard.css │ ├── img │ │ └── logo.png │ └── js │ │ └── dashboard.js ├── routes │ └── index.js └── views │ ├── error.ejs │ ├── index.ejs │ └── partials │ ├── footer.ejs │ └── head.ejs ├── events ├── guildCreate.js ├── guildDelete.js ├── guildMemberAdd.js ├── guildMemberRemove.js ├── message.js └── ready.js ├── index.js └── utils ├── functions.js ├── logger.js └── register.js /.env.sample: -------------------------------------------------------------------------------- 1 | # .env 2 | 3 | TZ=UTC 4 | PUID=99 5 | PGID=100 6 | UMASK=000 7 | 8 | DATABASE_HOST= 9 | DATABASE_PORT=3306 10 | DATABASE_NAME= 11 | DATABASE_USERNAME= 12 | DATABASE_PASSWORD= 13 | 14 | DISCORD_OWNER_ID=143720221977477120 15 | DISCORD_CLIENT_ID= 16 | DISCORD_CLIENT_SECRET= 17 | DISCORD_BOT_TOKEN= 18 | DISCORD_OAUTH2_URL=http://localhost:8080/callback 19 | 20 | WATCHDOG_PREFIX=? 21 | WATCHDOG_PORT=8080 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2019 9 | }, 10 | "rules": { 11 | "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], 12 | "comma-dangle": ["error", "always-multiline"], 13 | "comma-spacing": "error", 14 | "comma-style": "error", 15 | "curly": ["error", "multi-line", "consistent"], 16 | "dot-location": ["error", "property"], 17 | "handle-callback-err": "off", 18 | "indent": ["error", 2], 19 | "max-nested-callbacks": ["error", { "max": 4 }], 20 | "max-statements-per-line": ["error", { "max": 2 }], 21 | "no-console": "off", 22 | "no-empty-function": "error", 23 | "no-floating-decimal": "error", 24 | "no-inline-comments": "error", 25 | "no-lonely-if": "error", 26 | "no-multi-spaces": "error", 27 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], 28 | "no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }], 29 | "no-trailing-spaces": ["error"], 30 | "no-var": "error", 31 | "object-curly-spacing": ["error", "always"], 32 | "prefer-const": "error", 33 | "quotes": ["error", "single"], 34 | "semi": ["error", "always"], 35 | "space-before-blocks": "error", 36 | "space-before-function-paren": ["error", { 37 | "anonymous": "never", 38 | "named": "never", 39 | "asyncArrow": "always" 40 | }], 41 | "space-in-parens": "error", 42 | "space-infix-ops": "error", 43 | "space-unary-ops": "error", 44 | "spaced-comment": "error", 45 | "yoda": "error" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Define the default behavior where GitHub attempts to detect the file type and set the line endings correctly 2 | * text=auto 3 | 4 | # Define line endings to be unix (LF) for GitHub files 5 | .gitattributes text eol=lf 6 | .gitignore text eol=lf 7 | .gitconfig text eol=lf 8 | LICENSE text eol=lf 9 | *.md text eol=lf 10 | 11 | # Define line endings to be unix (LF) for source code 12 | *.php text eol=lf 13 | *.css text eol=lf 14 | *.sass text eol=lf 15 | *.scss text eol=lf 16 | *.less text eol=lf 17 | *.styl text eol=lf 18 | *.js text eol=lf 19 | *.coffee text eol=lf 20 | *.json text eol=lf 21 | *.htm text eol=lf 22 | *.html text eol=lf 23 | *.xml text eol=lf 24 | *.svg text eol=lf 25 | *.txt text eol=lf 26 | *.ini text eol=lf 27 | *.inc text eol=lf 28 | *.pl text eol=lf 29 | *.rb text eol=lf 30 | *.py text eol=lf 31 | *.scm text eol=lf 32 | *.sql text eol=lf 33 | *.sh text eol=lf 34 | *.bat text eol=lf 35 | *.txt text eol=lf 36 | *.tmpl text eol=lf 37 | Dockerfile text eol=lf 38 | 39 | # Define binary files to be excluded from modification 40 | *.png binary 41 | *.jpg binary 42 | *.jpeg binary 43 | *.gif binary 44 | *.ico binary 45 | *.flv binary 46 | *.fla binary 47 | *.swf binary 48 | *.gz binary 49 | *.tar binary 50 | *.rar binary 51 | *.zip binary 52 | *.7z binary 53 | *.egg binary 54 | *.ttf binary 55 | *.eot binary 56 | *.woff binary 57 | *.pyc binary 58 | *.pdf binary 59 | *.db binary 60 | *.xz binary 61 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Check for updates to GitHub Actions every weekday 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | issue-message: 'Thanks for opening your first issue here!' 12 | pr-message: 'Thanks for opening this pull request!' 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/linters/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: eslint:recommended 2 | 3 | env: 4 | node: true 5 | es6: true 6 | 7 | parserOptions: 8 | ecmaVersion: 2019 9 | 10 | rules: { 11 | brace-style: [error, stroustrup, { allowSingleLine: true }], 12 | comma-dangle: [error, always-multiline], 13 | comma-spacing: error, 14 | comma-style: error, 15 | curly: [error, multi-line, consistent], 16 | dot-location: [error, property], 17 | handle-callback-err: off, 18 | indent: [error, 2], 19 | max-nested-callbacks: [error, { max: 4 }], 20 | max-statements-per-line: [error, { max: 2 }], 21 | no-console: off, 22 | no-empty-function: error, 23 | no-floating-decimal: error, 24 | no-inline-comments: error, 25 | no-lonely-if: error, 26 | no-multi-spaces: error, 27 | no-multiple-empty-lines: [error, { max: 2, maxEOF: 1, maxBOF: 0 }], 28 | no-shadow: [error, { allow: [err, resolve, reject] }], 29 | no-trailing-spaces: [error], 30 | no-var: error, 31 | object-curly-spacing: [error, always], 32 | prefer-const: error, 33 | quotes: [error, single], 34 | semi: [error, always], 35 | space-before-blocks: error, 36 | space-before-function-paren: [error, { 37 | anonymous: never, 38 | named: never, 39 | asyncArrow: always 40 | }], 41 | space-in-parens: error, 42 | space-infix-ops: error, 43 | space-unary-ops: error, 44 | spaced-comment: error, 45 | yoda: error 46 | } 47 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v1 12 | with: 13 | stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions." 14 | stale-pr-message: "This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions." 15 | stale-issue-label: 'no-issue-activity' 16 | stale-pr-label: 'no-pr-activity' 17 | days-before-stale: 30 18 | days-before-close: 365 19 | exempt-issue-labels: 'awaiting-approval,work-in-progress' 20 | exempt-pr-labels: 'awaiting-approval,work-in-progress' 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ main ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ main ] 21 | schedule: 22 | - cron: '39 7 * * 4' 23 | 24 | jobs: 25 | analyze: 26 | name: CodeQL Analysis 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'javascript' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 34 | # Learn more... 35 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v3 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v3 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v3 69 | -------------------------------------------------------------------------------- /.github/workflows/docker-develop.yml: -------------------------------------------------------------------------------- 1 | name: Docker Develop 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | 7 | jobs: 8 | build: 9 | name: Create Docker Container 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v3 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v3 20 | 21 | - name: Login to GitHub Container Registry 22 | uses: docker/login-action@v3 23 | with: 24 | registry: ghcr.io 25 | username: ${{ github.repository_owner }} 26 | password: ${{ secrets.CR_PAT }} 27 | 28 | - name: Build and push 29 | uses: docker/build-push-action@v6 30 | with: 31 | push: true 32 | tags: ghcr.io/groveld/watchdog:develop 33 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Main 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | name: Create Docker Container 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v3 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v3 20 | 21 | - name: Login to GitHub Container Registry 22 | uses: docker/login-action@v3 23 | with: 24 | registry: ghcr.io 25 | username: ${{ github.repository_owner }} 26 | password: ${{ secrets.CR_PAT }} 27 | 28 | - name: Build and push 29 | uses: docker/build-push-action@v6 30 | with: 31 | push: true 32 | tags: ghcr.io/groveld/watchdog:latest 33 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | linter: 9 | name: Lint Code Base 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Code 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Lint Code Base 19 | uses: github/super-linter@v7 20 | env: 21 | VALIDATE_ALL_CODEBASE: false 22 | DEFAULT_BRANCH: main 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | VALIDATE_JAVASCRIPT_STANDARD: false 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific # 2 | #################### 3 | .env 4 | config 5 | node_modules 6 | package-lock.json 7 | npm-debug.log 8 | 9 | # OS generated files # 10 | ###################### 11 | .DS_Store 12 | .DS_Store? 13 | ._* 14 | .Spotlight-V100 15 | .Trashes 16 | ehthumbs.db 17 | Thumbs.db 18 | 19 | # Packages # 20 | ############ 21 | *.7z 22 | *.dmg 23 | *.gz 24 | *.iso 25 | *.jar 26 | *.rar 27 | *.tar 28 | *.zip 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.13-slim 2 | 3 | LABEL maintainer="Martin Groeneveld " 4 | 5 | RUN mkdir -p /usr/src/watchdog/app /usr/src/watchdog/config 6 | 7 | WORKDIR /usr/src/watchdog/app 8 | 9 | COPY package*.json ./ 10 | 11 | RUN npm install --production 12 | 13 | COPY --chown=node:node src ./ 14 | 15 | VOLUME /usr/src/watchdog/config 16 | 17 | EXPOSE 8080/tcp 18 | 19 | USER node 20 | 21 | CMD ["node", "index.js"] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Martin Groeneveld 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discord-watchdog 2 | 3 | ![Linter](https://github.com/groveld/discord-watchdog/workflows/Linter/badge.svg) 4 | ![CodeQL](https://github.com/groveld/discord-watchdog/workflows/CodeQL/badge.svg) 5 | ![Docker](https://github.com/groveld/discord-watchdog/workflows/Docker/badge.svg) 6 | 7 | A General Purpose Node.js Discord Bot. 8 | 9 | This is my personal bot code, and though I put it online with this nice readme for you to install and use it, I can't cater to other people's requests or desires. I update this when I feel like it with the features I want and I make breaking changes without consideration for other people's installations. 10 | 11 | [Join Discord Server](https://discord.gg/2dV4xRN) 12 | 13 | [Invite Discord-Bot](https://discordapp.com/oauth2/authorize?&client_id=431381992081326081&scope=bot) 14 | 15 | [Bot Control Panel](https://watchdog.groveld.com/) 16 | 17 | ## Features 18 | 19 | - [x] `Auto-ban` user if @mention more than 25 users in a single message. - Completed by [liam.#9999](https://github.com/LiamTownsley) 20 | - [ ] `Polls` 21 | - [ ] `Music player` 22 | - [x] `!ping` 23 | - [x] `!eval` 24 | - [x] `!help` 25 | - [x] `!invite` 26 | - [x] `!clean` 27 | - [x] `!stats` 28 | 29 | ## ToDo 30 | 31 | - Add and remove roles to users if Moderator or Admin. 32 | - Assign roles to users after being online for an X amount time. 33 | - Auto execute commands at defined times/intervals. (Birthday messages, server lockdown) 34 | 35 | ## RUN 36 | 37 | ### Requirements 38 | 39 | - Git 40 | - Node.js 41 | - NPM 42 | - MySQL 43 | 44 | ***Windows Only*** 45 | 46 | ```npm install --global --production --vs2015 --add-python-to-path windows-build-tools``` 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | watchdog: 4 | build: . 5 | image: ghcr.io/groveld/watchdog 6 | container_name: watchdog 7 | restart: always 8 | env_file: 9 | - .env 10 | networks: 11 | - backend 12 | ports: 13 | - "8080:8080" 14 | 15 | networks: 16 | backend: 17 | driver: bridge 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-watchdog", 3 | "version": "0.2.1", 4 | "description": "A General Purpose Discord.js Bot.", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "lint": "eslint src", 9 | "dev": "cd src && nodemon -r dotenv/config index.js dotenv_config_path=../.env", 10 | "dev-dash": "cd src/dashboard && nodemon -r dotenv/config index.js dotenv_config_path=../../.env", 11 | "start": "cd src && node index.js" 12 | }, 13 | "keywords": [ 14 | "nodejs", 15 | "discordjs", 16 | "discordapp", 17 | "discord", 18 | "bot" 19 | ], 20 | "author": "Martin Groeneveld (https://www.groveld.com)", 21 | "contributors": [ 22 | "Liam Townsley (https://liamtownsley.me)" 23 | ], 24 | "license": "ISC", 25 | "homepage": "https://github.com/groveld/discord-watchdog", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/groveld/discord-watchdog.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/groveld/discord-watchdog/issues" 32 | }, 33 | "devDependencies": { 34 | "dotenv": "^8.2.0", 35 | "eslint": "^6.8.0", 36 | "nodemon": "^2.0.3" 37 | }, 38 | "dependencies": { 39 | "body-parser": "^1.18.3", 40 | "chalk": "^2.3.2", 41 | "discord.js": "^12.1.1", 42 | "ejs": "^3.1.7", 43 | "express": "^4.16.3", 44 | "express-passport": "^0.1.0", 45 | "express-session": "^1.15.6", 46 | "helmet": "^3.13.0", 47 | "mariadb": "^2.3.1", 48 | "marked": "^4.0.10", 49 | "moment": "^2.21.0", 50 | "moment-duration-format": "^2.2.2", 51 | "passport": "^0.6.0", 52 | "passport-discord": "^0.1.3", 53 | "sequelize": ">=5.3.0", 54 | "winston": "^2.4.1", 55 | "node-fetch": "^2.6.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | const { Client, Collection } = require('discord.js'); 2 | const { registerCommands, registerEvents } = require('./utils/register'); 3 | const logger = require('./utils/logger'); 4 | 5 | const client = new Client(); 6 | client.commands = new Collection(); 7 | 8 | (async () => { 9 | await registerEvents(client, '../events'); 10 | await registerCommands(client, '../commands'); 11 | 12 | client.login(process.env.DISCORD_BOT_TOKEN) 13 | .catch(err => logger.error(err.message)); 14 | })(); 15 | 16 | client.on('error', err => logger.error(err)); 17 | 18 | client.on('warn', err => logger.warn(err)); 19 | 20 | client.on('debug', err => logger.debug(err)); 21 | -------------------------------------------------------------------------------- /src/commands/debug/args-info.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'args-info', 3 | description: 'Information about the arguments provided.', 4 | args:true, 5 | execute(message, args) { 6 | if (args[0] === 'foo') { 7 | return message.channel.send('bar'); 8 | } 9 | 10 | message.channel.send(`First argument: ${args[0]}`); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/commands/debug/eval.js: -------------------------------------------------------------------------------- 1 | /** 2 | * eval.js 3 | * 4 | * The EVAL command will execute **ANY** arbitrary javascript code given to it. 5 | * THIS IS PERMISSION LEVEL 10 FOR A REASON! It's perm level 10 because eval 6 | * can be used to do **anything** on your machine, from stealing information to 7 | * purging the hard drive. DO NOT LET ANYONE ELSE USE THIS 8 | * 9 | * However it's, like, super ultra useful for troubleshooting and doing stuff 10 | * you don't want to put in a command. 11 | */ 12 | 13 | module.exports = { 14 | name: 'eval', 15 | description: 'Evaluates arbitrary javascript.', 16 | usage: '[code]', 17 | args: true, 18 | execute(message, args) { 19 | const code = args.join(' '); 20 | try { 21 | const evaled = eval(code); 22 | // const clean = await client.clean(client, evaled); 23 | message.channel.send(`\`\`\`js\n${evaled}\n\`\`\``); 24 | } 25 | catch (err) { 26 | message.channel.send(`\`\`\`xl\n${err.message}\n\`\`\``); 27 | } 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/commands/debug/reload.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../utils/logger'); 2 | 3 | module.exports = { 4 | name: 'reload', 5 | description: 'Reloads a command', 6 | args: true, 7 | execute(message, args) { 8 | const commandName = args[0].toLowerCase(); 9 | const command = message.client.commands.get(commandName) 10 | || message.client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName)); 11 | 12 | if (!command) { 13 | return message.channel.send(`There is no command with name or alias \`${commandName}\`, ${message.author}!`); 14 | } 15 | 16 | delete require.cache[require.resolve(`./${command.name}.js`)]; 17 | 18 | try { 19 | const newCommand = require(`./${command.name}.js`); 20 | message.client.commands.set(newCommand.name, newCommand); 21 | } 22 | catch (err) { 23 | logger.error(err.message); 24 | return message.channel.send(`There was an error while reloading a command \`${command.name}\`:\n\`${err.message}\``); 25 | } 26 | message.channel.send(`Command \`${command.name}\` was reloaded!`); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/commands/fun/dog.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed } = require('discord.js'); 2 | const fetch = require('node-fetch'); 3 | 4 | const dogDescriptions = [ 5 | 'Here is a cute picture of a dog.', 6 | 'Want to see another picture?', 7 | 'How cute is this?', 8 | 'Awww 🥺', 9 | ]; 10 | 11 | module.exports = { 12 | name: 'dog', 13 | description: 'Shows a picture of a dog.', 14 | execute(message) { 15 | fetch('https://dog.ceo/api/breeds/image/random') 16 | .then(response => response.json()) 17 | .then(data => { 18 | const dogMessage = new MessageEmbed() 19 | .setTitle('Dog Image!') 20 | .setDescription(dogDescriptions[Math.floor(Math.random() * dogDescriptions.length)]) 21 | .setImage(data.message) 22 | .setTimestamp(new Date()) 23 | .setFooter(`Requested by ${message.author.tag}`, message.author.displayAvatarURL()); 24 | message.reply(dogMessage); 25 | }) 26 | .catch(err => console.warn(err)); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/commands/general/avatar.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'avatar', 3 | description: 'Get the avatar URL of the tagged user(s), or your own avatar.', 4 | aliases: ['icon', 'pfp'], 5 | execute(message) { 6 | if (!message.mentions.users.size) { 7 | return message.channel.send(`Your avatar: <${message.author.displayAvatarURL({ dynamic: true })}>`); 8 | } 9 | 10 | const avatarList = message.mentions.users.map(user => { 11 | return `${user.username}'s avatar: <${user.displayAvatarURL({ dynamic: true })}>`; 12 | }); 13 | 14 | message.channel.send(avatarList); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/commands/general/help.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../utils/logger'); 2 | const commandPrefix = process.env.WATCHDOG_PREFIX || '?'; 3 | 4 | module.exports = { 5 | name: 'help', 6 | description: 'List all of my commands or info about a specific command.', 7 | aliases: ['commands'], 8 | usage: '[command name]', 9 | cooldown: 5, 10 | guildOnly: true, 11 | execute(message, args) { 12 | const data = []; 13 | const { commands } = message.client; 14 | 15 | if (!args.length) { 16 | data.push('Here\'s a list of all my commands:'); 17 | data.push(commands.map(command => command.name).join(', ')); 18 | data.push(`\nYou can send \`${commandPrefix}help [command name]\` to get info on a specific command!`); 19 | 20 | return message.author.send(data, { split: true }) 21 | .then(() => { 22 | if (message.channel.type === 'dm') return; 23 | message.reply('I\'ve sent you a DM with all my commands!'); 24 | }) 25 | .catch(err => { 26 | logger.error(`Could not send help DM to ${message.author.tag}.\n`, err); 27 | message.reply('it seems like I can\'t DM you!'); 28 | }); 29 | } 30 | 31 | const name = args[0].toLowerCase(); 32 | const command = commands.get(name) || commands.find(c => c.aliases && c.aliases.includes(name)); 33 | 34 | if (!command) { 35 | return message.reply('that\'s not a valid command!'); 36 | } 37 | 38 | data.push(`**Name:** ${command.name}`); 39 | 40 | if (command.aliases) data.push(`**Aliases:** ${command.aliases.join(', ')}`); 41 | if (command.description) data.push(`**Description:** ${command.description}`); 42 | if (command.usage) data.push(`**Usage:** ${commandPrefix}${command.name} ${command.usage}`); 43 | 44 | data.push(`**Cooldown:** ${command.cooldown || 3} second(s)`); 45 | 46 | message.channel.send(data, { split: true }); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/commands/general/invite.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'invite', 3 | description: 'create an invite link', 4 | execute: (message) => { 5 | message.channel.send(`https://discordapp.com/oauth2/authorize?client_id=${message.client.user.id}&scope=bot`); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/commands/general/ping.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'ping', 3 | description: 'Ping!', 4 | execute(message) { 5 | message.channel.send('Pong.'); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/commands/general/stats.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed } = require('discord.js'); 2 | 3 | module.exports = { 4 | name: 'stats', 5 | description: 'Display info about this Guild or a Member.', 6 | execute(message, args) { 7 | const member = message.mentions.members.size === 1 ? message.mentions.members.first() : message.guild.members.cache.get(args[0]); 8 | if(member) { 9 | const memberEmbed = new MessageEmbed() 10 | .setAuthor(`${member.user.tag} (${member.id})`) 11 | .setThumbnail(member.user.displayAvatarURL()) 12 | .setDescription(`${member.roles.cache.map(role => role.toString()).join(' ')}`) 13 | .addField('Created On', member.user.createdAt.toLocaleString()) 14 | .addField('Joined On', member.joinedAt.toLocaleString()) 15 | .addField('Kickable', member.kickable) 16 | .addField('Voice Channel', member.voice.channel ? member.voice.channel.name + `(${member.voice.channel.id})` : 'None') 17 | .addField('Presence', member.presence.status); 18 | message.channel.send(memberEmbed); 19 | } 20 | else { 21 | const { guild } = message; 22 | const guildEmbed = new MessageEmbed() 23 | .setAuthor(`${guild.name} (${guild.id})`) 24 | .setThumbnail(guild.iconURL()) 25 | .setDescription(`${guild.roles.cache.map(role => role.toString()).join(' ')}`) 26 | .addField('Created On', guild.createdAt.toLocaleString()) 27 | .addField('Guild Owner', `${guild.owner.user.tag} (${guild.owner.user.id})`) 28 | .addField('Total Members', guild.memberCount) 29 | .addField('Total Real Members', guild.members.cache.filter(m => !m.user.bot).size) 30 | .addField('Total Bots', guild.members.cache.filter(m => m.user.bot).size) 31 | .addField('Total Channels', guild.channels.cache.size) 32 | .addField('Total Text Channels', guild.channels.cache.filter(channel => channel.type === 'text').size) 33 | .addField('Total Voice Channels', guild.channels.cache.filter(channel => channel.type === 'voice').size); 34 | message.channel.send(guildEmbed); 35 | } 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/commands/moderation/kick.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'kick', 3 | description: 'Tag a member and kick them (but not really).', 4 | guildOnly: true, 5 | execute(message) { 6 | if (!message.mentions.users.size) { 7 | return message.reply('you need to tag a user in order to kick them!'); 8 | } 9 | 10 | const user = message.mentions.users.first(); 11 | const member = message.guild.member(user); 12 | 13 | member 14 | .kick('Optional reason that will display in the audit logs') 15 | .then(() => { 16 | // We let the message author know we were able to kick the person 17 | message.reply(`Successfully kicked ${user.username}`); 18 | }) 19 | .catch(err => { 20 | // An error happened 21 | // This is generally due to the bot not being able to kick the member, 22 | // either due to missing permissions or role hierarchy 23 | message.reply(`I was unable to kick ${user.username}. Check if their roles are higher then mine or if they have administrative permissions!`); 24 | // Log the error 25 | console.error(err); 26 | }); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/commands/moderation/prune.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../utils/logger'); 2 | 3 | module.exports = { 4 | name: 'prune', 5 | description: 'Prune up to 99 messages.', 6 | aliases: ['purge', 'clear', 'rm'], 7 | usage: '', 8 | args: true, 9 | execute(message, args) { 10 | const amount = parseInt(args[0]) + 1; 11 | 12 | if (isNaN(amount)) { 13 | return message.reply('that doesn\'t seem to be a valid number.'); 14 | } 15 | else if (amount <= 1 || amount > 100) { 16 | return message.reply('you need to input a number between 1 and 99.'); 17 | } 18 | 19 | message.channel.bulkDelete(amount, true).catch(err => { 20 | logger.error(err); 21 | message.channel.send('there was an error trying to prune messages in this channel!'); 22 | }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/commands/moderation/server.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed } = require('discord.js'); 2 | 3 | module.exports = { 4 | name: 'server', 5 | description: 'server info', 6 | execute: async (message) => { 7 | const embed = new MessageEmbed() 8 | .setColor(0x00AE86) 9 | .setTitle('Server Info') 10 | .setThumbnail(message.guild.iconURL) 11 | .addField(':arrow_right: Name', message.guild.name, true) 12 | .addField(':arrow_right: ID', message.guild.id, true) 13 | .addField(':arrow_right: Region', message.guild.region.toUpperCase(), true) 14 | .addField(':arrow_right: Creation Date', message.guild.createdAt.toDateString(), true) 15 | .addField(':arrow_right: Owner', message.guild.owner.user.tag, true) 16 | .addField(':arrow_right: Members', message.guild.memberCount, true) 17 | .addField(':arrow_right: Roles', message.guild.roles.cache.map(role => role.toString()).join(' **|** '), true) 18 | .addField(':arrow_right: Categories', message.guild.channels.cache.filter(channel => channel.type === 'category').map(category => category.toString()).join(' **|** '), true) 19 | .addField(':arrow_right: Channels', message.guild.channels.cache.filter(channel => channel.type !== 'category').map(channel => channel.toString()).join(' **|** '), true); 20 | 21 | return message.channel.send(embed); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/dashboard/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | 4 | const app = express(); 5 | 6 | app.set('views', path.join(__dirname, 'views')); 7 | app.set('view engine', 'ejs'); 8 | 9 | app.use(express.json()); 10 | app.use(express.urlencoded({ extended: false })); 11 | 12 | app.use(express.static(path.join(__dirname, 'public'))); 13 | 14 | app.use('/', require('./routes/index')); 15 | 16 | // Handle 404 17 | app.use(function(req, res, next) { 18 | const error = new Error('Page Not Found'); 19 | error.status = 404; 20 | next(error); 21 | }); 22 | 23 | // Handle error 24 | // eslint-disable-next-line no-unused-vars 25 | app.use(function(err, req, res, next) { 26 | res.status(err.status || 500); 27 | res.render('error', { 28 | error: err.status, 29 | message: err.message, 30 | }); 31 | }); 32 | 33 | const port = process.env.WATCHDOG_PORT || 8080; 34 | app.listen(port, () => { 35 | console.log(`Running on http://localhost:${port}`); 36 | }); 37 | -------------------------------------------------------------------------------- /src/dashboard/public/css/dashboard.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,500'); 2 | 3 | html, body { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | background: #34383c; 10 | margin: 0 0; 11 | padding: 0 0; 12 | } 13 | 14 | .page-content { 15 | padding: 4px 42px; 16 | } 17 | 18 | h1 .page-content { 19 | margin-bottom: -10px; 20 | } 21 | 22 | h1, h2, h3, h4, h5, h6 { 23 | font-family: 'Roboto', sans-serif; 24 | font-weight: 500; 25 | color: #fff; 26 | } 27 | 28 | p, a { 29 | font-family: 'Roboto', sans-serif; 30 | font-weight: 300; 31 | color: #fff; 32 | } 33 | 34 | .navbar { 35 | background: #23272a; 36 | padding: 21px 42px; 37 | } 38 | 39 | .navbar a { 40 | transition: .25s ease; 41 | color: #fff; 42 | text-decoration: none; 43 | } 44 | 45 | .navbar a:hover { 46 | color: #7792f3; 47 | } 48 | 49 | .navbar a:not(:last-child) { 50 | margin-right: 12px; 51 | } 52 | 53 | .navbar .right { 54 | float: right; 55 | } 56 | -------------------------------------------------------------------------------- /src/dashboard/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groveld/discord-watchdog/10d0db396950b10e751ad8132427fa8f9ff2e9fb/src/dashboard/public/img/logo.png -------------------------------------------------------------------------------- /src/dashboard/public/js/dashboard.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/groveld/discord-watchdog/10d0db396950b10e751ad8132427fa8f9ff2e9fb/src/dashboard/public/js/dashboard.js -------------------------------------------------------------------------------- /src/dashboard/routes/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | router.get('/', (req, res) => { 4 | res.render('index'); 5 | }); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /src/dashboard/views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include('./partials/head') %> 4 | 5 | 6 |
7 |

error: <%= error %>

8 |

<%= message %>

9 |
10 | 11 | <%- include('./partials/footer') %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/dashboard/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include('./partials/head') %> 4 | 5 | 6 |
7 |

WatchDog Discord Bot

8 | Add to Discord 9 | Get Support 10 |
11 | 12 | <%- include('./partials/footer') %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/dashboard/views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/dashboard/views/partials/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WatchDog Dashboard 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/events/guildCreate.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | 3 | module.exports = async (client, guild) => { 4 | logger.info('Event guildCreate triggered.'); 5 | logger.info(`Guild has joined: ${guild.name} (${guild.id}) with ${guild.memberCount} members.`); 6 | }; 7 | -------------------------------------------------------------------------------- /src/events/guildDelete.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | 3 | module.exports = async (client, guild) => { 4 | logger.info('Event guildDelete triggered.'); 5 | logger.info(`Guild has left: ${guild.name} (${guild.id}) with ${guild.memberCount} members.`); 6 | }; 7 | -------------------------------------------------------------------------------- /src/events/guildMemberAdd.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | 3 | module.exports = (client, member) => { 4 | logger.info('Event guildMemberAdd triggered.'); 5 | logger.info(`${member.name} (${member.id})`); 6 | }; 7 | -------------------------------------------------------------------------------- /src/events/guildMemberRemove.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | 3 | module.exports = (client, member) => { 4 | logger.info('Event guildMemberRemove triggered.'); 5 | logger.info(`${member.name} (${member.id})`); 6 | }; 7 | -------------------------------------------------------------------------------- /src/events/message.js: -------------------------------------------------------------------------------- 1 | const { Collection } = require('discord.js'); 2 | const logger = require('../utils/logger'); 3 | const commandPrefix = process.env.WATCHDOG_PREFIX || '?'; 4 | 5 | module.exports = (client, message) => { 6 | if (!message.content.startsWith(commandPrefix) || message.author.bot) return; 7 | 8 | const args = message.content.slice(commandPrefix.length).split(/ +/); 9 | const commandName = args.shift().toLowerCase(); 10 | 11 | const command = client.commands.get(commandName) 12 | || client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName)); 13 | 14 | if (!command) return; 15 | 16 | if (command.guildOnly && message.channel.type !== 'text') { 17 | return message.reply('I can\'t execute that command inside DMs!'); 18 | } 19 | 20 | if (command.args && !args.length) { 21 | let reply = `You didn't provide any arguments, ${message.author}!`; 22 | 23 | if (command.usage) { 24 | reply += `\nThe proper usage would be: \`${commandPrefix}${command.name} ${command.usage}\``; 25 | } 26 | 27 | return message.channel.send(reply); 28 | } 29 | 30 | if (message.mentions.users.array().size >= 25 || message.mentions.members.array().size >= 25) { 31 | message.member.ban({ reason: 'Auto Moderation: Mentioned more than 25 members in a message.' }); 32 | } 33 | 34 | const cooldowns = new Collection(); 35 | 36 | if (!cooldowns.has(command.name)) { 37 | cooldowns.set(command.name, new Collection()); 38 | } 39 | 40 | const now = Date.now(); 41 | const timestamps = cooldowns.get(command.name); 42 | const cooldownAmount = (command.cooldown || 3) * 1000; 43 | 44 | if (timestamps.has(message.author.id)) { 45 | const expirationTime = timestamps.get(message.author.id) + cooldownAmount; 46 | 47 | if (now < expirationTime) { 48 | const timeLeft = (expirationTime - now) / 1000; 49 | return message.reply(`please wait ${timeLeft.toFixed(1)} more second(s) before reusing the \`${command.name}\` command.`); 50 | } 51 | } 52 | 53 | timestamps.set(message.author.id, now); 54 | setTimeout(() => timestamps.delete(message.author.id), cooldownAmount); 55 | 56 | // logger.info(cooldowns); 57 | 58 | try { 59 | command.execute(message, args); 60 | } 61 | catch (err) { 62 | logger.error(err.message); 63 | message.reply('there was an error trying to execute that command!'); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/events/ready.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | 3 | module.exports = async (client) => { 4 | // Why await here? Because the ready event isn't actually ready. 5 | // Sometimes guild information will come in *after* ready. 6 | await new Promise(r => setTimeout(r, 2000)); 7 | 8 | // Set bot presece 9 | client.user.setPresence({ 10 | // activity: { 11 | // type: 'WATCHING', 12 | // name: 'all of you!', 13 | // }, 14 | status: 'online', 15 | }); 16 | 17 | // Log that we're ready to serve, so we know the bot accepts commands. 18 | logger.info(`${client.user.tag}, ready to serve.`); 19 | }; 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const logger = require('./utils/logger'); 2 | const Dashboard = require('./dashboard'); 3 | const { ShardingManager } = require('discord.js'); 4 | const manager = new ShardingManager('./bot.js', { token: process.env.DISCORD_BOT_TOKEN }); 5 | 6 | manager.on('shardCreate', shard => logger.info(`Launched shard ${shard.id}`)); 7 | 8 | manager.spawn().catch(err => logger.error(err.message)); 9 | 10 | Dashboard; 11 | -------------------------------------------------------------------------------- /src/utils/functions.js: -------------------------------------------------------------------------------- 1 | module.exports = client => { 2 | /* 3 | MESSAGE CLEAN FUNCTION 4 | "Clean" removes @everyone pings, as well as tokens, and makes code blocks 5 | escaped so they're shown more easily. As a bonus it resolves promises 6 | and stringifies objects! 7 | This is mostly only used by the Eval and Exec commands. 8 | */ 9 | client.clean = async text => { 10 | if (text && text.constructor.name == 'Promise') {text = await text;} 11 | if (typeof evaled !== 'string') {text = require('util').inspect(text, { depth: 0 });} 12 | 13 | text = text 14 | .replace(/`/g, '`' + String.fromCharCode(8203)) 15 | .replace(/@/g, '@' + String.fromCharCode(8203)) 16 | .replace(process.env.DISCORD_BOT_TOKEN, 'mfa.VkO_2G4Qv3T--NO--lWetW_tjND--TOKEN--QFTm6YGtzq9PH--4U--tG0'); 17 | 18 | return text; 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const winston = require('winston'); 3 | const moment = require('moment'); 4 | const chalk = require('chalk'); 5 | 6 | const logDir = '../config'; 7 | if (!fs.existsSync(logDir)) fs.mkdirSync(logDir); 8 | 9 | const colorLevel = level => { 10 | switch (level) { 11 | case 'INFO': 12 | return chalk.cyan(level); 13 | case 'ERROR': 14 | return chalk.red(level); 15 | case 'WARN': 16 | return chalk.yellow(level); 17 | case 'DEBUG': 18 | return chalk.green(level); 19 | } 20 | }; 21 | 22 | const formatter = options => { 23 | const timestamp = options.timestamp; 24 | const level = options.level.toUpperCase(); 25 | const message = options.message ? options.message : ''; 26 | const meta = options.meta && Object.keys(options.meta).length ? `\n${chalk.cyan('META')}: ` + JSON.stringify(options.meta) : ''; 27 | 28 | return `${chalk.gray(`[${timestamp}]`)} [${colorLevel(level)}] ${message} ${meta}`; 29 | }; 30 | 31 | winston.emitErrs = true; 32 | const logger = new winston.Logger({ 33 | transports: [ 34 | new winston.transports.File({ 35 | level: 'debug', 36 | filename: `${logDir}/watchdog.log`, 37 | handleExceptions: true, 38 | json: false, 39 | // eslint-disable-next-line no-inline-comments 40 | maxsize: 5242880, // 5MB 41 | maxFiles: 5, 42 | timestamp: true, 43 | colorize: false, 44 | }), 45 | new winston.transports.Console({ 46 | level: 'info', 47 | handleExceptions: true, 48 | showLevel: true, 49 | timestamp: moment().format('DD-MM-YYYY HH:mm:ss'), 50 | formatter, 51 | }), 52 | ], 53 | exitOnError: false, 54 | }); 55 | 56 | module.exports = logger; 57 | -------------------------------------------------------------------------------- /src/utils/register.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const logger = require('./logger'); 4 | 5 | const getFiles = dir => 6 | fs.readdirSync(path.join(__dirname, dir)).reduce((files, file) => { 7 | const name = path.join(dir, file); 8 | const isDirectory = fs.statSync(path.join(__dirname, name)).isDirectory(); 9 | return isDirectory ? [...files, ...getFiles(name)] : [...files, name]; 10 | }, []); 11 | 12 | const registerEvents = async (client, dir) => { 13 | const files = await getFiles(dir).filter(file => file.endsWith('.js')); 14 | logger.info(`Loading a total of ${files.length} events.`); 15 | for (const file of files) { 16 | const event = require(path.join(__dirname, file)); 17 | const eventName = path.basename(file, path.extname(file)); 18 | logger.debug('Loading event:', eventName); 19 | client.on(eventName, event.bind(null, client)); 20 | delete require.cache[require.resolve(path.join(__dirname, file))]; 21 | } 22 | }; 23 | 24 | const registerCommands = async (client, dir) => { 25 | const files = await getFiles(dir).filter(file => file.endsWith('.js')); 26 | logger.info(`Loading a total of ${files.length} commands.`); 27 | for(const file of files) { 28 | const command = require(path.join(__dirname, file)); 29 | try { 30 | logger.debug('Loading command:', command.name); 31 | client.commands.set(command.name, command); 32 | delete require.cache[require.resolve(path.join(__dirname, file))]; 33 | } 34 | catch (err) { 35 | logger.error(err.message); 36 | } 37 | } 38 | }; 39 | 40 | module.exports = { 41 | registerEvents, 42 | registerCommands, 43 | }; 44 | --------------------------------------------------------------------------------