├── .commitlintrc.json ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── deploy.ts ├── images ├── Calypso.png ├── Calypso_Full_Signature.png ├── Calypso_Title.png ├── Calypso_WIP.png ├── Calypso_WIP_2.png └── Calypso_WIP_3.png ├── package-lock.json ├── package.json ├── prisma └── schema.prisma ├── src ├── app.ts ├── commands │ ├── animals │ │ ├── bird.ts │ │ ├── cat.ts │ │ ├── catfact.ts │ │ ├── dog.ts │ │ ├── dogfact.ts │ │ ├── duck.ts │ │ ├── fox.ts │ │ └── shibe.ts │ ├── color │ │ ├── color.ts │ │ └── randomcolor.ts │ ├── fun │ │ ├── coinflip.ts │ │ ├── meme.ts │ │ ├── thouart.ts │ │ ├── wholesomememe.ts │ │ ├── yesno.ts │ │ └── yomama.ts │ ├── information │ │ ├── avatar.ts │ │ ├── botinfo.ts │ │ ├── botstats.ts │ │ ├── channelinfo.ts │ │ ├── donate.ts │ │ ├── findid.ts │ │ ├── github.ts │ │ ├── help.ts │ │ ├── inviteme.ts │ │ ├── memberstatus.ts │ │ ├── permissions.ts │ │ ├── ping.ts │ │ ├── roleinfo.ts │ │ ├── servericon.ts │ │ ├── serverinfo.ts │ │ ├── supportserver.ts │ │ ├── uptime.ts │ │ └── userinfo.ts │ └── miscellaneous │ │ ├── feedback.ts │ │ └── reportbug.ts ├── components │ └── selectMenus │ │ └── help.ts ├── config.ts ├── enums.ts ├── events │ ├── debug.ts │ ├── error.ts │ ├── guildCreate.ts │ ├── interactionCreate.ts │ ├── messageCreate.ts │ ├── ready.ts │ └── warn.ts ├── logger.ts ├── prisma.ts ├── structures │ ├── Client.ts │ ├── Command.ts │ ├── Component.ts │ ├── ConfigCache.ts │ ├── Event.ts │ ├── PaginatedEmbed.ts │ └── index.ts ├── types.ts └── utils.ts └── tsconfig.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=18-bullseye 2 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 3 | 4 | # Install packages 5 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 6 | && apt-get -y install --no-install-recommends \ 7 | vim \ 8 | tree -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CalypsoBot Dev Container", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | "VARIANT": "18-bullseye" 7 | } 8 | }, 9 | "settings": { 10 | "terminal.integrated.profiles.linux": { 11 | "zsh": { 12 | "path": "/usr/bin/zsh" 13 | } 14 | }, 15 | "terminal.integrated.defaultProfile.linux": "zsh" 16 | }, 17 | "customizations": { 18 | "vscode": { 19 | "extensions": [ 20 | "dbaeumer.vscode-eslint", 21 | "esbenp.prettier-vscode", 22 | "streetsidesoftware.code-spell-checker", 23 | "Prisma.prisma" 24 | ] 25 | } 26 | }, 27 | "remoteUser": "node", 28 | "features": { 29 | "git": "latest" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 12, 6 | "sourceType": "module", 7 | "project": ["./tsconfig.json"] 8 | }, 9 | "plugins": ["@typescript-eslint", "import", "eslint-plugin-tsdoc"], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 14 | "plugin:@typescript-eslint/strict", 15 | "plugin:import/recommended", 16 | "plugin:import/typescript", 17 | "prettier" 18 | ], 19 | "rules": { 20 | "prefer-destructuring": ["error", { "object": true, "array": false }], 21 | "@typescript-eslint/no-unused-vars": "error", 22 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"], 23 | "@typescript-eslint/explicit-function-return-type": "error", 24 | "@typescript-eslint/explicit-member-accessibility": "error", 25 | "@typescript-eslint/no-floating-promises": [ 26 | "error", 27 | { "ignoreIIFE": true } 28 | ], 29 | "@typescript-eslint/restrict-template-expressions": "off", 30 | "@typescript-eslint/no-base-to-string": "off", 31 | "@typescript-eslint/consistent-type-imports": "warn", 32 | "import/no-named-as-default": "off", 33 | "sort-imports": [ 34 | "error", 35 | { 36 | "allowSeparatedGroups": true, 37 | "ignoreCase": false, 38 | "ignoreDeclarationSort": true, 39 | "ignoreMemberSort": false, 40 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] 41 | } 42 | ], 43 | "tsdoc/syntax": "warn" 44 | }, 45 | "settings": { 46 | "import/resolver": { 47 | "typescript": { 48 | "project": "./tsconfig.json" 49 | } 50 | } 51 | }, 52 | "env": { 53 | "browser": false, 54 | "node": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | node-version: [18.x] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | 33 | - name: Build 34 | run: npm run build 35 | 36 | - name: Lint 37 | run: npm run lint 38 | 39 | - name: Check format 40 | run: npm run check 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | if [ -z "${2-}" ]; then 5 | exec < /dev/tty && node_modules/.bin/cz --hook 6 | fi 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "bracketSpacing": true, 5 | "semi": false, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "streetsidesoftware.code-spell-checker", 6 | "Prisma.prisma" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "-r", 10 | "ts-node/register", 11 | "-r", 12 | "tsconfig-paths/register" 13 | ], 14 | "args": ["src/app.ts"], 15 | "cwd": "${workspaceFolder}", 16 | "console": "integratedTerminal", 17 | "internalConsoleOptions": "neverOpen", 18 | "env": { 19 | "TS_NODE_PROJECT": "tsconfig.json", 20 | "TS_NODE_TRANSPILE_ONLY": "true" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.insertSpaces": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.formatOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | }, 9 | "[prisma]": { 10 | "editor.defaultFormatter": "Prisma.prisma" 11 | }, 12 | "cSpell.enabled": true, 13 | "cSpell.words": [ 14 | "botinfo", 15 | "botstats", 16 | "bugreport", 17 | "catfact", 18 | "channelinfo", 19 | "coinflip", 20 | "commitlint", 21 | "dankmemes", 22 | "discordjs", 23 | "dogfact", 24 | "findid", 25 | "IIFE", 26 | "inviteme", 27 | "memberstatus", 28 | "precommit", 29 | "randomcolor", 30 | "reportbug", 31 | "roleinfo", 32 | "Seagrass", 33 | "servericon", 34 | "serverinfo", 35 | "Shiba", 36 | "shibe", 37 | "subreddits", 38 | "supportserver", 39 | "thouart", 40 | "tsdoc", 41 | "unvalidated", 42 | "wholesomememe", 43 | "wholesomememes", 44 | "yesno", 45 | "yomama" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | CalypsoBot - A fully customizable bot built with discord.js 635 | Copyright (C) 2018-2022 Sebastian Battle 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | CalypsoBot Copyright (C) 2018-2022 Sebastian Battle 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 |
5 | Calypso Discord Bot 6 |
7 |

8 | 9 |

A fully customizable bot built with discord.js

10 | 11 |
12 | 13 | 14 | shield.png 15 | 16 | 17 | 18 | shield.png 19 | 20 | 21 | 22 | shield.png 23 | 24 | 25 |
26 | 27 | ## Calypso is undergoing a complete rewrite. What you see here is heavily WIP. You may be looking for the old (outdated) version of Calypso, [here](https://github.com/sabattle/CalypsoBot/tree/archive) 28 | -------------------------------------------------------------------------------- /deploy.ts: -------------------------------------------------------------------------------- 1 | import { REST } from '@discordjs/rest' 2 | import { Routes } from 'discord-api-types/v10' 3 | import logger from 'logger' 4 | import config from 'config' 5 | import { basename, sep } from 'path' 6 | import { promisify } from 'util' 7 | import glob from 'glob' 8 | import { type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord.js' 9 | import type { StructureImport } from 'types' 10 | import type { Command } from '@structures' 11 | 12 | const { token, clientId, guildId } = config 13 | 14 | const glob_ = promisify(glob) 15 | 16 | const _loadCommands = async (): Promise< 17 | RESTPostAPIChatInputApplicationCommandsJSONBody[] 18 | > => { 19 | const commands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = [] 20 | const files = await glob_( 21 | `${__dirname.split(sep).join('/')}/src/commands/*/*{.ts,.js}`, 22 | ) 23 | if (files.length === 0) { 24 | logger.warn('No commands found') 25 | return commands 26 | } 27 | 28 | for (const f of files) { 29 | const name = basename(f, '.ts') 30 | try { 31 | const command = ((await import(f)) as StructureImport).default 32 | commands.push(command.data.toJSON()) 33 | } catch (err) { 34 | if (err instanceof Error) { 35 | logger.error(`Command failed to import: ${name}`) 36 | logger.error(err.stack) 37 | } else logger.error(err) 38 | } 39 | } 40 | 41 | return commands 42 | } 43 | 44 | const rest = new REST({ version: '10' }).setToken(token) 45 | 46 | logger.info('Deploying commands...') 47 | 48 | const applicationCommands = 49 | process.env.NODE_ENV === 'production' 50 | ? Routes.applicationCommands(clientId) 51 | : Routes.applicationGuildCommands(clientId, guildId) 52 | 53 | ;(async (): Promise => { 54 | try { 55 | const commands = await _loadCommands() 56 | await rest.put(applicationCommands, { body: commands }) 57 | logger.info(`Commands successfully deployed`) 58 | } catch (err) { 59 | if (err instanceof Error) logger.error(err.stack) 60 | else logger.error(err) 61 | } 62 | })() 63 | -------------------------------------------------------------------------------- /images/Calypso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabattle/CalypsoBot/db016cc48a9f543024b79db3aa470514b6b3277d/images/Calypso.png -------------------------------------------------------------------------------- /images/Calypso_Full_Signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabattle/CalypsoBot/db016cc48a9f543024b79db3aa470514b6b3277d/images/Calypso_Full_Signature.png -------------------------------------------------------------------------------- /images/Calypso_Title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabattle/CalypsoBot/db016cc48a9f543024b79db3aa470514b6b3277d/images/Calypso_Title.png -------------------------------------------------------------------------------- /images/Calypso_WIP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabattle/CalypsoBot/db016cc48a9f543024b79db3aa470514b6b3277d/images/Calypso_WIP.png -------------------------------------------------------------------------------- /images/Calypso_WIP_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabattle/CalypsoBot/db016cc48a9f543024b79db3aa470514b6b3277d/images/Calypso_WIP_2.png -------------------------------------------------------------------------------- /images/Calypso_WIP_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabattle/CalypsoBot/db016cc48a9f543024b79db3aa470514b6b3277d/images/Calypso_WIP_3.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calypso-bot", 3 | "version": "0.0.1", 4 | "description": "A fully customizable bot built with discord.js", 5 | "main": "app.ts", 6 | "engines": { 7 | "node": ">=18.12.0" 8 | }, 9 | "dependencies": { 10 | "@discordjs/rest": "^1.3.0", 11 | "@prisma/client": "^4.6.0", 12 | "chalk": "^4.1.2", 13 | "cli-table3": "^0.6.3", 14 | "common-tags": "^1.8.2", 15 | "dayjs": "^1.11.6", 16 | "discord.js": "^14.6.0", 17 | "dotenv": "^16.0.1", 18 | "glob": "^8.0.3", 19 | "lodash": "^4.17.21", 20 | "node-fetch": "^2.6.7", 21 | "winston": "^3.8.2", 22 | "yargs": "^17.6.2" 23 | }, 24 | "devDependencies": { 25 | "@commitlint/cli": "^17.0.3", 26 | "@commitlint/config-conventional": "^17.0.3", 27 | "@types/common-tags": "^1.8.1", 28 | "@types/glob": "^8.0.0", 29 | "@types/lodash": "^4.14.187", 30 | "@types/node": "^18.0.4", 31 | "@types/node-fetch": "^2.6.2", 32 | "@types/yargs": "^17.0.13", 33 | "@typescript-eslint/eslint-plugin": "^5.30.7", 34 | "@typescript-eslint/parser": "^5.30.7", 35 | "commitizen": "^4.2.5", 36 | "cross-env": "^7.0.3", 37 | "cz-conventional-changelog": "^3.3.0", 38 | "eslint": "^8.20.0", 39 | "eslint-config-prettier": "^8.5.0", 40 | "eslint-import-resolver-typescript": "^3.5.2", 41 | "eslint-plugin-import": "^2.26.0", 42 | "eslint-plugin-tsdoc": "^0.2.17", 43 | "husky": "8.0.1", 44 | "lint-staged": "13.0.3", 45 | "prettier": "2.7.1", 46 | "prisma": "^4.6.0", 47 | "ts-node": "^10.9.1", 48 | "ts-node-dev": "^2.0.0", 49 | "tsconfig-paths": "^4.1.0", 50 | "tslib": "^2.4.0", 51 | "typescript": "^4.7.4" 52 | }, 53 | "scripts": { 54 | "start": "cross-env NODE_ENV=development ts-node src/app.ts", 55 | "start:dev": "cross-env NODE_ENV=development ts-node-dev --exit-child -r tsconfig-paths/register src/app.ts", 56 | "start:debug": "cross-env NODE_ENV=development ts-node-dev -r tsconfig-paths/register src/app.ts --debug", 57 | "start:prod": "cross-env NODE_ENV=production TS_NODE_BASEURL=dist/src node -r tsconfig-paths/register dist/src/app.js", 58 | "deploy": "cross-env NODE_ENV=development ts-node deploy.ts", 59 | "deploy:dev": "npm run deploy", 60 | "deploy:prod": "cross-env NODE_ENV=production ts-node deploy.ts", 61 | "build": "tsc", 62 | "watch": "tsc -w", 63 | "lint": "eslint --ext .js,.ts src", 64 | "lint:fix": "eslint --ext .js,.ts --fix src", 65 | "check": "prettier --ignore-path .gitignore --check .", 66 | "format": "prettier --ignore-path .gitignore --write .", 67 | "precommit": "npx lint-staged", 68 | "prepare": "husky install" 69 | }, 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/sabattle/CalypsoBot.git" 73 | }, 74 | "author": "Sebastian Battle", 75 | "license": "GPL-3.0", 76 | "bugs": { 77 | "url": "https://github.com/sabattle/CalypsoBot/issues" 78 | }, 79 | "homepage": "https://github.com/sabattle/CalypsoBot#readme", 80 | "lint-staged": { 81 | "**/*.{js,ts}": "eslint --ext .js,.ts", 82 | "**/*.{js,ts,json,md}": "prettier --ignore-path .gitignore --write" 83 | }, 84 | "config": { 85 | "commitizen": { 86 | "path": "./node_modules/cz-conventional-changelog" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mongodb" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Guild { 11 | id String @id @default(auto()) @map("_id") @db.ObjectId 12 | guildId String @unique 13 | name String 14 | config Config 15 | 16 | @@map("guilds") 17 | } 18 | 19 | type Config { 20 | colorRolePrefix String @default("#") 21 | } 22 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@structures' 2 | import config from 'config' 3 | import { GatewayIntentBits, Partials } from 'discord.js' 4 | 5 | const client = new Client(config, { 6 | intents: [ 7 | GatewayIntentBits.Guilds, 8 | GatewayIntentBits.GuildMembers, 9 | GatewayIntentBits.GuildPresences, 10 | GatewayIntentBits.GuildMessages, 11 | GatewayIntentBits.DirectMessages, 12 | ], 13 | partials: [Partials.Channel], 14 | }) 15 | 16 | // Initialize bot 17 | ;(async (): Promise => await client.init())() 18 | -------------------------------------------------------------------------------- /src/commands/animals/bird.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('bird') 9 | .setDescription('Displays a random bird.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const res = await fetch('http://shibe.online/api/birds') 17 | const image = ((await res.json()) as string[])[0] 18 | 19 | const embed = new EmbedBuilder() 20 | .setTitle('🐦 Chirp! 🐦') 21 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 22 | .setImage(image) 23 | .setFooter({ 24 | text: member?.displayName ?? user.username, 25 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 26 | }) 27 | .setTimestamp() 28 | 29 | await client.reply(interaction, { embeds: [embed] }) 30 | } catch (err) { 31 | await client.replyWithError( 32 | interaction, 33 | ErrorType.CommandFailure, 34 | `Sorry ${member}, please try again later.`, 35 | ) 36 | } 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/commands/animals/cat.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('cat') 9 | .setDescription('Displays a random cat.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const api = 'https://cataas.com/cat' 17 | const res = await fetch(`${api}?json=true`) 18 | const id = ((await res.json()) as { _id: string })._id 19 | const image = api + '/' + id 20 | 21 | const embed = new EmbedBuilder() 22 | .setTitle('🐱 Meow! 🐱') 23 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 24 | .setImage(image) 25 | .setFooter({ 26 | text: member?.displayName ?? user.username, 27 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 28 | }) 29 | .setTimestamp() 30 | 31 | await client.reply(interaction, { embeds: [embed] }) 32 | } catch (err) { 33 | await client.replyWithError( 34 | interaction, 35 | ErrorType.CommandFailure, 36 | `Sorry ${member}, please try again later.`, 37 | ) 38 | } 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /src/commands/animals/catfact.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('catfact') 9 | .setDescription('Gets a random cat fact.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const res = await fetch('https://catfact.ninja/fact') 17 | const { fact } = (await res.json()) as { fact: string } 18 | 19 | const embed = new EmbedBuilder() 20 | .setTitle('🐱 Cat Fact 🐱') 21 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 22 | .setDescription(fact) 23 | .setFooter({ 24 | text: member?.displayName ?? user.username, 25 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 26 | }) 27 | .setTimestamp() 28 | 29 | await client.reply(interaction, { embeds: [embed] }) 30 | } catch (err) { 31 | await client.replyWithError( 32 | interaction, 33 | ErrorType.CommandFailure, 34 | `Sorry ${member}, please try again later.`, 35 | ) 36 | } 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/commands/animals/dog.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('dog') 9 | .setDescription('Displays a random dog.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const res = await fetch('https://dog.ceo/api/breeds/image/random') 17 | const image = ((await res.json()) as { message: string }).message 18 | 19 | const embed = new EmbedBuilder() 20 | .setTitle('🐶 Woof! 🐶') 21 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 22 | .setImage(image) 23 | .setFooter({ 24 | text: member?.displayName ?? user.username, 25 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 26 | }) 27 | .setTimestamp() 28 | 29 | await client.reply(interaction, { embeds: [embed] }) 30 | } catch (err) { 31 | await client.replyWithError( 32 | interaction, 33 | ErrorType.CommandFailure, 34 | `Sorry ${member}, please try again later.`, 35 | ) 36 | } 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/commands/animals/dogfact.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('dogfact') 9 | .setDescription('Gets a random dog fact.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const res = await fetch('https://dog-api.kinduff.com/api/facts') 17 | const fact = ((await res.json()) as { facts: string[] }).facts[0] 18 | 19 | const embed = new EmbedBuilder() 20 | .setTitle('🐶 Dog Fact 🐶') 21 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 22 | .setDescription(fact) 23 | .setFooter({ 24 | text: member?.displayName ?? user.username, 25 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 26 | }) 27 | .setTimestamp() 28 | 29 | await client.reply(interaction, { embeds: [embed] }) 30 | } catch (err) { 31 | await client.replyWithError( 32 | interaction, 33 | ErrorType.CommandFailure, 34 | `Sorry ${member}, please try again later.`, 35 | ) 36 | } 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/commands/animals/duck.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('duck') 9 | .setDescription('Displays a random duck.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const res = await fetch('https://random-d.uk/api/v2/random') 17 | const image = ((await res.json()) as { url: string }).url 18 | 19 | const embed = new EmbedBuilder() 20 | .setTitle('🦆 Quack! 🦆') 21 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 22 | .setImage(image) 23 | .setFooter({ 24 | text: member?.displayName ?? user.username, 25 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 26 | }) 27 | .setTimestamp() 28 | 29 | await client.reply(interaction, { embeds: [embed] }) 30 | } catch (err) { 31 | await client.replyWithError( 32 | interaction, 33 | ErrorType.CommandFailure, 34 | `Sorry ${member}, please try again later.`, 35 | ) 36 | } 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/commands/animals/fox.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('fox') 9 | .setDescription('Displays a random fox.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const res = await fetch('https://randomfox.ca/floof/') 17 | const { image } = (await res.json()) as { image: string } 18 | 19 | const embed = new EmbedBuilder() 20 | .setTitle('🦊 What does the fox say? 🦊') 21 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 22 | .setImage(image) 23 | .setFooter({ 24 | text: member?.displayName ?? user.username, 25 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 26 | }) 27 | .setTimestamp() 28 | 29 | await client.reply(interaction, { embeds: [embed] }) 30 | } catch (err) { 31 | await client.replyWithError( 32 | interaction, 33 | ErrorType.CommandFailure, 34 | `Sorry ${member}, please try again later.`, 35 | ) 36 | } 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/commands/animals/shibe.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('shibe') 9 | .setDescription('Displays a random Shiba Inu.'), 10 | type: CommandType.Animals, 11 | run: async (client, interaction): Promise => { 12 | const { user, guild } = interaction 13 | const { member } = Command.getMember(interaction) 14 | 15 | try { 16 | const res = await fetch('http://shibe.online/api/shibes') 17 | const image = ((await res.json()) as string[0])[0] 18 | 19 | const embed = new EmbedBuilder() 20 | .setTitle('🐶 Woof! 🐶') 21 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 22 | .setImage(image) 23 | .setFooter({ 24 | text: member?.displayName ?? user.username, 25 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 26 | }) 27 | .setTimestamp() 28 | 29 | await client.reply(interaction, { embeds: [embed] }) 30 | } catch (err) { 31 | await client.replyWithError( 32 | interaction, 33 | ErrorType.CommandFailure, 34 | `Sorry ${member}, please try again later.`, 35 | ) 36 | } 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/commands/color/color.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EmbedBuilder, 3 | PermissionFlagsBits, 4 | SlashCommandBuilder, 5 | } from 'discord.js' 6 | import { Command, PaginatedEmbed } from '@structures' 7 | import { Color, CommandType } from 'enums' 8 | 9 | export default new Command({ 10 | data: new SlashCommandBuilder() 11 | .setName('color') 12 | .setDescription('Displays a list of colors to choose between.') 13 | .setDMPermission(false), 14 | type: CommandType.Color, 15 | permissions: [ 16 | PermissionFlagsBits.ViewChannel, 17 | PermissionFlagsBits.SendMessages, 18 | PermissionFlagsBits.ManageRoles, 19 | ], 20 | run: async (client, interaction): Promise => { 21 | if (!interaction.inCachedGuild()) return 22 | const { guild, member } = interaction 23 | 24 | // Get colors 25 | const { colorRolePrefix } = (await client.configs.fetch(guild.id)) ?? {} 26 | if (!colorRolePrefix) return 27 | const colors = guild.roles.cache 28 | .filter((role) => role.name.startsWith(colorRolePrefix)) 29 | .sort((a, b) => b.position - a.position) 30 | .map((c) => c) 31 | 32 | const embed = new EmbedBuilder() 33 | .setThumbnail(guild.iconURL()) 34 | .setColor(guild.members.me?.displayHexColor ?? Color.Default) 35 | .setFooter({ 36 | text: member.displayName, 37 | iconURL: member.displayAvatarURL(), 38 | }) 39 | .setTimestamp() 40 | 41 | const interval = 25 42 | const pages = [] 43 | 44 | for (let i = 0; i < colors.length; i += interval) { 45 | const max = Math.min(i + interval, colors.length) 46 | pages.push( 47 | EmbedBuilder.from(embed) 48 | .setTitle(`Available Colors [${i + 1} - ${max}/${colors.length}]`) 49 | .setDescription(colors.slice(i, max).join(' ')), 50 | ) 51 | } 52 | 53 | await new PaginatedEmbed({ 54 | client, 55 | interaction, 56 | pages, 57 | }).run() 58 | }, 59 | }) 60 | -------------------------------------------------------------------------------- /src/commands/color/randomcolor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EmbedBuilder, 3 | PermissionFlagsBits, 4 | SlashCommandBuilder, 5 | } from 'discord.js' 6 | import { Command } from '@structures' 7 | import { CommandType, ErrorType } from 'enums' 8 | 9 | export default new Command({ 10 | data: new SlashCommandBuilder() 11 | .setName('randomcolor') 12 | .setDescription('Changes your current color to a randomly selected one.') 13 | .setDMPermission(false), 14 | type: CommandType.Color, 15 | permissions: [ 16 | PermissionFlagsBits.ViewChannel, 17 | PermissionFlagsBits.SendMessages, 18 | PermissionFlagsBits.ManageRoles, 19 | ], 20 | run: async (client, interaction): Promise => { 21 | if (!interaction.inCachedGuild()) return 22 | const { guild, member } = interaction 23 | 24 | // Get colors 25 | const { colorRolePrefix } = (await client.configs.fetch(guild.id)) ?? {} 26 | if (!colorRolePrefix) return 27 | const colors = guild.roles.cache.filter((role) => 28 | role.name.startsWith(colorRolePrefix), 29 | ) 30 | const randomColor = colors.random() 31 | const oldColor = member.roles.color ?? '`None`' 32 | if (colors.size === 0 || !randomColor) { 33 | await client.replyWithError( 34 | interaction, 35 | ErrorType.CommandFailure, 36 | `Sorry ${member}, there are no colors set on this server.`, 37 | ) 38 | return 39 | } 40 | 41 | // Assign random color 42 | try { 43 | await member.roles.remove(colors) 44 | await member.roles.add(randomColor) 45 | await client.reply(interaction, { 46 | embeds: [ 47 | new EmbedBuilder() 48 | .setTitle('Color Change') 49 | .setThumbnail(member.displayAvatarURL()) 50 | .setColor(randomColor.hexColor) 51 | .setFields([ 52 | { name: 'Member', value: `${member}`, inline: true }, 53 | { 54 | name: 'Color', 55 | value: `${oldColor} ➔ ${randomColor}`, 56 | inline: true, 57 | }, 58 | ]) 59 | .setFooter({ 60 | text: member.displayName, 61 | iconURL: member.displayAvatarURL(), 62 | }) 63 | .setTimestamp(), 64 | ], 65 | }) 66 | } catch (err) { 67 | await client.replyWithError( 68 | interaction, 69 | ErrorType.CommandFailure, 70 | `Sorry ${member}, please try again later.`, 71 | ) 72 | } 73 | }, 74 | }) 75 | -------------------------------------------------------------------------------- /src/commands/fun/coinflip.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType } from 'enums' 4 | 5 | export default new Command({ 6 | data: new SlashCommandBuilder() 7 | .setName('coinflip') 8 | .setDescription('Flips a coin.'), 9 | type: CommandType.Fun, 10 | run: async (client, interaction): Promise => { 11 | const { user, guild } = interaction 12 | const { member } = Command.getMember(interaction) 13 | 14 | const embed = new EmbedBuilder() 15 | .setTitle('🪙 Coinflip 🪙') 16 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 17 | .setDescription( 18 | `I flipped a coin for you, ${member}! It was **${ 19 | Math.round(Math.random()) ? 'heads' : 'tails' 20 | }**.`, 21 | ) 22 | .setFooter({ 23 | text: member?.displayName ?? user.username, 24 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 25 | }) 26 | .setTimestamp() 27 | 28 | await client.reply(interaction, { embeds: [embed] }) 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /src/commands/fun/meme.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('meme') 9 | .setDescription( 10 | 'Displays a random meme from the "memes", "dankmemes", or "me_irl" subreddits.', 11 | ), 12 | type: CommandType.Fun, 13 | run: async (client, interaction): Promise => { 14 | const { user, guild } = interaction 15 | const { member } = Command.getMember(interaction) 16 | 17 | try { 18 | const res = await fetch('https://meme-api.herokuapp.com/gimme') 19 | const { title, url } = (await res.json()) as { 20 | title: string 21 | url: string 22 | } 23 | 24 | const embed = new EmbedBuilder() 25 | .setTitle(title) 26 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 27 | .setImage(url) 28 | .setFooter({ 29 | text: member?.displayName ?? user.username, 30 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 31 | }) 32 | .setTimestamp() 33 | 34 | await client.reply(interaction, { embeds: [embed] }) 35 | } catch (err) { 36 | await client.replyWithError( 37 | interaction, 38 | ErrorType.CommandFailure, 39 | `Sorry ${member}, please try again later.`, 40 | ) 41 | } 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /src/commands/fun/thouart.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('thouart') 9 | .setDescription('Insults a user in an Elizabethan way.') 10 | .addUserOption((option) => 11 | option 12 | .setName('user') 13 | .setDescription('The user to insult.') 14 | .setRequired(false), 15 | ), 16 | type: CommandType.Fun, 17 | run: async (client, interaction): Promise => { 18 | const { guild } = interaction 19 | const { targetMember, member, targetUser, user } = 20 | Command.getMemberAndUser(interaction) 21 | 22 | try { 23 | const res = await fetch('http://quandyfactory.com/insult/json/') 24 | let { insult } = (await res.json()) as { insult: string } 25 | insult = insult.charAt(0).toLowerCase() + insult.slice(1) 26 | 27 | const embed = new EmbedBuilder() 28 | .setTitle('🎭 Thou Art 🎭') 29 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 30 | .setDescription(`${targetMember ?? targetUser}, ${insult}`) 31 | .setFooter({ 32 | text: member?.displayName ?? user.username, 33 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 34 | }) 35 | .setTimestamp() 36 | 37 | await client.reply(interaction, { embeds: [embed] }) 38 | } catch (err) { 39 | await client.replyWithError( 40 | interaction, 41 | ErrorType.CommandFailure, 42 | `Sorry ${member}, please try again later.`, 43 | ) 44 | } 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /src/commands/fun/wholesomememe.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('wholesomememe') 9 | .setDescription( 10 | 'Displays a random meme from the "wholesomememes" subreddit.', 11 | ), 12 | type: CommandType.Fun, 13 | run: async (client, interaction): Promise => { 14 | const { user, guild } = interaction 15 | const { member } = Command.getMember(interaction) 16 | 17 | try { 18 | const res = await fetch( 19 | 'https://meme-api.herokuapp.com/gimme/wholesomememes', 20 | ) 21 | const { title, url } = (await res.json()) as { 22 | title: string 23 | url: string 24 | } 25 | 26 | const embed = new EmbedBuilder() 27 | .setTitle(title) 28 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 29 | .setImage(url) 30 | .setFooter({ 31 | text: member?.displayName ?? user.username, 32 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 33 | }) 34 | .setTimestamp() 35 | 36 | await client.reply(interaction, { embeds: [embed] }) 37 | } catch (err) { 38 | await client.replyWithError( 39 | interaction, 40 | ErrorType.CommandFailure, 41 | `Sorry ${member}, please try again later.`, 42 | ) 43 | } 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /src/commands/fun/yesno.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | import capitalize from 'lodash/capitalize' 6 | 7 | export default new Command({ 8 | data: new SlashCommandBuilder() 9 | .setName('yesno') 10 | .setDescription('Displays a gif of a yes, a no, or a maybe.'), 11 | type: CommandType.Fun, 12 | run: async (client, interaction): Promise => { 13 | const { user, guild } = interaction 14 | const { member } = Command.getMember(interaction) 15 | 16 | try { 17 | const res = await fetch('http://yesno.wtf/api/') 18 | const json = (await res.json()) as { 19 | answer: string 20 | image: string 21 | } 22 | const answer = capitalize(json.answer) 23 | const { image } = json 24 | 25 | let title: string 26 | if (answer === 'Yes') title = '👍 ' + answer + '! 👍' 27 | else if (answer === 'No') title = '👎 ' + answer + '! 👎' 28 | else title = '👍 ' + answer + '? 👎' 29 | 30 | const embed = new EmbedBuilder() 31 | .setTitle(title) 32 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 33 | .setImage(image) 34 | .setFooter({ 35 | text: member?.displayName ?? user.username, 36 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 37 | }) 38 | .setTimestamp() 39 | 40 | await client.reply(interaction, { embeds: [embed] }) 41 | } catch (err) { 42 | await client.replyWithError( 43 | interaction, 44 | ErrorType.CommandFailure, 45 | `Sorry ${member}, please try again later.`, 46 | ) 47 | } 48 | }, 49 | }) 50 | -------------------------------------------------------------------------------- /src/commands/fun/yomama.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, ErrorType } from 'enums' 4 | import fetch from 'node-fetch' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('yomama') 9 | .setDescription("Insults a user's mother.") 10 | .addUserOption((option) => 11 | option 12 | .setName('user') 13 | .setDescription('The user to insult the mother of.') 14 | .setRequired(false), 15 | ), 16 | type: CommandType.Fun, 17 | run: async (client, interaction): Promise => { 18 | const { guild } = interaction 19 | const { targetMember, member, targetUser, user } = 20 | Command.getMemberAndUser(interaction) 21 | 22 | try { 23 | const res = await fetch('https://api.yomomma.info') 24 | let { joke } = (await res.json()) as { joke: string } 25 | joke = joke.charAt(0).toLowerCase() + joke.slice(1) 26 | if (!joke.endsWith('!') && !joke.endsWith('.') && !joke.endsWith('"')) 27 | joke += '!' // Cleanup joke 28 | 29 | const embed = new EmbedBuilder() 30 | .setTitle('👩 Yo Mama 👩') 31 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 32 | .setDescription(`${targetMember ?? targetUser}, ${joke}`) 33 | .setFooter({ 34 | text: member?.displayName ?? user.username, 35 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 36 | }) 37 | .setTimestamp() 38 | 39 | await client.reply(interaction, { embeds: [embed] }) 40 | } catch (err) { 41 | await client.replyWithError( 42 | interaction, 43 | ErrorType.CommandFailure, 44 | `Sorry ${member}, please try again later.`, 45 | ) 46 | } 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /src/commands/information/avatar.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { CommandType } from 'enums' 4 | 5 | export default new Command({ 6 | data: new SlashCommandBuilder() 7 | .setName('avatar') 8 | .setDescription("Displays a user's avatar.") 9 | .addUserOption((option) => 10 | option 11 | .setName('user') 12 | .setDescription('The user to get the avatar of.') 13 | .setRequired(false), 14 | ), 15 | type: CommandType.Information, 16 | run: async (client, interaction): Promise => { 17 | const { targetMember, member, targetUser, user } = 18 | Command.getMemberAndUser(interaction) 19 | 20 | const embed = new EmbedBuilder() 21 | .setTitle(`${targetMember?.displayName ?? targetUser.username}'s Avatar`) 22 | .setColor( 23 | targetMember?.displayHexColor ?? 24 | (await targetUser.fetch(true)).hexAccentColor ?? 25 | null, 26 | ) 27 | .setImage( 28 | targetMember?.displayAvatarURL({ size: 512 }) ?? 29 | targetUser.displayAvatarURL({ size: 512 }), 30 | ) 31 | .setFooter({ 32 | text: member?.displayName ?? user.username, 33 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 34 | }) 35 | .setTimestamp() 36 | 37 | await client.reply(interaction, { embeds: [embed] }) 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/commands/information/botinfo.ts: -------------------------------------------------------------------------------- 1 | import { oneLine, stripIndents } from 'common-tags' 2 | import { 3 | ActionRowBuilder, 4 | ButtonBuilder, 5 | ButtonStyle, 6 | EmbedBuilder, 7 | SlashCommandBuilder, 8 | type User, 9 | } from 'discord.js' 10 | import { Command } from '@structures' 11 | import { Color, CommandType, Emoji, Image, Url } from 'enums' 12 | import { dependencies, version } from '../../../package.json' 13 | 14 | export default new Command({ 15 | data: new SlashCommandBuilder() 16 | .setName('botinfo') 17 | .setDescription('Displays bot information.'), 18 | type: CommandType.Information, 19 | run: async (client, interaction): Promise => { 20 | const { user, guild } = interaction 21 | const { 22 | users, 23 | user: { id }, 24 | ownerIds, 25 | } = client 26 | const { member } = Command.getMember(interaction) 27 | 28 | const botOwners: User[] = [] 29 | for (const id of ownerIds) { 30 | botOwners.push(users.cache.get(id) ?? (await users.fetch(id))) 31 | } 32 | 33 | const embed = new EmbedBuilder() 34 | .setTitle( 35 | `${ 36 | guild?.members.me?.displayName ?? client.user.username 37 | }'s Information`, 38 | ) 39 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 40 | .setDescription( 41 | oneLine` 42 | Calypso is an open source, fully customizable Discord bot that is constantly growing. 43 | She comes packaged with a variety of commands and a multitude of settings that can be tailored to your server's specific needs. 44 | Her codebase also serves as a base framework to easily create Discord bots of all kinds. 45 | She first went live on **February 22nd, 2018**. 46 | `, 47 | ) 48 | .setFields([ 49 | { name: 'Client ID', value: `\`${id}\``, inline: true }, 50 | { 51 | name: `Developers ${Emoji.Owner}`, 52 | value: botOwners.join('\n'), 53 | inline: true, 54 | }, 55 | { 56 | name: 'Tech', 57 | value: stripIndents`\`\`\`asciidoc 58 | Version :: ${version} 59 | Library :: Discord.js v${ 60 | dependencies['discord.js'].substring(1) || '' 61 | } 62 | Environment :: Node.js ${process.version} 63 | Database :: MongoDB 64 | \`\`\``, 65 | }, 66 | ]) 67 | .setImage(Image.CalypsoTitle) 68 | .setFooter({ 69 | text: member?.displayName ?? user.username, 70 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 71 | }) 72 | .setTimestamp() 73 | 74 | const row = new ActionRowBuilder().setComponents( 75 | new ButtonBuilder() 76 | .setStyle(ButtonStyle.Link) 77 | .setURL(Url.Invite) 78 | .setLabel('Invite Me'), 79 | new ButtonBuilder() 80 | .setStyle(ButtonStyle.Link) 81 | .setURL(Url.SupportServer) 82 | .setLabel('Server'), 83 | new ButtonBuilder() 84 | .setStyle(ButtonStyle.Link) 85 | .setURL(Url.GithubRepository) 86 | .setLabel('GitHub'), 87 | new ButtonBuilder() 88 | .setStyle(ButtonStyle.Link) 89 | .setURL(Url.Donate) 90 | .setLabel('Donate'), 91 | ) 92 | 93 | await client.reply(interaction, { embeds: [embed], components: [row] }) 94 | }, 95 | }) 96 | -------------------------------------------------------------------------------- /src/commands/information/botstats.ts: -------------------------------------------------------------------------------- 1 | import { stripIndent } from 'common-tags' 2 | import dayjs from 'dayjs' 3 | import duration from 'dayjs/plugin/duration' 4 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 5 | import { Command } from '@structures' 6 | import { CommandType } from 'enums' 7 | import os from 'os' 8 | 9 | // eslint-disable-next-line import/no-named-as-default-member 10 | dayjs.extend(duration) 11 | 12 | export default new Command({ 13 | data: new SlashCommandBuilder() 14 | .setName('botstats') 15 | .setDescription('Displays bot statistics.'), 16 | type: CommandType.Information, 17 | run: async (client, interaction): Promise => { 18 | const { user, guild } = interaction 19 | const { member } = Command.getMember(interaction) 20 | const { guilds, channels, ws, uptime, commands } = client 21 | 22 | // Get bot uptime 23 | const d = dayjs.duration(uptime) 24 | const days = `${d.days()} day${d.days() == 1 ? '' : 's'}` 25 | const hours = `${d.hours()} hour${d.hours() == 1 ? '' : 's'}` 26 | 27 | // Build stats 28 | const clientStats = stripIndent` 29 | Servers :: ${guilds.cache.size} 30 | Users :: ${guilds.cache.reduce( 31 | (acc, guild) => acc + guild.memberCount, 32 | 0, 33 | )} 34 | Channels :: ${channels.cache.size} 35 | WS Ping :: ${Math.round(ws.ping)}ms 36 | Uptime :: ${days} and ${hours} 37 | ` 38 | const serverStats = stripIndent` 39 | Platform :: ${os.platform()} 40 | OS :: ${os.release()} 41 | Arch :: ${os.arch()} 42 | Hostname :: ${os.hostname()} 43 | CPUs :: ${[...new Set(os.cpus().map((x) => x.model))].join(',')} 44 | Cores :: ${os.cpus().length.toString()} 45 | RAM Total :: ${(os.totalmem() / 1024 / 1024).toFixed(2)} MB 46 | RAM Free :: ${(os.freemem() / 1024 / 1024).toFixed(2)} MB 47 | RAM Usage :: ${((1 - os.freemem() / os.totalmem()) * 100).toFixed(2)}% 48 | Uptime :: ${dayjs.duration(os.uptime()).days()} day(s) 49 | ` 50 | 51 | const embed = new EmbedBuilder() 52 | .setTitle( 53 | `${ 54 | guild?.members.me?.displayName ?? client.user.username 55 | }'s Statistics`, 56 | ) 57 | .setColor( 58 | guild?.members.me?.displayHexColor ?? 59 | (await client.user.fetch(true)).hexAccentColor ?? 60 | null, 61 | ) 62 | .addFields([ 63 | { 64 | name: 'Commands', 65 | value: `\`${commands.size}\` commands`, 66 | inline: true, 67 | }, 68 | { 69 | name: 'Command Types', 70 | value: `\`${Object.keys(CommandType).length}\` command types`, 71 | inline: true, 72 | }, 73 | { 74 | name: 'Bot Stats', 75 | value: `\`\`\`asciidoc\n${clientStats}\`\`\``, 76 | }, 77 | { name: 'Host Stats', value: `\`\`\`asciidoc\n${serverStats}\`\`\`` }, 78 | ]) 79 | .setFooter({ 80 | text: member?.displayName ?? user.username, 81 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 82 | }) 83 | .setTimestamp() 84 | 85 | await client.reply(interaction, { embeds: [embed] }) 86 | }, 87 | }) 88 | -------------------------------------------------------------------------------- /src/commands/information/channelinfo.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { Collection, EmbedBuilder, SlashCommandBuilder } from 'discord.js' 3 | import { Command } from '@structures' 4 | import { Color, CommandType } from 'enums' 5 | 6 | const channelTypes = { 7 | 0: 'Text', 8 | 2: 'Voice', 9 | 4: 'Category', 10 | 5: 'Announcement', 11 | 10: 'Announcement Thread', 12 | 11: 'Public Thread', 13 | 12: 'Private Thread', 14 | 13: 'Stage Voice', 15 | 15: 'Forum', 16 | } 17 | 18 | export default new Command({ 19 | data: new SlashCommandBuilder() 20 | .setName('channelinfo') 21 | .setDescription('Displays channel information.') 22 | .addChannelOption((option) => 23 | option 24 | .setName('channel') 25 | .setDescription('The channel to display the information of.') 26 | .setRequired(false), 27 | ) 28 | .setDMPermission(false), 29 | type: CommandType.Information, 30 | run: async (client, interaction): Promise => { 31 | if (!interaction.inCachedGuild()) return 32 | const { guild, member, options } = interaction 33 | 34 | await guild.members.fetch() // Fetch before snagging channel 35 | 36 | const channel = options.getChannel('channel') ?? interaction.channel 37 | if (!channel) return 38 | 39 | const { id, type, createdAt, members } = channel 40 | let memberCount: number 41 | let botCount: number 42 | 43 | if (members instanceof Collection) { 44 | memberCount = members.size 45 | botCount = members.filter((member) => member.user.bot).size 46 | } else { 47 | memberCount = members.cache.size 48 | botCount = members.cache.filter((member) => member.user?.bot).size 49 | } 50 | 51 | const embed = new EmbedBuilder() 52 | .setTitle('Channel Information') 53 | .setThumbnail(guild.iconURL()) 54 | .setColor(guild.members.me?.displayHexColor ?? Color.Default) 55 | .setFields([ 56 | { name: 'Channel', value: `${channel}`, inline: true }, 57 | { 58 | name: 'ID', 59 | value: `\`${id}\``, 60 | inline: true, 61 | }, 62 | { 63 | name: 'Type', 64 | value: `\`${channelTypes[type]}\``, 65 | inline: true, 66 | }, 67 | { 68 | name: 'Members', 69 | value: `\`${memberCount}\``, 70 | inline: true, 71 | }, 72 | { 73 | name: 'Bots', 74 | value: `\`${botCount}\``, 75 | inline: true, 76 | }, 77 | { 78 | name: 'Created On', 79 | value: `\`${dayjs(createdAt).format('MMM DD YYYY')}\``, 80 | inline: true, 81 | }, 82 | ]) 83 | .setFooter({ 84 | text: member.displayName, 85 | iconURL: member.displayAvatarURL(), 86 | }) 87 | .setTimestamp() 88 | 89 | await client.reply(interaction, { embeds: [embed] }) 90 | }, 91 | }) 92 | -------------------------------------------------------------------------------- /src/commands/information/donate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, 3 | ButtonBuilder, 4 | ButtonStyle, 5 | EmbedBuilder, 6 | SlashCommandBuilder, 7 | } from 'discord.js' 8 | import { Command } from '@structures' 9 | import { Color, CommandType, Image, Url } from 'enums' 10 | import { stripIndents } from 'common-tags' 11 | 12 | export default new Command({ 13 | data: new SlashCommandBuilder() 14 | .setName('donate') 15 | .setDescription("Provides a link to the bot's donation page."), 16 | type: CommandType.Information, 17 | run: async (client, interaction): Promise => { 18 | const { user, guild } = interaction 19 | const { member } = Command.getMember(interaction) 20 | 21 | const embed = new EmbedBuilder() 22 | .setTitle('Donate') 23 | .setThumbnail(Image.Calypso) 24 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 25 | .setDescription( 26 | stripIndents` 27 | Click [here](${Url.Donate}) to donate! 28 | Thank you for helping to keep me running! 29 | `, 30 | ) 31 | .setFooter({ 32 | text: member?.displayName ?? user.username, 33 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 34 | }) 35 | .setTimestamp() 36 | 37 | const row = new ActionRowBuilder().setComponents( 38 | new ButtonBuilder() 39 | .setStyle(ButtonStyle.Link) 40 | .setURL(Url.Invite) 41 | .setLabel('Invite Me'), 42 | new ButtonBuilder() 43 | .setStyle(ButtonStyle.Link) 44 | .setURL(Url.SupportServer) 45 | .setLabel('Server'), 46 | new ButtonBuilder() 47 | .setStyle(ButtonStyle.Link) 48 | .setURL(Url.GithubRepository) 49 | .setLabel('GitHub'), 50 | ) 51 | 52 | await client.reply(interaction, { embeds: [embed], components: [row] }) 53 | }, 54 | }) 55 | -------------------------------------------------------------------------------- /src/commands/information/findid.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType } from 'enums' 4 | 5 | export default new Command({ 6 | data: new SlashCommandBuilder() 7 | .setName('findid') 8 | .setDescription('Finds the ID of the given user or role.') 9 | .addMentionableOption((option) => 10 | option 11 | .setName('target') 12 | .setDescription('The target to find the ID of.') 13 | .setRequired(true), 14 | ) 15 | .setDMPermission(false), 16 | type: CommandType.Information, 17 | run: async (client, interaction): Promise => { 18 | if (!interaction.inCachedGuild()) return 19 | const { user, guild, member, options } = interaction 20 | const target = options.getMentionable('target') 21 | if (!target) return 22 | 23 | const embed = new EmbedBuilder() 24 | .setTitle('Find ID') 25 | .setColor(guild.members.me?.displayHexColor ?? Color.Default) 26 | .setFields([ 27 | { name: 'Target', value: `${target}`, inline: true }, 28 | { 29 | name: 'ID', 30 | value: `\`${target.id}\``, 31 | inline: true, 32 | }, 33 | ]) 34 | .setFooter({ 35 | text: member.displayName || user.username, 36 | iconURL: member.displayAvatarURL() || user.displayAvatarURL(), 37 | }) 38 | .setTimestamp() 39 | 40 | await client.reply(interaction, { embeds: [embed] }) 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /src/commands/information/github.ts: -------------------------------------------------------------------------------- 1 | import { stripIndents } from 'common-tags' 2 | import { 3 | ActionRowBuilder, 4 | ButtonBuilder, 5 | ButtonStyle, 6 | EmbedBuilder, 7 | SlashCommandBuilder, 8 | } from 'discord.js' 9 | import { Command } from '@structures' 10 | import { Color, CommandType, Image, Url } from 'enums' 11 | 12 | export default new Command({ 13 | data: new SlashCommandBuilder() 14 | .setName('github') 15 | .setDescription("Provides a link to the bot's GitHub repository."), 16 | type: CommandType.Information, 17 | run: async (client, interaction): Promise => { 18 | const { user, guild } = interaction 19 | const { member } = Command.getMember(interaction) 20 | 21 | const embed = new EmbedBuilder() 22 | .setTitle('GitHub Repository') 23 | .setThumbnail(Image.Calypso) 24 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 25 | .setDescription( 26 | stripIndents` 27 | Click [here](${Url.GithubRepository}) to visit my GitHub repository! 28 | Please support me by starring ⭐ my repo! 29 | `, 30 | ) 31 | .setFooter({ 32 | text: member?.displayName ?? user.username, 33 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 34 | }) 35 | .setTimestamp() 36 | 37 | const row = new ActionRowBuilder().setComponents( 38 | new ButtonBuilder() 39 | .setStyle(ButtonStyle.Link) 40 | .setURL(Url.Invite) 41 | .setLabel('Invite Me'), 42 | new ButtonBuilder() 43 | .setStyle(ButtonStyle.Link) 44 | .setURL(Url.SupportServer) 45 | .setLabel('Server'), 46 | new ButtonBuilder() 47 | .setStyle(ButtonStyle.Link) 48 | .setURL(Url.Donate) 49 | .setLabel('Donate'), 50 | ) 51 | 52 | await client.reply(interaction, { embeds: [embed], components: [row] }) 53 | }, 54 | }) 55 | -------------------------------------------------------------------------------- /src/commands/information/help.ts: -------------------------------------------------------------------------------- 1 | import { oneLine } from 'common-tags' 2 | import { 3 | type APIApplicationCommandOptionChoice, 4 | ActionRowBuilder, 5 | ButtonBuilder, 6 | ButtonStyle, 7 | EmbedBuilder, 8 | SelectMenuBuilder, 9 | SlashCommandBuilder, 10 | } from 'discord.js' 11 | import capitalize from 'lodash/capitalize' 12 | import { Command } from '@structures' 13 | import { Color, CommandType, Image, Url } from 'enums' 14 | 15 | export const descriptions = { 16 | [CommandType.Information]: 'Commands that provide various information.', 17 | [CommandType.Fun]: 'Commands for fun and games.', 18 | [CommandType.Animals]: 19 | 'Commands that display animal pictures or get animal facts.', 20 | [CommandType.Color]: 'Commands for manipulating your Discord color.', 21 | [CommandType.Miscellaneous]: 'Commands that do not belong to any other type.', 22 | } 23 | 24 | const categories: APIApplicationCommandOptionChoice[] = Object.entries( 25 | CommandType, 26 | ).map(([key, value]) => ({ name: key, value })) 27 | 28 | export default new Command({ 29 | data: new SlashCommandBuilder() 30 | .setName('help') 31 | .setDescription( 32 | oneLine` 33 | Lists all available commands, sorted by type. 34 | Provide a type for additional information. 35 | `, 36 | ) 37 | .addStringOption((option) => 38 | option 39 | .setName('type') 40 | .setDescription('The type to list the commands of.') 41 | .setChoices(...categories) 42 | .setRequired(false), 43 | ), 44 | type: CommandType.Information, 45 | run: async (client, interaction): Promise => { 46 | const { user, guild, options } = interaction 47 | const { member } = Command.getMember(interaction) 48 | 49 | const embed = new EmbedBuilder() 50 | .setTitle( 51 | `${guild?.members.me?.displayName ?? client.user.username}'s Commands`, 52 | ) 53 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 54 | .setImage(Image.CalypsoTitle) 55 | .setFooter({ 56 | text: member?.displayName ?? user.username, 57 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 58 | }) 59 | .setTimestamp() 60 | 61 | const type = options.getString('type') 62 | if (type) { 63 | const commands = client.commands.filter((command) => command.type == type) 64 | embed.setFields({ 65 | name: `**${capitalize(type)} [${commands.size}]**`, 66 | value: commands 67 | .map( 68 | (command) => 69 | `\`${command.data.name}\` **-** ${command.data.description}`, 70 | ) 71 | .join('\n'), 72 | }) 73 | } else { 74 | // Get all commands 75 | const commands: { [key in CommandType]: string[] } = { 76 | [CommandType.Information]: [], 77 | [CommandType.Fun]: [], 78 | [CommandType.Animals]: [], 79 | [CommandType.Color]: [], 80 | [CommandType.Miscellaneous]: [], 81 | } 82 | 83 | client.commands.forEach((command) => { 84 | commands[command.type].push(`\`${command.data.name}\``) 85 | }) 86 | 87 | for (const [key, value] of Object.entries(commands)) { 88 | embed.addFields([ 89 | { 90 | name: `**${capitalize(key)} [${value.length}]**`, 91 | value: value.join(' '), 92 | }, 93 | ]) 94 | } 95 | } 96 | 97 | const rows = [ 98 | new ActionRowBuilder().setComponents( 99 | new SelectMenuBuilder().setCustomId('help').setOptions( 100 | Object.entries(CommandType).map(([key, value]) => ({ 101 | label: key, 102 | value, 103 | description: descriptions[value], 104 | default: value === type, 105 | })), 106 | ), 107 | ), 108 | new ActionRowBuilder().setComponents( 109 | new ButtonBuilder() 110 | .setStyle(ButtonStyle.Link) 111 | .setURL(Url.Invite) 112 | .setLabel('Invite Me'), 113 | new ButtonBuilder() 114 | .setStyle(ButtonStyle.Link) 115 | .setURL(Url.SupportServer) 116 | .setLabel('Server'), 117 | new ButtonBuilder() 118 | .setStyle(ButtonStyle.Link) 119 | .setURL(Url.GithubRepository) 120 | .setLabel('GitHub'), 121 | new ButtonBuilder() 122 | .setStyle(ButtonStyle.Link) 123 | .setURL(Url.Donate) 124 | .setLabel('Donate'), 125 | ), 126 | ] 127 | 128 | await client.reply(interaction, { 129 | embeds: [embed], 130 | components: [...rows], 131 | ephemeral: true, 132 | }) 133 | }, 134 | }) 135 | -------------------------------------------------------------------------------- /src/commands/information/inviteme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, 3 | ButtonBuilder, 4 | ButtonStyle, 5 | EmbedBuilder, 6 | SlashCommandBuilder, 7 | } from 'discord.js' 8 | import { Command } from '@structures' 9 | import { Color, CommandType, Image, Url } from 'enums' 10 | 11 | export default new Command({ 12 | data: new SlashCommandBuilder() 13 | .setName('inviteme') 14 | .setDescription('Provides a link to invite the bot.'), 15 | type: CommandType.Information, 16 | run: async (client, interaction): Promise => { 17 | const { user, guild } = interaction 18 | const { member } = Command.getMember(interaction) 19 | 20 | const embed = new EmbedBuilder() 21 | .setTitle('Invite Me!') 22 | .setThumbnail(Image.Calypso) 23 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 24 | .setDescription(`Click [here](${Url.Invite}) to invite me!`) 25 | .setFooter({ 26 | text: member?.displayName ?? user.username, 27 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 28 | }) 29 | .setTimestamp() 30 | 31 | const row = new ActionRowBuilder().setComponents( 32 | new ButtonBuilder() 33 | .setStyle(ButtonStyle.Link) 34 | .setURL(Url.SupportServer) 35 | .setLabel('Server'), 36 | new ButtonBuilder() 37 | .setStyle(ButtonStyle.Link) 38 | .setURL(Url.GithubRepository) 39 | .setLabel('GitHub'), 40 | new ButtonBuilder() 41 | .setStyle(ButtonStyle.Link) 42 | .setURL(Url.Donate) 43 | .setLabel('Donate'), 44 | ) 45 | 46 | await client.reply(interaction, { embeds: [embed], components: [row] }) 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /src/commands/information/memberstatus.ts: -------------------------------------------------------------------------------- 1 | import { stripIndents } from 'common-tags' 2 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 3 | import { Command } from '@structures' 4 | import { Color, CommandType, Emoji } from 'enums' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('memberstatus') 9 | .setDescription( 10 | 'Gets how many server members are online, busy, AFK, and offline.', 11 | ) 12 | .setDMPermission(false), 13 | type: CommandType.Information, 14 | run: async (client, interaction): Promise => { 15 | if (!interaction.inCachedGuild()) return 16 | const { user, guild, member } = interaction 17 | const { members } = guild 18 | 19 | await members.fetch() 20 | 21 | // Count members by status 22 | const online = members.cache.filter( 23 | (member) => member.presence?.status === 'online', 24 | ).size 25 | const offline = members.cache.filter( 26 | (member) => 27 | member.presence?.status === 'offline' || 28 | member.presence?.status === undefined, 29 | ).size 30 | const dnd = members.cache.filter( 31 | (member) => member.presence?.status === 'dnd', 32 | ).size 33 | const afk = members.cache.filter( 34 | (member) => member.presence?.status === 'idle', 35 | ).size 36 | 37 | const embed = new EmbedBuilder() 38 | .setTitle(`Member Status [${members.cache.size}]`) 39 | .setThumbnail(guild.iconURL()) 40 | .setColor(guild.members.me?.displayHexColor ?? Color.Default) 41 | .setDescription( 42 | stripIndents` 43 | ${Emoji.Online} **Online:** \`${online}\` members 44 | ${Emoji.Dnd} **Busy:** \`${dnd}\` members 45 | ${Emoji.Idle} **AFK:** \`${afk}\` members 46 | ${Emoji.Offline} **Offline:** \`${offline}\` members 47 | `, 48 | ) 49 | .setFooter({ 50 | text: member.displayName || user.username, 51 | iconURL: member.displayAvatarURL() || user.displayAvatarURL(), 52 | }) 53 | .setTimestamp() 54 | 55 | await client.reply(interaction, { embeds: [embed] }) 56 | }, 57 | }) 58 | -------------------------------------------------------------------------------- /src/commands/information/permissions.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { CommandType } from 'enums' 4 | import { getPermissions } from 'utils' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('permissions') 9 | .setDescription("Displays a user's permissions.") 10 | .addUserOption((option) => 11 | option 12 | .setName('user') 13 | .setDescription('The user to get the permissions of.') 14 | .setRequired(false), 15 | ) 16 | .setDMPermission(false), 17 | type: CommandType.Information, 18 | run: async (client, interaction): Promise => { 19 | const { targetMember, member } = Command.getMember(interaction) 20 | if (!targetMember || !member) return 21 | 22 | const permissions = getPermissions(targetMember) 23 | 24 | const embed = new EmbedBuilder() 25 | .setTitle(`${targetMember.displayName}'s Permissions`) 26 | .setThumbnail(targetMember.displayAvatarURL()) 27 | .setColor(targetMember.displayHexColor) 28 | .setDescription(`\`\`\`diff\n${permissions.join('\n')}\`\`\``) 29 | .setFooter({ 30 | text: member.displayName, 31 | iconURL: member.displayAvatarURL(), 32 | }) 33 | .setTimestamp() 34 | 35 | await client.reply(interaction, { embeds: [embed] }) 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /src/commands/information/ping.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType, Emoji } from 'enums' 4 | 5 | export default new Command({ 6 | data: new SlashCommandBuilder() 7 | .setName('ping') 8 | .setDescription("Gets the bot's current ping."), 9 | type: CommandType.Information, 10 | run: async (client, interaction): Promise => { 11 | const { user, guild, createdTimestamp } = interaction 12 | const { member } = Command.getMember(interaction) 13 | 14 | const embed = new EmbedBuilder() 15 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 16 | .setDescription('`Pinging...`') 17 | 18 | const message = await client.reply(interaction, { 19 | embeds: [embed], 20 | fetchReply: true, 21 | }) 22 | 23 | const heartbeat = `\`\`\`ini\n[ ${Math.round(client.ws.ping)}ms ]\`\`\`` 24 | const latency = `\`\`\`ini\n[ ${Math.floor( 25 | message.createdTimestamp - createdTimestamp, 26 | )}ms ]\`\`\`` 27 | 28 | embed 29 | .setTitle(`Pong ${Emoji.Pong}`) 30 | .setDescription(null) 31 | .addFields( 32 | { name: 'Heartbeat', value: heartbeat, inline: true }, 33 | { name: 'API Latency', value: latency, inline: true }, 34 | ) 35 | .setFooter({ 36 | text: member?.displayName ?? user.username, 37 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 38 | }) 39 | .setTimestamp() 40 | 41 | await client.editReply(interaction, { embeds: [embed] }) 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /src/commands/information/roleinfo.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 3 | import { Command } from '@structures' 4 | import { CommandType } from 'enums' 5 | import { getPermissions } from 'utils' 6 | 7 | export default new Command({ 8 | data: new SlashCommandBuilder() 9 | .setName('roleinfo') 10 | .setDescription('Displays role information.') 11 | .addRoleOption((option) => 12 | option 13 | .setName('role') 14 | .setDescription('The role to display the information of.') 15 | .setRequired(true), 16 | ) 17 | .setDMPermission(false), 18 | type: CommandType.Information, 19 | run: async (client, interaction): Promise => { 20 | if (!interaction.inCachedGuild()) return 21 | const { guild, member, options } = interaction 22 | 23 | await guild.members.fetch() // Fetch before snagging role 24 | 25 | const role = options.getRole('role') 26 | if (!role) return 27 | 28 | const { 29 | id, 30 | position, 31 | mentionable, 32 | managed, 33 | hoist, 34 | hexColor, 35 | members, 36 | createdAt, 37 | } = role 38 | 39 | const permissions = getPermissions(role) 40 | 41 | const revPosition = `\`${guild.roles.cache.size - position}\`**/**\`${ 42 | guild.roles.cache.size 43 | }\`` 44 | 45 | const embed = new EmbedBuilder() 46 | .setTitle('Role Information') 47 | .setThumbnail(guild.iconURL()) 48 | .setColor(hexColor) 49 | .setFields([ 50 | { name: 'Role', value: `${role}`, inline: true }, 51 | { 52 | name: 'ID', 53 | value: `\`${id}\``, 54 | inline: true, 55 | }, 56 | { 57 | name: 'Position', 58 | value: `${revPosition}`, 59 | inline: true, 60 | }, 61 | { 62 | name: 'Mentionable', 63 | value: `\`${mentionable}\``, 64 | inline: true, 65 | }, 66 | { 67 | name: 'Bot Role', 68 | value: `\`${managed}\``, 69 | inline: true, 70 | }, 71 | { 72 | name: 'Hoisted', 73 | value: `\`${hoist}\``, 74 | inline: true, 75 | }, 76 | { 77 | name: 'Color', 78 | value: `\`${hexColor.toUpperCase()}\``, 79 | inline: true, 80 | }, 81 | { 82 | name: 'Members', 83 | value: `\`${members.size}\``, 84 | inline: true, 85 | }, 86 | { 87 | name: 'Created On', 88 | value: `\`${dayjs(createdAt).format('MMM DD YYYY')}\``, 89 | inline: true, 90 | }, 91 | { 92 | name: 'Permissions', 93 | value: `\`\`\`diff\n${permissions.join('\n')}\`\`\``, 94 | inline: true, 95 | }, 96 | ]) 97 | .setFooter({ 98 | text: member.displayName, 99 | iconURL: member.displayAvatarURL(), 100 | }) 101 | .setTimestamp() 102 | 103 | await client.reply(interaction, { embeds: [embed] }) 104 | }, 105 | }) 106 | -------------------------------------------------------------------------------- /src/commands/information/servericon.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 2 | import { Command } from '@structures' 3 | import { Color, CommandType } from 'enums' 4 | 5 | export default new Command({ 6 | data: new SlashCommandBuilder() 7 | .setName('servericon') 8 | .setDescription("Displays the server's icon.") 9 | .setDMPermission(false), 10 | type: CommandType.Information, 11 | run: async (client, interaction): Promise => { 12 | if (!interaction.inCachedGuild()) return 13 | const { guild, member } = interaction 14 | const embed = new EmbedBuilder() 15 | .setTitle(`${guild.name}'s Icon`) 16 | .setColor(guild.members.me?.displayHexColor ?? Color.Default) 17 | .setImage(guild.iconURL({ size: 512 })) 18 | .setFooter({ 19 | text: member.displayName, 20 | iconURL: member.displayAvatarURL(), 21 | }) 22 | .setTimestamp() 23 | 24 | await client.reply(interaction, { embeds: [embed] }) 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/commands/information/serverinfo.ts: -------------------------------------------------------------------------------- 1 | import { stripIndent } from 'common-tags' 2 | import dayjs from 'dayjs' 3 | import duration from 'dayjs/plugin/duration' 4 | import { ChannelType, EmbedBuilder, SlashCommandBuilder } from 'discord.js' 5 | import { Command } from '@structures' 6 | import { Color, CommandType, Emoji } from 'enums' 7 | 8 | // eslint-disable-next-line import/no-named-as-default-member 9 | dayjs.extend(duration) 10 | 11 | const verificationLevels = [ 12 | '`None`', 13 | '`Low`', 14 | '`Medium`', 15 | '`High`', 16 | '`Highest`', 17 | ] 18 | const notifications = ['`All`', '`Mentions`'] 19 | const premiumTiers = ['`None`', '`Tier 1`', '`Tier 2`', '`Tier 3`'] 20 | 21 | export default new Command({ 22 | data: new SlashCommandBuilder() 23 | .setName('serverinfo') 24 | .setDescription('Displays server information and statistics.') 25 | .setDMPermission(false), 26 | type: CommandType.Information, 27 | run: async (client, interaction): Promise => { 28 | if (!interaction.inCachedGuild()) return 29 | const { user, guild, member } = interaction 30 | const { id, channels, roles, members, emojis, createdAt } = guild 31 | 32 | // Get member stats 33 | await members.fetch() 34 | const memberCount = members.cache.size 35 | const botCount = members.cache.filter((member) => member.user.bot).size 36 | const online = members.cache.filter( 37 | (member) => member.presence?.status === 'online', 38 | ).size 39 | const offline = members.cache.filter( 40 | (member) => 41 | member.presence?.status === 'offline' || 42 | member.presence?.status === undefined, 43 | ).size 44 | const dnd = members.cache.filter( 45 | (member) => member.presence?.status === 'dnd', 46 | ).size 47 | const afk = members.cache.filter( 48 | (member) => member.presence?.status === 'idle', 49 | ).size 50 | 51 | // Get channel stats 52 | const channelCount = channels.cache.size 53 | const textChannels = channels.cache.filter( 54 | (channel) => channel.type === ChannelType.GuildText && channel.viewable, 55 | ).size 56 | const forumChannels = channels.cache.filter( 57 | (channel) => channel.type === ChannelType.GuildForum && channel.viewable, 58 | ).size 59 | const voiceChannels = channels.cache.filter( 60 | (channel) => 61 | channel.type === ChannelType.GuildVoice || 62 | channel.type === ChannelType.GuildStageVoice, 63 | ).size 64 | const newsChannels = channels.cache.filter( 65 | (channel) => channel.type === ChannelType.GuildAnnouncement, 66 | ).size 67 | const categoryChannels = channels.cache.filter( 68 | (channel) => channel.type === ChannelType.GuildCategory, 69 | ).size 70 | 71 | // Get role stats 72 | const roleCount = roles.cache.size - 1 // Don't count @everyone 73 | 74 | // Get emoji stats 75 | const emojiCount = emojis.cache.size 76 | 77 | const serverStats = stripIndent` 78 | Members :: [ ${memberCount} ] 79 | :: ${online} Online 80 | :: ${dnd} Busy 81 | :: ${afk} AFK 82 | :: ${offline} Offline 83 | :: ${botCount} Bots 84 | Channels :: [ ${channelCount} ] 85 | :: ${textChannels} Text 86 | :: ${forumChannels} Forum 87 | :: ${voiceChannels} Voice 88 | :: ${newsChannels} Announcement 89 | :: ${categoryChannels} Category 90 | Roles :: [ ${roleCount} ] 91 | Emojis :: [ ${emojiCount} ] 92 | ` 93 | 94 | const embed = new EmbedBuilder() 95 | .setTitle(`${guild.name}'s Information`) 96 | .setThumbnail(guild.iconURL()) 97 | .setColor(guild.members.me?.displayHexColor ?? Color.Default) 98 | .setFields([ 99 | { 100 | name: 'ID', 101 | value: `\`${id}\``, 102 | inline: true, 103 | }, 104 | { 105 | name: `Owner ${Emoji.Owner}`, 106 | value: `${members.cache.get(guild.ownerId)}`, 107 | inline: true, 108 | }, 109 | { 110 | name: 'Verification Level', 111 | value: verificationLevels[guild.verificationLevel], 112 | inline: true, 113 | }, 114 | { 115 | name: 'Rules Channel', 116 | value: guild.rulesChannel ? `${guild.rulesChannel}` : '`None`', 117 | inline: true, 118 | }, 119 | { 120 | name: 'System Channel', 121 | value: guild.systemChannel ? `${guild.systemChannel}` : '`None`', 122 | inline: true, 123 | }, 124 | { 125 | name: 'AFK Channel', 126 | value: guild.afkChannel 127 | ? `${Emoji.Voice} ${guild.afkChannel.name}` 128 | : '`None`', 129 | inline: true, 130 | }, 131 | { 132 | name: 'AFK Timeout', 133 | value: guild.afkChannel 134 | ? `\`${dayjs 135 | .duration(guild.afkTimeout * 1000) 136 | .asMinutes()} minutes\`` 137 | : '`None`', 138 | inline: true, 139 | }, 140 | { 141 | name: 'Default Notifications', 142 | value: notifications[guild.defaultMessageNotifications], 143 | inline: true, 144 | }, 145 | { 146 | name: 'Partnered', 147 | value: `\`${guild.partnered}\``, 148 | inline: true, 149 | }, 150 | { 151 | name: 'Premium Tier', 152 | value: premiumTiers[guild.premiumTier], 153 | inline: true, 154 | }, 155 | { 156 | name: 'Verified', 157 | value: `\`${guild.verified}\``, 158 | inline: true, 159 | }, 160 | { 161 | name: 'Created On', 162 | value: `\`${dayjs(createdAt).format('MMM DD YYYY')}\``, 163 | inline: true, 164 | }, 165 | { 166 | name: 'Server Stats', 167 | value: `\`\`\`asciidoc\n${serverStats}\`\`\``, 168 | }, 169 | ]) 170 | .setFooter({ 171 | text: member.displayName || user.username, 172 | iconURL: member.displayAvatarURL() || user.displayAvatarURL(), 173 | }) 174 | .setTimestamp() 175 | 176 | await client.reply(interaction, { embeds: [embed] }) 177 | }, 178 | }) 179 | -------------------------------------------------------------------------------- /src/commands/information/supportserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, 3 | ButtonBuilder, 4 | ButtonStyle, 5 | EmbedBuilder, 6 | SlashCommandBuilder, 7 | } from 'discord.js' 8 | import { Command } from '@structures' 9 | import { Color, CommandType, Image, Url } from 'enums' 10 | 11 | export default new Command({ 12 | data: new SlashCommandBuilder() 13 | .setName('supportserver') 14 | .setDescription("Provides a link to the bot's support server."), 15 | type: CommandType.Information, 16 | run: async (client, interaction): Promise => { 17 | const { user, guild } = interaction 18 | const { member } = Command.getMember(interaction) 19 | 20 | const embed = new EmbedBuilder() 21 | .setTitle('Support Server') 22 | .setThumbnail(Image.Calypso) 23 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 24 | .setDescription( 25 | `Click [here](${Url.SupportServer}) to join my support server!`, 26 | ) 27 | .setFooter({ 28 | text: member?.displayName ?? user.username, 29 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 30 | }) 31 | .setTimestamp() 32 | 33 | const row = new ActionRowBuilder().setComponents( 34 | new ButtonBuilder() 35 | .setStyle(ButtonStyle.Link) 36 | .setURL(Url.Invite) 37 | .setLabel('Invite Me'), 38 | new ButtonBuilder() 39 | .setStyle(ButtonStyle.Link) 40 | .setURL(Url.GithubRepository) 41 | .setLabel('GitHub'), 42 | new ButtonBuilder() 43 | .setStyle(ButtonStyle.Link) 44 | .setURL(Url.Donate) 45 | .setLabel('Donate'), 46 | ) 47 | 48 | await client.reply(interaction, { embeds: [embed], components: [row] }) 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /src/commands/information/uptime.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import duration from 'dayjs/plugin/duration' 3 | import advancedFormat from 'dayjs/plugin/advancedFormat' 4 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 5 | import { Command } from '@structures' 6 | import { Color, CommandType, Image } from 'enums' 7 | 8 | /* eslint-disable import/no-named-as-default-member */ 9 | dayjs.extend(duration) 10 | dayjs.extend(advancedFormat) 11 | /* eslint-enable import/no-named-as-default-member */ 12 | 13 | export default new Command({ 14 | data: new SlashCommandBuilder() 15 | .setName('uptime') 16 | .setDescription("Gets the bot's current uptime."), 17 | type: CommandType.Information, 18 | run: async (client, interaction): Promise => { 19 | const { user, guild } = interaction 20 | const { member } = Command.getMember(interaction) 21 | 22 | const d = dayjs.duration(client.uptime) 23 | const days = `${d.days()} day${d.days() == 1 ? '' : 's'}` 24 | const hours = `${d.hours()} hour${d.hours() == 1 ? '' : 's'}` 25 | const minutes = `${d.minutes()} minute${d.minutes() == 1 ? '' : 's'}` 26 | const seconds = `${d.seconds()} second${d.seconds() == 1 ? '' : 's'}` 27 | const date = dayjs().subtract(d.days(), 'day').format('dddd, MMMM Do YYYY') 28 | 29 | const embed = new EmbedBuilder() 30 | .setTitle( 31 | `${guild?.members.me?.displayName ?? client.user.username}'s Uptime`, 32 | ) 33 | .setThumbnail(Image.Calypso) 34 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 35 | .setDescription( 36 | `\`\`\`prolog\n${days}, ${hours}, ${minutes}, and ${seconds}\`\`\``, 37 | ) 38 | .setFields({ name: 'Date Launched', value: date, inline: true }) 39 | .setFooter({ 40 | text: member?.displayName ?? user.username, 41 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 42 | }) 43 | 44 | .setTimestamp() 45 | 46 | await client.reply(interaction, { embeds: [embed] }) 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /src/commands/information/userinfo.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import type { UserFlagsString } from 'discord.js' 3 | import { ActivityType, EmbedBuilder, SlashCommandBuilder } from 'discord.js' 4 | import { Command } from '@structures' 5 | import { CommandType, Emoji } from 'enums' 6 | 7 | const statuses = { 8 | online: `${Emoji.Online} \`Online\``, 9 | idle: `${Emoji.Idle} \`AFK\``, 10 | offline: `${Emoji.Offline} \`Offline\``, 11 | invisible: `${Emoji.Offline} \`Offline\``, 12 | dnd: `${Emoji.Dnd} \`Do Not Disturb\``, 13 | } 14 | 15 | const badges: Record = { 16 | Staff: `${Emoji.DiscordEmployee} \`Discord Employee\``, 17 | Partner: `${Emoji.DiscordPartner} \`Partnered Server Owner\``, 18 | BugHunterLevel1: `${Emoji.BugHunterLevel1} \`Bug Hunter (Level 1)\``, 19 | BugHunterLevel2: `${Emoji.BugHunterLevel2} \`Bug Hunter (Level 2)\``, 20 | Hypesquad: `${Emoji.HypeSquadEvents} \`HypeSquad Events\``, 21 | HypeSquadOnlineHouse1: `${Emoji.HouseBravery} \`House of Bravery\``, 22 | HypeSquadOnlineHouse2: `${Emoji.HouseBrilliance} \`House of Brilliance\``, 23 | HypeSquadOnlineHouse3: `${Emoji.HouseBalance} \`House of Balance\``, 24 | PremiumEarlySupporter: `${Emoji.EarlySupporter} \`Early Supporter\``, 25 | TeamPseudoUser: 'Team User', 26 | VerifiedBot: `${Emoji.VerifiedBot} \`Verified Bot\``, 27 | VerifiedDeveloper: `${Emoji.VerifiedDeveloper} \`Early Verified Bot Developer\``, 28 | CertifiedModerator: '', 29 | BotHTTPInteractions: '', 30 | Spammer: '', 31 | Quarantined: '', 32 | } 33 | 34 | export default new Command({ 35 | data: new SlashCommandBuilder() 36 | .setName('userinfo') 37 | .setDescription("Display's a user's information.") 38 | .addUserOption((option) => 39 | option 40 | .setName('user') 41 | .setDescription('The user to get the information of.') 42 | .setRequired(false), 43 | ) 44 | .setDMPermission(false), 45 | type: CommandType.Information, 46 | run: async (client, interaction): Promise => { 47 | if (!interaction.inCachedGuild()) return 48 | const { targetMember, member } = Command.getMember(interaction) 49 | const { 50 | id, 51 | user, 52 | presence, 53 | roles, 54 | displayName, 55 | displayHexColor, 56 | joinedAt, 57 | } = targetMember 58 | 59 | const userFlags = (await user.fetchFlags()).toArray() 60 | 61 | // Get activities 62 | const activities = [] 63 | let customStatus: string | null = null 64 | if (presence) 65 | for (const activity of presence.activities.values()) { 66 | switch (activity.type) { 67 | case ActivityType.Playing: 68 | activities.push(`Playing **${activity.name}**`) 69 | break 70 | case ActivityType.Listening: 71 | if (user.bot) activities.push(`Listening to **${activity.name}**`) 72 | else 73 | activities.push( 74 | `Listening to **${activity.details}** by **${activity.state}**`, 75 | ) 76 | break 77 | case ActivityType.Watching: 78 | activities.push(`Watching **${activity.name}**`) 79 | break 80 | case ActivityType.Streaming: 81 | activities.push(`Streaming **${activity.name}**`) 82 | break 83 | case ActivityType.Custom: 84 | customStatus = activity.state 85 | break 86 | } 87 | } 88 | 89 | const embed = new EmbedBuilder() 90 | .setTitle(`${displayName}'s Information`) 91 | .setThumbnail(targetMember.displayAvatarURL()) 92 | .setColor(displayHexColor) 93 | 94 | .setFields( 95 | { name: 'User', value: `${targetMember}`, inline: true }, 96 | { 97 | name: 'Discriminator', 98 | value: `\`${user.discriminator}\``, 99 | inline: true, 100 | }, 101 | { 102 | name: 'ID', 103 | value: `\`${id}\``, 104 | inline: true, 105 | }, 106 | { 107 | name: 'Status', 108 | value: statuses[presence?.status ?? 'offline'], 109 | inline: true, 110 | }, 111 | { 112 | name: 'Bot', 113 | value: `\`${user.bot}\``, 114 | inline: true, 115 | }, 116 | { 117 | name: 'Color Role', 118 | value: `${roles.color ?? '`None`'}`, 119 | inline: true, 120 | }, 121 | { 122 | name: 'Highest Role', 123 | value: `${roles.highest}`, 124 | inline: true, 125 | }, 126 | { 127 | name: 'Join Server on', 128 | value: `\`${dayjs(joinedAt).format('MMM DD YYYY')}\``, 129 | inline: true, 130 | }, 131 | { 132 | name: 'Joined Discord On', 133 | value: `\`${dayjs(user.createdAt).format('MMM DD YYYY')}\``, 134 | inline: true, 135 | }, 136 | ) 137 | .setFooter({ 138 | text: member.displayName, 139 | iconURL: member.displayAvatarURL(), 140 | }) 141 | .setTimestamp() 142 | if (activities.length > 0) embed.setDescription(activities.join('\n')) 143 | if (customStatus) 144 | embed.spliceFields(0, 0, { name: 'Custom Status', value: customStatus }) 145 | if (userFlags.length > 0) 146 | embed.addFields([ 147 | { 148 | name: 'Badges', 149 | value: userFlags.map((flag) => badges[flag]).join('\n'), 150 | }, 151 | ]) 152 | 153 | await client.reply(interaction, { embeds: [embed] }) 154 | }, 155 | }) 156 | -------------------------------------------------------------------------------- /src/commands/miscellaneous/feedback.ts: -------------------------------------------------------------------------------- 1 | import { stripIndents } from 'common-tags' 2 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 3 | import { Command } from '@structures' 4 | import { Color, CommandType, ErrorType, Image, Url } from 'enums' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('feedback') 9 | .setDescription( 10 | "Sends a message to the Calypso Support Server's feedback channel.", 11 | ) 12 | .addStringOption((option) => 13 | option 14 | .setName('feedback') 15 | .setDescription('The feedback to send.') 16 | .setRequired(true), 17 | ), 18 | type: CommandType.Miscellaneous, 19 | run: async (client, interaction): Promise => { 20 | const { user, guild, options } = interaction 21 | const { member } = Command.getMember(interaction) 22 | 23 | // Get feedback channel 24 | const feedbackChannel = client.channels.cache.get(client.feedbackChannelId) 25 | if (!feedbackChannel || !feedbackChannel.isTextBased()) 26 | return client.replyWithError( 27 | interaction, 28 | ErrorType.CommandFailure, 29 | 'Unable to fetch feedback channel. Please try again later.', 30 | ) 31 | 32 | const embed = new EmbedBuilder() 33 | .setTitle('Feedback') 34 | .setThumbnail(Image.Calypso) 35 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 36 | .setDescription(options.getString('feedback')) 37 | .setFields([ 38 | { name: 'User', value: user.tag, inline: true }, 39 | { 40 | name: 'Server', 41 | value: interaction.guild?.name ?? '`none`', 42 | inline: true, 43 | }, 44 | ]) 45 | .setTimestamp() 46 | await client.send(feedbackChannel, { embeds: [embed] }) 47 | 48 | await client.reply(interaction, { 49 | embeds: [ 50 | new EmbedBuilder() 51 | .setTitle('Feedback Sent') 52 | .setThumbnail(Image.Calypso) 53 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 54 | .setDescription( 55 | stripIndents` 56 | Your feedback was successfully sent! 57 | Please join the [Calypso Support Server](${Url.SupportServer}) for further discussion. 58 | `, 59 | ) 60 | .setFooter({ 61 | text: member?.displayName ?? user.username, 62 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 63 | }) 64 | .setTimestamp(), 65 | ], 66 | ephemeral: true, 67 | }) 68 | }, 69 | }) 70 | -------------------------------------------------------------------------------- /src/commands/miscellaneous/reportbug.ts: -------------------------------------------------------------------------------- 1 | import { stripIndents } from 'common-tags' 2 | import { EmbedBuilder, SlashCommandBuilder } from 'discord.js' 3 | import { Command } from '@structures' 4 | import { Color, CommandType, ErrorType, Image, Url } from 'enums' 5 | 6 | export default new Command({ 7 | data: new SlashCommandBuilder() 8 | .setName('reportbug') 9 | .setDescription( 10 | "Sends a message to the Calypso Support Server's bug reports channel.", 11 | ) 12 | .addStringOption((option) => 13 | option 14 | .setName('bugreport') 15 | .setDescription('The bug to report.') 16 | .setRequired(true), 17 | ), 18 | type: CommandType.Miscellaneous, 19 | run: async (client, interaction): Promise => { 20 | const { user, guild, options } = interaction 21 | const { member } = Command.getMember(interaction) 22 | 23 | // Get bug report channel 24 | const bugReportChannel = client.channels.cache.get( 25 | client.bugReportChannelId, 26 | ) 27 | if (!bugReportChannel || !bugReportChannel.isTextBased()) 28 | return client.replyWithError( 29 | interaction, 30 | ErrorType.CommandFailure, 31 | 'Unable to fetch bug report channel. Please try again later.', 32 | ) 33 | 34 | const embed = new EmbedBuilder() 35 | .setTitle('Bug Report') 36 | .setThumbnail(Image.Calypso) 37 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 38 | .setDescription(options.getString('bugreport')) 39 | .setFields([ 40 | { name: 'User', value: user.tag, inline: true }, 41 | { 42 | name: 'Server', 43 | value: interaction.guild?.name ?? '`none`', 44 | inline: true, 45 | }, 46 | ]) 47 | .setTimestamp() 48 | await client.send(bugReportChannel, { embeds: [embed] }) 49 | 50 | await client.reply(interaction, { 51 | embeds: [ 52 | new EmbedBuilder() 53 | .setTitle('Bug Report Sent') 54 | .setThumbnail(Image.Calypso) 55 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 56 | .setDescription( 57 | stripIndents` 58 | Your bug report was successfully sent! 59 | Please join the [Calypso Support Server](${Url.SupportServer}) for further discussion. 60 | `, 61 | ) 62 | .setFooter({ 63 | text: member?.displayName ?? user.username, 64 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 65 | }) 66 | .setTimestamp(), 67 | ], 68 | ephemeral: true, 69 | }) 70 | }, 71 | }) 72 | -------------------------------------------------------------------------------- /src/components/selectMenus/help.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, 3 | type ButtonBuilder, 4 | EmbedBuilder, 5 | SelectMenuBuilder, 6 | type SelectMenuComponent, 7 | type SelectMenuInteraction, 8 | } from 'discord.js' 9 | import capitalize from 'lodash/capitalize' 10 | import { CommandType } from 'enums' 11 | import { Component } from '@structures' 12 | import { descriptions } from '@commands/information/help' 13 | 14 | export default new Component({ 15 | customId: 'help', 16 | run: async (client, interaction): Promise => { 17 | const { 18 | message: { embeds, components }, 19 | } = interaction 20 | 21 | const type = interaction.values[0] 22 | 23 | const commands = client.commands.filter((command) => command.type == type) 24 | 25 | const embed = EmbedBuilder.from(embeds[0]) 26 | .setTitle(`**${capitalize(type)} [${commands.size}]**`) 27 | .setFields( 28 | commands.map((command) => { 29 | return { 30 | name: `\`${command.data.name}\``, 31 | value: command.data.description, 32 | inline: true, 33 | } 34 | }), 35 | ) 36 | 37 | const rows = [ 38 | ActionRowBuilder.from(components[0]).setComponents( 39 | SelectMenuBuilder.from( 40 | components[0].components[0] as SelectMenuComponent, 41 | ).setOptions( 42 | Object.entries(CommandType).map(([key, value]) => ({ 43 | label: key, 44 | value, 45 | description: descriptions[value], 46 | default: value === type, 47 | })), 48 | ), 49 | ) as ActionRowBuilder, 50 | ActionRowBuilder.from(components[1]) as ActionRowBuilder, 51 | ] 52 | 53 | await client.update(interaction, { embeds: [embed], components: [...rows] }) 54 | }, 55 | }) 56 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv' 2 | import { getEnvironmentVariable } from 'utils' 3 | import yargs from 'yargs/yargs' 4 | import { hideBin } from 'yargs/helpers' 5 | 6 | const argv = yargs(hideBin(process.argv)) 7 | .option('debug', { 8 | alias: 'd', 9 | type: 'boolean', 10 | default: false, 11 | description: 'Run with debug mode', 12 | }) 13 | .parseSync() 14 | 15 | config() 16 | 17 | export default { 18 | token: getEnvironmentVariable('TOKEN'), 19 | clientId: getEnvironmentVariable('CLIENT_ID'), 20 | guildId: getEnvironmentVariable('GUILD_ID'), 21 | ownerIds: getEnvironmentVariable('OWNER_IDS').split(','), 22 | feedbackChannelId: process.env.FEEDBACK_CHANNEL_ID ?? '', 23 | bugReportChannelId: process.env.BUG_REPORT_CHANNEL_ID ?? '', 24 | debug: argv.debug, 25 | } 26 | -------------------------------------------------------------------------------- /src/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum representing all possible command types. 3 | */ 4 | export enum CommandType { 5 | Information = 'information', 6 | Fun = 'fun', 7 | Animals = 'animals', 8 | Color = 'color', 9 | Miscellaneous = 'miscellaneous', 10 | } 11 | 12 | /** 13 | * List of all possible error types. 14 | */ 15 | export enum ErrorType { 16 | MissingPermissions = 'Missing Permissions', 17 | CommandFailure = 'Command Failure', 18 | } 19 | 20 | /** 21 | * Enum representing all possible emojis. 22 | * 23 | * @remarks 24 | * If cloning this project and self-hosting the bot, 25 | * you MUST replace the IDs of these values with emoji IDs from your own server. 26 | */ 27 | export enum Emoji { 28 | Pong = '<:pong:747295268201824307>', 29 | Fail = '<:fail:736449226120233031>', 30 | Owner = '<:owner:735338114230255616>', 31 | Voice = '<:voice:735665114870710413>', 32 | Online = '<:online:735341197450805279>', 33 | Dnd = '<:dnd:735341494537289768>', 34 | Idle = '<:idle:735341387842584648>', 35 | Offline = '<:offline:735341676121554986>', 36 | DiscordEmployee = '<:DISCORD_EMPLOYEE:735339014621626378>', 37 | DiscordPartner = '<:DISCORD_PARTNER:735339215746760784>', 38 | BugHunterLevel1 = '<:BUGHUNTER_LEVEL_1:735339352913346591>', 39 | BugHunterLevel2 = '<:BUGHUNTER_LEVEL_2:735339420667871293>', 40 | HypeSquadEvents = '<:HYPESQUAD_EVENTS:735339581087547392>', 41 | HouseBravery = '<:HOUSE_BRAVERY:735339756283756655>', 42 | HouseBrilliance = '<:HOUSE_BRILLIANCE:735339675102871642>', 43 | HouseBalance = '<:HOUSE_BALANCE:735339871018942466>', 44 | EarlySupporter = '<:EARLY_SUPPORTER:735340061226172589>', 45 | VerifiedBot = '<:VERIFIED_BOT:735345343037833267>', 46 | VerifiedDeveloper = '<:VERIFIED_DEVELOPER:735340154310361202>', 47 | } 48 | 49 | /** 50 | * Enum representing all color hexes used throughout the codebase. 51 | */ 52 | export enum Color { 53 | Default = '#1C5B4B', 54 | Error = '#FF0000', 55 | } 56 | 57 | /** 58 | * Enum representing all Calypso images used in commands. 59 | */ 60 | export enum Image { 61 | Calypso = 'https://raw.githubusercontent.com/sabattle/CalypsoBot/main/images/Calypso.png', 62 | CalypsoTitle = 'https://raw.githubusercontent.com/sabattle/CalypsoBot/main/images/Calypso_Title.png', 63 | } 64 | 65 | /** 66 | * Enum representing all URLs relating to Calypso. 67 | */ 68 | export enum Url { 69 | Invite = 'https://discord.com/api/oauth2/authorize?client_id=416451977380364288&permissions=1099914374230&scope=applications.commands%20bot', 70 | SupportServer = 'https://discord.gg/9SpsSG5VWh', 71 | GithubRepository = 'https://github.com/sabattle/CalypsoBot', 72 | Donate = 'https://www.paypal.com/paypalme/sebastianabattle', 73 | } 74 | -------------------------------------------------------------------------------- /src/events/debug.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@structures' 2 | import { Events } from 'discord.js' 3 | import logger from 'logger' 4 | 5 | export default new Event(Events.Debug, (message) => { 6 | logger.info(message) 7 | }) 8 | -------------------------------------------------------------------------------- /src/events/error.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@structures' 2 | import { Events } from 'discord.js' 3 | import logger from 'logger' 4 | 5 | export default new Event(Events.Error, (err) => { 6 | logger.error(err) 7 | }) 8 | -------------------------------------------------------------------------------- /src/events/guildCreate.ts: -------------------------------------------------------------------------------- 1 | import prisma from 'prisma' 2 | import { Events } from 'discord.js' 3 | import logger from 'logger' 4 | import { Event } from '@structures' 5 | 6 | export default new Event(Events.GuildCreate, async (client, guild) => { 7 | const { id: guildId, name } = guild 8 | 9 | await prisma.guild.create({ 10 | data: { 11 | guildId, 12 | name, 13 | config: {}, 14 | }, 15 | }) 16 | 17 | logger.info(`Calypso has joined ${name}`) 18 | }) 19 | -------------------------------------------------------------------------------- /src/events/interactionCreate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ButtonInteraction, 3 | type ChatInputCommandInteraction, 4 | Events, 5 | PermissionsBitField, 6 | type SelectMenuInteraction, 7 | } from 'discord.js' 8 | import logger from 'logger' 9 | import { ErrorType } from 'enums' 10 | import startCase from 'lodash/startCase' 11 | import { type Client, type Command, type Component, Event } from '@structures' 12 | 13 | /** 14 | * Utility function to check if the client is missing any necessary permissions. 15 | * 16 | * @param client - The instantiated client 17 | * @param interaction - The interaction that spawned the event 18 | * @param structure - The structure that is being executed 19 | * @returns `true` or `false` 20 | */ 21 | const hasPermission = async ( 22 | client: Client, 23 | interaction: 24 | | ChatInputCommandInteraction 25 | | ButtonInteraction 26 | | SelectMenuInteraction, 27 | structure: 28 | | Command 29 | | Component 30 | | Component, 31 | ): Promise => { 32 | if (!interaction.inCachedGuild()) return true 33 | const permissions: string[] = 34 | interaction.channel 35 | ?.permissionsFor(client.user) 36 | ?.missing(structure.permissions) 37 | .map((p) => startCase(String(new PermissionsBitField(p).toArray()))) ?? [] 38 | if (permissions.length != 0) { 39 | await client.replyWithError( 40 | interaction, 41 | ErrorType.MissingPermissions, 42 | `Sorry ${ 43 | interaction.member 44 | }, I need the following permissions:\n \`\`\`diff\n- ${permissions.join( 45 | '\n- ', 46 | )}\`\`\``, 47 | ) 48 | return false 49 | } 50 | return true 51 | } 52 | 53 | export default new Event( 54 | Events.InteractionCreate, 55 | async (client, interaction) => { 56 | if (!client.isReady()) return 57 | 58 | if (interaction.isChatInputCommand()) { 59 | const command = client.commands.get(interaction.commandName) 60 | 61 | if (!command || !(await hasPermission(client, interaction, command))) 62 | return 63 | 64 | // Run command 65 | try { 66 | await command.run(client, interaction) 67 | } catch (err) { 68 | if (err instanceof Error) logger.error(err.stack) 69 | else logger.error(err) 70 | } 71 | } else if (interaction.isSelectMenu()) { 72 | const selectMenu = client.selectMenus.get(interaction.customId) 73 | 74 | if ( 75 | !selectMenu || 76 | !(await hasPermission(client, interaction, selectMenu)) 77 | ) 78 | return 79 | 80 | // Run select menu 81 | try { 82 | await selectMenu.run(client, interaction) 83 | } catch (err) { 84 | if (err instanceof Error) logger.error(err.stack) 85 | else logger.error(err) 86 | } 87 | } 88 | }, 89 | ) 90 | -------------------------------------------------------------------------------- /src/events/messageCreate.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@structures' 2 | import { EmbedBuilder, Events } from 'discord.js' 3 | import { Color, Image } from 'enums' 4 | 5 | export default new Event(Events.MessageCreate, async (client, message) => { 6 | const { guild, channel, author, content } = message 7 | 8 | if (!client.isReady() || author.bot) return 9 | if ( 10 | content === `<@${client.user.id}>` || 11 | content === `<@!${client.user.id}>` 12 | ) { 13 | const embed = new EmbedBuilder() 14 | .setTitle( 15 | `Hi, I'm ${ 16 | guild?.members.me?.displayName ?? client.user.username 17 | }. Need help?`, 18 | ) 19 | .setThumbnail(Image.Calypso) 20 | .setColor(guild?.members.me?.displayHexColor ?? Color.Default) 21 | .setDescription( 22 | 'You can see everything I can do by using the `/help` command.', 23 | ) 24 | .setFooter({ 25 | text: 'DM Nettles#8880 to speak directly with the developer!', 26 | }) 27 | 28 | await client.send(channel, { embeds: [embed] }) 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/events/ready.ts: -------------------------------------------------------------------------------- 1 | import prisma from 'prisma' 2 | import { type ActivitiesOptions, ActivityType, Events } from 'discord.js' 3 | import logger from 'logger' 4 | import { Event } from '@structures' 5 | 6 | export default new Event(Events.ClientReady, async (client) => { 7 | if (!client.isReady()) return 8 | const { user, guilds } = client 9 | 10 | const activities: ActivitiesOptions[][] = [ 11 | [{ name: 'your commands', type: ActivityType.Listening }], 12 | [{ name: '@Calypso', type: ActivityType.Listening }], 13 | ] 14 | 15 | // Update presence 16 | user.setPresence({ status: 'online', activities: activities[0] }) 17 | 18 | let activity = 1 19 | 20 | // Update activity every 30 seconds 21 | setInterval(() => { 22 | activities[2] = [ 23 | { 24 | name: `${guilds.cache.size} servers`, 25 | type: ActivityType.Watching, 26 | }, 27 | ] // Update server count 28 | if (activity > 2) activity = 0 29 | user.setActivity(activities[activity][0]) 30 | activity++ 31 | }, 30000) 32 | 33 | // Update guilds 34 | logger.info('Updating guilds...') 35 | for (const guild of guilds.cache.values()) { 36 | const { id: guildId, name } = guild 37 | await prisma.guild.upsert({ 38 | where: { 39 | guildId, 40 | }, 41 | update: { 42 | name, 43 | }, 44 | create: { 45 | guildId, 46 | name, 47 | config: {}, 48 | }, 49 | }) 50 | } 51 | 52 | logger.info(`${user.username} is now online`) 53 | logger.info(`${user.username} is running on ${guilds.cache.size} server(s)`) 54 | }) 55 | -------------------------------------------------------------------------------- /src/events/warn.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@structures' 2 | import { Events } from 'discord.js' 3 | import logger from 'logger' 4 | 5 | export default new Event(Events.Warn, (message) => { 6 | logger.warn(message) 7 | }) 8 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston' 2 | 3 | // Instantiate logger 4 | const logger = createLogger({ 5 | transports: [new transports.Console()], 6 | format: format.combine( 7 | format.colorize(), 8 | format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 9 | format.printf(({ timestamp, level, message }) => { 10 | return `[${timestamp as string}] ${level}: ${message as string}` 11 | }), 12 | ), 13 | }) 14 | 15 | export default logger 16 | -------------------------------------------------------------------------------- /src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | export default new PrismaClient() 4 | -------------------------------------------------------------------------------- /src/structures/Client.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import Table from 'cli-table3' 3 | import { 4 | BaseGuildTextChannel, 5 | type ButtonInteraction, 6 | type ChatInputCommandInteraction, 7 | type ClientEvents, 8 | type ClientOptions, 9 | Collection, 10 | Client as DiscordClient, 11 | EmbedBuilder, 12 | type InteractionReplyOptions, 13 | type InteractionResponse, 14 | type InteractionUpdateOptions, 15 | type Message, 16 | type MessageComponentInteraction, 17 | type MessageCreateOptions, 18 | type MessagePayload, 19 | PermissionFlagsBits, 20 | type SelectMenuInteraction, 21 | type Snowflake, 22 | type TextBasedChannel, 23 | type WebhookEditMessageOptions, 24 | } from 'discord.js' 25 | import glob from 'glob' 26 | import logger from 'logger' 27 | import { basename, sep } from 'path' 28 | import { promisify } from 'util' 29 | import { Color, Emoji, type ErrorType } from 'enums' 30 | import type { Event } from '@structures/Event' 31 | import type { Command } from '@structures/Command' 32 | import type { Component } from '@structures/Component' 33 | import { ConfigCache } from '@structures/ConfigCache' 34 | import type { StructureImport } from 'types' 35 | 36 | const glob_ = promisify(glob) 37 | 38 | /** 39 | * Interface of all available options used by the client for its config. 40 | */ 41 | interface ClientConfig { 42 | token: string 43 | ownerIds: Snowflake[] 44 | feedbackChannelId: Snowflake 45 | bugReportChannelId: Snowflake 46 | debug: boolean 47 | } 48 | 49 | const styling: Table.TableConstructorOptions = { 50 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 51 | style: { 52 | head: ['yellow'], 53 | }, 54 | } 55 | 56 | /** 57 | * The Client class provides the structure for the bot itself. 58 | * 59 | * @remarks 60 | * This should only ever be instantiated once. 61 | */ 62 | export class Client< 63 | Ready extends boolean = boolean, 64 | > extends DiscordClient { 65 | /** The client token. */ 66 | readonly #token: string 67 | 68 | /** List of owner IDs. */ 69 | public readonly ownerIds: Snowflake[] 70 | 71 | /** The feedback channel ID. */ 72 | public readonly feedbackChannelId: Snowflake 73 | 74 | /** The bug report channel ID. */ 75 | public readonly bugReportChannelId: Snowflake 76 | 77 | /** Whether or not debug mode is enabled. */ 78 | public readonly debug: boolean 79 | 80 | /** 81 | * Collection of all guild configs mapped by their guild ID. 82 | * 83 | * @defaultValue `new ConfigCache()` 84 | */ 85 | public configs: ConfigCache = new ConfigCache() 86 | 87 | /** 88 | * Collection of all commands mapped by their name. 89 | * 90 | * @defaultValue `new Collection()` 91 | */ 92 | public commands: Collection = new Collection() 93 | 94 | /** 95 | * Collection of all buttons mapped by their custom ID. 96 | * 97 | * @defaultValue `new Collection()` 98 | */ 99 | public buttons: Collection> = 100 | new Collection() 101 | 102 | /** 103 | * Collection of all select menus mapped by their custom ID. 104 | * 105 | * @defaultValue `new Collection()` 106 | */ 107 | public selectMenus: Collection> = 108 | new Collection() 109 | 110 | public constructor( 111 | { 112 | token, 113 | ownerIds, 114 | feedbackChannelId, 115 | bugReportChannelId, 116 | debug, 117 | }: ClientConfig, 118 | options: ClientOptions, 119 | ) { 120 | super(options) 121 | 122 | this.#token = token 123 | this.ownerIds = ownerIds 124 | this.feedbackChannelId = feedbackChannelId 125 | this.bugReportChannelId = bugReportChannelId 126 | this.debug = debug 127 | } 128 | 129 | /** 130 | * Loads all events and registers them to the client. 131 | */ 132 | async #registerEvents(): Promise { 133 | logger.info('Registering events...') 134 | 135 | const files = await glob_( 136 | `${__dirname.split(sep).join('/')}/../events/*{.ts,.js}`, 137 | ) 138 | if (files.length === 0) { 139 | logger.warn('No events found') 140 | return 141 | } 142 | 143 | const table = new Table({ 144 | head: ['File', 'Name', 'Status'], 145 | ...styling, 146 | }) 147 | 148 | let count = 0 149 | 150 | for (const f of files) { 151 | let name = basename(f) 152 | name = name.substring(0, name.lastIndexOf('.')) || name 153 | if (name === 'debug' && !this.debug) continue 154 | 155 | try { 156 | const event = ( 157 | (await import(f)) as StructureImport> 158 | ).default 159 | this.on(event.event, event.run.bind(null, this)) 160 | table.push([f, name, chalk.green('pass')]) 161 | count++ 162 | } catch (err) { 163 | if (err instanceof Error) { 164 | logger.error(`Event failed to register: ${name}`) 165 | logger.error(err.stack) 166 | table.push([f, name, chalk.red('fail')]) 167 | } else logger.error(err) 168 | } 169 | } 170 | 171 | logger.info(`\n${table.toString()}`) 172 | logger.info(`Registered ${count} event(s)`) 173 | } 174 | 175 | /** 176 | * Handles loading commands and mapping them in the commands collection. 177 | */ 178 | async #registerCommands(): Promise { 179 | logger.info('Registering commands...') 180 | 181 | const files = await glob_( 182 | `${__dirname.split(sep).join('/')}/../commands/*/*{.ts,.js}`, 183 | ) 184 | if (files.length === 0) { 185 | logger.warn('No commands found') 186 | return 187 | } 188 | 189 | const table = new Table({ 190 | head: ['File', 'Name', 'Type', 'Status'], 191 | ...styling, 192 | }) 193 | 194 | let count = 0 195 | 196 | for (const f of files) { 197 | let name = basename(f) 198 | name = name.substring(0, name.lastIndexOf('.')) || name 199 | 200 | try { 201 | const command = ((await import(f)) as StructureImport).default 202 | if (command.data.name) { 203 | this.commands.set(command.data.name, command) 204 | table.push([f, name, command.type, chalk.green('pass')]) 205 | count++ 206 | } else throw Error(`Command name not set: ${name}`) 207 | } catch (err) { 208 | if (err instanceof Error) { 209 | logger.error(`Command failed to register: ${name}`) 210 | logger.error(err.stack) 211 | table.push([f, name, '', chalk.red('fail')]) 212 | } else logger.error(err) 213 | } 214 | } 215 | 216 | logger.info(`\n${table.toString()}`) 217 | logger.info(`Registered ${count} command(s)`) 218 | } 219 | 220 | /** 221 | * Handles loading components and mapping them in their respective collection. 222 | */ 223 | async #registerComponents(): Promise { 224 | logger.info('Registering components...') 225 | 226 | const files = await glob_( 227 | `${__dirname.split(sep).join('/')}/../components/*/*{.ts,.js}`, 228 | ) 229 | if (files.length === 0) { 230 | logger.warn('No components found') 231 | return 232 | } 233 | 234 | const table = new Table({ 235 | head: ['File', 'Name', 'Type', 'Status'], 236 | ...styling, 237 | }) 238 | 239 | let count = 0 240 | 241 | for (const f of files) { 242 | let name = basename(f) 243 | name = name.substring(0, name.lastIndexOf('.')) || name 244 | const type = f.split('/').at(-2) as 'buttons' | 'selectMenus' 245 | 246 | try { 247 | const component = ( 248 | (await import(f)) as StructureImport< 249 | Component 250 | > 251 | ).default 252 | const { customId } = component 253 | if (customId) { 254 | this[type].set(customId, component) 255 | table.push([f, name, type, chalk.green('pass')]) 256 | count++ 257 | } else throw Error(`Component custom ID not set: ${name}`) 258 | } catch (err) { 259 | if (err instanceof Error) { 260 | logger.error(`Component failed to register: ${name}`) 261 | logger.error(err.stack) 262 | table.push([f, name, type, chalk.red('fail')]) 263 | } else logger.error(err) 264 | } 265 | } 266 | 267 | logger.info(`\n${table.toString()}`) 268 | logger.info(`Registered ${count} component(s)`) 269 | } 270 | 271 | /** 272 | * Checks if the bot is allowed to respond in a channel. 273 | * 274 | * @param channel - The channel that should be checked 275 | * @returns `true` or `false` 276 | */ 277 | public isAllowed(channel: TextBasedChannel): boolean { 278 | if ( 279 | channel instanceof BaseGuildTextChannel && 280 | (!channel.guild.members.me || 281 | !channel.viewable || 282 | !channel 283 | .permissionsFor(channel.guild.members.me) 284 | .has( 285 | PermissionFlagsBits.ViewChannel | PermissionFlagsBits.SendMessages, 286 | )) 287 | ) 288 | return false 289 | else return true 290 | } 291 | 292 | /** 293 | * Sends a message safely by checking channel permissions before sending the message. 294 | * 295 | * @param channel - The channel to send the message in 296 | * @param options - Options for configuring the message 297 | * @returns The message sent 298 | */ 299 | public async send( 300 | channel: TextBasedChannel, 301 | options: string | MessagePayload | MessageCreateOptions, 302 | ): Promise { 303 | if (!this.isAllowed(channel)) return 304 | return channel.send(options) 305 | } 306 | 307 | /** 308 | * Replies safely by checking channel permissions before sending the response. 309 | * 310 | * @param options - Options for configuring the interaction reply 311 | * @returns The message or interaction response 312 | */ 313 | // Steal the overloads \o/ 314 | public reply( 315 | interaction: 316 | | ChatInputCommandInteraction 317 | | ButtonInteraction 318 | | SelectMenuInteraction, 319 | options: InteractionReplyOptions & { fetchReply: true }, 320 | ): Promise 321 | public reply( 322 | interaction: 323 | | ChatInputCommandInteraction 324 | | ButtonInteraction 325 | | SelectMenuInteraction, 326 | options: string | MessagePayload | InteractionReplyOptions, 327 | ): Promise 328 | public async reply( 329 | interaction: 330 | | ChatInputCommandInteraction 331 | | ButtonInteraction 332 | | SelectMenuInteraction, 333 | options: string | MessagePayload | InteractionReplyOptions, 334 | ): Promise { 335 | const { channel } = interaction 336 | if (interaction.inCachedGuild() && channel && !this.isAllowed(channel)) 337 | return 338 | return interaction.reply(options) 339 | } 340 | 341 | /** 342 | * Edits the reply safely by checking channel permissions before editing. 343 | * 344 | * @param options - Options for configuring the interaction edit 345 | * @returns The edited message 346 | */ 347 | public async editReply( 348 | interaction: 349 | | ChatInputCommandInteraction 350 | | ButtonInteraction 351 | | SelectMenuInteraction, 352 | options: string | MessagePayload | WebhookEditMessageOptions, 353 | ): Promise { 354 | const { channel } = interaction 355 | if (interaction.inCachedGuild() && channel && !this.isAllowed(channel)) 356 | return 357 | return interaction.editReply(options) 358 | } 359 | 360 | /** 361 | * Updates the interaction safely by checking channel permissions before updating. 362 | * 363 | * @param options - Options for configuring the interaction update 364 | * @returns The updated message or interaction response 365 | */ 366 | // Steal the overloads again \o/ \o/ 367 | public update( 368 | interaction: MessageComponentInteraction, 369 | options: InteractionUpdateOptions & { fetchReply: true }, 370 | ): Promise 371 | public update( 372 | interaction: MessageComponentInteraction, 373 | options: string | MessagePayload | InteractionUpdateOptions, 374 | ): Promise 375 | public async update( 376 | interaction: MessageComponentInteraction, 377 | options: string | MessagePayload | InteractionUpdateOptions, 378 | ): Promise { 379 | const { channel } = interaction 380 | if (interaction.inCachedGuild() && channel && !this.isAllowed(channel)) 381 | return 382 | return interaction.update(options) 383 | } 384 | 385 | /** 386 | * Helper function to provide a standardized way of responding to the user with an error message. 387 | * 388 | * @param type - The type of error 389 | * @param message - The error message to be sent to the user 390 | */ 391 | public async replyWithError( 392 | interaction: 393 | | ChatInputCommandInteraction 394 | | ButtonInteraction 395 | | SelectMenuInteraction, 396 | type: ErrorType, 397 | message: string, 398 | ): Promise { 399 | if (!this.isReady()) return 400 | const { user } = interaction 401 | const member = interaction.inCachedGuild() ? interaction.member : null 402 | await this.reply(interaction, { 403 | embeds: [ 404 | new EmbedBuilder() 405 | .setAuthor({ 406 | name: this.user.tag, 407 | iconURL: this.user.displayAvatarURL(), 408 | }) 409 | .setTitle(`${Emoji.Fail} Error: \`${type}\``) 410 | .setDescription(message) 411 | .setFooter({ 412 | text: member?.displayName ?? user.username, 413 | iconURL: member?.displayAvatarURL() ?? user.displayAvatarURL(), 414 | }) 415 | .setColor(Color.Error) 416 | .setTimestamp(), 417 | ], 418 | ephemeral: true, 419 | }) 420 | } 421 | 422 | /** 423 | * Initializes the client. 424 | */ 425 | public async init(): Promise { 426 | await this.#registerEvents() 427 | await this.#registerCommands() 428 | await this.#registerComponents() 429 | await this.login(this.#token) 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/structures/Command.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ChatInputCommandInteraction, 3 | type GuildMember, 4 | PermissionsBitField, 5 | type SlashCommandBuilder, 6 | type User, 7 | } from 'discord.js' 8 | import { CommandType } from 'enums' 9 | import type { Permissions, RunFunction } from 'types' 10 | 11 | /** 12 | * Type definition of a slash command. 13 | */ 14 | type SlashCommand = 15 | | SlashCommandBuilder 16 | | Omit 17 | 18 | /** 19 | * Interface of all available options used for command creation. 20 | */ 21 | interface CommandOptions { 22 | data: SlashCommand 23 | type?: CommandType 24 | permissions?: Permissions 25 | run: RunFunction 26 | } 27 | 28 | /** 29 | * The Command class provides the structure for all bot commands. 30 | */ 31 | export class Command { 32 | /** Data representing a slash command which will be sent to the Discord API. */ 33 | public readonly data: SlashCommand 34 | 35 | /** 36 | * The command type. 37 | * 38 | * @defaultValue `CommandType.Miscellaneous` 39 | */ 40 | public readonly type: CommandType 41 | 42 | /** 43 | * List of client permissions needed to run the command. 44 | * 45 | * @defaultValue `[PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.SendMessages]` 46 | */ 47 | public readonly permissions: Permissions 48 | 49 | /** Handles all logic relating to command execution. */ 50 | public run: RunFunction 51 | 52 | public constructor({ 53 | data, 54 | type = CommandType.Miscellaneous, 55 | permissions = [ 56 | PermissionsBitField.Flags.SendMessages, 57 | PermissionsBitField.Flags.ViewChannel, 58 | ], 59 | run, 60 | }: CommandOptions) { 61 | this.data = data 62 | this.type = type 63 | this.permissions = permissions 64 | this.run = run 65 | } 66 | 67 | /** 68 | * Determines the member the command is targeting. 69 | * If no user was given as a command argument, then the original user becomes the target. 70 | * 71 | * @remarks 72 | * `targetMember` should be used anywhere requiring the interaction option user. 73 | * `member` references the original user who created the interaction. 74 | * 75 | * @param interaction - The interaction that spawned the command 76 | * @returns An object containing the target member and original member 77 | */ 78 | public static getMember(interaction: ChatInputCommandInteraction<'cached'>): { 79 | targetMember: GuildMember 80 | member: GuildMember 81 | } 82 | public static getMember(interaction: ChatInputCommandInteraction): { 83 | targetMember: GuildMember | null 84 | member: GuildMember | null 85 | } 86 | public static getMember(interaction: ChatInputCommandInteraction): { 87 | targetMember: GuildMember | null 88 | member: GuildMember | null 89 | } { 90 | if (!interaction.inCachedGuild()) 91 | return { targetMember: null, member: null } 92 | const { member, options } = interaction 93 | const targetMember = options.getMember('user') ?? member 94 | return { targetMember, member } 95 | } 96 | 97 | /** 98 | * Determines the user or member the command is targeting. 99 | * If no user was given as a command argument, then the original user becomes the target. 100 | * 101 | * @remarks 102 | * `targetMember` and `targetUser` should be used anywhere requiring the interaction option user. 103 | * `member` and `user` reference the original user who created the interaction. 104 | * 105 | * @param interaction - The interaction that spawned the command 106 | * @returns An object containing the target member, original member, target user, and original user 107 | */ 108 | public static getMemberAndUser(interaction: ChatInputCommandInteraction): { 109 | targetMember: GuildMember | null 110 | member: GuildMember | null 111 | targetUser: User 112 | user: User 113 | } { 114 | const { user, options } = interaction 115 | const targetUser = options.getUser('user') ?? user 116 | const { targetMember, member } = this.getMember(interaction) 117 | return { targetMember, member, targetUser, user } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/structures/Component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type MessageComponentInteraction, 3 | PermissionsBitField, 4 | } from 'discord.js' 5 | import type { Permissions, RunFunction } from 'types' 6 | 7 | /** 8 | * Interface of all available options used for component creation. 9 | */ 10 | export interface ComponentOptions< 11 | TInteraction extends MessageComponentInteraction, 12 | > { 13 | customId: string 14 | permissions?: Permissions 15 | run: RunFunction 16 | } 17 | 18 | /** 19 | * The generic Component class provides the structure for all components. 20 | */ 21 | export class Component { 22 | public readonly customId: string 23 | 24 | /** 25 | * List of client permissions needed to use the component. 26 | * 27 | * @defaultValue `[PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.SendMessages]` 28 | */ 29 | public readonly permissions: Permissions 30 | 31 | /** Handles all logic relating to component execution. */ 32 | public run: RunFunction 33 | 34 | public constructor({ 35 | customId, 36 | permissions = [ 37 | PermissionsBitField.Flags.SendMessages, 38 | PermissionsBitField.Flags.ViewChannel, 39 | ], 40 | run, 41 | }: ComponentOptions) { 42 | this.customId = customId 43 | this.permissions = permissions 44 | this.run = run 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/structures/ConfigCache.ts: -------------------------------------------------------------------------------- 1 | import { Collection, type Snowflake } from 'discord.js' 2 | import type { Config } from '@prisma/client' 3 | import prisma from 'prisma' 4 | 5 | export class ConfigCache extends Collection { 6 | /** 7 | * Gets a cached guild config or fetches it from the database if not present. 8 | * 9 | * @param guildId - The ID of the guild to get or fetch 10 | * @returns The cached config 11 | */ 12 | public async fetch(guildId: Snowflake): Promise { 13 | if (!this.has(guildId)) { 14 | super.set( 15 | guildId, 16 | (await prisma.guild.findUnique({ where: { guildId } }))?.config, 17 | ) 18 | } 19 | return super.get(guildId) 20 | } 21 | 22 | /** 23 | * Updates a cached guild config field with the given value. 24 | * 25 | * @param guildId - The ID of the guild to update 26 | * @param field - The field of the guild's config to update 27 | * @param value - The new value of the field 28 | */ 29 | public async update( 30 | guildId: Snowflake, 31 | field: keyof Config, 32 | value: Config[K], 33 | ): Promise { 34 | const config = await this.fetch(guildId) 35 | if (!config) 36 | throw new Error( 37 | `Unable to find guild in cache or database with guild ID: ${guildId}`, 38 | ) 39 | config[field] = value 40 | await prisma.guild.update({ 41 | where: { guildId }, 42 | data: { config: { [field]: value } }, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/structures/Event.ts: -------------------------------------------------------------------------------- 1 | import type { ClientEvents } from 'discord.js' 2 | import type { Client } from '@structures/Client' 3 | 4 | /** 5 | * Generic Event class which provides the structure for all events. 6 | * 7 | * @typeParam K - Key which must be one of the following event types: {@link https://discord.js.org/#/docs/discord.js/main/typedef/Events} 8 | */ 9 | export class Event { 10 | public constructor( 11 | /** The event type */ 12 | public event: K, 13 | 14 | /** 15 | * Handles all logic relating to event execution. 16 | * 17 | * @param client - The client to bind to the event 18 | * @param args - List of arguments for the event 19 | */ 20 | public run: ( 21 | client: Client, 22 | ...args: ClientEvents[K] 23 | ) => Promise | void, 24 | ) {} 25 | } 26 | -------------------------------------------------------------------------------- /src/structures/PaginatedEmbed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, 3 | ButtonBuilder, 4 | type ButtonInteraction, 5 | ButtonStyle, 6 | type ChatInputCommandInteraction, 7 | ComponentType, 8 | type EmbedBuilder, 9 | type InteractionCollector, 10 | type User, 11 | } from 'discord.js' 12 | import type { Client } from '@structures' 13 | 14 | enum Button { 15 | Prev = 'prev', 16 | Next = 'next', 17 | } 18 | 19 | /** 20 | * Interface of all available options used for paginated embed creation. 21 | */ 22 | interface PaginatedEmbedOptions { 23 | client: Client 24 | interaction: ChatInputCommandInteraction 25 | pages: EmbedBuilder[] 26 | time?: number 27 | } 28 | 29 | /** 30 | * The PaginatedEmbed class provides the structure for all paginated embeds with button menus. 31 | */ 32 | export class PaginatedEmbed { 33 | private readonly client: Client 34 | 35 | private readonly interaction: ChatInputCommandInteraction 36 | 37 | public readonly user: User 38 | 39 | public pages: EmbedBuilder[] 40 | 41 | public readonly time: number 42 | 43 | #collector?: InteractionCollector 44 | 45 | public constructor({ 46 | client, 47 | interaction, 48 | pages, 49 | time, 50 | }: PaginatedEmbedOptions) { 51 | this.client = client 52 | this.interaction = interaction 53 | this.user = interaction.user 54 | this.pages = pages 55 | this.time = time ?? 60000 56 | } 57 | 58 | public async run(): Promise { 59 | const { client, interaction, user, pages, time } = this 60 | let index = 0 61 | 62 | const prev = new ButtonBuilder() 63 | .setCustomId(Button.Prev) 64 | .setStyle(ButtonStyle.Primary) 65 | .setDisabled(index == 0) 66 | .setEmoji({ name: '◀️' }) 67 | const next = new ButtonBuilder() 68 | .setCustomId(Button.Next) 69 | .setStyle(ButtonStyle.Primary) 70 | .setEmoji({ name: '▶️' }) 71 | const row = new ActionRowBuilder().setComponents(prev, next) 72 | 73 | const message = await client.reply(interaction, { 74 | embeds: [pages[index]], 75 | components: [row], 76 | fetchReply: true, 77 | }) 78 | 79 | this.#collector = message.createMessageComponentCollector({ 80 | componentType: ComponentType.Button, 81 | time, 82 | }) 83 | 84 | this.#collector.on('collect', async (i) => { 85 | if (i.user.id != user.id) return 86 | const { customId } = i 87 | switch (customId) { 88 | case Button.Prev: { 89 | index-- 90 | break 91 | } 92 | case Button.Next: { 93 | index++ 94 | break 95 | } 96 | } 97 | 98 | await client.update(i, { 99 | embeds: [pages[index]], 100 | components: [ 101 | row.setComponents( 102 | prev.setDisabled(index == 0), 103 | next.setDisabled(index == pages.length - 1), 104 | ), 105 | ], 106 | }) 107 | }) 108 | 109 | this.#collector.on('end', async () => { 110 | await message.edit({ 111 | embeds: [pages[index]], 112 | components: [], 113 | }) 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/structures/index.ts: -------------------------------------------------------------------------------- 1 | export { Client } from '@structures/Client' 2 | export { Event } from '@structures/Event' 3 | export { ConfigCache } from '@structures/ConfigCache' 4 | export { Command } from '@structures/Command' 5 | export { Component } from '@structures/Component' 6 | export { PaginatedEmbed } from '@structures/PaginatedEmbed' 7 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BaseInteraction, 3 | ClientEvents, 4 | MessageComponentInteraction, 5 | } from 'discord.js' 6 | import type { Client, Command, Component, Event } from '@structures' 7 | 8 | /** 9 | * Generic interface representing a structure import. 10 | */ 11 | export interface StructureImport< 12 | TStructure extends 13 | | Event 14 | | Command 15 | | Component, 16 | > { 17 | default: TStructure 18 | } 19 | 20 | /** 21 | * Generic definition of a structure's run function. 22 | * 23 | * @param Client - The instantiated client 24 | * @param interaction - The interaction attached to the structure 25 | */ 26 | export type RunFunction = ( 27 | client: Client, 28 | interaction: TInteraction, 29 | ) => Promise | void 30 | 31 | /** 32 | * Type definition of a list of permissions. 33 | */ 34 | export type Permissions = bigint[] 35 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { type GuildMember, PermissionFlagsBits, type Role } from 'discord.js' 2 | import startCase from 'lodash/startCase' 3 | 4 | /** 5 | * Ensures an environment variable exists or throws an error. 6 | * 7 | * @remarks 8 | * Provides a type safe way to load environment variables. 9 | * Should only be used when creating the bot config. 10 | * 11 | * @param unvalidatedEnvironmentVariable - The initial environment variable before it has been type-checked 12 | * @returns Validated environment variable 13 | */ 14 | const getEnvironmentVariable = ( 15 | unvalidatedEnvironmentVariable: string, 16 | ): string => { 17 | const environmentVariable = process.env[unvalidatedEnvironmentVariable] 18 | if (!environmentVariable) { 19 | throw new Error( 20 | `Environment variable not set: ${unvalidatedEnvironmentVariable}`, 21 | ) 22 | } else { 23 | return environmentVariable 24 | } 25 | } 26 | 27 | /** 28 | * Gets a list of all permissions of the target and marks them as enabled or disabled. 29 | * 30 | * @remarks 31 | * This is specifically designed to be used with the `diff` syntax highlighting. 32 | * 33 | * @param target - The member or role to get permissions of 34 | * @returns A list of all permissions 35 | */ 36 | const getPermissions = (target: GuildMember | Role): string[] => { 37 | const rolePermissions = target.permissions.toArray() as string[] 38 | const allPermissions = Object.keys(PermissionFlagsBits) 39 | const permissions = [] 40 | for (const permission of allPermissions) { 41 | if (rolePermissions.includes(permission)) 42 | permissions.push(`+ ${startCase(permission)}`) 43 | else permissions.push(`- ${startCase(permission)}`) 44 | } 45 | return permissions 46 | } 47 | 48 | export { getEnvironmentVariable, getPermissions } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "require": ["tsconfig-paths/register"] 4 | }, 5 | "compilerOptions": { 6 | "target": "ESNext", 7 | "module": "CommonJS", 8 | "lib": ["ESNext"], 9 | "sourceMap": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "importHelpers": true, 19 | "baseUrl": "src", 20 | "paths": { 21 | "@structures": ["structures"], 22 | "@structures/*": ["structures/*"], 23 | "@commands/*": ["commands/*"] 24 | }, 25 | "noImplicitAny": true, 26 | "resolveJsonModule": true 27 | }, 28 | "include": ["deploy.ts", "src"], 29 | "exclude": ["dist", "node_modules"] 30 | } 31 | --------------------------------------------------------------------------------