mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-01 20:46:08 +00:00
refactor: node goes brr 🐙 + turborepo support
This commit is contained in:
parent
0b5176dace
commit
c30d10618e
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
quote_type = single
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
build
|
||||
dist
|
||||
examples/**
|
175
.eslintrc.js
Normal file
175
.eslintrc.js
Normal file
@ -0,0 +1,175 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2020: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'prettier',
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'coverage',
|
||||
'**/*.js',
|
||||
'**/*.d.ts',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
global: {
|
||||
NodeJS: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/no-duplicate-imports': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{ selector: 'default', format: null },
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
|
||||
},
|
||||
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||
],
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/consistent-type-assertions': 'error',
|
||||
'@typescript-eslint/no-array-constructor': 'error',
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
'@typescript-eslint/no-inferrable-types': 'error',
|
||||
'@typescript-eslint/no-misused-new': 'error',
|
||||
'@typescript-eslint/no-namespace': 'error',
|
||||
'@typescript-eslint/no-this-alias': 'error',
|
||||
'@typescript-eslint/no-use-before-define': 'error',
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'@typescript-eslint/triple-slash-reference': 'error',
|
||||
'@typescript-eslint/type-annotation-spacing': 'error',
|
||||
'@typescript-eslint/array-type': 'error',
|
||||
'@typescript-eslint/no-unnecessary-qualifier': 'error',
|
||||
'@typescript-eslint/no-unnecessary-type-arguments': 'off', // disabled as it started to be buggy
|
||||
'@typescript-eslint/quotes': [
|
||||
'error',
|
||||
'single',
|
||||
{ avoidEscape: true, allowTemplateLiterals: true },
|
||||
],
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
'@typescript-eslint/no-redeclare': ['error'],
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: { delimiter: 'semi', requireLast: true },
|
||||
singleline: { delimiter: 'semi', requireLast: false },
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/space-before-function-paren': [
|
||||
'error',
|
||||
{
|
||||
anonymous: 'always',
|
||||
named: 'never',
|
||||
asyncArrow: 'always',
|
||||
},
|
||||
],
|
||||
'arrow-parens': ['error', 'as-needed'],
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
'prefer-rest-params': 'error',
|
||||
'prefer-spread': 'error',
|
||||
'constructor-super': 'error',
|
||||
'for-direction': 'error',
|
||||
'getter-return': 'error',
|
||||
'no-async-promise-executor': 'error',
|
||||
'no-case-declarations': 'error',
|
||||
'no-class-assign': 'error',
|
||||
'no-compare-neg-zero': 'error',
|
||||
'no-cond-assign': 'error',
|
||||
'no-const-assign': 'error',
|
||||
'no-constant-condition': 'error',
|
||||
'no-control-regex': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-delete-var': 'error',
|
||||
'no-dupe-args': 'error',
|
||||
'no-dupe-keys': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-empty': 'error',
|
||||
'no-empty-character-class': 'error',
|
||||
'no-empty-pattern': 'error',
|
||||
'no-ex-assign': 'error',
|
||||
'no-extra-boolean-cast': 'error',
|
||||
'no-extra-semi': 'error',
|
||||
'no-fallthrough': 'error',
|
||||
'no-func-assign': 'error',
|
||||
'no-global-assign': 'error',
|
||||
'no-inner-declarations': 'error',
|
||||
'no-invalid-regexp': 'error',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-misleading-character-class': 'error',
|
||||
'no-mixed-spaces-and-tabs': 'error',
|
||||
'no-new-symbol': 'error',
|
||||
'no-obj-calls': 'error',
|
||||
'no-octal': 'error',
|
||||
'no-prototype-builtins': 'error',
|
||||
'no-redeclare': 'off',
|
||||
'no-regex-spaces': 'error',
|
||||
'no-self-assign': 'error',
|
||||
'no-shadow-restricted-names': 'error',
|
||||
'no-sparse-arrays': 'error',
|
||||
'no-this-before-super': 'error',
|
||||
'no-undef': 'error',
|
||||
'no-unexpected-multiline': 'error',
|
||||
'no-unreachable': 'error',
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unsafe-negation': 'error',
|
||||
'no-unused-labels': 'error',
|
||||
'no-useless-catch': 'error',
|
||||
'no-useless-escape': 'error',
|
||||
'no-with': 'error',
|
||||
'require-yield': 'error',
|
||||
'use-isnan': 'error',
|
||||
'valid-typeof': 'error',
|
||||
// 'comma-dangle': ['error', 'never'], // always-multiline
|
||||
'dot-notation': 'error',
|
||||
'eol-last': 'error',
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||
'no-console': 'error',
|
||||
'no-duplicate-imports': 'off',
|
||||
'no-multiple-empty-lines': 'error',
|
||||
'no-throw-literal': 'error',
|
||||
'no-trailing-spaces': 'error',
|
||||
'no-undef-init': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'quote-props': ['error', 'consistent-as-needed'],
|
||||
'spaced-comment': 'error',
|
||||
yoda: 'error',
|
||||
curly: 'error',
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'lines-between-class-members': [
|
||||
'error',
|
||||
'always',
|
||||
{ exceptAfterSingleLine: true },
|
||||
],
|
||||
'padded-blocks': ['error', { classes: 'never' }], // always
|
||||
'no-else-return': 'error',
|
||||
'block-spacing': ['error', 'always'],
|
||||
'space-before-blocks': ['error', 'always'],
|
||||
'brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
'keyword-spacing': ['error', { before: true, after: true }],
|
||||
'space-in-parens': ['error', 'never'],
|
||||
},
|
||||
settings: {},
|
||||
};
|
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Bugs
|
||||
title: ''
|
||||
labels: Bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug** A clear and concise description of what the bug is.
|
||||
|
||||
**Biscuit Version** eg: 0.1.0-rc7
|
||||
|
||||
**To Reproduce** Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior** A clear and concise description of what you expected to happen.
|
||||
|
||||
**etc** Whatever
|
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,18 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggestions
|
||||
title: ''
|
||||
labels: Feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem
|
||||
is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like** A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered** A clear and concise description of any alternative solutions or features
|
||||
you've considered.
|
||||
|
||||
**Limitations** A set of limitations of the API or the library by itself
|
60
.gitignore
vendored
60
.gitignore
vendored
@ -1,18 +1,50 @@
|
||||
# editors
|
||||
.vim/
|
||||
.vscode/
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# build
|
||||
npm/
|
||||
|
||||
# etc
|
||||
# Enviorment
|
||||
.env
|
||||
|
||||
# Examples
|
||||
node_modules/
|
||||
package-lock.json
|
||||
package.json
|
||||
bun.lockb
|
||||
# NPM
|
||||
npm/
|
||||
|
||||
# Docs
|
||||
docs.json
|
||||
# DOCS
|
||||
docs.json
|
||||
packages/core/docs.json
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# node
|
||||
out/
|
||||
dist/
|
||||
build
|
||||
package-lock.json
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
*.vs
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
|
||||
# tests
|
||||
__tests__
|
||||
__test__
|
4
.prettierrc.js
Normal file
4
.prettierrc.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
singleQuote: true,
|
||||
};
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": false,
|
||||
"editor.detectIndentation": true
|
||||
}
|
0
CODE_OF_CONDUCT.md
Normal file
0
CODE_OF_CONDUCT.md
Normal file
0
CONTRIBUTING.md
Normal file
0
CONTRIBUTING.md
Normal file
201
LICENSE
201
LICENSE
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
92
README.md
92
README.md
@ -1,92 +0,0 @@
|
||||
# biscuit
|
||||
|
||||
## A brand new bleeding edge non bloated Discord library
|
||||
|
||||
[](https://nest.land/package/biscuit)
|
||||
[](https://www.npmjs.com/package/@oasisjs/biscuit)
|
||||
[](https://www.npmjs.com/package/@oasisjs/biscuit)
|
||||
[](https://deno.land/x/biscuit)
|
||||
|
||||
<img align="right" src="https://raw.githubusercontent.com/oasisjs/biscuit/main/assets/biscuit.svg" alt="biscuit"/>
|
||||
|
||||
### Install (for [node18](https://nodejs.org/en/download/))
|
||||
|
||||
```sh-session
|
||||
npm install @oasisjs/biscuit
|
||||
pnpm add @oasisjs/biscuit
|
||||
yarn add @oasisjs/biscuit
|
||||
```
|
||||
|
||||
get a quick bot: `deno run --allow-net https://crux.land/2CENgN [token]`
|
||||
|
||||
The biscuit Discord library is built ontop of Discordeno and webspec APIs, we aim to provide portability. Join our
|
||||
[Discord](https://discord.gg/zmuvzzEFz2)
|
||||
|
||||
### Most importantly, biscuit is:
|
||||
|
||||
- A modular [Discordeno](https://github.com/discordeno/discordeno) fork
|
||||
- A framework to build Discord bots
|
||||
- A bleeding edge API to contact Discord
|
||||
|
||||
Biscuit is primarily inspired by Discord.js and Discordeno but it does not include a cache layer by default, we believe
|
||||
that you should not make software that does things it is not supposed to do.
|
||||
|
||||
### Why biscuit?:
|
||||
|
||||
- [Minimal](https://en.wikipedia.org/wiki/Unix_philosophy), non feature-rich!
|
||||
- Crossplatform
|
||||
- Consistent
|
||||
- Performant
|
||||
- Small bundles
|
||||
|
||||
### Example bot (TS/JS)
|
||||
|
||||
```js
|
||||
import Biscuit, { GatewayIntents } from '@oasisjs/biscuit';
|
||||
|
||||
const intents = GatewayIntents.MessageContent | GatewayIntents.Guilds | GatewayIntents.GuildMessages;
|
||||
const session = new Biscuit({ token: 'your token', intents });
|
||||
|
||||
session.on('ready', ({ user }) => {
|
||||
console.log('Logged in as:', user.username);
|
||||
});
|
||||
|
||||
session.on('messageCreate', (message) => {
|
||||
if (message.content.startsWith('!ping')) {
|
||||
message.reply({ content: 'pong!' });
|
||||
}
|
||||
});
|
||||
|
||||
session.start();
|
||||
```
|
||||
|
||||
### Minimal style guide
|
||||
|
||||
- 4 spaces, no tabs
|
||||
- Semi-colons are mandatory
|
||||
- Run `deno fmt`
|
||||
- Avoid circular dependencies
|
||||
|
||||
### Contrib guide
|
||||
|
||||
- Install Deno extension [here](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno)
|
||||
- Run `deno check` to make sure the library works
|
||||
- Avoid sharing state between classes
|
||||
|
||||
### Compatibility (bun)
|
||||
|
||||
**⚠️ DISCLAIMER:** since bun is unstable I highly recommend running biscuit on node!
|
||||
|
||||
- We got the library running on EndeavourOS but it spams the ready event multiple times
|
||||
- We got the library running on Arch/Artix Linux but breaks when sending fetch requests
|
||||
- We got the library running on WSL (Ubuntu) without any trouble
|
||||
|
||||
> if you really want to use the library with bun remember to clone the repo instead of installing it via the registry
|
||||
|
||||
### Known issues:
|
||||
|
||||
- some properties may be not implemented yet
|
||||
- some structures are not implemented (see https://github.com/oasisjs/biscuit/issues)
|
||||
- cache (wip)
|
||||
- no optimal way to create embeds, should be fixed in builders tho
|
||||
- no optimal way to deliver a webspec bun version to the registry (#50)
|
0
SECURITY.md
Normal file
0
SECURITY.md
Normal file
0
examples/.env.example
Normal file
0
examples/.env.example
Normal file
6
examples/package.json
Normal file
6
examples/package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.1"
|
||||
}
|
||||
}
|
69
examples/src/index.ts
Normal file
69
examples/src/index.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import './utils/experimental.util';
|
||||
import 'dotenv/config';
|
||||
|
||||
import { colors } from './utils/colors.util';
|
||||
|
||||
import { DefaultRestAdapter } from '@biscuitland/rest';
|
||||
import { DefaultWsAdapter } from '@biscuitland/ws';
|
||||
|
||||
import { GatewayIntents } from '@biscuitland/api-types';
|
||||
import { Biscuit } from '@biscuitland/core';
|
||||
|
||||
import { ReadyEvent } from './operations';
|
||||
import { MeRest } from './operations';
|
||||
|
||||
import { AgentWs } from './operations';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
const boostrap = async () => {
|
||||
const biscuit = new Biscuit({
|
||||
intents: GatewayIntents.Guilds,
|
||||
token: process.env.AUTH!,
|
||||
});
|
||||
|
||||
await biscuit.start();
|
||||
await operations(biscuit);
|
||||
};
|
||||
|
||||
const operations = async (biscuit: Biscuit) => {
|
||||
switch (argv[0]) {
|
||||
case '--events':
|
||||
console.log(colors.cyan('Starting examples of events'));
|
||||
|
||||
switch (argv[1]) {
|
||||
case 'ready':
|
||||
new ReadyEvent(biscuit.events);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '--rest':
|
||||
console.log(colors.cyan('Starting examples of rest'));
|
||||
|
||||
switch (argv[1]) {
|
||||
case 'me':
|
||||
new MeRest(biscuit.rest as DefaultRestAdapter);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '--ws':
|
||||
console.log(colors.cyan('Starting examples of ws'));
|
||||
|
||||
switch (argv[1]) {
|
||||
case 'agent':
|
||||
new AgentWs(biscuit.ws as DefaultWsAdapter);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
boostrap();
|
17
examples/src/operations/events/ready.ws.ts
Normal file
17
examples/src/operations/events/ready.ws.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { EventAdapter } from '@biscuitland/core';
|
||||
|
||||
export class ReadyEvent {
|
||||
events: EventAdapter;
|
||||
|
||||
constructor(events: EventAdapter) {
|
||||
this.events = events;
|
||||
|
||||
if (events) {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
async execute() {
|
||||
this.events.on('ready', () => console.log('[1/1] successful'));
|
||||
}
|
||||
}
|
8
examples/src/operations/index.ts
Normal file
8
examples/src/operations/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/** events */
|
||||
export { ReadyEvent } from './events/ready.ws';
|
||||
|
||||
/** rest */
|
||||
export { MeRest } from './rest/me.rest';
|
||||
|
||||
/** ws */
|
||||
export { AgentWs } from './ws/agent.ws';
|
22
examples/src/operations/rest/me.rest.ts
Normal file
22
examples/src/operations/rest/me.rest.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { DiscordUser } from '@biscuitland/api-types';
|
||||
import { DefaultRestAdapter } from '@biscuitland/rest';
|
||||
|
||||
export class MeRest {
|
||||
rest: DefaultRestAdapter;
|
||||
|
||||
constructor(rest: DefaultRestAdapter) {
|
||||
this.rest = rest;
|
||||
|
||||
if (rest) {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const { username } = await this.rest.get<DiscordUser>('/users/@me');
|
||||
|
||||
if (username) {
|
||||
console.log('[1/1] successful [%s]', username);
|
||||
}
|
||||
}
|
||||
}
|
23
examples/src/operations/ws/agent.ws.ts
Normal file
23
examples/src/operations/ws/agent.ws.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { DefaultWsAdapter } from '@biscuitland/ws';
|
||||
|
||||
export class AgentWs {
|
||||
ws: DefaultWsAdapter;
|
||||
|
||||
constructor(ws: DefaultWsAdapter) {
|
||||
this.ws = ws;
|
||||
|
||||
if (ws) {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const shard = this.ws.agent.shards.get(0);
|
||||
|
||||
if (shard && shard.socket) {
|
||||
shard.socket.onmessage = (_message: any) => {
|
||||
// operations
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
7
examples/src/utils/colors.util.ts
Normal file
7
examples/src/utils/colors.util.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const wrap = (fn: (text: string) => string) => (text: string) => fn(text);
|
||||
|
||||
export const colors = {
|
||||
yellow: wrap((text: string) => `\x1b[33m${text}\x1B[39m`),
|
||||
white: wrap((text: string) => `\x1b[37m${text}\x1B[39m`),
|
||||
cyan: wrap((text: string) => `\x1b[36m${text}\x1B[39m`),
|
||||
};
|
15
examples/src/utils/experimental.util.ts
Normal file
15
examples/src/utils/experimental.util.ts
Normal file
@ -0,0 +1,15 @@
|
||||
const originalEmit = process.emit;
|
||||
|
||||
// @ts-ignore
|
||||
process.emit = function (name, data: any, ..._args: any[]) {
|
||||
if (
|
||||
name === `warning` &&
|
||||
typeof data === `object` &&
|
||||
data.name === `ExperimentalWarning`
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return originalEmit.apply(process, arguments);
|
||||
};
|
31
examples/tsconfig.json
Normal file
31
examples/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"lib": ["es2020"],
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"sourceMap": false,
|
||||
"strict": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"preserveConstEnums": true,
|
||||
|
||||
"outDir": "dist",
|
||||
|
||||
/* Type Checking */
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["**/node_modules", "**/dist"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
3906
package-lock.json
generated
Normal file
3906
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "biscuit",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"clean": "turbo run clean",
|
||||
"lint": "turbo run lint",
|
||||
"dev": "turbo run dev --parallel"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=7.0.0",
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||
"@typescript-eslint/parser": "^5.30.7",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"turbo": "^1.3.4",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"packageManager": "npm@8.14.0"
|
||||
}
|
18
packages/api-types/package.json
Normal file
18
packages/api-types/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@biscuitland/api-types",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist && rm -rf .turbo",
|
||||
"dev": "tsup --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.1.3"
|
||||
}
|
||||
}
|
1271
packages/api-types/src/common.ts
Normal file
1271
packages/api-types/src/common.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
packages/api-types/src/index.ts
Normal file
5
packages/api-types/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * as Constants from './utils/constants';
|
||||
export * from './utils/routes';
|
||||
|
||||
export * from './v10/index';
|
||||
export * from './common';
|
29
packages/api-types/src/utils/cdn.ts
Normal file
29
packages/api-types/src/utils/cdn.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { Snowflake } from '../common';
|
||||
import { baseEndpoints as Endpoints } from './constants';
|
||||
|
||||
export function USER_AVATAR(userId: Snowflake, icon: string): string {
|
||||
return `${Endpoints.CDN_URL}/avatars/${userId}/${icon}`;
|
||||
}
|
||||
|
||||
export function EMOJI_URL(id: Snowflake, animated = false): string {
|
||||
return `https://cdn.discordapp.com/emojis/${id}.${animated ? 'gif' : 'png'}`;
|
||||
}
|
||||
|
||||
export function USER_DEFAULT_AVATAR(
|
||||
/** user discriminator */
|
||||
altIcon: number,
|
||||
): string {
|
||||
return `${Endpoints.CDN_URL}/embed/avatars/${altIcon}.png`;
|
||||
}
|
||||
|
||||
export function GUILD_BANNER(guildId: Snowflake, icon: string): string {
|
||||
return `${Endpoints.CDN_URL}/banners/${guildId}/${icon}`;
|
||||
}
|
||||
|
||||
export function GUILD_SPLASH(guildId: Snowflake, icon: string): string {
|
||||
return `${Endpoints.CDN_URL}/splashes/${guildId}/${icon}`;
|
||||
}
|
||||
|
||||
export function GUILD_ICON(guildId: Snowflake, icon: string): string {
|
||||
return `${Endpoints.CDN_URL}/icons/${guildId}/${icon}`;
|
||||
}
|
26
packages/api-types/src/utils/constants.ts
Normal file
26
packages/api-types/src/utils/constants.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/** https://discord.com/developers/docs/reference#api-reference-base-url */
|
||||
export const BASE_URL = 'https://discord.com/api';
|
||||
|
||||
/** https://discord.com/developers/docs/reference#api-versioning-api-versions */
|
||||
export const API_VERSION = 10;
|
||||
|
||||
/** https://github.com/discordeno/discordeno/releases */
|
||||
export const BISCUIT_VERSION = '0.2.2';
|
||||
|
||||
/** https://discord.com/developers/docs/reference#user-agent */
|
||||
export const USER_AGENT = `DiscordBot (https://github.com/oasisjs/biscuit, v${BISCUIT_VERSION})`;
|
||||
|
||||
/** https://discord.com/developers/docs/reference#image-formatting-image-base-url */
|
||||
export const IMAGE_BASE_URL = 'https://cdn.discordapp.com';
|
||||
|
||||
// This can be modified by big brain bots and use a proxy
|
||||
export const baseEndpoints = {
|
||||
BASE_URL: `${BASE_URL}/v${API_VERSION}`,
|
||||
CDN_URL: IMAGE_BASE_URL,
|
||||
};
|
||||
|
||||
export const SLASH_COMMANDS_NAME_REGEX =
|
||||
/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
|
||||
export const CONTEXT_MENU_COMMANDS_NAME_REGEX = /^[\w-\s]{1,32}$/;
|
||||
export const CHANNEL_MENTION_REGEX = /<#[0-9]+>/g;
|
||||
export const DISCORD_SNOWFLAKE_REGEX = /^(?<id>\d{17,19})$/;
|
471
packages/api-types/src/utils/routes.ts
Normal file
471
packages/api-types/src/utils/routes.ts
Normal file
@ -0,0 +1,471 @@
|
||||
import type { Snowflake } from '../common';
|
||||
export * from './cdn';
|
||||
|
||||
export function USER(userId?: Snowflake): string {
|
||||
if (!userId) { return '/users/@me'; }
|
||||
return `/users/${userId}`;
|
||||
}
|
||||
|
||||
export function GATEWAY_BOT(): string {
|
||||
return '/gateway/bot';
|
||||
}
|
||||
|
||||
export interface GetMessagesOptions {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface GetMessagesOptions {
|
||||
around?: Snowflake;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface GetMessagesOptions {
|
||||
before?: Snowflake;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface GetMessagesOptions {
|
||||
after?: Snowflake;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export function CHANNEL(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}`;
|
||||
}
|
||||
|
||||
export function CHANNEL_INVITES(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/invites`;
|
||||
}
|
||||
|
||||
export function CHANNEL_TYPING(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/typing`;
|
||||
}
|
||||
|
||||
export function CHANNEL_CREATE_THREAD(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/threads`;
|
||||
}
|
||||
|
||||
export function MESSAGE_CREATE_THREAD(channelId: Snowflake, messageId: Snowflake): string {
|
||||
return `/channels/${channelId}/messages/${messageId}/threads`;
|
||||
}
|
||||
|
||||
/** used to send messages */
|
||||
export function CHANNEL_MESSAGES(channelId: Snowflake, options?: GetMessagesOptions): string {
|
||||
let url = `/channels/${channelId}/messages?`;
|
||||
|
||||
if (options) {
|
||||
if (options.after) { url += `after=${options.after}`; }
|
||||
if (options.before) { url += `&before=${options.before}`; }
|
||||
if (options.around) { url += `&around=${options.around}`; }
|
||||
if (options.limit) { url += `&limit=${options.limit}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/** used to edit messages */
|
||||
export function CHANNEL_MESSAGE(channelId: Snowflake, messageId: Snowflake): string {
|
||||
return `/channels/${channelId}/messages/${messageId}`;
|
||||
}
|
||||
|
||||
/** used to kick members */
|
||||
export function GUILD_MEMBER(guildId: Snowflake, userId: Snowflake): string {
|
||||
return `/guilds/${guildId}/members/${userId}`;
|
||||
}
|
||||
|
||||
/** used to ban members */
|
||||
export function GUILD_BAN(guildId: Snowflake, userId: Snowflake): string {
|
||||
return `/guilds/${guildId}/bans/${userId}`;
|
||||
}
|
||||
|
||||
export interface GetBans {
|
||||
limit?: number;
|
||||
before?: Snowflake;
|
||||
after?: Snowflake;
|
||||
}
|
||||
|
||||
/** used to unban members */
|
||||
export function GUILD_BANS(guildId: Snowflake, options?: GetBans): string {
|
||||
let url = `/guilds/${guildId}/bans?`;
|
||||
|
||||
if (options) {
|
||||
if (options.limit) { url += `limit=${options.limit}`; }
|
||||
if (options.after) { url += `&after=${options.after}`; }
|
||||
if (options.before) { url += `&before=${options.before}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function GUILD_ROLE(guildId: Snowflake, roleId: Snowflake): string {
|
||||
return `/guilds/${guildId}/roles/${roleId}`;
|
||||
}
|
||||
|
||||
export function GUILD_ROLES(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/roles`;
|
||||
}
|
||||
|
||||
export function USER_GUILDS(guildId?: Snowflake): string {
|
||||
if (guildId) { return `/users/@me/guilds/${guildId}`; }
|
||||
return `/users/@me/guilds/`;
|
||||
}
|
||||
|
||||
export function USER_DM() {
|
||||
return `/users/@me/channels`;
|
||||
}
|
||||
|
||||
export function GUILD_EMOJIS(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/emojis`;
|
||||
}
|
||||
|
||||
export function GUILD_EMOJI(guildId: Snowflake, emojiId: Snowflake): string {
|
||||
return `/guilds/${guildId}/emojis/${emojiId}`;
|
||||
}
|
||||
|
||||
export interface GetInvite {
|
||||
withCounts?: boolean;
|
||||
withExpiration?: boolean;
|
||||
scheduledEventId?: Snowflake;
|
||||
}
|
||||
|
||||
export function GUILDS(guildId?: Snowflake): string {
|
||||
if (guildId) { return `/guilds/${guildId}`; }
|
||||
return `/guilds`;
|
||||
}
|
||||
|
||||
export function AUTO_MODERATION_RULES(guildId: Snowflake, ruleId?: Snowflake): string {
|
||||
if (ruleId) {
|
||||
return `/guilds/${guildId}/auto-moderation/rules/${ruleId}`;
|
||||
}
|
||||
return `/guilds/${guildId}/auto-moderation/rules`;
|
||||
}
|
||||
|
||||
export function INVITE(inviteCode: string, options?: GetInvite): string {
|
||||
let url = `/invites/${inviteCode}?`;
|
||||
|
||||
if (options) {
|
||||
if (options.withCounts) { url += `with_counts=${options.withCounts}`; }
|
||||
if (options.withExpiration) { url += `&with_expiration=${options.withExpiration}`; }
|
||||
if (options.scheduledEventId) { url += `&guild_scheduled_event_id=${options.scheduledEventId}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function GUILD_INVITES(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/invites`;
|
||||
}
|
||||
|
||||
export function INTERACTION_ID_TOKEN(interactionId: Snowflake, token: string): string {
|
||||
return `/interactions/${interactionId}/${token}/callback`;
|
||||
}
|
||||
|
||||
export function WEBHOOK_MESSAGE_ORIGINAL(webhookId: Snowflake, token: string, options?: { threadId?: bigint }): string {
|
||||
let url = `/webhooks/${webhookId}/${token}/messages/@original?`;
|
||||
|
||||
if (options) {
|
||||
if (options.threadId) { url += `thread_id=${options.threadId}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function WEBHOOK_MESSAGE(
|
||||
webhookId: Snowflake,
|
||||
token: string,
|
||||
messageId: Snowflake,
|
||||
options?: { threadId?: Snowflake },
|
||||
): string {
|
||||
let url = `/webhooks/${webhookId}/${token}/messages/${messageId}?`;
|
||||
|
||||
if (options) {
|
||||
if (options.threadId) { url += `thread_id=${options.threadId}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function WEBHOOK_TOKEN(webhookId: Snowflake, token?: string): string {
|
||||
if (!token) { return `/webhooks/${webhookId}`; }
|
||||
return `/webhooks/${webhookId}/${token}`;
|
||||
}
|
||||
|
||||
export interface WebhookOptions {
|
||||
wait?: boolean;
|
||||
threadId?: Snowflake;
|
||||
}
|
||||
|
||||
export function WEBHOOK(webhookId: Snowflake, token: string, options?: WebhookOptions): string {
|
||||
let url = `/webhooks/${webhookId}/${token}`;
|
||||
|
||||
if (options?.wait) { url += `?wait=${options.wait}`; }
|
||||
if (options?.threadId) { url += `?thread_id=${options.threadId}`; }
|
||||
if (options?.wait && options.threadId) { url += `?wait=${options.wait}&thread_id=${options.threadId}`; }
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function USER_NICK(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/members/@me`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/guild#get-guild-prune-count
|
||||
*/
|
||||
export interface GetGuildPruneCountQuery {
|
||||
days?: number;
|
||||
includeRoles?: Snowflake | Snowflake[];
|
||||
}
|
||||
|
||||
export function GUILD_PRUNE(guildId: Snowflake, options?: GetGuildPruneCountQuery): string {
|
||||
let url = `/guilds/${guildId}/prune?`;
|
||||
|
||||
if (options?.days) { url += `days=${options.days}`; }
|
||||
if (options?.includeRoles) { url += `&include_roles=${options.includeRoles}`; }
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function CHANNEL_PIN(channelId: Snowflake, messageId: Snowflake): string {
|
||||
return `/channels/${channelId}/pins/${messageId}`;
|
||||
}
|
||||
|
||||
export function CHANNEL_PINS(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/pins`;
|
||||
}
|
||||
|
||||
export function CHANNEL_MESSAGE_REACTION_ME(channelId: Snowflake, messageId: Snowflake, emoji: string): string {
|
||||
return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`;
|
||||
}
|
||||
|
||||
export function CHANNEL_MESSAGE_REACTION_USER(
|
||||
channelId: Snowflake,
|
||||
messageId: Snowflake,
|
||||
emoji: string,
|
||||
userId: Snowflake,
|
||||
) {
|
||||
return `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/${userId}`;
|
||||
}
|
||||
|
||||
export function CHANNEL_MESSAGE_REACTIONS(channelId: Snowflake, messageId: Snowflake) {
|
||||
return `/channels/${channelId}/messages/${messageId}/reactions`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/channel#get-reactions-query-string-params
|
||||
*/
|
||||
export interface GetReactions {
|
||||
after?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export function CHANNEL_MESSAGE_REACTION(
|
||||
channelId: Snowflake,
|
||||
messageId: Snowflake,
|
||||
emoji: string,
|
||||
options?: GetReactions,
|
||||
): string {
|
||||
let url = `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}?`;
|
||||
|
||||
if (options?.after) { url += `after=${options.after}`; }
|
||||
if (options?.limit) { url += `&limit=${options.limit}`; }
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function CHANNEL_MESSAGE_CROSSPOST(channelId: Snowflake, messageId: Snowflake): string {
|
||||
return `/channels/${channelId}/messages/${messageId}/crosspost`;
|
||||
}
|
||||
|
||||
export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleId: Snowflake): string {
|
||||
return `/guilds/${guildId}/members/${memberId}/roles/${roleId}`;
|
||||
}
|
||||
|
||||
export function CHANNEL_WEBHOOKS(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/webhooks`;
|
||||
}
|
||||
|
||||
export function THREAD_START_PUBLIC(channelId: Snowflake, messageId: Snowflake): string {
|
||||
return `/channels/${channelId}/messages/${messageId}/threads`;
|
||||
}
|
||||
|
||||
export function THREAD_START_PRIVATE(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/threads`;
|
||||
}
|
||||
|
||||
export function THREAD_ACTIVE(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/threads/active`;
|
||||
}
|
||||
|
||||
export interface ListArchivedThreads {
|
||||
before?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export function THREAD_ME(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/thread-members/@me`;
|
||||
}
|
||||
|
||||
export function THREAD_MEMBERS(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/thread-members`;
|
||||
}
|
||||
|
||||
export function THREAD_USER(channelId: Snowflake, userId: Snowflake): string {
|
||||
return `/channels/${channelId}/thread-members/${userId}`;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/threads/archived`;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED_PUBLIC(channelId: Snowflake, options?: ListArchivedThreads): string {
|
||||
let url = `/channels/${channelId}/threads/archived/public?`;
|
||||
|
||||
if (options) {
|
||||
if (options.before) { url += `before=${new Date(options.before).toISOString()}`; }
|
||||
if (options.limit) { url += `&limit=${options.limit}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED_PRIVATE(channelId: Snowflake, options?: ListArchivedThreads): string {
|
||||
let url = `/channels/${channelId}/threads/archived/private?`;
|
||||
|
||||
if (options) {
|
||||
if (options.before) { url += `before=${new Date(options.before).toISOString()}`; }
|
||||
if (options.limit) { url += `&limit=${options.limit}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: ListArchivedThreads): string {
|
||||
let url = `/channels/${channelId}/users/@me/threads/archived/private?`;
|
||||
|
||||
if (options) {
|
||||
if (options.before) { url += `before=${new Date(options.before).toISOString()}`; }
|
||||
if (options.limit) { url += `&limit=${options.limit}`; }
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function FORUM_START(channelId: Snowflake): string {
|
||||
return `/channels/${channelId}/threads?has_message=true`;
|
||||
}
|
||||
|
||||
export function STAGE_INSTANCES(): string {
|
||||
return `/stage-instances`;
|
||||
}
|
||||
|
||||
export function STAGE_INSTANCE(channelId: Snowflake): string {
|
||||
return `/stage-instances/${channelId}`;
|
||||
}
|
||||
|
||||
export function APPLICATION_COMMANDS(appId: Snowflake, commandId?: Snowflake): string {
|
||||
if (commandId) { return `/applications/${appId}/commands/${commandId}`; }
|
||||
return `/applications/${appId}/commands`;
|
||||
}
|
||||
|
||||
export function GUILD_APPLICATION_COMMANDS(appId: Snowflake, guildId: Snowflake, commandId?: Snowflake): string {
|
||||
if (commandId) { return `/applications/${appId}/guilds/${guildId}/commands/${commandId}`; }
|
||||
return `/applications/${appId}/guilds/${guildId}/commands`;
|
||||
}
|
||||
|
||||
export function GUILD_APPLICATION_COMMANDS_PERMISSIONS(
|
||||
appId: Snowflake,
|
||||
guildId: Snowflake,
|
||||
commandId?: Snowflake,
|
||||
): string {
|
||||
if (commandId) { return `/applications/${appId}/guilds/${guildId}/commands/${commandId}/permissions`; }
|
||||
return `/applications/${appId}/guilds/${guildId}/commands/permissions`;
|
||||
}
|
||||
|
||||
export function APPLICATION_COMMANDS_LOCALIZATIONS(
|
||||
appId: Snowflake,
|
||||
commandId: Snowflake,
|
||||
withLocalizations?: boolean,
|
||||
): string {
|
||||
let url = `/applications/${appId}/commands/${commandId}?`;
|
||||
|
||||
if (withLocalizations !== undefined) {
|
||||
url += `withLocalizations=${withLocalizations}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function GUILD_APPLICATION_COMMANDS_LOCALIZATIONS(
|
||||
appId: Snowflake,
|
||||
guildId: Snowflake,
|
||||
commandId: Snowflake,
|
||||
withLocalizations?: boolean,
|
||||
): string {
|
||||
let url = `/applications/${appId}/guilds/${guildId}/commands/${commandId}?`;
|
||||
|
||||
if (withLocalizations !== undefined) {
|
||||
url += `with_localizations=${withLocalizations}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function STICKER(id: Snowflake): string {
|
||||
return `stickers/${id}`;
|
||||
}
|
||||
|
||||
export function STICKER_PACKS(): string {
|
||||
return `stickers-packs`;
|
||||
}
|
||||
|
||||
export function GUILD_STICKERS(guildId: Snowflake, stickerId?: Snowflake): string {
|
||||
if (stickerId) { return `/guilds/${guildId}/stickers/${stickerId}`; }
|
||||
return `/guilds/${guildId}/stickers`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the widget for the guild.
|
||||
* @link https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
|
||||
*/
|
||||
export interface GetWidget {
|
||||
get: 'json' | 'image' | 'settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* /guilds/{guildId}/widget
|
||||
* @link https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
|
||||
*/
|
||||
export function GUILD_WIDGET(guildId: Snowflake, options: GetWidget = { get: 'settings' }): string {
|
||||
let url = `/guilds/${guildId}/widget`;
|
||||
if (options.get === 'json') {
|
||||
url += '.json';
|
||||
} else if (options.get === 'image') {
|
||||
url += '.png';
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/** @link https://discord.com/developers/docs/resources/guild#get-guild-voice-regions */
|
||||
export function GUILD_VOICE_REGIONS(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/regions`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/guild#get-guild-vanity-url
|
||||
* @param guildId The guild
|
||||
* @returns Get vanity URL
|
||||
*/
|
||||
export function GUILD_VANITY(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/vanity-url`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/guild#get-guild-preview
|
||||
* @param guildId The guild
|
||||
* @returns Get guild preview url
|
||||
*/
|
||||
export function GUILD_PREVIEW(guildId: Snowflake): string {
|
||||
return `/guilds/${guildId}/preview`;
|
||||
}
|
2476
packages/api-types/src/v10/index.ts
Normal file
2476
packages/api-types/src/v10/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
7
packages/api-types/tsconfig.json
Normal file
7
packages/api-types/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
12
packages/api-types/tsup.config.ts
Normal file
12
packages/api-types/tsup.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
minify: isProduction,
|
||||
sourcemap: true,
|
||||
});
|
0
packages/cache/README.md
vendored
Normal file
0
packages/cache/README.md
vendored
Normal file
22
packages/cache/package.json
vendored
Normal file
22
packages/cache/package.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@biscuitland/cache",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist && rm -rf .turbo",
|
||||
"dev": "tsup --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^1.0.0",
|
||||
"ioredis": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.1.3"
|
||||
}
|
||||
}
|
31
packages/cache/src/adapters/cache-adapter.ts
vendored
Normal file
31
packages/cache/src/adapters/cache-adapter.ts
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
export interface CacheAdapter {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
get<T = unknown>(name: string): Promise<T | unknown>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
set(name: string, data: unknown): Promise<void>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
remove(name: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
clear(): Promise<void>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
close?(): Promise<void>;
|
||||
}
|
51
packages/cache/src/adapters/memory-cache-adapter.ts
vendored
Normal file
51
packages/cache/src/adapters/memory-cache-adapter.ts
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
import { CacheAdapter } from './cache-adapter';
|
||||
|
||||
export class MemoryCacheAdapter implements CacheAdapter {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private readonly data = new Map<string, any>();
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async get<T = unknown>(name: string): Promise<T | null> {
|
||||
const data = this.data.get(name);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async set(name: string, data: unknown): Promise<void> {
|
||||
const stringData = JSON.stringify(data, (_, v) =>
|
||||
typeof v === 'bigint' ? v.toString() : v
|
||||
);
|
||||
|
||||
this.data.set(name, stringData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async remove(name: string): Promise<void> {
|
||||
this.data.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
95
packages/cache/src/adapters/redis-cache-adapter.ts
vendored
Normal file
95
packages/cache/src/adapters/redis-cache-adapter.ts
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
import type { Redis, RedisOptions } from 'ioredis';
|
||||
|
||||
import { CacheAdapter } from './cache-adapter';
|
||||
import IORedis from 'ioredis';
|
||||
|
||||
export interface BaseOptions {
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
export interface BuildOptions extends BaseOptions, RedisOptions {}
|
||||
|
||||
export interface ClientOptions extends BaseOptions {
|
||||
client: Redis;
|
||||
}
|
||||
|
||||
export type Options = BuildOptions | ClientOptions;
|
||||
|
||||
export class RedisCacheAdapter implements CacheAdapter {
|
||||
static readonly DEFAULTS = {
|
||||
prefix: 'biscuitland',
|
||||
};
|
||||
|
||||
private readonly client: Redis;
|
||||
|
||||
options: Options;
|
||||
|
||||
constructor(options?: Options) {
|
||||
this.options = Object.assign(RedisCacheAdapter.DEFAULTS, options);
|
||||
|
||||
if ((this.options as ClientOptions).client) {
|
||||
this.client = (this.options as ClientOptions).client;
|
||||
} else {
|
||||
const { ...redisOpt } = this.options as BuildOptions;
|
||||
this.client = new IORedis(redisOpt);
|
||||
}
|
||||
}
|
||||
|
||||
_getPrefix(name: string) {
|
||||
return `${this.options.prefix}:${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async get<T = unknown>(name: string): Promise<T | null> {
|
||||
const completKey = this._getPrefix(name);
|
||||
const data = await this.client.get(completKey);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async set(name: string, data: unknown): Promise<void> {
|
||||
const stringData = JSON.stringify(data, (_, v) =>
|
||||
typeof v === 'bigint' ? v.toString() : v
|
||||
);
|
||||
|
||||
const completeKey = this._getPrefix(name);
|
||||
|
||||
await this.client.set(completeKey, stringData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async remove(name: string): Promise<void> {
|
||||
const completKey = this._getPrefix(name);
|
||||
await this.client.del(completKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.client.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.client.disconnect();
|
||||
}
|
||||
}
|
4
packages/cache/src/index.ts
vendored
Normal file
4
packages/cache/src/index.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export { CacheAdapter } from './adapters/cache-adapter';
|
||||
|
||||
export { MemoryCacheAdapter } from './adapters/memory-cache-adapter';
|
||||
export { RedisCacheAdapter } from './adapters/redis-cache-adapter';
|
7
packages/cache/tsconfig.json
vendored
Normal file
7
packages/cache/tsconfig.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
12
packages/cache/tsup.config.ts
vendored
Normal file
12
packages/cache/tsup.config.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
minify: isProduction,
|
||||
sourcemap: true,
|
||||
});
|
0
packages/core/README.md
Normal file
0
packages/core/README.md
Normal file
23
packages/core/package.json
Normal file
23
packages/core/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@biscuitland/core",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist && rm -rf .turbo",
|
||||
"dev": "tsup --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^1.0.0",
|
||||
"@biscuitland/rest": "^1.0.0",
|
||||
"@biscuitland/ws": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.1.3"
|
||||
}
|
||||
}
|
25
packages/core/src/adapters/default-event-adapter.ts
Normal file
25
packages/core/src/adapters/default-event-adapter.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { EventAdapter } from './event-adapter';
|
||||
import type { Events } from './events';
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
export class DefaultEventAdapter extends EventEmitter implements EventAdapter {
|
||||
override on<K extends keyof Events>(event: K, func: Events[K]): this;
|
||||
override on<K extends string>(event: K, func: (...args: unknown[]) => unknown): this {
|
||||
return super.on(event, func);
|
||||
}
|
||||
|
||||
override off<K extends keyof Events>(event: K, func: Events[K]): this;
|
||||
override off<K extends keyof Events>(event: K, func: (...args: unknown[]) => unknown): this {
|
||||
return super.off(event, func);
|
||||
}
|
||||
|
||||
override once<K extends keyof Events>(event: K, func: Events[K]): this;
|
||||
override once<K extends string>(event: K, func: (...args: unknown[]) => unknown): this {
|
||||
return super.once(event, func);
|
||||
}
|
||||
|
||||
override emit<K extends keyof Events>(event: K, ...params: Parameters<Events[K]>): boolean;
|
||||
override emit<K extends string>(event: K, ...params: unknown[]): boolean {
|
||||
return super.emit(event, ...params);
|
||||
}
|
||||
}
|
25
packages/core/src/adapters/event-adapter.ts
Normal file
25
packages/core/src/adapters/event-adapter.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { Events } from './events';
|
||||
|
||||
export interface EventAdapter extends Omit<NodeJS.EventEmitter, "emit" | "on" | "off" | "once"> {
|
||||
options?: any;
|
||||
|
||||
emit<K extends keyof Events>(
|
||||
event: K,
|
||||
...params: Parameters<Events[K]>
|
||||
): boolean;
|
||||
|
||||
on<K extends keyof Events>(
|
||||
event: K,
|
||||
func: Events[K]
|
||||
): unknown;
|
||||
|
||||
off<K extends keyof Events>(
|
||||
event: K,
|
||||
func: Events[K]
|
||||
): unknown;
|
||||
|
||||
once<K extends keyof Events>(
|
||||
event: K,
|
||||
func: Events[K]
|
||||
): unknown;
|
||||
}
|
784
packages/core/src/adapters/events.ts
Normal file
784
packages/core/src/adapters/events.ts
Normal file
@ -0,0 +1,784 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import type {
|
||||
DiscordAutoModerationActionExecution,
|
||||
DiscordAutoModerationRule,
|
||||
DiscordChannel,
|
||||
DiscordChannelPinsUpdate,
|
||||
DiscordEmoji,
|
||||
DiscordGuild,
|
||||
DiscordGuildBanAddRemove,
|
||||
DiscordGuildEmojisUpdate,
|
||||
DiscordGuildMemberAdd,
|
||||
DiscordGuildMemberRemove,
|
||||
DiscordGuildMemberUpdate,
|
||||
DiscordGuildRoleCreate,
|
||||
DiscordGuildRoleDelete,
|
||||
DiscordGuildRoleUpdate,
|
||||
DiscordIntegration,
|
||||
DiscordIntegrationDelete,
|
||||
DiscordInteraction,
|
||||
DiscordInviteCreate,
|
||||
DiscordInviteDelete,
|
||||
DiscordMemberWithUser,
|
||||
DiscordMessage,
|
||||
DiscordMessageDelete,
|
||||
DiscordMessageReactionAdd,
|
||||
DiscordMessageReactionRemove,
|
||||
DiscordMessageReactionRemoveAll,
|
||||
DiscordMessageReactionRemoveEmoji,
|
||||
DiscordPresenceUpdate,
|
||||
DiscordReady,
|
||||
DiscordRole,
|
||||
DiscordScheduledEvent,
|
||||
DiscordScheduledEventUserAdd,
|
||||
DiscordScheduledEventUserRemove,
|
||||
DiscordThreadListSync,
|
||||
DiscordThreadMembersUpdate,
|
||||
DiscordThreadMemberUpdate,
|
||||
DiscordTypingStart,
|
||||
DiscordUser,
|
||||
DiscordWebhookUpdate,
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Interaction } from '../structures/interactions';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
|
||||
import {
|
||||
AutoModerationRule,
|
||||
AutoModerationExecution,
|
||||
} from '../structures/automod';
|
||||
|
||||
import type { Channel } from '../structures/channels';
|
||||
import {
|
||||
ChannelFactory,
|
||||
GuildChannel,
|
||||
ThreadChannel,
|
||||
} from '../structures/channels';
|
||||
|
||||
import type { DiscordStageInstanceB } from '../structures/stage-instance';
|
||||
import { StageInstance } from '../structures/stage-instance';
|
||||
import { ScheduledEvent } from '../structures/scheduled-events';
|
||||
import { Presence } from '../structures/presence';
|
||||
|
||||
import { Member, ThreadMember } from '../structures/members';
|
||||
|
||||
import { Message } from '../structures/message';
|
||||
import { User } from '../structures/user';
|
||||
import { Integration } from '../structures/integration';
|
||||
|
||||
import { Guild } from '../structures/guilds';
|
||||
import { InteractionFactory } from '../structures/interactions';
|
||||
import type { InviteCreate } from '../structures/invite';
|
||||
import { NewInviteCreate } from '../structures/invite';
|
||||
|
||||
import type {
|
||||
MessageReactionAdd,
|
||||
MessageReactionRemove,
|
||||
MessageReactionRemoveAll,
|
||||
MessageReactionRemoveEmoji,
|
||||
} from '../structures/message-reaction';
|
||||
|
||||
import { NewMessageReactionAdd } from '../structures/message-reaction';
|
||||
|
||||
export type RawHandler<T> = (...args: [Session, number, T]) => void;
|
||||
export type Handler<T extends [obj?: unknown, ddy?: unknown]> = (
|
||||
...args: T
|
||||
) => unknown;
|
||||
|
||||
export const READY: RawHandler<DiscordReady> = (session, shardId, payload) => {
|
||||
session.applicationId = payload.application.id;
|
||||
session.botId = payload.user.id;
|
||||
session.events.emit(
|
||||
'ready',
|
||||
{ ...payload, user: new User(session, payload.user) },
|
||||
shardId
|
||||
);
|
||||
};
|
||||
|
||||
export const MESSAGE_CREATE: RawHandler<DiscordMessage> = (
|
||||
session,
|
||||
_shardId,
|
||||
message
|
||||
) => {
|
||||
session.events.emit('messageCreate', new Message(session, message));
|
||||
};
|
||||
|
||||
export const MESSAGE_UPDATE: RawHandler<DiscordMessage> = (
|
||||
session,
|
||||
_shardId,
|
||||
new_message
|
||||
) => {
|
||||
// message is partial
|
||||
if (!new_message.edited_timestamp) {
|
||||
const message = {
|
||||
// TODO: improve this
|
||||
// ...new_message,
|
||||
session,
|
||||
id: new_message.id,
|
||||
guildId: new_message.guild_id,
|
||||
channelId: new_message.channel_id,
|
||||
};
|
||||
|
||||
// all methods of Message can run on partial messages
|
||||
// we aknowledge people that their callback could be partial but giving them all functions of Message
|
||||
Object.setPrototypeOf(message, Message.prototype);
|
||||
|
||||
session.events.emit('messageUpdate', message);
|
||||
return;
|
||||
}
|
||||
|
||||
session.events.emit('messageUpdate', new Message(session, new_message));
|
||||
};
|
||||
|
||||
export const MESSAGE_DELETE: RawHandler<DiscordMessageDelete> = (
|
||||
session,
|
||||
_shardId,
|
||||
{ id, channel_id, guild_id }
|
||||
) => {
|
||||
session.events.emit('messageDelete', {
|
||||
id,
|
||||
channelId: channel_id,
|
||||
guildId: guild_id,
|
||||
});
|
||||
};
|
||||
|
||||
export const GUILD_CREATE: RawHandler<DiscordGuild> = (
|
||||
session,
|
||||
_shardId,
|
||||
guild
|
||||
) => {
|
||||
session.events.emit('guildCreate', new Guild(session, guild));
|
||||
};
|
||||
|
||||
export const GUILD_DELETE: RawHandler<DiscordGuild> = (
|
||||
session,
|
||||
_shardId,
|
||||
guild
|
||||
) => {
|
||||
session.events.emit('guildDelete', { id: guild.id, unavailable: true });
|
||||
};
|
||||
|
||||
export const GUILD_MEMBER_ADD: RawHandler<DiscordGuildMemberAdd> = (
|
||||
session,
|
||||
_shardId,
|
||||
member
|
||||
) => {
|
||||
session.events.emit(
|
||||
'guildMemberAdd',
|
||||
new Member(session, member, member.guild_id)
|
||||
);
|
||||
};
|
||||
|
||||
export const GUILD_MEMBER_UPDATE: RawHandler<DiscordGuildMemberUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
member
|
||||
) => {
|
||||
session.events.emit(
|
||||
'guildMemberUpdate',
|
||||
new Member(session, member, member.guild_id)
|
||||
);
|
||||
};
|
||||
|
||||
export const GUILD_MEMBER_REMOVE: RawHandler<DiscordGuildMemberRemove> = (
|
||||
session,
|
||||
_shardId,
|
||||
member
|
||||
) => {
|
||||
session.events.emit(
|
||||
'guildMemberRemove',
|
||||
new User(session, member.user),
|
||||
member.guild_id
|
||||
);
|
||||
};
|
||||
|
||||
export const GUILD_BAN_ADD: RawHandler<DiscordGuildBanAddRemove> = (
|
||||
session,
|
||||
_shardId,
|
||||
data
|
||||
) => {
|
||||
session.events.emit('guildBanAdd', {
|
||||
guildId: data.guild_id,
|
||||
user: data.user,
|
||||
});
|
||||
};
|
||||
|
||||
export const GUILD_BAN_REMOVE: RawHandler<DiscordGuildBanAddRemove> = (
|
||||
session,
|
||||
_shardId,
|
||||
data
|
||||
) => {
|
||||
session.events.emit('guildBanRemove', {
|
||||
guildId: data.guild_id,
|
||||
user: data.user,
|
||||
});
|
||||
};
|
||||
|
||||
export const GUILD_EMOJIS_UPDATE: RawHandler<DiscordGuildEmojisUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
data
|
||||
) => {
|
||||
session.events.emit('guildEmojisUpdate', {
|
||||
guildId: data.guild_id,
|
||||
emojis: data.emojis,
|
||||
});
|
||||
};
|
||||
|
||||
export const GUILD_ROLE_CREATE: RawHandler<DiscordGuildRoleCreate> = (
|
||||
session,
|
||||
_shardId,
|
||||
data
|
||||
) => {
|
||||
session.events.emit('guildRoleCreate', {
|
||||
guildId: data.guild_id,
|
||||
role: data.role,
|
||||
});
|
||||
};
|
||||
|
||||
export const GUILD_ROLE_UPDATE: RawHandler<DiscordGuildRoleUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
data
|
||||
) => {
|
||||
session.events.emit('guildRoleUpdate', {
|
||||
guildId: data.guild_id,
|
||||
role: data.role,
|
||||
});
|
||||
};
|
||||
|
||||
export const GUILD_ROLE_DELETE: RawHandler<DiscordGuildRoleDelete> = (
|
||||
session,
|
||||
_shardId,
|
||||
data
|
||||
) => {
|
||||
session.events.emit('guildRoleDelete', {
|
||||
guildId: data.guild_id,
|
||||
roleId: data.role_id,
|
||||
});
|
||||
};
|
||||
|
||||
export const TYPING_START: RawHandler<DiscordTypingStart> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('typingStart', {
|
||||
channelId: payload.channel_id,
|
||||
guildId: payload.guild_id ? payload.guild_id : undefined,
|
||||
userId: payload.user_id,
|
||||
timestamp: payload.timestamp,
|
||||
member: payload.guild_id
|
||||
? new Member(
|
||||
session,
|
||||
payload.member as DiscordMemberWithUser,
|
||||
payload.guild_id
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export const INTERACTION_CREATE: RawHandler<DiscordInteraction> = (
|
||||
session,
|
||||
_shardId,
|
||||
interaction
|
||||
) => {
|
||||
session.events.emit(
|
||||
'interactionCreate',
|
||||
InteractionFactory.from(session, interaction)
|
||||
);
|
||||
};
|
||||
|
||||
export const CHANNEL_CREATE: RawHandler<DiscordChannel> = (
|
||||
session,
|
||||
_shardId,
|
||||
channel
|
||||
) => {
|
||||
session.events.emit('channelCreate', ChannelFactory.from(session, channel));
|
||||
};
|
||||
|
||||
export const CHANNEL_UPDATE: RawHandler<DiscordChannel> = (
|
||||
session,
|
||||
_shardId,
|
||||
channel
|
||||
) => {
|
||||
session.events.emit('channelUpdate', ChannelFactory.from(session, channel));
|
||||
};
|
||||
|
||||
export const CHANNEL_DELETE: RawHandler<DiscordChannel> = (
|
||||
session,
|
||||
_shardId,
|
||||
channel
|
||||
) => {
|
||||
if (!channel.guild_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.events.emit(
|
||||
'channelDelete',
|
||||
new GuildChannel(session, channel, channel.guild_id)
|
||||
);
|
||||
};
|
||||
|
||||
export const THREAD_CREATE: RawHandler<DiscordChannel> = (
|
||||
session,
|
||||
_shardId,
|
||||
channel
|
||||
) => {
|
||||
if (!channel.guild_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.events.emit(
|
||||
'threadCreate',
|
||||
new ThreadChannel(session, channel, channel.guild_id)
|
||||
);
|
||||
};
|
||||
|
||||
export const THREAD_UPDATE: RawHandler<DiscordChannel> = (
|
||||
session,
|
||||
_shardId,
|
||||
channel
|
||||
) => {
|
||||
if (!channel.guild_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.events.emit(
|
||||
'threadUpdate',
|
||||
new ThreadChannel(session, channel, channel.guild_id)
|
||||
);
|
||||
};
|
||||
|
||||
export const THREAD_DELETE: RawHandler<DiscordChannel> = (
|
||||
session,
|
||||
_shardId,
|
||||
channel
|
||||
) => {
|
||||
if (!channel.guild_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.events.emit(
|
||||
'threadDelete',
|
||||
new ThreadChannel(session, channel, channel.guild_id)
|
||||
);
|
||||
};
|
||||
|
||||
export const THREAD_MEMBER_UPDATE: RawHandler<DiscordThreadMemberUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('threadMemberUpdate', {
|
||||
guildId: payload.guild_id,
|
||||
id: payload.id,
|
||||
userId: payload.user_id,
|
||||
joinedAt: payload.joined_at,
|
||||
flags: payload.flags,
|
||||
});
|
||||
};
|
||||
|
||||
export const THREAD_MEMBERS_UPDATE: RawHandler<DiscordThreadMembersUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('threadMembersUpdate', {
|
||||
memberCount: payload.member_count,
|
||||
addedMembers: payload.added_members
|
||||
? payload.added_members.map(tm => new ThreadMember(session, tm))
|
||||
: undefined,
|
||||
removedMemberIds: payload.removed_member_ids
|
||||
? payload.removed_member_ids
|
||||
: undefined,
|
||||
guildId: payload.guild_id,
|
||||
id: payload.id,
|
||||
});
|
||||
};
|
||||
|
||||
export const THREAD_LIST_SYNC: RawHandler<DiscordThreadListSync> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('threadListSync', {
|
||||
guildId: payload.guild_id,
|
||||
channelIds: payload.channel_ids ?? [],
|
||||
threads: payload.threads.map(
|
||||
channel => new ThreadChannel(session, channel, payload.guild_id)
|
||||
),
|
||||
members: payload.members.map(
|
||||
member => new ThreadMember(session, member)
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export const CHANNEL_PINS_UPDATE: RawHandler<DiscordChannelPinsUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('channelPinsUpdate', {
|
||||
guildId: payload.guild_id,
|
||||
channelId: payload.channel_id,
|
||||
lastPinTimestamp: payload.last_pin_timestamp
|
||||
? Date.parse(payload.last_pin_timestamp)
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export const USER_UPDATE: RawHandler<DiscordUser> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('userUpdate', new User(session, payload));
|
||||
};
|
||||
|
||||
export const PRESENCE_UPDATE: RawHandler<DiscordPresenceUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('presenceUpdate', new Presence(session, payload));
|
||||
};
|
||||
|
||||
export const WEBHOOKS_UPDATE: RawHandler<DiscordWebhookUpdate> = (
|
||||
session,
|
||||
_shardId,
|
||||
webhook
|
||||
) => {
|
||||
session.events.emit('webhooksUpdate', {
|
||||
guildId: webhook.guild_id,
|
||||
channelId: webhook.channel_id,
|
||||
});
|
||||
};
|
||||
|
||||
export const INTEGRATION_CREATE: RawHandler<
|
||||
DiscordIntegration & { guildId?: Snowflake }
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit('integrationCreate', new Integration(session, payload));
|
||||
};
|
||||
|
||||
export const INTEGRATION_UPDATE: RawHandler<
|
||||
DiscordIntegration & { guildId?: Snowflake }
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit('integrationCreate', new Integration(session, payload));
|
||||
};
|
||||
|
||||
export const INTEGRATION_DELETE: RawHandler<DiscordIntegrationDelete> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit('integrationDelete', {
|
||||
id: payload.id,
|
||||
guildId: payload.guild_id,
|
||||
applicationId: payload.application_id,
|
||||
});
|
||||
};
|
||||
|
||||
export const AUTO_MODERATION_RULE_CREATE: RawHandler<
|
||||
DiscordAutoModerationRule
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit(
|
||||
'autoModerationRuleCreate',
|
||||
new AutoModerationRule(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const AUTO_MODERATION_RULE_UPDATE: RawHandler<
|
||||
DiscordAutoModerationRule
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit(
|
||||
'autoModerationRuleUpdate',
|
||||
new AutoModerationRule(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const AUTO_MODERATION_RULE_DELETE: RawHandler<
|
||||
DiscordAutoModerationRule
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit(
|
||||
'autoModerationRuleDelete',
|
||||
new AutoModerationRule(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const AUTO_MODERATION_ACTION_EXECUTE: RawHandler<
|
||||
DiscordAutoModerationActionExecution
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit(
|
||||
'autoModerationActionExecution',
|
||||
new AutoModerationExecution(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const MESSAGE_REACTION_ADD: RawHandler<DiscordMessageReactionAdd> = (
|
||||
session,
|
||||
_shardId,
|
||||
reaction
|
||||
) => {
|
||||
session.events.emit(
|
||||
'messageReactionAdd',
|
||||
NewMessageReactionAdd(session, reaction)
|
||||
);
|
||||
};
|
||||
|
||||
export const MESSAGE_REACTION_REMOVE: RawHandler<
|
||||
DiscordMessageReactionRemove
|
||||
> = (session, _shardId, reaction) => {
|
||||
session.events.emit(
|
||||
'messageReactionRemove',
|
||||
NewMessageReactionAdd(session, reaction)
|
||||
);
|
||||
};
|
||||
|
||||
export const MESSAGE_REACTION_REMOVE_ALL: RawHandler<
|
||||
DiscordMessageReactionRemoveAll
|
||||
> = (session, _shardId, reaction) => {
|
||||
session.events.emit(
|
||||
'messageReactionRemoveAll',
|
||||
NewMessageReactionAdd(session, reaction as DiscordMessageReactionAdd)
|
||||
);
|
||||
};
|
||||
|
||||
export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler<
|
||||
DiscordMessageReactionRemoveEmoji
|
||||
> = (session, _shardId, reaction) => {
|
||||
session.events.emit(
|
||||
'messageReactionRemoveEmoji',
|
||||
NewMessageReactionAdd(session, reaction as DiscordMessageReactionAdd)
|
||||
);
|
||||
};
|
||||
|
||||
export const INVITE_CREATE: RawHandler<DiscordInviteCreate> = (
|
||||
session,
|
||||
_shardId,
|
||||
invite
|
||||
) => {
|
||||
session.events.emit('inviteCreate', NewInviteCreate(session, invite));
|
||||
};
|
||||
|
||||
export const INVITE_DELETE: RawHandler<DiscordInviteDelete> = (
|
||||
session,
|
||||
_shardId,
|
||||
data
|
||||
) => {
|
||||
session.events.emit('inviteDelete', {
|
||||
channelId: data.channel_id,
|
||||
guildId: data.guild_id,
|
||||
code: data.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const STAGE_INSTANCE_CREATE: RawHandler<DiscordStageInstanceB> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit(
|
||||
'stageInstanceCreate',
|
||||
new StageInstance(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const STAGE_INSTANCE_UPDATE: RawHandler<DiscordStageInstanceB> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit(
|
||||
'stageInstanceUpdate',
|
||||
new StageInstance(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const STAGE_INSTANCE_DELETE: RawHandler<DiscordStageInstanceB> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit(
|
||||
'stageInstanceDelete',
|
||||
new StageInstance(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const GUILD_SCHEDULED_EVENT_CREATE: RawHandler<DiscordScheduledEvent> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit(
|
||||
'guildScheduledEventCreate',
|
||||
new ScheduledEvent(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const GUILD_SCHEDULED_EVENT_UPDATE: RawHandler<DiscordScheduledEvent> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit(
|
||||
'guildScheduledEventUpdate',
|
||||
new ScheduledEvent(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const GUILD_SCHEDULED_EVENT_DELETE: RawHandler<DiscordScheduledEvent> = (
|
||||
session,
|
||||
_shardId,
|
||||
payload
|
||||
) => {
|
||||
session.events.emit(
|
||||
'guildScheduledEventDelete',
|
||||
new ScheduledEvent(session, payload)
|
||||
);
|
||||
};
|
||||
|
||||
export const GUILD_SCHEDULED_EVENT_USER_ADD: RawHandler<
|
||||
DiscordScheduledEventUserAdd
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit('guildScheduledEventUserAdd', {
|
||||
scheduledEventId: payload.guild_scheduled_event_id,
|
||||
userId: payload.user_id,
|
||||
guildId: payload.guild_id,
|
||||
});
|
||||
};
|
||||
|
||||
export const GUILD_SCHEDULED_EVENT_USER_REMOVE: RawHandler<
|
||||
DiscordScheduledEventUserRemove
|
||||
> = (session, _shardId, payload) => {
|
||||
session.events.emit('guildScheduledEventUserRemove', {
|
||||
scheduledEventId: payload.guild_scheduled_event_id,
|
||||
userId: payload.user_id,
|
||||
guildId: payload.guild_id,
|
||||
});
|
||||
};
|
||||
|
||||
export const raw: RawHandler<unknown> = (session, shardId, data) => {
|
||||
session.events.emit('raw', data as { t: string; d: unknown }, shardId);
|
||||
};
|
||||
|
||||
export interface Ready extends Omit<DiscordReady, 'user'> {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface Events {
|
||||
ready: Handler<[Ready, number]>;
|
||||
messageCreate: Handler<[Message]>;
|
||||
messageUpdate: Handler<[Partial<Message>]>;
|
||||
messageDelete: Handler<
|
||||
[{ id: Snowflake; channelId: Snowflake; guildId?: Snowflake }]
|
||||
>;
|
||||
messageReactionAdd: Handler<[MessageReactionAdd]>;
|
||||
messageReactionRemove: Handler<[MessageReactionRemove]>;
|
||||
messageReactionRemoveAll: Handler<[MessageReactionRemoveAll]>;
|
||||
messageReactionRemoveEmoji: Handler<[MessageReactionRemoveEmoji]>;
|
||||
guildCreate: Handler<[Guild]>;
|
||||
guildDelete: Handler<[{ id: Snowflake; unavailable: boolean }]>;
|
||||
guildMemberAdd: Handler<[Member]>;
|
||||
guildMemberUpdate: Handler<[Member]>;
|
||||
guildMemberRemove: Handler<[User, Snowflake]>;
|
||||
guildBanAdd: Handler<[{ guildId: Snowflake; user: DiscordUser }]>;
|
||||
guildBanRemove: Handler<[{ guildId: Snowflake; user: DiscordUser }]>;
|
||||
guildEmojisUpdate: Handler<
|
||||
[{ guildId: Snowflake; emojis: DiscordEmoji[] }]
|
||||
>;
|
||||
guildRoleCreate: Handler<[{ guildId: Snowflake; role: DiscordRole }]>;
|
||||
guildRoleUpdate: Handler<[{ guildId: Snowflake; role: DiscordRole }]>;
|
||||
guildRoleDelete: Handler<[{ guildId: Snowflake; roleId: Snowflake }]>;
|
||||
typingStart: Handler<
|
||||
[
|
||||
{
|
||||
channelId: Snowflake;
|
||||
guildId?: Snowflake;
|
||||
userId: Snowflake;
|
||||
timestamp: number;
|
||||
member?: Member;
|
||||
}
|
||||
]
|
||||
>;
|
||||
channelCreate: Handler<[Channel]>;
|
||||
channelUpdate: Handler<[Channel]>;
|
||||
channelDelete: Handler<[GuildChannel]>;
|
||||
channelPinsUpdate: Handler<
|
||||
[
|
||||
{
|
||||
guildId?: Snowflake;
|
||||
channelId: Snowflake;
|
||||
lastPinTimestamp?: number;
|
||||
}
|
||||
]
|
||||
>;
|
||||
threadCreate: Handler<[ThreadChannel]>;
|
||||
threadUpdate: Handler<[ThreadChannel]>;
|
||||
threadDelete: Handler<[ThreadChannel]>;
|
||||
threadListSync: Handler<
|
||||
[
|
||||
{
|
||||
guildId: Snowflake;
|
||||
channelIds: Snowflake[];
|
||||
threads: ThreadChannel[];
|
||||
members: ThreadMember[];
|
||||
}
|
||||
]
|
||||
>;
|
||||
threadMemberUpdate: Handler<
|
||||
[
|
||||
{
|
||||
id: Snowflake;
|
||||
userId: Snowflake;
|
||||
guildId: Snowflake;
|
||||
joinedAt: string;
|
||||
flags: number;
|
||||
}
|
||||
]
|
||||
>;
|
||||
threadMembersUpdate: Handler<
|
||||
[
|
||||
{
|
||||
id: Snowflake;
|
||||
memberCount: number;
|
||||
addedMembers?: ThreadMember[];
|
||||
guildId: Snowflake;
|
||||
removedMemberIds?: Snowflake[];
|
||||
}
|
||||
]
|
||||
>;
|
||||
interactionCreate: Handler<[Interaction]>;
|
||||
integrationCreate: Handler<[Integration]>;
|
||||
integrationUpdate: Handler<[Integration]>;
|
||||
integrationDelete: Handler<
|
||||
[{ id: Snowflake; guildId?: Snowflake; applicationId?: Snowflake }]
|
||||
>;
|
||||
inviteCreate: Handler<[InviteCreate]>;
|
||||
inviteDelete: Handler<
|
||||
[{ channelId: string; guildId?: string; code: string }]
|
||||
>;
|
||||
autoModerationRuleCreate: Handler<[AutoModerationRule]>;
|
||||
autoModerationRuleUpdate: Handler<[AutoModerationRule]>;
|
||||
autoModerationRuleDelete: Handler<[AutoModerationRule]>;
|
||||
autoModerationActionExecution: Handler<[AutoModerationExecution]>;
|
||||
stageInstanceCreate: Handler<[StageInstance]>;
|
||||
stageInstanceUpdate: Handler<[StageInstance]>;
|
||||
stageInstanceDelete: Handler<[StageInstance]>;
|
||||
guildScheduledEventCreate: Handler<[ScheduledEvent]>;
|
||||
guildScheduledEventUpdate: Handler<[ScheduledEvent]>;
|
||||
guildScheduledEventDelete: Handler<[ScheduledEvent]>;
|
||||
guildScheduledEventUserAdd: Handler<
|
||||
[{ scheduledEventId: Snowflake; userId: Snowflake; guildId: Snowflake }]
|
||||
>;
|
||||
guildScheduledEventUserRemove: Handler<
|
||||
[{ scheduledEventId: Snowflake; userId: Snowflake; guildId: Snowflake }]
|
||||
>;
|
||||
raw: Handler<[{ t: string; d: unknown }, number]>;
|
||||
webhooksUpdate: Handler<[{ guildId: Snowflake; channelId: Snowflake }]>;
|
||||
userUpdate: Handler<[User]>;
|
||||
presenceUpdate: Handler<[Presence]>;
|
||||
debug: Handler<[string]>;
|
||||
}
|
212
packages/core/src/biscuit.ts
Normal file
212
packages/core/src/biscuit.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import type {
|
||||
DiscordGatewayPayload,
|
||||
GatewayIntents,
|
||||
Snowflake,
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
// DiscordGetGatewayBot;
|
||||
|
||||
import type { RestAdapter } from '@biscuitland/rest';
|
||||
import { DefaultRestAdapter } from '@biscuitland/rest';
|
||||
|
||||
import type { WsAdapter } from '@biscuitland/ws';
|
||||
import { DefaultWsAdapter } from '@biscuitland/ws';
|
||||
|
||||
import type { EventAdapter } from './adapters/event-adapter';
|
||||
import { DefaultEventAdapter } from './adapters/default-event-adapter';
|
||||
|
||||
import { Util } from './utils/util';
|
||||
import { Shard } from '@biscuitland/ws';
|
||||
|
||||
export type DiscordRawEventHandler = (
|
||||
shard: Shard,
|
||||
data: MessageEvent<any>
|
||||
) => unknown;
|
||||
|
||||
export type PickOptions = Pick<
|
||||
BiscuitOptions,
|
||||
Exclude<keyof BiscuitOptions, keyof typeof Session.DEFAULTS>
|
||||
> &
|
||||
Partial<BiscuitOptions>;
|
||||
|
||||
export interface BiscuitOptions {
|
||||
intents?: GatewayIntents;
|
||||
token: string;
|
||||
|
||||
events?: {
|
||||
adapter?: { new (...args: any[]): EventAdapter };
|
||||
options: any;
|
||||
};
|
||||
|
||||
rest: {
|
||||
adapter?: { new (...args: any[]): RestAdapter };
|
||||
options: any;
|
||||
};
|
||||
|
||||
ws: {
|
||||
adapter?: { new (...args: any[]): WsAdapter };
|
||||
options: any;
|
||||
};
|
||||
}
|
||||
import * as Actions from './adapters/events';
|
||||
|
||||
export class Session {
|
||||
#applicationId?: Snowflake;
|
||||
#botId?: Snowflake;
|
||||
token: string;
|
||||
|
||||
set botId(snowflake: Snowflake) {
|
||||
this.#botId = snowflake;
|
||||
}
|
||||
|
||||
get botId(): Snowflake {
|
||||
return this.#botId ?? Util.getBotIdFromToken(this.token);
|
||||
}
|
||||
|
||||
set applicationId(snowflake: Snowflake) {
|
||||
this.#applicationId = snowflake;
|
||||
}
|
||||
|
||||
get applicationId(): Snowflake {
|
||||
return this.#applicationId ?? this.botId;
|
||||
}
|
||||
|
||||
static readonly DEFAULTS = {
|
||||
rest: {
|
||||
adapter: DefaultRestAdapter,
|
||||
options: null,
|
||||
},
|
||||
ws: {
|
||||
adapter: DefaultWsAdapter,
|
||||
options: null,
|
||||
},
|
||||
};
|
||||
|
||||
options: BiscuitOptions;
|
||||
|
||||
readonly events: EventAdapter;
|
||||
|
||||
readonly rest: RestAdapter;
|
||||
readonly ws: WsAdapter;
|
||||
|
||||
private adapters = new Map<string, any>();
|
||||
|
||||
constructor(options: PickOptions) {
|
||||
this.options = Object.assign(options, Session.DEFAULTS);
|
||||
|
||||
// makeRest
|
||||
|
||||
if (!this.options.rest.options) {
|
||||
this.options.rest.options = {
|
||||
intents: this.options.intents,
|
||||
token: this.options.token,
|
||||
};
|
||||
}
|
||||
|
||||
this.rest = this.getRest();
|
||||
|
||||
// makeWs
|
||||
|
||||
const defHandler: DiscordRawEventHandler = (shard, event) => {
|
||||
let message = event.data;
|
||||
let data = JSON.parse(message) as DiscordGatewayPayload;
|
||||
|
||||
Actions.raw(this, shard.id, data);
|
||||
|
||||
if (!data.t || !data.d) {
|
||||
return;
|
||||
}
|
||||
|
||||
Actions[data.t as keyof typeof Actions]?.(
|
||||
this,
|
||||
shard.id,
|
||||
data.d as any
|
||||
);
|
||||
};
|
||||
|
||||
if (!this.options.ws.options) {
|
||||
this.options.ws.options = {
|
||||
handleDiscordPayload: defHandler,
|
||||
|
||||
gatewayConfig: {
|
||||
token: this.options.token,
|
||||
intents: this.options.intents,
|
||||
},
|
||||
|
||||
intents: this.options.intents,
|
||||
token: this.options.token,
|
||||
};
|
||||
}
|
||||
|
||||
// makeEvents
|
||||
|
||||
this.events = this.options.events?.adapter
|
||||
? new this.options.events.adapter()
|
||||
: new DefaultEventAdapter();
|
||||
|
||||
this.ws = this.getWs();
|
||||
this.token = options.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private getAdapter<T extends { new (...args: any[]): InstanceType<T> }>(
|
||||
adapter: T,
|
||||
...args: ConstructorParameters<T>
|
||||
): InstanceType<T> {
|
||||
if (!this.adapters.has(adapter.name)) {
|
||||
const Class = adapter as { new (...args: any[]): T };
|
||||
this.adapters.set(adapter.name, new Class(...args));
|
||||
}
|
||||
|
||||
return this.adapters.get(adapter.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private getRest(): RestAdapter {
|
||||
return this.getAdapter(
|
||||
this.options.rest.adapter!,
|
||||
this.options.rest.options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private getWs(): WsAdapter {
|
||||
return this.getAdapter(
|
||||
this.options.ws.adapter!,
|
||||
this.options.ws.options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async start(): Promise<void> {
|
||||
const nonParsed = await this.rest.get<any>('/gateway/bot');
|
||||
|
||||
this.ws.options.gatewayBot = {
|
||||
url: nonParsed.url,
|
||||
shards: nonParsed.shards,
|
||||
sessionStartLimit: {
|
||||
total: nonParsed.session_start_limit.total,
|
||||
remaining: nonParsed.session_start_limit.remaining,
|
||||
resetAfter: nonParsed.session_start_limit.reset_after,
|
||||
maxConcurrency: nonParsed.session_start_limit.max_concurrency,
|
||||
},
|
||||
};
|
||||
|
||||
this.ws.options.lastShardId = this.ws.options.gatewayBot.shards - 1;
|
||||
this.ws.agent.options.totalShards = this.ws.options.gatewayBot.shards;
|
||||
|
||||
this.ws.shards();
|
||||
}
|
||||
}
|
49
packages/core/src/builders/components/InputTextBuilder.ts
Normal file
49
packages/core/src/builders/components/InputTextBuilder.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { DiscordInputTextComponent, MessageComponentTypes, TextStyles } from '@biscuitland/api-types';
|
||||
|
||||
export class InputTextBuilder {
|
||||
constructor() {
|
||||
this.#data = {} as DiscordInputTextComponent;
|
||||
this.type = 4;
|
||||
}
|
||||
#data: DiscordInputTextComponent;
|
||||
type: MessageComponentTypes.InputText;
|
||||
|
||||
setStyle(style: TextStyles): this {
|
||||
this.#data.style = style;
|
||||
return this;
|
||||
}
|
||||
|
||||
setLabel(label: string): this {
|
||||
this.#data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
setPlaceholder(placeholder: string): this {
|
||||
this.#data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
setLength(max?: number, min?: number): this {
|
||||
this.#data.max_length = max;
|
||||
this.#data.min_length = min;
|
||||
return this;
|
||||
}
|
||||
|
||||
setCustomId(id: string): this {
|
||||
this.#data.custom_id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(value: string): this {
|
||||
this.#data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setRequired(required = true): this {
|
||||
this.#data.required = required;
|
||||
return this;
|
||||
}
|
||||
toJSON(): DiscordInputTextComponent {
|
||||
return { ...this.#data, type: this.type };
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import type { DiscordActionRow, MessageComponentTypes } from '@biscuitland/api-types';
|
||||
import type { ComponentBuilder } from '../../utils/util';
|
||||
|
||||
export class ActionRowBuilder<T extends ComponentBuilder> {
|
||||
constructor() {
|
||||
this.components = [] as T[];
|
||||
this.type = 1;
|
||||
}
|
||||
components: T[];
|
||||
type: MessageComponentTypes.ActionRow;
|
||||
|
||||
addComponents(...components: T[]): this {
|
||||
this.components.push(...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
setComponents(...components: T[]): this {
|
||||
this.components.splice(
|
||||
0,
|
||||
this.components.length,
|
||||
...components,
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON(): DiscordActionRow {
|
||||
return {
|
||||
type: this.type,
|
||||
// @ts-ignore: socram fix this
|
||||
components: this.components.map((c) => c.toJSON()) as DiscordActionRow['components'],
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import { ButtonStyles, DiscordButtonComponent, MessageComponentTypes } from '@biscuitland/api-types';
|
||||
import type { ComponentEmoji } from '../../utils/util';
|
||||
|
||||
export class ButtonBuilder {
|
||||
constructor() {
|
||||
this.#data = {} as DiscordButtonComponent;
|
||||
this.type = MessageComponentTypes.Button;
|
||||
}
|
||||
#data: DiscordButtonComponent;
|
||||
type: MessageComponentTypes.Button;
|
||||
|
||||
setStyle(style: ButtonStyles): this {
|
||||
this.#data.style = style;
|
||||
return this;
|
||||
}
|
||||
|
||||
setLabel(label: string): this {
|
||||
this.#data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
setCustomId(id: string): this {
|
||||
this.#data.custom_id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
setEmoji(emoji: ComponentEmoji): this {
|
||||
this.#data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDisabled(disabled = true): this {
|
||||
this.#data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
setURL(url: string): this {
|
||||
this.#data.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON(): DiscordButtonComponent {
|
||||
return { ...this.#data, type: this.type };
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import type { DiscordSelectOption, DiscordSelectMenuComponent, } from '@biscuitland/api-types';
|
||||
import type { ComponentEmoji } from '../../utils/util';
|
||||
import { MessageComponentTypes } from '@biscuitland/api-types';
|
||||
|
||||
export class SelectMenuOptionBuilder {
|
||||
constructor() {
|
||||
this.#data = {} as DiscordSelectOption;
|
||||
}
|
||||
#data: DiscordSelectOption;
|
||||
|
||||
setLabel(label: string): SelectMenuOptionBuilder {
|
||||
this.#data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(value: string): SelectMenuOptionBuilder {
|
||||
this.#data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDescription(description: string): SelectMenuOptionBuilder {
|
||||
this.#data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDefault(Default = true): SelectMenuOptionBuilder {
|
||||
this.#data.default = Default;
|
||||
return this;
|
||||
}
|
||||
|
||||
setEmoji(emoji: ComponentEmoji): SelectMenuOptionBuilder {
|
||||
this.#data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON(): DiscordSelectOption {
|
||||
return { ...this.#data };
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectMenuBuilder {
|
||||
constructor() {
|
||||
this.#data = {} as DiscordSelectMenuComponent;
|
||||
this.type = MessageComponentTypes.SelectMenu;
|
||||
this.options = [];
|
||||
}
|
||||
#data: DiscordSelectMenuComponent;
|
||||
type: MessageComponentTypes.SelectMenu;
|
||||
options: SelectMenuOptionBuilder[];
|
||||
|
||||
setPlaceholder(placeholder: string): this {
|
||||
this.#data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
setValues(max?: number, min?: number): this {
|
||||
this.#data.max_values = max;
|
||||
this.#data.min_values = min;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDisabled(disabled = true): this {
|
||||
this.#data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
setCustomId(id: string): this {
|
||||
this.#data.custom_id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
setOptions(...options: SelectMenuOptionBuilder[]): this {
|
||||
this.options.splice(
|
||||
0,
|
||||
this.options.length,
|
||||
...options,
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
addOptions(...options: SelectMenuOptionBuilder[]): this {
|
||||
this.options.push(
|
||||
...options,
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON(): DiscordSelectMenuComponent {
|
||||
return { ...this.#data, type: this.type, options: this.options.map((option) => option.toJSON()) };
|
||||
}
|
||||
}
|
109
packages/core/src/builders/embed-builder.ts
Normal file
109
packages/core/src/builders/embed-builder.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import type { DiscordEmbed, DiscordEmbedField, DiscordEmbedProvider } from '@biscuitland/api-types';
|
||||
|
||||
export interface EmbedFooter {
|
||||
text: string;
|
||||
iconUrl?: string;
|
||||
proxyIconUrl?: string;
|
||||
}
|
||||
|
||||
export interface EmbedAuthor {
|
||||
name: string;
|
||||
text?: string;
|
||||
url?: string;
|
||||
iconUrl?: string;
|
||||
proxyIconUrl?: string;
|
||||
}
|
||||
|
||||
export interface EmbedVideo {
|
||||
height?: number;
|
||||
proxyUrl?: string;
|
||||
url?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export class EmbedBuilder {
|
||||
#data: DiscordEmbed;
|
||||
constructor(data: DiscordEmbed = {}) {
|
||||
this.#data = data;
|
||||
if (!this.#data.fields) this.#data.fields = [];
|
||||
}
|
||||
|
||||
setAuthor(author: EmbedAuthor): EmbedBuilder {
|
||||
this.#data.author = {
|
||||
name: author.name,
|
||||
icon_url: author.iconUrl,
|
||||
proxy_icon_url: author.proxyIconUrl,
|
||||
url: author.url,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
setColor(color: number): EmbedBuilder {
|
||||
this.#data.color = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDescription(description: string): EmbedBuilder {
|
||||
this.#data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
addField(field: DiscordEmbedField): EmbedBuilder {
|
||||
this.#data.fields!.push(field);
|
||||
return this;
|
||||
}
|
||||
|
||||
setFooter(footer: EmbedFooter): EmbedBuilder {
|
||||
this.#data.footer = {
|
||||
text: footer.text,
|
||||
icon_url: footer.iconUrl,
|
||||
proxy_icon_url: footer.proxyIconUrl,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
setImage(image: string): EmbedBuilder {
|
||||
this.#data.image = { url: image };
|
||||
return this;
|
||||
}
|
||||
|
||||
setProvider(provider: DiscordEmbedProvider): EmbedBuilder {
|
||||
this.#data.provider = provider;
|
||||
return this;
|
||||
}
|
||||
|
||||
setThumbnail(thumbnail: string): EmbedBuilder {
|
||||
this.#data.thumbnail = { url: thumbnail };
|
||||
return this;
|
||||
}
|
||||
|
||||
setTimestamp(timestamp: string | Date): EmbedBuilder {
|
||||
this.#data.timestamp = timestamp instanceof Date ? timestamp.toISOString() : timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
setTitle(title: string, url?: string): EmbedBuilder {
|
||||
this.#data.title = title;
|
||||
if (url) this.setUrl(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
setUrl(url: string): EmbedBuilder {
|
||||
this.#data.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
setVideo(video: EmbedVideo): EmbedBuilder {
|
||||
this.#data.video = {
|
||||
height: video.height,
|
||||
proxy_url: video.proxyUrl,
|
||||
url: video.url,
|
||||
width: video.width,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON(): DiscordEmbed {
|
||||
return this.#data;
|
||||
}
|
||||
}
|
129
packages/core/src/builders/slash/ApplicationCommand.ts
Normal file
129
packages/core/src/builders/slash/ApplicationCommand.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import type { Localization, PermissionStrings, DiscordApplicationCommandOption } from '@biscuitland/api-types';
|
||||
import type { PermissionResolvable } from '../../structures/special/permissions';
|
||||
import { ApplicationCommandTypes } from '@biscuitland/api-types';
|
||||
import { OptionBased } from './ApplicationCommandOption';
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/interactions/application-commands#endpoints-json-params
|
||||
*/
|
||||
export interface CreateApplicationCommand {
|
||||
name: string;
|
||||
nameLocalizations?: Localization;
|
||||
description: string;
|
||||
descriptionLocalizations?: Localization;
|
||||
type?: ApplicationCommandTypes;
|
||||
options?: DiscordApplicationCommandOption[];
|
||||
defaultMemberPermissions?: PermissionResolvable;
|
||||
dmPermission?: boolean;
|
||||
}
|
||||
|
||||
export abstract class ApplicationCommandBuilder implements CreateApplicationCommand {
|
||||
constructor(
|
||||
type: ApplicationCommandTypes = ApplicationCommandTypes.ChatInput,
|
||||
name: string = '',
|
||||
description: string = '',
|
||||
defaultMemberPermissions?: PermissionStrings[],
|
||||
nameLocalizations?: Localization,
|
||||
descriptionLocalizations?: Localization,
|
||||
dmPermission: boolean = true,
|
||||
) {
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.defaultMemberPermissions = defaultMemberPermissions;
|
||||
this.nameLocalizations = nameLocalizations;
|
||||
this.descriptionLocalizations = descriptionLocalizations;
|
||||
this.dmPermission = dmPermission;
|
||||
}
|
||||
type: ApplicationCommandTypes;
|
||||
name: string;
|
||||
description: string;
|
||||
defaultMemberPermissions?: PermissionStrings[];
|
||||
nameLocalizations?: Localization;
|
||||
descriptionLocalizations?: Localization;
|
||||
dmPermission: boolean;
|
||||
|
||||
setType(type: ApplicationCommandTypes): this {
|
||||
return (this.type = type), this;
|
||||
}
|
||||
|
||||
setName(name: string): this {
|
||||
return (this.name = name), this;
|
||||
}
|
||||
|
||||
setDescription(description: string): this {
|
||||
return (this.description = description), this;
|
||||
}
|
||||
|
||||
setDefaultMemberPermission(perm: PermissionStrings[]): this {
|
||||
return (this.defaultMemberPermissions = perm), this;
|
||||
}
|
||||
|
||||
setNameLocalizations(l: Localization): this {
|
||||
return (this.nameLocalizations = l), this;
|
||||
}
|
||||
|
||||
setDescriptionLocalizations(l: Localization): this {
|
||||
return (this.descriptionLocalizations = l), this;
|
||||
}
|
||||
|
||||
setDmPermission(perm: boolean): this {
|
||||
return (this.dmPermission = perm), this;
|
||||
}
|
||||
}
|
||||
|
||||
export type MessageApplicationCommandBuilderJSON = { name: string; type: ApplicationCommandTypes.Message };
|
||||
|
||||
export class MessageApplicationCommandBuilder {
|
||||
type: ApplicationCommandTypes;
|
||||
name?: string;
|
||||
constructor(
|
||||
type?: ApplicationCommandTypes,
|
||||
name?: string,
|
||||
) {
|
||||
this.type = type ?? ApplicationCommandTypes.Message;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
setName(name: string): this {
|
||||
return (this.name = name), this;
|
||||
}
|
||||
|
||||
toJSON(): MessageApplicationCommandBuilderJSON {
|
||||
if (!this.name) throw new TypeError('Propety \'name\' is required');
|
||||
|
||||
return {
|
||||
type: ApplicationCommandTypes.Message,
|
||||
name: this.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatInputApplicationCommandBuilder extends ApplicationCommandBuilder {
|
||||
type: ApplicationCommandTypes.ChatInput = ApplicationCommandTypes.ChatInput;
|
||||
|
||||
toJSON(): CreateApplicationCommand {
|
||||
if (!this.type) throw new TypeError('Propety \'type\' is required');
|
||||
if (!this.name) throw new TypeError('Propety \'name\' is required');
|
||||
if (!this.description) {
|
||||
throw new TypeError('Propety \'description\' is required');
|
||||
}
|
||||
|
||||
return {
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
options: this.options?.map((o) => o.toJSON()) ?? [],
|
||||
defaultMemberPermissions: this.defaultMemberPermissions,
|
||||
nameLocalizations: this.nameLocalizations,
|
||||
descriptionLocalizations: this.descriptionLocalizations,
|
||||
dmPermission: this.dmPermission,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
OptionBased.applyTo(ChatInputApplicationCommandBuilder);
|
||||
|
||||
export interface ChatInputApplicationCommandBuilder extends ApplicationCommandBuilder, OptionBased {
|
||||
// pass
|
||||
}
|
346
packages/core/src/builders/slash/ApplicationCommandOption.ts
Normal file
346
packages/core/src/builders/slash/ApplicationCommandOption.ts
Normal file
@ -0,0 +1,346 @@
|
||||
import { ApplicationCommandOptionTypes, ChannelTypes, Localization } from '@biscuitland/api-types';
|
||||
import { ApplicationCommandOptionChoice } from '../../structures/interactions';
|
||||
|
||||
export class ChoiceBuilder {
|
||||
name?: string;
|
||||
value?: string;
|
||||
|
||||
setName(name: string): ChoiceBuilder {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(value: string): this {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON(): ApplicationCommandOptionChoice {
|
||||
if (!this.name) throw new TypeError('Property \'name\' is required');
|
||||
if (!this.value) throw new TypeError('Property \'value\' is required');
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
value: this.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionBuilder {
|
||||
required?: boolean;
|
||||
autocomplete?: boolean;
|
||||
type?: ApplicationCommandOptionTypes;
|
||||
name?: string;
|
||||
description?: string;
|
||||
|
||||
constructor(type?: ApplicationCommandOptionTypes, name?: string, description?: string) {
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
setType(type: ApplicationCommandOptionTypes): this {
|
||||
return (this.type = type), this;
|
||||
}
|
||||
|
||||
setName(name: string): this {
|
||||
return (this.name = name), this;
|
||||
}
|
||||
|
||||
setDescription(description: string): this {
|
||||
return (this.description = description), this;
|
||||
}
|
||||
|
||||
setRequired(required: boolean): this {
|
||||
return (this.required = required), this;
|
||||
}
|
||||
|
||||
toJSON(): ApplicationCommandOption {
|
||||
if (!this.type) throw new TypeError('Property \'type\' is required');
|
||||
if (!this.name) throw new TypeError('Property \'name\' is required');
|
||||
if (!this.description) {
|
||||
throw new TypeError('Property \'description\' is required');
|
||||
}
|
||||
|
||||
const applicationCommandOption: ApplicationCommandOption = {
|
||||
type: this.type,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
required: this.required ? true : false,
|
||||
};
|
||||
|
||||
return applicationCommandOption;
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionBuilderLimitedValues extends OptionBuilder {
|
||||
choices?: ChoiceBuilder[];
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
|
||||
constructor(
|
||||
type?: ApplicationCommandOptionTypes.Integer | ApplicationCommandOptionTypes.Number,
|
||||
name?: string,
|
||||
description?: string,
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
setMinValue(n: number): this {
|
||||
return (this.minValue = n), this;
|
||||
}
|
||||
|
||||
setMaxValue(n: number): this {
|
||||
return (this.maxValue = n), this;
|
||||
}
|
||||
|
||||
addChoice(fn: (choice: ChoiceBuilder) => ChoiceBuilder): this {
|
||||
const choice = fn(new ChoiceBuilder());
|
||||
this.choices ??= [];
|
||||
this.choices.push(choice);
|
||||
return this;
|
||||
}
|
||||
|
||||
override toJSON(): ApplicationCommandOption {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
choices: this.choices?.map((c) => c.toJSON()) ?? [],
|
||||
minValue: this.minValue,
|
||||
maxValue: this.maxValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionBuilderString extends OptionBuilder {
|
||||
choices?: ChoiceBuilder[];
|
||||
constructor(
|
||||
type?: ApplicationCommandOptionTypes.String,
|
||||
name?: string,
|
||||
description?: string,
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this;
|
||||
}
|
||||
|
||||
addChoice(fn: (choice: ChoiceBuilder) => ChoiceBuilder): this {
|
||||
const choice = fn(new ChoiceBuilder());
|
||||
this.choices ??= [];
|
||||
this.choices.push(choice);
|
||||
return this;
|
||||
}
|
||||
|
||||
override toJSON(): ApplicationCommandOption {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
choices: this.choices?.map((c) => c.toJSON()) ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionBuilderChannel extends OptionBuilder {
|
||||
channelTypes?: ChannelTypes[];
|
||||
constructor(
|
||||
type?: ApplicationCommandOptionTypes.Channel,
|
||||
name?: string,
|
||||
description?: string,
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this;
|
||||
}
|
||||
|
||||
addChannelTypes(...channels: ChannelTypes[]): this {
|
||||
this.channelTypes ??= [];
|
||||
this.channelTypes.push(...channels);
|
||||
return this;
|
||||
}
|
||||
|
||||
override toJSON(): ApplicationCommandOption {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
channelTypes: this.channelTypes ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface OptionBuilderLike {
|
||||
toJSON(): ApplicationCommandOption;
|
||||
}
|
||||
|
||||
export class OptionBased {
|
||||
options?:
|
||||
& (
|
||||
| OptionBuilder[]
|
||||
| OptionBuilderString[]
|
||||
| OptionBuilderLimitedValues[]
|
||||
| OptionBuilderNested[]
|
||||
| OptionBuilderChannel[]
|
||||
)
|
||||
& OptionBuilderLike[];
|
||||
|
||||
addOption(fn: (option: OptionBuilder) => OptionBuilder, type?: ApplicationCommandOptionTypes): this {
|
||||
const option = fn(new OptionBuilder(type));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addNestedOption(fn: (option: OptionBuilder) => OptionBuilder): this {
|
||||
const option = fn(new OptionBuilder(ApplicationCommandOptionTypes.SubCommand));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addStringOption(fn: (option: OptionBuilderString) => OptionBuilderString): this {
|
||||
const option = fn(new OptionBuilderString(ApplicationCommandOptionTypes.String));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addIntegerOption(fn: (option: OptionBuilderLimitedValues) => OptionBuilderLimitedValues): this {
|
||||
const option = fn(new OptionBuilderLimitedValues(ApplicationCommandOptionTypes.Integer));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addNumberOption(fn: (option: OptionBuilderLimitedValues) => OptionBuilderLimitedValues): this {
|
||||
const option = fn(new OptionBuilderLimitedValues(ApplicationCommandOptionTypes.Number));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addBooleanOption(fn: (option: OptionBuilder) => OptionBuilder): this {
|
||||
return this.addOption(fn, ApplicationCommandOptionTypes.Boolean);
|
||||
}
|
||||
|
||||
addSubCommand(fn: (option: OptionBuilderNested) => OptionBuilderNested): this {
|
||||
const option = fn(new OptionBuilderNested(ApplicationCommandOptionTypes.SubCommand));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addSubCommandGroup(fn: (option: OptionBuilderNested) => OptionBuilderNested): this {
|
||||
const option = fn(new OptionBuilderNested(ApplicationCommandOptionTypes.SubCommandGroup));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addUserOption(fn: (option: OptionBuilder) => OptionBuilder): this {
|
||||
return this.addOption(fn, ApplicationCommandOptionTypes.User);
|
||||
}
|
||||
|
||||
addChannelOption(fn: (option: OptionBuilderChannel) => OptionBuilderChannel): this {
|
||||
const option = fn(new OptionBuilderChannel(ApplicationCommandOptionTypes.Channel));
|
||||
this.options ??= [];
|
||||
this.options.push(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addRoleOption(fn: (option: OptionBuilder) => OptionBuilder): this {
|
||||
return this.addOption(fn, ApplicationCommandOptionTypes.Role);
|
||||
}
|
||||
|
||||
addMentionableOption(fn: (option: OptionBuilder) => OptionBuilder): this {
|
||||
return this.addOption(fn, ApplicationCommandOptionTypes.Mentionable);
|
||||
}
|
||||
|
||||
// deno-lint-ignore ban-types
|
||||
static applyTo(klass: Function, ignore: Array<keyof OptionBased> = []): void {
|
||||
const methods: Array<keyof OptionBased> = [
|
||||
'addOption',
|
||||
'addNestedOption',
|
||||
'addStringOption',
|
||||
'addIntegerOption',
|
||||
'addNumberOption',
|
||||
'addBooleanOption',
|
||||
'addSubCommand',
|
||||
'addSubCommandGroup',
|
||||
'addUserOption',
|
||||
'addChannelOption',
|
||||
'addRoleOption',
|
||||
'addMentionableOption',
|
||||
];
|
||||
|
||||
for (const method of methods) {
|
||||
if (ignore.includes(method)) continue;
|
||||
|
||||
klass.prototype[method] = OptionBased.prototype[method];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OptionBuilderNested extends OptionBuilder {
|
||||
constructor(
|
||||
type?: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup,
|
||||
name?: string,
|
||||
description?: string,
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
override toJSON(): ApplicationCommandOption {
|
||||
if (!this.type) throw new TypeError('Property \'type\' is required');
|
||||
if (!this.name) throw new TypeError('Property \'name\' is required');
|
||||
if (!this.description) {
|
||||
throw new TypeError('Property \'description\' is required');
|
||||
}
|
||||
|
||||
return {
|
||||
type: this.type,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
options: this.options?.map((o) => o.toJSON()) ?? [],
|
||||
required: this.required ? true : false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
OptionBased.applyTo(OptionBuilderNested);
|
||||
|
||||
export interface OptionBuilderNested extends OptionBuilder, OptionBased {
|
||||
// pass
|
||||
}
|
||||
|
||||
export interface ApplicationCommandOption {
|
||||
/** Value of Application Command Option Type */
|
||||
type: ApplicationCommandOptionTypes;
|
||||
/** 1-32 character name matching lowercase `^[\w-]{1,32}$` */
|
||||
name: string;
|
||||
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
|
||||
nameLocalizations?: Localization;
|
||||
/** 1-100 character description */
|
||||
description: string;
|
||||
/** Localization object for the `description` field. Values follow the same restrictions as `description` */
|
||||
descriptionLocalizations?: Localization;
|
||||
/** If the parameter is required or optional--default `false` */
|
||||
required?: boolean;
|
||||
/** Choices for `string` and `int` types for the user to pick from */
|
||||
choices?: ApplicationCommandOptionChoice[];
|
||||
/** If the option is a subcommand or subcommand group type, this nested options will be the parameters */
|
||||
options?: ApplicationCommandOption[];
|
||||
/** if autocomplete interactions are enabled for this `String`, `Integer`, or `Number` type option */
|
||||
autocomplete?: boolean;
|
||||
/** If the option is a channel type, the channels shown will be restricted to these types */
|
||||
channelTypes?: ChannelTypes[];
|
||||
/** Minimum number desired. */
|
||||
minValue?: number;
|
||||
/** Maximum number desired. */
|
||||
maxValue?: number;
|
||||
}
|
21
packages/core/src/index.ts
Normal file
21
packages/core/src/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// SESSION
|
||||
export * as Actions from './adapters/events';
|
||||
|
||||
export { Session as Biscuit } from './biscuit';
|
||||
export * from './biscuit';
|
||||
|
||||
// STRUCTURES
|
||||
export * from './structures';
|
||||
|
||||
// EVENTS
|
||||
export * from './adapters/events';
|
||||
export * from './adapters/event-adapter';
|
||||
export * from './adapters/default-event-adapter';
|
||||
|
||||
// ETC
|
||||
export * from './snowflakes';
|
||||
|
||||
// UTIL
|
||||
export * from './utils/calculate-shard';
|
||||
export * from './utils/url-to-base-64';
|
||||
export * from './utils/util';
|
12
packages/core/src/snowflakes.ts
Normal file
12
packages/core/src/snowflakes.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/** snowflake type */
|
||||
export type Snowflake = string;
|
||||
|
||||
/** Discord epoch */
|
||||
export const DiscordEpoch = 14200704e5;
|
||||
|
||||
/** utilities for Snowflakes */
|
||||
export const Snowflake = {
|
||||
snowflakeToTimestamp(id: Snowflake): number {
|
||||
return (Number(id) >> 22) + DiscordEpoch;
|
||||
},
|
||||
};
|
42
packages/core/src/structures.ts
Normal file
42
packages/core/src/structures.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// STRUCTURES
|
||||
export * from './structures/application';
|
||||
export * from './structures/attachment';
|
||||
export * from './structures/automod';
|
||||
export * from './structures/base';
|
||||
export * from './structures/embed';
|
||||
export * from './structures/emojis';
|
||||
export * from './structures/scheduled-events';
|
||||
export * from './structures/integration';
|
||||
export * from './structures/invite';
|
||||
export * from './structures/members';
|
||||
export * from './structures/message';
|
||||
export * from './structures/message-reaction';
|
||||
export * from './structures/special/command-interaction-option-resolver';
|
||||
export * from './structures/special/permissions';
|
||||
export * from './structures/presence';
|
||||
export * from './structures/role';
|
||||
export * from './structures/stage-instance';
|
||||
export * from './structures/sticker';
|
||||
export * from './structures/user';
|
||||
export * from './structures/webhook';
|
||||
export * from './structures/welcome';
|
||||
|
||||
// INTERACTIONS
|
||||
export * from './structures/interactions';
|
||||
|
||||
// CHANNELS
|
||||
export * from './structures/channels';
|
||||
|
||||
// COMPONENTS
|
||||
export * from './structures/components';
|
||||
|
||||
// GUILDS
|
||||
export * from './structures/guilds';
|
||||
|
||||
// BUILDERS
|
||||
export * from './builders/components/InputTextBuilder';
|
||||
export * from './builders/components/MessageActionRowBuilder';
|
||||
export * from './builders/components/MessageButtonBuilder';
|
||||
export * from './builders/components/MessageSelectMenuBuilder';
|
||||
export * from './builders/slash/ApplicationCommand';
|
||||
export * from './builders/slash/ApplicationCommandOption';
|
112
packages/core/src/structures/application.ts
Normal file
112
packages/core/src/structures/application.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
DiscordApplication,
|
||||
DiscordInstallParams,
|
||||
DiscordTeam,
|
||||
DiscordUser,
|
||||
TeamMembershipStates,
|
||||
} from '@biscuitland/api-types';
|
||||
import { User } from './user';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type SummaryDeprecated = '';
|
||||
|
||||
/**
|
||||
* Discord team that holds members
|
||||
*/
|
||||
export interface Team {
|
||||
/** a hash of the image of the team's icon */
|
||||
icon?: string;
|
||||
/** the unique id of the team */
|
||||
id: string;
|
||||
/** the members of the team */
|
||||
members: TeamMember[];
|
||||
/** user id of the current team owner */
|
||||
ownerUserId: string;
|
||||
/** team name */
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
/** the user's membership state on the team */
|
||||
membershipState: TeamMembershipStates;
|
||||
permissions: '*'[];
|
||||
teamId: string;
|
||||
user: Partial<User> &
|
||||
Pick<User, 'avatarHash' | 'discriminator' | 'id' | 'username'>;
|
||||
}
|
||||
|
||||
// NewTeam create a new Team object for discord applications
|
||||
export function NewTeam(session: Session, data: DiscordTeam): Team {
|
||||
return {
|
||||
icon: data.icon ? data.icon : undefined,
|
||||
id: data.id,
|
||||
members: data.members.map(member => {
|
||||
return {
|
||||
membershipState: member.membership_state,
|
||||
permissions: member.permissions,
|
||||
teamId: member.team_id,
|
||||
user: new User(session, member.user),
|
||||
};
|
||||
}),
|
||||
ownerUserId: data.owner_user_id,
|
||||
name: data.name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/application#application-object
|
||||
*/
|
||||
export class Application implements Model {
|
||||
constructor(session: Session, data: DiscordApplication) {
|
||||
this.id = data.id;
|
||||
this.session = session;
|
||||
|
||||
this.name = data.name;
|
||||
this.icon = data.icon || undefined;
|
||||
this.description = data.description;
|
||||
this.rpcOrigins = data.rpc_origins;
|
||||
this.botPublic = data.bot_public;
|
||||
this.botRequireCodeGrant = data.bot_require_code_grant;
|
||||
this.termsOfServiceURL = data.terms_of_service_url;
|
||||
this.privacyPolicyURL = data.privacy_policy_url;
|
||||
this.owner = data.owner
|
||||
? new User(session, data.owner as DiscordUser)
|
||||
: undefined;
|
||||
this.summary = '';
|
||||
this.verifyKey = data.verify_key;
|
||||
this.team = data.team ? NewTeam(session, data.team) : undefined;
|
||||
this.guildId = data.guild_id;
|
||||
this.coverImage = data.cover_image;
|
||||
this.tags = data.tags;
|
||||
this.installParams = data.install_params;
|
||||
this.customInstallURL = data.custom_install_url;
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
id: Snowflake;
|
||||
name: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
rpcOrigins?: string[];
|
||||
botPublic: boolean;
|
||||
botRequireCodeGrant: boolean;
|
||||
termsOfServiceURL?: string;
|
||||
privacyPolicyURL?: string;
|
||||
owner?: Partial<User>;
|
||||
summary: SummaryDeprecated;
|
||||
verifyKey: string;
|
||||
team?: Team;
|
||||
guildId?: Snowflake;
|
||||
primarySkuId?: Snowflake;
|
||||
slug?: string;
|
||||
coverImage?: string;
|
||||
flags?: number;
|
||||
tags?: string[];
|
||||
installParams?: DiscordInstallParams;
|
||||
customInstallURL?: string;
|
||||
}
|
36
packages/core/src/structures/attachment.ts
Normal file
36
packages/core/src/structures/attachment.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { DiscordAttachment } from '@biscuitland/api-types';
|
||||
|
||||
/**
|
||||
* Represents an attachment
|
||||
* @link https://discord.com/developers/docs/resources/channel#attachment-object
|
||||
*/
|
||||
export class Attachment implements Model {
|
||||
constructor(session: Session, data: DiscordAttachment) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
|
||||
this.contentType = data.content_type ? data.content_type : undefined;
|
||||
this.attachment = data.url;
|
||||
this.proxyUrl = data.proxy_url;
|
||||
this.name = data.filename;
|
||||
this.size = data.size;
|
||||
this.height = data.height ? data.height : undefined;
|
||||
this.width = data.width ? data.width : undefined;
|
||||
this.ephemeral = !!data.ephemeral;
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
readonly id: Snowflake;
|
||||
|
||||
contentType?: string;
|
||||
attachment: string;
|
||||
proxyUrl: string;
|
||||
name: string;
|
||||
size: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
ephemeral: boolean;
|
||||
}
|
115
packages/core/src/structures/automod.ts
Normal file
115
packages/core/src/structures/automod.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import type { Model } from './base';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type {
|
||||
AutoModerationActionType,
|
||||
AutoModerationEventTypes,
|
||||
AutoModerationTriggerTypes,
|
||||
DiscordAutoModerationRule,
|
||||
DiscordAutoModerationRuleTriggerMetadataPresets,
|
||||
DiscordAutoModerationActionExecution,
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
export interface AutoModerationRuleTriggerMetadata {
|
||||
keywordFilter?: string[];
|
||||
presets?: DiscordAutoModerationRuleTriggerMetadataPresets[];
|
||||
}
|
||||
|
||||
export interface ActionMetadata {
|
||||
channelId: Snowflake;
|
||||
durationSeconds: number;
|
||||
}
|
||||
|
||||
export interface AutoModerationAction {
|
||||
type: AutoModerationActionType;
|
||||
metadata: ActionMetadata;
|
||||
}
|
||||
|
||||
export class AutoModerationRule implements Model {
|
||||
constructor(session: Session, data: DiscordAutoModerationRule) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.guildId = data.guild_id;
|
||||
this.name = data.name;
|
||||
this.creatorId = data.creator_id;
|
||||
this.eventType = data.event_type;
|
||||
this.triggerType = data.trigger_type;
|
||||
this.triggerMetadata = {
|
||||
keywordFilter: data.trigger_metadata.keyword_filter,
|
||||
presets: data.trigger_metadata.presets,
|
||||
};
|
||||
this.actions = data.actions.map(action =>
|
||||
Object.create({
|
||||
type: action.type,
|
||||
metadata: {
|
||||
channelId: action.metadata.channel_id,
|
||||
durationSeconds: action.metadata.duration_seconds,
|
||||
},
|
||||
})
|
||||
);
|
||||
this.enabled = !!data.enabled;
|
||||
this.exemptRoles = data.exempt_roles;
|
||||
this.exemptChannels = data.exempt_channels;
|
||||
}
|
||||
|
||||
session: Session;
|
||||
id: Snowflake;
|
||||
guildId: Snowflake;
|
||||
name: string;
|
||||
creatorId: Snowflake;
|
||||
eventType: AutoModerationEventTypes;
|
||||
triggerType: AutoModerationTriggerTypes;
|
||||
triggerMetadata: AutoModerationRuleTriggerMetadata;
|
||||
actions: AutoModerationAction[];
|
||||
enabled: boolean;
|
||||
exemptRoles: Snowflake[];
|
||||
exemptChannels: Snowflake[];
|
||||
}
|
||||
|
||||
export class AutoModerationExecution {
|
||||
constructor(session: Session, data: DiscordAutoModerationActionExecution) {
|
||||
this.session = session;
|
||||
this.guildId = data.guild_id;
|
||||
this.action = Object.create({
|
||||
type: data.action.type,
|
||||
metadata: {
|
||||
channelId: data.action.metadata.channel_id,
|
||||
durationSeconds: data.action.metadata.duration_seconds,
|
||||
},
|
||||
});
|
||||
this.ruleId = data.rule_id;
|
||||
this.ruleTriggerType = data.rule_trigger_type;
|
||||
this.userId = data.user_id;
|
||||
this.content = data.content;
|
||||
if (data.channel_id) {
|
||||
this.channelId = data.channel_id;
|
||||
}
|
||||
if (data.message_id) {
|
||||
this.messageId = data.message_id;
|
||||
}
|
||||
if (data.alert_system_message_id) {
|
||||
this.alertSystemMessageId = data.alert_system_message_id;
|
||||
}
|
||||
|
||||
if (data.matched_keyword) {
|
||||
this.matchedKeyword = data.matched_keyword;
|
||||
}
|
||||
|
||||
if (data.matched_content) {
|
||||
this.matched_content = data.matched_content;
|
||||
}
|
||||
}
|
||||
|
||||
session: Session;
|
||||
guildId: Snowflake;
|
||||
action: AutoModerationAction;
|
||||
ruleId: Snowflake;
|
||||
ruleTriggerType: AutoModerationTriggerTypes;
|
||||
userId: Snowflake;
|
||||
channelId?: Snowflake;
|
||||
messageId?: Snowflake;
|
||||
alertSystemMessageId?: Snowflake;
|
||||
content?: string;
|
||||
matchedKeyword?: string;
|
||||
matched_content?: string;
|
||||
}
|
12
packages/core/src/structures/base.ts
Normal file
12
packages/core/src/structures/base.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
|
||||
/**
|
||||
* Represents a Discord data model
|
||||
*/
|
||||
export interface Model {
|
||||
/** id of the model */
|
||||
id: Snowflake;
|
||||
/** reference to the client that instantiated the model */
|
||||
session: Session;
|
||||
}
|
897
packages/core/src/structures/channels.ts
Normal file
897
packages/core/src/structures/channels.ts
Normal file
@ -0,0 +1,897 @@
|
||||
/** Types */
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { PermissionsOverwrites } from '../utils/util';
|
||||
|
||||
/** Functions and others */
|
||||
// import { calculateShardId } from '../utils/calculate-shard';
|
||||
import { urlToBase64 } from '../utils/url-to-base-64';
|
||||
|
||||
/** Classes and routes */
|
||||
import type {
|
||||
DiscordChannel,
|
||||
DiscordInvite,
|
||||
DiscordInviteMetadata,
|
||||
DiscordListArchivedThreads,
|
||||
DiscordMessage,
|
||||
DiscordOverwrite,
|
||||
DiscordThreadMember,
|
||||
DiscordWebhook,
|
||||
TargetTypes,
|
||||
VideoQualityModes,
|
||||
GetReactions,
|
||||
GetMessagesOptions,
|
||||
ListArchivedThreads } from '@biscuitland/api-types';
|
||||
import {
|
||||
CHANNEL,
|
||||
CHANNEL_PINS,
|
||||
CHANNEL_INVITES,
|
||||
CHANNEL_TYPING,
|
||||
CHANNEL_MESSAGES,
|
||||
CHANNEL_WEBHOOKS,
|
||||
THREAD_USER,
|
||||
THREAD_ME,
|
||||
THREAD_MEMBERS,
|
||||
THREAD_START_PRIVATE,
|
||||
THREAD_ARCHIVED_PUBLIC,
|
||||
THREAD_ARCHIVED_PRIVATE_JOINED,
|
||||
THREAD_START_PUBLIC,
|
||||
ChannelTypes,
|
||||
GatewayOpcodes as _GatewayOpcodes
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
import type { CreateMessage, EditMessage, EmojiResolvable } from './message';
|
||||
import { Message } from './message';
|
||||
import { Invite } from './invite';
|
||||
import { Webhook } from './webhook';
|
||||
import { User } from './user';
|
||||
import { ThreadMember } from './members';
|
||||
import { Permissions } from './special/permissions';
|
||||
|
||||
/**
|
||||
* Abstract class that represents the base for creating a new channel.
|
||||
*/
|
||||
export abstract class BaseChannel implements Model {
|
||||
constructor(session: Session, data: DiscordChannel) {
|
||||
this.id = data.id;
|
||||
this.session = session;
|
||||
this.name = data.name;
|
||||
this.type = data.type;
|
||||
}
|
||||
|
||||
/** id's refers to the identification of the channel */
|
||||
readonly id: Snowflake;
|
||||
|
||||
/** The session that instantiated the channel */
|
||||
readonly session: Session;
|
||||
|
||||
/** Channel name defined by the entity */
|
||||
name?: string;
|
||||
|
||||
/** Refers to the possible channel type implemented (Guild, DM, Voice, News, etc...) */
|
||||
type: ChannelTypes;
|
||||
|
||||
/** If the channel is a TextChannel */
|
||||
isText(): this is TextChannel {
|
||||
return textBasedChannels.includes(this.type);
|
||||
}
|
||||
|
||||
/** If the channel is a VoiceChannel */
|
||||
isVoice(): this is VoiceChannel {
|
||||
return this.type === ChannelTypes.GuildVoice;
|
||||
}
|
||||
|
||||
/** If the channel is a DMChannel */
|
||||
isDM(): this is DMChannel {
|
||||
return this.type === ChannelTypes.DM;
|
||||
}
|
||||
|
||||
/** If the channel is a NewChannel */
|
||||
isNews(): this is NewsChannel {
|
||||
return this.type === ChannelTypes.GuildNews;
|
||||
}
|
||||
|
||||
/** If the channel is a ThreadChannel */
|
||||
isThread(): this is ThreadChannel {
|
||||
return this.type === ChannelTypes.GuildPublicThread || this.type === ChannelTypes.GuildPrivateThread;
|
||||
}
|
||||
|
||||
/** If the channel is a StageChannel */
|
||||
isStage(): this is StageChannel {
|
||||
return this.type === ChannelTypes.GuildStageVoice;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<#${this.id}>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a category channel.
|
||||
*/
|
||||
export class CategoryChannel extends BaseChannel {
|
||||
constructor(session: Session, data: DiscordChannel) {
|
||||
super(session, data);
|
||||
this.id = data.id;
|
||||
this.name = data.name ? data.name : '';
|
||||
this.nsfw = data.nsfw ? data.nsfw : false;
|
||||
this.guildId = data.guild_id ? data.guild_id : undefined;
|
||||
this.type = ChannelTypes.GuildCategory;
|
||||
this.position = data.position ? data.position : undefined;
|
||||
this.parentId = data.parent_id ? data.parent_id : undefined;
|
||||
|
||||
this.permissionOverwrites = data.permission_overwrites
|
||||
? ChannelFactory.permissionOverwrites(data.permission_overwrites)
|
||||
: [];
|
||||
}
|
||||
|
||||
id: Snowflake;
|
||||
parentId?: string;
|
||||
name: string;
|
||||
permissionOverwrites: PermissionsOverwrites[];
|
||||
nsfw: boolean;
|
||||
guildId?: Snowflake;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
/** TextChannel */
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params
|
||||
* Represents the options object to create an invitation
|
||||
*/
|
||||
export interface DiscordInviteOptions {
|
||||
/** duration of invite in seconds before expiry, or 0 for never. between 0 and 604800 (7 days) */
|
||||
maxAge?: number;
|
||||
/** max number of uses or 0 for unlimited. between 0 and 100 */
|
||||
maxUses?: number;
|
||||
/** if the invitation is unique. If it's true, don't try to reuse a similar invite (useful for creating many unique one time use invites) */
|
||||
unique?: boolean;
|
||||
/** whether this invite only grants temporary membership */
|
||||
temporary: boolean;
|
||||
reason?: string;
|
||||
/** the type of target for this voice channel invite */
|
||||
targetType?: TargetTypes;
|
||||
/** the id of the user whose stream to display for this invite, required if targetType is 1, the user must be streaming in the channel */
|
||||
targetUserId?: Snowflake;
|
||||
/** the id of the embedded application to open for this invite, required if targetType is 2, the application must have the EMBEDDED flag */
|
||||
targetApplicationId?: Snowflake;
|
||||
}
|
||||
|
||||
/** Webhook create object */
|
||||
export interface CreateWebhook {
|
||||
/** name of the webhook (1-80 characters) */
|
||||
name: string;
|
||||
/** image for the default webhook avatar */
|
||||
avatar?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** Available text-channel-types list */
|
||||
export const textBasedChannels: ChannelTypes[] = [
|
||||
ChannelTypes.DM,
|
||||
ChannelTypes.GroupDm,
|
||||
ChannelTypes.GuildPrivateThread,
|
||||
ChannelTypes.GuildPublicThread,
|
||||
ChannelTypes.GuildNews,
|
||||
ChannelTypes.GuildVoice,
|
||||
ChannelTypes.GuildText,
|
||||
];
|
||||
|
||||
/** Available text-channel-types */
|
||||
export type TextBasedChannels =
|
||||
| ChannelTypes.DM
|
||||
| ChannelTypes.GroupDm
|
||||
| ChannelTypes.GuildPrivateThread
|
||||
| ChannelTypes.GuildPublicThread
|
||||
| ChannelTypes.GuildNews
|
||||
| ChannelTypes.GuildVoice
|
||||
| ChannelTypes.GuildText;
|
||||
|
||||
/**
|
||||
* Represents a text channel.
|
||||
*/
|
||||
export class TextChannel {
|
||||
constructor(session: Session, data: DiscordChannel) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.type = data.type as number;
|
||||
this.rateLimitPerUser = data.rate_limit_per_user ?? 0;
|
||||
this.nsfw = !!data.nsfw ?? false;
|
||||
|
||||
if (data.last_message_id) {
|
||||
this.lastMessageId = data.last_message_id;
|
||||
}
|
||||
|
||||
if (data.last_pin_timestamp) {
|
||||
this.lastPinTimestamp = data.last_pin_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/** The session that instantiated the channel */
|
||||
readonly session: Session;
|
||||
|
||||
/** id's refers to the identification of the channel */
|
||||
readonly id: Snowflake;
|
||||
|
||||
/** Current channel name */
|
||||
name?: string;
|
||||
|
||||
/** The type of the channel */
|
||||
type: TextBasedChannels;
|
||||
|
||||
/** The id of the last message sent in this channel (or thread for GUILD_FORUM channels) (may not point to an existing or valid message or thread) */
|
||||
lastMessageId?: Snowflake;
|
||||
|
||||
/** When the last pinned message was pinned. This may be undefined in events such as GUILD_CREATE when a message is not pinned. */
|
||||
lastPinTimestamp?: string;
|
||||
|
||||
/** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */
|
||||
rateLimitPerUser: number;
|
||||
|
||||
/** If the channel is NSFW (Not-Safe-For-Work content) */
|
||||
nsfw: boolean;
|
||||
|
||||
/**
|
||||
* Mixin
|
||||
*/
|
||||
// deno-lint-ignore ban-types
|
||||
static applyTo(klass: Function, ignore: (keyof TextChannel)[] = []): void {
|
||||
const methods: (keyof TextChannel)[] = [
|
||||
'fetchPins',
|
||||
'createInvite',
|
||||
'fetchMessages',
|
||||
'sendTyping',
|
||||
'pinMessage',
|
||||
'unpinMessage',
|
||||
'addReaction',
|
||||
'removeReaction',
|
||||
'nukeReactions',
|
||||
'fetchPins',
|
||||
'sendMessage',
|
||||
'editMessage',
|
||||
'createWebhook',
|
||||
];
|
||||
|
||||
for (const method of methods) {
|
||||
if (ignore.includes(method)) { continue; }
|
||||
|
||||
klass.prototype[method] = TextChannel.prototype[method];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fetchPins makes an asynchronous request and gets the current channel pins.
|
||||
* @returns A promise that resolves with an array of Message objects.
|
||||
*/
|
||||
async fetchPins(): Promise<Message[] | []> {
|
||||
const messages = await this.session.rest.get<DiscordMessage[]>(
|
||||
CHANNEL_PINS(this.id),
|
||||
);
|
||||
|
||||
return messages[0] ? messages.map((x: DiscordMessage) => new Message(this.session, x)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* createInvite makes an asynchronous request to create a new invitation.
|
||||
* @param options - The options to create the invitation
|
||||
* @returns The created invite
|
||||
*/
|
||||
async createInvite(options?: DiscordInviteOptions): Promise<Invite> {
|
||||
const invite = await this.session.rest.post<DiscordInvite>(
|
||||
CHANNEL_INVITES(this.id),
|
||||
options
|
||||
? {
|
||||
max_age: options.maxAge,
|
||||
max_uses: options.maxUses,
|
||||
temporary: options.temporary,
|
||||
unique: options.unique,
|
||||
target_type: options.targetType,
|
||||
target_user_id: options.targetUserId,
|
||||
target_application_id: options.targetApplicationId,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
|
||||
return new Invite(this.session, invite);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetchMessages makes an asynchronous request and gets the channel messages
|
||||
* @param options - The options to get the messages
|
||||
* @returns The messages
|
||||
*/
|
||||
async fetchMessages(options?: GetMessagesOptions): Promise<Message[] | []> {
|
||||
if (options?.limit! > 100) { throw Error('Values must be between 0-100'); }
|
||||
const messages = await this.session.rest.get<DiscordMessage[]>(
|
||||
CHANNEL_MESSAGES(this.id, options),
|
||||
);
|
||||
|
||||
return messages[0] ? messages.map(x => new Message(this.session, x)) : [];
|
||||
}
|
||||
|
||||
/** sendTyping sends a typing POST request */
|
||||
async sendTyping(): Promise<void> {
|
||||
await this.session.rest.post<undefined>(CHANNEL_TYPING(this.id), {});
|
||||
}
|
||||
|
||||
/**
|
||||
* pinMessage pins a channel message.
|
||||
* Same as Message.pin().
|
||||
* @param messageId - The id of the message to pin
|
||||
* @returns The promise that resolves when the request is complete
|
||||
*/
|
||||
async pinMessage(messageId: Snowflake): Promise<void> {
|
||||
await Message.prototype.pin.call({ id: messageId, channelId: this.id, session: this.session });
|
||||
}
|
||||
|
||||
/**
|
||||
* unpinMessage unpin a channel message.
|
||||
* Same as Message.unpin()
|
||||
* @param messageId - The id of the message to unpin
|
||||
* @returns The promise of the request
|
||||
*/
|
||||
async unpinMessage(messageId: Snowflake): Promise<void> {
|
||||
await Message.prototype.unpin.call({ id: messageId, channelId: this.id, session: this.session });
|
||||
}
|
||||
|
||||
/**
|
||||
* addReaction adds a reaction to the message.
|
||||
* Same as Message.addReaction().
|
||||
* @param messageId - The message to add the reaction to
|
||||
* @param reaction - The reaction to add
|
||||
* @returns The promise of the request
|
||||
*/
|
||||
async addReaction(messageId: Snowflake, reaction: EmojiResolvable): Promise<void> {
|
||||
await Message.prototype.addReaction.call(
|
||||
{ channelId: this.id, id: messageId, session: this.session },
|
||||
reaction,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* removeReaction removes a reaction from the message.
|
||||
* Same as Message.removeReaction().
|
||||
* @param messageId - The id of the message to remove the reaction from
|
||||
* @param reaction - The reaction to remove
|
||||
* @param options - The user to remove the reaction from
|
||||
*/
|
||||
async removeReaction(
|
||||
messageId: Snowflake,
|
||||
reaction: EmojiResolvable,
|
||||
options?: { userId: Snowflake },
|
||||
): Promise<void> {
|
||||
await Message.prototype.removeReaction.call(
|
||||
{ channelId: this.id, id: messageId, session: this.session },
|
||||
reaction,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* removeReactionEmoji removes an emoji reaction from the messageId provided.
|
||||
* Same as Message.removeReactionEmoji().
|
||||
* @param messageId - The message id to remove the reaction from.
|
||||
*/
|
||||
async removeReactionEmoji(messageId: Snowflake, reaction: EmojiResolvable): Promise<void> {
|
||||
await Message.prototype.removeReactionEmoji.call(
|
||||
{ channelId: this.id, id: messageId, session: this.session },
|
||||
reaction,
|
||||
);
|
||||
}
|
||||
|
||||
/** nukeReactions nukes every reaction on the message.
|
||||
* Same as Message.nukeReactions().
|
||||
* @param messageId The message id to nuke reactions from.
|
||||
* @returns A promise that resolves when the reactions are nuked.
|
||||
*/
|
||||
async nukeReactions(messageId: Snowflake): Promise<void> {
|
||||
await Message.prototype.nukeReactions.call({ channelId: this.id, id: messageId });
|
||||
}
|
||||
|
||||
/**
|
||||
* fetchReactions gets the users who reacted with this emoji on the message.
|
||||
* Same as Message.fetchReactions().
|
||||
* @param messageId - The message id to get the reactions from.
|
||||
* @param reaction - The emoji to get the reactions from.
|
||||
* @param options - The options to get the reactions with.
|
||||
* @returns The users who reacted with this emoji on the message.
|
||||
*/
|
||||
async fetchReactions(
|
||||
messageId: Snowflake,
|
||||
reaction: EmojiResolvable,
|
||||
options?: GetReactions,
|
||||
): Promise<User[]> {
|
||||
const users = await Message.prototype.fetchReactions.call(
|
||||
{ channelId: this.id, id: messageId, session: this.session },
|
||||
reaction,
|
||||
options,
|
||||
);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* sendMessage sends a message to the channel.
|
||||
* Same as Message.reply().
|
||||
* @param options - Options for a new message.
|
||||
* @returns The sent message.
|
||||
*/
|
||||
sendMessage(options: CreateMessage): Promise<Message> {
|
||||
return Message.prototype.reply.call({ channelId: this.id, session: this.session }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* editMessage edits a message.
|
||||
* Same as Message.edit().
|
||||
* @param messageId - Message ID.
|
||||
* @param options - Options for edit a message.
|
||||
* @returns The edited message.
|
||||
*/
|
||||
editMessage(messageId: Snowflake, options: EditMessage): Promise<Message> {
|
||||
return Message.prototype.edit.call({ channelId: this.id, id: messageId, session: this.session }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* createWebhook creates a webhook.
|
||||
* @param options - Options for a new webhook.
|
||||
* @returns The created webhook.
|
||||
*/
|
||||
async createWebhook(options: CreateWebhook): Promise<Webhook> {
|
||||
const webhook = await this.session.rest.post<DiscordWebhook>(
|
||||
CHANNEL_WEBHOOKS(this.id),
|
||||
{
|
||||
name: options.name,
|
||||
avatar: options.avatar ? urlToBase64(options.avatar) : undefined,
|
||||
reason: options.reason,
|
||||
},
|
||||
);
|
||||
|
||||
return new Webhook(this.session, webhook);
|
||||
}
|
||||
}
|
||||
|
||||
/** GuildChannel */
|
||||
/**
|
||||
* Represent the options object to create a thread channel
|
||||
* @link https://discord.com/developers/docs/resources/channel#start-thread-without-message
|
||||
*/
|
||||
export interface ThreadCreateOptions {
|
||||
name: string;
|
||||
autoArchiveDuration?: 60 | 1440 | 4320 | 10080;
|
||||
type: 10 | 11 | 12;
|
||||
invitable?: boolean;
|
||||
rateLimitPerUser?: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Representations of the objects to edit a guild channel
|
||||
* @link https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel
|
||||
*/
|
||||
export interface EditGuildChannelOptions {
|
||||
name?: string;
|
||||
position?: number;
|
||||
permissionOverwrites?: PermissionsOverwrites[];
|
||||
}
|
||||
|
||||
export interface EditNewsChannelOptions extends EditGuildChannelOptions {
|
||||
type?: ChannelTypes.GuildNews | ChannelTypes.GuildText;
|
||||
topic?: string | null;
|
||||
nfsw?: boolean | null;
|
||||
parentId?: Snowflake | null;
|
||||
defaultAutoArchiveDuration?: number | null;
|
||||
}
|
||||
|
||||
export interface EditGuildTextChannelOptions extends EditNewsChannelOptions {
|
||||
rateLimitPerUser?: number | null;
|
||||
}
|
||||
|
||||
export interface EditStageChannelOptions extends EditGuildChannelOptions {
|
||||
bitrate?: number | null;
|
||||
rtcRegion?: Snowflake | null;
|
||||
}
|
||||
|
||||
export interface EditVoiceChannelOptions extends EditStageChannelOptions {
|
||||
nsfw?: boolean | null;
|
||||
userLimit?: number | null;
|
||||
parentId?: Snowflake | null;
|
||||
videoQualityMode?: VideoQualityModes | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the option object to create a thread channel from a message
|
||||
* @link https://discord.com/developers/docs/resources/channel#start-thread-from-message
|
||||
*/
|
||||
export interface ThreadCreateOptions {
|
||||
name: string;
|
||||
autoArchiveDuration?: 60 | 1440 | 4320 | 10080;
|
||||
rateLimitPerUser?: number;
|
||||
messageId: Snowflake;
|
||||
}
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/channel#list-public-archived-threads-response-body
|
||||
*/
|
||||
export interface ReturnThreadsArchive {
|
||||
threads: Record<Snowflake, ThreadChannel>;
|
||||
members: Record<Snowflake, ThreadMember>;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export class GuildChannel extends BaseChannel implements Model {
|
||||
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
|
||||
super(session, data);
|
||||
this.type = data.type as number;
|
||||
this.guildId = guildId;
|
||||
this.position = data.position;
|
||||
data.topic ? this.topic = data.topic : null;
|
||||
data.parent_id ? this.parentId = data.parent_id : undefined;
|
||||
this.permissionOverwrites = data.permission_overwrites
|
||||
? ChannelFactory.permissionOverwrites(data.permission_overwrites)
|
||||
: [];
|
||||
}
|
||||
|
||||
override type: Exclude<ChannelTypes, ChannelTypes.DM | ChannelTypes.GroupDm>;
|
||||
guildId: Snowflake;
|
||||
topic?: string;
|
||||
position?: number;
|
||||
parentId?: Snowflake;
|
||||
permissionOverwrites: PermissionsOverwrites[];
|
||||
|
||||
async fetchInvites(): Promise<Invite[]> {
|
||||
const invites = await this.session.rest.get<DiscordInviteMetadata[]>(CHANNEL_INVITES(this.id));
|
||||
|
||||
return invites.map(invite => new Invite(this.session, invite));
|
||||
}
|
||||
|
||||
async edit(options: EditNewsChannelOptions): Promise<NewsChannel>;
|
||||
async edit(options: EditStageChannelOptions): Promise<StageChannel>;
|
||||
async edit(options: EditVoiceChannelOptions): Promise<VoiceChannel>;
|
||||
async edit(
|
||||
options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions,
|
||||
): Promise<Channel> {
|
||||
const channel = await this.session.rest.patch<DiscordChannel>(
|
||||
CHANNEL(this.id),
|
||||
{
|
||||
name: options.name,
|
||||
type: 'type' in options ? options.type : undefined,
|
||||
position: options.position,
|
||||
topic: 'topic' in options ? options.topic : undefined,
|
||||
nsfw: 'nfsw' in options ? options.nfsw : undefined,
|
||||
rate_limit_per_user: 'rateLimitPerUser' in options ? options.rateLimitPerUser : undefined,
|
||||
bitrate: 'bitrate' in options ? options.bitrate : undefined,
|
||||
user_limit: 'userLimit' in options ? options.userLimit : undefined,
|
||||
permissions_overwrites: options.permissionOverwrites,
|
||||
parent_id: 'parentId' in options ? options.parentId : undefined,
|
||||
rtc_region: 'rtcRegion' in options ? options.rtcRegion : undefined,
|
||||
video_quality_mode: 'videoQualityMode' in options ? options.videoQualityMode : undefined,
|
||||
default_auto_archive_duration: 'defaultAutoArchiveDuration' in options
|
||||
? options.defaultAutoArchiveDuration
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
return ChannelFactory.from(this.session, channel);
|
||||
}
|
||||
|
||||
async getArchivedThreads(
|
||||
options: ListArchivedThreads & { type: 'public' | 'private' | 'privateJoinedThreads' },
|
||||
): Promise<ReturnThreadsArchive> {
|
||||
let func: (channelId: Snowflake, options: ListArchivedThreads) => string;
|
||||
|
||||
switch (options.type) {
|
||||
case 'public':
|
||||
func = THREAD_ARCHIVED_PUBLIC;
|
||||
break;
|
||||
case 'private':
|
||||
func = THREAD_START_PRIVATE;
|
||||
break;
|
||||
case 'privateJoinedThreads':
|
||||
func = THREAD_ARCHIVED_PRIVATE_JOINED;
|
||||
break;
|
||||
}
|
||||
|
||||
const { threads, members, has_more } = await this.session.rest.get<DiscordListArchivedThreads>(
|
||||
func(this.id, options),
|
||||
);
|
||||
|
||||
return {
|
||||
threads: Object.fromEntries(
|
||||
threads.map(thread => [thread.id, new ThreadChannel(this.session, thread, this.id)]),
|
||||
) as Record<Snowflake, ThreadChannel>,
|
||||
members: Object.fromEntries(
|
||||
members.map(threadMember => [threadMember.id, new ThreadMember(this.session, threadMember)]),
|
||||
) as Record<Snowflake, ThreadMember>,
|
||||
hasMore: has_more,
|
||||
};
|
||||
}
|
||||
|
||||
async createThread(options: ThreadCreateOptions): Promise<ThreadChannel> {
|
||||
const thread = await this.session.rest.post<DiscordChannel>(
|
||||
'messageId' in options
|
||||
? THREAD_START_PUBLIC(this.id, options.messageId)
|
||||
: THREAD_START_PRIVATE(this.id),
|
||||
{
|
||||
name: options.name,
|
||||
auto_archive_duration: options.autoArchiveDuration,
|
||||
},
|
||||
);
|
||||
|
||||
return new ThreadChannel(this.session, thread, thread.guild_id ?? this.guildId);
|
||||
}
|
||||
}
|
||||
|
||||
/** BaseVoiceChannel */
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/topics/gateway#update-voice-state
|
||||
*/
|
||||
export interface UpdateVoiceState {
|
||||
guildId: string;
|
||||
channelId?: string;
|
||||
selfMute: boolean;
|
||||
selfDeaf: boolean;
|
||||
}
|
||||
|
||||
export abstract class BaseVoiceChannel extends GuildChannel {
|
||||
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
|
||||
super(session, data, guildId);
|
||||
this.bitRate = data.bitrate;
|
||||
this.userLimit = data.user_limit ?? 0;
|
||||
this.videoQuality = data.video_quality_mode;
|
||||
this.nsfw = !!data.nsfw;
|
||||
this.type = data.type as number;
|
||||
|
||||
if (data.rtc_region) {
|
||||
this.rtcRegion = data.rtc_region;
|
||||
}
|
||||
}
|
||||
|
||||
override type: ChannelTypes.GuildVoice | ChannelTypes.GuildStageVoice;
|
||||
bitRate?: number;
|
||||
userLimit: number;
|
||||
rtcRegion?: Snowflake;
|
||||
|
||||
videoQuality?: VideoQualityModes;
|
||||
nsfw: boolean;
|
||||
|
||||
// TODO: CONNECT TO VOICE CHAT
|
||||
}
|
||||
|
||||
/** DMChannel */
|
||||
export class DMChannel extends BaseChannel implements Model {
|
||||
constructor(session: Session, data: DiscordChannel) {
|
||||
super(session, data);
|
||||
this.user = new User(this.session, data.recipents!.find(r => r.id !== this.session.botId)!);
|
||||
this.type = data.type as ChannelTypes.DM | ChannelTypes.GroupDm;
|
||||
if (data.last_message_id) {
|
||||
this.lastMessageId = data.last_message_id;
|
||||
}
|
||||
}
|
||||
|
||||
override type: ChannelTypes.DM | ChannelTypes.GroupDm;
|
||||
user: User;
|
||||
lastMessageId?: Snowflake;
|
||||
|
||||
async close(): Promise<DMChannel> {
|
||||
const channel = await this.session.rest.delete<DiscordChannel>(CHANNEL(this.id), {});
|
||||
|
||||
return new DMChannel(this.session, channel);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DMChannel extends Omit<TextChannel, 'type'>, Omit<BaseChannel, 'type'> {}
|
||||
|
||||
TextChannel.applyTo(DMChannel);
|
||||
|
||||
/** VoiceChannel */
|
||||
export class VoiceChannel extends BaseVoiceChannel {
|
||||
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
|
||||
super(session, data, guildId);
|
||||
this.type = data.type as number;
|
||||
}
|
||||
|
||||
override type: ChannelTypes.GuildVoice;
|
||||
}
|
||||
|
||||
export interface VoiceChannel extends TextChannel, BaseVoiceChannel {}
|
||||
|
||||
TextChannel.applyTo(VoiceChannel);
|
||||
|
||||
/** NewsChannel */
|
||||
export class NewsChannel extends GuildChannel {
|
||||
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
|
||||
super(session, data, guildId);
|
||||
this.type = data.type as ChannelTypes.GuildNews;
|
||||
this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
|
||||
}
|
||||
|
||||
override type: ChannelTypes.GuildNews;
|
||||
defaultAutoArchiveDuration?: number;
|
||||
|
||||
crosspostMessage(messageId: Snowflake): Promise<Message> {
|
||||
return Message.prototype.crosspost.call({ id: messageId, channelId: this.id, session: this.session });
|
||||
}
|
||||
|
||||
get publishMessage() {
|
||||
return this.crosspostMessage;
|
||||
}
|
||||
}
|
||||
|
||||
TextChannel.applyTo(NewsChannel);
|
||||
|
||||
export interface NewsChannel extends TextChannel, GuildChannel {}
|
||||
|
||||
/** StageChannel */
|
||||
export class StageChannel extends BaseVoiceChannel {
|
||||
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
|
||||
super(session, data, guildId);
|
||||
this.type = data.type as number;
|
||||
this.topic = data.topic ? data.topic : undefined;
|
||||
}
|
||||
|
||||
override type: ChannelTypes.GuildStageVoice;
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
/** ThreadChannel */
|
||||
export class ThreadChannel extends GuildChannel implements Model {
|
||||
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
|
||||
super(session, data, guildId);
|
||||
this.type = data.type as number;
|
||||
this.archived = !!data.thread_metadata?.archived;
|
||||
this.archiveTimestamp = data.thread_metadata?.archive_timestamp;
|
||||
this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration;
|
||||
this.locked = !!data.thread_metadata?.locked;
|
||||
this.messageCount = data.message_count;
|
||||
this.memberCount = data.member_count;
|
||||
this.ownerId = data.owner_id;
|
||||
|
||||
if (data.member) {
|
||||
this.member = new ThreadMember(session, data.member);
|
||||
}
|
||||
}
|
||||
|
||||
override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread;
|
||||
archived?: boolean;
|
||||
archiveTimestamp?: string;
|
||||
autoArchiveDuration?: number;
|
||||
locked?: boolean;
|
||||
messageCount?: number;
|
||||
memberCount?: number;
|
||||
member?: ThreadMember;
|
||||
ownerId?: Snowflake;
|
||||
|
||||
async joinThread(): Promise<void> {
|
||||
await this.session.rest.put<undefined>(THREAD_ME(this.id), {});
|
||||
}
|
||||
|
||||
async addToThread(guildMemberId: Snowflake): Promise<void> {
|
||||
await this.session.rest.put<undefined>(THREAD_USER(this.id, guildMemberId), {});
|
||||
}
|
||||
|
||||
async leaveToThread(guildMemberId: Snowflake): Promise<void> {
|
||||
await this.session.rest.delete<undefined>(THREAD_USER(this.id, guildMemberId), {});
|
||||
}
|
||||
|
||||
removeMember(memberId: Snowflake = this.session.botId): Promise<void> {
|
||||
return ThreadMember.prototype.quitThread.call({ id: this.id, session: this.session }, memberId);
|
||||
}
|
||||
|
||||
fetchMember(memberId: Snowflake = this.session.botId): Promise<ThreadMember> {
|
||||
return ThreadMember.prototype.fetchMember.call({ id: this.id, session: this.session }, memberId);
|
||||
}
|
||||
|
||||
async fetchMembers(): Promise<ThreadMember[]> {
|
||||
const members = await this.session.rest.get<DiscordThreadMember[]>(
|
||||
THREAD_MEMBERS(this.id),
|
||||
);
|
||||
|
||||
return members.map(threadMember => new ThreadMember(this.session, threadMember));
|
||||
}
|
||||
}
|
||||
|
||||
export interface ThreadChannel extends Omit<GuildChannel, 'type'>, Omit<TextChannel, 'type'> {}
|
||||
|
||||
TextChannel.applyTo(ThreadChannel);
|
||||
|
||||
export class GuildTextChannel extends GuildChannel {
|
||||
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
|
||||
super(session, data, guildId);
|
||||
this.type = data.type as ChannelTypes.GuildText;
|
||||
}
|
||||
|
||||
override type: ChannelTypes.GuildText;
|
||||
}
|
||||
|
||||
export interface GuildTextChannel extends GuildChannel, TextChannel {}
|
||||
|
||||
TextChannel.applyTo(GuildTextChannel);
|
||||
|
||||
/** ChannelFactory */
|
||||
export type Channel =
|
||||
| GuildTextChannel
|
||||
| TextChannel
|
||||
| VoiceChannel
|
||||
| DMChannel
|
||||
| NewsChannel
|
||||
| ThreadChannel
|
||||
| StageChannel
|
||||
| CategoryChannel;
|
||||
|
||||
export type ChannelInGuild =
|
||||
| GuildTextChannel
|
||||
| VoiceChannel
|
||||
| StageChannel
|
||||
| NewsChannel
|
||||
| ThreadChannel;
|
||||
|
||||
export type ChannelWithMessages =
|
||||
| GuildTextChannel
|
||||
| VoiceChannel
|
||||
| DMChannel
|
||||
| NewsChannel
|
||||
| ThreadChannel;
|
||||
|
||||
export type ChannelWithMessagesInGuild = Exclude<ChannelWithMessages, DMChannel>;
|
||||
|
||||
export type PartialChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
export class ChannelFactory {
|
||||
static fromGuildChannel(session: Session, channel: DiscordChannel): ChannelInGuild {
|
||||
switch (channel.type) {
|
||||
case ChannelTypes.GuildPublicThread:
|
||||
case ChannelTypes.GuildPrivateThread:
|
||||
return new ThreadChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildText:
|
||||
return new GuildTextChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildNews:
|
||||
return new NewsChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildVoice:
|
||||
return new VoiceChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildStageVoice:
|
||||
return new StageChannel(session, channel, channel.guild_id!);
|
||||
default:
|
||||
throw new Error('Channel was not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
static from(session: Session, channel: DiscordChannel): Channel {
|
||||
switch (channel.type) {
|
||||
case ChannelTypes.GuildPublicThread:
|
||||
case ChannelTypes.GuildPrivateThread:
|
||||
return new ThreadChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildText:
|
||||
return new GuildTextChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildNews:
|
||||
return new NewsChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.DM:
|
||||
return new DMChannel(session, channel);
|
||||
case ChannelTypes.GuildVoice:
|
||||
return new VoiceChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildStageVoice:
|
||||
return new StageChannel(session, channel, channel.guild_id!);
|
||||
case ChannelTypes.GuildCategory:
|
||||
return new CategoryChannel(session, channel);
|
||||
default:
|
||||
if (textBasedChannels.includes(channel.type)) {
|
||||
return new TextChannel(session, channel);
|
||||
}
|
||||
throw new Error('Channel was not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
static permissionOverwrites(overwrites: DiscordOverwrite[]): PermissionsOverwrites[] {
|
||||
return overwrites.map(v => {
|
||||
return {
|
||||
id: v.id,
|
||||
type: v.type,
|
||||
allow: new Permissions(parseInt(v.allow!)),
|
||||
deny: new Permissions(parseInt(v.deny!)),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
268
packages/core/src/structures/components.ts
Normal file
268
packages/core/src/structures/components.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
DiscordComponent,
|
||||
DiscordInputTextComponent,
|
||||
TextStyles,
|
||||
} from '@biscuitland/api-types';
|
||||
import { Emoji } from './emojis';
|
||||
import { ButtonStyles, MessageComponentTypes } from '@biscuitland/api-types';
|
||||
|
||||
export class BaseComponent {
|
||||
constructor(type: MessageComponentTypes) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
type: MessageComponentTypes;
|
||||
|
||||
isActionRow(): this is ActionRowComponent {
|
||||
return this.type === MessageComponentTypes.ActionRow;
|
||||
}
|
||||
|
||||
isButton(): this is ButtonComponent {
|
||||
return this.type === MessageComponentTypes.Button;
|
||||
}
|
||||
|
||||
isSelectMenu(): this is SelectMenuComponent {
|
||||
return this.type === MessageComponentTypes.SelectMenu;
|
||||
}
|
||||
|
||||
isTextInput(): this is TextInputComponent {
|
||||
return this.type === MessageComponentTypes.InputText;
|
||||
}
|
||||
}
|
||||
|
||||
/** Action Row Component */
|
||||
export interface ActionRowComponent {
|
||||
type: MessageComponentTypes.ActionRow;
|
||||
components: Exclude<Component, ActionRowComponent>[];
|
||||
}
|
||||
|
||||
/** All Components */
|
||||
export type Component =
|
||||
| ActionRowComponent
|
||||
| ButtonComponent
|
||||
| LinkButtonComponent
|
||||
| SelectMenuComponent
|
||||
| TextInputComponent;
|
||||
|
||||
/** Button Component */
|
||||
export type ClassicButton = Exclude<ButtonStyles, ButtonStyles.Link>;
|
||||
|
||||
export type ComponentsWithoutRow = Exclude<Component, ActionRowComponent>;
|
||||
|
||||
export interface ButtonComponent {
|
||||
type: MessageComponentTypes.Button;
|
||||
style: ClassicButton;
|
||||
label?: string;
|
||||
emoji?: Emoji;
|
||||
customId?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/** Link Button Component */
|
||||
export interface LinkButtonComponent {
|
||||
type: MessageComponentTypes.Button;
|
||||
style: ButtonStyles.Link;
|
||||
label?: string;
|
||||
url: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/** Select Menu Component */
|
||||
export interface SelectMenuComponent {
|
||||
type: MessageComponentTypes.SelectMenu;
|
||||
customId: string;
|
||||
options: SelectMenuOption[];
|
||||
placeholder?: string;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/** Text Input Component */
|
||||
export interface TextInputComponent {
|
||||
type: MessageComponentTypes.InputText;
|
||||
customId: string;
|
||||
style: TextStyles;
|
||||
label: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface SelectMenuOption {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
emoji?: Emoji;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export class Button extends BaseComponent implements ButtonComponent {
|
||||
constructor(session: Session, data: DiscordComponent) {
|
||||
super(data.type);
|
||||
|
||||
this.session = session;
|
||||
this.type = data.type as MessageComponentTypes.Button;
|
||||
this.customId = data.custom_id;
|
||||
this.label = data.label;
|
||||
this.style = data.style as ClassicButton;
|
||||
this.disabled = data.disabled;
|
||||
|
||||
if (data.emoji) {
|
||||
this.emoji = new Emoji(session, data.emoji);
|
||||
}
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
override type: MessageComponentTypes.Button;
|
||||
customId?: string;
|
||||
label?: string;
|
||||
style: ClassicButton;
|
||||
disabled?: boolean;
|
||||
emoji?: Emoji;
|
||||
}
|
||||
|
||||
export class LinkButton extends BaseComponent implements LinkButtonComponent {
|
||||
constructor(session: Session, data: DiscordComponent) {
|
||||
super(data.type);
|
||||
|
||||
this.session = session;
|
||||
this.type = data.type as MessageComponentTypes.Button;
|
||||
this.url = data.url!;
|
||||
this.label = data.label;
|
||||
this.style = data.style as number;
|
||||
this.disabled = data.disabled;
|
||||
|
||||
if (data.emoji) {
|
||||
this.emoji = new Emoji(session, data.emoji);
|
||||
}
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
override type: MessageComponentTypes.Button;
|
||||
url: string;
|
||||
label?: string;
|
||||
style: ButtonStyles.Link;
|
||||
disabled?: boolean;
|
||||
emoji?: Emoji;
|
||||
}
|
||||
|
||||
export class SelectMenu extends BaseComponent implements SelectMenuComponent {
|
||||
constructor(session: Session, data: DiscordComponent) {
|
||||
super(data.type);
|
||||
|
||||
this.session = session;
|
||||
this.type = data.type as MessageComponentTypes.SelectMenu;
|
||||
this.customId = data.custom_id!;
|
||||
this.options = data.options!.map(option => {
|
||||
return <SelectMenuOption>{
|
||||
label: option.label,
|
||||
description: option.description,
|
||||
emoji: option.emoji || new Emoji(session, option.emoji!),
|
||||
value: option.value,
|
||||
};
|
||||
});
|
||||
this.placeholder = data.placeholder;
|
||||
this.minValues = data.min_values;
|
||||
this.maxValues = data.max_values;
|
||||
this.disabled = data.disabled;
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
override type: MessageComponentTypes.SelectMenu;
|
||||
customId: string;
|
||||
options: SelectMenuOption[];
|
||||
placeholder?: string;
|
||||
minValues?: number;
|
||||
maxValues?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export class TextInput extends BaseComponent implements TextInputComponent {
|
||||
constructor(session: Session, data: DiscordInputTextComponent) {
|
||||
super(data.type);
|
||||
|
||||
this.session = session;
|
||||
this.type = data.type as MessageComponentTypes.InputText;
|
||||
this.customId = data.custom_id!;
|
||||
this.label = data.label!;
|
||||
this.style = data.style as TextStyles;
|
||||
|
||||
this.placeholder = data.placeholder;
|
||||
this.value = data.value;
|
||||
|
||||
this.minLength = data.min_length;
|
||||
this.maxLength = data.max_length;
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
override type: MessageComponentTypes.InputText;
|
||||
style: TextStyles;
|
||||
customId: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
export class ActionRow extends BaseComponent implements ActionRowComponent {
|
||||
constructor(session: Session, data: DiscordComponent) {
|
||||
super(data.type);
|
||||
|
||||
this.session = session;
|
||||
this.type = data.type as MessageComponentTypes.ActionRow;
|
||||
this.components = data.components!.map(component => {
|
||||
switch (component.type) {
|
||||
case MessageComponentTypes.Button:
|
||||
if (component.style === ButtonStyles.Link) {
|
||||
return new LinkButton(session, component);
|
||||
}
|
||||
return new Button(session, component);
|
||||
case MessageComponentTypes.SelectMenu:
|
||||
return new SelectMenu(session, component);
|
||||
case MessageComponentTypes.InputText:
|
||||
return new TextInput(
|
||||
session,
|
||||
component as DiscordInputTextComponent
|
||||
);
|
||||
case MessageComponentTypes.ActionRow:
|
||||
throw new Error(
|
||||
'Cannot have an action row inside an action row'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
override type: MessageComponentTypes.ActionRow;
|
||||
components: ComponentsWithoutRow[];
|
||||
}
|
||||
|
||||
export class ComponentFactory {
|
||||
/**
|
||||
* Component factory
|
||||
* @internal
|
||||
*/
|
||||
static from(session: Session, component: DiscordComponent): Component {
|
||||
switch (component.type) {
|
||||
case MessageComponentTypes.ActionRow:
|
||||
return new ActionRow(session, component);
|
||||
case MessageComponentTypes.Button:
|
||||
if (component.style === ButtonStyles.Link) {
|
||||
return new LinkButton(session, component);
|
||||
}
|
||||
return new Button(session, component);
|
||||
case MessageComponentTypes.SelectMenu:
|
||||
return new SelectMenu(session, component);
|
||||
case MessageComponentTypes.InputText:
|
||||
return new TextInput(
|
||||
session,
|
||||
component as DiscordInputTextComponent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
100
packages/core/src/structures/embed.ts
Normal file
100
packages/core/src/structures/embed.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import type { DiscordEmbed, EmbedTypes } from '@biscuitland/api-types';
|
||||
|
||||
export interface Embed {
|
||||
title?: string;
|
||||
timestamp?: string;
|
||||
type?: EmbedTypes;
|
||||
url?: string;
|
||||
color?: number;
|
||||
description?: string;
|
||||
author?: {
|
||||
name: string;
|
||||
iconURL?: string;
|
||||
proxyIconURL?: string;
|
||||
url?: string;
|
||||
};
|
||||
footer?: {
|
||||
text: string;
|
||||
iconURL?: string;
|
||||
proxyIconURL?: string;
|
||||
};
|
||||
fields?: {
|
||||
name: string;
|
||||
value: string;
|
||||
inline?: boolean;
|
||||
}[];
|
||||
thumbnail?: {
|
||||
url: string;
|
||||
proxyURL?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
video?: {
|
||||
url?: string;
|
||||
proxyURL?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
image?: {
|
||||
url: string;
|
||||
proxyURL?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
provider?: {
|
||||
url?: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function embed(data: Embed): DiscordEmbed {
|
||||
return {
|
||||
title: data.title,
|
||||
timestamp: data.timestamp,
|
||||
type: data.type,
|
||||
url: data.url,
|
||||
color: data.color,
|
||||
description: data.description,
|
||||
author: data.author && {
|
||||
name: data.author.name,
|
||||
url: data.author.url,
|
||||
icon_url: data.author.iconURL,
|
||||
proxy_icon_url: data.author.proxyIconURL,
|
||||
},
|
||||
footer: data.footer && {
|
||||
text: data.footer.text,
|
||||
icon_url: data.footer.iconURL,
|
||||
proxy_icon_url: data.footer.proxyIconURL,
|
||||
},
|
||||
fields: data.fields?.map(f => {
|
||||
return {
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
inline: f.inline,
|
||||
};
|
||||
}),
|
||||
thumbnail: data.thumbnail && {
|
||||
url: data.thumbnail.url,
|
||||
proxy_url: data.thumbnail.proxyURL,
|
||||
width: data.thumbnail.width,
|
||||
height: data.thumbnail.height,
|
||||
},
|
||||
video: {
|
||||
url: data.video?.url,
|
||||
proxy_url: data.video?.proxyURL,
|
||||
width: data.video?.width,
|
||||
height: data.video?.height,
|
||||
},
|
||||
image: data.image && {
|
||||
url: data.image.url,
|
||||
proxy_url: data.image.proxyURL,
|
||||
width: data.image.width,
|
||||
height: data.image.height,
|
||||
},
|
||||
provider: {
|
||||
url: data.provider?.url,
|
||||
name: data.provider?.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
70
packages/core/src/structures/emojis.ts
Normal file
70
packages/core/src/structures/emojis.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { DiscordEmoji } from '@biscuitland/api-types';
|
||||
import type { ModifyGuildEmoji } from './guilds';
|
||||
import { Guild } from './guilds';
|
||||
import { User } from './user';
|
||||
import { EMOJI_URL } from '@biscuitland/api-types';
|
||||
|
||||
export class Emoji implements Partial<Model> {
|
||||
constructor(session: Session, data: DiscordEmoji) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.animated = !!data.animated;
|
||||
this.available = !!data.available;
|
||||
this.requireColons = !!data.require_colons;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
readonly id?: Snowflake;
|
||||
readonly session: Session;
|
||||
|
||||
name?: string;
|
||||
animated: boolean;
|
||||
available: boolean;
|
||||
requireColons: boolean;
|
||||
}
|
||||
|
||||
export class GuildEmoji extends Emoji implements Model {
|
||||
constructor(session: Session, data: DiscordEmoji, guildId: Snowflake) {
|
||||
super(session, data);
|
||||
this.guildId = guildId;
|
||||
this.roles = data.roles;
|
||||
this.user = data.user ? new User(this.session, data.user) : undefined;
|
||||
this.managed = !!data.managed;
|
||||
this.id = super.id!;
|
||||
}
|
||||
|
||||
guildId: Snowflake;
|
||||
roles?: Snowflake[];
|
||||
user?: User;
|
||||
managed?: boolean;
|
||||
|
||||
// id cannot be null in a GuildEmoji
|
||||
override id: Snowflake;
|
||||
|
||||
async edit(options: ModifyGuildEmoji): Promise<GuildEmoji> {
|
||||
const emoji = await Guild.prototype.editEmoji.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.id,
|
||||
options
|
||||
);
|
||||
|
||||
return emoji;
|
||||
}
|
||||
|
||||
async delete(reason?: string): Promise<GuildEmoji> {
|
||||
await Guild.prototype.deleteEmoji.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.id,
|
||||
reason
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return EMOJI_URL(this.id, this.animated);
|
||||
}
|
||||
}
|
1204
packages/core/src/structures/guilds.ts
Normal file
1204
packages/core/src/structures/guilds.ts
Normal file
File diff suppressed because it is too large
Load Diff
87
packages/core/src/structures/integration.ts
Normal file
87
packages/core/src/structures/integration.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
DiscordIntegration,
|
||||
IntegrationExpireBehaviors,
|
||||
} from '@biscuitland/api-types';
|
||||
import { User } from './user';
|
||||
|
||||
export type IntegrationTypes = 'twitch' | 'youtube' | 'discord';
|
||||
|
||||
export interface IntegrationAccount {
|
||||
id: Snowflake;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IntegrationApplication {
|
||||
id: Snowflake;
|
||||
name: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
bot?: User;
|
||||
}
|
||||
|
||||
export class Integration implements Model {
|
||||
constructor(
|
||||
session: Session,
|
||||
data: DiscordIntegration & { guild_id?: Snowflake }
|
||||
) {
|
||||
this.id = data.id;
|
||||
this.session = session;
|
||||
|
||||
data.guild_id ? (this.guildId = data.guild_id) : null;
|
||||
|
||||
this.name = data.name;
|
||||
this.type = data.type;
|
||||
this.enabled = !!data.enabled;
|
||||
this.syncing = !!data.syncing;
|
||||
this.roleId = data.role_id;
|
||||
this.enableEmoticons = !!data.enable_emoticons;
|
||||
this.expireBehavior = data.expire_behavior;
|
||||
this.expireGracePeriod = data.expire_grace_period;
|
||||
this.syncedAt = data.synced_at;
|
||||
this.subscriberCount = data.subscriber_count;
|
||||
this.revoked = !!data.revoked;
|
||||
|
||||
this.user = data.user ? new User(session, data.user) : undefined;
|
||||
this.account = {
|
||||
id: data.account.id,
|
||||
name: data.account.name,
|
||||
};
|
||||
|
||||
if (data.application) {
|
||||
this.application = {
|
||||
id: data.application.id,
|
||||
name: data.application.name,
|
||||
icon: data.application.icon ? data.application.icon : undefined,
|
||||
description: data.application.description,
|
||||
bot: data.application.bot
|
||||
? new User(session, data.application.bot)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
id: Snowflake;
|
||||
guildId?: Snowflake;
|
||||
|
||||
name: string;
|
||||
type: IntegrationTypes;
|
||||
enabled?: boolean;
|
||||
syncing?: boolean;
|
||||
roleId?: string;
|
||||
enableEmoticons?: boolean;
|
||||
expireBehavior?: IntegrationExpireBehaviors;
|
||||
expireGracePeriod?: number;
|
||||
syncedAt?: string;
|
||||
subscriberCount?: number;
|
||||
revoked?: boolean;
|
||||
|
||||
user?: User;
|
||||
account: IntegrationAccount;
|
||||
application?: IntegrationApplication;
|
||||
}
|
||||
|
||||
export default Integration;
|
602
packages/core/src/structures/interactions.ts
Normal file
602
packages/core/src/structures/interactions.ts
Normal file
@ -0,0 +1,602 @@
|
||||
import type { Model } from './base';
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
ApplicationCommandTypes,
|
||||
DiscordInteraction,
|
||||
DiscordMessage,
|
||||
DiscordMessageComponents,
|
||||
DiscordMemberWithUser,
|
||||
DiscordMessageInteraction,
|
||||
} from '@biscuitland/api-types';
|
||||
import type { CreateMessage } from './message';
|
||||
import type { MessageFlags } from '../utils/util';
|
||||
import type { EditWebhookMessage } from './webhook';
|
||||
import {
|
||||
InteractionResponseTypes,
|
||||
InteractionTypes,
|
||||
MessageComponentTypes,
|
||||
} from '@biscuitland/api-types';
|
||||
import { Role } from './role';
|
||||
import { Attachment } from './attachment';
|
||||
import { Snowflake } from '../snowflakes';
|
||||
import { User } from './user';
|
||||
import { Member } from './members';
|
||||
import { Message } from './message';
|
||||
import { Permissions } from './special/permissions';
|
||||
import { Webhook } from './webhook';
|
||||
import { CommandInteractionOptionResolver } from './special/command-interaction-option-resolver';
|
||||
import {
|
||||
INTERACTION_ID_TOKEN,
|
||||
WEBHOOK_MESSAGE,
|
||||
WEBHOOK_MESSAGE_ORIGINAL,
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
export type InteractionResponseWith = {
|
||||
with: InteractionApplicationCommandCallbackData;
|
||||
};
|
||||
export type InteractionResponseWithData =
|
||||
| InteractionResponse
|
||||
| InteractionResponseWith;
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response
|
||||
*/
|
||||
export interface InteractionResponse {
|
||||
type: InteractionResponseTypes;
|
||||
data?: InteractionApplicationCommandCallbackData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata
|
||||
*/
|
||||
export interface InteractionApplicationCommandCallbackData
|
||||
extends Pick<
|
||||
CreateMessage,
|
||||
'allowedMentions' | 'content' | 'embeds' | 'files'
|
||||
> {
|
||||
customId?: string;
|
||||
title?: string;
|
||||
components?: DiscordMessageComponents;
|
||||
flags?: MessageFlags;
|
||||
choices?: ApplicationCommandOptionChoice[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice
|
||||
*/
|
||||
export interface ApplicationCommandOptionChoice {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export abstract class BaseInteraction implements Model {
|
||||
constructor(session: Session, data: DiscordInteraction) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.token = data.token;
|
||||
this.type = data.type;
|
||||
this.guildId = data.guild_id;
|
||||
this.channelId = data.channel_id;
|
||||
this.applicationId = data.application_id;
|
||||
this.version = data.version;
|
||||
|
||||
// @ts-expect-error: vendor error
|
||||
const perms = data.app_permissions as string;
|
||||
|
||||
if (perms) {
|
||||
this.appPermissions = new Permissions(BigInt(perms));
|
||||
}
|
||||
|
||||
if (!data.guild_id) {
|
||||
this.user = new User(session, data.user!);
|
||||
} else {
|
||||
this.member = new Member(session, data.member!, data.guild_id);
|
||||
}
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
readonly id: Snowflake;
|
||||
readonly token: string;
|
||||
|
||||
type: InteractionTypes;
|
||||
guildId?: Snowflake;
|
||||
channelId?: Snowflake;
|
||||
applicationId?: Snowflake;
|
||||
user?: User;
|
||||
member?: Member;
|
||||
appPermissions?: Permissions;
|
||||
|
||||
readonly version: 1;
|
||||
|
||||
responded = false;
|
||||
|
||||
get createdTimestamp(): number {
|
||||
return Snowflake.snowflakeToTimestamp(this.id);
|
||||
}
|
||||
|
||||
get createdAt(): Date {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
isCommand(): this is CommandInteraction {
|
||||
return this.type === InteractionTypes.ApplicationCommand;
|
||||
}
|
||||
|
||||
isAutoComplete(): this is AutoCompleteInteraction {
|
||||
return this.type === InteractionTypes.ApplicationCommandAutocomplete;
|
||||
}
|
||||
|
||||
isComponent(): this is ComponentInteraction {
|
||||
return this.type === InteractionTypes.MessageComponent;
|
||||
}
|
||||
|
||||
isPing(): this is PingInteraction {
|
||||
return this.type === InteractionTypes.Ping;
|
||||
}
|
||||
|
||||
isModalSubmit(): this is ModalSubmitInteraction {
|
||||
return this.type === InteractionTypes.ModalSubmit;
|
||||
}
|
||||
|
||||
inGuild(): this is this & { guildId: Snowflake } {
|
||||
return !!this.guildId;
|
||||
}
|
||||
|
||||
// webhooks methods:
|
||||
|
||||
async editReply(
|
||||
options: EditWebhookMessage & { messageId?: Snowflake }
|
||||
): Promise<Message | undefined> {
|
||||
const message = await this.session.rest.patch<
|
||||
DiscordMessage | undefined
|
||||
>(
|
||||
options.messageId
|
||||
? WEBHOOK_MESSAGE(this.id, this.token, options.messageId)
|
||||
: WEBHOOK_MESSAGE_ORIGINAL(this.id, this.token),
|
||||
{
|
||||
content: options.content,
|
||||
embeds: options.embeds,
|
||||
file: options.files,
|
||||
components: options.components,
|
||||
allowed_mentions: options.allowedMentions && {
|
||||
parse: options.allowedMentions.parse,
|
||||
replied_user: options.allowedMentions.repliedUser,
|
||||
users: options.allowedMentions.users,
|
||||
roles: options.allowedMentions.roles,
|
||||
},
|
||||
attachments: options.attachments?.map(attachment => {
|
||||
return {
|
||||
id: attachment.id,
|
||||
filename: attachment.name,
|
||||
content_type: attachment.contentType,
|
||||
size: attachment.size,
|
||||
url: attachment.attachment,
|
||||
proxy_url: attachment.proxyUrl,
|
||||
height: attachment.height,
|
||||
width: attachment.width,
|
||||
};
|
||||
}),
|
||||
message_id: options.messageId,
|
||||
}
|
||||
);
|
||||
|
||||
if (!message || !options.messageId) {
|
||||
return message as undefined;
|
||||
}
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
|
||||
async sendFollowUp(
|
||||
options: InteractionApplicationCommandCallbackData
|
||||
): Promise<Message> {
|
||||
const message = await Webhook.prototype.execute.call(
|
||||
{
|
||||
id: this.applicationId!,
|
||||
token: this.token,
|
||||
session: this.session,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
return message!;
|
||||
}
|
||||
|
||||
async editFollowUp(
|
||||
messageId: Snowflake,
|
||||
options?: { threadId: Snowflake }
|
||||
): Promise<Message> {
|
||||
const message = await Webhook.prototype.editMessage.call(
|
||||
{
|
||||
id: this.session.applicationId,
|
||||
token: this.token,
|
||||
},
|
||||
messageId,
|
||||
options
|
||||
);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
async deleteFollowUp(
|
||||
messageId: Snowflake,
|
||||
threadId?: Snowflake
|
||||
): Promise<void> {
|
||||
await Webhook.prototype.deleteMessage.call(
|
||||
{
|
||||
id: this.session.applicationId,
|
||||
token: this.token,
|
||||
},
|
||||
messageId,
|
||||
threadId
|
||||
);
|
||||
}
|
||||
|
||||
async fetchFollowUp(
|
||||
messageId: Snowflake,
|
||||
threadId?: Snowflake
|
||||
): Promise<Message | undefined> {
|
||||
const message = await Webhook.prototype.fetchMessage.call(
|
||||
{
|
||||
id: this.session.applicationId,
|
||||
token: this.token,
|
||||
},
|
||||
messageId,
|
||||
threadId
|
||||
);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// end webhook methods
|
||||
|
||||
async respond(resp: InteractionResponse): Promise<Message | undefined>;
|
||||
async respond(resp: InteractionResponseWith): Promise<Message | undefined>;
|
||||
async respond(
|
||||
resp: InteractionResponseWithData
|
||||
): Promise<Message | undefined> {
|
||||
const options = 'with' in resp ? resp.with : resp.data;
|
||||
const type =
|
||||
'type' in resp
|
||||
? resp.type
|
||||
: InteractionResponseTypes.ChannelMessageWithSource;
|
||||
|
||||
const data = {
|
||||
content: options?.content,
|
||||
custom_id: options?.customId,
|
||||
file: options?.files,
|
||||
allowed_mentions: options?.allowedMentions,
|
||||
flags: options?.flags,
|
||||
chocies: options?.choices,
|
||||
embeds: options?.embeds,
|
||||
title: options?.title,
|
||||
components: options?.components,
|
||||
};
|
||||
|
||||
if (!this.responded) {
|
||||
await this.session.rest.post<undefined>(
|
||||
INTERACTION_ID_TOKEN(this.id, this.token),
|
||||
{
|
||||
file: options?.files,
|
||||
type,
|
||||
data,
|
||||
}
|
||||
);
|
||||
|
||||
this.responded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sendFollowUp(data);
|
||||
}
|
||||
|
||||
// start custom methods
|
||||
|
||||
async respondWith(
|
||||
resp: InteractionApplicationCommandCallbackData
|
||||
): Promise<Message | undefined> {
|
||||
const m = await this.respond({ with: resp });
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
async defer() {
|
||||
await this.respond({
|
||||
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
}
|
||||
|
||||
async autocomplete() {
|
||||
await this.respond({
|
||||
type: InteractionResponseTypes.ApplicationCommandAutocompleteResult,
|
||||
});
|
||||
}
|
||||
|
||||
// end custom methods
|
||||
}
|
||||
|
||||
export class AutoCompleteInteraction extends BaseInteraction implements Model {
|
||||
constructor(session: Session, data: DiscordInteraction) {
|
||||
super(session, data);
|
||||
this.type = data.type as number;
|
||||
this.commandId = data.data!.id;
|
||||
this.commandName = data.data!.name;
|
||||
this.commandType = data.data!.type;
|
||||
this.commandGuildId = data.data!.guild_id;
|
||||
}
|
||||
|
||||
override type: InteractionTypes.ApplicationCommandAutocomplete;
|
||||
commandId: Snowflake;
|
||||
commandName: string;
|
||||
commandType: ApplicationCommandTypes;
|
||||
commandGuildId?: Snowflake;
|
||||
|
||||
async respondWithChoices(
|
||||
choices: ApplicationCommandOptionChoice[]
|
||||
): Promise<void> {
|
||||
await this.session.rest.post<undefined>(
|
||||
INTERACTION_ID_TOKEN(this.id, this.token),
|
||||
{
|
||||
data: { choices },
|
||||
type: InteractionResponseTypes.ApplicationCommandAutocompleteResult,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommandInteractionDataResolved {
|
||||
users: Map<Snowflake, User>;
|
||||
members: Map<Snowflake, Member>;
|
||||
roles: Map<Snowflake, Role>;
|
||||
attachments: Map<Snowflake, Attachment>;
|
||||
messages: Map<Snowflake, Message>;
|
||||
}
|
||||
|
||||
export class CommandInteraction extends BaseInteraction implements Model {
|
||||
constructor(session: Session, data: DiscordInteraction) {
|
||||
super(session, data);
|
||||
this.type = data.type as number;
|
||||
this.commandId = data.data!.id;
|
||||
this.commandName = data.data!.name;
|
||||
this.commandType = data.data!.type;
|
||||
this.commandGuildId = data.data!.guild_id;
|
||||
this.options = new CommandInteractionOptionResolver(
|
||||
data.data!.options ?? []
|
||||
);
|
||||
|
||||
this.resolved = {
|
||||
users: new Map(),
|
||||
members: new Map(),
|
||||
roles: new Map(),
|
||||
attachments: new Map(),
|
||||
messages: new Map(),
|
||||
};
|
||||
|
||||
if (data.data!.resolved?.users) {
|
||||
for (const [id, u] of Object.entries(data.data!.resolved.users)) {
|
||||
this.resolved.users.set(id, new User(session, u));
|
||||
}
|
||||
}
|
||||
|
||||
if (data.data!.resolved?.members && !!super.guildId) {
|
||||
for (const [id, m] of Object.entries(data.data!.resolved.members)) {
|
||||
this.resolved.members.set(
|
||||
id,
|
||||
new Member(
|
||||
session,
|
||||
m as DiscordMemberWithUser,
|
||||
super.guildId!
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.data!.resolved?.roles && !!super.guildId) {
|
||||
for (const [id, r] of Object.entries(data.data!.resolved.roles)) {
|
||||
this.resolved.roles.set(
|
||||
id,
|
||||
new Role(session, r, super.guildId!)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.data!.resolved?.attachments) {
|
||||
for (const [id, a] of Object.entries(
|
||||
data.data!.resolved.attachments
|
||||
)) {
|
||||
this.resolved.attachments.set(id, new Attachment(session, a));
|
||||
}
|
||||
}
|
||||
|
||||
if (data.data!.resolved?.messages) {
|
||||
for (const [id, m] of Object.entries(
|
||||
data.data!.resolved.messages
|
||||
)) {
|
||||
this.resolved.messages.set(id, new Message(session, m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override type: InteractionTypes.ApplicationCommand;
|
||||
commandId: Snowflake;
|
||||
commandName: string;
|
||||
commandType: ApplicationCommandTypes;
|
||||
commandGuildId?: Snowflake;
|
||||
resolved: CommandInteractionDataResolved;
|
||||
options: CommandInteractionOptionResolver;
|
||||
}
|
||||
|
||||
export type ModalInMessage = ModalSubmitInteraction & {
|
||||
message: Message;
|
||||
};
|
||||
|
||||
export class ModalSubmitInteraction extends BaseInteraction implements Model {
|
||||
constructor(session: Session, data: DiscordInteraction) {
|
||||
super(session, data);
|
||||
this.type = data.type as number;
|
||||
this.componentType = data.data!.component_type!;
|
||||
this.customId = data.data!.custom_id;
|
||||
this.targetId = data.data!.target_id;
|
||||
this.values = data.data!.values;
|
||||
|
||||
this.components = data.data?.components?.map(
|
||||
ModalSubmitInteraction.transformComponent
|
||||
);
|
||||
|
||||
if (data.message) {
|
||||
this.message = new Message(session, data.message);
|
||||
}
|
||||
}
|
||||
|
||||
override type: InteractionTypes.MessageComponent;
|
||||
componentType: MessageComponentTypes;
|
||||
customId?: string;
|
||||
targetId?: Snowflake;
|
||||
values?: string[];
|
||||
message?: Message;
|
||||
components;
|
||||
|
||||
static transformComponent(component: DiscordMessageComponents[number]) {
|
||||
return {
|
||||
type: component.type,
|
||||
components: component.components.map(component => {
|
||||
return {
|
||||
customId: component.custom_id,
|
||||
value: (component as typeof component & { value: string })
|
||||
.value,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
inMessage(): this is ModalInMessage {
|
||||
return !!this.message;
|
||||
}
|
||||
}
|
||||
|
||||
export class PingInteraction extends BaseInteraction implements Model {
|
||||
constructor(session: Session, data: DiscordInteraction) {
|
||||
super(session, data);
|
||||
this.type = data.type as number;
|
||||
this.commandId = data.data!.id;
|
||||
this.commandName = data.data!.name;
|
||||
this.commandType = data.data!.type;
|
||||
this.commandGuildId = data.data!.guild_id;
|
||||
}
|
||||
|
||||
override type: InteractionTypes.Ping;
|
||||
commandId: Snowflake;
|
||||
commandName: string;
|
||||
commandType: ApplicationCommandTypes;
|
||||
commandGuildId?: Snowflake;
|
||||
|
||||
async pong(): Promise<void> {
|
||||
await this.session.rest.post<undefined>(
|
||||
INTERACTION_ID_TOKEN(this.id, this.token),
|
||||
{
|
||||
type: InteractionResponseTypes.Pong,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentInteraction extends BaseInteraction implements Model {
|
||||
constructor(session: Session, data: DiscordInteraction) {
|
||||
super(session, data);
|
||||
this.type = data.type as number;
|
||||
this.componentType = data.data!.component_type!;
|
||||
this.customId = data.data!.custom_id;
|
||||
this.targetId = data.data!.target_id;
|
||||
this.values = data.data!.values;
|
||||
this.message = new Message(session, data.message!);
|
||||
}
|
||||
|
||||
override type: InteractionTypes.MessageComponent;
|
||||
componentType: MessageComponentTypes;
|
||||
customId?: string;
|
||||
targetId?: Snowflake;
|
||||
values?: string[];
|
||||
message: Message;
|
||||
|
||||
isButton(): boolean {
|
||||
return this.componentType === MessageComponentTypes.Button;
|
||||
}
|
||||
|
||||
isActionRow(): boolean {
|
||||
return this.componentType === MessageComponentTypes.ActionRow;
|
||||
}
|
||||
|
||||
isTextInput(): boolean {
|
||||
return this.componentType === MessageComponentTypes.InputText;
|
||||
}
|
||||
|
||||
isSelectMenu(): boolean {
|
||||
return this.componentType === MessageComponentTypes.SelectMenu;
|
||||
}
|
||||
|
||||
async deferUpdate() {
|
||||
await this.respond({
|
||||
type: InteractionResponseTypes.DeferredUpdateMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure
|
||||
*/
|
||||
export interface MessageInteraction {
|
||||
/** id of the interaction */
|
||||
id: Snowflake;
|
||||
/** type of interaction */
|
||||
type: InteractionTypes;
|
||||
/** name of the application command, including subcommands and subcommand groups */
|
||||
name: string;
|
||||
/** user who invoked the interaction */
|
||||
user: User;
|
||||
/** member who invoked the interaction in the guild */
|
||||
member?: Partial<Member>;
|
||||
}
|
||||
|
||||
export type Interaction =
|
||||
| CommandInteraction
|
||||
| ComponentInteraction
|
||||
| PingInteraction
|
||||
| AutoCompleteInteraction
|
||||
| ModalSubmitInteraction;
|
||||
|
||||
export class InteractionFactory {
|
||||
static from(
|
||||
session: Session,
|
||||
interaction: DiscordInteraction
|
||||
): Interaction {
|
||||
switch (interaction.type) {
|
||||
case InteractionTypes.Ping:
|
||||
return new PingInteraction(session, interaction);
|
||||
case InteractionTypes.ApplicationCommand:
|
||||
return new CommandInteraction(session, interaction);
|
||||
case InteractionTypes.MessageComponent:
|
||||
return new ComponentInteraction(session, interaction);
|
||||
case InteractionTypes.ApplicationCommandAutocomplete:
|
||||
return new AutoCompleteInteraction(session, interaction);
|
||||
case InteractionTypes.ModalSubmit:
|
||||
return new ModalSubmitInteraction(session, interaction);
|
||||
}
|
||||
}
|
||||
|
||||
static fromMessage(
|
||||
session: Session,
|
||||
interaction: DiscordMessageInteraction,
|
||||
_guildId?: Snowflake
|
||||
): MessageInteraction {
|
||||
const obj = {
|
||||
id: interaction.id,
|
||||
type: interaction.type,
|
||||
name: interaction.name,
|
||||
user: new User(session, interaction.user),
|
||||
// TODO: Parse member somehow with the guild id passed in message
|
||||
};
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
207
packages/core/src/structures/invite.ts
Normal file
207
packages/core/src/structures/invite.ts
Normal file
@ -0,0 +1,207 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type {
|
||||
DiscordApplication,
|
||||
DiscordChannel,
|
||||
DiscordInvite,
|
||||
DiscordInviteCreate,
|
||||
DiscordMemberWithUser,
|
||||
DiscordScheduledEventEntityMetadata,
|
||||
ScheduledEventEntityType,
|
||||
ScheduledEventPrivacyLevel,
|
||||
ScheduledEventStatus,
|
||||
TargetTypes,
|
||||
} from '@biscuitland/api-types';
|
||||
import { GuildChannel } from './channels';
|
||||
import { Member } from './members';
|
||||
import { Guild, InviteGuild } from './guilds';
|
||||
import { User } from './user';
|
||||
import { Application } from './application';
|
||||
|
||||
export interface InviteStageInstance {
|
||||
/** The members speaking in the Stage */
|
||||
members: Partial<Member>[];
|
||||
/** The number of users in the Stage */
|
||||
participantCount: number;
|
||||
/** The number of users speaking in the Stage */
|
||||
speakerCount: number;
|
||||
/** The topic of the Stage instance (1-120 characters) */
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export interface InviteScheduledEvent {
|
||||
id: Snowflake;
|
||||
guildId: string;
|
||||
channelId?: string;
|
||||
creatorId?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
scheduledStartTime: string;
|
||||
scheduledEndTime?: string;
|
||||
privacyLevel: ScheduledEventPrivacyLevel;
|
||||
status: ScheduledEventStatus;
|
||||
entityType: ScheduledEventEntityType;
|
||||
entityId?: string;
|
||||
entityMetadata?: DiscordScheduledEventEntityMetadata;
|
||||
creator?: User;
|
||||
userCount?: number;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface InviteCreate {
|
||||
channelId: string;
|
||||
code: string;
|
||||
createdAt: string;
|
||||
guildId?: string;
|
||||
inviter?: User;
|
||||
maxAge: number;
|
||||
maxUses: number;
|
||||
targetType: TargetTypes;
|
||||
targetUser?: User;
|
||||
targetApplication?: Partial<Application>;
|
||||
temporary: boolean;
|
||||
uses: number;
|
||||
}
|
||||
|
||||
export function NewInviteCreate(
|
||||
session: Session,
|
||||
invite: DiscordInviteCreate
|
||||
): InviteCreate {
|
||||
return {
|
||||
channelId: invite.channel_id,
|
||||
code: invite.code,
|
||||
createdAt: invite.created_at,
|
||||
guildId: invite.guild_id,
|
||||
inviter: invite.inviter ? new User(session, invite.inviter) : undefined,
|
||||
maxAge: invite.max_age,
|
||||
maxUses: invite.max_uses,
|
||||
targetType: invite.target_type,
|
||||
targetUser: invite.target_user
|
||||
? new User(session, invite.target_user)
|
||||
: undefined,
|
||||
targetApplication:
|
||||
invite.target_application &&
|
||||
new Application(
|
||||
session,
|
||||
invite.target_application as DiscordApplication
|
||||
),
|
||||
temporary: invite.temporary,
|
||||
uses: invite.uses,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/invite#invite-object
|
||||
*/
|
||||
export class Invite {
|
||||
constructor(session: Session, data: DiscordInvite) {
|
||||
this.session = session;
|
||||
|
||||
this.guild = data.guild
|
||||
? new InviteGuild(session, data.guild)
|
||||
: undefined;
|
||||
this.approximateMemberCount = data.approximate_member_count
|
||||
? data.approximate_member_count
|
||||
: undefined;
|
||||
this.approximatePresenceCount = data.approximate_presence_count
|
||||
? data.approximate_presence_count
|
||||
: undefined;
|
||||
this.code = data.code;
|
||||
this.expiresAt = data.expires_at
|
||||
? Number.parseInt(data.expires_at)
|
||||
: undefined;
|
||||
this.inviter = data.inviter
|
||||
? new User(session, data.inviter)
|
||||
: undefined;
|
||||
this.targetUser = data.target_user
|
||||
? new User(session, data.target_user)
|
||||
: undefined;
|
||||
this.targetApplication = data.target_application
|
||||
? new Application(
|
||||
session,
|
||||
data.target_application as DiscordApplication
|
||||
)
|
||||
: undefined;
|
||||
this.targetType = data.target_type;
|
||||
|
||||
if (data.channel) {
|
||||
const guildId = data.guild?.id ? data.guild.id : '';
|
||||
this.channel = new GuildChannel(
|
||||
session,
|
||||
data.channel as DiscordChannel,
|
||||
guildId
|
||||
);
|
||||
}
|
||||
|
||||
if (data.guild_scheduled_event) {
|
||||
this.guildScheduledEvent = {
|
||||
id: data.guild_scheduled_event.id,
|
||||
guildId: data.guild_scheduled_event.guild_id,
|
||||
channelId: data.guild_scheduled_event.channel_id
|
||||
? data.guild_scheduled_event.channel_id
|
||||
: undefined,
|
||||
creatorId: data.guild_scheduled_event.creator_id
|
||||
? data.guild_scheduled_event.creator_id
|
||||
: undefined,
|
||||
name: data.guild_scheduled_event.name,
|
||||
description: data.guild_scheduled_event.description
|
||||
? data.guild_scheduled_event.description
|
||||
: undefined,
|
||||
scheduledStartTime:
|
||||
data.guild_scheduled_event.scheduled_start_time,
|
||||
scheduledEndTime: data.guild_scheduled_event.scheduled_end_time
|
||||
? data.guild_scheduled_event.scheduled_end_time
|
||||
: undefined,
|
||||
privacyLevel: data.guild_scheduled_event.privacy_level,
|
||||
status: data.guild_scheduled_event.status,
|
||||
entityType: data.guild_scheduled_event.entity_type,
|
||||
entityId: data.guild ? data.guild.id : undefined,
|
||||
entityMetadata: data.guild_scheduled_event.entity_metadata
|
||||
? data.guild_scheduled_event.entity_metadata
|
||||
: undefined,
|
||||
creator: data.guild_scheduled_event.creator
|
||||
? new User(session, data.guild_scheduled_event.creator)
|
||||
: undefined,
|
||||
userCount: data.guild_scheduled_event.user_count
|
||||
? data.guild_scheduled_event.user_count
|
||||
: undefined,
|
||||
image: data.guild_scheduled_event.image
|
||||
? data.guild_scheduled_event.image
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (data.stage_instance) {
|
||||
const guildId = data.guild?.id ? data.guild.id : '';
|
||||
this.stageInstance = {
|
||||
members: data.stage_instance.members.map(
|
||||
m =>
|
||||
new Member(session, m as DiscordMemberWithUser, guildId)
|
||||
),
|
||||
participantCount: data.stage_instance.participant_count,
|
||||
speakerCount: data.stage_instance.speaker_count,
|
||||
topic: data.stage_instance.topic,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
guild?: InviteGuild;
|
||||
approximateMemberCount?: number;
|
||||
approximatePresenceCount?: number;
|
||||
code: string;
|
||||
expiresAt?: number;
|
||||
inviter?: User;
|
||||
targetUser?: User;
|
||||
targetType?: TargetTypes;
|
||||
channel?: Partial<GuildChannel>;
|
||||
stageInstance?: InviteStageInstance;
|
||||
guildScheduledEvent?: InviteScheduledEvent;
|
||||
targetApplication?: Partial<Application>;
|
||||
|
||||
async delete(): Promise<Invite> {
|
||||
await Guild.prototype.deleteInvite.call(this.guild, this.code);
|
||||
return this;
|
||||
}
|
||||
}
|
227
packages/core/src/structures/members.ts
Normal file
227
packages/core/src/structures/members.ts
Normal file
@ -0,0 +1,227 @@
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
DiscordMemberWithUser,
|
||||
DiscordThreadMember,
|
||||
} from '@biscuitland/api-types';
|
||||
import type { CreateGuildBan, ModifyGuildMember } from './guilds';
|
||||
import type { AvatarOptions } from './user';
|
||||
import { User } from './user';
|
||||
import { Guild } from './guilds';
|
||||
import { Util } from '../utils/util';
|
||||
import {
|
||||
USER_AVATAR,
|
||||
USER_DEFAULT_AVATAR,
|
||||
THREAD_USER,
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
/**
|
||||
* Represents a guild member
|
||||
* @link https://discord.com/developers/docs/resources/guild#guild-member-object
|
||||
*/
|
||||
export class Member implements Model {
|
||||
constructor(
|
||||
session: Session,
|
||||
data: DiscordMemberWithUser,
|
||||
guildId: Snowflake
|
||||
) {
|
||||
this.session = session;
|
||||
this.user = new User(session, data.user);
|
||||
this.guildId = guildId;
|
||||
this.avatarHash = data.avatar
|
||||
? data.avatar
|
||||
: undefined;
|
||||
|
||||
this.nickname = data.nick ? data.nick : undefined;
|
||||
this.premiumSince = data.premium_since
|
||||
? Number.parseInt(data.premium_since)
|
||||
: undefined;
|
||||
|
||||
this.joinedTimestamp = Number.parseInt(data.joined_at);
|
||||
this.roles = data.roles;
|
||||
this.deaf = !!data.deaf;
|
||||
this.mute = !!data.mute;
|
||||
this.pending = !!data.pending;
|
||||
this.communicationDisabledUntilTimestamp =
|
||||
data.communication_disabled_until
|
||||
? Number.parseInt(data.communication_disabled_until)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** the session that instantiated this member */
|
||||
readonly session: Session;
|
||||
|
||||
/** the user this guild member represents */
|
||||
user: User;
|
||||
|
||||
/** the choosen guild id */
|
||||
guildId: Snowflake;
|
||||
|
||||
/** the member's guild avatar hash optimized as a bigint */
|
||||
avatarHash?: string;
|
||||
|
||||
/** this user's guild nickname */
|
||||
nickname?: string;
|
||||
|
||||
/** when the user started boosting the guild */
|
||||
premiumSince?: number;
|
||||
|
||||
/** when the user joined the guild */
|
||||
joinedTimestamp: number;
|
||||
|
||||
/** array of role object ids */
|
||||
roles: Snowflake[];
|
||||
|
||||
/** whether the user is deafened in voice channels */
|
||||
deaf: boolean;
|
||||
|
||||
/** whether the user is muted in voice channels */
|
||||
mute: boolean;
|
||||
|
||||
/** whether the user has not yet passed the guild's Membership Screening requirements */
|
||||
pending: boolean;
|
||||
|
||||
/** when the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out */
|
||||
communicationDisabledUntilTimestamp?: number;
|
||||
|
||||
/** shorthand to User.id */
|
||||
get id(): Snowflake {
|
||||
return this.user.id;
|
||||
}
|
||||
|
||||
/** gets the nickname or the username */
|
||||
get nicknameOrUsername(): string {
|
||||
return this.nickname ?? this.user.username;
|
||||
}
|
||||
|
||||
/** gets the joinedAt timestamp as a Date */
|
||||
get joinedAt(): Date {
|
||||
return new Date(this.joinedTimestamp);
|
||||
}
|
||||
|
||||
/** bans a member from this guild and delete previous messages sent by the member */
|
||||
async ban(options: CreateGuildBan): Promise<Member> {
|
||||
await Guild.prototype.banMember.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.user.id,
|
||||
options
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** kicks a member from this guild */
|
||||
async kick(options: { reason?: string }): Promise<Member> {
|
||||
await Guild.prototype.kickMember.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.user.id,
|
||||
options.reason
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** unbans a member from this guild */
|
||||
async unban(): Promise<void> {
|
||||
await Guild.prototype.unbanMember.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.user.id
|
||||
);
|
||||
}
|
||||
|
||||
/** edits member's nickname, roles, etc */
|
||||
async edit(options: ModifyGuildMember): Promise<Member> {
|
||||
const member = await Guild.prototype.editMember.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.user.id,
|
||||
options
|
||||
);
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
/** adds a role to this member */
|
||||
async addRole(roleId: Snowflake, reason?: string): Promise<void> {
|
||||
await Guild.prototype.addRole.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.user.id,
|
||||
roleId,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
/** removes a role from this member */
|
||||
async removeRole(
|
||||
roleId: Snowflake,
|
||||
options: { reason?: string } = {}
|
||||
): Promise<void> {
|
||||
await Guild.prototype.removeRole.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.user.id,
|
||||
roleId,
|
||||
options.reason
|
||||
);
|
||||
}
|
||||
|
||||
/** gets the members's guild avatar, not to be confused with Member.user.avatarURL() */
|
||||
avatarURL(options: AvatarOptions): string {
|
||||
let url: string;
|
||||
|
||||
if (this.user.bot) {
|
||||
return this.user.avatarURL(options);
|
||||
}
|
||||
|
||||
if (!this.avatarHash) {
|
||||
url = USER_DEFAULT_AVATAR(Number(this.user.discriminator) % 5);
|
||||
} else {
|
||||
url = USER_AVATAR(
|
||||
this.user.id,
|
||||
this.avatarHash
|
||||
);
|
||||
}
|
||||
|
||||
return Util.formatImageURL(url, options.size ?? 128, options.format);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<@!${this.user.id}>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A member that comes from a thread
|
||||
* @link https://discord.com/developers/docs/resources/channel#thread-member-object
|
||||
* **/
|
||||
export class ThreadMember implements Model {
|
||||
constructor(session: Session, data: DiscordThreadMember) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.flags = data.flags;
|
||||
this.timestamp = Date.parse(data.join_timestamp);
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
readonly id: Snowflake;
|
||||
flags: number;
|
||||
timestamp: number;
|
||||
|
||||
get threadId(): Snowflake {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
async quitThread(memberId?: Snowflake): Promise<void> {
|
||||
await this.session.rest.delete<undefined>(
|
||||
THREAD_USER(this.id, memberId ?? this.session.botId),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
async fetchMember(memberId?: Snowflake): Promise<ThreadMember> {
|
||||
const member = await this.session.rest.get<DiscordThreadMember>(
|
||||
THREAD_USER(this.id, memberId ?? this.session.botId)
|
||||
);
|
||||
|
||||
return new ThreadMember(this.session, member);
|
||||
}
|
||||
}
|
98
packages/core/src/structures/message-reaction.ts
Normal file
98
packages/core/src/structures/message-reaction.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
DiscordMemberWithUser,
|
||||
DiscordMessageReactionAdd,
|
||||
DiscordReaction,
|
||||
} from '@biscuitland/api-types';
|
||||
import { Emoji } from './emojis';
|
||||
import { Member } from './members';
|
||||
|
||||
/**
|
||||
* Represents when a new reaction was added to a message.
|
||||
* @link https://discord.com/developers/docs/topics/gateway#message-reaction-add
|
||||
*/
|
||||
export interface MessageReactionAdd {
|
||||
userId: string;
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
guildId?: string;
|
||||
member?: Member;
|
||||
emoji: Partial<Emoji>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents when a reaction was removed from a message.
|
||||
* Equal to MessageReactionAdd but without 'member' property.
|
||||
* @see {@link MessageReactionAdd}
|
||||
* @link https://discord.com/developers/docs/topics/gateway#message-reaction-remove-message-reaction-remove-event-fields
|
||||
*/
|
||||
export type MessageReactionRemove = Omit<MessageReactionAdd, 'member'>;
|
||||
|
||||
/**
|
||||
* Represents when all reactions were removed from a message.
|
||||
* Equals to MessageReactionAdd but with 'channelId', 'messageId' and 'guildId' properties guaranteed.
|
||||
* @see {@link MessageReactionAdd}
|
||||
* @link https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all
|
||||
*/
|
||||
export type MessageReactionRemoveAll = Pick<
|
||||
MessageReactionAdd,
|
||||
'channelId' | 'messageId' | 'guildId'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Represents when a reaction-emoji was removed from a message.
|
||||
* Equals to MessageReactionAdd but with 'channelId', 'messageId', 'emoji' and 'guildId' properties guaranteed.
|
||||
* @see {@link MessageReactionRemove}
|
||||
* @see {@link Emoji}
|
||||
* @link https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji
|
||||
*/
|
||||
export type MessageReactionRemoveEmoji = Pick<
|
||||
MessageReactionAdd,
|
||||
'channelId' | 'guildId' | 'messageId' | 'emoji'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Creates a new MessageReactionAdd object.
|
||||
* @param session - Current application session.
|
||||
* @param data - Discord message reaction to parse.
|
||||
*/
|
||||
export function NewMessageReactionAdd(
|
||||
session: Session,
|
||||
data: DiscordMessageReactionAdd
|
||||
): MessageReactionAdd {
|
||||
return {
|
||||
userId: data.user_id,
|
||||
channelId: data.channel_id,
|
||||
messageId: data.message_id,
|
||||
guildId: data.guild_id,
|
||||
member: data.member
|
||||
? new Member(
|
||||
session,
|
||||
data.member as DiscordMemberWithUser,
|
||||
data.guild_id || ''
|
||||
)
|
||||
: undefined,
|
||||
emoji: new Emoji(session, data.emoji),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a reaction
|
||||
* @link https://discord.com/developers/docs/resources/channel#reaction-object
|
||||
*/
|
||||
export class MessageReaction {
|
||||
constructor(session: Session, data: DiscordReaction) {
|
||||
this.session = session;
|
||||
this.me = data.me;
|
||||
this.count = data.count;
|
||||
this.emoji = new Emoji(session, data.emoji);
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
me: boolean;
|
||||
count: number;
|
||||
emoji: Emoji;
|
||||
}
|
||||
|
||||
export default MessageReaction;
|
615
packages/core/src/structures/message.ts
Normal file
615
packages/core/src/structures/message.ts
Normal file
@ -0,0 +1,615 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import type { Model } from './base';
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
AllowedMentionsTypes,
|
||||
DiscordEmbed,
|
||||
DiscordMessage,
|
||||
DiscordMessageComponents,
|
||||
DiscordUser,
|
||||
FileContent,
|
||||
MessageActivityTypes,
|
||||
MessageTypes,
|
||||
GetReactions,
|
||||
} from '@biscuitland/api-types';
|
||||
import type { Channel } from './channels';
|
||||
import type { Component } from './components';
|
||||
import type { MessageInteraction } from './interactions';
|
||||
import { MessageFlags, Util } from '../utils/util';
|
||||
import { Snowflake } from '../snowflakes';
|
||||
import { ChannelFactory, ThreadChannel } from './channels';
|
||||
import { User } from './user';
|
||||
import { Member } from './members';
|
||||
import { Attachment } from './attachment';
|
||||
import { ComponentFactory } from './components';
|
||||
import { MessageReaction } from './message-reaction';
|
||||
import { Application, NewTeam } from './application';
|
||||
import { InteractionFactory } from './interactions';
|
||||
import type { StickerItem } from './sticker';
|
||||
|
||||
import {
|
||||
CHANNEL_PIN,
|
||||
CHANNEL_MESSAGE,
|
||||
CHANNEL_MESSAGES,
|
||||
CHANNEL_MESSAGE_REACTION_ME,
|
||||
CHANNEL_MESSAGE_REACTION_USER,
|
||||
CHANNEL_MESSAGE_REACTION,
|
||||
CHANNEL_MESSAGE_REACTIONS,
|
||||
CHANNEL_MESSAGE_CROSSPOST,
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
export interface GuildMessage extends Message {
|
||||
guildId: Snowflake;
|
||||
}
|
||||
|
||||
export type WebhookMessage = Message & {
|
||||
author: Partial<User>;
|
||||
webhook: WebhookAuthor;
|
||||
member: undefined;
|
||||
};
|
||||
|
||||
export interface MessageActivity {
|
||||
partyId?: Snowflake;
|
||||
type: MessageActivityTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/channel#allowed-mentions-object
|
||||
*/
|
||||
export interface AllowedMentions {
|
||||
parse?: AllowedMentionsTypes[];
|
||||
repliedUser?: boolean;
|
||||
roles?: Snowflake[];
|
||||
users?: Snowflake[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://github.com/denoland/deno_doc/blob/main/lib/types.d.ts
|
||||
* channelId is optional when creating a reply, but will always be present when receiving an event/response that includes this data model.
|
||||
*/
|
||||
export interface CreateMessageReference {
|
||||
messageId: Snowflake;
|
||||
channelId?: Snowflake;
|
||||
guildId?: Snowflake;
|
||||
failIfNotExists?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/channel#create-message-json-params
|
||||
* Posts a message to a guild text or DM channel. Returns a message object. Fires a Message Create Gateway event.
|
||||
*/
|
||||
export interface CreateMessage {
|
||||
embeds?: DiscordEmbed[];
|
||||
content?: string;
|
||||
allowedMentions?: AllowedMentions;
|
||||
files?: FileContent[];
|
||||
messageReference?: CreateMessageReference;
|
||||
tts?: boolean;
|
||||
components?: DiscordMessageComponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/channel#edit-message-json-params
|
||||
* Edit a previously sent message.
|
||||
* Returns a {@link Message} object. Fires a Message Update Gateway event.
|
||||
*/
|
||||
export interface EditMessage extends Partial<CreateMessage> {
|
||||
flags?: MessageFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a guild or unicode {@link Emoji}
|
||||
*/
|
||||
export type EmojiResolvable =
|
||||
| string
|
||||
| {
|
||||
name: string;
|
||||
id: Snowflake;
|
||||
};
|
||||
|
||||
/**
|
||||
* A partial {@link User} to represent the author of a message sent by a webhook
|
||||
*/
|
||||
export interface WebhookAuthor {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
avatar?: bigint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/channel#message-object
|
||||
* Represents a message
|
||||
*/
|
||||
export class Message implements Model {
|
||||
constructor(session: Session, data: DiscordMessage) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
|
||||
this.type = data.type;
|
||||
this.channelId = data.channel_id;
|
||||
this.guildId = data.guild_id;
|
||||
this.applicationId = data.application_id;
|
||||
|
||||
this.mentions = {
|
||||
users: data.mentions?.map(user => new User(session, user)) ?? [],
|
||||
roleIds: data.mention_roles ?? [],
|
||||
channels:
|
||||
data.mention_channels?.map(channel =>
|
||||
ChannelFactory.from(session, channel)
|
||||
) ?? [],
|
||||
};
|
||||
|
||||
if (!data.webhook_id) {
|
||||
this.author = new User(session, data.author);
|
||||
}
|
||||
|
||||
this.flags = data.flags;
|
||||
this.pinned = !!data.pinned;
|
||||
this.tts = !!data.tts;
|
||||
this.content = data.content!;
|
||||
this.nonce = data.nonce;
|
||||
this.mentionEveryone = data.mention_everyone;
|
||||
|
||||
this.timestamp = Date.parse(data.timestamp);
|
||||
this.editedTimestamp = data.edited_timestamp
|
||||
? Date.parse(data.edited_timestamp)
|
||||
: undefined;
|
||||
|
||||
this.reactions =
|
||||
data.reactions?.map(react => new MessageReaction(session, react)) ??
|
||||
[];
|
||||
this.attachments = data.attachments.map(
|
||||
attachment => new Attachment(session, attachment)
|
||||
);
|
||||
this.embeds = data.embeds;
|
||||
|
||||
if (data.interaction) {
|
||||
this.interaction = InteractionFactory.fromMessage(
|
||||
session,
|
||||
data.interaction,
|
||||
data.guild_id
|
||||
);
|
||||
}
|
||||
|
||||
if (data.thread && data.guild_id) {
|
||||
this.thread = new ThreadChannel(
|
||||
session,
|
||||
data.thread,
|
||||
data.guild_id
|
||||
);
|
||||
}
|
||||
|
||||
// webhook handling
|
||||
if (data.webhook_id && data.author.discriminator === '0000') {
|
||||
this.webhook = {
|
||||
id: data.webhook_id!,
|
||||
username: data.author.username,
|
||||
discriminator: data.author.discriminator,
|
||||
avatar: data.author.avatar
|
||||
? Util.iconHashToBigInt(data.author.avatar)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// user is always null on MessageCreate and its replaced with author
|
||||
if (data.guild_id && data.member && !this.isWebhookMessage()) {
|
||||
this.member = new Member(
|
||||
session,
|
||||
{ ...data.member, user: data.author },
|
||||
data.guild_id
|
||||
);
|
||||
}
|
||||
|
||||
this.components =
|
||||
data.components?.map(component =>
|
||||
ComponentFactory.from(session, component)
|
||||
) ?? [];
|
||||
|
||||
if (data.activity) {
|
||||
this.activity = {
|
||||
partyId: data.activity.party_id,
|
||||
type: data.activity.type,
|
||||
};
|
||||
}
|
||||
|
||||
if (data.sticker_items) {
|
||||
this.stickers = data.sticker_items.map(si => {
|
||||
return {
|
||||
id: si.id,
|
||||
name: si.name,
|
||||
formatType: si.format_type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (data.application) {
|
||||
const application: Partial<Application> = {
|
||||
id: data.application.id,
|
||||
icon: data.application.icon ? data.application.icon : undefined,
|
||||
name: data.application.name,
|
||||
guildId: data.application.guild_id,
|
||||
flags: data.application.flags,
|
||||
botPublic: data.application.bot_public,
|
||||
owner: data.application.owner
|
||||
? new User(session, data.application.owner as DiscordUser)
|
||||
: undefined,
|
||||
botRequireCodeGrant: data.application.bot_require_code_grant,
|
||||
coverImage: data.application.cover_image,
|
||||
customInstallURL: data.application.custom_install_url,
|
||||
description: data.application.description,
|
||||
installParams: data.application.install_params,
|
||||
tags: data.application.tags,
|
||||
verifyKey: data.application.verify_key,
|
||||
team: data.application.team
|
||||
? NewTeam(session, data.application.team)
|
||||
: undefined,
|
||||
primarySkuId: data.application.primary_sku_id,
|
||||
privacyPolicyURL: data.application.privacy_policy_url,
|
||||
rpcOrigins: data.application.rpc_origins,
|
||||
slug: data.application.slug,
|
||||
};
|
||||
|
||||
Object.setPrototypeOf(application, Application.prototype);
|
||||
|
||||
this.application = application;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reference to the client that instantiated this Message */
|
||||
readonly session: Session;
|
||||
|
||||
/** id of the message */
|
||||
readonly id: Snowflake;
|
||||
|
||||
/** type of message */
|
||||
type: MessageTypes;
|
||||
|
||||
/** id of the channel the message was sent in */
|
||||
channelId: Snowflake;
|
||||
|
||||
/** id of the guild the message was sent in, this should exist on MESSAGE_CREATE and MESSAGE_UPDATE events */
|
||||
guildId?: Snowflake;
|
||||
|
||||
/** if the message is an Interaction or application-owned webhook, this is the id of the application */
|
||||
applicationId?: Snowflake;
|
||||
|
||||
/** mentions if any */
|
||||
mentions: {
|
||||
/** users specifically mentioned in the message */
|
||||
users: User[];
|
||||
|
||||
/** roles specifically mentioned in this message */
|
||||
roleIds: Snowflake[];
|
||||
|
||||
/** channels specifically mentioned in the message */
|
||||
channels: Channel[];
|
||||
};
|
||||
|
||||
/** sent if the message is a response to an Interaction */
|
||||
interaction?: MessageInteraction;
|
||||
|
||||
/** the author of this message, this field is **not** sent on webhook messages */
|
||||
author!: User;
|
||||
|
||||
/** message flags combined as a bitfield */
|
||||
flags?: MessageFlags;
|
||||
|
||||
/** whether this message is pinned */
|
||||
pinned: boolean;
|
||||
|
||||
/** whether this was a TTS message */
|
||||
tts: boolean;
|
||||
|
||||
/** contents of the message */
|
||||
content: string;
|
||||
|
||||
/** used for validating a message was sent */
|
||||
nonce?: string | number;
|
||||
|
||||
/** whether this message mentions everyone */
|
||||
mentionEveryone: boolean;
|
||||
|
||||
/** when this message was sent */
|
||||
timestamp: number;
|
||||
|
||||
/** when this message was edited */
|
||||
editedTimestamp?: number;
|
||||
|
||||
/**
|
||||
* sent if the message contains stickers
|
||||
* **this contains sticker items not stickers**
|
||||
*/
|
||||
stickers?: StickerItem[];
|
||||
|
||||
/** reactions to the message */
|
||||
reactions: MessageReaction[];
|
||||
|
||||
/** any attached files */
|
||||
attachments: Attachment[];
|
||||
|
||||
/** any embedded content */
|
||||
embeds: DiscordEmbed[];
|
||||
|
||||
/** member properties for this message's author */
|
||||
member?: Member;
|
||||
|
||||
/** the thread that was started from this message, includes {@link ThreadMember} */
|
||||
thread?: ThreadChannel;
|
||||
|
||||
/** sent if the message contains components like buttons, action rows, or other interactive components */
|
||||
components: Component[];
|
||||
|
||||
/** if the message is generated by a webhook, this is the webhook's author data */
|
||||
webhook?: WebhookAuthor;
|
||||
|
||||
/** sent with Rich Presence-related chat embeds */
|
||||
application?: Partial<Application>;
|
||||
|
||||
/** sent with Rich Presence-related chat embeds */
|
||||
activity?: MessageActivity;
|
||||
|
||||
/** gets the timestamp of this message, this does not requires the timestamp field */
|
||||
get createdTimestamp(): number {
|
||||
return Snowflake.snowflakeToTimestamp(this.id);
|
||||
}
|
||||
|
||||
/** gets the timestamp of this message as a Date */
|
||||
get createdAt(): Date {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/** gets the timestamp of this message (sent by the API) */
|
||||
get sentAt(): Date {
|
||||
return new Date(this.timestamp);
|
||||
}
|
||||
|
||||
/** gets the edited timestamp as a Date */
|
||||
get editedAt(): Date | undefined {
|
||||
return this.editedTimestamp
|
||||
? new Date(this.editedTimestamp)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** whether this message was edited */
|
||||
get edited(): number | undefined {
|
||||
return this.editedTimestamp;
|
||||
}
|
||||
|
||||
/** gets the url of the message that points to the message */
|
||||
get url(): string {
|
||||
return `https://discord.com/channels/${this.guildId ?? '@me'}/${
|
||||
this.channelId
|
||||
}/${this.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility with Discordeno
|
||||
* same as Message.author.bot
|
||||
*/
|
||||
get isBot(): boolean {
|
||||
return this.author.bot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins this message
|
||||
*/
|
||||
async pin(): Promise<void> {
|
||||
await this.session.rest.put<undefined>(
|
||||
CHANNEL_PIN(this.channelId, this.id),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpins this message
|
||||
*/
|
||||
async unpin(): Promise<void> {
|
||||
await this.session.rest.delete<undefined>(
|
||||
CHANNEL_PIN(this.channelId, this.id),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/** Edits the current message */
|
||||
async edit(options: EditMessage): Promise<Message> {
|
||||
const message = await this.session.rest.post<DiscordMessage>(
|
||||
CHANNEL_MESSAGE(this.id, this.channelId),
|
||||
{
|
||||
content: options.content,
|
||||
allowed_mentions: {
|
||||
parse: options.allowedMentions?.parse,
|
||||
roles: options.allowedMentions?.roles,
|
||||
users: options.allowedMentions?.users,
|
||||
replied_user: options.allowedMentions?.repliedUser,
|
||||
},
|
||||
flags: options.flags,
|
||||
embeds: options.embeds,
|
||||
}
|
||||
);
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
|
||||
/** edits the current message flags to supress its embeds */
|
||||
async suppressEmbeds(suppress: true): Promise<Message>;
|
||||
async suppressEmbeds(suppress: false): Promise<Message | undefined>;
|
||||
async suppressEmbeds(suppress = true) {
|
||||
if (this.flags === MessageFlags.SupressEmbeds && suppress === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.edit({ flags: MessageFlags.SupressEmbeds });
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/** deletes this message */
|
||||
async delete(reason?: string): Promise<Message> {
|
||||
await this.session.rest.delete<undefined>(
|
||||
CHANNEL_MESSAGE(this.channelId, this.id),
|
||||
{ reason }
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Replies directly in the channel where the message was sent */
|
||||
async reply(options: CreateMessage): Promise<Message> {
|
||||
const message = await this.session.rest.post<DiscordMessage>(
|
||||
CHANNEL_MESSAGES(this.channelId),
|
||||
{
|
||||
content: options.content,
|
||||
file: options.files,
|
||||
allowed_mentions: {
|
||||
parse: options.allowedMentions?.parse,
|
||||
roles: options.allowedMentions?.roles,
|
||||
users: options.allowedMentions?.users,
|
||||
replied_user: options.allowedMentions?.repliedUser,
|
||||
},
|
||||
message_reference: options.messageReference
|
||||
? {
|
||||
message_id: options.messageReference.messageId,
|
||||
channel_id: options.messageReference.channelId,
|
||||
guild_id: options.messageReference.guildId,
|
||||
fail_if_not_exists:
|
||||
options.messageReference.failIfNotExists ??
|
||||
true,
|
||||
}
|
||||
: undefined,
|
||||
embeds: options.embeds,
|
||||
tts: options.tts,
|
||||
components: options.components,
|
||||
}
|
||||
);
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
|
||||
/** alias for Message.addReaction */
|
||||
get react() {
|
||||
return this.addReaction;
|
||||
}
|
||||
|
||||
/** adds a Reaction */
|
||||
async addReaction(reaction: EmojiResolvable): Promise<void> {
|
||||
const r =
|
||||
typeof reaction === 'string'
|
||||
? reaction
|
||||
: `${reaction.name}:${reaction.id}`;
|
||||
|
||||
await this.session.rest.put<undefined>(
|
||||
CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/** removes a reaction from someone */
|
||||
async removeReaction(
|
||||
reaction: EmojiResolvable,
|
||||
options?: { userId: Snowflake }
|
||||
): Promise<void> {
|
||||
const r =
|
||||
typeof reaction === 'string'
|
||||
? reaction
|
||||
: `${reaction.name}:${reaction.id}`;
|
||||
|
||||
await this.session.rest.delete<undefined>(
|
||||
options?.userId
|
||||
? CHANNEL_MESSAGE_REACTION_USER(
|
||||
this.channelId,
|
||||
this.id,
|
||||
r,
|
||||
options.userId
|
||||
)
|
||||
: CHANNEL_MESSAGE_REACTION_ME(this.channelId, this.id, r),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users who reacted with this emoji
|
||||
* not recommended since the cache handles reactions already
|
||||
*/
|
||||
async fetchReactions(
|
||||
reaction: EmojiResolvable,
|
||||
options?: GetReactions
|
||||
): Promise<User[]> {
|
||||
const r =
|
||||
typeof reaction === 'string'
|
||||
? reaction
|
||||
: `${reaction.name}:${reaction.id}`;
|
||||
|
||||
const users = await this.session.rest.get<DiscordUser[]>(
|
||||
CHANNEL_MESSAGE_REACTION(
|
||||
this.channelId,
|
||||
this.id,
|
||||
encodeURIComponent(r),
|
||||
options
|
||||
)
|
||||
);
|
||||
|
||||
return users.map(user => new User(this.session, user));
|
||||
}
|
||||
|
||||
/**
|
||||
* same as Message.removeReaction but removes using a unicode emoji
|
||||
*/
|
||||
async removeReactionEmoji(reaction: EmojiResolvable): Promise<void> {
|
||||
const r =
|
||||
typeof reaction === 'string'
|
||||
? reaction
|
||||
: `${reaction.name}:${reaction.id}`;
|
||||
|
||||
await this.session.rest.delete<undefined>(
|
||||
CHANNEL_MESSAGE_REACTION(this.channelId, this.id, r),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/** nukes every reaction on the message */
|
||||
async nukeReactions(): Promise<void> {
|
||||
await this.session.rest.delete<undefined>(
|
||||
CHANNEL_MESSAGE_REACTIONS(this.channelId, this.id),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/** publishes/crossposts a message to all followers */
|
||||
async crosspost(): Promise<Message> {
|
||||
const message = await this.session.rest.post<DiscordMessage>(
|
||||
CHANNEL_MESSAGE_CROSSPOST(this.channelId, this.id),
|
||||
{}
|
||||
);
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
|
||||
/** fetches this message, meant to be used with Function.call since its redundant */
|
||||
async fetch(): Promise<Message | undefined> {
|
||||
const message = await this.session.rest.get<DiscordMessage>(
|
||||
CHANNEL_MESSAGE(this.channelId, this.id)
|
||||
);
|
||||
|
||||
if (!message?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
|
||||
/** alias of Message.crosspost */
|
||||
get publish() {
|
||||
return this.crosspost;
|
||||
}
|
||||
|
||||
/** wheter the message comes from a guild **/
|
||||
inGuild(): this is GuildMessage {
|
||||
return !!this.guildId;
|
||||
}
|
||||
|
||||
/** wheter the messages comes from a Webhook */
|
||||
isWebhookMessage(): this is WebhookMessage {
|
||||
return !!this.webhook;
|
||||
}
|
||||
}
|
94
packages/core/src/structures/presence.ts
Normal file
94
packages/core/src/structures/presence.ts
Normal file
@ -0,0 +1,94 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import type {
|
||||
ActivityTypes,
|
||||
DiscordActivityButton,
|
||||
DiscordActivitySecrets,
|
||||
DiscordClientStatus,
|
||||
DiscordPresenceUpdate,
|
||||
} from '@biscuitland/api-types';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { ComponentEmoji } from '../utils/util';
|
||||
import { User } from './user';
|
||||
|
||||
export interface ActivityAssets {
|
||||
largeImage?: string;
|
||||
largeText?: string;
|
||||
smallImage?: string;
|
||||
smallText?: string;
|
||||
}
|
||||
|
||||
export interface Activities {
|
||||
name: string;
|
||||
type: ActivityTypes;
|
||||
url?: string;
|
||||
createdAt: number;
|
||||
timestamps?: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
applicationId?: Snowflake;
|
||||
details?: string;
|
||||
state?: string;
|
||||
emoji?: ComponentEmoji;
|
||||
party?: {
|
||||
id?: string;
|
||||
size?: number[];
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
secrets?: DiscordActivitySecrets;
|
||||
instance?: boolean;
|
||||
flags?: number;
|
||||
buttons?: DiscordActivityButton;
|
||||
}
|
||||
|
||||
export enum StatusTypes {
|
||||
online = 0,
|
||||
dnd = 1,
|
||||
idle = 2,
|
||||
invisible = 3,
|
||||
offline = 4,
|
||||
}
|
||||
|
||||
export class Presence {
|
||||
constructor(session: Session, data: DiscordPresenceUpdate) {
|
||||
this.session = session;
|
||||
this.user = new User(this.session, data.user);
|
||||
this.guildId = data.guild_id;
|
||||
this.status = StatusTypes[data.status];
|
||||
this.activities = data.activities.map<Activities>(activity =>
|
||||
Object({
|
||||
name: activity.name,
|
||||
type: activity.type,
|
||||
url: activity.url ? activity.url : undefined,
|
||||
createdAt: activity.created_at,
|
||||
timestamps: activity.timestamps,
|
||||
applicationId: activity.application_id,
|
||||
details: activity.details ? activity.details : undefined,
|
||||
state: activity.state,
|
||||
emoji: activity.emoji ? activity.emoji : undefined,
|
||||
party: activity.party ? activity.party : undefined,
|
||||
assets: activity.assets
|
||||
? {
|
||||
largeImage: activity.assets.large_image,
|
||||
largeText: activity.assets.large_text,
|
||||
smallImage: activity.assets.small_image,
|
||||
smallText: activity.assets.small_text,
|
||||
}
|
||||
: null,
|
||||
secrets: activity.secrets ? activity.secrets : undefined,
|
||||
instance: !!activity.instance,
|
||||
flags: activity.flags,
|
||||
buttons: activity.buttons,
|
||||
})
|
||||
);
|
||||
this.clientStatus = data.client_status;
|
||||
}
|
||||
|
||||
session: Session;
|
||||
user: User;
|
||||
guildId: Snowflake;
|
||||
status: StatusTypes;
|
||||
activities: Activities[];
|
||||
clientStatus: DiscordClientStatus;
|
||||
}
|
95
packages/core/src/structures/role.ts
Normal file
95
packages/core/src/structures/role.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import type { Model } from './base';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { DiscordRole } from '@biscuitland/api-types';
|
||||
import type { ModifyGuildRole } from './guilds';
|
||||
import { Snowflake } from '../snowflakes';
|
||||
import { Guild } from './guilds';
|
||||
import { Util } from '../utils/util';
|
||||
import { Permissions } from './special/permissions';
|
||||
|
||||
export class Role implements Model {
|
||||
constructor(session: Session, data: DiscordRole, guildId: Snowflake) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.guildId = guildId;
|
||||
this.hoist = data.hoist;
|
||||
this.iconHash = data.icon
|
||||
? Util.iconHashToBigInt(data.icon)
|
||||
: undefined;
|
||||
this.color = data.color;
|
||||
this.name = data.name;
|
||||
this.unicodeEmoji = data.unicode_emoji;
|
||||
this.mentionable = data.mentionable;
|
||||
this.managed = data.managed;
|
||||
this.permissions = new Permissions(BigInt(data.permissions));
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
readonly id: Snowflake;
|
||||
|
||||
readonly guildId: Snowflake;
|
||||
|
||||
hoist: boolean;
|
||||
iconHash?: bigint;
|
||||
color: number;
|
||||
name: string;
|
||||
unicodeEmoji?: string;
|
||||
mentionable: boolean;
|
||||
managed: boolean;
|
||||
permissions: Permissions;
|
||||
|
||||
get createdTimestamp(): number {
|
||||
return Snowflake.snowflakeToTimestamp(this.id);
|
||||
}
|
||||
|
||||
get createdAt(): Date {
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
get hexColor(): string {
|
||||
return `#${this.color.toString(16).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
await Guild.prototype.deleteRole.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.id
|
||||
);
|
||||
}
|
||||
|
||||
async edit(options: ModifyGuildRole): Promise<Role> {
|
||||
const role = await Guild.prototype.editRole.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
this.id,
|
||||
options
|
||||
);
|
||||
return role;
|
||||
}
|
||||
|
||||
async add(memberId: Snowflake, reason?: string): Promise<void> {
|
||||
await Guild.prototype.addRole.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
memberId,
|
||||
this.id,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
async remove(memberId: Snowflake, reason?: string): Promise<void> {
|
||||
await Guild.prototype.removeRole.call(
|
||||
{ id: this.guildId, session: this.session },
|
||||
memberId,
|
||||
this.id,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
switch (this.id) {
|
||||
case this.guildId:
|
||||
return '@everyone';
|
||||
default:
|
||||
return `<@&${this.id}>`;
|
||||
}
|
||||
}
|
||||
}
|
53
packages/core/src/structures/scheduled-events.ts
Normal file
53
packages/core/src/structures/scheduled-events.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import type {
|
||||
DiscordScheduledEvent,
|
||||
DiscordScheduledEventEntityMetadata,
|
||||
ScheduledEventEntityType,
|
||||
ScheduledEventStatus,
|
||||
} from '@biscuitland/api-types';
|
||||
import { PrivacyLevels } from './stage-instance';
|
||||
import { User } from './user';
|
||||
|
||||
export class ScheduledEvent implements Model {
|
||||
constructor(session: Session, data: DiscordScheduledEvent) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.guildId = data.guild_id;
|
||||
this.channelId = data.channel_id;
|
||||
this.creatorId = data.creator_id ? data.creator_id : undefined;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.scheduledStartTime = data.scheduled_start_time;
|
||||
this.scheduledEndTime = data.scheduled_end_time;
|
||||
this.privacyLevel = PrivacyLevels.GuildOnly;
|
||||
this.status = data.status;
|
||||
this.entityType = data.entity_type;
|
||||
this.entityMetadata = data.entity_metadata
|
||||
? data.entity_metadata
|
||||
: undefined;
|
||||
this.creator = data.creator
|
||||
? new User(session, data.creator)
|
||||
: undefined;
|
||||
this.userCount = data.user_count;
|
||||
this.image = data.image ? data.image : undefined;
|
||||
}
|
||||
|
||||
session: Session;
|
||||
id: Snowflake;
|
||||
guildId: Snowflake;
|
||||
channelId: Snowflake | null;
|
||||
creatorId?: Snowflake;
|
||||
name: string;
|
||||
description?: string;
|
||||
scheduledStartTime: string;
|
||||
scheduledEndTime: string | null;
|
||||
privacyLevel: PrivacyLevels;
|
||||
status: ScheduledEventStatus;
|
||||
entityType: ScheduledEventEntityType;
|
||||
entityMetadata?: DiscordScheduledEventEntityMetadata;
|
||||
creator?: User;
|
||||
userCount?: number;
|
||||
image?: string;
|
||||
}
|
@ -0,0 +1,337 @@
|
||||
import type {
|
||||
DiscordInteractionDataOption,
|
||||
DiscordInteractionDataResolved,
|
||||
} from '@biscuitland/api-types';
|
||||
import { ApplicationCommandOptionTypes } from '@biscuitland/api-types';
|
||||
|
||||
export function transformOasisInteractionDataOption(
|
||||
o: DiscordInteractionDataOption
|
||||
): CommandInteractionOption {
|
||||
const output: CommandInteractionOption = {
|
||||
...o,
|
||||
Otherwise: o.value as string | boolean | number | undefined,
|
||||
};
|
||||
|
||||
switch (o.type) {
|
||||
case ApplicationCommandOptionTypes.String:
|
||||
output.String = o.value as string;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Number:
|
||||
output.Number = o.value as number;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Integer:
|
||||
output.Integer = o.value as number;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Boolean:
|
||||
output.Boolean = o.value as boolean;
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Role:
|
||||
output.Role = BigInt(o.value as string);
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.User:
|
||||
output.User = BigInt(o.value as string);
|
||||
break;
|
||||
case ApplicationCommandOptionTypes.Channel:
|
||||
output.Channel = BigInt(o.value as string);
|
||||
break;
|
||||
|
||||
case ApplicationCommandOptionTypes.Mentionable:
|
||||
case ApplicationCommandOptionTypes.SubCommand:
|
||||
case ApplicationCommandOptionTypes.SubCommandGroup:
|
||||
default:
|
||||
output.Otherwise = o.value as string | boolean | number | undefined;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export interface CommandInteractionOption
|
||||
extends Omit<DiscordInteractionDataOption, 'value'> {
|
||||
Attachment?: string;
|
||||
Boolean?: boolean;
|
||||
User?: bigint;
|
||||
Role?: bigint;
|
||||
Number?: number;
|
||||
Integer?: number;
|
||||
Channel?: bigint;
|
||||
String?: string;
|
||||
Mentionable?: string;
|
||||
Otherwise: string | number | boolean | bigint | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class to get the resolved options for a command
|
||||
* It is really typesafe
|
||||
* @example const option = ctx.options.getStringOption("name");
|
||||
*/
|
||||
export class CommandInteractionOptionResolver {
|
||||
#subcommand?: string;
|
||||
#group?: string;
|
||||
|
||||
hoistedOptions: CommandInteractionOption[];
|
||||
resolved?: DiscordInteractionDataResolved;
|
||||
|
||||
constructor(
|
||||
options?: DiscordInteractionDataOption[],
|
||||
resolved?: DiscordInteractionDataResolved
|
||||
) {
|
||||
this.hoistedOptions =
|
||||
options?.map(transformOasisInteractionDataOption) ?? [];
|
||||
|
||||
// warning: black magic do not edit and thank djs authors
|
||||
|
||||
if (
|
||||
this.hoistedOptions[0]?.type ===
|
||||
ApplicationCommandOptionTypes.SubCommandGroup
|
||||
) {
|
||||
this.#group = this.hoistedOptions[0].name;
|
||||
this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(
|
||||
transformOasisInteractionDataOption
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.hoistedOptions[0]?.type ===
|
||||
ApplicationCommandOptionTypes.SubCommand
|
||||
) {
|
||||
this.#subcommand = this.hoistedOptions[0].name;
|
||||
this.hoistedOptions = (this.hoistedOptions[0].options ?? []).map(
|
||||
transformOasisInteractionDataOption
|
||||
);
|
||||
}
|
||||
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
private getTypedOption(
|
||||
name: string | number,
|
||||
type: ApplicationCommandOptionTypes,
|
||||
properties: (keyof CommandInteractionOption)[],
|
||||
required: boolean
|
||||
): CommandInteractionOption | void {
|
||||
const option: CommandInteractionOption | undefined = this.get(
|
||||
name,
|
||||
required
|
||||
);
|
||||
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.type !== type) {
|
||||
// pass
|
||||
}
|
||||
|
||||
if (
|
||||
required === true &&
|
||||
properties.every(prop => typeof option[prop] === 'undefined')
|
||||
) {
|
||||
throw new TypeError(
|
||||
`Properties ${properties.join(
|
||||
', '
|
||||
)} are missing in option ${name}`
|
||||
);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
get(name: string | number, required: true): CommandInteractionOption;
|
||||
get(
|
||||
name: string | number,
|
||||
required: boolean
|
||||
): CommandInteractionOption | undefined;
|
||||
|
||||
get(name: string | number, required?: boolean) {
|
||||
const option: CommandInteractionOption | undefined =
|
||||
this.hoistedOptions.find(o =>
|
||||
typeof name === 'number'
|
||||
? o.name === name.toString()
|
||||
: o.name === name
|
||||
);
|
||||
|
||||
if (!option) {
|
||||
if (required && name in this.hoistedOptions.map(o => o.name)) {
|
||||
throw new TypeError('Option marked as required was undefined');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
/** searches for a string option */
|
||||
getString(name: string | number, required: true): string;
|
||||
getString(name: string | number, required?: boolean): string | undefined;
|
||||
getString(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.String,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for a number option */
|
||||
getNumber(name: string | number, required: true): number;
|
||||
getNumber(name: string | number, required?: boolean): number | undefined;
|
||||
getNumber(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.Number,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searhces for an integer option */
|
||||
getInteger(name: string | number, required: true): number;
|
||||
getInteger(name: string | number, required?: boolean): number | undefined;
|
||||
getInteger(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.Integer,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for a boolean option */
|
||||
getBoolean(name: string | number, required: true): boolean;
|
||||
getBoolean(name: string | number, required?: boolean): boolean | undefined;
|
||||
getBoolean(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.Boolean,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for a user option */
|
||||
getUser(name: string | number, required: true): bigint;
|
||||
getUser(name: string | number, required?: boolean): bigint | undefined;
|
||||
getUser(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.User,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for a channel option */
|
||||
getChannel(name: string | number, required: true): bigint;
|
||||
getChannel(name: string | number, required?: boolean): bigint | undefined;
|
||||
getChannel(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.Channel,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for a mentionable-based option */
|
||||
getMentionable(name: string | number, required: true): string;
|
||||
getMentionable(
|
||||
name: string | number,
|
||||
required?: boolean
|
||||
): string | undefined;
|
||||
|
||||
getMentionable(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.Mentionable,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for a mentionable-based option */
|
||||
getRole(name: string | number, required: true): bigint;
|
||||
getRole(name: string | number, required?: boolean): bigint | undefined;
|
||||
getRole(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.Role,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for an attachment option */
|
||||
getAttachment(name: string | number, required: true): string;
|
||||
getAttachment(
|
||||
name: string | number,
|
||||
required?: boolean
|
||||
): string | undefined;
|
||||
|
||||
getAttachment(name: string | number, required = false) {
|
||||
const option: CommandInteractionOption | void = this.getTypedOption(
|
||||
name,
|
||||
ApplicationCommandOptionTypes.Attachment,
|
||||
['Otherwise'],
|
||||
required
|
||||
);
|
||||
|
||||
return option?.Otherwise ?? undefined;
|
||||
}
|
||||
|
||||
/** searches for the focused option */
|
||||
getFocused(
|
||||
full = false
|
||||
):
|
||||
| string
|
||||
| number
|
||||
| bigint
|
||||
| boolean
|
||||
| undefined
|
||||
| CommandInteractionOption {
|
||||
const focusedOption: CommandInteractionOption | void =
|
||||
this.hoistedOptions.find(option => option.focused);
|
||||
|
||||
if (!focusedOption) {
|
||||
throw new TypeError('No option found');
|
||||
}
|
||||
|
||||
return full ? focusedOption : focusedOption.Otherwise;
|
||||
}
|
||||
|
||||
getSubCommand(
|
||||
required = true
|
||||
): (string | CommandInteractionOption[] | undefined)[] {
|
||||
if (required && !this.#subcommand) {
|
||||
throw new TypeError('Option marked as required was undefined');
|
||||
}
|
||||
|
||||
return [this.#subcommand, this.hoistedOptions];
|
||||
}
|
||||
|
||||
getSubCommandGroup(
|
||||
required = false
|
||||
): (string | CommandInteractionOption[] | undefined)[] {
|
||||
if (required && !this.#group) {
|
||||
throw new TypeError('Option marked as required was undefined');
|
||||
}
|
||||
|
||||
return [this.#group, this.hoistedOptions];
|
||||
}
|
||||
}
|
50
packages/core/src/structures/special/permissions.ts
Normal file
50
packages/core/src/structures/special/permissions.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { BitwisePermissionFlags } from '@biscuitland/api-types';
|
||||
|
||||
export type PermissionString = keyof typeof BitwisePermissionFlags;
|
||||
export type PermissionResolvable =
|
||||
| bigint
|
||||
| PermissionString
|
||||
| PermissionString[]
|
||||
| BitwisePermissionFlags;
|
||||
|
||||
export class Permissions {
|
||||
static Flags = BitwisePermissionFlags;
|
||||
bitfield: bigint;
|
||||
|
||||
constructor(bitfield: PermissionResolvable) {
|
||||
this.bitfield = Permissions.resolve(bitfield);
|
||||
}
|
||||
|
||||
has(bit: PermissionResolvable): boolean {
|
||||
if (this.bitfield & BigInt(Permissions.Flags.ADMINISTRATOR)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!(this.bitfield & Permissions.resolve(bit));
|
||||
}
|
||||
|
||||
static resolve(bit: PermissionResolvable): bigint {
|
||||
switch (typeof bit) {
|
||||
case 'bigint':
|
||||
return bit;
|
||||
case 'number':
|
||||
return BigInt(bit);
|
||||
case 'string':
|
||||
return BigInt(Permissions.Flags[bit]);
|
||||
case 'object':
|
||||
return Permissions.resolve(
|
||||
bit
|
||||
.map(p => BigInt(Permissions.Flags[p]))
|
||||
.reduce((acc, cur) => acc | cur, 0n)
|
||||
);
|
||||
default:
|
||||
throw new TypeError(`Cannot resolve permission: ${bit}`);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): { fields: string[] } {
|
||||
const fields = Object.keys(Permissions.Flags).filter((bit) => typeof bit !== 'string' && this.has(bit));
|
||||
|
||||
return { fields };
|
||||
}
|
||||
}
|
63
packages/core/src/structures/stage-instance.ts
Normal file
63
packages/core/src/structures/stage-instance.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type { Model } from './base';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { DiscordStageInstance as DiscordAutoClosingStageInstance } from '@biscuitland/api-types';
|
||||
import { STAGE_INSTANCE } from '@biscuitland/api-types';
|
||||
|
||||
export interface DiscordStageInstanceB extends DiscordAutoClosingStageInstance {
|
||||
privacy_level: PrivacyLevels;
|
||||
discoverable_disabled: boolean;
|
||||
guild_scheduled_event_id: Snowflake;
|
||||
}
|
||||
|
||||
export enum PrivacyLevels {
|
||||
Public = 1,
|
||||
GuildOnly = 2,
|
||||
}
|
||||
|
||||
export type StageEditOptions = {
|
||||
topic?: string;
|
||||
privacy?: PrivacyLevels;
|
||||
};
|
||||
|
||||
export class StageInstance implements Model {
|
||||
constructor(session: Session, data: DiscordStageInstanceB) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.channelId = data.channel_id;
|
||||
this.guildId = data.guild_id;
|
||||
this.topic = data.topic;
|
||||
this.privacyLevel = data.privacy_level;
|
||||
this.discoverableDisabled = data.discoverable_disabled;
|
||||
this.guildScheduledEventId = data.guild_scheduled_event_id;
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
readonly id: Snowflake;
|
||||
|
||||
channelId: Snowflake;
|
||||
guildId: Snowflake;
|
||||
topic: string;
|
||||
|
||||
// TODO: see if this works
|
||||
privacyLevel: PrivacyLevels;
|
||||
discoverableDisabled: boolean;
|
||||
guildScheduledEventId: Snowflake;
|
||||
|
||||
async edit(options: StageEditOptions): Promise<StageInstance> {
|
||||
const stageInstance =
|
||||
await this.session.rest.patch<DiscordStageInstanceB>(
|
||||
STAGE_INSTANCE(this.id),
|
||||
{
|
||||
topic: options.topic,
|
||||
privacy_level: options.privacy,
|
||||
}
|
||||
);
|
||||
|
||||
return new StageInstance(this.session, stageInstance);
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
await this.session.rest.delete<undefined>(STAGE_INSTANCE(this.id), {});
|
||||
}
|
||||
}
|
73
packages/core/src/structures/sticker.ts
Normal file
73
packages/core/src/structures/sticker.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import type {
|
||||
DiscordSticker,
|
||||
DiscordStickerPack,
|
||||
StickerFormatTypes,
|
||||
StickerTypes,
|
||||
} from '@biscuitland/api-types';
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import { User } from './user';
|
||||
import { STICKER_PACKS } from '@biscuitland/api-types';
|
||||
|
||||
export interface StickerItem {
|
||||
id: Snowflake;
|
||||
name: string;
|
||||
formatType: StickerFormatTypes;
|
||||
}
|
||||
|
||||
export interface StickerPack {
|
||||
id: Snowflake;
|
||||
stickers: Sticker[];
|
||||
name: string;
|
||||
skuId: Snowflake;
|
||||
coverStickerId?: Snowflake;
|
||||
description: string;
|
||||
bannerAssetId?: Snowflake;
|
||||
}
|
||||
|
||||
export class Sticker implements Model {
|
||||
constructor(session: Session, data: DiscordSticker) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.packId = data.pack_id;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.tags = data.tags.split(',');
|
||||
this.type = data.type;
|
||||
this.formatType = data.format_type;
|
||||
this.available = !!data.available;
|
||||
this.guildId = data.guild_id;
|
||||
this.user = data.user ? new User(this.session, data.user) : undefined;
|
||||
this.sortValue = data.sort_value;
|
||||
}
|
||||
|
||||
session: Session;
|
||||
id: Snowflake;
|
||||
packId?: Snowflake;
|
||||
name: string;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
type: StickerTypes;
|
||||
formatType: StickerFormatTypes;
|
||||
available?: boolean;
|
||||
guildId?: Snowflake;
|
||||
user?: User;
|
||||
sortValue?: number;
|
||||
|
||||
async fetchPremiumPack(): Promise<StickerPack> {
|
||||
const data = await this.session.rest.get<DiscordStickerPack>(
|
||||
STICKER_PACKS()
|
||||
);
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
stickers: data.stickers.map(st => new Sticker(this.session, st)),
|
||||
name: data.name,
|
||||
skuId: data.sku_id,
|
||||
coverStickerId: data.cover_sticker_id,
|
||||
description: data.description,
|
||||
bannerAssetId: data.banner_asset_id,
|
||||
};
|
||||
}
|
||||
}
|
119
packages/core/src/structures/user.ts
Normal file
119
packages/core/src/structures/user.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { DiscordUser, PremiumTypes, UserFlags } from '@biscuitland/api-types';
|
||||
import type { ImageFormat, ImageSize } from '../utils/util';
|
||||
import { USER, USER_AVATAR, USER_DEFAULT_AVATAR } from '@biscuitland/api-types';
|
||||
import { Util } from '../utils/util';
|
||||
|
||||
export type AvatarOptions = {
|
||||
format?: ImageFormat;
|
||||
size?: ImageSize;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a user
|
||||
* @link https://discord.com/developers/docs/resources/user#user-object
|
||||
*/
|
||||
export class User implements Model {
|
||||
constructor(session: Session, data: DiscordUser) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
|
||||
this.username = data.username;
|
||||
this.discriminator = data.discriminator;
|
||||
this.avatarHash = data.avatar
|
||||
? data.avatar
|
||||
: undefined;
|
||||
|
||||
this.accentColor = data.accent_color;
|
||||
this.bot = !!data.bot;
|
||||
this.system = !!data.system;
|
||||
this.banner = data.banner
|
||||
? data.banner
|
||||
: undefined;
|
||||
|
||||
this.mfaEnabled = !!data.mfa_enabled;
|
||||
this.locale = data.locale;
|
||||
this.email = data.email ? data.email : undefined;
|
||||
this.verified = !!data.verified;
|
||||
this.flags = data.flags;
|
||||
}
|
||||
|
||||
/** the session that instantiated this User */
|
||||
readonly session: Session;
|
||||
|
||||
/** the user's id */
|
||||
readonly id: Snowflake;
|
||||
|
||||
/** the user's username, not unique across the platform */
|
||||
username: string;
|
||||
|
||||
/** the user's 4-digit discord-tag */
|
||||
discriminator: string;
|
||||
|
||||
/** the user's avatar hash */
|
||||
avatarHash?: string;
|
||||
|
||||
/** the user's banner color encoded as an integer representation of hexadecimal color code */
|
||||
accentColor?: number;
|
||||
|
||||
/** whether the user belongs to an OAuth2 application */
|
||||
bot: boolean;
|
||||
|
||||
/** whether the user is an Official Discord System user (part of the urgent message system) */
|
||||
system: boolean;
|
||||
|
||||
/** the user's banner hash */
|
||||
banner?: string;
|
||||
|
||||
/** whether the user has two factor enabled on their account */
|
||||
mfaEnabled: boolean;
|
||||
|
||||
/** the user's chosen language option */
|
||||
locale?: string;
|
||||
|
||||
/** the user's email */
|
||||
email?: string;
|
||||
|
||||
/** the flags on a user's account */
|
||||
flags?: UserFlags;
|
||||
|
||||
/** whether the email on this account has been verified */
|
||||
verified: boolean;
|
||||
|
||||
/** the type of Nitro subscription on a user's account */
|
||||
premiumType?: PremiumTypes;
|
||||
|
||||
/** the public flags on a user's account */
|
||||
publicFlags?: UserFlags;
|
||||
|
||||
/** gets the user's username#discriminator */
|
||||
get tag(): string {
|
||||
return `${this.username}#${this.discriminator}}`;
|
||||
}
|
||||
|
||||
/** fetches this user */
|
||||
async fetch(): Promise<User> {
|
||||
const user = await this.session.rest.get<DiscordUser>(USER(this.id));
|
||||
|
||||
return new User(this.session, user);
|
||||
}
|
||||
|
||||
/** gets the user's avatar */
|
||||
avatarURL(options: AvatarOptions): string {
|
||||
let url: string;
|
||||
|
||||
if (!this.avatarHash) {
|
||||
url = USER_DEFAULT_AVATAR(Number(this.discriminator) % 5);
|
||||
} else {
|
||||
url = USER_AVATAR(this.id, this.avatarHash);
|
||||
}
|
||||
|
||||
return Util.formatImageURL(url, options.size ?? 128, options.format);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<@${this.id}>`;
|
||||
}
|
||||
}
|
188
packages/core/src/structures/webhook.ts
Normal file
188
packages/core/src/structures/webhook.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import type { Model } from './base';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type {
|
||||
DiscordEmbed,
|
||||
DiscordMessage,
|
||||
DiscordMessageComponents,
|
||||
DiscordWebhook,
|
||||
FileContent,
|
||||
WebhookTypes,
|
||||
WebhookOptions,
|
||||
} from '@biscuitland/api-types';
|
||||
import type { Attachment } from './attachment';
|
||||
import type { AllowedMentions, CreateMessage } from './message';
|
||||
import { User } from './user';
|
||||
import { Message } from './message';
|
||||
import { Util } from '../utils/util';
|
||||
import {
|
||||
WEBHOOK,
|
||||
WEBHOOK_TOKEN,
|
||||
WEBHOOK_MESSAGE,
|
||||
WEBHOOK_MESSAGE_ORIGINAL,
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
export type ExecuteWebhookOptions = WebhookOptions &
|
||||
CreateMessage & { avatarUrl?: string; username?: string };
|
||||
export type EditMessageWithThread = EditWebhookMessage & {
|
||||
threadId?: Snowflake;
|
||||
};
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/webhook#edit-webhook-message-jsonform-params
|
||||
*/
|
||||
export interface EditWebhookMessage {
|
||||
content?: string;
|
||||
embeds?: DiscordEmbed[];
|
||||
files?: FileContent[];
|
||||
allowedMentions?: AllowedMentions;
|
||||
attachments?: Attachment[];
|
||||
components?: DiscordMessageComponents;
|
||||
}
|
||||
|
||||
export class Webhook implements Model {
|
||||
constructor(session: Session, data: DiscordWebhook) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.type = data.type;
|
||||
this.token = data.token;
|
||||
|
||||
if (data.avatar) {
|
||||
this.avatar = Util.iconHashToBigInt(data.avatar);
|
||||
}
|
||||
|
||||
if (data.user) {
|
||||
this.user = new User(session, data.user);
|
||||
}
|
||||
|
||||
if (data.guild_id) {
|
||||
this.guildId = data.guild_id;
|
||||
}
|
||||
|
||||
if (data.channel_id) {
|
||||
this.channelId = data.channel_id;
|
||||
}
|
||||
|
||||
if (data.application_id) {
|
||||
this.applicationId = data.application_id;
|
||||
}
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
readonly id: Snowflake;
|
||||
type: WebhookTypes;
|
||||
token?: string;
|
||||
avatar?: bigint;
|
||||
applicationId?: Snowflake;
|
||||
channelId?: Snowflake;
|
||||
guildId?: Snowflake;
|
||||
user?: User;
|
||||
|
||||
async execute(
|
||||
options?: ExecuteWebhookOptions
|
||||
): Promise<Message | undefined> {
|
||||
if (!this.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
content: options?.content,
|
||||
embeds: options?.embeds,
|
||||
tts: options?.tts,
|
||||
allowed_mentions: options?.allowedMentions,
|
||||
components: options?.components,
|
||||
file: options?.files,
|
||||
};
|
||||
|
||||
const message = await this.session.rest.post<DiscordMessage>(
|
||||
WEBHOOK(this.id, this.token, {
|
||||
wait: options?.wait,
|
||||
threadId: options?.threadId,
|
||||
}),
|
||||
data
|
||||
);
|
||||
|
||||
return options?.wait ?? true
|
||||
? new Message(this.session, message)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async fetch(): Promise<Webhook> {
|
||||
const message = await this.session.rest.get<DiscordWebhook>(
|
||||
WEBHOOK_TOKEN(this.id, this.token)
|
||||
);
|
||||
|
||||
return new Webhook(this.session, message);
|
||||
}
|
||||
|
||||
async fetchMessage(
|
||||
messageId: Snowflake,
|
||||
threadId?: Snowflake
|
||||
): Promise<Message | undefined> {
|
||||
if (!this.token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.session.rest.get<DiscordMessage>(
|
||||
WEBHOOK_MESSAGE(this.id, this.token, messageId, { threadId })
|
||||
);
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
|
||||
async deleteMessage(
|
||||
messageId: Snowflake,
|
||||
threadId?: Snowflake
|
||||
): Promise<void> {
|
||||
if (!this.token) {
|
||||
throw new Error('No token found');
|
||||
}
|
||||
|
||||
await this.session.rest.delete<undefined>(
|
||||
WEBHOOK_MESSAGE(this.id, this.token, messageId, { threadId }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
async editMessage(
|
||||
messageId?: Snowflake,
|
||||
options?: EditMessageWithThread
|
||||
): Promise<Message> {
|
||||
if (!this.token) {
|
||||
throw new Error('No token found');
|
||||
}
|
||||
|
||||
const message = await this.session.rest.patch<DiscordMessage>(
|
||||
messageId
|
||||
? WEBHOOK_MESSAGE(this.id, this.token, messageId)
|
||||
: WEBHOOK_MESSAGE_ORIGINAL(this.id, this.token),
|
||||
{
|
||||
content: options?.content,
|
||||
embeds: options?.embeds,
|
||||
file: options?.files,
|
||||
components: options?.components,
|
||||
allowed_mentions: options?.allowedMentions && {
|
||||
parse: options?.allowedMentions.parse,
|
||||
replied_user: options?.allowedMentions.repliedUser,
|
||||
users: options?.allowedMentions.users,
|
||||
roles: options?.allowedMentions.roles,
|
||||
},
|
||||
attachments: options?.attachments?.map(attachment => {
|
||||
return {
|
||||
id: attachment.id,
|
||||
filename: attachment.name,
|
||||
content_type: attachment.contentType,
|
||||
size: attachment.size,
|
||||
url: attachment.attachment,
|
||||
proxy_url: attachment.proxyUrl,
|
||||
height: attachment.height,
|
||||
width: attachment.width,
|
||||
ephemeral: attachment.ephemeral,
|
||||
};
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return new Message(this.session, message);
|
||||
}
|
||||
}
|
55
packages/core/src/structures/welcome.ts
Normal file
55
packages/core/src/structures/welcome.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Model } from './base';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type {
|
||||
DiscordWelcomeScreen,
|
||||
DiscordWelcomeScreenChannel,
|
||||
} from '@biscuitland/api-types';
|
||||
import { Emoji } from './emojis';
|
||||
|
||||
/**
|
||||
* Not a channel
|
||||
* @link https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-channel-structure
|
||||
*/
|
||||
export class WelcomeChannel implements Model {
|
||||
constructor(session: Session, data: DiscordWelcomeScreenChannel) {
|
||||
this.session = session;
|
||||
this.channelId = data.channel_id;
|
||||
this.description = data.description;
|
||||
this.emoji = new Emoji(session, {
|
||||
name: data.emoji_name ? data.emoji_name : undefined,
|
||||
id: data.emoji_id ? data.emoji_id : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
session: Session;
|
||||
channelId: Snowflake;
|
||||
description: string;
|
||||
emoji: Emoji;
|
||||
|
||||
/** alias for WelcomeScreenChannel.channelId */
|
||||
get id(): Snowflake {
|
||||
return this.channelId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/resources/guild#welcome-screen-object
|
||||
*/
|
||||
export class WelcomeScreen {
|
||||
constructor(session: Session, data: DiscordWelcomeScreen) {
|
||||
this.session = session;
|
||||
this.welcomeChannels = data.welcome_channels.map(
|
||||
welcomeChannel => new WelcomeChannel(session, welcomeChannel)
|
||||
);
|
||||
|
||||
if (data.description) {
|
||||
this.description = data.description;
|
||||
}
|
||||
}
|
||||
|
||||
readonly session: Session;
|
||||
|
||||
description?: string;
|
||||
welcomeChannels: WelcomeChannel[];
|
||||
}
|
41
packages/core/src/structures/widget.ts
Normal file
41
packages/core/src/structures/widget.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { Model } from './base';
|
||||
import type { Session } from '../biscuit';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
import type { DiscordGuildWidget } from '@biscuitland/api-types';
|
||||
import type { PartialChannel } from './channels';
|
||||
|
||||
export interface WidgetMember {
|
||||
id?: string;
|
||||
username: string;
|
||||
avatar?: string | null;
|
||||
status: string;
|
||||
avatarURL: string;
|
||||
}
|
||||
|
||||
export class Widget implements Model {
|
||||
constructor(session: Session, data: DiscordGuildWidget) {
|
||||
this.session = session;
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.instantInvite = data.instant_invite;
|
||||
this.channels = data.channels;
|
||||
this.members = data.members.map(x => {
|
||||
return {
|
||||
id: x.id,
|
||||
username: x.username,
|
||||
avatar: x.avatar,
|
||||
status: x.status,
|
||||
avatarURL: x.avatar_url,
|
||||
};
|
||||
});
|
||||
this.presenceCount = data.presence_count;
|
||||
}
|
||||
|
||||
session: Session;
|
||||
id: Snowflake;
|
||||
name: string;
|
||||
instantInvite?: string;
|
||||
channels: PartialChannel[];
|
||||
members: WidgetMember[];
|
||||
presenceCount: number;
|
||||
}
|
5
packages/core/src/utils/calculate-shard.ts
Normal file
5
packages/core/src/utils/calculate-shard.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function calculateShardId(totalShards: number, guildId: bigint) {
|
||||
if (totalShards === 1) return 0;
|
||||
|
||||
return Number((guildId >> 22n) % BigInt(totalShards - 1));
|
||||
}
|
69
packages/core/src/utils/url-to-base-64.ts
Normal file
69
packages/core/src/utils/url-to-base-64.ts
Normal file
@ -0,0 +1,69 @@
|
||||
/** Converts a url to base 64. Useful for example, uploading/creating server emojis. */
|
||||
export async function urlToBase64(url: string): Promise<string> {
|
||||
const buffer = await fetch(url).then((res) => res.arrayBuffer());
|
||||
const imageStr = encode(buffer);
|
||||
const type = url.substring(url.lastIndexOf('.') + 1);
|
||||
return `data:image/${type};base64,${imageStr}`;
|
||||
}
|
||||
|
||||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
// deno-fmt-ignore
|
||||
const base64abc: string[] = [
|
||||
"A", "B", "C",
|
||||
"D", "E", "F",
|
||||
"G", "H", "I",
|
||||
"J", "K", "L",
|
||||
"M", "N", "O",
|
||||
"P", "Q", "R",
|
||||
"S", "T", "U",
|
||||
"V", "W", "X",
|
||||
"Y", "Z", "a",
|
||||
"b", "c", "d",
|
||||
"e", "f", "g",
|
||||
"h", "i", "j",
|
||||
"k", "l", "m",
|
||||
"n", "o", "p",
|
||||
"q", "r", "s",
|
||||
"t", "u", "v",
|
||||
"w", "x", "y",
|
||||
"z", "0", "1",
|
||||
"2", "3", "4",
|
||||
"5", "6", "7",
|
||||
"8", "9", "+", "/",
|
||||
];
|
||||
|
||||
/**
|
||||
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
|
||||
* Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation
|
||||
* @param data
|
||||
*/
|
||||
export function encode(data: ArrayBuffer | string): string {
|
||||
const uint8: Uint8Array = typeof data === 'string'
|
||||
? new TextEncoder().encode(data)
|
||||
: data instanceof Uint8Array
|
||||
? data
|
||||
: new Uint8Array(data);
|
||||
let result = '',
|
||||
i;
|
||||
const l: number = uint8.length;
|
||||
for (i = 2; i < l; i += 3) {
|
||||
result += base64abc[uint8[i - 2] >> 2];
|
||||
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
|
||||
result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)];
|
||||
result += base64abc[uint8[i] & 0x3f];
|
||||
}
|
||||
if (i === l + 1) {
|
||||
// 1 octet yet to write
|
||||
result += base64abc[uint8[i - 2] >> 2];
|
||||
result += base64abc[(uint8[i - 2] & 0x03) << 4];
|
||||
result += '==';
|
||||
}
|
||||
if (i === l) {
|
||||
// 2 octets yet to write
|
||||
result += base64abc[uint8[i - 2] >> 2];
|
||||
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
|
||||
result += base64abc[(uint8[i - 1] & 0x0f) << 2];
|
||||
result += '=';
|
||||
}
|
||||
return result;
|
||||
}
|
106
packages/core/src/utils/util.ts
Normal file
106
packages/core/src/utils/util.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import type { ButtonBuilder } from '../builders/components/MessageButtonBuilder';
|
||||
import type { InputTextBuilder } from '../builders/components/InputTextBuilder';
|
||||
import type { SelectMenuBuilder } from '../builders/components/MessageSelectMenuBuilder';
|
||||
import type { Permissions } from '../structures/special/permissions';
|
||||
import type { Snowflake } from '../snowflakes';
|
||||
|
||||
/*
|
||||
* Represents a session's cache
|
||||
* */
|
||||
export interface SymCache {
|
||||
readonly cache: symbol;
|
||||
}
|
||||
|
||||
/*
|
||||
* @link https://discord.com/developers/docs/resources/channel#message-object-message-flags
|
||||
*/
|
||||
export enum MessageFlags {
|
||||
/** this message has been published to subscribed channels (via Channel Following) */
|
||||
CrossPosted = 1 << 0,
|
||||
/** this message originated from a message in another channel (via Channel Following) */
|
||||
IsCrosspost = 1 << 1,
|
||||
/** do not include any embeds when serializing this message */
|
||||
SupressEmbeds = 1 << 2,
|
||||
/** the source message for this crosspost has been deleted (via Channel Following) */
|
||||
SourceMessageDeleted = 1 << 3,
|
||||
/** this message came from the urgent message system */
|
||||
Urgent = 1 << 4,
|
||||
/** this message has an associated thread, with the same id as the message */
|
||||
HasThread = 1 << 5,
|
||||
/** this message is only visible to the user who invoked the Interaction */
|
||||
Ephemeral = 1 << 6,
|
||||
/** this message is an Interaction Response and the bot is "thinking" */
|
||||
Loading = 1 << 7,
|
||||
/** this message failed to mention some roles and add their members to the thread */
|
||||
FailedToMentionSomeRolesInThread = 1 << 8,
|
||||
}
|
||||
|
||||
export type ComponentBuilder =
|
||||
| InputTextBuilder
|
||||
| SelectMenuBuilder
|
||||
| ButtonBuilder;
|
||||
|
||||
/** *
|
||||
* Utility type
|
||||
*/
|
||||
export type ComponentEmoji = {
|
||||
id: Snowflake;
|
||||
name: string;
|
||||
animated?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility type
|
||||
*/
|
||||
export interface PermissionsOverwrites {
|
||||
id: Snowflake;
|
||||
type: 0 | 1;
|
||||
allow: Permissions;
|
||||
deny: Permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/reference#image-formatting
|
||||
*/
|
||||
export type ImageFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'gif' | 'json';
|
||||
|
||||
/**
|
||||
* @link https://discord.com/developers/docs/reference#image-formatting
|
||||
*/
|
||||
export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
export abstract class Util {
|
||||
static formatImageURL(url: string, size: ImageSize = 128, format?: ImageFormat): string {
|
||||
return `${url}.${format || (url.includes('/a_') ? 'gif' : 'jpg')}?size=${size}`;
|
||||
}
|
||||
|
||||
static iconHashToBigInt(hash: string): bigint {
|
||||
return BigInt('0x' + (hash.startsWith('a_') ? `a${hash.substring(2)}` : `b${hash}`));
|
||||
}
|
||||
|
||||
static iconBigintToHash(icon: bigint): string {
|
||||
const hash: string = icon.toString(16);
|
||||
|
||||
return hash.startsWith('a') ? `a_${hash.substring(1)}` : hash.substring(1);
|
||||
}
|
||||
|
||||
/** Removes the Bot before the token. */
|
||||
static removeTokenPrefix(token?: string, type: 'GATEWAY' | 'REST' = 'REST'): string {
|
||||
// If no token is provided, throw an error
|
||||
if (!token) throw new Error(`The ${type} was not given a token. Please provide a token and try again.`);
|
||||
|
||||
// If the token does not have a prefix just return token
|
||||
if (!token.startsWith('Bot ')) return token;
|
||||
|
||||
// Remove the prefix and return only the token.
|
||||
return token.substring(token.indexOf(' ') + 1);
|
||||
}
|
||||
|
||||
/** Get the bot id from the bot token. WARNING: Discord staff has mentioned this may not be stable forever. Use at your own risk. However, note for over 5 years this has never broken. */
|
||||
static getBotIdFromToken(token: string): string {
|
||||
return atob(token.split('.')[0]);
|
||||
}
|
||||
}
|
7
packages/core/tsconfig.json
Normal file
7
packages/core/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
12
packages/core/tsup.config.ts
Normal file
12
packages/core/tsup.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
minify: isProduction,
|
||||
sourcemap: true,
|
||||
});
|
0
packages/rest/README.md
Normal file
0
packages/rest/README.md
Normal file
21
packages/rest/package.json
Normal file
21
packages/rest/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@biscuitland/rest",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist && rm -rf .turbo",
|
||||
"dev": "tsup --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.1.3"
|
||||
}
|
||||
}
|
809
packages/rest/src/adapters/default-rest-adapter.ts
Normal file
809
packages/rest/src/adapters/default-rest-adapter.ts
Normal file
@ -0,0 +1,809 @@
|
||||
import type {
|
||||
CreateRequestBodyOptions,
|
||||
RestAdapter,
|
||||
RestPayload,
|
||||
RestRateLimitedPath,
|
||||
RestRequest,
|
||||
RestRequestRejection,
|
||||
RestRequestResponse,
|
||||
RestSendRequestOptions,
|
||||
} from './rest-adapter';
|
||||
|
||||
import type { FileContent } from '@biscuitland/api-types';
|
||||
|
||||
import { Constants, HTTPResponseCodes } from '@biscuitland/api-types';
|
||||
|
||||
export class DefaultRestAdapter implements RestAdapter {
|
||||
static readonly DEFAULTS = {
|
||||
url: '',
|
||||
|
||||
version: Constants.API_VERSION,
|
||||
|
||||
maxRetryCount: 10,
|
||||
};
|
||||
|
||||
options: Options;
|
||||
|
||||
/** current invalid amount */
|
||||
protected invalidRequests = 0;
|
||||
|
||||
/** max invalid requests allowed until ban */
|
||||
protected maxInvalidRequests = 10000;
|
||||
|
||||
/** 10 minutes */
|
||||
protected invalidRequestsInterval = 600000;
|
||||
|
||||
/** timer to reset to 0 */
|
||||
protected invalidRequestsTimeoutId = 0;
|
||||
|
||||
/** how safe to be from max */
|
||||
protected invalidRequestsSafetyAmount = 1;
|
||||
|
||||
/** when first request in this period was made */
|
||||
protected invalidRequestErrorStatuses = [401, 403, 429];
|
||||
|
||||
protected processingQueue = false;
|
||||
|
||||
protected processingRateLimitedPaths = false;
|
||||
|
||||
protected globallyRateLimited = false;
|
||||
|
||||
protected globalQueueProcessing = false;
|
||||
|
||||
private rateLimitedPaths = new Map<string, RestRateLimitedPath>();
|
||||
|
||||
private globalQueue = [] as {
|
||||
request: RestRequest;
|
||||
payload: RestPayload;
|
||||
basicURL: string;
|
||||
urlToUse: string;
|
||||
}[];
|
||||
|
||||
private pathQueues = new Map<
|
||||
string,
|
||||
{
|
||||
isWaiting: boolean;
|
||||
requests: {
|
||||
request: RestRequest;
|
||||
payload: RestPayload;
|
||||
}[];
|
||||
}
|
||||
>();
|
||||
|
||||
private url: string;
|
||||
|
||||
constructor(options: DefaultRestOptions) {
|
||||
this.options = Object.assign(options, DefaultRestAdapter.DEFAULTS);
|
||||
|
||||
if (this.options.url) {
|
||||
this.url = `${options.url}/v${this.options.version}`;
|
||||
} else {
|
||||
this.url = `${Constants.BASE_URL}/v${this.options.version}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async get<T>(
|
||||
route: string,
|
||||
body?: any,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T> {
|
||||
const url = route[0] === '/' ? `${this.url}${route}` : route;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.processRequest(
|
||||
{
|
||||
url,
|
||||
method: 'GET',
|
||||
reject: (data: RestRequestRejection) => {
|
||||
const restError = this.convertRestError(
|
||||
new Error('Location:'),
|
||||
data
|
||||
);
|
||||
reject(restError);
|
||||
},
|
||||
resolve: (data: RestRequestResponse) =>
|
||||
resolve(
|
||||
data.status !== 204
|
||||
? JSON.parse(data.body ?? '{}')
|
||||
: (undefined as unknown as T)
|
||||
),
|
||||
},
|
||||
{
|
||||
bucketId: options?.bucketId,
|
||||
body: body as Record<string, unknown> | undefined,
|
||||
retryCount: options?.retryCount ?? 0,
|
||||
headers: options?.headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async put<T>(
|
||||
route: string,
|
||||
body?: any,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T> {
|
||||
const url = route[0] === '/' ? `${this.url}${route}` : route;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.processRequest(
|
||||
{
|
||||
url,
|
||||
method: 'PUT',
|
||||
reject: (data: RestRequestRejection) => {
|
||||
const restError = this.convertRestError(
|
||||
new Error('Location:'),
|
||||
data
|
||||
);
|
||||
reject(restError);
|
||||
},
|
||||
resolve: (data: RestRequestResponse) =>
|
||||
resolve(
|
||||
data.status !== 204
|
||||
? JSON.parse(data.body ?? '{}')
|
||||
: (undefined as unknown as T)
|
||||
),
|
||||
},
|
||||
{
|
||||
bucketId: options?.bucketId,
|
||||
body: body as Record<string, unknown> | undefined,
|
||||
retryCount: options?.retryCount ?? 0,
|
||||
headers: options?.headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async post<T>(
|
||||
route: string,
|
||||
body?: any,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T> {
|
||||
const url = route[0] === '/' ? `${this.url}${route}` : route;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.processRequest(
|
||||
{
|
||||
url,
|
||||
method: 'POST',
|
||||
reject: err => reject(err),
|
||||
resolve: (data: RestRequestResponse) =>
|
||||
resolve(
|
||||
data.status !== 204
|
||||
? JSON.parse(data.body ?? '{}')
|
||||
: (undefined as unknown as T)
|
||||
),
|
||||
},
|
||||
{
|
||||
bucketId: options?.bucketId,
|
||||
body: body as Record<string, unknown> | undefined,
|
||||
retryCount: options?.retryCount ?? 0,
|
||||
headers: options?.headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async patch<T>(
|
||||
route: string,
|
||||
body?: any,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T> {
|
||||
const url = route[0] === '/' ? `${this.url}${route}` : route;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.processRequest(
|
||||
{
|
||||
url,
|
||||
method: 'PATCH',
|
||||
reject: (data: RestRequestRejection) => {
|
||||
const restError = this.convertRestError(
|
||||
new Error('Location:'),
|
||||
data
|
||||
);
|
||||
reject(restError);
|
||||
},
|
||||
resolve: (data: RestRequestResponse) =>
|
||||
resolve(
|
||||
data.status !== 204
|
||||
? JSON.parse(data.body ?? '{}')
|
||||
: (undefined as unknown as T)
|
||||
),
|
||||
},
|
||||
{
|
||||
bucketId: options?.bucketId,
|
||||
body: body as Record<string, unknown> | undefined,
|
||||
retryCount: options?.retryCount ?? 0,
|
||||
headers: options?.headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async delete<T>(
|
||||
route: string,
|
||||
body?: any,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T> {
|
||||
const url = route[0] === '/' ? `${this.url}${route}` : route;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.processRequest(
|
||||
{
|
||||
url,
|
||||
method: 'DELETE',
|
||||
reject: (data: RestRequestRejection) => {
|
||||
const restError = this.convertRestError(
|
||||
new Error('Location:'),
|
||||
data
|
||||
);
|
||||
reject(restError);
|
||||
},
|
||||
resolve: (data: any) => resolve(JSON.parse(data.body)),
|
||||
},
|
||||
{
|
||||
bucketId: options?.bucketId,
|
||||
body: body as Record<string, unknown> | undefined,
|
||||
retryCount: options?.retryCount ?? 0,
|
||||
headers: options?.headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private async sendRequest(options: RestSendRequestOptions) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
new Request(options.url, {
|
||||
method: options.method,
|
||||
headers: options.payload?.headers,
|
||||
body: options.payload?.body,
|
||||
})
|
||||
);
|
||||
|
||||
const bucketIdFromHeaders = this.processRequestHeaders(
|
||||
options.url,
|
||||
response.headers
|
||||
);
|
||||
|
||||
if (bucketIdFromHeaders) {
|
||||
options.bucketId = bucketIdFromHeaders;
|
||||
}
|
||||
|
||||
if (response.status < 200 || response.status >= 400) {
|
||||
let error = 'REQUEST_UNKNOWN_ERROR';
|
||||
|
||||
switch (response.status) {
|
||||
case HTTPResponseCodes.BadRequest:
|
||||
error =
|
||||
"The options was improperly formatted, or the server couldn't understand it.";
|
||||
break;
|
||||
case HTTPResponseCodes.Unauthorized:
|
||||
error =
|
||||
'The Authorization header was missing or invalid.';
|
||||
break;
|
||||
case HTTPResponseCodes.Forbidden:
|
||||
error =
|
||||
'The Authorization token you passed did not have permission to the resource.';
|
||||
break;
|
||||
case HTTPResponseCodes.NotFound:
|
||||
error =
|
||||
"The resource at the location specified doesn't exist.";
|
||||
break;
|
||||
case HTTPResponseCodes.MethodNotAllowed:
|
||||
error =
|
||||
'The HTTP method used is not valid for the location specified.';
|
||||
break;
|
||||
case HTTPResponseCodes.GatewayUnavailable:
|
||||
error =
|
||||
'There was not a gateway available to process your options. Wait a bit and retry.';
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
this.invalidRequestErrorStatuses.includes(
|
||||
response.status
|
||||
) &&
|
||||
!(
|
||||
response.status === 429 &&
|
||||
response.headers.get('X-RateLimit-Scope')
|
||||
)
|
||||
) {
|
||||
++this.invalidRequests;
|
||||
|
||||
if (!this.invalidRequestsTimeoutId) {
|
||||
const it: any = setTimeout(() => {
|
||||
this.invalidRequests = 0;
|
||||
this.invalidRequestsTimeoutId = 0;
|
||||
}, this.invalidRequestsInterval);
|
||||
|
||||
this.invalidRequestsTimeoutId = it;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status !== 429) {
|
||||
options.reject?.({
|
||||
ok: false,
|
||||
status: response.status,
|
||||
error,
|
||||
body: response.type
|
||||
? JSON.stringify(await response.json())
|
||||
: undefined,
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
status: response.status,
|
||||
error,
|
||||
body: response.type
|
||||
? JSON.stringify(await response.json())
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
options.retryCount &&
|
||||
options.retryCount++ >= this.options.maxRetryCount
|
||||
) {
|
||||
options.reject?.({
|
||||
ok: false,
|
||||
status: response.status,
|
||||
error: 'The options was rate limited and it maxed out the retries limit.',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
options.resolve?.({
|
||||
ok: true,
|
||||
status: 204,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.stringify(await response.json());
|
||||
|
||||
options.resolve?.({
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: json,
|
||||
});
|
||||
|
||||
return JSON.parse(json);
|
||||
} catch (error) {
|
||||
options.reject?.({
|
||||
ok: false,
|
||||
status: 599,
|
||||
error: 'Internal Error',
|
||||
});
|
||||
|
||||
throw new Error('Something went wrong in sendRequest');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private processRequest(request: RestRequest, payload: RestPayload) {
|
||||
const queue = this.pathQueues.get(request.url);
|
||||
|
||||
if (queue) {
|
||||
queue.requests.push({ request, payload });
|
||||
} else {
|
||||
this.pathQueues.set(request.url, {
|
||||
isWaiting: false,
|
||||
requests: [
|
||||
{
|
||||
request,
|
||||
payload,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.processQueue(request.url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private processQueue(id: string) {
|
||||
const queue = this.pathQueues.get(id);
|
||||
|
||||
if (!queue) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (queue.requests.length) {
|
||||
const request = queue.requests[0];
|
||||
|
||||
if (!request) {
|
||||
break;
|
||||
}
|
||||
|
||||
const basicURL = request.request.url + '@' + request.request.method;
|
||||
const urlResetIn = this.checkRateLimits(basicURL);
|
||||
|
||||
if (urlResetIn) {
|
||||
if (!queue.isWaiting) {
|
||||
queue.isWaiting = true;
|
||||
|
||||
setTimeout(() => {
|
||||
queue.isWaiting = false;
|
||||
|
||||
this.processQueue(id);
|
||||
}, urlResetIn);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const bucketResetIn = request.payload.bucketId
|
||||
? this.checkRateLimits(request.payload.bucketId)
|
||||
: false;
|
||||
|
||||
if (bucketResetIn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.globalQueue.push({
|
||||
...request,
|
||||
urlToUse: request.request.url,
|
||||
basicURL,
|
||||
});
|
||||
|
||||
this.processGlobalQueue();
|
||||
queue.requests.shift();
|
||||
}
|
||||
|
||||
this.cleanupQueues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private async processGlobalQueue() {
|
||||
if (!this.globalQueue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.globalQueueProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.globalQueueProcessing = true;
|
||||
|
||||
while (this.globalQueue.length) {
|
||||
if (this.globallyRateLimited) {
|
||||
setTimeout(() => {
|
||||
this.processGlobalQueue();
|
||||
}, 1000);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
this.invalidRequests ===
|
||||
this.maxInvalidRequests - this.invalidRequestsSafetyAmount
|
||||
) {
|
||||
setTimeout(() => {
|
||||
this.processGlobalQueue();
|
||||
}, 1000);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const request = this.globalQueue.shift();
|
||||
|
||||
if (!request) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const urlResetIn = this.checkRateLimits(request.basicURL);
|
||||
|
||||
const bucketResetIn = request.payload.bucketId
|
||||
? this.checkRateLimits(request.payload.bucketId)
|
||||
: false;
|
||||
|
||||
if (urlResetIn || bucketResetIn) {
|
||||
setTimeout(() => {
|
||||
this.globalQueue.unshift(request);
|
||||
this.processGlobalQueue();
|
||||
}, urlResetIn || (bucketResetIn as number));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.sendRequest({
|
||||
url: request.urlToUse,
|
||||
method: request.request.method,
|
||||
bucketId: request.payload.bucketId,
|
||||
reject: request.request.reject,
|
||||
resolve: request.request.resolve,
|
||||
retryCount: request.payload.retryCount ?? 0,
|
||||
payload: this.createRequestBody({
|
||||
method: request.request.method,
|
||||
body: request.payload.body,
|
||||
}),
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
this.globalQueueProcessing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private processRequestHeaders(url: string, headers: Headers) {
|
||||
let rateLimited = false;
|
||||
|
||||
const remaining = headers.get('x-ratelimit-remaining');
|
||||
const retryAfter = headers.get('x-ratelimit-reset-after');
|
||||
const reset = Date.now() + Number(retryAfter) * 1000;
|
||||
const global = headers.get('x-ratelimit-global');
|
||||
|
||||
const bucketId = headers.get('x-ratelimit-bucket') || undefined;
|
||||
|
||||
if (remaining === '0') {
|
||||
rateLimited = true;
|
||||
|
||||
this.rateLimitedPaths.set(url, {
|
||||
url,
|
||||
resetTimestamp: reset,
|
||||
bucketId,
|
||||
});
|
||||
|
||||
if (bucketId) {
|
||||
this.rateLimitedPaths.set(bucketId, {
|
||||
url,
|
||||
resetTimestamp: reset,
|
||||
bucketId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (global) {
|
||||
const retryAfter = headers.get('retry-after');
|
||||
const globalReset = Date.now() + Number(retryAfter) * 1000;
|
||||
|
||||
this.globallyRateLimited = true;
|
||||
rateLimited = true;
|
||||
|
||||
this.rateLimitedPaths.set('global', {
|
||||
url: 'global',
|
||||
resetTimestamp: globalReset,
|
||||
bucketId,
|
||||
});
|
||||
|
||||
if (bucketId) {
|
||||
this.rateLimitedPaths.set(bucketId, {
|
||||
url: 'global',
|
||||
resetTimestamp: globalReset,
|
||||
bucketId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rateLimited && !this.processingRateLimitedPaths) {
|
||||
this.processRateLimitedPaths();
|
||||
}
|
||||
|
||||
return rateLimited ? bucketId : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private processRateLimitedPaths() {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [key, value] of this.rateLimitedPaths.entries()) {
|
||||
if (value.resetTimestamp > now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.rateLimitedPaths.delete(key);
|
||||
|
||||
if (key === 'global') {
|
||||
this.globallyRateLimited = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.rateLimitedPaths.size) {
|
||||
this.processingRateLimitedPaths = false;
|
||||
} else {
|
||||
this.processingRateLimitedPaths = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.processRateLimitedPaths();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private createRequestBody(options: CreateRequestBodyOptions) {
|
||||
const headers: Record<string, string> = {
|
||||
'user-agent': Constants.USER_AGENT,
|
||||
};
|
||||
|
||||
if (!options.unauthorized) {
|
||||
headers.authorization = `Bot ${this.options.token}`;
|
||||
}
|
||||
|
||||
if (options.headers) {
|
||||
for (const key in options.headers) {
|
||||
headers[key.toLowerCase()] = options.headers[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (options.method === 'GET') {
|
||||
options.body = undefined;
|
||||
}
|
||||
|
||||
if (options.body?.reason) {
|
||||
headers['X-Audit-Log-Reason'] = encodeURIComponent(
|
||||
options.body.reason as string
|
||||
);
|
||||
options.body.reason = undefined;
|
||||
}
|
||||
|
||||
if (options.body?.file) {
|
||||
if (!Array.isArray(options.body.file)) {
|
||||
options.body.file = [options.body.file];
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < (options.body.file as FileContent[]).length;
|
||||
i++
|
||||
) {
|
||||
form.append(
|
||||
`file${i}`,
|
||||
(options.body.file as FileContent[])[i].blob,
|
||||
(options.body.file as FileContent[])[i].name
|
||||
);
|
||||
}
|
||||
|
||||
form.append(
|
||||
'payload_json',
|
||||
JSON.stringify({ ...options.body, file: undefined })
|
||||
);
|
||||
options.body.file = form;
|
||||
} else if (
|
||||
options.body &&
|
||||
!['GET', 'DELETE'].includes(options.method)
|
||||
) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
body: (options.body?.file ?? JSON.stringify(options.body)) as
|
||||
| FormData
|
||||
| string,
|
||||
method: options.method,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private checkRateLimits(url: string) {
|
||||
const limited = this.rateLimitedPaths.get(url);
|
||||
const global = this.rateLimitedPaths.get('global');
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (limited && now < limited.resetTimestamp) {
|
||||
return limited.resetTimestamp - now;
|
||||
}
|
||||
|
||||
if (global && now < global.resetTimestamp) {
|
||||
return global.resetTimestamp - now;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private convertRestError(errorStack: Error, data: RestRequestRejection) {
|
||||
errorStack.message = `[${data.status}] ${data.error}\n${data.body}`;
|
||||
return errorStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private cleanupQueues() {
|
||||
for (const [key, queue] of this.pathQueues) {
|
||||
if (queue.requests.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.pathQueues.delete(key);
|
||||
}
|
||||
|
||||
if (!this.pathQueues.size) {
|
||||
this.processingQueue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultRestOptions = Pick<
|
||||
Options,
|
||||
Exclude<keyof Options, keyof typeof DefaultRestAdapter.DEFAULTS>
|
||||
> &
|
||||
Partial<Options>;
|
||||
|
||||
export interface Options {
|
||||
url: string;
|
||||
|
||||
token: string;
|
||||
version: number;
|
||||
|
||||
maxRetryCount: number;
|
||||
}
|
125
packages/rest/src/adapters/rest-adapter.ts
Normal file
125
packages/rest/src/adapters/rest-adapter.ts
Normal file
@ -0,0 +1,125 @@
|
||||
export interface RestRequest {
|
||||
url: string;
|
||||
method: RequestMethod;
|
||||
reject: (payload: RestRequestRejection) => unknown;
|
||||
resolve: (payload: RestRequestResponse) => unknown;
|
||||
}
|
||||
|
||||
export interface RestRequestResponse {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
export interface RestRequestRejection extends RestRequestResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface RestPayload {
|
||||
bucketId?: string;
|
||||
body?: Record<string, unknown>;
|
||||
retryCount: number;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface RestRateLimitedPath {
|
||||
url: string;
|
||||
resetTimestamp: number;
|
||||
bucketId?: string;
|
||||
}
|
||||
|
||||
export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
|
||||
export interface RestSendRequestOptions {
|
||||
url: string;
|
||||
method: RequestMethod;
|
||||
bucketId?: string;
|
||||
reject?: CallableFunction;
|
||||
resolve?: CallableFunction;
|
||||
retryCount?: number;
|
||||
payload?: {
|
||||
headers: Record<string, string>;
|
||||
body: string | FormData;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateRequestBodyOptions {
|
||||
headers?: Record<string, string>;
|
||||
method: RequestMethod;
|
||||
body?: Record<string, unknown>;
|
||||
unauthorized?: boolean;
|
||||
}
|
||||
|
||||
export interface RestAdapter {
|
||||
options: any;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
get<T>(
|
||||
route: string,
|
||||
data?: unknown,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
put<T>(
|
||||
router: string,
|
||||
data: unknown,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
post<T>(
|
||||
router: string,
|
||||
data: unknown,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
patch<T>(
|
||||
router: string,
|
||||
data: unknown,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
delete<T>(
|
||||
router: string,
|
||||
data: unknown,
|
||||
options?: {
|
||||
retryCount?: number;
|
||||
bucketId?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<T>;
|
||||
}
|
3
packages/rest/src/index.ts
Normal file
3
packages/rest/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { RestAdapter } from './adapters/rest-adapter';
|
||||
|
||||
export { DefaultRestAdapter } from './adapters/default-rest-adapter';
|
7
packages/rest/tsconfig.json
Normal file
7
packages/rest/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
12
packages/rest/tsup.config.ts
Normal file
12
packages/rest/tsup.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
minify: isProduction,
|
||||
sourcemap: true,
|
||||
});
|
0
packages/ws/README.md
Normal file
0
packages/ws/README.md
Normal file
23
packages/ws/package.json
Normal file
23
packages/ws/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@biscuitland/ws",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist && rm -rf .turbo",
|
||||
"dev": "tsup --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^1.0.0",
|
||||
"ws": "^8.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ws": "^8.5.3",
|
||||
"tsup": "^6.1.3"
|
||||
}
|
||||
}
|
233
packages/ws/src/adapters/default-ws-adapter.ts
Normal file
233
packages/ws/src/adapters/default-ws-adapter.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { createLeakyBucket } from '../utils/bucket-util';
|
||||
|
||||
import type { LeakyBucket } from '../utils/bucket-util';
|
||||
|
||||
import type { GatewayBot, PickPartial } from '@biscuitland/api-types';
|
||||
import type { WsAdapter } from './ws-adapter';
|
||||
|
||||
import type {
|
||||
Shard,
|
||||
ShardGatewayConfig,
|
||||
ShardOptions,
|
||||
} from '../services/shard';
|
||||
|
||||
import { Agent } from '../services/agent';
|
||||
|
||||
export class DefaultWsAdapter implements WsAdapter {
|
||||
static readonly DEFAULTS = {
|
||||
spawnShardDelay: 5000,
|
||||
|
||||
shardsPerWorker: 5,
|
||||
totalWorkers: 1,
|
||||
|
||||
gatewayBot: {
|
||||
url: 'wss://gateway.discord.gg',
|
||||
shards: 1,
|
||||
|
||||
sessionStartLimit: {
|
||||
total: 1000,
|
||||
remaining: 1000,
|
||||
resetAfter: 0,
|
||||
maxConcurrency: 1,
|
||||
},
|
||||
},
|
||||
|
||||
firstShardId: 0, // remove
|
||||
|
||||
lastShardId: 1, // remove
|
||||
};
|
||||
|
||||
buckets = new Map<
|
||||
number,
|
||||
{
|
||||
workers: { id: number; queue: number[] }[];
|
||||
leak: LeakyBucket;
|
||||
}
|
||||
>();
|
||||
|
||||
options: Options;
|
||||
|
||||
agent: Agent;
|
||||
|
||||
constructor(options: DefaultWsOptions) {
|
||||
this.options = Object.assign(options, DefaultWsAdapter.DEFAULTS);
|
||||
|
||||
this.agent = new Agent({
|
||||
totalShards: this.options.totalShards ?? 1,
|
||||
gatewayConfig: this.options.gatewayConfig,
|
||||
createShardOptions: this.options.createShardOptions,
|
||||
|
||||
handleMessage: (shard: Shard, message: MessageEvent<any>) => {
|
||||
return this.options.handleDiscordPayload(shard, message);
|
||||
},
|
||||
|
||||
handleIdentify: (id: number) => {
|
||||
return this.buckets.get(id)!.leak.acquire(1);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
prepareBuckets() {
|
||||
for (
|
||||
let i = 0;
|
||||
i < this.options.gatewayBot.sessionStartLimit.maxConcurrency;
|
||||
++i
|
||||
) {
|
||||
this.buckets.set(i, {
|
||||
workers: [],
|
||||
leak: createLeakyBucket({
|
||||
max: 1,
|
||||
refillAmount: 1,
|
||||
refillInterval: this.options.spawnShardDelay,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
for (
|
||||
let shardId = this.options.firstShardId;
|
||||
shardId <= this.options.lastShardId;
|
||||
++shardId
|
||||
) {
|
||||
if (shardId >= this.agent.options.totalShards) {
|
||||
throw new Error(
|
||||
`Shard (id: ${shardId}) is bigger or equal to the used amount of used shards which is ${this.agent.options.totalShards}`
|
||||
);
|
||||
}
|
||||
|
||||
const bucketId =
|
||||
shardId %
|
||||
this.options.gatewayBot.sessionStartLimit.maxConcurrency;
|
||||
const bucket = this.buckets.get(bucketId);
|
||||
|
||||
if (!bucket) {
|
||||
throw new Error(
|
||||
`Shard (id: ${shardId}) got assigned to an illegal bucket id: ${bucketId}, expected a bucket id between 0 and ${
|
||||
this.options.gatewayBot.sessionStartLimit
|
||||
.maxConcurrency - 1
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const workerId = this.workerId(shardId);
|
||||
const worker = bucket.workers.find(w => w.id === workerId);
|
||||
|
||||
if (worker) {
|
||||
worker.queue.push(shardId);
|
||||
} else {
|
||||
bucket.workers.push({ id: workerId, queue: [shardId] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
prepareShards() {
|
||||
this.buckets.forEach((bucket, bucketId) => {
|
||||
for (const worker of bucket.workers) {
|
||||
for (const shardId of worker.queue) {
|
||||
this.workerToIdentify(worker.id, shardId, bucketId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
calculateTotalShards(): number {
|
||||
if (this.agent.options.totalShards < 100) {
|
||||
return this.agent.options.totalShards;
|
||||
}
|
||||
|
||||
return (
|
||||
Math.ceil(
|
||||
this.agent.options.totalShards /
|
||||
(this.options.gatewayBot.sessionStartLimit
|
||||
.maxConcurrency === 1
|
||||
? 16
|
||||
: this.options.gatewayBot.sessionStartLimit
|
||||
.maxConcurrency)
|
||||
) * this.options.gatewayBot.sessionStartLimit.maxConcurrency
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
workerToIdentify(_workerId: number, shardId: number, _bucketId: number) {
|
||||
return this.agent.identify(shardId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
workerId(shardId: number) {
|
||||
let workerId = Math.floor(shardId / this.options.shardsPerWorker);
|
||||
|
||||
if (workerId >= this.options.totalWorkers) {
|
||||
workerId = this.options.totalWorkers - 1;
|
||||
}
|
||||
|
||||
return workerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
shards() {
|
||||
this.prepareBuckets();
|
||||
|
||||
this.prepareShards();
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultWsOptions = Pick<
|
||||
Options,
|
||||
Exclude<keyof Options, keyof typeof DefaultWsAdapter.DEFAULTS>
|
||||
> &
|
||||
Partial<Options>;
|
||||
|
||||
interface Options {
|
||||
/** Delay in milliseconds to wait before spawning next shard. */
|
||||
spawnShardDelay: number;
|
||||
|
||||
/** The amount of shards to load per worker. */
|
||||
shardsPerWorker: number;
|
||||
|
||||
/** The total amount of workers to use for your bot. */
|
||||
totalWorkers: number;
|
||||
|
||||
/** Total amount of shards your bot uses. Useful for zero-downtime updates or resharding. */
|
||||
totalShards: number;
|
||||
|
||||
/** Id of the first Shard which should get controlled by this manager. */
|
||||
firstShardId: number;
|
||||
|
||||
/** Id of the last Shard which should get controlled by this manager. */
|
||||
lastShardId: number;
|
||||
|
||||
createShardOptions?: Omit<
|
||||
ShardOptions,
|
||||
'id' | 'totalShards' | 'requestIdentify' | 'gatewayConfig'
|
||||
>;
|
||||
|
||||
/** Important data which is used by the manager to connect shards to the gateway. */
|
||||
gatewayBot: GatewayBot;
|
||||
|
||||
// REMOVE THIS
|
||||
|
||||
gatewayConfig: PickPartial<ShardGatewayConfig, 'token'>;
|
||||
|
||||
/** Sends the discord payload to another guild. */
|
||||
handleDiscordPayload: (shard: Shard, data: MessageEvent<any>) => any;
|
||||
}
|
29
packages/ws/src/adapters/ws-adapter.ts
Normal file
29
packages/ws/src/adapters/ws-adapter.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { Agent } from '../services/agent';
|
||||
import { GatewayBot } from '@biscuitland/api-types';
|
||||
|
||||
export interface WsAdapter {
|
||||
options: Partial<Options | any>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
agent: Agent;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
shards(): void;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
/** Id of the first Shard which should get controlled by this manager. */
|
||||
firstShardId: number;
|
||||
|
||||
/** Id of the last Shard which should get controlled by this manager. */
|
||||
lastShardId: number;
|
||||
|
||||
/** Important data which is used by the manager to connect shards to the gateway. */
|
||||
gatewayBot: GatewayBot;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user