Compare commits
338 commits
Author | SHA1 | Date | |
---|---|---|---|
|
7d621128c3 | ||
|
a25aef6400 | ||
|
90e4bd6ccf | ||
|
7570dfffbe | ||
|
d52d636641 | ||
|
a7547f85ca | ||
|
5df70e5904 | ||
|
ce455c50c3 | ||
|
a5fd1de25e | ||
|
c08565cde9 | ||
|
53030ea941 | ||
|
b686c02cdc | ||
|
258ec12906 | ||
|
b22563ffc3 | ||
|
461e1557c5 | ||
|
60e7c75113 | ||
|
e8371408e2 | ||
|
fee3e0cfa0 | ||
|
6e084af4a3 | ||
|
52ec39a24c | ||
|
8e49dc4306 | ||
|
b64bc29fbe | ||
|
a013388ec9 | ||
|
a8bb3cc49b | ||
|
54b0356a72 | ||
|
057b3ede53 | ||
|
de4e207d85 | ||
|
222f1d0fa8 | ||
|
b1aa995808 | ||
|
718a4658eb | ||
|
b3220fa155 | ||
|
8c4f0d948d | ||
|
36d3e944ff | ||
|
9d6feaff8d | ||
|
1c9d17263b | ||
|
bffead6680 | ||
|
dcbf635860 | ||
|
0a871023c8 | ||
|
e6c0c378de | ||
|
9c18ec2d1f | ||
|
97298f17f8 | ||
|
eac9c4bceb | ||
|
1c25e2d3a1 | ||
|
58ad6fcd3d | ||
|
e7715b75cf | ||
|
c88e4a67b2 | ||
|
2cdb671fb1 | ||
|
8ac716d6a9 | ||
|
ff655e0b7d | ||
|
7f96dfebc3 | ||
|
d9925a4ac4 | ||
|
1beccb57ba | ||
|
332f0f2742 | ||
|
3fe3a84928 | ||
|
a7c442d340 | ||
|
45bfbff3a3 | ||
|
381cb13e6c | ||
|
67d81c1bf0 | ||
|
66031d56c2 | ||
|
9d88c5d113 | ||
|
962d90aca8 | ||
|
addf9cffb6 | ||
|
945bcc6f40 | ||
|
a1ead7e15e | ||
|
b97ec3c225 | ||
|
17e74dce45 | ||
|
3868d29e3e | ||
|
30cd8e10dc | ||
|
ea221f8962 | ||
|
b324263a7b | ||
|
8d08378413 | ||
|
b13cdbe480 | ||
|
229f5f079c | ||
|
c3d1714a6e | ||
|
d0e3dc9fba | ||
|
ef3dcacf0b | ||
|
ca3d68e6a0 | ||
|
ab19a6b624 | ||
|
9a72f66931 | ||
|
34e261a9e1 | ||
|
f3da37bcb1 | ||
|
cacf8e6d14 | ||
|
2745d5c445 | ||
|
f8ecaba024 | ||
|
30be804166 | ||
|
5e2a057462 | ||
|
d17e845b2b | ||
|
7018bf6094 | ||
|
12e6971b0a | ||
|
bd5153fd15 | ||
|
fe6ee7ddac | ||
|
ce843b189b | ||
|
758db4cfb2 | ||
|
8d18f6b1ab | ||
|
97ce6d1f36 | ||
|
d681dc41a7 | ||
|
bd530ecb15 | ||
|
08f2f653f9 | ||
|
f2a8c86dde | ||
|
b479cdc743 | ||
|
5e65673107 | ||
|
1e53d7d770 | ||
|
2123978715 | ||
|
6293a1f940 | ||
|
d63d4f4f12 | ||
|
6f4f06fd8b | ||
|
d829c91c3e | ||
|
a8c9c2e7e0 | ||
|
66e7e48bed | ||
|
eca3cf4e0e | ||
|
f7adfd0f7e | ||
|
38c02bb981 | ||
|
1c7416628d | ||
|
de7996d552 | ||
|
46cbd66166 | ||
|
f812e06d17 | ||
|
22e041f440 | ||
|
7dc316c76f | ||
|
2b46b38908 | ||
|
38b11f4076 | ||
|
f8d7cbccc7 | ||
|
8bf2c1e99d | ||
|
22eb7e0437 | ||
|
0dfbcd7a7b | ||
|
582b296393 | ||
|
01f92b35bf | ||
|
5add0283f9 | ||
|
5a8514949c | ||
|
5e89e3b8e4 | ||
|
3fe5ce3063 | ||
|
96b273ed04 | ||
|
51b1d71e9a | ||
|
d2cedeceb1 | ||
|
064c3a6d76 | ||
|
9c5aea1ef8 | ||
|
b4a1ae389d | ||
|
82431168d3 | ||
|
95145c1bc2 | ||
|
75620ee7ea | ||
|
68cf1105c1 | ||
|
218e3e7ddf | ||
|
a89318c3c3 | ||
|
768fec7195 | ||
|
62b2aa07de | ||
|
4462d7d832 | ||
|
b775d3e323 | ||
|
5fc58bd901 | ||
|
d3ed30ce17 | ||
|
0e57401a6b | ||
|
0692276f3a | ||
|
004bf22126 | ||
|
4dc18f7266 | ||
|
010a48c7f0 | ||
|
37de39daa7 | ||
|
1cdcf045ad | ||
|
3f7372d6a7 | ||
|
ab365f9878 | ||
|
1738204406 | ||
|
a89ab53fa1 | ||
|
7669ded67c | ||
|
7e9c86cc2b | ||
|
aac841101a | ||
|
2472f3c348 | ||
|
f6545b0cbd | ||
|
f3115af2b9 | ||
|
07dae683dc | ||
|
21ccf4c054 | ||
|
b532a99eb4 | ||
|
11fa1281cf | ||
|
9023606faa | ||
|
fccac82fdc | ||
|
4f71717047 | ||
|
6641778578 | ||
|
2286c9af3f | ||
|
711f78002e | ||
|
c1a14fdac1 | ||
|
b02b16ffcc | ||
|
1b77ea0411 | ||
|
2ca5005517 | ||
|
dcb07a9aaa | ||
|
e65fa8fded | ||
|
5f75fc3e71 | ||
|
c836ca8f42 | ||
|
5b015c6c08 | ||
|
90159fa2da | ||
|
b94253284e | ||
|
1c515dcb37 | ||
|
a47d5ae770 | ||
|
7624d2c3c6 | ||
|
3e81002c33 | ||
|
88519d9420 | ||
|
d26059fdc8 | ||
|
9cd9eb9c6f | ||
|
00dae42f7b | ||
|
c8d976762b | ||
|
30fa9429c5 | ||
|
d5634f676f | ||
|
2b0db63f82 | ||
|
8e5a76dbe8 | ||
|
ff80750ca4 | ||
|
548b6bf2ad | ||
|
de089ce610 | ||
|
d5a8207690 | ||
|
f2f292b0ee | ||
|
4d93d66757 | ||
|
b222d91f7c | ||
|
2d6f3d46f3 | ||
|
84baae279e | ||
|
dffce5bd0b | ||
|
7bdbb165f4 | ||
|
98874dd7e7 | ||
|
a800f394ac | ||
|
f4b4c640b3 | ||
|
086e4a95c3 | ||
|
5007dc6029 | ||
|
e0edb1a088 | ||
|
cc7a587040 | ||
|
921d7323a3 | ||
|
361baa39eb | ||
|
950243af5c | ||
|
17a625032b | ||
|
7af4553fbe | ||
|
6a8e4ddc43 | ||
|
57863a6ed5 | ||
|
24c3ea45ca | ||
|
3828a6b911 | ||
|
38dea00369 | ||
|
96bba8d68e | ||
|
ee51609c8c | ||
|
fb609c18bf | ||
|
b844a053e5 | ||
|
dbb80f30b4 | ||
|
b7fee5a41f | ||
|
aa5075451b | ||
|
a16d209a20 | ||
|
b4dd929a3e | ||
|
d7647a88ee | ||
|
6a58a69031 | ||
|
6d21c762de | ||
|
60e2655085 | ||
|
bf0c82f273 | ||
|
2a38fc1e00 | ||
|
be3a793fa1 | ||
|
87c15a9283 | ||
|
54e0e41dc7 | ||
|
926d360d90 | ||
|
11fc7e1e21 | ||
|
0edc74b1db | ||
|
c2e7916aa3 | ||
|
d36922c0b7 | ||
|
dce4c99cbd | ||
|
c1cd6276ae | ||
|
30b5a17492 | ||
|
108bd3ea62 | ||
|
c904d854d9 | ||
|
c880a73771 | ||
|
04c4e7760b | ||
|
2dcce6e2bb | ||
|
dfeb784dfc | ||
|
71ded8ea2e | ||
|
0b995d732d | ||
|
ebb4be897a | ||
|
ad3a0d4b79 | ||
|
2402795006 | ||
|
d4b557e95b | ||
|
28268e779f | ||
|
1827eb4a9b | ||
|
32f55b875e | ||
|
8734e9a7a6 | ||
|
928e18f586 | ||
|
dcbbe15f38 | ||
|
3dde9688ab | ||
|
0cf60b7def | ||
|
b2a93769ff | ||
|
e83dd3353e | ||
|
65e9583445 | ||
|
dc1e2bbc6e | ||
|
06997a1c18 | ||
|
8927939dea | ||
|
eae49afcbe | ||
|
77b08747ab | ||
|
cc75a34d56 | ||
|
cc657cc4d8 | ||
|
617e3b6204 | ||
|
e99bf61f3a | ||
|
8b564a4b49 | ||
|
2d27068f5c | ||
|
989706e71a | ||
|
e7b0804616 | ||
|
0cae198f2c | ||
|
8ad1a2ac6f | ||
|
78ae0bbb56 | ||
|
a54f434d02 | ||
|
52314cea27 | ||
|
556a3c5e2f | ||
|
19e2020e38 | ||
|
7e11b8a351 | ||
|
35da2dbe98 | ||
|
d8e65a4328 | ||
|
3f436b2b3f | ||
|
e3033b4cbf | ||
|
68029084c4 | ||
|
0de9b57204 | ||
|
48976e779b | ||
|
a630e9466c | ||
|
1d562f6997 | ||
|
3fe0001e44 | ||
|
53b49e45aa | ||
|
2fadcfa407 | ||
|
fbd6eae244 | ||
|
734133bcca | ||
|
60164122bf | ||
|
6b5c15d19a | ||
|
c4a3fbc45c | ||
|
4364d3879b | ||
|
e009ae3127 | ||
|
4cbc2b344d | ||
|
68fa36ce3c | ||
|
39c7761a96 | ||
|
cff738b2e9 | ||
|
055a030c4e | ||
|
6c3f71669d | ||
|
c322c25fb0 | ||
|
6e8af1f7da | ||
|
e9f461fef4 | ||
|
b112b6ae36 | ||
|
33f103279d | ||
|
432555e2fb | ||
|
8f4433dc9f | ||
|
c3fafdfcf0 | ||
|
e3bce85f09 | ||
|
b344c2e24a | ||
|
8edef36ead | ||
|
df66f4ea3e | ||
|
7a2b71b648 | ||
|
417854b1bb | ||
|
ae61efe73b | ||
|
30724dc791 |
203 changed files with 10001 additions and 3506 deletions
3
.eggignore
Normal file
3
.eggignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
extends .gitignore
|
||||||
|
./src/test/**/*
|
||||||
|
|
|
@ -17,6 +17,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/restrict-template-expressions': 'off'
|
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## About
|
||||||
|
|
||||||
|
<!-- describe what changes does this PR introduce and why are these needed -->
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- [ ] These changes have been tested against Discord API or contain no API change.
|
||||||
|
- [ ] This PR includes only documentation changes, no code change.
|
||||||
|
- [ ] This PR introduces some Breaking changes.
|
4
.github/workflows/deno.yml
vendored
4
.github/workflows/deno.yml
vendored
|
@ -20,14 +20,14 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
deno: ['v1.x', 'nightly']
|
deno: ['v1.x', 'canary']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup repo
|
- name: Setup repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denolib/setup-deno@v2.3.0
|
uses: denoland/setup-deno@main
|
||||||
with:
|
with:
|
||||||
deno-version: ${{ matrix.deno }} # tests across multiple Deno versions
|
deno-version: ${{ matrix.deno }} # tests across multiple Deno versions
|
||||||
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -109,6 +109,7 @@ yarn.lock
|
||||||
|
|
||||||
# PRIVACY XDDDD
|
# PRIVACY XDDDD
|
||||||
src/test/config.ts
|
src/test/config.ts
|
||||||
|
test/config.ts
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# macOS is shit xD
|
# macOS is shit xD
|
||||||
|
|
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at djdeveloperr@gmail.com. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 Harmony Org
|
Copyright (c) 2020-2021 Harmonyland
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
59
README.md
59
README.md
|
@ -3,7 +3,7 @@
|
||||||
<p align=center><i><b>An easy to use Discord API Library for Deno</b></i></p>
|
<p align=center><i><b>An easy to use Discord API Library for Deno</b></i></p>
|
||||||
<p align=center>
|
<p align=center>
|
||||||
<img src="https://img.shields.io/badge/standard--readme-OK-green.svg?style=for-the-badge"/>
|
<img src="https://img.shields.io/badge/standard--readme-OK-green.svg?style=for-the-badge"/>
|
||||||
<a href=https://discord.gg/WVN2JF2FRv>
|
<a href=https://discord.gg/harmony>
|
||||||
<img src="https://img.shields.io/discord/783319033205751809.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge" alt="Support">
|
<img src="https://img.shields.io/discord/783319033205751809.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge" alt="Support">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -28,7 +28,9 @@
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can import the package from https://deno.land/x/harmony/mod.ts (with latest version) or can add a version too, and raw GitHub URL (latest unpublished version) https://raw.githubusercontent.com/harmony-org/harmony/main/mod.ts too.
|
You can import the package from https://deno.land/x/harmony/mod.ts (with latest version) or can add a version too, and raw GitHub URL (latest unpublished version) https://raw.githubusercontent.com/harmonyland/harmony/main/mod.ts too.
|
||||||
|
|
||||||
|
You can also check(not import) the module in https://nest.land/package/harmony (link for importing is in the site).
|
||||||
|
|
||||||
For a quick example, run this:
|
For a quick example, run this:
|
||||||
|
|
||||||
|
@ -36,12 +38,16 @@ For a quick example, run this:
|
||||||
deno run --allow-net https://deno.land/x/harmony/examples/ping.ts
|
deno run --allow-net https://deno.land/x/harmony/examples/ping.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
And input your bot's token and Intents.
|
And input your bot's token.
|
||||||
|
|
||||||
Here is a small example of how to use harmony,
|
Here is a small example of how to use harmony,
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Client, Message, Intents } from 'https://deno.land/x/harmony/mod.ts'
|
import {
|
||||||
|
Client,
|
||||||
|
Message,
|
||||||
|
GatewayIntents
|
||||||
|
} from 'https://deno.land/x/harmony/mod.ts'
|
||||||
|
|
||||||
const client = new Client()
|
const client = new Client()
|
||||||
|
|
||||||
|
@ -53,13 +59,16 @@ client.on('ready', () => {
|
||||||
// Listen for event whenever a Message is sent
|
// Listen for event whenever a Message is sent
|
||||||
client.on('messageCreate', (msg: Message): void => {
|
client.on('messageCreate', (msg: Message): void => {
|
||||||
if (msg.content === '!ping') {
|
if (msg.content === '!ping') {
|
||||||
msg.channel.send(`Pong! WS Ping: ${client.ping}`)
|
msg.channel.send(`Pong! WS Ping: ${client.gateway.ping}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Connect to gateway
|
// Connect to gateway
|
||||||
// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers)
|
client.connect('super secret token comes here', [
|
||||||
client.connect('super secret token comes here', Intents.All)
|
GatewayIntents.DIRECT_MESSAGES,
|
||||||
|
GatewayIntents.GUILDS,
|
||||||
|
GatewayIntents.GUILD_MESSAGES
|
||||||
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with CommandClient!
|
Or with CommandClient!
|
||||||
|
@ -69,8 +78,7 @@ import {
|
||||||
CommandClient,
|
CommandClient,
|
||||||
Command,
|
Command,
|
||||||
CommandContext,
|
CommandContext,
|
||||||
Message,
|
GatewayIntents
|
||||||
Intents
|
|
||||||
} from 'https://deno.land/x/harmony/mod.ts'
|
} from 'https://deno.land/x/harmony/mod.ts'
|
||||||
|
|
||||||
const client = new CommandClient({
|
const client = new CommandClient({
|
||||||
|
@ -87,26 +95,29 @@ class PingCommand extends Command {
|
||||||
name = 'ping'
|
name = 'ping'
|
||||||
|
|
||||||
execute(ctx: CommandContext) {
|
execute(ctx: CommandContext) {
|
||||||
ctx.message.reply(`pong! Ping: ${ctx.client.ping}ms`)
|
ctx.message.reply(`pong! Ping: ${ctx.client.gateway.ping}ms`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.commands.add(PingCommand)
|
client.commands.add(PingCommand)
|
||||||
|
|
||||||
// Connect to gateway
|
// Connect to gateway
|
||||||
// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers)
|
client.connect('super secret token comes here', [
|
||||||
client.connect('super secret token comes here', Intents.All)
|
GatewayIntents.DIRECT_MESSAGES,
|
||||||
|
GatewayIntents.GUILDS,
|
||||||
|
GatewayIntents.GUILD_MESSAGES
|
||||||
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with Decorators!
|
Or with Decorators!
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import {
|
import {
|
||||||
Client,
|
|
||||||
event,
|
event,
|
||||||
Intents,
|
CommandClient,
|
||||||
command,
|
command,
|
||||||
CommandContext
|
CommandContext,
|
||||||
|
GatewayIntents
|
||||||
} from 'https://deno.land/x/harmony/mod.ts'
|
} from 'https://deno.land/x/harmony/mod.ts'
|
||||||
|
|
||||||
class MyClient extends CommandClient {
|
class MyClient extends CommandClient {
|
||||||
|
@ -128,22 +139,24 @@ class MyClient extends CommandClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to gateway
|
new MyClient().connect('super secret token comes here', [
|
||||||
// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers)
|
GatewayIntents.DIRECT_MESSAGES,
|
||||||
new MyClient().connect('super secret token comes here', Intents.All)
|
GatewayIntents.GUILDS,
|
||||||
|
GatewayIntents.GUILD_MESSAGES
|
||||||
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Documentation is available for `main` (branch) and `stable` (release).
|
Documentation is available for `main` (branch) and `stable` (release).
|
||||||
|
|
||||||
- [Main](https://doc.deno.land/https/raw.githubusercontent.com/harmony-org/harmony/main/mod.ts)
|
- [Main](https://doc.deno.land/https/raw.githubusercontent.com/harmonyland/harmony/main/mod.ts)
|
||||||
- [Stable](https://doc.deno.land/https/deno.land/x/harmony/mod.ts)
|
- [Stable](https://doc.deno.land/https/deno.land/x/harmony/mod.ts)
|
||||||
- [Guide](https://harmony-org.github.io)
|
- [Guide](https://harmony.mod.land)
|
||||||
|
|
||||||
## Found a bug or want support? Join our discord server!
|
## Found a bug or want support? Join our discord server!
|
||||||
|
|
||||||
[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/WVN2JF2FRv)
|
[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/harmony)
|
||||||
|
|
||||||
## Maintainer
|
## Maintainer
|
||||||
|
|
||||||
|
@ -159,6 +172,6 @@ Small note: If editing the README, please conform to the [standard-readme](https
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT © 2020 Harmony Org](LICENSE)
|
[MIT © 2020-2021 Harmonyland](LICENSE)
|
||||||
|
|
||||||
#### Made with ❤ by Harmony-org
|
#### Made with ❤ by Harmonyland
|
||||||
|
|
151
deploy.ts
Normal file
151
deploy.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import { Interaction } from './src/structures/interactions.ts'
|
||||||
|
import {
|
||||||
|
SlashCommandsManager,
|
||||||
|
SlashClient,
|
||||||
|
SlashCommandHandlerCallback,
|
||||||
|
SlashCommandHandler
|
||||||
|
} from './src/interactions/mod.ts'
|
||||||
|
import {
|
||||||
|
InteractionResponseType,
|
||||||
|
InteractionType
|
||||||
|
} from './src/types/interactions.ts'
|
||||||
|
|
||||||
|
export interface DeploySlashInitOptions {
|
||||||
|
env?: boolean
|
||||||
|
publicKey?: string
|
||||||
|
token?: string
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current Slash Client being used to handle commands */
|
||||||
|
let client: SlashClient
|
||||||
|
/** Manage Slash Commands right in Deploy */
|
||||||
|
let commands: SlashCommandsManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Slash Commands Handler for [Deno Deploy](https://deno.com/deploy).
|
||||||
|
* Easily create Serverless Slash Commands on the fly.
|
||||||
|
*
|
||||||
|
* **Examples**
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* init({
|
||||||
|
* publicKey: "my public key",
|
||||||
|
* token: "my bot's token", // only required if you want to manage slash commands in code
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* // takes up `PUBLIC_KEY` and `TOKEN` from ENV
|
||||||
|
* init({ env: true })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param options Initialization options
|
||||||
|
*/
|
||||||
|
export function init(options: { env: boolean; path?: string }): void
|
||||||
|
export function init(options: {
|
||||||
|
publicKey: string
|
||||||
|
token?: string
|
||||||
|
path?: string
|
||||||
|
}): void
|
||||||
|
export function init(options: DeploySlashInitOptions): void {
|
||||||
|
if (client !== undefined) throw new Error('Already initialized')
|
||||||
|
if (options.env === true) {
|
||||||
|
options.publicKey = Deno.env.get('PUBLIC_KEY')
|
||||||
|
options.token = Deno.env.get('TOKEN')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.publicKey === undefined)
|
||||||
|
throw new Error('Public Key not provided')
|
||||||
|
|
||||||
|
client = new SlashClient({
|
||||||
|
token: options.token,
|
||||||
|
publicKey: options.publicKey
|
||||||
|
})
|
||||||
|
|
||||||
|
commands = client.commands
|
||||||
|
|
||||||
|
const cb = async (evt: {
|
||||||
|
respondWith: CallableFunction
|
||||||
|
request: Request
|
||||||
|
}): Promise<void> => {
|
||||||
|
if (options.path !== undefined) {
|
||||||
|
if (new URL(evt.request.url).pathname !== options.path) return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// we have to wrap because there are some weird scope errors
|
||||||
|
const d = await client.verifyFetchEvent({
|
||||||
|
respondWith: (...args: any[]) => evt.respondWith(...args),
|
||||||
|
request: evt.request
|
||||||
|
})
|
||||||
|
if (d === false) {
|
||||||
|
await evt.respondWith(
|
||||||
|
new Response('Not Authorized', {
|
||||||
|
status: 400
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.type === InteractionType.PING) {
|
||||||
|
await d.respond({ type: InteractionResponseType.PONG })
|
||||||
|
client.emit('ping')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await (client as any)._process(d)
|
||||||
|
} catch (e) {
|
||||||
|
await client.emit('interactionError', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener('fetch', cb as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Slash Command handler.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* handle("ping", (interaction) => {
|
||||||
|
* interaction.reply("Pong!")
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Also supports Sub Command and Group handling out of the box!
|
||||||
|
* ```ts
|
||||||
|
* handle("command-name group-name sub-command", (i) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* handle("command-name sub-command", (i) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param cmd Command to handle. Either Handler object or command name followed by handler function in next parameter.
|
||||||
|
* @param handler Handler function (required if previous argument was command name)
|
||||||
|
*/
|
||||||
|
export function handle(
|
||||||
|
cmd: string | SlashCommandHandler,
|
||||||
|
handler?: SlashCommandHandlerCallback
|
||||||
|
): void {
|
||||||
|
if (client === undefined)
|
||||||
|
throw new Error('Slash Client not initialized. Call `init` first')
|
||||||
|
client.handle(cmd, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interactions(cb: (i: Interaction) => any): void {
|
||||||
|
client.on('interaction', cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { commands, client }
|
||||||
|
export * from './src/types/slashCommands.ts'
|
||||||
|
export * from './src/types/interactions.ts'
|
||||||
|
export * from './src/structures/slash.ts'
|
||||||
|
export * from './src/interactions/mod.ts'
|
||||||
|
export * from './src/types/channel.ts'
|
||||||
|
export * from './src/structures/interactions.ts'
|
||||||
|
export * from './src/structures/message.ts'
|
||||||
|
export * from './src/structures/embed.ts'
|
15
deps.ts
15
deps.ts
|
@ -1,9 +1,6 @@
|
||||||
export { EventEmitter } from 'https://deno.land/std@0.82.0/node/events.ts'
|
export { EventEmitter } from 'https://deno.land/x/event@1.0.0/mod.ts'
|
||||||
export { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts'
|
export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts'
|
||||||
export { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
|
export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts'
|
||||||
export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'
|
export { walk } from 'https://deno.land/std@0.95.0/fs/walk.ts'
|
||||||
export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts'
|
export { join } from 'https://deno.land/std@0.95.0/path/mod.ts'
|
||||||
export type {
|
export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0'
|
||||||
Redis,
|
|
||||||
RedisConnectOptions
|
|
||||||
} from 'https://deno.land/x/redis@v0.14.1/mod.ts'
|
|
||||||
|
|
24
egg.json
Normal file
24
egg.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://x.nest.land/eggs@0.3.4/src/schema.json",
|
||||||
|
"name": "harmony",
|
||||||
|
"entry": "./mod.ts",
|
||||||
|
"description": "An easy to use Discord API Library for Deno.",
|
||||||
|
"homepage": "https://github.com/harmonyland/harmony",
|
||||||
|
"version": "v1.1.4",
|
||||||
|
"files": [
|
||||||
|
"./src/**/*",
|
||||||
|
"./deps.ts",
|
||||||
|
"./README.md",
|
||||||
|
"./LICENSE",
|
||||||
|
"./banner.png",
|
||||||
|
"./CONTRIBUTING.md",
|
||||||
|
"./CODE_OF_CONDUCT.md",
|
||||||
|
"./examples/*"
|
||||||
|
],
|
||||||
|
"checkFormat": "npx eslint src",
|
||||||
|
"checkTests": false,
|
||||||
|
"checkInstallation": false,
|
||||||
|
"check": true,
|
||||||
|
"unlisted": false,
|
||||||
|
"ignore": []
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Client, Message, Intents } from '../mod.ts'
|
import { Client, Message, GatewayIntents } from '../mod.ts'
|
||||||
|
|
||||||
const client = new Client()
|
const client = new Client()
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ client.on('messageCreate', (msg: Message) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('harmony - ping example')
|
console.log('Harmony - Ping Example')
|
||||||
|
|
||||||
const token = prompt('Input Bot Token:')
|
const token = prompt('Input Bot Token:')
|
||||||
if (token === null) {
|
if (token === null) {
|
||||||
|
@ -21,23 +21,8 @@ if (token === null) {
|
||||||
Deno.exit()
|
Deno.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
const intents = prompt(
|
client.connect(token, [
|
||||||
'Input Intents (0 = All, 1 = Presence, 2 = Server Members, 3 = None):'
|
GatewayIntents.GUILD_MESSAGES,
|
||||||
)
|
GatewayIntents.GUILDS,
|
||||||
if (intents === null || !['0', '1', '2', '3'].includes(intents)) {
|
GatewayIntents.DIRECT_MESSAGES
|
||||||
console.log('No intents provided')
|
])
|
||||||
Deno.exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
let ints
|
|
||||||
if (intents === '0') {
|
|
||||||
ints = Intents.All
|
|
||||||
} else if (intents === '1') {
|
|
||||||
ints = Intents.Presence
|
|
||||||
} else if (intents === '2') {
|
|
||||||
ints = Intents.GuildMembers
|
|
||||||
} else {
|
|
||||||
ints = Intents.None
|
|
||||||
}
|
|
||||||
|
|
||||||
client.connect(token, ints)
|
|
||||||
|
|
121
mod.ts
121
mod.ts
|
@ -1,37 +1,47 @@
|
||||||
export { GatewayIntents } from './src/types/gateway.ts'
|
export { GatewayIntents } from './src/types/gateway.ts'
|
||||||
export { Base } from './src/structures/base.ts'
|
export { Base } from './src/structures/base.ts'
|
||||||
export { Gateway } from './src/gateway/index.ts'
|
export { Gateway } from './src/gateway/mod.ts'
|
||||||
export type { ClientEvents } from './src/gateway/handlers/index.ts'
|
export type { GatewayTypedEvents } from './src/gateway/mod.ts'
|
||||||
export * from './src/models/client.ts'
|
export type { ClientEvents } from './src/gateway/handlers/mod.ts'
|
||||||
export * from './src/models/slashClient.ts'
|
export * from './src/client/mod.ts'
|
||||||
export { RESTManager } from './src/models/rest.ts'
|
export * from './src/interactions/mod.ts'
|
||||||
export * from './src/models/cacheAdapter.ts'
|
export {
|
||||||
|
RESTManager,
|
||||||
|
TokenType,
|
||||||
|
HttpResponseCode,
|
||||||
|
DiscordAPIError
|
||||||
|
} from './src/rest/mod.ts'
|
||||||
|
export * from './src/rest/mod.ts'
|
||||||
|
export * from './src/cache/adapter.ts'
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandBuilder,
|
CommandBuilder,
|
||||||
CommandCategory,
|
CommandCategory,
|
||||||
CommandsManager,
|
CommandsManager,
|
||||||
CategoriesManager
|
CategoriesManager,
|
||||||
} from './src/models/command.ts'
|
CommandsLoader
|
||||||
export type { CommandContext, CommandOptions } from './src/models/command.ts'
|
} from './src/commands/command.ts'
|
||||||
|
export type { CommandContext, CommandOptions } from './src/commands/command.ts'
|
||||||
export {
|
export {
|
||||||
Extension,
|
Extension,
|
||||||
ExtensionCommands,
|
ExtensionCommands,
|
||||||
ExtensionsManager
|
ExtensionsManager
|
||||||
} from './src/models/extensions.ts'
|
} from './src/commands/extension.ts'
|
||||||
export { SlashModule } from './src/models/slashModule.ts'
|
export { SlashModule } from './src/interactions/slashModule.ts'
|
||||||
export { CommandClient, command } from './src/models/commandClient.ts'
|
export { CommandClient, command } from './src/commands/client.ts'
|
||||||
export type { CommandClientOptions } from './src/models/commandClient.ts'
|
export type { CommandClientOptions } from './src/commands/client.ts'
|
||||||
export { BaseManager } from './src/managers/base.ts'
|
export { BaseManager } from './src/managers/base.ts'
|
||||||
export { BaseChildManager } from './src/managers/baseChild.ts'
|
export { BaseChildManager } from './src/managers/baseChild.ts'
|
||||||
export { ChannelsManager } from './src/managers/channels.ts'
|
export { ChannelsManager } from './src/managers/channels.ts'
|
||||||
export { EmojisManager } from './src/managers/emojis.ts'
|
export { EmojisManager } from './src/managers/emojis.ts'
|
||||||
export { GatewayCache } from './src/managers/gatewayCache.ts'
|
export { GatewayCache } from './src/managers/gatewayCache.ts'
|
||||||
export { GuildChannelsManager } from './src/managers/guildChannels.ts'
|
export { GuildChannelsManager } from './src/managers/guildChannels.ts'
|
||||||
export type { GuildChannel } from './src/managers/guildChannels.ts'
|
|
||||||
export { GuildManager } from './src/managers/guilds.ts'
|
export { GuildManager } from './src/managers/guilds.ts'
|
||||||
|
export * from './src/structures/base.ts'
|
||||||
export * from './src/structures/slash.ts'
|
export * from './src/structures/slash.ts'
|
||||||
export * from './src/types/slash.ts'
|
export * from './src/structures/interactions.ts'
|
||||||
|
export * from './src/types/slashCommands.ts'
|
||||||
|
export * from './src/types/interactions.ts'
|
||||||
export { GuildEmojisManager } from './src/managers/guildEmojis.ts'
|
export { GuildEmojisManager } from './src/managers/guildEmojis.ts'
|
||||||
export { MembersManager } from './src/managers/members.ts'
|
export { MembersManager } from './src/managers/members.ts'
|
||||||
export { MessageReactionsManager } from './src/managers/messageReactions.ts'
|
export { MessageReactionsManager } from './src/managers/messageReactions.ts'
|
||||||
|
@ -39,9 +49,11 @@ export { ReactionUsersManager } from './src/managers/reactionUsers.ts'
|
||||||
export { MessagesManager } from './src/managers/messages.ts'
|
export { MessagesManager } from './src/managers/messages.ts'
|
||||||
export { RolesManager } from './src/managers/roles.ts'
|
export { RolesManager } from './src/managers/roles.ts'
|
||||||
export { UsersManager } from './src/managers/users.ts'
|
export { UsersManager } from './src/managers/users.ts'
|
||||||
|
export { InviteManager } from './src/managers/invites.ts'
|
||||||
export { Application } from './src/structures/application.ts'
|
export { Application } from './src/structures/application.ts'
|
||||||
// export { ImageURL } from './src/structures/cdn.ts'
|
export { ImageURL } from './src/structures/cdn.ts'
|
||||||
export { Channel } from './src/structures/channel.ts'
|
export { Channel, GuildChannel } from './src/structures/channel.ts'
|
||||||
|
export type { EditOverwriteOptions } from './src/structures/channel.ts'
|
||||||
export { DMChannel } from './src/structures/dmChannel.ts'
|
export { DMChannel } from './src/structures/dmChannel.ts'
|
||||||
export { Embed } from './src/structures/embed.ts'
|
export { Embed } from './src/structures/embed.ts'
|
||||||
export { Emoji } from './src/structures/emoji.ts'
|
export { Emoji } from './src/structures/emoji.ts'
|
||||||
|
@ -57,7 +69,11 @@ export { NewsChannel } from './src/structures/guildNewsChannel.ts'
|
||||||
export { VoiceChannel } from './src/structures/guildVoiceChannel.ts'
|
export { VoiceChannel } from './src/structures/guildVoiceChannel.ts'
|
||||||
export { Invite } from './src/structures/invite.ts'
|
export { Invite } from './src/structures/invite.ts'
|
||||||
export * from './src/structures/member.ts'
|
export * from './src/structures/member.ts'
|
||||||
export { Message } from './src/structures/message.ts'
|
export {
|
||||||
|
Message,
|
||||||
|
MessageAttachment,
|
||||||
|
MessageInteraction
|
||||||
|
} from './src/structures/message.ts'
|
||||||
export { MessageMentions } from './src/structures/messageMentions.ts'
|
export { MessageMentions } from './src/structures/messageMentions.ts'
|
||||||
export {
|
export {
|
||||||
Presence,
|
Presence,
|
||||||
|
@ -66,7 +82,13 @@ export {
|
||||||
} from './src/structures/presence.ts'
|
} from './src/structures/presence.ts'
|
||||||
export { Role } from './src/structures/role.ts'
|
export { Role } from './src/structures/role.ts'
|
||||||
export { Snowflake } from './src/utils/snowflake.ts'
|
export { Snowflake } from './src/utils/snowflake.ts'
|
||||||
export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts'
|
export { TextChannel } from './src/structures/textChannel.ts'
|
||||||
|
export {
|
||||||
|
GuildTextBasedChannel,
|
||||||
|
GuildTextChannel,
|
||||||
|
checkGuildTextBasedChannel
|
||||||
|
} from './src/structures/guildTextChannel.ts'
|
||||||
|
export type { AllMessageOptions } from './src/structures/textChannel.ts'
|
||||||
export { MessageReaction } from './src/structures/messageReaction.ts'
|
export { MessageReaction } from './src/structures/messageReaction.ts'
|
||||||
export { User } from './src/structures/user.ts'
|
export { User } from './src/structures/user.ts'
|
||||||
export { Webhook } from './src/structures/webhook.ts'
|
export { Webhook } from './src/structures/webhook.ts'
|
||||||
|
@ -75,7 +97,8 @@ export { Intents } from './src/utils/intents.ts'
|
||||||
// export { getBuildInfo } from './src/utils/buildInfo.ts'
|
// export { getBuildInfo } from './src/utils/buildInfo.ts'
|
||||||
export * from './src/utils/permissions.ts'
|
export * from './src/utils/permissions.ts'
|
||||||
export { UserFlagsManager } from './src/utils/userFlags.ts'
|
export { UserFlagsManager } from './src/utils/userFlags.ts'
|
||||||
export type { EveryChannelTypes } from './src/utils/getChannelByType.ts'
|
export { HarmonyEventEmitter } from './src/utils/events.ts'
|
||||||
|
export type { EveryChannelTypes } from './src/utils/channel.ts'
|
||||||
export * from './src/utils/bitfield.ts'
|
export * from './src/utils/bitfield.ts'
|
||||||
export type {
|
export type {
|
||||||
ActivityGame,
|
ActivityGame,
|
||||||
|
@ -83,7 +106,15 @@ export type {
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
StatusType
|
StatusType
|
||||||
} from './src/types/presence.ts'
|
} from './src/types/presence.ts'
|
||||||
export { ChannelTypes } from './src/types/channel.ts'
|
export {
|
||||||
|
ChannelTypes,
|
||||||
|
OverwriteType,
|
||||||
|
OverrideType
|
||||||
|
} from './src/types/channel.ts'
|
||||||
|
export type {
|
||||||
|
OverwriteAsOptions,
|
||||||
|
OverwritePayload
|
||||||
|
} from './src/types/channel.ts'
|
||||||
export type { ApplicationPayload } from './src/types/application.ts'
|
export type { ApplicationPayload } from './src/types/application.ts'
|
||||||
export type { ImageFormats, ImageSize } from './src/types/cdn.ts'
|
export type { ImageFormats, ImageSize } from './src/types/cdn.ts'
|
||||||
export type {
|
export type {
|
||||||
|
@ -95,14 +126,33 @@ export type {
|
||||||
GuildChannelPayload,
|
GuildChannelPayload,
|
||||||
GuildTextChannelPayload,
|
GuildTextChannelPayload,
|
||||||
GuildVoiceChannelPayload,
|
GuildVoiceChannelPayload,
|
||||||
GroupDMChannelPayload
|
GroupDMChannelPayload,
|
||||||
|
MessageOptions,
|
||||||
|
MessagePayload,
|
||||||
|
MessageInteractionPayload,
|
||||||
|
MessageReference,
|
||||||
|
MessageActivity,
|
||||||
|
MessageActivityTypes,
|
||||||
|
MessageApplication,
|
||||||
|
MessageFlags,
|
||||||
|
MessageStickerFormatTypes,
|
||||||
|
MessageStickerPayload,
|
||||||
|
MessageTypes,
|
||||||
|
OverwriteAsArg,
|
||||||
|
Overwrite
|
||||||
} from './src/types/channel.ts'
|
} from './src/types/channel.ts'
|
||||||
export type { EmojiPayload } from './src/types/emoji.ts'
|
export type { EmojiPayload } from './src/types/emoji.ts'
|
||||||
|
export { Verification } from './src/types/guild.ts'
|
||||||
export type {
|
export type {
|
||||||
|
GuildIntegrationPayload,
|
||||||
|
GuildPayload,
|
||||||
GuildBanPayload,
|
GuildBanPayload,
|
||||||
GuildFeatures,
|
GuildFeatures,
|
||||||
GuildIntegrationPayload,
|
GuildChannels,
|
||||||
GuildPayload
|
GuildTextBasedChannels,
|
||||||
|
GuildCreateOptions,
|
||||||
|
GuildCreateChannelOptions,
|
||||||
|
GuildCreateRolePayload
|
||||||
} from './src/types/guild.ts'
|
} from './src/types/guild.ts'
|
||||||
export type { InvitePayload, PartialInvitePayload } from './src/types/invite.ts'
|
export type { InvitePayload, PartialInvitePayload } from './src/types/invite.ts'
|
||||||
export { PermissionFlags } from './src/types/permissionFlags.ts'
|
export { PermissionFlags } from './src/types/permissionFlags.ts'
|
||||||
|
@ -122,3 +172,26 @@ export type { UserPayload } from './src/types/user.ts'
|
||||||
export { UserFlags } from './src/types/userFlags.ts'
|
export { UserFlags } from './src/types/userFlags.ts'
|
||||||
export type { VoiceStatePayload } from './src/types/voice.ts'
|
export type { VoiceStatePayload } from './src/types/voice.ts'
|
||||||
export type { WebhookPayload } from './src/types/webhook.ts'
|
export type { WebhookPayload } from './src/types/webhook.ts'
|
||||||
|
export * from './src/client/collectors.ts'
|
||||||
|
export type { Dict } from './src/utils/dict.ts'
|
||||||
|
export * from './src/cache/redis.ts'
|
||||||
|
export { ColorUtil } from './src/utils/colorutil.ts'
|
||||||
|
export type { Colors } from './src/utils/colorutil.ts'
|
||||||
|
export { StoreChannel } from './src/structures/guildStoreChannel.ts'
|
||||||
|
export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts'
|
||||||
|
export {
|
||||||
|
isCategoryChannel,
|
||||||
|
isDMChannel,
|
||||||
|
isGroupDMChannel,
|
||||||
|
isGuildBasedTextChannel,
|
||||||
|
isGuildChannel,
|
||||||
|
isGuildTextChannel,
|
||||||
|
isNewsChannel,
|
||||||
|
isStageVoiceChannel,
|
||||||
|
isStoreChannel,
|
||||||
|
isTextChannel,
|
||||||
|
isVoiceChannel,
|
||||||
|
default as getChannelByType
|
||||||
|
} from './src/utils/channel.ts'
|
||||||
|
export * from './src/utils/interactions.ts'
|
||||||
|
export * from "./src/utils/command.ts"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "harmony",
|
"name": "harmony",
|
||||||
"version": "0.9.2",
|
"version": "0.9.3",
|
||||||
"description": "Discord Deno API that is easy to use.",
|
"description": "Discord Deno API that is easy to use.",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"repository": "https://github.com/harmony-org/harmony.git",
|
"repository": "https://github.com/harmony-org/harmony.git",
|
||||||
|
|
22
src/cache/adapter.ts
vendored
Normal file
22
src/cache/adapter.ts
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony.
|
||||||
|
*
|
||||||
|
* Methods can return Promises too.
|
||||||
|
*/
|
||||||
|
export interface ICacheAdapter {
|
||||||
|
/** Gets a key from a Cache */
|
||||||
|
get: (cacheName: string, key: string) => Promise<any> | any
|
||||||
|
/** Sets a key to value in a Cache Name with optional expire value in MS */
|
||||||
|
set: (
|
||||||
|
cacheName: string,
|
||||||
|
key: string,
|
||||||
|
value: any,
|
||||||
|
expire?: number
|
||||||
|
) => Promise<any> | any
|
||||||
|
/** Deletes a key from a Cache */
|
||||||
|
delete: (cacheName: string, key: string) => Promise<boolean> | boolean
|
||||||
|
/** Gets array of all values in a Cache */
|
||||||
|
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined>
|
||||||
|
/** Entirely deletes a Cache */
|
||||||
|
deleteCache: (cacheName: string) => any
|
||||||
|
}
|
50
src/cache/default.ts
vendored
Normal file
50
src/cache/default.ts
vendored
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Collection } from '../utils/collection.ts'
|
||||||
|
import type { ICacheAdapter } from './adapter.ts'
|
||||||
|
|
||||||
|
/** Default Cache Adapter for in-memory caching. */
|
||||||
|
export class DefaultCacheAdapter implements ICacheAdapter {
|
||||||
|
data: {
|
||||||
|
[name: string]: Collection<string, any>
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
async get(cacheName: string, key: string): Promise<undefined | any> {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) return
|
||||||
|
return cache.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(
|
||||||
|
cacheName: string,
|
||||||
|
key: string,
|
||||||
|
value: any,
|
||||||
|
expire?: number
|
||||||
|
): Promise<any> {
|
||||||
|
let cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) {
|
||||||
|
this.data[cacheName] = new Collection()
|
||||||
|
cache = this.data[cacheName]
|
||||||
|
}
|
||||||
|
cache.set(key, value)
|
||||||
|
if (expire !== undefined)
|
||||||
|
setTimeout(() => {
|
||||||
|
cache.delete(key)
|
||||||
|
}, expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(cacheName: string, key: string): Promise<boolean> {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) return false
|
||||||
|
return cache.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async array(cacheName: string): Promise<any[] | undefined> {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) return
|
||||||
|
return cache.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCache(cacheName: string): Promise<boolean> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
return delete this.data[cacheName]
|
||||||
|
}
|
||||||
|
}
|
4
src/cache/mod.ts
vendored
Normal file
4
src/cache/mod.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './adapter.ts'
|
||||||
|
export * from './default.ts'
|
||||||
|
// Not exported by default
|
||||||
|
// export * from './redis.ts'
|
76
src/models/cacheAdapter.ts → src/cache/redis.ts
vendored
76
src/models/cacheAdapter.ts → src/cache/redis.ts
vendored
|
@ -1,80 +1,10 @@
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { ICacheAdapter } from './adapter.ts'
|
||||||
|
// Not in deps.ts to allow optional dep loading
|
||||||
import {
|
import {
|
||||||
connect,
|
connect,
|
||||||
Redis,
|
Redis,
|
||||||
RedisConnectOptions
|
RedisConnectOptions
|
||||||
} from '../../deps.ts'
|
} from 'https://deno.land/x/redis@v0.22.0/mod.ts'
|
||||||
|
|
||||||
/**
|
|
||||||
* ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony.
|
|
||||||
*
|
|
||||||
* Methods can return Promises too.
|
|
||||||
*/
|
|
||||||
export interface ICacheAdapter {
|
|
||||||
/** Gets a key from a Cache */
|
|
||||||
get: (cacheName: string, key: string) => Promise<any> | any
|
|
||||||
/** Sets a key to value in a Cache Name with optional expire value in MS */
|
|
||||||
set: (
|
|
||||||
cacheName: string,
|
|
||||||
key: string,
|
|
||||||
value: any,
|
|
||||||
expire?: number
|
|
||||||
) => Promise<any> | any
|
|
||||||
/** Deletes a key from a Cache */
|
|
||||||
delete: (cacheName: string, key: string) => Promise<boolean> | boolean
|
|
||||||
/** Gets array of all values in a Cache */
|
|
||||||
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined>
|
|
||||||
/** Entirely deletes a Cache */
|
|
||||||
deleteCache: (cacheName: string) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Default Cache Adapter for in-memory caching. */
|
|
||||||
export class DefaultCacheAdapter implements ICacheAdapter {
|
|
||||||
data: {
|
|
||||||
[name: string]: Collection<string, any>
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
async get(cacheName: string, key: string): Promise<undefined | any> {
|
|
||||||
const cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) return
|
|
||||||
return cache.get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(
|
|
||||||
cacheName: string,
|
|
||||||
key: string,
|
|
||||||
value: any,
|
|
||||||
expire?: number
|
|
||||||
): Promise<any> {
|
|
||||||
let cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) {
|
|
||||||
this.data[cacheName] = new Collection()
|
|
||||||
cache = this.data[cacheName]
|
|
||||||
}
|
|
||||||
cache.set(key, value)
|
|
||||||
if (expire !== undefined)
|
|
||||||
setTimeout(() => {
|
|
||||||
cache.delete(key)
|
|
||||||
}, expire)
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(cacheName: string, key: string): Promise<boolean> {
|
|
||||||
const cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) return false
|
|
||||||
return cache.delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
async array(cacheName: string): Promise<any[] | undefined> {
|
|
||||||
const cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) return
|
|
||||||
return cache.array()
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteCache(cacheName: string): Promise<boolean> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
return delete this.data[cacheName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Redis Cache Adapter for using Redis as a cache-provider. */
|
/** Redis Cache Adapter for using Redis as a cache-provider. */
|
||||||
export class RedisCacheAdapter implements ICacheAdapter {
|
export class RedisCacheAdapter implements ICacheAdapter {
|
462
src/client/client.ts
Normal file
462
src/client/client.ts
Normal file
|
@ -0,0 +1,462 @@
|
||||||
|
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||||
|
import type { User } from '../structures/user.ts'
|
||||||
|
import { GatewayIntents } from '../types/gateway.ts'
|
||||||
|
import { Gateway } from '../gateway/mod.ts'
|
||||||
|
import { RESTManager, RESTOptions, TokenType } from '../rest/mod.ts'
|
||||||
|
import { DefaultCacheAdapter, ICacheAdapter } from '../cache/mod.ts'
|
||||||
|
import { UsersManager } from '../managers/users.ts'
|
||||||
|
import { GuildManager } from '../managers/guilds.ts'
|
||||||
|
import { ChannelsManager } from '../managers/channels.ts'
|
||||||
|
import { ClientPresence } from '../structures/presence.ts'
|
||||||
|
import { EmojisManager } from '../managers/emojis.ts'
|
||||||
|
import { ActivityGame, ClientActivity } from '../types/presence.ts'
|
||||||
|
import type { Extension } from '../commands/extension.ts'
|
||||||
|
import { SlashClient } from '../interactions/slashClient.ts'
|
||||||
|
import { ShardManager } from './shard.ts'
|
||||||
|
import { Application } from '../structures/application.ts'
|
||||||
|
import { Invite } from '../structures/invite.ts'
|
||||||
|
import { INVITE } from '../types/endpoint.ts'
|
||||||
|
import type { ClientEvents } from '../gateway/handlers/mod.ts'
|
||||||
|
import type { Collector } from './collectors.ts'
|
||||||
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
import type { VoiceRegion } from '../types/voice.ts'
|
||||||
|
import { fetchAuto } from '../../deps.ts'
|
||||||
|
import type { DMChannel } from '../structures/dmChannel.ts'
|
||||||
|
import { Template } from '../structures/template.ts'
|
||||||
|
import { VoiceManager } from './voice.ts'
|
||||||
|
|
||||||
|
/** OS related properties sent with Gateway Identify */
|
||||||
|
export interface ClientProperties {
|
||||||
|
os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string
|
||||||
|
browser?: 'harmony' | string
|
||||||
|
device?: 'harmony' | string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Some Client Options to modify behaviour */
|
||||||
|
export interface ClientOptions {
|
||||||
|
/** ID of the Client/Application to initialize Slash Client REST */
|
||||||
|
id?: string
|
||||||
|
/** Token of the Bot/User */
|
||||||
|
token?: string
|
||||||
|
/** Gateway Intents */
|
||||||
|
intents?: Array<GatewayIntents | keyof typeof GatewayIntents>
|
||||||
|
/** Cache Adapter to use, defaults to Collections one */
|
||||||
|
cache?: ICacheAdapter
|
||||||
|
/** Force New Session and don't use cached Session (by persistent caching) */
|
||||||
|
forceNewSession?: boolean
|
||||||
|
/** Startup presence of client */
|
||||||
|
presence?: ClientPresence | ClientActivity | ActivityGame
|
||||||
|
/** Force all requests to Canary API */
|
||||||
|
canary?: boolean
|
||||||
|
/** Time till which Messages are to be cached, in MS. Default is 3600000 */
|
||||||
|
messageCacheLifetime?: number
|
||||||
|
/** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */
|
||||||
|
reactionCacheLifetime?: number
|
||||||
|
/** Whether to fetch Uncached Message of Reaction or not? */
|
||||||
|
fetchUncachedReactions?: boolean
|
||||||
|
/** Client Properties */
|
||||||
|
clientProperties?: ClientProperties
|
||||||
|
/** Enable/Disable Slash Commands Integration (enabled by default) */
|
||||||
|
enableSlash?: boolean
|
||||||
|
/** Disable taking token from env if not provided (token is taken from env if present by default) */
|
||||||
|
disableEnvToken?: boolean
|
||||||
|
/** Override REST Options */
|
||||||
|
restOptions?: RESTOptions
|
||||||
|
/** Whether to fetch Gateway info or not */
|
||||||
|
fetchGatewayInfo?: boolean
|
||||||
|
/** ADVANCED: Shard ID to launch on */
|
||||||
|
shard?: number
|
||||||
|
/** ADVACNED: Shard count. */
|
||||||
|
shardCount?: number | 'auto'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Harmony Client. Provides high-level interface over the REST and WebSocket API.
|
||||||
|
*/
|
||||||
|
export class Client extends HarmonyEventEmitter<ClientEvents> {
|
||||||
|
/** REST Manager - used to make all requests */
|
||||||
|
rest: RESTManager
|
||||||
|
/** User which Client logs in to, undefined until logs in */
|
||||||
|
user?: User
|
||||||
|
|
||||||
|
#token?: string
|
||||||
|
|
||||||
|
/** Token of the Bot/User */
|
||||||
|
get token(): string | undefined {
|
||||||
|
return this.#token
|
||||||
|
}
|
||||||
|
|
||||||
|
set token(val: string | undefined) {
|
||||||
|
this.#token = val
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cache Adapter */
|
||||||
|
get cache(): ICacheAdapter {
|
||||||
|
return this.#cache
|
||||||
|
}
|
||||||
|
|
||||||
|
set cache(val: ICacheAdapter) {
|
||||||
|
this.#cache = val
|
||||||
|
}
|
||||||
|
|
||||||
|
#cache: ICacheAdapter = new DefaultCacheAdapter()
|
||||||
|
|
||||||
|
/** Gateway Intents */
|
||||||
|
intents?: GatewayIntents[]
|
||||||
|
/** Whether to force new session or not */
|
||||||
|
forceNewSession?: boolean
|
||||||
|
/** Time till messages to stay cached, in MS. */
|
||||||
|
messageCacheLifetime: number = 3600000
|
||||||
|
/** Time till messages to stay cached, in MS. */
|
||||||
|
reactionCacheLifetime: number = 3600000
|
||||||
|
/** Whether to fetch Uncached Message of Reaction or not? */
|
||||||
|
fetchUncachedReactions: boolean = false
|
||||||
|
|
||||||
|
/** Client Properties */
|
||||||
|
readonly clientProperties!: ClientProperties
|
||||||
|
|
||||||
|
/** Slash-Commands Management client */
|
||||||
|
slash: SlashClient
|
||||||
|
/** Whether to fetch Gateway info or not */
|
||||||
|
fetchGatewayInfo: boolean = true
|
||||||
|
|
||||||
|
/** Voice Connections Manager */
|
||||||
|
readonly voice = new VoiceManager(this)
|
||||||
|
|
||||||
|
/** Users Manager, containing all Users cached */
|
||||||
|
readonly users: UsersManager = new UsersManager(this)
|
||||||
|
/** Guilds Manager, providing cache & API interface to Guilds */
|
||||||
|
readonly guilds: GuildManager = new GuildManager(this)
|
||||||
|
/** Channels Manager, providing cache interface to Channels */
|
||||||
|
readonly channels: ChannelsManager = new ChannelsManager(this)
|
||||||
|
/** Channels Manager, providing cache interface to Channels */
|
||||||
|
readonly emojis: EmojisManager = new EmojisManager(this)
|
||||||
|
|
||||||
|
/** Last READY timestamp */
|
||||||
|
upSince?: Date
|
||||||
|
|
||||||
|
/** Client's presence. Startup one if set before connecting */
|
||||||
|
presence: ClientPresence = new ClientPresence()
|
||||||
|
|
||||||
|
_id?: string
|
||||||
|
|
||||||
|
/** Shard on which this Client is */
|
||||||
|
shard?: number
|
||||||
|
/** Shard Count */
|
||||||
|
shardCount: number | 'auto' = 'auto'
|
||||||
|
/** Shard Manager of this Client if Sharded */
|
||||||
|
shards: ShardManager
|
||||||
|
/** Collectors set */
|
||||||
|
collectors: Set<Collector> = new Set()
|
||||||
|
|
||||||
|
/** Since when is Client online (ready). */
|
||||||
|
get uptime(): number {
|
||||||
|
if (this.upSince === undefined) return 0
|
||||||
|
else {
|
||||||
|
const dif = Date.now() - this.upSince.getTime()
|
||||||
|
if (dif < 0) return 0
|
||||||
|
else return dif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get Shard 0's Gateway */
|
||||||
|
get gateway(): Gateway {
|
||||||
|
return this.shards.list.get('0')!
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationID?: string
|
||||||
|
applicationFlags?: number
|
||||||
|
|
||||||
|
constructor(options: ClientOptions = {}) {
|
||||||
|
super()
|
||||||
|
this._id = options.id
|
||||||
|
this.token = options.token
|
||||||
|
this.intents = options.intents?.map((e) =>
|
||||||
|
typeof e === 'string' ? GatewayIntents[e] : e
|
||||||
|
)
|
||||||
|
this.shards = new ShardManager(this)
|
||||||
|
this.forceNewSession = options.forceNewSession
|
||||||
|
if (options.cache !== undefined) this.cache = options.cache
|
||||||
|
if (options.presence !== undefined)
|
||||||
|
this.presence =
|
||||||
|
options.presence instanceof ClientPresence
|
||||||
|
? options.presence
|
||||||
|
: new ClientPresence(options.presence)
|
||||||
|
if (options.messageCacheLifetime !== undefined)
|
||||||
|
this.messageCacheLifetime = options.messageCacheLifetime
|
||||||
|
if (options.reactionCacheLifetime !== undefined)
|
||||||
|
this.reactionCacheLifetime = options.reactionCacheLifetime
|
||||||
|
if (options.fetchUncachedReactions === true)
|
||||||
|
this.fetchUncachedReactions = true
|
||||||
|
|
||||||
|
if (
|
||||||
|
(this as any)._decoratedEvents !== undefined &&
|
||||||
|
Object.keys((this as any)._decoratedEvents).length !== 0
|
||||||
|
) {
|
||||||
|
Object.entries((this as any)._decoratedEvents).forEach((entry) => {
|
||||||
|
this.on(entry[0] as keyof ClientEvents, (entry as any)[1].bind(this))
|
||||||
|
})
|
||||||
|
;(this as any)._decoratedEvents = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'clientProperties', {
|
||||||
|
value:
|
||||||
|
options.clientProperties === undefined
|
||||||
|
? {
|
||||||
|
os: Deno.build.os,
|
||||||
|
browser: 'harmony',
|
||||||
|
device: 'harmony'
|
||||||
|
}
|
||||||
|
: options.clientProperties,
|
||||||
|
enumerable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (options.shard !== undefined) this.shard = options.shard
|
||||||
|
if (options.shardCount !== undefined) this.shardCount = options.shardCount
|
||||||
|
|
||||||
|
this.fetchGatewayInfo = options.fetchGatewayInfo ?? true
|
||||||
|
|
||||||
|
if (this.token === undefined) {
|
||||||
|
try {
|
||||||
|
const token = Deno.env.get('DISCORD_TOKEN')
|
||||||
|
if (token !== undefined) {
|
||||||
|
this.token = token
|
||||||
|
this.debug('Info', 'Found token in ENV')
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restOptions: RESTOptions = {
|
||||||
|
token: () => this.token,
|
||||||
|
tokenType: TokenType.Bot,
|
||||||
|
canary: options.canary,
|
||||||
|
client: this
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.restOptions !== undefined)
|
||||||
|
Object.assign(restOptions, options.restOptions)
|
||||||
|
this.rest = new RESTManager(restOptions)
|
||||||
|
|
||||||
|
this.slash = new SlashClient({
|
||||||
|
id: () => this.getEstimatedID(),
|
||||||
|
client: this,
|
||||||
|
enabled: options.enableSlash
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets Cache Adapter
|
||||||
|
*
|
||||||
|
* Should NOT be set after bot is already logged in or using current cache.
|
||||||
|
* Please look into using `cache` option.
|
||||||
|
*/
|
||||||
|
setAdapter(adapter: ICacheAdapter): Client {
|
||||||
|
this.cache = adapter
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Changes Presence of Client */
|
||||||
|
setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void {
|
||||||
|
if (presence instanceof ClientPresence) {
|
||||||
|
this.presence = presence
|
||||||
|
} else this.presence = new ClientPresence(presence)
|
||||||
|
this.gateway?.sendPresence(this.presence.create())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emits debug event */
|
||||||
|
debug(tag: string, msg: string): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.emit('debug', `[${tag}] ${msg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
getEstimatedID(): string {
|
||||||
|
if (this.user !== undefined) return this.user.id
|
||||||
|
else if (this.token !== undefined) {
|
||||||
|
try {
|
||||||
|
return atob(this.token.split('.')[0])
|
||||||
|
} catch (e) {
|
||||||
|
return this._id ?? 'unknown'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this._id ?? 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch Application of the Client */
|
||||||
|
async fetchApplication(): Promise<Application> {
|
||||||
|
const app = await this.rest.api.oauth2.applications['@me'].get()
|
||||||
|
return new Application(this, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch an Invite */
|
||||||
|
async fetchInvite(id: string): Promise<Invite> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
this.rest
|
||||||
|
.get(INVITE(id))
|
||||||
|
.then((data) => {
|
||||||
|
resolve(new Invite(this, data))
|
||||||
|
})
|
||||||
|
.catch((e) => reject(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used for connecting to discord.
|
||||||
|
* @param token Your token. This is required if not given in ClientOptions.
|
||||||
|
* @param intents Gateway intents in array. This is required if not given in ClientOptions.
|
||||||
|
*/
|
||||||
|
async connect(
|
||||||
|
token?: string,
|
||||||
|
intents?: Array<GatewayIntents | keyof typeof GatewayIntents>
|
||||||
|
): Promise<Client> {
|
||||||
|
token ??= this.token
|
||||||
|
if (token === undefined) throw new Error('No Token Provided')
|
||||||
|
this.token = token
|
||||||
|
if (intents !== undefined && this.intents !== undefined) {
|
||||||
|
this.debug(
|
||||||
|
'client',
|
||||||
|
'Intents were set in both client and connect function. Using the one in the connect function...'
|
||||||
|
)
|
||||||
|
} else if (intents === undefined && this.intents !== undefined) {
|
||||||
|
intents = this.intents
|
||||||
|
} else if (intents !== undefined && this.intents === undefined) {
|
||||||
|
this.intents = intents.map((e) =>
|
||||||
|
typeof e === 'string' ? GatewayIntents[e] : e
|
||||||
|
)
|
||||||
|
} else throw new Error('No Gateway Intents were provided')
|
||||||
|
|
||||||
|
this.rest.token = token
|
||||||
|
if (this.shard !== undefined) {
|
||||||
|
if (typeof this.shardCount === 'number')
|
||||||
|
this.shards.cachedShardCount = this.shardCount
|
||||||
|
await this.shards.launch(this.shard)
|
||||||
|
} else await this.shards.connect()
|
||||||
|
return this.waitFor('ready', () => true).then(() => this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Destroy the Gateway connection */
|
||||||
|
async destroy(): Promise<Client> {
|
||||||
|
this.gateway.initialized = false
|
||||||
|
this.gateway.sequenceID = undefined
|
||||||
|
this.gateway.sessionID = undefined
|
||||||
|
await this.gateway.cache.delete('seq')
|
||||||
|
await this.gateway.cache.delete('session_id')
|
||||||
|
this.gateway.close()
|
||||||
|
this.user = undefined
|
||||||
|
this.upSince = undefined
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attempt to Close current Gateway connection and Resume */
|
||||||
|
async reconnect(): Promise<Client> {
|
||||||
|
this.gateway.close()
|
||||||
|
this.gateway.initWebsocket()
|
||||||
|
return this.waitFor('ready', () => true).then(() => this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a new Collector */
|
||||||
|
addCollector(collector: Collector): boolean {
|
||||||
|
if (this.collectors.has(collector)) return false
|
||||||
|
else {
|
||||||
|
this.collectors.add(collector)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a Collector */
|
||||||
|
removeCollector(collector: Collector): boolean {
|
||||||
|
if (!this.collectors.has(collector)) return false
|
||||||
|
else {
|
||||||
|
this.collectors.delete(collector)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> {
|
||||||
|
const collectors: Collector[] = []
|
||||||
|
for (const collector of this.collectors.values()) {
|
||||||
|
if (collector.event === event) collectors.push(collector)
|
||||||
|
}
|
||||||
|
if (collectors.length !== 0) {
|
||||||
|
this.collectors.forEach((collector) => collector._fire(...args))
|
||||||
|
}
|
||||||
|
// TODO(DjDeveloperr): Fix this ts-ignore
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
|
||||||
|
// @ts-ignore
|
||||||
|
return super.emit(event, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns an array of voice region objects that can be used when creating servers. */
|
||||||
|
async fetchVoiceRegions(): Promise<VoiceRegion[]> {
|
||||||
|
return this.rest.api.voice.regions.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modify current (Client) User. */
|
||||||
|
async editUser(data: {
|
||||||
|
username?: string
|
||||||
|
avatar?: string
|
||||||
|
}): Promise<Client> {
|
||||||
|
if (data.username === undefined && data.avatar === undefined)
|
||||||
|
throw new Error(
|
||||||
|
'Either username or avatar or both must be specified to edit'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (data.avatar?.startsWith('http') === true) {
|
||||||
|
data.avatar = await fetchAuto(data.avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.rest.api.users['@me'].patch({
|
||||||
|
username: data.username,
|
||||||
|
avatar: data.avatar
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Change Username of the Client User */
|
||||||
|
async setUsername(username: string): Promise<Client> {
|
||||||
|
return await this.editUser({ username })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Change Avatar of the Client User */
|
||||||
|
async setAvatar(avatar: string): Promise<Client> {
|
||||||
|
return await this.editUser({ avatar })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a DM Channel with a User */
|
||||||
|
async createDM(user: User | string): Promise<DMChannel> {
|
||||||
|
const id = typeof user === 'object' ? user.id : user
|
||||||
|
const dmPayload = await this.rest.api.users['@me'].channels.post({
|
||||||
|
recipient_id: id
|
||||||
|
})
|
||||||
|
await this.channels.set(dmPayload.id, dmPayload)
|
||||||
|
return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a template object for the given code. */
|
||||||
|
async fetchTemplate(code: string): Promise<Template> {
|
||||||
|
const payload = await this.rest.api.guilds.templates[code].get()
|
||||||
|
return new Template(this, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Event decorator to create an Event handler from function */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
export function event(name?: keyof ClientEvents) {
|
||||||
|
return function (
|
||||||
|
client: Client | Extension,
|
||||||
|
prop: keyof ClientEvents | string
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
const c = client as any
|
||||||
|
const listener = ((client as unknown) as {
|
||||||
|
[name in keyof ClientEvents]: (...args: ClientEvents[name]) => any
|
||||||
|
})[(prop as unknown) as keyof ClientEvents]
|
||||||
|
if (typeof listener !== 'function')
|
||||||
|
throw new Error('@event decorator requires a function')
|
||||||
|
|
||||||
|
if (c._decoratedEvents === undefined) c._decoratedEvents = {}
|
||||||
|
const key = name === undefined ? prop : name
|
||||||
|
|
||||||
|
c._decoratedEvents[key] = listener
|
||||||
|
}
|
||||||
|
}
|
170
src/client/collectors.ts
Normal file
170
src/client/collectors.ts
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import { Collection } from '../utils/collection.ts'
|
||||||
|
import type { Client } from '../client/client.ts'
|
||||||
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
|
||||||
|
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
|
||||||
|
|
||||||
|
export interface CollectorOptions {
|
||||||
|
/** Event name to listen for */
|
||||||
|
event: string
|
||||||
|
/** Optionally Client object for deinitOnEnd functionality */
|
||||||
|
client?: Client
|
||||||
|
/** Filter function */
|
||||||
|
filter?: CollectorFilter
|
||||||
|
/** Max entries to collect */
|
||||||
|
max?: number
|
||||||
|
/** Whether or not to de-initialize on end */
|
||||||
|
deinitOnEnd?: boolean
|
||||||
|
/** Timeout to end the Collector if not fulfilled if any filter or max */
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type CollectorEvents = {
|
||||||
|
start: []
|
||||||
|
end: []
|
||||||
|
collect: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Collector extends HarmonyEventEmitter<CollectorEvents> {
|
||||||
|
client?: Client
|
||||||
|
private _started: boolean = false
|
||||||
|
event: string
|
||||||
|
filter: CollectorFilter = () => true
|
||||||
|
collected: Collection<string, any[]> = new Collection()
|
||||||
|
max?: number
|
||||||
|
deinitOnEnd: boolean = false
|
||||||
|
timeout?: number
|
||||||
|
private _timer?: number
|
||||||
|
|
||||||
|
get started(): boolean {
|
||||||
|
return this._started
|
||||||
|
}
|
||||||
|
|
||||||
|
set started(d: boolean) {
|
||||||
|
if (d !== this._started) {
|
||||||
|
this._started = d
|
||||||
|
if (d) this.emit('start')
|
||||||
|
else {
|
||||||
|
if (this.deinitOnEnd && this.client !== undefined)
|
||||||
|
this.deinit(this.client)
|
||||||
|
this.emit('end')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: CollectorOptions | string) {
|
||||||
|
super()
|
||||||
|
if (typeof options === 'string') this.event = options
|
||||||
|
else {
|
||||||
|
this.event = options.event
|
||||||
|
this.client = options.client
|
||||||
|
this.filter = options.filter ?? (() => true)
|
||||||
|
this.max = options.max
|
||||||
|
this.deinitOnEnd = options.deinitOnEnd ?? false
|
||||||
|
this.timeout = options.timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Start collecting */
|
||||||
|
collect(): Collector {
|
||||||
|
this.started = true
|
||||||
|
if (this.client !== undefined) this.init(this.client)
|
||||||
|
if (this._timer !== undefined) clearTimeout(this._timer)
|
||||||
|
if (this.timeout !== undefined) {
|
||||||
|
this._timer = setTimeout(() => {
|
||||||
|
this.end()
|
||||||
|
}, this.timeout)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** End collecting */
|
||||||
|
end(): Collector {
|
||||||
|
this.started = false
|
||||||
|
if (this._timer !== undefined) clearTimeout(this._timer)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset collector and start again */
|
||||||
|
reset(): Collector {
|
||||||
|
this.collected = new Collection()
|
||||||
|
this.collect()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init the Collector on Client */
|
||||||
|
init(client: Client): Collector {
|
||||||
|
this.client = client
|
||||||
|
client.addCollector(this)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** De initialize the Collector i.e. remove cleanly */
|
||||||
|
deinit(client: Client): Collector {
|
||||||
|
client.removeCollector(this)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks we may want to perform on an extended version of Collector */
|
||||||
|
protected check(..._args: any[]): boolean | Promise<boolean> {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fire the Collector */
|
||||||
|
async _fire(...args: any[]): Promise<void> {
|
||||||
|
if (!this.started) return
|
||||||
|
const check = await this.check(...args)
|
||||||
|
if (!check) return
|
||||||
|
const filter = await this.filter(...args)
|
||||||
|
if (!filter) return
|
||||||
|
this.collected.set((Number(this.collected.size) + 1).toString(), args)
|
||||||
|
this.emit('collect', ...args)
|
||||||
|
if (
|
||||||
|
this.max !== undefined &&
|
||||||
|
// linter: send help
|
||||||
|
this.max < Number(this.collected.size) + 1
|
||||||
|
) {
|
||||||
|
this.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set filter of the Collector */
|
||||||
|
when(filter: CollectorFilter): Collector {
|
||||||
|
this.filter = filter
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a new listener for 'collect' event */
|
||||||
|
each(handler: CallableFunction): Collector {
|
||||||
|
this.on('collect', () => handler())
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a Promise resolved when Collector ends or a timeout occurs */
|
||||||
|
async wait(timeout?: number): Promise<Collector> {
|
||||||
|
if (timeout === undefined) timeout = this.timeout ?? 0
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!timeout)
|
||||||
|
throw new Error(
|
||||||
|
'Timeout is required parameter if not given in CollectorOptions'
|
||||||
|
)
|
||||||
|
|
||||||
|
let done = false
|
||||||
|
const onend = (): void => {
|
||||||
|
done = true
|
||||||
|
this.off('end', onend)
|
||||||
|
resolve(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on('end', onend)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!done) {
|
||||||
|
this.off('end', onend)
|
||||||
|
reject(new Error('Timeout'))
|
||||||
|
}
|
||||||
|
}, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
3
src/client/mod.ts
Normal file
3
src/client/mod.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './client.ts'
|
||||||
|
export * from './collectors.ts'
|
||||||
|
export * from './shard.ts'
|
133
src/client/shard.ts
Normal file
133
src/client/shard.ts
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import { Collection } from '../utils/collection.ts'
|
||||||
|
import type { Client } from './client.ts'
|
||||||
|
import { RESTManager } from '../rest/mod.ts'
|
||||||
|
import { Gateway } from '../gateway/mod.ts'
|
||||||
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
import { GatewayEvents } from '../types/gateway.ts'
|
||||||
|
import { delay } from '../utils/delay.ts'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type ShardManagerEvents = {
|
||||||
|
launch: [number]
|
||||||
|
shardReady: [number]
|
||||||
|
shardDisconnect: [number, number | undefined, string | undefined]
|
||||||
|
shardError: [number, Error, ErrorEvent]
|
||||||
|
shardResume: [number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
|
||||||
|
list: Collection<string, Gateway> = new Collection()
|
||||||
|
client: Client
|
||||||
|
cachedShardCount?: number
|
||||||
|
queueProcessing: boolean = false
|
||||||
|
queue: CallableFunction[] = []
|
||||||
|
|
||||||
|
get rest(): RESTManager {
|
||||||
|
return this.client.rest
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
super()
|
||||||
|
this.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(msg: string): void {
|
||||||
|
this.client.debug('Shards', msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueIdentify(fn: CallableFunction): ShardManager {
|
||||||
|
this.queue.push(fn)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
if (!this.queueProcessing) this.processQueue()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processQueue(): Promise<void> {
|
||||||
|
if (this.queueProcessing || this.queue.length === 0) return
|
||||||
|
this.queueProcessing = true
|
||||||
|
const item = this.queue[0]
|
||||||
|
await item()
|
||||||
|
this.queue.shift()
|
||||||
|
await delay(5000)
|
||||||
|
this.queueProcessing = false
|
||||||
|
if (this.queue.length === 0) {
|
||||||
|
this.queueProcessing = false
|
||||||
|
} else {
|
||||||
|
await this.processQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShardCount(): Promise<number> {
|
||||||
|
let shardCount: number
|
||||||
|
if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount
|
||||||
|
else {
|
||||||
|
if (
|
||||||
|
this.client.shardCount === 'auto' &&
|
||||||
|
this.client.fetchGatewayInfo !== false
|
||||||
|
) {
|
||||||
|
this.debug('Fetch /gateway/bot...')
|
||||||
|
const info = await this.client.rest.api.gateway.bot.get()
|
||||||
|
this.debug(`Recommended Shards: ${info.shards}`)
|
||||||
|
this.debug('=== Session Limit Info ===')
|
||||||
|
this.debug(
|
||||||
|
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
|
||||||
|
)
|
||||||
|
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
|
||||||
|
shardCount = info.shards as number
|
||||||
|
} else
|
||||||
|
shardCount =
|
||||||
|
typeof this.client.shardCount === 'string'
|
||||||
|
? 1
|
||||||
|
: this.client.shardCount ?? 1
|
||||||
|
}
|
||||||
|
this.cachedShardCount = shardCount
|
||||||
|
return this.cachedShardCount
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Launches a new Shard */
|
||||||
|
async launch(id: number): Promise<ShardManager> {
|
||||||
|
if (this.list.has(id.toString()) === true)
|
||||||
|
throw new Error(`Shard ${id} already launched`)
|
||||||
|
|
||||||
|
this.debug(`Launching Shard: ${id}`)
|
||||||
|
const shardCount = await this.getShardCount()
|
||||||
|
|
||||||
|
const gw = new Gateway(this.client, [Number(id), shardCount])
|
||||||
|
this.list.set(id.toString(), gw)
|
||||||
|
|
||||||
|
gw.initWebsocket()
|
||||||
|
this.emit('launch', id)
|
||||||
|
|
||||||
|
gw.on(GatewayEvents.Ready, () => this.emit('shardReady', id))
|
||||||
|
gw.on('error', (err: Error, evt: ErrorEvent) =>
|
||||||
|
this.emit('shardError', id, err, evt)
|
||||||
|
)
|
||||||
|
gw.on(GatewayEvents.Resumed, () => this.emit('shardResume', id))
|
||||||
|
gw.on('close', (code: number, reason: string) =>
|
||||||
|
this.emit('shardDisconnect', id, code, reason)
|
||||||
|
)
|
||||||
|
|
||||||
|
return gw.waitFor(GatewayEvents.Ready, () => true).then(() => this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Launches all Shards */
|
||||||
|
async connect(): Promise<ShardManager> {
|
||||||
|
const shardCount = await this.getShardCount()
|
||||||
|
this.client.shardCount = shardCount
|
||||||
|
this.debug(`Launching ${shardCount} shard${shardCount === 1 ? '' : 's'}...`)
|
||||||
|
const startTime = Date.now()
|
||||||
|
for (let i = 0; i < shardCount; i++) {
|
||||||
|
await this.launch(i)
|
||||||
|
}
|
||||||
|
const endTime = Date.now()
|
||||||
|
const diff = endTime - startTime
|
||||||
|
this.debug(
|
||||||
|
`Launched ${shardCount} shards! Time taken: ${Math.floor(diff / 1000)}s`
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: number): Gateway | undefined {
|
||||||
|
return this.list.get(id.toString())
|
||||||
|
}
|
||||||
|
}
|
116
src/client/voice.ts
Normal file
116
src/client/voice.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import type { VoiceServerUpdateData } from '../gateway/handlers/mod.ts'
|
||||||
|
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
||||||
|
import type { VoiceStateOptions } from '../gateway/mod.ts'
|
||||||
|
import { VoiceState } from '../structures/voiceState.ts'
|
||||||
|
import { ChannelTypes } from '../types/channel.ts'
|
||||||
|
import type { Guild } from '../structures/guild.ts'
|
||||||
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
import type { Client } from './client.ts'
|
||||||
|
|
||||||
|
export interface VoiceServerData extends VoiceServerUpdateData {
|
||||||
|
userID: string
|
||||||
|
sessionID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VoiceChannelJoinOptions extends VoiceStateOptions {
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VoiceManager extends HarmonyEventEmitter<{
|
||||||
|
voiceStateUpdate: [VoiceState]
|
||||||
|
}> {
|
||||||
|
#pending = new Map<string, [number, CallableFunction]>()
|
||||||
|
|
||||||
|
readonly client!: Client
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
super()
|
||||||
|
Object.defineProperty(this, 'client', {
|
||||||
|
value: client,
|
||||||
|
enumerable: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async join(
|
||||||
|
channel: string | VoiceChannel,
|
||||||
|
options?: VoiceChannelJoinOptions
|
||||||
|
): Promise<VoiceServerData> {
|
||||||
|
const id = typeof channel === 'string' ? channel : channel.id
|
||||||
|
const chan = await this.client.channels.get<VoiceChannel>(id)
|
||||||
|
if (chan === undefined) throw new Error('Voice Channel not cached')
|
||||||
|
if (
|
||||||
|
chan.type !== ChannelTypes.GUILD_VOICE &&
|
||||||
|
chan.type !== ChannelTypes.GUILD_STAGE_VOICE
|
||||||
|
)
|
||||||
|
throw new Error('Cannot join non-voice channel')
|
||||||
|
|
||||||
|
const pending = this.#pending.get(chan.guild.id)
|
||||||
|
if (pending !== undefined) {
|
||||||
|
clearTimeout(pending[0])
|
||||||
|
pending[1](new Error('Voice Connection timed out'))
|
||||||
|
this.#pending.delete(chan.guild.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
let vcdata: VoiceServerData
|
||||||
|
let done = 0
|
||||||
|
|
||||||
|
const onVoiceStateAdd = (state: VoiceState): void => {
|
||||||
|
if (state.user.id !== this.client.user?.id) return
|
||||||
|
if (state.channel?.id !== id) return
|
||||||
|
this.client.off('voiceStateAdd', onVoiceStateAdd)
|
||||||
|
done++
|
||||||
|
vcdata = vcdata ?? {}
|
||||||
|
vcdata.sessionID = state.sessionID
|
||||||
|
vcdata.userID = state.user.id
|
||||||
|
if (done >= 2) {
|
||||||
|
this.#pending.delete(chan.guild.id)
|
||||||
|
resolve(vcdata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => {
|
||||||
|
if (data.guild.id !== chan.guild.id) return
|
||||||
|
vcdata = Object.assign(vcdata ?? {}, data)
|
||||||
|
this.client.off('voiceServerUpdate', onVoiceServerUpdate)
|
||||||
|
done++
|
||||||
|
if (done >= 2) {
|
||||||
|
this.#pending.delete(chan.guild.id)
|
||||||
|
resolve(vcdata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.shards
|
||||||
|
.get(chan.guild.shardID)!
|
||||||
|
.updateVoiceState(chan.guild.id, chan.id, options)
|
||||||
|
|
||||||
|
this.on('voiceStateUpdate', onVoiceStateAdd)
|
||||||
|
this.client.on('voiceServerUpdate', onVoiceServerUpdate)
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (done < 2) {
|
||||||
|
this.client.off('voiceServerUpdate', onVoiceServerUpdate)
|
||||||
|
this.client.off('voiceStateAdd', onVoiceStateAdd)
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
"Connection timed out - couldn't connect to Voice Channel"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, options?.timeout ?? 1000 * 30)
|
||||||
|
|
||||||
|
this.#pending.set(chan.guild.id, [timer, reject])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async leave(guildOrID: Guild | string): Promise<void> {
|
||||||
|
const id = typeof guildOrID === 'string' ? guildOrID : guildOrID.id
|
||||||
|
const guild = await this.client.guilds.get(id)
|
||||||
|
if (guild === undefined) throw new Error('Guild not cached')
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||||
|
const vcs = await guild.voiceStates.get(this.client.user?.id!)
|
||||||
|
if (vcs === undefined) throw new Error('Not in Voice Channel')
|
||||||
|
|
||||||
|
this.client.shards.get(guild.shardID)!.updateVoiceState(guild, undefined)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { Message } from '../structures/message.ts'
|
import type { Message } from '../structures/message.ts'
|
||||||
import { GuildTextChannel } from '../structures/textChannel.ts'
|
import type { GuildTextBasedChannel } from '../structures/guildTextChannel.ts'
|
||||||
import { awaitSync } from '../utils/mixedPromise.ts'
|
import { Client, ClientOptions } from '../client/mod.ts'
|
||||||
import { Client, ClientOptions } from './client.ts'
|
|
||||||
import {
|
import {
|
||||||
CategoriesManager,
|
CategoriesManager,
|
||||||
Command,
|
Command,
|
||||||
|
@ -10,7 +9,8 @@ import {
|
||||||
CommandsManager,
|
CommandsManager,
|
||||||
parseCommand
|
parseCommand
|
||||||
} from './command.ts'
|
} from './command.ts'
|
||||||
import { Extension, ExtensionsManager } from './extensions.ts'
|
import { parseArgs } from '../utils/command.ts'
|
||||||
|
import { Extension, ExtensionsManager } from './extension.ts'
|
||||||
|
|
||||||
type PrefixReturnType = string | string[] | Promise<string | string[]>
|
type PrefixReturnType = string | string[] | Promise<string | string[]>
|
||||||
|
|
||||||
|
@ -44,6 +44,11 @@ export interface CommandClientOptions extends ClientOptions {
|
||||||
caseSensitive?: boolean
|
caseSensitive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Harmony Client with extended functionality for Message based Commands parsing and handling.
|
||||||
|
*
|
||||||
|
* See SlashClient (`Client#slash`) for more info about Slash Commands.
|
||||||
|
*/
|
||||||
export class CommandClient extends Client implements CommandClientOptions {
|
export class CommandClient extends Client implements CommandClientOptions {
|
||||||
prefix: string | string[]
|
prefix: string | string[]
|
||||||
mentionPrefix: boolean
|
mentionPrefix: boolean
|
||||||
|
@ -66,8 +71,6 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
commands: CommandsManager = new CommandsManager(this)
|
commands: CommandsManager = new CommandsManager(this)
|
||||||
categories: CategoriesManager = new CategoriesManager(this)
|
categories: CategoriesManager = new CategoriesManager(this)
|
||||||
|
|
||||||
_decoratedCommands?: { [name: string]: Command }
|
|
||||||
|
|
||||||
constructor(options: CommandClientOptions) {
|
constructor(options: CommandClientOptions) {
|
||||||
super(options)
|
super(options)
|
||||||
this.prefix = options.prefix
|
this.prefix = options.prefix
|
||||||
|
@ -112,11 +115,12 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
this.caseSensitive =
|
this.caseSensitive =
|
||||||
options.caseSensitive === undefined ? false : options.caseSensitive
|
options.caseSensitive === undefined ? false : options.caseSensitive
|
||||||
|
|
||||||
if (this._decoratedCommands !== undefined) {
|
const self = this as any
|
||||||
Object.values(this._decoratedCommands).forEach((entry) => {
|
if (self._decoratedCommands !== undefined) {
|
||||||
|
Object.values(self._decoratedCommands).forEach((entry: any) => {
|
||||||
this.commands.add(entry)
|
this.commands.add(entry)
|
||||||
})
|
})
|
||||||
this._decoratedCommands = undefined
|
self._decoratedCommands = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
this.on(
|
this.on(
|
||||||
|
@ -129,35 +133,29 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
async processMessage(msg: Message): Promise<any> {
|
async processMessage(msg: Message): Promise<any> {
|
||||||
if (!this.allowBots && msg.author.bot === true) return
|
if (!this.allowBots && msg.author.bot === true) return
|
||||||
|
|
||||||
const isUserBlacklisted = await awaitSync(
|
const isUserBlacklisted = await this.isUserBlacklisted(msg.author.id)
|
||||||
this.isUserBlacklisted(msg.author.id)
|
if (isUserBlacklisted) return
|
||||||
)
|
|
||||||
if (isUserBlacklisted === true) return
|
|
||||||
|
|
||||||
const isChannelBlacklisted = await awaitSync(
|
const isChannelBlacklisted = await this.isChannelBlacklisted(msg.channel.id)
|
||||||
this.isChannelBlacklisted(msg.channel.id)
|
if (isChannelBlacklisted) return
|
||||||
)
|
|
||||||
if (isChannelBlacklisted === true) return
|
|
||||||
|
|
||||||
if (msg.guild !== undefined) {
|
if (msg.guild !== undefined) {
|
||||||
const isGuildBlacklisted = await awaitSync(
|
const isGuildBlacklisted = await this.isGuildBlacklisted(msg.guild.id)
|
||||||
this.isGuildBlacklisted(msg.guild.id)
|
if (isGuildBlacklisted) return
|
||||||
)
|
|
||||||
if (isGuildBlacklisted === true) return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let prefix: string | string[] = []
|
let prefix: string | string[] = []
|
||||||
if (typeof this.prefix === 'string') prefix = [...prefix, this.prefix]
|
if (typeof this.prefix === 'string') prefix = [...prefix, this.prefix]
|
||||||
else prefix = [...prefix, ...this.prefix]
|
else prefix = [...prefix, ...this.prefix]
|
||||||
|
|
||||||
const userPrefix = await awaitSync(this.getUserPrefix(msg.author.id))
|
const userPrefix = await this.getUserPrefix(msg.author.id)
|
||||||
if (userPrefix !== undefined) {
|
if (userPrefix !== undefined) {
|
||||||
if (typeof userPrefix === 'string') prefix = [...prefix, userPrefix]
|
if (typeof userPrefix === 'string') prefix = [...prefix, userPrefix]
|
||||||
else prefix = [...prefix, ...userPrefix]
|
else prefix = [...prefix, ...userPrefix]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.guild !== undefined) {
|
if (msg.guild !== undefined) {
|
||||||
const guildPrefix = await awaitSync(this.getGuildPrefix(msg.guild.id))
|
const guildPrefix = await this.getGuildPrefix(msg.guild.id)
|
||||||
if (guildPrefix !== undefined) {
|
if (guildPrefix !== undefined) {
|
||||||
if (typeof guildPrefix === 'string') prefix = [...prefix, guildPrefix]
|
if (typeof guildPrefix === 'string') prefix = [...prefix, guildPrefix]
|
||||||
else prefix = [...prefix, ...guildPrefix]
|
else prefix = [...prefix, ...guildPrefix]
|
||||||
|
@ -242,7 +240,7 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
client: this,
|
client: this,
|
||||||
name: parsed.name,
|
name: parsed.name,
|
||||||
prefix,
|
prefix,
|
||||||
args: parsed.args,
|
args: parseArgs(command.args, parsed.args),
|
||||||
argString: parsed.argString,
|
argString: parsed.argString,
|
||||||
message: msg,
|
message: msg,
|
||||||
author: msg.author,
|
author: msg.author,
|
||||||
|
@ -259,7 +257,7 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
: category.ownerOnly) === true &&
|
: category.ownerOnly) === true &&
|
||||||
!this.owners.includes(msg.author.id)
|
!this.owners.includes(msg.author.id)
|
||||||
)
|
)
|
||||||
return this.emit('commandOwnerOnly', ctx, command)
|
return this.emit('commandOwnerOnly', ctx)
|
||||||
|
|
||||||
// Checks if Command is only for Guild
|
// Checks if Command is only for Guild
|
||||||
if (
|
if (
|
||||||
|
@ -268,7 +266,7 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
: category.guildOnly) === true &&
|
: category.guildOnly) === true &&
|
||||||
msg.guild === undefined
|
msg.guild === undefined
|
||||||
)
|
)
|
||||||
return this.emit('commandGuildOnly', ctx, command)
|
return this.emit('commandGuildOnly', ctx)
|
||||||
|
|
||||||
// Checks if Command is only for DMs
|
// Checks if Command is only for DMs
|
||||||
if (
|
if (
|
||||||
|
@ -277,14 +275,14 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
: category.dmOnly) === true &&
|
: category.dmOnly) === true &&
|
||||||
msg.guild !== undefined
|
msg.guild !== undefined
|
||||||
)
|
)
|
||||||
return this.emit('commandDmOnly', ctx, command)
|
return this.emit('commandDmOnly', ctx)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
command.nsfw === true &&
|
command.nsfw === true &&
|
||||||
(msg.guild === undefined ||
|
(msg.guild === undefined ||
|
||||||
((msg.channel as unknown) as GuildTextChannel).nsfw !== true)
|
((msg.channel as unknown) as GuildTextBasedChannel).nsfw !== true)
|
||||||
)
|
)
|
||||||
return this.emit('commandNSFW', ctx, command)
|
return this.emit('commandNSFW', ctx)
|
||||||
|
|
||||||
const allPermissions =
|
const allPermissions =
|
||||||
command.permissions !== undefined
|
command.permissions !== undefined
|
||||||
|
@ -293,7 +291,8 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(command.botPermissions !== undefined ||
|
(command.botPermissions !== undefined ||
|
||||||
category?.permissions !== undefined) &&
|
category?.botPermissions !== undefined ||
|
||||||
|
allPermissions !== undefined) &&
|
||||||
msg.guild !== undefined
|
msg.guild !== undefined
|
||||||
) {
|
) {
|
||||||
// TODO: Check Overwrites too
|
// TODO: Check Overwrites too
|
||||||
|
@ -316,18 +315,14 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missing.length !== 0)
|
if (missing.length !== 0)
|
||||||
return this.emit(
|
return this.emit('commandBotMissingPermissions', ctx, missing)
|
||||||
'commandBotMissingPermissions',
|
|
||||||
ctx,
|
|
||||||
command,
|
|
||||||
missing
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(command.userPermissions !== undefined ||
|
(command.userPermissions !== undefined ||
|
||||||
category?.userPermissions !== undefined) &&
|
category?.userPermissions !== undefined ||
|
||||||
|
allPermissions !== undefined) &&
|
||||||
msg.guild !== undefined
|
msg.guild !== undefined
|
||||||
) {
|
) {
|
||||||
let permissions =
|
let permissions =
|
||||||
|
@ -349,51 +344,52 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missing.length !== 0)
|
if (missing.length !== 0)
|
||||||
return this.emit(
|
return this.emit('commandUserMissingPermissions', ctx, missing)
|
||||||
'commandUserMissingPermissions',
|
|
||||||
command,
|
|
||||||
missing,
|
|
||||||
ctx
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.args !== undefined) {
|
if (command.args !== undefined) {
|
||||||
if (typeof command.args === 'boolean' && parsed.args.length === 0)
|
if (typeof command.args === 'boolean' && parsed.args.length === 0)
|
||||||
return this.emit('commandMissingArgs', ctx, command)
|
return this.emit('commandMissingArgs', ctx)
|
||||||
else if (
|
else if (
|
||||||
typeof command.args === 'number' &&
|
typeof command.args === 'number' &&
|
||||||
parsed.args.length < command.args
|
parsed.args.length < command.args
|
||||||
)
|
)
|
||||||
this.emit('commandMissingArgs', ctx, command)
|
this.emit('commandMissingArgs', ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.emit('commandUsed', ctx, command)
|
this.emit('commandUsed', ctx)
|
||||||
|
|
||||||
const beforeExecute = await awaitSync(command.beforeExecute(ctx))
|
const beforeExecute = await command.beforeExecute(ctx)
|
||||||
if (beforeExecute === false) return
|
if (beforeExecute === false) return
|
||||||
|
|
||||||
const result = await awaitSync(command.execute(ctx))
|
const result = await command.execute(ctx)
|
||||||
command.afterExecute(ctx, result)
|
await command.afterExecute(ctx, result)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.emit('commandError', command, ctx, e)
|
try {
|
||||||
|
await command.onError(ctx, e)
|
||||||
|
} catch (e) {
|
||||||
|
this.emit('commandError', ctx, e)
|
||||||
|
}
|
||||||
|
this.emit('commandError', ctx, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command decorator. Decorates the function with optional metadata as a Command registered upon constructing class.
|
||||||
|
*/
|
||||||
export function command(options?: CommandOptions) {
|
export function command(options?: CommandOptions) {
|
||||||
return function (target: CommandClient | Extension, name: string) {
|
return function (target: CommandClient | Extension, name: string) {
|
||||||
if (target._decoratedCommands === undefined) target._decoratedCommands = {}
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
const c = target as any
|
||||||
|
if (c._decoratedCommands === undefined) c._decoratedCommands = {}
|
||||||
|
|
||||||
const prop = ((target as unknown) as {
|
const prop = c[name]
|
||||||
[name: string]: (ctx: CommandContext) => any
|
|
||||||
})[name]
|
|
||||||
|
|
||||||
if (prop instanceof Command) {
|
if (typeof prop !== 'function')
|
||||||
target._decoratedCommands[prop.name] = prop
|
throw new Error('@command decorator can only be used on class methods')
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = new Command()
|
const command = new Command()
|
||||||
|
|
||||||
|
@ -404,6 +400,6 @@ export function command(options?: CommandOptions) {
|
||||||
|
|
||||||
if (target instanceof Extension) command.extension = target
|
if (target instanceof Extension) command.extension = target
|
||||||
|
|
||||||
target._decoratedCommands[command.name] = command
|
c._decoratedCommands[command.name] = command
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Message } from '../structures/message.ts'
|
import type { Message } from '../structures/message.ts'
|
||||||
import { TextChannel } from '../structures/textChannel.ts'
|
import type { TextChannel } from '../structures/textChannel.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import { CommandClient } from './commandClient.ts'
|
import type { CommandClient } from './client.ts'
|
||||||
import { Extension } from './extensions.ts'
|
import type { Extension } from './extension.ts'
|
||||||
import { parse } from '../../deps.ts'
|
import { join, walk } from '../../deps.ts'
|
||||||
|
import type { Args } from '../utils/command.ts'
|
||||||
export interface CommandContext {
|
export interface CommandContext {
|
||||||
/** The Client object */
|
/** The Client object */
|
||||||
client: CommandClient
|
client: CommandClient
|
||||||
|
@ -23,7 +23,7 @@ export interface CommandContext {
|
||||||
/** Name of Command which was used */
|
/** Name of Command which was used */
|
||||||
name: string
|
name: string
|
||||||
/** Array of Arguments used with Command */
|
/** Array of Arguments used with Command */
|
||||||
args: string[]
|
args: Record<string, unknown> | null
|
||||||
/** Complete Raw String of Arguments */
|
/** Complete Raw String of Arguments */
|
||||||
argString: string
|
argString: string
|
||||||
/** Guild which the command has called */
|
/** Guild which the command has called */
|
||||||
|
@ -46,7 +46,7 @@ export interface CommandOptions {
|
||||||
/** Usage Example of Command, only Arguments (without Prefix and Name) */
|
/** Usage Example of Command, only Arguments (without Prefix and Name) */
|
||||||
examples?: string | string[]
|
examples?: string | string[]
|
||||||
/** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */
|
/** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */
|
||||||
args?: number | boolean | string[]
|
args?: Args[]
|
||||||
/** Permissions(s) required by both User and Bot in order to use Command */
|
/** Permissions(s) required by both User and Bot in order to use Command */
|
||||||
permissions?: string | string[]
|
permissions?: string | string[]
|
||||||
/** Permission(s) required for using Command */
|
/** Permission(s) required for using Command */
|
||||||
|
@ -72,6 +72,8 @@ export interface CommandOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Command implements CommandOptions {
|
export class Command implements CommandOptions {
|
||||||
|
static meta?: CommandOptions
|
||||||
|
|
||||||
name: string = ''
|
name: string = ''
|
||||||
description?: string
|
description?: string
|
||||||
category?: string
|
category?: string
|
||||||
|
@ -79,7 +81,7 @@ export class Command implements CommandOptions {
|
||||||
extension?: Extension
|
extension?: Extension
|
||||||
usage?: string | string[]
|
usage?: string | string[]
|
||||||
examples?: string | string[]
|
examples?: string | string[]
|
||||||
args?: number | boolean | string[]
|
args?: Args[]
|
||||||
permissions?: string | string[]
|
permissions?: string | string[]
|
||||||
userPermissions?: string | string[]
|
userPermissions?: string | string[]
|
||||||
botPermissions?: string | string[]
|
botPermissions?: string | string[]
|
||||||
|
@ -92,6 +94,9 @@ export class Command implements CommandOptions {
|
||||||
dmOnly?: boolean
|
dmOnly?: boolean
|
||||||
ownerOnly?: boolean
|
ownerOnly?: boolean
|
||||||
|
|
||||||
|
/** Method called when the command errors */
|
||||||
|
onError(ctx: CommandContext, error: Error): any {}
|
||||||
|
|
||||||
/** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */
|
/** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */
|
||||||
beforeExecute(ctx: CommandContext): boolean | Promise<boolean> {
|
beforeExecute(ctx: CommandContext): boolean | Promise<boolean> {
|
||||||
return true
|
return true
|
||||||
|
@ -284,13 +289,107 @@ export class CommandBuilder extends Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CommandsLoader {
|
||||||
|
client: CommandClient
|
||||||
|
#importSeq: { [name: string]: number } = {}
|
||||||
|
|
||||||
|
constructor(client: CommandClient) {
|
||||||
|
this.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a Command from file.
|
||||||
|
*
|
||||||
|
* NOTE: Relative paths resolve from cwd
|
||||||
|
*
|
||||||
|
* @param filePath Path of Command file.
|
||||||
|
* @param exportName Export name. Default is the "default" export.
|
||||||
|
*/
|
||||||
|
async load(
|
||||||
|
filePath: string,
|
||||||
|
exportName: string = 'default',
|
||||||
|
onlyRead?: boolean
|
||||||
|
): Promise<Command> {
|
||||||
|
const stat = await Deno.stat(filePath).catch(() => undefined)
|
||||||
|
if (stat === undefined || stat.isFile !== true)
|
||||||
|
throw new Error(`File not found on path ${filePath}`)
|
||||||
|
|
||||||
|
let seq: number | undefined
|
||||||
|
|
||||||
|
if (this.#importSeq[filePath] !== undefined) seq = this.#importSeq[filePath]
|
||||||
|
const mod = await import(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||||
|
'file:///' +
|
||||||
|
join(Deno.cwd(), filePath) +
|
||||||
|
(seq === undefined ? '' : `#${seq}`)
|
||||||
|
)
|
||||||
|
if (this.#importSeq[filePath] === undefined) this.#importSeq[filePath] = 0
|
||||||
|
else this.#importSeq[filePath]++
|
||||||
|
|
||||||
|
const Cmd = mod[exportName]
|
||||||
|
if (Cmd === undefined)
|
||||||
|
throw new Error(`Command not exported as ${exportName} from ${filePath}`)
|
||||||
|
|
||||||
|
let cmd: Command
|
||||||
|
try {
|
||||||
|
if (Cmd instanceof Command) cmd = Cmd
|
||||||
|
else cmd = new Cmd()
|
||||||
|
if (!(cmd instanceof Command)) throw new Error('failed')
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Failed to load Command from ${filePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlyRead !== true) this.client.commands.add(cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load commands from a Directory.
|
||||||
|
*
|
||||||
|
* NOTE: Relative paths resolve from cwd
|
||||||
|
*
|
||||||
|
* @param path Path of the directory.
|
||||||
|
* @param options Options to configure loading.
|
||||||
|
*/
|
||||||
|
async loadDirectory(
|
||||||
|
path: string,
|
||||||
|
options?: {
|
||||||
|
recursive?: boolean
|
||||||
|
exportName?: string
|
||||||
|
maxDepth?: number
|
||||||
|
exts?: string[]
|
||||||
|
onlyRead?: boolean
|
||||||
|
}
|
||||||
|
): Promise<Command[]> {
|
||||||
|
const commands: Command[] = []
|
||||||
|
|
||||||
|
for await (const entry of walk(path, {
|
||||||
|
maxDepth: options?.maxDepth,
|
||||||
|
exts: options?.exts,
|
||||||
|
includeDirs: false
|
||||||
|
})) {
|
||||||
|
if (entry.isFile !== true) continue
|
||||||
|
const cmd = await this.load(
|
||||||
|
entry.path,
|
||||||
|
options?.exportName,
|
||||||
|
options?.onlyRead
|
||||||
|
)
|
||||||
|
commands.push(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class CommandsManager {
|
export class CommandsManager {
|
||||||
client: CommandClient
|
client: CommandClient
|
||||||
list: Collection<string, Command> = new Collection()
|
list: Collection<string, Command> = new Collection()
|
||||||
disabled: Set<string> = new Set()
|
disabled: Set<string> = new Set()
|
||||||
|
loader: CommandsLoader
|
||||||
|
|
||||||
constructor(client: CommandClient) {
|
constructor(client: CommandClient) {
|
||||||
this.client = client
|
this.client = client
|
||||||
|
this.loader = new CommandsLoader(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Number of loaded Commands */
|
/** Number of loaded Commands */
|
||||||
|
@ -393,12 +492,16 @@ export class CommandsManager {
|
||||||
|
|
||||||
/** Add a Command */
|
/** Add a Command */
|
||||||
add(cmd: Command | typeof Command): boolean {
|
add(cmd: Command | typeof Command): boolean {
|
||||||
// eslint-disable-next-line new-cap
|
if (!(cmd instanceof Command)) {
|
||||||
if (!(cmd instanceof Command)) cmd = new cmd()
|
const CmdClass = cmd
|
||||||
|
cmd = new CmdClass()
|
||||||
|
Object.assign(cmd, CmdClass.meta ?? {})
|
||||||
|
}
|
||||||
if (this.exists(cmd, cmd.extension?.subPrefix))
|
if (this.exists(cmd, cmd.extension?.subPrefix))
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to add Command '${cmd.toString()}' with name/alias already exists.`
|
`Failed to add Command '${cmd.toString()}' with name/alias already exists.`
|
||||||
)
|
)
|
||||||
|
if (cmd.name === '') throw new Error('Command has no name')
|
||||||
this.list.set(
|
this.list.set(
|
||||||
`${cmd.name}-${
|
`${cmd.name}-${
|
||||||
this.list.filter((e) =>
|
this.list.filter((e) =>
|
||||||
|
@ -506,7 +609,7 @@ export const parseCommand = (
|
||||||
): ParsedCommand | undefined => {
|
): ParsedCommand | undefined => {
|
||||||
let content = msg.content.slice(prefix.length)
|
let content = msg.content.slice(prefix.length)
|
||||||
if (client.spacesAfterPrefix === true) content = content.trim()
|
if (client.spacesAfterPrefix === true) content = content.trim()
|
||||||
const args = parse(content)._.map((e) => e.toString())
|
const args = content.split(' ')
|
||||||
|
|
||||||
const name = args.shift()
|
const name = args.shift()
|
||||||
if (name === undefined) return
|
if (name === undefined) return
|
|
@ -1,6 +1,7 @@
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import { Command } from './command.ts'
|
import { Command } from './command.ts'
|
||||||
import { CommandClient } from './commandClient.ts'
|
import { CommandClient } from './client.ts'
|
||||||
|
import type { ClientEvents } from '../gateway/handlers/mod.ts'
|
||||||
|
|
||||||
export type ExtensionEventCallback = (ext: Extension, ...args: any[]) => any
|
export type ExtensionEventCallback = (ext: Extension, ...args: any[]) => any
|
||||||
|
|
||||||
|
@ -72,32 +73,30 @@ export class Extension {
|
||||||
/** Events registered by this Extension */
|
/** Events registered by this Extension */
|
||||||
events: { [name: string]: (...args: any[]) => {} } = {}
|
events: { [name: string]: (...args: any[]) => {} } = {}
|
||||||
|
|
||||||
_decoratedCommands?: { [name: string]: Command }
|
|
||||||
_decoratedEvents?: { [name: string]: (...args: any[]) => any }
|
|
||||||
|
|
||||||
constructor(client: CommandClient) {
|
constructor(client: CommandClient) {
|
||||||
this.client = client
|
this.client = client
|
||||||
if (this._decoratedCommands !== undefined) {
|
const self = this as any
|
||||||
Object.entries(this._decoratedCommands).forEach((entry) => {
|
if (self._decoratedCommands !== undefined) {
|
||||||
|
Object.entries(self._decoratedCommands).forEach((entry: any) => {
|
||||||
entry[1].extension = this
|
entry[1].extension = this
|
||||||
this.commands.add(entry[1])
|
this.commands.add(entry[1])
|
||||||
})
|
})
|
||||||
this._decoratedCommands = undefined
|
self._decoratedCommands = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this._decoratedEvents !== undefined &&
|
self._decoratedEvents !== undefined &&
|
||||||
Object.keys(this._decoratedEvents).length !== 0
|
Object.keys(self._decoratedEvents).length !== 0
|
||||||
) {
|
) {
|
||||||
Object.entries(this._decoratedEvents).forEach((entry) => {
|
Object.entries(self._decoratedEvents).forEach((entry: any) => {
|
||||||
this.listen(entry[0], entry[1])
|
this.listen(entry[0] as keyof ClientEvents, entry[1].bind(this))
|
||||||
})
|
})
|
||||||
this._decoratedEvents = undefined
|
self._decoratedEvents = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Listens for an Event through Extension. */
|
/** Listens for an Event through Extension. */
|
||||||
listen(event: string, cb: ExtensionEventCallback): boolean {
|
listen(event: keyof ClientEvents, cb: ExtensionEventCallback): boolean {
|
||||||
if (this.events[event] !== undefined) return false
|
if (this.events[event] !== undefined) return false
|
||||||
else {
|
else {
|
||||||
const fn = (...args: any[]): any => {
|
const fn = (...args: any[]): any => {
|
||||||
|
@ -152,7 +151,7 @@ export class ExtensionsManager {
|
||||||
if (extension === undefined) return false
|
if (extension === undefined) return false
|
||||||
extension.commands.deleteAll()
|
extension.commands.deleteAll()
|
||||||
for (const [k, v] of Object.entries(extension.events)) {
|
for (const [k, v] of Object.entries(extension.events)) {
|
||||||
this.client.removeListener(k, v)
|
this.client.off(k as keyof ClientEvents, v)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete extension.events[k]
|
delete extension.events[k]
|
||||||
}
|
}
|
3
src/commands/mod.ts
Normal file
3
src/commands/mod.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './client.ts'
|
||||||
|
export * from './command.ts'
|
||||||
|
export * from './extension.ts'
|
|
@ -1,9 +0,0 @@
|
||||||
export const DISCORD_API_URL: string = 'https://discord.com/api'
|
|
||||||
|
|
||||||
export const DISCORD_GATEWAY_URL: string = 'wss://gateway.discord.gg'
|
|
||||||
|
|
||||||
export const DISCORD_CDN_URL: string = 'https://cdn.discordapp.com'
|
|
||||||
|
|
||||||
export const DISCORD_API_VERSION: number = 8
|
|
||||||
|
|
||||||
export const DISCORD_VOICE_VERSION: number = 4
|
|
15
src/gateway/handlers/applicationCommandCreate.ts
Normal file
15
src/gateway/handlers/applicationCommandCreate.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
|
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
||||||
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
|
export const applicationCommandCreate: GatewayEventHandler = async (
|
||||||
|
gateway: Gateway,
|
||||||
|
d: ApplicationCommandPayload
|
||||||
|
) => {
|
||||||
|
const guild =
|
||||||
|
d.guild_id === undefined
|
||||||
|
? undefined
|
||||||
|
: await gateway.client.guilds.get(d.guild_id)
|
||||||
|
const cmd = new SlashCommand(gateway.client.slash.commands, d, guild)
|
||||||
|
gateway.client.emit('slashCommandCreate', cmd)
|
||||||
|
}
|
15
src/gateway/handlers/applicationCommandDelete.ts
Normal file
15
src/gateway/handlers/applicationCommandDelete.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
|
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
||||||
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
|
export const applicationCommandDelete: GatewayEventHandler = async (
|
||||||
|
gateway: Gateway,
|
||||||
|
d: ApplicationCommandPayload
|
||||||
|
) => {
|
||||||
|
const guild =
|
||||||
|
d.guild_id === undefined
|
||||||
|
? undefined
|
||||||
|
: await gateway.client.guilds.get(d.guild_id)
|
||||||
|
const cmd = new SlashCommand(gateway.client.slash.commands, d, guild)
|
||||||
|
gateway.client.emit('slashCommandDelete', cmd)
|
||||||
|
}
|
15
src/gateway/handlers/applicationCommandUpdate.ts
Normal file
15
src/gateway/handlers/applicationCommandUpdate.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
|
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
||||||
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
|
export const applicationCommandUpdate: GatewayEventHandler = async (
|
||||||
|
gateway: Gateway,
|
||||||
|
d: ApplicationCommandPayload
|
||||||
|
) => {
|
||||||
|
const guild =
|
||||||
|
d.guild_id === undefined
|
||||||
|
? undefined
|
||||||
|
: await gateway.client.guilds.get(d.guild_id)
|
||||||
|
const cmd = new SlashCommand(gateway.client.slash.commands, d, guild)
|
||||||
|
gateway.client.emit('slashCommandUpdate', cmd)
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import getChannelByType from '../../utils/getChannelByType.ts'
|
import getChannelByType from '../../utils/channel.ts'
|
||||||
import { ChannelPayload, GuildChannelPayload } from '../../types/channel.ts'
|
import type {
|
||||||
import { Guild } from '../../structures/guild.ts'
|
ChannelPayload,
|
||||||
|
GuildChannelPayload
|
||||||
|
} from '../../types/channel.ts'
|
||||||
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
|
|
||||||
export const channelCreate: GatewayEventHandler = async (
|
export const channelCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { ChannelPayload } from '../../types/channel.ts'
|
import type { ChannelPayload } from '../../types/channel.ts'
|
||||||
|
|
||||||
export const channelDelete: GatewayEventHandler = async (
|
export const channelDelete: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
@ -7,7 +7,7 @@ export const channelDelete: GatewayEventHandler = async (
|
||||||
) => {
|
) => {
|
||||||
const channel = await gateway.client.channels.get(d.id)
|
const channel = await gateway.client.channels.get(d.id)
|
||||||
if (channel !== undefined) {
|
if (channel !== undefined) {
|
||||||
await gateway.client.channels.delete(d.id)
|
await gateway.client.channels._delete(d.id)
|
||||||
gateway.client.emit('channelDelete', channel)
|
gateway.client.emit('channelDelete', channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { ChannelPinsUpdatePayload } from '../../types/gateway.ts'
|
import type { ChannelPinsUpdatePayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
export const channelPinsUpdate: GatewayEventHandler = async (
|
export const channelPinsUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Channel } from '../../structures/channel.ts'
|
import type { Channel } from '../../structures/channel.ts'
|
||||||
import { ChannelPayload } from '../../types/channel.ts'
|
import type { ChannelPayload } from '../../types/channel.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const channelUpdate: GatewayEventHandler = async (
|
export const channelUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildBanAddPayload } from '../../types/gateway.ts'
|
import { GuildBanAddPayload } from '../../types/gateway.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildBanRemovePayload } from '../../types/gateway.ts'
|
import { GuildBanRemovePayload } from '../../types/gateway.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import { GuildPayload } from '../../types/guild.ts'
|
||||||
import { GuildChannelPayload } from '../../types/channel.ts'
|
import { GuildChannelPayload } from '../../types/channel.ts'
|
||||||
|
@ -27,8 +27,13 @@ export const guildCreate: GatewayEventHandler = async (
|
||||||
if (d.voice_states !== undefined)
|
if (d.voice_states !== undefined)
|
||||||
await guild.voiceStates.fromPayload(d.voice_states)
|
await guild.voiceStates.fromPayload(d.voice_states)
|
||||||
|
|
||||||
|
for (const emojiPayload of d.emojis) {
|
||||||
|
if (emojiPayload.id === null) continue
|
||||||
|
await gateway.client.emojis.set(emojiPayload.id, emojiPayload)
|
||||||
|
}
|
||||||
|
|
||||||
if (hasGuild === undefined) {
|
if (hasGuild === undefined) {
|
||||||
// It wasn't lazy load, so emit event
|
// It wasn't lazy load, so emit event
|
||||||
gateway.client.emit('guildCreate', guild)
|
gateway.client.emit('guildCreate', guild)
|
||||||
}
|
} else gateway.client.emit('guildLoaded', guild)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import { GuildPayload } from '../../types/guild.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const guildDelete: GatewayEventHandler = async (
|
export const guildDelete: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
@ -13,7 +13,8 @@ export const guildDelete: GatewayEventHandler = async (
|
||||||
await guild.channels.flush()
|
await guild.channels.flush()
|
||||||
await guild.roles.flush()
|
await guild.roles.flush()
|
||||||
await guild.presences.flush()
|
await guild.presences.flush()
|
||||||
await gateway.client.guilds.delete(d.id)
|
await guild.emojis.flush()
|
||||||
|
await gateway.client.guilds._delete(d.id)
|
||||||
|
|
||||||
gateway.client.emit('guildDelete', guild)
|
gateway.client.emit('guildDelete', guild)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Emoji } from '../../structures/emoji.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { EmojiPayload } from '../../types/emoji.ts'
|
import { EmojiPayload } from '../../types/emoji.ts'
|
||||||
import { GuildEmojiUpdatePayload } from '../../types/gateway.ts'
|
import { GuildEmojiUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const guildEmojiUpdate: GatewayEventHandler = async (
|
export const guildEmojiUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
@ -44,16 +44,18 @@ export const guildEmojiUpdate: GatewayEventHandler = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gateway.client.emit('guildEmojisUpdate', guild)
|
||||||
|
|
||||||
for (const emoji of deleted) {
|
for (const emoji of deleted) {
|
||||||
gateway.client.emit('guildEmojiDelete', guild, emoji)
|
gateway.client.emit('guildEmojiDelete', emoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const emoji of added) {
|
for (const emoji of added) {
|
||||||
gateway.client.emit('guildEmojiAdd', guild, emoji)
|
gateway.client.emit('guildEmojiAdd', emoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const emoji of updated) {
|
for (const emoji of updated) {
|
||||||
gateway.client.emit('guildEmojiUpdate', guild, emoji.before, emoji.after)
|
gateway.client.emit('guildEmojiUpdate', emoji.before, emoji.after)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildIntegrationsUpdatePayload } from '../../types/gateway.ts'
|
import { GuildIntegrationsUpdatePayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildMemberAddPayload } from '../../types/gateway.ts'
|
import { GuildMemberAddPayload } from '../../types/gateway.ts'
|
||||||
import { Member } from '../../structures/member.ts'
|
import { Member } from '../../structures/member.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildMemberRemovePayload } from '../../types/gateway.ts'
|
import { GuildMemberRemovePayload } from '../../types/gateway.ts'
|
||||||
|
@ -12,7 +12,7 @@ export const guildMemberRemove: GatewayEventHandler = async (
|
||||||
if (guild === undefined) return
|
if (guild === undefined) return
|
||||||
|
|
||||||
const member = await guild.members.get(d.user.id)
|
const member = await guild.members.get(d.user.id)
|
||||||
await guild.members.delete(d.user.id)
|
await guild.members._delete(d.user.id)
|
||||||
|
|
||||||
if (member !== undefined) gateway.client.emit('guildMemberRemove', member)
|
if (member !== undefined) gateway.client.emit('guildMemberRemove', member)
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildMemberUpdatePayload } from '../../types/gateway.ts'
|
import { GuildMemberUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { MemberPayload } from '../../types/guild.ts'
|
import { MemberPayload } from '../../types/guild.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildMemberChunkPayload } from '../../types/gateway.ts'
|
import { GuildMemberChunkPayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildRoleCreatePayload } from '../../types/gateway.ts'
|
import { GuildRoleCreatePayload } from '../../types/gateway.ts'
|
||||||
import { Role } from '../../structures/role.ts'
|
import { Role } from '../../structures/role.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildRoleDeletePayload } from '../../types/gateway.ts'
|
import { GuildRoleDeletePayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ export const guildRoleDelete: GatewayEventHandler = async (
|
||||||
const role = await guild.roles.get(d.role_id)
|
const role = await guild.roles.get(d.role_id)
|
||||||
// Shouldn't happen either
|
// Shouldn't happen either
|
||||||
if (role === undefined) return
|
if (role === undefined) return
|
||||||
|
await guild.roles._delete(d.role_id)
|
||||||
|
|
||||||
gateway.client.emit('guildRoleDelete', role)
|
gateway.client.emit('guildRoleDelete', role)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildRoleUpdatePayload } from '../../types/gateway.ts'
|
import { GuildRoleUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Role } from '../../structures/role.ts'
|
import { Role } from '../../structures/role.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import { GuildPayload } from '../../types/guild.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,173 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||||
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { Member } from '../../structures/member.ts'
|
import { Member } from '../../structures/member.ts'
|
||||||
import { Interaction } from '../../structures/slash.ts'
|
import {
|
||||||
import { GuildTextChannel } from '../../structures/textChannel.ts'
|
InteractionApplicationCommandResolved,
|
||||||
import { InteractionPayload } from '../../types/slash.ts'
|
SlashCommandInteraction
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
} from '../../structures/slash.ts'
|
||||||
|
import {
|
||||||
|
Interaction,
|
||||||
|
InteractionChannel
|
||||||
|
} from '../../structures/interactions.ts'
|
||||||
|
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
|
import {
|
||||||
|
InteractionPayload,
|
||||||
|
InteractionType
|
||||||
|
} from '../../types/interactions.ts'
|
||||||
|
import { UserPayload } from '../../types/user.ts'
|
||||||
|
import { Permissions } from '../../utils/permissions.ts'
|
||||||
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
import { User } from '../../structures/user.ts'
|
||||||
|
import { Role } from '../../structures/role.ts'
|
||||||
|
import { RolePayload } from '../../types/role.ts'
|
||||||
|
import {
|
||||||
|
InteractionApplicationCommandData,
|
||||||
|
InteractionChannelPayload
|
||||||
|
} from '../../types/slashCommands.ts'
|
||||||
|
import { Message } from '../../structures/message.ts'
|
||||||
|
import { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
|
||||||
export const interactionCreate: GatewayEventHandler = async (
|
export const interactionCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: InteractionPayload
|
d: InteractionPayload
|
||||||
) => {
|
) => {
|
||||||
const guild = await gateway.client.guilds.get(d.guild_id)
|
// NOTE(DjDeveloperr): Mason once mentioned that channel_id can be optional in Interaction.
|
||||||
if (guild === undefined) return
|
// This case can be seen in future proofing Interactions, and one he mentioned was
|
||||||
|
// that bots will be able to add custom context menus. In that case, Interaction will not have it.
|
||||||
|
// Ref: https://github.com/discord/discord-api-docs/pull/2568/files#r569025697
|
||||||
|
if (d.channel_id === undefined) return
|
||||||
|
|
||||||
await guild.members.set(d.member.user.id, d.member)
|
const guild =
|
||||||
const member = ((await guild.members.get(
|
d.guild_id === undefined
|
||||||
d.member.user.id
|
? undefined
|
||||||
)) as unknown) as Member
|
: (await gateway.client.guilds.get(d.guild_id)) ??
|
||||||
|
new Guild(gateway.client, { unavailable: true, id: d.guild_id } as any)
|
||||||
|
|
||||||
const channel =
|
if (d.member !== undefined)
|
||||||
(await gateway.client.channels.get<GuildTextChannel>(d.channel_id)) ??
|
await guild?.members.set(d.member.user.id, d.member)
|
||||||
(await gateway.client.channels.fetch<GuildTextChannel>(d.channel_id))
|
const member =
|
||||||
|
d.member !== undefined
|
||||||
|
? (await guild?.members.get(d.member.user.id))! ??
|
||||||
|
new Member(
|
||||||
|
gateway.client,
|
||||||
|
d.member!,
|
||||||
|
new User(gateway.client, d.member.user),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
guild!,
|
||||||
|
new Permissions(d.member.permissions)
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user)
|
||||||
|
const dmUser =
|
||||||
|
d.user !== undefined ? await gateway.client.users.get(d.user.id) : undefined
|
||||||
|
|
||||||
|
const user = member !== undefined ? member.user : dmUser
|
||||||
|
if (user === undefined) return
|
||||||
|
|
||||||
|
const channel = await gateway.client.channels.get<GuildTextBasedChannel>(
|
||||||
|
d.channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved: InteractionApplicationCommandResolved = {
|
||||||
|
users: {},
|
||||||
|
channels: {},
|
||||||
|
members: {},
|
||||||
|
roles: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((d.data as InteractionApplicationCommandData)?.resolved !== undefined) {
|
||||||
|
for (const [id, data] of Object.entries(
|
||||||
|
(d.data as any)?.resolved.users ?? {}
|
||||||
|
)) {
|
||||||
|
await gateway.client.users.set(id, data as UserPayload)
|
||||||
|
resolved.users[id] = ((await gateway.client.users.get(
|
||||||
|
id
|
||||||
|
)) as unknown) as User
|
||||||
|
if (resolved.members[id] !== undefined)
|
||||||
|
resolved.users[id].member = resolved.members[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, data] of Object.entries(
|
||||||
|
(d.data as InteractionApplicationCommandData)?.resolved?.members ?? {}
|
||||||
|
)) {
|
||||||
|
const roles = await guild?.roles.array()
|
||||||
|
let permissions = new Permissions(Permissions.DEFAULT)
|
||||||
|
if (roles !== undefined) {
|
||||||
|
const mRoles = roles.filter(
|
||||||
|
(r) =>
|
||||||
|
((data as any)?.roles?.includes(r.id) as boolean) ||
|
||||||
|
r.id === guild?.id
|
||||||
|
)
|
||||||
|
permissions = new Permissions(mRoles.map((r) => r.permissions))
|
||||||
|
}
|
||||||
|
;(data as any).user = ((d.data as any).resolved.users?.[
|
||||||
|
id
|
||||||
|
] as unknown) as UserPayload
|
||||||
|
resolved.members[id] = new Member(
|
||||||
|
gateway.client,
|
||||||
|
data as any,
|
||||||
|
resolved.users[id],
|
||||||
|
guild as Guild,
|
||||||
|
permissions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, data] of Object.entries(
|
||||||
|
(d.data as InteractionApplicationCommandData).resolved?.roles ?? {}
|
||||||
|
)) {
|
||||||
|
if (guild !== undefined) {
|
||||||
|
await guild.roles.set(id, data as RolePayload)
|
||||||
|
resolved.roles[id] = ((await guild.roles.get(id)) as unknown) as Role
|
||||||
|
} else {
|
||||||
|
resolved.roles[id] = new Role(
|
||||||
|
gateway.client,
|
||||||
|
data as any,
|
||||||
|
(guild as unknown) as Guild
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, data] of Object.entries(
|
||||||
|
(d.data as InteractionApplicationCommandData).resolved?.channels ?? {}
|
||||||
|
)) {
|
||||||
|
resolved.channels[id] = new InteractionChannel(
|
||||||
|
gateway.client,
|
||||||
|
data as InteractionChannelPayload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: Message | undefined
|
||||||
|
if (d.message !== undefined) {
|
||||||
|
const channel = (await gateway.client.channels.get<TextChannel>(
|
||||||
|
d.message.channel_id
|
||||||
|
))!
|
||||||
|
message = new Message(
|
||||||
|
gateway.client,
|
||||||
|
d.message,
|
||||||
|
channel,
|
||||||
|
new User(gateway.client, d.message.author)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let interaction
|
||||||
|
if (d.type === InteractionType.APPLICATION_COMMAND) {
|
||||||
|
interaction = new SlashCommandInteraction(gateway.client, d, {
|
||||||
|
member,
|
||||||
|
guild,
|
||||||
|
channel,
|
||||||
|
user,
|
||||||
|
resolved
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
interaction = new Interaction(gateway.client, d, {
|
||||||
|
member,
|
||||||
|
guild,
|
||||||
|
channel,
|
||||||
|
user,
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const interaction = new Interaction(gateway.client, d, {
|
|
||||||
member,
|
|
||||||
guild,
|
|
||||||
channel
|
|
||||||
})
|
|
||||||
gateway.client.emit('interactionCreate', interaction)
|
gateway.client.emit('interactionCreate', interaction)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { InviteCreatePayload } from '../../types/gateway.ts'
|
import { InviteCreatePayload } from '../../types/gateway.ts'
|
||||||
import { ChannelPayload } from '../../types/channel.ts'
|
import { ChannelPayload } from '../../types/channel.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { InviteDeletePayload } from '../../types/gateway.ts'
|
import { InviteDeletePayload } from '../../types/gateway.ts'
|
||||||
import { PartialInvitePayload } from '../../types/invite.ts'
|
import { PartialInvitePayload } from '../../types/invite.ts'
|
||||||
|
@ -19,7 +19,6 @@ export const inviteDelete: GatewayEventHandler = async (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const cachedGuild = await gateway.client.guilds.get(d.guild_id!)
|
const cachedGuild = await gateway.client.guilds.get(d.guild_id!)
|
||||||
|
|
||||||
// TODO(DjDeveloperr): Make it support self-bots and make Guild not always defined
|
|
||||||
if (cachedInvite === undefined) {
|
if (cachedInvite === undefined) {
|
||||||
const uncachedInvite: PartialInvitePayload = {
|
const uncachedInvite: PartialInvitePayload = {
|
||||||
guild: (cachedGuild as unknown) as Guild,
|
guild: (cachedGuild as unknown) as Guild,
|
||||||
|
@ -28,7 +27,7 @@ export const inviteDelete: GatewayEventHandler = async (
|
||||||
}
|
}
|
||||||
return gateway.client.emit('inviteDeleteUncached', uncachedInvite)
|
return gateway.client.emit('inviteDeleteUncached', uncachedInvite)
|
||||||
} else {
|
} else {
|
||||||
await guild.invites.delete(d.code)
|
await guild.invites._delete(d.code)
|
||||||
gateway.client.emit('inviteDelete', cachedInvite)
|
gateway.client.emit('inviteDelete', cachedInvite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Message } from '../../structures/message.ts'
|
import { Message } from '../../structures/message.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { MessagePayload } from '../../types/channel.ts'
|
import type { MessagePayload } from '../../types/channel.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageCreate: GatewayEventHandler = async (
|
export const messageCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { MessageDeletePayload } from '../../types/gateway.ts'
|
import type { MessageDeletePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageDelete: GatewayEventHandler = async (
|
export const messageDelete: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
@ -15,6 +15,6 @@ export const messageDelete: GatewayEventHandler = async (
|
||||||
const message = await channel.messages.get(d.id)
|
const message = await channel.messages.get(d.id)
|
||||||
if (message === undefined)
|
if (message === undefined)
|
||||||
return gateway.client.emit('messageDeleteUncached', d)
|
return gateway.client.emit('messageDeleteUncached', d)
|
||||||
await channel.messages.delete(d.id)
|
await channel.messages._delete(d.id)
|
||||||
gateway.client.emit('messageDelete', message)
|
gateway.client.emit('messageDelete', message)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { Message } from '../../structures/message.ts'
|
import type { Message } from '../../structures/message.ts'
|
||||||
import { GuildTextChannel } from '../../structures/textChannel.ts'
|
import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
import { MessageDeleteBulkPayload } from '../../types/gateway.ts'
|
import type { MessageDeleteBulkPayload } from '../../types/gateway.ts'
|
||||||
import { Collection } from '../../utils/collection.ts'
|
import { Collection } from '../../utils/collection.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageDeleteBulk: GatewayEventHandler = async (
|
export const messageDeleteBulk: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: MessageDeleteBulkPayload
|
d: MessageDeleteBulkPayload
|
||||||
) => {
|
) => {
|
||||||
let channel = await gateway.client.channels.get<GuildTextChannel>(
|
let channel = await gateway.client.channels.get<GuildTextBasedChannel>(
|
||||||
d.channel_id
|
d.channel_id
|
||||||
)
|
)
|
||||||
// Fetch the channel if not cached
|
// Fetch the channel if not cached
|
||||||
|
@ -16,7 +16,7 @@ export const messageDeleteBulk: GatewayEventHandler = async (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
channel = (await gateway.client.channels.fetch(
|
channel = (await gateway.client.channels.fetch(
|
||||||
d.channel_id
|
d.channel_id
|
||||||
)) as GuildTextChannel
|
)) as GuildTextBasedChannel
|
||||||
|
|
||||||
const messages = new Collection<string, Message>()
|
const messages = new Collection<string, Message>()
|
||||||
const uncached = new Set<string>()
|
const uncached = new Set<string>()
|
||||||
|
@ -25,7 +25,7 @@ export const messageDeleteBulk: GatewayEventHandler = async (
|
||||||
if (message === undefined) uncached.add(id)
|
if (message === undefined) uncached.add(id)
|
||||||
else {
|
else {
|
||||||
messages.set(id, message)
|
messages.set(id, message)
|
||||||
await channel.messages.delete(id)
|
await channel.messages._delete(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionAddPayload } from '../../types/gateway.ts'
|
import type { MessageReactionAddPayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { MessageReaction } from '../../structures/messageReaction.ts'
|
import type { MessageReaction } from '../../structures/messageReaction.ts'
|
||||||
import { UserPayload } from '../../types/user.ts'
|
import type { UserPayload } from '../../types/user.ts'
|
||||||
|
|
||||||
export const messageReactionAdd: GatewayEventHandler = async (
|
export const messageReactionAdd: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionRemovePayload } from '../../types/gateway.ts'
|
import type { MessageReactionRemovePayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
|
||||||
export const messageReactionRemove: GatewayEventHandler = async (
|
export const messageReactionRemove: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
@ -31,7 +31,7 @@ export const messageReactionRemove: GatewayEventHandler = async (
|
||||||
const reaction = await message.reactions.get(emojiID)
|
const reaction = await message.reactions.get(emojiID)
|
||||||
if (reaction === undefined) return
|
if (reaction === undefined) return
|
||||||
|
|
||||||
reaction.users.delete(d.user_id)
|
reaction.users._delete(d.user_id)
|
||||||
|
|
||||||
gateway.client.emit('messageReactionRemove', reaction, user)
|
gateway.client.emit('messageReactionRemove', reaction, user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionRemoveAllPayload } from '../../types/gateway.ts'
|
import type { MessageReactionRemoveAllPayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
|
||||||
export const messageReactionRemoveAll: GatewayEventHandler = async (
|
export const messageReactionRemoveAll: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts'
|
import type { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
|
||||||
export const messageReactionRemoveEmoji: GatewayEventHandler = async (
|
export const messageReactionRemoveEmoji: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Message } from '../../structures/message.ts'
|
import type { Message } from '../../structures/message.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { MessagePayload } from '../../types/channel.ts'
|
import type { MessagePayload } from '../../types/channel.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageUpdate: GatewayEventHandler = async (
|
export const messageUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { GatewayEventHandler } from '../index.ts'
|
import type { GatewayEventHandler } from '../mod.ts'
|
||||||
import { GatewayEvents, TypingStartGuildData } from '../../types/gateway.ts'
|
import type {
|
||||||
|
GatewayEvents,
|
||||||
|
MessageDeletePayload,
|
||||||
|
TypingStartGuildData
|
||||||
|
} from '../../types/gateway.ts'
|
||||||
import { channelCreate } from './channelCreate.ts'
|
import { channelCreate } from './channelCreate.ts'
|
||||||
import { channelDelete } from './channelDelete.ts'
|
import { channelDelete } from './channelDelete.ts'
|
||||||
import { channelUpdate } from './channelUpdate.ts'
|
import { channelUpdate } from './channelUpdate.ts'
|
||||||
|
@ -27,17 +31,18 @@ import { webhooksUpdate } from './webhooksUpdate.ts'
|
||||||
import { messageDeleteBulk } from './messageDeleteBulk.ts'
|
import { messageDeleteBulk } from './messageDeleteBulk.ts'
|
||||||
import { userUpdate } from './userUpdate.ts'
|
import { userUpdate } from './userUpdate.ts'
|
||||||
import { typingStart } from './typingStart.ts'
|
import { typingStart } from './typingStart.ts'
|
||||||
import { GuildTextChannel, TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { Emoji } from '../../structures/emoji.ts'
|
import type { User } from '../../structures/user.ts'
|
||||||
import { Member } from '../../structures/member.ts'
|
import type { Emoji } from '../../structures/emoji.ts'
|
||||||
import { Role } from '../../structures/role.ts'
|
import type { Member } from '../../structures/member.ts'
|
||||||
import { Message } from '../../structures/message.ts'
|
import type { Role } from '../../structures/role.ts'
|
||||||
import { Collection } from '../../utils/collection.ts'
|
import type { Message } from '../../structures/message.ts'
|
||||||
|
import type { Collection } from '../../utils/collection.ts'
|
||||||
import { voiceServerUpdate } from './voiceServerUpdate.ts'
|
import { voiceServerUpdate } from './voiceServerUpdate.ts'
|
||||||
import { voiceStateUpdate } from './voiceStateUpdate.ts'
|
import { voiceStateUpdate } from './voiceStateUpdate.ts'
|
||||||
import { VoiceState } from '../../structures/voiceState.ts'
|
import type { VoiceState } from '../../structures/voiceState.ts'
|
||||||
import { messageReactionAdd } from './messageReactionAdd.ts'
|
import { messageReactionAdd } from './messageReactionAdd.ts'
|
||||||
import { messageReactionRemove } from './messageReactionRemove.ts'
|
import { messageReactionRemove } from './messageReactionRemove.ts'
|
||||||
import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts'
|
import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts'
|
||||||
|
@ -46,20 +51,32 @@ import { guildMembersChunk } from './guildMembersChunk.ts'
|
||||||
import { presenceUpdate } from './presenceUpdate.ts'
|
import { presenceUpdate } from './presenceUpdate.ts'
|
||||||
import { inviteCreate } from './inviteCreate.ts'
|
import { inviteCreate } from './inviteCreate.ts'
|
||||||
import { inviteDelete } from './inviteDelete.ts'
|
import { inviteDelete } from './inviteDelete.ts'
|
||||||
import { MessageReaction } from '../../structures/messageReaction.ts'
|
import type { MessageReaction } from '../../structures/messageReaction.ts'
|
||||||
import { Invite } from '../../structures/invite.ts'
|
import type { Invite } from '../../structures/invite.ts'
|
||||||
import { Presence } from '../../structures/presence.ts'
|
import type { Presence } from '../../structures/presence.ts'
|
||||||
import {
|
import type {
|
||||||
EveryChannelTypes,
|
EveryChannelTypes,
|
||||||
EveryTextChannelTypes
|
EveryTextChannelTypes
|
||||||
} from '../../utils/getChannelByType.ts'
|
} from '../../utils/channel.ts'
|
||||||
import { interactionCreate } from './interactionCreate.ts'
|
import { interactionCreate } from './interactionCreate.ts'
|
||||||
import { Interaction } from '../../structures/slash.ts'
|
import type { Interaction } from '../../structures/interactions.ts'
|
||||||
|
import type { SlashCommandInteraction } from '../../structures/slash.ts'
|
||||||
|
import type { CommandContext } from '../../commands/command.ts'
|
||||||
|
import type { RequestMethods } from '../../rest/types.ts'
|
||||||
|
import type { PartialInvitePayload } from '../../types/invite.ts'
|
||||||
|
import type { GuildChannels } from '../../types/guild.ts'
|
||||||
|
import { applicationCommandCreate } from './applicationCommandCreate.ts'
|
||||||
|
import { applicationCommandDelete } from './applicationCommandDelete.ts'
|
||||||
|
import { applicationCommandUpdate } from './applicationCommandUpdate.ts'
|
||||||
|
import type { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
|
|
||||||
export const gatewayHandlers: {
|
export const gatewayHandlers: {
|
||||||
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
|
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
|
||||||
} = {
|
} = {
|
||||||
READY: ready,
|
READY: ready,
|
||||||
|
APPLICATION_COMMAND_CREATE: applicationCommandCreate,
|
||||||
|
APPLICATION_COMMAND_DELETE: applicationCommandDelete,
|
||||||
|
APPLICATION_COMMAND_UPDATE: applicationCommandUpdate,
|
||||||
RECONNECT: reconnect,
|
RECONNECT: reconnect,
|
||||||
RESUMED: resume,
|
RESUMED: resume,
|
||||||
CHANNEL_CREATE: channelCreate,
|
CHANNEL_CREATE: channelCreate,
|
||||||
|
@ -105,13 +122,15 @@ export interface VoiceServerUpdateData {
|
||||||
guild: Guild
|
guild: Guild
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientEvents {
|
/** All Client Events */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type ClientEvents = {
|
||||||
/** When Client has successfully connected to Discord */
|
/** When Client has successfully connected to Discord */
|
||||||
ready: []
|
ready: [shard: number]
|
||||||
/** When a successful reconnect has been made */
|
/** When a reconnect was requested by Discord */
|
||||||
reconnect: []
|
reconnect: [shard: number]
|
||||||
/** When a successful session resume has been done */
|
/** When a successful session resume has been done */
|
||||||
resumed: []
|
resumed: [shard: number]
|
||||||
/**
|
/**
|
||||||
* When a new Channel is created
|
* When a new Channel is created
|
||||||
* @param channel New Channel object
|
* @param channel New Channel object
|
||||||
|
@ -154,6 +173,11 @@ export interface ClientEvents {
|
||||||
* @param guild The new Guild object
|
* @param guild The new Guild object
|
||||||
*/
|
*/
|
||||||
guildCreate: [guild: Guild]
|
guildCreate: [guild: Guild]
|
||||||
|
/**
|
||||||
|
* A Guild was successfully loaded.
|
||||||
|
* @param guild The Guild object
|
||||||
|
*/
|
||||||
|
guildLoaded: [guild: Guild]
|
||||||
/**
|
/**
|
||||||
* A Guild in which Client was either deleted, or bot was kicked
|
* A Guild in which Client was either deleted, or bot was kicked
|
||||||
* @param guild The Guild object
|
* @param guild The Guild object
|
||||||
|
@ -164,25 +188,28 @@ export interface ClientEvents {
|
||||||
* @param guild Guild in which Emoji was added
|
* @param guild Guild in which Emoji was added
|
||||||
* @param emoji The Emoji which was added
|
* @param emoji The Emoji which was added
|
||||||
*/
|
*/
|
||||||
guildEmojiAdd: [guild: Guild, emoji: Emoji]
|
guildEmojiAdd: [emoji: Emoji]
|
||||||
/**
|
/**
|
||||||
* An Emoji was deleted from Guild
|
* An Emoji was deleted from Guild
|
||||||
* @param guild Guild from which Emoji was deleted
|
|
||||||
* @param emoji Emoji which was deleted
|
* @param emoji Emoji which was deleted
|
||||||
*/
|
*/
|
||||||
guildEmojiDelete: [Guild, Emoji]
|
guildEmojiDelete: [emoji: Emoji]
|
||||||
/**
|
/**
|
||||||
* An Emoji in a Guild was updated
|
* An Emoji in a Guild was updated
|
||||||
* @param guild Guild in which Emoji was updated
|
|
||||||
* @param before Emoji object before update
|
* @param before Emoji object before update
|
||||||
* @param after Emoji object after update
|
* @param after Emoji object after update
|
||||||
*/
|
*/
|
||||||
guildEmojiUpdate: [guild: Guild, before: Emoji, after: Emoji]
|
guildEmojiUpdate: [before: Emoji, after: Emoji]
|
||||||
/**
|
/**
|
||||||
* Guild's Integrations were updated
|
* Guild's Integrations were updated
|
||||||
* @param guild The Guild object
|
* @param guild The Guild object
|
||||||
*/
|
*/
|
||||||
guildIntegrationsUpdate: [guild: Guild]
|
guildIntegrationsUpdate: [guild: Guild]
|
||||||
|
/**
|
||||||
|
* Guild's Emojis were updated
|
||||||
|
* @param guild The Guild object
|
||||||
|
*/
|
||||||
|
guildEmojisUpdate: [guild: Guild]
|
||||||
/**
|
/**
|
||||||
* A new Member has joined a Guild
|
* A new Member has joined a Guild
|
||||||
* @param member The Member object
|
* @param member The Member object
|
||||||
|
@ -238,7 +265,7 @@ export interface ClientEvents {
|
||||||
* @param uncached Set of Messages deleted's IDs which were not cached
|
* @param uncached Set of Messages deleted's IDs which were not cached
|
||||||
*/
|
*/
|
||||||
messageDeleteBulk: [
|
messageDeleteBulk: [
|
||||||
channel: GuildTextChannel,
|
channel: GuildTextBasedChannel,
|
||||||
messages: Collection<string, Message>,
|
messages: Collection<string, Message>,
|
||||||
uncached: Set<string>
|
uncached: Set<string>
|
||||||
]
|
]
|
||||||
|
@ -331,16 +358,71 @@ export interface ClientEvents {
|
||||||
* @param guild Guild in which Webhooks were updated
|
* @param guild Guild in which Webhooks were updated
|
||||||
* @param channel Channel of which Webhooks were updated
|
* @param channel Channel of which Webhooks were updated
|
||||||
*/
|
*/
|
||||||
webhooksUpdate: [guild: Guild, channel: GuildTextChannel]
|
webhooksUpdate: [guild: Guild, channel: GuildTextBasedChannel]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Interaction was created
|
* An Interaction was created
|
||||||
* @param interaction Created interaction object
|
* @param interaction Created interaction object
|
||||||
*/
|
*/
|
||||||
interactionCreate: [interaction: Interaction]
|
interactionCreate: [interaction: Interaction | SlashCommandInteraction]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When debug message was made
|
* When debug message was made
|
||||||
* @param message Debug message
|
* @param message Debug message
|
||||||
*/
|
*/
|
||||||
debug: [message: string]
|
debug: [message: string]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw event which gives you access to raw events DISPATCH'd from Gateway
|
||||||
|
* @param evt Event name string
|
||||||
|
* @param payload Payload JSON of the event
|
||||||
|
*/
|
||||||
|
raw: [evt: string, payload: any]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An uncached Message was deleted.
|
||||||
|
* @param payload Message Delete Payload
|
||||||
|
*/
|
||||||
|
messageDeleteUncached: [payload: MessageDeletePayload]
|
||||||
|
|
||||||
|
guildMembersChunk: [
|
||||||
|
guild: Guild,
|
||||||
|
info: {
|
||||||
|
chunkIndex: number
|
||||||
|
chunkCount: number
|
||||||
|
members: string[]
|
||||||
|
presences: string[] | undefined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
guildMembersChunked: [guild: Guild, chunks: number]
|
||||||
|
rateLimit: [
|
||||||
|
data: {
|
||||||
|
method: RequestMethods
|
||||||
|
path: string
|
||||||
|
global: boolean
|
||||||
|
timeout: number
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
]
|
||||||
|
inviteDeleteUncached: [invite: PartialInvitePayload]
|
||||||
|
voiceStateRemoveUncached: [data: { guild: Guild; member: Member }]
|
||||||
|
userUpdateUncached: [user: User]
|
||||||
|
webhooksUpdateUncached: [guild: Guild, channelID: string]
|
||||||
|
guildRoleUpdateUncached: [role: Role]
|
||||||
|
guildMemberUpdateUncached: [member: Member]
|
||||||
|
guildMemberRemoveUncached: [member: Member]
|
||||||
|
channelUpdateUncached: [channel: GuildChannels]
|
||||||
|
slashCommandCreate: [cmd: SlashCommand]
|
||||||
|
slashCommandUpdate: [cmd: SlashCommand]
|
||||||
|
slashCommandDelete: [cmd: SlashCommand]
|
||||||
|
commandOwnerOnly: [ctx: CommandContext]
|
||||||
|
commandGuildOnly: [ctx: CommandContext]
|
||||||
|
commandDmOnly: [ctx: CommandContext]
|
||||||
|
commandNSFW: [ctx: CommandContext]
|
||||||
|
commandBotMissingPermissions: [ctx: CommandContext, missing: string[]]
|
||||||
|
commandUserMissingPermissions: [ctx: CommandContext, missing: string[]]
|
||||||
|
commandMissingArgs: [ctx: CommandContext]
|
||||||
|
commandUsed: [ctx: CommandContext]
|
||||||
|
commandError: [ctx: CommandContext, err: Error]
|
||||||
|
gatewayError: [err: ErrorEvent, shards: [number, number]]
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { PresenceUpdatePayload } from '../../types/gateway.ts'
|
import type { PresenceUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const presenceUpdate: GatewayEventHandler = async (
|
export const presenceUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { Ready } from '../../types/gateway.ts'
|
import type { Ready } from '../../types/gateway.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import type { GuildPayload } from '../../types/guild.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const ready: GatewayEventHandler = async (
|
export const ready: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: Ready
|
d: Ready
|
||||||
) => {
|
) => {
|
||||||
|
gateway.client.upSince = new Date()
|
||||||
|
|
||||||
|
if ('application' in d) {
|
||||||
|
gateway.client.applicationID = d.application.id
|
||||||
|
gateway.client.applicationFlags = d.application.flags
|
||||||
|
}
|
||||||
|
|
||||||
await gateway.client.guilds.flush()
|
await gateway.client.guilds.flush()
|
||||||
|
|
||||||
await gateway.client.users.set(d.user.id, d.user)
|
await gateway.client.users.set(d.user.id, d.user)
|
||||||
|
@ -19,5 +26,5 @@ export const ready: GatewayEventHandler = async (
|
||||||
gateway.client.guilds.set(guild.id, guild)
|
gateway.client.guilds.set(guild.id, guild)
|
||||||
})
|
})
|
||||||
|
|
||||||
gateway.client.emit('ready')
|
gateway.client.emit('ready', gateway.shards?.[0] ?? 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const reconnect: GatewayEventHandler = async (
|
export const reconnect: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: any
|
d: any
|
||||||
) => {
|
) => {
|
||||||
|
gateway.client.emit('reconnect', gateway.shards?.[0] ?? 0)
|
||||||
gateway.reconnect()
|
gateway.reconnect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { CLIENT_USER } from '../../types/endpoint.ts'
|
import { CLIENT_USER } from '../../types/endpoint.ts'
|
||||||
import { Resume } from '../../types/gateway.ts'
|
import type { Resume } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const resume: GatewayEventHandler = async (
|
export const resume: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: Resume
|
d: Resume
|
||||||
) => {
|
) => {
|
||||||
gateway.debug(`Session Resumed!`)
|
gateway.debug(`Session Resumed!`)
|
||||||
gateway.client.emit('resume')
|
gateway.client.emit('resumed', gateway.shards?.[0] ?? 0)
|
||||||
if (gateway.client.user === undefined)
|
if (gateway.client.user === undefined)
|
||||||
gateway.client.user = new User(
|
gateway.client.user = new User(
|
||||||
gateway.client,
|
gateway.client,
|
||||||
await gateway.client.rest.get(CLIENT_USER())
|
await gateway.client.rest.get(CLIENT_USER())
|
||||||
)
|
)
|
||||||
gateway.client.emit('ready')
|
gateway.client.emit('ready', gateway.shards?.[0] ?? 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Member } from '../../structures/member.ts'
|
import { Member } from '../../structures/member.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { TypingStartPayload } from '../../types/gateway.ts'
|
import type { TypingStartPayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
// TODO: Do we need to add uncached events here?
|
// TODO: Do we need to add uncached events here?
|
||||||
export const typingStart: GatewayEventHandler = async (
|
export const typingStart: GatewayEventHandler = async (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { User } from '../../structures/user.ts'
|
import type { User } from '../../structures/user.ts'
|
||||||
import { UserPayload } from '../../types/user.ts'
|
import type { UserPayload } from '../../types/user.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const userUpdate: GatewayEventHandler = async (
|
export const userUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { VoiceServerUpdatePayload } from '../../types/gateway.ts'
|
import type { VoiceServerUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const voiceServerUpdate: GatewayEventHandler = async (
|
export const voiceServerUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { VoiceState } from '../../structures/voiceState.ts'
|
import type { VoiceState } from '../../structures/voiceState.ts'
|
||||||
import { MemberPayload } from '../../types/guild.ts'
|
import type { MemberPayload } from '../../types/guild.ts'
|
||||||
import { VoiceStatePayload } from '../../types/voice.ts'
|
import type { VoiceStatePayload } from '../../types/voice.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const voiceStateUpdate: GatewayEventHandler = async (
|
export const voiceStateUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
@ -24,7 +24,7 @@ export const voiceStateUpdate: GatewayEventHandler = async (
|
||||||
return gateway.client.emit('voiceStateRemoveUncached', { guild, member })
|
return gateway.client.emit('voiceStateRemoveUncached', { guild, member })
|
||||||
}
|
}
|
||||||
// No longer in the channel, so delete
|
// No longer in the channel, so delete
|
||||||
await guild.voiceStates.delete(d.user_id)
|
await guild.voiceStates._delete(d.user_id)
|
||||||
gateway.client.emit(
|
gateway.client.emit(
|
||||||
'voiceStateRemove',
|
'voiceStateRemove',
|
||||||
(voiceState as unknown) as VoiceState
|
(voiceState as unknown) as VoiceState
|
||||||
|
@ -33,17 +33,15 @@ export const voiceStateUpdate: GatewayEventHandler = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
await guild.voiceStates.set(d.user_id, d)
|
await guild.voiceStates.set(d.user_id, d)
|
||||||
const newVoiceState = await guild.voiceStates.get(d.user_id)
|
const newVoiceState = (await guild.voiceStates.get(d.user_id))!
|
||||||
|
|
||||||
|
if (d.user_id === gateway.client.user!.id) {
|
||||||
|
gateway.client.voice.emit('voiceStateUpdate', newVoiceState)
|
||||||
|
}
|
||||||
|
|
||||||
if (voiceState === undefined) {
|
if (voiceState === undefined) {
|
||||||
gateway.client.emit(
|
gateway.client.emit('voiceStateAdd', newVoiceState)
|
||||||
'voiceStateAdd',
|
|
||||||
(newVoiceState as unknown) as VoiceState
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
gateway.client.emit(
|
gateway.client.emit('voiceStateUpdate', voiceState, newVoiceState)
|
||||||
'voiceStateUpdate',
|
|
||||||
voiceState,
|
|
||||||
(newVoiceState as unknown) as VoiceState
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { WebhooksUpdatePayload } from '../../types/gateway.ts'
|
import type { WebhooksUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { GuildTextChannel } from '../../structures/textChannel.ts'
|
import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
|
|
||||||
export const webhooksUpdate: GatewayEventHandler = async (
|
export const webhooksUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
@ -10,9 +10,9 @@ export const webhooksUpdate: GatewayEventHandler = async (
|
||||||
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
|
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
|
||||||
if (guild === undefined) return
|
if (guild === undefined) return
|
||||||
|
|
||||||
const channel: GuildTextChannel | undefined = (await guild.channels.get(
|
const channel: GuildTextBasedChannel | undefined = (await guild.channels.get(
|
||||||
d.channel_id
|
d.channel_id
|
||||||
)) as GuildTextChannel
|
)) as GuildTextBasedChannel
|
||||||
if (channel === undefined)
|
if (channel === undefined)
|
||||||
gateway.client.emit('webhooksUpdateUncached', guild, d.channel_id)
|
gateway.client.emit('webhooksUpdateUncached', guild, d.channel_id)
|
||||||
else gateway.client.emit('webhooksUpdate', guild, channel)
|
else gateway.client.emit('webhooksUpdate', guild, channel)
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
import { unzlib, EventEmitter } from '../../deps.ts'
|
import { unzlib } from '../../deps.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import {
|
|
||||||
DISCORD_GATEWAY_URL,
|
|
||||||
DISCORD_API_VERSION
|
|
||||||
} from '../consts/urlsAndVersions.ts'
|
|
||||||
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
||||||
import {
|
import {
|
||||||
GatewayOpcodes,
|
GatewayOpcodes,
|
||||||
GatewayIntents,
|
|
||||||
GatewayCloseCodes,
|
GatewayCloseCodes,
|
||||||
IdentityPayload,
|
IdentityPayload,
|
||||||
StatusUpdatePayload
|
StatusUpdatePayload,
|
||||||
|
GatewayEvents
|
||||||
} from '../types/gateway.ts'
|
} from '../types/gateway.ts'
|
||||||
import { gatewayHandlers } from './handlers/index.ts'
|
import { gatewayHandlers } from './handlers/mod.ts'
|
||||||
import { GATEWAY_BOT } from '../types/endpoint.ts'
|
|
||||||
import { GatewayCache } from '../managers/gatewayCache.ts'
|
import { GatewayCache } from '../managers/gatewayCache.ts'
|
||||||
import { delay } from '../utils/delay.ts'
|
import { delay } from '../utils/delay.ts'
|
||||||
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
import { decodeText } from '../utils/encoding.ts'
|
||||||
|
import { Constants } from '../types/constants.ts'
|
||||||
|
|
||||||
export interface RequestMembersOptions {
|
export interface RequestMembersOptions {
|
||||||
limit?: number
|
limit?: number
|
||||||
|
@ -33,15 +31,29 @@ export interface VoiceStateOptions {
|
||||||
|
|
||||||
export const RECONNECT_REASON = 'harmony-reconnect'
|
export const RECONNECT_REASON = 'harmony-reconnect'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type GatewayTypedEvents = {
|
||||||
|
[name in GatewayEvents]: [any]
|
||||||
|
} & {
|
||||||
|
connect: []
|
||||||
|
ping: [number]
|
||||||
|
resume: []
|
||||||
|
reconnectRequired: []
|
||||||
|
close: [number, string]
|
||||||
|
error: [Error, ErrorEvent]
|
||||||
|
sentIdentify: []
|
||||||
|
sentResume: []
|
||||||
|
reconnecting: []
|
||||||
|
init: []
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles Discord Gateway connection.
|
* Handles Discord Gateway connection.
|
||||||
*
|
*
|
||||||
* You should not use this and rather use Client class.
|
* You should not use this and rather use Client class.
|
||||||
*/
|
*/
|
||||||
export class Gateway extends EventEmitter {
|
export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
websocket: WebSocket
|
websocket?: WebSocket
|
||||||
token: string
|
|
||||||
intents: GatewayIntents[]
|
|
||||||
connected = false
|
connected = false
|
||||||
initialized = false
|
initialized = false
|
||||||
heartbeatInterval = 0
|
heartbeatInterval = 0
|
||||||
|
@ -50,26 +62,17 @@ export class Gateway extends EventEmitter {
|
||||||
lastPingTimestamp = 0
|
lastPingTimestamp = 0
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
private heartbeatServerResponded = false
|
private heartbeatServerResponded = false
|
||||||
client: Client
|
client!: Client
|
||||||
cache: GatewayCache
|
cache: GatewayCache
|
||||||
private timedIdentify: number | null = null
|
private timedIdentify: number | null = null
|
||||||
|
shards?: number[]
|
||||||
|
ping: number = 0
|
||||||
|
|
||||||
constructor(client: Client, token: string, intents: GatewayIntents[]) {
|
constructor(client: Client, shards?: number[]) {
|
||||||
super()
|
super()
|
||||||
this.token = token
|
Object.defineProperty(this, 'client', { value: client, enumerable: false })
|
||||||
this.intents = intents
|
|
||||||
this.client = client
|
|
||||||
this.cache = new GatewayCache(client)
|
this.cache = new GatewayCache(client)
|
||||||
this.websocket = new WebSocket(
|
this.shards = shards
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
||||||
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
this.websocket.binaryType = 'arraybuffer'
|
|
||||||
this.websocket.onopen = this.onopen.bind(this)
|
|
||||||
this.websocket.onmessage = this.onmessage.bind(this)
|
|
||||||
this.websocket.onclose = this.onclose.bind(this)
|
|
||||||
this.websocket.onerror = this.onerror.bind(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onopen(): void {
|
private onopen(): void {
|
||||||
|
@ -85,7 +88,7 @@ export class Gateway extends EventEmitter {
|
||||||
}
|
}
|
||||||
if (data instanceof Uint8Array) {
|
if (data instanceof Uint8Array) {
|
||||||
data = unzlib(data)
|
data = unzlib(data)
|
||||||
data = new TextDecoder('utf-8').decode(data)
|
data = decodeText(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
|
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
|
||||||
|
@ -104,7 +107,7 @@ export class Gateway extends EventEmitter {
|
||||||
|
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
await this.sendIdentify(this.client.forceNewSession)
|
this.enqueueIdentify(this.client.forceNewSession)
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.sendResume()
|
this.sendResume()
|
||||||
|
@ -113,11 +116,9 @@ export class Gateway extends EventEmitter {
|
||||||
|
|
||||||
case GatewayOpcodes.HEARTBEAT_ACK:
|
case GatewayOpcodes.HEARTBEAT_ACK:
|
||||||
this.heartbeatServerResponded = true
|
this.heartbeatServerResponded = true
|
||||||
this.client.ping = Date.now() - this.lastPingTimestamp
|
this.ping = Date.now() - this.lastPingTimestamp
|
||||||
this.emit('ping', this.client.ping)
|
this.emit('ping', this.ping)
|
||||||
this.debug(
|
this.debug(`Received Heartbeat Ack. Ping Recognized: ${this.ping}ms`)
|
||||||
`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case GatewayOpcodes.INVALID_SESSION:
|
case GatewayOpcodes.INVALID_SESSION:
|
||||||
|
@ -127,14 +128,14 @@ export class Gateway extends EventEmitter {
|
||||||
)
|
)
|
||||||
if (d !== true) {
|
if (d !== true) {
|
||||||
this.debug(`Session was invalidated, deleting from cache`)
|
this.debug(`Session was invalidated, deleting from cache`)
|
||||||
await this.cache.delete('session_id')
|
await this.cache.delete(`session_id_${this.shards?.join('-') ?? '0'}`)
|
||||||
await this.cache.delete('seq')
|
await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`)
|
||||||
this.sessionID = undefined
|
this.sessionID = undefined
|
||||||
this.sequenceID = undefined
|
this.sequenceID = undefined
|
||||||
}
|
}
|
||||||
this.timedIdentify = setTimeout(async () => {
|
this.timedIdentify = setTimeout(async () => {
|
||||||
this.timedIdentify = null
|
this.timedIdentify = null
|
||||||
await this.sendIdentify(!(d as boolean))
|
this.enqueueIdentify(!(d as boolean))
|
||||||
}, 5000)
|
}, 5000)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -142,15 +143,15 @@ export class Gateway extends EventEmitter {
|
||||||
this.heartbeatServerResponded = true
|
this.heartbeatServerResponded = true
|
||||||
if (s !== null) {
|
if (s !== null) {
|
||||||
this.sequenceID = s
|
this.sequenceID = s
|
||||||
await this.cache.set('seq', s)
|
await this.cache.set(`seq_${this.shards?.join('-') ?? '0'}`, s)
|
||||||
}
|
}
|
||||||
if (t !== null && t !== undefined) {
|
if (t !== null && t !== undefined) {
|
||||||
this.emit(t, d)
|
this.emit(t as any, d)
|
||||||
this.client.emit('raw', t, d)
|
this.client.emit('raw', t, d)
|
||||||
|
|
||||||
const handler = gatewayHandlers[t]
|
const handler = gatewayHandlers[t]
|
||||||
|
|
||||||
if (handler !== undefined) {
|
if (handler !== undefined && d !== null) {
|
||||||
handler(this, d)
|
handler(this, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,15 +161,18 @@ export class Gateway extends EventEmitter {
|
||||||
// this.token = d.token
|
// this.token = d.token
|
||||||
this.sessionID = d.session_id
|
this.sessionID = d.session_id
|
||||||
this.sequenceID = d.seq
|
this.sequenceID = d.seq
|
||||||
await this.cache.set('seq', d.seq)
|
await this.cache.set(`seq_${this.shards?.join('-') ?? '0'}`, d.seq)
|
||||||
await this.cache.set('session_id', this.sessionID)
|
await this.cache.set(
|
||||||
|
`session_id_${this.shards?.join('-') ?? '0'}`,
|
||||||
|
this.sessionID
|
||||||
|
)
|
||||||
this.emit('resume')
|
this.emit('resume')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case GatewayOpcodes.RECONNECT: {
|
case GatewayOpcodes.RECONNECT: {
|
||||||
this.emit('reconnectRequired')
|
this.emit('reconnectRequired')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
this.debug('Received OpCode RECONNECT')
|
||||||
this.reconnect()
|
await this.reconnect()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -184,11 +188,12 @@ export class Gateway extends EventEmitter {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case GatewayCloseCodes.UNKNOWN_ERROR:
|
case GatewayCloseCodes.UNKNOWN_ERROR:
|
||||||
this.debug('API has encountered Unknown Error. Reconnecting...')
|
this.debug('API has encountered Unknown Error. Reconnecting...')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect()
|
||||||
this.reconnect()
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.UNKNOWN_OPCODE:
|
case GatewayCloseCodes.UNKNOWN_OPCODE:
|
||||||
throw new Error("Unknown OP Code was sent. This shouldn't happen!")
|
throw new Error(
|
||||||
|
"Invalid OP Code or Payload was sent. This shouldn't happen!"
|
||||||
|
)
|
||||||
case GatewayCloseCodes.DECODE_ERROR:
|
case GatewayCloseCodes.DECODE_ERROR:
|
||||||
throw new Error("Invalid Payload was sent. This shouldn't happen!")
|
throw new Error("Invalid Payload was sent. This shouldn't happen!")
|
||||||
case GatewayCloseCodes.NOT_AUTHENTICATED:
|
case GatewayCloseCodes.NOT_AUTHENTICATED:
|
||||||
|
@ -197,20 +202,17 @@ export class Gateway extends EventEmitter {
|
||||||
throw new Error('Invalid Token provided!')
|
throw new Error('Invalid Token provided!')
|
||||||
case GatewayCloseCodes.INVALID_SEQ:
|
case GatewayCloseCodes.INVALID_SEQ:
|
||||||
this.debug('Invalid Seq was sent. Reconnecting.')
|
this.debug('Invalid Seq was sent. Reconnecting.')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect()
|
||||||
this.reconnect()
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.RATE_LIMITED:
|
case GatewayCloseCodes.RATE_LIMITED:
|
||||||
throw new Error("You're ratelimited. Calm down.")
|
throw new Error("You're ratelimited. Calm down.")
|
||||||
case GatewayCloseCodes.SESSION_TIMED_OUT:
|
case GatewayCloseCodes.SESSION_TIMED_OUT:
|
||||||
this.debug('Session Timeout. Reconnecting.')
|
this.debug('Session Timeout. Reconnecting.')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect(true)
|
||||||
this.reconnect(true)
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.INVALID_SHARD:
|
case GatewayCloseCodes.INVALID_SHARD:
|
||||||
this.debug('Invalid Shard was sent. Reconnecting.')
|
this.debug('Invalid Shard was sent. Reconnecting.')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect()
|
||||||
this.reconnect()
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.SHARDING_REQUIRED:
|
case GatewayCloseCodes.SHARDING_REQUIRED:
|
||||||
throw new Error("Couldn't connect. Sharding is required!")
|
throw new Error("Couldn't connect. Sharding is required!")
|
||||||
|
@ -224,37 +226,49 @@ export class Gateway extends EventEmitter {
|
||||||
this.debug(
|
this.debug(
|
||||||
'Unknown Close code, probably connection error. Reconnecting in 5s.'
|
'Unknown Close code, probably connection error. Reconnecting in 5s.'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.timedIdentify !== null) {
|
if (this.timedIdentify !== null) {
|
||||||
clearTimeout(this.timedIdentify)
|
clearTimeout(this.timedIdentify)
|
||||||
this.debug('Timed Identify found. Cleared timeout.')
|
this.debug('Timed Identify found. Cleared timeout.')
|
||||||
}
|
}
|
||||||
|
|
||||||
await delay(5000)
|
await delay(5000)
|
||||||
await this.reconnect(true)
|
await this.reconnect(true)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onerror(event: Event | ErrorEvent): void {
|
private async onerror(event: ErrorEvent): Promise<void> {
|
||||||
const eventError = event as ErrorEvent
|
const error = new Error(
|
||||||
this.emit('error', eventError)
|
Deno.inspect({
|
||||||
|
message: event.message,
|
||||||
|
error: event.error,
|
||||||
|
type: event.type,
|
||||||
|
target: event.target
|
||||||
|
})
|
||||||
|
)
|
||||||
|
error.name = 'ErrorEvent'
|
||||||
|
console.log(error)
|
||||||
|
this.emit('error', error, event)
|
||||||
|
this.client.emit('gatewayError', event, this.shards)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enqueueIdentify(forceNew?: boolean): void {
|
||||||
|
this.client.shards.enqueueIdentify(
|
||||||
|
async () => await this.sendIdentify(forceNew)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendIdentify(forceNewSession?: boolean): Promise<void> {
|
private async sendIdentify(forceNewSession?: boolean): Promise<void> {
|
||||||
this.debug('Fetching /gateway/bot...')
|
if (typeof this.client.token !== 'string')
|
||||||
const info = await this.client.rest.get(GATEWAY_BOT())
|
throw new Error('Token not specified')
|
||||||
if (info.session_start_limit.remaining === 0)
|
if (typeof this.client.intents !== 'object')
|
||||||
throw new Error(
|
throw new Error('Intents not specified')
|
||||||
`Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms`
|
|
||||||
)
|
|
||||||
this.debug(`Recommended Shards: ${info.shards}`)
|
|
||||||
this.debug('=== Session Limit Info ===')
|
|
||||||
this.debug(
|
|
||||||
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
|
|
||||||
)
|
|
||||||
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
|
|
||||||
|
|
||||||
if (forceNewSession === undefined || !forceNewSession) {
|
if (forceNewSession === undefined || !forceNewSession) {
|
||||||
const sessionIDCached = await this.cache.get('session_id')
|
const sessionIDCached = await this.cache.get(
|
||||||
|
`session_id_${this.shards?.join('-') ?? '0'}`
|
||||||
|
)
|
||||||
if (sessionIDCached !== undefined) {
|
if (sessionIDCached !== undefined) {
|
||||||
this.debug(`Found Cached SessionID: ${sessionIDCached}`)
|
this.debug(`Found Cached SessionID: ${sessionIDCached}`)
|
||||||
this.sessionID = sessionIDCached
|
this.sessionID = sessionIDCached
|
||||||
|
@ -263,15 +277,18 @@ export class Gateway extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: IdentityPayload = {
|
const payload: IdentityPayload = {
|
||||||
token: this.token,
|
token: this.client.token,
|
||||||
properties: {
|
properties: {
|
||||||
$os: this.client.clientProperties.os ?? Deno.build.os,
|
$os: this.client.clientProperties.os ?? Deno.build.os,
|
||||||
$browser: this.client.clientProperties.browser ?? 'harmony',
|
$browser: this.client.clientProperties.browser ?? 'harmony',
|
||||||
$device: this.client.clientProperties.device ?? 'harmony'
|
$device: this.client.clientProperties.device ?? 'harmony'
|
||||||
},
|
},
|
||||||
compress: true,
|
compress: true,
|
||||||
shard: [0, 1], // TODO: Make sharding possible
|
shard:
|
||||||
intents: this.intents.reduce(
|
this.shards === undefined
|
||||||
|
? [0, 1]
|
||||||
|
: [this.shards[0] ?? 0, this.shards[1] ?? 1],
|
||||||
|
intents: this.client.intents.reduce(
|
||||||
(previous, current) => previous | current,
|
(previous, current) => previous | current,
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
|
@ -287,20 +304,27 @@ export class Gateway extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendResume(): Promise<void> {
|
private async sendResume(): Promise<void> {
|
||||||
|
if (typeof this.client.token !== 'string')
|
||||||
|
throw new Error('Token not specified')
|
||||||
|
|
||||||
if (this.sessionID === undefined) {
|
if (this.sessionID === undefined) {
|
||||||
this.sessionID = await this.cache.get('session_id')
|
this.sessionID = await this.cache.get(
|
||||||
if (this.sessionID === undefined) return await this.sendIdentify()
|
`session_id_${this.shards?.join('-') ?? '0'}`
|
||||||
|
)
|
||||||
|
if (this.sessionID === undefined) return this.enqueueIdentify()
|
||||||
}
|
}
|
||||||
this.debug(`Preparing to resume with Session: ${this.sessionID}`)
|
this.debug(`Preparing to resume with Session: ${this.sessionID}`)
|
||||||
if (this.sequenceID === undefined) {
|
if (this.sequenceID === undefined) {
|
||||||
const cached = await this.cache.get('seq')
|
const cached = await this.cache.get(
|
||||||
|
`seq_${this.shards?.join('-') ?? '0'}`
|
||||||
|
)
|
||||||
if (cached !== undefined)
|
if (cached !== undefined)
|
||||||
this.sequenceID = typeof cached === 'string' ? parseInt(cached) : cached
|
this.sequenceID = typeof cached === 'string' ? parseInt(cached) : cached
|
||||||
}
|
}
|
||||||
const resumePayload = {
|
const resumePayload = {
|
||||||
op: GatewayOpcodes.RESUME,
|
op: GatewayOpcodes.RESUME,
|
||||||
d: {
|
d: {
|
||||||
token: this.token,
|
token: this.client.token,
|
||||||
session_id: this.sessionID,
|
session_id: this.sessionID,
|
||||||
seq: this.sequenceID ?? null
|
seq: this.sequenceID ?? null
|
||||||
}
|
}
|
||||||
|
@ -320,8 +344,8 @@ export class Gateway extends EventEmitter {
|
||||||
op: GatewayOpcodes.REQUEST_GUILD_MEMBERS,
|
op: GatewayOpcodes.REQUEST_GUILD_MEMBERS,
|
||||||
d: {
|
d: {
|
||||||
guild_id: guild,
|
guild_id: guild,
|
||||||
query: options.query,
|
query: options.query ?? '',
|
||||||
limit: options.limit,
|
limit: options.limit ?? 0,
|
||||||
presences: options.presences,
|
presences: options.presences,
|
||||||
user_ids: options.users,
|
user_ids: options.users,
|
||||||
nonce
|
nonce
|
||||||
|
@ -345,8 +369,18 @@ export class Gateway extends EventEmitter {
|
||||||
: typeof channel === 'string'
|
: typeof channel === 'string'
|
||||||
? channel
|
? channel
|
||||||
: channel?.id,
|
: channel?.id,
|
||||||
self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute,
|
self_mute:
|
||||||
self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf
|
channel === undefined
|
||||||
|
? false
|
||||||
|
: voiceOptions.mute === undefined
|
||||||
|
? false
|
||||||
|
: voiceOptions.mute,
|
||||||
|
self_deaf:
|
||||||
|
channel === undefined
|
||||||
|
? false
|
||||||
|
: voiceOptions.deaf === undefined
|
||||||
|
? false
|
||||||
|
: voiceOptions.deaf
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -357,12 +391,15 @@ export class Gateway extends EventEmitter {
|
||||||
|
|
||||||
async reconnect(forceNew?: boolean): Promise<void> {
|
async reconnect(forceNew?: boolean): Promise<void> {
|
||||||
this.emit('reconnecting')
|
this.emit('reconnecting')
|
||||||
|
this.debug('Reconnecting... (force new: ' + String(forceNew) + ')')
|
||||||
|
|
||||||
clearInterval(this.heartbeatIntervalID)
|
clearInterval(this.heartbeatIntervalID)
|
||||||
if (forceNew === true) {
|
if (forceNew === true) {
|
||||||
await this.cache.delete('session_id')
|
await this.cache.delete(`session_id_${this.shards?.join('-') ?? '0'}`)
|
||||||
await this.cache.delete('seq')
|
await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`)
|
||||||
}
|
}
|
||||||
this.close(1000, RECONNECT_REASON)
|
|
||||||
|
this.closeGateway(1000, RECONNECT_REASON)
|
||||||
this.initWebsocket()
|
this.initWebsocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,30 +408,34 @@ export class Gateway extends EventEmitter {
|
||||||
this.debug('Initializing WebSocket...')
|
this.debug('Initializing WebSocket...')
|
||||||
this.websocket = new WebSocket(
|
this.websocket = new WebSocket(
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
|
`${Constants.DISCORD_GATEWAY_URL}/?v=${Constants.DISCORD_API_VERSION}&encoding=json`,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
this.websocket.binaryType = 'arraybuffer'
|
this.websocket.binaryType = 'arraybuffer'
|
||||||
this.websocket.onopen = this.onopen.bind(this)
|
this.websocket.onopen = this.onopen.bind(this)
|
||||||
this.websocket.onmessage = this.onmessage.bind(this)
|
this.websocket.onmessage = this.onmessage.bind(this)
|
||||||
this.websocket.onclose = this.onclose.bind(this)
|
this.websocket.onclose = this.onclose.bind(this)
|
||||||
this.websocket.onerror = this.onerror.bind(this)
|
this.websocket.onerror = this.onerror.bind(this) as any
|
||||||
}
|
}
|
||||||
|
|
||||||
close(code: number = 1000, reason?: string): void {
|
closeGateway(code: number = 1000, reason?: string): void {
|
||||||
return this.websocket.close(code, reason)
|
this.debug(
|
||||||
|
`Closing with code ${code}${
|
||||||
|
reason !== undefined && reason !== '' ? ` and reason ${reason}` : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
return this.websocket?.close(code, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
send(data: GatewayResponse): boolean {
|
send(data: GatewayResponse): boolean {
|
||||||
if (this.websocket.readyState !== this.websocket.OPEN) return false
|
if (this.websocket?.readyState !== this.websocket?.OPEN) return false
|
||||||
this.websocket.send(
|
const packet = JSON.stringify({
|
||||||
JSON.stringify({
|
op: data.op,
|
||||||
op: data.op,
|
d: data.d,
|
||||||
d: data.d,
|
s: typeof data.s === 'number' ? data.s : null,
|
||||||
s: typeof data.s === 'number' ? data.s : null,
|
t: data.t === undefined ? null : data.t
|
||||||
t: data.t === undefined ? null : data.t
|
})
|
||||||
})
|
this.websocket?.send(packet)
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
3
src/interactions/mod.ts
Normal file
3
src/interactions/mod.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './slashClient.ts'
|
||||||
|
export * from './slashModule.ts'
|
||||||
|
export * from './slashCommand.ts'
|
477
src/interactions/slashClient.ts
Normal file
477
src/interactions/slashClient.ts
Normal file
|
@ -0,0 +1,477 @@
|
||||||
|
import {
|
||||||
|
SlashCommandInteraction,
|
||||||
|
InteractionApplicationCommandResolved
|
||||||
|
} from '../structures/slash.ts'
|
||||||
|
import { Interaction } from '../structures/interactions.ts'
|
||||||
|
import {
|
||||||
|
InteractionPayload,
|
||||||
|
InteractionResponsePayload,
|
||||||
|
InteractionType
|
||||||
|
} from '../types/interactions.ts'
|
||||||
|
import { SlashCommandOptionType } from '../types/slashCommands.ts'
|
||||||
|
import type { Client } from '../client/mod.ts'
|
||||||
|
import { RESTManager } from '../rest/mod.ts'
|
||||||
|
import { SlashModule } from './slashModule.ts'
|
||||||
|
import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts'
|
||||||
|
import { User } from '../structures/user.ts'
|
||||||
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
import { encodeText, decodeText } from '../utils/encoding.ts'
|
||||||
|
import { SlashCommandsManager } from './slashCommand.ts'
|
||||||
|
|
||||||
|
export type SlashCommandHandlerCallback = (
|
||||||
|
interaction: SlashCommandInteraction
|
||||||
|
) => unknown
|
||||||
|
export interface SlashCommandHandler {
|
||||||
|
name: string
|
||||||
|
guild?: string
|
||||||
|
parent?: string
|
||||||
|
group?: string
|
||||||
|
handler: SlashCommandHandlerCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Options for SlashClient */
|
||||||
|
export interface SlashOptions {
|
||||||
|
id?: string | (() => string)
|
||||||
|
client?: Client
|
||||||
|
enabled?: boolean
|
||||||
|
token?: string
|
||||||
|
rest?: RESTManager
|
||||||
|
publicKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type SlashClientEvents = {
|
||||||
|
interaction: [Interaction]
|
||||||
|
interactionError: [Error]
|
||||||
|
ping: []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slash Client represents an Interactions Client which can be used without Harmony Client. */
|
||||||
|
export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> {
|
||||||
|
id: string | (() => string)
|
||||||
|
client?: Client
|
||||||
|
|
||||||
|
#token?: string
|
||||||
|
|
||||||
|
get token(): string | undefined {
|
||||||
|
return this.#token
|
||||||
|
}
|
||||||
|
|
||||||
|
set token(val: string | undefined) {
|
||||||
|
this.#token = val
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled: boolean = true
|
||||||
|
commands: SlashCommandsManager
|
||||||
|
handlers: SlashCommandHandler[] = []
|
||||||
|
readonly rest!: RESTManager
|
||||||
|
modules: SlashModule[] = []
|
||||||
|
publicKey?: string
|
||||||
|
|
||||||
|
constructor(options: SlashOptions) {
|
||||||
|
super()
|
||||||
|
let id = options.id
|
||||||
|
if (options.token !== undefined) id = atob(options.token?.split('.')[0])
|
||||||
|
if (id === undefined)
|
||||||
|
throw new Error('ID could not be found. Pass at least client or token')
|
||||||
|
this.id = id
|
||||||
|
|
||||||
|
if (options.client !== undefined) {
|
||||||
|
Object.defineProperty(this, 'client', {
|
||||||
|
value: options.client,
|
||||||
|
enumerable: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token = options.token
|
||||||
|
this.publicKey = options.publicKey
|
||||||
|
|
||||||
|
this.enabled = options.enabled ?? true
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
const client = this.client as any
|
||||||
|
if (client?._decoratedSlash !== undefined) {
|
||||||
|
client._decoratedSlash.forEach((e: any) => {
|
||||||
|
e.handler = e.handler.bind(this.client)
|
||||||
|
this.handlers.push(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this as any
|
||||||
|
if (self._decoratedSlash !== undefined) {
|
||||||
|
self._decoratedSlash.forEach((e: any) => {
|
||||||
|
e.handler = e.handler.bind(this.client)
|
||||||
|
self.handlers.push(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'rest', {
|
||||||
|
value:
|
||||||
|
options.client === undefined
|
||||||
|
? options.rest === undefined
|
||||||
|
? new RESTManager({
|
||||||
|
token: this.token
|
||||||
|
})
|
||||||
|
: options.rest
|
||||||
|
: options.client.rest,
|
||||||
|
enumerable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.client?.on(
|
||||||
|
'interactionCreate',
|
||||||
|
async (interaction) => await this._process(interaction)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.commands = new SlashCommandsManager(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
getID(): string {
|
||||||
|
return typeof this.id === 'string' ? this.id : this.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds a new Slash Command Handler */
|
||||||
|
handle(
|
||||||
|
cmd: string | SlashCommandHandler,
|
||||||
|
handler?: SlashCommandHandlerCallback
|
||||||
|
): SlashClient {
|
||||||
|
const handle = {
|
||||||
|
name: typeof cmd === 'string' ? cmd : cmd.name,
|
||||||
|
...(handler !== undefined ? { handler } : {}),
|
||||||
|
...(typeof cmd === 'string' ? {} : cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle.handler === undefined)
|
||||||
|
throw new Error('Invalid usage. Handler function not provided')
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof handle.name === 'string' &&
|
||||||
|
handle.name.includes(' ') &&
|
||||||
|
handle.parent === undefined &&
|
||||||
|
handle.group === undefined
|
||||||
|
) {
|
||||||
|
const parts = handle.name.split(/ +/).filter((e) => e !== '')
|
||||||
|
if (parts.length > 3 || parts.length < 1)
|
||||||
|
throw new Error('Invalid command name')
|
||||||
|
const root = parts.shift() as string
|
||||||
|
const group = parts.length === 2 ? parts.shift() : undefined
|
||||||
|
const sub = parts.shift()
|
||||||
|
|
||||||
|
handle.name = sub ?? root
|
||||||
|
handle.group = group
|
||||||
|
handle.parent = sub === undefined ? undefined : root
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers.push(handle as any)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load a Slash Module */
|
||||||
|
loadModule(module: SlashModule): SlashClient {
|
||||||
|
this.modules.push(module)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all Handlers. Including Slash Modules */
|
||||||
|
getHandlers(): SlashCommandHandler[] {
|
||||||
|
let res = this.handlers
|
||||||
|
for (const mod of this.modules) {
|
||||||
|
if (mod === undefined) continue
|
||||||
|
res = [
|
||||||
|
...res,
|
||||||
|
...mod.commands.map((cmd) => {
|
||||||
|
cmd.handler = cmd.handler.bind(mod)
|
||||||
|
return cmd
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */
|
||||||
|
private _getCommand(
|
||||||
|
i: SlashCommandInteraction
|
||||||
|
): SlashCommandHandler | undefined {
|
||||||
|
return this.getHandlers().find((e) => {
|
||||||
|
const hasGroupOrParent = e.group !== undefined || e.parent !== undefined
|
||||||
|
const groupMatched =
|
||||||
|
e.group !== undefined && e.parent !== undefined
|
||||||
|
? i.options
|
||||||
|
.find(
|
||||||
|
(o) =>
|
||||||
|
o.name === e.group &&
|
||||||
|
o.type === SlashCommandOptionType.SUB_COMMAND_GROUP
|
||||||
|
)
|
||||||
|
?.options?.find((o) => o.name === e.name) !== undefined
|
||||||
|
: true
|
||||||
|
const subMatched =
|
||||||
|
e.group === undefined && e.parent !== undefined
|
||||||
|
? i.options.find(
|
||||||
|
(o) =>
|
||||||
|
o.name === e.name &&
|
||||||
|
o.type === SlashCommandOptionType.SUB_COMMAND
|
||||||
|
) !== undefined
|
||||||
|
: true
|
||||||
|
const nameMatched1 = e.name === i.name
|
||||||
|
const parentMatched = hasGroupOrParent ? e.parent === i.name : true
|
||||||
|
const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1
|
||||||
|
|
||||||
|
const matched = groupMatched && subMatched && nameMatched
|
||||||
|
return matched
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Process an incoming Interaction */
|
||||||
|
private async _process(
|
||||||
|
interaction: Interaction | SlashCommandInteraction
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.enabled) return
|
||||||
|
|
||||||
|
if (interaction.type !== InteractionType.APPLICATION_COMMAND) return
|
||||||
|
|
||||||
|
const cmd =
|
||||||
|
this._getCommand(interaction as SlashCommandInteraction) ??
|
||||||
|
this.getHandlers().find((e) => e.name === '*')
|
||||||
|
if (cmd?.group !== undefined)
|
||||||
|
(interaction as SlashCommandInteraction).data.options =
|
||||||
|
(interaction as SlashCommandInteraction).data.options[0].options ?? []
|
||||||
|
if (cmd?.parent !== undefined)
|
||||||
|
(interaction as SlashCommandInteraction).data.options =
|
||||||
|
(interaction as SlashCommandInteraction).data.options[0].options ?? []
|
||||||
|
|
||||||
|
if (cmd === undefined) return
|
||||||
|
|
||||||
|
await this.emit('interaction', interaction)
|
||||||
|
try {
|
||||||
|
await cmd.handler(interaction as SlashCommandInteraction)
|
||||||
|
} catch (e) {
|
||||||
|
await this.emit('interactionError', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify HTTP based Interaction */
|
||||||
|
async verifyKey(
|
||||||
|
rawBody: string | Uint8Array,
|
||||||
|
signature: string | Uint8Array,
|
||||||
|
timestamp: string | Uint8Array
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (this.publicKey === undefined)
|
||||||
|
throw new Error('Public Key is not present')
|
||||||
|
|
||||||
|
const fullBody = new Uint8Array([
|
||||||
|
...(typeof timestamp === 'string' ? encodeText(timestamp) : timestamp),
|
||||||
|
...(typeof rawBody === 'string' ? encodeText(rawBody) : rawBody)
|
||||||
|
])
|
||||||
|
|
||||||
|
return edverify(signature, fullBody, this.publicKey).catch(() => false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction. **Data present in Interaction returned by this method is very different from actual typings as there is no real `Client` behind the scenes to cache things.** */
|
||||||
|
async verifyServerRequest(req: {
|
||||||
|
headers: Headers
|
||||||
|
method: string
|
||||||
|
body: Deno.Reader | Uint8Array
|
||||||
|
respond: (options: {
|
||||||
|
status?: number
|
||||||
|
headers?: Headers
|
||||||
|
body?: any
|
||||||
|
}) => Promise<void>
|
||||||
|
}): Promise<false | Interaction> {
|
||||||
|
if (req.method.toLowerCase() !== 'post') return false
|
||||||
|
|
||||||
|
const signature = req.headers.get('x-signature-ed25519')
|
||||||
|
const timestamp = req.headers.get('x-signature-timestamp')
|
||||||
|
if (signature === null || timestamp === null) return false
|
||||||
|
|
||||||
|
const rawbody =
|
||||||
|
req.body instanceof Uint8Array ? req.body : await Deno.readAll(req.body)
|
||||||
|
const verify = await this.verifyKey(rawbody, signature, timestamp)
|
||||||
|
if (!verify) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: InteractionPayload = JSON.parse(decodeText(rawbody))
|
||||||
|
|
||||||
|
// TODO: Maybe fix all this hackery going on here?
|
||||||
|
let res
|
||||||
|
if (payload.type === InteractionType.APPLICATION_COMMAND) {
|
||||||
|
res = new SlashCommandInteraction(this as any, payload, {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
user: new User(this as any, (payload.member?.user ?? payload.user)!),
|
||||||
|
member: payload.member as any,
|
||||||
|
guild: payload.guild_id as any,
|
||||||
|
channel: payload.channel_id as any,
|
||||||
|
resolved: (((payload.data as any)
|
||||||
|
?.resolved as unknown) as InteractionApplicationCommandResolved) ?? {
|
||||||
|
users: {},
|
||||||
|
members: {},
|
||||||
|
roles: {},
|
||||||
|
channels: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res = new Interaction(this as any, payload, {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
user: new User(this as any, (payload.member?.user ?? payload.user)!),
|
||||||
|
member: payload.member as any,
|
||||||
|
guild: payload.guild_id as any,
|
||||||
|
channel: payload.channel_id as any
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res._httpRespond = async (d: InteractionResponsePayload | FormData) =>
|
||||||
|
await req.respond({
|
||||||
|
status: 200,
|
||||||
|
headers: new Headers({
|
||||||
|
'content-type':
|
||||||
|
d instanceof FormData ? 'multipart/form-data' : 'application/json'
|
||||||
|
}),
|
||||||
|
body: d instanceof FormData ? d : JSON.stringify(d)
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify FetchEvent (for Service Worker usage) and return Interaction if valid */
|
||||||
|
async verifyFetchEvent({
|
||||||
|
request: req,
|
||||||
|
respondWith
|
||||||
|
}: {
|
||||||
|
respondWith: CallableFunction
|
||||||
|
request: Request
|
||||||
|
}): Promise<false | Interaction> {
|
||||||
|
if (req.bodyUsed === true) throw new Error('Request Body already used')
|
||||||
|
if (req.body === null) return false
|
||||||
|
const body = (await req.body.getReader().read()).value
|
||||||
|
if (body === undefined) return false
|
||||||
|
|
||||||
|
return await this.verifyServerRequest({
|
||||||
|
headers: req.headers,
|
||||||
|
body,
|
||||||
|
method: req.method,
|
||||||
|
respond: async (options) => {
|
||||||
|
await respondWith(
|
||||||
|
new Response(options.body, {
|
||||||
|
headers: options.headers,
|
||||||
|
status: options.status
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOpineRequest(req: any): Promise<boolean> {
|
||||||
|
const signature = req.headers.get('x-signature-ed25519')
|
||||||
|
const timestamp = req.headers.get('x-signature-timestamp')
|
||||||
|
const contentLength = req.headers.get('content-length')
|
||||||
|
|
||||||
|
if (signature === null || timestamp === null || contentLength === null)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const body = new Uint8Array(parseInt(contentLength))
|
||||||
|
await req.body.read(body)
|
||||||
|
|
||||||
|
const verified = await this.verifyKey(body, signature, timestamp)
|
||||||
|
if (!verified) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Middleware to verify request in Opine framework. */
|
||||||
|
async verifyOpineMiddleware(
|
||||||
|
req: any,
|
||||||
|
res: any,
|
||||||
|
next: CallableFunction
|
||||||
|
): Promise<any> {
|
||||||
|
const verified = await this.verifyOpineRequest(req)
|
||||||
|
if (!verified) return res.setStatus(401).end()
|
||||||
|
|
||||||
|
await next()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: create verifyOakMiddleware too
|
||||||
|
/** Method to verify Request from Oak server "Context". */
|
||||||
|
async verifyOakRequest(ctx: any): Promise<any> {
|
||||||
|
const signature = ctx.request.headers.get('x-signature-ed25519')
|
||||||
|
const timestamp = ctx.request.headers.get('x-signature-timestamp')
|
||||||
|
const contentLength = ctx.request.headers.get('content-length')
|
||||||
|
|
||||||
|
if (
|
||||||
|
signature === null ||
|
||||||
|
timestamp === null ||
|
||||||
|
contentLength === null ||
|
||||||
|
ctx.request.hasBody !== true
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await ctx.request.body().value
|
||||||
|
|
||||||
|
const verified = await this.verifyKey(body, signature, timestamp)
|
||||||
|
if (!verified) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decorator to create a Slash Command handler */
|
||||||
|
export function slash(name?: string, guild?: string) {
|
||||||
|
return function (client: Client | SlashClient | SlashModule, prop: string) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
const c = client as any
|
||||||
|
if (c._decoratedSlash === undefined) c._decoratedSlash = []
|
||||||
|
const item = (client as { [name: string]: any })[prop]
|
||||||
|
if (typeof item !== 'function') {
|
||||||
|
throw new Error('@slash decorator requires a function')
|
||||||
|
} else
|
||||||
|
c._decoratedSlash.push({
|
||||||
|
name: name ?? prop,
|
||||||
|
guild,
|
||||||
|
handler: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decorator to create a Sub-Slash Command handler */
|
||||||
|
export function subslash(parent: string, name?: string, guild?: string) {
|
||||||
|
return function (client: Client | SlashModule | SlashClient, prop: string) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
const c = client as any
|
||||||
|
if (c._decoratedSlash === undefined) c._decoratedSlash = []
|
||||||
|
const item = (client as { [name: string]: any })[prop]
|
||||||
|
if (typeof item !== 'function') {
|
||||||
|
throw new Error('@subslash decorator requires a function')
|
||||||
|
} else
|
||||||
|
c._decoratedSlash.push({
|
||||||
|
parent,
|
||||||
|
name: name ?? prop,
|
||||||
|
guild,
|
||||||
|
handler: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decorator to create a Grouped Slash Command handler */
|
||||||
|
export function groupslash(
|
||||||
|
parent: string,
|
||||||
|
group: string,
|
||||||
|
name?: string,
|
||||||
|
guild?: string
|
||||||
|
) {
|
||||||
|
return function (client: Client | SlashModule | SlashClient, prop: string) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
const c = client as any
|
||||||
|
if (c._decoratedSlash === undefined) c._decoratedSlash = []
|
||||||
|
const item = (client as { [name: string]: any })[prop]
|
||||||
|
if (typeof item !== 'function') {
|
||||||
|
throw new Error('@groupslash decorator requires a function')
|
||||||
|
} else
|
||||||
|
c._decoratedSlash.push({
|
||||||
|
group,
|
||||||
|
parent,
|
||||||
|
name: name ?? prop,
|
||||||
|
guild,
|
||||||
|
handler: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,14 @@
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { RESTManager } from '../rest/manager.ts'
|
||||||
import { Interaction } from '../structures/slash.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import {
|
import {
|
||||||
InteractionType,
|
|
||||||
SlashCommandChoice,
|
SlashCommandChoice,
|
||||||
SlashCommandOption,
|
SlashCommandOption,
|
||||||
SlashCommandOptionType,
|
SlashCommandOptionType,
|
||||||
SlashCommandPartial,
|
SlashCommandPartial,
|
||||||
SlashCommandPayload
|
SlashCommandPayload
|
||||||
} from '../types/slash.ts'
|
} from '../types/slashCommands.ts'
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import { Client } from './client.ts'
|
import type { SlashClient, SlashCommandHandlerCallback } from './slashClient.ts'
|
||||||
import { RESTManager } from './rest.ts'
|
|
||||||
import { SlashModule } from './slashModule.ts'
|
|
||||||
import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts'
|
|
||||||
import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts'
|
|
||||||
import {
|
|
||||||
Request as ORequest,
|
|
||||||
Response as OResponse
|
|
||||||
} from 'https://deno.land/x/opine@1.0.0/src/types.ts'
|
|
||||||
import { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts'
|
|
||||||
|
|
||||||
export class SlashCommand {
|
export class SlashCommand {
|
||||||
slash: SlashCommandsManager
|
slash: SlashCommandsManager
|
||||||
|
@ -27,15 +17,21 @@ export class SlashCommand {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
options: SlashCommandOption[]
|
options: SlashCommandOption[]
|
||||||
|
guild?: Guild
|
||||||
_guild?: string
|
_guild?: string
|
||||||
|
|
||||||
constructor(manager: SlashCommandsManager, data: SlashCommandPayload) {
|
constructor(
|
||||||
|
manager: SlashCommandsManager,
|
||||||
|
data: SlashCommandPayload,
|
||||||
|
guild?: Guild
|
||||||
|
) {
|
||||||
this.slash = manager
|
this.slash = manager
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.applicationID = data.application_id
|
this.applicationID = data.application_id
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.description = data.description
|
this.description = data.description
|
||||||
this.options = data.options ?? []
|
this.options = data.options ?? []
|
||||||
|
this.guild = guild
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(): Promise<void> {
|
async delete(): Promise<void> {
|
||||||
|
@ -154,6 +150,7 @@ function buildOptionsArray(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Slash Command Builder */
|
||||||
export class SlashBuilder {
|
export class SlashBuilder {
|
||||||
data: SlashCommandPartial
|
data: SlashCommandPartial
|
||||||
|
|
||||||
|
@ -199,15 +196,17 @@ export class SlashBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */
|
||||||
export class SlashCommandsManager {
|
export class SlashCommandsManager {
|
||||||
slash: SlashClient
|
readonly slash!: SlashClient
|
||||||
|
readonly rest!: RESTManager
|
||||||
get rest(): RESTManager {
|
|
||||||
return this.slash.rest
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(client: SlashClient) {
|
constructor(client: SlashClient) {
|
||||||
this.slash = client
|
Object.defineProperty(this, 'slash', { value: client, enumerable: false })
|
||||||
|
Object.defineProperty(this, 'rest', {
|
||||||
|
enumerable: false,
|
||||||
|
value: client.rest
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get all Global Slash Commands */
|
/** Get all Global Slash Commands */
|
||||||
|
@ -238,8 +237,13 @@ export class SlashCommandsManager {
|
||||||
].commands.get()) as SlashCommandPayload[]
|
].commands.get()) as SlashCommandPayload[]
|
||||||
if (!Array.isArray(res)) return col
|
if (!Array.isArray(res)) return col
|
||||||
|
|
||||||
|
const _guild =
|
||||||
|
typeof guild === 'object'
|
||||||
|
? guild
|
||||||
|
: await this.slash.client?.guilds.get(guild)
|
||||||
|
|
||||||
for (const raw of res) {
|
for (const raw of res) {
|
||||||
const cmd = new SlashCommand(this, raw)
|
const cmd = new SlashCommand(this, raw, _guild)
|
||||||
cmd._guild = typeof guild === 'string' ? guild : guild.id
|
cmd._guild = typeof guild === 'string' ? guild : guild.id
|
||||||
col.set(raw.id, cmd)
|
col.set(raw.id, cmd)
|
||||||
}
|
}
|
||||||
|
@ -261,7 +265,14 @@ export class SlashCommandsManager {
|
||||||
|
|
||||||
const payload = await route.post(data)
|
const payload = await route.post(data)
|
||||||
|
|
||||||
const cmd = new SlashCommand(this, payload)
|
const _guild =
|
||||||
|
typeof guild === 'object'
|
||||||
|
? guild
|
||||||
|
: guild === undefined
|
||||||
|
? undefined
|
||||||
|
: await this.slash.client?.guilds.get(guild)
|
||||||
|
|
||||||
|
const cmd = new SlashCommand(this, payload, _guild)
|
||||||
cmd._guild =
|
cmd._guild =
|
||||||
typeof guild === 'string' || guild === undefined ? guild : guild.id
|
typeof guild === 'string' || guild === undefined ? guild : guild.id
|
||||||
|
|
||||||
|
@ -312,238 +323,30 @@ export class SlashCommandsManager {
|
||||||
|
|
||||||
const data = await route.get()
|
const data = await route.get()
|
||||||
|
|
||||||
return new SlashCommand(this, data)
|
const _guild =
|
||||||
}
|
typeof guild === 'object'
|
||||||
}
|
? guild
|
||||||
|
: guild === undefined
|
||||||
|
? undefined
|
||||||
|
: await this.slash.client?.guilds.get(guild)
|
||||||
|
|
||||||
export type SlashCommandHandlerCallback = (interaction: Interaction) => any
|
return new SlashCommand(this, data, _guild)
|
||||||
export interface SlashCommandHandler {
|
|
||||||
name: string
|
|
||||||
guild?: string
|
|
||||||
parent?: string
|
|
||||||
group?: string
|
|
||||||
handler: SlashCommandHandlerCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashOptions {
|
|
||||||
id?: string | (() => string)
|
|
||||||
client?: Client
|
|
||||||
enabled?: boolean
|
|
||||||
token?: string
|
|
||||||
rest?: RESTManager
|
|
||||||
publicKey?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SlashClient {
|
|
||||||
id: string | (() => string)
|
|
||||||
client?: Client
|
|
||||||
token?: string
|
|
||||||
enabled: boolean = true
|
|
||||||
commands: SlashCommandsManager
|
|
||||||
handlers: SlashCommandHandler[] = []
|
|
||||||
rest: RESTManager
|
|
||||||
modules: SlashModule[] = []
|
|
||||||
publicKey?: string
|
|
||||||
|
|
||||||
_decoratedSlash?: Array<{
|
|
||||||
name: string
|
|
||||||
guild?: string
|
|
||||||
parent?: string
|
|
||||||
group?: string
|
|
||||||
handler: (interaction: Interaction) => any
|
|
||||||
}>
|
|
||||||
|
|
||||||
_decoratedSlashModules?: SlashModule[]
|
|
||||||
|
|
||||||
constructor(options: SlashOptions) {
|
|
||||||
let id = options.id
|
|
||||||
if (options.token !== undefined) id = atob(options.token?.split('.')[0])
|
|
||||||
if (id === undefined)
|
|
||||||
throw new Error('ID could not be found. Pass at least client or token')
|
|
||||||
this.id = id
|
|
||||||
this.client = options.client
|
|
||||||
this.token = options.token
|
|
||||||
this.commands = new SlashCommandsManager(this)
|
|
||||||
this.publicKey = options.publicKey
|
|
||||||
|
|
||||||
if (options !== undefined) {
|
|
||||||
this.enabled = options.enabled ?? true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.client?._decoratedSlash !== undefined) {
|
|
||||||
this.client._decoratedSlash.forEach((e) => {
|
|
||||||
this.handlers.push(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.client?._decoratedSlashModules !== undefined) {
|
|
||||||
this.client._decoratedSlashModules.forEach((e) => {
|
|
||||||
this.modules.push(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._decoratedSlash !== undefined) {
|
|
||||||
this._decoratedSlash.forEach((e) => {
|
|
||||||
this.handlers.push(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._decoratedSlashModules !== undefined) {
|
|
||||||
this._decoratedSlashModules.forEach((e) => {
|
|
||||||
this.modules.push(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rest =
|
|
||||||
options.client === undefined
|
|
||||||
? options.rest === undefined
|
|
||||||
? new RESTManager({
|
|
||||||
token: this.token
|
|
||||||
})
|
|
||||||
: options.rest
|
|
||||||
: options.client.rest
|
|
||||||
|
|
||||||
this.client?.on('interactionCreate', (interaction) =>
|
|
||||||
this._process(interaction)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getID(): string {
|
/** Bulk Edit Global or Guild Slash Commands */
|
||||||
return typeof this.id === 'string' ? this.id : this.id()
|
async bulkEdit(
|
||||||
}
|
cmds: Array<SlashCommandPartial | SlashCommandPayload>,
|
||||||
|
guild?: Guild | string
|
||||||
|
): Promise<SlashCommandsManager> {
|
||||||
|
const route =
|
||||||
|
guild === undefined
|
||||||
|
? this.rest.api.applications[this.slash.getID()].commands
|
||||||
|
: this.rest.api.applications[this.slash.getID()].guilds[
|
||||||
|
typeof guild === 'string' ? guild : guild.id
|
||||||
|
].commands
|
||||||
|
|
||||||
|
await route.put(cmds)
|
||||||
|
|
||||||
/** Adds a new Slash Command Handler */
|
|
||||||
handle(handler: SlashCommandHandler): SlashClient {
|
|
||||||
this.handlers.push(handler)
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
loadModule(module: SlashModule): SlashClient {
|
|
||||||
this.modules.push(module)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
getHandlers(): SlashCommandHandler[] {
|
|
||||||
let res = this.handlers
|
|
||||||
for (const mod of this.modules) {
|
|
||||||
if (mod === undefined) continue
|
|
||||||
res = [
|
|
||||||
...res,
|
|
||||||
...mod.commands.map((cmd) => {
|
|
||||||
cmd.handler = cmd.handler.bind(mod)
|
|
||||||
return cmd
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getCommand(i: Interaction): SlashCommandHandler | undefined {
|
|
||||||
return this.getHandlers().find((e) => {
|
|
||||||
const hasGroupOrParent = e.group !== undefined || e.parent !== undefined
|
|
||||||
const groupMatched =
|
|
||||||
e.group !== undefined && e.parent !== undefined
|
|
||||||
? i.options
|
|
||||||
.find((o) => o.name === e.group)
|
|
||||||
?.options?.find((o) => o.name === e.name) !== undefined
|
|
||||||
: true
|
|
||||||
const subMatched =
|
|
||||||
e.group === undefined && e.parent !== undefined
|
|
||||||
? i.options.find((o) => o.name === e.name) !== undefined
|
|
||||||
: true
|
|
||||||
const nameMatched1 = e.name === i.name
|
|
||||||
const parentMatched = hasGroupOrParent ? e.parent === i.name : true
|
|
||||||
const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1
|
|
||||||
|
|
||||||
const matched = groupMatched && subMatched && nameMatched
|
|
||||||
return matched
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Process an incoming Slash Command (interaction) */
|
|
||||||
private _process(interaction: Interaction): void {
|
|
||||||
if (!this.enabled) return
|
|
||||||
|
|
||||||
if (interaction.type !== InteractionType.APPLICATION_COMMAND) return
|
|
||||||
|
|
||||||
const cmd = this._getCommand(interaction)
|
|
||||||
|
|
||||||
if (cmd === undefined) return
|
|
||||||
|
|
||||||
cmd.handler(interaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyKey(
|
|
||||||
rawBody: string | Uint8Array | Buffer,
|
|
||||||
signature: string,
|
|
||||||
timestamp: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
if (this.publicKey === undefined)
|
|
||||||
throw new Error('Public Key is not present')
|
|
||||||
return edverify(
|
|
||||||
signature,
|
|
||||||
Buffer.concat([
|
|
||||||
Buffer.from(timestamp, 'utf-8'),
|
|
||||||
Buffer.from(
|
|
||||||
rawBody instanceof Uint8Array
|
|
||||||
? new TextDecoder().decode(rawBody)
|
|
||||||
: rawBody
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
this.publicKey
|
|
||||||
).catch(() => false)
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyOpineRequest(req: ORequest): Promise<boolean> {
|
|
||||||
const signature = req.headers.get('x-signature-ed25519')
|
|
||||||
const timestamp = req.headers.get('x-signature-timestamp')
|
|
||||||
const contentLength = req.headers.get('content-length')
|
|
||||||
|
|
||||||
if (signature === null || timestamp === null || contentLength === null)
|
|
||||||
return false
|
|
||||||
|
|
||||||
const body = new Uint8Array(parseInt(contentLength))
|
|
||||||
await req.body.read(body)
|
|
||||||
|
|
||||||
const verified = await this.verifyKey(body, signature, timestamp)
|
|
||||||
if (!verified) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Middleware to verify request in Opine framework. */
|
|
||||||
async verifyOpineMiddleware(
|
|
||||||
req: ORequest,
|
|
||||||
res: OResponse,
|
|
||||||
next: CallableFunction
|
|
||||||
): Promise<any> {
|
|
||||||
const verified = await this.verifyOpineRequest(req)
|
|
||||||
if (!verified) return res.setStatus(401).end()
|
|
||||||
|
|
||||||
await next()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: create verifyOakMiddleware too
|
|
||||||
/** Method to verify Request from Oak server "Context". */
|
|
||||||
async verifyOakRequest(ctx: Context): Promise<any> {
|
|
||||||
const signature = ctx.request.headers.get('x-signature-ed25519')
|
|
||||||
const timestamp = ctx.request.headers.get('x-signature-timestamp')
|
|
||||||
const contentLength = ctx.request.headers.get('content-length')
|
|
||||||
|
|
||||||
if (
|
|
||||||
signature === null ||
|
|
||||||
timestamp === null ||
|
|
||||||
contentLength === null ||
|
|
||||||
ctx.request.hasBody !== true
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await ctx.request.body().value
|
|
||||||
|
|
||||||
const verified = await this.verifyKey(body as any, signature, timestamp)
|
|
||||||
if (!verified) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
import { SlashCommandHandler } from './slashClient.ts'
|
import type { SlashCommandHandler } from './slashClient.ts'
|
||||||
|
|
||||||
export class SlashModule {
|
export class SlashModule {
|
||||||
name: string = ''
|
name: string = ''
|
||||||
commands: SlashCommandHandler[] = []
|
commands: SlashCommandHandler[] = []
|
||||||
_decoratedSlash?: SlashCommandHandler[]
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (this._decoratedSlash !== undefined) {
|
if ((this as any)._decoratedSlash !== undefined) {
|
||||||
this.commands = this._decoratedSlash
|
;(this as any).commands = (this as any)._decoratedSlash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
0
src/managers/_util.ts
Normal file
0
src/managers/_util.ts
Normal file
|
@ -1,4 +1,5 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
|
import { Base } from '../structures/base.ts'
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,15 +7,14 @@ import { Collection } from '../utils/collection.ts'
|
||||||
*
|
*
|
||||||
* You should not be making Managers yourself.
|
* You should not be making Managers yourself.
|
||||||
*/
|
*/
|
||||||
export class BaseManager<T, T2> {
|
export class BaseManager<T, T2> extends Base {
|
||||||
client: Client
|
|
||||||
/** Caches Name or Key used to differentiate caches */
|
/** Caches Name or Key used to differentiate caches */
|
||||||
cacheName: string
|
cacheName: string
|
||||||
/** Which data type does this cache have */
|
/** Which data type does this cache have */
|
||||||
DataType: any
|
DataType: any
|
||||||
|
|
||||||
constructor(client: Client, cacheName: string, DataType: any) {
|
constructor(client: Client, cacheName: string, DataType: any) {
|
||||||
this.client = client
|
super(client)
|
||||||
this.cacheName = cacheName
|
this.cacheName = cacheName
|
||||||
this.DataType = DataType
|
this.DataType = DataType
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,6 @@ export class BaseManager<T, T2> {
|
||||||
return this.client.cache.delete(this.cacheName, key)
|
return this.client.cache.delete(this.cacheName, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alias to _delete (cache) for compatibility purposes */
|
|
||||||
async delete(key: string): Promise<boolean> {
|
|
||||||
return await this._delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets an Array of values from Cache */
|
/** Gets an Array of values from Cache */
|
||||||
async array(): Promise<T2[]> {
|
async array(): Promise<T2[]> {
|
||||||
let arr = await (this.client.cache.array(this.cacheName) as T[])
|
let arr = await (this.client.cache.array(this.cacheName) as T[])
|
||||||
|
@ -65,8 +60,35 @@ export class BaseManager<T, T2> {
|
||||||
return collection
|
return collection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
|
||||||
|
const arr = (await this.array()) ?? []
|
||||||
|
const { readable, writable } = new TransformStream()
|
||||||
|
const writer = writable.getWriter()
|
||||||
|
arr.forEach((el: unknown) => writer.write(el))
|
||||||
|
writer.close()
|
||||||
|
yield* readable
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(...args: unknown[]): Promise<T2 | undefined> {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try to get value from cache, if not found then fetch */
|
||||||
|
async resolve(key: string): Promise<T2 | undefined> {
|
||||||
|
const cacheValue = await this.get(key)
|
||||||
|
if (cacheValue !== undefined) return cacheValue
|
||||||
|
else {
|
||||||
|
const fetchValue = await this.fetch(key).catch(() => undefined)
|
||||||
|
if (fetchValue !== undefined) return fetchValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Deletes everything from Cache */
|
/** Deletes everything from Cache */
|
||||||
flush(): any {
|
flush(): any {
|
||||||
return this.client.cache.deleteCache(this.cacheName)
|
return this.client.cache.deleteCache(this.cacheName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Deno.customInspect](): string {
|
||||||
|
return `Manager(${this.cacheName})`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
|
import { Base } from '../structures/base.ts'
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
/** Child Managers validate data from their parents i.e. from Managers */
|
/** Child Managers validate data from their parents i.e. from Managers */
|
||||||
export class BaseChildManager<T, T2> {
|
export class BaseChildManager<T, T2> extends Base {
|
||||||
client: Client
|
|
||||||
/** Parent Manager */
|
/** Parent Manager */
|
||||||
parent: BaseManager<T, T2>
|
parent: BaseManager<T, T2>
|
||||||
|
|
||||||
constructor(client: Client, parent: BaseManager<T, T2>) {
|
constructor(client: Client, parent: BaseManager<T, T2>) {
|
||||||
this.client = client
|
super(client)
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,4 +39,31 @@ export class BaseChildManager<T, T2> {
|
||||||
}
|
}
|
||||||
return collection
|
return collection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
|
||||||
|
const arr = (await this.array()) ?? []
|
||||||
|
const { readable, writable } = new TransformStream()
|
||||||
|
const writer = writable.getWriter()
|
||||||
|
arr.forEach((el: unknown) => writer.write(el))
|
||||||
|
writer.close()
|
||||||
|
yield* readable
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(...args: unknown[]): Promise<T2 | undefined> {
|
||||||
|
return this.parent.fetch(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try to get value from cache, if not found then fetch */
|
||||||
|
async resolve(key: string): Promise<T2 | undefined> {
|
||||||
|
const cacheValue = await this.get(key)
|
||||||
|
if (cacheValue !== undefined) return cacheValue
|
||||||
|
else {
|
||||||
|
const fetchValue = await this.fetch(key).catch(() => undefined)
|
||||||
|
if (fetchValue !== undefined) return fetchValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Deno.customInspect](): string {
|
||||||
|
return `ChildManager(${this.parent.cacheName})`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,40 @@
|
||||||
import { Client } from '../models/client.ts'
|
import { Client } from '../client/mod.ts'
|
||||||
import { Channel } from '../structures/channel.ts'
|
import { Channel } from '../structures/channel.ts'
|
||||||
import { ChannelPayload, GuildChannelPayload } from '../types/channel.ts'
|
import { Embed } from '../structures/embed.ts'
|
||||||
|
import { Message } from '../structures/message.ts'
|
||||||
|
import type { TextChannel } from '../structures/textChannel.ts'
|
||||||
|
import type { User } from '../structures/user.ts'
|
||||||
|
import type {
|
||||||
|
ChannelPayload,
|
||||||
|
GuildChannelPayload,
|
||||||
|
MessageOptions
|
||||||
|
} from '../types/channel.ts'
|
||||||
import { CHANNEL } from '../types/endpoint.ts'
|
import { CHANNEL } from '../types/endpoint.ts'
|
||||||
import getChannelByType from '../utils/getChannelByType.ts'
|
import getChannelByType from '../utils/channel.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
|
export type AllMessageOptions = MessageOptions | Embed
|
||||||
|
|
||||||
export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
||||||
constructor(client: Client) {
|
constructor(client: Client) {
|
||||||
super(client, 'channels', Channel)
|
super(client, 'channels', Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserDM(user: User | string): Promise<string | undefined> {
|
||||||
|
return this.client.cache.get(
|
||||||
|
'user_dms',
|
||||||
|
typeof user === 'string' ? user : user.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserDM(user: User | string, id: string): Promise<void> {
|
||||||
|
await this.client.cache.set(
|
||||||
|
'user_dms',
|
||||||
|
typeof user === 'string' ? user : user.id,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Override get method as Generic
|
// Override get method as Generic
|
||||||
async get<T = Channel>(key: string): Promise<T | undefined> {
|
async get<T = Channel>(key: string): Promise<T | undefined> {
|
||||||
const data = await this._get(key)
|
const data = await this._get(key)
|
||||||
|
@ -29,6 +54,7 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
||||||
const arr = await (this.client.cache.array(
|
const arr = await (this.client.cache.array(
|
||||||
this.cacheName
|
this.cacheName
|
||||||
) as ChannelPayload[])
|
) as ChannelPayload[])
|
||||||
|
if (arr === undefined) return []
|
||||||
const result: any[] = []
|
const result: any[] = []
|
||||||
for (const elem of arr) {
|
for (const elem of arr) {
|
||||||
let guild
|
let guild
|
||||||
|
@ -65,4 +91,109 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
||||||
.catch((e) => reject(e))
|
.catch((e) => reject(e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendMessage(
|
||||||
|
channel: string | TextChannel,
|
||||||
|
content?: string | AllMessageOptions,
|
||||||
|
option?: AllMessageOptions
|
||||||
|
): Promise<Message> {
|
||||||
|
const channelID = typeof channel === 'string' ? channel : channel.id
|
||||||
|
|
||||||
|
if (typeof content === 'object') {
|
||||||
|
option = content
|
||||||
|
content = undefined
|
||||||
|
}
|
||||||
|
if (content === undefined && option === undefined) {
|
||||||
|
throw new Error('Either text or option is necessary.')
|
||||||
|
}
|
||||||
|
if (option instanceof Embed) {
|
||||||
|
option = {
|
||||||
|
embed: option
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: any = {
|
||||||
|
content: content ?? option?.content,
|
||||||
|
embed: option?.embed,
|
||||||
|
file: option?.file,
|
||||||
|
files: option?.files,
|
||||||
|
tts: option?.tts,
|
||||||
|
allowed_mentions: option?.allowedMentions,
|
||||||
|
message_reference:
|
||||||
|
option?.reply === undefined
|
||||||
|
? undefined
|
||||||
|
: typeof option.reply === 'string'
|
||||||
|
? {
|
||||||
|
message_id: option.reply
|
||||||
|
}
|
||||||
|
: typeof option.reply === 'object'
|
||||||
|
? option.reply instanceof Message
|
||||||
|
? {
|
||||||
|
message_id: option.reply.id,
|
||||||
|
channel_id: option.reply.channel.id,
|
||||||
|
guild_id: option.reply.guild?.id
|
||||||
|
}
|
||||||
|
: option.reply
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.content === undefined && payload.embed === undefined) {
|
||||||
|
payload.content = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await this.client.rest.api.channels[channelID].messages.post(
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
const chan =
|
||||||
|
typeof channel === 'string'
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
(await this.get<TextChannel>(channel))!
|
||||||
|
: channel
|
||||||
|
const res = new Message(this.client, resp, chan, this.client.user as any)
|
||||||
|
await res.mentions.fromPayload(resp)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
async editMessage(
|
||||||
|
channel: string | TextChannel,
|
||||||
|
message: Message | string,
|
||||||
|
text?: string | MessageOptions,
|
||||||
|
option?: MessageOptions
|
||||||
|
): Promise<Message> {
|
||||||
|
const channelID = typeof channel === 'string' ? channel : channel.id
|
||||||
|
|
||||||
|
if (text === undefined && option === undefined) {
|
||||||
|
throw new Error('Either text or option is necessary.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.client.user === undefined) {
|
||||||
|
throw new Error('Client user has not initialized.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof text === 'object') {
|
||||||
|
if (typeof option === 'object') Object.assign(option, text)
|
||||||
|
else option = text
|
||||||
|
text = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMsg = await this.client.rest.api.channels[channelID].messages[
|
||||||
|
typeof message === 'string' ? message : message.id
|
||||||
|
].patch({
|
||||||
|
content: text ?? option?.content,
|
||||||
|
embed: option?.embed !== undefined ? option.embed.toJSON() : undefined,
|
||||||
|
// Cannot upload new files with Message
|
||||||
|
// file: option?.file,
|
||||||
|
tts: option?.tts,
|
||||||
|
allowed_mentions: option?.allowedMentions
|
||||||
|
})
|
||||||
|
|
||||||
|
const chan =
|
||||||
|
typeof channel === 'string'
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
(await this.get<TextChannel>(channel))!
|
||||||
|
: channel
|
||||||
|
const res = new Message(this.client, newMsg, chan, this.client.user)
|
||||||
|
await res.mentions.fromPayload(newMsg)
|
||||||
|
return res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Emoji } from '../structures/emoji.ts'
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
import { EmojiPayload } from '../types/emoji.ts'
|
import type { EmojiPayload } from '../types/emoji.ts'
|
||||||
import { GUILD_EMOJI } from '../types/endpoint.ts'
|
import { GUILD_EMOJI } from '../types/endpoint.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache Manager used for Caching values related to Gateway connection
|
* Cache Manager used for Caching values related to Gateway connection
|
||||||
|
|
39
src/managers/guildChannelVoiceStates.ts
Normal file
39
src/managers/guildChannelVoiceStates.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import type { Client } from '../client/mod.ts'
|
||||||
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
|
import type { VoiceStatePayload } from '../types/voice.ts'
|
||||||
|
import { VoiceState } from '../structures/voiceState.ts'
|
||||||
|
import { GuildVoiceStatesManager } from './guildVoiceStates.ts'
|
||||||
|
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
||||||
|
|
||||||
|
export class GuildChannelVoiceStatesManager extends BaseChildManager<
|
||||||
|
VoiceStatePayload,
|
||||||
|
VoiceState
|
||||||
|
> {
|
||||||
|
channel: VoiceChannel
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
client: Client,
|
||||||
|
parent: GuildVoiceStatesManager,
|
||||||
|
channel: VoiceChannel
|
||||||
|
) {
|
||||||
|
super(client, parent as any)
|
||||||
|
this.channel = channel
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string): Promise<VoiceState | undefined> {
|
||||||
|
const res = await this.parent.get(id)
|
||||||
|
if (res !== undefined && res.channel?.id === this.channel.id) return res
|
||||||
|
else return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async array(): Promise<VoiceState[]> {
|
||||||
|
const arr = (await this.parent.array()) as VoiceState[]
|
||||||
|
return arr.filter((c: any) => c.channel?.id === this.channel.id) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
async fromPayload(d: VoiceStatePayload[]): Promise<void> {
|
||||||
|
for (const data of d) {
|
||||||
|
await this.set(data.user_id, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,33 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Channel } from '../structures/channel.ts'
|
import { Channel } from '../structures/channel.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { Guild } from '../structures/guild.ts'
|
||||||
import { CategoryChannel } from '../structures/guildCategoryChannel.ts'
|
import type { CategoryChannel } from '../structures/guildCategoryChannel.ts'
|
||||||
import { GuildTextChannel } from '../structures/textChannel.ts'
|
|
||||||
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
|
||||||
import {
|
import {
|
||||||
GuildCategoryChannelPayload,
|
ChannelTypes,
|
||||||
GuildTextChannelPayload,
|
GuildChannelPayload,
|
||||||
GuildVoiceChannelPayload
|
OverwritePayload
|
||||||
} from '../types/channel.ts'
|
} from '../types/channel.ts'
|
||||||
import { CHANNEL } from '../types/endpoint.ts'
|
import type { GuildChannels, GuildChannelPayloads } from '../types/guild.ts'
|
||||||
|
import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.ts'
|
||||||
import { BaseChildManager } from './baseChild.ts'
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
import { ChannelsManager } from './channels.ts'
|
import type { ChannelsManager } from './channels.ts'
|
||||||
|
|
||||||
export type GuildChannelPayloads =
|
export interface CreateChannelOptions {
|
||||||
| GuildTextChannelPayload
|
name: string
|
||||||
| GuildVoiceChannelPayload
|
type?: ChannelTypes
|
||||||
| GuildCategoryChannelPayload
|
topic?: string
|
||||||
export type GuildChannel = GuildTextChannel | VoiceChannel | CategoryChannel
|
bitrate?: number
|
||||||
|
userLimit?: number
|
||||||
|
rateLimitPerUser?: number
|
||||||
|
position?: number
|
||||||
|
permissionOverwrites?: OverwritePayload[]
|
||||||
|
parent?: CategoryChannel | string
|
||||||
|
nsfw?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export class GuildChannelsManager extends BaseChildManager<
|
export class GuildChannelsManager extends BaseChildManager<
|
||||||
GuildChannelPayloads,
|
GuildChannelPayloads,
|
||||||
GuildChannel
|
GuildChannels
|
||||||
> {
|
> {
|
||||||
guild: Guild
|
guild: Guild
|
||||||
|
|
||||||
|
@ -30,7 +36,7 @@ export class GuildChannelsManager extends BaseChildManager<
|
||||||
this.guild = guild
|
this.guild = guild
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string): Promise<GuildChannel | undefined> {
|
async get(id: string): Promise<GuildChannels | undefined> {
|
||||||
const res = await this.parent.get(id)
|
const res = await this.parent.get(id)
|
||||||
if (res !== undefined && res.guild.id === this.guild.id) return res
|
if (res !== undefined && res.guild.id === this.guild.id) return res
|
||||||
else return undefined
|
else return undefined
|
||||||
|
@ -41,7 +47,7 @@ export class GuildChannelsManager extends BaseChildManager<
|
||||||
return this.client.rest.delete(CHANNEL(id))
|
return this.client.rest.delete(CHANNEL(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
async array(): Promise<GuildChannel[]> {
|
async array(): Promise<GuildChannels[]> {
|
||||||
const arr = (await this.parent.array()) as Channel[]
|
const arr = (await this.parent.array()) as Channel[]
|
||||||
return arr.filter(
|
return arr.filter(
|
||||||
(c: any) => c.guild !== undefined && c.guild.id === this.guild.id
|
(c: any) => c.guild !== undefined && c.guild.id === this.guild.id
|
||||||
|
@ -51,8 +57,51 @@ export class GuildChannelsManager extends BaseChildManager<
|
||||||
async flush(): Promise<boolean> {
|
async flush(): Promise<boolean> {
|
||||||
const arr = await this.array()
|
const arr = await this.array()
|
||||||
for (const elem of arr) {
|
for (const elem of arr) {
|
||||||
this.parent.delete(elem.id)
|
this.parent._delete(elem.id)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a new Guild Channel */
|
||||||
|
async create(options: CreateChannelOptions): Promise<GuildChannels> {
|
||||||
|
if (options.name === undefined)
|
||||||
|
throw new Error('name is required for GuildChannelsManager#create')
|
||||||
|
const res = ((await this.client.rest.post(GUILD_CHANNELS(this.guild.id), {
|
||||||
|
name: options.name,
|
||||||
|
type: options.type,
|
||||||
|
topic: options.topic,
|
||||||
|
bitrate: options.bitrate,
|
||||||
|
user_limit: options.userLimit,
|
||||||
|
rate_limit_per_user: options.rateLimitPerUser,
|
||||||
|
position: options.position,
|
||||||
|
permission_overwrites: options.permissionOverwrites,
|
||||||
|
parent_id:
|
||||||
|
options.parent === undefined
|
||||||
|
? undefined
|
||||||
|
: typeof options.parent === 'object'
|
||||||
|
? options.parent.id
|
||||||
|
: options.parent,
|
||||||
|
nsfw: options.nsfw
|
||||||
|
})) as unknown) as GuildChannelPayload
|
||||||
|
|
||||||
|
await this.set(res.id, res)
|
||||||
|
const channel = await this.get(res.id)
|
||||||
|
return (channel as unknown) as GuildChannels
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modify the positions of a set of channel positions for the guild. */
|
||||||
|
async editPositions(
|
||||||
|
...positions: Array<{ id: string | GuildChannels; position: number | null }>
|
||||||
|
): Promise<GuildChannelsManager> {
|
||||||
|
if (positions.length === 0)
|
||||||
|
throw new Error('No channel positions to change specified')
|
||||||
|
|
||||||
|
await this.client.rest.api.guilds[this.guild.id].channels.patch(
|
||||||
|
positions.map((e) => ({
|
||||||
|
id: typeof e.id === 'string' ? e.id : e.id.id,
|
||||||
|
position: e.position ?? null
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Emoji } from '../structures/emoji.ts'
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { EmojiPayload } from '../types/emoji.ts'
|
import type { EmojiPayload } from '../types/emoji.ts'
|
||||||
import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts'
|
import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts'
|
||||||
import { BaseChildManager } from './baseChild.ts'
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
import { EmojisManager } from './emojis.ts'
|
import type { EmojisManager } from './emojis.ts'
|
||||||
import { fetchAuto } from '../../deps.ts'
|
import { fetchAuto } from '../../deps.ts'
|
||||||
|
|
||||||
export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
|
export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
|
||||||
|
@ -88,7 +88,7 @@ export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
|
||||||
const arr = await this.array()
|
const arr = await this.array()
|
||||||
for (const elem of arr) {
|
for (const elem of arr) {
|
||||||
const emojiID = elem.id !== null ? elem.id : elem.name
|
const emojiID = elem.id !== null ? elem.id : elem.name
|
||||||
this.parent.delete(emojiID as string)
|
this.parent._delete(emojiID as string)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { VoiceState } from '../structures/voiceState.ts'
|
import { VoiceState } from '../structures/voiceState.ts'
|
||||||
import { VoiceStatePayload } from '../types/voice.ts'
|
import type { VoiceStatePayload } from '../types/voice.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export class GuildVoiceStatesManager extends BaseManager<
|
export class GuildVoiceStatesManager extends BaseManager<
|
||||||
|
@ -17,13 +17,20 @@ export class GuildVoiceStatesManager extends BaseManager<
|
||||||
this.guild = guild
|
this.guild = guild
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get Client's Voice State in the Guild */
|
||||||
|
async me(): Promise<VoiceState | undefined> {
|
||||||
|
const member = await this.guild.me()
|
||||||
|
return await this.get(member.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a Voice State by User ID */
|
||||||
async get(key: string): Promise<VoiceState | undefined> {
|
async get(key: string): Promise<VoiceState | undefined> {
|
||||||
const raw = await this._get(key)
|
const raw = await this._get(key)
|
||||||
if (raw === undefined) return
|
if (raw === undefined) return
|
||||||
|
|
||||||
const guild =
|
const guild =
|
||||||
raw.guild_id === undefined
|
raw.guild_id === undefined
|
||||||
? undefined
|
? this.guild
|
||||||
: await this.client.guilds.get(raw.guild_id)
|
: await this.client.guilds.get(raw.guild_id)
|
||||||
|
|
||||||
return new VoiceState(this.client, raw, {
|
return new VoiceState(this.client, raw, {
|
||||||
|
@ -50,7 +57,7 @@ export class GuildVoiceStatesManager extends BaseManager<
|
||||||
arr.map(async (raw) => {
|
arr.map(async (raw) => {
|
||||||
const guild =
|
const guild =
|
||||||
raw.guild_id === undefined
|
raw.guild_id === undefined
|
||||||
? undefined
|
? this.guild
|
||||||
: await this.client.guilds.get(raw.guild_id)
|
: await this.client.guilds.get(raw.guild_id)
|
||||||
|
|
||||||
return new VoiceState(this.client, raw, {
|
return new VoiceState(this.client, raw, {
|
||||||
|
|
|
@ -1,9 +1,24 @@
|
||||||
import { Client } from '../models/client.ts'
|
import { fetchAuto } from '../../deps.ts'
|
||||||
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { Guild } from '../structures/guild.ts'
|
||||||
import { GUILD } from '../types/endpoint.ts'
|
import type { Template } from '../structures/template.ts'
|
||||||
import { GuildPayload, MemberPayload } from '../types/guild.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
|
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
|
||||||
|
import type {
|
||||||
|
GuildPayload,
|
||||||
|
MemberPayload,
|
||||||
|
GuildCreateRolePayload,
|
||||||
|
GuildCreatePayload,
|
||||||
|
GuildCreateChannelPayload,
|
||||||
|
GuildPreview,
|
||||||
|
GuildPreviewPayload,
|
||||||
|
GuildModifyOptions,
|
||||||
|
GuildModifyPayload,
|
||||||
|
GuildCreateOptions
|
||||||
|
} from '../types/guild.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
import { MembersManager } from './members.ts'
|
import { MembersManager } from './members.ts'
|
||||||
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
|
|
||||||
export class GuildManager extends BaseManager<GuildPayload, Guild> {
|
export class GuildManager extends BaseManager<GuildPayload, Guild> {
|
||||||
constructor(client: Client) {
|
constructor(client: Client) {
|
||||||
|
@ -15,7 +30,7 @@ export class GuildManager extends BaseManager<GuildPayload, Guild> {
|
||||||
this.client.rest
|
this.client.rest
|
||||||
.get(GUILD(id))
|
.get(GUILD(id))
|
||||||
.then(async (data: any) => {
|
.then(async (data: any) => {
|
||||||
this.set(id, data)
|
await this.set(id, data)
|
||||||
|
|
||||||
const guild = new Guild(this.client, data)
|
const guild = new Guild(this.client, data)
|
||||||
|
|
||||||
|
@ -32,4 +47,207 @@ export class GuildManager extends BaseManager<GuildPayload, Guild> {
|
||||||
.catch((e) => reject(e))
|
.catch((e) => reject(e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a new guild based on a template. */
|
||||||
|
async createFromTemplate(
|
||||||
|
template: Template | string,
|
||||||
|
name: string,
|
||||||
|
icon?: string
|
||||||
|
): Promise<Guild> {
|
||||||
|
if (icon?.startsWith('http') === true) icon = await fetchAuto(icon)
|
||||||
|
const guild = await this.client.rest.api.guilds.templates[
|
||||||
|
typeof template === 'object' ? template.code : template
|
||||||
|
].post({ name, icon })
|
||||||
|
return new Guild(this.client, guild)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a guild. Returns Guild. Fires guildCreate event.
|
||||||
|
* @param options Options for creating a guild
|
||||||
|
*/
|
||||||
|
async create(options: GuildCreateOptions): Promise<Guild> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (options.icon !== undefined && !options.icon.startsWith('data:')) {
|
||||||
|
options.icon = await fetchAuto(options.icon)
|
||||||
|
}
|
||||||
|
if (options.roles !== undefined && options.roles[0].name !== '@everyone') {
|
||||||
|
options.roles.unshift({
|
||||||
|
id: Math.floor(Math.random() * 18392375458).toString(),
|
||||||
|
name: '@everyone'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: GuildCreatePayload = {
|
||||||
|
name: options.name,
|
||||||
|
region: options.region,
|
||||||
|
icon: options.icon,
|
||||||
|
verification_level: options.verificationLevel,
|
||||||
|
roles: options.roles?.map((obj) => {
|
||||||
|
let result: GuildCreateRolePayload
|
||||||
|
if (obj instanceof Role) {
|
||||||
|
result = {
|
||||||
|
id: obj.id,
|
||||||
|
name: obj.name,
|
||||||
|
color: obj.color,
|
||||||
|
hoist: obj.hoist,
|
||||||
|
position: obj.position,
|
||||||
|
permissions: obj.permissions.bitfield.toString(),
|
||||||
|
managed: obj.managed,
|
||||||
|
mentionable: obj.mentionable
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}),
|
||||||
|
channels: options.channels?.map(
|
||||||
|
(obj): GuildCreateChannelPayload => ({
|
||||||
|
id: obj.id,
|
||||||
|
name: obj.name,
|
||||||
|
type: obj.type,
|
||||||
|
parent_id: obj.parentID
|
||||||
|
})
|
||||||
|
),
|
||||||
|
afk_channel_id: options.afkChannelID,
|
||||||
|
afk_timeout: options.afkTimeout,
|
||||||
|
system_channel_id: options.systemChannelID
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: GuildPayload = await this.client.rest.post(GUILDS(), body)
|
||||||
|
const guild = new Guild(this.client, result)
|
||||||
|
|
||||||
|
return guild
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a preview of a guild. Returns GuildPreview.
|
||||||
|
* @param guildID Guild id
|
||||||
|
*/
|
||||||
|
async preview(guildID: string): Promise<GuildPreview> {
|
||||||
|
const resp: GuildPreviewPayload = await this.client.rest.get(
|
||||||
|
GUILD_PREVIEW(guildID)
|
||||||
|
)
|
||||||
|
|
||||||
|
const result: GuildPreview = {
|
||||||
|
id: resp.id,
|
||||||
|
name: resp.name,
|
||||||
|
icon: resp.icon,
|
||||||
|
splash: resp.splash,
|
||||||
|
discoverySplash: resp.discovery_splash,
|
||||||
|
emojis: resp.emojis.map((emoji) => new Emoji(this.client, emoji)),
|
||||||
|
features: resp.features,
|
||||||
|
approximateMemberCount: resp.approximate_member_count,
|
||||||
|
approximatePresenceCount: resp.approximate_presence_count,
|
||||||
|
description: resp.description
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets a value to Cache */
|
||||||
|
async set(key: string, value: GuildPayload): Promise<any> {
|
||||||
|
value = { ...value }
|
||||||
|
if ('roles' in value) value.roles = []
|
||||||
|
if ('emojis' in value) value.emojis = []
|
||||||
|
if ('members' in value) value.members = []
|
||||||
|
if ('presences' in value) value.presences = []
|
||||||
|
if ('voice_states' in value) value.voice_states = []
|
||||||
|
return this.client.cache.set(this.cacheName, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits a guild. Returns edited guild.
|
||||||
|
* @param guild Guild or guild id
|
||||||
|
* @param options Guild edit options
|
||||||
|
* @param asRaw true for get raw data, false for get guild(defaults to false)
|
||||||
|
*/
|
||||||
|
async edit(
|
||||||
|
guild: Guild | string,
|
||||||
|
options: GuildModifyOptions,
|
||||||
|
asRaw: false
|
||||||
|
): Promise<Guild>
|
||||||
|
async edit(
|
||||||
|
guild: Guild | string,
|
||||||
|
options: GuildModifyOptions,
|
||||||
|
asRaw: true
|
||||||
|
): Promise<GuildPayload>
|
||||||
|
async edit(
|
||||||
|
guild: Guild | string,
|
||||||
|
options: GuildModifyOptions,
|
||||||
|
asRaw: boolean = false
|
||||||
|
): Promise<Guild | GuildPayload> {
|
||||||
|
if (
|
||||||
|
options.icon !== undefined &&
|
||||||
|
options.icon !== null &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
!options.icon.startsWith('data:')
|
||||||
|
) {
|
||||||
|
options.icon = await fetchAuto(options.icon)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.splash !== undefined &&
|
||||||
|
options.splash !== null &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
!options.splash.startsWith('data:')
|
||||||
|
) {
|
||||||
|
options.splash = await fetchAuto(options.splash)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.banner !== undefined &&
|
||||||
|
options.banner !== null &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
!options.banner.startsWith('data:')
|
||||||
|
) {
|
||||||
|
options.banner = await fetchAuto(options.banner)
|
||||||
|
}
|
||||||
|
if (guild instanceof Guild) {
|
||||||
|
guild = guild.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: GuildModifyPayload = {
|
||||||
|
name: options.name,
|
||||||
|
region: options.region,
|
||||||
|
verification_level: options.verificationLevel,
|
||||||
|
default_message_notifications: options.defaultMessageNotifications,
|
||||||
|
explicit_content_filter: options.explicitContentFilter,
|
||||||
|
afk_channel_id: options.afkChannelID,
|
||||||
|
afk_timeout: options.afkTimeout,
|
||||||
|
owner_id: options.ownerID,
|
||||||
|
icon: options.icon,
|
||||||
|
splash: options.splash,
|
||||||
|
banner: options.banner,
|
||||||
|
system_channel_id: options.systemChannelID,
|
||||||
|
rules_channel_id: options.rulesChannelID,
|
||||||
|
public_updates_channel_id: options.publicUpdatesChannelID,
|
||||||
|
preferred_locale: options.preferredLocale
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: GuildPayload = await this.client.rest.patch(
|
||||||
|
GUILD(guild),
|
||||||
|
body
|
||||||
|
)
|
||||||
|
|
||||||
|
if (asRaw) {
|
||||||
|
const guild = new Guild(this.client, result)
|
||||||
|
return guild
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a guild. Returns deleted guild.
|
||||||
|
* @param guild Guild or guild id
|
||||||
|
*/
|
||||||
|
async delete(guild: Guild | string): Promise<Guild | undefined> {
|
||||||
|
if (guild instanceof Guild) {
|
||||||
|
guild = guild.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldGuild = await this.get(guild)
|
||||||
|
|
||||||
|
await this.client.rest.delete(GUILD(guild))
|
||||||
|
return oldGuild
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { GuildTextChannel, User } from '../../mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Invite } from '../structures/invite.ts'
|
import { Invite } from '../structures/invite.ts'
|
||||||
import { INVITE } from '../types/endpoint.ts'
|
import { CHANNEL_INVITES, GUILD_INVITES, INVITE } from '../types/endpoint.ts'
|
||||||
import { InvitePayload } from '../types/invite.ts'
|
import type { InvitePayload } from '../types/invite.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
|
export enum InviteTargetUserType {
|
||||||
|
STREAM = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateInviteOptions {
|
||||||
|
maxAge?: number
|
||||||
|
maxUses?: number
|
||||||
|
temporary?: boolean
|
||||||
|
unique?: boolean
|
||||||
|
targetUser?: string | User
|
||||||
|
targetUserType?: InviteTargetUserType
|
||||||
|
}
|
||||||
|
|
||||||
export class InviteManager extends BaseManager<InvitePayload, Invite> {
|
export class InviteManager extends BaseManager<InvitePayload, Invite> {
|
||||||
guild: Guild
|
guild: Guild
|
||||||
|
|
||||||
|
@ -20,10 +34,10 @@ export class InviteManager extends BaseManager<InvitePayload, Invite> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch an Invite */
|
/** Fetch an Invite */
|
||||||
async fetch(id: string): Promise<Invite> {
|
async fetch(id: string, withCounts: boolean = true): Promise<Invite> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
this.client.rest
|
this.client.rest
|
||||||
.get(INVITE(id))
|
.get(`${INVITE(id)}${withCounts ? '?with_counts=true' : ''}`)
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
this.set(id, data as InvitePayload)
|
this.set(id, data as InvitePayload)
|
||||||
const newInvite = await this.get(data.code)
|
const newInvite = await this.get(data.code)
|
||||||
|
@ -33,6 +47,57 @@ export class InviteManager extends BaseManager<InvitePayload, Invite> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch all Invites of a Guild or a specific Channel */
|
||||||
|
async fetchAll(channel?: string | GuildTextChannel): Promise<Invite[]> {
|
||||||
|
const rawInvites = (await this.client.rest.get(
|
||||||
|
channel === undefined
|
||||||
|
? GUILD_INVITES(this.guild.id)
|
||||||
|
: CHANNEL_INVITES(typeof channel === 'string' ? channel : channel.id)
|
||||||
|
)) as InvitePayload[]
|
||||||
|
|
||||||
|
const res: Invite[] = []
|
||||||
|
|
||||||
|
for (const raw of rawInvites) {
|
||||||
|
await this.set(raw.code, raw)
|
||||||
|
res.push(new Invite(this.client, raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete an Invite */
|
||||||
|
async delete(invite: string | Invite): Promise<boolean> {
|
||||||
|
await this.client.rest.delete(
|
||||||
|
INVITE(typeof invite === 'string' ? invite : invite.code)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create an Invite */
|
||||||
|
async create(
|
||||||
|
channel: string | GuildTextChannel,
|
||||||
|
options?: CreateInviteOptions
|
||||||
|
): Promise<Invite> {
|
||||||
|
const raw = ((await this.client.rest.post(
|
||||||
|
CHANNEL_INVITES(typeof channel === 'string' ? channel : channel.id),
|
||||||
|
{
|
||||||
|
max_age: options?.maxAge,
|
||||||
|
max_uses: options?.maxUses,
|
||||||
|
temporary: options?.temporary,
|
||||||
|
unique: options?.unique,
|
||||||
|
target_user:
|
||||||
|
options?.targetUser === undefined
|
||||||
|
? undefined
|
||||||
|
: typeof options.targetUser === 'string'
|
||||||
|
? options.targetUser
|
||||||
|
: options.targetUser.id,
|
||||||
|
target_user_type: options?.targetUserType
|
||||||
|
}
|
||||||
|
)) as unknown) as InvitePayload
|
||||||
|
|
||||||
|
return new Invite(this.client, raw)
|
||||||
|
}
|
||||||
|
|
||||||
async fromPayload(invites: InvitePayload[]): Promise<boolean> {
|
async fromPayload(invites: InvitePayload[]): Promise<boolean> {
|
||||||
for (const invite of invites) {
|
for (const invite of invites) {
|
||||||
await this.set(invite.code, invite)
|
await this.set(invite.code, invite)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { BaseChildManager } from './baseChild.ts'
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
import { RolePayload } from '../types/role.ts'
|
import type { RolePayload } from '../types/role.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { Member } from '../structures/member.ts'
|
import type { Member } from '../structures/member.ts'
|
||||||
import { RolesManager } from './roles.ts'
|
import type { RolesManager } from './roles.ts'
|
||||||
import { MemberPayload } from '../types/guild.ts'
|
import type { MemberPayload } from '../types/guild.ts'
|
||||||
import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts'
|
import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts'
|
||||||
|
|
||||||
export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
|
export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
|
||||||
|
@ -42,7 +42,7 @@ export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
|
||||||
async flush(): Promise<boolean> {
|
async flush(): Promise<boolean> {
|
||||||
const arr = await this.array()
|
const arr = await this.array()
|
||||||
for (const elem of arr) {
|
for (const elem of arr) {
|
||||||
this.parent.delete(elem.id)
|
this.parent._delete(elem.id)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Member } from '../structures/member.ts'
|
import { Member } from '../structures/member.ts'
|
||||||
import { GUILD_MEMBER } from '../types/endpoint.ts'
|
import { GUILD_MEMBER } from '../types/endpoint.ts'
|
||||||
import { MemberPayload } from '../types/guild.ts'
|
import type { MemberPayload } from '../types/guild.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
import { Permissions } from '../utils/permissions.ts'
|
import { Permissions } from '../utils/permissions.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Emoji } from '../structures/emoji.ts'
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Message } from '../structures/message.ts'
|
||||||
import { Message } from '../structures/message.ts'
|
|
||||||
import { MessageReaction } from '../structures/messageReaction.ts'
|
import { MessageReaction } from '../structures/messageReaction.ts'
|
||||||
import { Reaction } from '../types/channel.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { MESSAGE_REACTION, MESSAGE_REACTIONS } from '../types/endpoint.ts'
|
import type { Reaction } from '../types/channel.ts'
|
||||||
|
import {
|
||||||
|
MESSAGE_REACTION,
|
||||||
|
MESSAGE_REACTIONS,
|
||||||
|
MESSAGE_REACTION_USER
|
||||||
|
} from '../types/endpoint.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export class MessageReactionsManager extends BaseManager<
|
export class MessageReactionsManager extends BaseManager<
|
||||||
|
@ -14,10 +18,18 @@ export class MessageReactionsManager extends BaseManager<
|
||||||
message: Message
|
message: Message
|
||||||
|
|
||||||
constructor(client: Client, message: Message) {
|
constructor(client: Client, message: Message) {
|
||||||
super(client, `reactions:${message.id}`, Guild)
|
super(client, `reactions:${message.id}`, MessageReaction)
|
||||||
this.message = message
|
this.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateRefs(): Promise<void> {
|
||||||
|
const newVal = await this.message.channel.messages.get(this.message.id)
|
||||||
|
if (newVal !== undefined) {
|
||||||
|
this.message = newVal
|
||||||
|
}
|
||||||
|
await this.message.updateRefs()
|
||||||
|
}
|
||||||
|
|
||||||
async get(id: string): Promise<MessageReaction | undefined> {
|
async get(id: string): Promise<MessageReaction | undefined> {
|
||||||
const raw = await this._get(id)
|
const raw = await this._get(id)
|
||||||
if (raw === undefined) return
|
if (raw === undefined) return
|
||||||
|
@ -27,6 +39,7 @@ export class MessageReactionsManager extends BaseManager<
|
||||||
let emoji = await this.client.emojis.get(emojiID as string)
|
let emoji = await this.client.emojis.get(emojiID as string)
|
||||||
if (emoji === undefined) emoji = new Emoji(this.client, raw.emoji)
|
if (emoji === undefined) emoji = new Emoji(this.client, raw.emoji)
|
||||||
|
|
||||||
|
await this.updateRefs()
|
||||||
const reaction = new MessageReaction(this.client, raw, this.message, emoji)
|
const reaction = new MessageReaction(this.client, raw, this.message, emoji)
|
||||||
return reaction
|
return reaction
|
||||||
}
|
}
|
||||||
|
@ -77,4 +90,23 @@ export class MessageReactionsManager extends BaseManager<
|
||||||
)
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Remove a specific Emoji from Reactions */
|
||||||
|
async removeUser(
|
||||||
|
emoji: Emoji | string,
|
||||||
|
user: User | string
|
||||||
|
): Promise<MessageReactionsManager> {
|
||||||
|
const val = encodeURIComponent(
|
||||||
|
(typeof emoji === 'object' ? emoji.id ?? emoji.name : emoji) as string
|
||||||
|
)
|
||||||
|
await this.client.rest.delete(
|
||||||
|
MESSAGE_REACTION_USER(
|
||||||
|
this.message.channel.id,
|
||||||
|
this.message.id,
|
||||||
|
val,
|
||||||
|
typeof user === 'string' ? user : user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Message } from '../structures/message.ts'
|
import { Message } from '../structures/message.ts'
|
||||||
import { TextChannel } from '../structures/textChannel.ts'
|
import type { TextChannel } from '../structures/textChannel.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { MessagePayload } from '../types/channel.ts'
|
import type { MessagePayload } from '../types/channel.ts'
|
||||||
import { CHANNEL_MESSAGE } from '../types/endpoint.ts'
|
import { CHANNEL_MESSAGE } from '../types/endpoint.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
|
@ -93,18 +93,12 @@ export class MessagesManager extends BaseManager<MessagePayload, Message> {
|
||||||
if (channel === undefined)
|
if (channel === undefined)
|
||||||
channel = await this.client.channels.fetch(this.channel.id)
|
channel = await this.client.channels.fetch(this.channel.id)
|
||||||
|
|
||||||
const author = new User(this.client, (data as MessagePayload).author)
|
|
||||||
await this.client.users.set(
|
await this.client.users.set(
|
||||||
author.id,
|
data.author.id,
|
||||||
(data as MessagePayload).author
|
(data as MessagePayload).author
|
||||||
)
|
)
|
||||||
|
|
||||||
const res = new Message(
|
const res = (await this.get(data.id)) as Message
|
||||||
this.client,
|
|
||||||
data as MessagePayload,
|
|
||||||
channel as TextChannel,
|
|
||||||
author
|
|
||||||
)
|
|
||||||
|
|
||||||
await res.mentions.fromPayload(data)
|
await res.mentions.fromPayload(data)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Presence } from '../structures/presence.ts'
|
import { Presence } from '../structures/presence.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { PresenceUpdatePayload } from '../types/gateway.ts'
|
import type { PresenceUpdatePayload } from '../types/gateway.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export class GuildPresencesManager extends BaseManager<
|
export class GuildPresencesManager extends BaseManager<
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { MessageReaction } from '../structures/messageReaction.ts'
|
import type { MessageReaction } from '../structures/messageReaction.ts'
|
||||||
|
import type { User } from '../structures/user.ts'
|
||||||
import { UsersManager } from './users.ts'
|
import { UsersManager } from './users.ts'
|
||||||
|
|
||||||
export class ReactionUsersManager extends UsersManager {
|
export class ReactionUsersManager extends UsersManager {
|
||||||
|
@ -10,4 +11,14 @@ export class ReactionUsersManager extends UsersManager {
|
||||||
this.cacheName = `reaction_users:${reaction.message.id}`
|
this.cacheName = `reaction_users:${reaction.message.id}`
|
||||||
this.reaction = reaction
|
this.reaction = reaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Remove all Users from this Reaction */
|
||||||
|
async removeAll(): Promise<void> {
|
||||||
|
await this.reaction.message.reactions.removeEmoji(this.reaction.emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a specific User from this Reaction */
|
||||||
|
async remove(user: User | string): Promise<void> {
|
||||||
|
await this.reaction.message.reactions.removeUser(this.reaction.emoji, user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Permissions } from '../../mod.ts'
|
import { Permissions } from '../../mod.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts'
|
import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts'
|
||||||
import { RolePayload } from '../types/role.ts'
|
import type { RoleModifyPayload, RolePayload } from '../types/role.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export interface CreateGuildRoleOptions {
|
export interface CreateGuildRoleOptions {
|
||||||
|
@ -22,19 +22,34 @@ export class RolesManager extends BaseManager<RolePayload, Role> {
|
||||||
this.guild = guild
|
this.guild = guild
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch a Guild Role (from API) */
|
/** Fetch All Guild Roles */
|
||||||
async fetch(id: string): Promise<Role> {
|
async fetchAll(): Promise<Role[]> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
this.client.rest
|
this.client.rest.api.guilds[this.guild.id].roles.get
|
||||||
.get(GUILD_ROLE(this.guild.id, id))
|
.then(async (data: RolePayload[]) => {
|
||||||
.then(async (data) => {
|
const roles: Role[] = []
|
||||||
await this.set(id, data as RolePayload)
|
for (const raw of data) {
|
||||||
resolve(((await this.get(id)) as unknown) as Role)
|
await this.set(raw.id, raw)
|
||||||
|
roles.push(new Role(this.client, raw, this.guild))
|
||||||
|
}
|
||||||
|
resolve(roles)
|
||||||
})
|
})
|
||||||
.catch((e) => reject(e))
|
.catch((e) => reject(e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get(key: string): Promise<Role | undefined> {
|
||||||
|
const raw = await this._get(key)
|
||||||
|
if (raw === undefined) return
|
||||||
|
return new Role(this.client, raw, this.guild)
|
||||||
|
}
|
||||||
|
|
||||||
|
async array(): Promise<Role[]> {
|
||||||
|
let arr = await (this.client.cache.array(this.cacheName) as RolePayload[])
|
||||||
|
if (arr === undefined) arr = []
|
||||||
|
return arr.map((e) => new Role(this.client, e, this.guild))
|
||||||
|
}
|
||||||
|
|
||||||
async fromPayload(roles: RolePayload[]): Promise<boolean> {
|
async fromPayload(roles: RolePayload[]): Promise<boolean> {
|
||||||
for (const role of roles) {
|
for (const role of roles) {
|
||||||
await this.set(role.id, role)
|
await this.set(role.id, role)
|
||||||
|
@ -74,10 +89,41 @@ export class RolesManager extends BaseManager<RolePayload, Role> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delete a Guild Role */
|
/** Delete a Guild Role */
|
||||||
async delete(role: Role | string): Promise<boolean> {
|
async delete(role: Role | string): Promise<Role | undefined> {
|
||||||
|
const oldRole = await this.get(typeof role === 'object' ? role.id : role)
|
||||||
|
|
||||||
await this.client.rest.delete(
|
await this.client.rest.delete(
|
||||||
GUILD_ROLE(this.guild.id, typeof role === 'object' ? role.id : role)
|
GUILD_ROLE(this.guild.id, typeof role === 'object' ? role.id : role)
|
||||||
)
|
)
|
||||||
return true
|
|
||||||
|
return oldRole
|
||||||
|
}
|
||||||
|
|
||||||
|
async edit(role: Role | string, options: RoleModifyPayload): Promise<Role> {
|
||||||
|
if (role instanceof Role) {
|
||||||
|
role = role.id
|
||||||
|
}
|
||||||
|
const resp: RolePayload = await this.client.rest.patch(
|
||||||
|
GUILD_ROLE(this.guild.id, role),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Role(this.client, resp, this.guild)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modify the positions of a set of role positions for the guild. */
|
||||||
|
async editPositions(
|
||||||
|
...positions: Array<{ id: string | Role; position: number | null }>
|
||||||
|
): Promise<RolesManager> {
|
||||||
|
if (positions.length === 0)
|
||||||
|
throw new Error('No role positions to change specified')
|
||||||
|
|
||||||
|
await this.client.rest.api.guilds[this.guild.id].roles.patch(
|
||||||
|
positions.map((e) => ({
|
||||||
|
id: typeof e.id === 'string' ? e.id : e.id.id,
|
||||||
|
position: e.position ?? null
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { USER } from '../types/endpoint.ts'
|
import { USER } from '../types/endpoint.ts'
|
||||||
import { UserPayload } from '../types/user.ts'
|
import type { UserPayload } from '../types/user.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export class UsersManager extends BaseManager<UserPayload, User> {
|
export class UsersManager extends BaseManager<UserPayload, User> {
|
||||||
|
|
|
@ -1,376 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
|
||||||
import { User } from '../structures/user.ts'
|
|
||||||
import { GatewayIntents } from '../types/gateway.ts'
|
|
||||||
import { Gateway } from '../gateway/index.ts'
|
|
||||||
import { RESTManager } from './rest.ts'
|
|
||||||
import { EventEmitter } from '../../deps.ts'
|
|
||||||
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
|
|
||||||
import { UsersManager } from '../managers/users.ts'
|
|
||||||
import { GuildManager } from '../managers/guilds.ts'
|
|
||||||
import { ChannelsManager } from '../managers/channels.ts'
|
|
||||||
import { ClientPresence } from '../structures/presence.ts'
|
|
||||||
import { EmojisManager } from '../managers/emojis.ts'
|
|
||||||
import { ActivityGame, ClientActivity } from '../types/presence.ts'
|
|
||||||
import { Extension } from './extensions.ts'
|
|
||||||
import { SlashClient } from './slashClient.ts'
|
|
||||||
import { Interaction } from '../structures/slash.ts'
|
|
||||||
import { SlashModule } from './slashModule.ts'
|
|
||||||
import type { ShardManager } from './shard.ts'
|
|
||||||
import { Application } from '../structures/application.ts'
|
|
||||||
import { Invite } from '../structures/invite.ts'
|
|
||||||
import { INVITE } from '../types/endpoint.ts'
|
|
||||||
import { ClientEvents } from '../gateway/handlers/index.ts'
|
|
||||||
|
|
||||||
/** OS related properties sent with Gateway Identify */
|
|
||||||
export interface ClientProperties {
|
|
||||||
os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string
|
|
||||||
browser?: 'harmony' | string
|
|
||||||
device?: 'harmony' | string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Some Client Options to modify behaviour */
|
|
||||||
export interface ClientOptions {
|
|
||||||
/** ID of the Client/Application to initialize Slash Client REST */
|
|
||||||
id?: string
|
|
||||||
/** Token of the Bot/User */
|
|
||||||
token?: string
|
|
||||||
/** Gateway Intents */
|
|
||||||
intents?: GatewayIntents[]
|
|
||||||
/** Cache Adapter to use, defaults to Collections one */
|
|
||||||
cache?: ICacheAdapter
|
|
||||||
/** Force New Session and don't use cached Session (by persistent caching) */
|
|
||||||
forceNewSession?: boolean
|
|
||||||
/** Startup presence of client */
|
|
||||||
presence?: ClientPresence | ClientActivity | ActivityGame
|
|
||||||
/** Force all requests to Canary API */
|
|
||||||
canary?: boolean
|
|
||||||
/** Time till which Messages are to be cached, in MS. Default is 3600000 */
|
|
||||||
messageCacheLifetime?: number
|
|
||||||
/** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */
|
|
||||||
reactionCacheLifetime?: number
|
|
||||||
/** Whether to fetch Uncached Message of Reaction or not? */
|
|
||||||
fetchUncachedReactions?: boolean
|
|
||||||
/** Client Properties */
|
|
||||||
clientProperties?: ClientProperties
|
|
||||||
/** Enable/Disable Slash Commands Integration (enabled by default) */
|
|
||||||
enableSlash?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare interface Client {
|
|
||||||
on<K extends keyof ClientEvents>(
|
|
||||||
event: K,
|
|
||||||
listener: (...args: ClientEvents[K]) => void
|
|
||||||
): this
|
|
||||||
on(event: string | symbol, listener: (...args: any[]) => void): this
|
|
||||||
|
|
||||||
once<K extends keyof ClientEvents>(
|
|
||||||
event: K,
|
|
||||||
listener: (...args: ClientEvents[K]) => void
|
|
||||||
): this
|
|
||||||
once(event: string | symbol, listener: (...args: any[]) => void): this
|
|
||||||
|
|
||||||
emit<K extends keyof ClientEvents>(
|
|
||||||
event: K,
|
|
||||||
...args: ClientEvents[K]
|
|
||||||
): boolean
|
|
||||||
emit(event: string | symbol, ...args: any[]): boolean
|
|
||||||
|
|
||||||
off<K extends keyof ClientEvents>(
|
|
||||||
event: K,
|
|
||||||
listener: (...args: ClientEvents[K]) => void
|
|
||||||
): this
|
|
||||||
off(event: string | symbol, listener: (...args: any[]) => void): this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discord Client.
|
|
||||||
*/
|
|
||||||
export class Client extends EventEmitter {
|
|
||||||
/** Gateway object */
|
|
||||||
gateway?: Gateway
|
|
||||||
/** REST Manager - used to make all requests */
|
|
||||||
rest: RESTManager = new RESTManager(this)
|
|
||||||
/** User which Client logs in to, undefined until logs in */
|
|
||||||
user?: User
|
|
||||||
/** WebSocket ping of Client */
|
|
||||||
ping = 0
|
|
||||||
/** Token of the Bot/User */
|
|
||||||
token?: string
|
|
||||||
/** Cache Adapter */
|
|
||||||
cache: ICacheAdapter = new DefaultCacheAdapter()
|
|
||||||
/** Gateway Intents */
|
|
||||||
intents?: GatewayIntents[]
|
|
||||||
/** Whether to force new session or not */
|
|
||||||
forceNewSession?: boolean
|
|
||||||
/** Time till messages to stay cached, in MS. */
|
|
||||||
messageCacheLifetime: number = 3600000
|
|
||||||
/** Time till messages to stay cached, in MS. */
|
|
||||||
reactionCacheLifetime: number = 3600000
|
|
||||||
/** Whether to fetch Uncached Message of Reaction or not? */
|
|
||||||
fetchUncachedReactions: boolean = false
|
|
||||||
/** Client Properties */
|
|
||||||
clientProperties: ClientProperties
|
|
||||||
/** Slash-Commands Management client */
|
|
||||||
slash: SlashClient
|
|
||||||
|
|
||||||
users: UsersManager = new UsersManager(this)
|
|
||||||
guilds: GuildManager = new GuildManager(this)
|
|
||||||
channels: ChannelsManager = new ChannelsManager(this)
|
|
||||||
emojis: EmojisManager = new EmojisManager(this)
|
|
||||||
|
|
||||||
/** Whether the REST Manager will use Canary API or not */
|
|
||||||
canary: boolean = false
|
|
||||||
/** Client's presence. Startup one if set before connecting */
|
|
||||||
presence: ClientPresence = new ClientPresence()
|
|
||||||
_decoratedEvents?: {
|
|
||||||
[name: string]: (...args: any[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
_decoratedSlash?: Array<{
|
|
||||||
name: string
|
|
||||||
guild?: string
|
|
||||||
parent?: string
|
|
||||||
group?: string
|
|
||||||
handler: (interaction: Interaction) => any
|
|
||||||
}>
|
|
||||||
|
|
||||||
_decoratedSlashModules?: SlashModule[]
|
|
||||||
_id?: string
|
|
||||||
|
|
||||||
/** Shard on which this Client is */
|
|
||||||
shard: number = 0
|
|
||||||
/** Shard Manager of this Client if Sharded */
|
|
||||||
shardManager?: ShardManager
|
|
||||||
|
|
||||||
constructor(options: ClientOptions = {}) {
|
|
||||||
super()
|
|
||||||
this._id = options.id
|
|
||||||
this.token = options.token
|
|
||||||
this.intents = options.intents
|
|
||||||
this.forceNewSession = options.forceNewSession
|
|
||||||
if (options.cache !== undefined) this.cache = options.cache
|
|
||||||
if (options.presence !== undefined)
|
|
||||||
this.presence =
|
|
||||||
options.presence instanceof ClientPresence
|
|
||||||
? options.presence
|
|
||||||
: new ClientPresence(options.presence)
|
|
||||||
if (options.canary === true) this.canary = true
|
|
||||||
if (options.messageCacheLifetime !== undefined)
|
|
||||||
this.messageCacheLifetime = options.messageCacheLifetime
|
|
||||||
if (options.reactionCacheLifetime !== undefined)
|
|
||||||
this.reactionCacheLifetime = options.reactionCacheLifetime
|
|
||||||
if (options.fetchUncachedReactions === true)
|
|
||||||
this.fetchUncachedReactions = true
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._decoratedEvents !== undefined &&
|
|
||||||
Object.keys(this._decoratedEvents).length !== 0
|
|
||||||
) {
|
|
||||||
Object.entries(this._decoratedEvents).forEach((entry) => {
|
|
||||||
this.on(entry[0], entry[1])
|
|
||||||
})
|
|
||||||
this._decoratedEvents = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clientProperties =
|
|
||||||
options.clientProperties === undefined
|
|
||||||
? {
|
|
||||||
os: Deno.build.os,
|
|
||||||
browser: 'harmony',
|
|
||||||
device: 'harmony'
|
|
||||||
}
|
|
||||||
: options.clientProperties
|
|
||||||
|
|
||||||
this.slash = new SlashClient({
|
|
||||||
id: () => this.getEstimatedID(),
|
|
||||||
client: this,
|
|
||||||
enabled: options.enableSlash
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets Cache Adapter
|
|
||||||
*
|
|
||||||
* Should NOT be set after bot is already logged in or using current cache.
|
|
||||||
* Please look into using `cache` option.
|
|
||||||
*/
|
|
||||||
setAdapter(adapter: ICacheAdapter): Client {
|
|
||||||
this.cache = adapter
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Changes Presence of Client */
|
|
||||||
setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void {
|
|
||||||
if (presence instanceof ClientPresence) {
|
|
||||||
this.presence = presence
|
|
||||||
} else this.presence = new ClientPresence(presence)
|
|
||||||
this.gateway?.sendPresence(this.presence.create())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Emits debug event */
|
|
||||||
debug(tag: string, msg: string): void {
|
|
||||||
this.emit('debug', `[${tag}] ${msg}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
getEstimatedID(): string {
|
|
||||||
if (this.user !== undefined) return this.user.id
|
|
||||||
else if (this.token !== undefined) {
|
|
||||||
try {
|
|
||||||
return atob(this.token.split('.')[0])
|
|
||||||
} catch (e) {
|
|
||||||
return this._id ?? 'unknown'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return this._id ?? 'unknown'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fetch Application of the Client */
|
|
||||||
async fetchApplication(): Promise<Application> {
|
|
||||||
const app = await this.rest.api.oauth2.applications['@me'].get()
|
|
||||||
return new Application(this, app)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fetch an Invite */
|
|
||||||
async fetchInvite(id: string): Promise<Invite> {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
this.rest
|
|
||||||
.get(INVITE(id))
|
|
||||||
.then((data) => {
|
|
||||||
resolve(new Invite(this, data))
|
|
||||||
})
|
|
||||||
.catch((e) => reject(e))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is used for connecting to discord.
|
|
||||||
* @param token Your token. This is required.
|
|
||||||
* @param intents Gateway intents in array. This is required.
|
|
||||||
*/
|
|
||||||
connect(token?: string, intents?: GatewayIntents[]): void {
|
|
||||||
if (token === undefined && this.token !== undefined) token = this.token
|
|
||||||
else if (this.token === undefined && token !== undefined) {
|
|
||||||
this.token = token
|
|
||||||
} else throw new Error('No Token Provided')
|
|
||||||
if (intents !== undefined && this.intents !== undefined) {
|
|
||||||
this.debug(
|
|
||||||
'client',
|
|
||||||
'Intents were set in both client and connect function. Using the one in the connect function...'
|
|
||||||
)
|
|
||||||
} else if (intents === undefined && this.intents !== undefined) {
|
|
||||||
intents = this.intents
|
|
||||||
} else if (intents !== undefined && this.intents === undefined) {
|
|
||||||
this.intents = intents
|
|
||||||
} else throw new Error('No Gateway Intents were provided')
|
|
||||||
this.gateway = new Gateway(this, token, intents)
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitFor<K extends keyof ClientEvents>(
|
|
||||||
event: K,
|
|
||||||
checkFunction: (...args: ClientEvents[K]) => boolean,
|
|
||||||
timeout?: number
|
|
||||||
): Promise<ClientEvents[K] | []> {
|
|
||||||
return await new Promise((resolve) => {
|
|
||||||
let timeoutID: number | undefined
|
|
||||||
if (timeout !== undefined) {
|
|
||||||
timeoutID = setTimeout(() => {
|
|
||||||
this.off(event, eventFunc)
|
|
||||||
resolve([])
|
|
||||||
}, timeout)
|
|
||||||
}
|
|
||||||
const eventFunc = (...args: ClientEvents[K]): void => {
|
|
||||||
if (checkFunction(...args)) {
|
|
||||||
resolve(args)
|
|
||||||
this.off(event, eventFunc)
|
|
||||||
if (timeoutID !== undefined) clearTimeout(timeoutID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.on(event, eventFunc)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function event(name?: keyof ClientEvents) {
|
|
||||||
return function (client: Client | Extension, prop: keyof ClientEvents) {
|
|
||||||
const listener = ((client as unknown) as {
|
|
||||||
[name in keyof ClientEvents]: (...args: ClientEvents[name]) => any
|
|
||||||
})[prop]
|
|
||||||
if (typeof listener !== 'function')
|
|
||||||
throw new Error('@event decorator requires a function')
|
|
||||||
if (client._decoratedEvents === undefined) client._decoratedEvents = {}
|
|
||||||
const key = name === undefined ? prop : name
|
|
||||||
|
|
||||||
client._decoratedEvents[key] = listener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decorator to create a Slash Command handler */
|
|
||||||
export function slash(name?: string, guild?: string) {
|
|
||||||
return function (client: Client | SlashModule, prop: string) {
|
|
||||||
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
|
||||||
const item = (client as { [name: string]: any })[prop]
|
|
||||||
if (typeof item !== 'function') {
|
|
||||||
client._decoratedSlash.push(item)
|
|
||||||
} else
|
|
||||||
client._decoratedSlash.push({
|
|
||||||
name: name ?? prop,
|
|
||||||
guild,
|
|
||||||
handler: item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decorator to create a Sub-Slash Command handler */
|
|
||||||
export function subslash(parent: string, name?: string, guild?: string) {
|
|
||||||
return function (client: Client | SlashModule, prop: string) {
|
|
||||||
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
|
||||||
const item = (client as { [name: string]: any })[prop]
|
|
||||||
if (typeof item !== 'function') {
|
|
||||||
item.parent = parent
|
|
||||||
client._decoratedSlash.push(item)
|
|
||||||
} else
|
|
||||||
client._decoratedSlash.push({
|
|
||||||
parent,
|
|
||||||
name: name ?? prop,
|
|
||||||
guild,
|
|
||||||
handler: item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decorator to create a Grouped Slash Command handler */
|
|
||||||
export function groupslash(
|
|
||||||
parent: string,
|
|
||||||
group: string,
|
|
||||||
name?: string,
|
|
||||||
guild?: string
|
|
||||||
) {
|
|
||||||
return function (client: Client | SlashModule | SlashClient, prop: string) {
|
|
||||||
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
|
||||||
const item = (client as { [name: string]: any })[prop]
|
|
||||||
if (typeof item !== 'function') {
|
|
||||||
item.parent = parent
|
|
||||||
item.group = group
|
|
||||||
client._decoratedSlash.push(item)
|
|
||||||
} else
|
|
||||||
client._decoratedSlash.push({
|
|
||||||
group,
|
|
||||||
parent,
|
|
||||||
name: name ?? prop,
|
|
||||||
guild,
|
|
||||||
handler: item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decorator to add a Slash Module to Client */
|
|
||||||
export function slashModule() {
|
|
||||||
return function (client: Client, prop: string) {
|
|
||||||
if (client._decoratedSlashModules === undefined)
|
|
||||||
client._decoratedSlashModules = []
|
|
||||||
|
|
||||||
const mod = ((client as unknown) as { [key: string]: any })[prop]
|
|
||||||
client._decoratedSlashModules.push(mod)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,539 +0,0 @@
|
||||||
import * as baseEndpoints from '../consts/urlsAndVersions.ts'
|
|
||||||
import { Collection } from '../utils/collection.ts'
|
|
||||||
|
|
||||||
export type RequestMethods =
|
|
||||||
| 'get'
|
|
||||||
| 'post'
|
|
||||||
| 'put'
|
|
||||||
| 'patch'
|
|
||||||
| 'head'
|
|
||||||
| 'delete'
|
|
||||||
|
|
||||||
export enum HttpResponseCode {
|
|
||||||
Ok = 200,
|
|
||||||
Created = 201,
|
|
||||||
NoContent = 204,
|
|
||||||
NotModified = 304,
|
|
||||||
BadRequest = 400,
|
|
||||||
Unauthorized = 401,
|
|
||||||
Forbidden = 403,
|
|
||||||
NotFound = 404,
|
|
||||||
MethodNotAllowed = 405,
|
|
||||||
TooManyRequests = 429,
|
|
||||||
GatewayUnavailable = 502
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestHeaders {
|
|
||||||
[name: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DiscordAPIError extends Error {
|
|
||||||
name = 'DiscordAPIError'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueuedItem {
|
|
||||||
bucket?: string | null
|
|
||||||
url: string
|
|
||||||
onComplete: () => Promise<
|
|
||||||
| {
|
|
||||||
rateLimited: any
|
|
||||||
bucket?: string | null
|
|
||||||
before: boolean
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RateLimit {
|
|
||||||
url: string
|
|
||||||
resetAt: number
|
|
||||||
bucket: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['get', 'post', 'patch', 'put', 'delete', 'head']
|
|
||||||
|
|
||||||
export type MethodFunction = (
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries?: number,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
) => Promise<any>
|
|
||||||
|
|
||||||
export interface APIMap extends MethodFunction {
|
|
||||||
get: APIMap
|
|
||||||
post: APIMap
|
|
||||||
patch: APIMap
|
|
||||||
put: APIMap
|
|
||||||
delete: APIMap
|
|
||||||
head: APIMap
|
|
||||||
[name: string]: APIMap
|
|
||||||
}
|
|
||||||
|
|
||||||
export const builder = (rest: RESTManager, acum = '/'): APIMap => {
|
|
||||||
const routes = {}
|
|
||||||
const proxy = new Proxy(routes, {
|
|
||||||
get: (_, p, __) => {
|
|
||||||
if (p === 'toString') return () => acum
|
|
||||||
if (METHODS.includes(String(p))) {
|
|
||||||
const method = ((rest as unknown) as {
|
|
||||||
[name: string]: MethodFunction
|
|
||||||
})[String(p)]
|
|
||||||
return async (...args: any[]) =>
|
|
||||||
await method.bind(rest)(
|
|
||||||
`${baseEndpoints.DISCORD_API_URL}/v${rest.version}${acum.substring(
|
|
||||||
0,
|
|
||||||
acum.length - 1
|
|
||||||
)}`,
|
|
||||||
...args
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return builder(rest, acum + String(p) + '/')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return (proxy as unknown) as APIMap
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RESTOptions {
|
|
||||||
token?: string
|
|
||||||
headers?: { [name: string]: string | undefined }
|
|
||||||
canary?: boolean
|
|
||||||
version?: 6 | 7 | 8
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RESTManager {
|
|
||||||
client?: RESTOptions
|
|
||||||
queues: { [key: string]: QueuedItem[] } = {}
|
|
||||||
rateLimits = new Collection<string, RateLimit>()
|
|
||||||
globalRateLimit: boolean = false
|
|
||||||
processing: boolean = false
|
|
||||||
version: number = 8
|
|
||||||
api: APIMap
|
|
||||||
|
|
||||||
constructor(client?: RESTOptions) {
|
|
||||||
this.client = client
|
|
||||||
this.api = builder(this)
|
|
||||||
if (client?.version !== undefined) this.version = client.version
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.handleRateLimits()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkQueues(): Promise<void> {
|
|
||||||
Object.entries(this.queues).forEach(([key, value]) => {
|
|
||||||
if (value.length === 0) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
delete this.queues[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private queue(request: QueuedItem): void {
|
|
||||||
const route = request.url.substring(
|
|
||||||
// eslint seriously?
|
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
||||||
baseEndpoints.DISCORD_API_URL.length + 1
|
|
||||||
)
|
|
||||||
const parts = route.split('/')
|
|
||||||
parts.shift()
|
|
||||||
const [id] = parts
|
|
||||||
|
|
||||||
if (this.queues[id] !== undefined) {
|
|
||||||
this.queues[id].push(request)
|
|
||||||
} else {
|
|
||||||
this.queues[id] = [request]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processQueue(): Promise<void> {
|
|
||||||
if (Object.keys(this.queues).length !== 0 && !this.globalRateLimit) {
|
|
||||||
await Promise.allSettled(
|
|
||||||
Object.values(this.queues).map(async (pathQueue) => {
|
|
||||||
const request = pathQueue.shift()
|
|
||||||
if (request === undefined) return
|
|
||||||
|
|
||||||
const rateLimitedURLResetIn = await this.isRateLimited(request.url)
|
|
||||||
|
|
||||||
if (typeof request.bucket === 'string') {
|
|
||||||
const rateLimitResetIn = await this.isRateLimited(request.bucket)
|
|
||||||
if (rateLimitResetIn !== false) {
|
|
||||||
this.queue(request)
|
|
||||||
} else {
|
|
||||||
const result = await request.onComplete()
|
|
||||||
if (result?.rateLimited !== undefined) {
|
|
||||||
this.queue({
|
|
||||||
...request,
|
|
||||||
bucket: result.bucket ?? request.bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rateLimitedURLResetIn !== false) {
|
|
||||||
this.queue(request)
|
|
||||||
} else {
|
|
||||||
const result = await request.onComplete()
|
|
||||||
if (result?.rateLimited !== undefined) {
|
|
||||||
this.queue({
|
|
||||||
...request,
|
|
||||||
bucket: result.bucket ?? request.bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(this.queues).length !== 0) {
|
|
||||||
// await delay(100)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.processQueue()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.checkQueues()
|
|
||||||
} else this.processing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private prepare(body: any, method: RequestMethods): { [key: string]: any } {
|
|
||||||
const headers: RequestHeaders = {
|
|
||||||
'User-Agent': `DiscordBot (harmony, https://github.com/harmony-org/harmony)`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.client !== undefined)
|
|
||||||
headers.Authorization = `Bot ${this.client.token}`
|
|
||||||
|
|
||||||
if (this.client?.token === undefined) delete headers.Authorization
|
|
||||||
|
|
||||||
if (method === 'get' || method === 'head' || method === 'delete')
|
|
||||||
body = undefined
|
|
||||||
|
|
||||||
if (body?.reason !== undefined) {
|
|
||||||
headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body?.file !== undefined) {
|
|
||||||
const form = new FormData()
|
|
||||||
form.append('file', body.file.blob, body.file.name)
|
|
||||||
form.append('payload_json', JSON.stringify({ ...body, file: undefined }))
|
|
||||||
body.file = form
|
|
||||||
} else if (
|
|
||||||
body !== undefined &&
|
|
||||||
!['get', 'delete'].includes(method.toLowerCase())
|
|
||||||
) {
|
|
||||||
headers['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.client?.headers !== undefined)
|
|
||||||
Object.assign(headers, this.client.headers)
|
|
||||||
|
|
||||||
const data: { [name: string]: any } = {
|
|
||||||
headers,
|
|
||||||
body: body?.file ?? JSON.stringify(body),
|
|
||||||
method: method.toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
private async isRateLimited(url: string): Promise<number | false> {
|
|
||||||
const global = this.rateLimits.get('global')
|
|
||||||
const rateLimited = this.rateLimits.get(url)
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
if (rateLimited !== undefined && now < rateLimited.resetAt) {
|
|
||||||
return rateLimited.resetAt - now
|
|
||||||
}
|
|
||||||
if (global !== undefined && now < global.resetAt) {
|
|
||||||
return global.resetAt - now
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private processHeaders(
|
|
||||||
url: string,
|
|
||||||
headers: Headers
|
|
||||||
): string | null | undefined {
|
|
||||||
let rateLimited = false
|
|
||||||
|
|
||||||
const global = headers.get('x-ratelimit-global')
|
|
||||||
const bucket = headers.get('x-ratelimit-bucket')
|
|
||||||
const remaining = headers.get('x-ratelimit-remaining')
|
|
||||||
const resetAt = headers.get('x-ratelimit-reset')
|
|
||||||
const retryAfter = headers.get('retry-after')
|
|
||||||
|
|
||||||
if (remaining !== null && remaining === '0') {
|
|
||||||
rateLimited = true
|
|
||||||
|
|
||||||
this.rateLimits.set(url, {
|
|
||||||
url,
|
|
||||||
resetAt: Number(resetAt) * 1000,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
|
|
||||||
if (bucket !== null) {
|
|
||||||
this.rateLimits.set(bucket, {
|
|
||||||
url,
|
|
||||||
resetAt: Number(resetAt) * 1000,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global !== null) {
|
|
||||||
const reset = Date.now() + Number(retryAfter)
|
|
||||||
this.globalRateLimit = true
|
|
||||||
rateLimited = true
|
|
||||||
|
|
||||||
this.rateLimits.set('global', {
|
|
||||||
url: 'global',
|
|
||||||
resetAt: reset,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
|
|
||||||
if (bucket !== null) {
|
|
||||||
this.rateLimits.set(bucket, {
|
|
||||||
url: 'global',
|
|
||||||
resetAt: reset,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rateLimited ? bucket : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleStatusCode(
|
|
||||||
response: Response,
|
|
||||||
body: any,
|
|
||||||
data: { [key: string]: any },
|
|
||||||
reject: CallableFunction
|
|
||||||
): Promise<undefined> {
|
|
||||||
const status = response.status
|
|
||||||
|
|
||||||
if (
|
|
||||||
(status >= 200 && status < 400) ||
|
|
||||||
status === HttpResponseCode.NoContent ||
|
|
||||||
status === HttpResponseCode.TooManyRequests
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
let text: undefined | string = Deno.inspect(
|
|
||||||
body.errors === undefined ? body : body.errors
|
|
||||||
)
|
|
||||||
if (text === 'undefined') text = undefined
|
|
||||||
|
|
||||||
if (status === HttpResponseCode.Unauthorized)
|
|
||||||
reject(
|
|
||||||
new DiscordAPIError(
|
|
||||||
`Request was not successful (Unauthorized). Invalid Token.\n${text}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// At this point we know it is error
|
|
||||||
const error: { [name: string]: any } = {
|
|
||||||
url: response.url,
|
|
||||||
status,
|
|
||||||
method: data.method,
|
|
||||||
code: body?.code,
|
|
||||||
message: body?.message,
|
|
||||||
errors: Object.fromEntries(
|
|
||||||
Object.entries(
|
|
||||||
(body?.errors as {
|
|
||||||
[name: string]: {
|
|
||||||
_errors: Array<{ code: string; message: string }>
|
|
||||||
}
|
|
||||||
}) ?? {}
|
|
||||||
).map((entry) => {
|
|
||||||
return [entry[0], entry[1]._errors]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (typeof error.errors === 'object') {
|
|
||||||
// const errors = error.errors as {
|
|
||||||
// [name: string]: { _errors: Array<{ code: string; message: string }> }
|
|
||||||
// }
|
|
||||||
// console.log(`%cREST Error:`, 'color: #F14C39;')
|
|
||||||
// Object.entries(errors).forEach((entry) => {
|
|
||||||
// console.log(` %c${entry[0]}:`, 'color: #12BC79;')
|
|
||||||
// entry[1]._errors.forEach((e) => {
|
|
||||||
// console.log(
|
|
||||||
// ` %c${e.code}: %c${e.message}`,
|
|
||||||
// 'color: skyblue;',
|
|
||||||
// 'color: #CECECE;'
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
HttpResponseCode.BadRequest,
|
|
||||||
HttpResponseCode.NotFound,
|
|
||||||
HttpResponseCode.Forbidden,
|
|
||||||
HttpResponseCode.MethodNotAllowed
|
|
||||||
].includes(status)
|
|
||||||
) {
|
|
||||||
reject(new DiscordAPIError(Deno.inspect(error)))
|
|
||||||
} else if (status === HttpResponseCode.GatewayUnavailable) {
|
|
||||||
reject(new DiscordAPIError(Deno.inspect(error)))
|
|
||||||
} else reject(new DiscordAPIError('Request - Unknown Error'))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a Request to Discord API
|
|
||||||
* @param method HTTP Method to use
|
|
||||||
* @param url URL of the Request
|
|
||||||
* @param body Body to send with Request
|
|
||||||
* @param maxRetries Number of Max Retries to perform
|
|
||||||
* @param bucket BucketID of the Request
|
|
||||||
* @param rawResponse Whether to get Raw Response or body itself
|
|
||||||
*/
|
|
||||||
async make(
|
|
||||||
method: RequestMethods,
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const onComplete = async (): Promise<undefined | any> => {
|
|
||||||
try {
|
|
||||||
const rateLimitResetIn = await this.isRateLimited(url)
|
|
||||||
if (rateLimitResetIn !== false) {
|
|
||||||
return {
|
|
||||||
rateLimited: rateLimitResetIn,
|
|
||||||
before: true,
|
|
||||||
bucket
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query =
|
|
||||||
method === 'get' && body !== undefined
|
|
||||||
? Object.entries(body as any)
|
|
||||||
.filter(([k, v]) => v !== undefined)
|
|
||||||
.map(
|
|
||||||
([key, value]) =>
|
|
||||||
`${encodeURIComponent(key)}=${encodeURIComponent(
|
|
||||||
value as any
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
.join('&')
|
|
||||||
: ''
|
|
||||||
let urlToUse =
|
|
||||||
method === 'get' && query !== '' ? `${url}?${query}` : url
|
|
||||||
|
|
||||||
if (this.client?.canary === true) {
|
|
||||||
const split = urlToUse.split('//')
|
|
||||||
urlToUse = split[0] + '//canary.' + split[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestData = this.prepare(body, method)
|
|
||||||
|
|
||||||
const response = await fetch(urlToUse, requestData)
|
|
||||||
const bucketFromHeaders = this.processHeaders(url, response.headers)
|
|
||||||
|
|
||||||
if (response.status === 204)
|
|
||||||
return resolve(
|
|
||||||
rawResponse === true ? { response, body: null } : undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
const json: any = await response.json()
|
|
||||||
await this.handleStatusCode(response, json, requestData, reject)
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.retry_after !== undefined ||
|
|
||||||
json.message === 'You are being rate limited.'
|
|
||||||
) {
|
|
||||||
if (maxRetries > 10) {
|
|
||||||
throw new Error('Max RateLimit Retries hit')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
rateLimited: json.retry_after,
|
|
||||||
before: false,
|
|
||||||
bucket: bucketFromHeaders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolve(rawResponse === true ? { response, body: json } : json)
|
|
||||||
} catch (error) {
|
|
||||||
return reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queue({
|
|
||||||
onComplete,
|
|
||||||
bucket,
|
|
||||||
url
|
|
||||||
})
|
|
||||||
if (!this.processing) {
|
|
||||||
this.processing = true
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.processQueue()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleRateLimits(): Promise<void> {
|
|
||||||
const now = Date.now()
|
|
||||||
this.rateLimits.forEach((value, key) => {
|
|
||||||
if (value.resetAt > now) return
|
|
||||||
this.rateLimits.delete(key)
|
|
||||||
if (key === 'global') this.globalRateLimit = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a GET Request to API */
|
|
||||||
async get(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('get', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a POST Request to API */
|
|
||||||
async post(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('post', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a DELETE Request to API */
|
|
||||||
async delete(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('delete', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a PATCH Request to API */
|
|
||||||
async patch(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('patch', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a PUT Request to API */
|
|
||||||
async put(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('put', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { Collection } from '../utils/collection.ts'
|
|
||||||
import { Client, ClientOptions } from './client.ts'
|
|
||||||
import {EventEmitter} from '../../deps.ts'
|
|
||||||
import { RESTManager } from './rest.ts'
|
|
||||||
// import { GATEWAY_BOT } from '../types/endpoint.ts'
|
|
||||||
// import { GatewayBotPayload } from '../types/gatewayBot.ts'
|
|
||||||
|
|
||||||
// TODO(DjDeveloperr)
|
|
||||||
// I'm kinda confused; will continue on this later once
|
|
||||||
// Deno namespace in Web Worker is stable!
|
|
||||||
export interface ShardManagerOptions {
|
|
||||||
client: Client | typeof Client
|
|
||||||
token?: string
|
|
||||||
intents?: number[]
|
|
||||||
options?: ClientOptions
|
|
||||||
shards: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShardManagerInitOptions {
|
|
||||||
file: string
|
|
||||||
token?: string
|
|
||||||
intents?: number[]
|
|
||||||
options?: ClientOptions
|
|
||||||
shards?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ShardManager extends EventEmitter {
|
|
||||||
workers: Collection<string, Worker> = new Collection()
|
|
||||||
token: string
|
|
||||||
intents: number[]
|
|
||||||
shardCount: number
|
|
||||||
private readonly __client: Client
|
|
||||||
|
|
||||||
get rest(): RESTManager {
|
|
||||||
return this.__client.rest
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(options: ShardManagerOptions) {
|
|
||||||
super()
|
|
||||||
this.__client =
|
|
||||||
options.client instanceof Client
|
|
||||||
? options.client
|
|
||||||
: // eslint-disable-next-line new-cap
|
|
||||||
new options.client(options.options)
|
|
||||||
|
|
||||||
if (this.__client.token === undefined || options.token === undefined)
|
|
||||||
throw new Error('Token should be provided when constructing ShardManager')
|
|
||||||
if (this.__client.intents === undefined || options.intents === undefined)
|
|
||||||
throw new Error(
|
|
||||||
'Intents should be provided when constructing ShardManager'
|
|
||||||
)
|
|
||||||
|
|
||||||
this.token = this.__client.token ?? options.token
|
|
||||||
this.intents = this.__client.intents ?? options.intents
|
|
||||||
this.shardCount = options.shards
|
|
||||||
}
|
|
||||||
|
|
||||||
// static async init(): Promise<ShardManager> {}
|
|
||||||
|
|
||||||
// async start(): Promise<ShardManager> {
|
|
||||||
// const info = ((await this.rest.get(
|
|
||||||
// GATEWAY_BOT()
|
|
||||||
// )) as unknown) as GatewayBotPayload
|
|
||||||
|
|
||||||
// const totalShards = this.__shardCount ?? info.shards
|
|
||||||
|
|
||||||
// return this
|
|
||||||
// }
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { Guild } from '../structures/guild.ts'
|
|
||||||
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
|
||||||
import { Client } from './client.ts'
|
|
||||||
|
|
||||||
export interface VoiceOptions {
|
|
||||||
guild: Guild
|
|
||||||
channel: VoiceChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VoiceClient {
|
|
||||||
client: Client
|
|
||||||
ws?: WebSocket
|
|
||||||
guild: Guild
|
|
||||||
channel: VoiceChannel
|
|
||||||
|
|
||||||
constructor(client: Client, options: VoiceOptions) {
|
|
||||||
this.client = client
|
|
||||||
this.guild = options.guild
|
|
||||||
this.channel = options.channel
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect(): Promise<VoiceClient> {
|
|
||||||
// TODO(DjDeveloperr): understand docs
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
245
src/rest/bucket.ts
Normal file
245
src/rest/bucket.ts
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
// based on https://github.com/discordjs/discord.js/blob/master/src/rest/RequestHandler.js
|
||||||
|
// adapted to work with harmony rest manager
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||||
|
|
||||||
|
import { delay } from '../utils/delay.ts'
|
||||||
|
import { DiscordAPIError, HTTPError } from './error.ts'
|
||||||
|
import type { RESTManager } from './manager.ts'
|
||||||
|
import { RequestQueue } from './queue.ts'
|
||||||
|
import { APIRequest } from './request.ts'
|
||||||
|
|
||||||
|
function parseResponse(res: Response, raw: boolean): any {
|
||||||
|
let result
|
||||||
|
if (res.status === 204) result = Promise.resolve(undefined)
|
||||||
|
else if (
|
||||||
|
res.headers.get('content-type')?.startsWith('application/json') === true
|
||||||
|
)
|
||||||
|
result = res.json()
|
||||||
|
else result = res.arrayBuffer().then((e) => new Uint8Array(e))
|
||||||
|
|
||||||
|
if (raw) {
|
||||||
|
return { response: res, body: result }
|
||||||
|
} else return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAPIOffset(serverDate: number | string): number {
|
||||||
|
return new Date(serverDate).getTime() - Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateReset(
|
||||||
|
reset: number | string,
|
||||||
|
serverDate: number | string
|
||||||
|
): number {
|
||||||
|
return new Date(Number(reset) * 1000).getTime() - getAPIOffset(serverDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
let invalidCount = 0
|
||||||
|
let invalidCountResetTime: number | null = null
|
||||||
|
|
||||||
|
export class BucketHandler {
|
||||||
|
queue = new RequestQueue()
|
||||||
|
reset = -1
|
||||||
|
remaining = -1
|
||||||
|
limit = -1
|
||||||
|
|
||||||
|
constructor(public manager: RESTManager) {}
|
||||||
|
|
||||||
|
async push(request: APIRequest): Promise<any> {
|
||||||
|
await this.queue.wait()
|
||||||
|
try {
|
||||||
|
return await this.execute(request)
|
||||||
|
} finally {
|
||||||
|
this.queue.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get globalLimited(): boolean {
|
||||||
|
return (
|
||||||
|
this.manager.globalRemaining <= 0 &&
|
||||||
|
Date.now() < Number(this.manager.globalReset)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get localLimited(): boolean {
|
||||||
|
return this.remaining <= 0 && Date.now() < this.reset
|
||||||
|
}
|
||||||
|
|
||||||
|
get limited(): boolean {
|
||||||
|
return this.globalLimited || this.localLimited
|
||||||
|
}
|
||||||
|
|
||||||
|
get inactive(): boolean {
|
||||||
|
return this.queue.remaining === 0 && !this.limited
|
||||||
|
}
|
||||||
|
|
||||||
|
async globalDelayFor(ms: number): Promise<void> {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
this.manager.setTimeout(() => {
|
||||||
|
this.manager.globalDelay = null
|
||||||
|
resolve()
|
||||||
|
}, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(request: APIRequest): Promise<any> {
|
||||||
|
while (this.limited) {
|
||||||
|
const isGlobal = this.globalLimited
|
||||||
|
let limit, timeout, delayPromise
|
||||||
|
|
||||||
|
if (isGlobal) {
|
||||||
|
limit = this.manager.globalLimit
|
||||||
|
timeout =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||||
|
Number(this.manager.globalReset) +
|
||||||
|
this.manager.restTimeOffset -
|
||||||
|
Date.now()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!this.manager.globalDelay) {
|
||||||
|
this.manager.globalDelay = this.globalDelayFor(timeout) as any
|
||||||
|
}
|
||||||
|
delayPromise = this.manager.globalDelay
|
||||||
|
} else {
|
||||||
|
limit = this.limit
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||||
|
timeout = this.reset + this.manager.restTimeOffset - Date.now()
|
||||||
|
delayPromise = delay(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager.client?.emit('rateLimit', {
|
||||||
|
timeout,
|
||||||
|
limit,
|
||||||
|
method: request.method,
|
||||||
|
path: request.path,
|
||||||
|
global: isGlobal
|
||||||
|
})
|
||||||
|
|
||||||
|
await delayPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!this.manager.globalReset || this.manager.globalReset < Date.now()) {
|
||||||
|
this.manager.globalReset = Date.now() + 1000
|
||||||
|
this.manager.globalRemaining = this.manager.globalLimit
|
||||||
|
}
|
||||||
|
this.manager.globalRemaining--
|
||||||
|
|
||||||
|
// Perform the request
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = await request.execute()
|
||||||
|
} catch (error) {
|
||||||
|
if (request.retries === this.manager.retryLimit) {
|
||||||
|
throw new HTTPError(
|
||||||
|
error.message,
|
||||||
|
error.constructor.name,
|
||||||
|
error.status,
|
||||||
|
request.method,
|
||||||
|
request.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.retries++
|
||||||
|
return await this.execute(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sublimitTimeout
|
||||||
|
if (res?.headers !== undefined) {
|
||||||
|
const serverDate = res.headers.get('date')
|
||||||
|
const limit = res.headers.get('x-ratelimit-limit')
|
||||||
|
const remaining = res.headers.get('x-ratelimit-remaining')
|
||||||
|
const reset = res.headers.get('x-ratelimit-reset')
|
||||||
|
this.limit = limit !== null ? Number(limit) : Infinity
|
||||||
|
this.remaining = remaining !== null ? Number(remaining) : 1
|
||||||
|
this.reset =
|
||||||
|
reset !== null ? calculateReset(reset, serverDate!) : Date.now()
|
||||||
|
|
||||||
|
if (request.path.includes('reactions') === true) {
|
||||||
|
this.reset =
|
||||||
|
new Date(serverDate!).getTime() - getAPIOffset(serverDate!) + 250
|
||||||
|
}
|
||||||
|
|
||||||
|
let retryAfter: number | null | string = res.headers.get('retry-after')
|
||||||
|
retryAfter = retryAfter !== null ? Number(retryAfter) * 1000 : -1
|
||||||
|
if (retryAfter > 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (res.headers.get('x-ratelimit-global')) {
|
||||||
|
this.manager.globalRemaining = 0
|
||||||
|
this.manager.globalReset = Date.now() + retryAfter
|
||||||
|
} else if (!this.localLimited) {
|
||||||
|
sublimitTimeout = retryAfter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 401 || res.status === 403 || res.status === 429) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
|
||||||
|
invalidCountResetTime = Date.now() + 1000 * 60 * 10
|
||||||
|
invalidCount = 0
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
invalidCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.ok === true) {
|
||||||
|
return parseResponse(res, request.options.rawResponse ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status >= 400 && res.status < 500) {
|
||||||
|
if (res.status === 429) {
|
||||||
|
this.manager.client?.emit(
|
||||||
|
'debug',
|
||||||
|
`Rate-Limited on route ${request.path}${
|
||||||
|
sublimitTimeout !== undefined ? ' for sublimit' : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (sublimitTimeout !== undefined) {
|
||||||
|
await delay(sublimitTimeout)
|
||||||
|
}
|
||||||
|
return await this.execute(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = await parseResponse(res, false)
|
||||||
|
} catch (err) {
|
||||||
|
throw new HTTPError(
|
||||||
|
err.message,
|
||||||
|
err.constructor.name,
|
||||||
|
err.status,
|
||||||
|
request.method,
|
||||||
|
request.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DiscordAPIError({
|
||||||
|
url: request.path,
|
||||||
|
errors: data?.errors,
|
||||||
|
status: res.status,
|
||||||
|
method: request.method,
|
||||||
|
message: data?.message,
|
||||||
|
code: data?.code,
|
||||||
|
requestData: request.options.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status >= 500 && res.status < 600) {
|
||||||
|
if (request.retries === this.manager.retryLimit) {
|
||||||
|
throw new HTTPError(
|
||||||
|
res.statusText,
|
||||||
|
res.constructor.name,
|
||||||
|
res.status,
|
||||||
|
request.method,
|
||||||
|
request.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.retries++
|
||||||
|
return await this.execute(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue