├── .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 |
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 |
--------------------------------------------------------------------------------