Compare commits

...

175 Commits
v1.1.5 ... main

Author SHA1 Message Date
zeromomentum121 7d621128c3
id support for args 2021-05-06 16:01:25 -05:00
DjDeveloper a25aef6400
Merge pull request #148 from DjDeveloperr/vc-fix
refactor: voice join/leave
2021-05-06 19:01:10 +05:30
DjDeveloperr 90e4bd6ccf fix 2021-05-06 09:21:41 +05:30
DjDeveloperr 7570dfffbe merge 2021-05-06 08:15:49 +05:30
Helloyunho d52d636641 🐛 Fix setting contentType even tho there's no body 2021-05-03 20:54:39 +09:00
DjDeveloper a7547f85ca
Merge pull request #147 from MierenManz/fix_args_parser
FIX(command) incorrect value for rest default value
2021-05-03 16:10:33 +05:30
mierenmanz 5df70e5904 FIX(command) incorrect value for rest default value 2021-05-03 11:24:09 +02:00
DjDeveloper ce455c50c3
Merge pull request #145 from lamebox/lamebox-patch-1 2021-05-03 08:06:23 +05:30
lamebox a5fd1de25e
Update deploy.ts 2021-05-02 16:05:24 -07:00
lamebox c08565cde9
Remove messageComponent from exports since that isn't merged 2021-05-02 10:14:56 -07:00
DjDeveloper 53030ea941
chore(readme): update invite 2021-05-02 11:19:48 +05:30
DjDeveloper b686c02cdc
Merge pull request #138 from DjDeveloperr/design-fix
BREAKING: Split up Interaction structure into different types
2021-05-01 14:14:28 +05:30
DjDeveloperr 258ec12906 Merge branch 'design-fix' of https://github.com/DjDeveloperr/harmony into design-fix 2021-05-01 10:33:26 +05:30
DjDeveloperr b22563ffc3 requested changes 2021-05-01 10:33:08 +05:30
Helloyunho 461e1557c5
Merge branch 'main' into design-fix 2021-04-30 23:47:13 +09:00
Helloyunho 60e7c75113
Merge pull request #136 from MierenManz/main
Argument parsing for commands
2021-04-30 23:46:38 +09:00
DjDeveloperr e8371408e2 x 2021-04-30 18:07:01 +05:30
DjDeveloper fee3e0cfa0
Merge pull request #139 from MierenManz/update_deps
chore: update deps
2021-04-30 12:20:37 +05:30
mierenmanz 6e084af4a3 add tests aswell 2021-04-30 08:15:13 +02:00
mierenmanz 52ec39a24c `rest` string[] => string 2021-04-30 08:13:45 +02:00
DjDeveloper 8e49dc4306
Merge pull request #142 from DjDeveloperr/main
feat: add User#send
2021-04-30 11:37:35 +05:30
DjDeveloperr b64bc29fbe feat: User#send 2021-04-30 11:35:19 +05:30
mierenmanz a013388ec9 update redis 2021-04-29 22:26:27 +02:00
mierenmanz a8bb3cc49b add mention tests 2021-04-29 19:34:16 +02:00
mierenmanz 54b0356a72 Merge branch 'main' of https://github.com/harmonyland/harmony 2021-04-29 13:11:53 +02:00
mierenmanz 057b3ede53 fix test for new behavior 2021-04-29 12:43:21 +02:00
mierenmanz de4e207d85 remove global to fix bug 2021-04-29 12:43:03 +02:00
mierenmanz 222f1d0fa8 parseMention now parses channel & roles mentions 2021-04-29 12:10:50 +02:00
DjDeveloper b1aa995808
Merge pull request #140 from DjDeveloperr/main
fix raw response
2021-04-29 14:46:17 +05:30
DjDeveloperr 718a4658eb fix raw response compat 2021-04-29 14:45:05 +05:30
mierenmanz b3220fa155 don't assume that the first string found is a mention 2021-04-29 11:05:31 +02:00
mierenmanz 8c4f0d948d fmt 2021-04-29 10:47:57 +02:00
mierenmanz 36d3e944ff chore: update deps 2021-04-29 10:45:12 +02:00
mierenmanz 9d6feaff8d add generic type (I give up on generated types for now) 2021-04-28 21:28:30 +02:00
mierenmanz 1c9d17263b extend test 2021-04-28 21:28:09 +02:00
mierenmanz bffead6680 fmt 2021-04-28 20:29:27 +02:00
mierenmanz dcbf635860 formatting 2021-04-28 20:28:19 +02:00
mierenmanz 0a871023c8 fixed 2 bugs related to parsing 2021-04-28 20:23:06 +02:00
mierenmanz e6c0c378de added more tests 2021-04-28 20:22:54 +02:00
mierenmanz 9c18ec2d1f change type to allow for proper defaults 2021-04-28 16:56:30 +02:00
mierenmanz 97298f17f8 add default value 2021-04-28 16:23:39 +02:00
mierenmanz eac9c4bceb fix test 2021-04-28 16:17:39 +02:00
mierenmanz 1c25e2d3a1 add test (fails at the moment) 2021-04-28 15:57:59 +02:00
mierenmanz 58ad6fcd3d misc changes 2021-04-28 15:09:39 +02:00
mierenmanz e7715b75cf fix lint? 2021-04-28 13:50:13 +02:00
DjDeveloperr c88e4a67b2 oops wrong file commit 2021-04-28 16:57:57 +05:30
DjDeveloperr 2cdb671fb1 interactions api design fix 2021-04-28 16:54:22 +05:30
mierenmanz 8ac716d6a9 change enum to type 2021-04-28 12:43:16 +02:00
mierenmanz ff655e0b7d export utils/command.ts 2021-04-28 12:43:01 +02:00
mierenmanz 7f96dfebc3 add args parser into command class (not tested) 2021-04-28 11:52:05 +02:00
mierenmanz d9925a4ac4 add basic argument parsing (Still WIP) 2021-04-27 23:04:27 +02:00
DjDeveloperr 1beccb57ba fix 2021-04-25 16:28:48 +05:30
DjDeveloperr 332f0f2742 add support got path 2021-04-25 16:27:34 +05:30
DjDeveloperr 3fe3a84928 fix 2021-04-25 16:04:13 +05:30
DjDeveloperr a7c442d340 x 2021-04-25 16:00:06 +05:30
DjDeveloperr 45bfbff3a3 fix deploy 2021-04-25 15:17:37 +05:30
DjDeveloperr 381cb13e6c add disabled to component 2021-04-24 03:17:06 -07:00
DjDeveloperr 67d81c1bf0 x 2021-04-24 14:33:15 +05:30
DjDeveloper 66031d56c2
fix slash module decorators 2021-04-24 14:32:40 +05:30
DjDeveloperr 9d88c5d113 merge 2021-04-23 11:22:37 +05:30
DjDeveloper 962d90aca8
Merge pull request #133 from DjDeveloperr/fix-decorators
Fix decorators
2021-04-23 11:20:59 +05:30
DjDeveloperr addf9cffb6 oops 2021-04-23 11:20:12 +05:30
DjDeveloperr 945bcc6f40 fix decorators 2021-04-23 11:18:59 +05:30
DjDeveloperr a1ead7e15e fix 2021-04-23 11:08:51 +05:30
DjDeveloperr b97ec3c225 message component interactions 2021-04-23 11:07:55 +05:30
DjDeveloperr 17e74dce45 make interactions model better 2021-04-22 09:18:45 +05:30
DjDeveloper 3868d29e3e
Merge pull request #132 from DjDeveloperr/refactor
breaking: client.ping -> client.gateway.ping, some jsdoc, RolesManager#fetch -> RolesManager#fetchAll
2021-04-17 14:05:36 +05:30
DjDeveloperr 30cd8e10dc fix .ping usage 2021-04-17 13:49:26 +05:30
DjDeveloperr ea221f8962 client.ping -> client.gateway.ping 2021-04-17 13:47:46 +05:30
DjDeveloperr b324263a7b Roles#fetch -> Roles#fetchAll & jsdoc 2021-04-17 13:37:26 +05:30
DjDeveloper 8d08378413
Merge pull request #131 from DjDeveloperr/refactor
fix: Command.onError
2021-04-17 13:12:37 +05:30
DjDeveloperr b13cdbe480 fix: Command.onError 2021-04-17 13:11:07 +05:30
DjDeveloper 229f5f079c
Merge pull request #130 from DjDeveloperr/refactor
feat(docs): add jsdoc for deploy
2021-04-15 18:23:42 +05:30
DjDeveloperr c3d1714a6e x 2021-04-15 18:23:26 +05:30
DjDeveloperr d0e3dc9fba feat(docs): add jsdoc for deploy 2021-04-15 18:20:47 +05:30
DjDeveloper ef3dcacf0b
Merge pull request #129 from DjDeveloperr/refactor
feat(deploy): remove unnecessary timer polyfill
2021-04-15 18:10:26 +05:30
DjDeveloperr ca3d68e6a0 feat(deploy): remove unnecessary timer polyfill 2021-04-15 18:08:42 +05:30
DjDeveloper ab19a6b624
Merge pull request #128 from DjDeveloperr/refactor
fix leaving voice channel
2021-04-15 12:07:01 +05:30
DjDeveloperr 9a72f66931 fix vc leave 2021-04-15 12:05:38 +05:30
DjDeveloper 34e261a9e1
Merge pull request #127 from DjDeveloperr/refactor
fix(voicechannel): wrong return type of join method
2021-04-15 11:42:49 +05:30
DjDeveloperr f3da37bcb1 fix 2021-04-15 11:41:18 +05:30
AkiaCode cacf8e6d14
Change invite url 2021-04-15 00:53:07 +09:00
DjDeveloper 2745d5c445
fix guild structure 2021-04-14 14:39:11 +05:30
DjDeveloper f8ecaba024
feat(structures/guild): add nsfw: boolean 2021-04-14 14:38:02 +05:30
DjDeveloper 30be804166
feat(types/guild): add nsfw: boolean 2021-04-14 14:36:33 +05:30
Helloyunho 5e2a057462
Update deno.yml 2021-04-11 21:10:14 +09:00
Helloyunho d17e845b2b
Use official deno github action 2021-04-11 21:04:23 +09:00
DjDeveloper 7018bf6094
Merge pull request #124 from DjDeveloperr/refactor
fix slash commands in deploy
2021-04-10 09:30:30 +05:30
DjDeveloperr 12e6971b0a fix 2021-04-09 11:02:31 +05:30
DjDeveloperr bd5153fd15 cmt 2021-04-09 10:39:35 +05:30
DjDeveloperr fe6ee7ddac fix deploy 2021-04-09 10:37:11 +05:30
DjDeveloper ce843b189b
Merge pull request #122 from DjDeveloperr/refactor
feat: refactor project structure
2021-04-08 15:30:40 +05:30
DjDeveloperr 758db4cfb2 fix 2021-04-08 15:29:31 +05:30
DjDeveloperr 8d18f6b1ab remove deprecated response types 2021-04-08 15:21:42 +05:30
DjDeveloperr 97ce6d1f36 add cmd meta 2021-04-08 15:10:50 +05:30
DjDeveloperr d681dc41a7 fix 2021-04-08 14:52:00 +05:30
DjDeveloperr bd530ecb15 hmm 2021-04-08 14:51:26 +05:30
DjDeveloperr 08f2f653f9 x 2021-04-08 14:45:26 +05:30
DjDeveloperr f2a8c86dde fix ratelimit event 2021-04-08 14:39:44 +05:30
DjDeveloperr b479cdc743 rework rest impl 2021-04-08 14:22:33 +05:30
DjDeveloperr 5e65673107 fix async iterator 2021-04-04 19:16:34 +05:30
DjDeveloperr 1e53d7d770 fix 2021-04-04 19:12:16 +05:30
DjDeveloperr 2123978715 x 2021-04-04 16:59:26 +05:30
DjDeveloperr 6293a1f940 stage channel 2021-04-04 16:57:02 +05:30
DjDeveloperr d63d4f4f12 x 2021-04-04 15:57:28 +05:30
Helloyunho 6f4f06fd8b 🐛 Fix voice channel cycle import 2021-04-04 19:11:56 +09:00
DjDeveloperr d829c91c3e i tried 2021-04-04 14:59:56 +05:30
DjDeveloperr a8c9c2e7e0 fix 2021-04-04 13:58:08 +05:30
DjDeveloperr 66e7e48bed no default log 2021-04-04 13:53:05 +05:30
DjDeveloperr eca3cf4e0e export channel types 2021-04-04 13:52:07 +05:30
DjDeveloperr f7adfd0f7e better handling 2021-04-04 13:51:08 +05:30
DjDeveloperr 38c02bb981 x 2021-04-04 13:16:15 +05:30
DjDeveloperr 1c7416628d add RESTManager#endpoints 2021-04-04 13:15:37 +05:30
DjDeveloperr de7996d552 clean up 2021-04-04 11:24:22 +05:30
DjDeveloperr 46cbd66166 move out src/test to test 2021-04-04 11:22:47 +05:30
DjDeveloperr f812e06d17 more 2021-04-04 11:15:41 +05:30
DjDeveloperr 22e041f440 refactor 2021-04-04 11:12:15 +05:30
DjDeveloper 7dc316c76f
Merge pull request #106 from DjDeveloperr/slash
BREAKING: DM Slash Commands, new Interactions API changes
2021-04-04 10:34:32 +05:30
DjDeveloperr 2b46b38908 encoding util 2021-04-04 10:21:39 +05:30
DjDeveloperr 38b11f4076 fix 2021-04-04 10:14:40 +05:30
DjDeveloperr f8d7cbccc7 x 2021-04-04 10:13:26 +05:30
DjDeveloperr 8bf2c1e99d required changes 2021-04-04 10:11:00 +05:30
DjDeveloperr 22eb7e0437 x 2021-04-03 11:14:05 +05:30
DjDeveloperr 0dfbcd7a7b more 2021-04-03 11:06:03 +05:30
DjDeveloperr 582b296393 feat: stage channels 2021-04-03 11:02:25 +05:30
DjDeveloper 01f92b35bf
Merge pull request #119 from Helloyunho/voiceState-updates
Add things to `<VoiceState>` and Add `<GuildChannelVoiceStatesManager>`
2021-04-03 10:20:38 +05:30
DjDeveloper 5add0283f9
Merge pull request #120 from Snowflake107/patch-1
export colorutil
2021-04-03 10:20:10 +05:30
MegaPixel 5a8514949c
make it happy I guess 2021-04-03 10:24:43 +05:45
MegaPixel 5e89e3b8e4
export colorutil 2021-04-03 10:13:25 +05:45
Helloyunho 3fe5ce3063 Add voice channel disconnect test 2021-03-31 11:29:07 +09:00
Helloyunho 96b273ed04 Add disconnection functions to voice channel 2021-03-31 11:05:21 +09:00
Helloyunho 51b1d71e9a Add guild channel voice states manager 2021-03-31 02:18:24 +09:00
Helloyunho d2cedeceb1 Add voice kick test 2021-03-31 02:01:19 +09:00
Helloyunho 064c3a6d76 🐛 Make <Member>.edit check channel type more precisely 2021-03-31 02:00:25 +09:00
Helloyunho 9c5aea1ef8 🐛 Replace the guild as the manager's guild if it's undefined 2021-03-31 01:54:21 +09:00
Helloyunho b4a1ae389d Add some few things that are useful to voiceState 2021-03-31 01:43:01 +09:00
DjDeveloperr 82431168d3 fix 2021-03-30 17:45:38 +05:30
DjDeveloperr 95145c1bc2 support for root, group and sub command parsing from name 2021-03-30 17:22:13 +05:30
DjDeveloperr 75620ee7ea x 2021-03-30 15:37:13 +05:30
DjDeveloperr 68cf1105c1 x 2021-03-30 15:35:52 +05:30
DjDeveloperr 218e3e7ddf x 2021-03-30 15:33:43 +05:30
DjDeveloperr a89318c3c3 x 2021-03-30 15:31:50 +05:30
DjDeveloperr 768fec7195 deploy built in support 2021-03-30 15:21:29 +05:30
DjDeveloperr 62b2aa07de remove redis dep from deps.ts and move to redisCache.ts 2021-03-29 19:41:50 +05:30
DjDeveloperr 4462d7d832 x 2021-03-29 05:43:50 -07:00
DjDeveloperr b775d3e323 x 2021-03-29 05:41:21 -07:00
DjDeveloperr 5fc58bd901 service worker 2021-03-29 05:30:40 -07:00
DjDeveloper d3ed30ce17
Merge pull request #118 from DjDeveloperr/main
fix typo
2021-03-29 10:28:42 +05:30
DjDeveloperr 0e57401a6b fix 2021-03-29 10:24:18 +05:30
DjDeveloperr 0692276f3a x 2021-03-29 10:22:52 +05:30
DjDeveloperr 004bf22126 lol i forgot to save 2021-03-29 10:15:09 +05:30
DjDeveloperr 4dc18f7266 fix typo 2021-03-29 10:10:26 +05:30
DjDeveloperr 010a48c7f0 debug logs 2021-03-29 09:39:37 +05:30
DjDeveloperr 3f7372d6a7 finna 2021-03-26 12:47:11 +05:30
DjDeveloperr ab365f9878 for real 2021-03-26 12:38:32 +05:30
DjDeveloperr 1738204406 x 2021-03-26 12:28:06 +05:30
DjDeveloperr a89ab53fa1 fix 2021-03-25 15:32:51 +05:30
DjDeveloperr c1a14fdac1 http slash commands 2021-03-14 15:46:44 +05:30
DjDeveloperr b02b16ffcc i give up 2021-03-14 15:17:19 +05:30
DjDeveloperr 1b77ea0411 wt 2021-03-14 15:15:37 +05:30
DjDeveloperr 2ca5005517 wtf 2021-03-14 15:14:28 +05:30
DjDeveloperr dcb07a9aaa x 2021-03-14 15:12:05 +05:30
DjDeveloperr e65fa8fded try fix verifyKey 2021-03-14 14:20:15 +05:30
DjDeveloper 5f75fc3e71
fix ffs 2021-03-12 15:12:38 +05:30
DjDeveloper c836ca8f42
fix 2021-03-12 15:05:43 +05:30
DjDeveloper 5b015c6c08
fix interaction user 2021-03-12 15:01:14 +05:30
DjDeveloper 90159fa2da
fix interaction channel 2021-03-12 13:31:48 +05:30
Helloyunho 3e81002c33
Merge branch 'main' into slash 2021-03-05 19:47:30 +09:00
DjDeveloperr f2f292b0ee Merge remote-tracking branch 'origin/main' into slash 2021-02-22 19:15:34 +05:30
DjDeveloperr dffce5bd0b new 2021-02-17 14:25:04 +05:30
DjDeveloperr 7bdbb165f4 up 2021-02-17 14:00:07 +05:30
DjDeveloperr 98874dd7e7 fix 2021-02-15 13:38:07 +05:30
DjDeveloperr a800f394ac new things 2021-02-12 17:07:38 +05:30
DjDeveloperr f4b4c640b3 Merge remote-tracking branch 'origin/main' into slash 2021-02-10 18:00:10 +05:30
DjDeveloperr 086e4a95c3 lot of stuff 2021-02-10 17:59:21 +05:30
182 changed files with 6241 additions and 2823 deletions

View File

@ -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

3
.gitignore vendored
View File

@ -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
@ -117,4 +118,4 @@ src/test/config.ts
# Webstorm dont forget this duude :) # Webstorm dont forget this duude :)
.idea/ .idea/
src/test/music.mp3 src/test/music.mp3

View File

@ -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>
@ -59,7 +59,7 @@ 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}`)
} }
}) })
@ -95,7 +95,7 @@ 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`)
} }
} }
@ -156,7 +156,7 @@ Documentation is available for `main` (branch) and `stable` (release).
## 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

151
deploy.ts Normal file
View 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'

11
deps.ts
View File

@ -1,11 +1,6 @@
export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' export { EventEmitter } from 'https://deno.land/x/event@1.0.0/mod.ts'
export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts' export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts'
export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts' export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts'
export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts' export { walk } from 'https://deno.land/std@0.95.0/fs/walk.ts'
export type { export { join } from 'https://deno.land/std@0.95.0/path/mod.ts'
Redis,
RedisConnectOptions
} from 'https://deno.land/x/redis@v0.14.1/mod.ts'
export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts'
export { join } from 'https://deno.land/std@0.86.0/path/mod.ts'
export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0'

98
mod.ts
View File

@ -1,14 +1,18 @@
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 { GatewayTypedEvents } from './src/gateway/index.ts' export type { GatewayTypedEvents } from './src/gateway/mod.ts'
export type { ClientEvents } from './src/gateway/handlers/index.ts' export type { ClientEvents } from './src/gateway/handlers/mod.ts'
export * from './src/models/client.ts' export * from './src/client/mod.ts'
export * from './src/models/slashClient.ts' export * from './src/interactions/mod.ts'
export { RESTManager, TokenType, HttpResponseCode } from './src/models/rest.ts' export {
export type { RequestHeaders } from './src/models/rest.ts' RESTManager,
export type { RESTOptions } from './src/models/rest.ts' TokenType,
export * from './src/models/cacheAdapter.ts' 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,
@ -16,16 +20,16 @@ export {
CommandsManager, CommandsManager,
CategoriesManager, CategoriesManager,
CommandsLoader CommandsLoader
} from './src/models/command.ts' } from './src/commands/command.ts'
export type { CommandContext, CommandOptions } from './src/models/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'
@ -35,7 +39,9 @@ export { GuildChannelsManager } 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/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'
@ -45,8 +51,9 @@ 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 { 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'
@ -62,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, MessageAttachment } 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,
@ -87,7 +98,7 @@ export { Intents } from './src/utils/intents.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 { HarmonyEventEmitter } from './src/utils/events.ts' export { HarmonyEventEmitter } from './src/utils/events.ts'
export type { EveryChannelTypes } from './src/utils/getChannelByType.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,
@ -95,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 {
@ -109,9 +128,18 @@ export type {
GuildVoiceChannelPayload, GuildVoiceChannelPayload,
GroupDMChannelPayload, GroupDMChannelPayload,
MessageOptions, MessageOptions,
MessagePayload,
MessageInteractionPayload,
MessageReference,
MessageActivity,
MessageActivityTypes,
MessageApplication,
MessageFlags,
MessageStickerFormatTypes,
MessageStickerPayload,
MessageTypes,
OverwriteAsArg, OverwriteAsArg,
Overwrite, Overwrite
OverwriteAsOptions
} 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 { Verification } from './src/types/guild.ts'
@ -144,4 +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/models/collectors.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"

22
src/cache/adapter.ts vendored Normal file
View 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
View 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
View File

@ -0,0 +1,4 @@
export * from './adapter.ts'
export * from './default.ts'
// Not exported by default
// export * from './redis.ts'

View File

@ -1,176 +1,110 @@
import { Collection } from '../utils/collection.ts' import { ICacheAdapter } from './adapter.ts'
import { connect, Redis, RedisConnectOptions } from '../../deps.ts' // Not in deps.ts to allow optional dep loading
import {
/** connect,
* ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. Redis,
* RedisConnectOptions
* Methods can return Promises too. } from 'https://deno.land/x/redis@v0.22.0/mod.ts'
*/
export interface ICacheAdapter { /** Redis Cache Adapter for using Redis as a cache-provider. */
/** Gets a key from a Cache */ export class RedisCacheAdapter implements ICacheAdapter {
get: (cacheName: string, key: string) => Promise<any> | any _redis: Promise<Redis>
/** Sets a key to value in a Cache Name with optional expire value in MS */ redis?: Redis
set: ( ready: boolean = false
cacheName: string, readonly _expireIntervalTimer: number = 5000
key: string, private _expireInterval?: number
value: any,
expire?: number constructor(options: RedisConnectOptions) {
) => Promise<any> | any this._redis = connect(options)
/** Deletes a key from a Cache */ this._redis.then(
delete: (cacheName: string, key: string) => Promise<boolean> | boolean (redis) => {
/** Gets array of all values in a Cache */ this.redis = redis
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> this.ready = true
/** Entirely deletes a Cache */ this._startExpireInterval()
deleteCache: (cacheName: string) => any },
} () => {
// TODO: Make error for this
/** Default Cache Adapter for in-memory caching. */ }
export class DefaultCacheAdapter implements ICacheAdapter { )
data: { }
[name: string]: Collection<string, any>
} = {} private _startExpireInterval(): void {
this._expireInterval = setInterval(() => {
async get(cacheName: string, key: string): Promise<undefined | any> { this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => {
const cache = this.data[cacheName] for (const name of names) {
if (cache === undefined) return this.redis?.hvals(name).then((vals) => {
return cache.get(key) for (const val of vals) {
} const expireVal: {
name: string
async set( key: string
cacheName: string, at: number
key: string, } = JSON.parse(val)
value: any, const expired = new Date().getTime() > expireVal.at
expire?: number if (expired) this.redis?.hdel(expireVal.name, expireVal.key)
): Promise<any> { }
let cache = this.data[cacheName] })
if (cache === undefined) { }
this.data[cacheName] = new Collection() })
cache = this.data[cacheName] }, this._expireIntervalTimer)
} }
cache.set(key, value)
if (expire !== undefined) async _checkReady(): Promise<void> {
setTimeout(() => { if (!this.ready) await this._redis
cache.delete(key) }
}, expire)
} async get(cacheName: string, key: string): Promise<string | undefined> {
await this._checkReady()
async delete(cacheName: string, key: string): Promise<boolean> { const cache = await this.redis?.hget(cacheName, key)
const cache = this.data[cacheName] if (cache === undefined) return
if (cache === undefined) return false try {
return cache.delete(key) return JSON.parse(cache)
} } catch (e) {
return cache
async array(cacheName: string): Promise<any[] | undefined> { }
const cache = this.data[cacheName] }
if (cache === undefined) return
return cache.array() async set(
} cacheName: string,
key: string,
async deleteCache(cacheName: string): Promise<boolean> { value: any,
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete expire?: number
return delete this.data[cacheName] ): Promise<number | undefined> {
} await this._checkReady()
} const result = await this.redis?.hset(
cacheName,
/** Redis Cache Adapter for using Redis as a cache-provider. */ key,
export class RedisCacheAdapter implements ICacheAdapter { typeof value === 'object' ? JSON.stringify(value) : value
_redis: Promise<Redis> )
redis?: Redis if (expire !== undefined) {
ready: boolean = false await this.redis?.hset(
readonly _expireIntervalTimer: number = 5000 `${cacheName}:expires`,
private _expireInterval?: number key,
JSON.stringify({
constructor(options: RedisConnectOptions) { name: cacheName,
this._redis = connect(options) key,
this._redis.then( at: new Date().getTime() + expire
(redis) => { })
this.redis = redis )
this.ready = true }
this._startExpireInterval() return result
}, }
() => {
// TODO: Make error for this async delete(cacheName: string, key: string): Promise<boolean> {
} await this._checkReady()
) const exists = await this.redis?.hexists(cacheName, key)
} if (exists === 0) return false
await this.redis?.hdel(cacheName, key)
private _startExpireInterval(): void { return true
this._expireInterval = setInterval(() => { }
this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => {
for (const name of names) { async array(cacheName: string): Promise<any[] | undefined> {
this.redis?.hvals(name).then((vals) => { await this._checkReady()
for (const val of vals) { const data = await this.redis?.hvals(cacheName)
const expireVal: { return data?.map((e: string) => JSON.parse(e))
name: string }
key: string
at: number async deleteCache(cacheName: string): Promise<boolean> {
} = JSON.parse(val) await this._checkReady()
const expired = new Date().getTime() > expireVal.at return (await this.redis?.del(cacheName)) !== 0
if (expired) this.redis?.hdel(expireVal.name, expireVal.key) }
} }
})
}
})
}, this._expireIntervalTimer)
}
async _checkReady(): Promise<void> {
if (!this.ready) await this._redis
}
async get(cacheName: string, key: string): Promise<string | undefined> {
await this._checkReady()
const cache = await this.redis?.hget(cacheName, key)
if (cache === undefined) return
try {
return JSON.parse(cache)
} catch (e) {
return cache
}
}
async set(
cacheName: string,
key: string,
value: any,
expire?: number
): Promise<number | undefined> {
await this._checkReady()
const result = await this.redis?.hset(
cacheName,
key,
typeof value === 'object' ? JSON.stringify(value) : value
)
if (expire !== undefined) {
await this.redis?.hset(
`${cacheName}:expires`,
key,
JSON.stringify({
name: cacheName,
key,
at: new Date().getTime() + expire
})
)
}
return result
}
async delete(cacheName: string, key: string): Promise<boolean> {
await this._checkReady()
const exists = await this.redis?.hexists(cacheName, key)
if (exists === 0) return false
await this.redis?.hdel(cacheName, key)
return true
}
async array(cacheName: string): Promise<any[] | undefined> {
await this._checkReady()
const data = await this.redis?.hvals(cacheName)
return data?.map((e: string) => JSON.parse(e))
}
async deleteCache(cacheName: string): Promise<boolean> {
await this._checkReady()
return (await this.redis?.del(cacheName)) !== 0
}
}

View File

@ -1,494 +1,462 @@
/* eslint-disable @typescript-eslint/method-signature-style */ /* eslint-disable @typescript-eslint/method-signature-style */
import { User } from '../structures/user.ts' import type { User } from '../structures/user.ts'
import { GatewayIntents } from '../types/gateway.ts' import { GatewayIntents } from '../types/gateway.ts'
import { Gateway } from '../gateway/index.ts' import { Gateway } from '../gateway/mod.ts'
import { RESTManager, RESTOptions, TokenType } from './rest.ts' import { RESTManager, RESTOptions, TokenType } from '../rest/mod.ts'
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' import { DefaultCacheAdapter, ICacheAdapter } from '../cache/mod.ts'
import { UsersManager } from '../managers/users.ts' import { UsersManager } from '../managers/users.ts'
import { GuildManager } from '../managers/guilds.ts' import { GuildManager } from '../managers/guilds.ts'
import { ChannelsManager } from '../managers/channels.ts' import { ChannelsManager } from '../managers/channels.ts'
import { ClientPresence } from '../structures/presence.ts' import { ClientPresence } from '../structures/presence.ts'
import { EmojisManager } from '../managers/emojis.ts' import { EmojisManager } from '../managers/emojis.ts'
import { ActivityGame, ClientActivity } from '../types/presence.ts' import { ActivityGame, ClientActivity } from '../types/presence.ts'
import { Extension } from './extensions.ts' import type { Extension } from '../commands/extension.ts'
import { SlashClient } from './slashClient.ts' import { SlashClient } from '../interactions/slashClient.ts'
import { Interaction } from '../structures/slash.ts' import { ShardManager } from './shard.ts'
import { SlashModule } from './slashModule.ts' import { Application } from '../structures/application.ts'
import { ShardManager } from './shard.ts' import { Invite } from '../structures/invite.ts'
import { Application } from '../structures/application.ts' import { INVITE } from '../types/endpoint.ts'
import { Invite } from '../structures/invite.ts' import type { ClientEvents } from '../gateway/handlers/mod.ts'
import { INVITE } from '../types/endpoint.ts' import type { Collector } from './collectors.ts'
import { ClientEvents } from '../gateway/handlers/index.ts' import { HarmonyEventEmitter } from '../utils/events.ts'
import type { Collector } from './collectors.ts' import type { VoiceRegion } from '../types/voice.ts'
import { HarmonyEventEmitter } from '../utils/events.ts' import { fetchAuto } from '../../deps.ts'
import { VoiceRegion } from '../types/voice.ts' import type { DMChannel } from '../structures/dmChannel.ts'
import { fetchAuto } from '../../deps.ts' import { Template } from '../structures/template.ts'
import { DMChannel } from '../structures/dmChannel.ts' import { VoiceManager } from './voice.ts'
import { Template } from '../structures/template.ts'
/** OS related properties sent with Gateway Identify */
/** OS related properties sent with Gateway Identify */ export interface ClientProperties {
export interface ClientProperties { os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string
os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string browser?: 'harmony' | string
browser?: 'harmony' | string device?: 'harmony' | string
device?: 'harmony' | string }
}
/** Some Client Options to modify behaviour */
/** Some Client Options to modify behaviour */ export interface ClientOptions {
export interface ClientOptions { /** ID of the Client/Application to initialize Slash Client REST */
/** ID of the Client/Application to initialize Slash Client REST */ id?: string
id?: string /** Token of the Bot/User */
/** Token of the Bot/User */ token?: string
token?: string /** Gateway Intents */
/** Gateway Intents */ intents?: Array<GatewayIntents | keyof typeof GatewayIntents>
intents?: GatewayIntents[] /** Cache Adapter to use, defaults to Collections one */
/** Cache Adapter to use, defaults to Collections one */ cache?: ICacheAdapter
cache?: ICacheAdapter /** Force New Session and don't use cached Session (by persistent caching) */
/** Force New Session and don't use cached Session (by persistent caching) */ forceNewSession?: boolean
forceNewSession?: boolean /** Startup presence of client */
/** Startup presence of client */ presence?: ClientPresence | ClientActivity | ActivityGame
presence?: ClientPresence | ClientActivity | ActivityGame /** Force all requests to Canary API */
/** Force all requests to Canary API */ canary?: boolean
canary?: boolean /** Time till which Messages are to be cached, in MS. Default is 3600000 */
/** Time till which Messages are to be cached, in MS. Default is 3600000 */ messageCacheLifetime?: number
messageCacheLifetime?: number /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */
/** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ reactionCacheLifetime?: number
reactionCacheLifetime?: number /** Whether to fetch Uncached Message of Reaction or not? */
/** Whether to fetch Uncached Message of Reaction or not? */ fetchUncachedReactions?: boolean
fetchUncachedReactions?: boolean /** Client Properties */
/** Client Properties */ clientProperties?: ClientProperties
clientProperties?: ClientProperties /** Enable/Disable Slash Commands Integration (enabled by default) */
/** Enable/Disable Slash Commands Integration (enabled by default) */ enableSlash?: boolean
enableSlash?: boolean /** Disable taking token from env if not provided (token is taken from env if present by default) */
/** Disable taking token from env if not provided (token is taken from env if present by default) */ disableEnvToken?: boolean
disableEnvToken?: boolean /** Override REST Options */
/** Override REST Options */ restOptions?: RESTOptions
restOptions?: RESTOptions /** Whether to fetch Gateway info or not */
/** Whether to fetch Gateway info or not */ fetchGatewayInfo?: boolean
fetchGatewayInfo?: boolean /** ADVANCED: Shard ID to launch on */
/** ADVANCED: Shard ID to launch on */ shard?: number
shard?: number /** ADVACNED: Shard count. */
/** ADVACNED: Shard count. */ shardCount?: number | 'auto'
shardCount?: number | 'auto' }
}
/**
/** * Harmony Client. Provides high-level interface over the REST and WebSocket API.
* Discord Client. */
*/ export class Client extends HarmonyEventEmitter<ClientEvents> {
export class Client extends HarmonyEventEmitter<ClientEvents> { /** REST Manager - used to make all requests */
/** REST Manager - used to make all requests */ rest: RESTManager
rest: RESTManager /** User which Client logs in to, undefined until logs in */
/** User which Client logs in to, undefined until logs in */ user?: User
user?: User
/** WebSocket ping of Client */ #token?: string
ping = 0
/** Token of the Bot/User */ /** Token of the Bot/User */
token?: string get token(): string | undefined {
/** Cache Adapter */ return this.#token
cache: ICacheAdapter = new DefaultCacheAdapter() }
/** Gateway Intents */
intents?: GatewayIntents[] set token(val: string | undefined) {
/** Whether to force new session or not */ this.#token = val
forceNewSession?: boolean }
/** Time till messages to stay cached, in MS. */
messageCacheLifetime: number = 3600000 /** Cache Adapter */
/** Time till messages to stay cached, in MS. */ get cache(): ICacheAdapter {
reactionCacheLifetime: number = 3600000 return this.#cache
/** Whether to fetch Uncached Message of Reaction or not? */ }
fetchUncachedReactions: boolean = false
/** Client Properties */ set cache(val: ICacheAdapter) {
clientProperties: ClientProperties this.#cache = val
/** Slash-Commands Management client */ }
slash: SlashClient
/** Whether to fetch Gateway info or not */ #cache: ICacheAdapter = new DefaultCacheAdapter()
fetchGatewayInfo: boolean = true
/** Gateway Intents */
/** Users Manager, containing all Users cached */ intents?: GatewayIntents[]
users: UsersManager = new UsersManager(this) /** Whether to force new session or not */
/** Guilds Manager, providing cache & API interface to Guilds */ forceNewSession?: boolean
guilds: GuildManager = new GuildManager(this) /** Time till messages to stay cached, in MS. */
/** Channels Manager, providing cache interface to Channels */ messageCacheLifetime: number = 3600000
channels: ChannelsManager = new ChannelsManager(this) /** Time till messages to stay cached, in MS. */
/** Channels Manager, providing cache interface to Channels */ reactionCacheLifetime: number = 3600000
emojis: EmojisManager = new EmojisManager(this) /** Whether to fetch Uncached Message of Reaction or not? */
fetchUncachedReactions: boolean = false
/** Last READY timestamp */
upSince?: Date /** Client Properties */
readonly clientProperties!: ClientProperties
/** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence() /** Slash-Commands Management client */
_decoratedEvents?: { slash: SlashClient
[name: string]: (...args: any[]) => void /** Whether to fetch Gateway info or not */
} fetchGatewayInfo: boolean = true
_decoratedSlash?: Array<{ /** Voice Connections Manager */
name: string readonly voice = new VoiceManager(this)
guild?: string
parent?: string /** Users Manager, containing all Users cached */
group?: string readonly users: UsersManager = new UsersManager(this)
handler: (interaction: Interaction) => any /** Guilds Manager, providing cache & API interface to Guilds */
}> readonly guilds: GuildManager = new GuildManager(this)
/** Channels Manager, providing cache interface to Channels */
_id?: string readonly channels: ChannelsManager = new ChannelsManager(this)
/** Channels Manager, providing cache interface to Channels */
/** Shard on which this Client is */ readonly emojis: EmojisManager = new EmojisManager(this)
shard?: number
/** Shard Count */ /** Last READY timestamp */
shardCount: number | 'auto' = 'auto' upSince?: Date
/** Shard Manager of this Client if Sharded */
shards: ShardManager /** Client's presence. Startup one if set before connecting */
/** Collectors set */ presence: ClientPresence = new ClientPresence()
collectors: Set<Collector> = new Set()
_id?: string
/** Since when is Client online (ready). */
get uptime(): number { /** Shard on which this Client is */
if (this.upSince === undefined) return 0 shard?: number
else { /** Shard Count */
const dif = Date.now() - this.upSince.getTime() shardCount: number | 'auto' = 'auto'
if (dif < 0) return 0 /** Shard Manager of this Client if Sharded */
else return dif shards: ShardManager
} /** Collectors set */
} collectors: Set<Collector> = new Set()
get gateway(): Gateway { /** Since when is Client online (ready). */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion get uptime(): number {
return this.shards.list.get('0') as Gateway if (this.upSince === undefined) return 0
} else {
const dif = Date.now() - this.upSince.getTime()
applicationID?: string if (dif < 0) return 0
applicationFlags?: number else return dif
}
constructor(options: ClientOptions = {}) { }
super()
this._id = options.id /** Get Shard 0's Gateway */
this.token = options.token get gateway(): Gateway {
this.intents = options.intents return this.shards.list.get('0')!
this.shards = new ShardManager(this) }
this.forceNewSession = options.forceNewSession
if (options.cache !== undefined) this.cache = options.cache applicationID?: string
if (options.presence !== undefined) applicationFlags?: number
this.presence =
options.presence instanceof ClientPresence constructor(options: ClientOptions = {}) {
? options.presence super()
: new ClientPresence(options.presence) this._id = options.id
if (options.messageCacheLifetime !== undefined) this.token = options.token
this.messageCacheLifetime = options.messageCacheLifetime this.intents = options.intents?.map((e) =>
if (options.reactionCacheLifetime !== undefined) typeof e === 'string' ? GatewayIntents[e] : e
this.reactionCacheLifetime = options.reactionCacheLifetime )
if (options.fetchUncachedReactions === true) this.shards = new ShardManager(this)
this.fetchUncachedReactions = true this.forceNewSession = options.forceNewSession
if (options.cache !== undefined) this.cache = options.cache
if ( if (options.presence !== undefined)
this._decoratedEvents !== undefined && this.presence =
Object.keys(this._decoratedEvents).length !== 0 options.presence instanceof ClientPresence
) { ? options.presence
Object.entries(this._decoratedEvents).forEach((entry) => { : new ClientPresence(options.presence)
this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) if (options.messageCacheLifetime !== undefined)
}) this.messageCacheLifetime = options.messageCacheLifetime
this._decoratedEvents = undefined if (options.reactionCacheLifetime !== undefined)
} this.reactionCacheLifetime = options.reactionCacheLifetime
if (options.fetchUncachedReactions === true)
this.clientProperties = this.fetchUncachedReactions = true
options.clientProperties === undefined
? { if (
os: Deno.build.os, (this as any)._decoratedEvents !== undefined &&
browser: 'harmony', Object.keys((this as any)._decoratedEvents).length !== 0
device: 'harmony' ) {
} Object.entries((this as any)._decoratedEvents).forEach((entry) => {
: options.clientProperties this.on(entry[0] as keyof ClientEvents, (entry as any)[1].bind(this))
})
if (options.shard !== undefined) this.shard = options.shard ;(this as any)._decoratedEvents = undefined
if (options.shardCount !== undefined) this.shardCount = options.shardCount }
this.fetchGatewayInfo = options.fetchGatewayInfo ?? true Object.defineProperty(this, 'clientProperties', {
value:
if (this.token === undefined) { options.clientProperties === undefined
try { ? {
const token = Deno.env.get('DISCORD_TOKEN') os: Deno.build.os,
if (token !== undefined) { browser: 'harmony',
this.token = token device: 'harmony'
this.debug('Info', 'Found token in ENV') }
} : options.clientProperties,
} catch (e) {} enumerable: false
} })
const restOptions: RESTOptions = { if (options.shard !== undefined) this.shard = options.shard
token: () => this.token, if (options.shardCount !== undefined) this.shardCount = options.shardCount
tokenType: TokenType.Bot,
canary: options.canary, this.fetchGatewayInfo = options.fetchGatewayInfo ?? true
client: this
} if (this.token === undefined) {
try {
if (options.restOptions !== undefined) const token = Deno.env.get('DISCORD_TOKEN')
Object.assign(restOptions, options.restOptions) if (token !== undefined) {
this.rest = new RESTManager(restOptions) this.token = token
this.debug('Info', 'Found token in ENV')
this.slash = new SlashClient({ }
id: () => this.getEstimatedID(), } catch (e) {}
client: this, }
enabled: options.enableSlash
}) const restOptions: RESTOptions = {
} token: () => this.token,
tokenType: TokenType.Bot,
/** canary: options.canary,
* Sets Cache Adapter client: this
* }
* Should NOT be set after bot is already logged in or using current cache.
* Please look into using `cache` option. if (options.restOptions !== undefined)
*/ Object.assign(restOptions, options.restOptions)
setAdapter(adapter: ICacheAdapter): Client { this.rest = new RESTManager(restOptions)
this.cache = adapter
return this this.slash = new SlashClient({
} id: () => this.getEstimatedID(),
client: this,
/** Changes Presence of Client */ enabled: options.enableSlash
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()) * Sets Cache Adapter
} *
* Should NOT be set after bot is already logged in or using current cache.
/** Emits debug event */ * Please look into using `cache` option.
debug(tag: string, msg: string): void { */
// eslint-disable-next-line @typescript-eslint/no-floating-promises setAdapter(adapter: ICacheAdapter): Client {
this.emit('debug', `[${tag}] ${msg}`) this.cache = adapter
} return this
}
getEstimatedID(): string {
if (this.user !== undefined) return this.user.id /** Changes Presence of Client */
else if (this.token !== undefined) { setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void {
try { if (presence instanceof ClientPresence) {
return atob(this.token.split('.')[0]) this.presence = presence
} catch (e) { } else this.presence = new ClientPresence(presence)
return this._id ?? 'unknown' this.gateway?.sendPresence(this.presence.create())
} }
} else {
return this._id ?? 'unknown' /** Emits debug event */
} debug(tag: string, msg: string): void {
} // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.emit('debug', `[${tag}] ${msg}`)
/** Fetch Application of the Client */ }
async fetchApplication(): Promise<Application> {
const app = await this.rest.api.oauth2.applications['@me'].get() getEstimatedID(): string {
return new Application(this, app) if (this.user !== undefined) return this.user.id
} else if (this.token !== undefined) {
try {
/** Fetch an Invite */ return atob(this.token.split('.')[0])
async fetchInvite(id: string): Promise<Invite> { } catch (e) {
return await new Promise((resolve, reject) => { return this._id ?? 'unknown'
this.rest }
.get(INVITE(id)) } else {
.then((data) => { return this._id ?? 'unknown'
resolve(new Invite(this, data)) }
}) }
.catch((e) => reject(e))
}) /** Fetch Application of the Client */
} async fetchApplication(): Promise<Application> {
const app = await this.rest.api.oauth2.applications['@me'].get()
/** return new Application(this, app)
* 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. /** Fetch an Invite */
*/ async fetchInvite(id: string): Promise<Invite> {
async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> { return await new Promise((resolve, reject) => {
token ??= this.token this.rest
if (token === undefined) throw new Error('No Token Provided') .get(INVITE(id))
this.token = token .then((data) => {
if (intents !== undefined && this.intents !== undefined) { resolve(new Invite(this, data))
this.debug( })
'client', .catch((e) => reject(e))
'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 function is used for connecting to discord.
this.intents = intents * @param token Your token. This is required if not given in ClientOptions.
} else throw new Error('No Gateway Intents were provided') * @param intents Gateway intents in array. This is required if not given in ClientOptions.
*/
this.rest.token = token async connect(
if (this.shard !== undefined) { token?: string,
if (typeof this.shardCount === 'number') intents?: Array<GatewayIntents | keyof typeof GatewayIntents>
this.shards.cachedShardCount = this.shardCount ): Promise<Client> {
await this.shards.launch(this.shard) token ??= this.token
} else await this.shards.connect() if (token === undefined) throw new Error('No Token Provided')
return this.waitFor('ready', () => true).then(() => this) this.token = token
} if (intents !== undefined && this.intents !== undefined) {
this.debug(
/** Destroy the Gateway connection */ 'client',
async destroy(): Promise<Client> { 'Intents were set in both client and connect function. Using the one in the connect function...'
this.gateway.initialized = false )
this.gateway.sequenceID = undefined } else if (intents === undefined && this.intents !== undefined) {
this.gateway.sessionID = undefined intents = this.intents
await this.gateway.cache.delete('seq') } else if (intents !== undefined && this.intents === undefined) {
await this.gateway.cache.delete('session_id') this.intents = intents.map((e) =>
this.gateway.close() typeof e === 'string' ? GatewayIntents[e] : e
this.user = undefined )
this.upSince = undefined } else throw new Error('No Gateway Intents were provided')
return this
} this.rest.token = token
if (this.shard !== undefined) {
/** Attempt to Close current Gateway connection and Resume */ if (typeof this.shardCount === 'number')
async reconnect(): Promise<Client> { this.shards.cachedShardCount = this.shardCount
this.gateway.close() await this.shards.launch(this.shard)
this.gateway.initWebsocket() } else await this.shards.connect()
return this.waitFor('ready', () => true).then(() => this) return this.waitFor('ready', () => true).then(() => this)
} }
/** Add a new Collector */ /** Destroy the Gateway connection */
addCollector(collector: Collector): boolean { async destroy(): Promise<Client> {
if (this.collectors.has(collector)) return false this.gateway.initialized = false
else { this.gateway.sequenceID = undefined
this.collectors.add(collector) this.gateway.sessionID = undefined
return true await this.gateway.cache.delete('seq')
} await this.gateway.cache.delete('session_id')
} this.gateway.close()
this.user = undefined
/** Remove a Collector */ this.upSince = undefined
removeCollector(collector: Collector): boolean { return this
if (!this.collectors.has(collector)) return false }
else {
this.collectors.delete(collector) /** Attempt to Close current Gateway connection and Resume */
return true async reconnect(): Promise<Client> {
} this.gateway.close()
} this.gateway.initWebsocket()
return this.waitFor('ready', () => true).then(() => this)
async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> { }
const collectors: Collector[] = []
for (const collector of this.collectors.values()) { /** Add a new Collector */
if (collector.event === event) collectors.push(collector) addCollector(collector: Collector): boolean {
} if (this.collectors.has(collector)) return false
if (collectors.length !== 0) { else {
this.collectors.forEach((collector) => collector._fire(...args)) this.collectors.add(collector)
} return true
// TODO(DjDeveloperr): Fix this ts-ignore }
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error }
// @ts-ignore
return super.emit(event, ...args) /** Remove a Collector */
} removeCollector(collector: Collector): boolean {
if (!this.collectors.has(collector)) return false
/** Returns an array of voice region objects that can be used when creating servers. */ else {
async fetchVoiceRegions(): Promise<VoiceRegion[]> { this.collectors.delete(collector)
return this.rest.api.voice.regions.get() return true
} }
}
/** Modify current (Client) User. */
async editUser(data: { async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> {
username?: string const collectors: Collector[] = []
avatar?: string for (const collector of this.collectors.values()) {
}): Promise<Client> { if (collector.event === event) collectors.push(collector)
if (data.username === undefined && data.avatar === undefined) }
throw new Error( if (collectors.length !== 0) {
'Either username or avatar or both must be specified to edit' this.collectors.forEach((collector) => collector._fire(...args))
) }
// TODO(DjDeveloperr): Fix this ts-ignore
if (data.avatar?.startsWith('http') === true) { // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
data.avatar = await fetchAuto(data.avatar) // @ts-ignore
} return super.emit(event, ...args)
}
await this.rest.api.users['@me'].patch({
username: data.username, /** Returns an array of voice region objects that can be used when creating servers. */
avatar: data.avatar async fetchVoiceRegions(): Promise<VoiceRegion[]> {
}) return this.rest.api.voice.regions.get()
return this }
}
/** Modify current (Client) User. */
/** Change Username of the Client User */ async editUser(data: {
async setUsername(username: string): Promise<Client> { username?: string
return await this.editUser({ username }) avatar?: string
} }): Promise<Client> {
if (data.username === undefined && data.avatar === undefined)
/** Change Avatar of the Client User */ throw new Error(
async setAvatar(avatar: string): Promise<Client> { 'Either username or avatar or both must be specified to edit'
return await this.editUser({ avatar }) )
}
if (data.avatar?.startsWith('http') === true) {
/** Create a DM Channel with a User */ data.avatar = await fetchAuto(data.avatar)
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({ await this.rest.api.users['@me'].patch({
recipient_id: id username: data.username,
}) avatar: data.avatar
await this.channels.set(dmPayload.id, dmPayload) })
return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel return this
} }
/** Returns a template object for the given code. */ /** Change Username of the Client User */
async fetchTemplate(code: string): Promise<Template> { async setUsername(username: string): Promise<Client> {
const payload = await this.rest.api.guilds.templates[code].get() return await this.editUser({ username })
return new Template(this, payload) }
}
} /** Change Avatar of the Client User */
async setAvatar(avatar: string): Promise<Client> {
/** Event decorator to create an Event handler from function */ return await this.editUser({ avatar })
export function event(name?: keyof ClientEvents) { }
return function (
client: Client | Extension, /** Create a DM Channel with a User */
prop: keyof ClientEvents | string async createDM(user: User | string): Promise<DMChannel> {
) { const id = typeof user === 'object' ? user.id : user
const listener = ((client as unknown) as { const dmPayload = await this.rest.api.users['@me'].channels.post({
[name in keyof ClientEvents]: (...args: ClientEvents[name]) => any recipient_id: id
})[(prop as unknown) as keyof ClientEvents] })
if (typeof listener !== 'function') await this.channels.set(dmPayload.id, dmPayload)
throw new Error('@event decorator requires a function') return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel
if (client._decoratedEvents === undefined) client._decoratedEvents = {} }
const key = name === undefined ? prop : name
/** Returns a template object for the given code. */
client._decoratedEvents[key] = listener async fetchTemplate(code: string): Promise<Template> {
} const payload = await this.rest.api.guilds.templates[code].get()
} return new Template(this, payload)
}
/** Decorator to create a Slash Command handler */ }
export function slash(name?: string, guild?: string) {
return function (client: Client | SlashClient | SlashModule, prop: string) { /** Event decorator to create an Event handler from function */
if (client._decoratedSlash === undefined) client._decoratedSlash = [] // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const item = (client as { [name: string]: any })[prop] export function event(name?: keyof ClientEvents) {
if (typeof item !== 'function') { return function (
throw new Error('@slash decorator requires a function') client: Client | Extension,
} else prop: keyof ClientEvents | string
client._decoratedSlash.push({ ) {
name: name ?? prop, // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
guild, const c = client as any
handler: item 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')
/** Decorator to create a Sub-Slash Command handler */
export function subslash(parent: string, name?: string, guild?: string) { if (c._decoratedEvents === undefined) c._decoratedEvents = {}
return function (client: Client | SlashModule | SlashClient, prop: string) { const key = name === undefined ? prop : name
if (client._decoratedSlash === undefined) client._decoratedSlash = []
const item = (client as { [name: string]: any })[prop] c._decoratedEvents[key] = listener
if (typeof item !== 'function') { }
throw new Error('@subslash decorator requires a function') }
} 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') {
throw new Error('@groupslash decorator requires a function')
} else
client._decoratedSlash.push({
group,
parent,
name: name ?? prop,
guild,
handler: item
})
}
}

View File

@ -1,5 +1,5 @@
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import type { Client } from './client.ts' import type { Client } from '../client/client.ts'
import { HarmonyEventEmitter } from '../utils/events.ts' import { HarmonyEventEmitter } from '../utils/events.ts'
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean> export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>

3
src/client/mod.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './client.ts'
export * from './collectors.ts'
export * from './shard.ts'

View File

@ -1,7 +1,7 @@
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import type { Client } from './client.ts' import type { Client } from './client.ts'
import { RESTManager } from './rest.ts' import { RESTManager } from '../rest/mod.ts'
import { Gateway } from '../gateway/index.ts' import { Gateway } from '../gateway/mod.ts'
import { HarmonyEventEmitter } from '../utils/events.ts' import { HarmonyEventEmitter } from '../utils/events.ts'
import { GatewayEvents } from '../types/gateway.ts' import { GatewayEvents } from '../types/gateway.ts'
import { delay } from '../utils/delay.ts' import { delay } from '../utils/delay.ts'
@ -61,10 +61,24 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
let shardCount: number let shardCount: number
if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount
else { else {
if (this.client.shardCount === 'auto') { if (
this.client.shardCount === 'auto' &&
this.client.fetchGatewayInfo !== false
) {
this.debug('Fetch /gateway/bot...')
const info = await this.client.rest.api.gateway.bot.get() 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 shardCount = info.shards as number
} else shardCount = this.client.shardCount ?? 1 } else
shardCount =
typeof this.client.shardCount === 'string'
? 1
: this.client.shardCount ?? 1
} }
this.cachedShardCount = shardCount this.cachedShardCount = shardCount
return this.cachedShardCount return this.cachedShardCount
@ -79,8 +93,6 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
const shardCount = await this.getShardCount() const shardCount = await this.getShardCount()
const gw = new Gateway(this.client, [Number(id), shardCount]) const gw = new Gateway(this.client, [Number(id), shardCount])
gw.token = this.client.token
gw.intents = this.client.intents
this.list.set(id.toString(), gw) this.list.set(id.toString(), gw)
gw.initWebsocket() gw.initWebsocket()

116
src/client/voice.ts Normal file
View 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)
}
}

View File

@ -1,6 +1,6 @@
import { Message } from '../structures/message.ts' import type { Message } from '../structures/message.ts'
import { GuildTextBasedChannel } from '../structures/guildTextChannel.ts' import type { GuildTextBasedChannel } from '../structures/guildTextChannel.ts'
import { Client, ClientOptions } from './client.ts' import { Client, ClientOptions } from '../client/mod.ts'
import { import {
CategoriesManager, CategoriesManager,
Command, Command,
@ -9,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[]>
@ -43,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
@ -65,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
@ -111,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(
@ -235,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,
@ -362,22 +367,26 @@ export class CommandClient extends Client implements CommandClientOptions {
const result = await command.execute(ctx) const result = await command.execute(ctx)
await command.afterExecute(ctx, result) await command.afterExecute(ctx, result)
} catch (e) { } catch (e) {
await command try {
.onError(ctx, e) await command.onError(ctx, e)
.catch((e: Error) => this.emit('commandError', ctx, e)) } catch (e) {
this.emit('commandError', ctx, e)
}
this.emit('commandError', ctx, e) this.emit('commandError', ctx, e)
} }
} }
} }
/** Command decorator */ /**
* 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 (typeof prop !== 'function') if (typeof prop !== 'function')
throw new Error('@command decorator can only be used on class methods') throw new Error('@command decorator can only be used on class methods')
@ -391,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
} }
} }

View File

@ -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 { join, walk } 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[]
@ -298,6 +300,8 @@ export class CommandsLoader {
/** /**
* Load a Command from file. * Load a Command from file.
* *
* NOTE: Relative paths resolve from cwd
*
* @param filePath Path of Command file. * @param filePath Path of Command file.
* @param exportName Export name. Default is the "default" export. * @param exportName Export name. Default is the "default" export.
*/ */
@ -342,6 +346,8 @@ export class CommandsLoader {
/** /**
* Load commands from a Directory. * Load commands from a Directory.
* *
* NOTE: Relative paths resolve from cwd
*
* @param path Path of the directory. * @param path Path of the directory.
* @param options Options to configure loading. * @param options Options to configure loading.
*/ */
@ -486,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) =>

View File

@ -1,7 +1,7 @@
import { ClientEvents } from '../../mod.ts'
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
@ -73,27 +73,25 @@ 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] as keyof ClientEvents, entry[1].bind(this)) this.listen(entry[0] as keyof ClientEvents, entry[1].bind(this))
}) })
this._decoratedEvents = undefined self._decoratedEvents = undefined
} }
} }

3
src/commands/mod.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './client.ts'
export * from './command.ts'
export * from './extension.ts'

View File

@ -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

View File

@ -1,6 +1,6 @@
import { SlashCommand } from '../../models/slashClient.ts' import { SlashCommand } from '../../interactions/slashCommand.ts'
import { ApplicationCommandPayload } from '../../types/gateway.ts' import { ApplicationCommandPayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts' import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const applicationCommandCreate: GatewayEventHandler = async ( export const applicationCommandCreate: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,

View File

@ -1,6 +1,6 @@
import { SlashCommand } from '../../models/slashClient.ts' import { SlashCommand } from '../../interactions/slashCommand.ts'
import { ApplicationCommandPayload } from '../../types/gateway.ts' import { ApplicationCommandPayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts' import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const applicationCommandDelete: GatewayEventHandler = async ( export const applicationCommandDelete: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,

View File

@ -1,6 +1,6 @@
import { SlashCommand } from '../../models/slashClient.ts' import { SlashCommand } from '../../interactions/slashCommand.ts'
import { ApplicationCommandPayload } from '../../types/gateway.ts' import { ApplicationCommandPayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts' import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const applicationCommandUpdate: GatewayEventHandler = async ( export const applicationCommandUpdate: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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,

View File

@ -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,

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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 {
InteractionApplicationCommandResolved,
SlashCommandInteraction
} from '../../structures/slash.ts'
import {
Interaction,
InteractionChannel
} from '../../structures/interactions.ts'
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
import { InteractionPayload } from '../../types/slash.ts' import {
import { Gateway, GatewayEventHandler } from '../index.ts' 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<GuildTextBasedChannel>(d.channel_id)) ?? await guild?.members.set(d.member.user.id, d.member)
(await gateway.client.channels.fetch<GuildTextBasedChannel>(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)
} }

View File

@ -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'

View File

@ -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'

View File

@ -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,

View File

@ -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,

View File

@ -1,8 +1,8 @@
import { Message } from '../../structures/message.ts' import type { Message } from '../../structures/message.ts'
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -1,5 +1,5 @@
import { GatewayEventHandler } from '../index.ts' import type { GatewayEventHandler } from '../mod.ts'
import { import type {
GatewayEvents, GatewayEvents,
MessageDeletePayload, MessageDeletePayload,
TypingStartGuildData TypingStartGuildData
@ -31,18 +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 { TextChannel } from '../../structures/textChannel.ts' import type { TextChannel } from '../../structures/textChannel.ts'
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
import { Guild } from '../../structures/guild.ts' import type { Guild } from '../../structures/guild.ts'
import { User } from '../../structures/user.ts' import type { User } from '../../structures/user.ts'
import { Emoji } from '../../structures/emoji.ts' import type { Emoji } from '../../structures/emoji.ts'
import { Member } from '../../structures/member.ts' import type { Member } from '../../structures/member.ts'
import { Role } from '../../structures/role.ts' import type { Role } from '../../structures/role.ts'
import { Message } from '../../structures/message.ts' import type { Message } from '../../structures/message.ts'
import { Collection } from '../../utils/collection.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'
@ -51,23 +51,24 @@ 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 { CommandContext } from '../../models/command.ts' import type { SlashCommandInteraction } from '../../structures/slash.ts'
import { RequestMethods } from '../../models/rest.ts' import type { CommandContext } from '../../commands/command.ts'
import { PartialInvitePayload } from '../../types/invite.ts' import type { RequestMethods } from '../../rest/types.ts'
import { GuildChannels } from '../../types/guild.ts' import type { PartialInvitePayload } from '../../types/invite.ts'
import type { GuildChannels } from '../../types/guild.ts'
import { applicationCommandCreate } from './applicationCommandCreate.ts' import { applicationCommandCreate } from './applicationCommandCreate.ts'
import { applicationCommandDelete } from './applicationCommandDelete.ts' import { applicationCommandDelete } from './applicationCommandDelete.ts'
import { applicationCommandUpdate } from './applicationCommandUpdate.ts' import { applicationCommandUpdate } from './applicationCommandUpdate.ts'
import { SlashCommand } from '../../models/slashClient.ts' import type { SlashCommand } from '../../interactions/slashCommand.ts'
export const gatewayHandlers: { export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined [eventCode in GatewayEvents]: GatewayEventHandler | undefined
@ -358,11 +359,12 @@ export type ClientEvents = {
* @param channel Channel of which Webhooks were updated * @param channel Channel of which Webhooks were updated
*/ */
webhooksUpdate: [guild: Guild, channel: GuildTextBasedChannel] 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
@ -393,7 +395,15 @@ export type ClientEvents = {
} }
] ]
guildMembersChunked: [guild: Guild, chunks: number] guildMembersChunked: [guild: Guild, chunks: number]
rateLimit: [data: { method: RequestMethods; url: string; body: any }] rateLimit: [
data: {
method: RequestMethods
path: string
global: boolean
timeout: number
limit: number
}
]
inviteDeleteUncached: [invite: PartialInvitePayload] inviteDeleteUncached: [invite: PartialInvitePayload]
voiceStateRemoveUncached: [data: { guild: Guild; member: Member }] voiceStateRemoveUncached: [data: { guild: Guild; member: Member }]
userUpdateUncached: [user: User] userUpdateUncached: [user: User]
@ -414,4 +424,5 @@ export type ClientEvents = {
commandMissingArgs: [ctx: CommandContext] commandMissingArgs: [ctx: CommandContext]
commandUsed: [ctx: CommandContext] commandUsed: [ctx: CommandContext]
commandError: [ctx: CommandContext, err: Error] commandError: [ctx: CommandContext, err: Error]
gatewayError: [err: ErrorEvent, shards: [number, number]]
} }

View File

@ -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,

View File

@ -1,7 +1,7 @@
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,

View File

@ -1,4 +1,4 @@
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,

View File

@ -1,7 +1,7 @@
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,

View File

@ -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 (

View File

@ -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,

View File

@ -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,

View File

@ -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,
@ -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
)
} }
} }

View File

@ -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 { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
export const webhooksUpdate: GatewayEventHandler = async ( export const webhooksUpdate: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,

View File

@ -1,24 +1,21 @@
import { unzlib } 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 GatewayEvents
} from '../types/gateway.ts' } from '../types/gateway.ts'
import { gatewayHandlers } from './handlers/index.ts' import { gatewayHandlers } from './handlers/mod.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 { 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
@ -57,8 +54,6 @@ export type GatewayTypedEvents = {
*/ */
export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { 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
@ -67,14 +62,15 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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[] shards?: number[]
ping: number = 0
constructor(client: Client, shards?: number[]) { constructor(client: Client, shards?: number[]) {
super() super()
this.client = client Object.defineProperty(this, 'client', { value: client, enumerable: false })
this.cache = new GatewayCache(client) this.cache = new GatewayCache(client)
this.shards = shards this.shards = shards
} }
@ -92,7 +88,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
} }
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)
@ -120,11 +116,9 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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:
@ -157,7 +151,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
const handler = gatewayHandlers[t] const handler = gatewayHandlers[t]
if (handler !== undefined) { if (handler !== undefined && d !== null) {
handler(this, d) handler(this, d)
} }
} }
@ -177,8 +171,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
} }
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:
@ -194,8 +188,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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( throw new Error(
@ -209,20 +202,17 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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!")
@ -260,6 +250,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
error.name = 'ErrorEvent' error.name = 'ErrorEvent'
console.log(error) console.log(error)
this.emit('error', error, event) this.emit('error', error, event)
this.client.emit('gatewayError', event, this.shards)
} }
private enqueueIdentify(forceNew?: boolean): void { private enqueueIdentify(forceNew?: boolean): void {
@ -269,25 +260,11 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
} }
private async sendIdentify(forceNewSession?: boolean): Promise<void> { private async sendIdentify(forceNewSession?: boolean): Promise<void> {
if (typeof this.token !== 'string') throw new Error('Token not specified') if (typeof this.client.token !== 'string')
if (typeof this.intents !== 'object') throw new Error('Token not specified')
if (typeof this.client.intents !== 'object')
throw new Error('Intents not specified') throw new Error('Intents not specified')
if (this.client.fetchGatewayInfo === true) {
this.debug('Fetching /gateway/bot...')
const info = await this.client.rest.api.gateway.bot.get()
if (info.session_start_limit.remaining === 0)
throw new Error(
`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( const sessionIDCached = await this.cache.get(
`session_id_${this.shards?.join('-') ?? '0'}` `session_id_${this.shards?.join('-') ?? '0'}`
@ -300,7 +277,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
} }
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',
@ -311,7 +288,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
this.shards === undefined this.shards === undefined
? [0, 1] ? [0, 1]
: [this.shards[0] ?? 0, this.shards[1] ?? 1], : [this.shards[0] ?? 0, this.shards[1] ?? 1],
intents: this.intents.reduce( intents: this.client.intents.reduce(
(previous, current) => previous | current, (previous, current) => previous | current,
0 0
), ),
@ -327,9 +304,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
} }
private async sendResume(): Promise<void> { private async sendResume(): Promise<void> {
if (typeof this.token !== 'string') throw new Error('Token not specified') if (typeof this.client.token !== 'string')
if (typeof this.intents !== 'object') throw new Error('Token not specified')
throw new Error('Intents not specified')
if (this.sessionID === undefined) { if (this.sessionID === undefined) {
this.sessionID = await this.cache.get( this.sessionID = await this.cache.get(
@ -348,7 +324,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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
} }
@ -393,8 +369,18 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
: 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
} }
}) })
} }
@ -405,6 +391,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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) {
@ -412,7 +399,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`) await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`)
} }
this.close(1000, RECONNECT_REASON) this.closeGateway(1000, RECONNECT_REASON)
this.initWebsocket() this.initWebsocket()
} }
@ -421,7 +408,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
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'
@ -431,7 +418,12 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
this.websocket.onerror = this.onerror.bind(this) as any this.websocket.onerror = this.onerror.bind(this) as any
} }
close(code: number = 1000, reason?: string): void { closeGateway(code: number = 1000, reason?: string): void {
this.debug(
`Closing with code ${code}${
reason !== undefined && reason !== '' ? ` and reason ${reason}` : ''
}`
)
return this.websocket?.close(code, reason) return this.websocket?.close(code, reason)
} }

3
src/interactions/mod.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './slashClient.ts'
export * from './slashModule.ts'
export * from './slashCommand.ts'

View 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
})
}
}

View File

@ -1,19 +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'
export class SlashCommand { export class SlashCommand {
slash: SlashCommandsManager slash: SlashCommandsManager
@ -155,6 +150,7 @@ function buildOptionsArray(
) )
} }
/** Slash Command Builder */
export class SlashBuilder { export class SlashBuilder {
data: SlashCommandPartial data: SlashCommandPartial
@ -200,13 +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
rest: RESTManager readonly rest!: RESTManager
constructor(client: SlashClient) { constructor(client: SlashClient) {
this.slash = client Object.defineProperty(this, 'slash', { value: client, enumerable: false })
this.rest = client.rest Object.defineProperty(this, 'rest', {
enumerable: false,
value: client.rest
})
} }
/** Get all Global Slash Commands */ /** Get all Global Slash Commands */
@ -350,229 +350,3 @@ export class SlashCommandsManager {
return this return this
} }
} }
export type SlashCommandHandlerCallback = (interaction: Interaction) => any
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
}>
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.publicKey = options.publicKey
this.enabled = options.enabled ?? true
if (this.client?._decoratedSlash !== undefined) {
this.client._decoratedSlash.forEach((e) => {
e.handler = e.handler.bind(this.client)
this.handlers.push(e)
})
}
if (this._decoratedSlash !== undefined) {
this._decoratedSlash.forEach((e) => {
e.handler = e.handler.bind(this.client)
this.handlers.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)
)
this.commands = new SlashCommandsManager(this)
}
getID(): string {
return typeof this.id === 'string' ? this.id : this.id()
}
/** Adds a new Slash Command Handler */
handle(handler: SlashCommandHandler): SlashClient {
this.handlers.push(handler)
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: 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?.group !== undefined)
interaction.data.options = interaction.data.options[0].options ?? []
if (cmd?.parent !== undefined)
interaction.data.options = interaction.data.options[0].options ?? []
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: 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
}
}

View File

@ -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
View File

View 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
} }
@ -63,7 +63,9 @@ export class BaseManager<T, T2> {
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
const arr = (await this.array()) ?? [] const arr = (await this.array()) ?? []
const { readable, writable } = new TransformStream() const { readable, writable } = new TransformStream()
arr.forEach((el) => writable.getWriter().write(el)) const writer = writable.getWriter()
arr.forEach((el: unknown) => writer.write(el))
writer.close()
yield* readable yield* readable
} }
@ -85,4 +87,8 @@ export class BaseManager<T, T2> {
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})`
}
} }

View File

@ -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
} }
@ -43,7 +43,9 @@ export class BaseChildManager<T, T2> {
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
const arr = (await this.array()) ?? [] const arr = (await this.array()) ?? []
const { readable, writable } = new TransformStream() const { readable, writable } = new TransformStream()
arr.forEach((el: unknown) => writable.getWriter().write(el)) const writer = writable.getWriter()
arr.forEach((el: unknown) => writer.write(el))
writer.close()
yield* readable yield* readable
} }
@ -60,4 +62,8 @@ export class BaseChildManager<T, T2> {
if (fetchValue !== undefined) return fetchValue if (fetchValue !== undefined) return fetchValue
} }
} }
[Deno.customInspect](): string {
return `ChildManager(${this.parent.cacheName})`
}
} }

View File

@ -1,15 +1,16 @@
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 { Embed } from '../structures/embed.ts' import { Embed } from '../structures/embed.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 { import type { User } from '../structures/user.ts'
import type {
ChannelPayload, ChannelPayload,
GuildChannelPayload, GuildChannelPayload,
MessageOptions MessageOptions
} from '../types/channel.ts' } 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 type AllMessageOptions = MessageOptions | Embed
@ -19,6 +20,21 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
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)
@ -97,7 +113,7 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
} }
const payload: any = { const payload: any = {
content: content, content: content ?? option?.content,
embed: option?.embed, embed: option?.embed,
file: option?.file, file: option?.file,
files: option?.files, files: option?.files,
@ -121,6 +137,10 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
: undefined : undefined
} }
if (payload.content === undefined && payload.embed === undefined) {
payload.content = ''
}
const resp = await this.client.rest.api.channels[channelID].messages.post( const resp = await this.client.rest.api.channels[channelID].messages.post(
payload payload
) )
@ -159,7 +179,7 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
const newMsg = await this.client.rest.api.channels[channelID].messages[ const newMsg = await this.client.rest.api.channels[channelID].messages[
typeof message === 'string' ? message : message.id typeof message === 'string' ? message : message.id
].patch({ ].patch({
content: text, content: text ?? option?.content,
embed: option?.embed !== undefined ? option.embed.toJSON() : undefined, embed: option?.embed !== undefined ? option.embed.toJSON() : undefined,
// Cannot upload new files with Message // Cannot upload new files with Message
// file: option?.file, // file: option?.file,

View File

@ -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'

View File

@ -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

View 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)
}
}
}

View File

@ -1,16 +1,16 @@
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 { import {
ChannelTypes, ChannelTypes,
GuildChannelPayload, GuildChannelPayload,
OverwritePayload OverwritePayload
} from '../types/channel.ts' } from '../types/channel.ts'
import { GuildChannels, GuildChannelPayloads } from '../types/guild.ts' import type { GuildChannels, GuildChannelPayloads } from '../types/guild.ts'
import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.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 interface CreateChannelOptions { export interface CreateChannelOptions {
name: string name: string

View File

@ -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> {

View File

@ -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<
@ -30,7 +30,7 @@ export class GuildVoiceStatesManager extends BaseManager<
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, {
@ -57,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, {

View File

@ -1,10 +1,10 @@
import { fetchAuto } from '../../deps.ts' import { fetchAuto } from '../../deps.ts'
import { Client } from '../models/client.ts' import type { Client } from '../client/mod.ts'
import { Guild } from '../structures/guild.ts' import { Guild } from '../structures/guild.ts'
import { Template } from '../structures/template.ts' import type { Template } from '../structures/template.ts'
import { Role } from '../structures/role.ts' import { Role } from '../structures/role.ts'
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts' import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
import { import type {
GuildPayload, GuildPayload,
MemberPayload, MemberPayload,
GuildCreateRolePayload, GuildCreateRolePayload,

View File

@ -1,9 +1,9 @@
import { GuildTextChannel, User } from '../../mod.ts' import type { GuildTextChannel, User } 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 { Invite } from '../structures/invite.ts' import { Invite } from '../structures/invite.ts'
import { CHANNEL_INVITES, GUILD_INVITES, 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 { export enum InviteTargetUserType {

View File

@ -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> {

View File

@ -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'

View File

@ -1,10 +1,9 @@
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 { User } from '../structures/user.ts' import type { User } from '../structures/user.ts'
import { Reaction } from '../types/channel.ts' import type { Reaction } from '../types/channel.ts'
import { import {
MESSAGE_REACTION, MESSAGE_REACTION,
MESSAGE_REACTIONS, MESSAGE_REACTIONS,
@ -19,7 +18,7 @@ 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
} }

View File

@ -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'

View File

@ -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<

View File

@ -1,6 +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 { User } from '../structures/user.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 {

View File

@ -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 { RoleModifyPayload, 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,14 +22,17 @@ 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))
}) })

View File

@ -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> {

View File

@ -1,676 +0,0 @@
import * as baseEndpoints from '../consts/urlsAndVersions.ts'
import { Embed } from '../structures/embed.ts'
import { MessageAttachment } from '../structures/message.ts'
import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts'
import { simplifyAPIError } from '../utils/err_fmt.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 interface DiscordAPIErrorPayload {
url: string
status: number
method: string
code?: number
message?: string
errors: object
requestData: { [key: string]: any }
}
export class DiscordAPIError extends Error {
name = 'DiscordAPIError'
error?: DiscordAPIErrorPayload
constructor(error: string | DiscordAPIErrorPayload) {
super()
const fmt = Object.entries(
typeof error === 'object' ? simplifyAPIError(error.errors) : {}
)
this.message =
typeof error === 'string'
? `${error} `
: `\n${error.method} ${error.url.slice(7)} returned ${error.status}\n(${
error.code ?? 'unknown'
}) ${error.message}${
fmt.length === 0
? ''
: `\n${fmt
.map(
(e) =>
` at ${e[0]}:\n${e[1]
.map((e) => ` - ${e}`)
.join('\n')}`
)
.join('\n')}\n`
}`
if (typeof error === 'object') this.error = error
}
}
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 {
/** Make a GET request to current route */
get: APIMap
/** Make a POST request to current route */
post: APIMap
/** Make a PATCH request to current route */
patch: APIMap
/** Make a PUT request to current route */
put: APIMap
/** Make a DELETE request to current route */
delete: APIMap
/** Make a HEAD request to current route */
head: APIMap
/** Continue building API Route */
[name: string]: APIMap
}
/** API Route builder function */
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 to use for authorization */
token?: string | (() => string | undefined)
/** Headers to patch with if any */
headers?: { [name: string]: string | undefined }
/** Whether to use Canary instance of Discord API or not */
canary?: boolean
/** Discord REST API version to use */
version?: 6 | 7 | 8
/** Token Type to use for Authorization */
tokenType?: TokenType
/** User Agent to use (Header) */
userAgent?: string
/** Optional Harmony client */
client?: Client
}
/** Token Type for REST API. */
export enum TokenType {
/** Token type for Bot User */
Bot = 'Bot',
/** Token Type for OAuth2 */
Bearer = 'Bearer',
/** No Token Type. Can be used for User accounts. */
None = ''
}
/** An easier to use interface for interacting with Discord REST API. */
export class RESTManager {
queues: { [key: string]: QueuedItem[] } = {}
rateLimits = new Collection<string, RateLimit>()
/** Whether we are globally ratelimited or not */
globalRateLimit: boolean = false
/** Whether requests are being processed or not */
processing: boolean = false
/** API Version being used by REST Manager */
version: number = 8
/**
* API Map - easy to use way for interacting with Discord API.
*
* Examples:
* * ```ts
* rest.api.users['123'].get().then(userPayload => doSomething)
* ```
* * ```ts
* rest.api.guilds['123'].channels.post({ name: 'my-channel', type: 0 }).then(channelPayload => {})
* ```
*/
api: APIMap
/** Token being used for Authorization */
token?: string | (() => string | undefined)
/** Token Type of the Token if any */
tokenType: TokenType = TokenType.Bot
/** Headers object which patch the current ones */
headers: any = {}
/** Optional custom User Agent (header) */
userAgent?: string
/** Whether REST Manager is using Canary API */
canary?: boolean
/** Optional Harmony Client object */
client?: Client
constructor(options?: RESTOptions) {
this.api = builder(this)
if (options?.token !== undefined) this.token = options.token
if (options?.version !== undefined) this.version = options.version
if (options?.headers !== undefined) this.headers = options.headers
if (options?.tokenType !== undefined) this.tokenType = options.tokenType
if (options?.userAgent !== undefined) this.userAgent = options.userAgent
if (options?.canary !== undefined) this.canary = options.canary
if (options?.client !== undefined) this.client = options.client
this.handleRateLimits()
}
/** Checks the queues of buckets, if empty, delete entry */
private checkQueues(): 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]
}
})
}
/** Adds a Request to Queue */
private queue(request: QueuedItem): void {
const route = request.url.substring(
Number(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) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.processQueue()
this.checkQueues()
} else this.processing = false
}
private prepare(body: any, method: RequestMethods): { [key: string]: any } {
const headers: RequestHeaders = {
'User-Agent':
this.userAgent ??
`DiscordBot (harmony, https://github.com/harmonyland/harmony)`
}
if (this.token !== undefined) {
const token = typeof this.token === 'string' ? this.token : this.token()
if (token !== undefined)
headers.Authorization = `${this.tokenType} ${token}`.trim()
}
if (method === 'get' || method === 'head' || method === 'delete')
body = undefined
if (body?.reason !== undefined) {
headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason)
}
let _files: undefined | MessageAttachment[]
if (body?.embed?.files !== undefined && Array.isArray(body?.embed?.files)) {
_files = body?.embed?.files
}
if (body?.embeds !== undefined && Array.isArray(body?.embeds)) {
const files1 = body?.embeds
.map((e: Embed) => e.files)
.filter((e: MessageAttachment[]) => e !== undefined)
for (const files of files1) {
for (const file of files) {
if (_files === undefined) _files = []
_files?.push(file)
}
}
}
if (
body?.file !== undefined ||
body?.files !== undefined ||
_files !== undefined
) {
const files: Array<{ blob: Blob; name: string }> = []
if (body?.file !== undefined) files.push(body.file)
if (body?.files !== undefined && Array.isArray(body.files)) {
for (const file of body.files) {
files.push(file)
}
}
if (_files !== undefined) {
for (const file of _files) {
files.push(file)
}
}
const form = new FormData()
files.forEach((file, index) =>
form.append(`file${index + 1}`, file.blob, file.name)
)
const json = JSON.stringify(body)
form.append('payload_json', json)
if (body === undefined) body = {}
body.file = form
} else if (
body !== undefined &&
!['get', 'delete'].includes(method.toLowerCase())
) {
headers['Content-Type'] = 'application/json'
}
if (this.headers !== undefined) Object.assign(headers, this.headers)
const data: { [name: string]: any } = {
headers,
body: body?.file ?? JSON.stringify(body),
method: method.toUpperCase()
}
return data
}
private isRateLimited(url: string): 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
}
/** Processes headers of the Response */
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
}
/** Handles status code of response and acts as required */
private handleStatusCode(
response: Response,
body: any,
data: { [key: string]: any },
reject: CallableFunction
): void {
const status = response.status
// We have hit ratelimit - this should not happen
if (status === HttpResponseCode.TooManyRequests) {
if (this.client !== undefined)
this.client.emit('rateLimit', {
method: data.method,
url: response.url,
body
})
reject(new Error('RateLimited'))
return
}
// It's a normal status code... just continue
if (
(status >= 200 && status < 400) ||
status === HttpResponseCode.NoContent
)
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 Unauthorized. Invalid Token.\n${text}`)
)
const _data = { ...data }
if (_data?.headers !== undefined) delete _data.headers
if (_data?.method !== undefined) delete _data.method
// At this point we know it is error
const error: DiscordAPIErrorPayload = {
url: new URL(response.url).pathname,
status,
method: data.method,
code: body?.code,
message: body?.message,
errors: body?.errors ?? {},
requestData: _data
}
if (
[
HttpResponseCode.BadRequest,
HttpResponseCode.NotFound,
HttpResponseCode.Forbidden,
HttpResponseCode.MethodNotAllowed
].includes(status)
) {
reject(new DiscordAPIError(error))
} else if (status === HttpResponseCode.GatewayUnavailable) {
reject(new DiscordAPIError(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
// It doesn't start with HTTP, that means it's an incomplete URL
if (!urlToUse.startsWith('http')) {
if (!urlToUse.startsWith('/')) urlToUse = `/${urlToUse}`
urlToUse =
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
baseEndpoints.DISCORD_API_URL +
'/v' +
baseEndpoints.DISCORD_API_VERSION +
urlToUse
}
if (this.canary === true && urlToUse.startsWith('http')) {
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()
}
})
}
/** Checks for RateLimits times and deletes if already over */
private handleRateLimits(): void {
const now = Date.now()
this.rateLimits.forEach((value, key) => {
// Ratelimit has not ended
if (value.resetAt > now) return
// It ended, so delete
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)
}
}

245
src/rest/bucket.ts Normal file
View 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
}
}

1294
src/rest/endpoints.ts Normal file

File diff suppressed because it is too large Load Diff

44
src/rest/error.ts Normal file
View File

@ -0,0 +1,44 @@
import { simplifyAPIError } from '../utils/err_fmt.ts'
import { DiscordAPIErrorPayload } from './types.ts'
export class DiscordAPIError extends Error {
name = 'DiscordAPIError'
error?: DiscordAPIErrorPayload
constructor(error: string | DiscordAPIErrorPayload) {
super()
const fmt = Object.entries(
typeof error === 'object' ? simplifyAPIError(error.errors ?? {}) : {}
)
this.message =
typeof error === 'string'
? `${error} `
: `\n${error.method.toUpperCase()} ${error.url} returned ${
error.status
}\n(${error.code ?? 'unknown'}) ${error.message}${
fmt.length === 0
? ''
: `\n${fmt
.map(
(e) =>
` at ${e[0]}:\n${e[1]
.map((e) => ` - ${e}`)
.join('\n')}`
)
.join('\n')}\n`
}`
if (typeof error === 'object') this.error = error
}
}
export class HTTPError extends Error {
constructor(
public message: string,
public name: string,
public code: number,
public method: string,
public path: string
) {
super(message)
}
}

329
src/rest/manager.ts Normal file
View File

@ -0,0 +1,329 @@
import { Collection } from '../utils/collection.ts'
import type { Client } from '../client/mod.ts'
import { RequestMethods, METHODS } from './types.ts'
import { Constants } from '../types/constants.ts'
import { RESTEndpoints } from './endpoints.ts'
import { BucketHandler } from './bucket.ts'
import { APIRequest, RequestOptions } from './request.ts'
export type MethodFunction = (
body?: unknown,
maxRetries?: number,
bucket?: string | null,
rawResponse?: boolean,
options?: RequestOptions
) => Promise<any>
export interface APIMap extends MethodFunction {
/** Make a GET request to current route */
get: APIMap
/** Make a POST request to current route */
post: APIMap
/** Make a PATCH request to current route */
patch: APIMap
/** Make a PUT request to current route */
put: APIMap
/** Make a DELETE request to current route */
delete: APIMap
/** Make a HEAD request to current route */
head: APIMap
/** Continue building API Route */
[name: string]: APIMap
}
/** API Route builder function */
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)) === true) {
const method = ((rest as unknown) as {
[name: string]: MethodFunction
})[String(p)]
return async (...args: any[]) =>
await method.bind(rest)(
`${Constants.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 to use for authorization */
token?: string | (() => string | undefined)
/** Headers to patch with if any */
headers?: { [name: string]: string | undefined }
/** Whether to use Canary instance of Discord API or not */
canary?: boolean
/** Discord REST API version to use */
version?: 6 | 7 | 8
/** Token Type to use for Authorization */
tokenType?: TokenType
/** User Agent to use (Header) */
userAgent?: string
/** Optional Harmony client */
client?: Client
/** Requests Timeout (in MS, default 30s) */
requestTimeout?: number
/** Retry Limit (default 1) */
retryLimit?: number
}
/** Token Type for REST API. */
export enum TokenType {
/** Token type for Bot User */
Bot = 'Bot',
/** Token Type for OAuth2 */
Bearer = 'Bearer',
/** No Token Type. Can be used for User accounts. */
None = ''
}
/** An easier to use interface for interacting with Discord REST API. */
export class RESTManager {
/** API Version being used by REST Manager */
version: number = 8
/**
* API Map - easy to use way for interacting with Discord API.
*
* Examples:
* * ```ts
* rest.api.users['123'].get().then(userPayload => doSomething)
* ```
* * ```ts
* rest.api.guilds['123'].channels.post({ name: 'my-channel', type: 0 }).then(channelPayload => {})
* ```
*/
api: APIMap
#token?: string | (() => string | undefined)
/** Token being used for Authorization */
get token(): string | (() => string | undefined) | undefined {
return this.#token
}
set token(val: string | (() => string | undefined) | undefined) {
this.#token = val
}
/** Token Type of the Token if any */
tokenType: TokenType = TokenType.Bot
/** Headers object which patch the current ones */
headers: any = {}
/** Optional custom User Agent (header) */
userAgent?: string
/** Whether REST Manager is using Canary API */
canary?: boolean
/** Optional Harmony Client object */
readonly client?: Client
endpoints: RESTEndpoints
requestTimeout = 30000
readonly timers!: Set<number>
apiURL = Constants.DISCORD_API_URL
readonly handlers!: Collection<string, BucketHandler>
globalLimit = Infinity
globalRemaining = this.globalLimit
globalReset: number | null = null
globalDelay: number | null = null
retryLimit = 1
restTimeOffset = 0
constructor(options?: RESTOptions) {
this.api = builder(this)
if (options?.token !== undefined) this.token = options.token
if (options?.version !== undefined) this.version = options.version
if (options?.headers !== undefined) this.headers = options.headers
if (options?.tokenType !== undefined) this.tokenType = options.tokenType
if (options?.userAgent !== undefined) this.userAgent = options.userAgent
if (options?.canary !== undefined) this.canary = options.canary
if (options?.retryLimit !== undefined) this.retryLimit = options.retryLimit
if (options?.requestTimeout !== undefined)
this.requestTimeout = options.requestTimeout
if (options?.client !== undefined) {
Object.defineProperty(this, 'client', {
value: options.client,
enumerable: false
})
}
this.endpoints = new RESTEndpoints(this)
Object.defineProperty(this, 'timers', {
value: new Set(),
enumerable: false
})
Object.defineProperty(this, 'handlers', {
value: new Collection<string, BucketHandler>(),
enumerable: false
})
}
setTimeout(fn: (...args: any[]) => any, ms: number): number {
const timer = setTimeout(async () => {
this.timers.delete(timer)
await fn()
}, ms)
this.timers.add(timer)
return timer
}
async request<T = any>(
method: RequestMethods,
path: string,
options: RequestOptions = {}
): Promise<T> {
const req = new APIRequest(this, method, path, options)
let handler = this.handlers.get(req.path)
if (handler === undefined) {
handler = new BucketHandler(this)
this.handlers.set(req.route, handler)
}
return handler.push(req)
}
/**
* 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,
options: RequestOptions = {}
): Promise<any> {
return await this.request(
method,
url,
Object.assign(
{
data: body,
rawResponse,
route: bucket ?? undefined
},
options
)
)
}
/** Makes a GET Request to API */
async get(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean,
options?: RequestOptions
): Promise<any> {
return await this.make(
'get',
url,
body,
maxRetries,
bucket,
rawResponse,
options
)
}
/** Makes a POST Request to API */
async post(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean,
options?: RequestOptions
): Promise<any> {
return await this.make(
'post',
url,
body,
maxRetries,
bucket,
rawResponse,
options
)
}
/** Makes a DELETE Request to API */
async delete(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean,
options?: RequestOptions
): Promise<any> {
return await this.make(
'delete',
url,
body,
maxRetries,
bucket,
rawResponse,
options
)
}
/** Makes a PATCH Request to API */
async patch(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean,
options?: RequestOptions
): Promise<any> {
return await this.make(
'patch',
url,
body,
maxRetries,
bucket,
rawResponse,
options
)
}
/** Makes a PUT Request to API */
async put(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean,
options?: RequestOptions
): Promise<any> {
return await this.make(
'put',
url,
body,
maxRetries,
bucket,
rawResponse,
options
)
}
}

7
src/rest/mod.ts Normal file
View File

@ -0,0 +1,7 @@
export * from './manager.ts'
export * from './types.ts'
export * from './endpoints.ts'
export * from './error.ts'
export * from './bucket.ts'
export * from './queue.ts'
export * from './request.ts'

37
src/rest/queue.ts Normal file
View File

@ -0,0 +1,37 @@
// based on https://github.com/discordjs/discord.js/blob/master/src/rest/AsyncQueue.js
export interface RequestPromise {
resolve: CallableFunction
promise: Promise<any>
}
export class RequestQueue {
promises: RequestPromise[] = []
get remaining(): number {
return this.promises.length
}
async wait(): Promise<any> {
const next =
this.promises.length !== 0
? this.promises[this.promises.length - 1].promise
: Promise.resolve()
let resolveFn: CallableFunction | undefined
const promise = new Promise((resolve) => {
resolveFn = resolve
})
this.promises.push({
resolve: resolveFn!,
promise
})
return next
}
shift(): void {
const deferred = this.promises.shift()
if (typeof deferred !== 'undefined') deferred.resolve()
}
}

132
src/rest/request.ts Normal file
View File

@ -0,0 +1,132 @@
import type { Embed } from '../structures/embed.ts'
import type { MessageAttachment } from '../structures/message.ts'
import type { RESTManager } from './manager.ts'
import type { RequestMethods } from './types.ts'
export interface RequestOptions {
headers?: { [name: string]: string }
query?: { [name: string]: string }
files?: MessageAttachment[]
data?: any
reason?: string
rawResponse?: boolean
route?: string
}
export class APIRequest {
retries = 0
route: string
constructor(
public rest: RESTManager,
public method: RequestMethods,
public path: string,
public options: RequestOptions
) {
this.route = options.route ?? path
if (typeof options.query === 'object') {
const entries = Object.entries(options.query)
if (entries.length > 0) {
this.path += '?'
entries.forEach((entry, i) => {
this.path += `${i === 0 ? '' : '&'}${encodeURIComponent(
entry[0]
)}=${encodeURIComponent(entry[1])}`
})
}
}
let _files: undefined | MessageAttachment[]
if (
options.data?.embed?.files !== undefined &&
Array.isArray(options.data?.embed?.files)
) {
_files = [...options.data?.embed?.files]
}
if (
options.data?.embeds !== undefined &&
Array.isArray(options.data?.embeds)
) {
const files1 = options.data?.embeds
.map((e: Embed) => e.files)
.filter((e: MessageAttachment[]) => e !== undefined)
for (const files of files1) {
for (const file of files) {
if (_files === undefined) _files = []
_files?.push(file)
}
}
}
if (options.data?.file !== undefined) {
if (_files === undefined) _files = []
_files.push(options.data?.file)
}
if (
options.data?.files !== undefined &&
Array.isArray(options.data?.files)
) {
if (_files === undefined) _files = []
options.data?.files.forEach((file: any) => {
_files!.push(file)
})
}
if (_files !== undefined && _files.length > 0) {
if (options.files === undefined) options.files = _files
else options.files = [...options.files, ..._files]
}
}
async execute(): Promise<Response> {
let contentType: string | undefined
let body: any = this.options.data
if (this.options.files !== undefined && this.options.files.length > 0) {
contentType = undefined
const form = new FormData()
this.options.files.forEach((file, i) =>
form.append(`file${i === 0 ? '' : i}`, file.blob, file.name)
)
form.append('payload_json', JSON.stringify(body))
body = form
} else if (body !== undefined) {
contentType = 'application/json'
body = JSON.stringify(body)
}
const controller = new AbortController()
const timer = setTimeout(() => {
controller.abort()
}, this.rest.requestTimeout)
this.rest.timers.add(timer)
const url = this.path.startsWith('http')
? this.path
: `${this.rest.apiURL}/v${this.rest.version}${this.path}`
const headers: any = {
'User-Agent':
this.rest.userAgent ??
`DiscordBot (harmony, https://github.com/harmonyland/harmony)`,
Authorization:
this.rest.token === undefined
? undefined
: `${this.rest.tokenType} ${this.rest.token}`.trim()
}
if (contentType !== undefined) headers['Content-Type'] = contentType
const init: RequestInit = {
method: this.method.toUpperCase(),
signal: controller.signal,
headers: Object.assign(headers, this.rest.headers, this.options.headers),
body
}
return fetch(url, init).finally(() => {
clearTimeout(timer)
this.rest.timers.delete(timer)
})
}
}

37
src/rest/types.ts Normal file
View File

@ -0,0 +1,37 @@
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 interface DiscordAPIErrorPayload {
url: string
status: number
method: string
code?: number
message?: string
errors: object
requestData: { [key: string]: any }
}
export const METHODS = ['get', 'post', 'patch', 'put', 'delete', 'head']

View File

@ -1,5 +1,5 @@
import { Client } from '../models/client.ts' import type { Client } from '../client/mod.ts'
import { ApplicationPayload } from '../types/application.ts' import type { ApplicationPayload } from '../types/application.ts'
import { SnowflakeBase } from './base.ts' import { SnowflakeBase } from './base.ts'
import { User } from './user.ts' import { User } from './user.ts'

View File

@ -1,11 +1,11 @@
import { Client } from '../models/client.ts' import type { Client } from '../client/mod.ts'
import { Snowflake } from '../utils/snowflake.ts' import { Snowflake } from '../utils/snowflake.ts'
export class Base { export class Base {
client: Client client!: Client
constructor(client: Client, _data?: any) { constructor(client: Client, _data?: any) {
this.client = client Object.defineProperty(this, 'client', { value: client, enumerable: false })
} }
} }

View File

@ -1,4 +1,4 @@
import { ImageFormats, ImageSize } from '../types/cdn.ts' import type { ImageFormats, ImageSize } from '../types/cdn.ts'
/** Function to get Image URL from a resource on Discord CDN */ /** Function to get Image URL from a resource on Discord CDN */
export const ImageURL = ( export const ImageURL = (

View File

@ -1,20 +1,20 @@
import { Client } from '../models/client.ts' import type { Client } from '../client/mod.ts'
import { import type {
ChannelPayload, ChannelPayload,
ChannelTypes, ChannelTypes,
ModifyChannelOption, ModifyChannelOption,
ModifyChannelPayload, ModifyChannelPayload,
Overwrite, Overwrite,
OverwritePayload, OverwritePayload,
OverwriteAsArg, OverwriteAsArg
OverrideType
} from '../types/channel.ts' } from '../types/channel.ts'
import { OverrideType } from '../types/channel.ts'
import { CHANNEL } from '../types/endpoint.ts' import { CHANNEL } from '../types/endpoint.ts'
import { GuildChannelPayloads, GuildChannels } from '../types/guild.ts' import type { GuildChannelPayloads, GuildChannels } from '../types/guild.ts'
import getChannelByType from '../utils/getChannelByType.ts' import getChannelByType from '../utils/channel.ts'
import { Permissions } from '../utils/permissions.ts' import { Permissions } from '../utils/permissions.ts'
import { SnowflakeBase } from './base.ts' import { SnowflakeBase } from './base.ts'
import { Guild } from './guild.ts' import type { Guild } from './guild.ts'
import { Member } from './member.ts' import { Member } from './member.ts'
import { Role } from './role.ts' import { Role } from './role.ts'
@ -38,6 +38,13 @@ export class Channel extends SnowflakeBase {
} }
} }
export interface EditOverwriteOptions {
/** Allow Override Type */
allow?: OverrideType
/** Deny Override Type */
deny?: OverrideType
}
export class GuildChannel extends Channel { export class GuildChannel extends Channel {
guildID: string guildID: string
name: string name: string
@ -257,12 +264,9 @@ export class GuildChannel extends Channel {
async editOverwrite( async editOverwrite(
overwrite: OverwriteAsArg, overwrite: OverwriteAsArg,
{ {
overriteAllow = OverrideType.ADD, allow: overwriteAllow = OverrideType.ADD,
overriteDeny = OverrideType.ADD deny: overwriteDeny = OverrideType.ADD
}: { }: EditOverwriteOptions
overriteAllow?: OverrideType
overriteDeny?: OverrideType
}
): Promise<GuildChannels> { ): Promise<GuildChannels> {
const id = typeof overwrite.id === 'string' ? overwrite.id : overwrite.id.id const id = typeof overwrite.id === 'string' ? overwrite.id : overwrite.id.id
const index = this.permissionOverwrites.findIndex((e) => e.id === id) const index = this.permissionOverwrites.findIndex((e) => e.id === id)
@ -274,9 +278,9 @@ export class GuildChannel extends Channel {
if ( if (
overwrite.allow !== undefined && overwrite.allow !== undefined &&
overriteAllow !== OverrideType.REPLACE overwriteAllow !== OverrideType.REPLACE
) { ) {
switch (overriteAllow) { switch (overwriteAllow) {
case OverrideType.ADD: { case OverrideType.ADD: {
const originalAllow = new Permissions(overwrites[index].allow) const originalAllow = new Permissions(overwrites[index].allow)
const newAllow = new Permissions(overwrite.allow) const newAllow = new Permissions(overwrite.allow)
@ -299,8 +303,11 @@ export class GuildChannel extends Channel {
: overwrite.allow?.toJSON() ?? overwrites[index].allow : overwrite.allow?.toJSON() ?? overwrites[index].allow
} }
if (overwrite.deny !== undefined && overriteDeny !== OverrideType.REPLACE) { if (
switch (overriteDeny) { overwrite.deny !== undefined &&
overwriteDeny !== OverrideType.REPLACE
) {
switch (overwriteDeny) {
case OverrideType.ADD: { case OverrideType.ADD: {
const originalDeny = new Permissions(overwrites[index].deny) const originalDeny = new Permissions(overwrites[index].deny)
const newDeny = new Permissions(overwrite.deny) const newDeny = new Permissions(overwrite.deny)

Some files were not shown because too many files have changed in this diff Show More