Compare commits

...

338 Commits
v0.9.3 ... 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
Helloyunho 37de39daa7 🔧 Edit linter config (disable no-non-null-assertion) 2021-03-26 21:54:15 +09:00
Helloyunho 1cdcf045ad 🐛 Temporary remove quote grouping 2021-03-26 20:33:21 +09:00
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
DjDeveloper 7669ded67c
Merge pull request #117 from waterflamev8/feat/embed-validation
feat: add embed validation
2021-03-20 11:46:40 +05:30
waterflamev8 7e9c86cc2b
Make eslint happy 2021-03-20 13:29:53 +08:00
waterflamev8 aac841101a
Add embed validation 2021-03-20 13:21:35 +08:00
DjDeveloper 2472f3c348
Merge pull request #116 from Helloyunho/guild-channels-changes
Add 'Overwrite' type easy to use, Make GuildChannel for base, etc
2021-03-20 09:40:21 +05:30
Helloyunho f6545b0cbd 📌 pin ts-mixer version 2021-03-20 13:05:11 +09:00
Helloyunho f3115af2b9 Do some more tests 2021-03-20 00:48:24 +09:00
Helloyunho 07dae683dc ✏️ Fix question mark not included 2021-03-20 00:47:23 +09:00
Helloyunho 21ccf4c054 Add "easy to use" Overwrite type, Make GuildChannel(for base), Separate GuildTextChannel from textChannel.ts 2021-03-20 00:39:14 +09:00
DjDeveloper b532a99eb4
Merge pull request #115 from DjDeveloperr/catch
add ChannelsManager#{sendMessage, editMessage} and Command#onError
2021-03-19 17:33:39 +05:30
DjDeveloperr 11fa1281cf fix deprecated warn 2021-03-19 17:24:48 +05:30
DjDeveloperr 9023606faa fix 2021-03-19 17:20:46 +05:30
DjDeveloperr fccac82fdc x 2021-03-19 17:18:37 +05:30
DjDeveloperr 4f71717047 resolve 2021-03-19 17:07:57 +05:30
DjDeveloperr 6641778578 part 2 2021-03-19 16:51:25 +05:30
DjDeveloperr 2286c9af3f required changes 2021-03-19 16:48:11 +05:30
DjDeveloperr 711f78002e add channels.sendMessage 2021-03-19 16:20:16 +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
DjDeveloper b94253284e
Merge pull request #113 from DjDeveloperr/fixes
fix(rest): addReaction and removeReaction not working with custom emojis
2021-03-09 14:53:26 +05:30
DjDeveloperr 1c515dcb37 x 2021-03-08 16:26:59 +05:30
DjDeveloperr a47d5ae770 x 2021-03-08 16:06:30 +05:30
Aki 7624d2c3c6
Fix url 2021-03-08 11:26:32 +09:00
Helloyunho 3e81002c33
Merge branch 'main' into slash 2021-03-05 19:47:30 +09:00
Helloyunho 88519d9420 Add missing cdn functions, dynamic image format 2021-03-04 21:13:50 +09:00
Helloyunho d26059fdc8 Export invite manager 2021-03-04 10:50:55 +09:00
Helloyunho 9cd9eb9c6f
Merge pull request #111 from DjDeveloperr/main
add Async Interator support for Managers
2021-02-26 19:33:39 +09:00
DjDeveloperr 00dae42f7b add AsyncInterator support for Managers 2021-02-25 16:15:13 +05:30
Helloyunho c8d976762b
Merge pull request #110 from DjDeveloperr/main
fix(readme): intents no longer needed for ping example
2021-02-25 19:31:47 +09:00
DjDeveloperr 30fa9429c5 intents no longer needed for ping example 2021-02-25 15:59:42 +05:30
Helloyunho d5634f676f Ready to publish on nest.land 2021-02-25 13:23:01 +09:00
DjDeveloper 2b0db63f82
fix(cache): guild props being removed before saving to cache 2021-02-23 13:31:55 +05:30
Helloyunho 8e5a76dbe8 Fix the real problem part 2021-02-23 13:47:14 +09:00
Helloyunho ff80750ca4 Fix bitfield type error 2021-02-23 13:07:27 +09:00
Helloyunho 548b6bf2ad Fix guildChannelManager.create SMALL bracket mistake 2021-02-23 12:13:46 +09:00
Helloyunho de089ce610 Add request data to error 2021-02-23 12:09:38 +09:00
Helloyunho d5a8207690
Export CommandsLoader class too 2021-02-22 23:07:17 +09:00
DjDeveloperr f2f292b0ee Merge remote-tracking branch 'origin/main' into slash 2021-02-22 19:15:34 +05:30
DjDeveloper 4d93d66757
Merge pull request #107 from DjDeveloperr/permissions
fix: use bigint for bitfield (permissions)
2021-02-22 19:14:22 +05:30
DjDeveloper b222d91f7c
Merge pull request #108 from DjDeveloperr/cmd-loader
add commands loader
2021-02-22 19:06:34 +05:30
DjDeveloperr 2d6f3d46f3 add commands loader 2021-02-21 18:48:18 +05:30
DjDeveloperr 84baae279e use bigint for bitfield (perms) 2021-02-21 18:44:14 +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
DjDeveloper 5007dc6029
Merge pull request #105 from DjDeveloperr/slash
fix deno doc issues
2021-02-08 12:37:18 +05:30
DjDeveloperr e0edb1a088 fix deno doc issues 2021-02-08 12:35:54 +05:30
DjDeveloper cc7a587040
Merge pull request #104 from DjDeveloperr/slash
Add updateRefs to Message and MessageReactionsManager, don't double save Guild-related cache
2021-02-06 19:48:36 +05:30
DjDeveloperr 921d7323a3 rm voice 2021-02-06 19:45:31 +05:30
DjDeveloperr 361baa39eb dont double save guild related cache 2021-02-06 19:31:28 +05:30
DjDeveloperr 950243af5c add updateRefs 2021-02-06 19:25:45 +05:30
DjDeveloper 17a625032b
Merge pull request #103 from netux/token-priority
Fix Client#connect()'s logic to prioritize the token parameter over Client#token
2021-02-03 10:01:15 +05:30
Martín (Netux) Rodríguez 7af4553fbe Fix Client#connect()'s logic to prioritize the token parameter over Client#token
When Client#token was set and the token parameter is passed to Client#connect(), the module would throw an 'No Token Provided' error. This change makes it prioritize the parameter over Client#token.
2021-02-03 01:17:01 -03:00
Helloyunho 6a8e4ddc43
Merge pull request #102 from DjDeveloperr/slash
update readme examples
2021-02-02 14:23:42 +09:00
DjDeveloperr 57863a6ed5 update readme examples 2021-02-02 10:51:12 +05:30
DjDeveloper 24c3ea45ca
Merge pull request #101 from DjDeveloperr/slash
Remove Opine and Oak deps
2021-02-02 10:41:16 +05:30
DjDeveloper 3828a6b911
Merge pull request #100 from invalidCards/fix-bitfield-has
fix(BitField): resolve the bit before checking
2021-02-02 08:53:55 +05:30
invalidCards 38dea00369 Remove semicolon to conform to code style 2021-02-01 20:49:27 +01:00
invalidCards 96bba8d68e fix(BitField): resolve the bit before checking 2021-02-01 20:46:39 +01:00
DjDeveloperr ee51609c8c Merge remote-tracking branch 'origin/main' into slash 2021-02-01 18:31:20 +05:30
DjDeveloperr fb609c18bf dont use github imports 2021-02-01 14:25:03 +05:30
DjDeveloperr b844a053e5 remove opine and oak deps and start adding tests 2021-02-01 14:07:54 +05:30
DjDeveloper dbb80f30b4
Merge pull request #99 from DjDeveloperr/slash
Use a fork of Denoflate to have cache-able WASM
2021-01-30 18:33:32 +05:30
DjDeveloperr b7fee5a41f forgot to save file lol 2021-01-29 15:04:18 +05:30
DjDeveloperr aa5075451b Merge remote-tracking branch 'origin/main' into slash 2021-01-29 14:59:02 +05:30
DjDeveloperr a16d209a20 use fork of denoflate which is cacheable 2021-01-29 14:58:47 +05:30
DjDeveloper b4dd929a3e
Merge pull request #98 from ayntee/patch-1
fix(models/rest): unique key for form data item
2021-01-28 21:58:29 +05:30
ayntee d7647a88ee
Update src/models/rest.ts 2021-01-28 20:26:41 +04:00
ayntee 6a58a69031
fix(models/rest): unique key for form data item 2021-01-28 20:05:12 +04:00
DjDeveloper 6d21c762de
Merge pull request #97 from DjDeveloperr/slash
support multiple attachments
2021-01-28 20:52:38 +05:30
DjDeveloperr 60e2655085 support multiple attachments 2021-01-28 19:55:37 +05:30
Helloyunho bf0c82f273 fix unpatched part 2021-01-28 04:14:39 +09:00
Helloyunho 2a38fc1e00 Fix member nick type 2021-01-28 04:10:11 +09:00
DjDeveloper be3a793fa1
Merge pull request #96 from DjDeveloperr/slash
add APPLICATION_COMMAND_* events
2021-01-27 18:52:44 +05:30
DjDeveloperr 87c15a9283 add APPLICATION_COMMAND_* events 2021-01-27 01:45:47 +05:30
Helloyunho 54e0e41dc7 just test thing 2021-01-25 23:02:43 +09:00
Helloyunho 926d360d90
Merge pull request #95 from DjDeveloperr/slash
Add SnowflakeBase, useful methods for permissions to GuildTextChannel, and jsdoc in a lot of places
2021-01-25 22:39:23 +09:00
DjDeveloperr 11fc7e1e21 fix 2021-01-25 19:00:58 +05:30
DjDeveloperr 0edc74b1db fix 2021-01-25 18:56:32 +05:30
DjDeveloperr c2e7916aa3 some changes to Role 2021-01-25 17:08:56 +05:30
DjDeveloperr d36922c0b7 Merge remote-tracking branch 'origin/main' into slash 2021-01-25 17:04:44 +05:30
DjDeveloperr dce4c99cbd many things 2021-01-25 17:04:43 +05:30
DjDeveloperr c1cd6276ae feat: GuildTextChannel#{permissionsFor,overwritesFor}, TextChannel#triggerTyping, many other things 2021-01-25 16:58:46 +05:30
Helloyunho 30b5a17492 Change `getDMchannel` to `createDM` 2021-01-25 13:18:56 +09:00
DjDeveloper 108bd3ea62
Merge pull request #94 from Helloyunho/some-fixes
Fix Some Bugs and Add Some Useful Things
2021-01-25 00:30:07 +05:30
DjDeveloper c904d854d9
Merge pull request #93 from DjDeveloperr/slash
feat: sharding support
2021-01-25 00:25:00 +05:30
Helloyunho c880a73771 Do some tests and fix (or add) some 2021-01-25 03:49:08 +09:00
DjDeveloperr 04c4e7760b fix many things fr 2021-01-25 00:06:19 +05:30
DjDeveloperr 2dcce6e2bb feat: sharding (!) 2021-01-24 22:16:10 +05:30
Helloyunho dfeb784dfc Merge branch 'main' of https://github.com/harmonyland/harmony into some-fixes 2021-01-25 01:07:21 +09:00
DjDeveloper 71ded8ea2e
Merge pull request #92 from DjDeveloperr/slash
Add support for Message Attachment and add SlashCommandsManager#editBulk
2021-01-24 21:09:27 +05:30
DjDeveloperr 0b995d732d merge main 2021-01-24 19:52:04 +05:30
DjDeveloperr ebb4be897a add file support (really) & slash.commands.bulkEdit 2021-01-24 19:50:49 +05:30
Helloyunho ad3a0d4b79 dj says he was drunk so I made a fix 2021-01-24 22:20:11 +09:00
DjDeveloper 2402795006
Rename .github/PULL_REQUEST_TEMPLATE/pull_request_template.md to .github/PULL_REQUEST_TEMPLATE.md 2021-01-21 22:09:18 +05:30
DjDeveloper d4b557e95b
Add PR template 2021-01-21 22:06:28 +05:30
DjDeveloper 28268e779f
Merge pull request #91 from harmonyland/add-code-of-conduct-1
Add Code of Conduct
2021-01-21 21:59:04 +05:30
DjDeveloper 1827eb4a9b
Add Code of Conduct 2021-01-21 21:58:16 +05:30
DjDeveloper 32f55b875e
Merge pull request #90 from 6days9weeks/main
bonk the org, welcome harmonyland
2021-01-21 20:37:09 +05:30
DjDeveloperr 8734e9a7a6 update url 2021-01-21 20:27:06 +05:30
❥sora 928e18f586
bonk part 2 2021-01-21 23:55:05 +09:00
❥sora dcbbe15f38
bonk the org, welcome harmonyland 2021-01-21 23:54:43 +09:00
DjDeveloper 3dde9688ab
Merge pull request #89 from DjDeveloperr/slash
Remove unnecessary asyncs from RESTManager
2021-01-21 20:17:25 +05:30
DjDeveloperr 0cf60b7def remove unnecessary asyncs from rest 2021-01-21 20:13:07 +05:30
DjDeveloper b2a93769ff
Merge pull request #88 from DjDeveloperr/slash
BREAKING: Adds rest of the REST and migrate to Typed EventEmitter, better Gateway interfacing
2021-01-21 19:22:20 +05:30
DjDeveloperr e83dd3353e requested changes 2021-01-21 19:11:35 +05:30
DjDeveloperr 65e9583445 FIXED ERORRS 2021-01-21 18:50:43 +05:30
DjDeveloperr dc1e2bbc6e fix errors (github resolve editor sucks) 2021-01-21 18:47:03 +05:30
DjDeveloper 06997a1c18
Merge branch 'main' into slash 2021-01-21 18:43:47 +05:30
DjDeveloper 8927939dea
Merge pull request #87 from Helloyunho/guild-update
Add Missing Guild Implementations
2021-01-21 18:40:19 +05:30
DjDeveloperr eae49afcbe Merge remote-tracking branch 'origin/main' into slash 2021-01-21 18:38:24 +05:30
DjDeveloperr 77b08747ab fuck you linter 2021-01-21 18:15:31 +05:30
DjDeveloperr cc75a34d56 complete rest added 2021-01-21 18:09:51 +05:30
Helloyunho cc657cc4d8 Fix return types and make it much better 2021-01-21 15:16:45 +09:00
Helloyunho 617e3b6204 Make prune type a bit better 2021-01-21 14:54:33 +09:00
Helloyunho e99bf61f3a Fix the requested part and mute eslint for now 2021-01-21 14:45:55 +09:00
Helloyunho 8b564a4b49 Add guild prune function 2021-01-21 14:37:10 +09:00
Helloyunho 2d27068f5c Add get guild prune. add role delete in role structure 2021-01-21 14:24:41 +09:00
Helloyunho 989706e71a Add role edit feature, add guild property to role(idk why it wasn't there) 2021-01-21 01:28:18 +09:00
DjDeveloperr e7b0804616 feat: port to typed EventEmitter 2021-01-20 15:35:15 +05:30
Helloyunho 0cae198f2c Merge branch 'main' of https://github.com/harmony-org/harmony into guild-update 2021-01-20 16:02:49 +09:00
Helloyunho 8ad1a2ac6f Add jsdoc comments 2021-01-18 20:56:51 +09:00
Helloyunho 78ae0bbb56 Make guild delete and remove delete function from BaseManager 2021-01-16 23:22:13 +09:00
Aki a54f434d02
Update LICENSE 2021-01-16 15:36:20 +09:00
Aki 52314cea27
Update README.md 2021-01-16 15:36:02 +09:00
Aki 556a3c5e2f
Merge pull request #85 from DjDeveloperr/slash
new options & collectors
2021-01-16 15:35:33 +09:00
Helloyunho 19e2020e38 Add guild edit feature, fix some types 2021-01-16 02:15:24 +09:00
Helloyunho 7e11b8a351 Add preview in guild structure 2021-01-16 01:48:08 +09:00
Helloyunho 35da2dbe98 Add guild preview feature 2021-01-16 01:45:08 +09:00
Helloyunho d8e65a4328 Add guild create function and change the name of GuildChannel 2021-01-16 01:15:52 +09:00
DjDeveloperr 3f436b2b3f Merge remote-tracking branch 'origin/main' into slash
merge main
2021-01-14 18:24:05 +05:30
Aki e3033b4cbf
Update README.md 2021-01-10 19:31:47 +09:00
Aki 68029084c4
feat(LICENSE) 2021-01-10 19:31:31 +09:00
DjDeveloper 0de9b57204
Merge pull request #71 from ayntee/interac-member
fix: add permissions field to InteractionPayload#member
2021-01-10 15:17:46 +05:30
DjDeveloperr 48976e779b collectors, rest options and all that 2021-01-07 19:16:56 +05:30
DjDeveloper a630e9466c
Merge pull request #82 from ZiomaleQ/main
Guild awaitAvailiable method and removing redundant code in guild structure
2021-01-07 15:44:56 +05:30
ZiomaleQ 1d562f6997 adding timeout to awaitAvailability 2021-01-07 11:08:37 +01:00
Aki 3fe0001e44
feat(LICENSE) 2021-01-06 12:39:17 +09:00
Aki 53b49e45aa
Hello, 2021 2021-01-06 12:38:42 +09:00
ZiomaleQ 2fadcfa407 awaitAvailability function returns Promise<Guild> now 2021-01-05 16:27:03 +01:00
ZiomaleQ fbd6eae244 deleting unused function parameter 2021-01-05 16:24:56 +01:00
ZiomaleQ 734133bcca Code fix #3, requirements again 2021-01-05 14:53:00 +01:00
ZiomaleQ 60164122bf Adding promise ending ')' 2021-01-05 14:51:04 +01:00
ZiomaleQ 6b5c15d19a event based await in guildAvailability method 2021-01-05 14:42:27 +01:00
ZiomaleQ c4a3fbc45c Code fix #2, requirements again 2021-01-05 14:14:24 +01:00
ZiomaleQ 4364d3879b Code fix to match requirements 2021-01-05 14:12:21 +01:00
ZiomaleQ e009ae3127 rewriting awaitAvailability 2021-01-05 14:10:16 +01:00
ZiomaleQ 4cbc2b344d
Merge pull request #1 from harmony-org/main
Update fork
2021-01-05 13:33:33 +01:00
Helloyunho 68fa36ce3c
Merge pull request #81 from DjDeveloperr/slash
Quick fix for Events to use Manager#_delete and adding full Invite support
2021-01-05 12:00:58 +09:00
DjDeveloper 39c7761a96
feat: delete cname 2021-01-02 08:14:52 +05:30
DjDeveloper cff738b2e9
feat: add CNAME 2021-01-02 08:14:16 +05:30
Radoslaw Partyka 055a030c4e Code / style fixes 2021-01-01 17:09:41 +01:00
Radoslaw Partyka 6c3f71669d awaitAvailability is awaitAvailability now instead of awaitAvailiable 2021-01-01 16:55:27 +01:00
Radoslaw Partyka c322c25fb0 awaitAvailability in guild struct 2021-01-01 16:54:33 +01:00
Radoslaw Partyka 6e8af1f7da Removing redundant code from guild struct 2021-01-01 16:27:30 +01:00
DjDeveloperr e9f461fef4 ok linter 2021-01-01 15:05:53 +05:30
DjDeveloperr b112b6ae36 remove slashModule decorator 2021-01-01 14:22:15 +05:30
DjDeveloperr 33f103279d try fix again 2021-01-01 13:46:22 +05:30
DjDeveloperr 432555e2fb try fix 2021-01-01 13:41:44 +05:30
DjDeveloperr 8f4433dc9f allow SlashClient in slash decorators 2021-01-01 13:30:13 +05:30
DjDeveloperr c3fafdfcf0 more things 2021-01-01 11:25:23 +05:30
DjDeveloperr e3bce85f09 fix channels.array and add guildLoaded event 2021-01-01 10:30:11 +05:30
DjDeveloperr b344c2e24a fix: Guild#chunk 2021-01-01 10:18:18 +05:30
DjDeveloperr 8edef36ead fix 2020-12-31 10:42:13 +05:30
DjDeveloperr df66f4ea3e Merge remote-tracking branch 'origin/main' into slash 2020-12-31 10:38:24 +05:30
DjDeveloperr 7a2b71b648 feat: full invite support 2020-12-31 10:37:40 +05:30
DjDeveloperr 417854b1bb quick fix - use _delete for cache delete 2020-12-31 10:19:58 +05:30
Aki ae61efe73b
Update package.json 2020-12-30 08:58:45 +09:00
ayntee 30724dc791 fix: add permissions field to InteractionPayload#member 2020-12-25 16:22:10 +04:00
203 changed files with 10001 additions and 3506 deletions

3
.eggignore Normal file
View File

@ -0,0 +1,3 @@
extends .gitignore
./src/test/**/*

View File

@ -17,6 +17,7 @@ module.exports = {
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/restrict-template-expressions': 'off'
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-non-null-assertion': 'off'
}
}

9
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,9 @@
## About
<!-- describe what changes does this PR introduce and why are these needed -->
## Status
- [ ] These changes have been tested against Discord API or contain no API change.
- [ ] This PR includes only documentation changes, no code change.
- [ ] This PR introduces some Breaking changes.

View File

@ -20,14 +20,14 @@ jobs:
strategy:
matrix:
deno: ['v1.x', 'nightly']
deno: ['v1.x', 'canary']
steps:
- name: Setup repo
uses: actions/checkout@v2
- name: Setup Deno
uses: denolib/setup-deno@v2.3.0
uses: denoland/setup-deno@main
with:
deno-version: ${{ matrix.deno }} # tests across multiple Deno versions

3
.gitignore vendored
View File

@ -109,6 +109,7 @@ yarn.lock
# PRIVACY XDDDD
src/test/config.ts
test/config.ts
.vscode
# macOS is shit xD
@ -117,4 +118,4 @@ src/test/config.ts
# Webstorm dont forget this duude :)
.idea/
src/test/music.mp3
src/test/music.mp3

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at djdeveloperr@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Harmony Org
Copyright (c) 2020-2021 Harmonyland
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

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>
<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">
</a>
</p>
@ -28,7 +28,9 @@
## Usage
You can import the package from https://deno.land/x/harmony/mod.ts (with latest version) or can add a version too, and raw GitHub URL (latest unpublished version) https://raw.githubusercontent.com/harmony-org/harmony/main/mod.ts too.
You can import the package from https://deno.land/x/harmony/mod.ts (with latest version) or can add a version too, and raw GitHub URL (latest unpublished version) https://raw.githubusercontent.com/harmonyland/harmony/main/mod.ts too.
You can also check(not import) the module in https://nest.land/package/harmony (link for importing is in the site).
For a quick example, run this:
@ -36,12 +38,16 @@ For a quick example, run this:
deno run --allow-net https://deno.land/x/harmony/examples/ping.ts
```
And input your bot's token and Intents.
And input your bot's token.
Here is a small example of how to use harmony,
```ts
import { Client, Message, Intents } from 'https://deno.land/x/harmony/mod.ts'
import {
Client,
Message,
GatewayIntents
} from 'https://deno.land/x/harmony/mod.ts'
const client = new Client()
@ -53,13 +59,16 @@ client.on('ready', () => {
// Listen for event whenever a Message is sent
client.on('messageCreate', (msg: Message): void => {
if (msg.content === '!ping') {
msg.channel.send(`Pong! WS Ping: ${client.ping}`)
msg.channel.send(`Pong! WS Ping: ${client.gateway.ping}`)
}
})
// Connect to gateway
// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers)
client.connect('super secret token comes here', Intents.All)
client.connect('super secret token comes here', [
GatewayIntents.DIRECT_MESSAGES,
GatewayIntents.GUILDS,
GatewayIntents.GUILD_MESSAGES
])
```
Or with CommandClient!
@ -69,8 +78,7 @@ import {
CommandClient,
Command,
CommandContext,
Message,
Intents
GatewayIntents
} from 'https://deno.land/x/harmony/mod.ts'
const client = new CommandClient({
@ -87,26 +95,29 @@ class PingCommand extends Command {
name = 'ping'
execute(ctx: CommandContext) {
ctx.message.reply(`pong! Ping: ${ctx.client.ping}ms`)
ctx.message.reply(`pong! Ping: ${ctx.client.gateway.ping}ms`)
}
}
client.commands.add(PingCommand)
// Connect to gateway
// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers)
client.connect('super secret token comes here', Intents.All)
client.connect('super secret token comes here', [
GatewayIntents.DIRECT_MESSAGES,
GatewayIntents.GUILDS,
GatewayIntents.GUILD_MESSAGES
])
```
Or with Decorators!
```ts
import {
Client,
event,
Intents,
CommandClient,
command,
CommandContext
CommandContext,
GatewayIntents
} from 'https://deno.land/x/harmony/mod.ts'
class MyClient extends CommandClient {
@ -128,22 +139,24 @@ class MyClient extends CommandClient {
}
}
// Connect to gateway
// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers)
new MyClient().connect('super secret token comes here', Intents.All)
new MyClient().connect('super secret token comes here', [
GatewayIntents.DIRECT_MESSAGES,
GatewayIntents.GUILDS,
GatewayIntents.GUILD_MESSAGES
])
```
## Docs
Documentation is available for `main` (branch) and `stable` (release).
- [Main](https://doc.deno.land/https/raw.githubusercontent.com/harmony-org/harmony/main/mod.ts)
- [Main](https://doc.deno.land/https/raw.githubusercontent.com/harmonyland/harmony/main/mod.ts)
- [Stable](https://doc.deno.land/https/deno.land/x/harmony/mod.ts)
- [Guide](https://harmony-org.github.io)
- [Guide](https://harmony.mod.land)
## 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
@ -159,6 +172,6 @@ Small note: If editing the README, please conform to the [standard-readme](https
## License
[MIT © 2020 Harmony Org](LICENSE)
[MIT © 2020-2021 Harmonyland](LICENSE)
#### Made with ❤ by Harmony-org
#### Made with ❤ by Harmonyland

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

15
deps.ts
View File

@ -1,9 +1,6 @@
export { EventEmitter } from 'https://deno.land/std@0.82.0/node/events.ts'
export { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts'
export { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'
export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts'
export type {
Redis,
RedisConnectOptions
} from 'https://deno.land/x/redis@v0.14.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 { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts'
export { walk } from 'https://deno.land/std@0.95.0/fs/walk.ts'
export { join } from 'https://deno.land/std@0.95.0/path/mod.ts'
export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0'

24
egg.json Normal file
View File

@ -0,0 +1,24 @@
{
"$schema": "https://x.nest.land/eggs@0.3.4/src/schema.json",
"name": "harmony",
"entry": "./mod.ts",
"description": "An easy to use Discord API Library for Deno.",
"homepage": "https://github.com/harmonyland/harmony",
"version": "v1.1.4",
"files": [
"./src/**/*",
"./deps.ts",
"./README.md",
"./LICENSE",
"./banner.png",
"./CONTRIBUTING.md",
"./CODE_OF_CONDUCT.md",
"./examples/*"
],
"checkFormat": "npx eslint src",
"checkTests": false,
"checkInstallation": false,
"check": true,
"unlisted": false,
"ignore": []
}

View File

@ -1,4 +1,4 @@
import { Client, Message, Intents } from '../mod.ts'
import { Client, Message, GatewayIntents } from '../mod.ts'
const client = new Client()
@ -13,7 +13,7 @@ client.on('messageCreate', (msg: Message) => {
}
})
console.log('harmony - ping example')
console.log('Harmony - Ping Example')
const token = prompt('Input Bot Token:')
if (token === null) {
@ -21,23 +21,8 @@ if (token === null) {
Deno.exit()
}
const intents = prompt(
'Input Intents (0 = All, 1 = Presence, 2 = Server Members, 3 = None):'
)
if (intents === null || !['0', '1', '2', '3'].includes(intents)) {
console.log('No intents provided')
Deno.exit()
}
let ints
if (intents === '0') {
ints = Intents.All
} else if (intents === '1') {
ints = Intents.Presence
} else if (intents === '2') {
ints = Intents.GuildMembers
} else {
ints = Intents.None
}
client.connect(token, ints)
client.connect(token, [
GatewayIntents.GUILD_MESSAGES,
GatewayIntents.GUILDS,
GatewayIntents.DIRECT_MESSAGES
])

121
mod.ts
View File

@ -1,37 +1,47 @@
export { GatewayIntents } from './src/types/gateway.ts'
export { Base } from './src/structures/base.ts'
export { Gateway } from './src/gateway/index.ts'
export type { ClientEvents } from './src/gateway/handlers/index.ts'
export * from './src/models/client.ts'
export * from './src/models/slashClient.ts'
export { RESTManager } from './src/models/rest.ts'
export * from './src/models/cacheAdapter.ts'
export { Gateway } from './src/gateway/mod.ts'
export type { GatewayTypedEvents } from './src/gateway/mod.ts'
export type { ClientEvents } from './src/gateway/handlers/mod.ts'
export * from './src/client/mod.ts'
export * from './src/interactions/mod.ts'
export {
RESTManager,
TokenType,
HttpResponseCode,
DiscordAPIError
} from './src/rest/mod.ts'
export * from './src/rest/mod.ts'
export * from './src/cache/adapter.ts'
export {
Command,
CommandBuilder,
CommandCategory,
CommandsManager,
CategoriesManager
} from './src/models/command.ts'
export type { CommandContext, CommandOptions } from './src/models/command.ts'
CategoriesManager,
CommandsLoader
} from './src/commands/command.ts'
export type { CommandContext, CommandOptions } from './src/commands/command.ts'
export {
Extension,
ExtensionCommands,
ExtensionsManager
} from './src/models/extensions.ts'
export { SlashModule } from './src/models/slashModule.ts'
export { CommandClient, command } from './src/models/commandClient.ts'
export type { CommandClientOptions } from './src/models/commandClient.ts'
} from './src/commands/extension.ts'
export { SlashModule } from './src/interactions/slashModule.ts'
export { CommandClient, command } from './src/commands/client.ts'
export type { CommandClientOptions } from './src/commands/client.ts'
export { BaseManager } from './src/managers/base.ts'
export { BaseChildManager } from './src/managers/baseChild.ts'
export { ChannelsManager } from './src/managers/channels.ts'
export { EmojisManager } from './src/managers/emojis.ts'
export { GatewayCache } from './src/managers/gatewayCache.ts'
export { GuildChannelsManager } from './src/managers/guildChannels.ts'
export type { GuildChannel } from './src/managers/guildChannels.ts'
export { GuildManager } from './src/managers/guilds.ts'
export * from './src/structures/base.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 { MembersManager } from './src/managers/members.ts'
export { MessageReactionsManager } from './src/managers/messageReactions.ts'
@ -39,9 +49,11 @@ export { ReactionUsersManager } from './src/managers/reactionUsers.ts'
export { MessagesManager } from './src/managers/messages.ts'
export { RolesManager } from './src/managers/roles.ts'
export { UsersManager } from './src/managers/users.ts'
export { InviteManager } from './src/managers/invites.ts'
export { Application } from './src/structures/application.ts'
// export { ImageURL } from './src/structures/cdn.ts'
export { Channel } from './src/structures/channel.ts'
export { ImageURL } from './src/structures/cdn.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 { Embed } from './src/structures/embed.ts'
export { Emoji } from './src/structures/emoji.ts'
@ -57,7 +69,11 @@ export { NewsChannel } from './src/structures/guildNewsChannel.ts'
export { VoiceChannel } from './src/structures/guildVoiceChannel.ts'
export { Invite } from './src/structures/invite.ts'
export * from './src/structures/member.ts'
export { Message } from './src/structures/message.ts'
export {
Message,
MessageAttachment,
MessageInteraction
} from './src/structures/message.ts'
export { MessageMentions } from './src/structures/messageMentions.ts'
export {
Presence,
@ -66,7 +82,13 @@ export {
} from './src/structures/presence.ts'
export { Role } from './src/structures/role.ts'
export { Snowflake } from './src/utils/snowflake.ts'
export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts'
export { TextChannel } from './src/structures/textChannel.ts'
export {
GuildTextBasedChannel,
GuildTextChannel,
checkGuildTextBasedChannel
} from './src/structures/guildTextChannel.ts'
export type { AllMessageOptions } from './src/structures/textChannel.ts'
export { MessageReaction } from './src/structures/messageReaction.ts'
export { User } from './src/structures/user.ts'
export { Webhook } from './src/structures/webhook.ts'
@ -75,7 +97,8 @@ export { Intents } from './src/utils/intents.ts'
// export { getBuildInfo } from './src/utils/buildInfo.ts'
export * from './src/utils/permissions.ts'
export { UserFlagsManager } from './src/utils/userFlags.ts'
export type { EveryChannelTypes } from './src/utils/getChannelByType.ts'
export { HarmonyEventEmitter } from './src/utils/events.ts'
export type { EveryChannelTypes } from './src/utils/channel.ts'
export * from './src/utils/bitfield.ts'
export type {
ActivityGame,
@ -83,7 +106,15 @@ export type {
ClientStatus,
StatusType
} 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 { ImageFormats, ImageSize } from './src/types/cdn.ts'
export type {
@ -95,14 +126,33 @@ export type {
GuildChannelPayload,
GuildTextChannelPayload,
GuildVoiceChannelPayload,
GroupDMChannelPayload
GroupDMChannelPayload,
MessageOptions,
MessagePayload,
MessageInteractionPayload,
MessageReference,
MessageActivity,
MessageActivityTypes,
MessageApplication,
MessageFlags,
MessageStickerFormatTypes,
MessageStickerPayload,
MessageTypes,
OverwriteAsArg,
Overwrite
} from './src/types/channel.ts'
export type { EmojiPayload } from './src/types/emoji.ts'
export { Verification } from './src/types/guild.ts'
export type {
GuildIntegrationPayload,
GuildPayload,
GuildBanPayload,
GuildFeatures,
GuildIntegrationPayload,
GuildPayload
GuildChannels,
GuildTextBasedChannels,
GuildCreateOptions,
GuildCreateChannelOptions,
GuildCreateRolePayload
} from './src/types/guild.ts'
export type { InvitePayload, PartialInvitePayload } from './src/types/invite.ts'
export { PermissionFlags } from './src/types/permissionFlags.ts'
@ -122,3 +172,26 @@ export type { UserPayload } from './src/types/user.ts'
export { UserFlags } from './src/types/userFlags.ts'
export type { VoiceStatePayload } from './src/types/voice.ts'
export type { WebhookPayload } from './src/types/webhook.ts'
export * from './src/client/collectors.ts'
export type { Dict } from './src/utils/dict.ts'
export * from './src/cache/redis.ts'
export { ColorUtil } from './src/utils/colorutil.ts'
export type { Colors } from './src/utils/colorutil.ts'
export { StoreChannel } from './src/structures/guildStoreChannel.ts'
export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts'
export {
isCategoryChannel,
isDMChannel,
isGroupDMChannel,
isGuildBasedTextChannel,
isGuildChannel,
isGuildTextChannel,
isNewsChannel,
isStageVoiceChannel,
isStoreChannel,
isTextChannel,
isVoiceChannel,
default as getChannelByType
} from './src/utils/channel.ts'
export * from './src/utils/interactions.ts'
export * from "./src/utils/command.ts"

View File

@ -1,6 +1,6 @@
{
"name": "harmony",
"version": "0.9.2",
"version": "0.9.3",
"description": "Discord Deno API that is easy to use.",
"main": "mod.ts",
"repository": "https://github.com/harmony-org/harmony.git",

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

462
src/client/client.ts Normal file
View File

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

170
src/client/collectors.ts Normal file
View File

@ -0,0 +1,170 @@
import { Collection } from '../utils/collection.ts'
import type { Client } from '../client/client.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
export interface CollectorOptions {
/** Event name to listen for */
event: string
/** Optionally Client object for deinitOnEnd functionality */
client?: Client
/** Filter function */
filter?: CollectorFilter
/** Max entries to collect */
max?: number
/** Whether or not to de-initialize on end */
deinitOnEnd?: boolean
/** Timeout to end the Collector if not fulfilled if any filter or max */
timeout?: number
}
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type CollectorEvents = {
start: []
end: []
collect: any
}
export class Collector extends HarmonyEventEmitter<CollectorEvents> {
client?: Client
private _started: boolean = false
event: string
filter: CollectorFilter = () => true
collected: Collection<string, any[]> = new Collection()
max?: number
deinitOnEnd: boolean = false
timeout?: number
private _timer?: number
get started(): boolean {
return this._started
}
set started(d: boolean) {
if (d !== this._started) {
this._started = d
if (d) this.emit('start')
else {
if (this.deinitOnEnd && this.client !== undefined)
this.deinit(this.client)
this.emit('end')
}
}
}
constructor(options: CollectorOptions | string) {
super()
if (typeof options === 'string') this.event = options
else {
this.event = options.event
this.client = options.client
this.filter = options.filter ?? (() => true)
this.max = options.max
this.deinitOnEnd = options.deinitOnEnd ?? false
this.timeout = options.timeout
}
}
/** Start collecting */
collect(): Collector {
this.started = true
if (this.client !== undefined) this.init(this.client)
if (this._timer !== undefined) clearTimeout(this._timer)
if (this.timeout !== undefined) {
this._timer = setTimeout(() => {
this.end()
}, this.timeout)
}
return this
}
/** End collecting */
end(): Collector {
this.started = false
if (this._timer !== undefined) clearTimeout(this._timer)
return this
}
/** Reset collector and start again */
reset(): Collector {
this.collected = new Collection()
this.collect()
return this
}
/** Init the Collector on Client */
init(client: Client): Collector {
this.client = client
client.addCollector(this)
return this
}
/** De initialize the Collector i.e. remove cleanly */
deinit(client: Client): Collector {
client.removeCollector(this)
return this
}
/** Checks we may want to perform on an extended version of Collector */
protected check(..._args: any[]): boolean | Promise<boolean> {
return true
}
/** Fire the Collector */
async _fire(...args: any[]): Promise<void> {
if (!this.started) return
const check = await this.check(...args)
if (!check) return
const filter = await this.filter(...args)
if (!filter) return
this.collected.set((Number(this.collected.size) + 1).toString(), args)
this.emit('collect', ...args)
if (
this.max !== undefined &&
// linter: send help
this.max < Number(this.collected.size) + 1
) {
this.end()
}
}
/** Set filter of the Collector */
when(filter: CollectorFilter): Collector {
this.filter = filter
return this
}
/** Add a new listener for 'collect' event */
each(handler: CallableFunction): Collector {
this.on('collect', () => handler())
return this
}
/** Returns a Promise resolved when Collector ends or a timeout occurs */
async wait(timeout?: number): Promise<Collector> {
if (timeout === undefined) timeout = this.timeout ?? 0
return await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!timeout)
throw new Error(
'Timeout is required parameter if not given in CollectorOptions'
)
let done = false
const onend = (): void => {
done = true
this.off('end', onend)
resolve(this)
}
this.on('end', onend)
setTimeout(() => {
if (!done) {
this.off('end', onend)
reject(new Error('Timeout'))
}
}, timeout)
})
}
}

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

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

133
src/client/shard.ts Normal file
View File

@ -0,0 +1,133 @@
import { Collection } from '../utils/collection.ts'
import type { Client } from './client.ts'
import { RESTManager } from '../rest/mod.ts'
import { Gateway } from '../gateway/mod.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
import { GatewayEvents } from '../types/gateway.ts'
import { delay } from '../utils/delay.ts'
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ShardManagerEvents = {
launch: [number]
shardReady: [number]
shardDisconnect: [number, number | undefined, string | undefined]
shardError: [number, Error, ErrorEvent]
shardResume: [number]
}
export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
list: Collection<string, Gateway> = new Collection()
client: Client
cachedShardCount?: number
queueProcessing: boolean = false
queue: CallableFunction[] = []
get rest(): RESTManager {
return this.client.rest
}
constructor(client: Client) {
super()
this.client = client
}
debug(msg: string): void {
this.client.debug('Shards', msg)
}
enqueueIdentify(fn: CallableFunction): ShardManager {
this.queue.push(fn)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
if (!this.queueProcessing) this.processQueue()
return this
}
private async processQueue(): Promise<void> {
if (this.queueProcessing || this.queue.length === 0) return
this.queueProcessing = true
const item = this.queue[0]
await item()
this.queue.shift()
await delay(5000)
this.queueProcessing = false
if (this.queue.length === 0) {
this.queueProcessing = false
} else {
await this.processQueue()
}
}
async getShardCount(): Promise<number> {
let shardCount: number
if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount
else {
if (
this.client.shardCount === 'auto' &&
this.client.fetchGatewayInfo !== false
) {
this.debug('Fetch /gateway/bot...')
const info = await this.client.rest.api.gateway.bot.get()
this.debug(`Recommended Shards: ${info.shards}`)
this.debug('=== Session Limit Info ===')
this.debug(
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
)
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
shardCount = info.shards as number
} else
shardCount =
typeof this.client.shardCount === 'string'
? 1
: this.client.shardCount ?? 1
}
this.cachedShardCount = shardCount
return this.cachedShardCount
}
/** Launches a new Shard */
async launch(id: number): Promise<ShardManager> {
if (this.list.has(id.toString()) === true)
throw new Error(`Shard ${id} already launched`)
this.debug(`Launching Shard: ${id}`)
const shardCount = await this.getShardCount()
const gw = new Gateway(this.client, [Number(id), shardCount])
this.list.set(id.toString(), gw)
gw.initWebsocket()
this.emit('launch', id)
gw.on(GatewayEvents.Ready, () => this.emit('shardReady', id))
gw.on('error', (err: Error, evt: ErrorEvent) =>
this.emit('shardError', id, err, evt)
)
gw.on(GatewayEvents.Resumed, () => this.emit('shardResume', id))
gw.on('close', (code: number, reason: string) =>
this.emit('shardDisconnect', id, code, reason)
)
return gw.waitFor(GatewayEvents.Ready, () => true).then(() => this)
}
/** Launches all Shards */
async connect(): Promise<ShardManager> {
const shardCount = await this.getShardCount()
this.client.shardCount = shardCount
this.debug(`Launching ${shardCount} shard${shardCount === 1 ? '' : 's'}...`)
const startTime = Date.now()
for (let i = 0; i < shardCount; i++) {
await this.launch(i)
}
const endTime = Date.now()
const diff = endTime - startTime
this.debug(
`Launched ${shardCount} shards! Time taken: ${Math.floor(diff / 1000)}s`
)
return this
}
get(id: number): Gateway | undefined {
return this.list.get(id.toString())
}
}

116
src/client/voice.ts Normal file
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,7 +1,6 @@
import { Message } from '../structures/message.ts'
import { GuildTextChannel } from '../structures/textChannel.ts'
import { awaitSync } from '../utils/mixedPromise.ts'
import { Client, ClientOptions } from './client.ts'
import type { Message } from '../structures/message.ts'
import type { GuildTextBasedChannel } from '../structures/guildTextChannel.ts'
import { Client, ClientOptions } from '../client/mod.ts'
import {
CategoriesManager,
Command,
@ -10,7 +9,8 @@ import {
CommandsManager,
parseCommand
} 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[]>
@ -44,6 +44,11 @@ export interface CommandClientOptions extends ClientOptions {
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 {
prefix: string | string[]
mentionPrefix: boolean
@ -66,8 +71,6 @@ export class CommandClient extends Client implements CommandClientOptions {
commands: CommandsManager = new CommandsManager(this)
categories: CategoriesManager = new CategoriesManager(this)
_decoratedCommands?: { [name: string]: Command }
constructor(options: CommandClientOptions) {
super(options)
this.prefix = options.prefix
@ -112,11 +115,12 @@ export class CommandClient extends Client implements CommandClientOptions {
this.caseSensitive =
options.caseSensitive === undefined ? false : options.caseSensitive
if (this._decoratedCommands !== undefined) {
Object.values(this._decoratedCommands).forEach((entry) => {
const self = this as any
if (self._decoratedCommands !== undefined) {
Object.values(self._decoratedCommands).forEach((entry: any) => {
this.commands.add(entry)
})
this._decoratedCommands = undefined
self._decoratedCommands = undefined
}
this.on(
@ -129,35 +133,29 @@ export class CommandClient extends Client implements CommandClientOptions {
async processMessage(msg: Message): Promise<any> {
if (!this.allowBots && msg.author.bot === true) return
const isUserBlacklisted = await awaitSync(
this.isUserBlacklisted(msg.author.id)
)
if (isUserBlacklisted === true) return
const isUserBlacklisted = await this.isUserBlacklisted(msg.author.id)
if (isUserBlacklisted) return
const isChannelBlacklisted = await awaitSync(
this.isChannelBlacklisted(msg.channel.id)
)
if (isChannelBlacklisted === true) return
const isChannelBlacklisted = await this.isChannelBlacklisted(msg.channel.id)
if (isChannelBlacklisted) return
if (msg.guild !== undefined) {
const isGuildBlacklisted = await awaitSync(
this.isGuildBlacklisted(msg.guild.id)
)
if (isGuildBlacklisted === true) return
const isGuildBlacklisted = await this.isGuildBlacklisted(msg.guild.id)
if (isGuildBlacklisted) return
}
let prefix: string | string[] = []
if (typeof this.prefix === 'string') prefix = [...prefix, this.prefix]
else prefix = [...prefix, ...this.prefix]
const userPrefix = await awaitSync(this.getUserPrefix(msg.author.id))
const userPrefix = await this.getUserPrefix(msg.author.id)
if (userPrefix !== undefined) {
if (typeof userPrefix === 'string') prefix = [...prefix, userPrefix]
else prefix = [...prefix, ...userPrefix]
}
if (msg.guild !== undefined) {
const guildPrefix = await awaitSync(this.getGuildPrefix(msg.guild.id))
const guildPrefix = await this.getGuildPrefix(msg.guild.id)
if (guildPrefix !== undefined) {
if (typeof guildPrefix === 'string') prefix = [...prefix, guildPrefix]
else prefix = [...prefix, ...guildPrefix]
@ -242,7 +240,7 @@ export class CommandClient extends Client implements CommandClientOptions {
client: this,
name: parsed.name,
prefix,
args: parsed.args,
args: parseArgs(command.args, parsed.args),
argString: parsed.argString,
message: msg,
author: msg.author,
@ -259,7 +257,7 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.ownerOnly) === true &&
!this.owners.includes(msg.author.id)
)
return this.emit('commandOwnerOnly', ctx, command)
return this.emit('commandOwnerOnly', ctx)
// Checks if Command is only for Guild
if (
@ -268,7 +266,7 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.guildOnly) === true &&
msg.guild === undefined
)
return this.emit('commandGuildOnly', ctx, command)
return this.emit('commandGuildOnly', ctx)
// Checks if Command is only for DMs
if (
@ -277,14 +275,14 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.dmOnly) === true &&
msg.guild !== undefined
)
return this.emit('commandDmOnly', ctx, command)
return this.emit('commandDmOnly', ctx)
if (
command.nsfw === true &&
(msg.guild === undefined ||
((msg.channel as unknown) as GuildTextChannel).nsfw !== true)
((msg.channel as unknown) as GuildTextBasedChannel).nsfw !== true)
)
return this.emit('commandNSFW', ctx, command)
return this.emit('commandNSFW', ctx)
const allPermissions =
command.permissions !== undefined
@ -293,7 +291,8 @@ export class CommandClient extends Client implements CommandClientOptions {
if (
(command.botPermissions !== undefined ||
category?.permissions !== undefined) &&
category?.botPermissions !== undefined ||
allPermissions !== undefined) &&
msg.guild !== undefined
) {
// TODO: Check Overwrites too
@ -316,18 +315,14 @@ export class CommandClient extends Client implements CommandClientOptions {
}
if (missing.length !== 0)
return this.emit(
'commandBotMissingPermissions',
ctx,
command,
missing
)
return this.emit('commandBotMissingPermissions', ctx, missing)
}
}
if (
(command.userPermissions !== undefined ||
category?.userPermissions !== undefined) &&
category?.userPermissions !== undefined ||
allPermissions !== undefined) &&
msg.guild !== undefined
) {
let permissions =
@ -349,51 +344,52 @@ export class CommandClient extends Client implements CommandClientOptions {
}
if (missing.length !== 0)
return this.emit(
'commandUserMissingPermissions',
command,
missing,
ctx
)
return this.emit('commandUserMissingPermissions', ctx, missing)
}
}
if (command.args !== undefined) {
if (typeof command.args === 'boolean' && parsed.args.length === 0)
return this.emit('commandMissingArgs', ctx, command)
return this.emit('commandMissingArgs', ctx)
else if (
typeof command.args === 'number' &&
parsed.args.length < command.args
)
this.emit('commandMissingArgs', ctx, command)
this.emit('commandMissingArgs', ctx)
}
try {
this.emit('commandUsed', ctx, command)
this.emit('commandUsed', ctx)
const beforeExecute = await awaitSync(command.beforeExecute(ctx))
const beforeExecute = await command.beforeExecute(ctx)
if (beforeExecute === false) return
const result = await awaitSync(command.execute(ctx))
command.afterExecute(ctx, result)
const result = await command.execute(ctx)
await command.afterExecute(ctx, result)
} catch (e) {
this.emit('commandError', command, ctx, e)
try {
await command.onError(ctx, e)
} catch (e) {
this.emit('commandError', ctx, e)
}
this.emit('commandError', ctx, e)
}
}
}
/**
* Command decorator. Decorates the function with optional metadata as a Command registered upon constructing class.
*/
export function command(options?: CommandOptions) {
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 {
[name: string]: (ctx: CommandContext) => any
})[name]
const prop = c[name]
if (prop instanceof Command) {
target._decoratedCommands[prop.name] = prop
return
}
if (typeof prop !== 'function')
throw new Error('@command decorator can only be used on class methods')
const command = new Command()
@ -404,6 +400,6 @@ export function command(options?: CommandOptions) {
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 { Message } from '../structures/message.ts'
import { TextChannel } from '../structures/textChannel.ts'
import { User } from '../structures/user.ts'
import type { Guild } from '../structures/guild.ts'
import type { Message } from '../structures/message.ts'
import type { TextChannel } from '../structures/textChannel.ts'
import type { User } from '../structures/user.ts'
import { Collection } from '../utils/collection.ts'
import { CommandClient } from './commandClient.ts'
import { Extension } from './extensions.ts'
import { parse } from '../../deps.ts'
import type { CommandClient } from './client.ts'
import type { Extension } from './extension.ts'
import { join, walk } from '../../deps.ts'
import type { Args } from '../utils/command.ts'
export interface CommandContext {
/** The Client object */
client: CommandClient
@ -23,7 +23,7 @@ export interface CommandContext {
/** Name of Command which was used */
name: string
/** Array of Arguments used with Command */
args: string[]
args: Record<string, unknown> | null
/** Complete Raw String of Arguments */
argString: string
/** Guild which the command has called */
@ -46,7 +46,7 @@ export interface CommandOptions {
/** Usage Example of Command, only Arguments (without Prefix and Name) */
examples?: string | string[]
/** 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?: string | string[]
/** Permission(s) required for using Command */
@ -72,6 +72,8 @@ export interface CommandOptions {
}
export class Command implements CommandOptions {
static meta?: CommandOptions
name: string = ''
description?: string
category?: string
@ -79,7 +81,7 @@ export class Command implements CommandOptions {
extension?: Extension
usage?: string | string[]
examples?: string | string[]
args?: number | boolean | string[]
args?: Args[]
permissions?: string | string[]
userPermissions?: string | string[]
botPermissions?: string | string[]
@ -92,6 +94,9 @@ export class Command implements CommandOptions {
dmOnly?: boolean
ownerOnly?: boolean
/** Method called when the command errors */
onError(ctx: CommandContext, error: Error): any {}
/** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */
beforeExecute(ctx: CommandContext): boolean | Promise<boolean> {
return true
@ -284,13 +289,107 @@ export class CommandBuilder extends Command {
}
}
export class CommandsLoader {
client: CommandClient
#importSeq: { [name: string]: number } = {}
constructor(client: CommandClient) {
this.client = client
}
/**
* Load a Command from file.
*
* NOTE: Relative paths resolve from cwd
*
* @param filePath Path of Command file.
* @param exportName Export name. Default is the "default" export.
*/
async load(
filePath: string,
exportName: string = 'default',
onlyRead?: boolean
): Promise<Command> {
const stat = await Deno.stat(filePath).catch(() => undefined)
if (stat === undefined || stat.isFile !== true)
throw new Error(`File not found on path ${filePath}`)
let seq: number | undefined
if (this.#importSeq[filePath] !== undefined) seq = this.#importSeq[filePath]
const mod = await import(
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
'file:///' +
join(Deno.cwd(), filePath) +
(seq === undefined ? '' : `#${seq}`)
)
if (this.#importSeq[filePath] === undefined) this.#importSeq[filePath] = 0
else this.#importSeq[filePath]++
const Cmd = mod[exportName]
if (Cmd === undefined)
throw new Error(`Command not exported as ${exportName} from ${filePath}`)
let cmd: Command
try {
if (Cmd instanceof Command) cmd = Cmd
else cmd = new Cmd()
if (!(cmd instanceof Command)) throw new Error('failed')
} catch (e) {
throw new Error(`Failed to load Command from ${filePath}`)
}
if (onlyRead !== true) this.client.commands.add(cmd)
return cmd
}
/**
* Load commands from a Directory.
*
* NOTE: Relative paths resolve from cwd
*
* @param path Path of the directory.
* @param options Options to configure loading.
*/
async loadDirectory(
path: string,
options?: {
recursive?: boolean
exportName?: string
maxDepth?: number
exts?: string[]
onlyRead?: boolean
}
): Promise<Command[]> {
const commands: Command[] = []
for await (const entry of walk(path, {
maxDepth: options?.maxDepth,
exts: options?.exts,
includeDirs: false
})) {
if (entry.isFile !== true) continue
const cmd = await this.load(
entry.path,
options?.exportName,
options?.onlyRead
)
commands.push(cmd)
}
return commands
}
}
export class CommandsManager {
client: CommandClient
list: Collection<string, Command> = new Collection()
disabled: Set<string> = new Set()
loader: CommandsLoader
constructor(client: CommandClient) {
this.client = client
this.loader = new CommandsLoader(client)
}
/** Number of loaded Commands */
@ -393,12 +492,16 @@ export class CommandsManager {
/** Add a Command */
add(cmd: Command | typeof Command): boolean {
// eslint-disable-next-line new-cap
if (!(cmd instanceof Command)) cmd = new cmd()
if (!(cmd instanceof Command)) {
const CmdClass = cmd
cmd = new CmdClass()
Object.assign(cmd, CmdClass.meta ?? {})
}
if (this.exists(cmd, cmd.extension?.subPrefix))
throw new Error(
`Failed to add Command '${cmd.toString()}' with name/alias already exists.`
)
if (cmd.name === '') throw new Error('Command has no name')
this.list.set(
`${cmd.name}-${
this.list.filter((e) =>
@ -506,7 +609,7 @@ export const parseCommand = (
): ParsedCommand | undefined => {
let content = msg.content.slice(prefix.length)
if (client.spacesAfterPrefix === true) content = content.trim()
const args = parse(content)._.map((e) => e.toString())
const args = content.split(' ')
const name = args.shift()
if (name === undefined) return

View File

@ -1,6 +1,7 @@
import { Collection } from '../utils/collection.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
@ -72,32 +73,30 @@ export class Extension {
/** Events registered by this Extension */
events: { [name: string]: (...args: any[]) => {} } = {}
_decoratedCommands?: { [name: string]: Command }
_decoratedEvents?: { [name: string]: (...args: any[]) => any }
constructor(client: CommandClient) {
this.client = client
if (this._decoratedCommands !== undefined) {
Object.entries(this._decoratedCommands).forEach((entry) => {
const self = this as any
if (self._decoratedCommands !== undefined) {
Object.entries(self._decoratedCommands).forEach((entry: any) => {
entry[1].extension = this
this.commands.add(entry[1])
})
this._decoratedCommands = undefined
self._decoratedCommands = undefined
}
if (
this._decoratedEvents !== undefined &&
Object.keys(this._decoratedEvents).length !== 0
self._decoratedEvents !== undefined &&
Object.keys(self._decoratedEvents).length !== 0
) {
Object.entries(this._decoratedEvents).forEach((entry) => {
this.listen(entry[0], entry[1])
Object.entries(self._decoratedEvents).forEach((entry: any) => {
this.listen(entry[0] as keyof ClientEvents, entry[1].bind(this))
})
this._decoratedEvents = undefined
self._decoratedEvents = undefined
}
}
/** Listens for an Event through Extension. */
listen(event: string, cb: ExtensionEventCallback): boolean {
listen(event: keyof ClientEvents, cb: ExtensionEventCallback): boolean {
if (this.events[event] !== undefined) return false
else {
const fn = (...args: any[]): any => {
@ -152,7 +151,7 @@ export class ExtensionsManager {
if (extension === undefined) return false
extension.commands.deleteAll()
for (const [k, v] of Object.entries(extension.events)) {
this.client.removeListener(k, v)
this.client.off(k as keyof ClientEvents, v)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete extension.events[k]
}

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

@ -0,0 +1,15 @@
import { SlashCommand } from '../../interactions/slashCommand.ts'
import { ApplicationCommandPayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const applicationCommandCreate: GatewayEventHandler = async (
gateway: Gateway,
d: ApplicationCommandPayload
) => {
const guild =
d.guild_id === undefined
? undefined
: await gateway.client.guilds.get(d.guild_id)
const cmd = new SlashCommand(gateway.client.slash.commands, d, guild)
gateway.client.emit('slashCommandCreate', cmd)
}

View File

@ -0,0 +1,15 @@
import { SlashCommand } from '../../interactions/slashCommand.ts'
import { ApplicationCommandPayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const applicationCommandDelete: GatewayEventHandler = async (
gateway: Gateway,
d: ApplicationCommandPayload
) => {
const guild =
d.guild_id === undefined
? undefined
: await gateway.client.guilds.get(d.guild_id)
const cmd = new SlashCommand(gateway.client.slash.commands, d, guild)
gateway.client.emit('slashCommandDelete', cmd)
}

View File

@ -0,0 +1,15 @@
import { SlashCommand } from '../../interactions/slashCommand.ts'
import { ApplicationCommandPayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const applicationCommandUpdate: GatewayEventHandler = async (
gateway: Gateway,
d: ApplicationCommandPayload
) => {
const guild =
d.guild_id === undefined
? undefined
: await gateway.client.guilds.get(d.guild_id)
const cmd = new SlashCommand(gateway.client.slash.commands, d, guild)
gateway.client.emit('slashCommandUpdate', cmd)
}

View File

@ -1,7 +1,10 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import getChannelByType from '../../utils/getChannelByType.ts'
import { ChannelPayload, GuildChannelPayload } from '../../types/channel.ts'
import { Guild } from '../../structures/guild.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import getChannelByType from '../../utils/channel.ts'
import type {
ChannelPayload,
GuildChannelPayload
} from '../../types/channel.ts'
import type { Guild } from '../../structures/guild.ts'
export const channelCreate: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,5 +1,5 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { ChannelPayload } from '../../types/channel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import type { ChannelPayload } from '../../types/channel.ts'
export const channelDelete: GatewayEventHandler = async (
gateway: Gateway,
@ -7,7 +7,7 @@ export const channelDelete: GatewayEventHandler = async (
) => {
const channel = await gateway.client.channels.get(d.id)
if (channel !== undefined) {
await gateway.client.channels.delete(d.id)
await gateway.client.channels._delete(d.id)
gateway.client.emit('channelDelete', channel)
}
}

View File

@ -1,6 +1,6 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { TextChannel } from '../../structures/textChannel.ts'
import { ChannelPinsUpdatePayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
import type { ChannelPinsUpdatePayload } from '../../types/gateway.ts'
export const channelPinsUpdate: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,6 +1,6 @@
import { Channel } from '../../structures/channel.ts'
import { ChannelPayload } from '../../types/channel.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { Channel } from '../../structures/channel.ts'
import type { ChannelPayload } from '../../types/channel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const channelUpdate: GatewayEventHandler = async (
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 { User } from '../../structures/user.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 { User } from '../../structures/user.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 { GuildPayload } from '../../types/guild.ts'
import { GuildChannelPayload } from '../../types/channel.ts'
@ -27,8 +27,13 @@ export const guildCreate: GatewayEventHandler = async (
if (d.voice_states !== undefined)
await guild.voiceStates.fromPayload(d.voice_states)
for (const emojiPayload of d.emojis) {
if (emojiPayload.id === null) continue
await gateway.client.emojis.set(emojiPayload.id, emojiPayload)
}
if (hasGuild === undefined) {
// It wasn't lazy load, so emit event
gateway.client.emit('guildCreate', guild)
}
} else gateway.client.emit('guildLoaded', guild)
}

View File

@ -1,6 +1,6 @@
import { Guild } from '../../structures/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 (
gateway: Gateway,
@ -13,7 +13,8 @@ export const guildDelete: GatewayEventHandler = async (
await guild.channels.flush()
await guild.roles.flush()
await guild.presences.flush()
await gateway.client.guilds.delete(d.id)
await guild.emojis.flush()
await gateway.client.guilds._delete(d.id)
gateway.client.emit('guildDelete', guild)
}

View File

@ -2,7 +2,7 @@ import { Emoji } from '../../structures/emoji.ts'
import { Guild } from '../../structures/guild.ts'
import { EmojiPayload } from '../../types/emoji.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 (
gateway: Gateway,
@ -44,16 +44,18 @@ export const guildEmojiUpdate: GatewayEventHandler = async (
}
}
gateway.client.emit('guildEmojisUpdate', guild)
for (const emoji of deleted) {
gateway.client.emit('guildEmojiDelete', guild, emoji)
gateway.client.emit('guildEmojiDelete', emoji)
}
for (const emoji of added) {
gateway.client.emit('guildEmojiAdd', guild, emoji)
gateway.client.emit('guildEmojiAdd', emoji)
}
for (const emoji of updated) {
gateway.client.emit('guildEmojiUpdate', guild, emoji.before, emoji.after)
gateway.client.emit('guildEmojiUpdate', emoji.before, emoji.after)
}
}
}

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 { 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 { GuildMemberAddPayload } from '../../types/gateway.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 { User } from '../../structures/user.ts'
import { GuildMemberRemovePayload } from '../../types/gateway.ts'
@ -12,7 +12,7 @@ export const guildMemberRemove: GatewayEventHandler = async (
if (guild === undefined) return
const member = await guild.members.get(d.user.id)
await guild.members.delete(d.user.id)
await guild.members._delete(d.user.id)
if (member !== undefined) gateway.client.emit('guildMemberRemove', member)
else {

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 { GuildMemberUpdatePayload } from '../../types/gateway.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 { 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 { GuildRoleCreatePayload } from '../../types/gateway.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 { GuildRoleDeletePayload } from '../../types/gateway.ts'
@ -13,6 +13,7 @@ export const guildRoleDelete: GatewayEventHandler = async (
const role = await guild.roles.get(d.role_id)
// Shouldn't happen either
if (role === undefined) return
await guild.roles._delete(d.role_id)
gateway.client.emit('guildRoleDelete', role)
}

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 { GuildRoleUpdatePayload } from '../../types/gateway.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 { 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 { Interaction } from '../../structures/slash.ts'
import { GuildTextChannel } from '../../structures/textChannel.ts'
import { InteractionPayload } from '../../types/slash.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import {
InteractionApplicationCommandResolved,
SlashCommandInteraction
} from '../../structures/slash.ts'
import {
Interaction,
InteractionChannel
} from '../../structures/interactions.ts'
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
import {
InteractionPayload,
InteractionType
} from '../../types/interactions.ts'
import { UserPayload } from '../../types/user.ts'
import { Permissions } from '../../utils/permissions.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import { User } from '../../structures/user.ts'
import { Role } from '../../structures/role.ts'
import { RolePayload } from '../../types/role.ts'
import {
InteractionApplicationCommandData,
InteractionChannelPayload
} from '../../types/slashCommands.ts'
import { Message } from '../../structures/message.ts'
import { TextChannel } from '../../structures/textChannel.ts'
export const interactionCreate: GatewayEventHandler = async (
gateway: Gateway,
d: InteractionPayload
) => {
const guild = await gateway.client.guilds.get(d.guild_id)
if (guild === undefined) return
// NOTE(DjDeveloperr): Mason once mentioned that channel_id can be optional in Interaction.
// 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 member = ((await guild.members.get(
d.member.user.id
)) as unknown) as Member
const guild =
d.guild_id === undefined
? undefined
: (await gateway.client.guilds.get(d.guild_id)) ??
new Guild(gateway.client, { unavailable: true, id: d.guild_id } as any)
const channel =
(await gateway.client.channels.get<GuildTextChannel>(d.channel_id)) ??
(await gateway.client.channels.fetch<GuildTextChannel>(d.channel_id))
if (d.member !== undefined)
await guild?.members.set(d.member.user.id, d.member)
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)
}

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 { InviteCreatePayload } from '../../types/gateway.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 { InviteDeletePayload } from '../../types/gateway.ts'
import { PartialInvitePayload } from '../../types/invite.ts'
@ -19,7 +19,6 @@ export const inviteDelete: GatewayEventHandler = async (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cachedGuild = await gateway.client.guilds.get(d.guild_id!)
// TODO(DjDeveloperr): Make it support self-bots and make Guild not always defined
if (cachedInvite === undefined) {
const uncachedInvite: PartialInvitePayload = {
guild: (cachedGuild as unknown) as Guild,
@ -28,7 +27,7 @@ export const inviteDelete: GatewayEventHandler = async (
}
return gateway.client.emit('inviteDeleteUncached', uncachedInvite)
} else {
await guild.invites.delete(d.code)
await guild.invites._delete(d.code)
gateway.client.emit('inviteDelete', cachedInvite)
}
}

View File

@ -1,8 +1,8 @@
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 { MessagePayload } from '../../types/channel.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { MessagePayload } from '../../types/channel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const messageCreate: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,6 +1,6 @@
import { TextChannel } from '../../structures/textChannel.ts'
import { MessageDeletePayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
import type { MessageDeletePayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const messageDelete: GatewayEventHandler = async (
gateway: Gateway,
@ -15,6 +15,6 @@ export const messageDelete: GatewayEventHandler = async (
const message = await channel.messages.get(d.id)
if (message === undefined)
return gateway.client.emit('messageDeleteUncached', d)
await channel.messages.delete(d.id)
await channel.messages._delete(d.id)
gateway.client.emit('messageDelete', message)
}

View File

@ -1,14 +1,14 @@
import { Message } from '../../structures/message.ts'
import { GuildTextChannel } from '../../structures/textChannel.ts'
import { MessageDeleteBulkPayload } from '../../types/gateway.ts'
import type { Message } from '../../structures/message.ts'
import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
import type { MessageDeleteBulkPayload } from '../../types/gateway.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 (
gateway: Gateway,
d: MessageDeleteBulkPayload
) => {
let channel = await gateway.client.channels.get<GuildTextChannel>(
let channel = await gateway.client.channels.get<GuildTextBasedChannel>(
d.channel_id
)
// Fetch the channel if not cached
@ -16,7 +16,7 @@ export const messageDeleteBulk: GatewayEventHandler = async (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
channel = (await gateway.client.channels.fetch(
d.channel_id
)) as GuildTextChannel
)) as GuildTextBasedChannel
const messages = new Collection<string, Message>()
const uncached = new Set<string>()
@ -25,7 +25,7 @@ export const messageDeleteBulk: GatewayEventHandler = async (
if (message === undefined) uncached.add(id)
else {
messages.set(id, message)
await channel.messages.delete(id)
await channel.messages._delete(id)
}
}

View File

@ -1,8 +1,8 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { MessageReactionAddPayload } from '../../types/gateway.ts'
import { TextChannel } from '../../structures/textChannel.ts'
import { MessageReaction } from '../../structures/messageReaction.ts'
import { UserPayload } from '../../types/user.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import type { MessageReactionAddPayload } from '../../types/gateway.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
import type { MessageReaction } from '../../structures/messageReaction.ts'
import type { UserPayload } from '../../types/user.ts'
export const messageReactionAdd: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,6 +1,6 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { MessageReactionRemovePayload } from '../../types/gateway.ts'
import { TextChannel } from '../../structures/textChannel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import type { MessageReactionRemovePayload } from '../../types/gateway.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
export const messageReactionRemove: GatewayEventHandler = async (
gateway: Gateway,
@ -31,7 +31,7 @@ export const messageReactionRemove: GatewayEventHandler = async (
const reaction = await message.reactions.get(emojiID)
if (reaction === undefined) return
reaction.users.delete(d.user_id)
reaction.users._delete(d.user_id)
gateway.client.emit('messageReactionRemove', reaction, user)
}

View File

@ -1,6 +1,6 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { MessageReactionRemoveAllPayload } from '../../types/gateway.ts'
import { TextChannel } from '../../structures/textChannel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import type { MessageReactionRemoveAllPayload } from '../../types/gateway.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
export const messageReactionRemoveAll: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,6 +1,6 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts'
import { TextChannel } from '../../structures/textChannel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import type { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
export const messageReactionRemoveEmoji: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,7 +1,7 @@
import { Message } from '../../structures/message.ts'
import { TextChannel } from '../../structures/textChannel.ts'
import { MessagePayload } from '../../types/channel.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { Message } from '../../structures/message.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
import type { MessagePayload } from '../../types/channel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const messageUpdate: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,5 +1,9 @@
import { GatewayEventHandler } from '../index.ts'
import { GatewayEvents, TypingStartGuildData } from '../../types/gateway.ts'
import type { GatewayEventHandler } from '../mod.ts'
import type {
GatewayEvents,
MessageDeletePayload,
TypingStartGuildData
} from '../../types/gateway.ts'
import { channelCreate } from './channelCreate.ts'
import { channelDelete } from './channelDelete.ts'
import { channelUpdate } from './channelUpdate.ts'
@ -27,17 +31,18 @@ import { webhooksUpdate } from './webhooksUpdate.ts'
import { messageDeleteBulk } from './messageDeleteBulk.ts'
import { userUpdate } from './userUpdate.ts'
import { typingStart } from './typingStart.ts'
import { GuildTextChannel, TextChannel } from '../../structures/textChannel.ts'
import { Guild } from '../../structures/guild.ts'
import { User } from '../../structures/user.ts'
import { Emoji } from '../../structures/emoji.ts'
import { Member } from '../../structures/member.ts'
import { Role } from '../../structures/role.ts'
import { Message } from '../../structures/message.ts'
import { Collection } from '../../utils/collection.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
import type { Guild } from '../../structures/guild.ts'
import type { User } from '../../structures/user.ts'
import type { Emoji } from '../../structures/emoji.ts'
import type { Member } from '../../structures/member.ts'
import type { Role } from '../../structures/role.ts'
import type { Message } from '../../structures/message.ts'
import type { Collection } from '../../utils/collection.ts'
import { voiceServerUpdate } from './voiceServerUpdate.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 { messageReactionRemove } from './messageReactionRemove.ts'
import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts'
@ -46,20 +51,32 @@ import { guildMembersChunk } from './guildMembersChunk.ts'
import { presenceUpdate } from './presenceUpdate.ts'
import { inviteCreate } from './inviteCreate.ts'
import { inviteDelete } from './inviteDelete.ts'
import { MessageReaction } from '../../structures/messageReaction.ts'
import { Invite } from '../../structures/invite.ts'
import { Presence } from '../../structures/presence.ts'
import {
import type { MessageReaction } from '../../structures/messageReaction.ts'
import type { Invite } from '../../structures/invite.ts'
import type { Presence } from '../../structures/presence.ts'
import type {
EveryChannelTypes,
EveryTextChannelTypes
} from '../../utils/getChannelByType.ts'
} from '../../utils/channel.ts'
import { interactionCreate } from './interactionCreate.ts'
import { Interaction } from '../../structures/slash.ts'
import type { Interaction } from '../../structures/interactions.ts'
import type { SlashCommandInteraction } from '../../structures/slash.ts'
import type { CommandContext } from '../../commands/command.ts'
import type { RequestMethods } from '../../rest/types.ts'
import type { PartialInvitePayload } from '../../types/invite.ts'
import type { GuildChannels } from '../../types/guild.ts'
import { applicationCommandCreate } from './applicationCommandCreate.ts'
import { applicationCommandDelete } from './applicationCommandDelete.ts'
import { applicationCommandUpdate } from './applicationCommandUpdate.ts'
import type { SlashCommand } from '../../interactions/slashCommand.ts'
export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
} = {
READY: ready,
APPLICATION_COMMAND_CREATE: applicationCommandCreate,
APPLICATION_COMMAND_DELETE: applicationCommandDelete,
APPLICATION_COMMAND_UPDATE: applicationCommandUpdate,
RECONNECT: reconnect,
RESUMED: resume,
CHANNEL_CREATE: channelCreate,
@ -105,13 +122,15 @@ export interface VoiceServerUpdateData {
guild: Guild
}
export interface ClientEvents {
/** All Client Events */
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ClientEvents = {
/** When Client has successfully connected to Discord */
ready: []
/** When a successful reconnect has been made */
reconnect: []
ready: [shard: number]
/** When a reconnect was requested by Discord */
reconnect: [shard: number]
/** When a successful session resume has been done */
resumed: []
resumed: [shard: number]
/**
* When a new Channel is created
* @param channel New Channel object
@ -154,6 +173,11 @@ export interface ClientEvents {
* @param guild The new Guild object
*/
guildCreate: [guild: Guild]
/**
* A Guild was successfully loaded.
* @param guild The Guild object
*/
guildLoaded: [guild: Guild]
/**
* A Guild in which Client was either deleted, or bot was kicked
* @param guild The Guild object
@ -164,25 +188,28 @@ export interface ClientEvents {
* @param guild Guild in which Emoji was added
* @param emoji The Emoji which was added
*/
guildEmojiAdd: [guild: Guild, emoji: Emoji]
guildEmojiAdd: [emoji: Emoji]
/**
* An Emoji was deleted from Guild
* @param guild Guild from which Emoji was deleted
* @param emoji Emoji which was deleted
*/
guildEmojiDelete: [Guild, Emoji]
guildEmojiDelete: [emoji: Emoji]
/**
* An Emoji in a Guild was updated
* @param guild Guild in which Emoji was updated
* @param before Emoji object before update
* @param after Emoji object after update
*/
guildEmojiUpdate: [guild: Guild, before: Emoji, after: Emoji]
guildEmojiUpdate: [before: Emoji, after: Emoji]
/**
* Guild's Integrations were updated
* @param guild The Guild object
*/
guildIntegrationsUpdate: [guild: Guild]
/**
* Guild's Emojis were updated
* @param guild The Guild object
*/
guildEmojisUpdate: [guild: Guild]
/**
* A new Member has joined a Guild
* @param member The Member object
@ -238,7 +265,7 @@ export interface ClientEvents {
* @param uncached Set of Messages deleted's IDs which were not cached
*/
messageDeleteBulk: [
channel: GuildTextChannel,
channel: GuildTextBasedChannel,
messages: Collection<string, Message>,
uncached: Set<string>
]
@ -331,16 +358,71 @@ export interface ClientEvents {
* @param guild Guild in which Webhooks were updated
* @param channel Channel of which Webhooks were updated
*/
webhooksUpdate: [guild: Guild, channel: GuildTextChannel]
webhooksUpdate: [guild: Guild, channel: GuildTextBasedChannel]
/**
* An Interaction was created
* @param interaction Created interaction object
*/
interactionCreate: [interaction: Interaction]
interactionCreate: [interaction: Interaction | SlashCommandInteraction]
/**
* When debug message was made
* @param message Debug message
*/
debug: [message: string]
/**
* Raw event which gives you access to raw events DISPATCH'd from Gateway
* @param evt Event name string
* @param payload Payload JSON of the event
*/
raw: [evt: string, payload: any]
/**
* An uncached Message was deleted.
* @param payload Message Delete Payload
*/
messageDeleteUncached: [payload: MessageDeletePayload]
guildMembersChunk: [
guild: Guild,
info: {
chunkIndex: number
chunkCount: number
members: string[]
presences: string[] | undefined
}
]
guildMembersChunked: [guild: Guild, chunks: number]
rateLimit: [
data: {
method: RequestMethods
path: string
global: boolean
timeout: number
limit: number
}
]
inviteDeleteUncached: [invite: PartialInvitePayload]
voiceStateRemoveUncached: [data: { guild: Guild; member: Member }]
userUpdateUncached: [user: User]
webhooksUpdateUncached: [guild: Guild, channelID: string]
guildRoleUpdateUncached: [role: Role]
guildMemberUpdateUncached: [member: Member]
guildMemberRemoveUncached: [member: Member]
channelUpdateUncached: [channel: GuildChannels]
slashCommandCreate: [cmd: SlashCommand]
slashCommandUpdate: [cmd: SlashCommand]
slashCommandDelete: [cmd: SlashCommand]
commandOwnerOnly: [ctx: CommandContext]
commandGuildOnly: [ctx: CommandContext]
commandDmOnly: [ctx: CommandContext]
commandNSFW: [ctx: CommandContext]
commandBotMissingPermissions: [ctx: CommandContext, missing: string[]]
commandUserMissingPermissions: [ctx: CommandContext, missing: string[]]
commandMissingArgs: [ctx: CommandContext]
commandUsed: [ctx: CommandContext]
commandError: [ctx: CommandContext, err: Error]
gatewayError: [err: ErrorEvent, shards: [number, number]]
}

View File

@ -1,5 +1,5 @@
import { PresenceUpdatePayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { PresenceUpdatePayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const presenceUpdate: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,12 +1,19 @@
import { User } from '../../structures/user.ts'
import { Ready } from '../../types/gateway.ts'
import { GuildPayload } from '../../types/guild.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { Ready } from '../../types/gateway.ts'
import type { GuildPayload } from '../../types/guild.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const ready: GatewayEventHandler = async (
gateway: Gateway,
d: Ready
) => {
gateway.client.upSince = new Date()
if ('application' in d) {
gateway.client.applicationID = d.application.id
gateway.client.applicationFlags = d.application.flags
}
await gateway.client.guilds.flush()
await gateway.client.users.set(d.user.id, d.user)
@ -19,5 +26,5 @@ export const ready: GatewayEventHandler = async (
gateway.client.guilds.set(guild.id, guild)
})
gateway.client.emit('ready')
gateway.client.emit('ready', gateway.shards?.[0] ?? 0)
}

View File

@ -1,8 +1,9 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const reconnect: GatewayEventHandler = async (
gateway: Gateway,
d: any
) => {
gateway.client.emit('reconnect', gateway.shards?.[0] ?? 0)
gateway.reconnect()
}

View File

@ -1,18 +1,18 @@
import { User } from '../../structures/user.ts'
import { CLIENT_USER } from '../../types/endpoint.ts'
import { Resume } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { Resume } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const resume: GatewayEventHandler = async (
gateway: Gateway,
d: Resume
) => {
gateway.debug(`Session Resumed!`)
gateway.client.emit('resume')
gateway.client.emit('resumed', gateway.shards?.[0] ?? 0)
if (gateway.client.user === undefined)
gateway.client.user = new User(
gateway.client,
await gateway.client.rest.get(CLIENT_USER())
)
gateway.client.emit('ready')
gateway.client.emit('ready', gateway.shards?.[0] ?? 0)
}

View File

@ -1,7 +1,7 @@
import { Member } from '../../structures/member.ts'
import { TextChannel } from '../../structures/textChannel.ts'
import { TypingStartPayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { TextChannel } from '../../structures/textChannel.ts'
import type { TypingStartPayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
// TODO: Do we need to add uncached events here?
export const typingStart: GatewayEventHandler = async (

View File

@ -1,6 +1,6 @@
import { User } from '../../structures/user.ts'
import { UserPayload } from '../../types/user.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { User } from '../../structures/user.ts'
import type { UserPayload } from '../../types/user.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const userUpdate: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,6 +1,6 @@
import { Guild } from '../../structures/guild.ts'
import { VoiceServerUpdatePayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { Guild } from '../../structures/guild.ts'
import type { VoiceServerUpdatePayload } from '../../types/gateway.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const voiceServerUpdate: GatewayEventHandler = async (
gateway: Gateway,

View File

@ -1,8 +1,8 @@
import { Guild } from '../../structures/guild.ts'
import { VoiceState } from '../../structures/voiceState.ts'
import { MemberPayload } from '../../types/guild.ts'
import { VoiceStatePayload } from '../../types/voice.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
import type { Guild } from '../../structures/guild.ts'
import type { VoiceState } from '../../structures/voiceState.ts'
import type { MemberPayload } from '../../types/guild.ts'
import type { VoiceStatePayload } from '../../types/voice.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
export const voiceStateUpdate: GatewayEventHandler = async (
gateway: Gateway,
@ -24,7 +24,7 @@ export const voiceStateUpdate: GatewayEventHandler = async (
return gateway.client.emit('voiceStateRemoveUncached', { guild, member })
}
// No longer in the channel, so delete
await guild.voiceStates.delete(d.user_id)
await guild.voiceStates._delete(d.user_id)
gateway.client.emit(
'voiceStateRemove',
(voiceState as unknown) as VoiceState
@ -33,17 +33,15 @@ export const voiceStateUpdate: GatewayEventHandler = async (
}
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) {
gateway.client.emit(
'voiceStateAdd',
(newVoiceState as unknown) as VoiceState
)
gateway.client.emit('voiceStateAdd', newVoiceState)
} else {
gateway.client.emit(
'voiceStateUpdate',
voiceState,
(newVoiceState as unknown) as VoiceState
)
gateway.client.emit('voiceStateUpdate', voiceState, newVoiceState)
}
}

View File

@ -1,7 +1,7 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { Guild } from '../../structures/guild.ts'
import { WebhooksUpdatePayload } from '../../types/gateway.ts'
import { GuildTextChannel } from '../../structures/textChannel.ts'
import type { Gateway, GatewayEventHandler } from '../mod.ts'
import type { Guild } from '../../structures/guild.ts'
import type { WebhooksUpdatePayload } from '../../types/gateway.ts'
import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
export const webhooksUpdate: GatewayEventHandler = async (
gateway: Gateway,
@ -10,9 +10,9 @@ export const webhooksUpdate: GatewayEventHandler = async (
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
if (guild === undefined) return
const channel: GuildTextChannel | undefined = (await guild.channels.get(
const channel: GuildTextBasedChannel | undefined = (await guild.channels.get(
d.channel_id
)) as GuildTextChannel
)) as GuildTextBasedChannel
if (channel === undefined)
gateway.client.emit('webhooksUpdateUncached', guild, d.channel_id)
else gateway.client.emit('webhooksUpdate', guild, channel)

View File

@ -1,23 +1,21 @@
import { unzlib, EventEmitter } from '../../deps.ts'
import { Client } from '../models/client.ts'
import {
DISCORD_GATEWAY_URL,
DISCORD_API_VERSION
} from '../consts/urlsAndVersions.ts'
import { unzlib } from '../../deps.ts'
import type { Client } from '../client/mod.ts'
import { GatewayResponse } from '../types/gatewayResponse.ts'
import {
GatewayOpcodes,
GatewayIntents,
GatewayCloseCodes,
IdentityPayload,
StatusUpdatePayload
StatusUpdatePayload,
GatewayEvents
} from '../types/gateway.ts'
import { gatewayHandlers } from './handlers/index.ts'
import { GATEWAY_BOT } from '../types/endpoint.ts'
import { gatewayHandlers } from './handlers/mod.ts'
import { GatewayCache } from '../managers/gatewayCache.ts'
import { delay } from '../utils/delay.ts'
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import { Guild } from '../structures/guild.ts'
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import type { Guild } from '../structures/guild.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
import { decodeText } from '../utils/encoding.ts'
import { Constants } from '../types/constants.ts'
export interface RequestMembersOptions {
limit?: number
@ -33,15 +31,29 @@ export interface VoiceStateOptions {
export const RECONNECT_REASON = 'harmony-reconnect'
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type GatewayTypedEvents = {
[name in GatewayEvents]: [any]
} & {
connect: []
ping: [number]
resume: []
reconnectRequired: []
close: [number, string]
error: [Error, ErrorEvent]
sentIdentify: []
sentResume: []
reconnecting: []
init: []
}
/**
* Handles Discord Gateway connection.
*
* You should not use this and rather use Client class.
*/
export class Gateway extends EventEmitter {
websocket: WebSocket
token: string
intents: GatewayIntents[]
export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
websocket?: WebSocket
connected = false
initialized = false
heartbeatInterval = 0
@ -50,26 +62,17 @@ export class Gateway extends EventEmitter {
lastPingTimestamp = 0
sessionID?: string
private heartbeatServerResponded = false
client: Client
client!: Client
cache: GatewayCache
private timedIdentify: number | null = null
shards?: number[]
ping: number = 0
constructor(client: Client, token: string, intents: GatewayIntents[]) {
constructor(client: Client, shards?: number[]) {
super()
this.token = token
this.intents = intents
this.client = client
Object.defineProperty(this, 'client', { value: client, enumerable: false })
this.cache = new GatewayCache(client)
this.websocket = new WebSocket(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
[]
)
this.websocket.binaryType = 'arraybuffer'
this.websocket.onopen = this.onopen.bind(this)
this.websocket.onmessage = this.onmessage.bind(this)
this.websocket.onclose = this.onclose.bind(this)
this.websocket.onerror = this.onerror.bind(this)
this.shards = shards
}
private onopen(): void {
@ -85,7 +88,7 @@ export class Gateway extends EventEmitter {
}
if (data instanceof Uint8Array) {
data = unzlib(data)
data = new TextDecoder('utf-8').decode(data)
data = decodeText(data)
}
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
@ -104,7 +107,7 @@ export class Gateway extends EventEmitter {
if (!this.initialized) {
this.initialized = true
await this.sendIdentify(this.client.forceNewSession)
this.enqueueIdentify(this.client.forceNewSession)
} else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendResume()
@ -113,11 +116,9 @@ export class Gateway extends EventEmitter {
case GatewayOpcodes.HEARTBEAT_ACK:
this.heartbeatServerResponded = true
this.client.ping = Date.now() - this.lastPingTimestamp
this.emit('ping', this.client.ping)
this.debug(
`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`
)
this.ping = Date.now() - this.lastPingTimestamp
this.emit('ping', this.ping)
this.debug(`Received Heartbeat Ack. Ping Recognized: ${this.ping}ms`)
break
case GatewayOpcodes.INVALID_SESSION:
@ -127,14 +128,14 @@ export class Gateway extends EventEmitter {
)
if (d !== true) {
this.debug(`Session was invalidated, deleting from cache`)
await this.cache.delete('session_id')
await this.cache.delete('seq')
await this.cache.delete(`session_id_${this.shards?.join('-') ?? '0'}`)
await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`)
this.sessionID = undefined
this.sequenceID = undefined
}
this.timedIdentify = setTimeout(async () => {
this.timedIdentify = null
await this.sendIdentify(!(d as boolean))
this.enqueueIdentify(!(d as boolean))
}, 5000)
break
@ -142,15 +143,15 @@ export class Gateway extends EventEmitter {
this.heartbeatServerResponded = true
if (s !== null) {
this.sequenceID = s
await this.cache.set('seq', s)
await this.cache.set(`seq_${this.shards?.join('-') ?? '0'}`, s)
}
if (t !== null && t !== undefined) {
this.emit(t, d)
this.emit(t as any, d)
this.client.emit('raw', t, d)
const handler = gatewayHandlers[t]
if (handler !== undefined) {
if (handler !== undefined && d !== null) {
handler(this, d)
}
}
@ -160,15 +161,18 @@ export class Gateway extends EventEmitter {
// this.token = d.token
this.sessionID = d.session_id
this.sequenceID = d.seq
await this.cache.set('seq', d.seq)
await this.cache.set('session_id', this.sessionID)
await this.cache.set(`seq_${this.shards?.join('-') ?? '0'}`, d.seq)
await this.cache.set(
`session_id_${this.shards?.join('-') ?? '0'}`,
this.sessionID
)
this.emit('resume')
break
}
case GatewayOpcodes.RECONNECT: {
this.emit('reconnectRequired')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
this.debug('Received OpCode RECONNECT')
await this.reconnect()
break
}
default:
@ -184,11 +188,12 @@ export class Gateway extends EventEmitter {
switch (code) {
case GatewayCloseCodes.UNKNOWN_ERROR:
this.debug('API has encountered Unknown Error. Reconnecting...')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
await this.reconnect()
break
case GatewayCloseCodes.UNKNOWN_OPCODE:
throw new Error("Unknown OP Code was sent. This shouldn't happen!")
throw new Error(
"Invalid OP Code or Payload was sent. This shouldn't happen!"
)
case GatewayCloseCodes.DECODE_ERROR:
throw new Error("Invalid Payload was sent. This shouldn't happen!")
case GatewayCloseCodes.NOT_AUTHENTICATED:
@ -197,20 +202,17 @@ export class Gateway extends EventEmitter {
throw new Error('Invalid Token provided!')
case GatewayCloseCodes.INVALID_SEQ:
this.debug('Invalid Seq was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
await this.reconnect()
break
case GatewayCloseCodes.RATE_LIMITED:
throw new Error("You're ratelimited. Calm down.")
case GatewayCloseCodes.SESSION_TIMED_OUT:
this.debug('Session Timeout. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect(true)
await this.reconnect(true)
break
case GatewayCloseCodes.INVALID_SHARD:
this.debug('Invalid Shard was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
await this.reconnect()
break
case GatewayCloseCodes.SHARDING_REQUIRED:
throw new Error("Couldn't connect. Sharding is required!")
@ -224,37 +226,49 @@ export class Gateway extends EventEmitter {
this.debug(
'Unknown Close code, probably connection error. Reconnecting in 5s.'
)
if (this.timedIdentify !== null) {
clearTimeout(this.timedIdentify)
this.debug('Timed Identify found. Cleared timeout.')
}
await delay(5000)
await this.reconnect(true)
break
}
}
private onerror(event: Event | ErrorEvent): void {
const eventError = event as ErrorEvent
this.emit('error', eventError)
private async onerror(event: ErrorEvent): Promise<void> {
const error = new Error(
Deno.inspect({
message: event.message,
error: event.error,
type: event.type,
target: event.target
})
)
error.name = 'ErrorEvent'
console.log(error)
this.emit('error', error, event)
this.client.emit('gatewayError', event, this.shards)
}
private enqueueIdentify(forceNew?: boolean): void {
this.client.shards.enqueueIdentify(
async () => await this.sendIdentify(forceNew)
)
}
private async sendIdentify(forceNewSession?: boolean): Promise<void> {
this.debug('Fetching /gateway/bot...')
const info = await this.client.rest.get(GATEWAY_BOT())
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 (typeof this.client.token !== 'string')
throw new Error('Token not specified')
if (typeof this.client.intents !== 'object')
throw new Error('Intents not specified')
if (forceNewSession === undefined || !forceNewSession) {
const sessionIDCached = await this.cache.get('session_id')
const sessionIDCached = await this.cache.get(
`session_id_${this.shards?.join('-') ?? '0'}`
)
if (sessionIDCached !== undefined) {
this.debug(`Found Cached SessionID: ${sessionIDCached}`)
this.sessionID = sessionIDCached
@ -263,15 +277,18 @@ export class Gateway extends EventEmitter {
}
const payload: IdentityPayload = {
token: this.token,
token: this.client.token,
properties: {
$os: this.client.clientProperties.os ?? Deno.build.os,
$browser: this.client.clientProperties.browser ?? 'harmony',
$device: this.client.clientProperties.device ?? 'harmony'
},
compress: true,
shard: [0, 1], // TODO: Make sharding possible
intents: this.intents.reduce(
shard:
this.shards === undefined
? [0, 1]
: [this.shards[0] ?? 0, this.shards[1] ?? 1],
intents: this.client.intents.reduce(
(previous, current) => previous | current,
0
),
@ -287,20 +304,27 @@ export class Gateway extends EventEmitter {
}
private async sendResume(): Promise<void> {
if (typeof this.client.token !== 'string')
throw new Error('Token not specified')
if (this.sessionID === undefined) {
this.sessionID = await this.cache.get('session_id')
if (this.sessionID === undefined) return await this.sendIdentify()
this.sessionID = await this.cache.get(
`session_id_${this.shards?.join('-') ?? '0'}`
)
if (this.sessionID === undefined) return this.enqueueIdentify()
}
this.debug(`Preparing to resume with Session: ${this.sessionID}`)
if (this.sequenceID === undefined) {
const cached = await this.cache.get('seq')
const cached = await this.cache.get(
`seq_${this.shards?.join('-') ?? '0'}`
)
if (cached !== undefined)
this.sequenceID = typeof cached === 'string' ? parseInt(cached) : cached
}
const resumePayload = {
op: GatewayOpcodes.RESUME,
d: {
token: this.token,
token: this.client.token,
session_id: this.sessionID,
seq: this.sequenceID ?? null
}
@ -320,8 +344,8 @@ export class Gateway extends EventEmitter {
op: GatewayOpcodes.REQUEST_GUILD_MEMBERS,
d: {
guild_id: guild,
query: options.query,
limit: options.limit,
query: options.query ?? '',
limit: options.limit ?? 0,
presences: options.presences,
user_ids: options.users,
nonce
@ -345,8 +369,18 @@ export class Gateway extends EventEmitter {
: typeof channel === 'string'
? channel
: channel?.id,
self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute,
self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf
self_mute:
channel === undefined
? false
: voiceOptions.mute === undefined
? false
: voiceOptions.mute,
self_deaf:
channel === undefined
? false
: voiceOptions.deaf === undefined
? false
: voiceOptions.deaf
}
})
}
@ -357,12 +391,15 @@ export class Gateway extends EventEmitter {
async reconnect(forceNew?: boolean): Promise<void> {
this.emit('reconnecting')
this.debug('Reconnecting... (force new: ' + String(forceNew) + ')')
clearInterval(this.heartbeatIntervalID)
if (forceNew === true) {
await this.cache.delete('session_id')
await this.cache.delete('seq')
await this.cache.delete(`session_id_${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()
}
@ -371,30 +408,34 @@ export class Gateway extends EventEmitter {
this.debug('Initializing WebSocket...')
this.websocket = new WebSocket(
// 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.onopen = this.onopen.bind(this)
this.websocket.onmessage = this.onmessage.bind(this)
this.websocket.onclose = this.onclose.bind(this)
this.websocket.onerror = this.onerror.bind(this)
this.websocket.onerror = this.onerror.bind(this) as any
}
close(code: number = 1000, reason?: string): void {
return this.websocket.close(code, reason)
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)
}
send(data: GatewayResponse): boolean {
if (this.websocket.readyState !== this.websocket.OPEN) return false
this.websocket.send(
JSON.stringify({
op: data.op,
d: data.d,
s: typeof data.s === 'number' ? data.s : null,
t: data.t === undefined ? null : data.t
})
)
if (this.websocket?.readyState !== this.websocket?.OPEN) return false
const packet = JSON.stringify({
op: data.op,
d: data.d,
s: typeof data.s === 'number' ? data.s : null,
t: data.t === undefined ? null : data.t
})
this.websocket?.send(packet)
return true
}

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,24 +1,14 @@
import { Guild } from '../structures/guild.ts'
import { Interaction } from '../structures/slash.ts'
import { RESTManager } from '../rest/manager.ts'
import type { Guild } from '../structures/guild.ts'
import {
InteractionType,
SlashCommandChoice,
SlashCommandOption,
SlashCommandOptionType,
SlashCommandPartial,
SlashCommandPayload
} from '../types/slash.ts'
} from '../types/slashCommands.ts'
import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts'
import { RESTManager } from './rest.ts'
import { SlashModule } from './slashModule.ts'
import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts'
import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts'
import {
Request as ORequest,
Response as OResponse
} from 'https://deno.land/x/opine@1.0.0/src/types.ts'
import { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts'
import type { SlashClient, SlashCommandHandlerCallback } from './slashClient.ts'
export class SlashCommand {
slash: SlashCommandsManager
@ -27,15 +17,21 @@ export class SlashCommand {
name: string
description: string
options: SlashCommandOption[]
guild?: Guild
_guild?: string
constructor(manager: SlashCommandsManager, data: SlashCommandPayload) {
constructor(
manager: SlashCommandsManager,
data: SlashCommandPayload,
guild?: Guild
) {
this.slash = manager
this.id = data.id
this.applicationID = data.application_id
this.name = data.name
this.description = data.description
this.options = data.options ?? []
this.guild = guild
}
async delete(): Promise<void> {
@ -154,6 +150,7 @@ function buildOptionsArray(
)
}
/** Slash Command Builder */
export class SlashBuilder {
data: SlashCommandPartial
@ -199,15 +196,17 @@ export class SlashBuilder {
}
}
/** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */
export class SlashCommandsManager {
slash: SlashClient
get rest(): RESTManager {
return this.slash.rest
}
readonly slash!: SlashClient
readonly rest!: RESTManager
constructor(client: SlashClient) {
this.slash = client
Object.defineProperty(this, 'slash', { value: client, enumerable: false })
Object.defineProperty(this, 'rest', {
enumerable: false,
value: client.rest
})
}
/** Get all Global Slash Commands */
@ -238,8 +237,13 @@ export class SlashCommandsManager {
].commands.get()) as SlashCommandPayload[]
if (!Array.isArray(res)) return col
const _guild =
typeof guild === 'object'
? guild
: await this.slash.client?.guilds.get(guild)
for (const raw of res) {
const cmd = new SlashCommand(this, raw)
const cmd = new SlashCommand(this, raw, _guild)
cmd._guild = typeof guild === 'string' ? guild : guild.id
col.set(raw.id, cmd)
}
@ -261,7 +265,14 @@ export class SlashCommandsManager {
const payload = await route.post(data)
const cmd = new SlashCommand(this, payload)
const _guild =
typeof guild === 'object'
? guild
: guild === undefined
? undefined
: await this.slash.client?.guilds.get(guild)
const cmd = new SlashCommand(this, payload, _guild)
cmd._guild =
typeof guild === 'string' || guild === undefined ? guild : guild.id
@ -312,238 +323,30 @@ export class SlashCommandsManager {
const data = await route.get()
return new SlashCommand(this, data)
}
}
const _guild =
typeof guild === 'object'
? guild
: guild === undefined
? undefined
: await this.slash.client?.guilds.get(guild)
export type SlashCommandHandlerCallback = (interaction: Interaction) => any
export interface SlashCommandHandler {
name: string
guild?: string
parent?: string
group?: string
handler: SlashCommandHandlerCallback
}
export interface SlashOptions {
id?: string | (() => string)
client?: Client
enabled?: boolean
token?: string
rest?: RESTManager
publicKey?: string
}
export class SlashClient {
id: string | (() => string)
client?: Client
token?: string
enabled: boolean = true
commands: SlashCommandsManager
handlers: SlashCommandHandler[] = []
rest: RESTManager
modules: SlashModule[] = []
publicKey?: string
_decoratedSlash?: Array<{
name: string
guild?: string
parent?: string
group?: string
handler: (interaction: Interaction) => any
}>
_decoratedSlashModules?: SlashModule[]
constructor(options: SlashOptions) {
let id = options.id
if (options.token !== undefined) id = atob(options.token?.split('.')[0])
if (id === undefined)
throw new Error('ID could not be found. Pass at least client or token')
this.id = id
this.client = options.client
this.token = options.token
this.commands = new SlashCommandsManager(this)
this.publicKey = options.publicKey
if (options !== undefined) {
this.enabled = options.enabled ?? true
}
if (this.client?._decoratedSlash !== undefined) {
this.client._decoratedSlash.forEach((e) => {
this.handlers.push(e)
})
}
if (this.client?._decoratedSlashModules !== undefined) {
this.client._decoratedSlashModules.forEach((e) => {
this.modules.push(e)
})
}
if (this._decoratedSlash !== undefined) {
this._decoratedSlash.forEach((e) => {
this.handlers.push(e)
})
}
if (this._decoratedSlashModules !== undefined) {
this._decoratedSlashModules.forEach((e) => {
this.modules.push(e)
})
}
this.rest =
options.client === undefined
? options.rest === undefined
? new RESTManager({
token: this.token
})
: options.rest
: options.client.rest
this.client?.on('interactionCreate', (interaction) =>
this._process(interaction)
)
return new SlashCommand(this, data, _guild)
}
getID(): string {
return typeof this.id === 'string' ? this.id : this.id()
}
/** Bulk Edit Global or Guild Slash Commands */
async bulkEdit(
cmds: Array<SlashCommandPartial | SlashCommandPayload>,
guild?: Guild | string
): Promise<SlashCommandsManager> {
const route =
guild === undefined
? this.rest.api.applications[this.slash.getID()].commands
: this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].commands
await route.put(cmds)
/** Adds a new Slash Command Handler */
handle(handler: SlashCommandHandler): SlashClient {
this.handlers.push(handler)
return this
}
loadModule(module: SlashModule): SlashClient {
this.modules.push(module)
return this
}
getHandlers(): SlashCommandHandler[] {
let res = this.handlers
for (const mod of this.modules) {
if (mod === undefined) continue
res = [
...res,
...mod.commands.map((cmd) => {
cmd.handler = cmd.handler.bind(mod)
return cmd
})
]
}
return res
}
private _getCommand(i: Interaction): SlashCommandHandler | undefined {
return this.getHandlers().find((e) => {
const hasGroupOrParent = e.group !== undefined || e.parent !== undefined
const groupMatched =
e.group !== undefined && e.parent !== undefined
? i.options
.find((o) => o.name === e.group)
?.options?.find((o) => o.name === e.name) !== undefined
: true
const subMatched =
e.group === undefined && e.parent !== undefined
? i.options.find((o) => o.name === e.name) !== undefined
: true
const nameMatched1 = e.name === i.name
const parentMatched = hasGroupOrParent ? e.parent === i.name : true
const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1
const matched = groupMatched && subMatched && nameMatched
return matched
})
}
/** Process an incoming Slash Command (interaction) */
private _process(interaction: Interaction): void {
if (!this.enabled) return
if (interaction.type !== InteractionType.APPLICATION_COMMAND) return
const cmd = this._getCommand(interaction)
if (cmd === undefined) return
cmd.handler(interaction)
}
async verifyKey(
rawBody: string | Uint8Array | Buffer,
signature: string,
timestamp: string
): Promise<boolean> {
if (this.publicKey === undefined)
throw new Error('Public Key is not present')
return edverify(
signature,
Buffer.concat([
Buffer.from(timestamp, 'utf-8'),
Buffer.from(
rawBody instanceof Uint8Array
? new TextDecoder().decode(rawBody)
: rawBody
)
]),
this.publicKey
).catch(() => false)
}
async verifyOpineRequest(req: ORequest): Promise<boolean> {
const signature = req.headers.get('x-signature-ed25519')
const timestamp = req.headers.get('x-signature-timestamp')
const contentLength = req.headers.get('content-length')
if (signature === null || timestamp === null || contentLength === null)
return false
const body = new Uint8Array(parseInt(contentLength))
await req.body.read(body)
const verified = await this.verifyKey(body, signature, timestamp)
if (!verified) return false
return true
}
/** Middleware to verify request in Opine framework. */
async verifyOpineMiddleware(
req: ORequest,
res: OResponse,
next: CallableFunction
): Promise<any> {
const verified = await this.verifyOpineRequest(req)
if (!verified) return res.setStatus(401).end()
await next()
return true
}
// TODO: create verifyOakMiddleware too
/** Method to verify Request from Oak server "Context". */
async verifyOakRequest(ctx: Context): Promise<any> {
const signature = ctx.request.headers.get('x-signature-ed25519')
const timestamp = ctx.request.headers.get('x-signature-timestamp')
const contentLength = ctx.request.headers.get('content-length')
if (
signature === null ||
timestamp === null ||
contentLength === null ||
ctx.request.hasBody !== true
) {
return false
}
const body = await ctx.request.body().value
const verified = await this.verifyKey(body as any, signature, timestamp)
if (!verified) return false
return true
}
}

View File

@ -1,13 +1,12 @@
import { SlashCommandHandler } from './slashClient.ts'
import type { SlashCommandHandler } from './slashClient.ts'
export class SlashModule {
name: string = ''
commands: SlashCommandHandler[] = []
_decoratedSlash?: SlashCommandHandler[]
constructor() {
if (this._decoratedSlash !== undefined) {
this.commands = this._decoratedSlash
if ((this as any)._decoratedSlash !== undefined) {
;(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'
/**
@ -6,15 +7,14 @@ import { Collection } from '../utils/collection.ts'
*
* You should not be making Managers yourself.
*/
export class BaseManager<T, T2> {
client: Client
export class BaseManager<T, T2> extends Base {
/** Caches Name or Key used to differentiate caches */
cacheName: string
/** Which data type does this cache have */
DataType: any
constructor(client: Client, cacheName: string, DataType: any) {
this.client = client
super(client)
this.cacheName = cacheName
this.DataType = DataType
}
@ -41,11 +41,6 @@ export class BaseManager<T, T2> {
return this.client.cache.delete(this.cacheName, key)
}
/** Alias to _delete (cache) for compatibility purposes */
async delete(key: string): Promise<boolean> {
return await this._delete(key)
}
/** Gets an Array of values from Cache */
async array(): Promise<T2[]> {
let arr = await (this.client.cache.array(this.cacheName) as T[])
@ -65,8 +60,35 @@ export class BaseManager<T, T2> {
return collection
}
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
const arr = (await this.array()) ?? []
const { readable, writable } = new TransformStream()
const writer = writable.getWriter()
arr.forEach((el: unknown) => writer.write(el))
writer.close()
yield* readable
}
async fetch(...args: unknown[]): Promise<T2 | undefined> {
return undefined
}
/** Try to get value from cache, if not found then fetch */
async resolve(key: string): Promise<T2 | undefined> {
const cacheValue = await this.get(key)
if (cacheValue !== undefined) return cacheValue
else {
const fetchValue = await this.fetch(key).catch(() => undefined)
if (fetchValue !== undefined) return fetchValue
}
}
/** Deletes everything from Cache */
flush(): any {
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 { BaseManager } from './base.ts'
/** Child Managers validate data from their parents i.e. from Managers */
export class BaseChildManager<T, T2> {
client: Client
export class BaseChildManager<T, T2> extends Base {
/** Parent Manager */
parent: BaseManager<T, T2>
constructor(client: Client, parent: BaseManager<T, T2>) {
this.client = client
super(client)
this.parent = parent
}
@ -39,4 +39,31 @@ export class BaseChildManager<T, T2> {
}
return collection
}
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
const arr = (await this.array()) ?? []
const { readable, writable } = new TransformStream()
const writer = writable.getWriter()
arr.forEach((el: unknown) => writer.write(el))
writer.close()
yield* readable
}
async fetch(...args: unknown[]): Promise<T2 | undefined> {
return this.parent.fetch(...args)
}
/** Try to get value from cache, if not found then fetch */
async resolve(key: string): Promise<T2 | undefined> {
const cacheValue = await this.get(key)
if (cacheValue !== undefined) return cacheValue
else {
const fetchValue = await this.fetch(key).catch(() => undefined)
if (fetchValue !== undefined) return fetchValue
}
}
[Deno.customInspect](): string {
return `ChildManager(${this.parent.cacheName})`
}
}

View File

@ -1,15 +1,40 @@
import { Client } from '../models/client.ts'
import { Client } from '../client/mod.ts'
import { Channel } from '../structures/channel.ts'
import { ChannelPayload, GuildChannelPayload } from '../types/channel.ts'
import { Embed } from '../structures/embed.ts'
import { Message } from '../structures/message.ts'
import type { TextChannel } from '../structures/textChannel.ts'
import type { User } from '../structures/user.ts'
import type {
ChannelPayload,
GuildChannelPayload,
MessageOptions
} from '../types/channel.ts'
import { CHANNEL } from '../types/endpoint.ts'
import getChannelByType from '../utils/getChannelByType.ts'
import getChannelByType from '../utils/channel.ts'
import { BaseManager } from './base.ts'
export type AllMessageOptions = MessageOptions | Embed
export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
constructor(client: Client) {
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
async get<T = Channel>(key: string): Promise<T | undefined> {
const data = await this._get(key)
@ -29,6 +54,7 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
const arr = await (this.client.cache.array(
this.cacheName
) as ChannelPayload[])
if (arr === undefined) return []
const result: any[] = []
for (const elem of arr) {
let guild
@ -65,4 +91,109 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
.catch((e) => reject(e))
})
}
async sendMessage(
channel: string | TextChannel,
content?: string | AllMessageOptions,
option?: AllMessageOptions
): Promise<Message> {
const channelID = typeof channel === 'string' ? channel : channel.id
if (typeof content === 'object') {
option = content
content = undefined
}
if (content === undefined && option === undefined) {
throw new Error('Either text or option is necessary.')
}
if (option instanceof Embed) {
option = {
embed: option
}
}
const payload: any = {
content: content ?? option?.content,
embed: option?.embed,
file: option?.file,
files: option?.files,
tts: option?.tts,
allowed_mentions: option?.allowedMentions,
message_reference:
option?.reply === undefined
? undefined
: typeof option.reply === 'string'
? {
message_id: option.reply
}
: typeof option.reply === 'object'
? option.reply instanceof Message
? {
message_id: option.reply.id,
channel_id: option.reply.channel.id,
guild_id: option.reply.guild?.id
}
: option.reply
: undefined
}
if (payload.content === undefined && payload.embed === undefined) {
payload.content = ''
}
const resp = await this.client.rest.api.channels[channelID].messages.post(
payload
)
const chan =
typeof channel === 'string'
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(await this.get<TextChannel>(channel))!
: channel
const res = new Message(this.client, resp, chan, this.client.user as any)
await res.mentions.fromPayload(resp)
return res
}
async editMessage(
channel: string | TextChannel,
message: Message | string,
text?: string | MessageOptions,
option?: MessageOptions
): Promise<Message> {
const channelID = typeof channel === 'string' ? channel : channel.id
if (text === undefined && option === undefined) {
throw new Error('Either text or option is necessary.')
}
if (this.client.user === undefined) {
throw new Error('Client user has not initialized.')
}
if (typeof text === 'object') {
if (typeof option === 'object') Object.assign(option, text)
else option = text
text = undefined
}
const newMsg = await this.client.rest.api.channels[channelID].messages[
typeof message === 'string' ? message : message.id
].patch({
content: text ?? option?.content,
embed: option?.embed !== undefined ? option.embed.toJSON() : undefined,
// Cannot upload new files with Message
// file: option?.file,
tts: option?.tts,
allowed_mentions: option?.allowedMentions
})
const chan =
typeof channel === 'string'
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(await this.get<TextChannel>(channel))!
: channel
const res = new Message(this.client, newMsg, chan, this.client.user)
await res.mentions.fromPayload(newMsg)
return res
}
}

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 { EmojiPayload } from '../types/emoji.ts'
import type { EmojiPayload } from '../types/emoji.ts'
import { GUILD_EMOJI } from '../types/endpoint.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

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,27 +1,33 @@
import { Client } from '../models/client.ts'
import type { Client } from '../client/mod.ts'
import { Channel } from '../structures/channel.ts'
import { Guild } from '../structures/guild.ts'
import { CategoryChannel } from '../structures/guildCategoryChannel.ts'
import { GuildTextChannel } from '../structures/textChannel.ts'
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import type { CategoryChannel } from '../structures/guildCategoryChannel.ts'
import {
GuildCategoryChannelPayload,
GuildTextChannelPayload,
GuildVoiceChannelPayload
ChannelTypes,
GuildChannelPayload,
OverwritePayload
} from '../types/channel.ts'
import { CHANNEL } from '../types/endpoint.ts'
import type { GuildChannels, GuildChannelPayloads } from '../types/guild.ts'
import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.ts'
import { BaseChildManager } from './baseChild.ts'
import { ChannelsManager } from './channels.ts'
import type { ChannelsManager } from './channels.ts'
export type GuildChannelPayloads =
| GuildTextChannelPayload
| GuildVoiceChannelPayload
| GuildCategoryChannelPayload
export type GuildChannel = GuildTextChannel | VoiceChannel | CategoryChannel
export interface CreateChannelOptions {
name: string
type?: ChannelTypes
topic?: string
bitrate?: number
userLimit?: number
rateLimitPerUser?: number
position?: number
permissionOverwrites?: OverwritePayload[]
parent?: CategoryChannel | string
nsfw?: boolean
}
export class GuildChannelsManager extends BaseChildManager<
GuildChannelPayloads,
GuildChannel
GuildChannels
> {
guild: Guild
@ -30,7 +36,7 @@ export class GuildChannelsManager extends BaseChildManager<
this.guild = guild
}
async get(id: string): Promise<GuildChannel | undefined> {
async get(id: string): Promise<GuildChannels | undefined> {
const res = await this.parent.get(id)
if (res !== undefined && res.guild.id === this.guild.id) return res
else return undefined
@ -41,7 +47,7 @@ export class GuildChannelsManager extends BaseChildManager<
return this.client.rest.delete(CHANNEL(id))
}
async array(): Promise<GuildChannel[]> {
async array(): Promise<GuildChannels[]> {
const arr = (await this.parent.array()) as Channel[]
return arr.filter(
(c: any) => c.guild !== undefined && c.guild.id === this.guild.id
@ -51,8 +57,51 @@ export class GuildChannelsManager extends BaseChildManager<
async flush(): Promise<boolean> {
const arr = await this.array()
for (const elem of arr) {
this.parent.delete(elem.id)
this.parent._delete(elem.id)
}
return true
}
/** Create a new Guild Channel */
async create(options: CreateChannelOptions): Promise<GuildChannels> {
if (options.name === undefined)
throw new Error('name is required for GuildChannelsManager#create')
const res = ((await this.client.rest.post(GUILD_CHANNELS(this.guild.id), {
name: options.name,
type: options.type,
topic: options.topic,
bitrate: options.bitrate,
user_limit: options.userLimit,
rate_limit_per_user: options.rateLimitPerUser,
position: options.position,
permission_overwrites: options.permissionOverwrites,
parent_id:
options.parent === undefined
? undefined
: typeof options.parent === 'object'
? options.parent.id
: options.parent,
nsfw: options.nsfw
})) as unknown) as GuildChannelPayload
await this.set(res.id, res)
const channel = await this.get(res.id)
return (channel as unknown) as GuildChannels
}
/** Modify the positions of a set of channel positions for the guild. */
async editPositions(
...positions: Array<{ id: string | GuildChannels; position: number | null }>
): Promise<GuildChannelsManager> {
if (positions.length === 0)
throw new Error('No channel positions to change specified')
await this.client.rest.api.guilds[this.guild.id].channels.patch(
positions.map((e) => ({
id: typeof e.id === 'string' ? e.id : e.id.id,
position: e.position ?? null
}))
)
return this
}
}

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 { Guild } from '../structures/guild.ts'
import type { Guild } from '../structures/guild.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 { BaseChildManager } from './baseChild.ts'
import { EmojisManager } from './emojis.ts'
import type { EmojisManager } from './emojis.ts'
import { fetchAuto } from '../../deps.ts'
export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
@ -88,7 +88,7 @@ export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
const arr = await this.array()
for (const elem of arr) {
const emojiID = elem.id !== null ? elem.id : elem.name
this.parent.delete(emojiID as string)
this.parent._delete(emojiID as string)
}
return true
}

View File

@ -1,9 +1,9 @@
import { Client } from '../models/client.ts'
import { Guild } from '../structures/guild.ts'
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import { User } from '../structures/user.ts'
import type { Client } from '../client/mod.ts'
import type { Guild } from '../structures/guild.ts'
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import type { User } from '../structures/user.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'
export class GuildVoiceStatesManager extends BaseManager<
@ -17,13 +17,20 @@ export class GuildVoiceStatesManager extends BaseManager<
this.guild = guild
}
/** Get Client's Voice State in the Guild */
async me(): Promise<VoiceState | undefined> {
const member = await this.guild.me()
return await this.get(member.id)
}
/** Get a Voice State by User ID */
async get(key: string): Promise<VoiceState | undefined> {
const raw = await this._get(key)
if (raw === undefined) return
const guild =
raw.guild_id === undefined
? undefined
? this.guild
: await this.client.guilds.get(raw.guild_id)
return new VoiceState(this.client, raw, {
@ -50,7 +57,7 @@ export class GuildVoiceStatesManager extends BaseManager<
arr.map(async (raw) => {
const guild =
raw.guild_id === undefined
? undefined
? this.guild
: await this.client.guilds.get(raw.guild_id)
return new VoiceState(this.client, raw, {

View File

@ -1,9 +1,24 @@
import { Client } from '../models/client.ts'
import { fetchAuto } from '../../deps.ts'
import type { Client } from '../client/mod.ts'
import { Guild } from '../structures/guild.ts'
import { GUILD } from '../types/endpoint.ts'
import { GuildPayload, MemberPayload } from '../types/guild.ts'
import type { Template } from '../structures/template.ts'
import { Role } from '../structures/role.ts'
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
import type {
GuildPayload,
MemberPayload,
GuildCreateRolePayload,
GuildCreatePayload,
GuildCreateChannelPayload,
GuildPreview,
GuildPreviewPayload,
GuildModifyOptions,
GuildModifyPayload,
GuildCreateOptions
} from '../types/guild.ts'
import { BaseManager } from './base.ts'
import { MembersManager } from './members.ts'
import { Emoji } from '../structures/emoji.ts'
export class GuildManager extends BaseManager<GuildPayload, Guild> {
constructor(client: Client) {
@ -15,7 +30,7 @@ export class GuildManager extends BaseManager<GuildPayload, Guild> {
this.client.rest
.get(GUILD(id))
.then(async (data: any) => {
this.set(id, data)
await this.set(id, data)
const guild = new Guild(this.client, data)
@ -32,4 +47,207 @@ export class GuildManager extends BaseManager<GuildPayload, Guild> {
.catch((e) => reject(e))
})
}
/** Create a new guild based on a template. */
async createFromTemplate(
template: Template | string,
name: string,
icon?: string
): Promise<Guild> {
if (icon?.startsWith('http') === true) icon = await fetchAuto(icon)
const guild = await this.client.rest.api.guilds.templates[
typeof template === 'object' ? template.code : template
].post({ name, icon })
return new Guild(this.client, guild)
}
/**
* Creates a guild. Returns Guild. Fires guildCreate event.
* @param options Options for creating a guild
*/
async create(options: GuildCreateOptions): Promise<Guild> {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (options.icon !== undefined && !options.icon.startsWith('data:')) {
options.icon = await fetchAuto(options.icon)
}
if (options.roles !== undefined && options.roles[0].name !== '@everyone') {
options.roles.unshift({
id: Math.floor(Math.random() * 18392375458).toString(),
name: '@everyone'
})
}
const body: GuildCreatePayload = {
name: options.name,
region: options.region,
icon: options.icon,
verification_level: options.verificationLevel,
roles: options.roles?.map((obj) => {
let result: GuildCreateRolePayload
if (obj instanceof Role) {
result = {
id: obj.id,
name: obj.name,
color: obj.color,
hoist: obj.hoist,
position: obj.position,
permissions: obj.permissions.bitfield.toString(),
managed: obj.managed,
mentionable: obj.mentionable
}
} else {
result = obj
}
return result
}),
channels: options.channels?.map(
(obj): GuildCreateChannelPayload => ({
id: obj.id,
name: obj.name,
type: obj.type,
parent_id: obj.parentID
})
),
afk_channel_id: options.afkChannelID,
afk_timeout: options.afkTimeout,
system_channel_id: options.systemChannelID
}
const result: GuildPayload = await this.client.rest.post(GUILDS(), body)
const guild = new Guild(this.client, result)
return guild
}
/**
* Gets a preview of a guild. Returns GuildPreview.
* @param guildID Guild id
*/
async preview(guildID: string): Promise<GuildPreview> {
const resp: GuildPreviewPayload = await this.client.rest.get(
GUILD_PREVIEW(guildID)
)
const result: GuildPreview = {
id: resp.id,
name: resp.name,
icon: resp.icon,
splash: resp.splash,
discoverySplash: resp.discovery_splash,
emojis: resp.emojis.map((emoji) => new Emoji(this.client, emoji)),
features: resp.features,
approximateMemberCount: resp.approximate_member_count,
approximatePresenceCount: resp.approximate_presence_count,
description: resp.description
}
return result
}
/** Sets a value to Cache */
async set(key: string, value: GuildPayload): Promise<any> {
value = { ...value }
if ('roles' in value) value.roles = []
if ('emojis' in value) value.emojis = []
if ('members' in value) value.members = []
if ('presences' in value) value.presences = []
if ('voice_states' in value) value.voice_states = []
return this.client.cache.set(this.cacheName, key, value)
}
/**
* Edits a guild. Returns edited guild.
* @param guild Guild or guild id
* @param options Guild edit options
* @param asRaw true for get raw data, false for get guild(defaults to false)
*/
async edit(
guild: Guild | string,
options: GuildModifyOptions,
asRaw: false
): Promise<Guild>
async edit(
guild: Guild | string,
options: GuildModifyOptions,
asRaw: true
): Promise<GuildPayload>
async edit(
guild: Guild | string,
options: GuildModifyOptions,
asRaw: boolean = false
): Promise<Guild | GuildPayload> {
if (
options.icon !== undefined &&
options.icon !== null &&
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
!options.icon.startsWith('data:')
) {
options.icon = await fetchAuto(options.icon)
}
if (
options.splash !== undefined &&
options.splash !== null &&
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
!options.splash.startsWith('data:')
) {
options.splash = await fetchAuto(options.splash)
}
if (
options.banner !== undefined &&
options.banner !== null &&
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
!options.banner.startsWith('data:')
) {
options.banner = await fetchAuto(options.banner)
}
if (guild instanceof Guild) {
guild = guild.id
}
const body: GuildModifyPayload = {
name: options.name,
region: options.region,
verification_level: options.verificationLevel,
default_message_notifications: options.defaultMessageNotifications,
explicit_content_filter: options.explicitContentFilter,
afk_channel_id: options.afkChannelID,
afk_timeout: options.afkTimeout,
owner_id: options.ownerID,
icon: options.icon,
splash: options.splash,
banner: options.banner,
system_channel_id: options.systemChannelID,
rules_channel_id: options.rulesChannelID,
public_updates_channel_id: options.publicUpdatesChannelID,
preferred_locale: options.preferredLocale
}
const result: GuildPayload = await this.client.rest.patch(
GUILD(guild),
body
)
if (asRaw) {
const guild = new Guild(this.client, result)
return guild
} else {
return result
}
}
/**
* Deletes a guild. Returns deleted guild.
* @param guild Guild or guild id
*/
async delete(guild: Guild | string): Promise<Guild | undefined> {
if (guild instanceof Guild) {
guild = guild.id
}
const oldGuild = await this.get(guild)
await this.client.rest.delete(GUILD(guild))
return oldGuild
}
}

View File

@ -1,10 +1,24 @@
import { Client } from '../models/client.ts'
import { Guild } from '../structures/guild.ts'
import type { GuildTextChannel, User } from '../../mod.ts'
import type { Client } from '../client/mod.ts'
import type { Guild } from '../structures/guild.ts'
import { Invite } from '../structures/invite.ts'
import { INVITE } from '../types/endpoint.ts'
import { InvitePayload } from '../types/invite.ts'
import { CHANNEL_INVITES, GUILD_INVITES, INVITE } from '../types/endpoint.ts'
import type { InvitePayload } from '../types/invite.ts'
import { BaseManager } from './base.ts'
export enum InviteTargetUserType {
STREAM = 1
}
export interface CreateInviteOptions {
maxAge?: number
maxUses?: number
temporary?: boolean
unique?: boolean
targetUser?: string | User
targetUserType?: InviteTargetUserType
}
export class InviteManager extends BaseManager<InvitePayload, Invite> {
guild: Guild
@ -20,10 +34,10 @@ export class InviteManager extends BaseManager<InvitePayload, Invite> {
}
/** Fetch an Invite */
async fetch(id: string): Promise<Invite> {
async fetch(id: string, withCounts: boolean = true): Promise<Invite> {
return await new Promise((resolve, reject) => {
this.client.rest
.get(INVITE(id))
.get(`${INVITE(id)}${withCounts ? '?with_counts=true' : ''}`)
.then(async (data) => {
this.set(id, data as InvitePayload)
const newInvite = await this.get(data.code)
@ -33,6 +47,57 @@ export class InviteManager extends BaseManager<InvitePayload, Invite> {
})
}
/** Fetch all Invites of a Guild or a specific Channel */
async fetchAll(channel?: string | GuildTextChannel): Promise<Invite[]> {
const rawInvites = (await this.client.rest.get(
channel === undefined
? GUILD_INVITES(this.guild.id)
: CHANNEL_INVITES(typeof channel === 'string' ? channel : channel.id)
)) as InvitePayload[]
const res: Invite[] = []
for (const raw of rawInvites) {
await this.set(raw.code, raw)
res.push(new Invite(this.client, raw))
}
return res
}
/** Delete an Invite */
async delete(invite: string | Invite): Promise<boolean> {
await this.client.rest.delete(
INVITE(typeof invite === 'string' ? invite : invite.code)
)
return true
}
/** Create an Invite */
async create(
channel: string | GuildTextChannel,
options?: CreateInviteOptions
): Promise<Invite> {
const raw = ((await this.client.rest.post(
CHANNEL_INVITES(typeof channel === 'string' ? channel : channel.id),
{
max_age: options?.maxAge,
max_uses: options?.maxUses,
temporary: options?.temporary,
unique: options?.unique,
target_user:
options?.targetUser === undefined
? undefined
: typeof options.targetUser === 'string'
? options.targetUser
: options.targetUser.id,
target_user_type: options?.targetUserType
}
)) as unknown) as InvitePayload
return new Invite(this.client, raw)
}
async fromPayload(invites: InvitePayload[]): Promise<boolean> {
for (const invite of invites) {
await this.set(invite.code, invite)

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 { RolePayload } from '../types/role.ts'
import type { RolePayload } from '../types/role.ts'
import { Role } from '../structures/role.ts'
import { Member } from '../structures/member.ts'
import { RolesManager } from './roles.ts'
import { MemberPayload } from '../types/guild.ts'
import type { Member } from '../structures/member.ts'
import type { RolesManager } from './roles.ts'
import type { MemberPayload } from '../types/guild.ts'
import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts'
export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
@ -42,7 +42,7 @@ export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
async flush(): Promise<boolean> {
const arr = await this.array()
for (const elem of arr) {
this.parent.delete(elem.id)
this.parent._delete(elem.id)
}
return true
}

View File

@ -1,9 +1,9 @@
import { User } from '../structures/user.ts'
import { Client } from '../models/client.ts'
import { Guild } from '../structures/guild.ts'
import type { Client } from '../client/mod.ts'
import type { Guild } from '../structures/guild.ts'
import { Member } from '../structures/member.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 { Permissions } from '../utils/permissions.ts'

View File

@ -1,10 +1,14 @@
import { Client } from '../models/client.ts'
import type { Client } from '../client/mod.ts'
import { Emoji } from '../structures/emoji.ts'
import { Guild } from '../structures/guild.ts'
import { Message } from '../structures/message.ts'
import type { Message } from '../structures/message.ts'
import { MessageReaction } from '../structures/messageReaction.ts'
import { Reaction } from '../types/channel.ts'
import { MESSAGE_REACTION, MESSAGE_REACTIONS } from '../types/endpoint.ts'
import type { User } from '../structures/user.ts'
import type { Reaction } from '../types/channel.ts'
import {
MESSAGE_REACTION,
MESSAGE_REACTIONS,
MESSAGE_REACTION_USER
} from '../types/endpoint.ts'
import { BaseManager } from './base.ts'
export class MessageReactionsManager extends BaseManager<
@ -14,10 +18,18 @@ export class MessageReactionsManager extends BaseManager<
message: Message
constructor(client: Client, message: Message) {
super(client, `reactions:${message.id}`, Guild)
super(client, `reactions:${message.id}`, MessageReaction)
this.message = message
}
async updateRefs(): Promise<void> {
const newVal = await this.message.channel.messages.get(this.message.id)
if (newVal !== undefined) {
this.message = newVal
}
await this.message.updateRefs()
}
async get(id: string): Promise<MessageReaction | undefined> {
const raw = await this._get(id)
if (raw === undefined) return
@ -27,6 +39,7 @@ export class MessageReactionsManager extends BaseManager<
let emoji = await this.client.emojis.get(emojiID as string)
if (emoji === undefined) emoji = new Emoji(this.client, raw.emoji)
await this.updateRefs()
const reaction = new MessageReaction(this.client, raw, this.message, emoji)
return reaction
}
@ -77,4 +90,23 @@ export class MessageReactionsManager extends BaseManager<
)
return this
}
/** Remove a specific Emoji from Reactions */
async removeUser(
emoji: Emoji | string,
user: User | string
): Promise<MessageReactionsManager> {
const val = encodeURIComponent(
(typeof emoji === 'object' ? emoji.id ?? emoji.name : emoji) as string
)
await this.client.rest.delete(
MESSAGE_REACTION_USER(
this.message.channel.id,
this.message.id,
val,
typeof user === 'string' ? user : user.id
)
)
return this
}
}

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 { TextChannel } from '../structures/textChannel.ts'
import type { TextChannel } from '../structures/textChannel.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 { BaseManager } from './base.ts'
@ -93,18 +93,12 @@ export class MessagesManager extends BaseManager<MessagePayload, Message> {
if (channel === undefined)
channel = await this.client.channels.fetch(this.channel.id)
const author = new User(this.client, (data as MessagePayload).author)
await this.client.users.set(
author.id,
data.author.id,
(data as MessagePayload).author
)
const res = new Message(
this.client,
data as MessagePayload,
channel as TextChannel,
author
)
const res = (await this.get(data.id)) as Message
await res.mentions.fromPayload(data)

View File

@ -1,8 +1,8 @@
import { Client } from '../models/client.ts'
import { Guild } from '../structures/guild.ts'
import type { Client } from '../client/mod.ts'
import type { Guild } from '../structures/guild.ts'
import { Presence } from '../structures/presence.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'
export class GuildPresencesManager extends BaseManager<

View File

@ -1,5 +1,6 @@
import { Client } from '../models/client.ts'
import { MessageReaction } from '../structures/messageReaction.ts'
import type { Client } from '../client/mod.ts'
import type { MessageReaction } from '../structures/messageReaction.ts'
import type { User } from '../structures/user.ts'
import { UsersManager } from './users.ts'
export class ReactionUsersManager extends UsersManager {
@ -10,4 +11,14 @@ export class ReactionUsersManager extends UsersManager {
this.cacheName = `reaction_users:${reaction.message.id}`
this.reaction = reaction
}
/** Remove all Users from this Reaction */
async removeAll(): Promise<void> {
await this.reaction.message.reactions.removeEmoji(this.reaction.emoji)
}
/** Remove a specific User from this Reaction */
async remove(user: User | string): Promise<void> {
await this.reaction.message.reactions.removeUser(this.reaction.emoji, user)
}
}

View File

@ -1,9 +1,9 @@
import { Permissions } from '../../mod.ts'
import { Client } from '../models/client.ts'
import { Guild } from '../structures/guild.ts'
import type { Client } from '../client/mod.ts'
import type { Guild } from '../structures/guild.ts'
import { Role } from '../structures/role.ts'
import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts'
import { RolePayload } from '../types/role.ts'
import type { RoleModifyPayload, RolePayload } from '../types/role.ts'
import { BaseManager } from './base.ts'
export interface CreateGuildRoleOptions {
@ -22,19 +22,34 @@ export class RolesManager extends BaseManager<RolePayload, Role> {
this.guild = guild
}
/** Fetch a Guild Role (from API) */
async fetch(id: string): Promise<Role> {
/** Fetch All Guild Roles */
async fetchAll(): Promise<Role[]> {
return await new Promise((resolve, reject) => {
this.client.rest
.get(GUILD_ROLE(this.guild.id, id))
.then(async (data) => {
await this.set(id, data as RolePayload)
resolve(((await this.get(id)) as unknown) as Role)
this.client.rest.api.guilds[this.guild.id].roles.get
.then(async (data: RolePayload[]) => {
const roles: Role[] = []
for (const raw of data) {
await this.set(raw.id, raw)
roles.push(new Role(this.client, raw, this.guild))
}
resolve(roles)
})
.catch((e) => reject(e))
})
}
async get(key: string): Promise<Role | undefined> {
const raw = await this._get(key)
if (raw === undefined) return
return new Role(this.client, raw, this.guild)
}
async array(): Promise<Role[]> {
let arr = await (this.client.cache.array(this.cacheName) as RolePayload[])
if (arr === undefined) arr = []
return arr.map((e) => new Role(this.client, e, this.guild))
}
async fromPayload(roles: RolePayload[]): Promise<boolean> {
for (const role of roles) {
await this.set(role.id, role)
@ -74,10 +89,41 @@ export class RolesManager extends BaseManager<RolePayload, Role> {
}
/** Delete a Guild Role */
async delete(role: Role | string): Promise<boolean> {
async delete(role: Role | string): Promise<Role | undefined> {
const oldRole = await this.get(typeof role === 'object' ? role.id : role)
await this.client.rest.delete(
GUILD_ROLE(this.guild.id, typeof role === 'object' ? role.id : role)
)
return true
return oldRole
}
async edit(role: Role | string, options: RoleModifyPayload): Promise<Role> {
if (role instanceof Role) {
role = role.id
}
const resp: RolePayload = await this.client.rest.patch(
GUILD_ROLE(this.guild.id, role),
options
)
return new Role(this.client, resp, this.guild)
}
/** Modify the positions of a set of role positions for the guild. */
async editPositions(
...positions: Array<{ id: string | Role; position: number | null }>
): Promise<RolesManager> {
if (positions.length === 0)
throw new Error('No role positions to change specified')
await this.client.rest.api.guilds[this.guild.id].roles.patch(
positions.map((e) => ({
id: typeof e.id === 'string' ? e.id : e.id.id,
position: e.position ?? null
}))
)
return this
}
}

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 '../types/endpoint.ts'
import { UserPayload } from '../types/user.ts'
import type { UserPayload } from '../types/user.ts'
import { BaseManager } from './base.ts'
export class UsersManager extends BaseManager<UserPayload, User> {

View File

@ -1,376 +0,0 @@
/* eslint-disable @typescript-eslint/method-signature-style */
import { User } from '../structures/user.ts'
import { GatewayIntents } from '../types/gateway.ts'
import { Gateway } from '../gateway/index.ts'
import { RESTManager } from './rest.ts'
import { EventEmitter } from '../../deps.ts'
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
import { UsersManager } from '../managers/users.ts'
import { GuildManager } from '../managers/guilds.ts'
import { ChannelsManager } from '../managers/channels.ts'
import { ClientPresence } from '../structures/presence.ts'
import { EmojisManager } from '../managers/emojis.ts'
import { ActivityGame, ClientActivity } from '../types/presence.ts'
import { Extension } from './extensions.ts'
import { SlashClient } from './slashClient.ts'
import { Interaction } from '../structures/slash.ts'
import { SlashModule } from './slashModule.ts'
import type { ShardManager } from './shard.ts'
import { Application } from '../structures/application.ts'
import { Invite } from '../structures/invite.ts'
import { INVITE } from '../types/endpoint.ts'
import { ClientEvents } from '../gateway/handlers/index.ts'
/** OS related properties sent with Gateway Identify */
export interface ClientProperties {
os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string
browser?: 'harmony' | string
device?: 'harmony' | string
}
/** Some Client Options to modify behaviour */
export interface ClientOptions {
/** ID of the Client/Application to initialize Slash Client REST */
id?: string
/** Token of the Bot/User */
token?: string
/** Gateway Intents */
intents?: GatewayIntents[]
/** Cache Adapter to use, defaults to Collections one */
cache?: ICacheAdapter
/** Force New Session and don't use cached Session (by persistent caching) */
forceNewSession?: boolean
/** Startup presence of client */
presence?: ClientPresence | ClientActivity | ActivityGame
/** Force all requests to Canary API */
canary?: boolean
/** Time till which Messages are to be cached, in MS. Default is 3600000 */
messageCacheLifetime?: number
/** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */
reactionCacheLifetime?: number
/** Whether to fetch Uncached Message of Reaction or not? */
fetchUncachedReactions?: boolean
/** Client Properties */
clientProperties?: ClientProperties
/** Enable/Disable Slash Commands Integration (enabled by default) */
enableSlash?: boolean
}
export declare interface Client {
on<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
on(event: string | symbol, listener: (...args: any[]) => void): this
once<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
once(event: string | symbol, listener: (...args: any[]) => void): this
emit<K extends keyof ClientEvents>(
event: K,
...args: ClientEvents[K]
): boolean
emit(event: string | symbol, ...args: any[]): boolean
off<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
off(event: string | symbol, listener: (...args: any[]) => void): this
}
/**
* Discord Client.
*/
export class Client extends EventEmitter {
/** Gateway object */
gateway?: Gateway
/** REST Manager - used to make all requests */
rest: RESTManager = new RESTManager(this)
/** User which Client logs in to, undefined until logs in */
user?: User
/** WebSocket ping of Client */
ping = 0
/** Token of the Bot/User */
token?: string
/** Cache Adapter */
cache: ICacheAdapter = new DefaultCacheAdapter()
/** Gateway Intents */
intents?: GatewayIntents[]
/** Whether to force new session or not */
forceNewSession?: boolean
/** Time till messages to stay cached, in MS. */
messageCacheLifetime: number = 3600000
/** Time till messages to stay cached, in MS. */
reactionCacheLifetime: number = 3600000
/** Whether to fetch Uncached Message of Reaction or not? */
fetchUncachedReactions: boolean = false
/** Client Properties */
clientProperties: ClientProperties
/** Slash-Commands Management client */
slash: SlashClient
users: UsersManager = new UsersManager(this)
guilds: GuildManager = new GuildManager(this)
channels: ChannelsManager = new ChannelsManager(this)
emojis: EmojisManager = new EmojisManager(this)
/** Whether the REST Manager will use Canary API or not */
canary: boolean = false
/** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence()
_decoratedEvents?: {
[name: string]: (...args: any[]) => void
}
_decoratedSlash?: Array<{
name: string
guild?: string
parent?: string
group?: string
handler: (interaction: Interaction) => any
}>
_decoratedSlashModules?: SlashModule[]
_id?: string
/** Shard on which this Client is */
shard: number = 0
/** Shard Manager of this Client if Sharded */
shardManager?: ShardManager
constructor(options: ClientOptions = {}) {
super()
this._id = options.id
this.token = options.token
this.intents = options.intents
this.forceNewSession = options.forceNewSession
if (options.cache !== undefined) this.cache = options.cache
if (options.presence !== undefined)
this.presence =
options.presence instanceof ClientPresence
? options.presence
: new ClientPresence(options.presence)
if (options.canary === true) this.canary = true
if (options.messageCacheLifetime !== undefined)
this.messageCacheLifetime = options.messageCacheLifetime
if (options.reactionCacheLifetime !== undefined)
this.reactionCacheLifetime = options.reactionCacheLifetime
if (options.fetchUncachedReactions === true)
this.fetchUncachedReactions = true
if (
this._decoratedEvents !== undefined &&
Object.keys(this._decoratedEvents).length !== 0
) {
Object.entries(this._decoratedEvents).forEach((entry) => {
this.on(entry[0], entry[1])
})
this._decoratedEvents = undefined
}
this.clientProperties =
options.clientProperties === undefined
? {
os: Deno.build.os,
browser: 'harmony',
device: 'harmony'
}
: options.clientProperties
this.slash = new SlashClient({
id: () => this.getEstimatedID(),
client: this,
enabled: options.enableSlash
})
}
/**
* Sets Cache Adapter
*
* Should NOT be set after bot is already logged in or using current cache.
* Please look into using `cache` option.
*/
setAdapter(adapter: ICacheAdapter): Client {
this.cache = adapter
return this
}
/** Changes Presence of Client */
setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void {
if (presence instanceof ClientPresence) {
this.presence = presence
} else this.presence = new ClientPresence(presence)
this.gateway?.sendPresence(this.presence.create())
}
/** Emits debug event */
debug(tag: string, msg: string): void {
this.emit('debug', `[${tag}] ${msg}`)
}
getEstimatedID(): string {
if (this.user !== undefined) return this.user.id
else if (this.token !== undefined) {
try {
return atob(this.token.split('.')[0])
} catch (e) {
return this._id ?? 'unknown'
}
} else {
return this._id ?? 'unknown'
}
}
/** Fetch Application of the Client */
async fetchApplication(): Promise<Application> {
const app = await this.rest.api.oauth2.applications['@me'].get()
return new Application(this, app)
}
/** Fetch an Invite */
async fetchInvite(id: string): Promise<Invite> {
return await new Promise((resolve, reject) => {
this.rest
.get(INVITE(id))
.then((data) => {
resolve(new Invite(this, data))
})
.catch((e) => reject(e))
})
}
/**
* This function is used for connecting to discord.
* @param token Your token. This is required.
* @param intents Gateway intents in array. This is required.
*/
connect(token?: string, intents?: GatewayIntents[]): void {
if (token === undefined && this.token !== undefined) token = this.token
else if (this.token === undefined && token !== undefined) {
this.token = token
} else throw new Error('No Token Provided')
if (intents !== undefined && this.intents !== undefined) {
this.debug(
'client',
'Intents were set in both client and connect function. Using the one in the connect function...'
)
} else if (intents === undefined && this.intents !== undefined) {
intents = this.intents
} else if (intents !== undefined && this.intents === undefined) {
this.intents = intents
} else throw new Error('No Gateway Intents were provided')
this.gateway = new Gateway(this, token, intents)
}
async waitFor<K extends keyof ClientEvents>(
event: K,
checkFunction: (...args: ClientEvents[K]) => boolean,
timeout?: number
): Promise<ClientEvents[K] | []> {
return await new Promise((resolve) => {
let timeoutID: number | undefined
if (timeout !== undefined) {
timeoutID = setTimeout(() => {
this.off(event, eventFunc)
resolve([])
}, timeout)
}
const eventFunc = (...args: ClientEvents[K]): void => {
if (checkFunction(...args)) {
resolve(args)
this.off(event, eventFunc)
if (timeoutID !== undefined) clearTimeout(timeoutID)
}
}
this.on(event, eventFunc)
})
}
}
export function event(name?: keyof ClientEvents) {
return function (client: Client | Extension, prop: keyof ClientEvents) {
const listener = ((client as unknown) as {
[name in keyof ClientEvents]: (...args: ClientEvents[name]) => any
})[prop]
if (typeof listener !== 'function')
throw new Error('@event decorator requires a function')
if (client._decoratedEvents === undefined) client._decoratedEvents = {}
const key = name === undefined ? prop : name
client._decoratedEvents[key] = listener
}
}
/** Decorator to create a Slash Command handler */
export function slash(name?: string, guild?: string) {
return function (client: Client | SlashModule, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = []
const item = (client as { [name: string]: any })[prop]
if (typeof item !== 'function') {
client._decoratedSlash.push(item)
} else
client._decoratedSlash.push({
name: name ?? prop,
guild,
handler: item
})
}
}
/** Decorator to create a Sub-Slash Command handler */
export function subslash(parent: string, name?: string, guild?: string) {
return function (client: Client | SlashModule, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = []
const item = (client as { [name: string]: any })[prop]
if (typeof item !== 'function') {
item.parent = parent
client._decoratedSlash.push(item)
} else
client._decoratedSlash.push({
parent,
name: name ?? prop,
guild,
handler: item
})
}
}
/** Decorator to create a Grouped Slash Command handler */
export function groupslash(
parent: string,
group: string,
name?: string,
guild?: string
) {
return function (client: Client | SlashModule | SlashClient, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = []
const item = (client as { [name: string]: any })[prop]
if (typeof item !== 'function') {
item.parent = parent
item.group = group
client._decoratedSlash.push(item)
} else
client._decoratedSlash.push({
group,
parent,
name: name ?? prop,
guild,
handler: item
})
}
}
/** Decorator to add a Slash Module to Client */
export function slashModule() {
return function (client: Client, prop: string) {
if (client._decoratedSlashModules === undefined)
client._decoratedSlashModules = []
const mod = ((client as unknown) as { [key: string]: any })[prop]
client._decoratedSlashModules.push(mod)
}
}

View File

@ -1,539 +0,0 @@
import * as baseEndpoints from '../consts/urlsAndVersions.ts'
import { Collection } from '../utils/collection.ts'
export type RequestMethods =
| 'get'
| 'post'
| 'put'
| 'patch'
| 'head'
| 'delete'
export enum HttpResponseCode {
Ok = 200,
Created = 201,
NoContent = 204,
NotModified = 304,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
TooManyRequests = 429,
GatewayUnavailable = 502
}
export interface RequestHeaders {
[name: string]: string
}
export class DiscordAPIError extends Error {
name = 'DiscordAPIError'
}
export interface QueuedItem {
bucket?: string | null
url: string
onComplete: () => Promise<
| {
rateLimited: any
bucket?: string | null
before: boolean
}
| undefined
>
}
export interface RateLimit {
url: string
resetAt: number
bucket: string | null
}
const METHODS = ['get', 'post', 'patch', 'put', 'delete', 'head']
export type MethodFunction = (
body?: unknown,
maxRetries?: number,
bucket?: string | null,
rawResponse?: boolean
) => Promise<any>
export interface APIMap extends MethodFunction {
get: APIMap
post: APIMap
patch: APIMap
put: APIMap
delete: APIMap
head: APIMap
[name: string]: APIMap
}
export const builder = (rest: RESTManager, acum = '/'): APIMap => {
const routes = {}
const proxy = new Proxy(routes, {
get: (_, p, __) => {
if (p === 'toString') return () => acum
if (METHODS.includes(String(p))) {
const method = ((rest as unknown) as {
[name: string]: MethodFunction
})[String(p)]
return async (...args: any[]) =>
await method.bind(rest)(
`${baseEndpoints.DISCORD_API_URL}/v${rest.version}${acum.substring(
0,
acum.length - 1
)}`,
...args
)
}
return builder(rest, acum + String(p) + '/')
}
})
return (proxy as unknown) as APIMap
}
export interface RESTOptions {
token?: string
headers?: { [name: string]: string | undefined }
canary?: boolean
version?: 6 | 7 | 8
}
export class RESTManager {
client?: RESTOptions
queues: { [key: string]: QueuedItem[] } = {}
rateLimits = new Collection<string, RateLimit>()
globalRateLimit: boolean = false
processing: boolean = false
version: number = 8
api: APIMap
constructor(client?: RESTOptions) {
this.client = client
this.api = builder(this)
if (client?.version !== undefined) this.version = client.version
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.handleRateLimits()
}
private async checkQueues(): Promise<void> {
Object.entries(this.queues).forEach(([key, value]) => {
if (value.length === 0) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.queues[key]
}
})
}
private queue(request: QueuedItem): void {
const route = request.url.substring(
// eslint seriously?
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
baseEndpoints.DISCORD_API_URL.length + 1
)
const parts = route.split('/')
parts.shift()
const [id] = parts
if (this.queues[id] !== undefined) {
this.queues[id].push(request)
} else {
this.queues[id] = [request]
}
}
private async processQueue(): Promise<void> {
if (Object.keys(this.queues).length !== 0 && !this.globalRateLimit) {
await Promise.allSettled(
Object.values(this.queues).map(async (pathQueue) => {
const request = pathQueue.shift()
if (request === undefined) return
const rateLimitedURLResetIn = await this.isRateLimited(request.url)
if (typeof request.bucket === 'string') {
const rateLimitResetIn = await this.isRateLimited(request.bucket)
if (rateLimitResetIn !== false) {
this.queue(request)
} else {
const result = await request.onComplete()
if (result?.rateLimited !== undefined) {
this.queue({
...request,
bucket: result.bucket ?? request.bucket
})
}
}
} else {
if (rateLimitedURLResetIn !== false) {
this.queue(request)
} else {
const result = await request.onComplete()
if (result?.rateLimited !== undefined) {
this.queue({
...request,
bucket: result.bucket ?? request.bucket
})
}
}
}
})
)
}
if (Object.keys(this.queues).length !== 0) {
// await delay(100)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.processQueue()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.checkQueues()
} else this.processing = false
}
private prepare(body: any, method: RequestMethods): { [key: string]: any } {
const headers: RequestHeaders = {
'User-Agent': `DiscordBot (harmony, https://github.com/harmony-org/harmony)`
}
if (this.client !== undefined)
headers.Authorization = `Bot ${this.client.token}`
if (this.client?.token === undefined) delete headers.Authorization
if (method === 'get' || method === 'head' || method === 'delete')
body = undefined
if (body?.reason !== undefined) {
headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason)
}
if (body?.file !== undefined) {
const form = new FormData()
form.append('file', body.file.blob, body.file.name)
form.append('payload_json', JSON.stringify({ ...body, file: undefined }))
body.file = form
} else if (
body !== undefined &&
!['get', 'delete'].includes(method.toLowerCase())
) {
headers['Content-Type'] = 'application/json'
}
if (this.client?.headers !== undefined)
Object.assign(headers, this.client.headers)
const data: { [name: string]: any } = {
headers,
body: body?.file ?? JSON.stringify(body),
method: method.toUpperCase()
}
return data
}
private async isRateLimited(url: string): Promise<number | false> {
const global = this.rateLimits.get('global')
const rateLimited = this.rateLimits.get(url)
const now = Date.now()
if (rateLimited !== undefined && now < rateLimited.resetAt) {
return rateLimited.resetAt - now
}
if (global !== undefined && now < global.resetAt) {
return global.resetAt - now
}
return false
}
private processHeaders(
url: string,
headers: Headers
): string | null | undefined {
let rateLimited = false
const global = headers.get('x-ratelimit-global')
const bucket = headers.get('x-ratelimit-bucket')
const remaining = headers.get('x-ratelimit-remaining')
const resetAt = headers.get('x-ratelimit-reset')
const retryAfter = headers.get('retry-after')
if (remaining !== null && remaining === '0') {
rateLimited = true
this.rateLimits.set(url, {
url,
resetAt: Number(resetAt) * 1000,
bucket
})
if (bucket !== null) {
this.rateLimits.set(bucket, {
url,
resetAt: Number(resetAt) * 1000,
bucket
})
}
}
if (global !== null) {
const reset = Date.now() + Number(retryAfter)
this.globalRateLimit = true
rateLimited = true
this.rateLimits.set('global', {
url: 'global',
resetAt: reset,
bucket
})
if (bucket !== null) {
this.rateLimits.set(bucket, {
url: 'global',
resetAt: reset,
bucket
})
}
}
return rateLimited ? bucket : undefined
}
private async handleStatusCode(
response: Response,
body: any,
data: { [key: string]: any },
reject: CallableFunction
): Promise<undefined> {
const status = response.status
if (
(status >= 200 && status < 400) ||
status === HttpResponseCode.NoContent ||
status === HttpResponseCode.TooManyRequests
)
return
let text: undefined | string = Deno.inspect(
body.errors === undefined ? body : body.errors
)
if (text === 'undefined') text = undefined
if (status === HttpResponseCode.Unauthorized)
reject(
new DiscordAPIError(
`Request was not successful (Unauthorized). Invalid Token.\n${text}`
)
)
// At this point we know it is error
const error: { [name: string]: any } = {
url: response.url,
status,
method: data.method,
code: body?.code,
message: body?.message,
errors: Object.fromEntries(
Object.entries(
(body?.errors as {
[name: string]: {
_errors: Array<{ code: string; message: string }>
}
}) ?? {}
).map((entry) => {
return [entry[0], entry[1]._errors]
})
)
}
// if (typeof error.errors === 'object') {
// const errors = error.errors as {
// [name: string]: { _errors: Array<{ code: string; message: string }> }
// }
// console.log(`%cREST Error:`, 'color: #F14C39;')
// Object.entries(errors).forEach((entry) => {
// console.log(` %c${entry[0]}:`, 'color: #12BC79;')
// entry[1]._errors.forEach((e) => {
// console.log(
// ` %c${e.code}: %c${e.message}`,
// 'color: skyblue;',
// 'color: #CECECE;'
// )
// })
// })
// }
if (
[
HttpResponseCode.BadRequest,
HttpResponseCode.NotFound,
HttpResponseCode.Forbidden,
HttpResponseCode.MethodNotAllowed
].includes(status)
) {
reject(new DiscordAPIError(Deno.inspect(error)))
} else if (status === HttpResponseCode.GatewayUnavailable) {
reject(new DiscordAPIError(Deno.inspect(error)))
} else reject(new DiscordAPIError('Request - Unknown Error'))
}
/**
* Makes a Request to Discord API
* @param method HTTP Method to use
* @param url URL of the Request
* @param body Body to send with Request
* @param maxRetries Number of Max Retries to perform
* @param bucket BucketID of the Request
* @param rawResponse Whether to get Raw Response or body itself
*/
async make(
method: RequestMethods,
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean
): Promise<any> {
return await new Promise((resolve, reject) => {
const onComplete = async (): Promise<undefined | any> => {
try {
const rateLimitResetIn = await this.isRateLimited(url)
if (rateLimitResetIn !== false) {
return {
rateLimited: rateLimitResetIn,
before: true,
bucket
}
}
const query =
method === 'get' && body !== undefined
? Object.entries(body as any)
.filter(([k, v]) => v !== undefined)
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
value as any
)}`
)
.join('&')
: ''
let urlToUse =
method === 'get' && query !== '' ? `${url}?${query}` : url
if (this.client?.canary === true) {
const split = urlToUse.split('//')
urlToUse = split[0] + '//canary.' + split[1]
}
const requestData = this.prepare(body, method)
const response = await fetch(urlToUse, requestData)
const bucketFromHeaders = this.processHeaders(url, response.headers)
if (response.status === 204)
return resolve(
rawResponse === true ? { response, body: null } : undefined
)
const json: any = await response.json()
await this.handleStatusCode(response, json, requestData, reject)
if (
json.retry_after !== undefined ||
json.message === 'You are being rate limited.'
) {
if (maxRetries > 10) {
throw new Error('Max RateLimit Retries hit')
}
return {
rateLimited: json.retry_after,
before: false,
bucket: bucketFromHeaders
}
}
return resolve(rawResponse === true ? { response, body: json } : json)
} catch (error) {
return reject(error)
}
}
this.queue({
onComplete,
bucket,
url
})
if (!this.processing) {
this.processing = true
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.processQueue()
}
})
}
private async handleRateLimits(): Promise<void> {
const now = Date.now()
this.rateLimits.forEach((value, key) => {
if (value.resetAt > now) return
this.rateLimits.delete(key)
if (key === 'global') this.globalRateLimit = false
})
}
/** Makes a GET Request to API */
async get(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean
): Promise<any> {
return await this.make('get', url, body, maxRetries, bucket, rawResponse)
}
/** Makes a POST Request to API */
async post(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean
): Promise<any> {
return await this.make('post', url, body, maxRetries, bucket, rawResponse)
}
/** Makes a DELETE Request to API */
async delete(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean
): Promise<any> {
return await this.make('delete', url, body, maxRetries, bucket, rawResponse)
}
/** Makes a PATCH Request to API */
async patch(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean
): Promise<any> {
return await this.make('patch', url, body, maxRetries, bucket, rawResponse)
}
/** Makes a PUT Request to API */
async put(
url: string,
body?: unknown,
maxRetries = 0,
bucket?: string | null,
rawResponse?: boolean
): Promise<any> {
return await this.make('put', url, body, maxRetries, bucket, rawResponse)
}
}

View File

@ -1,69 +0,0 @@
import { Collection } from '../utils/collection.ts'
import { Client, ClientOptions } from './client.ts'
import {EventEmitter} from '../../deps.ts'
import { RESTManager } from './rest.ts'
// import { GATEWAY_BOT } from '../types/endpoint.ts'
// import { GatewayBotPayload } from '../types/gatewayBot.ts'
// TODO(DjDeveloperr)
// I'm kinda confused; will continue on this later once
// Deno namespace in Web Worker is stable!
export interface ShardManagerOptions {
client: Client | typeof Client
token?: string
intents?: number[]
options?: ClientOptions
shards: number
}
export interface ShardManagerInitOptions {
file: string
token?: string
intents?: number[]
options?: ClientOptions
shards?: number
}
export class ShardManager extends EventEmitter {
workers: Collection<string, Worker> = new Collection()
token: string
intents: number[]
shardCount: number
private readonly __client: Client
get rest(): RESTManager {
return this.__client.rest
}
constructor(options: ShardManagerOptions) {
super()
this.__client =
options.client instanceof Client
? options.client
: // eslint-disable-next-line new-cap
new options.client(options.options)
if (this.__client.token === undefined || options.token === undefined)
throw new Error('Token should be provided when constructing ShardManager')
if (this.__client.intents === undefined || options.intents === undefined)
throw new Error(
'Intents should be provided when constructing ShardManager'
)
this.token = this.__client.token ?? options.token
this.intents = this.__client.intents ?? options.intents
this.shardCount = options.shards
}
// static async init(): Promise<ShardManager> {}
// async start(): Promise<ShardManager> {
// const info = ((await this.rest.get(
// GATEWAY_BOT()
// )) as unknown) as GatewayBotPayload
// const totalShards = this.__shardCount ?? info.shards
// return this
// }
}

View File

@ -1,26 +0,0 @@
import { Guild } from '../structures/guild.ts'
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import { Client } from './client.ts'
export interface VoiceOptions {
guild: Guild
channel: VoiceChannel
}
export class VoiceClient {
client: Client
ws?: WebSocket
guild: Guild
channel: VoiceChannel
constructor(client: Client, options: VoiceOptions) {
this.client = client
this.guild = options.guild
this.channel = options.channel
}
async connect(): Promise<VoiceClient> {
// TODO(DjDeveloperr): understand docs
return this
}
}

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

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