Compare commits

...

421 Commits
v0.9.2 ... 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
DjDeveloper 2e28093e33
Merge pull request #79 from fishuke/main
Fix JSDoc, typo and using before initialization.
2020-12-29 15:43:27 +05:30
Helloyunho e1a8a8526a
Merge branch 'main' into main 2020-12-29 15:30:41 +09:00
Helloyunho 41891b45c7
Merge pull request #78 from DjDeveloperr/slash
feat: new rest methods
2020-12-29 15:26:56 +09:00
DjDeveloperr 2ddb6bf5a5 fix 2020-12-29 11:47:36 +05:30
DjDeveloper 8463e59d4f
Merge branch 'main' into slash 2020-12-29 11:44:17 +05:30
DjDeveloper 205f88281b
Merge pull request #77 from ayntee/role-struct
feat: add Role#tags prop
2020-12-29 11:42:52 +05:30
DjDeveloperr 63ae6f4312 feat: rename fetch -> fetchInvite 2020-12-29 10:13:28 +05:30
Helloyunho 492ebe91f8
Merge pull request #74 from ayntee/emoji-methods
feat(structures): add Emoji methods
2020-12-29 12:46:04 +09:00
DjDeveloper 8bd59f925b
Merge pull request #80 from Helloyunho/waitFor
Change the event typing way and add <Client>.waitFor method
2020-12-29 08:51:36 +05:30
Helloyunho 991770d211 Change the event typing way and add <Client>.waitFor method 2020-12-29 11:52:30 +09:00
Fishuke e217036dd8 Add webstorm 2020-12-29 01:07:26 +03:00
Fishuke 28734cea17 Fix using before initialization. 2020-12-29 01:04:41 +03:00
Fishuke 7d441688e2 fix typo 2020-12-29 01:02:43 +03:00
Fishuke 36e174a50a fix JSDoc 2020-12-29 00:44:51 +03:00
Aki 61531e2349
Update deps.ts 2020-12-29 00:32:05 +09:00
DjDeveloperr 9178e6cec1 feat: new rest methods 2020-12-28 18:05:08 +05:30
Ayyan e37e0c3885
Update role.ts 2020-12-28 12:18:09 +04:00
Ayyan b7a93a280f
Update role.ts 2020-12-28 12:14:10 +04:00
ayntee 93eee2f15d I have no idea what I am doing x2 2020-12-26 11:12:06 +04:00
ayntee 4cf153aaac I have no idea what I am doing 2020-12-26 11:09:09 +04:00
ayntee 900f6c3c3b Make linter happy 2020-12-26 11:07:33 +04:00
ayntee 95c2200386 Make DjDev happy 2020-12-26 11:05:26 +04:00
ayntee c7d117cd54 Make CI happy x2 2020-12-26 10:49:52 +04:00
ayntee e25b7a8f5b Make CI happy 2020-12-26 10:47:13 +04:00
ayntee 678e988aec Remove redundant null assertion 2020-12-26 10:38:20 +04:00
ayntee 0a066a32e4 Goddamn it, i gotta install prettier plugin on vim 2020-12-26 10:36:52 +04:00
ayntee a14104ad14 Github online editor FTW 2020-12-26 10:36:09 +04:00
ayntee a69988b653 ci: add Emoji#id check 2020-12-26 10:34:15 +04:00
ayntee 73e9ba9a25 fix: DELETE request instead of PATCH 2020-12-26 10:17:54 +04:00
ayntee 6c576bec0a chore: implement role resolution internally 2020-12-26 10:11:26 +04:00
Helloyunho 8d693157da
Merge pull request #68 from Helloyunho/reaction
Add `addReaction` and `removeReaction` feature, Handle null-id emojis
2020-12-26 12:10:33 +09:00
Helloyunho 4129ea699b
Fix mistake 2020-12-26 12:07:15 +09:00
Helloyunho 65b5c50ec3
Merge branch 'main' into reaction 2020-12-26 12:04:59 +09:00
DjDeveloper d46cd0cbe5
Merge pull request #67 from DjDeveloperr/slash
feat: HTTP based Slash Commands util
2020-12-26 08:24:38 +05:30
DjDeveloper 31b3c08580
Merge pull request #70 from ayntee/sep-deps
deps: segregate third-party deps to deps.ts file
2020-12-26 08:18:09 +05:30
DjDeveloper 6be099398f
Merge pull request #76 from ayntee/ref
refactor: move snowflake.ts to utils
2020-12-26 08:16:42 +05:30
DjDeveloper 523fc432e9
Merge pull request #75 from ayntee/invite-methods
feat: add Invite#delete() method
2020-12-26 08:15:09 +05:30
DjDeveloper 0e6814690d
Merge pull request #73 from ayntee/switch-case
refactor(gateway): use switch-case to avoid repetitive code
2020-12-26 08:10:00 +05:30
DjDeveloper 3a80e162c0
Merge pull request #69 from ayntee/guild-template
feat: add template structure and methods
2020-12-26 08:08:05 +05:30
ayntee 1e753cf51d feat: add Role#tags prop 2020-12-25 22:29:26 +04:00
ayntee f2ee69c130 refactor: move snowflake.ts to utils 2020-12-25 20:47:23 +04:00
ayntee 104aa4249e chore: consistent return type 2020-12-25 20:45:21 +04:00
ayntee e919ad38de feat: add Invite#delete() method 2020-12-25 20:44:05 +04:00
ayntee 6dd40afd3e Make linter happy :) 2020-12-25 20:41:21 +04:00
ayntee bef4d45a40 style: format files 2020-12-25 20:38:43 +04:00
ayntee f3fa4ca380 fix: remove type "null" from Emoji#id 2020-12-25 20:38:06 +04:00
ayntee 6c6334ee07 fix: add Emoji#guild property back 2020-12-25 20:36:23 +04:00
ayntee c59d8f1639 style: format structures/emoji.ts 2020-12-25 20:33:45 +04:00
ayntee 9c048686c8 feat(structures): add Emoji methods 2020-12-25 20:31:53 +04:00
ayntee 264d6b49fb refactor: destructure code and reason from event 2020-12-25 20:18:35 +04:00
ayntee f9e07f4f43 refactor(gateway): use switch-case to avoid repetitive code 2020-12-25 20:13:06 +04:00
Helloyunho 4ab29bc91c Merge branch 'main' of https://github.com/Helloyunho/harmony into reaction 2020-12-25 22:14:19 +09:00
Helloyunho 65eace4046 Update message and textchannel 2020-12-25 22:09:29 +09:00
ayntee 30724dc791 fix: add permissions field to InteractionPayload#member 2020-12-25 16:22:10 +04:00
ayntee 255bc2d4e9 style: format files 2020-12-25 16:11:41 +04:00
ayntee 0a14264b0e deps: segregate third-party deps to deps.ts file 2020-12-25 15:47:24 +04:00
ayntee b76c26ff7c docs: capitalize props' description 2020-12-25 15:20:48 +04:00
ayntee be662a230b chore: add return type and handle raw API payload 2020-12-25 14:59:24 +04:00
ayntee d1fcce7b83 feat: add template structure and methods 2020-12-25 14:21:01 +04:00
Helloyunho 540f3b1877 Support removing other user's reactions 2020-12-25 13:44:42 +09:00
Helloyunho eac5d52d83 Handle null-id emojis 2020-12-24 09:59:32 +09:00
Helloyunho 315fd0d0da Make a test command 2020-12-24 09:59:01 +09:00
Helloyunho 9e6d2c1047 Add reactions 2020-12-24 09:37:57 +09:00
DjDeveloperr 015b7951d8 fix slash modules 2020-12-23 15:26:02 +05:30
DjDeveloperr 94a447921d feat: member screening types update 2020-12-22 15:40:19 +05:30
DjDeveloperr a9338ad88a Merge remote-tracking branch 'origin/main' into slash 2020-12-22 12:30:48 +05:30
DjDeveloperr 03ea5df551 feat: middlewares and http-based slash util 2020-12-22 12:28:45 +05:30
DjDeveloperr da0bfc12c7 feat: slash builde 2020-12-21 19:18:46 +05:30
Aki 3695b81de6
Merge pull request #66 from DjDeveloperr/slash
SlashClient & RESTManager can run alone and added APIMap
2020-12-20 21:01:39 +09:00
DjDeveloperr cac3c5c9e3 chore: make slashclient standalone and restmanager accept options 2020-12-20 15:15:49 +05:30
Aki a8fc3ef463
Merge pull request #65 from Helloyunho/channel-edit
Add channel edit feature
2020-12-20 18:20:40 +09:00
Helloyunho 9688d248e3 Add edit channel feature 2020-12-20 18:11:37 +09:00
DjDeveloperr 4d8eabcc2c Merge remote-tracking branch 'origin/main' into slash 2020-12-20 14:12:57 +05:30
DjDeveloperr 5fae38fce6 feat: add api route builder 2020-12-20 14:12:24 +05:30
Helloyunho dced786e60 Add interfaces for channel edit 2020-12-20 17:14:57 +09:00
Helloyunho dd70d96d50 Change category channel payload name 2020-12-20 17:14:05 +09:00
Helloyunho 837f045bdd Remove nsfw property in some channels
Some guild channels can't be a nsfw channel
2020-12-20 17:13:25 +09:00
Helloyunho b0da81f308
Merge pull request #64 from Helloyunho/fix-managers
Fix managers' array function
2020-12-19 19:15:06 +09:00
Helloyunho a04f6ab973 Fix managers' array function 2020-12-19 16:58:40 +09:00
Helloyunho 920cf133bc Add issue templates
...so we can make issues more beautiful!
2020-12-17 18:48:58 +09:00
DjDeveloper 6c8d1bc44c
Update README.md 2020-12-17 14:29:36 +05:30
Helloyunho 7a67fb67d9
Merge pull request #63 from Helloyunho/sticker
Add Sticker Feature (related to #62)
2020-12-17 14:36:44 +09:00
Helloyunho d7f4b244c1 Add Sticker Feature 2020-12-17 10:20:06 +09:00
Aki e27002fc55
We need Update 2020-12-16 23:08:42 +09:00
211 changed files with 11489 additions and 3622 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'
}
}

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug] "
labels: "bug \U0001F41B"
assignees: ''
---
**What's the bug?**
<!-- Please tell us about the bug as short and easy to understand. -->
**How do we reproduce it?**
<!-- Do:
1. Use `something function` in `messageCreate` event
2. Make a message
3. Bamm!
Don't:
1. Run code
2. Error!
-->
**What should have happened?**
<!-- We might not know what you were expecting. Please tell us. -->
**What is actually happening?**
<!-- Please tell us the result of the code. -->
**What versions you're using?**
- OS: <!-- [e.g. macOS 11.1 Big Sur] -->
- Deno: <!-- [e.g. Deno 1.6.1] -->
- Harmony: <!-- [e.g. Harmony 0.9.0] -->
**Do you have anything to tell us more about the bug?**
<!-- If you do, please tell us more in here. -->

View File

@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request] "
labels: enhancement ✨
assignees: ''
---
**What do you want to request?**
<!-- Please tell us about your request as short and easy to understand. -->
<!-- !!REMOVE THIS TAG IF YOU ALREADY HAVE(..or thought about) SOME SOLUTIONS!!
**Do you have any solutions?**
If you have, please tell us so we can add(or at least think about) your solution!
-->
**Do you have anything to tell us about your request?**
<!-- If you do, please tell us more in here. -->

11
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,11 @@
---
name: Question
about: Question about this project
title: "[Question] "
labels: question ❓
assignees: ''
---
**What question do you have?**
<!-- Please tell us your question as short and easy to understand. -->

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

4
.gitignore vendored
View File

@ -109,9 +109,13 @@ yarn.lock
# PRIVACY XDDDD
src/test/config.ts
test/config.ts
.vscode
# macOS is shit xD
**/.DS_Store
# Webstorm dont forget this duude :)
.idea/
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,22 +3,19 @@
<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>
<br>
- Lightweight and easy to use.
- Built-in Command Framework,
- Easily build Commands on the fly.
- Completely Customizable.
- Complete Object-Oriented approach.
- 100% Discord API Coverage.
- Customizable caching.
- Built in support for Redis.
- Write Custom Cache Adapters.
- Complete TypeScript support.
- Complete Object-Oriented approach.
- Slash Commands supported.
- Built-in Commands framework.
- Customizable Caching, with Redis support.
- Use `@decorators` to easily make things!
- Made with ❤️ TypeScript.
## Table of Contents
@ -31,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:
@ -39,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()
@ -56,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!
@ -72,8 +78,7 @@ import {
CommandClient,
Command,
CommandContext,
Message,
Intents
GatewayIntents
} from 'https://deno.land/x/harmony/mod.ts'
const client = new CommandClient({
@ -90,25 +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 Decorator!
Or with Decorators!
```ts
import {
CommandClient,
event,
Intents,
CommandClient,
command,
CommandContext,
GatewayIntents
} from 'https://deno.land/x/harmony/mod.ts'
class MyClient extends CommandClient {
@ -130,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)
client.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.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
@ -161,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'

6
deps.ts Normal file
View File

@ -0,0 +1,6 @@
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
])

126
mod.ts
View File

@ -1,38 +1,47 @@
export { GatewayIntents } from './src/types/gateway.ts'
export { default as EventEmitter } from 'https://deno.land/std@0.74.0/node/events.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'
@ -40,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'
@ -58,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,8 +81,14 @@ export {
ActivityTypes
} from './src/structures/presence.ts'
export { Role } from './src/structures/role.ts'
export { Snowflake } from './src/structures/snowflake.ts'
export { TextChannel, GuildTextChannel } from './src/structures/textChannel.ts'
export { Snowflake } from './src/utils/snowflake.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'
@ -76,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,
@ -84,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 {
@ -92,18 +122,37 @@ export type {
ChannelPayload,
FollowedChannel,
GuildNewsChannelPayload,
GuildChannelCategoryPayload,
GuildCategoryChannelPayload,
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'
@ -123,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.1",
"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 'https://denopkg.com/keroxp/deno-redis/mod.ts'
/**
* ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony.
*
* Methods can return Promises too.
*/
export interface ICacheAdapter {
/** Gets a key from a Cache */
get: (cacheName: string, key: string) => Promise<any> | any
/** Sets a key to value in a Cache Name with optional expire value in MS */
set: (
cacheName: string,
key: string,
value: any,
expire?: number
) => Promise<any> | any
/** Deletes a key from a Cache */
delete: (cacheName: string, key: string) => Promise<boolean> | boolean
/** Gets array of all values in a Cache */
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined>
/** Entirely deletes a Cache */
deleteCache: (cacheName: string) => any
}
/** Default Cache Adapter for in-memory caching. */
export class DefaultCacheAdapter implements ICacheAdapter {
data: {
[name: string]: Collection<string, any>
} = {}
async get(cacheName: string, key: string): Promise<undefined | any> {
const cache = this.data[cacheName]
if (cache === undefined) return
return cache.get(key)
}
async set(
cacheName: string,
key: string,
value: any,
expire?: number
): Promise<any> {
let cache = this.data[cacheName]
if (cache === undefined) {
this.data[cacheName] = new Collection()
cache = this.data[cacheName]
}
cache.set(key, value)
if (expire !== undefined)
setTimeout(() => {
cache.delete(key)
}, expire)
}
async delete(cacheName: string, key: string): Promise<boolean> {
const cache = this.data[cacheName]
if (cache === undefined) return false
return cache.delete(key)
}
async array(cacheName: string): Promise<any[] | undefined> {
const cache = this.data[cacheName]
if (cache === undefined) return
return cache.array()
}
async deleteCache(cacheName: string): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
return delete this.data[cacheName]
}
}
/** Redis Cache Adatper 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 'https://deno.land/x/mutil@0.1.2/mod.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
@ -18,12 +18,12 @@ export interface CommandContext {
channel: TextChannel
/** Prefix which was used */
prefix: string
/** Oject of Command which was used */
/** Object of Command which was used */
command: Command
/** 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
@ -67,37 +68,35 @@ export class Extension {
description?: string
/** Extensions's Commands Manager */
commands: ExtensionCommands = new ExtensionCommands(this)
/** Sub-Prefix to be used for ALL of Extenion's Commands. */
/** Sub-Prefix to be used for ALL of Extension's Commands. */
subPrefix?: string
/** 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,
@ -17,37 +17,45 @@ export const guildEmojiUpdate: GatewayEventHandler = async (
const _updated: EmojiPayload[] = []
for (const raw of d.emojis) {
const has = emojis.get(raw.id)
const emojiID = (raw.id !== null ? raw.id : raw.name) as string
const has = emojis.get(emojiID)
if (has === undefined) {
await guild.emojis.set(raw.id, raw)
const emoji = (await guild.emojis.get(raw.id)) as Emoji
await guild.emojis.set(emojiID, raw)
const emoji = (await guild.emojis.get(emojiID)) as Emoji
added.push(emoji)
} else _updated.push(raw)
}
for (const emoji of emojis.values()) {
const find = _updated.find((e) => emoji.id === e.id)
const emojiID = (emoji.id !== null ? emoji.id : emoji.name) as string
const find = _updated.find((e) => {
const eID = e.id !== null ? e.id : e.name
return emojiID === eID
})
if (find === undefined) {
await guild.emojis.delete(emoji.id)
await guild.emojis.delete(emojiID)
deleted.push(emoji)
} else {
const before = (await guild.emojis.get(find.id)) as Emoji
await guild.emojis.set(find.id, find)
const after = (await guild.emojis.get(find.id)) as Emoji
const foundID = (find.id !== null ? find.id : find.name) as string
const before = (await guild.emojis.get(foundID)) as Emoji
await guild.emojis.set(foundID, find)
const after = (await guild.emojis.get(foundID)) as Emoji
updated.push({ before, after })
}
}
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'
@ -6,7 +6,6 @@ export const guildIntegrationsUpdate: GatewayEventHandler = async (
gateway: Gateway,
d: GuildIntegrationsUpdatePayload
) => {
console.log(d)
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
if (guild === undefined) return

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,
@ -29,15 +29,16 @@ export const messageReactionAdd: GatewayEventHandler = async (
} else return
}
let reaction = await message.reactions.get(d.emoji.id)
const emojiID = (d.emoji.id !== null ? d.emoji.id : d.emoji.name) as string
let reaction = await message.reactions.get(emojiID)
if (reaction === undefined) {
await message.reactions.set(d.emoji.id, {
await message.reactions.set(emojiID, {
count: 1,
emoji: d.emoji,
me: d.user_id === gateway.client.user?.id
})
reaction = ((await message.reactions.get(
d.emoji.id
emojiID
)) as unknown) as MessageReaction
}

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,
@ -27,10 +27,11 @@ export const messageReactionRemove: GatewayEventHandler = async (
} else return
}
const reaction = await message.reactions.get(d.emoji.id)
const emojiID = (d.emoji.id !== null ? d.emoji.id : d.emoji.name) as string
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,
@ -19,7 +19,8 @@ export const messageReactionRemoveEmoji: GatewayEventHandler = async (
} else return
}
const reaction = await message.reactions.get(d.emoji.id)
const emojiID = (d.emoji.id !== null ? d.emoji.id : d.emoji.name) as string
const reaction = await message.reactions.get(emojiID)
if (reaction === undefined) return
await reaction.users.flush()

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,
@ -99,238 +116,313 @@ export const gatewayHandlers: {
INTERACTION_CREATE: interactionCreate
}
export interface EventTypes {
[name: string]: (...args: any[]) => void
}
export interface VoiceServerUpdateData {
token: string
endpoint: string
guild: Guild
}
export interface ClientEvents extends EventTypes {
/** All Client Events */
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ClientEvents = {
/** When Client has successfully connected to Discord */
ready: () => void
/** When a successful reconnect has been made */
reconnect: () => void
ready: [shard: number]
/** When a reconnect was requested by Discord */
reconnect: [shard: number]
/** When a successful session resume has been done */
resumed: () => void
resumed: [shard: number]
/**
* When a new Channel is created
* @param channel New Channel object
*/
channelCreate: (channel: EveryChannelTypes) => void
channelCreate: [channel: EveryChannelTypes]
/**
* When a Channel was deleted
* @param channel Channel object which was deleted
*/
channelDelete: (channel: EveryChannelTypes) => void
channelDelete: [channel: EveryChannelTypes]
/**
* Channel's Pinned Messages were updated
* @param before Channel object before update
* @param after Channel object after update
*/
channelPinsUpdate: (
channelPinsUpdate: [
before: EveryTextChannelTypes,
after: EveryTextChannelTypes
) => void
]
/**
* A Channel was updated
* @param before Channel object before update
* @param after Channel object after update
*/
channelUpdate: (before: EveryChannelTypes, after: EveryChannelTypes) => void
channelUpdate: [before: EveryChannelTypes, after: EveryChannelTypes]
/**
* A User was banned from a Guild
* @param guild The Guild from which User was banned
* @param user The User who was banned
*/
guildBanAdd: (guild: Guild, user: User) => void
guildBanAdd: [guild: Guild, user: User]
/**
* A ban from a User in Guild was elevated
* @param guild Guild from which ban was removed
* @param user User of which ban was elevated
*/
guildBanRemove: (guild: Guild, user: User) => void
guildBanRemove: [guild: Guild, user: User]
/**
* Client has joined a new Guild.
* @param guild The new Guild object
*/
guildCreate: (guild: Guild) => void
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
*/
guildDelete: (guild: Guild) => void
guildDelete: [guild: Guild]
/**
* A new Emoji was added to Guild
* @param guild Guild in which Emoji was added
* @param emoji The Emoji which was added
*/
guildEmojiAdd: (guild: Guild, emoji: Emoji) => void
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: Guild, emoji: Emoji) => void
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) => void
guildEmojiUpdate: [before: Emoji, after: Emoji]
/**
* Guild's Integrations were updated
* @param guild The Guild object
*/
guildIntegrationsUpdate: (guild: Guild) => void
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
*/
guildMemberAdd: (member: Member) => void
guildMemberAdd: [member: Member]
/**
* A Guild Member has either left or was kicked from Guild
* @param member The Member object
*/
guildMemberRemove: (member: Member) => void
guildMemberRemove: [member: Member]
/**
* A Guild Member was updated. Nickname changed, role assigned, etc.
* @param before Member object before update
* @param after Meber object after update
* @param after Member object after update
*/
guildMemberUpdate: (before: Member, after: Member) => void
guildMemberUpdate: [before: Member, after: Member]
/**
* A new Role was created in Guild
* @param role The new Role object
*/
guildRoleCreate: (role: Role) => void
guildRoleCreate: [role: Role]
/**
* A Role was deleted from the Guild
* @param role The Role object
*/
guildRoleDelete: (role: Role) => void
guildRoleDelete: [role: Role]
/**
* A Role was updated in a Guild
* @param before Role object before update
* @param after Role object after updated
*/
guildRoleUpdate: (before: Role, after: Role) => void
guildRoleUpdate: [before: Role, after: Role]
/**
* A Guild has been updated. For example name, icon, etc.
* @param before Guild object before update
* @param after Guild object after update
*/
guildUpdate: (before: Guild, after: Guild) => void
guildUpdate: [before: Guild, after: Guild]
/**
* A new Message was created (sent)
* @param message The new Message object
*/
messageCreate: (message: Message) => void
messageCreate: [message: Message]
/**
* A Message was deleted.
* @param message The Message object
*/
messageDelete: (message: Message) => void
messageDelete: [message: Message]
/**
* Messages were bulk deleted in a Guild Text Channel
* @param channel Channel in which Messages were deleted
* @param messages Collection of Messages deleted
* @param uncached Set of Messages deleted's IDs which were not cached
*/
messageDeleteBulk: (
channel: GuildTextChannel,
messageDeleteBulk: [
channel: GuildTextBasedChannel,
messages: Collection<string, Message>,
uncached: Set<string>
) => void
]
/**
* A Message was updated. For example content, embed, etc.
* @param before Message object before update
* @param after Message object after update
*/
messageUpdate: (before: Message, after: Message) => void
messageUpdate: [before: Message, after: Message]
/**
* Reaction was added to a Message
* @param reaction Reaction object
* @param user User who added the reaction
*/
messageReactionAdd: (reaction: MessageReaction, user: User) => void
messageReactionAdd: [reaction: MessageReaction, user: User]
/**
* Reaction was removed fro a Message
* @param reaction Reaction object
* @param user User to who removed the reaction
*/
messageReactionRemove: (reaction: MessageReaction, user: User) => void
messageReactionRemove: [reaction: MessageReaction, user: User]
/**
* All reactions were removed from a Message
* @param message Message from which reactions were removed
*/
messageReactionRemoveAll: (message: Message) => void
messageReactionRemoveAll: [message: Message]
/**
* All reactions of a single Emoji were removed
* @param message The Message object
* @param emoji The Emoji object
*/
messageReactionRemoveEmoji: (message: Message, emoji: Emoji) => void
messageReactionRemoveEmoji: [message: Message, emoji: Emoji]
/**
* A User has started typing in a Text Channel
* @param user User who started typing
* @param channel Channel which user started typing in
* @param at Date when user started typing
* @param guild Guild which user started typing in (can be undefined)
*/
typingStart: (
typingStart: [
user: User,
channel: TextChannel,
at: Date,
guildData?: TypingStartGuildData
) => void
guild: TypingStartGuildData | undefined
]
/**
* A new Invite was created
* @param invite New Invite object
*/
inviteCreate: (invite: Invite) => void
inviteCreate: [invite: Invite]
/**
* An Invite was deleted
* @param invite Invite object
*/
inviteDelete: (invite: Invite) => void
inviteDelete: [invite: Invite]
/**
* A User was updated. For example username, avatar, etc.
* @param before The User object before update
* @param after The User object after update
*/
userUpdate: (before: User, after: User) => void
userUpdate: [before: User, after: User]
/**
* Client has received credentials for establishing connection to Voice Server
* @param data Updated voice server object
*/
voiceServerUpdate: (data: VoiceServerUpdateData) => void
voiceServerUpdate: [data: VoiceServerUpdateData]
/**
* A User has joined a Voice Channel
* @param state Added voice state object
*/
voiceStateAdd: (state: VoiceState) => void
voiceStateAdd: [state: VoiceState]
/**
* A User has left a Voice Channel
* @param state Removed voice state object
*/
voiceStateRemove: (state: VoiceState) => void
voiceStateRemove: [state: VoiceState]
/**
* Voice State of a User has been updated
* @param before Voice State object before update
* @param after Voice State object after update
*/
voiceStateUpdate: (state: VoiceState, after: VoiceState) => void
voiceStateUpdate: [before: VoiceState, after: VoiceState]
/**
* A User's presence has been updated
* @param presence New Presence
*/
presenceUpdate: (presence: Presence) => void
presenceUpdate: [presence: Presence]
/**
* Webhooks of a Channel in a Guild has been updated
* @param guild Guild in which Webhooks were updated
* @param channel Channel of which Webhooks were updated
*/
webhooksUpdate: (guild: Guild, channel: GuildTextChannel) => void
webhooksUpdate: [guild: Guild, channel: GuildTextBasedChannel]
/**
* An Interaction was created
* @param interaction Created interaction object
*/
interactionCreate: (interaction: Interaction) => void
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,418 +0,0 @@
import { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts'
import { Client } from '../models/client.ts'
import {
DISCORD_GATEWAY_URL,
DISCORD_API_VERSION
} from '../consts/urlsAndVersions.ts'
import { GatewayResponse } from '../types/gatewayResponse.ts'
import {
GatewayOpcodes,
GatewayIntents,
GatewayCloseCodes,
IdentityPayload,
StatusUpdatePayload
} from '../types/gateway.ts'
import { gatewayHandlers } from './handlers/index.ts'
import { GATEWAY_BOT } from '../types/endpoint.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'
export interface RequestMembersOptions {
limit?: number
presences?: boolean
query?: string
users?: string[]
}
export interface VoiceStateOptions {
mute?: boolean
deaf?: boolean
}
export const RECONNECT_REASON = 'harmony-reconnect'
/**
* Handles Discord gateway connection.
*
* You should not use this and rather use Client class.
*/
class Gateway {
websocket: WebSocket
token: string
intents: GatewayIntents[]
connected = false
initialized = false
heartbeatInterval = 0
heartbeatIntervalID?: number
sequenceID?: number
lastPingTimestamp = 0
sessionID?: string
private heartbeatServerResponded = false
client: Client
cache: GatewayCache
private timedIdentify: number | null = null
constructor(client: Client, token: string, intents: GatewayIntents[]) {
this.token = token
this.intents = intents
this.client = client
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)
}
private onopen(): void {
this.connected = true
this.debug('Connected to Gateway!')
}
private async onmessage(event: MessageEvent): Promise<void> {
let data = event.data
if (data instanceof ArrayBuffer) {
data = new Uint8Array(data)
}
if (data instanceof Uint8Array) {
data = unzlib(data)
data = new TextDecoder('utf-8').decode(data)
}
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
switch (op) {
case GatewayOpcodes.HELLO:
this.heartbeatInterval = d.heartbeat_interval
this.debug(
`Received HELLO. Heartbeat Interval: ${this.heartbeatInterval}`
)
this.sendHeartbeat()
this.heartbeatIntervalID = setInterval(() => {
this.heartbeat()
}, this.heartbeatInterval)
if (!this.initialized) {
this.initialized = true
await this.sendIdentify(this.client.forceNewSession)
} else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendResume()
}
break
case GatewayOpcodes.HEARTBEAT_ACK:
this.heartbeatServerResponded = true
this.client.ping = Date.now() - this.lastPingTimestamp
this.debug(
`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`
)
break
case GatewayOpcodes.INVALID_SESSION:
// Because we know this gonna be bool
this.debug(
`Invalid Session received! Resumeable? ${d === true ? 'Yes' : 'No'}`
)
if (d !== true) {
this.debug(`Session was invalidated, deleting from cache`)
await this.cache.delete('session_id')
await this.cache.delete('seq')
this.sessionID = undefined
this.sequenceID = undefined
}
this.timedIdentify = setTimeout(async () => {
this.timedIdentify = null
await this.sendIdentify(!(d as boolean))
}, 5000)
break
case GatewayOpcodes.DISPATCH: {
this.heartbeatServerResponded = true
if (s !== null) {
this.sequenceID = s
await this.cache.set('seq', s)
}
if (t !== null && t !== undefined) {
this.client.emit('raw', t, d)
const handler = gatewayHandlers[t]
if (handler !== undefined) {
handler(this, d)
}
}
break
}
case GatewayOpcodes.RESUME: {
// 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)
break
}
case GatewayOpcodes.RECONNECT: {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
break
}
default:
break
}
}
private async onclose(event: CloseEvent): Promise<void> {
if (event.reason === RECONNECT_REASON) return
this.debug(`Connection Closed with code: ${event.code}`)
if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) {
this.debug('API has encountered Unknown Error. Reconnecting...')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
} else if (event.code === GatewayCloseCodes.UNKNOWN_OPCODE) {
throw new Error("Unknown OP Code was sent. This shouldn't happen!")
} else if (event.code === GatewayCloseCodes.DECODE_ERROR) {
throw new Error("Invalid Payload was sent. This shouldn't happen!")
} else if (event.code === GatewayCloseCodes.NOT_AUTHENTICATED) {
throw new Error('Not Authorized: Payload was sent before Identifying.')
} else if (event.code === GatewayCloseCodes.AUTHENTICATION_FAILED) {
throw new Error('Invalid Token provided!')
} else if (event.code === GatewayCloseCodes.INVALID_SEQ) {
this.debug('Invalid Seq was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
} else if (event.code === GatewayCloseCodes.RATE_LIMITED) {
throw new Error("You're ratelimited. Calm down.")
} else if (event.code === GatewayCloseCodes.SESSION_TIMED_OUT) {
this.debug('Session Timeout. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect(true)
} else if (event.code === GatewayCloseCodes.INVALID_SHARD) {
this.debug('Invalid Shard was sent. Reconnecting.')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
} else if (event.code === GatewayCloseCodes.SHARDING_REQUIRED) {
throw new Error("Couldn't connect. Sharding is required!")
} else if (event.code === GatewayCloseCodes.INVALID_API_VERSION) {
throw new Error("Invalid API Version was used. This shouldn't happen!")
} else if (event.code === GatewayCloseCodes.INVALID_INTENTS) {
throw new Error('Invalid Intents')
} else if (event.code === GatewayCloseCodes.DISALLOWED_INTENTS) {
throw new Error("Given Intents aren't allowed")
} else {
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)
}
}
private onerror(event: Event | ErrorEvent): void {
const eventError = event as ErrorEvent
console.log(eventError)
}
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 (forceNewSession === undefined || !forceNewSession) {
const sessionIDCached = await this.cache.get('session_id')
if (sessionIDCached !== undefined) {
this.debug(`Found Cached SessionID: ${sessionIDCached}`)
this.sessionID = sessionIDCached
return await this.sendResume()
}
}
const payload: IdentityPayload = {
token: this.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(
(previous, current) => previous | current,
0
),
presence: this.client.presence.create()
}
this.debug('Sending Identify payload...')
this.send({
op: GatewayOpcodes.IDENTIFY,
d: payload
})
}
private async sendResume(): Promise<void> {
if (this.sessionID === undefined) {
this.sessionID = await this.cache.get('session_id')
if (this.sessionID === undefined) return await this.sendIdentify()
}
this.debug(`Preparing to resume with Session: ${this.sessionID}`)
if (this.sequenceID === undefined) {
const cached = await this.cache.get('seq')
if (cached !== undefined)
this.sequenceID = typeof cached === 'string' ? parseInt(cached) : cached
}
const resumePayload = {
op: GatewayOpcodes.RESUME,
d: {
token: this.token,
session_id: this.sessionID,
seq: this.sequenceID ?? null
}
}
this.debug('Sending Resume payload...')
this.send(resumePayload)
}
requestMembers(guild: string, options: RequestMembersOptions = {}): string {
if (options.query !== undefined && options.limit === undefined)
throw new Error(
'Missing limit property when specifying query for Requesting Members!'
)
const nonce = `${guild}_${new Date().getTime()}`
this.send({
op: GatewayOpcodes.REQUEST_GUILD_MEMBERS,
d: {
guild_id: guild,
query: options.query,
limit: options.limit,
presences: options.presences,
user_ids: options.users,
nonce
}
})
return nonce
}
updateVoiceState(
guild: Guild | string,
channel?: VoiceChannel | string,
voiceOptions: VoiceStateOptions = {}
): void {
this.send({
op: GatewayOpcodes.VOICE_STATE_UPDATE,
d: {
guild_id: typeof guild === 'string' ? guild : guild.id,
channel_id:
channel === undefined
? null
: typeof channel === 'string'
? channel
: channel?.id,
self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute,
self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf
}
})
}
debug(msg: string): void {
this.client.debug('Gateway', msg)
}
async reconnect(forceNew?: boolean): Promise<void> {
clearInterval(this.heartbeatIntervalID)
if (forceNew === true) {
await this.cache.delete('session_id')
await this.cache.delete('seq')
}
this.close(1000, RECONNECT_REASON)
this.initWebsocket()
}
initWebsocket(): void {
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`,
[]
)
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)
}
close(code: number = 1000, reason?: string): void {
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
})
)
return true
}
sendPresence(data: StatusUpdatePayload): void {
this.send({
op: GatewayOpcodes.PRESENCE_UPDATE,
d: data
})
}
sendHeartbeat(): void {
const payload = {
op: GatewayOpcodes.HEARTBEAT,
d: this.sequenceID ?? null
}
this.send(payload)
this.lastPingTimestamp = Date.now()
}
heartbeat(): void {
if (this.heartbeatServerResponded) {
this.heartbeatServerResponded = false
} else {
this.debug('Found dead connection, reconnecting...')
clearInterval(this.heartbeatIntervalID)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
return
}
this.sendHeartbeat()
}
}
export type GatewayEventHandler = (gateway: Gateway, d: any) => void
export { Gateway }

474
src/gateway/mod.ts Normal file
View File

@ -0,0 +1,474 @@
import { unzlib } from '../../deps.ts'
import type { Client } from '../client/mod.ts'
import { GatewayResponse } from '../types/gatewayResponse.ts'
import {
GatewayOpcodes,
GatewayCloseCodes,
IdentityPayload,
StatusUpdatePayload,
GatewayEvents
} from '../types/gateway.ts'
import { gatewayHandlers } from './handlers/mod.ts'
import { GatewayCache } from '../managers/gatewayCache.ts'
import { delay } from '../utils/delay.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
presences?: boolean
query?: string
users?: string[]
}
export interface VoiceStateOptions {
mute?: boolean
deaf?: boolean
}
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 HarmonyEventEmitter<GatewayTypedEvents> {
websocket?: WebSocket
connected = false
initialized = false
heartbeatInterval = 0
heartbeatIntervalID?: number
sequenceID?: number
lastPingTimestamp = 0
sessionID?: string
private heartbeatServerResponded = false
client!: Client
cache: GatewayCache
private timedIdentify: number | null = null
shards?: number[]
ping: number = 0
constructor(client: Client, shards?: number[]) {
super()
Object.defineProperty(this, 'client', { value: client, enumerable: false })
this.cache = new GatewayCache(client)
this.shards = shards
}
private onopen(): void {
this.connected = true
this.debug('Connected to Gateway!')
this.emit('connect')
}
private async onmessage(event: MessageEvent): Promise<void> {
let data = event.data
if (data instanceof ArrayBuffer) {
data = new Uint8Array(data)
}
if (data instanceof Uint8Array) {
data = unzlib(data)
data = decodeText(data)
}
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
switch (op) {
case GatewayOpcodes.HELLO:
this.heartbeatInterval = d.heartbeat_interval
this.debug(
`Received HELLO. Heartbeat Interval: ${this.heartbeatInterval}`
)
this.sendHeartbeat()
this.heartbeatIntervalID = setInterval(() => {
this.heartbeat()
}, this.heartbeatInterval)
if (!this.initialized) {
this.initialized = true
this.enqueueIdentify(this.client.forceNewSession)
} else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendResume()
}
break
case GatewayOpcodes.HEARTBEAT_ACK:
this.heartbeatServerResponded = true
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:
// Because we know this gonna be bool
this.debug(
`Invalid Session received! Resumeable? ${d === true ? 'Yes' : 'No'}`
)
if (d !== true) {
this.debug(`Session was invalidated, deleting from cache`)
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
this.enqueueIdentify(!(d as boolean))
}, 5000)
break
case GatewayOpcodes.DISPATCH: {
this.heartbeatServerResponded = true
if (s !== null) {
this.sequenceID = s
await this.cache.set(`seq_${this.shards?.join('-') ?? '0'}`, s)
}
if (t !== null && t !== undefined) {
this.emit(t as any, d)
this.client.emit('raw', t, d)
const handler = gatewayHandlers[t]
if (handler !== undefined && d !== null) {
handler(this, d)
}
}
break
}
case GatewayOpcodes.RESUME: {
// this.token = d.token
this.sessionID = d.session_id
this.sequenceID = d.seq
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')
this.debug('Received OpCode RECONNECT')
await this.reconnect()
break
}
default:
break
}
}
private async onclose({ reason, code }: CloseEvent): Promise<void> {
if (reason === RECONNECT_REASON) return
this.emit('close', code, reason)
this.debug(`Connection Closed with code: ${code}`)
switch (code) {
case GatewayCloseCodes.UNKNOWN_ERROR:
this.debug('API has encountered Unknown Error. Reconnecting...')
await this.reconnect()
break
case GatewayCloseCodes.UNKNOWN_OPCODE:
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:
throw new Error('Not Authorized: Payload was sent before Identifying.')
case GatewayCloseCodes.AUTHENTICATION_FAILED:
throw new Error('Invalid Token provided!')
case GatewayCloseCodes.INVALID_SEQ:
this.debug('Invalid Seq was sent. Reconnecting.')
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.')
await this.reconnect(true)
break
case GatewayCloseCodes.INVALID_SHARD:
this.debug('Invalid Shard was sent. Reconnecting.')
await this.reconnect()
break
case GatewayCloseCodes.SHARDING_REQUIRED:
throw new Error("Couldn't connect. Sharding is required!")
case GatewayCloseCodes.INVALID_API_VERSION:
throw new Error("Invalid API Version was used. This shouldn't happen!")
case GatewayCloseCodes.INVALID_INTENTS:
throw new Error('Invalid Intents')
case GatewayCloseCodes.DISALLOWED_INTENTS:
throw new Error("Given Intents aren't allowed")
default:
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 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> {
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_${this.shards?.join('-') ?? '0'}`
)
if (sessionIDCached !== undefined) {
this.debug(`Found Cached SessionID: ${sessionIDCached}`)
this.sessionID = sessionIDCached
return await this.sendResume()
}
}
const payload: IdentityPayload = {
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:
this.shards === undefined
? [0, 1]
: [this.shards[0] ?? 0, this.shards[1] ?? 1],
intents: this.client.intents.reduce(
(previous, current) => previous | current,
0
),
presence: this.client.presence.create()
}
this.debug('Sending Identify payload...')
this.emit('sentIdentify')
this.send({
op: GatewayOpcodes.IDENTIFY,
d: payload
})
}
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_${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_${this.shards?.join('-') ?? '0'}`
)
if (cached !== undefined)
this.sequenceID = typeof cached === 'string' ? parseInt(cached) : cached
}
const resumePayload = {
op: GatewayOpcodes.RESUME,
d: {
token: this.client.token,
session_id: this.sessionID,
seq: this.sequenceID ?? null
}
}
this.emit('sentResume')
this.debug('Sending Resume payload...')
this.send(resumePayload)
}
requestMembers(guild: string, options: RequestMembersOptions = {}): string {
if (options.query !== undefined && options.limit === undefined)
throw new Error(
'Missing limit property when specifying query for Requesting Members!'
)
const nonce = `${guild}_${new Date().getTime()}`
this.send({
op: GatewayOpcodes.REQUEST_GUILD_MEMBERS,
d: {
guild_id: guild,
query: options.query ?? '',
limit: options.limit ?? 0,
presences: options.presences,
user_ids: options.users,
nonce
}
})
return nonce
}
updateVoiceState(
guild: Guild | string,
channel?: VoiceChannel | string,
voiceOptions: VoiceStateOptions = {}
): void {
this.send({
op: GatewayOpcodes.VOICE_STATE_UPDATE,
d: {
guild_id: typeof guild === 'string' ? guild : guild.id,
channel_id:
channel === undefined
? null
: typeof channel === 'string'
? channel
: channel?.id,
self_mute:
channel === undefined
? false
: voiceOptions.mute === undefined
? false
: voiceOptions.mute,
self_deaf:
channel === undefined
? false
: voiceOptions.deaf === undefined
? false
: voiceOptions.deaf
}
})
}
debug(msg: string): void {
this.client.debug('Gateway', msg)
}
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_${this.shards?.join('-') ?? '0'}`)
await this.cache.delete(`seq_${this.shards?.join('-') ?? '0'}`)
}
this.closeGateway(1000, RECONNECT_REASON)
this.initWebsocket()
}
initWebsocket(): void {
this.emit('init')
this.debug('Initializing WebSocket...')
this.websocket = new WebSocket(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${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) as any
}
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
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
}
sendPresence(data: StatusUpdatePayload): void {
this.send({
op: GatewayOpcodes.PRESENCE_UPDATE,
d: data
})
}
sendHeartbeat(): void {
const payload = {
op: GatewayOpcodes.HEARTBEAT,
d: this.sequenceID ?? null
}
this.send(payload)
this.lastPingTimestamp = Date.now()
}
heartbeat(): void {
if (this.heartbeatServerResponded) {
this.heartbeatServerResponded = false
} else {
this.debug('Found dead connection, reconnecting...')
clearInterval(this.heartbeatIntervalID)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect()
return
}
this.sendHeartbeat()
}
}
export type GatewayEventHandler = (gateway: Gateway, d: any) => void

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

@ -0,0 +1,352 @@
import { RESTManager } from '../rest/manager.ts'
import type { Guild } from '../structures/guild.ts'
import {
SlashCommandChoice,
SlashCommandOption,
SlashCommandOptionType,
SlashCommandPartial,
SlashCommandPayload
} from '../types/slashCommands.ts'
import { Collection } from '../utils/collection.ts'
import type { SlashClient, SlashCommandHandlerCallback } from './slashClient.ts'
export class SlashCommand {
slash: SlashCommandsManager
id: string
applicationID: string
name: string
description: string
options: SlashCommandOption[]
guild?: Guild
_guild?: string
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> {
await this.slash.delete(this.id, this._guild)
}
async edit(data: SlashCommandPartial): Promise<void> {
await this.slash.edit(this.id, data, this._guild)
}
/** Create a handler for this Slash Command */
handle(
func: SlashCommandHandlerCallback,
options?: { parent?: string; group?: string }
): SlashCommand {
this.slash.slash.handle({
name: this.name,
parent: options?.parent,
group: options?.group,
guild: this._guild,
handler: func
})
return this
}
}
export interface CreateOptions {
name: string
description?: string
options?: Array<SlashCommandOption | SlashOptionCallable>
choices?: Array<SlashCommandChoice | string>
}
function createSlashOption(
type: SlashCommandOptionType,
data: CreateOptions
): SlashCommandOption {
return {
name: data.name,
type,
description:
type === 0 || type === 1
? undefined
: data.description ?? 'No description.',
options: data.options?.map((e) =>
typeof e === 'function' ? e(SlashOption) : e
),
choices:
data.choices === undefined
? undefined
: data.choices.map((e) =>
typeof e === 'string' ? { name: e, value: e } : e
)
}
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class SlashOption {
static string(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.STRING, data)
}
static bool(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.BOOLEAN, data)
}
static subCommand(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.SUB_COMMAND, data)
}
static subCommandGroup(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.SUB_COMMAND_GROUP, data)
}
static role(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.ROLE, data)
}
static channel(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.CHANNEL, data)
}
static user(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.USER, data)
}
static number(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.INTEGER, data)
}
}
export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption
export type SlashBuilderOptionsData =
| Array<SlashCommandOption | SlashOptionCallable>
| {
[name: string]:
| {
description: string
type: SlashCommandOptionType
options?: SlashCommandOption[]
choices?: SlashCommandChoice[]
}
| SlashOptionCallable
}
function buildOptionsArray(
options: SlashBuilderOptionsData
): SlashCommandOption[] {
return Array.isArray(options)
? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op))
: Object.entries(options).map((entry) =>
typeof entry[1] === 'function'
? entry[1](SlashOption)
: Object.assign(entry[1], { name: entry[0] })
)
}
/** Slash Command Builder */
export class SlashBuilder {
data: SlashCommandPartial
constructor(
name?: string,
description?: string,
options?: SlashBuilderOptionsData
) {
this.data = {
name: name ?? '',
description: description ?? 'No description.',
options: options === undefined ? [] : buildOptionsArray(options)
}
}
name(name: string): SlashBuilder {
this.data.name = name
return this
}
description(desc: string): SlashBuilder {
this.data.description = desc
return this
}
option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder {
if (this.data.options === undefined) this.data.options = []
this.data.options.push(
typeof option === 'function' ? option(SlashOption) : option
)
return this
}
options(options: SlashBuilderOptionsData): SlashBuilder {
this.data.options = buildOptionsArray(options)
return this
}
export(): SlashCommandPartial {
if (this.data.name === '')
throw new Error('Name was not provided in Slash Builder')
return this.data
}
}
/** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */
export class SlashCommandsManager {
readonly slash!: SlashClient
readonly rest!: RESTManager
constructor(client: SlashClient) {
Object.defineProperty(this, 'slash', { value: client, enumerable: false })
Object.defineProperty(this, 'rest', {
enumerable: false,
value: client.rest
})
}
/** Get all Global Slash Commands */
async all(): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()
const res = (await this.rest.api.applications[
this.slash.getID()
].commands.get()) as SlashCommandPayload[]
if (!Array.isArray(res)) return col
for (const raw of res) {
const cmd = new SlashCommand(this, raw)
col.set(raw.id, cmd)
}
return col
}
/** Get a Guild's Slash Commands */
async guild(
guild: Guild | string
): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()
const res = (await this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].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, _guild)
cmd._guild = typeof guild === 'string' ? guild : guild.id
col.set(raw.id, cmd)
}
return col
}
/** Create a Slash Command (global or Guild) */
async create(
data: SlashCommandPartial,
guild?: Guild | string
): Promise<SlashCommand> {
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
const payload = await route.post(data)
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
return cmd
}
/** Edit a Slash Command (global or Guild) */
async edit(
id: string,
data: SlashCommandPartial,
guild?: Guild | string
): Promise<SlashCommandsManager> {
const route =
guild === undefined
? this.rest.api.applications[this.slash.getID()].commands[id]
: this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].commands[id]
await route.patch(data)
return this
}
/** Delete a Slash Command (global or Guild) */
async delete(
id: string,
guild?: Guild | string
): Promise<SlashCommandsManager> {
const route =
guild === undefined
? this.rest.api.applications[this.slash.getID()].commands[id]
: this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].commands[id]
await route.delete()
return this
}
/** Get a Slash Command (global or Guild) */
async get(id: string, guild?: Guild | string): Promise<SlashCommand> {
const route =
guild === undefined
? this.rest.api.applications[this.slash.getID()].commands[id]
: this.rest.api.applications[this.slash.getID()].guilds[
typeof guild === 'string' ? guild : guild.id
].commands[id]
const data = await route.get()
const _guild =
typeof guild === 'object'
? guild
: guild === undefined
? undefined
: await this.slash.client?.guilds.get(guild)
return new SlashCommand(this, data, _guild)
}
/** 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)
return this
}
}

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
}
@ -37,12 +37,12 @@ export class BaseManager<T, T2> {
}
/** Deletes a key from Cache */
async delete(key: string): Promise<boolean> {
async _delete(key: string): Promise<boolean> {
return this.client.cache.delete(this.cacheName, key)
}
/** Gets an Array of values from Cache */
async array(): Promise<undefined | T2[]> {
async array(): Promise<T2[]> {
let arr = await (this.client.cache.array(this.cacheName) as T[])
if (arr === undefined) arr = []
return arr.map((e) => new this.DataType(this.client, e)) as any
@ -60,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)
@ -25,10 +50,11 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
return res as any
}
async array(): Promise<undefined | Channel[]> {
async array(): Promise<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 {
GuildChannelCategoryPayload,
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
| GuildChannelCategoryPayload
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,17 +36,18 @@ 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
}
/** Delete a Guild Channel */
async delete(id: string): Promise<boolean> {
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
@ -50,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,12 +1,12 @@
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 { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
import type { EmojisManager } from './emojis.ts'
import { fetchAuto } from '../../deps.ts'
export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
guild: Guild
@ -87,7 +87,8 @@ export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
async flush(): Promise<boolean> {
const arr = await this.array()
for (const elem of arr) {
this.parent.delete(elem.id)
const emojiID = elem.id !== null ? elem.id : elem.name
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,19 +17,26 @@ 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, {
user: ((await this.client.users.get(raw.user_id)) as unknown) as User,
channel:
raw.channel_id == null
raw.channel_id === null
? null
: (((await this.client.channels.get<VoiceChannel>(
raw.channel_id
@ -40,6 +47,37 @@ export class GuildVoiceStatesManager extends BaseManager<
})
}
async array(): Promise<VoiceState[]> {
let arr = await (this.client.cache.array(
this.cacheName
) as VoiceStatePayload[])
if (arr === undefined) arr = []
return await Promise.all(
arr.map(async (raw) => {
const guild =
raw.guild_id === undefined
? this.guild
: await this.client.guilds.get(raw.guild_id)
return new VoiceState(this.client, raw, {
user: ((await this.client.users.get(raw.user_id)) as unknown) as User,
channel:
raw.channel_id === null
? null
: (((await this.client.channels.get<VoiceChannel>(
raw.channel_id
)) as unknown) as VoiceChannel),
guild,
member:
guild === undefined
? undefined
: await guild.members.get(raw.user_id)
})
})
)
}
async fromPayload(d: VoiceStatePayload[]): Promise<void> {
for (const data of d) {
await this.set(data.user_id, data)

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 { GUILD_INVITES } 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
@ -19,19 +33,71 @@ export class InviteManager extends BaseManager<InvitePayload, Invite> {
return new Invite(this.client, raw)
}
async fetch(id: string): Promise<Invite | undefined> {
/** Fetch an Invite */
async fetch(id: string, withCounts: boolean = true): Promise<Invite> {
return await new Promise((resolve, reject) => {
this.client.rest
.get(GUILD_INVITES(this.guild.id))
.get(`${INVITE(id)}${withCounts ? '?with_counts=true' : ''}`)
.then(async (data) => {
this.set(id, data as InvitePayload)
const newInvite = await this.get(data.code)
resolve(newInvite)
resolve(newInvite as Invite)
})
.catch((e) => reject(e))
})
}
/** 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
}
@ -60,7 +60,7 @@ export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
true
)
return res.status === 204
return res.response.status === 204
}
async remove(role: string | Role): Promise<boolean> {
@ -76,6 +76,6 @@ export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
true
)
return res.status === 204
return res.response.status === 204
}
}

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'
@ -37,6 +37,28 @@ export class MembersManager extends BaseManager<MemberPayload, Member> {
return res
}
async array(): Promise<Member[]> {
let arr = await (this.client.cache.array(this.cacheName) as MemberPayload[])
if (arr === undefined) arr = []
return await Promise.all(
arr.map(async (raw) => {
const user = new User(this.client, raw.user)
const roles = await this.guild.roles.array()
let permissions = new Permissions(Permissions.DEFAULT)
if (roles !== undefined) {
const mRoles = roles.filter(
(r) =>
(raw.roles.includes(r.id) as boolean) || r.id === this.guild.id
)
permissions = new Permissions(mRoles.map((r) => r.permissions))
}
return new Member(this.client, raw, user, this.guild, permissions)
})
)
}
/** Fetch a Guild Member */
async fetch(id: string): Promise<Member> {
return await new Promise((resolve, reject) => {
this.client.rest

View File

@ -1,9 +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 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<
@ -13,17 +18,28 @@ 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
let emoji = await this.client.emojis.get(raw.emoji.id)
const emojiID = raw.emoji.id !== null ? raw.emoji.id : raw.emoji.name
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
}
@ -37,8 +53,60 @@ export class MessageReactionsManager extends BaseManager<
)
}
async array(): Promise<MessageReaction[]> {
let arr = await (this.client.cache.array(this.cacheName) as Reaction[])
if (arr === undefined) arr = []
return await Promise.all(
arr.map(async (raw) => {
const emojiID = raw.emoji.id !== null ? raw.emoji.id : raw.emoji.name
let emoji = await this.client.emojis.get(emojiID as string)
if (emoji === undefined) emoji = new Emoji(this.client, raw.emoji)
return new MessageReaction(this.client, raw, this.message, emoji)
})
)
}
async flush(): Promise<any> {
await this.client.cache.deleteCache(`reaction_users:${this.message.id}`)
return this.client.cache.deleteCache(this.cacheName)
}
/** Remove all Reactions from the Message */
async removeAll(): Promise<void> {
await this.client.rest.delete(
MESSAGE_REACTIONS(this.message.channel.id, this.message.id)
)
}
/** Remove a specific Emoji from Reactions */
async removeEmoji(emoji: Emoji | string): Promise<MessageReactionsManager> {
const val = encodeURIComponent(
(typeof emoji === 'object' ? emoji.id ?? emoji.name : emoji) as string
)
await this.client.rest.delete(
MESSAGE_REACTION(this.message.channel.id, this.message.id, val)
)
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'
@ -44,6 +44,42 @@ export class MessagesManager extends BaseManager<MessagePayload, Message> {
)
}
async array(): Promise<Message[]> {
let arr = await (this.client.cache.array(
this.cacheName
) as MessagePayload[])
if (arr === undefined) arr = []
const result: Message[] = []
await Promise.all(
arr.map(async (raw) => {
if (raw.author === undefined) return
let channel = await this.client.channels.get(raw.channel_id)
if (channel === undefined)
channel = await this.client.channels.fetch(raw.channel_id)
if (channel === undefined) return
let author = ((await this.client.users.get(
raw.author.id
)) as unknown) as User
if (author === undefined) author = new User(this.client, raw.author)
const res = new Message(
this.client,
raw,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
channel as TextChannel,
author
)
await res.mentions.fromPayload(raw)
result.push(res)
})
)
return result
}
async fetch(id: string): Promise<Message> {
return await new Promise((resolve, reject) => {
this.client.rest
@ -57,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<
@ -27,6 +27,25 @@ export class GuildPresencesManager extends BaseManager<
return presence
}
async array(): Promise<Presence[]> {
let arr = await (this.client.cache.array(
this.cacheName
) as PresenceUpdatePayload[])
if (arr === undefined) arr = []
const result: Presence[] = []
await Promise.all(
arr.map(async (raw) => {
let user = await this.client.users.get(raw.user.id)
if (user === undefined) user = new User(this.client, raw.user)
const guild = await this.client.guilds.get(raw.guild_id)
if (guild === undefined) return
result.push(new Presence(this.client, raw, user, guild))
})
)
return result
}
async fromPayload(
data: PresenceUpdatePayload[]
): Promise<GuildPresencesManager> {

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,10 +1,19 @@
import { Client } from '../models/client.ts'
import { Guild } from '../structures/guild.ts'
import { Permissions } from '../../mod.ts'
import type { Client } from '../client/mod.ts'
import type { Guild } from '../structures/guild.ts'
import { Role } from '../structures/role.ts'
import { GUILD_ROLE } from '../types/endpoint.ts'
import { RolePayload } from '../types/role.ts'
import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts'
import type { RoleModifyPayload, RolePayload } from '../types/role.ts'
import { BaseManager } from './base.ts'
export interface CreateGuildRoleOptions {
name?: string
permissions?: number | string | Permissions
color?: number | string
hoist?: boolean
mentionable?: boolean
}
export class RolesManager extends BaseManager<RolePayload, Role> {
guild: Guild
@ -13,22 +22,108 @@ export class RolesManager extends BaseManager<RolePayload, Role> {
this.guild = guild
}
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((data) => {
this.set(id, data as RolePayload)
resolve(new Role(this.client, data as RolePayload))
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)
}
return true
}
/** Create a Guild Role */
async create(data?: CreateGuildRoleOptions): Promise<Role> {
if (typeof data?.color === 'string') {
if (data.color.startsWith('#')) data.color = data.color.slice(1)
}
const roleRaw = ((await this.client.rest.post(GUILD_ROLES(this.guild.id), {
name: data?.name,
permissions:
data?.permissions === undefined
? undefined
: (typeof data.permissions === 'object'
? data.permissions.bitfield
: data.permissions
).toString(),
color:
data?.color === undefined
? undefined
: typeof data.color === 'string'
? isNaN(parseInt(data.color, 16))
? 0
: parseInt(data.color, 16)
: data.color,
hoist: data?.hoist ?? false,
mentionable: data?.mentionable ?? false
})) as unknown) as RolePayload
await this.set(roleRaw.id, roleRaw)
return ((await this.get(roleRaw.id)) as unknown) as Role
}
/** Delete a Guild Role */
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 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,291 +0,0 @@
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 'https://deno.land/std@0.74.0/node/events.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 { ClientEvents } from '../gateway/handlers/index.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'
/** 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 {
/** 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
}
/**
* 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[]) => any }
_decoratedSlash?: Array<{
name: string
guild?: string
parent?: string
group?: string
handler: (interaction: Interaction) => any
}>
_decoratedSlashModules?: SlashModule[]
private readonly _untypedOn = this.on
private readonly _untypedEmit = this.emit
public on = <K extends string>(event: K, listener: ClientEvents[K]): this =>
this._untypedOn(event, listener)
public emit = <K extends string>(
event: K,
...args: Parameters<ClientEvents[K]>
): boolean => this._untypedEmit(event, ...args)
/** Shard on which this Client is */
shard: number = 0
/** Shard Manager of this Client if Sharded */
shardManager?: ShardManager
constructor(options: ClientOptions = {}) {
super()
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(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}`)
}
// TODO(DjDeveloperr): Implement this
// fetchApplication(): Promise<Application>
/**
* 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)
}
}
export function event(name?: string) {
return function (client: Client | Extension, prop: string) {
const listener = ((client as unknown) as {
[name: string]: (...args: any[]) => any
})[prop]
if (typeof listener !== 'function')
throw new Error('@event decorator requires a function')
if (client._decoratedEvents === undefined) client._decoratedEvents = {}
client._decoratedEvents[name === undefined ? prop : name] = listener
}
}
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
})
}
}
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
})
}
}
export function groupslash(
parent: string,
group: 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
item.group = group
client._decoratedSlash.push(item)
} else
client._decoratedSlash.push({
group,
parent,
name: name ?? prop,
guild,
handler: item
})
}
}
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)
}
}

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