├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── app.json ├── bin └── update_dashboard ├── dist └── twopg-dashboard │ ├── browser │ ├── 3rdpartylicenses.txt │ ├── assets │ │ ├── css │ │ │ ├── animations.css │ │ │ ├── main.css │ │ │ ├── nav-icon.css │ │ │ ├── spinner.css │ │ │ ├── utils.css │ │ │ └── vs2015.css │ │ ├── docs │ │ │ ├── api.md │ │ │ ├── auto-mod.md │ │ │ ├── changelog.md │ │ │ ├── commands.md │ │ │ ├── config-updates.md │ │ │ ├── crates.md │ │ │ ├── credits.md │ │ │ ├── docs-guide.md │ │ │ ├── faq.md │ │ │ ├── general.md │ │ │ ├── get-started.md │ │ │ ├── how-to-auto-clear-channel-at-intervals.md │ │ │ ├── img │ │ │ │ ├── customize-bot.gif │ │ │ │ ├── dashboard-home-v1.x.png │ │ │ │ ├── dashboard-v1.x.png │ │ │ │ ├── dashboard-v2.0.0a.png │ │ │ │ ├── dashboard-v2.0.1a.png │ │ │ │ ├── dashboard-v2.0.2a.png │ │ │ │ ├── dashboard-v2.1.0b.png │ │ │ │ ├── dashboard-v2.1.1b.png │ │ │ │ ├── dashboard-v2.1.2b.png │ │ │ │ ├── dashboard-v2.2.0b.png │ │ │ │ ├── dashboard-v2.3.0b.png │ │ │ │ ├── invite-bot.gif │ │ │ │ └── use-bot.gif │ │ │ ├── leveling.md │ │ │ ├── logs.md │ │ │ ├── music.md │ │ │ ├── privacy.md │ │ │ ├── reaction-roles.md │ │ │ ├── timers.md │ │ │ ├── troubleshooting.md │ │ │ └── tutorials.md │ │ └── img │ │ │ ├── 3PGAvatarTransparent.ico │ │ │ ├── 3PGAvatarTransparent.svg │ │ │ ├── 3PGProAvatarTransparent.webp │ │ │ ├── 404.svg │ │ │ ├── badges │ │ │ ├── alpha.svg │ │ │ ├── bug-destroyer.svg │ │ │ ├── early-supporter.svg │ │ │ └── legend.svg │ │ │ ├── crates │ │ │ ├── nothing.webp │ │ │ └── vote-crate.webp │ │ │ ├── earth.svg │ │ │ ├── home │ │ │ ├── announce.png │ │ │ ├── leaderboard.png │ │ │ ├── music-player.gif │ │ │ ├── music-player.png │ │ │ └── timers.png │ │ │ ├── logo.png │ │ │ ├── mars.svg │ │ │ ├── moon.svg │ │ │ ├── overlay_stars.svg │ │ │ ├── pro.png │ │ │ ├── pro.svg │ │ │ └── rocket.svg │ ├── favicon.ico │ ├── index.html │ ├── main-es2015.11e1a7931d13b5165a2a.js │ ├── main-es5.11e1a7931d13b5165a2a.js │ ├── overlay_stars.21a32cc9bc0c3068cb9a.svg │ ├── polyfills-es2015.50fce3929b1a53f318c7.js │ ├── polyfills-es5.ddaaccbf1d0ef19804ff.js │ ├── runtime-es2015.41eb3616844b6c96e4f3.js │ ├── runtime-es5.41eb3616844b6c96e4f3.js │ ├── scripts.a0997f3a9f749b2c277f.js │ └── styles.9307f795cbd8833f09c8.css │ └── server │ └── main.js ├── package-lock.json ├── package.json ├── src ├── api │ ├── modules │ │ ├── api-utils.ts │ │ ├── audit-logger.ts │ │ ├── error-logger.ts │ │ ├── image │ │ │ ├── image-generator.ts │ │ │ ├── wallpaper.png │ │ │ └── xp-card-generator.ts │ │ ├── ranks.ts │ │ ├── rate-limiter.ts │ │ └── stats.ts │ ├── routes │ │ ├── api-routes.ts │ │ ├── guilds-routes.ts │ │ ├── music-routes.ts │ │ ├── pay-routes.ts │ │ └── user-routes.ts │ ├── server.ts │ ├── websocket.ts │ └── ws-events │ │ ├── guild-drag.ts │ │ └── ws-event.ts ├── bot.ts ├── commands │ ├── clear.ts │ ├── command.ts │ ├── dashboard.ts │ ├── flip.ts │ ├── help.ts │ ├── info.ts │ ├── leaderboard.ts │ ├── levels.ts │ ├── list.ts │ ├── lock.ts │ ├── music.ts │ ├── mute.ts │ ├── pause.ts │ ├── ping.ts │ ├── play.ts │ ├── resume.ts │ ├── role.ts │ ├── say.ts │ ├── seek.ts │ ├── server.ts │ ├── shuffle.ts │ ├── skip.ts │ ├── stop.ts │ ├── unlock.ts │ ├── unmute.ts │ ├── vote.ts │ ├── warn.ts │ ├── warnings.ts │ └── xp.ts ├── data │ ├── commands.ts │ ├── db-wrapper.ts │ ├── guilds.ts │ ├── logs.ts │ ├── members.ts │ ├── models │ │ ├── command.ts │ │ ├── guild.ts │ │ ├── log.ts │ │ ├── member.ts │ │ └── user.ts │ ├── snowflake-entity.ts │ └── users.ts ├── keep-alive.ts ├── modules │ ├── announce │ │ └── event-variables.ts │ ├── auto-mod │ │ ├── auto-mod.ts │ │ └── validators │ │ │ ├── bad-link.validator.ts │ │ │ ├── bad-word.validator.ts │ │ │ ├── content-validator.ts │ │ │ ├── emoji.validator.ts │ │ │ ├── explicit-word.validator.ts │ │ │ ├── mass-caps.validator.ts │ │ │ ├── mass-mention.validator.ts │ │ │ └── zalgo.validator.ts │ ├── general │ │ └── reaction-roles.ts │ ├── music │ │ └── music.ts │ ├── timers │ │ └── timers.ts │ └── xp │ │ └── leveling.ts ├── services │ ├── command.service.ts │ ├── cooldowns.ts │ ├── custom-handlers │ │ ├── config-update.handler.ts │ │ ├── level-up.handler.ts │ │ ├── user-mute.handler.ts │ │ ├── user-unmute.handler.ts │ │ └── user-warn.handler.ts │ ├── emit.ts │ ├── events.service.ts │ ├── handlers │ │ ├── event-handler.ts │ │ ├── guild-ban-add.handler.ts │ │ ├── guild-ban-remove.handler.ts │ │ ├── guild-create.handler.ts │ │ ├── logs-handler.ts │ │ ├── member-join.handler.ts │ │ ├── member-leave.handler.ts │ │ ├── message-deleted.handler.ts │ │ ├── message-reaction-add.handler.ts │ │ ├── message-reaction-remove.handler.ts │ │ ├── message.handler.ts │ │ └── ready.handler.ts │ └── validators.ts └── utils │ ├── command-utils.ts │ ├── deps.ts │ └── log.ts ├── test ├── integration │ ├── auto-mod.tests.ts │ ├── command.service.tests.ts │ ├── logs.tests.ts │ ├── routes.tests.ts │ └── timers.tests.ts ├── mock.ts └── unit │ ├── audit-logger.tests.ts │ ├── auto-mod-validators.tests.ts │ ├── commands.tests.ts │ ├── cooldowns.tests.ts │ ├── data.tests.ts │ ├── event-variables.tests.ts │ ├── leveling.tests.ts │ ├── logs.tests.ts │ ├── ranks.tests.ts │ ├── stats.tests.ts │ └── validators.tests.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | lib/ 4 | node_modules/ 5 | 6 | logs/*/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/lib/bot.js", 15 | "outFiles": [ 16 | "${workspaceFolder}/lib/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "typescript", 6 | "tsconfig": "tsconfig.json", 7 | "option": "watch", 8 | "problemMatcher": [ 9 | "$tsc-watch" 10 | ], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "label": "tsc: watch - tsconfig.json" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 3PG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deprecated 2 | > :warning: 3PG is deprecated. Please use [2PG](https://github.com/twopg) instead. More Info - https://github.com/3PG/Bot/issues/10 3 | 4 | # 3PG 5 | The all-in-one, highly customizable Discord bot. 6 | 7 | ![Discord](https://img.shields.io/discord/685862664223850497?color=482f5d&label=Support&style=for-the-badge) 8 | ![Lines of Code](https://img.shields.io/tokei/lines/github/3PG/Bot?color=482f5d&style=for-the-badge) 9 | ![Repo Stars](https://img.shields.io/github/stars/3PG/Bot?color=482f5d&style=for-the-badge) 10 | 11 | ![Dashboard Preview](https://3pg.xyz/assets/docs/img/dashboard-v2.2.0b.png) 12 | 13 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/3PG/Bot/tree/stable) 14 | 15 | # Non Heroku Setup 16 | 1) Make a .env file in the root project directory. 17 | 2) Fill in the .env 18 | 19 | `.env` 20 | ```env 21 | API_URL="http://localhost:3000/api" 22 | BOT_ID="533947001578979328" 23 | BOT_TOKEN="NTI1OTM1MzM1OTE4NjY1NzYw.XB3lcw.z1_F0px-RxqnnJ2ni3Bgn9Fo9aw" 24 | CLIENT_SECRET="FqRcAkRTJVXM6sdFdJ_EhqNE1sBStKjJ" 25 | DASHBOARD_URL="http://localhost:4200" 26 | GUILD_ID="599596068145201152" 27 | PORT=3000 28 | PREMIUM_ROLE_ID="598565371162656788" 29 | OWNER_ID="218459216145285121" 30 | MONGO_URI="mongodb://localhost/3PG" 31 | STRIPE_SECRET_KEY="sk_test_yUSJD1JOVcg7WHJmIZjtfLwG00RbMMuCpS" 32 | STRIPE_WEBHOOK_SECRET="" 33 | ``` 34 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3PG - Highly Customizable Discord Bot", 3 | "description": "Host a bot without code, with one simple step.", 4 | "keywords": [ 5 | "bot", 6 | "discord bot", 7 | "customizable discord bot", 8 | "3pg" 9 | ], 10 | "repository": "https://github.com/theADAMJR/6PG", 11 | "env": { 12 | "API_URL": { 13 | "description": "The API URL [https://.herokuapp.com/api]", 14 | "value": "https://.herokuapp.com/api" 15 | }, 16 | "BOT_ID": { 17 | "description": "Client ID from https://discord.com/developers.", 18 | "value": "" 19 | }, 20 | "BOT_TOKEN": { 21 | "description": "Bot Token from https://discord.com/developers.", 22 | "value": "" 23 | }, 24 | "CLIENT_SECRET": { 25 | "description": "Client Secret from https://discord.com/developers.", 26 | "value": "" 27 | }, 28 | "DASHBOARD_URL": { 29 | "description": "The Website URL [https://.herokuapp.com]", 30 | "value": "https://.herokuapp.com" 31 | }, 32 | "GUILD_ID": { 33 | "description": "Guild ID with the premium role in (optional).", 34 | "value": "https://.herokuapp.com", 35 | "required": false 36 | }, 37 | "MONGO_URI": { 38 | "description": "The MongoDB URI to your database [i.e. from MongoDB Atlas].", 39 | "value": "" 40 | }, 41 | "OWNER_ID": { 42 | "description": "Discord bot owner ID.", 43 | "value": "", 44 | "required": false 45 | }, 46 | "PORT": { 47 | "description": "Port for Heroku to use (default: 3000).", 48 | "value": "3000" 49 | }, 50 | "PREMIUM_ROLE_ID": { 51 | "description": "Premium role ID in the GUILD_ID guild.", 52 | "value": "", 53 | "required": false 54 | }, 55 | "STRIPE_SECRET_KEY": { 56 | "description": "Secret key used for payments (optional).", 57 | "value": "", 58 | "required": false 59 | }, 60 | "STRIPE_WEBHOOK_SECRET": { 61 | "description": "Stripe webhook secret [whsec...] (optional).", 62 | "value": "", 63 | "required": false 64 | } 65 | }, 66 | "buildpacks": [ 67 | { 68 | "url": "heroku/nodejs" 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /bin/update_dashboard: -------------------------------------------------------------------------------- 1 | DASHBOARD_PATH="$(realpath ../../Dashboard)" 2 | echo -e "\e[36mUsing $DASHBOARD_PATH to build dashboard." 3 | 4 | cd $DASHBOARD_PATH 5 | 6 | ng build --prod 7 | 8 | cp -rf "$DASHBOARD_PATH/dist/twopg-dashboard/browser" "$(pwd)/dist/twopg-dashboard/browser" 9 | 10 | echo -e "\e[32Updated Dashboard" -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/css/animations.css: -------------------------------------------------------------------------------- 1 | /* float */ 2 | @keyframes float { 3 | 0% { transform: translatey(0px); } 4 | 50% { transform: translatey(-10px); } 5 | 100% { transform: translatey(0px); } 6 | } 7 | 8 | .float { 9 | font-size: 3em; 10 | overflow: hidden; 11 | transform: translatey(0px); 12 | animation: float 3s ease-in-out infinite; 13 | } -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/css/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-primary: #36393F; 3 | --background-secondary: rgb(37, 38, 42); 4 | --background-tertiary: #202225; 5 | } 6 | 7 | body { 8 | color: transparent; 9 | background: radial-gradient(circle, var(--background-primary) 0%, var(--background-secondary) 100%) !important; 10 | height: 100vh; 11 | } -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/css/nav-icon.css: -------------------------------------------------------------------------------- 1 | /* icon */ 2 | .toggle { 3 | background: transparent; 4 | border: none; 5 | } 6 | 7 | .toggle:focus { 8 | outline: none; 9 | } 10 | 11 | #nav-icon1 { 12 | width: 60px; 13 | height: 45px; 14 | position: relative; 15 | -webkit-transform: rotate(0deg); 16 | -moz-transform: rotate(0deg); 17 | -o-transform: rotate(0deg); 18 | transform: rotate(0deg); 19 | -webkit-transition: .5s ease-in-out; 20 | -moz-transition: .5s ease-in-out; 21 | -o-transition: .5s ease-in-out; 22 | transition: .5s ease-in-out; 23 | cursor: pointer; 24 | } 25 | 26 | #nav-icon1 span { 27 | display: block; 28 | position: absolute; 29 | height: 5px; 30 | width: 100%; 31 | background: var(--primary); 32 | border-radius: 2px; 33 | opacity: 1; 34 | left: 0; 35 | -webkit-transform: rotate(0deg); 36 | -moz-transform: rotate(0deg); 37 | -o-transform: rotate(0deg); 38 | transform: rotate(0deg); 39 | -webkit-transition: .25s ease-in-out; 40 | -moz-transition: .25s ease-in-out; 41 | -o-transition: .25s ease-in-out; 42 | transition: .25s ease-in-out; 43 | } 44 | 45 | #nav-icon1 span:nth-child(1) { 46 | top: 0px; 47 | } 48 | 49 | #nav-icon1 span:nth-child(2) { 50 | top: 10px; 51 | } 52 | 53 | #nav-icon1 span:nth-child(3) { 54 | top: 20px; 55 | } 56 | 57 | #nav-icon1.open span:nth-child(1) { 58 | top: 18px; 59 | -webkit-transform: rotate(135deg); 60 | -moz-transform: rotate(135deg); 61 | -o-transform: rotate(135deg); 62 | transform: rotate(135deg); 63 | } 64 | 65 | #nav-icon1.open span:nth-child(2) { 66 | opacity: 0; 67 | left: -60px; 68 | } 69 | 70 | #nav-icon1.open span:nth-child(3) { 71 | top: 18px; 72 | -webkit-transform: rotate(-135deg); 73 | -moz-transform: rotate(-135deg); 74 | -o-transform: rotate(-135deg); 75 | transform: rotate(-135deg); 76 | } 77 | 78 | #nav-icon1 { 79 | max-width: 50%; 80 | max-height: 50%; 81 | } -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/css/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 40px; 3 | height: 40px; 4 | border-radius: 5px; 5 | background-color: var(--font); 6 | 7 | margin: 100px auto; 8 | -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out; 9 | animation: sk-rotateplane 1.2s infinite ease-in-out; 10 | } 11 | 12 | @-webkit-keyframes sk-rotateplane { 13 | 0% { -webkit-transform: perspective(120px) } 14 | 50% { -webkit-transform: perspective(120px) rotateY(180deg) } 15 | 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) } 16 | } 17 | 18 | @keyframes sk-rotateplane { 19 | 0% { 20 | transform: perspective(120px) rotateX(0deg) rotateY(0deg); 21 | -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg) 22 | } 50% { 23 | transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); 24 | -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg) 25 | } 100% { 26 | transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); 27 | -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); 28 | } 29 | } -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/css/utils.css: -------------------------------------------------------------------------------- 1 | .text-center { 2 | text-align: center; 3 | } 4 | .text-decoration-none { 5 | text-decoration: none !important; 6 | } 7 | 8 | .uppercase { 9 | text-transform: uppercase; 10 | } 11 | 12 | /* border radius */ 13 | .rounded { 14 | border-radius: 5px !important; 15 | } 16 | 17 | .rounded-lg { 18 | border-radius: 25px !important; 19 | } 20 | 21 | /* lists */ 22 | .list-none { 23 | list-style-type: none !important; 24 | list-style: none !important; 25 | } 26 | 27 | /* shadow */ 28 | .shadow { 29 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 30 | } 31 | .shadow-inner { 32 | box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); 33 | } 34 | .cursor-pointer { 35 | cursor: pointer; 36 | } -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/css/vs2015.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:.5em;background:#1E1E1E;color:#DCDCDC}.hljs-keyword,.hljs-literal,.hljs-symbol,.hljs-name{color:#569CD6}.hljs-link{color:#569CD6;text-decoration:underline}.hljs-built_in,.hljs-type{color:#4EC9B0}.hljs-number,.hljs-class{color:#B8D7A3}.hljs-string,.hljs-meta-string{color:#D69D85}.hljs-regexp,.hljs-template-tag{color:#9A5334}.hljs-subst,.hljs-function,.hljs-title,.hljs-params,.hljs-formula{color:#DCDCDC}.hljs-comment,.hljs-quote{color:#57A64A;font-style:italic}.hljs-doctag{color:#608B4E}.hljs-meta,.hljs-meta-keyword,.hljs-tag{color:#9B9B9B}.hljs-variable,.hljs-template-variable{color:#BD63C5}.hljs-attr,.hljs-attribute,.hljs-builtin-name{color:#9CDCFE}.hljs-section{color:gold}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}.hljs-bullet,.hljs-selector-tag,.hljs-selector-id,.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo{color:#D7BA7D}.hljs-addition{background-color:#144212;display:inline-block;width:100%}.hljs-deletion{background-color:#600;display:inline-block;width:100%} -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | Interact with 3PG through HTTP requests 3 | 4 | --- 5 | 6 | ## Uses 7 | - Bot interaction for webapp 8 | - XP Cards 9 | - Creating payment sessions 10 | - OAuth2 Discord authorization 11 | 12 | ## Status Codes 13 | Code | Description 14 | -----|------------- 15 | 400 | Key is invalid, or an error occurred with the request 16 | 401 | Unauthorized; key not provided or authorized 17 | 404 | Route could not be found 18 | 429 | Too many requests to the API 19 | 500 | Internal server error (rare) 20 | 21 | ## Rate Limiting 22 | 3PG API has rate limiting to limit misuse of the API. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/auto-mod.md: -------------------------------------------------------------------------------- 1 | # Auto Mod 2 | Let 3PG filter explicit content, spam and more! 3 | 4 | --- 5 | 6 | ## Duration 7 | `Not implemented` 8 | 9 | Value | Duration 10 | :-----|:--------- 11 | `3s` | 3 seconds 12 | `3m` | 3 months 13 | `3h` | 3 hours 14 | `3d` | 3 days 15 | `3w` | 3 weeks 16 | `3mo` | 3 months (3 x 30 days) 17 | `3y` | 3 years (3 x 365 days) 18 | `forever` `-1` | Forever 19 | 20 | --- 21 | 22 | ### Auto Delete Messages `true` 23 | Whether to automatically delete messages that are filtered. 24 | 25 | ### Auto Warn Users `false` 26 | Whether to automatically warn users, that type a filtered message. 27 | 28 | ### Ban Links `[]` 29 | Single characters, or parts of a word, that trigger the `Links` filter. 30 | 31 | ### Ban Words `[]` 32 | Single words that trigger the `Words` filter. 33 | 34 | ### Filters `[]` 35 | Filter message content, based on specific conditions. 36 | 37 | | Filter | Trigger Example | Condition 38 | |:-------------------|:-----------------------------------------|:-------------------------------------------| 39 | | MASS_CAPS | TESTING123?!?!?!? | Message Length > `Threshold` and (`Threshold` * 10)% caps (<=50% caps by default) 40 | | LINKS | [saved bad link address] | Message contains custom bad link 41 | | WORDS | [listed bad word] | Any words equal custom bad word 42 | | EMOJI | 🤔🤔🤔🤔🤔🤔🤔🤔🤔 | `Threshold` emojis 43 | | EXPLICIT | [any explicit word blocked by Google] | `Threshold` emojis 44 | | MASS_MENTION | | `Threshold` mentions 45 | | ZALGO | Mͭͭͬu̔ͨ͊tͣ̃̚eͨͭ͐ ҉̴̴̢ | Any zalgo symbols 46 | 47 | --- 48 | 49 | 50 | ### Filter Threshold `5` 51 | The strictness of most of the variable filters. 52 | 53 | ### Ignored Roles `[]` 54 | Roles that are not affected by auto-mod. 55 | A use case for this may be an Admin role, where Admins could `SPAM CAPS IN CHAT` and watch other members suffer trying to do the same thing. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | --- 4 | 5 | ## Cooldowns 6 | Some commands have cooldowns which limit spam. 7 | When a command is in cooldown, its execution is ignored. 8 | 9 | --- 10 | 11 | ## Configs `[]` 12 | Customize existing commands, to your own preferences. 13 | 14 | --- 15 | 16 | ### Name `''` 17 | The name of the command. 18 | 19 | ### Whitelisted Roles `[]` 20 | The roles that can execute the command. 21 | When a user does not have a whitelisted role, they cannot execute the command. 22 | Leave blank to ignore roles in command execution. 23 | 24 | ### Whitelisted Channels `[]` 25 | The channels that the command can be executed in. 26 | For example, if `#general` is a whitelisted channel, then that command can only be executed in `#general` 27 | Leave blank for the command to be executable in all channels. 28 | 29 | ### Enabled `true` 30 | Whether the command can be executed. 31 | When a command is disabled, 3PG will respond with an error message.` 32 | 33 | --- 34 | 35 | ## Custom Commands 36 | Custom commands allow you to create short versions of commands based off existing commands. For example `yt-discord-js` -> `say https://www.youtube.com/watch?v=PuJjkD8zKVI` 37 | 38 | ## Alias 39 | The name of the new command. 40 | 41 | ## Command 42 | The old command syntax. 43 | -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/config-updates.md: -------------------------------------------------------------------------------- 1 | # Get 3PG Dashboard Updates in Your Server 2 | 3 | -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/crates.md: -------------------------------------------------------------------------------- 1 | # Crates 2 | 3 | You can unlock crates by voting for 3PG. 4 | 5 | --- 6 | 7 | ## Rewards 8 | As of 30/05/2020 the chances of getting a reward have increased. 9 | Therefore these chances are not completely accurate. 10 | 11 | ### How it works 12 | A decimal number is rolled from 0 to 1. 13 | If the roll is below the **Chance** value, the reward is increased, until `roll > chance`. 14 | For example if you rolled 0.1 you would get __Tier 3 Legend Badge__. 15 | 16 | The difficulty is currently `2`. 17 | **Chance** is given by `(1 / (i + 1)^difficulty) * 100` where `i` is the index of the reward. 18 | For example, if there are 8 rewards, the possible values for `i` would be from 0-7. 19 | 20 | Reward | Chance | Stackable 21 | :-------|:------|:----- 22 | Nothing | 50% | Yes 23 | Vote Crate | 25% | Yes 24 | Tier 3 Legend Badge | 12.5% | No 25 | Tier 2 Legend Badge | 6.25% | No 26 | Tier 1 Legend Badge | 3.125% | No 27 | 7 days | 1.5625% | Yes 28 | 1 month | 0.78125% | Yes 29 | 3 months | 0.390625% | Yes 30 | 31 | --- 32 | 33 | #### Stackable 34 | Whether the same reward can be earned multiple times. 35 | For example, if unlocks a Tier 3 badge, they cannot unlock the same Tier 3 badge again later. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/credits.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | --- 4 | 5 | ## Development 6 | 7 | **3PG** - ADAMJR#0001 8 | **Avatar** - [aliffitra](https://www.fiverr.com/aliffitra) 9 | 10 | --- 11 | 12 | ## Ideas 13 | 14 | **Command Timers** - Cwis.exe#6169 15 | **Reaction Roles** - Cwis.exe#6169 16 | 17 | **Skilled Bug Slayer** - Griffler#7298 -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/docs-guide.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | A guide on how to maximize your experience, using the 3PG docs. 3 | 4 | --- 5 | 6 | ### Input Name `defaultValue` 7 | 8 | **Default Values**: 9 | `[]` - empty 10 | `''` - empty string (nothing) 11 | `true` - enabled/on 12 | `false` - disabled/off 13 | 14 | **Alerts**: 15 | [!] This is a warning to warn about possibly dangerous things. 16 | [i] This is information to inform about things worth knowing. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | --- 4 | 5 | ## What is the 3PG Dashboard? 6 | The 3PG dashboard is used for customizing saved settings for your Discord server (with 3PG). 7 | 8 | ## Why does my guild not show up? 9 | You must be in the guild and have the `MANAGE_GUILD` permission to edit a server. 10 | 11 | --- 12 | 13 | # Technical Questions 14 | 15 | --- 16 | 17 | ## How was 3PG made? 18 | 3PG is an extension of [2PG](https://2PG.xyz). 19 | 20 | ## What is 3PG's server host? 21 | [Digital Ocean](https://m.do.co/c/be464b522714). 22 | 23 | *"Everyone you refer gets $100 in credit over 60 days."* as of `30/05/2020`. 24 | 25 | An uninterrupted and trustworthy service used by many developers. 26 | I have not had any issues with them over a year so far, and their billing system is extremely flexible. 27 | 28 | They have a Linux control panel to manage their servers, but as long as you know what you're doing, the experience should be smooth. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/general.md: -------------------------------------------------------------------------------- 1 | # General 2 | 3 | --- 4 | 5 | ### Auto Roles `[]` 6 | Roles automatically given to a member upon joining the server. 7 | For example, if `@Member` and `@Special Snowflake` was an auto role, then the member would automatically be given both roles. 8 | 9 | [!] Avoid giving an auto role administrator permissions, otherwise new members could cause a lot of damage. 10 | 11 | ### Ignored Channels `[]` 12 | The channels where command execution is ignored. 13 | For example, if `#general` was ignored, `/ping` would not work in that `#general`. 14 | 15 | ### Prefix `.` 16 | The characters preceding a command. 17 | For example, `/ping` -> prefix is `/` as it precedes the command name. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/get-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | This guide will show you how to get started with 3PG 3 | 4 | --- 5 | 6 | ## Step 1 7 | **Invite the bot** 8 | Invite the bot to your server: https://3PG.xyz/invite. 9 | This will redirect you to a Discord oauth link where you can add the bot. 10 | 11 | ![Invite 3PG via Discord](assets/docs/img/invite-bot.gif) 12 | 13 | --- 14 | 15 | ## Step 2 16 | **Customize 3PG with the Dashboard** 17 | This can be done at https://3PG.xyz/dashboard and clicking on your server, 18 | or by https://3PG.xyz/servers/[yourServerId] and replacing `[yourServerId]` with your server ID. 19 | 20 | ![Customize 3PG via Dashboard](assets/docs/img/customize-bot.gif) 21 | 22 | --- 23 | 24 | ## Step 3 25 | **Use 3PG in your server** 26 | Type commands with the set prefix (default: `.`). 27 | 28 | ![Use 3PG](assets/docs/img/use-bot.gif) -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/how-to-auto-clear-channel-at-intervals.md: -------------------------------------------------------------------------------- 1 | # How to auto clear a Discord text channel at specific intervals 2 | This tutorial will show you how to clear a channel every 12 hours, with 3PG command timers. 3 | 4 | ## Add the Command timer 5 | Go to your `Dashboard -> [your server] -> Timers`. 6 | 7 | ![Command Timer](https://i.gyazo.com/df1992907382a06a1d6115bba246c4f8.png) 8 | 9 | This should clear the channel every 12 hours, starting from the default time: now. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/customize-bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/customize-bot.gif -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-home-v1.x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-home-v1.x.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v1.x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v1.x.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.0.0a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.0.0a.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.0.1a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.0.1a.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.0.2a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.0.2a.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.1.0b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.1.0b.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.1.1b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.1.1b.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.1.2b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.1.2b.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.2.0b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.2.0b.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.3.0b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/dashboard-v2.3.0b.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/invite-bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/invite-bot.gif -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/img/use-bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/docs/img/use-bot.gif -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/leveling.md: -------------------------------------------------------------------------------- 1 | # Leveling 2 | 3 | --- 4 | 5 | # How XP Works 6 | Members earn XP through typing a message. 7 | Each time they send a message, `xpPerMessage` is added to their current XP. 8 | 9 | **Limiting XP**: 10 | - Max messages per minute 11 | - XP per message 12 | 13 | `Precise Level = (-75 + sqrt(75^2 - 300(-150 - xp))) / 150` 14 | 15 | `XP For Next Level = (75(level + 1)^2 + 75(level + 1) - 150) - xp` 16 | 17 | **Earning XP**: 18 | - Auto mod filtered messages do not earn EXP 19 | 20 | ## Ranking 21 | Members rank's are based on their total XP. 22 | There's no secondary ranking system (tiebreakers) - when two users have the same XP. 23 | 24 | --- 25 | 26 | ### Ignored Roles 27 | Roles that are exempt from earning XP. 28 | For example, if you had the `@NoXP` role and the role was an ignored role, you would not be able to earn XP. 29 | 30 | ### Level Roles 31 | Earn a specific role when reaching a level. 32 | For example, if `Level 5` had a level role `@Bronze`, and you just reached level 5, you would receive that role. 33 | 34 | ### Max Messages Per Minute `3` 35 | How many messages, during the same minute, will earn XP. 36 | This helps prevent spam, by limiting the amount of messages that can earn XP within a minute. 37 | 38 | If a user sends more message than this value, they will simply not earn EXP with that message, for that minute. 39 | If it is 16:41 and sends 4 messages, they will only earn EXP for 3 messages and can start earning again when the time is 16:42. 40 | 41 | ### XP Per Message `50` 42 | How much XP is added, each time a user earns XP. 43 | -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/logs.md: -------------------------------------------------------------------------------- 1 | # Logs 2 | Receive messages when something happens. 3 | 4 | [i] Ensure has access to the channel to be able to send messages. 5 | 6 | --- 7 | 8 | ## Events 9 | Custom responses to specific events. 10 | 11 | --- 12 | 13 | ## Types 14 | These types combine both Discord events and 3PG events. 15 | 16 | Event | Description 17 | :-------------------|:---------------------------- 18 | Ban | User is banned on Discord 19 | Config Update | Bot config is changed 20 | Level Up | User reaches a new XP level 21 | Member Join | User joins the guild 22 | Member Leave | User leaves the guild 23 | Message Deleted | User message is deleted 24 | Unban | User is unbanned on Discord 25 | Warn | User is warned with the `warn` command 26 | Mute | User is muted with the `mute` command 27 | 28 | --- 29 | 30 | ## Event Variables 31 | Event variables are used in the message and provide more context to a message. 32 | 33 | Variable | Description | Example | Events 34 | :---------------|:--------------------------------------|:--------------|:-----------------------------| 35 | `[GUILD]` | Guild name | Test Guild | All 36 | `[INSTIGATOR]` | User mention of the punisher | | WARN 37 | `[MEMBER_COUNT]` | Number of members in guild | 420 | All 38 | `[MESSAGE]` | Content of a message | Hello Earth | MESSAGE_DELETED 39 | `[MODULE]` | The name of the module that was updated | General | CONFIG_UPDATE 40 | `[NEW_LEVEL]` | The new level of a member | 2 | LEVEL_UP 41 | `[NEW_VALUE]` | The new value of the config | { "prefix": "." } | CONFIG_UPDATE 42 | `[OLD_LEVEL]` | The old level of a member | 1 | LEVEL_UP 43 | `[OLD_VALUE]` | The old value of the config | { "prefix": "/" } | CONFIG_UPDATE 44 | `[REASON]` | Logged reason for punishment | Spamming '🤔' continuously | WARN 45 | `[USER]` | User mention | | All 46 | `[XP]` | The current xp of a member | 69425 | LEVEL_UP 47 | -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/music.md: -------------------------------------------------------------------------------- 1 | # Music 2 | 3 | --- 4 | 5 | ### Max Track Hours `2` 6 | The max hours that a track can be to be played. 7 | For example, if a track was 20 hours long and the max track hours was 19, then the track would not be playable. 8 | 9 | --- 10 | 11 | # Music Manager 12 | To use the music manager you must first be in a voice channel. 13 | 14 | ## Pause/Resume 15 | This will pause or resume playback. 16 | 17 | ## Play 18 | This will add a track to your playlist, and will join your channel, to play the track. 19 | Make sure that the track duration is less than or equal to `Max Track Hours`, or it will not be queued. 20 | 21 | ## Stop 22 | This will stop playback, clear the playlist, and leave the voice channel. 23 | 24 | ## Volume 25 | This will set the player volume, but not 's volume, to a specified value. 26 | 27 | --- 28 | 29 | ## Skip 30 | This will play the next queued song, if there is one. 31 | 32 | ## Shuffle 33 | This will randomize the current playlist. 34 | 35 | --- 36 | 37 | ## Useful Keybinds (coming soon) 38 | 39 | Key | Description 40 | :---|:---------- 41 | J | Seek +10 seconds 42 | K | Toggle playback (pause/resume) 43 | L | Seek -10 seconds -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/privacy.md: -------------------------------------------------------------------------------- 1 | # Privacy 2 | 3 | --- 4 | 5 | # What is public? 6 | Can be found on search engines. 7 | 8 | - Public leaderboards 9 | 10 | --- 11 | 12 | # What is unlisted? 13 | Only accessible with URL. 14 | 15 | Only members in your guilds, or who have your Guild ID (18 digit number), will be able to use the 3PG API to view these URLs. 16 | 17 | Item | Requirement | URL | Uses 18 | :-----------|:--------------|:--------------|:------------- 19 | A Specific Message | Guild, Channel, and Message ID | `https://3pg.xyz/api/guilds/[guildId]/channels/[channelId]/messages/[messageId]` | Reaction role message preview 20 | Dashboard Audit Log | Guild ID | `https://3pg.xyz/api/guilds/[guildId]/log` | Audit log and Dashboard overview 21 | Channels | Guild ID | `https://3pg.xyz/api/guilds/[guildId]/channels` | Channel input selection 22 | Members | Guild ID | `https://3pg.xyz/api/guilds/[guildId]/members` | Leaderboard 23 | Roles | Guild ID | `https://3pg.xyz/api/guilds/[guildId]/public` | Role input selection 24 | 25 | --- 26 | 27 | # What is private? 28 | Only your Discord account has access. 29 | 30 | - What guilds you are in 31 | - Your current saved guild config 32 | 33 | [!] DISCLAIMER: We try our best to secure your data, but not all items will be listed here, and this document is subject to change with new updates. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/reaction-roles.md: -------------------------------------------------------------------------------- 1 | # Reaction Roles 2 | 3 | --- 4 | 5 | ## Reaction Roles `[]` 6 | React to messages, to get roles. 7 | 8 | ### How does it work? 9 | When a user reacts to a message they get a role. 10 | When they remove the reaction, they lose assigned role. 11 | 12 | --- 13 | 14 | ### Channel `''` 15 | The channel containing the reaction role message. 16 | This is used for getting the message within the channel. 17 | 18 | ### Emote `''` 19 | The emote itself used for the reaction message. 20 | For example, if `🤔` was the emote, then `🤔` will give the assigned role. 21 | 22 | ### Message ID `''` 23 | The ID of the reaction role message. 24 | This is used for identifying the message, and all reaction role operations. 25 | 26 | ### Role `''` 27 | The role given after reacting to the reaction role message. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/timers.md: -------------------------------------------------------------------------------- 1 | # Timers 2 | Automatically perform tasks at specific intervals. 3 | 4 | --- 5 | 6 | ## Command Timers `[]` 7 | Automatically execute commands at specific intervals. 8 | A command prefix is not required for the input as commands are internally executed; not executed in chat. 9 | 10 | **Pattern**: `A-Za-z 0-9` 11 | 12 | ## Message Timers `[]` 13 | Automatically send messages at specific intervals. 14 | The timer is sent at the from date. 15 | 16 | --- 17 | 18 | ## Schedule 19 | This is the current, non-saved, task schedule for 3PG. 20 | This schedule is not saved, hence a restart would reset the schedule. 21 | 22 | Status | Description 23 | :-------|:-------------------------- 24 | Pending | The timer is waiting to start. 25 | Active | The timer has started. 26 | Failed | There is a problem with the timer config, and an error was thrown. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | A guide to fixing 3PG related issues. 3 | 4 | --- 5 | 6 | # Reporting Bugs 7 | Reporting bugs may help finally solve the issue you, and others, are facing with the 3PG dashboard. 8 | 9 | --- 10 | 11 | ## My Dashboard won't load 12 | This could be due to an expired login token, or other possible issues. 13 | 14 | - **Check console**: report errors you see to help reduce bugs 15 | - **Reset cookies**: this should reset login 16 | 17 | --- 18 | 19 | ## 3PG is not responding to messages 20 | This could be the result of your guild config being corrupt. 21 | To fix this, go to `Dashboard -> Settings` and click `Restore Defaults`. -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/docs/tutorials.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | --- 4 | 5 | # [How to auto clear a Discord text channel at specific intervals](/docs/how-to-auto-clear-channel-at-intervals) -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/3PGAvatarTransparent.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/3PGAvatarTransparent.ico -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/3PGProAvatarTransparent.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/3PGProAvatarTransparent.webp -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/badges/alpha.svg: -------------------------------------------------------------------------------- 1 | 5 icon setA -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/badges/bug-destroyer.svg: -------------------------------------------------------------------------------- 1 | 5 icon set -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/badges/early-supporter.svg: -------------------------------------------------------------------------------- 1 | 5 icon set -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/badges/legend.svg: -------------------------------------------------------------------------------- 1 | 5 icon set -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/crates/nothing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/crates/nothing.webp -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/crates/vote-crate.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/crates/vote-crate.webp -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/earth.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | earth -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/home/announce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/home/announce.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/home/leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/home/leaderboard.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/home/music-player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/home/music-player.gif -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/home/music-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/home/music-player.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/home/timers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/home/timers.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/logo.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/overlay_stars.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | overlay_stars_1 -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/assets/img/pro.png -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/assets/img/rocket.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | rocket_1 -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/dist/twopg-dashboard/browser/favicon.ico -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3PG - Customizable Discord Bot 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
© 2020 3PG
32 | 33 | 34 | -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/overlay_stars.21a32cc9bc0c3068cb9a.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | overlay_stars_1 -------------------------------------------------------------------------------- /dist/twopg-dashboard/browser/runtime-es2015.41eb3616844b6c96e4f3.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],p=r[2],c=0,s=[];c(Client); 6 | 7 | export async function getUser(key: any) { 8 | return await auth.getUser(key); 9 | } 10 | 11 | export async function validateBotOwner(key: any) { 12 | if (!key) 13 | throw new TypeError('No key provided.'); 14 | const { id } = await getUser(key); 15 | 16 | if (id !== process.env.OWNER_ID) 17 | throw TypeError('Unauthorized.'); 18 | } 19 | 20 | export async function validateGuildManager(key: any, guildId: string) { 21 | if (!key) 22 | throw new TypeError('No key provided.'); 23 | 24 | const guilds = await getManagableGuilds(key); 25 | if (!guilds.some(g => g.id === guildId)) 26 | throw TypeError('Guild not manageable.'); 27 | } 28 | 29 | export async function getManagableGuilds(key: any) { 30 | return (await auth.getGuilds(key)) 31 | .array() 32 | .filter(g => g.permissions.includes('MANAGE_GUILD')) 33 | .map(g => bot.guilds.cache.get(g.id)) 34 | .filter(g => g); 35 | } 36 | 37 | export function leaderboardMember(user: User, xpInfo: any) { 38 | return { 39 | id: user.id, 40 | username: user.username, 41 | tag: '#' + user.discriminator, 42 | displayAvatarURL: user.displayAvatarURL({ dynamic: true }), 43 | ...xpInfo 44 | }; 45 | } 46 | 47 | export function sendError(res: any, code: number, error: Error) { 48 | return res.status(code).json({ code, message: error?.message }) 49 | } 50 | -------------------------------------------------------------------------------- /src/api/modules/audit-logger.ts: -------------------------------------------------------------------------------- 1 | import { Change } from '../../data/models/log'; 2 | 3 | export default class AuditLogger { 4 | static getChanges(values: { old: {}, new: {} }, module: string, by: string) { 5 | let changes = { old: {}, new: {} }; 6 | 7 | for (const key in values.old) { 8 | const changed = JSON.stringify(values.old[key]) !== JSON.stringify(values.new[key]); 9 | if (changed) { 10 | changes.old[key] = values.old[key]; 11 | changes.new[key] = values.new[key]; 12 | } 13 | } 14 | return new Change(by, changes, module); 15 | } 16 | } -------------------------------------------------------------------------------- /src/api/modules/error-logger.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { promisify } from 'util'; 3 | import { resolve } from 'path'; 4 | 5 | const appendFile = promisify(fs.appendFile); 6 | 7 | export class ErrorLogger { 8 | private logsPath = resolve('./logs'); 9 | private sessionDate = new Date() 10 | .toISOString() 11 | .replace(/:/g, ''); 12 | 13 | private get timestamp() { 14 | return new Date().toISOString(); 15 | } 16 | 17 | async dashboard(message: string) { 18 | await appendFile( 19 | `${this.logsPath}/dashboard/${this.sessionDate}.log`, 20 | `[${this.timestamp}] ${message}\n` 21 | ); 22 | } 23 | 24 | async api(status: number, message: string, route: string) { 25 | await appendFile( 26 | `${this.logsPath}/api/${this.sessionDate}.log`, 27 | `[${this.timestamp}] [${status}] [${route}] ${message}\n` 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/api/modules/image/image-generator.ts: -------------------------------------------------------------------------------- 1 | import { loadImage, Canvas } from 'canvas'; 2 | 3 | export default abstract class ImageGenerator { 4 | abstract generate(...args: any): Promise < Buffer > | Buffer; 5 | 6 | async addBackgroundToCanvas(context, canvas, backgroundURL: string) { 7 | if (backgroundURL && backgroundURL.includes('api')) 8 | throw Error('I don\'t think that\'s a good idea... 🤔'); 9 | 10 | let background = null; 11 | try { 12 | background = await loadImage(backgroundURL || 'api/modules/image/wallpaper.png') 13 | } catch { 14 | return; 15 | } 16 | 17 | context.drawImage(background, 0, 0, canvas.width, canvas.height); 18 | } 19 | async addAvatarToCanvas(context: CanvasRenderingContext2D, imageURL: string) { 20 | context.beginPath(); 21 | context.arc(125, 125, 100, 0, Math.PI * 2, true); 22 | context.closePath(); 23 | context.clip(); 24 | 25 | const avatar: any = await loadImage(imageURL); 26 | context.drawImage(avatar, 25, 25, 200, 200); 27 | } 28 | applyText(canvas: Canvas, text: string) { 29 | const context = canvas.getContext('2d'); 30 | let fontSize = 70; 31 | 32 | do { 33 | context.font = `${fontSize -= 8}px Roboto, sans-serif`; 34 | } 35 | while (context.measureText(text).width > canvas.width - 275); 36 | return context.font; 37 | } 38 | wrapText(context, text, x, y, maxWidth, lineHeight) { 39 | let words = text.split(' '); 40 | let line = ''; 41 | 42 | for (let n = 0; n < words.length; n++) { 43 | let testLine = line + words[n] + ' '; 44 | let metrics = context.measureText(testLine); 45 | let testWidth = metrics.width; 46 | if (testWidth > maxWidth && n > 0) { 47 | context.fillText(line, x, y); 48 | line = words[n] + ' '; 49 | y += lineHeight; 50 | } else { 51 | line = testLine; 52 | } 53 | } 54 | context.fillText(line, x, y); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/api/modules/image/wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3PG/Bot/22134c2f7eab117e3cef99c46b3d091ad1807533/src/api/modules/image/wallpaper.png -------------------------------------------------------------------------------- /src/api/modules/image/xp-card-generator.ts: -------------------------------------------------------------------------------- 1 | import ImageGenerator from './image-generator'; 2 | import { Canvas, createCanvas } from 'canvas'; 3 | import { User, Client } from 'discord.js'; 4 | import { MemberDocument } from '../../../data/models/member'; 5 | import { UserDocument, XPCard } from '../../../data/models/user'; 6 | import Leveling from '../../../modules/xp/leveling'; 7 | import Deps from '../../../utils/deps'; 8 | 9 | export class XPCardGenerator extends ImageGenerator { 10 | colors = { 11 | primary: '#F4F2F3', 12 | secondary: '#46828D', 13 | tertiary: '#36E2CA' 14 | } 15 | 16 | discordUser: User; 17 | 18 | constructor( 19 | private user: UserDocument, 20 | private rank: number, 21 | bot = Deps.get(Client)) { 22 | super(); 23 | 24 | this.discordUser = bot.users.cache.get(user.id); 25 | } 26 | 27 | async generate(savedMember: MemberDocument, preview?: XPCard) { 28 | if (preview) 29 | this.user.xpCard = preview; 30 | 31 | const canvas = createCanvas(700, 250); 32 | const context = canvas.getContext('2d'); 33 | 34 | await super.addBackgroundToCanvas(context, canvas, 35 | this.user.xpCard.backgroundURL); 36 | await this.addXPInfo(context, canvas, savedMember.xp); 37 | this.addUserText(context, canvas); 38 | await super.addAvatarToCanvas(context, 39 | this.discordUser.displayAvatarURL({ format: 'png' })); 40 | 41 | return canvas.toBuffer(); 42 | } 43 | 44 | private addUserText(context, canvas: Canvas) { 45 | let card = this.user.xpCard; 46 | 47 | context.fillStyle = card.tertiary || this.colors.tertiary; 48 | context.font = '32px Roboto, sans-serif'; 49 | context.fillText(`#${this.rank}`, canvas.width / 2.5, canvas.height / 2.5); 50 | 51 | context.fillStyle = card.primary || this.colors.primary; 52 | context.font = super.applyText(canvas, this.discordUser.username); 53 | context.fillText(this.discordUser.username, canvas.width / 2.7, canvas.height / 1.6); 54 | } 55 | 56 | private async addXPInfo(context: CanvasRenderingContext2D, canvas, xp: number) { 57 | let card = this.user.xpCard; 58 | 59 | const sizeOffset = 325; 60 | const position = { x: 275, y: canvas.height * 0.775 }; 61 | const height = 25; 62 | 63 | const { nextLevelXP, level, levelCompletion } = Leveling.xpInfo(xp); 64 | 65 | context.fillStyle = card.secondary || this.colors.secondary; 66 | context.fillRect(position.x, position.y, canvas.width - sizeOffset - 1, height); 67 | 68 | context.fillStyle = card.tertiary || this.colors.tertiary; 69 | context.fillRect(position.x, position.y, 70 | (canvas.width - sizeOffset) * (levelCompletion), height); 71 | 72 | context.fillStyle = card.primary || this.colors.primary; 73 | context.font = '16px Roboto, sans-serif'; 74 | context.fillText(xp.toString(), canvas.width / 2.5, canvas.height / 1.175); 75 | 76 | context.fillStyle = '#0F0F0F'; 77 | context.fillText(`/`, canvas.width / 2.5 + 78 | context.measureText(xp.toString()).width, canvas.height / 1.175); 79 | 80 | context.fillStyle = card.primary || this.colors.primary; 81 | context.fillText(`${nextLevelXP}XP`, canvas.width / 2.5 + 82 | context.measureText(`${xp}/`).width, canvas.height / 1.175); 83 | 84 | context.fillStyle = card.primary || this.colors.primary; 85 | context.fillText(`LEVEL ${level}`, canvas.width / 2.5, canvas.height / 1.35); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/api/modules/ranks.ts: -------------------------------------------------------------------------------- 1 | import { MemberDocument } from '../../data/models/member'; 2 | import { GuildMember } from 'discord.js'; 3 | 4 | export default class Ranks { 5 | static get(member: GuildMember, savedMembers: MemberDocument[]) { 6 | return savedMembers 7 | .sort((a, b) => b.xp - a.xp) 8 | .findIndex(m => m.userId === member.id) + 1; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/api/modules/rate-limiter.ts: -------------------------------------------------------------------------------- 1 | import rateLimit from 'express-rate-limit'; 2 | import RateLimitStore from 'rate-limit-mongo'; 3 | 4 | module.exports = rateLimit({ 5 | max: 300, 6 | message: JSON.stringify({ code: 429, message: 'You are being rate limited.' }), 7 | store: new RateLimitStore({ uri: process.env.MONGO_URI }), 8 | windowMs: 60 * 1000 9 | }); 10 | -------------------------------------------------------------------------------- /src/api/modules/stats.ts: -------------------------------------------------------------------------------- 1 | import Deps from '../../utils/deps'; 2 | import Logs from '../../data/logs'; 3 | import { LogDocument } from '../../data/models/log'; 4 | 5 | const distinct = (v, i, a) => a.indexOf(v) === i; 6 | 7 | export default class Stats { 8 | private savedLogs: LogDocument[] = []; 9 | private initialized = false; 10 | 11 | private _commands: CommandStats[]; 12 | private _general: GeneralStats; 13 | private _inputs: InputStats[]; 14 | private _modules: ModuleStats[]; 15 | 16 | get commands(): CommandStats[] { 17 | if (this.initialized) 18 | return this._commands; 19 | 20 | const names = this.savedLogs 21 | .flatMap(l => l.commands 22 | .flatMap(c => c.name)); 23 | 24 | return names 25 | .filter(distinct) 26 | .map(name => ({ name, count: names.filter(n => n === name).length })) 27 | .sort((a, b) => b.count - a.count); 28 | } 29 | 30 | get general(): GeneralStats { 31 | if (this.initialized) 32 | return this._general; 33 | 34 | const commandsExecuted = this.savedLogs 35 | .reduce((a, b) => a + b.commands.length, 0); 36 | 37 | return { 38 | commandsExecuted, 39 | inputsChanged: this.inputs 40 | .reduce((a, b) => a + b.count, 0), 41 | inputsCount: this.inputs 42 | .map(c => c.path) 43 | .filter(distinct).length, 44 | iq: 10 45 | } 46 | } 47 | 48 | get inputs(): InputStats[] { 49 | if (this.initialized) 50 | return this._inputs; 51 | 52 | const paths = this.savedLogs 53 | .flatMap(l => l.changes 54 | .flatMap(c => Object.keys(c.changes.new) 55 | .flatMap(key => `${c.module}.${key}`))); 56 | 57 | return paths 58 | .filter(distinct) 59 | .map(path => ({ path, count: paths.filter(p => p === path).length })) 60 | .sort((a, b) => b.count - a.count); 61 | } 62 | 63 | get modules(): ModuleStats[] { 64 | if (this.initialized) 65 | return this._modules; 66 | 67 | const moduleNames = this.savedLogs 68 | .flatMap(l => l.changes.map(c => c.module)); 69 | 70 | return moduleNames 71 | .filter(distinct) 72 | .map(name => ({ name, count: moduleNames.filter(m => m === name).length })) 73 | .sort((a, b) => b.count - a.count); 74 | } 75 | 76 | constructor(private logs = Deps.get(Logs)) {} 77 | 78 | async init() { 79 | await this.updateValues(); 80 | 81 | const interval = 30 * 60 * 1000; 82 | setInterval(() => this.updateValues(), interval); 83 | } 84 | 85 | async updateValues() { 86 | this.savedLogs = await this.logs.getAll(); 87 | 88 | this.initialized = false; 89 | 90 | this._commands = this.commands; 91 | this._general = this.general; 92 | this._inputs = this.inputs; 93 | this._modules = this.modules; 94 | 95 | this.initialized = true; 96 | } 97 | } 98 | 99 | export interface CommandStats { 100 | name: string; 101 | count: number; 102 | } 103 | 104 | export interface GeneralStats { 105 | commandsExecuted: number; 106 | inputsCount: number; 107 | inputsChanged: number; 108 | iq: number; 109 | } 110 | 111 | export interface InputStats { 112 | path: string; 113 | count: number; 114 | } 115 | 116 | export interface ModuleStats { 117 | name: string; 118 | count: number; 119 | } -------------------------------------------------------------------------------- /src/api/routes/api-routes.ts: -------------------------------------------------------------------------------- 1 | import { MessageEmbed, Client } from 'discord.js'; 2 | import { Router } from 'express'; 3 | import { CommandDocument, SavedCommand } from '../../data/models/command'; 4 | import Deps from '../../utils/deps'; 5 | import { validateBotOwner, sendError } from '../modules/api-utils'; 6 | import Stats from '../modules/stats'; 7 | import { auth } from '../server'; 8 | import { ErrorLogger } from '../modules/error-logger'; 9 | 10 | export const router = Router(); 11 | 12 | const errorLogger = Deps.get(ErrorLogger); 13 | const stats = Deps.get(Stats); 14 | 15 | let commands: CommandDocument[] = []; 16 | SavedCommand.find().then(cmds => commands = cmds); 17 | 18 | router.get('/', (req, res) => res.json({ hello: 'earth' })); 19 | 20 | router.get('/commands', async (req, res) => res.json(commands)); 21 | 22 | router.get('/auth', async (req, res) => { 23 | try { 24 | const key = await auth.getAccess(req.query.code.toString()); 25 | res.redirect(`${process.env.DASHBOARD_URL}/auth?key=${key}`); 26 | } catch (error) { sendError(res, 400, error); } 27 | }); 28 | 29 | router.post('/error', async(req, res) => { 30 | try { 31 | await errorLogger.dashboard(req.body.message); 32 | } catch (error) { sendError(res, 400, error); } 33 | }); 34 | 35 | router.get('/stats', async (req, res) => { 36 | try { 37 | await validateBotOwner(req.query.key); 38 | 39 | res.json({ 40 | general: stats.general, 41 | commands: stats.commands, 42 | inputs: stats.inputs, 43 | modules: stats.modules 44 | }); 45 | } catch (error) { sendError(res, 400, error); } 46 | }); 47 | 48 | router.get('/invite', (req, res) => 49 | res.redirect(`https://discord.com/api/oauth2/authorize?client_id=${process.env.BOT_ID}&redirect_uri=${process.env.DASHBOARD_URL}/dashboard&response_type=code&permissions=8&scope=bot`)); 50 | 51 | router.get('/login', (req, res) => res.redirect(auth.authCodeLink.url)); 52 | -------------------------------------------------------------------------------- /src/api/routes/pay-routes.ts: -------------------------------------------------------------------------------- 1 | import { stripe } from '../server'; 2 | import { Stripe } from 'stripe'; 3 | import { Router } from 'express'; 4 | import { getUser, sendError } from '../modules/api-utils'; 5 | import Deps from '../../utils/deps'; 6 | import Users from '../../data/users'; 7 | import bodyParser from 'body-parser'; 8 | 9 | const items: Stripe.Checkout.SessionCreateParams.LineItem[] = [ 10 | { 11 | name: '3PG PRO [1 Month]', 12 | description: 'Support 3PG, and unlock exclusive features!', 13 | amount: 500, 14 | currency: 'usd', 15 | quantity: 1 16 | }, 17 | { 18 | name: '3PG PRO [3 Months]', 19 | description: 'Support 3PG, and unlock exclusive features!', 20 | amount: 1000, 21 | currency: 'usd', 22 | quantity: 1 23 | }, 24 | { 25 | name: '3PG PRO [Forever]', 26 | description: 'Support 3PG, and unlock exclusive features!', 27 | amount: 2500, 28 | currency: 'usd', 29 | quantity: 1 30 | } 31 | ]; 32 | 33 | export const router = Router(); 34 | 35 | const users = Deps.get(Users); 36 | 37 | router.get('/user/pay', async(req, res) => { 38 | try { 39 | const { key, plan } = req.query as any; 40 | const { id } = await getUser(key); 41 | 42 | const session = await stripe.checkout.sessions.create({ 43 | success_url: `${process.env.DASHBOARD_URL}/payment-success`, 44 | cancel_url: `${process.env.DASHBOARD_URL}/plus`, 45 | payment_method_types: ['card'], 46 | metadata: { id, plan }, 47 | line_items: [ items[+plan] ] 48 | }); 49 | res.send(session); 50 | } catch (error) { sendError(res, 400, error); } 51 | }); 52 | 53 | router.post('/stripe-webhook', bodyParser.raw({ type: 'application/json' }), async(req, res) => { 54 | try { 55 | let event = stripe.webhooks.constructEvent( 56 | req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET); 57 | 58 | if (event.type === 'checkout.session.completed') { 59 | const { id, plan } = (event.data.object as any).metadata; 60 | await users.givePro(id, +plan); 61 | 62 | return res.json({ success: true }); 63 | } 64 | res.json({ received: true }); 65 | } catch (error) { sendError(res, 400, error); } 66 | }); -------------------------------------------------------------------------------- /src/api/routes/user-routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { XPCardGenerator } from '../modules/image/xp-card-generator'; 3 | import { SavedMember } from '../../data/models/member'; 4 | import Deps from '../../utils/deps'; 5 | import Users, { Plan } from '../../data/users'; 6 | import { sendError } from '../modules/api-utils'; 7 | import { getUser } from '../modules/api-utils'; 8 | import { Client, User } from 'discord.js'; 9 | import { UserDocument } from '../../data/models/user'; 10 | 11 | export const router = Router(); 12 | 13 | const bot = Deps.get(Client), 14 | users = Deps.get(Users); 15 | 16 | router.get('/', async (req, res) => { 17 | try { 18 | const user = await getUser(req.query.key); 19 | res.json(user); 20 | } catch (error) { sendError(res, 400, error); } 21 | }); 22 | 23 | router.get('/saved', async (req, res) => { 24 | try { 25 | const user = await getUser(req.query.key); 26 | const savedUser = await users.get(user); 27 | res.json(savedUser); 28 | } catch (error) { sendError(res, 400, error); } 29 | }); 30 | 31 | router.get('/xp-card-preview', async (req, res) => { 32 | try { 33 | delete req.query.cache; 34 | 35 | const user = await getUser(req.query.key); 36 | const savedUser = await users.get(user); 37 | if (!savedUser) 38 | return res.status(404).send('User not found'); 39 | 40 | const rank = 1; 41 | const generator = new XPCardGenerator(savedUser, rank); 42 | 43 | const member = new SavedMember(); 44 | member.xp = 1800; 45 | 46 | delete req.query.key; 47 | const image = await generator.generate(member, { ...savedUser.xpCard, ...req.query }); 48 | 49 | res.set({'Content-Type': 'image/png'}).send(image); 50 | } catch (error) { sendError(res, 400, error); } 51 | }); 52 | 53 | router.put('/xp-card', async (req, res) => { 54 | try { 55 | const user = await getUser(req.query.key); 56 | const savedUser = await users.get(user); 57 | 58 | savedUser.xpCard = req.body; 59 | await savedUser.save(); 60 | 61 | res.send(savedUser); 62 | } catch (error) { sendError(res, 400, error); } 63 | }); 64 | 65 | router.put('/refer', async (req, res) => { 66 | try { 67 | const user = await getUser(req.query.key); 68 | const savedUser = await users.get(user); 69 | 70 | const targetUser = await validateReferral(req.body.tag, user, savedUser); 71 | 72 | savedUser.referralIds.push(targetUser.id); 73 | await savedUser.save(); 74 | 75 | if (savedUser.referralIds.length === 3) 76 | await users.givePro(user.id, Plan.One); 77 | 78 | res.send(savedUser); 79 | } catch (error) { sendError(res, 400, error); } 80 | }); 81 | 82 | router.get('/:id', (req, res) => { 83 | try { 84 | res.send(bot.users.cache.get(req.params.id)); 85 | } catch (error) { sendError(res, 400, error); } 86 | }); 87 | 88 | async function validateReferral(tag: string, user: User | any, savedUser: UserDocument) { 89 | const isValidUserTag = /^.+#\d{4}/.test(tag); 90 | if (!isValidUserTag) 91 | throw new TypeError('Target user tag is invalid.'); 92 | 93 | const targetUser = bot.users.cache.find(u => u.tag === tag); 94 | if (!targetUser) 95 | throw new TypeError('Target user not found in any serverss.'); 96 | 97 | const owns3PGGuild = bot.guilds.cache.some(g => g.ownerID === targetUser.id); 98 | if (!owns3PGGuild) 99 | throw new TypeError('Target user does own a server with 3PG.'); 100 | 101 | if (targetUser.id === user.id) 102 | throw new TypeError('You cannot refer yourself!'); 103 | if (targetUser.bot) 104 | throw new TypeError('You cannot refer a bot.'); 105 | 106 | const savedUsers = await users.getAll(); 107 | if (savedUser.referralIds.includes(targetUser.id)) 108 | throw new TypeError('You have already referred this user.'); 109 | 110 | const alreadyReferred = savedUsers 111 | .some(su => su.referralIds.includes(targetUser.id)); 112 | if (alreadyReferred) 113 | throw new TypeError('Someone else has already referred this user.'); 114 | 115 | return targetUser; 116 | } 117 | -------------------------------------------------------------------------------- /src/api/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import AuthClient from '@2pg/oauth'; 4 | import bodyParser from 'body-parser'; 5 | import { Stripe } from 'stripe'; 6 | import { join } from 'path'; 7 | import Log from '../utils/log'; 8 | import Stats from './modules/stats'; 9 | import Deps from '../utils/deps'; 10 | import { WebSocket } from './websocket'; 11 | 12 | import { router as apiRoutes } from './routes/api-routes'; 13 | import { router as guildsRoutes } from './routes/guilds-routes'; 14 | import { router as musicRoutes } from './routes/music-routes'; 15 | import { router as payRoutes } from './routes/pay-routes'; 16 | import { router as userRoutes } from './routes/user-routes'; 17 | 18 | export const app = express(); 19 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2020-08-27' }); 20 | export const auth = new AuthClient({ 21 | id: process.env.BOT_ID, 22 | secret: process.env.CLIENT_SECRET, 23 | redirectURI: `${process.env.API_URL}/auth`, 24 | scopes: ['identify', 'guilds'] 25 | }); 26 | 27 | export default class API { 28 | constructor( 29 | private stats = Deps.get(Stats), 30 | private ws = Deps.get(WebSocket)) { 31 | app.use(cors()); 32 | 33 | app.use('/api', payRoutes); 34 | 35 | app.use(bodyParser.json()); 36 | 37 | app.use('/api/guilds/:id/music', musicRoutes); 38 | app.use('/api/guilds', guildsRoutes); 39 | app.use('/api/user', userRoutes); 40 | app.use('/api', apiRoutes); 41 | 42 | app.get('/api/*', (req, res) => res 43 | .status(404) 44 | .json({ code: 404 })); 45 | 46 | const distPath = join(process.cwd(), '/dist/twopg-dashboard/browser'); 47 | app.use(express.static(distPath)); 48 | 49 | app.all('*', (req, res) => res 50 | .status(200) 51 | .sendFile(`${distPath}/index.html`)); 52 | 53 | const port = process.env.PORT || 3000; 54 | const server = app.listen(port, () => Log.info(`API is live on port ${port}`)); 55 | 56 | this.ws.init(server); 57 | this.stats.init(); 58 | } 59 | } -------------------------------------------------------------------------------- /src/api/websocket.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import { listen, Server as SocketServer } from 'socket.io'; 3 | import Log from '../utils/log'; 4 | import WSEvent from './ws-events/ws-event'; 5 | import { resolve } from 'path'; 6 | import { readdirSync } from 'fs'; 7 | 8 | export class WebSocket { 9 | events: WSEvent[] = []; 10 | io: SocketServer; 11 | 12 | sessions = new Map(); 13 | 14 | get connectedUserIds() { 15 | return Array.from(this.sessions.values()); 16 | } 17 | 18 | init(server: Server) { 19 | this.io = listen(server); 20 | 21 | const dir = resolve(`${__dirname}/ws-events`); 22 | const files = readdirSync(dir); 23 | 24 | for (const file of files) { 25 | const Event = require(`./ws-events/${file}`).default; 26 | try { 27 | const event = new Event(); 28 | this.events.push(event); 29 | } catch {} 30 | } 31 | 32 | Log.info(`Loaded ${this.events.length} handlers`, 'ws'); 33 | 34 | this.io.on('connection', (client) => { 35 | for (const event of this.events) 36 | client.on(event.on, (data) => event.invoke.bind(event)(this, client, data)); 37 | }); 38 | 39 | Log.info('Started WebSocket', 'ws'); 40 | } 41 | } -------------------------------------------------------------------------------- /src/api/ws-events/guild-drag.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io'; 2 | import Users from '../../data/users'; 3 | import Deps from '../../utils/deps'; 4 | import { WebSocket } from '../websocket'; 5 | import WSEvent from './ws-event'; 6 | 7 | export default class implements WSEvent { 8 | on = 'GUILD_DRAG'; 9 | 10 | constructor(private users = Deps.get(Users)) {} 11 | 12 | async invoke(ws: WebSocket, client: Socket, { userId, guildPositions }: any) { 13 | const savedUser = await this.users.get({ id: userId }); 14 | savedUser.guildPositions = guildPositions; 15 | await savedUser.save(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/api/ws-events/ws-event.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io'; 2 | import { WebSocket } from '../websocket'; 3 | 4 | export default interface WSEvent { 5 | on: string; 6 | 7 | invoke: (ws: WebSocket, client: Socket, data: any) => any; 8 | } -------------------------------------------------------------------------------- /src/bot.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | config(); 3 | 4 | import { Client } from 'discord.js'; 5 | import mongoose from 'mongoose'; 6 | import Deps from './utils/deps'; 7 | 8 | import EventService from './services/events.service'; 9 | import Log from './utils/log'; 10 | import API from './api/server'; 11 | import EventsService from './services/events.service'; 12 | 13 | export const bot = Deps.get(Client); 14 | bot.options.messageCacheLifetime = 0; 15 | bot.options.messageCacheMaxSize = 16; 16 | bot.options.partials = ['GUILD_MEMBER', 'MESSAGE', 'REACTION']; 17 | 18 | bot.login(process.env.BOT_TOKEN); 19 | 20 | Deps.get(EventsService).init(); 21 | Deps.build(API); 22 | 23 | mongoose.connect(process.env.MONGO_URI, { 24 | useUnifiedTopology: true, 25 | useNewUrlParser: true, 26 | useFindAndModify: false 27 | }, (error) => error 28 | ? Log.error(error.message, 'data') 29 | : Log.info('Connected to db', 'data')); 30 | 31 | import './keep-alive'; -------------------------------------------------------------------------------- /src/commands/clear.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class ClearCommand implements Command { 4 | precondition: Permission = 'MANAGE_MESSAGES'; 5 | name = 'clear'; 6 | usage = 'clear [count = 100]'; 7 | summary = 'Clear all messages that are less than 2 weeks old.'; 8 | cooldown = 5; 9 | module = 'Auto-mod'; 10 | 11 | execute = async(ctx: CommandContext, count = '100') => { 12 | const msgs = await ctx.channel.bulkDelete(+count); 13 | const reminder = await ctx.channel.send(`Deleted \`${msgs.size}\` messages`); 14 | setTimeout(() => reminder.delete(), 3 * 1000); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/command.ts: -------------------------------------------------------------------------------- 1 | import { Message, GuildMember, TextChannel, Guild, User, Client, PermissionString } from 'discord.js'; 2 | 3 | export type Permission = '' | PermissionString; 4 | 5 | export interface Command { 6 | aliases?: string[]; 7 | name: string; 8 | summary: string; 9 | module: string; 10 | precondition: string; 11 | usage?: string; 12 | cooldown?: number; 13 | 14 | execute: (ctx: CommandContext, ...args: any) => Promise | void; 15 | } 16 | 17 | export class CommandContext { 18 | msg: Message; 19 | member: GuildMember; 20 | channel: TextChannel; 21 | guild: Guild; 22 | user: User; 23 | bot: Client; 24 | 25 | constructor(msg: any) { 26 | this.msg = msg; 27 | this.member = msg.member; 28 | this.channel = msg.channel; 29 | this.guild = msg.guild; 30 | this.user = msg.member.user; 31 | this.bot = msg.client; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class DashboardCommand implements Command { 4 | precondition: Permission = 'MANAGE_GUILD'; 5 | name = 'dashboard'; 6 | summary = `Get a link to the server's dashboard`; 7 | cooldown = 3; 8 | module = 'General'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | return ctx.channel.send(`${process.env.DASHBOARD_URL}/servers/${ctx.guild.id}`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/flip.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class FlipCommand implements Command { 4 | precondition: Permission = ''; 5 | name = 'flip'; 6 | summary = 'Heads or Tails?'; 7 | cooldown = 1; 8 | module = 'General'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | const result = (Math.random() >= 0.5) ? 'Heads' : 'Tails'; 12 | return ctx.channel.send(result); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class HelpCommand implements Command { 4 | precondition: Permission = ''; 5 | name = 'help'; 6 | summary = 'Send help...'; 7 | cooldown = 3; 8 | module = 'General'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | ctx.channel.send(`${process.env.DASHBOARD_URL}/commands`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/info.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import { MessageEmbed } from 'discord.js'; 3 | 4 | export default class InfoCommand implements Command { 5 | precondition: Permission = ''; 6 | name = 'info'; 7 | summary = 'Get stats about 3PG'; 8 | cooldown = 1; 9 | module = 'General'; 10 | 11 | execute = async(ctx: CommandContext) => { 12 | const uptimeHours = (ctx.bot.uptime / 1000 / 60 / 60).toFixed(2); 13 | 14 | return ctx.channel.send(new MessageEmbed({ 15 | title: `**__${ctx.bot.user.tag} Info__**`, 16 | fields: [ 17 | { name: 'Created', value: `\`${ctx.bot.user.createdAt.toDateString()}\``, inline: true }, 18 | { name: 'Creator', value: `<@!${process.env.OWNER_ID}>`, inline: true }, 19 | { name: 'ID', value: `\`${ctx.bot.user.id}\``, inline: true }, 20 | { name: 'IQ', value: `\`1000\``, inline: true }, 21 | { name: 'Memes', value: `\`0\``, inline: true }, 22 | { name: 'Shards', value: `\`${ctx.bot.shard?.count ?? 0}\``, inline: true }, 23 | { name: 'Uptime', value: `\`${uptimeHours} hours\``, inline: true } 24 | ], 25 | 26 | }).setThumbnail(ctx.bot.user.avatarURL())); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/leaderboard.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class LeaderboardCommand implements Command { 4 | precondition: Permission = ''; 5 | name = 'leaderboard'; 6 | summary = `Get a link to the server's leaderboard`; 7 | cooldown = 3; 8 | module = 'Leveling'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | ctx.channel.send(`${process.env.DASHBOARD_URL}/leaderboard/${ctx.guild.id}`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/levels.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Guilds from '../data/guilds'; 3 | import Deps from '../utils/deps'; 4 | 5 | export default class LevelsCommand implements Command { 6 | precondition: Permission = ''; 7 | name = 'levels'; 8 | summary = `List all your server's level roles`; 9 | cooldown = 3; 10 | module = 'Leveling'; 11 | 12 | constructor(private guilds = Deps.get(Guilds)) {} 13 | 14 | execute = async(ctx: CommandContext) => { 15 | const savedGuild = await this.guilds.get(ctx.guild); 16 | 17 | let details = ''; 18 | for (const levelRole of savedGuild.leveling.levelRoles) 19 | details += `**Level \`${levelRole.level}\`**: <@&${levelRole.role}>\n`; 20 | 21 | return ctx.channel.send(details || 'No level roles set.'); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/list.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import Music from '../modules/music/music'; 4 | import { Track } from '@2pg/music'; 5 | 6 | export default class ListCommand implements Command { 7 | aliases = ['q']; 8 | name = 'list'; 9 | summary = 'Display the current track list.'; 10 | precondition: Permission = 'SPEAK'; 11 | cooldown = 3; 12 | module = 'Music'; 13 | 14 | constructor(private music = Deps.get(Music)) {} 15 | 16 | execute = async(ctx: CommandContext) => { 17 | const player = this.music.joinAndGetPlayer(ctx.member.voice.channel, ctx.channel); 18 | 19 | let details = ''; 20 | for (let i = 0; i < player.q.length; i++) { 21 | const track: Track = player.q.items[i]; 22 | const prefix = (i === 0) 23 | ? `**Now Playing**:` 24 | : `**[${i + 1}]**`; 25 | details += `${prefix} \`${track.title}\` \`${this.music.getDuration(player, track)}\`\n`; 26 | } 27 | return ctx.channel.send(details || 'No tracks in list.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/lock.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class LockCommand implements Command { 4 | precondition: Permission = 'MANAGE_CHANNELS'; 5 | name = 'lock'; 6 | summary = 'Stop messages in the current channel.'; 7 | cooldown = 5; 8 | module = 'Auto-mod'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | ctx.channel.overwritePermissions([ 12 | { 13 | id: ctx.guild.roles.everyone.id, 14 | type: 'role', 15 | deny: ['SEND_MESSAGES'], 16 | }, 17 | ], 'Channel locked'); 18 | 19 | return ctx.channel.send(`🔒 Locked <#${ctx.channel.id}>`); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/music.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class MusicCommand implements Command { 4 | precondition: Permission = 'MANAGE_GUILD'; 5 | name = 'music'; 6 | summary = `Get a link to the server's music manager`; 7 | cooldown = 3; 8 | module = 'Music'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | return ctx.channel.send(`${process.env.DASHBOARD_URL}/servers/${ctx.guild.id}/music`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/mute.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import AutoMod from '../modules/auto-mod/auto-mod'; 3 | import Deps from '../utils/deps'; 4 | import { getMemberFromMention, parseDuration } from '../utils/command-utils'; 5 | 6 | export default class MuteCommand implements Command { 7 | precondition: Permission = 'MUTE_MEMBERS'; 8 | name = 'mute'; 9 | usage = `mute user [reason = 'Unspecified']`; 10 | summary = 'Stop a user from sending messages. Check docs for duration values.'; 11 | cooldown = 3; 12 | module = 'Auto-mod'; 13 | 14 | constructor( 15 | private autoMod = Deps.get(AutoMod)) {} 16 | 17 | execute = async(ctx: CommandContext, targetMention: string, ...args: string[]) => { 18 | const target = getMemberFromMention(targetMention, ctx.guild); 19 | 20 | const reason = args?.join(' ') || 'Unspecified'; 21 | await this.autoMod.mute(target, { instigator: ctx.user, reason }); 22 | 23 | await ctx.channel.send(`> <@!${target.id}> was muted for \`${reason}\``); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/pause.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import Music from '../modules/music/music'; 4 | 5 | export default class PauseCommand implements Command { 6 | name = 'pause'; 7 | summary = 'Pause playback if playing.'; 8 | precondition: Permission = 'SPEAK'; 9 | module = 'Music'; 10 | 11 | constructor(private music = Deps.get(Music)) {} 12 | 13 | execute = async (ctx: CommandContext) => { 14 | const player = this.music.joinAndGetPlayer(ctx.member.voice.channel, ctx.channel); 15 | 16 | if (player.isPaused) 17 | throw new TypeError('Player is already paused.'); 18 | 19 | await player.pause(); 20 | 21 | ctx.channel.send(`**Paused**: \`${player.q.peek()?.title}\``); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/ping.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class PingCommand implements Command { 4 | precondition: Permission = ''; 5 | name = 'ping'; 6 | summary = 'Probably the best command ever created.'; 7 | cooldown = 3; 8 | module = 'General'; 9 | 10 | execute = (ctx: CommandContext) => ctx.channel.send(`🏓 Pong! \`${ctx.bot.ws.ping}ms\``); 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/play.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import { GuildMember } from 'discord.js'; 4 | import Music from '../modules/music/music'; 5 | 6 | export default class PlayCommand implements Command { 7 | aliases = ['p']; 8 | cooldown = 2; 9 | module = 'Music'; 10 | name = 'play'; 11 | precondition: Permission = 'SPEAK'; 12 | summary = 'Join and play a YouTube result.'; 13 | usage = 'play query' 14 | 15 | constructor(private music = Deps.get(Music)) {} 16 | 17 | execute = async(ctx: CommandContext, ...args: string[]) => { 18 | const query = args?.join(' '); 19 | if (!query) 20 | throw new TypeError('Query must be provided.'); 21 | 22 | const player = this.music.joinAndGetPlayer(ctx.member.voice.channel, ctx.channel); 23 | 24 | const maxQueueSize = 5; 25 | if (player.q.length >= maxQueueSize) 26 | throw new TypeError(`Max queue size of \`${maxQueueSize}\` reached.`); 27 | 28 | const track = await player.play(query); 29 | if (player.isPlaying) 30 | return ctx.channel.send(`**Added**: \`${track.title}\` to list.`); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/resume.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import Music from '../modules/music/music'; 4 | 5 | export default class ResumeCommand implements Command { 6 | name = 'resume'; 7 | summary = 'Resume playing a track if paused.'; 8 | precondition: Permission = 'SPEAK'; 9 | module = 'Music'; 10 | 11 | constructor(private music = Deps.get(Music)) {} 12 | 13 | execute = async (ctx: CommandContext) => { 14 | const player = this.music.joinAndGetPlayer(ctx.member.voice.channel, ctx.channel); 15 | 16 | if (!player.isPaused) 17 | throw new TypeError('Player is already resumed.'); 18 | 19 | await player.resume(); 20 | 21 | ctx.channel.send(`**Resumed**: \`${player.q.peek().title}\``); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/role.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import { MessageEmbed } from 'discord.js'; 3 | import { getRoleFromMention } from '../utils/command-utils'; 4 | 5 | export default class RoleCommand implements Command { 6 | precondition: Permission = ''; 7 | name = 'role'; 8 | summary = 'Get info about a specific role'; 9 | cooldown = 1; 10 | usage = 'role [role]'; 11 | module = 'General'; 12 | 13 | execute = async(ctx: CommandContext, roleMention: string) => { 14 | const role = getRoleFromMention(roleMention, ctx.guild); 15 | 16 | const emojiBoolean = (condition) => condition ? '✅' : '❌'; 17 | 18 | return ctx.channel.send(new MessageEmbed({ 19 | color: role.color, 20 | title: `@${role.name}`, 21 | fields: [ 22 | { name: 'ID', value: `\`${role.id}\``, inline: true }, 23 | { name: 'Created', value: `\`${role.createdAt.toDateString()}\``, inline: true }, 24 | { name: 'Position', value: `\`${role.position}\``, inline: true }, 25 | { name: 'Members', value: `\`${role.members.size}\``, inline: true }, 26 | { name: 'Mentionable', value: emojiBoolean(role.mentionable), inline: true }, 27 | { name: 'Hoisted', value: emojiBoolean(role.hoist), inline: true }, 28 | { name: 'Managed', value: emojiBoolean(role.managed), inline: true }, 29 | ] 30 | }).setThumbnail(ctx.guild.iconURL())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/say.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class SayCommand implements Command { 4 | precondition: Permission = 'MANAGE_MESSAGES'; 5 | name = 'say'; 6 | usage = 'say 3PG is the best bot'; 7 | summary = 'Get 3PG to say... anything.'; 8 | cooldown = 3; 9 | module = 'General'; 10 | 11 | execute = async(ctx: CommandContext, ...args: string[]) => { 12 | return ctx.channel.send(args?.join(' ')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/seek.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import Music from '../modules/music/music'; 4 | 5 | export default class SeekCommand implements Command { 6 | precondition: Permission = 'SPEAK'; 7 | name = 'seek'; 8 | usage = 'seek [position]'; 9 | summary = 'View current track position, or go to a position in a track.'; 10 | cooldown = 1; 11 | module = 'Music'; 12 | 13 | constructor(private music = Deps.get(Music)) {} 14 | 15 | execute = async(ctx: CommandContext, position: string) => { 16 | const player = this.music.joinAndGetPlayer(ctx.member.voice.channel, ctx.channel); 17 | 18 | if (player.q.length <= 0) 19 | throw new TypeError('No tracks currently playing'); 20 | 21 | const pos = Number(position); 22 | if (!pos) 23 | return ctx.channel.send(`Track at: \`${this.music.getDuration(player)}\``); 24 | 25 | // await player.seek(pos * 1000); // TODO: implement 26 | 27 | return ctx.channel.send(`Now at \`${this.music.getDuration(player)}\`.`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/server.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import { MessageEmbed } from 'discord.js'; 3 | 4 | export default class ServerCommand implements Command { 5 | precondition: Permission = ''; 6 | name = 'server'; 7 | summary = 'Get stats about your server'; 8 | cooldown = 1; 9 | module = 'General'; 10 | 11 | execute = async(ctx: CommandContext) => { 12 | return ctx.channel.send(new MessageEmbed({ 13 | title: `**__${ctx.guild.name}__**`, 14 | fields: [ 15 | { name: 'Channels', value: `\`${ctx.guild.channels.cache.size}\``, inline: true }, 16 | { name: 'Created', value: `\`${ctx.guild.createdAt.toDateString()}\``, inline: true }, 17 | { name: 'ID', value: `\`${ctx.guild.id}\``, inline: true }, 18 | { name: 'Members', value: `\`${ctx.guild.members.cache.size}\``, inline: true }, 19 | { name: 'Owner', value: `<@!${ctx.guild.ownerID}>`, inline: true }, 20 | { name: 'Roles', value: `\`${ctx.guild.roles.cache.size}\``, inline: true } 21 | ], 22 | 23 | }).setThumbnail(ctx.guild.iconURL())); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/shuffle.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import Music from '../modules/music/music'; 4 | 5 | export default class ShuffleCommand implements Command { 6 | precondition: Permission = 'SPEAK'; 7 | name = 'shuffle'; 8 | summary = 'Shuffle a playlist.'; 9 | cooldown = 3; 10 | module = 'Music'; 11 | 12 | constructor(private music = Deps.get(Music)) {} 13 | 14 | execute = async(ctx: CommandContext) => { 15 | const player = this.music.joinAndGetPlayer(ctx.member.voice.channel, ctx.channel); 16 | player.q.shuffle(); 17 | 18 | return ctx.channel.send('List shuffled.'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/skip.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import Music from '../modules/music/music'; 4 | 5 | export default class SkipCommand implements Command { 6 | name = 'skip'; 7 | summary = 'Skip current playing track'; 8 | precondition: Permission = 'SPEAK'; 9 | cooldown = 5; 10 | module = 'Music'; 11 | 12 | constructor(private music = Deps.get(Music)) {} 13 | 14 | execute = async(ctx: CommandContext) => { 15 | const player = this.music.joinAndGetPlayer(ctx.member.voice.channel, ctx.channel); 16 | player.skip(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/stop.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Deps from '../utils/deps'; 3 | import Music from '../modules/music/music'; 4 | 5 | export default class StopCommand implements Command { 6 | aliases = ['leave']; 7 | name = 'stop'; 8 | summary = 'Stop playback, clear list, and leave channel'; 9 | precondition: Permission = 'SPEAK'; 10 | cooldown = 5; 11 | module = 'Music'; 12 | 13 | constructor(private music = Deps.get(Music)) {} 14 | 15 | execute = (ctx: CommandContext) => { 16 | 17 | const player = this.music.client.players.get(ctx.guild.id) 18 | if (!player) 19 | throw new TypeError('Not currently playing any track.'); 20 | 21 | player.stop(); 22 | player.leave(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/unlock.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class UnlockCommand implements Command { 4 | precondition: Permission = 'MANAGE_CHANNELS'; 5 | name = 'unlock'; 6 | summary = 'Allow messages in the current channel.'; 7 | cooldown = 5; 8 | module = 'Auto-mod'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | ctx.channel.overwritePermissions([ 12 | { 13 | id: ctx.guild.roles.everyone.id, 14 | type: 'role', 15 | allow: ['SEND_MESSAGES'], 16 | }, 17 | ], 'Channel unlocked'); 18 | 19 | return ctx.channel.send(`🔓 Unlocked <#${ctx.channel.id}>`); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/unmute.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import AutoMod from '../modules/auto-mod/auto-mod'; 3 | import Deps from '../utils/deps'; 4 | import { getMemberFromMention } from '../utils/command-utils'; 5 | import Guilds from '../data/guilds'; 6 | 7 | export default class UnmuteCommand implements Command { 8 | precondition: Permission = 'MUTE_MEMBERS'; 9 | name = 'unmute'; 10 | usage = 'unmute target_id/mention'; 11 | summary = 'Allow a user to send messages.'; 12 | cooldown = 3; 13 | module = 'Auto-mod'; 14 | 15 | constructor( 16 | private autoMod = Deps.get(AutoMod)) {} 17 | 18 | execute = async(ctx: CommandContext, targetMention: string, ...args: string[]) => { 19 | const target = getMemberFromMention(targetMention, ctx.guild); 20 | 21 | const reason = args?.join(' ') || 'Unspecified'; 22 | await this.autoMod.unmute(target, { instigator: ctx.user, reason }); 23 | 24 | await ctx.channel.send(`<@!${target.id}> was unmuted for \`${reason}\``); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/vote.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | 3 | export default class FlipCommand implements Command { 4 | precondition: Permission = ''; 5 | name = 'vote'; 6 | summary = 'Get 3PG voting links, and support 3PG'; 7 | cooldown = 1; 8 | module = 'General'; 9 | 10 | execute = async(ctx: CommandContext) => { 11 | return ctx.channel.send(` 12 | https://dbots.co/bots/525935335918665760/vote 13 | https://top.gg/bot/525935335918665760/vote 14 | https://discordbotlist.com/bots/525935335918665760/upvote`.trim() 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/warn.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import AutoMod from '../modules/auto-mod/auto-mod'; 3 | import Deps from '../utils/deps'; 4 | import { getMemberFromMention } from '../utils/command-utils'; 5 | 6 | export default class WarnCommand implements Command { 7 | precondition: Permission = 'KICK_MEMBERS'; 8 | name = 'warn'; 9 | usage = 'warn user reason'; 10 | summary = 'Warn a user and add a warning to their account.'; 11 | cooldown = 5; 12 | module = 'Auto-mod'; 13 | 14 | constructor(private autoMod = Deps.get(AutoMod)) {} 15 | 16 | execute = async(ctx: CommandContext, targetMention: string, ...args: string[]) => { 17 | const reason = args?.join(' '); 18 | if (!reason) 19 | throw new TypeError('Why warn someone for no reason :thinking: :joy:?'); 20 | 21 | const target = (targetMention) ? 22 | getMemberFromMention(targetMention, ctx.guild) : ctx.member; 23 | 24 | await this.autoMod.warn(target, ctx.channel, { instigator: ctx.user, reason }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/warnings.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import Members from '../data/members'; 3 | import { TextChannel } from 'discord.js'; 4 | import { MemberDocument } from '../data/models/member'; 5 | import Deps from '../utils/deps'; 6 | import { getMemberFromMention } from '../utils/command-utils'; 7 | 8 | export default class WarningsCommand implements Command { 9 | precondition: Permission = 'VIEW_AUDIT_LOG'; 10 | name = 'warnings'; 11 | usage = 'warnings [user]' 12 | summary = 'Display your warnings, or the warnings of a member.'; 13 | cooldown = 3; 14 | module = 'Auto-mod'; 15 | 16 | constructor( 17 | private members = Deps.get(Members)) {} 18 | 19 | execute = async(ctx: CommandContext, userMention?: string, position?: string) => { 20 | const target = (userMention) ? 21 | getMemberFromMention(userMention, ctx.guild) : ctx.member; 22 | 23 | const savedMember = await this.members.get(target); 24 | 25 | if (position) 26 | return this.displayWarning(+position, savedMember, ctx.channel); 27 | 28 | await ctx.channel.send(`User has \`${savedMember.warnings.length}\` warnings.`) 29 | } 30 | 31 | private async displayWarning(position: number, savedMember: MemberDocument, channel: TextChannel) { 32 | if (position <= 0 || position > savedMember.warnings.length) 33 | throw new TypeError('Warning at position not found on user.'); 34 | 35 | const warning = savedMember.warnings[position - 1]; 36 | const instigator = channel.client.users.cache.get(warning.instigatorId); 37 | channel.send(`**Warning #${position}**\n**By**: <@!${instigator ?? 'N/A'}>\n**For**: \`${warning.reason}\``); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/xp.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandContext, Permission } from './command'; 2 | import { getMemberFromMention } from '../utils/command-utils'; 3 | 4 | export default class XPCommand implements Command { 5 | precondition: Permission = ''; 6 | name = 'xp'; 7 | usage = 'xp [target_id/mention]' 8 | summary = 'Display your XP card, or the XP card of another user.'; 9 | cooldown = 3; 10 | module = 'XP'; 11 | 12 | execute = (ctx: CommandContext, userMention: string) => { 13 | const target = (userMention) ? 14 | getMemberFromMention(userMention, ctx.guild) : ctx.member; 15 | 16 | if (target.user.bot) 17 | throw new TypeError(`Bot users cannot earn XP`); 18 | 19 | const xpCardURL = `${process.env.API_URL}/guilds/${ctx.guild.id}/members/${target.id}/xp-card`; 20 | return ctx.channel.send({ 21 | files: [{ attachment: xpCardURL, name: 'xp-card.png' }] 22 | }); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/data/commands.ts: -------------------------------------------------------------------------------- 1 | import DBWrapper from './db-wrapper'; 2 | import { Command } from '../commands/command'; 3 | import { SavedCommand, CommandDocument } from '../data/models/command'; 4 | 5 | export default class Commands extends DBWrapper { 6 | protected async getOrCreate(command: Command) { 7 | return await SavedCommand.findOne({ name: command.name }) 8 | ?? await this.create(command); 9 | } 10 | 11 | protected async create(command: Command) { 12 | return SavedCommand.create({ 13 | aliases: command.aliases, 14 | summary: command.summary, 15 | module: command.module, 16 | name: command.name, 17 | precondition: command.precondition, 18 | usage: command.usage// ?? this.getCommandUsage(command) 19 | }); 20 | } 21 | 22 | async deleteAll() { 23 | return await SavedCommand.deleteMany({}); 24 | } 25 | 26 | getCommandUsage(command: Command) { 27 | const args = command.execute 28 | .toString() 29 | .split('{')[0] 30 | .replace(/function \(|\)/g, '') 31 | .replace(/,/g, '') 32 | .replace(/ctx/, '') 33 | .trim(); 34 | return (args) ? `${command.name} ${args}` : command.name; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/data/db-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export default abstract class DBWrapper { 4 | get(type: T1) { 5 | return this.getOrCreate(type); 6 | } 7 | 8 | protected abstract getOrCreate(type: T1): Promise; 9 | protected abstract create(type: T1): Promise; 10 | 11 | save(savedType: T2) { 12 | return savedType.save(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/data/guilds.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from 'discord.js'; 2 | import { GuildDocument, SavedGuild } from '../data/models/guild'; 3 | import DBWrapper from './db-wrapper'; 4 | 5 | export default class Guilds extends DBWrapper { 6 | protected async getOrCreate(guild: Guild) { 7 | if (!guild) return null; 8 | 9 | return await SavedGuild.findById(guild.id) 10 | ?? this.create(guild); 11 | } 12 | 13 | protected create(guild: Guild) { 14 | return new SavedGuild({ _id: guild.id }).save(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/data/logs.ts: -------------------------------------------------------------------------------- 1 | import { Guild, Message } from 'discord.js'; 2 | import DBWrapper from './db-wrapper'; 3 | import { LogDocument, SavedLog, MessageValidationMetadata, Change } from '../data/models/log'; 4 | import { Command } from '../commands/command'; 5 | 6 | export default class Logs extends DBWrapper { 7 | protected async getOrCreate(guild: Guild) { 8 | const log = await SavedLog.findById(guild.id) ?? await this.create(guild); 9 | 10 | log.changes = log.changes.slice(log.changes.length - 100); 11 | 12 | return log; 13 | } 14 | 15 | protected async create(guild: Guild) { 16 | return new SavedLog({ _id: guild.id }).save(); 17 | } 18 | 19 | async logChanges(change: Change, guild: Guild) { 20 | const log = await this.get(guild) as any; 21 | log.changes.push(change); 22 | return log.save(); 23 | } 24 | 25 | async logCommand(msg: Message, command: Command) { 26 | const log = await this.get(msg.guild) as any; 27 | if (log.__v > 1000) return; 28 | 29 | log.commands.push({ 30 | at: new Date(), 31 | by: msg.author.id, 32 | name: command.name 33 | }); 34 | return log.save(); 35 | } 36 | 37 | async logMessage(msg: Message, validation: MessageValidationMetadata) { 38 | const log = await this.get(msg.guild) as any; 39 | 40 | log.messages.push({ at: new Date(), validation }); 41 | return log.save(); 42 | } 43 | 44 | async getAll() { 45 | return await SavedLog.find(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/data/members.ts: -------------------------------------------------------------------------------- 1 | import { GuildMember } from 'discord.js'; 2 | import { MemberDocument, SavedMember } from '../data/models/member'; 3 | import DBWrapper from './db-wrapper'; 4 | 5 | export default class Members extends DBWrapper { 6 | protected async getOrCreate(member: GuildMember) { 7 | if (member.user.bot) 8 | throw new TypeError(`Bots don't have accounts`); 9 | 10 | return await SavedMember.findOne({ 11 | userId: member.id, 12 | guildId: member.guild.id 13 | }) ?? this.create(member); 14 | } 15 | 16 | protected create(member: GuildMember) { 17 | return new SavedMember({ 18 | userId: member.id, 19 | guildId: member.guild.id 20 | }).save(); 21 | } 22 | 23 | getAll() { 24 | return SavedMember.find(); 25 | } 26 | } -------------------------------------------------------------------------------- /src/data/models/command.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | import { PermissionString } from 'discord.js'; 3 | 4 | export interface CommandDocument extends Document { 5 | aliases: string[]; 6 | name: string; 7 | summary: string; 8 | module: string; 9 | usage: string; 10 | precondition?: string; 11 | } 12 | 13 | export const SavedCommand = model('command', new Schema({ 14 | aliases: { type: Object, default: [] }, 15 | name: String, 16 | summary: String, 17 | module: String, 18 | usage: String, 19 | precondition: String 20 | })); 21 | -------------------------------------------------------------------------------- /src/data/models/guild.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | 3 | export class Module { 4 | enabled = true; 5 | } 6 | 7 | export class LogsModule extends Module { 8 | events: LogEvent[] = [ 9 | { 10 | enabled: true, 11 | event: EventType.LevelUp, 12 | channel: '', 13 | message: `**Level UP** :sparkles: 14 | [USER] - **[XP]XP** 15 | LVL \`[OLD_LEVEL]\` -> \`[NEW_LEVEL]\`` 16 | } 17 | ]; 18 | } 19 | 20 | export enum EventType { 21 | Ban = 'BAN', 22 | ConfigUpdate = 'CONFIG_UPDATE', 23 | LevelUp = 'LEVEL_UP', 24 | MessageDeleted = 'MESSAGE_DELETED', 25 | MemberJoin = 'MEMBER_JOIN', 26 | MemberLeave = 'MEMBER_LEAVE', 27 | Mute = 'MUTE', 28 | Unban = 'UNBAN', 29 | Unmute = 'UNMUTE', 30 | Warn ='WARN', 31 | } 32 | 33 | export interface LogEvent { 34 | enabled: boolean; 35 | event: EventType; 36 | channel: string; 37 | message: string; 38 | } 39 | 40 | export class AutoModModule extends Module { 41 | ignoredRoles: string[] = []; 42 | autoDeleteMessages = true; 43 | filters: MessageFilter[] = []; 44 | banWords: string[] = []; 45 | banLinks: string[] = []; 46 | filterThreshold = 5; 47 | autoWarnUsers = true; 48 | } 49 | 50 | export class CommandsModule extends Module { 51 | configs: CommandConfig[] = []; 52 | custom: CustomCommand[] = []; 53 | } 54 | export interface CommandConfig { 55 | name: string; 56 | roles: string[]; 57 | channels: string[]; 58 | enabled: boolean; 59 | } 60 | export interface CustomCommand { 61 | alias: string; 62 | anywhere: boolean; 63 | command: string; 64 | } 65 | 66 | export enum MessageFilter { 67 | Emoji = 'EMOJI', 68 | ExplicitWords = 'EXPLICIT_WORDS', 69 | Links = 'LINKS', 70 | MassCaps = 'MASS_CAPS', 71 | MassMention = 'MASS_MENTION', 72 | Toxicity = 'TOXICITY', 73 | Words = 'WORDS', 74 | Zalgo = 'ZALGO' 75 | } 76 | 77 | export class GeneralModule extends Module { 78 | prefix = '.'; 79 | ignoredChannels: string[] = []; 80 | autoRoles: string[] = []; 81 | } 82 | 83 | export class TimersModule extends Module { 84 | commandTimers: CommandTimer[] = []; 85 | messageTimers: MessageTimer[] = []; 86 | } 87 | 88 | export interface Timer { 89 | enabled: boolean; 90 | interval: string; 91 | from: Date; 92 | } 93 | export interface CommandTimer extends Timer { 94 | channel: string; 95 | command: string; 96 | } 97 | export interface MessageTimer extends Timer { 98 | channel: string; 99 | message: string; 100 | } 101 | 102 | export class LevelingModule extends Module { 103 | levelRoles: LevelRole[] = []; 104 | ignoredRoles: string[] = []; 105 | xpPerMessage = 50; 106 | maxMessagesPerMinute = 3; 107 | } 108 | export interface LevelRole { 109 | level: number; 110 | role: string; 111 | } 112 | 113 | export class MusicModule extends Module { 114 | maxTrackLength = 24; 115 | } 116 | 117 | export class ReactionRolesModule extends Module { 118 | configs: ReactionRole[] = []; 119 | } 120 | export interface ReactionRole { 121 | channel: string, 122 | messageId: string, 123 | emote: string, 124 | role: string 125 | } 126 | 127 | export class DashboardSettings { 128 | privateLeaderboard = false; 129 | } 130 | 131 | export interface GuildDocument extends Document { 132 | _id: string; 133 | autoMod: AutoModModule; 134 | commands: CommandsModule; 135 | general: GeneralModule; 136 | leveling: LevelingModule; 137 | logs: LogsModule; 138 | music: MusicModule; 139 | reactionRoles: ReactionRolesModule; 140 | timers: TimersModule; 141 | settings: DashboardSettings; 142 | } 143 | 144 | export const SavedGuild = model('guild', new Schema({ 145 | _id: String, 146 | autoMod: { type: Object, default: new AutoModModule() }, 147 | commands: { type: Object, default: new CommandsModule() }, 148 | general: { type: Object, default: new GeneralModule() }, 149 | leveling: { type: Object, default: new LevelingModule() }, 150 | logs: { type: Object, default: new LogsModule() }, 151 | timers: { type: Object, default: new TimersModule() }, 152 | music: { type: Object, default: new MusicModule() }, 153 | reactionRoles: { type: Object, default: new ReactionRolesModule() }, 154 | settings: { type: Object, default: new DashboardSettings() } 155 | })); 156 | -------------------------------------------------------------------------------- /src/data/models/log.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | import { MessageFilter } from './guild'; 3 | 4 | export class Change { 5 | public at = new Date(); 6 | 7 | constructor( 8 | public by: string, 9 | public changes: { old: {}, new: {}}, 10 | public module: string) {} 11 | } 12 | 13 | export interface CommandLog { 14 | name: string, 15 | by: string, 16 | at: Date 17 | } 18 | 19 | export interface MessageLog { 20 | at: Date; 21 | validation: MessageValidationMetadata; 22 | } 23 | 24 | export interface MessageValidationMetadata { 25 | earnedXP: boolean; 26 | filter?: MessageFilter | null; 27 | } 28 | 29 | const LogSchema = new Schema({ 30 | _id: String, 31 | changes: { type: Array, default: [] }, 32 | commands: { type: Array, default: [] }, 33 | messages: { type: Array, default: [] } 34 | }); 35 | 36 | export interface LogDocument extends Document { 37 | _id: string; 38 | changes: Change[]; 39 | commands: CommandLog[]; 40 | messages: MessageLog[]; 41 | } 42 | 43 | export const SavedLog = model('log', LogSchema); -------------------------------------------------------------------------------- /src/data/models/member.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | 3 | const memberSchema = new Schema({ 4 | userId: String, 5 | guildId: String, 6 | xp: { type: Number, default: 0 }, 7 | recentMessages: { type: Array, default: [] }, 8 | warnings: { type: Array, default: [] }, 9 | mutes: { type: Array, default: [] } 10 | }); 11 | 12 | export interface MemberDocument extends Document { 13 | userId: string; 14 | guildId: string; 15 | xp: number; 16 | recentMessages: Date[]; 17 | warnings: Punishment[]; 18 | mutes: Punishment[]; 19 | } 20 | 21 | export interface Punishment { 22 | reason: string; 23 | instigatorId: string; 24 | at: Date; 25 | } 26 | 27 | export const SavedMember = model('member', memberSchema); -------------------------------------------------------------------------------- /src/data/models/user.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, Document } from 'mongoose'; 2 | 3 | export class XPCard { 4 | backgroundURL = ''; 5 | primary = ''; 6 | secondary = ''; 7 | tertiary = ''; 8 | } 9 | 10 | export interface UserDocument extends Document { 11 | _id: string; 12 | guildPositions: string[]; 13 | premium: boolean; 14 | premiumExpiration: Date, 15 | xpCard: XPCard; 16 | votes: number; 17 | referralIds: string[]; 18 | } 19 | 20 | export const SavedUser = model('user', new Schema({ 21 | _id: String, 22 | guildPositions: { type: Array, default: [] }, 23 | premium: { type: Boolean, default: false }, 24 | premiumExpiration: { type: Date, default: null }, 25 | votes: { type: Number, default: 0 }, 26 | xpCard: { type: Object, default: new XPCard() }, 27 | referralIds: { type: Array, default: [] } 28 | })); -------------------------------------------------------------------------------- /src/data/snowflake-entity.ts: -------------------------------------------------------------------------------- 1 | export default interface SnowflakeEntity { 2 | id: string; 3 | } -------------------------------------------------------------------------------- /src/data/users.ts: -------------------------------------------------------------------------------- 1 | import { SavedUser, UserDocument } from '../data/models/user'; 2 | import DBWrapper from './db-wrapper'; 3 | import SnowflakeEntity from './snowflake-entity'; 4 | import Deps from '../utils/deps'; 5 | import { Client } from 'discord.js'; 6 | 7 | export default class Users extends DBWrapper { 8 | protected async getOrCreate({ id }: SnowflakeEntity) { 9 | const savedUser = await SavedUser.findById(id); 10 | if (savedUser 11 | && savedUser.premiumExpiration 12 | && savedUser.premiumExpiration <= new Date()) 13 | await this.removePremium(savedUser); 14 | 15 | return savedUser ?? this.create({ id }); 16 | } 17 | 18 | async givePro(id: string, plan: Plan) { 19 | const savedUser = await this.get({ id }); 20 | savedUser.premium = true; 21 | savedUser.premiumExpiration = this.getExpiration(plan); 22 | return savedUser.save(); 23 | } 24 | private getExpiration(plan: Plan) { 25 | let date = new Date(); 26 | switch (plan) { 27 | case Plan.One: 28 | date.setDate(date.getDate() + 30) 29 | break; 30 | case Plan.Three: 31 | date.setDate(date.getDate() + 90) 32 | break; 33 | default: 34 | date = null; 35 | break; 36 | } 37 | return date; 38 | } 39 | private removePremium(savedUser: UserDocument) { 40 | savedUser.premium = false; 41 | return savedUser.save(); 42 | } 43 | 44 | protected async create({ id }: SnowflakeEntity) { 45 | return new SavedUser({ _id: id }).save(); 46 | } 47 | 48 | getAll() { 49 | return SavedUser.find(); 50 | } 51 | } 52 | 53 | export enum Plan { One, Three, Forever } -------------------------------------------------------------------------------- /src/keep-alive.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import Log from './utils/log'; 3 | 4 | Log.info('Keeping self alive every 5 minutes', 'ping'); 5 | 6 | setInterval(async() => { 7 | await fetch(process.env.DASHBOARD_URL); 8 | Log.info('Kept app alive.'); 9 | }, 5 * 60 * 1000); 10 | -------------------------------------------------------------------------------- /src/modules/announce/event-variables.ts: -------------------------------------------------------------------------------- 1 | import { User, Guild, Message } from 'discord.js'; 2 | 3 | export default class EventVariables { 4 | constructor(private content: string) {} 5 | 6 | toString() { return this.content; } 7 | 8 | private replace(regex: RegExp, replacement: string) { 9 | this.content = this.content.replace(regex, replacement); 10 | return this; 11 | } 12 | 13 | guild(guild: Guild) { 14 | return this.replace(/\[GUILD\]/g, guild.name); 15 | } 16 | 17 | instigator(user: User) { 18 | return this.replace(/\[INSTIGATOR\]/g, `<@!${user.id}>`); 19 | } 20 | 21 | module(name: string) { 22 | return this.replace(/\[MODULE\]/g, name); 23 | } 24 | 25 | memberCount(guild: Guild) { 26 | return this.replace(/\[MEMBER_COUNT\]/g, guild.memberCount.toString()); 27 | } 28 | 29 | message(msg: Message) { 30 | return this.replace(/\[MESSAGE\]/g, msg.content); 31 | } 32 | 33 | oldValue(value: any) { 34 | return this.replace(/\[OLD_VALUE\]/g, JSON.stringify(value, null, 2)); 35 | } 36 | 37 | oldLevel(level: number) { 38 | return this.replace(/\[OLD_LEVEL\]/g, level.toString()); 39 | } 40 | 41 | newValue(value: any) { 42 | return this.replace(/\[NEW_VALUE\]/g, JSON.stringify(value, null, 2)); 43 | } 44 | 45 | newLevel(level: number) { 46 | return this.replace(/\[NEW_LEVEL\]/g, level.toString()); 47 | } 48 | 49 | reason(reason: string) { 50 | return this.replace(/\[REASON\]/g, reason); 51 | } 52 | 53 | user(user: User) { 54 | return this.replace(/\[USER\]/g, `<@!${user.id}>`); 55 | } 56 | 57 | warnings(warnings: number) { 58 | return this.replace(/\[WARNINGS\]/g, warnings.toString()); 59 | } 60 | 61 | xp(xp: number) { 62 | return this.replace(/\[XP\]/g, xp.toString()); 63 | } 64 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/bad-link.validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | import { ContentValidator } from './content-validator'; 3 | import { ValidationError } from '../auto-mod'; 4 | 5 | export default class BadLinkValidator implements ContentValidator { 6 | filter = MessageFilter.Links; 7 | 8 | validate(content: string, guild: GuildDocument) { 9 | const isExplicit = guild.autoMod.banLinks 10 | .some(l => content.includes(l)); 11 | if (isExplicit) { 12 | throw new ValidationError('Message contains banned links.', this.filter); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/bad-word.validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | import { ContentValidator } from './content-validator'; 3 | import { ValidationError } from '../auto-mod'; 4 | 5 | /** @deprecated */ 6 | export default class BadWordValidator implements ContentValidator { 7 | filter = MessageFilter.Words; 8 | 9 | validate(content: string, guild: GuildDocument) { 10 | const msgWords = content.split(' '); 11 | for (const word of msgWords) { 12 | const isExplicit = guild.autoMod.banWords 13 | .some(w => w.toLowerCase() === word.toLowerCase()); 14 | if (isExplicit) { 15 | throw new ValidationError('Message contains banned words.', this.filter); 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/content-validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | 3 | export interface ContentValidator { 4 | filter: MessageFilter; 5 | 6 | validate(content: string, savedGuild: GuildDocument): void | Promise; 7 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/emoji.validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | import { ContentValidator } from './content-validator'; 3 | import { ValidationError } from '../auto-mod'; 4 | 5 | export default class EmojiValidator implements ContentValidator { 6 | filter = MessageFilter.Emoji; 7 | 8 | validate(content: string, savedGuild: GuildDocument) { 9 | const pattern = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/gm; 10 | const severity = savedGuild.autoMod.filterThreshold; 11 | 12 | const invalid = content.match(pattern)?.length >= severity; 13 | if (invalid) 14 | throw new ValidationError('Message contains too many emojis.', this.filter); 15 | } 16 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/explicit-word.validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | import { ContentValidator } from './content-validator'; 3 | import { ValidationError, explicitWords } from '../auto-mod'; 4 | 5 | export default class ExplicitWordValidator implements ContentValidator { 6 | filter = MessageFilter.ExplicitWords; 7 | 8 | async validate(content: string, guild: GuildDocument) { 9 | const msgWords = content.split(' '); 10 | for (const word of msgWords) { 11 | const isExplicit = explicitWords 12 | .some(w => w.toLowerCase() === word.toLowerCase()); 13 | if (isExplicit) 14 | throw new ValidationError('Message contains banned words.', this.filter); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/mass-caps.validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | import { ContentValidator } from './content-validator'; 3 | import { ValidationError } from '../auto-mod'; 4 | 5 | export default class MassCapsValidator implements ContentValidator { 6 | filter = MessageFilter.MassCaps; 7 | 8 | validate(content: string, guild: GuildDocument) { 9 | const pattern = /[A-Z]/g; 10 | const severity = guild.autoMod.filterThreshold; 11 | 12 | const invalid = content.length > 5 13 | && (content.match(pattern)?.length / content.length) >= (severity / 10); 14 | if (invalid) 15 | throw new ValidationError('Message contains too many capital letters.', this.filter); 16 | } 17 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/mass-mention.validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | import { ContentValidator } from './content-validator'; 3 | import { ValidationError } from '../auto-mod'; 4 | 5 | export default class MassMentionValidator implements ContentValidator { 6 | filter = MessageFilter.MassMention; 7 | 8 | validate(content: string, guild: GuildDocument) { 9 | const pattern = /<@![0-9]{18}>/gm; 10 | const severity = guild.autoMod.filterThreshold; 11 | 12 | const invalid = content.match(pattern)?.length >= severity; 13 | if (invalid) 14 | throw new ValidationError('Message contains too many mentions.', this.filter); 15 | } 16 | } -------------------------------------------------------------------------------- /src/modules/auto-mod/validators/zalgo.validator.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument, MessageFilter } from '../../../data/models/guild'; 2 | import { ContentValidator } from './content-validator'; 3 | import { ValidationError } from '../auto-mod'; 4 | 5 | export default class ZalgoValidator implements ContentValidator { 6 | filter = MessageFilter.Zalgo; 7 | 8 | validate(content: string, guild: GuildDocument) { 9 | const pattern = /%CC%/g; 10 | 11 | const invalid = pattern.test(encodeURIComponent(content)) 12 | if (invalid) 13 | throw new ValidationError('Message contains zalgo.', this.filter); 14 | } 15 | } -------------------------------------------------------------------------------- /src/modules/general/reaction-roles.ts: -------------------------------------------------------------------------------- 1 | import { GuildDocument } from '../../data/models/guild'; 2 | import { MessageReaction, User } from 'discord.js'; 3 | 4 | export default class ReactionRoles { 5 | async checkToAdd(user: User, reaction: MessageReaction, savedGuild: GuildDocument) { 6 | const config = this.getReactionRole(reaction, savedGuild); 7 | if (!config) return; 8 | 9 | const { guild } = reaction.message; 10 | const member = guild.members.cache.get(user.id); 11 | const role = guild.roles.cache.get(config.role); 12 | if (role) 13 | await member.roles.add(role); 14 | } 15 | 16 | async checkToRemove(user: User, reaction: MessageReaction, savedGuild: GuildDocument) { 17 | const config = this.getReactionRole(reaction, savedGuild); 18 | if (!config) return; 19 | 20 | const { guild } = reaction.message; 21 | const member = guild.members.cache.get(user.id); 22 | const role = guild.roles.cache.get(config.role); 23 | if (role) 24 | await member.roles.remove(role); 25 | } 26 | 27 | private getReactionRole(reaction: MessageReaction, savedGuild: GuildDocument) { 28 | const msg = reaction.message; 29 | const toHex = (a: string) => a.codePointAt(0).toString(16); 30 | 31 | return savedGuild.reactionRoles.enabled 32 | ? savedGuild.reactionRoles.configs 33 | .find(r => r.channel === msg.channel.id 34 | && r.messageId === msg.id 35 | && toHex(r.emote) === toHex(reaction.emoji.name)) 36 | : null; 37 | } 38 | } -------------------------------------------------------------------------------- /src/modules/music/music.ts: -------------------------------------------------------------------------------- 1 | import { TextChannel, VoiceChannel } from 'discord.js'; 2 | import { MusicClient, Player, Track } from '@2pg/music'; 3 | 4 | export default class Music { 5 | private _client = {} as MusicClient; 6 | get client() { return this._client; } 7 | 8 | initialize() { 9 | this._client = new MusicClient(); 10 | 11 | this.hookEvents(); 12 | } 13 | 14 | private hookEvents() { 15 | this.client.on('trackStart', (player, track) => player.textChannel?.send(`**Now Playing**: \`${track.title}\` 🎵`)); 16 | this.client.on('queueEnd', (player) => player.textChannel?.send(`**Queue has Ended** 🎵`)); 17 | } 18 | 19 | joinAndGetPlayer(voiceChannel?: VoiceChannel, textChannel?: TextChannel) { 20 | if (!voiceChannel) 21 | throw new TypeError('You must be in a voice channel to play music.'); 22 | 23 | return this.client.get(voiceChannel.guild.id) 24 | ?? this.client.create(voiceChannel.guild.id, { textChannel, voiceChannel }); 25 | } 26 | 27 | getDuration(player: Player, track?: Track) { 28 | if (!player.isPlaying) 29 | throw new TypeError('No track is currently playing.'); 30 | 31 | const positionInSeconds = (track === player.q.peek()) 32 | ? player.position / 1000 33 | : 0; 34 | track = (track ?? player.q.peek()) as Track; 35 | 36 | return `${Math.floor(positionInSeconds / 60)}:${Math.floor(positionInSeconds % 60).toString().padStart(2, '0')} / ` + 37 | `${Math.floor(track.duration.seconds / 60)}:${Math.floor(track.duration.seconds % 60).toString().padStart(2, '0')}`; 38 | } 39 | 40 | async findTrack(query: string, maxTrackLength: number) { 41 | const track: Track = await this.searchForTrack(query); 42 | 43 | const maxHoursInSeconds = maxTrackLength * 60 * 60; 44 | if (track.duration.seconds > maxHoursInSeconds) 45 | throw new TypeError(`Track length must be less than or equal to \`${maxTrackLength} hours\``); 46 | return track; 47 | } 48 | 49 | private async searchForTrack(query: string) { 50 | const videos = await this.client.search(query); 51 | return videos[0]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/xp/leveling.ts: -------------------------------------------------------------------------------- 1 | import { Message, GuildMember } from 'discord.js'; 2 | import { GuildDocument } from '../../data/models/guild'; 3 | import Members from '../../data/members'; 4 | import Deps from '../../utils/deps'; 5 | import { MemberDocument } from '../../data/models/member'; 6 | import Emit from '../../services/emit'; 7 | 8 | export default class Leveling { 9 | constructor( 10 | private emit = Deps.get(Emit), 11 | private members = Deps.get(Members)) {} 12 | 13 | async validateXPMsg(msg: Message, savedGuild: GuildDocument) { 14 | if (!msg?.member || !savedGuild 15 | || this.hasIgnoredXPRole(msg.member, savedGuild)) 16 | throw new TypeError('User cannot earn XP'); 17 | 18 | const savedMember = await this.members.get(msg.member); 19 | 20 | this.handleCooldown(savedMember, savedGuild); 21 | 22 | const oldLevel = this.getLevel(savedMember.xp); 23 | savedMember.xp += savedGuild.leveling.xpPerMessage; 24 | const newLevel = this.getLevel(savedMember.xp); 25 | 26 | if (newLevel > oldLevel) { 27 | this.emit.levelUp({ newLevel, oldLevel }, msg, savedMember); 28 | this.checkLevelRoles(msg, newLevel, savedGuild); 29 | } 30 | await savedMember.save(); 31 | } 32 | 33 | private handleCooldown(savedMember: MemberDocument, savedGuild: GuildDocument) { 34 | const inCooldown = savedMember.recentMessages 35 | .filter(m => m.getMinutes() === new Date().getMinutes()) 36 | .length > savedGuild.leveling.maxMessagesPerMinute; 37 | if (inCooldown) 38 | throw new TypeError('User is in cooldown'); 39 | 40 | const lastMessage = savedMember.recentMessages[savedMember.recentMessages.length - 1]; 41 | if (lastMessage && lastMessage.getMinutes() !== new Date().getMinutes()) 42 | savedMember.recentMessages = []; 43 | 44 | savedMember.recentMessages.push(new Date()); 45 | } 46 | 47 | private hasIgnoredXPRole(member: GuildMember, guild: GuildDocument) { 48 | for (const entry of member.roles.cache) { 49 | const role = entry[1]; 50 | if (guild.leveling.ignoredRoles.some(id => id === role.id)) 51 | return true; 52 | } 53 | return false; 54 | } 55 | 56 | private checkLevelRoles(msg: Message, newLevel: number, guild: GuildDocument) { 57 | const levelRole = this.getLevelRole(newLevel, guild); 58 | if (levelRole) 59 | msg.member?.roles.add(levelRole); 60 | } 61 | private getLevelRole(level: number, guild: GuildDocument) { 62 | return guild.leveling.levelRoles.find(r => r.level === level)?.role; 63 | } 64 | 65 | getLevel(xp: number) { 66 | const preciseLevel = (-75 + Math.sqrt(Math.pow(75, 2) - 300 * (-150 - xp))) / 150; 67 | return Math.floor(preciseLevel); 68 | } 69 | static xpInfo(xp: number) { 70 | const preciseLevel = (-75 + Math.sqrt(Math.pow(75, 2) - 300 * (-150 - xp))) / 150; 71 | const level = Math.floor(preciseLevel); 72 | 73 | const xpForNextLevel = this.xpForNextLevel(level, xp); 74 | const nextLevelXP = xp + xpForNextLevel; 75 | 76 | const levelCompletion = preciseLevel - level; 77 | 78 | return { level, xp, xpForNextLevel, levelCompletion, nextLevelXP }; 79 | } 80 | private static xpForNextLevel(currentLevel: number, xp: number) { 81 | return ((75 * Math.pow(currentLevel + 1, 2)) + (75 * (currentLevel + 1)) - 150) - xp; 82 | } 83 | 84 | static getRank(member: MemberDocument, members: MemberDocument[]) { 85 | return members 86 | .sort((a, b) => b.xp - a.xp) 87 | .findIndex(m => m.id === member.id) + 1; 88 | } 89 | } -------------------------------------------------------------------------------- /src/services/command.service.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { Message, TextChannel } from 'discord.js'; 3 | import { Command, CommandContext } from '../commands/command'; 4 | import Log from '../utils/log'; 5 | import Deps from '../utils/deps'; 6 | import Commands from '../data/commands'; 7 | import Logs from '../data/logs'; 8 | import { GuildDocument } from '../data/models/guild'; 9 | import Cooldowns from './cooldowns'; 10 | import Validators from './validators'; 11 | import { promisify } from 'util'; 12 | 13 | const readdir = promisify(fs.readdir); 14 | 15 | export default class CommandService { 16 | private commands = new Map(); 17 | 18 | constructor( 19 | private logs = Deps.get(Logs), 20 | private cooldowns = Deps.get(Cooldowns), 21 | private validators = Deps.get(Validators), 22 | private savedCommands = Deps.get(Commands)) {} 23 | 24 | async init() { 25 | const files = await readdir('./src/commands'); 26 | 27 | for (const fileName of files) { 28 | const cleanName = fileName.replace(/(\..*)/, ''); 29 | 30 | const Command = await require(`../commands/${cleanName}`).default; 31 | if (!Command) continue; 32 | 33 | const command = new Command(); 34 | this.commands.set(command.name, command); 35 | 36 | await this.savedCommands.get(command); 37 | } 38 | Log.info(`Loaded: ${this.commands.size} commands`, `cmds`); 39 | } 40 | 41 | async handle(msg: Message, savedGuild: GuildDocument) { 42 | if (!(msg.member && msg.content && msg.guild && !msg.author.bot)) return; 43 | 44 | return this.handleCommand(msg, savedGuild); 45 | } 46 | async handleCommand(msg: Message, savedGuild: GuildDocument) { 47 | try { 48 | const prefix = savedGuild.general.prefix; 49 | const slicedContent = msg.content.slice(prefix.length); 50 | 51 | const command = this.findCommand(slicedContent, savedGuild); 52 | 53 | const customCommand = this.getCustomCommand(slicedContent, savedGuild); 54 | this.validators.checkChannel(msg.channel as TextChannel, savedGuild, customCommand); 55 | 56 | if (!command || this.cooldowns.active(msg.author, command)) return; 57 | 58 | this.validators.checkCommand(command, savedGuild, msg); 59 | this.validators.checkPreconditions(command, msg.member); 60 | 61 | await command.execute(new CommandContext(msg), 62 | ...this.getCommandArgs(slicedContent, savedGuild)); 63 | 64 | this.cooldowns.add(msg.author, command); 65 | 66 | await this.logs.logCommand(msg, command); 67 | } catch (error) { 68 | const content = error?.message ?? 'Un unknown error occurred'; 69 | msg.channel.send(':warning: ' + content); 70 | } 71 | } 72 | 73 | private findCommand(slicedContent: string, savedGuild: GuildDocument) { 74 | const name = this.getCommandName(slicedContent); 75 | return this.commands.get(name) 76 | ?? this.findByAlias(name) 77 | ?? this.findCustomCommand(name, savedGuild); 78 | } 79 | private findByAlias(name: string) { 80 | return Array.from(this.commands.values()) 81 | .find(c => c.aliases?.some(a => a === name)); 82 | } 83 | private findCustomCommand(customName: string, { commands }: GuildDocument) { 84 | const ccName = this.getCommandName(commands.custom 85 | ?.find(c => c.alias === customName)?.command); 86 | return this.commands.get(ccName); 87 | } 88 | 89 | private getCommandArgs(slicedContent: string, savedGuild: GuildDocument) { 90 | const customCommand = this 91 | .getCustomCommand(slicedContent, savedGuild)?.command; 92 | return (customCommand ?? slicedContent) 93 | .split(' ') 94 | .slice(1) 95 | } 96 | private getCustomCommand(slicedContent: string, savedGuild: GuildDocument) { 97 | const name = this.getCommandName(slicedContent); 98 | return savedGuild.commands.custom 99 | ?.find(c => c.alias === name); 100 | } 101 | 102 | private getCommandName(slicedContent: string) { 103 | return slicedContent 104 | ?.toLowerCase() 105 | .split(' ')[0]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/services/cooldowns.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'discord.js'; 2 | import { Command } from '../commands/command'; 3 | 4 | export default class Cooldowns { 5 | private cooldowns: CommandCooldown[] = []; 6 | 7 | active(author: User, command: Command) { 8 | return this.cooldowns 9 | .some(c => c.userId === author.id && c.commandName === command.name); 10 | } 11 | add(user: User, command: Command) { 12 | const cooldown = { userId: user.id, commandName: command.name }; 13 | 14 | if (!this.active(user, command)) 15 | this.cooldowns.push(cooldown); 16 | 17 | const seconds = (command.cooldown ?? 0) * 1000; 18 | setTimeout(() => this.remove(user, command), seconds); 19 | } 20 | remove(user: User, command: Command) { 21 | const index = this.cooldowns 22 | .findIndex(c => c.userId === user.id && c.commandName === command.name); 23 | this.cooldowns.splice(index, 1); 24 | } 25 | } 26 | 27 | export interface CommandCooldown { 28 | userId: string; 29 | commandName: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/services/custom-handlers/config-update.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from '../handlers/logs-handler'; 2 | import { EventType } from '../../data/models/guild'; 3 | import EventVariables from '../../modules/announce/event-variables'; 4 | import { ConfigUpdateArgs } from '../emit'; 5 | 6 | export default class ConfigUpdateHandler extends LogsHandler { 7 | on = 'configUpdate'; 8 | event = EventType.ConfigUpdate; 9 | 10 | async invoke(args: ConfigUpdateArgs) { 11 | await super.announce(args.guild, [ args ]); 12 | } 13 | 14 | protected async applyEventVariables(content: string, args: ConfigUpdateArgs) { 15 | return new EventVariables(content) 16 | .guild(args.guild) 17 | .instigator(args.instigator) 18 | .memberCount(args.guild) 19 | .module(args.module) 20 | .newValue(args.new) 21 | .oldValue(args.old) 22 | .toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/custom-handlers/level-up.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from '../handlers/logs-handler'; 2 | import { EventType } from '../../data/models/guild'; 3 | import EventVariables from '../../modules/announce/event-variables'; 4 | import { LevelUpEventArgs } from '../emit'; 5 | 6 | export default class LevelUpHandler extends LogsHandler { 7 | on = 'levelUp'; 8 | event = EventType.LevelUp; 9 | 10 | async invoke(args: LevelUpEventArgs) { 11 | await super.announce(args.guild, [ args ]); 12 | } 13 | 14 | protected async applyEventVariables(content: string, args: LevelUpEventArgs) { 15 | return new EventVariables(content) 16 | .guild(args.guild) 17 | .memberCount(args.guild) 18 | .user(args.user) 19 | .oldLevel(args.oldLevel) 20 | .newLevel(args.newLevel) 21 | .xp(args.xp) 22 | .toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/custom-handlers/user-mute.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from '../handlers/logs-handler'; 2 | import { EventType } from '../../data/models/guild'; 3 | import EventVariables from '../../modules/announce/event-variables'; 4 | import { PunishmentEventArgs } from '../emit'; 5 | 6 | export default class UserMuteHandler extends LogsHandler { 7 | on = 'userMute'; 8 | event = EventType.Mute; 9 | 10 | async invoke(args: PunishmentEventArgs) { 11 | await super.announce(args.guild, [ args ]); 12 | } 13 | 14 | protected async applyEventVariables(content: string, args: PunishmentEventArgs) { 15 | return new EventVariables(content) 16 | .guild(args.guild) 17 | .instigator(args.instigator) 18 | .memberCount(args.guild) 19 | .reason(args.reason) 20 | .user(args.user) 21 | .warnings(args.warnings) 22 | .toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/custom-handlers/user-unmute.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from '../handlers/logs-handler'; 2 | import { EventType } from '../../data/models/guild'; 3 | import EventVariables from '../../modules/announce/event-variables'; 4 | import { PunishmentEventArgs } from '../emit'; 5 | 6 | export default class UserUnmuteHandler extends LogsHandler { 7 | on = 'userUnmute'; 8 | event = EventType.Unmute; 9 | 10 | async invoke(args: PunishmentEventArgs) { 11 | await super.announce(args.guild, [ args ]); 12 | } 13 | 14 | protected async applyEventVariables(content: string, args: PunishmentEventArgs) { 15 | return new EventVariables(content) 16 | .guild(args.guild) 17 | .instigator(args.instigator) 18 | .memberCount(args.guild) 19 | .reason(args.reason) 20 | .user(args.user) 21 | .warnings(args.warnings) 22 | .toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/custom-handlers/user-warn.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from '../handlers/logs-handler'; 2 | import { EventType } from '../../data/models/guild'; 3 | import EventVariables from '../../modules/announce/event-variables'; 4 | import { PunishmentEventArgs } from '../emit'; 5 | 6 | export default class UserWarnHandler extends LogsHandler { 7 | on = 'userWarn'; 8 | event = EventType.Warn; 9 | 10 | async invoke(args: PunishmentEventArgs) { 11 | await super.announce(args.guild, [ args ]); 12 | } 13 | 14 | protected async applyEventVariables(content: string, args: PunishmentEventArgs) { 15 | return new EventVariables(content) 16 | .guild(args.guild) 17 | .instigator(args.instigator) 18 | .memberCount(args.guild) 19 | .reason(args.reason) 20 | .user(args.user) 21 | .warnings(args.warnings) 22 | .toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/emit.ts: -------------------------------------------------------------------------------- 1 | import { Guild, User, GuildMember, Message } from 'discord.js'; 2 | import { PunishmentArgs } from '../modules/auto-mod/auto-mod'; 3 | import { MemberDocument } from '../data/models/member'; 4 | import { Change } from '../data/models/log'; 5 | import Deps from '../utils/deps'; 6 | import { EventEmitter } from 'events'; 7 | 8 | /** 9 | * Used for emitting custom events. 10 | */ 11 | export default class Emit { 12 | constructor(private emitter = Deps.get(EventEmitter)) {} 13 | 14 | configSaved(guild: Guild, user: User | any, change: Change) { 15 | const eventArgs: ConfigUpdateArgs = { 16 | guild, 17 | instigator: user, 18 | module: change.module, 19 | new: change.changes.new, 20 | old: change.changes.old 21 | }; 22 | this.emitter.emit('configUpdate', eventArgs); 23 | } 24 | 25 | levelUp(args: { newLevel: number, oldLevel: number }, msg: Message, savedMember: MemberDocument) { 26 | const eventArgs: LevelUpEventArgs = { 27 | ...args, 28 | guild: msg.guild, 29 | xp: savedMember.xp, 30 | user: msg.member.user 31 | }; 32 | this.emitter.emit('levelUp', eventArgs); 33 | } 34 | 35 | mute(args: PunishmentArgs, target: GuildMember, savedMember: MemberDocument) { 36 | const eventArgs: PunishmentEventArgs = { 37 | ...args, 38 | guild: target.guild, 39 | user: target.user, 40 | warnings: savedMember.warnings.length 41 | }; 42 | this.emitter.emit('userMute', eventArgs); 43 | } 44 | 45 | unmute(args: PunishmentArgs, target: GuildMember, savedMember: MemberDocument) { 46 | const eventArgs: PunishmentEventArgs = { 47 | guild: target.guild, 48 | instigator: args.instigator, 49 | user: target.user, 50 | reason: args.reason, 51 | warnings: savedMember.warnings.length 52 | }; 53 | this.emitter.emit('userUnmute', eventArgs); 54 | } 55 | 56 | warning(args: PunishmentArgs, target: GuildMember, savedMember: MemberDocument) { 57 | const eventArgs: PunishmentEventArgs = { 58 | ...args, 59 | guild: target.guild, 60 | reason: args.reason, 61 | user: target.user, 62 | warnings: savedMember.warnings.length 63 | } 64 | this.emitter.emit('userWarn', eventArgs); 65 | } 66 | } 67 | 68 | export interface ConfigUpdateArgs { 69 | guild: Guild; 70 | instigator: User; 71 | module: string; 72 | new: any; 73 | old: any; 74 | } 75 | 76 | export interface LevelUpEventArgs { 77 | guild: Guild; 78 | newLevel: number; 79 | oldLevel: number; 80 | xp: number; 81 | user: User; 82 | } 83 | 84 | export interface PunishmentEventArgs { 85 | until?: Date; 86 | guild: Guild; 87 | user: User; 88 | instigator: User; 89 | warnings: number; 90 | reason: string; 91 | } -------------------------------------------------------------------------------- /src/services/events.service.ts: -------------------------------------------------------------------------------- 1 | import Log from '../utils/log'; 2 | import fs from 'fs'; 3 | import { promisify } from 'util'; 4 | import EventHandler from './handlers/event-handler'; 5 | import { EventEmitter } from 'events'; 6 | import { Client } from 'discord.js'; 7 | import Deps from '../utils/deps'; 8 | 9 | const readdir = promisify(fs.readdir); 10 | 11 | export default class EventsService { 12 | private readonly handlers: EventHandler[] = []; 13 | private readonly customHandlers: EventHandler[] = []; 14 | 15 | constructor( 16 | private bot = Deps.get(Client), 17 | private emitter = Deps.get(EventEmitter)) {} 18 | 19 | async init() { 20 | const handlerFiles = await readdir(`${__dirname}/handlers`); 21 | const customHandlerFiles = await readdir(`${__dirname}/custom-handlers`); 22 | 23 | for (const file of handlerFiles) { 24 | const Handler = await require(`./handlers/${file}`).default; 25 | const handler = Handler && new Handler(); 26 | if (!handler?.on) continue; 27 | 28 | this.handlers.push(new Handler()); 29 | } 30 | for (const file of customHandlerFiles) { 31 | const Handler = await require(`./custom-handlers/${file}`).default; 32 | const handler = Handler && new Handler(); 33 | if (!handler?.on) continue; 34 | 35 | this.customHandlers.push(new Handler()); 36 | } 37 | this.hookEvents(); 38 | } 39 | 40 | private hookEvents() { 41 | for (const handler of this.handlers) 42 | this.bot.on(handler.on as any, handler.invoke.bind(handler)); 43 | 44 | for (const handler of this.customHandlers) 45 | this.emitter.on(handler.on, handler.invoke.bind(handler)); 46 | 47 | Log.info(`Loaded: ${this.handlers.length} handlers`, 'events'); 48 | Log.info(`Loaded: ${this.customHandlers.length} custom handlers`, 'events'); 49 | } 50 | } -------------------------------------------------------------------------------- /src/services/handlers/event-handler.ts: -------------------------------------------------------------------------------- 1 | import { ClientEvents } from 'discord.js'; 2 | 3 | export default interface EventHandler { 4 | on: keyof ClientEvents | any; 5 | 6 | invoke(...args: any[]): Promise | void; 7 | } -------------------------------------------------------------------------------- /src/services/handlers/guild-ban-add.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from './logs-handler'; 2 | import { Guild, User, ClientEvents } from 'discord.js'; 3 | import { EventType } from '../../data/models/guild'; 4 | import EventVariables from '../../modules/announce/event-variables'; 5 | 6 | export default class GuildBanAddHandler extends LogsHandler { 7 | on: keyof ClientEvents = 'guildBanAdd'; 8 | event = EventType.Ban; 9 | 10 | async invoke(guild: Guild, user: User) { 11 | await super.announce(guild, [ guild, user ]); 12 | } 13 | 14 | protected async applyEventVariables(content: string, guild: Guild, user: User) { 15 | const ban = await guild.fetchBan(user); 16 | 17 | return new EventVariables(content) 18 | .guild(guild) 19 | .memberCount(guild) 20 | .reason(ban.reason) 21 | .user(user) 22 | .toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/handlers/guild-ban-remove.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from './logs-handler'; 2 | import { Guild, User, ClientEvents } from 'discord.js'; 3 | import { EventType } from '../../data/models/guild'; 4 | import EventVariables from '../../modules/announce/event-variables'; 5 | 6 | export default class GuildBanAddHandler extends LogsHandler { 7 | on: keyof ClientEvents = 'guildBanRemove'; 8 | event = EventType.Unban; 9 | 10 | async invoke(guild: Guild, user: User) { 11 | await super.announce(guild, [ guild, user ]); 12 | } 13 | 14 | protected async applyEventVariables(content: string, guild: Guild, user: User) { 15 | const ban = await guild.fetchBan(user); 16 | 17 | return new EventVariables(content) 18 | .guild(guild) 19 | .memberCount(guild) 20 | .reason(ban.reason) 21 | .user(user) 22 | .toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/services/handlers/guild-create.handler.ts: -------------------------------------------------------------------------------- 1 | import EventHandler from './event-handler'; 2 | import { Guild, TextChannel, ClientEvents } from 'discord.js'; 3 | import Deps from '../../utils/deps'; 4 | import Guilds from '../../data/guilds'; 5 | 6 | export default class GuildCreateHandler implements EventHandler { 7 | on: keyof ClientEvents = 'guildCreate'; 8 | 9 | constructor(private guilds = Deps.get(Guilds)) {} 10 | 11 | async invoke(guild: Guild): Promise { 12 | await this.guilds.get(guild); 13 | this.sendWelcomeMessage(guild.systemChannel); 14 | } 15 | 16 | private sendWelcomeMessage(channel: TextChannel | null) { 17 | const url = `${process.env.DASHBOARD_URL}/servers/${channel.guild.id}`; 18 | channel?.send(`Hey, I'm 3PG! Customize me at ${url}`); 19 | } 20 | } -------------------------------------------------------------------------------- /src/services/handlers/logs-handler.ts: -------------------------------------------------------------------------------- 1 | import { EventType, LogEvent } from '../../data/models/guild'; 2 | import Guilds from '../../data/guilds'; 3 | import { Guild, TextChannel } from 'discord.js'; 4 | import Deps from '../../utils/deps'; 5 | import EventHandler from './event-handler'; 6 | 7 | export default abstract class LogsHandler implements EventHandler { 8 | abstract on: any; 9 | abstract event: EventType; 10 | 11 | constructor(protected guilds = Deps.get(Guilds)) {} 12 | 13 | protected async getEvent(guild: Guild) { 14 | const savedGuild = await this.guilds.get(guild); 15 | 16 | const activeEvent = savedGuild.logs.events.find(e => e.event === this.event); 17 | return (savedGuild.logs.enabled && activeEvent) ? activeEvent : null; 18 | } 19 | 20 | protected getChannel(config: LogEvent, guild: Guild) { 21 | return guild.channels.cache.get(config?.channel) as TextChannel; 22 | } 23 | 24 | protected async announce(guild: Guild, applyEventArgs: any[]) { 25 | const config = await this.getEvent(guild); 26 | if (!config) return; 27 | 28 | const message = await this.applyEventVariables(config.message, ...applyEventArgs); 29 | if (message.length <= 0) return; 30 | 31 | try { 32 | let channel = this.getChannel(config, guild); 33 | await channel?.send(message); 34 | } catch { 35 | console.log('Insufficient permissions to announce channel'); 36 | } 37 | } 38 | 39 | protected abstract applyEventVariables(...args: any[]): string | Promise; 40 | 41 | abstract invoke(...args: any[]): Promise | void; 42 | } 43 | -------------------------------------------------------------------------------- /src/services/handlers/member-join.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from './logs-handler'; 2 | import { GuildMember, ClientEvents } from 'discord.js'; 3 | import { EventType } from '../../data/models/guild'; 4 | import EventVariables from '../../modules/announce/event-variables'; 5 | 6 | export default class MemberJoinHandler extends LogsHandler { 7 | on: keyof ClientEvents = 'guildMemberAdd'; 8 | event = EventType.MemberJoin; 9 | 10 | async invoke(member: GuildMember) { 11 | await super.announce(member.guild, [ member ]); 12 | await this.addAutoRoles(member); 13 | } 14 | 15 | private async addAutoRoles(member: GuildMember) { 16 | const guild = await this.guilds.get(member.guild); 17 | 18 | await member.roles.add(guild.general.autoRoles, 'Auto role'); 19 | } 20 | 21 | protected applyEventVariables(content: string, member: GuildMember) { 22 | return new EventVariables(content) 23 | .user(member.user) 24 | .guild(member.guild) 25 | .memberCount(member.guild) 26 | .toString(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/services/handlers/member-leave.handler.ts: -------------------------------------------------------------------------------- 1 | import { GuildMember, TextChannel, ClientEvents } from 'discord.js'; 2 | import { EventType } from '../../data/models/guild'; 3 | import LogsHandler from './logs-handler'; 4 | import EventVariables from '../../modules/announce/event-variables'; 5 | 6 | export default class MemberLeaveHandler extends LogsHandler { 7 | on: keyof ClientEvents = 'guildMemberRemove'; 8 | event = EventType.MemberLeave; 9 | 10 | async invoke(member: GuildMember) { 11 | await super.announce(member.guild, [ member ]); 12 | } 13 | 14 | protected applyEventVariables(content: string, member: GuildMember) { 15 | return new EventVariables(content) 16 | .user(member.user) 17 | .guild(member.guild) 18 | .memberCount(member.guild) 19 | .toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/services/handlers/message-deleted.handler.ts: -------------------------------------------------------------------------------- 1 | import LogsHandler from './logs-handler'; 2 | import { Message, ClientEvents } from 'discord.js'; 3 | import { EventType } from '../../data/models/guild'; 4 | import EventVariables from '../../modules/announce/event-variables'; 5 | 6 | export default class MessageDeleteHandler extends LogsHandler { 7 | on: keyof ClientEvents = 'messageDelete'; 8 | event = EventType.MessageDeleted; 9 | 10 | async invoke(msg: Message) { 11 | if (msg?.author && !msg.author.bot) 12 | await super.announce(msg.guild, [ msg ]); 13 | } 14 | 15 | protected applyEventVariables(content: string, msg: Message) { 16 | return new EventVariables(content) 17 | .guild(msg.guild) 18 | .memberCount(msg.guild) 19 | .message(msg) 20 | .user(msg.author) 21 | .toString(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/handlers/message-reaction-add.handler.ts: -------------------------------------------------------------------------------- 1 | import { User, MessageReaction, ClientEvents } from 'discord.js'; 2 | import EventHandler from './event-handler'; 3 | import Deps from '../../utils/deps'; 4 | import ReactionRoles from '../../modules/general/reaction-roles'; 5 | import Guilds from '../../data/guilds'; 6 | 7 | export default class MessageReactionAddHandler implements EventHandler { 8 | on: keyof ClientEvents = 'messageReactionAdd'; 9 | 10 | constructor( 11 | private guilds = Deps.get(Guilds), 12 | private reactionRoles = Deps.get(ReactionRoles)) {} 13 | 14 | async invoke(reaction: MessageReaction, user: User) { 15 | await reaction.fetch(); 16 | 17 | const guild = reaction.message.guild; 18 | if (!guild) return; 19 | 20 | const savedGuild = await this.guilds.get(guild); 21 | await this.reactionRoles.checkToAdd(user, reaction, savedGuild); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/handlers/message-reaction-remove.handler.ts: -------------------------------------------------------------------------------- 1 | import { User, MessageReaction, ClientEvents } from 'discord.js'; 2 | import EventHandler from './event-handler'; 3 | import Guilds from '../../data/guilds'; 4 | import ReactionRoles from '../../modules/general/reaction-roles'; 5 | import Deps from '../../utils/deps'; 6 | 7 | export default class MessageReactionRemoveHandler implements EventHandler { 8 | on: keyof ClientEvents = 'messageReactionRemove'; 9 | 10 | constructor( 11 | private guilds = Deps.get(Guilds), 12 | private reactionRoles = Deps.get(ReactionRoles)) {} 13 | 14 | async invoke(reaction: MessageReaction, user: User) { 15 | const guild = reaction.message.guild; 16 | if (!guild) return; 17 | 18 | await reaction.fetch(); 19 | 20 | const savedGuild = await this.guilds.get(guild); 21 | await this.reactionRoles.checkToRemove(user, reaction, savedGuild); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/handlers/message.handler.ts: -------------------------------------------------------------------------------- 1 | import EventHandler from './event-handler'; 2 | import Deps from '../../utils/deps'; 3 | import CommandService from '../command.service'; 4 | import Guilds from '../../data/guilds'; 5 | import AutoMod from '../../modules/auto-mod/auto-mod'; 6 | import Leveling from '../../modules/xp/leveling'; 7 | import { Message, ClientEvents } from 'discord.js'; 8 | import Logs from '../../data/logs'; 9 | 10 | export default class MessageHandler implements EventHandler { 11 | on: keyof ClientEvents = 'message'; 12 | 13 | constructor( 14 | private autoMod = Deps.get(AutoMod), 15 | private commands = Deps.get(CommandService), 16 | private guilds = Deps.get(Guilds), 17 | private leveling = Deps.get(Leveling), 18 | private logs = Deps.get(Logs)) {} 19 | 20 | async invoke(msg: Message) { 21 | if (msg.author.bot) return; 22 | 23 | const savedGuild = await this.guilds.get(msg.guild); 24 | 25 | const isCommand = msg.content.startsWith(savedGuild.general.prefix); 26 | if (isCommand) { 27 | const command = await this.commands.handle(msg, savedGuild); 28 | return command;// && this.logs.logCommand(msg, command); 29 | } 30 | 31 | let filter = undefined; 32 | let earnedXP = false; 33 | try { 34 | if (savedGuild.autoMod.enabled) 35 | await this.autoMod.validate(msg, savedGuild); 36 | if (savedGuild.leveling.enabled) { 37 | await this.leveling.validateXPMsg(msg, savedGuild); 38 | earnedXP = true; 39 | } 40 | } catch (validation) { 41 | filter = validation.filter; 42 | } finally { 43 | // await this.logs.logMessage(msg, { earnedXP, filter }); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/services/handlers/ready.handler.ts: -------------------------------------------------------------------------------- 1 | import Log from '../../utils/log'; 2 | import EventHandler from './event-handler'; 3 | import Deps from '../../utils/deps'; 4 | import Music from '../../modules/music/music'; 5 | import Timers from '../../modules/timers/timers'; 6 | import CommandService from '../command.service'; 7 | import AutoMod from '../../modules/auto-mod/auto-mod'; 8 | import { ClientEvents, Client } from 'discord.js'; 9 | 10 | export default class ReadyHandler implements EventHandler { 11 | started = false; 12 | 13 | on: keyof ClientEvents = 'ready'; 14 | 15 | constructor( 16 | private autoMod = Deps.get(AutoMod), 17 | private bot = Deps.get(Client), 18 | private commandService = Deps.get(CommandService), 19 | private music = Deps.get(Music), 20 | private timers = Deps.get(Timers)) {} 21 | 22 | async invoke() { 23 | Log.info(`It's live!`, `events`); 24 | 25 | if (this.started) return; 26 | this.started = true; 27 | 28 | await this.autoMod.init(); 29 | await this.commandService.init(); 30 | this.music.initialize(); 31 | await this.timers.init(); 32 | 33 | this.bot.user?.setActivity(`3PG.xyz`); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/services/validators.ts: -------------------------------------------------------------------------------- 1 | import { Command, Permission } from '../commands/command'; 2 | import { GuildMember, TextChannel, Message } from 'discord.js'; 3 | import { GuildDocument, CustomCommand } from '../data/models/guild'; 4 | 5 | export default class Validators { 6 | checkCommand(command: Command, guild: GuildDocument, msg: Message) { 7 | const config = guild.commands.configs.find(c => c.name === command.name); 8 | if (!config) return; 9 | 10 | if (!config.enabled) 11 | throw new TypeError('Command not enabled!'); 12 | 13 | const hasWhitelistedRole = config.roles?.some(id => msg.member.roles.cache.has(id)); 14 | if (config.roles?.length > 0 && !hasWhitelistedRole) 15 | throw new TypeError(`You don't have the role to execute this command.`); 16 | 17 | const inWhitelistedChannel = config.channels 18 | ?.some(id => msg.channel.id === id); 19 | if (config.channels.length > 0 && !inWhitelistedChannel) 20 | throw new TypeError(`Command cannot be executed in this channel.`); 21 | } 22 | 23 | checkPreconditions(command: Command, executor: GuildMember) { 24 | if (command.precondition && !executor.hasPermission(command.precondition as any)) 25 | throw new TypeError(`**Required Permission**: \`${command.precondition}\``); 26 | } 27 | 28 | checkChannel(channel: TextChannel, savedGuild: GuildDocument, customCommand?: CustomCommand) { 29 | const isIgnored = savedGuild.general.ignoredChannels 30 | .some(id => id === channel.id); 31 | 32 | if (isIgnored && !customCommand) 33 | throw new TypeError('Commands cannot be executed in this channel.'); 34 | else if (isIgnored && !customCommand.anywhere) 35 | throw new TypeError('This custom command cannot be executed in this channel.'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/utils/command-utils.ts: -------------------------------------------------------------------------------- 1 | import { GuildMember, Guild } from 'discord.js'; 2 | 3 | export function getMemberFromMention(mention: string, guild: Guild): GuildMember { 4 | const id = getIdFromMention(mention); 5 | const member = guild.members.cache.get(id); 6 | if (!member) 7 | throw new TypeError('Member not found.'); 8 | 9 | return member; 10 | } 11 | 12 | function getIdFromMention(mention: string) { 13 | return mention?.match(/\d+/g)[0]; 14 | } 15 | 16 | export function getRoleFromMention(mention: string, guild: Guild) { 17 | const id = getIdFromMention(mention); 18 | const role = guild.roles.cache.get(id); 19 | if (!role) 20 | throw new TypeError('Role not found.'); 21 | 22 | return role; 23 | } 24 | 25 | export function generateUUID() { 26 | let time = new Date().getTime(); 27 | let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 28 | let random = (time + Math.random() * 16) % 16 | 0; 29 | time = Math.floor(time / 16); 30 | return ((c == 'x') ? random :(random&0x3|0x8)).toString(16); 31 | }); 32 | return uuid; 33 | } 34 | 35 | export function parseDuration(str: string) { 36 | if (!str || str == '-1' || str.toLowerCase() == 'forever') 37 | return -1; 38 | 39 | const letters = str.match(/[a-z]/g).join(''); 40 | const time = Number(str.match(/[0-9]/g).join('')); 41 | 42 | switch (letters) { 43 | case 'y': return time * 1000 * 60 * 60 * 24 * 365; 44 | case 'mo': return time * 1000 * 60 * 60 * 24 * 30; 45 | case 'w': return time * 1000 * 60 * 60 * 24 * 7; 46 | case 'd': return time * 1000 * 60 * 60 * 24; 47 | case 'h': return time * 1000 * 60 * 60; 48 | case 'm': return time * 1000 * 60; 49 | case 's': return time * 1000; 50 | } 51 | throw new TypeError('Could not parse duration. Make sure you typed the duration correctly.'); 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/deps.ts: -------------------------------------------------------------------------------- 1 | export default class Deps { 2 | static testing = false; 3 | 4 | private static deps: any[] = []; 5 | 6 | static build(...types: any) { 7 | if (this.testing) return; 8 | 9 | for (const Type of types) { 10 | try { this.deps.push(new Type()); } 11 | catch {} 12 | } 13 | } 14 | 15 | static get(type: any): T { 16 | if (!type) return null; 17 | 18 | const service = this.deps.find(t => t instanceof type); 19 | return service || this.add(new type()); 20 | } 21 | 22 | private static add(instance: T): T { 23 | this.deps.push(instance); 24 | return instance; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | export default class Log { 2 | static getSource(src?: string) { 3 | return src?.toUpperCase() || 'OTHER'; 4 | } 5 | static info(message?: any, src?: string) { 6 | console.log(`[${this.toHHMMSS(new Date())}] INFO [${this.getSource(src)}] ${message}`) 7 | } 8 | static error(err?: any, src?: string) { 9 | const message = err?.message || err || 'Unknown error'; 10 | console.error(`[${this.toHHMMSS(new Date())}] ERROR [${this.getSource(src)}] ${message}`) 11 | } 12 | 13 | private static toHHMMSS(time: Date) { 14 | let hours = time.getHours().toString().padStart(2, '0'); 15 | let minutes = time.getMinutes().toString().padStart(2, '0'); 16 | let seconds = time.getSeconds().toString().padStart(2, '0'); 17 | return `${hours}:${minutes}:${seconds}`; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/integration/auto-mod.tests.ts: -------------------------------------------------------------------------------- 1 | import { use, should, expect } from 'chai'; 2 | import { GuildDocument, MessageFilter } from '../../src/data/models/guild'; 3 | import { mock } from 'ts-mockito'; 4 | import AutoMod from '../../src/modules/auto-mod/auto-mod'; 5 | import { Message } from 'discord.js'; 6 | import chaiAsPromised from 'chai-as-promised'; 7 | import { SavedMember } from '../../src/data/models/member'; 8 | import Members from '../../src/data/members'; 9 | import Emit from '../../src/services/emit'; 10 | 11 | describe('modules/auto-mod', () => { 12 | let autoMod: AutoMod; 13 | 14 | beforeEach(() => { 15 | const members = mock(); 16 | members.get = (): any => new SavedMember(); 17 | 18 | autoMod = new AutoMod(mock(), members); 19 | }); 20 | 21 | describe('validateMsg', () => { 22 | it('contains ban word, has filter, error thrown', async() => { 23 | const guild = mock(); 24 | const msg = mock(); 25 | 26 | guild.autoMod.filters = [MessageFilter.Words]; 27 | guild.autoMod.banWords = ['a']; 28 | msg.content = 'a'; 29 | 30 | const result = () => autoMod.validate(msg, guild); 31 | 32 | result().should.eventually.throw(); 33 | }); 34 | 35 | it('contains ban word, has filter, auto deleted, error thrown', async() => { 36 | const guild = mock(); 37 | const msg = mock(); 38 | 39 | guild.autoMod.filters = [MessageFilter.Words]; 40 | guild.autoMod.banWords = ['a']; 41 | msg.content = 'a'; 42 | msg.delete = () => { throw new TypeError('deleted'); } 43 | 44 | const result = () => autoMod.validate(msg, guild); 45 | 46 | result().should.eventually.throw('deleted'); 47 | }); 48 | 49 | it('contains ban word, no filter, ignored', async() => { 50 | const guild = mock(); 51 | const msg = mock(); 52 | 53 | guild.autoMod.filters = []; 54 | guild.autoMod.banWords = []; 55 | msg.content = 'a'; 56 | 57 | const result = () => autoMod.validate(msg, guild); 58 | 59 | result().should.not.eventually.throw(); 60 | }); 61 | 62 | it('contains ban link, has filter, error thrown', async() => { 63 | const guild = mock(); 64 | const msg = mock(); 65 | 66 | guild.autoMod.filters = [MessageFilter.Links]; 67 | guild.autoMod.banLinks = ['a']; 68 | msg.content = 'a'; 69 | 70 | const result = () => autoMod.validate(msg, guild); 71 | 72 | result().should.eventually.throw(); 73 | }); 74 | 75 | it('contains ban link, no filter, ignored', async() => { 76 | const guild = mock(); 77 | const msg = mock(); 78 | 79 | guild.autoMod.filters = []; 80 | guild.autoMod.banLinks = ['a']; 81 | msg.content = 'a'; 82 | 83 | const result = () => autoMod.validate(msg, guild); 84 | 85 | result().should.not.eventually.throw(); 86 | }); 87 | }); 88 | 89 | describe('warnMember', () => { 90 | it('warn member, message sent to user', async() => { 91 | const member: any = { id: '123', send: () => { throw new TypeError() }, user: { bot: false }}; 92 | const instigator: any = { id: '321' }; 93 | 94 | const result = () => autoMod.warn(member, instigator); 95 | 96 | result().should.eventually.throw(); 97 | }); 98 | 99 | it('warn self member, error thrown', async() => { 100 | const member: any = { id: '123', user: { bot: false } }; 101 | const instigator: any = { id: '123' }; 102 | 103 | const result = () => autoMod.warn(member, instigator); 104 | 105 | result().should.eventually.throw(); 106 | }); 107 | 108 | it('warn bot member, error thrown', async() => { 109 | const member: any = { id: '123', user: { bot: true }}; 110 | const instigator: any = { id: '321' }; 111 | 112 | const result = () => autoMod.warn(member, instigator); 113 | 114 | result().should.eventually.throw(); 115 | }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /test/integration/command.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { expect, use, should } from 'chai'; 2 | import CommandService from '../../src/services/command.service'; 3 | import { mock } from 'ts-mockito'; 4 | import chaiAsPromised from 'chai-as-promised'; 5 | import Commands from '../../src/data/commands'; 6 | import { SavedGuild, GuildDocument } from '../../src/data/models/guild' 7 | import Cooldowns from '../../src/services/cooldowns'; 8 | import Validators from '../../src/services/validators'; 9 | 10 | should(); 11 | use(chaiAsPromised); 12 | 13 | describe('services/command-service', () => { 14 | let savedGuild: GuildDocument; 15 | let service: CommandService; 16 | 17 | beforeEach(() => { 18 | 19 | savedGuild = new SavedGuild(); 20 | savedGuild.general.prefix = '.'; 21 | 22 | service = new CommandService( 23 | mock(), 24 | mock(), 25 | mock()); 26 | }); 27 | 28 | describe('handle', () => { 29 | it('empty message gets ignored', () => { 30 | const msg: any = { content: '', channel: { reply: () => { throw Error() }}}; 31 | 32 | const result = () => service.handle(msg, savedGuild); 33 | 34 | expect(result()).to.eventually.throw(); 35 | }); 36 | 37 | it('no found command message gets ignored', () => { 38 | const msg: any = { content: '.pong', reply: () => { throw Error(); }}; 39 | 40 | const result = () => service.handle(msg, savedGuild); 41 | 42 | expect(result()).to.eventually.throw(); 43 | }); 44 | 45 | it('found command gets executed', () => { 46 | const msg: any = { content: '.ping', reply: () => { throw Error(); }}; 47 | 48 | const result = () => service.handle(msg, savedGuild); 49 | 50 | expect(result()).to.eventually.throw(); 51 | }); 52 | 53 | it('found command, with extra args, gets executed', async () => { 54 | const msg: any = { content: '.ping pong', reply: () => { throw Error(); }}; 55 | 56 | const result = () => service.handle(msg, savedGuild); 57 | 58 | expect(result()).to.eventually.throw(); 59 | }); 60 | 61 | it('found command, with unmet precondition, gets ignored', async () => { 62 | const msg: any = { content: '.warnings', reply: () => { throw Error(); }}; 63 | 64 | await service.handle(msg, savedGuild); 65 | }); 66 | 67 | it('command override disabled command, throws error', () => { 68 | const msg: any = { content: '.ping', reply: () => { throw Error(); }}; 69 | 70 | savedGuild.commands.configs.push({ name: 'ping', enabled: false, roles: [], channels: [] }); 71 | 72 | const result = () => service.handle(msg, savedGuild); 73 | 74 | expect(result).to.eventually.throw(); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/integration/logs.tests.ts: -------------------------------------------------------------------------------- 1 | import { use, should } from 'chai'; 2 | import { SavedGuild, EventType } from '../../src/data/models/guild'; 3 | import { mock } from 'ts-mockito'; 4 | import { TextChannel, GuildMember } from 'discord.js'; 5 | import chaiAsPromised from 'chai-as-promised'; 6 | import MemberJoinHandler from '../../src/services/handlers/member-join.handler' 7 | import Guilds from '../../src/data/guilds'; 8 | 9 | use(chaiAsPromised); 10 | should(); 11 | 12 | describe('modules/logs', () => { 13 | let guilds: Guilds; 14 | 15 | beforeEach(() => { 16 | guilds = mock(); 17 | guilds.get = (): any => new SavedGuild(); 18 | }); 19 | 20 | describe('member join handler', () => { 21 | let member: GuildMember; 22 | 23 | it('member join, member undefined, returns', () => { 24 | const result = () => new MemberJoinHandler(guilds).invoke(member); 25 | 26 | result().should.eventually.not.throw(); 27 | }); 28 | 29 | it('member join, event not active, returns', () => { 30 | const result = () => new MemberJoinHandler(guilds).invoke(member); 31 | 32 | result().should.eventually.not.throw(); 33 | }); 34 | 35 | it('member join, channel not found, returns', () => { 36 | guilds.get = (): any => { 37 | const guild = new SavedGuild(); 38 | guild.logs.events.push({ 39 | enabled: true, 40 | event: EventType.MemberJoin, 41 | message: 'test', 42 | channel: '321' 43 | }); 44 | } 45 | 46 | const result = () => new MemberJoinHandler(guilds).invoke(member); 47 | 48 | result().should.eventually.not.throw(); 49 | }); 50 | 51 | it('member join, event active, message is sent', () => { 52 | guilds.get = (): any => { 53 | const guild = new SavedGuild(); 54 | guild.logs.events.push({ 55 | enabled: true, 56 | event: EventType.MemberJoin, 57 | message: 'test', 58 | channel: '123' 59 | }); 60 | } 61 | 62 | const result = () => new MemberJoinHandler(guilds).invoke(member); 63 | 64 | result().should.eventually.throw('test'); 65 | }); 66 | 67 | it('member join, event active, message is sent with applied guild variables', () => { 68 | guilds.get = (): any => { 69 | const guild = new SavedGuild(); 70 | guild.logs.events.push({ 71 | enabled: true, 72 | event: EventType.MemberJoin, 73 | message: '[USER] joined!', 74 | channel: '123' 75 | }); 76 | } 77 | 78 | const result = () => new MemberJoinHandler(guilds).invoke(member); 79 | 80 | result().should.eventually.throws(new TypeError('<@!123> joined!')); 81 | }); 82 | }); 83 | }); -------------------------------------------------------------------------------- /test/integration/routes.tests.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { app } from '../../src/api/server'; 3 | 4 | const testConfig = { 5 | guildId: '' 6 | }; 7 | 8 | describe('routes/api', () => { 9 | describe('/', () => { 10 | it('returns 200', (done) => { 11 | request(app).get('/api') 12 | .expect(200) 13 | .end(done); 14 | }); 15 | }); 16 | 17 | describe('/commands', () => { 18 | const url = '/api/commands'; 19 | 20 | it('returns 200', (done) => { 21 | request(app).get(url) 22 | .expect(200) 23 | .end(done); 24 | }); 25 | }); 26 | 27 | describe('/auth', () => { 28 | const url = '/api/auth'; 29 | 30 | it('no code, returns 400', (done) => { 31 | request(app).get(url) 32 | .expect(400) 33 | .end(done); 34 | }); 35 | }); 36 | 37 | describe('/user', () => { 38 | const url = '/api/user'; 39 | 40 | it('no key, returns 400', (done) => { 41 | request(app).get(url) 42 | .expect(400) 43 | .end(done); 44 | }); 45 | }); 46 | 47 | it('any url returns 404', (done) => { 48 | request(app).get('/api/a') 49 | .expect(404) 50 | .end(done); 51 | }); 52 | }); 53 | 54 | describe('routes/api/guilds', () => { 55 | let url: string; 56 | 57 | beforeEach(() => url = '/api/guilds'); 58 | 59 | describe('GET /:id/log', () => { 60 | it('found guild, returns guild', (done) => { 61 | url += `/${testConfig.guildId}/public`; 62 | 63 | request(app).get(url) 64 | .expect(200) 65 | .end(done); 66 | }); 67 | }); 68 | 69 | describe('GET /:id/public', () => { 70 | it('found guild, returns guild', (done) => { 71 | url += `/${testConfig.guildId}/public`; 72 | 73 | request(app).get(url) 74 | .expect(200) 75 | .end(done); 76 | }); 77 | 78 | it('unknown guild, returns undefined', (done) => { 79 | url += '/321/public'; 80 | 81 | request(app).get(url) 82 | .expect(200) 83 | .expect(undefined) 84 | .end(done); 85 | }); 86 | }); 87 | 88 | describe('GET /', () => { 89 | it('no key, returns 400', (done) => { 90 | request(app).get(url) 91 | .expect(400) 92 | .end(done); 93 | }); 94 | }); 95 | 96 | describe('POST /', () => { 97 | it('no key, returns 400', (done) => { 98 | request(app).get(url) 99 | .expect(400) 100 | .end(done); 101 | }); 102 | }); 103 | 104 | describe('GET /:id/users', () => { 105 | url += '/123/users'; 106 | 107 | it('unknown guild, returns 404', (done) => { 108 | request(app).get(url) 109 | .expect(404) 110 | .end(done); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/integration/timers.tests.ts: -------------------------------------------------------------------------------- 1 | import Timers from '../../src/modules/timers/timers'; 2 | import { expect } from 'chai'; 3 | import { SavedGuild, GuildDocument } from '../../src/data/models/guild'; 4 | 5 | describe('modules/timers', () => { 6 | let commandsService: any; 7 | let guilds: any; 8 | let savedGuild: GuildDocument; 9 | let timers: Timers; 10 | 11 | beforeEach(() => { 12 | savedGuild = new SavedGuild(); 13 | 14 | commandsService = {}; 15 | guilds = { get: () => savedGuild }; 16 | 17 | timers = new Timers(commandsService, guilds); 18 | }); 19 | 20 | it('cancelTimers, removes timers for a guild', () => { 21 | timers.startTimers('123'); 22 | 23 | timers.endTimers('123'); 24 | 25 | const result = timers.get('123').length; 26 | expect(result).to.equal(0); 27 | }); 28 | 29 | it('startTimers, starts all saved timers for a guild', async() => { 30 | savedGuild.timers.messageTimers.push({ 31 | enabled: true, 32 | interval: '00:10', 33 | from: new Date(), 34 | channel: null, 35 | message: '' 36 | }); 37 | savedGuild.timers.commandTimers.push({ 38 | enabled: true, 39 | interval: '00:10', 40 | from: new Date(), 41 | channel: '', 42 | command: 'ping' 43 | }); 44 | 45 | await timers.startTimers('123'); 46 | 47 | const result = timers.get('123').length; 48 | expect(result).to.equal(2); 49 | }); 50 | 51 | it('startTimers, more than 8 scheduled tasks and no PRO, extra tasks not added') 52 | 53 | it('getInterval, returns hours interval in milliseconds', () => { 54 | const expected = 60 * 60 * 1000; 55 | const result = timers.getInterval('01:00'); 56 | 57 | expect(result).to.equal(expected); 58 | }); 59 | 60 | it('getInterval, returns hours and minutes interval in milliseconds', () => { 61 | const expected = (60 * 60 * 1000) + (45 * 60 * 1000); 62 | const result = timers.getInterval('01:45'); 63 | 64 | expect(result).to.equal(expected); 65 | }); 66 | }); -------------------------------------------------------------------------------- /test/mock.ts: -------------------------------------------------------------------------------- 1 | import { User, GuildMember, Guild } from 'discord.js'; 2 | import { mock } from 'ts-mockito'; 3 | import { CommandContext } from '../src/commands/command'; 4 | 5 | export class Mock { 6 | static guild() { 7 | const guild = mock(); 8 | 9 | guild.id = '533947001578979322'; 10 | guild.name = 'Test Server'; 11 | 12 | return guild; 13 | } 14 | 15 | static member() { 16 | const member = mock(); 17 | 18 | member.guild = Mock.guild(); 19 | member.user = Mock.user(); 20 | 21 | return member; 22 | } 23 | 24 | static user() { 25 | const user = mock(); 26 | 27 | user.username = 'User'; 28 | user.discriminator = '0001'; 29 | user.id = '533947001578979328'; 30 | 31 | return user; 32 | } 33 | } -------------------------------------------------------------------------------- /test/unit/audit-logger.tests.ts: -------------------------------------------------------------------------------- 1 | import AuditLogger from '../../src/api/modules/audit-logger'; 2 | import { expect } from 'chai'; 3 | 4 | describe('api/modules/audit-logger', () => { 5 | it('no changes, empty array returned', () => { 6 | const values = { 7 | old: { a: 'a', b: 'b' }, 8 | new: { a: 'a', b: 'b' } 9 | }; 10 | 11 | const expected = { old: {}, new: {} }; 12 | const result = AuditLogger.getChanges(values, 'a', '123').changes; 13 | 14 | expect(result).to.deep.equal(expected); 15 | }); 16 | 17 | it('1 change, 1 change returned', () => { 18 | const values = { 19 | old: { a: 'a', b: 'b' }, 20 | new: { a: 'b', b: 'b' } 21 | }; 22 | 23 | const expected = { 24 | old: { a: 'a' }, 25 | new: { a: 'b' } 26 | }; 27 | const result = AuditLogger.getChanges(values, 'a', '123').changes; 28 | 29 | expect(result).to.deep.equal(expected); 30 | }); 31 | 32 | it('3 changes, 3 changes returned', () => { 33 | const values = { 34 | old: { 35 | a: 'a', 36 | b: 'b', 37 | c: 'c' 38 | }, 39 | new: { 40 | a: '1', 41 | b: '2', 42 | c: '3' 43 | } 44 | }; 45 | 46 | const expected = values; 47 | const result = AuditLogger.getChanges(values, 'a', '123').changes; 48 | 49 | expect(result).to.deep.equal(expected); 50 | }); 51 | }); -------------------------------------------------------------------------------- /test/unit/auto-mod-validators.tests.ts: -------------------------------------------------------------------------------- 1 | import { SavedGuild, GuildDocument, MessageFilter } from '../../src/data/models/guild'; 2 | import { Message } from 'discord.js'; 3 | import { expect } from 'chai'; 4 | import EmojiValidator from '../../src/modules/auto-mod/validators/emoji.validator'; 5 | import MassMentionValidator from '../../src/modules/auto-mod/validators/mass-mention.validator'; 6 | import MassCapsValidator from '../../src/modules/auto-mod/validators/mass-caps.validator'; 7 | import ZalgoValidator from '../../src/modules/auto-mod/validators/zalgo.validator'; 8 | 9 | describe('auto-mod/validators', () => { 10 | let guild: GuildDocument; 11 | beforeEach(() => guild = new SavedGuild()); 12 | 13 | describe('emoji validator', () => { 14 | it('no emojis, does not throw', () => { 15 | const validator = new EmojiValidator(); 16 | 17 | const result = () => validator.validate('', guild); 18 | 19 | expect(result).to.not.throw(); 20 | }); 21 | it('nearly too many emojis, does not throw', () => { 22 | const validator = new EmojiValidator(); 23 | 24 | const result = () => validator.validate('🤔🤔🤔🤔', guild); 25 | 26 | expect(result).to.not.throw(); 27 | }); 28 | it('too many emojis, throws error', () => { 29 | const validator = new EmojiValidator(); 30 | 31 | const result = () => validator.validate('🤔🤔🤔🤔🤔', guild); 32 | 33 | expect(result).to.throw(); 34 | }); 35 | }); 36 | 37 | describe('mass mention validator', () => { 38 | it('no mentions, does not throw', () => { 39 | const validator = new MassMentionValidator(); 40 | 41 | const result = () => validator.validate('', guild); 42 | 43 | expect(result).to.not.throw(); 44 | }); 45 | it('nearly too many mentions, does not throw', () => { 46 | const validator = new MassMentionValidator(); 47 | 48 | const result = () => validator.validate('<@!704656805208522833><@!704656805208522833><@!704656805208522833><@!704656805208522833>', guild); 49 | 50 | expect(result).to.not.throw(); 51 | }); 52 | it('too many mentions, throws error', () => { 53 | const validator = new MassMentionValidator(); 54 | 55 | const result = () => validator.validate('<@!704656805208522833><@!704656805208522833><@!704656805208522833><@!704656805208522833><@!704656805208522833>', guild); 56 | 57 | expect(result).to.throw(); 58 | }); 59 | }); 60 | 61 | describe('all caps validator', () => { 62 | it('no caps, does not throw', () => { 63 | const validator = new MassCapsValidator(); 64 | 65 | const result = () => validator.validate('a', guild); 66 | 67 | expect(result).to.not.throw(); 68 | }); 69 | it('nearly too many caps, does not throw', () => { 70 | const validator = new MassCapsValidator(); 71 | 72 | const result = () => validator.validate('AAAAaaaaaa', guild); 73 | 74 | expect(result).to.not.throw(); 75 | }); 76 | it('too many caps, throws error', () => { 77 | const validator = new MassCapsValidator(); 78 | 79 | const result = () => validator.validate('AAaa', guild); 80 | 81 | expect(result).to.throw(); 82 | }); 83 | }); 84 | 85 | describe('zalgo validator', () => { 86 | it('no zalgo, does not throw', () => { 87 | const validator = new ZalgoValidator(); 88 | 89 | const result = () => validator.validate('a 🤔', guild); 90 | 91 | expect(result).to.not.throw(); 92 | }); 93 | it('zalgo, throws error', () => { 94 | const validator = new ZalgoValidator(); 95 | 96 | const result = () => validator.validate('a̵͎̟̻̟̺͇̭͚͇̳̔̍͊̈́̀̕͝', guild); 97 | 98 | expect(result).to.throw(); 99 | }); 100 | }); 101 | }); -------------------------------------------------------------------------------- /test/unit/commands.tests.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Player } from 'erela.js'; 3 | import { mock } from 'ts-mockito'; 4 | import { CommandContext } from '../../src/commands/command'; 5 | import PauseCommand from '../../src/commands/pause'; 6 | import PlayCommand from '../../src/commands/play'; 7 | import ResumeCommand from '../../src/commands/resume'; 8 | import StopCommand from '../../src/commands/stop'; 9 | import XPCommand from '../../src/commands/xp'; 10 | import WarningsCommand from '../../src/commands/warnings'; 11 | 12 | 13 | describe('commands/play', () => { 14 | it('null query, throws error', () => { 15 | const ctx = mock(); 16 | ctx.member = { voice: { channel: null }} as any; 17 | 18 | const result = () => new PlayCommand().execute(ctx); 19 | 20 | result().should.eventually.throw(); 21 | }); 22 | 23 | it('null channel, throws error', () => { 24 | const ctx = mock(); 25 | ctx.member = { voice: { channel: null }} as any; 26 | 27 | const result = () => new PlayCommand().execute(ctx, 'a'); 28 | 29 | result().should.eventually.throw(); 30 | }); 31 | }); 32 | 33 | describe('commands/warnings', () => { 34 | it('null channel, throws error', () => 35 | { 36 | const ctx = mock(); 37 | 38 | const result = () => new WarningsCommand().execute(ctx, '1'); 39 | 40 | result().should.eventually.throw(); 41 | }); 42 | }); 43 | 44 | describe('commands/xp', () => { 45 | let command: XPCommand; 46 | 47 | beforeEach(() => command = new XPCommand()); 48 | 49 | it('mentioned user not found, error thrown', () => { 50 | const result = () => command.execute({} as any, '<@!>'); 51 | 52 | expect(result).to.throw(); 53 | }); 54 | 55 | it('xp bot user, error thrown', () => { 56 | const ctx = { member: { user: { bot: true }}} as any; 57 | 58 | const result = () => command.execute(ctx, ''); 59 | 60 | expect(result).to.throw(); 61 | }); 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /test/unit/cooldowns.tests.ts: -------------------------------------------------------------------------------- 1 | import Cooldowns from '../../src/services/cooldowns'; 2 | import { User } from 'discord.js'; 3 | import { mock } from 'ts-mockito'; 4 | import { Command } from '../../src/commands/command'; 5 | import { expect } from 'chai'; 6 | 7 | describe('services/cooldowns', () => { 8 | let cooldowns: Cooldowns; 9 | let user: User; 10 | let command: Command; 11 | 12 | beforeEach(() => { 13 | cooldowns = new Cooldowns(); 14 | user = mock(User); 15 | command = mock(command); 16 | 17 | user.id = '123'; 18 | command.name = 'ping'; 19 | }); 20 | 21 | it('no cooldowns, active', () => { 22 | const result = cooldowns.active(user, command); 23 | 24 | expect(result).to.be.false; 25 | }); 26 | 27 | it('user in cooldown, inactive', () => { 28 | cooldowns.add(user, command); 29 | 30 | const result = cooldowns.active(user, command); 31 | 32 | expect(result).to.be.true; 33 | }); 34 | 35 | it('user cooldown removed, inactive', () => { 36 | cooldowns.add(user, command); 37 | cooldowns.remove(user, command); 38 | 39 | const result = cooldowns.active(user, command); 40 | 41 | expect(result).to.be.false; 42 | }); 43 | }); -------------------------------------------------------------------------------- /test/unit/data.tests.ts: -------------------------------------------------------------------------------- 1 | import Commands from '../../src/data/commands'; 2 | import { expect } from 'chai'; 3 | 4 | describe('data/commands', () => { 5 | it('getCommandUsage returns valid command usage with no args', () => { 6 | const result = new Commands().getCommandUsage({ 7 | name: 'ping', 8 | execute: (ctx: any) => {} 9 | } as any); 10 | 11 | expect(result).to.equal('ping'); 12 | }); 13 | 14 | it('getCommandUsage returns valid command with args', () => { 15 | const result = new Commands().getCommandUsage({ 16 | name: 'a', 17 | execute: (ctx: any, b: any) => {} 18 | } as any); 19 | 20 | expect(result).to.equal('a b'); 21 | }); 22 | }); -------------------------------------------------------------------------------- /test/unit/event-variables.tests.ts: -------------------------------------------------------------------------------- 1 | import EventVariables from '../../src/modules/announce/event-variables'; 2 | import { expect } from 'chai'; 3 | 4 | describe('modules/announce/event-variables', () => { 5 | it('GUILD', () => { 6 | const variables = new EventVariables('[GUILD] is good server'); 7 | 8 | const user = { name: 'test' } as any; 9 | const result = variables.guild(user).toString(); 10 | 11 | expect(result).to.equal('test is good server'); 12 | }); 13 | 14 | it('INSTIGATOR', () => { 15 | const variables = new EventVariables('[INSTIGATOR] banned User'); 16 | 17 | const user = { id: '123' } as any; 18 | const result = variables.instigator(user).toString(); 19 | 20 | expect(result).to.equal('<@!123> banned User'); 21 | }); 22 | 23 | it('MEMBER_COUNT', () => { 24 | const variables = new EventVariables('[MEMBER_COUNT] member(s)'); 25 | 26 | const guild = { memberCount: 1 } as any; 27 | const result = variables.memberCount(guild).toString(); 28 | 29 | expect(result).to.equal('1 member(s)'); 30 | }); 31 | 32 | it('MESSAGE', () => { 33 | const variables = new EventVariables('Message: `[MESSAGE]`'); 34 | 35 | const message = { content: 'hi' } as any; 36 | const result = variables.message(message).toString(); 37 | 38 | expect(result).to.equal('Message: `hi`'); 39 | }); 40 | 41 | it('NEW_LEVEL', () => { 42 | const variables = new EventVariables('New: `[NEW_LEVEL]`'); 43 | 44 | const level = 2; 45 | const result = variables.newLevel(level).toString(); 46 | 47 | expect(result).to.equal('New: `2`'); 48 | }); 49 | 50 | it('NEW_value', () => { 51 | const variables = new EventVariables('New: [NEW_VALUE]'); 52 | 53 | const change = { a: 'b' }; 54 | const result = variables.newValue(change).toString(); 55 | 56 | expect(result).to.equal(`New: ${JSON.stringify(change, null, 2)}`); 57 | }); 58 | 59 | it('OLD_LEVEL', () => { 60 | const variables = new EventVariables('Old: `[OLD_LEVEL]`'); 61 | 62 | const level = 1; 63 | const result = variables.oldLevel(level).toString(); 64 | 65 | expect(result).to.equal('Old: `1`'); 66 | }); 67 | 68 | it('OLD_VALUE', () => { 69 | const variables = new EventVariables('Old: [OLD_VALUE]'); 70 | 71 | const change = { a: 'a' }; 72 | const result = variables.oldValue(change).toString(); 73 | 74 | expect(result).to.equal(`Old: ${JSON.stringify(change, null, 2)}`); 75 | }); 76 | 77 | it('REASON', () => { 78 | const variables = new EventVariables('User was banned for `[REASON]`'); 79 | 80 | const reason = 'hacking'; 81 | const result = variables.reason(reason).toString(); 82 | 83 | expect(result).to.equal('User was banned for `hacking`'); 84 | }); 85 | 86 | it('USER', () => { 87 | const variables = new EventVariables('[USER] = trash'); 88 | 89 | const user = { id: '123' } as any; 90 | const result = variables.user(user).toString(); 91 | 92 | expect(result).to.equal('<@!123> = trash'); 93 | }); 94 | 95 | it('WARNINGS', () => { 96 | const variables = new EventVariables('User has [WARNINGS] warnings'); 97 | 98 | const warnings = 4; 99 | const result = variables.warnings(warnings).toString(); 100 | 101 | expect(result).to.equal('User has 4 warnings'); 102 | }); 103 | }); -------------------------------------------------------------------------------- /test/unit/leveling.tests.ts: -------------------------------------------------------------------------------- 1 | import { should, use, expect } from 'chai'; 2 | import { mock } from 'ts-mockito'; 3 | import Leveling from '../../src/modules/xp/leveling'; 4 | import { GuildDocument } from '../../src/data/models/guild'; 5 | import chaiAsPromised from 'chai-as-promised'; 6 | import Deps from '../../src/utils/deps'; 7 | 8 | use(chaiAsPromised); 9 | should(); 10 | 11 | describe('modules/leveling', () => { 12 | let leveling: Leveling; 13 | 14 | beforeEach(() => { 15 | leveling = new Leveling(); 16 | Deps.testing = true; 17 | }); 18 | 19 | describe('validateXPMsg', () => { 20 | it('null message member throws exception', () => { 21 | const guild = mock(); 22 | let msg: any = { member: null }; 23 | 24 | const result = () => leveling.validateXPMsg(msg, guild); 25 | 26 | result().should.eventually.throw(); 27 | }); 28 | 29 | it('member with ignored role throws exception', () => { 30 | const guild = mock(); 31 | let msg: any = { member: { roles: { cache: [{ id: '123' }] }}}; 32 | guild.leveling.ignoredRoles = ['123']; 33 | 34 | const result = () => leveling.validateXPMsg(msg, guild); 35 | 36 | result().should.eventually.throw(); 37 | }); 38 | }); 39 | 40 | describe('getLevel', () => { 41 | it('0 returns level 1', () => { 42 | const result = new Leveling().getLevel(0); 43 | 44 | expect(result).to.deep.equal(1); 45 | }); 46 | 47 | it('floored level returned, min level messages', () => { 48 | const result = new Leveling().getLevel(300); 49 | 50 | expect(result).to.equal(2); 51 | }); 52 | 53 | it('floored level returned, greater than min level messages', () => { 54 | const result = new Leveling().getLevel(400); 55 | 56 | expect(result).to.equal(2); 57 | }); 58 | }); 59 | describe('getLevel', () => { 60 | it('0 returns level 1', () => { 61 | const result = Leveling.xpInfo(0).level; 62 | 63 | expect(result).to.deep.equal(1); 64 | }); 65 | 66 | it('floored level returned, min level messages', () => { 67 | const result = Leveling.xpInfo(300).level; 68 | 69 | expect(result).to.equal(2); 70 | }); 71 | 72 | it('floored level returned, greater than min level messages', () => { 73 | const result = Leveling.xpInfo(400).level; 74 | 75 | expect(result).to.equal(2); 76 | }); 77 | }); 78 | 79 | describe('xpForNextLevel', () => { 80 | it('0 xp returns max xp for next level', () => { 81 | const result = Leveling.xpInfo(0).xpForNextLevel; 82 | 83 | expect(result).to.equal(300); 84 | }); 85 | 86 | it('minimum level xp returns max xp for next level', () => { 87 | const result = Leveling.xpInfo(300).xpForNextLevel; 88 | 89 | expect(result).to.equal(450); 90 | }); 91 | 92 | it('250XP returns 50XP for next level', () => { 93 | const result = Leveling.xpInfo(250).xpForNextLevel; 94 | 95 | expect(result).to.equal(50); 96 | }); 97 | }); 98 | 99 | describe('levelCompletion', () => { 100 | it('no level completion, returns 0', () => { 101 | const result = Leveling.xpInfo(0).levelCompletion; 102 | 103 | expect(result).to.equal(0); 104 | }); 105 | 106 | it('250/300 level completion, returns 0.83333...', () => { 107 | const result = Leveling.xpInfo(250).levelCompletion; 108 | 109 | expect(result).to.be.approximately(0.833, 0.05); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/unit/logs.tests.ts: -------------------------------------------------------------------------------- 1 | import EventVariables from '../../src/modules/announce/event-variables'; 2 | import { expect } from 'chai'; 3 | 4 | describe('modules/logs/event-variables', () => { 5 | it('GUILD', () => { 6 | const variables = new EventVariables('[GUILD] is good server'); 7 | 8 | const user = { name: 'test' } as any; 9 | const result = variables.guild(user).toString(); 10 | 11 | expect(result).to.equal('test is good server'); 12 | }); 13 | 14 | it('INSTIGATOR', () => { 15 | const variables = new EventVariables('[INSTIGATOR] banned User'); 16 | 17 | const user = { id: '123' } as any; 18 | const result = variables.instigator(user).toString(); 19 | 20 | expect(result).to.equal('<@!123> banned User'); 21 | }); 22 | 23 | it('MEMBER_COUNT', () => { 24 | const variables = new EventVariables('[MEMBER_COUNT] member(s)'); 25 | 26 | const guild = { memberCount: 1 } as any; 27 | const result = variables.memberCount(guild).toString(); 28 | 29 | expect(result).to.equal('1 member(s)'); 30 | }); 31 | 32 | it('MESSAGE', () => { 33 | const variables = new EventVariables('Message: `[MESSAGE]`'); 34 | 35 | const message = { content: 'hi' } as any; 36 | const result = variables.message(message).toString(); 37 | 38 | expect(result).to.equal('Message: `hi`'); 39 | }); 40 | 41 | it('NEW_LEVEL', () => { 42 | const variables = new EventVariables('New: `[NEW_LEVEL]`'); 43 | 44 | const level = 2; 45 | const result = variables.newLevel(level).toString(); 46 | 47 | expect(result).to.equal('New: `2`'); 48 | }); 49 | 50 | it('NEW_VALUE', () => { 51 | const variables = new EventVariables('New: [NEW_VALUE]'); 52 | 53 | const change = { a: 'b' }; 54 | const result = variables.newValue(change).toString(); 55 | 56 | expect(result).to.equal(`New: ${JSON.stringify(change, null, 2)}`); 57 | }); 58 | 59 | it('OLD_LEVEL', () => { 60 | const variables = new EventVariables('Old: `[OLD_LEVEL]`'); 61 | 62 | const level = 1; 63 | const result = variables.oldLevel(level).toString(); 64 | 65 | expect(result).to.equal('Old: `1`'); 66 | }); 67 | 68 | it('OLD_VALUE', () => { 69 | const variables = new EventVariables('Old: [OLD_VALUE]'); 70 | 71 | const change = { a: 'a' }; 72 | const result = variables.oldValue(change).toString(); 73 | 74 | expect(result).to.equal(`Old: ${JSON.stringify(change, null, 2)}`); 75 | }); 76 | 77 | it('REASON', () => { 78 | const variables = new EventVariables('User was banned for `[REASON]`'); 79 | 80 | const reason = 'hacking'; 81 | const result = variables.reason(reason).toString(); 82 | 83 | expect(result).to.equal('User was banned for `hacking`'); 84 | }); 85 | 86 | it('USER', () => { 87 | const variables = new EventVariables('[USER] = trash'); 88 | 89 | const user = { id: '123' } as any; 90 | const result = variables.user(user).toString(); 91 | 92 | expect(result).to.equal('<@!123> = trash'); 93 | }); 94 | 95 | it('WARNINGS', () => { 96 | const variables = new EventVariables('User has [WARNINGS] warnings'); 97 | 98 | const warnings = 4; 99 | const result = variables.warnings(warnings).toString(); 100 | 101 | expect(result).to.equal('User has 4 warnings'); 102 | }); 103 | }); -------------------------------------------------------------------------------- /test/unit/ranks.tests.ts: -------------------------------------------------------------------------------- 1 | import Ranks from '../../src/api/modules/ranks'; 2 | import { expect } from 'chai'; 3 | 4 | describe('api/ranks', () => { 5 | it('lowest xp messages returns lowest rank', () => { 6 | const members = [ 7 | { xp: '100', userId: '1' }, 8 | { xp: '200', userId: '2' }, 9 | { xp: '300', userId: '3' } 10 | ] as any; 11 | 12 | const result = Ranks.get({ id: '1' } as any, members); 13 | 14 | expect(result).to.equal(3); 15 | }); 16 | 17 | it('highest xp messages returns highest rank', () => { 18 | const members = [ 19 | { xp: '100', userId: '1' }, 20 | { xp: '999', userId: '2' }, 21 | { xp: '300', userId: '3' } 22 | ] as any; 23 | 24 | const result = Ranks.get({ id: '2' } as any, members); 25 | 26 | expect(result).to.equal(1); 27 | }); 28 | 29 | it('medium xp messages returns middle rank', () => { 30 | const members = [ 31 | { xp: '100', userId: '1' }, 32 | { xp: '999', userId: '2' }, 33 | { xp: '300', userId: '3' } 34 | ] as any; 35 | 36 | const result = Ranks.get({ id: '3' } as any, members); 37 | 38 | expect(result).to.equal(2); 39 | }); 40 | }); -------------------------------------------------------------------------------- /test/unit/stats.tests.ts: -------------------------------------------------------------------------------- 1 | import { LogDocument, SavedLog } from '../../src/data/models/log'; 2 | import Stats from '../../src/api/modules/stats'; 3 | import Logs from '../../src/data/logs'; 4 | import { mock } from 'ts-mockito'; 5 | import { expect } from 'chai'; 6 | 7 | describe('api/modules/stats', () => { 8 | let savedLog: LogDocument; 9 | let stats: Stats; 10 | 11 | beforeEach(async() => { 12 | let logs = mock(); 13 | logs.getAll = (): any => [savedLog]; 14 | 15 | savedLog = new SavedLog(); 16 | stats = new Stats(logs); 17 | 18 | savedLog.changes.push( 19 | { 20 | module: 'general', 21 | at: new Date(), 22 | by: '', 23 | changes: { old: { prefix: '/' }, new: { prefix: '.' } } 24 | }, 25 | { 26 | module: 'general', 27 | at: new Date(), 28 | by: '', 29 | changes: { old: { prefix: '/' }, new: { prefix: '.' } } 30 | }, 31 | { 32 | module: 'xp', 33 | at: new Date(), 34 | by: '', 35 | changes: { old: { xpPerMessage: 50 }, new: { xpPerMessage: 100 } } 36 | } 37 | ); 38 | 39 | savedLog.commands.push( 40 | { name: 'ping', by: '', at: new Date() }, 41 | { name: 'ping', by: '', at: new Date() }, 42 | { name: 'dashboard', by: '', at: new Date() } 43 | ); 44 | 45 | await stats.init(); 46 | }); 47 | 48 | it('get commands, returns correct count', () => { 49 | const result = stats.commands[0].count; 50 | 51 | expect(result).to.equal(2); 52 | }); 53 | 54 | it('get commands, returns correct sorted item', () => { 55 | const result = stats.commands[0].name; 56 | 57 | expect(result).to.equal('ping'); 58 | }); 59 | 60 | it('get inputs, returns correct count', () => { 61 | const result = stats.inputs[0].count; 62 | 63 | expect(result).to.equal(2); 64 | }); 65 | 66 | it('get inputs, returns correct sorted item', () => { 67 | const result = stats.inputs[0].path; 68 | 69 | expect(result).to.equal('general.prefix'); 70 | }); 71 | 72 | it('get modules, returns correct count', () => { 73 | const result = stats.modules[0].count; 74 | 75 | expect(result).to.equal(2); 76 | }); 77 | 78 | it('get modules, returns correct sorted item', () => { 79 | const result = stats.modules[0].name; 80 | 81 | expect(result).to.equal('general'); 82 | }); 83 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "inlineSourceMap": true, 7 | "lib": ["es2020"], 8 | "module": "CommonJS", 9 | "noImplicitAny": false, 10 | "outDir": "lib", 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "strictNullChecks": false, 14 | "target": "ES5", 15 | "watch": true 16 | }, 17 | "include": ["src"] 18 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "jsRules": {}, 6 | "rules": { 7 | "max-line-length": { 8 | "options": [80] 9 | }, 10 | "new-parens": true, 11 | "no-arg": true, 12 | "no-bitwise": true, 13 | "no-conditional-assignment": true, 14 | "no-consecutive-blank-lines": false, 15 | "quotemark": [ 16 | true, 17 | "single" 18 | ] 19 | }, 20 | "rulesDirectory": [] 21 | } --------------------------------------------------------------------------------