Compare commits

...

161 Commits

Author SHA1 Message Date
semantic-release-bot
2c8740e489 chore(release): 1.5.2 [skip ci]
## @revanced/discord-bot [1.5.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.5.1...@revanced/discord-bot@1.5.2) (2025-09-25)

### Bug Fixes

* **bots/discord:** correct `respondToReply` logic ([6fe1530](6fe15301a2))
2025-09-25 16:19:07 +00:00
PalmDevs
6fe15301a2 fix(bots/discord): correct respondToReply logic 2025-09-25 23:17:29 +07:00
semantic-release-bot
6885e18976 chore(release): 1.5.1 [skip ci]
## @revanced/discord-bot [1.5.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.5.0...@revanced/discord-bot@1.5.1) (2025-09-13)

### Bug Fixes

* **bots/discord:** only fetch reference when it exists when `respondToReply` is set ([42038e6](42038e6b38))
2025-09-13 17:34:48 +00:00
Palm
42038e6b38 fix(bots/discord): only fetch reference when it exists when respondToReply is set
whoops, testing in production
2025-09-14 00:34:00 +07:00
semantic-release-bot
51c0252b44 chore(release): 1.5.0 [skip ci]
# @revanced/discord-bot [1.5.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.4.1...@revanced/discord-bot@1.5.0) (2025-09-13)

### Features

* **bots/discord:** add additional options for `respondToReply` ([399c201](399c201f8c))
2025-09-13 13:43:19 +00:00
PalmDevs
399c201f8c feat(bots/discord): add additional options for respondToReply 2025-09-13 20:42:34 +07:00
semantic-release-bot
0a5a9c3e27 chore(release): 1.4.1 [skip ci]
## @revanced/discord-bot [1.4.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.4.0...@revanced/discord-bot@1.4.1) (2025-09-13)

### Bug Fixes

* **bots/discord/database:** use non-deprecated way to create indexes ([8cc2377](8cc2377cbf))
* **bots/discord:** allow partial users ([dd3e7d2](dd3e7d2ee0))
* **bots/discord:** error in cache `keepOverLimit` comparison ([567c5d2](567c5d2c7f))
2025-09-13 12:28:35 +00:00
PalmDevs
50a205c430 chore: update dependencies 2025-09-13 19:27:45 +07:00
PalmDevs
dd3e7d2ee0 fix(bots/discord): allow partial users 2025-09-13 19:27:43 +07:00
PalmDevs
567c5d2c7f fix(bots/discord): error in cache keepOverLimit comparison 2025-09-13 19:27:42 +07:00
PalmDevs
8cc2377cbf fix(bots/discord/database): use non-deprecated way to create indexes 2025-09-13 19:27:41 +07:00
semantic-release-bot
f3c199d573 chore(release): 1.4.0 [skip ci]
# @revanced/discord-bot [1.4.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.3.0...@revanced/discord-bot@1.4.0) (2025-07-11)

### Bug Fixes

* **bots/discord:** pass non-empty out of scope label to discord ([fbd9480](fbd9480036))

### Features

* **bots/discord:** react to label classified response ([96a9b83](96a9b83c48))
2025-07-11 18:23:24 +00:00
PalmDevs
42c0facef1 chore: format 2025-07-12 01:22:30 +07:00
PalmDevs
96a9b83c48 feat(bots/discord): react to label classified response 2025-07-12 01:22:28 +07:00
PalmDevs
fbd9480036 fix(bots/discord): pass non-empty out of scope label to discord 2025-07-12 01:22:27 +07:00
semantic-release-bot
cc02c0a775 chore(release): 1.3.0 [skip ci]
# @revanced/discord-bot [1.3.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.3...@revanced/discord-bot@1.3.0) (2025-07-11)

### Features

* **bots/discord:** support training without label ([c68cfd1](c68cfd1c01))
2025-07-11 18:08:35 +00:00
semantic-release-bot
c83e219088 chore(release): 1.1.0 [skip ci]
# @revanced/bot-websocket-api [1.1.0](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.2...@revanced/bot-websocket-api@1.1.0) (2025-07-11)

### Features

* **apis/websocket:** support training without label ([670cc70](670cc70057))
2025-07-11 18:07:46 +00:00
PalmDevs
6164e6c1a5 chore: format 2025-07-12 01:06:45 +07:00
PalmDevs
47d2f8c015 chore: update dependencies and remove outdated patches 2025-07-12 01:06:43 +07:00
PalmDevs
c68cfd1c01 feat(bots/discord): support training without label 2025-07-12 01:06:41 +07:00
PalmDevs
2b1928e116 feat(packages/api): support training without label 2025-07-12 01:06:39 +07:00
PalmDevs
670cc70057 feat(apis/websocket): support training without label 2025-07-12 01:06:37 +07:00
Palm
439f301872 chore: merge dev to main 2025-07-11 21:05:10 +07:00
Pun Butrach
aeccf12906 ci: Use default fetch-depth of 1 for checkout step (#47) 2025-07-08 18:14:54 +02:00
semantic-release-bot
b4ac8433f4 chore(release): 1.2.3 [skip ci]
## @revanced/discord-bot [1.2.3](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.2...@revanced/discord-bot@1.2.3) (2025-06-23)

### Bug Fixes

* **bots/discord:** disable unneeded cache, enable message cache sweeping ([3a0f0fe](3a0f0fe786))
2025-06-23 13:33:41 +00:00
semantic-release-bot
b01a4accee chore(release): 1.0.2 [skip ci]
## @revanced/bot-websocket-api [1.0.2](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.1...@revanced/bot-websocket-api@1.0.2) (2025-06-23)
2025-06-23 13:32:58 +00:00
PalmDevs
858e989bdc build(Needs bump): update dependencies 2025-06-23 20:32:18 +07:00
PalmDevs
3a0f0fe786 fix(bots/discord): disable unneeded cache, enable message cache sweeping 2025-06-23 20:30:28 +07:00
semantic-release-bot
409e850768 chore(release): 1.2.2 [skip ci]
## @revanced/discord-bot [1.2.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.1...@revanced/discord-bot@1.2.2) (2025-06-10)

### Bug Fixes

* **bots/discord:** use intervals for checking expired presets ([6e89b87](6e89b874cd))
2025-06-10 17:40:44 +00:00
PalmDevs
6e89b874cd fix(bots/discord): use intervals for checking expired presets 2025-06-11 00:40:08 +07:00
semantic-release-bot
b4a5c62549 chore(release): 1.2.1 [skip ci]
## @revanced/discord-bot [1.2.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.0...@revanced/discord-bot@1.2.1) (2025-05-02)

### Bug Fixes

* **bots/discord:** fix timeout overflow check for role presets ([495f686](495f686292))
2025-05-02 16:11:20 +00:00
PalmDevs
495f686292 fix(bots/discord): fix timeout overflow check for role presets 2025-05-02 23:01:37 +07:00
semantic-release-bot
71eb11b67f chore(release): 1.2.0 [skip ci]
# @revanced/discord-bot [1.2.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.1.2...@revanced/discord-bot@1.2.0) (2025-05-02)

### Features

* **bots/discord:** switch duration parser to `@sapphire/duration` ([04ce825](04ce8252c0))
2025-05-02 15:58:00 +00:00
PalmDevs
04ce8252c0 feat(bots/discord): switch duration parser to @sapphire/duration 2025-05-02 22:57:13 +07:00
semantic-release-bot
27e06db1d9 chore(release): 1.1.2 [skip ci]
## @revanced/discord-bot [1.1.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.1.1...@revanced/discord-bot@1.1.2) (2025-04-16)

### Bug Fixes

* **bots/discord/commands/admin/reload:** fix type error ([3908854](3908854fe0))
* **bots/discord/commands/moderation:** check if timeout amount is safe in role-preset commands ([0c1382c](0c1382c558))
* **bots/discord:** replace use of deprecated `options.ephemeral` in replies ([31e5cf7](31e5cf7fc5))
2025-04-16 13:41:16 +00:00
PalmDevs
3908854fe0 fix(bots/discord/commands/admin/reload): fix type error 2025-04-16 20:40:16 +07:00
PalmDevs
0c1382c558 fix(bots/discord/commands/moderation): check if timeout amount is safe in role-preset commands 2025-04-16 20:40:14 +07:00
PalmDevs
410d289297 ci(release): hash new lockfile for dependency caching 2025-04-16 20:40:12 +07:00
PalmDevs
31e5cf7fc5 fix(bots/discord): replace use of deprecated options.ephemeral in replies 2025-04-16 20:40:11 +07:00
semantic-release-bot
4e797a2cfd chore(release): 1.1.1 [skip ci]
## @revanced/discord-bot [1.1.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.1.0...@revanced/discord-bot@1.1.1) (2025-04-14)

### Bug Fixes

* **bots/discord:** fix sticky msg force timer always starting, add more logging ([cb4dc42](cb4dc42dfa))
2025-04-14 14:09:38 +00:00
PalmDevs
cb4dc42dfa fix(bots/discord): fix sticky msg force timer always starting, add more logging 2025-04-14 21:08:47 +07:00
semantic-release-bot
33ba5b1f61 chore(release): 1.1.0 [skip ci]
# @revanced/discord-bot [1.1.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.4...@revanced/discord-bot@1.1.0) (2025-04-14)

### Features

* **bots/discord:** delete and send sticky msg concurrently, add more logging ([247a00f](247a00f57f))
2025-04-14 14:01:23 +00:00
PalmDevs
247a00f57f feat(bots/discord): delete and send sticky msg concurrently, add more logging 2025-04-14 21:00:24 +07:00
semantic-release-bot
0da3c989cd chore(release): 1.0.4 [skip ci]
## @revanced/discord-bot [1.0.4](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.3...@revanced/discord-bot@1.0.4) (2025-04-14)

### Bug Fixes

* **bots/discord:** fix sticky messages logic again ([aa7501c](aa7501c309))
* **bots/discord:** remove expired presets from db if unapplying is not possible ([9d705e5](9d705e580c))
2025-04-14 12:54:56 +00:00
PalmDevs
16d97f409c chore: format 2025-04-14 19:53:58 +07:00
PalmDevs
539025f2d4 chore(bots/discord): separate discord ready event handlers 2025-04-14 19:53:56 +07:00
PalmDevs
9d705e580c fix(bots/discord): remove expired presets from db if unapplying is not possible 2025-04-14 19:53:54 +07:00
PalmDevs
aa7501c309 fix(bots/discord): fix sticky messages logic again 2025-04-14 19:53:53 +07:00
semantic-release-bot
00118b4a1b chore(release): 1.0.3 [skip ci]
## @revanced/discord-bot [1.0.3](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.2...@revanced/discord-bot@1.0.3) (2025-04-09)

### Bug Fixes

* **bots/discord:** attempt to fix sticky messages one last time ([65288ec](65288ec424))
2025-04-09 14:06:27 +00:00
PalmDevs
65288ec424 fix(bots/discord): attempt to fix sticky messages one last time 2025-04-09 21:05:37 +07:00
PalmDevs
a5067889b2 chore: update monorepo README 2025-04-09 21:05:35 +07:00
semantic-release-bot
8efa9091a4 chore(release): 1.0.2 [skip ci]
## @revanced/discord-bot [1.0.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.1...@revanced/discord-bot@1.0.2) (2025-04-09)
2025-04-09 13:48:21 +00:00
semantic-release-bot
0e44bb5ffe chore(release): 1.0.1 [skip ci]
## @revanced/bot-websocket-api [1.0.1](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0...@revanced/bot-websocket-api@1.0.1) (2025-04-09)

### Bug Fixes

* **bots/discord:** attempt to fix sticky messages again ([7564b1a](7564b1a8f0))
2025-04-09 13:47:36 +00:00
PalmDevs
8942d27453 build(Needs bump): fix webhook not triggering 2025-04-09 20:46:53 +07:00
semantic-release-bot
ca3ac64390 chore(release): 1.0.1 [skip ci]
## @revanced/discord-bot [1.0.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0...@revanced/discord-bot@1.0.1) (2025-04-09)

### Bug Fixes

* **bots/discord:** attempt to fix sticky messages again ([7564b1a](7564b1a8f0))
2025-04-09 13:35:59 +00:00
semantic-release-bot
5c2429aed7 chore(release): 1.0.1 [skip ci]
## @revanced/bot-websocket-api [1.0.1](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0...@revanced/bot-websocket-api@1.0.1) (2025-04-09)

### Bug Fixes

* **bots/discord:** attempt to fix sticky messages again ([7564b1a](7564b1a8f0))
2025-04-09 13:35:20 +00:00
PalmDevs
7564b1a8f0 fix(bots/discord): attempt to fix sticky messages again 2025-04-09 20:14:32 +07:00
Palm
5c9f4c6638 ci(release): specify correct permissions for job 2025-04-09 19:53:47 +07:00
Palm
69de750e6a ci(release): use secrets.GITHUB_TOKEN 2025-04-09 19:41:49 +07:00
semantic-release-bot
83f9780f9c chore(release): 1.0.0 [skip ci]
# @revanced/discord-bot 1.0.0 (2025-04-04)

### Bug Fixes

* **bot/discord:** start remove preset timeout for `role-preset` command ([cbf9116](cbf91162e2))
* **bots/discord/commands/eval:** evaluate in current context ([5925d90](5925d90209))
* **bots/discord/commands/mute:** use existing `parseDuration` util function' ([3e07429](3e07429664))
* **bots/discord/commands/unmute:** fix option description and embeds ([7fdf8c0](7fdf8c0dc7))
* **bots/discord/commands:** minor issues ([3b2596e](3b2596e748))
* **bots/discord/commands:** refactor and add checks ([a2bf3ea](a2bf3eade9))
* **bots/discord/database:** fix schema for role presets ([4aa138a](4aa138a9af))
* **bots/discord/scripts/build:** check if dist dir exists before cleaning ([c06033e](c06033e573))
* **bots/discord/scripts:** unintentional escaping on windows ([09dc706](09dc70632d))
* **bots/discord/utils/discord/embeds:** set thumbnail on moderation embeds ([b056291](b056291ad0))
* **bots/discord/utils/discord/rolePresets:** correct property access for presets ([4c6ad11](4c6ad11be3))
* **bots/discord/utils/discord:** add `moderation` module related functions ([7e8270f](7e8270f7d2))
* **bots/discord/utils/duration:** fix empty string returning with duration is 0 ([83c314e](83c314ef5f))
* **bots/discord/utils/duration:** fix specified default unit not working ([d138af4](d138af46d1))
* **bots/discord/utils/duration:** make second the default unit ([5d1af3c](5d1af3c31c))
* **bots/discord:** add GuildMember partial ([8e3946a](8e3946a666))
* **bots/discord:** allow access to `context` in `/eval` and await result ([99f65f0](99f65f07f5))
* **bots/discord:** always true check causing no messages to be scanned ([98ec37b](98ec37b5d1))
* **bots/discord:** apply active role presets if members rejoin ([f50b26b](f50b26b82d))
* **bots/discord:** attempt to fix stuck sticky message timeouts ([3ed5bd1](3ed5bd11ac))
* **bots/discord:** await when putting entries into db ([4da6175](4da6175cf5))
* **bots/discord:** broken regex when prefix set to special characters ([ab62e55](ab62e55e76))
* **bots/discord:** check token before connecting to bot api ([f3e4408](f3e4408aa2))
* **bots/discord:** ci issues causing database to not be auto generated ([673aa18](673aa189be))
* **bots/discord:** clear role presets after they expire ([faa81f4](faa81f4d88))
* **bots/discord:** connect to discord API even if initial bot API connection fails ([6658b58](6658b582db))
* **bots/discord:** contextify object before sandboxing ([062735f](062735f6d5))
* **bots/discord:** correct permission check logic ([dd8872c](dd8872c027))
* **bots/discord:** correct sticky messages logic ([de8bef6](de8bef6520))
* **bots/discord:** correct timer active condition for sticky messages ([96065ff](96065ff175))
* **bots/discord:** correct whitelist logic ([49c29be](49c29bebfb))
* **bots/discord:** cross-device link build errors ([38c0699](38c06997b4))
* **bots/discord:** decrease length of an option in `ban` command ([22d3eea](22d3eea88d))
* **bots/discord:** delete expired appliedPresets entries after unapplying ([14c98e8](14c98e87df))
* **bots/discord:** deployment runtime errors due to minification ([a60c60c](a60c60c0f9))
* **bots/discord:** do decancer after resetting nickname ([0303fe3](0303fe3e36))
* **bots/discord:** do not remove unrelated reactions ([031fd26](031fd26b26))
* **bots/discord:** don't refresh timer if force timer is active for sticky messages ([4abac0c](4abac0c890))
* **bots/discord:** filter out text triggers correctly from image-only scans ([8c0dd67](8c0dd67d03))
* **bots/discord:** fix freeze on prod builds ([8efb549](8efb549453))
* **bots/discord:** fix get response logic ([3261294](3261294822))
* **bots/discord:** fix reload not working ([11582d5](11582d5034))
* **bots/discord:** follow-up if reply is already sent when error ([f75060b](f75060bc9c))
* **bots/discord:** give only removed roles for role presets ([522ad28](522ad28fd8))
* **bots/discord:** hanging process when disconnecting from API too many times ([d31616e](d31616ebcb))
* **bots/discord:** import `config` from context ([763ef25](763ef253f9))
* **bots/discord:** make `/eval` work ([eaa25f2](eaa25f2eb5))
* **bots/discord:** messed up file name for `unmute` command ([399dca7](399dca7153))
* **bots/discord:** only check for member permissions when specified while correcting responses ([b79a1c7](b79a1c7575))
* **bots/discord:** open database as read-write ([c366840](c36684091d))
* **bots/discord:** owners cannot bypass checks on some commands ([39cba97](39cba97341))
* **bots/discord:** parse larger units of durations, fix wrong timestamp in mod embed ([6c8dce0](6c8dce0593))
* **bots/discord:** persist changes in context for eval command ([5b4965d](5b4965dcc7))
* **bots/discord:** provide discord token for `reload` command ([dd21a5a](dd21a5abad))
* **bots/discord:** remove auto-generated files ([fb8af00](fb8af00866))
* **bots/discord:** remove bad text channel checks ([f5939e2](f5939e2528))
* **bots/discord:** remove redundant footer for response embeds ([412e003](412e00317d))
* **bots/discord:** remove usage of macros ([7f27c56](7f27c5607c))
* **bots/discord:** remove useless feature ([d830e48](d830e48bc2))
* **bots/discord:** replace duration parser with a library ([94c4fed](94c4fedc06))
* **bots/discord:** reset counter when reconnected to api, redo message scan filter logic ([d234d79](d234d79310))
* **bots/discord:** revert dist denesting, fixes config not found ([0d4898d](0d4898dae8))
* **bots/discord:** send right response for after regexes ([a7688fa](a7688fa9b9))
* **bots/discord:** set the `label` property correctly for message scans ([6d463df](6d463df586))
* **bots/discord:** set timeout for eligible mutes to unmute faster ([1f5c5a9](1f5c5a92a6))
* **bots/discord:** some configuration values not applying after running `/reload` ([a976dd2](a976dd2acc))
* **bots/discord:** use `APIEmbed` for response config ([35b9448](35b944800a))
* **bots/discord:** use env for initializing database ([af3759c](af3759caf4))
* **bots/discord:** wrong command file path being imported ([fa0159c](fa0159c3a8))
* **bots/discord:** wrong database schema path ([875bd20](875bd209b2))
* config file not being read ([474a8be](474a8be4af))
* **discord-bot:** also execute slash commands ([f0d45b2](f0d45b2c92))
* **discord-bot:** check for role position ([d332043](d332043b1a))
* **discord-bot:** check if the member has the role ([9bff68c](9bff68c8c4))
* **discord-bot:** not executing slash commands ([aa08087](aa0808768b))
* **discord-bot:** only send lowercased text ([7803758](78037580dc))
* dislike button not working properly ([85eba55](85eba55424))
* fix deprecation ([4373ede](4373ede855))
* fix the fiter for the interaction collector ([a9ff003](a9ff00394a))
* fix typings and formatting ([479812e](479812e199))
* ignore message if there's no content ([3cbebc2](3cbebc2842))
* move modules to `/bots` ([cd7156e](cd7156e792))
* other small issues ([bc437a5](bc437a5ec7))
* run projects with `--bun` ([bb2182e](bb2182e707))
* trainAI not using the bin location ([bd29943](bd2994388b))
* update repo url ([a21aa34](a21aa348d7))

### chore

* fix more build issues ([77fefb9](77fefb9bef))

### Features

* add wit.ai support ([1909e2c](1909e2c421))
* **bots/discord/commands/reply:** send stacktrace when failed ([9f1ac37](9f1ac37927))
* **bots/discord/commands:** add `ban` and `unban` commands ([dc4863d](dc4863dc20))
* **bots/discord/commands:** add `eval` command ([e64d1da](e64d1da00c))
* **bots/discord/commands:** add `mute` and `unmute` commands ([c0fa2fe](c0fa2fe1c3))
* **bots/discord/commands:** add `purge` and `role-preset` commands ([fb01ce5](fb01ce5740))
* **bots/discord/commands:** add `reload` command ([6875b32](6875b32fd0))
* **bots/discord/commands:** allow process exception in `exception-test` ([ca47535](ca475356ad))
* **bots/discord/utils/discord/embeds:** expose `applyCommonEmbedStyles` fn ([2d794ed](2d794ede7d))
* **bots/discord/utils/embeds:** make title parameter nullable ([ee885ca](ee885ca758))
* **bots/discord/utils/fs:** use `recursive` option for listing files ([da21e1a](da21e1a6f7))
* **bots/discord/utils:** add duration utility ([a9add9e](a9add9ea9a))
* **bots/discord/utils:** add functions for role presets ([fb32a04](fb32a04ad3))
* **bots/discord/utils:** allow loading commands from custom dir ([8b690b8](8b690b879b))
* **bots/discord:** add `api.disconnectRetryInterval` config ([2f86586](2f86586179))
* **bots/discord:** add `moderation.roles` config to be used in `moderation` commands ([39d5b3a](39d5b3a479))
* **bots/discord:** add `ocrTriggers` resp config, embed footer scan mode ([744a56a](744a56a4fd))
* **bots/discord:** add `replyToReplied` option in response config ([27662ed](27662ed91a))
* **bots/discord:** add `train` commands ([ee90ef2](ee90ef247b))
* **bots/discord:** add a better way to manage databases ([a68d726](a68d726875))
* **bots/discord:** add code to actually scan text files correctly ([80aeb19](80aeb19020))
* **bots/discord:** add default durations for moderation commands ([27d3b39](27d3b39209))
* **bots/discord:** add more fallbacks for decancering ([2e1e009](2e1e009b42))
* **bots/discord:** add more month aliases to duration parser ([c2009ca](c2009ca6d4))
* **bots/discord:** add more options for curing, fix default regex ([1a4ec1e](1a4ec1ece8))
* **bots/discord:** add source ([f9d50a0](f9d50a0a6b))
* **bots/discord:** add sticky messages ([bf66155](bf661556e1))
* **bots/discord:** add trigger to context for eval ([b5f4097](b5f4097538))
* **bots/discord:** allow admins to bypass permission checks ([620f933](620f9339f0))
* **bots/discord:** blacklist and whitelist for filters ([cdb6001](cdb6001955))
* **bots/discord:** cure on every event ([8ff6086](8ff6086028))
* **bots/discord:** don't nest builds in src directory, autogen db when missing ([4834685](4834685186))
* **bots/discord:** framework changes and new features ([646ec8d](646ec8da87))
* **bots/discord:** improve admin commands ([0346741](0346741188))
* **bots/discord:** improve logs ([6abb740](6abb740994))
* **bots/discord:** sanitize `BasicDatabase` inputs ([fd76e0a](fd76e0af72))
* **bots/discord:** support nickname decancering ([1723e8c](1723e8cacf))
* **bots/discord:** switch to `drizzle-orm` ([e204b7b](e204b7b756))
* **bots/discord:** update config ([197d2ac](197d2acea8))
* **bots/discord:** update example config file ([bc9951c](bc9951c9b5))
* **bots/discord:** update to newer command definition framework ([97f2795](97f2795df4))
* discord bot scanning messages ([d1bd3b2](d1bd3b2b7e))
* **discord-bot:** a way to train AI ([355a508](355a50803a))
* **discord-bot:** command handler and train cmd ([6aee8a4](6aee8a4c63))
* **discord-bot:** event handler ([0ad5ece](0ad5ece085))
* GODEL AI ([0ba525c](0ba525c4a5))
* initalize discord bot ([bb4a5a7](bb4a5a77ee))
* initialize helper client ([7f9ca77](7f9ca77e03))
* message buttons for training ([6551ca9](6551ca9dad))
* platform specific responses ([18e57b0](18e57b0c32))
* prettier and eslint ([1c27ccb](1c27ccb17c))
* refactor and new features ([#7](https://github.com/revanced/revanced-bots/issues/7)) ([8b9f45d](8b9f45dc22))
* run bots in one process ([d26d533](d26d533174))
* training and replies changed ([715aa91](715aa918cf))
* **utils/discord/embeds:** allow adding extra fields for moderation embeds ([49ce9a7](49ce9a7ca3))

### BREAKING CHANGES

* In `@revanced/discord-bot`, its environment variable
                 `DATABASE_URL` has been renamed to `DATABASE_PATH`
                 and the `file:` prefix is no longer needed
2025-04-04 17:24:04 +00:00
semantic-release-bot
5d862401a0 chore(release): 1.0.0 [skip ci]
# @revanced/bot-websocket-api 1.0.0 (2025-04-04)

### Bug Fixes

* **apis/websocket:** also include tesseract core files in build ([7dfbf6c](7dfbf6c92c))
* **apis/websocket:** attempt to fix missing remote address ([9b2888b](9b2888b944))
* **apis/websocket:** build and runtime issues ([89d8ab1](89d8ab1ee5))
* **apis/websocket:** builds not working due to dynamic import requirement ([fc7be22](fc7be22c6c))
* **apis/websocket:** don't bundle `tesseract.js` ([51a6fb6](51a6fb65f0))
* **apis/websocket:** fix forever stuck Promise ([168f40d](168f40def6))
* **apis/websocket:** fix undefined error ([2f03800](2f03800c61))
* **apis/websocket:** hardcoded paths in tesseract worker builds ([38e00eb](38e00eb4e5))
* **apis/websocket:** improve logging and error handling ([b6cbe9d](b6cbe9d64c))
* **bots/discord:** hanging process when disconnecting from API too many times ([d31616e](d31616ebcb))
* fix typings and formatting ([479812e](479812e199))
* other small issues ([bc437a5](bc437a5ec7))
* remove error cb handling for `socket.send()` calls ([29544d4](29544d4e01))
* run projects with `--bun` ([bb2182e](bb2182e707))
* **types:** fix issues with typings ([669e24c](669e24ca81))
* update repo url ([a21aa34](a21aa348d7))

### chore

* fix more build issues ([77fefb9](77fefb9bef))

### Features

* **apis/websocket:** clear old client sessions and instances ([43bd0a0](43bd0a021c))
* **apis/websocket:** return `true` for data on a `TrainedMessage` packet ([65add4d](65add4dfee))
* **packages/shared:** add logger factory ([17c6be7](17c6be7bee))

### BREAKING CHANGES

* In `@revanced/discord-bot`, its environment variable
                 `DATABASE_URL` has been renamed to `DATABASE_PATH`
                 and the `file:` prefix is no longer needed
2025-04-04 17:23:29 +00:00
Palm
91bac934ab chore: merge dev to main 2025-04-05 00:22:22 +07:00
semantic-release-bot
d74fba4092 chore(release): 1.0.0-dev.38 [skip ci]
# @revanced/discord-bot [1.0.0-dev.38](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0-dev.37...@revanced/discord-bot@1.0.0-dev.38) (2025-04-04)

### Bug Fixes

* run projects with `--bun` ([bb2182e](bb2182e707))
2025-04-04 16:56:56 +00:00
semantic-release-bot
ce76c5f08e chore(release): 1.0.0-dev.11 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.11](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0-dev.10...@revanced/bot-websocket-api@1.0.0-dev.11) (2025-04-04)

### Bug Fixes

* **apis/websocket:** attempt to fix missing remote address ([9b2888b](9b2888b944))
* run projects with `--bun` ([bb2182e](bb2182e707))
2025-04-04 16:56:18 +00:00
PalmDevs
bb2182e707 fix: run projects with --bun 2025-04-04 23:55:06 +07:00
PalmDevs
9b2888b944 fix(apis/websocket): attempt to fix missing remote address 2025-04-04 23:55:04 +07:00
PalmDevs
ba1a467e20 chore: update deps 2025-04-04 23:55:02 +07:00
semantic-release-bot
c97fffb32f chore(release): 1.0.0-dev.37 [skip ci]
# @revanced/discord-bot [1.0.0-dev.37](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0-dev.36...@revanced/discord-bot@1.0.0-dev.37) (2025-03-08)

### Bug Fixes

* **bots/discord/utils/duration:** fix specified default unit not working ([d138af4](d138af46d1))
2025-03-08 23:34:30 +00:00
PalmDevs
d138af46d1 fix(bots/discord/utils/duration): fix specified default unit not working 2025-03-09 06:33:21 +07:00
semantic-release-bot
75a57b0e16 chore(release): 1.0.0-dev.36 [skip ci]
# @revanced/discord-bot [1.0.0-dev.36](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0-dev.35...@revanced/discord-bot@1.0.0-dev.36) (2025-03-03)

### Bug Fixes

* **bots/discord/scripts/build:** check if dist dir exists before cleaning ([c06033e](c06033e573))
* **bots/discord/utils/duration:** make second the default unit ([5d1af3c](5d1af3c31c))
* **bots/discord:** add GuildMember partial ([8e3946a](8e3946a666))
* **bots/discord:** decrease length of an option in `ban` command ([22d3eea](22d3eea88d))
* **bots/discord:** delete expired appliedPresets entries after unapplying ([14c98e8](14c98e87df))
* fix typings and formatting ([479812e](479812e199))
* update repo url ([a21aa34](a21aa348d7))

### Features

* **bots/discord:** add more month aliases to duration parser ([c2009ca](c2009ca6d4))
2025-03-03 19:40:39 +00:00
PalmDevs
c06033e573 fix(bots/discord/scripts/build): check if dist dir exists before cleaning 2025-03-04 02:39:08 +07:00
semantic-release-bot
d290417ff3 chore(release): 1.0.0-dev.10 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.10](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0-dev.9...@revanced/bot-websocket-api@1.0.0-dev.10) (2025-03-03)

### Bug Fixes

* fix typings and formatting ([479812e](479812e199))
* update repo url ([a21aa34](a21aa348d7))
2025-03-03 19:36:29 +00:00
PalmDevs
a21aa348d7 fix: update repo url 2025-03-04 02:35:19 +07:00
PalmDevs
479812e199 fix: fix typings and formatting 2025-03-04 02:27:48 +07:00
PalmDevs
f6119946f8 chore: update deps 2025-03-04 02:19:06 +07:00
PalmDevs
5d1af3c31c fix(bots/discord/utils/duration): make second the default unit 2025-03-04 02:17:59 +07:00
PalmDevs
14c98e87df fix(bots/discord): delete expired appliedPresets entries after unapplying 2025-03-04 02:15:19 +07:00
PalmDevs
8e3946a666 fix(bots/discord): add GuildMember partial 2025-03-04 02:09:59 +07:00
PalmDevs
c2009ca6d4 feat(bots/discord): add more month aliases to duration parser 2024-10-30 18:56:54 +07:00
PalmDevs
22d3eea88d fix(bots/discord): decrease length of an option in ban command 2024-10-30 18:50:22 +07:00
semantic-release-bot
14d301eeb4 chore(release): 1.0.0-dev.35 [skip ci]
# @revanced/discord-bot [1.0.0-dev.35](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.34...@revanced/discord-bot@1.0.0-dev.35) (2024-10-17)

### Bug Fixes

* **bots/discord:** fix freeze on prod builds ([8efb549](8efb549453))
2024-10-17 18:34:04 +00:00
PalmDevs
8efb549453 fix(bots/discord): fix freeze on prod builds 2024-10-18 01:32:42 +07:00
PalmDevs
79fea8b286 fix: update patches for dependencies 2024-10-18 00:02:06 +07:00
semantic-release-bot
e798a9ef32 chore(release): 1.0.0-dev.34 [skip ci]
# @revanced/discord-bot [1.0.0-dev.34](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.33...@revanced/discord-bot@1.0.0-dev.34) (2024-10-17)

### Bug Fixes

* **bots/discord:** attempt to fix stuck sticky message timeouts ([3ed5bd1](3ed5bd11ac))
* **bots/discord:** fix reload not working ([11582d5](11582d5034))

### Features

* **bots/discord:** add default durations for moderation commands ([27d3b39](27d3b39209))
* **bots/discord:** cure on every event ([8ff6086](8ff6086028))
2024-10-17 16:56:34 +00:00
PalmDevs
7b5d4fa1a2 chore: update dependencies 2024-10-17 23:55:17 +07:00
PalmDevs
11582d5034 fix(bots/discord): fix reload not working 2024-10-17 23:20:02 +07:00
PalmDevs
488d37e65b fix(packages/shareed): add missing imports 2024-10-17 21:47:14 +07:00
PalmDevs
3ed5bd11ac fix(bots/discord): attempt to fix stuck sticky message timeouts 2024-10-17 21:40:41 +07:00
PalmDevs
8ff6086028 feat(bots/discord): cure on every event 2024-10-17 21:39:05 +07:00
PalmDevs
27d3b39209 feat(bots/discord): add default durations for moderation commands 2024-10-17 21:38:54 +07:00
semantic-release-bot
6e181c0e7f chore(release): 1.0.0-dev.33 [skip ci]
# @revanced/discord-bot [1.0.0-dev.33](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.32...@revanced/discord-bot@1.0.0-dev.33) (2024-09-25)

### Features

* **bots/discord:** add trigger to context for eval ([b5f4097](b5f4097538))
2024-09-25 06:02:32 +00:00
PalmDevs
b5f4097538 feat(bots/discord): add trigger to context for eval 2024-09-25 13:00:53 +07:00
semantic-release-bot
f6d2e25130 chore(release): 1.0.0-dev.32 [skip ci]
# @revanced/discord-bot [1.0.0-dev.32](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.31...@revanced/discord-bot@1.0.0-dev.32) (2024-09-25)

### Bug Fixes

* **bots/discord:** contextify object before sandboxing ([062735f](062735f6d5))
2024-09-25 05:50:49 +00:00
PalmDevs
062735f6d5 fix(bots/discord): contextify object before sandboxing 2024-09-25 12:49:29 +07:00
semantic-release-bot
8aefcdb2e8 chore(release): 1.0.0-dev.31 [skip ci]
# @revanced/discord-bot [1.0.0-dev.31](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.30...@revanced/discord-bot@1.0.0-dev.31) (2024-09-25)

### Bug Fixes

* **bots/discord:** persist changes in context for eval command ([5b4965d](5b4965dcc7))
2024-09-25 05:46:18 +00:00
PalmDevs
5b4965dcc7 fix(bots/discord): persist changes in context for eval command 2024-09-25 12:43:42 +07:00
semantic-release-bot
37e64a2eb8 chore(release): 1.0.0-dev.30 [skip ci]
# @revanced/discord-bot [1.0.0-dev.30](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.29...@revanced/discord-bot@1.0.0-dev.30) (2024-09-24)

### Features

* **bots/discord:** improve admin commands ([0346741](0346741188))
2024-09-24 23:48:53 +00:00
PalmDevs
59dd803529 chore(bots/discord): temporarily fix timer statuses being both active 2024-09-25 06:47:15 +07:00
PalmDevs
2ef66fbc87 chore(bots/discord): add more debug logs on messageScan 2024-09-25 06:47:13 +07:00
PalmDevs
0346741188 feat(bots/discord): improve admin commands
- The reload command now properly reloads configuration changes by skipping the configuration cache
- The eval command now sends a file if the output is too long
- The eval command now restricts access to the bot token by removing it and sandboxing the input code execution
2024-09-25 06:45:52 +07:00
semantic-release-bot
e0e40237fa chore(release): 1.0.0-dev.29 [skip ci]
# @revanced/discord-bot [1.0.0-dev.29](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.28...@revanced/discord-bot@1.0.0-dev.29) (2024-09-21)

### Bug Fixes

* **bots/discord:** fix get response logic ([3261294](3261294822))
2024-09-21 21:03:36 +00:00
PalmDevs
d3c56222be chore: update dependencies 2024-09-22 04:02:20 +07:00
PalmDevs
3261294822 fix(bots/discord): fix get response logic 2024-09-22 04:01:36 +07:00
semantic-release-bot
f035994f9e chore(release): 1.0.0-dev.28 [skip ci]
# @revanced/discord-bot [1.0.0-dev.28](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.27...@revanced/discord-bot@1.0.0-dev.28) (2024-09-20)

### Bug Fixes

* **bots/discord:** don't refresh timer if force timer is active for sticky messages ([4abac0c](4abac0c890))
* **bots/discord:** filter out text triggers correctly from image-only scans ([8c0dd67](8c0dd67d03))
2024-09-20 06:53:32 +00:00
PalmDevs
4abac0c890 fix(bots/discord): don't refresh timer if force timer is active for sticky messages 2024-09-20 13:51:56 +07:00
PalmDevs
8c0dd67d03 fix(bots/discord): filter out text triggers correctly from image-only scans 2024-09-20 13:49:38 +07:00
semantic-release-bot
7a379a2cae chore(release): 1.0.0-dev.27 [skip ci]
# @revanced/discord-bot [1.0.0-dev.27](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.26...@revanced/discord-bot@1.0.0-dev.27) (2024-09-05)

### Bug Fixes

* **bots/discord:** correct permission check logic ([dd8872c](dd8872c027))
* **bots/discord:** give only removed roles for role presets ([522ad28](522ad28fd8))
* **bots/discord:** replace duration parser with a library ([94c4fed](94c4fedc06))
2024-09-05 10:34:11 +00:00
Palm
dd8872c027 fix(bots/discord): correct permission check logic
Members were being previously treated as users and some requirements are passing by default when they must not.
2024-09-05 17:33:06 +07:00
semantic-release-bot
33a0e18e7b chore(release): 1.0.0-dev.27 [skip ci]
# @revanced/discord-bot [1.0.0-dev.27](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.26...@revanced/discord-bot@1.0.0-dev.27) (2024-08-23)

### Bug Fixes

* **bots/discord:** give only removed roles for role presets ([522ad28](522ad28fd8))
* **bots/discord:** replace duration parser with a library ([94c4fed](94c4fedc06))
2024-08-23 11:07:18 +00:00
PalmDevs
522ad28fd8 fix(bots/discord): give only removed roles for role presets 2024-08-23 18:05:48 +07:00
PalmDevs
94c4fedc06 fix(bots/discord): replace duration parser with a library 2024-08-23 17:59:02 +07:00
semantic-release-bot
e3dcbab508 chore(release): 1.0.0-dev.26 [skip ci]
# @revanced/discord-bot [1.0.0-dev.26](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.25...@revanced/discord-bot@1.0.0-dev.26) (2024-08-15)

### Bug Fixes

* **bots/discord:** correct timer active condition for sticky messages ([96065ff](96065ff175))
2024-08-15 15:34:29 +00:00
Palm
96065ff175 fix(bots/discord): correct timer active condition for sticky messages 2024-08-15 22:33:12 +07:00
semantic-release-bot
845dd5d914 chore(release): 1.0.0-dev.25 [skip ci]
# @revanced/discord-bot [1.0.0-dev.25](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.24...@revanced/discord-bot@1.0.0-dev.25) (2024-08-15)

### Bug Fixes

* **bots/discord:** allow access to `context` in `/eval` and await result ([99f65f0](99f65f07f5))
2024-08-15 05:11:31 +00:00
Palm
99f65f07f5 fix(bots/discord): allow access to context in /eval and await result 2024-08-15 12:10:04 +07:00
semantic-release-bot
51f877f321 chore(release): 1.0.0-dev.24 [skip ci]
# @revanced/discord-bot [1.0.0-dev.24](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.23...@revanced/discord-bot@1.0.0-dev.24) (2024-08-14)

### Bug Fixes

* **bots/discord:** do not remove unrelated reactions ([031fd26](031fd26b26))
2024-08-14 04:54:54 +00:00
PalmDevs
031fd26b26 fix(bots/discord): do not remove unrelated reactions 2024-08-14 11:53:29 +07:00
semantic-release-bot
ef07039083 chore(release): 1.0.0-dev.23 [skip ci]
# @revanced/discord-bot [1.0.0-dev.23](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.22...@revanced/discord-bot@1.0.0-dev.23) (2024-08-13)

### Features

* **bots/discord:** add `train` commands ([ee90ef2](ee90ef247b))
* **bots/discord:** update to newer command definition framework ([97f2795](97f2795df4))
2024-08-13 14:20:22 +00:00
PalmDevs
6dc7f0211e chore: update lockfile 2024-08-13 21:19:18 +07:00
PalmDevs
ee90ef247b feat(bots/discord): add train commands 2024-08-13 21:16:28 +07:00
PalmDevs
97f2795df4 feat(bots/discord): update to newer command definition framework 2024-08-13 21:16:26 +07:00
semantic-release-bot
82fac783ea chore(release): 1.0.0-dev.22 [skip ci]
# @revanced/discord-bot [1.0.0-dev.22](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.21...@revanced/discord-bot@1.0.0-dev.22) (2024-08-10)

### Bug Fixes

* **bots/discord:** parse larger units of durations, fix wrong timestamp in mod embed ([6c8dce0](6c8dce0593))
* **bots/discord:** provide discord token for `reload` command ([dd21a5a](dd21a5abad))

### Features

* **bots/discord:** add code to actually scan text files correctly ([80aeb19](80aeb19020))
2024-08-10 15:31:57 +00:00
PalmDevs
dd21a5abad fix(bots/discord): provide discord token for reload command 2024-08-10 22:30:45 +07:00
PalmDevs
80aeb19020 feat(bots/discord): add code to actually scan text files correctly 2024-08-10 22:30:45 +07:00
PalmDevs
6c8dce0593 fix(bots/discord): parse larger units of durations, fix wrong timestamp in mod embed 2024-08-10 22:30:44 +07:00
semantic-release-bot
9897f244e0 chore(release): 1.0.0-dev.21 [skip ci]
# @revanced/discord-bot [1.0.0-dev.21](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.20...@revanced/discord-bot@1.0.0-dev.21) (2024-08-04)

### Bug Fixes

* **bots/discord:** correct sticky messages logic ([de8bef6](de8bef6520))
* **bots/discord:** make `/eval` work ([eaa25f2](eaa25f2eb5))
* **bots/discord:** some configuration values not applying after running `/reload` ([a976dd2](a976dd2acc))
2024-08-04 17:45:52 +00:00
PalmDevs
eaa25f2eb5 fix(bots/discord): make /eval work 2024-08-05 00:44:35 +07:00
PalmDevs
a976dd2acc fix(bots/discord): some configuration values not applying after running /reload 2024-08-05 00:44:34 +07:00
PalmDevs
c567ef25c6 feat(packages/api): allow setting new options for ClientWebsocketManager 2024-08-05 00:44:33 +07:00
PalmDevs
de8bef6520 fix(bots/discord): correct sticky messages logic 2024-08-05 00:44:32 +07:00
semantic-release-bot
98dea81eeb chore(release): 1.0.0-dev.20 [skip ci]
# @revanced/discord-bot [1.0.0-dev.20](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.19...@revanced/discord-bot@1.0.0-dev.20) (2024-08-03)

### Bug Fixes

* **bots/discord:** await when putting entries into db ([4da6175](4da6175cf5))
2024-08-03 19:53:58 +00:00
semantic-release-bot
9f3295cc0f chore(release): 1.0.0-dev.9 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.9](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.8...@revanced/bot-websocket-api@1.0.0-dev.9) (2024-08-03)

### Features

* **apis/websocket:** return `true` for data on a `TrainedMessage` packet ([65add4d](65add4dfee))
2024-08-03 19:53:18 +00:00
PalmDevs
4da6175cf5 fix(bots/discord): await when putting entries into db 2024-08-04 02:52:07 +07:00
PalmDevs
d90ad5c955 fix(packages/api): handle close event as a disconnect 2024-08-04 02:52:06 +07:00
PalmDevs
65add4dfee feat(apis/websocket): return true for data on a TrainedMessage packet 2024-08-04 02:52:04 +07:00
semantic-release-bot
2c2f6b76d4 chore(release): 1.0.0-dev.19 [skip ci]
# @revanced/discord-bot [1.0.0-dev.19](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.18...@revanced/discord-bot@1.0.0-dev.19) (2024-08-03)

### Bug Fixes

* **bots/discord:** correct whitelist logic ([49c29be](49c29bebfb))
2024-08-03 17:31:01 +00:00
PalmDevs
49c29bebfb fix(bots/discord): correct whitelist logic 2024-08-04 00:29:20 +07:00
semantic-release-bot
4e889d4991 chore(release): 1.0.0-dev.18 [skip ci]
# @revanced/discord-bot [1.0.0-dev.18](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.17...@revanced/discord-bot@1.0.0-dev.18) (2024-08-03)

### Bug Fixes

* **bots/discord:** set the `label` property correctly for message scans ([6d463df](6d463df586))
2024-08-03 15:24:18 +00:00
PalmDevs
6d463df586 fix(bots/discord): set the label property correctly for message scans 2024-08-03 22:22:42 +07:00
semantic-release-bot
2d8688bd4c chore(release): 1.0.0-dev.17 [skip ci]
# @revanced/discord-bot [1.0.0-dev.17](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.16...@revanced/discord-bot@1.0.0-dev.17) (2024-08-02)

### Bug Fixes

* **bots/discord/commands/eval:** evaluate in current context ([5925d90](5925d90209))
* **bots/discord:** send right response for after regexes ([a7688fa](a7688fa9b9))

### Features

* **bots/discord:** update example config file ([bc9951c](bc9951c9b5))
2024-08-02 18:40:39 +00:00
PalmDevs
bc9951c9b5 feat(bots/discord): update example config file 2024-08-03 01:36:19 +07:00
PalmDevs
a7688fa9b9 fix(bots/discord): send right response for after regexes 2024-08-03 01:31:45 +07:00
PalmDevs
5925d90209 fix(bots/discord/commands/eval): evaluate in current context 2024-08-03 01:31:44 +07:00
semantic-release-bot
5506518635 chore(release): 1.0.0-dev.16 [skip ci]
# @revanced/discord-bot [1.0.0-dev.16](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.15...@revanced/discord-bot@1.0.0-dev.16) (2024-08-02)

### Bug Fixes

* **bots/discord:** open database as read-write ([c366840](c36684091d))
* **bots/discord:** remove bad text channel checks ([f5939e2](f5939e2528))
* **bots/discord:** remove redundant footer for response embeds ([412e003](412e00317d))

### Features

* **bots/discord/commands:** add `reload` command ([6875b32](6875b32fd0))
2024-08-02 12:31:21 +00:00
PalmDevs
b9d08fff64 feat(bots/discord)!: add more attachment scan options 2024-08-02 19:26:20 +07:00
PalmDevs
6875b32fd0 feat(bots/discord/commands): add reload command 2024-08-02 19:26:20 +07:00
PalmDevs
c36684091d fix(bots/discord): open database as read-write 2024-08-02 19:26:19 +07:00
PalmDevs
f5939e2528 fix(bots/discord): remove bad text channel checks 2024-08-02 19:26:18 +07:00
PalmDevs
412e00317d fix(bots/discord): remove redundant footer for response embeds 2024-08-02 19:26:17 +07:00
PalmDevs
8fe78e424e fix(bots/discord)!: rename config replyToReplied to respondToReply 2024-08-02 19:26:12 +07:00
semantic-release-bot
9fe6b4ca70 chore(release): 1.0.0-dev.15 [skip ci]
# @revanced/discord-bot [1.0.0-dev.15](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.14...@revanced/discord-bot@1.0.0-dev.15) (2024-07-31)

### Bug Fixes

* **bots/discord:** import `config` from context ([763ef25](763ef253f9))

### Features

* **bots/discord:** add sticky messages ([bf66155](bf661556e1))
2024-07-31 19:31:21 +00:00
PalmDevs
bf661556e1 feat(bots/discord): add sticky messages 2024-08-01 02:29:49 +07:00
PalmDevs
763ef253f9 fix(bots/discord): import config from context 2024-07-31 21:25:12 +07:00
semantic-release-bot
561426028c chore(release): 1.0.0-dev.14 [skip ci]
# @revanced/discord-bot [1.0.0-dev.14](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.13...@revanced/discord-bot@1.0.0-dev.14) (2024-07-31)

### Bug Fixes

* **bots/discord:** always true check causing no messages to be scanned ([98ec37b](98ec37b5d1))
* other small issues ([bc437a5](bc437a5ec7))

### Features

* **bots/discord:** add more options for curing, fix default regex ([1a4ec1e](1a4ec1ece8))
* **bots/discord:** allow admins to bypass permission checks ([620f933](620f9339f0))
2024-07-31 12:36:12 +00:00
semantic-release-bot
b726c40fd4 chore(release): 1.0.0-dev.8 [skip ci]
# @revanced/bot-websocket-api [1.0.0-dev.8](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.7...@revanced/bot-websocket-api@1.0.0-dev.8) (2024-07-31)

### Bug Fixes

* other small issues ([bc437a5](bc437a5ec7))
2024-07-31 12:35:31 +00:00
PalmDevs
bc437a5ec7 fix: other small issues 2024-07-31 19:34:12 +07:00
PalmDevs
620f9339f0 feat(bots/discord): allow admins to bypass permission checks 2024-07-31 19:34:11 +07:00
PalmDevs
1a4ec1ece8 feat(bots/discord): add more options for curing, fix default regex 2024-07-31 19:34:10 +07:00
PalmDevs
98ec37b5d1 fix(bots/discord): always true check causing no messages to be scanned 2024-07-31 19:34:10 +07:00
PalmDevs
711f57f4a1 fix(packages/api): null errors being thrown 2024-07-31 19:34:09 +07:00
PalmDevs
e29e9c3dd1 fix(packages/api): properly await failed packets 2024-07-31 19:34:08 +07:00
85 changed files with 4678 additions and 847 deletions

View File

@@ -12,23 +12,20 @@ jobs:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: write # to be able to publish a GitHub release
packages: write issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
packages: write # to be able to publish GitHub packages
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: '**/node_modules' path: '**/node_modules'
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: ${{ runner.os }}-bun- restore-keys: ${{ runner.os }}-bun-
- name: Setup Bun - name: Setup Bun
@@ -48,22 +45,11 @@ jobs:
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build and release - name: Build, release, publish
env: env:
RELEASE_WORKFLOW_STEP: release GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
DOCKER_REGISTRY_USER: ${{ github.repository_owner }} DOCKER_REGISTRY_USER: ${{ github.repository_owner }}
DOCKER_REGISTRY_PASSWORD: ${{ secrets.REPOSITORY_PUSH_ACCESS }} DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
DEBUG: semantic-release:*
run: bunx multi-semantic-release --debug
# We call multi-semantic-release twice to publish in a different step
# An environment variable determines which plugins in the config to run
- name: Trigger Portainer webhooks
if: github.ref == 'refs/heads/main'
env:
RELEASE_WORKFLOW_STEP: publish
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
WEBSOCKET_API_PORTAINER_WEBHOOK_URL: ${{ secrets.WEBSOCKET_API_PORTAINER_WEBHOOK_URL }} WEBSOCKET_API_PORTAINER_WEBHOOK_URL: ${{ secrets.WEBSOCKET_API_PORTAINER_WEBHOOK_URL }}
DISCORD_BOT_PORTAINER_WEBHOOK_URL: ${{ secrets.DISCORD_BOT_PORTAINER_WEBHOOK_URL }} DISCORD_BOT_PORTAINER_WEBHOOK_URL: ${{ secrets.DISCORD_BOT_PORTAINER_WEBHOOK_URL }}
run: bunx multi-semantic-release run: bunx multi-semantic-release

View File

@@ -58,7 +58,7 @@
Continuing the legacy of Vanced Continuing the legacy of Vanced
</p> </p>
# 🤖 ReVanced Helper # 🤖 ReVanced Bots
Bots assisting ReVanced on multiple platforms. Bots assisting ReVanced on multiple platforms.

View File

@@ -1,17 +1,7 @@
import defineSubprojectReleaseConfig from '../../semantic-release-config.js' import defineSubprojectReleaseConfig from '../../semantic-release-config.js'
export default defineSubprojectReleaseConfig({ export default defineSubprojectReleaseConfig({
plugins: plugins: [
process.env.RELEASE_WORKFLOW_STEP === 'publish'
? [
[
'@semantic-release/exec',
{
publishCmd: 'bun run scripts/trigger-portainer-webhook.ts',
},
],
]
: [
[ [
'@codedependant/semantic-release-docker', '@codedependant/semantic-release-docker',
{ {
@@ -28,5 +18,11 @@ export default defineSubprojectReleaseConfig({
], ],
}, },
], ],
[
'@semantic-release/exec',
{
successCmd: 'bun run scripts/trigger-portainer-webhook.ts',
},
],
], ],
}) })

View File

@@ -1,3 +1,97 @@
# @revanced/bot-websocket-api [1.1.0](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.2...@revanced/bot-websocket-api@1.1.0) (2025-07-11)
### Features
* **apis/websocket:** support training without label ([670cc70](https://github.com/revanced/revanced-bots/commit/670cc700570b32924738c3f24acb9f5312d2dcdb))
## @revanced/bot-websocket-api [1.0.2](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.1...@revanced/bot-websocket-api@1.0.2) (2025-06-23)
## @revanced/bot-websocket-api [1.0.1](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0...@revanced/bot-websocket-api@1.0.1) (2025-04-09)
### Bug Fixes
* **bots/discord:** attempt to fix sticky messages again ([7564b1a](https://github.com/revanced/revanced-bots/commit/7564b1a8f066183df390887ddfd4d73e0baa87ac))
## @revanced/bot-websocket-api [1.0.1](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0...@revanced/bot-websocket-api@1.0.1) (2025-04-09)
### Bug Fixes
* **bots/discord:** attempt to fix sticky messages again ([7564b1a](https://github.com/revanced/revanced-bots/commit/7564b1a8f066183df390887ddfd4d73e0baa87ac))
# @revanced/bot-websocket-api 1.0.0 (2025-04-04)
### Bug Fixes
* **apis/websocket:** also include tesseract core files in build ([7dfbf6c](https://github.com/revanced/revanced-bots/commit/7dfbf6c92c49100954fa4aca471dce4ab9fd9565))
* **apis/websocket:** attempt to fix missing remote address ([9b2888b](https://github.com/revanced/revanced-bots/commit/9b2888b944ea1d61d31aa5df3536768e9a2dadf8))
* **apis/websocket:** build and runtime issues ([89d8ab1](https://github.com/revanced/revanced-bots/commit/89d8ab1ee58278a9a96cdc31c679d0a0a0d865af))
* **apis/websocket:** builds not working due to dynamic import requirement ([fc7be22](https://github.com/revanced/revanced-bots/commit/fc7be22c6c15974c7394790e93de2a23a6627153))
* **apis/websocket:** don't bundle `tesseract.js` ([51a6fb6](https://github.com/revanced/revanced-bots/commit/51a6fb65f0df3409eacffb297430840a0e326989))
* **apis/websocket:** fix forever stuck Promise ([168f40d](https://github.com/revanced/revanced-bots/commit/168f40def64ca213cd2b549f4bafed4c0e1e3695))
* **apis/websocket:** fix undefined error ([2f03800](https://github.com/revanced/revanced-bots/commit/2f03800c61c00e59e512567d273a195e605d6736))
* **apis/websocket:** hardcoded paths in tesseract worker builds ([38e00eb](https://github.com/revanced/revanced-bots/commit/38e00eb4e59c763bd74d27b9b9b482ea66e4dcf4))
* **apis/websocket:** improve logging and error handling ([b6cbe9d](https://github.com/revanced/revanced-bots/commit/b6cbe9d64c01ff11feab8351fb801bc1aee48325))
* **bots/discord:** hanging process when disconnecting from API too many times ([d31616e](https://github.com/revanced/revanced-bots/commit/d31616ebcba6f1dcd8bde183bcb8d1adb1501b61))
* fix typings and formatting ([479812e](https://github.com/revanced/revanced-bots/commit/479812e199b52cdb295a5746e0767306afab3413))
* other small issues ([bc437a5](https://github.com/revanced/revanced-bots/commit/bc437a5ec7ce1d339094d608e2a61ac5f460c163))
* remove error cb handling for `socket.send()` calls ([29544d4](https://github.com/revanced/revanced-bots/commit/29544d4e0127173465796b7e3c62161f4db59c8b))
* run projects with `--bun` ([bb2182e](https://github.com/revanced/revanced-bots/commit/bb2182e707fa40c555d56138972eeea28f1b3cf9))
* **types:** fix issues with typings ([669e24c](https://github.com/revanced/revanced-bots/commit/669e24ca8103ea051b4e61160dd0f978e36707ea))
* update repo url ([a21aa34](https://github.com/revanced/revanced-bots/commit/a21aa348d7f32cd0ee65b371e9594520c0a9d3f1))
### chore
* fix more build issues ([77fefb9](https://github.com/revanced/revanced-bots/commit/77fefb9bef286a22f40a4d76b79c64fcc5a2467f))
### Features
* **apis/websocket:** clear old client sessions and instances ([43bd0a0](https://github.com/revanced/revanced-bots/commit/43bd0a021cd885a3d74a1f307ec2935e81d17458))
* **apis/websocket:** return `true` for data on a `TrainedMessage` packet ([65add4d](https://github.com/revanced/revanced-bots/commit/65add4dfeed2fa067c2c8e2377f7d01d505ade54))
* **packages/shared:** add logger factory ([17c6be7](https://github.com/revanced/revanced-bots/commit/17c6be7bee5b5c24fd4a5279e73374b0bb7a6229))
### BREAKING CHANGES
* In `@revanced/discord-bot`, its environment variable
`DATABASE_URL` has been renamed to `DATABASE_PATH`
and the `file:` prefix is no longer needed
# @revanced/bot-websocket-api [1.0.0-dev.11](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0-dev.10...@revanced/bot-websocket-api@1.0.0-dev.11) (2025-04-04)
### Bug Fixes
* **apis/websocket:** attempt to fix missing remote address ([9b2888b](https://github.com/revanced/revanced-bots/commit/9b2888b944ea1d61d31aa5df3536768e9a2dadf8))
* run projects with `--bun` ([bb2182e](https://github.com/revanced/revanced-bots/commit/bb2182e707fa40c555d56138972eeea28f1b3cf9))
# @revanced/bot-websocket-api [1.0.0-dev.10](https://github.com/revanced/revanced-bots/compare/@revanced/bot-websocket-api@1.0.0-dev.9...@revanced/bot-websocket-api@1.0.0-dev.10) (2025-03-03)
### Bug Fixes
* fix typings and formatting ([479812e](https://github.com/revanced/revanced-bots/commit/479812e199b52cdb295a5746e0767306afab3413))
* update repo url ([a21aa34](https://github.com/revanced/revanced-bots/commit/a21aa348d7f32cd0ee65b371e9594520c0a9d3f1))
# @revanced/bot-websocket-api [1.0.0-dev.9](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.8...@revanced/bot-websocket-api@1.0.0-dev.9) (2024-08-03)
### Features
* **apis/websocket:** return `true` for data on a `TrainedMessage` packet ([65add4d](https://github.com/revanced/revanced-helper/commit/65add4dfeed2fa067c2c8e2377f7d01d505ade54))
# @revanced/bot-websocket-api [1.0.0-dev.8](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.7...@revanced/bot-websocket-api@1.0.0-dev.8) (2024-07-31)
### Bug Fixes
* other small issues ([bc437a5](https://github.com/revanced/revanced-helper/commit/bc437a5ec7ce1d339094d608e2a61ac5f460c163))
# @revanced/bot-websocket-api [1.0.0-dev.7](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.6...@revanced/bot-websocket-api@1.0.0-dev.7) (2024-07-30) # @revanced/bot-websocket-api [1.0.0-dev.7](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.6...@revanced/bot-websocket-api@1.0.0-dev.7) (2024-07-30)
# @revanced/bot-websocket-api [1.0.0-dev.6](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.5...@revanced/bot-websocket-api@1.0.0-dev.6) (2024-07-30) # @revanced/bot-websocket-api [1.0.0-dev.6](https://github.com/revanced/revanced-helper/compare/@revanced/bot-websocket-api@1.0.0-dev.5...@revanced/bot-websocket-api@1.0.0-dev.6) (2024-07-30)

View File

@@ -14,4 +14,4 @@ WORKDIR /app
COPY --from=build /build/apis/websocket/dist /app COPY --from=build /build/apis/websocket/dist /app
USER 1000:1000 USER 1000:1000
ENTRYPOINT [ "bun", "run", "index.js" ] ENTRYPOINT [ "bun", "--bun", "run", "index.js" ]

View File

@@ -2,7 +2,7 @@
"name": "@revanced/bot-websocket-api", "name": "@revanced/bot-websocket-api",
"type": "module", "type": "module",
"private": true, "private": true,
"version": "1.0.0-dev.7", "version": "1.1.0",
"description": "🧦 WebSocket API server for bots assisting ReVanced", "description": "🧦 WebSocket API server for bots assisting ReVanced",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
@@ -13,7 +13,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/revanced/revanced-helper.git", "url": "git+https://github.com/revanced/revanced-bots.git",
"directory": "apis/websocket" "directory": "apis/websocket"
}, },
"author": "Palm <contact@palmdevs.me> (https://palmdevs.me)", "author": "Palm <contact@palmdevs.me> (https://palmdevs.me)",
@@ -23,18 +23,18 @@
], ],
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/revanced/revanced-helper/issues" "url": "https://github.com/revanced/revanced-bots/issues"
}, },
"homepage": "https://github.com/revanced/revanced-helper#readme", "homepage": "https://github.com/revanced/revanced-bots#readme",
"dependencies": { "dependencies": {
"@revanced/bot-shared": "workspace:*", "@revanced/bot-shared": "workspace:*",
"@sapphire/async-queue": "^1.5.2", "@sapphire/async-queue": "^1.5.5",
"chalk": "^5.3.0", "chalk": "^5.6.2",
"tesseract.js": "^5.1.0", "tesseract.js": "^6.0.1",
"ws": "^8.17.1" "ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/ws": "^8.5.10", "@types/ws": "^8.18.1",
"typed-emitter": "^2.1.0" "typed-emitter": "^2.1.0"
} }
} }

View File

@@ -1,10 +1,10 @@
import { createLogger } from '@revanced/bot-shared' import { createLogger } from '@revanced/bot-shared'
import { cp, rm } from 'fs/promises' import { cp, exists, rm } from 'fs/promises'
const logger = createLogger() const logger = createLogger()
logger.info('Cleaning previous build...') logger.info('Cleaning previous build...')
await rm('./dist', { recursive: true }) if (await exists('./dist')) await rm('./dist', { recursive: true })
logger.info('Building WebSocket API...') logger.info('Building WebSocket API...')
await Bun.build({ await Bun.build({

View File

@@ -1,15 +1,14 @@
import { EventEmitter } from 'events'
import { import {
ClientOperation, ClientOperation,
DisconnectReason, DisconnectReason,
type Packet,
ServerOperation,
deserializePacket, deserializePacket,
isClientPacket, isClientPacket,
type Packet,
ServerOperation,
serializePacket, serializePacket,
uncapitalize, uncapitalize,
} from '@revanced/bot-shared' } from '@revanced/bot-shared'
import { EventEmitter } from 'events'
import type TypedEmitter from 'typed-emitter' import type TypedEmitter from 'typed-emitter'
import type { RawData, WebSocket } from 'ws' import type { RawData, WebSocket } from 'ws'
@@ -100,7 +99,7 @@ export default class Client {
// @ts-expect-error TypeScript doesn't know that the above line will negate the type enough // @ts-expect-error TypeScript doesn't know that the above line will negate the type enough
packet, packet,
) )
} catch (e) { } catch (_e) {
// TODO: add error fields to sent packet so we can log what went wrong // TODO: add error fields to sent packet so we can log what went wrong
this.disconnect(DisconnectReason.InvalidPacket) this.disconnect(DisconnectReason.InvalidPacket)
} }
@@ -110,7 +109,7 @@ export default class Client {
protected _toBuffer(data: RawData) { protected _toBuffer(data: RawData) {
if (data instanceof Buffer) return data if (data instanceof Buffer) return data
if (data instanceof ArrayBuffer) return Buffer.from(data) if (data instanceof ArrayBuffer) return Buffer.from(data)
return Buffer.concat(data) return Buffer.concat(data as Uint8Array[])
} }
} }

View File

@@ -1,8 +1,7 @@
import { OEM, createWorker as createTesseractWorker } from 'tesseract.js'
import { join as joinPath } from 'path'
import { createLogger } from '@revanced/bot-shared' import { createLogger } from '@revanced/bot-shared'
import { exists as pathExists } from 'fs/promises' import { exists as pathExists } from 'fs/promises'
import { join as joinPath } from 'path'
import { createWorker as createTesseractWorker, OEM } from 'tesseract.js'
import { getConfig } from './utils/config' import { getConfig } from './utils/config'
export const config = getConfig() export const config = getConfig()
@@ -24,12 +23,12 @@ export const wit = {
if (!res.ok) throw new Error(`Failed to fetch from Wit.ai: ${res.statusText} (${res.status})`) if (!res.ok) throw new Error(`Failed to fetch from Wit.ai: ${res.statusText} (${res.status})`)
return await res.json() return (await res.json()) as WitMessageResponse
}, },
message(text: string) { message(text: string) {
return this.fetch(`/message?q=${encodeURIComponent(text)}&n=8`) as Promise<WitMessageResponse> return this.fetch(`/message?q=${encodeURIComponent(text)}&n=8`) as Promise<WitMessageResponse>
}, },
async train(text: string, label: string) { async train(text: string, label?: string) {
await this.fetch('/utterances', { await this.fetch('/utterances', {
body: JSON.stringify([ body: JSON.stringify([
{ {
@@ -42,7 +41,14 @@ export const wit = {
method: 'POST', method: 'POST',
}) })
}, },
} as const } satisfies Wit
export interface Wit {
token: string
fetch(route: string, options?: RequestInit): Promise<WitMessageResponse>
message(text: string): Promise<WitMessageResponse>
train(text: string, label?: string): Promise<void>
}
export interface WitMessageResponse { export interface WitMessageResponse {
text: string text: string

View File

@@ -1,12 +1,11 @@
import type { ClientOperation } from '@revanced/bot-shared' import type { ClientOperation, Logger } from '@revanced/bot-shared'
import type { Logger } from '@revanced/bot-shared'
import type { Worker as TesseractWorker } from 'tesseract.js' import type { Worker as TesseractWorker } from 'tesseract.js'
import type { ClientPacketObject } from '../classes/Client' import type { ClientPacketObject } from '../classes/Client'
import type { WitMessageResponse } from '../context' import type { Wit } from '../context'
import type { Config } from '../utils/config' import type { Config } from '../utils/config'
export { default as parseTextEventHandler } from './parseText'
export { default as parseImageEventHandler } from './parseImage' export { default as parseImageEventHandler } from './parseImage'
export { default as parseTextEventHandler } from './parseText'
export { default as trainMessageEventHandler } from './trainMessage' export { default as trainMessageEventHandler } from './trainMessage'
export type EventHandler<POp extends ClientOperation> = ( export type EventHandler<POp extends ClientOperation> = (
@@ -15,10 +14,7 @@ export type EventHandler<POp extends ClientOperation> = (
) => void | Promise<void> ) => void | Promise<void>
export type EventContext = { export type EventContext = {
wit: { wit: Wit
train(text: string, label: string): Promise<void>
message(text: string): Promise<WitMessageResponse>
}
tesseract: TesseractWorker tesseract: TesseractWorker
logger: Logger logger: Logger
config: Config config: Config

View File

@@ -1,6 +1,5 @@
import { type ClientOperation, ServerOperation } from '@revanced/bot-shared' import { type ClientOperation, ServerOperation } from '@revanced/bot-shared'
import { AsyncQueue } from '@sapphire/async-queue' import { AsyncQueue } from '@sapphire/async-queue'
import type { EventHandler } from '.' import type { EventHandler } from '.'
const queue = new AsyncQueue() const queue = new AsyncQueue()

View File

@@ -1,5 +1,4 @@
import { type ClientOperation, ServerOperation } from '@revanced/bot-shared' import { type ClientOperation, ServerOperation } from '@revanced/bot-shared'
import type { EventHandler } from '.' import type { EventHandler } from '.'
const parseTextEventHandler: EventHandler<ClientOperation.ParseText> = async (packet, { wit, logger }) => { const parseTextEventHandler: EventHandler<ClientOperation.ParseText> = async (packet, { wit, logger }) => {

View File

@@ -1,5 +1,4 @@
import { type ClientOperation, ServerOperation } from '@revanced/bot-shared' import { type ClientOperation, ServerOperation } from '@revanced/bot-shared'
import type { EventHandler } from '.' import type { EventHandler } from '.'
const trainMessageEventHandler: EventHandler<ClientOperation.TrainMessage> = async (packet, { wit, logger }) => { const trainMessageEventHandler: EventHandler<ClientOperation.TrainMessage> = async (packet, { wit, logger }) => {
@@ -11,14 +10,14 @@ const trainMessageEventHandler: EventHandler<ClientOperation.TrainMessage> = asy
const nextSeq = client.currentSequence++ const nextSeq = client.currentSequence++
const actualText = text.slice(0, 279) const actualText = text.slice(0, 279)
logger.debug(`${client.id} requested to train label ${label} (${nextSeq}) with:`, actualText) logger.debug(`${client.id} requested to train label ${label ?? '<out of scope>'} (${nextSeq}) with:`, actualText)
try { try {
await wit.train(actualText, label) await wit.train(actualText, label)
client.send( client.send(
{ {
op: ServerOperation.TrainedMessage, op: ServerOperation.TrainedMessage,
d: null, d: true,
}, },
nextSeq, nextSeq,
) )

View File

@@ -1,14 +1,10 @@
import { inspect as inspectObject } from 'util'
import Client from './classes/Client'
import { type EventContext, parseImageEventHandler, parseTextEventHandler, trainMessageEventHandler } from './events'
import { DisconnectReason, HumanizedDisconnectReason } from '@revanced/bot-shared' import { DisconnectReason, HumanizedDisconnectReason } from '@revanced/bot-shared'
import { createServer } from 'http' import { createServer } from 'http'
import { inspect as inspectObject } from 'util'
import { type WebSocket, WebSocketServer } from 'ws' import { type WebSocket, WebSocketServer } from 'ws'
import Client from './classes/Client'
import { config, logger, tesseract, wit } from './context' import { config, logger, tesseract, wit } from './context'
import { type EventContext, parseImageEventHandler, parseTextEventHandler, trainMessageEventHandler } from './events'
// Load config, init logger, check environment // Load config, init logger, check environment
@@ -56,12 +52,13 @@ const wss = new WebSocketServer({
wss.on('connection', async (socket, request) => { wss.on('connection', async (socket, request) => {
try { try {
if (!request.socket.remoteAddress) { const addrInfo = request.socket.address()
if (!('address' in addrInfo)) {
socket.close() socket.close()
return logger.warn('Connection failed because client is missing remote address') return logger.warn('Connection failed because client is missing remote address. addrInfo =', addrInfo)
} }
const id = `${request.socket.remoteAddress}:${request.socket.remotePort}` const id = `${addrInfo.address}:${addrInfo.port}`
if (clientIds.has(id)) { if (clientIds.has(id)) {
logger.warn(`Client ${id} already connected, disconnecting old session`) logger.warn(`Client ${id} already connected, disconnecting old session`)

View File

@@ -1,6 +1,20 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": { "organizeImports": {
"level": "on",
"options": {
"groups": [
{
"type": false
}
]
}
}
}
},
"enabled": true "enabled": true
}, },
"linter": { "linter": {
@@ -24,7 +38,15 @@
}, },
"useNumberNamespace": { "useNumberNamespace": {
"level": "off" "level": "off"
} },
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
} }
} }
}, },
@@ -54,7 +76,12 @@
}, },
"files": { "files": {
"ignoreUnknown": true, "ignoreUnknown": true,
"include": ["*.js", "*.json", "*.ts"], "includes": [
"ignore": ["dist/**/*", "node_modules/**/*"] "**/*.js",
"**/*.json",
"**/*.ts",
"!**/dist/**/*",
"!**/node_modules/**/*"
]
} }
} }

View File

@@ -174,9 +174,6 @@ dist
# Finder (MacOS) folder config # Finder (MacOS) folder config
.DS_Store .DS_Store
# Config
config.ts
# DB # DB
*.db *.db
*.sqlite *.sqlite

View File

@@ -1,17 +1,7 @@
import defineSubprojectReleaseConfig from '../../semantic-release-config.js' import defineSubprojectReleaseConfig from '../../semantic-release-config.js'
export default defineSubprojectReleaseConfig({ export default defineSubprojectReleaseConfig({
plugins: plugins: [
process.env.RELEASE_WORKFLOW_STEP === 'publish'
? [
[
'@semantic-release/exec',
{
publishCmd: 'bun run scripts/trigger-portainer-webhook.ts',
},
],
]
: [
[ [
'@codedependant/semantic-release-docker', '@codedependant/semantic-release-docker',
{ {
@@ -28,5 +18,11 @@ export default defineSubprojectReleaseConfig({
], ],
}, },
], ],
[
'@semantic-release/exec',
{
successCmd: 'bun run scripts/trigger-portainer-webhook.ts',
},
],
], ],
}) })

View File

@@ -1,3 +1,528 @@
## @revanced/discord-bot [1.5.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.5.1...@revanced/discord-bot@1.5.2) (2025-09-25)
### Bug Fixes
* **bots/discord:** correct `respondToReply` logic ([6fe1530](https://github.com/revanced/revanced-bots/commit/6fe15301a21fdc196fded8d6fb13236a7bb826f5))
## @revanced/discord-bot [1.5.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.5.0...@revanced/discord-bot@1.5.1) (2025-09-13)
### Bug Fixes
* **bots/discord:** only fetch reference when it exists when `respondToReply` is set ([42038e6](https://github.com/revanced/revanced-bots/commit/42038e6b38983fefe79481359bad300dcb5e83b4))
# @revanced/discord-bot [1.5.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.4.1...@revanced/discord-bot@1.5.0) (2025-09-13)
### Features
* **bots/discord:** add additional options for `respondToReply` ([399c201](https://github.com/revanced/revanced-bots/commit/399c201f8c3e9e116050b49c2ffccdd79b02f39b))
## @revanced/discord-bot [1.4.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.4.0...@revanced/discord-bot@1.4.1) (2025-09-13)
### Bug Fixes
* **bots/discord/database:** use non-deprecated way to create indexes ([8cc2377](https://github.com/revanced/revanced-bots/commit/8cc2377cbfcc74c2c3228ed18da2495b4efd45aa))
* **bots/discord:** allow partial users ([dd3e7d2](https://github.com/revanced/revanced-bots/commit/dd3e7d2ee0cdf40e083af7f6db35ac3508ddf763))
* **bots/discord:** error in cache `keepOverLimit` comparison ([567c5d2](https://github.com/revanced/revanced-bots/commit/567c5d2c7f41dae7d7fec9946a7dd2ac1b10cc2a))
# @revanced/discord-bot [1.4.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.3.0...@revanced/discord-bot@1.4.0) (2025-07-11)
### Bug Fixes
* **bots/discord:** pass non-empty out of scope label to discord ([fbd9480](https://github.com/revanced/revanced-bots/commit/fbd948003631b48a1914eb7b2551ead4b05089b7))
### Features
* **bots/discord:** react to label classified response ([96a9b83](https://github.com/revanced/revanced-bots/commit/96a9b83c486fdc6e78f4c59e197fa6c1dab09161))
# @revanced/discord-bot [1.3.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.3...@revanced/discord-bot@1.3.0) (2025-07-11)
### Features
* **bots/discord:** support training without label ([c68cfd1](https://github.com/revanced/revanced-bots/commit/c68cfd1c01703fad17f233ad4e13cf81913afbb6))
## @revanced/discord-bot [1.2.3](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.2...@revanced/discord-bot@1.2.3) (2025-06-23)
### Bug Fixes
* **bots/discord:** disable unneeded cache, enable message cache sweeping ([3a0f0fe](https://github.com/revanced/revanced-bots/commit/3a0f0fe7861d73a4d81ecaba0e12bd60c06f8eb8))
## @revanced/discord-bot [1.2.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.1...@revanced/discord-bot@1.2.2) (2025-06-10)
### Bug Fixes
* **bots/discord:** use intervals for checking expired presets ([6e89b87](https://github.com/revanced/revanced-bots/commit/6e89b874cdfee8a1c215559271c741f43ba578ce))
## @revanced/discord-bot [1.2.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.2.0...@revanced/discord-bot@1.2.1) (2025-05-02)
### Bug Fixes
* **bots/discord:** fix timeout overflow check for role presets ([495f686](https://github.com/revanced/revanced-bots/commit/495f686292ebdcf51902c1dc75ac1510d7fdbd9c))
# @revanced/discord-bot [1.2.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.1.2...@revanced/discord-bot@1.2.0) (2025-05-02)
### Features
* **bots/discord:** switch duration parser to `@sapphire/duration` ([04ce825](https://github.com/revanced/revanced-bots/commit/04ce8252c05a23dbb4a91fded4f1a3d63b5c8a64))
## @revanced/discord-bot [1.1.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.1.1...@revanced/discord-bot@1.1.2) (2025-04-16)
### Bug Fixes
* **bots/discord/commands/admin/reload:** fix type error ([3908854](https://github.com/revanced/revanced-bots/commit/3908854fe090dda67b0d90225ab76f75e95db4c0))
* **bots/discord/commands/moderation:** check if timeout amount is safe in role-preset commands ([0c1382c](https://github.com/revanced/revanced-bots/commit/0c1382c55856ed1e54c9e53dbb37e9297c5da37c))
* **bots/discord:** replace use of deprecated `options.ephemeral` in replies ([31e5cf7](https://github.com/revanced/revanced-bots/commit/31e5cf7fc5c7cd0c6ca3b1f3b9410a88b95d8273))
## @revanced/discord-bot [1.1.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.1.0...@revanced/discord-bot@1.1.1) (2025-04-14)
### Bug Fixes
* **bots/discord:** fix sticky msg force timer always starting, add more logging ([cb4dc42](https://github.com/revanced/revanced-bots/commit/cb4dc42dfab8cf9821b03316cf56b405abd497ae))
# @revanced/discord-bot [1.1.0](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.4...@revanced/discord-bot@1.1.0) (2025-04-14)
### Features
* **bots/discord:** delete and send sticky msg concurrently, add more logging ([247a00f](https://github.com/revanced/revanced-bots/commit/247a00f57fc2a45fe828cc41e6f0e38e67e83a20))
## @revanced/discord-bot [1.0.4](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.3...@revanced/discord-bot@1.0.4) (2025-04-14)
### Bug Fixes
* **bots/discord:** fix sticky messages logic again ([aa7501c](https://github.com/revanced/revanced-bots/commit/aa7501c3097a790265e4ea624d07c4a9c3c1b908))
* **bots/discord:** remove expired presets from db if unapplying is not possible ([9d705e5](https://github.com/revanced/revanced-bots/commit/9d705e580c05d8b25df6f845d3aac747adaca116))
## @revanced/discord-bot [1.0.3](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.2...@revanced/discord-bot@1.0.3) (2025-04-09)
### Bug Fixes
* **bots/discord:** attempt to fix sticky messages one last time ([65288ec](https://github.com/revanced/revanced-bots/commit/65288ec4242b32d0b5e213b3d7af602bb9a829ca))
## @revanced/discord-bot [1.0.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.1...@revanced/discord-bot@1.0.2) (2025-04-09)
## @revanced/discord-bot [1.0.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0...@revanced/discord-bot@1.0.1) (2025-04-09)
### Bug Fixes
* **bots/discord:** attempt to fix sticky messages again ([7564b1a](https://github.com/revanced/revanced-bots/commit/7564b1a8f066183df390887ddfd4d73e0baa87ac))
# @revanced/discord-bot 1.0.0 (2025-04-04)
### Bug Fixes
* **bot/discord:** start remove preset timeout for `role-preset` command ([cbf9116](https://github.com/revanced/revanced-bots/commit/cbf91162e27dd4c1ecb976927ab708f1d882abca))
* **bots/discord/commands/eval:** evaluate in current context ([5925d90](https://github.com/revanced/revanced-bots/commit/5925d902095acef5f6396ca03583a9cbb0862498))
* **bots/discord/commands/mute:** use existing `parseDuration` util function' ([3e07429](https://github.com/revanced/revanced-bots/commit/3e07429664f7dbb6ce653083e0adb1a232737fde))
* **bots/discord/commands/unmute:** fix option description and embeds ([7fdf8c0](https://github.com/revanced/revanced-bots/commit/7fdf8c0dc722e21fe5a3ad6ef8d3a306ef85f532))
* **bots/discord/commands:** minor issues ([3b2596e](https://github.com/revanced/revanced-bots/commit/3b2596e748cf2cde1500ef2ded55f0faabc2c272))
* **bots/discord/commands:** refactor and add checks ([a2bf3ea](https://github.com/revanced/revanced-bots/commit/a2bf3eade99b46f9ffb55d45b8caf1bcf3d22a9b))
* **bots/discord/database:** fix schema for role presets ([4aa138a](https://github.com/revanced/revanced-bots/commit/4aa138a9af8db7093ef637470fcfdea1f5341236))
* **bots/discord/scripts/build:** check if dist dir exists before cleaning ([c06033e](https://github.com/revanced/revanced-bots/commit/c06033e5730f82438e8052b9b519a8f8e2d25437))
* **bots/discord/scripts:** unintentional escaping on windows ([09dc706](https://github.com/revanced/revanced-bots/commit/09dc70632da0597fdb26677acee3f6fccbb2b9b5))
* **bots/discord/utils/discord/embeds:** set thumbnail on moderation embeds ([b056291](https://github.com/revanced/revanced-bots/commit/b056291ad0f2e2eac5eec8aa71f15dbc769aa0f9))
* **bots/discord/utils/discord/rolePresets:** correct property access for presets ([4c6ad11](https://github.com/revanced/revanced-bots/commit/4c6ad11be30c1d6af97c4ae40fc62d05fa7bdd57))
* **bots/discord/utils/discord:** add `moderation` module related functions ([7e8270f](https://github.com/revanced/revanced-bots/commit/7e8270f7d260322e1950e058b221ab088bd595d0))
* **bots/discord/utils/duration:** fix empty string returning with duration is 0 ([83c314e](https://github.com/revanced/revanced-bots/commit/83c314ef5f721abc355272db0e4c182dcfe5d943))
* **bots/discord/utils/duration:** fix specified default unit not working ([d138af4](https://github.com/revanced/revanced-bots/commit/d138af46d1f25a11b6f8ab3790ecaa70b1d716a9))
* **bots/discord/utils/duration:** make second the default unit ([5d1af3c](https://github.com/revanced/revanced-bots/commit/5d1af3c31c3379b6a13684dfb07583737908c8aa))
* **bots/discord:** add GuildMember partial ([8e3946a](https://github.com/revanced/revanced-bots/commit/8e3946a66602838715787090008c7bfaf72b67e9))
* **bots/discord:** allow access to `context` in `/eval` and await result ([99f65f0](https://github.com/revanced/revanced-bots/commit/99f65f07f5f8830c6e8ea4ae171e986af4d3f1f6))
* **bots/discord:** always true check causing no messages to be scanned ([98ec37b](https://github.com/revanced/revanced-bots/commit/98ec37b5d18cade85270ab83b0ed0abe41244dd9))
* **bots/discord:** apply active role presets if members rejoin ([f50b26b](https://github.com/revanced/revanced-bots/commit/f50b26b82d66c88fd1dbb8c07d77c177c0e781df))
* **bots/discord:** attempt to fix stuck sticky message timeouts ([3ed5bd1](https://github.com/revanced/revanced-bots/commit/3ed5bd11acc3b4fbd57b0d632c68eb9f77365b8a))
* **bots/discord:** await when putting entries into db ([4da6175](https://github.com/revanced/revanced-bots/commit/4da6175cf58b1fa6144bdc71ec806766d32c1025))
* **bots/discord:** broken regex when prefix set to special characters ([ab62e55](https://github.com/revanced/revanced-bots/commit/ab62e55e76005f5999d7413d1158e54053f28d1f))
* **bots/discord:** check token before connecting to bot api ([f3e4408](https://github.com/revanced/revanced-bots/commit/f3e4408aa28fb6a9d21365af8c1bea3d07b481de))
* **bots/discord:** ci issues causing database to not be auto generated ([673aa18](https://github.com/revanced/revanced-bots/commit/673aa189bef1009a3e32ba3b1291a5ee84f2def3))
* **bots/discord:** clear role presets after they expire ([faa81f4](https://github.com/revanced/revanced-bots/commit/faa81f4d887eaeae809651f5b68187d033a260f2))
* **bots/discord:** connect to discord API even if initial bot API connection fails ([6658b58](https://github.com/revanced/revanced-bots/commit/6658b582dbeba7e072a7a04c4efa255e7f634aef))
* **bots/discord:** contextify object before sandboxing ([062735f](https://github.com/revanced/revanced-bots/commit/062735f6d552890404d6192244c51a11b0709580))
* **bots/discord:** correct permission check logic ([dd8872c](https://github.com/revanced/revanced-bots/commit/dd8872c027c7e7e1a00f38d659b4d6e79274238c))
* **bots/discord:** correct sticky messages logic ([de8bef6](https://github.com/revanced/revanced-bots/commit/de8bef6520d53a1299f0478458320a7eb75c5e1d))
* **bots/discord:** correct timer active condition for sticky messages ([96065ff](https://github.com/revanced/revanced-bots/commit/96065ff17584ff99a56ca5008327863ca5a7852b))
* **bots/discord:** correct whitelist logic ([49c29be](https://github.com/revanced/revanced-bots/commit/49c29bebfbe348ae4e2cc1b3a83bfa41eb26ccd1))
* **bots/discord:** cross-device link build errors ([38c0699](https://github.com/revanced/revanced-bots/commit/38c06997b4d0f7bb3f1e62618a5e3f088c522e30))
* **bots/discord:** decrease length of an option in `ban` command ([22d3eea](https://github.com/revanced/revanced-bots/commit/22d3eea88d532792c1237d1a1ab18bc02e57816a))
* **bots/discord:** delete expired appliedPresets entries after unapplying ([14c98e8](https://github.com/revanced/revanced-bots/commit/14c98e87df1ec4fd762bbc48ca4c06470cb110a2))
* **bots/discord:** deployment runtime errors due to minification ([a60c60c](https://github.com/revanced/revanced-bots/commit/a60c60c0f994a4c256b7d0582e99a1731209cf49))
* **bots/discord:** do decancer after resetting nickname ([0303fe3](https://github.com/revanced/revanced-bots/commit/0303fe3e367c07e92f831365d5548ca5b03435b2))
* **bots/discord:** do not remove unrelated reactions ([031fd26](https://github.com/revanced/revanced-bots/commit/031fd26b2619ecafeff3964e50accacb87de6108))
* **bots/discord:** don't refresh timer if force timer is active for sticky messages ([4abac0c](https://github.com/revanced/revanced-bots/commit/4abac0c890c0548e14cb56723cae919353a8e726))
* **bots/discord:** filter out text triggers correctly from image-only scans ([8c0dd67](https://github.com/revanced/revanced-bots/commit/8c0dd67d03d5a1747993da08a5bf82a39de43789))
* **bots/discord:** fix freeze on prod builds ([8efb549](https://github.com/revanced/revanced-bots/commit/8efb549453a04fab1ac6414a7f7f8bf702df3c93))
* **bots/discord:** fix get response logic ([3261294](https://github.com/revanced/revanced-bots/commit/3261294822b0a9faec094536ed5be2d3e1d5e17b))
* **bots/discord:** fix reload not working ([11582d5](https://github.com/revanced/revanced-bots/commit/11582d50345cae9fb645a65ca4e621596de6a408))
* **bots/discord:** follow-up if reply is already sent when error ([f75060b](https://github.com/revanced/revanced-bots/commit/f75060bc9cda44902cf872def73c116a6df039d7))
* **bots/discord:** give only removed roles for role presets ([522ad28](https://github.com/revanced/revanced-bots/commit/522ad28fd83565e9ca411dbce86c8447574288fd))
* **bots/discord:** hanging process when disconnecting from API too many times ([d31616e](https://github.com/revanced/revanced-bots/commit/d31616ebcba6f1dcd8bde183bcb8d1adb1501b61))
* **bots/discord:** import `config` from context ([763ef25](https://github.com/revanced/revanced-bots/commit/763ef253f9d4ff70a8b79969a7f4f41cba7f3c59))
* **bots/discord:** make `/eval` work ([eaa25f2](https://github.com/revanced/revanced-bots/commit/eaa25f2eb58a9e2d25bb98633ad668485e099714))
* **bots/discord:** messed up file name for `unmute` command ([399dca7](https://github.com/revanced/revanced-bots/commit/399dca71538fe5c8831977694a97058254a17578))
* **bots/discord:** only check for member permissions when specified while correcting responses ([b79a1c7](https://github.com/revanced/revanced-bots/commit/b79a1c7575e94c3e62654c87775cac497be4a50a))
* **bots/discord:** open database as read-write ([c366840](https://github.com/revanced/revanced-bots/commit/c36684091dddf67880505dc459e4334a8a5492f4))
* **bots/discord:** owners cannot bypass checks on some commands ([39cba97](https://github.com/revanced/revanced-bots/commit/39cba973418027ba6ed67e1ae5ab5c6458807562))
* **bots/discord:** parse larger units of durations, fix wrong timestamp in mod embed ([6c8dce0](https://github.com/revanced/revanced-bots/commit/6c8dce059366a6ef85f5b8b1794c056515b9f5b6))
* **bots/discord:** persist changes in context for eval command ([5b4965d](https://github.com/revanced/revanced-bots/commit/5b4965dcc7285676b2b3b6756c249bd56eaf8485))
* **bots/discord:** provide discord token for `reload` command ([dd21a5a](https://github.com/revanced/revanced-bots/commit/dd21a5abad560f3d00b8c58912786d4b6bd520e9))
* **bots/discord:** remove auto-generated files ([fb8af00](https://github.com/revanced/revanced-bots/commit/fb8af008661bf37389e01cba19d64a8b4fc82139))
* **bots/discord:** remove bad text channel checks ([f5939e2](https://github.com/revanced/revanced-bots/commit/f5939e25288fea2022fdeec9085ecb9ffada6111))
* **bots/discord:** remove redundant footer for response embeds ([412e003](https://github.com/revanced/revanced-bots/commit/412e00317d1eaca23e9c1375e16f94a5f2fa8d86))
* **bots/discord:** remove usage of macros ([7f27c56](https://github.com/revanced/revanced-bots/commit/7f27c5607ceeeef56d67097e88f68caa1b8791b3))
* **bots/discord:** remove useless feature ([d830e48](https://github.com/revanced/revanced-bots/commit/d830e48bc2de7aa457eab3a5f96ae652a93178f9))
* **bots/discord:** replace duration parser with a library ([94c4fed](https://github.com/revanced/revanced-bots/commit/94c4fedc06e20051e4123508e3134b97eb84782a))
* **bots/discord:** reset counter when reconnected to api, redo message scan filter logic ([d234d79](https://github.com/revanced/revanced-bots/commit/d234d79310caed9c43e14a905f9ef46a110e071d))
* **bots/discord:** revert dist denesting, fixes config not found ([0d4898d](https://github.com/revanced/revanced-bots/commit/0d4898dae8b26f8466d3f6b8f62875866f581644))
* **bots/discord:** send right response for after regexes ([a7688fa](https://github.com/revanced/revanced-bots/commit/a7688fa9b91919a87f74071b502cd0a87cd1c1fa))
* **bots/discord:** set the `label` property correctly for message scans ([6d463df](https://github.com/revanced/revanced-bots/commit/6d463df586dee5dd8fe8d6cff1c5316f7809b32a))
* **bots/discord:** set timeout for eligible mutes to unmute faster ([1f5c5a9](https://github.com/revanced/revanced-bots/commit/1f5c5a92a639973b83a1204355538936e69a4454))
* **bots/discord:** some configuration values not applying after running `/reload` ([a976dd2](https://github.com/revanced/revanced-bots/commit/a976dd2accc4b74914651245acde0979c30c92f5))
* **bots/discord:** use `APIEmbed` for response config ([35b9448](https://github.com/revanced/revanced-bots/commit/35b944800a3943c187d5b0e0d3e465ad7d2056fe))
* **bots/discord:** use env for initializing database ([af3759c](https://github.com/revanced/revanced-bots/commit/af3759caf428fada3b3f4a51852543d6fb280018))
* **bots/discord:** wrong command file path being imported ([fa0159c](https://github.com/revanced/revanced-bots/commit/fa0159c3a8dd4dad8778ccdb75b9e7c02ebbb64f))
* **bots/discord:** wrong database schema path ([875bd20](https://github.com/revanced/revanced-bots/commit/875bd209b252566414bf89349839cabc01697e1c))
* config file not being read ([474a8be](https://github.com/revanced/revanced-bots/commit/474a8be4af4eb2bae6e80a893439d846ad4f7503))
* **discord-bot:** also execute slash commands ([f0d45b2](https://github.com/revanced/revanced-bots/commit/f0d45b2c926ed753e2d21f2e06e24d7e6c43880a))
* **discord-bot:** check for role position ([d332043](https://github.com/revanced/revanced-bots/commit/d332043b1a4bb7ac9698a2fc912832e184130b4b))
* **discord-bot:** check if the member has the role ([9bff68c](https://github.com/revanced/revanced-bots/commit/9bff68c8c40c692764e4dec15a058e35059efbc9))
* **discord-bot:** not executing slash commands ([aa08087](https://github.com/revanced/revanced-bots/commit/aa0808768b90844c5fbd3e75d9f2d01c723b0151))
* **discord-bot:** only send lowercased text ([7803758](https://github.com/revanced/revanced-bots/commit/78037580dc92883f5ca21157e45268850cb5db90))
* dislike button not working properly ([85eba55](https://github.com/revanced/revanced-bots/commit/85eba554247738066af72a8efd0de215ec1164dc))
* fix deprecation ([4373ede](https://github.com/revanced/revanced-bots/commit/4373ede855333f209676551162a525238656e1f8))
* fix the fiter for the interaction collector ([a9ff003](https://github.com/revanced/revanced-bots/commit/a9ff00394a73f68a6793c2b35ff184675ee5a72c))
* fix typings and formatting ([479812e](https://github.com/revanced/revanced-bots/commit/479812e199b52cdb295a5746e0767306afab3413))
* ignore message if there's no content ([3cbebc2](https://github.com/revanced/revanced-bots/commit/3cbebc284277808495e64cf0fb47c555924ad9c5))
* move modules to `/bots` ([cd7156e](https://github.com/revanced/revanced-bots/commit/cd7156e792e65777ad1ab5a6f5d828b9ef6a9754))
* other small issues ([bc437a5](https://github.com/revanced/revanced-bots/commit/bc437a5ec7ce1d339094d608e2a61ac5f460c163))
* run projects with `--bun` ([bb2182e](https://github.com/revanced/revanced-bots/commit/bb2182e707fa40c555d56138972eeea28f1b3cf9))
* trainAI not using the bin location ([bd29943](https://github.com/revanced/revanced-bots/commit/bd2994388bc65f720120ef49edb6ba8163260309))
* update repo url ([a21aa34](https://github.com/revanced/revanced-bots/commit/a21aa348d7f32cd0ee65b371e9594520c0a9d3f1))
### chore
* fix more build issues ([77fefb9](https://github.com/revanced/revanced-bots/commit/77fefb9bef286a22f40a4d76b79c64fcc5a2467f))
### Features
* add wit.ai support ([1909e2c](https://github.com/revanced/revanced-bots/commit/1909e2c42148d635dcd045c738d88f65c8be16e3))
* **bots/discord/commands/reply:** send stacktrace when failed ([9f1ac37](https://github.com/revanced/revanced-bots/commit/9f1ac379276c11da65235577a9c6717e01cb02eb))
* **bots/discord/commands:** add `ban` and `unban` commands ([dc4863d](https://github.com/revanced/revanced-bots/commit/dc4863dc208b3fede4d4def323306ab58daffe04))
* **bots/discord/commands:** add `eval` command ([e64d1da](https://github.com/revanced/revanced-bots/commit/e64d1da00cc2ba718da5a4b0da141fe86a0e48d2))
* **bots/discord/commands:** add `mute` and `unmute` commands ([c0fa2fe](https://github.com/revanced/revanced-bots/commit/c0fa2fe1c36acdc7c52cde277aa7da867065f55e))
* **bots/discord/commands:** add `purge` and `role-preset` commands ([fb01ce5](https://github.com/revanced/revanced-bots/commit/fb01ce57400130c93751a11573eb444c0ba103eb))
* **bots/discord/commands:** add `reload` command ([6875b32](https://github.com/revanced/revanced-bots/commit/6875b32fd0c6ce3034da9dc6c704d425afb26f2e))
* **bots/discord/commands:** allow process exception in `exception-test` ([ca47535](https://github.com/revanced/revanced-bots/commit/ca475356ad95fec86e8e8b5bf4bbf17b70add5fe))
* **bots/discord/utils/discord/embeds:** expose `applyCommonEmbedStyles` fn ([2d794ed](https://github.com/revanced/revanced-bots/commit/2d794ede7d7a208bd3616c45e8e6d2a2cd83e9ed))
* **bots/discord/utils/embeds:** make title parameter nullable ([ee885ca](https://github.com/revanced/revanced-bots/commit/ee885ca7585a55fdc31e137ae29dc13a37ce2fb2))
* **bots/discord/utils/fs:** use `recursive` option for listing files ([da21e1a](https://github.com/revanced/revanced-bots/commit/da21e1a6f76deaeb477203b04263bd170863825b))
* **bots/discord/utils:** add duration utility ([a9add9e](https://github.com/revanced/revanced-bots/commit/a9add9ea9affb42bdfcb17cf4b268feec5729854))
* **bots/discord/utils:** add functions for role presets ([fb32a04](https://github.com/revanced/revanced-bots/commit/fb32a04ad38be8d0836dc99259b6ef05a0825830))
* **bots/discord/utils:** allow loading commands from custom dir ([8b690b8](https://github.com/revanced/revanced-bots/commit/8b690b879bb5c6023c8fc863afbd9fd1d02719bb))
* **bots/discord:** add `api.disconnectRetryInterval` config ([2f86586](https://github.com/revanced/revanced-bots/commit/2f8658617923c07f6847cbf1fdfc5f5379d95b6c))
* **bots/discord:** add `moderation.roles` config to be used in `moderation` commands ([39d5b3a](https://github.com/revanced/revanced-bots/commit/39d5b3a479b4d856aabe12cc31177c24f88ae23e))
* **bots/discord:** add `ocrTriggers` resp config, embed footer scan mode ([744a56a](https://github.com/revanced/revanced-bots/commit/744a56a4fdc8844e37959a88bcf81ee39fe726ef))
* **bots/discord:** add `replyToReplied` option in response config ([27662ed](https://github.com/revanced/revanced-bots/commit/27662ed91a79bfac7d3f091834e859a7b57366ce))
* **bots/discord:** add `train` commands ([ee90ef2](https://github.com/revanced/revanced-bots/commit/ee90ef247b4bf2b3c0698606b947116f2dc1b868))
* **bots/discord:** add a better way to manage databases ([a68d726](https://github.com/revanced/revanced-bots/commit/a68d72687584332587455962b0202a306288057d))
* **bots/discord:** add code to actually scan text files correctly ([80aeb19](https://github.com/revanced/revanced-bots/commit/80aeb1902063140a2e78cfaed9424e5101ab03f1))
* **bots/discord:** add default durations for moderation commands ([27d3b39](https://github.com/revanced/revanced-bots/commit/27d3b392092141a1e3b4b0298131ff7817458dc1))
* **bots/discord:** add more fallbacks for decancering ([2e1e009](https://github.com/revanced/revanced-bots/commit/2e1e009b4272495798313bd3bd61f258875c62e1))
* **bots/discord:** add more month aliases to duration parser ([c2009ca](https://github.com/revanced/revanced-bots/commit/c2009ca6d42e4387bc5f375d76ecf72991b7fe32))
* **bots/discord:** add more options for curing, fix default regex ([1a4ec1e](https://github.com/revanced/revanced-bots/commit/1a4ec1ece80becd9156582cc490f6681cb2a1f39))
* **bots/discord:** add source ([f9d50a0](https://github.com/revanced/revanced-bots/commit/f9d50a0a6bef8beaa428a0a555bfa4f879f685f1))
* **bots/discord:** add sticky messages ([bf66155](https://github.com/revanced/revanced-bots/commit/bf661556e131bf0ef24e47f658fbcd701960e312))
* **bots/discord:** add trigger to context for eval ([b5f4097](https://github.com/revanced/revanced-bots/commit/b5f40975386677ffff343c42f8ffac21f847a0b7))
* **bots/discord:** allow admins to bypass permission checks ([620f933](https://github.com/revanced/revanced-bots/commit/620f9339f0737b79d72c66d90ffa42ea3f987710))
* **bots/discord:** blacklist and whitelist for filters ([cdb6001](https://github.com/revanced/revanced-bots/commit/cdb600195520dba33110c40841629259e317055e))
* **bots/discord:** cure on every event ([8ff6086](https://github.com/revanced/revanced-bots/commit/8ff6086028132cc4b49ee60846e8d6ef909f5a89))
* **bots/discord:** don't nest builds in src directory, autogen db when missing ([4834685](https://github.com/revanced/revanced-bots/commit/48346851864c4d4b6276388644dd24ce16222b3e))
* **bots/discord:** framework changes and new features ([646ec8d](https://github.com/revanced/revanced-bots/commit/646ec8da87617e6c8f48a89e8054e2cba91da549))
* **bots/discord:** improve admin commands ([0346741](https://github.com/revanced/revanced-bots/commit/03467411882b8598e2c06f389a09ef2e201bb43f))
* **bots/discord:** improve logs ([6abb740](https://github.com/revanced/revanced-bots/commit/6abb7409945c10bd3af451fb45ef4b4d4ebe9489))
* **bots/discord:** sanitize `BasicDatabase` inputs ([fd76e0a](https://github.com/revanced/revanced-bots/commit/fd76e0af72fe28b414ae3b5e8d3886e58561e57e))
* **bots/discord:** support nickname decancering ([1723e8c](https://github.com/revanced/revanced-bots/commit/1723e8cacf96e8c6bdee22cfd30e89524fdcef74))
* **bots/discord:** switch to `drizzle-orm` ([e204b7b](https://github.com/revanced/revanced-bots/commit/e204b7b7566fd7fa423baef32977a8575d44a9e0))
* **bots/discord:** update config ([197d2ac](https://github.com/revanced/revanced-bots/commit/197d2acea89c38e43858d52736508d449152e804))
* **bots/discord:** update example config file ([bc9951c](https://github.com/revanced/revanced-bots/commit/bc9951c9b5e007c3e1b3076aa0966ccf29bb18bc))
* **bots/discord:** update to newer command definition framework ([97f2795](https://github.com/revanced/revanced-bots/commit/97f2795df4ede4d12a08193dba453c1bc765a4c2))
* discord bot scanning messages ([d1bd3b2](https://github.com/revanced/revanced-bots/commit/d1bd3b2b7e4985a64e9b070ab006cc6f3508c46e))
* **discord-bot:** a way to train AI ([355a508](https://github.com/revanced/revanced-bots/commit/355a50803adc85b5579155b55ddbba4fa0449237))
* **discord-bot:** command handler and train cmd ([6aee8a4](https://github.com/revanced/revanced-bots/commit/6aee8a4c63eb108800fcb0a23ca61f200d8f1f2a))
* **discord-bot:** event handler ([0ad5ece](https://github.com/revanced/revanced-bots/commit/0ad5ece08593c0db111fa4a592b42c6e0348fd1c))
* GODEL AI ([0ba525c](https://github.com/revanced/revanced-bots/commit/0ba525c4a5802106d582c75f713728accf2f151a))
* initalize discord bot ([bb4a5a7](https://github.com/revanced/revanced-bots/commit/bb4a5a77eefbc7ac88536f73a111df1050b235e7))
* initialize helper client ([7f9ca77](https://github.com/revanced/revanced-bots/commit/7f9ca77e0331ec143160ee51ed7c3aa9e4e70b9c))
* message buttons for training ([6551ca9](https://github.com/revanced/revanced-bots/commit/6551ca9dadc2e3ddfe98875e80ed61f7d71a1651))
* platform specific responses ([18e57b0](https://github.com/revanced/revanced-bots/commit/18e57b0c320732a937bb60db11c5d6794ed11522))
* prettier and eslint ([1c27ccb](https://github.com/revanced/revanced-bots/commit/1c27ccb17c85f0f6982db45de426181d2c231d0e))
* refactor and new features ([#7](https://github.com/revanced/revanced-bots/issues/7)) ([8b9f45d](https://github.com/revanced/revanced-bots/commit/8b9f45dc22de29dc2ccb1cfab9a026db00457e25))
* run bots in one process ([d26d533](https://github.com/revanced/revanced-bots/commit/d26d53317440c64fb775cea609a87d29be6c8b40))
* training and replies changed ([715aa91](https://github.com/revanced/revanced-bots/commit/715aa918cf84213c9b19591a398d7532eb3f232a))
* **utils/discord/embeds:** allow adding extra fields for moderation embeds ([49ce9a7](https://github.com/revanced/revanced-bots/commit/49ce9a7ca3d8558b73a9b94dfe7a01d809db6fff))
### BREAKING CHANGES
* In `@revanced/discord-bot`, its environment variable
`DATABASE_URL` has been renamed to `DATABASE_PATH`
and the `file:` prefix is no longer needed
# @revanced/discord-bot [1.0.0-dev.38](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0-dev.37...@revanced/discord-bot@1.0.0-dev.38) (2025-04-04)
### Bug Fixes
* run projects with `--bun` ([bb2182e](https://github.com/revanced/revanced-bots/commit/bb2182e707fa40c555d56138972eeea28f1b3cf9))
# @revanced/discord-bot [1.0.0-dev.37](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0-dev.36...@revanced/discord-bot@1.0.0-dev.37) (2025-03-08)
### Bug Fixes
* **bots/discord/utils/duration:** fix specified default unit not working ([d138af4](https://github.com/revanced/revanced-bots/commit/d138af46d1f25a11b6f8ab3790ecaa70b1d716a9))
# @revanced/discord-bot [1.0.0-dev.36](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.0.0-dev.35...@revanced/discord-bot@1.0.0-dev.36) (2025-03-03)
### Bug Fixes
* **bots/discord/scripts/build:** check if dist dir exists before cleaning ([c06033e](https://github.com/revanced/revanced-bots/commit/c06033e5730f82438e8052b9b519a8f8e2d25437))
* **bots/discord/utils/duration:** make second the default unit ([5d1af3c](https://github.com/revanced/revanced-bots/commit/5d1af3c31c3379b6a13684dfb07583737908c8aa))
* **bots/discord:** add GuildMember partial ([8e3946a](https://github.com/revanced/revanced-bots/commit/8e3946a66602838715787090008c7bfaf72b67e9))
* **bots/discord:** decrease length of an option in `ban` command ([22d3eea](https://github.com/revanced/revanced-bots/commit/22d3eea88d532792c1237d1a1ab18bc02e57816a))
* **bots/discord:** delete expired appliedPresets entries after unapplying ([14c98e8](https://github.com/revanced/revanced-bots/commit/14c98e87df1ec4fd762bbc48ca4c06470cb110a2))
* fix typings and formatting ([479812e](https://github.com/revanced/revanced-bots/commit/479812e199b52cdb295a5746e0767306afab3413))
* update repo url ([a21aa34](https://github.com/revanced/revanced-bots/commit/a21aa348d7f32cd0ee65b371e9594520c0a9d3f1))
### Features
* **bots/discord:** add more month aliases to duration parser ([c2009ca](https://github.com/revanced/revanced-bots/commit/c2009ca6d42e4387bc5f375d76ecf72991b7fe32))
# @revanced/discord-bot [1.0.0-dev.35](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.34...@revanced/discord-bot@1.0.0-dev.35) (2024-10-17)
### Bug Fixes
* **bots/discord:** fix freeze on prod builds ([8efb549](https://github.com/revanced/revanced-helper/commit/8efb549453a04fab1ac6414a7f7f8bf702df3c93))
# @revanced/discord-bot [1.0.0-dev.34](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.33...@revanced/discord-bot@1.0.0-dev.34) (2024-10-17)
### Bug Fixes
* **bots/discord:** attempt to fix stuck sticky message timeouts ([3ed5bd1](https://github.com/revanced/revanced-helper/commit/3ed5bd11acc3b4fbd57b0d632c68eb9f77365b8a))
* **bots/discord:** fix reload not working ([11582d5](https://github.com/revanced/revanced-helper/commit/11582d50345cae9fb645a65ca4e621596de6a408))
### Features
* **bots/discord:** add default durations for moderation commands ([27d3b39](https://github.com/revanced/revanced-helper/commit/27d3b392092141a1e3b4b0298131ff7817458dc1))
* **bots/discord:** cure on every event ([8ff6086](https://github.com/revanced/revanced-helper/commit/8ff6086028132cc4b49ee60846e8d6ef909f5a89))
# @revanced/discord-bot [1.0.0-dev.33](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.32...@revanced/discord-bot@1.0.0-dev.33) (2024-09-25)
### Features
* **bots/discord:** add trigger to context for eval ([b5f4097](https://github.com/revanced/revanced-helper/commit/b5f40975386677ffff343c42f8ffac21f847a0b7))
# @revanced/discord-bot [1.0.0-dev.32](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.31...@revanced/discord-bot@1.0.0-dev.32) (2024-09-25)
### Bug Fixes
* **bots/discord:** contextify object before sandboxing ([062735f](https://github.com/revanced/revanced-helper/commit/062735f6d552890404d6192244c51a11b0709580))
# @revanced/discord-bot [1.0.0-dev.31](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.30...@revanced/discord-bot@1.0.0-dev.31) (2024-09-25)
### Bug Fixes
* **bots/discord:** persist changes in context for eval command ([5b4965d](https://github.com/revanced/revanced-helper/commit/5b4965dcc7285676b2b3b6756c249bd56eaf8485))
# @revanced/discord-bot [1.0.0-dev.30](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.29...@revanced/discord-bot@1.0.0-dev.30) (2024-09-24)
### Features
* **bots/discord:** improve admin commands ([0346741](https://github.com/revanced/revanced-helper/commit/03467411882b8598e2c06f389a09ef2e201bb43f))
# @revanced/discord-bot [1.0.0-dev.29](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.28...@revanced/discord-bot@1.0.0-dev.29) (2024-09-21)
### Bug Fixes
* **bots/discord:** fix get response logic ([3261294](https://github.com/revanced/revanced-helper/commit/3261294822b0a9faec094536ed5be2d3e1d5e17b))
# @revanced/discord-bot [1.0.0-dev.28](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.27...@revanced/discord-bot@1.0.0-dev.28) (2024-09-20)
### Bug Fixes
* **bots/discord:** don't refresh timer if force timer is active for sticky messages ([4abac0c](https://github.com/revanced/revanced-helper/commit/4abac0c890c0548e14cb56723cae919353a8e726))
* **bots/discord:** filter out text triggers correctly from image-only scans ([8c0dd67](https://github.com/revanced/revanced-helper/commit/8c0dd67d03d5a1747993da08a5bf82a39de43789))
# @revanced/discord-bot [1.0.0-dev.27](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.26...@revanced/discord-bot@1.0.0-dev.27) (2024-09-05)
### Bug Fixes
* **bots/discord:** correct permission check logic ([dd8872c](https://github.com/revanced/revanced-helper/commit/dd8872c027c7e7e1a00f38d659b4d6e79274238c))
* **bots/discord:** give only removed roles for role presets ([522ad28](https://github.com/revanced/revanced-helper/commit/522ad28fd83565e9ca411dbce86c8447574288fd))
* **bots/discord:** replace duration parser with a library ([94c4fed](https://github.com/revanced/revanced-helper/commit/94c4fedc06e20051e4123508e3134b97eb84782a))
# @revanced/discord-bot [1.0.0-dev.27](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.26...@revanced/discord-bot@1.0.0-dev.27) (2024-08-23)
### Bug Fixes
* **bots/discord:** give only removed roles for role presets ([522ad28](https://github.com/revanced/revanced-helper/commit/522ad28fd83565e9ca411dbce86c8447574288fd))
* **bots/discord:** replace duration parser with a library ([94c4fed](https://github.com/revanced/revanced-helper/commit/94c4fedc06e20051e4123508e3134b97eb84782a))
# @revanced/discord-bot [1.0.0-dev.26](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.25...@revanced/discord-bot@1.0.0-dev.26) (2024-08-15)
### Bug Fixes
* **bots/discord:** correct timer active condition for sticky messages ([96065ff](https://github.com/revanced/revanced-helper/commit/96065ff17584ff99a56ca5008327863ca5a7852b))
# @revanced/discord-bot [1.0.0-dev.25](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.24...@revanced/discord-bot@1.0.0-dev.25) (2024-08-15)
### Bug Fixes
* **bots/discord:** allow access to `context` in `/eval` and await result ([99f65f0](https://github.com/revanced/revanced-helper/commit/99f65f07f5f8830c6e8ea4ae171e986af4d3f1f6))
# @revanced/discord-bot [1.0.0-dev.24](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.23...@revanced/discord-bot@1.0.0-dev.24) (2024-08-14)
### Bug Fixes
* **bots/discord:** do not remove unrelated reactions ([031fd26](https://github.com/revanced/revanced-helper/commit/031fd26b2619ecafeff3964e50accacb87de6108))
# @revanced/discord-bot [1.0.0-dev.23](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.22...@revanced/discord-bot@1.0.0-dev.23) (2024-08-13)
### Features
* **bots/discord:** add `train` commands ([ee90ef2](https://github.com/revanced/revanced-helper/commit/ee90ef247b4bf2b3c0698606b947116f2dc1b868))
* **bots/discord:** update to newer command definition framework ([97f2795](https://github.com/revanced/revanced-helper/commit/97f2795df4ede4d12a08193dba453c1bc765a4c2))
# @revanced/discord-bot [1.0.0-dev.22](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.21...@revanced/discord-bot@1.0.0-dev.22) (2024-08-10)
### Bug Fixes
* **bots/discord:** parse larger units of durations, fix wrong timestamp in mod embed ([6c8dce0](https://github.com/revanced/revanced-helper/commit/6c8dce059366a6ef85f5b8b1794c056515b9f5b6))
* **bots/discord:** provide discord token for `reload` command ([dd21a5a](https://github.com/revanced/revanced-helper/commit/dd21a5abad560f3d00b8c58912786d4b6bd520e9))
### Features
* **bots/discord:** add code to actually scan text files correctly ([80aeb19](https://github.com/revanced/revanced-helper/commit/80aeb1902063140a2e78cfaed9424e5101ab03f1))
# @revanced/discord-bot [1.0.0-dev.21](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.20...@revanced/discord-bot@1.0.0-dev.21) (2024-08-04)
### Bug Fixes
* **bots/discord:** correct sticky messages logic ([de8bef6](https://github.com/revanced/revanced-helper/commit/de8bef6520d53a1299f0478458320a7eb75c5e1d))
* **bots/discord:** make `/eval` work ([eaa25f2](https://github.com/revanced/revanced-helper/commit/eaa25f2eb58a9e2d25bb98633ad668485e099714))
* **bots/discord:** some configuration values not applying after running `/reload` ([a976dd2](https://github.com/revanced/revanced-helper/commit/a976dd2accc4b74914651245acde0979c30c92f5))
# @revanced/discord-bot [1.0.0-dev.20](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.19...@revanced/discord-bot@1.0.0-dev.20) (2024-08-03)
### Bug Fixes
* **bots/discord:** await when putting entries into db ([4da6175](https://github.com/revanced/revanced-helper/commit/4da6175cf58b1fa6144bdc71ec806766d32c1025))
# @revanced/discord-bot [1.0.0-dev.19](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.18...@revanced/discord-bot@1.0.0-dev.19) (2024-08-03)
### Bug Fixes
* **bots/discord:** correct whitelist logic ([49c29be](https://github.com/revanced/revanced-helper/commit/49c29bebfbe348ae4e2cc1b3a83bfa41eb26ccd1))
# @revanced/discord-bot [1.0.0-dev.18](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.17...@revanced/discord-bot@1.0.0-dev.18) (2024-08-03)
### Bug Fixes
* **bots/discord:** set the `label` property correctly for message scans ([6d463df](https://github.com/revanced/revanced-helper/commit/6d463df586dee5dd8fe8d6cff1c5316f7809b32a))
# @revanced/discord-bot [1.0.0-dev.17](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.16...@revanced/discord-bot@1.0.0-dev.17) (2024-08-02)
### Bug Fixes
* **bots/discord/commands/eval:** evaluate in current context ([5925d90](https://github.com/revanced/revanced-helper/commit/5925d902095acef5f6396ca03583a9cbb0862498))
* **bots/discord:** send right response for after regexes ([a7688fa](https://github.com/revanced/revanced-helper/commit/a7688fa9b91919a87f74071b502cd0a87cd1c1fa))
### Features
* **bots/discord:** update example config file ([bc9951c](https://github.com/revanced/revanced-helper/commit/bc9951c9b5e007c3e1b3076aa0966ccf29bb18bc))
# @revanced/discord-bot [1.0.0-dev.16](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.15...@revanced/discord-bot@1.0.0-dev.16) (2024-08-02)
### Bug Fixes
* **bots/discord:** open database as read-write ([c366840](https://github.com/revanced/revanced-helper/commit/c36684091dddf67880505dc459e4334a8a5492f4))
* **bots/discord:** remove bad text channel checks ([f5939e2](https://github.com/revanced/revanced-helper/commit/f5939e25288fea2022fdeec9085ecb9ffada6111))
* **bots/discord:** remove redundant footer for response embeds ([412e003](https://github.com/revanced/revanced-helper/commit/412e00317d1eaca23e9c1375e16f94a5f2fa8d86))
### Features
* **bots/discord/commands:** add `reload` command ([6875b32](https://github.com/revanced/revanced-helper/commit/6875b32fd0c6ce3034da9dc6c704d425afb26f2e))
# @revanced/discord-bot [1.0.0-dev.15](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.14...@revanced/discord-bot@1.0.0-dev.15) (2024-07-31)
### Bug Fixes
* **bots/discord:** import `config` from context ([763ef25](https://github.com/revanced/revanced-helper/commit/763ef253f9d4ff70a8b79969a7f4f41cba7f3c59))
### Features
* **bots/discord:** add sticky messages ([bf66155](https://github.com/revanced/revanced-helper/commit/bf661556e131bf0ef24e47f658fbcd701960e312))
# @revanced/discord-bot [1.0.0-dev.14](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.13...@revanced/discord-bot@1.0.0-dev.14) (2024-07-31)
### Bug Fixes
* **bots/discord:** always true check causing no messages to be scanned ([98ec37b](https://github.com/revanced/revanced-helper/commit/98ec37b5d18cade85270ab83b0ed0abe41244dd9))
* other small issues ([bc437a5](https://github.com/revanced/revanced-helper/commit/bc437a5ec7ce1d339094d608e2a61ac5f460c163))
### Features
* **bots/discord:** add more options for curing, fix default regex ([1a4ec1e](https://github.com/revanced/revanced-helper/commit/1a4ec1ece80becd9156582cc490f6681cb2a1f39))
* **bots/discord:** allow admins to bypass permission checks ([620f933](https://github.com/revanced/revanced-helper/commit/620f9339f0737b79d72c66d90ffa42ea3f987710))
# @revanced/discord-bot [1.0.0-dev.13](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.12...@revanced/discord-bot@1.0.0-dev.13) (2024-07-30) # @revanced/discord-bot [1.0.0-dev.13](https://github.com/revanced/revanced-helper/compare/@revanced/discord-bot@1.0.0-dev.12...@revanced/discord-bot@1.0.0-dev.13) (2024-07-30)

View File

@@ -15,4 +15,4 @@ COPY --from=build /build/bots/discord/dist /app
USER 1000:1000 USER 1000:1000
ENTRYPOINT [ "bun", "run", "src/index.js" ] ENTRYPOINT [ "bun", "--bun", "run", "src/index.js" ]

View File

@@ -11,8 +11,21 @@ export default {
GUILD_ID_HERE: ['ROLE_ID_HERE'], GUILD_ID_HERE: ['ROLE_ID_HERE'],
}, },
}, },
stickyMessages: {
GUILD_ID_HERE: {
CHANNEL_ID_HERE: {
message: {
content: 'This is a sticky message!',
},
timeout: 60000,
forceSendTimeout: 300000,
},
},
},
moderation: { moderation: {
cure: { cure: {
minimumNameLength: 3,
removeCharactersRegex: /[^a-zA-Z0-9 \-_]/g,
defaultName: 'Server member', defaultName: 'Server member',
}, },
roles: ['ROLE_ID_HERE'], roles: ['ROLE_ID_HERE'],
@@ -53,7 +66,6 @@ export default {
}, },
}, },
humanCorrections: { humanCorrections: {
falsePositiveLabel: 'false_positive',
allow: { allow: {
members: { members: {
permissions: 8n, permissions: 8n,
@@ -61,7 +73,11 @@ export default {
}, },
}, },
}, },
allowedAttachmentMimeTypes: ['image/jpeg', 'image/png', 'image/webp'], attachments: {
scanAttachments: true,
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'text/plain'],
maxTextFileSize: 512000,
},
responses: [ responses: [
{ {
filterOverride: { filterOverride: {

View File

@@ -6,9 +6,12 @@ export type Config = {
users?: string[] users?: string[]
roles?: Record<string, string[]> roles?: Record<string, string[]>
} }
stickyMessages?: Record<string, Record<string, StickyMessageConfig>>
moderation?: { moderation?: {
roles: string[] roles: string[]
cure?: { cure?: {
minimumNameLength?: number
removeCharactersRegex?: RegExp
defaultName: string defaultName: string
} }
log?: { log?: {
@@ -23,13 +26,16 @@ export type Config = {
messageScan?: { messageScan?: {
scanBots?: boolean scanBots?: boolean
scanOutsideGuilds?: boolean scanOutsideGuilds?: boolean
allowedAttachmentMimeTypes: string[] attachments?: {
scanAttachments?: boolean
allowedMimeTypes?: string[]
maxTextFileSize?: number
}
filter?: { filter?: {
whitelist?: Filter whitelist?: Filter
blacklist?: Filter blacklist?: Filter
} }
humanCorrections: { humanCorrections: {
falsePositiveLabel: string
allow?: { allow?: {
users?: string[] users?: string[]
members?: { members?: {
@@ -48,6 +54,12 @@ export type Config = {
} }
} }
export type StickyMessageConfig = {
timeout: number
forceSendTimeout?: number
message: BaseMessageOptions
}
export type RolePresetConfig = { export type RolePresetConfig = {
give: string[] give: string[]
take: string[] take: string[]
@@ -59,8 +71,8 @@ export type ConfigMessageScanResponse = {
image?: Array<RegExp> image?: Array<RegExp>
} }
filterOverride?: NonNullable<Config['messageScan']>['filter'] filterOverride?: NonNullable<Config['messageScan']>['filter']
response: ConfigMessageScanResponseMessage | null response: ConfigMessageScanResponseMessage
replyToReplied?: boolean respondToReply?: boolean | 'only_regex' | 'only_labeled'
} }
export type ConfigMessageScanResponseLabelConfig = { export type ConfigMessageScanResponseLabelConfig = {

View File

@@ -2,20 +2,19 @@
"name": "@revanced/discord-bot", "name": "@revanced/discord-bot",
"type": "module", "type": "module",
"private": true, "private": true,
"version": "1.0.0-dev.13", "version": "1.5.2",
"description": "🤖 Discord bot assisting ReVanced", "description": "🤖 Discord bot assisting ReVanced",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"register": "bun run scripts/reload-slash-commands.ts",
"start": "bun prepare && bun run src/index.ts", "start": "bun prepare && bun run src/index.ts",
"dev": "bun prepare && bun --watch src/index.ts", "dev": "bun prepare && bun --watch src/index.ts",
"build": "bun prepare && bun run scripts/build.ts", "build": "bun prepare && bun run scripts/build.ts",
"watch": "bun dev", "watch": "bun dev",
"prepare": "bun run scripts/generate-indexes.ts && bunx drizzle-kit generate --name=schema" "prepare": "bun run scripts/generate-indexes.ts && bunx --bun drizzle-kit generate --name=schema"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/revanced/revanced-helper.git", "url": "git+https://github.com/revanced/revanced-bots.git",
"directory": "bots/discord" "directory": "bots/discord"
}, },
"author": "Palm <contact@palmdevs.me> (https://palmdevs.me)", "author": "Palm <contact@palmdevs.me> (https://palmdevs.me)",
@@ -25,22 +24,23 @@
], ],
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/revanced/revanced-helper/issues" "url": "https://github.com/revanced/revanced-bots/issues"
}, },
"homepage": "https://github.com/revanced/revanced-helper#readme", "homepage": "https://github.com/revanced/revanced-bots#readme",
"dependencies": { "dependencies": {
"@discordjs/builders": "^1.8.2", "@discordjs/builders": "^1.11.3",
"@discordjs/rest": "^2.3.0", "@discordjs/rest": "^2.6.0",
"@revanced/bot-api": "workspace:*", "@revanced/bot-api": "workspace:*",
"@revanced/bot-shared": "workspace:*", "@revanced/bot-shared": "workspace:*",
"chalk": "^5.3.0", "@sapphire/duration": "^1.2.0",
"decancer": "^3.2.3", "chalk": "^5.6.2",
"discord.js": "^14.15.3", "decancer": "^3.3.3",
"drizzle-orm": "^0.31.4" "discord.js": "^14.22.1",
"drizzle-orm": "^0.44.5"
}, },
"devDependencies": { "devDependencies": {
"@libsql/client": "^0.7.0", "@libsql/client": "^0.15.15",
"discord-api-types": "^0.37.92", "discord-api-types": "^0.38.24",
"drizzle-kit": "^0.22.8" "drizzle-kit": "^0.31.4"
} }
} }

View File

@@ -1,10 +1,10 @@
import { createLogger } from '@revanced/bot-shared' import { createLogger } from '@revanced/bot-shared'
import { cp, rm } from 'fs/promises' import { cp, exists, rm } from 'fs/promises'
const logger = createLogger() const logger = createLogger()
logger.warn('Cleaning previous build...') logger.warn('Cleaning previous build...')
await rm('./dist', { recursive: true }) if (await exists('./dist')) await rm('./dist', { recursive: true })
logger.info('Building bot...') logger.info('Building bot...')
await Bun.build({ await Bun.build({

View File

@@ -1,76 +1,143 @@
import { ApplicationCommandOptionType } from 'discord.js' import { ApplicationCommandOptionType, ApplicationCommandType } from 'discord.js'
import { createErrorEmbed } from '$/utils/discord/embeds'
import { isAdmin } from '$/utils/discord/permissions'
import { config } from '../context' import { config } from '../context'
import { isAdmin } from '../utils/discord/permissions'
import CommandError, { CommandErrorType } from './CommandError' import CommandError, { CommandErrorType } from './CommandError'
import type { Filter } from 'config.schema'
import type { import type {
APIApplicationCommandChannelOption, APIApplicationCommandChannelOption,
CacheType, CacheType,
Channel, Channel,
ChatInputCommandInteraction, ChatInputCommandInteraction,
CommandInteraction,
CommandInteractionOption, CommandInteractionOption,
GuildMember, GuildMember,
Message, Message,
MessageContextMenuCommandInteraction,
RESTPostAPIApplicationCommandsJSONBody,
RESTPostAPIChatInputApplicationCommandsJSONBody, RESTPostAPIChatInputApplicationCommandsJSONBody,
Role, Role,
User, User,
UserContextMenuCommandInteraction,
UserResolvable,
} from 'discord.js' } from 'discord.js'
export enum CommandType {
ChatGlobal = 1,
ChatGuild,
ContextMenuUser,
ContextMenuMessage,
ContextMenuGuildMessage,
ContextMenuGuildMember,
}
export default class Command< export default class Command<
Global extends boolean = false, const Type extends CommandType = CommandType.ChatGuild,
Options extends CommandOptionsOptions | undefined = undefined, const Options extends If<IsContextMenu<Type>, undefined, CommandOptionsOptions | undefined> = undefined,
AllowMessageCommand extends boolean = false, const AllowMessageCommand extends If<IsContextMenu<Type>, false, boolean> = false,
> { > {
name: string name: string
description: string description: string
requirements?: CommandRequirements requirements?: CommandRequirements
options?: Options options?: Options
global?: Global type: Type
#execute: CommandExecuteFunction<Global, Options, AllowMessageCommand> allowMessageCommand: AllowMessageCommand
#execute: CommandExecuteFunction<Type, Options, AllowMessageCommand>
static OptionType = ApplicationCommandOptionType static OptionType = ApplicationCommandOptionType
static Type = CommandType
constructor({ constructor({
name, name,
description, description,
requirements, requirements,
options, options,
global, type,
allowMessageCommand,
execute, execute,
}: CommandOptions<Global, Options, AllowMessageCommand>) { }: CommandOptions<Type, Options, AllowMessageCommand>) {
this.name = name this.name = name
this.description = description this.description = description!
this.requirements = requirements this.requirements = requirements
this.options = options this.options = options
this.global = global // @ts-expect-error: Default is `CommandType.GuildOnly`, it makes sense
this.type = type ?? CommandType.ChatGuild
// @ts-expect-error: Default is `false`, it makes sense
this.allowMessageCommand = allowMessageCommand ?? false
this.#execute = execute this.#execute = execute
} }
isGuildSpecific(): this is Command<
CommandType.ChatGuild | CommandType.ContextMenuGuildMember | CommandType.ContextMenuGuildMessage,
Options,
AllowMessageCommand
> {
return [
CommandType.ChatGuild,
CommandType.ContextMenuGuildMessage,
CommandType.ContextMenuGuildMember,
].includes(this.type)
}
isContextMenuSpecific(): this is Command<
| CommandType.ContextMenuGuildMessage
| CommandType.ContextMenuGuildMember
| CommandType.ContextMenuUser
| CommandType.ContextMenuMessage,
undefined,
false
> {
return [
CommandType.ContextMenuMessage,
CommandType.ContextMenuUser,
CommandType.ContextMenuGuildMessage,
CommandType.ContextMenuGuildMember,
].includes(this.type)
}
isGuildContextMenuSpecific(): this is Command<
CommandType.ContextMenuGuildMessage | CommandType.ContextMenuGuildMember,
undefined,
false
> {
return [CommandType.ContextMenuGuildMessage, CommandType.ContextMenuGuildMember].includes(this.type)
}
async onContextMenuInteraction(
context: typeof import('../context'),
interaction: If<
Extends<Type, CommandType.ContextMenuGuildMessage>,
MessageContextMenuCommandInteraction<ToCacheType<Type>>,
UserContextMenuCommandInteraction<ToCacheType<Type>>
>,
): Promise<unknown> {
if (!this.isGuildSpecific() && !interaction.inGuild())
throw new CommandError(CommandErrorType.InteractionNotInGuild)
const executor = await this.#fetchInteractionExecutor(interaction)
const target =
this.type === CommandType.ContextMenuGuildMember
? this.isGuildSpecific()
? fetchMember(interaction as CommandInteraction<'raw' | 'cached'>, interaction.targetId)
: interaction.client.users.fetch(interaction.targetId)
: interaction.channel?.messages.fetch(interaction.targetId)
if (!target) throw new CommandError(CommandErrorType.FetchManagerNotFound)
// @ts-expect-error: Type mismatch (again!) because TypeScript is not smart enough
return await this.#execute({ ...context, executor, target }, interaction, undefined)
}
async onMessage( async onMessage(
context: typeof import('../context'), context: typeof import('../context'),
msg: Message<If<Global, false, true>>, msg: Message<IsGuildSpecific<Type>>,
args: CommandArguments, args: CommandArguments,
): Promise<unknown> { ): Promise<unknown> {
if (!this.global && !msg.inGuild()) if (!this.allowMessageCommand) return
return await msg.reply({ if (!this.isGuildSpecific() && !msg.guildId) throw new CommandError(CommandErrorType.InteractionNotInGuild)
embeds: [createErrorEmbed('Cannot run this command', 'This command can only be used in a server.')],
})
const executor = this.global ? msg.author : await msg.guild?.members.fetch(msg.author.id)! const executor = this.isGuildSpecific()
? await msg.guild?.members.fetch(msg.author)!
if (!(await this.canExecute(executor, msg.channelId))) : await msg.client.users.fetch(msg.author)
return await msg.reply({ if (!(await this.canExecute(executor))) throw new CommandError(CommandErrorType.RequirementsNotMet)
embeds: [
createErrorEmbed(
'Cannot run this command',
'You do not meet the requirements to run this command.',
),
],
})
const options = this.options const options = this.options
? ((await this.#resolveMessageOptions(msg, this.options, args)) as CommandExecuteFunctionOptionsParameter< ? ((await this.#resolveMessageOptions(msg, this.options, args)) as CommandExecuteFunctionOptionsParameter<
@@ -120,14 +187,18 @@ export default class Command<
`Invalid type for argument **${name}**.${argExplainationString}\n\nExpected type: **${expectedType}**\nGot type: **${ApplicationCommandOptionType[arg.type]}**${choicesString}`, `Invalid type for argument **${name}**.${argExplainationString}\n\nExpected type: **${expectedType}**\nGot type: **${ApplicationCommandOptionType[arg.type]}**${choicesString}`,
) )
if ('choices' in option && option.choices && !option.choices.some(({ value }) => value === arg)) const argValue = typeof arg === 'string' ? arg : arg?.id
if (
'choices' in option &&
option.choices &&
!option.choices.some(({ value }) => value === (typeof value === 'number' ? Number(argValue) : argValue))
)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidArgument, CommandErrorType.InvalidArgument,
`Invalid choice for argument **${name}**.\n${argExplainationString}\n\n${choicesString}\n`, `Invalid choice for argument **${name}**.\n${argExplainationString}${choicesString}\n`,
) )
const argValue = typeof arg === 'string' ? arg : arg?.id
if (argValue && arg) { if (argValue && arg) {
if (isSubcommandLikeOption) { if (isSubcommandLikeOption) {
const [subcommandName, subcommandOption] = iterableOptions.find(([name]) => name === argValue)! const [subcommandName, subcommandOption] = iterableOptions.find(([name]) => name === argValue)!
@@ -142,6 +213,16 @@ export default class Command<
break break
} }
if (
type === ApplicationCommandOptionType.String &&
((typeof option.minLength === 'number' && argValue.length < option.minLength) ||
(typeof option.maxLength === 'number' && argValue.length > option.maxLength))
)
throw new CommandError(
CommandErrorType.InvalidArgument,
`Invalid string length for argument **${name}**.\nLengths allowed: ${option.minLength ?? '(any)'} - ${option.maxLength ?? '(any)'}.${argExplainationString}`,
)
if ( if (
(type === ApplicationCommandOptionType.Channel || (type === ApplicationCommandOptionType.Channel ||
type === ApplicationCommandOptionType.User || type === ApplicationCommandOptionType.User ||
@@ -153,14 +234,21 @@ export default class Command<
`Malformed ID for argument **${name}**.${argExplainationString}`, `Malformed ID for argument **${name}**.${argExplainationString}`,
) )
if ( if (type === ApplicationCommandOptionType.Number || type === ApplicationCommandOptionType.Integer) {
(type === ApplicationCommandOptionType.Number || type === ApplicationCommandOptionType.Integer) && if (Number.isNaN(Number(argValue)))
Number.isNaN(Number(argValue))
) {
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidArgument, CommandErrorType.InvalidArgument,
`Invalid number for argument **${name}**.${argExplainationString}`, `Invalid number for argument **${name}**.${argExplainationString}`,
) )
if (
(typeof option.min === 'number' && Number(argValue) < option.min) ||
(typeof option.max === 'number' && Number(argValue) > option.max)
)
throw new CommandError(
CommandErrorType.InvalidArgument,
`Number out of range for argument **${name}**.\nRange allowed: ${option.min ?? '(any)'} - ${option.max ?? '(any)'}.${argExplainationString}`,
)
} }
if ( if (
@@ -177,7 +265,7 @@ export default class Command<
type === ApplicationCommandOptionType.Number || type === ApplicationCommandOptionType.Integer type === ApplicationCommandOptionType.Number || type === ApplicationCommandOptionType.Integer
? Number(argValue) ? Number(argValue)
: type === ApplicationCommandOptionType.Boolean : type === ApplicationCommandOptionType.Boolean
? argValue[0] === 't' || argValue[0] === 'y' ? ['t', 'y', 'yes', 'true'].some(value => value === argValue.toLowerCase())
: type === ApplicationCommandOptionType.Channel : type === ApplicationCommandOptionType.Channel
? await msg.client.channels.fetch(argValue) ? await msg.client.channels.fetch(argValue)
: type === ApplicationCommandOptionType.User : type === ApplicationCommandOptionType.User
@@ -191,44 +279,27 @@ export default class Command<
return _options return _options
} }
#fetchInteractionExecutor(interaction: CommandInteraction) {
return this.isGuildSpecific()
? fetchMember(interaction as CommandInteraction<'raw' | 'cached'>)
: fetchUser(interaction)
}
async onInteraction( async onInteraction(
context: typeof import('../context'), context: typeof import('../context'),
interaction: ChatInputCommandInteraction, interaction: ChatInputCommandInteraction,
): Promise<unknown> { ): Promise<unknown> {
const { logger } = context if (interaction.commandName !== this.name)
throw new CommandError(
if (interaction.commandName !== this.name) { CommandErrorType.InteractionDataMismatch,
logger.warn(`Command name mismatch, expected ${this.name}, but got ${interaction.commandName}!`)
return await interaction.reply({
embeds: [
createErrorEmbed(
'Internal command name mismatch',
'The interaction command name does not match the expected command name.', 'The interaction command name does not match the expected command name.',
), )
],
})
}
if (!this.global && !interaction.inGuild()) { if (!this.isGuildSpecific() && !interaction.inGuild())
logger.error(`Command ${this.name} cannot be run in DMs, but was registered as global`) throw new CommandError(CommandErrorType.InteractionNotInGuild)
return await interaction.reply({
embeds: [createErrorEmbed('Cannot run this command', 'This command can only be used in a server.')],
ephemeral: true,
})
}
const executor = this.global ? interaction.user : await interaction.guild?.members.fetch(interaction.user.id)! const executor = await this.#fetchInteractionExecutor(interaction)
if (!(await this.canExecute(executor))) throw new CommandError(CommandErrorType.RequirementsNotMet)
if (!(await this.canExecute(executor, interaction.channelId)))
return await interaction.reply({
embeds: [
createErrorEmbed(
'Cannot run this command',
'You do not meet the requirements to run this command.',
),
],
ephemeral: true,
})
const options = this.options const options = this.options
? ((await this.#resolveInteractionOptions(interaction)) as CommandExecuteFunctionOptionsParameter< ? ((await this.#resolveInteractionOptions(interaction)) as CommandExecuteFunctionOptionsParameter<
@@ -237,14 +308,10 @@ export default class Command<
: undefined : undefined
if (options === null) if (options === null)
return await interaction.reply({ throw new CommandError(
embeds: [ CommandErrorType.InteractionDataMismatch,
createErrorEmbed( 'The registered interaction command option type does not match the expected command option type.',
'Internal command option type mismatch', )
'The interaction command option type does not match the expected command option type.',
),
],
})
// @ts-expect-error: Type mismatch (again!) because TypeScript is not smart enough // @ts-expect-error: Type mismatch (again!) because TypeScript is not smart enough
return await this.#execute({ ...context, executor }, interaction, options) return await this.#execute({ ...context, executor }, interaction, options)
@@ -288,12 +355,14 @@ export default class Command<
return _options return _options
} }
async canExecute(executor: User | GuildMember, channelId: string): Promise<boolean> { async canExecute(executor: User | GuildMember): Promise<boolean> {
if (!this.requirements) return false if (!this.requirements) return false
const isExecutorAdmin = isAdmin(executor)
if (isExecutorAdmin) return true
const { const {
adminOnly, adminOnly,
channels,
roles, roles,
permissions, permissions,
users, users,
@@ -302,16 +371,23 @@ export default class Command<
memberRequirementsForUsers = 'pass', memberRequirementsForUsers = 'pass',
} = this.requirements } = this.requirements
const member = this.global ? null : (executor as GuildMember) const member = this.isGuildSpecific() ? (executor as GuildMember) : null
const bDefCond = defaultCondition !== 'fail' const boolDefaultCondition = defaultCondition !== 'fail'
const bMemReqForUsers = memberRequirementsForUsers !== 'fail' const boolMemberRequirementsForUsers = memberRequirementsForUsers !== 'fail'
const conditions = [ const conditions = [
adminOnly ? isAdmin(executor) : bDefCond, adminOnly ? isExecutorAdmin : boolDefaultCondition,
channels ? channels.includes(channelId) : bDefCond, users ? users.includes(executor.id) : boolDefaultCondition,
member ? (roles ? roles.some(role => member.roles.cache.has(role)) : bDefCond) : bMemReqForUsers, member
member ? (permissions ? member.permissions.has(permissions) : bDefCond) : bMemReqForUsers, ? roles
users ? users.includes(executor.id) : bDefCond, ? roles.some(role => member.roles.cache.has(role))
: boolDefaultCondition
: boolMemberRequirementsForUsers,
member
? permissions
? member.permissions.has(permissions)
: boolDefaultCondition
: boolMemberRequirementsForUsers,
] ]
if (mode === 'all' && conditions.some(condition => !condition)) return false if (mode === 'all' && conditions.some(condition => !condition)) return false
@@ -320,14 +396,27 @@ export default class Command<
return true return true
} }
get json(): RESTPostAPIChatInputApplicationCommandsJSONBody & { contexts: Array<0 | 1 | 2> } { get json(): RESTPostAPIApplicationCommandsJSONBody {
return { // @ts-expect-error: I hate union types in TypeScript
const base: RESTPostAPIApplicationCommandsJSONBody = {
name: this.name, name: this.name,
type:
this.type === CommandType.ContextMenuGuildMessage || this.type === CommandType.ContextMenuMessage
? ApplicationCommandType.Message
: this.type === CommandType.ContextMenuGuildMember || this.type === CommandType.ContextMenuUser
? ApplicationCommandType.User
: ApplicationCommandType.ChatInput,
}
if (this.isContextMenuSpecific()) return base
return {
...base,
description: this.description, description: this.description,
options: this.options ? this.#transformOptions(this.options) : undefined, options: this.options ? this.#transformOptions(this.options) : undefined,
// https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types // https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-context-types
contexts: this.global ? [0] : [0, 1], contexts: this.isGuildSpecific() ? [0] : [0, 1, 2],
} } as RESTPostAPIChatInputApplicationCommandsJSONBody & { contexts: Array<0 | 1 | 2> }
} }
#transformOptions(optionsObject: Record<string, CommandOption>) { #transformOptions(optionsObject: Record<string, CommandOption>) {
@@ -374,8 +463,8 @@ export default class Command<
export class ModerationCommand< export class ModerationCommand<
Options extends CommandOptionsOptions, Options extends CommandOptionsOptions,
AllowMessageCommand extends boolean = true, AllowMessageCommand extends boolean = true,
> extends Command<false, Options, AllowMessageCommand> { > extends Command<CommandType.ChatGuild, Options, AllowMessageCommand> {
constructor(options: ExtendedCommandOptions<false, Options, AllowMessageCommand>) { constructor(options: ExtendedCommandOptions<CommandType.ChatGuild, Options, AllowMessageCommand>) {
super({ super({
...options, ...options,
requirements: { requirements: {
@@ -385,17 +474,16 @@ export class ModerationCommand<
}, },
// @ts-expect-error: No thanks // @ts-expect-error: No thanks
allowMessageCommand: options.allowMessageCommand ?? true, allowMessageCommand: options.allowMessageCommand ?? true,
global: false, type: CommandType.ChatGuild,
}) })
} }
} }
export class AdminCommand<Options extends CommandOptionsOptions, AllowMessageCommand extends boolean> extends Command< export class AdminCommand<
true, Options extends CommandOptionsOptions,
Options, AllowMessageCommand extends boolean = true,
AllowMessageCommand > extends Command<CommandType.ChatGlobal, Options, AllowMessageCommand> {
> { constructor(options: ExtendedCommandOptions<CommandType.ChatGlobal, Options, AllowMessageCommand>) {
constructor(options: ExtendedCommandOptions<true, Options, AllowMessageCommand>) {
super({ super({
...options, ...options,
requirements: { requirements: {
@@ -403,38 +491,52 @@ export class AdminCommand<Options extends CommandOptionsOptions, AllowMessageCom
adminOnly: true, adminOnly: true,
defaultCondition: 'pass', defaultCondition: 'pass',
}, },
global: true, allowMessageCommand: options.allowMessageCommand ?? (true as AllowMessageCommand),
type: CommandType.ChatGlobal,
}) })
} }
} }
const fetchMember = async (
interaction: CommandInteraction<'raw' | 'cached'>,
source: UserResolvable = interaction.user,
manager = interaction.guild?.members,
) => {
const _manager = manager ?? (await interaction.client.guilds.fetch(interaction.guildId).then(it => it.members))
if (!_manager) throw new CommandError(CommandErrorType.FetchManagerNotFound, 'Cannot fetch member.')
return await _manager.fetch(source)
}
const fetchUser = (interaction: CommandInteraction, source: UserResolvable = interaction.user) => {
return interaction.client.users.fetch(source)
}
/* TODO: /* TODO:
APIApplicationCommandAttachmentOption APIApplicationCommandAttachmentOption
APIApplicationCommandMentionableOption APIApplicationCommandMentionableOption
APIApplicationCommandRoleOption APIApplicationCommandRoleOption
*/ */
export interface CommandOptions< export type CommandOptions<
Global extends boolean, Type extends CommandType,
Options extends CommandOptionsOptions | undefined, Options extends CommandOptionsOptions | undefined,
AllowMessageCommand extends boolean, AllowMessageCommand extends boolean,
> { > = {
name: string name: string
description: string
requirements?: CommandRequirements requirements?: CommandRequirements
options?: Options options?: Options
execute: CommandExecuteFunction<Global, Options, AllowMessageCommand> execute: CommandExecuteFunction<Type, Options, AllowMessageCommand>
global?: Global type?: Type
allowMessageCommand?: AllowMessageCommand allowMessageCommand?: AllowMessageCommand
} } & If<IsContextMenu<Type>, { description?: never }, { description: string }>
export type CommandArguments = Array<string | CommandSpecialArgument> export type CommandArguments = Array<string | CommandSpecialArgument>
export type CommandSpecialArgument = { export type CommandSpecialArgument = {
type: (typeof CommandSpecialArgumentType)[keyof typeof CommandSpecialArgumentType] type: (typeof CommandSpecialArgumentType)[keyof typeof CommandSpecialArgumentType]
id: string id: string
} }
//! If things ever get minified, this will most likely break property access via string names
export const CommandSpecialArgumentType = { export const CommandSpecialArgumentType = {
Channel: ApplicationCommandOptionType.Channel, Channel: ApplicationCommandOptionType.Channel,
Role: ApplicationCommandOptionType.Role, Role: ApplicationCommandOptionType.Role,
@@ -442,31 +544,56 @@ export const CommandSpecialArgumentType = {
} }
type ExtendedCommandOptions< type ExtendedCommandOptions<
Global extends boolean, Type extends CommandType,
Options extends CommandOptionsOptions, Options extends CommandOptionsOptions,
AllowMessageCommand extends boolean, AllowMessageCommand extends boolean,
> = Omit<CommandOptions<Global, Options, AllowMessageCommand>, 'global'> & { > = Omit<CommandOptions<Type, Options, AllowMessageCommand>, 'type'> & {
requirements?: Omit<CommandOptions<false, Options, AllowMessageCommand>['requirements'], 'defaultCondition'> requirements?: Omit<CommandOptions<Type, Options, AllowMessageCommand>['requirements'], 'defaultCondition'>
} }
export type CommandOptionsOptions = Record<string, CommandOption> export type CommandOptionsOptions = Record<string, CommandOption>
type ToCacheType<Type extends CommandType> = If<IsGuildSpecific<Type>, 'raw' | 'cached', CacheType>
type CommandExecuteFunction< type CommandExecuteFunction<
Global extends boolean, Type extends CommandType,
Options extends CommandOptionsOptions | undefined, Options extends CommandOptionsOptions | undefined,
AllowMessageCommand extends boolean, AllowMessageCommand extends boolean,
> = ( > = (
context: CommandContext<Global>, context: CommandContext<Type>,
trigger: If< trigger: If<
AllowMessageCommand, AllowMessageCommand,
Message<InvertBoolean<Global>> | ChatInputCommandInteraction<If<Global, CacheType, 'raw' | 'cached'>>, Message<IsGuildSpecific<Type>> | CommandTypeToInteractionMap<ToCacheType<Type>>[Type],
ChatInputCommandInteraction<If<Global, CacheType, 'raw' | 'cached'>> CommandTypeToInteractionMap<ToCacheType<Type>>[Type]
>, >,
options: Options extends CommandOptionsOptions ? CommandExecuteFunctionOptionsParameter<Options> : never, options: Options extends CommandOptionsOptions ? CommandExecuteFunctionOptionsParameter<Options> : never,
) => Promise<unknown> | unknown ) => Promise<unknown> | unknown
type CommandTypeToInteractionMap<CT extends CacheType> = {
[CommandType.ChatGlobal]: ChatInputCommandInteraction<CT>
[CommandType.ChatGuild]: ChatInputCommandInteraction<CT>
[CommandType.ContextMenuUser]: UserContextMenuCommandInteraction<CT>
[CommandType.ContextMenuMessage]: MessageContextMenuCommandInteraction<CT>
[CommandType.ContextMenuGuildMessage]: MessageContextMenuCommandInteraction<CT>
[CommandType.ContextMenuGuildMember]: MessageContextMenuCommandInteraction<CT>
}
type IsContextMenu<Type extends CommandType> = Extends<
Type,
| CommandType.ContextMenuGuildMessage
| CommandType.ContextMenuGuildMember
| CommandType.ContextMenuMessage
| CommandType.ContextMenuUser
>
type IsGuildSpecific<Type extends CommandType> = Extends<
Type,
CommandType.ChatGuild | CommandType.ContextMenuGuildMember | CommandType.ContextMenuGuildMessage
>
type Extends<T, U> = T extends U ? true : false
type If<T extends boolean | undefined, U, V> = T extends true ? U : V type If<T extends boolean | undefined, U, V> = T extends true ? U : V
type InvertBoolean<T extends boolean> = If<T, false, true> // type InvertBoolean<T extends boolean> = If<T, false, true>
type CommandExecuteFunctionOptionsParameter<Options extends CommandOptionsOptions> = { type CommandExecuteFunctionOptionsParameter<Options extends CommandOptionsOptions> = {
[K in keyof Options]: Options[K]['type'] extends [K in keyof Options]: Options[K]['type'] extends
@@ -481,8 +608,13 @@ type CommandExecuteFunctionOptionsParameter<Options extends CommandOptionsOption
> >
} }
type CommandContext<Global extends boolean> = typeof import('../context') & { type CommandContext<Type extends CommandType> = typeof import('../context') & {
executor: CommandExecutor<Global> executor: CommandExecutor<Type>
target: If<
Extends<Type, CommandType.ContextMenuGuildMember>,
GuildMember,
If<Extends<Type, CommandType.ContextMenuGuildMessage>, Message<true>, never>
>
} }
type CommandOptionValueMap = { type CommandOptionValueMap = {
@@ -508,7 +640,7 @@ type CommandOption =
| CommandSubcommandOption | CommandSubcommandOption
| CommandSubcommandGroupOption | CommandSubcommandGroupOption
type CommandExecutor<Global extends boolean> = If<Global, User, GuildMember> type CommandExecutor<Type extends CommandType> = If<IsGuildSpecific<Type>, GuildMember, User>
type CommandOptionBase<Type extends ApplicationCommandOptionType> = { type CommandOptionBase<Type extends ApplicationCommandOptionType> = {
type: Type type: Type
@@ -582,10 +714,12 @@ interface CommandSubcommandLikeOption<
type CommandSubcommandOption = CommandSubcommandLikeOption<ApplicationCommandOptionType.Subcommand> type CommandSubcommandOption = CommandSubcommandLikeOption<ApplicationCommandOptionType.Subcommand>
type CommandSubcommandGroupOption = CommandSubcommandLikeOption<ApplicationCommandOptionType.SubcommandGroup> type CommandSubcommandGroupOption = CommandSubcommandLikeOption<ApplicationCommandOptionType.SubcommandGroup>
export type CommandRequirements = Filter & { export type CommandRequirements = {
mode?: 'all' | 'any' users?: string[]
adminOnly?: boolean roles?: string[]
permissions?: bigint permissions?: bigint
adminOnly?: boolean
defaultCondition?: 'fail' | 'pass' defaultCondition?: 'fail' | 'pass'
memberRequirementsForUsers?: 'pass' | 'fail' memberRequirementsForUsers?: 'fail' | 'pass'
mode?: 'all' | 'any'
} }

View File

@@ -1,9 +1,9 @@
import { createErrorEmbed } from '$/utils/discord/embeds' import { createErrorEmbed } from '../utils/discord/embeds'
export default class CommandError extends Error { export default class CommandError extends Error {
type: CommandErrorType type: CommandErrorType
constructor(type: CommandErrorType, message?: string) { constructor(type: CommandErrorType, message: string = ErrorMessageMap[type]) {
super(message) super(message)
this.name = 'CommandError' this.name = 'CommandError'
this.type = type this.type = type
@@ -15,19 +15,34 @@ export default class CommandError extends Error {
} }
export enum CommandErrorType { export enum CommandErrorType {
Generic, Generic = 1,
InteractionNotInGuild,
InteractionDataMismatch,
FetchManagerNotFound,
FetchNotFound,
RequirementsNotMet = 100,
MissingArgument, MissingArgument,
InvalidArgument, InvalidArgument,
InvalidUser,
InvalidChannel,
InvalidDuration,
} }
const ErrorTitleMap: Record<CommandErrorType, string> = { const ErrorTitleMap: Record<CommandErrorType, string> = {
[CommandErrorType.Generic]: 'An exception was thrown', [CommandErrorType.Generic]: 'An exception was thrown',
[CommandErrorType.InteractionNotInGuild]: 'This command can only be used in servers',
[CommandErrorType.InteractionDataMismatch]: 'Command data mismatch',
[CommandErrorType.FetchManagerNotFound]: 'Cannot fetch data (manager not found)',
[CommandErrorType.FetchNotFound]: 'Cannot fetch data (source not found)',
[CommandErrorType.RequirementsNotMet]: 'Command requirements not met',
[CommandErrorType.MissingArgument]: 'Missing argument', [CommandErrorType.MissingArgument]: 'Missing argument',
[CommandErrorType.InvalidArgument]: 'Invalid argument', [CommandErrorType.InvalidArgument]: 'Invalid argument',
[CommandErrorType.InvalidUser]: 'Invalid user', }
[CommandErrorType.InvalidChannel]: 'Invalid channel',
[CommandErrorType.InvalidDuration]: 'Invalid duration', const ErrorMessageMap: Record<CommandErrorType, string> = {
[CommandErrorType.Generic]: 'An generic exception was thrown.',
[CommandErrorType.InteractionNotInGuild]: 'This command can only be used in servers.',
[CommandErrorType.InteractionDataMismatch]: 'Interaction command data does not match the expected command data.',
[CommandErrorType.FetchManagerNotFound]: 'Cannot fetch required data.',
[CommandErrorType.FetchNotFound]: 'Cannot fetch target.',
[CommandErrorType.RequirementsNotMet]: 'You do not meet the requirements to use this command.',
[CommandErrorType.MissingArgument]: 'You are missing a required argument.',
[CommandErrorType.InvalidArgument]: 'You provided an invalid argument.',
} }

View File

@@ -1,9 +1,11 @@
import { ApplicationCommandOptionType, MessageFlags } from 'discord.js'
import { unlinkSync, writeFileSync } from 'fs'
import { join } from 'path'
import { inspect } from 'util' import { inspect } from 'util'
import { runInNewContext } from 'vm' import { createContext, runInContext } from 'vm'
import { ApplicationCommandOptionType } from 'discord.js'
import { AdminCommand } from '$/classes/Command' import { AdminCommand } from '$/classes/Command'
import { createSuccessEmbed } from '$/utils/discord/embeds' import { createSuccessEmbed } from '$/utils/discord/embeds'
import { parseDuration } from '$/utils/duration'
export default new AdminCommand({ export default new AdminCommand({
name: 'eval', name: 'eval',
@@ -19,16 +21,74 @@ export default new AdminCommand({
type: ApplicationCommandOptionType.Boolean, type: ApplicationCommandOptionType.Boolean,
required: false, required: false,
}, },
['inspect-depth']: {
description: 'How many times to recurse while formatting the object (default: 1)',
type: ApplicationCommandOptionType.Integer,
required: false,
}, },
async execute(context, trigger, { code, 'show-hidden': showHidden }) { timeout: {
await trigger.reply({ description: 'Timeout for the evaluation (default: 10s)',
ephemeral: true, type: ApplicationCommandOptionType.String,
embeds: [ required: false,
createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``).addFields({ },
name: 'Result', },
value: `\`\`\`js\n${inspect(runInNewContext(code, { client: trigger.client, context, trigger }), { depth: 1, showHidden, getters: true, numericSeparator: true, showProxy: true })}\`\`\``, async execute(context, trigger, { code, 'show-hidden': showHidden, timeout, ['inspect-depth']: inspectDepth }) {
const currentToken = context.discord.client.token
const currentEnvToken = process.env['DISCORD_TOKEN']
context.discord.client.token = null
process.env['DISCORD_TOKEN'] = undefined
// This allows developers to access and modify the context object to apply changes
// to the bot while the bot is running, minus malicious actors getting the token to perform malicious actions
const output = await runInContext(
code,
createContext({
...globalThis,
context,
trigger,
}), }),
], {
timeout: parseDuration(timeout ?? '10s'),
filename: 'eval',
displayErrors: true,
},
)
context.discord.client.token = currentToken
process.env['DISCORD_TOKEN'] = currentEnvToken
const inspectedOutput = inspect(output, {
depth: inspectDepth ?? 1,
showHidden,
getters: showHidden,
numericSeparator: true,
showProxy: showHidden,
}) })
const embed = createSuccessEmbed('Evaluate', `\`\`\`js\n${code}\`\`\``)
const files: string[] = []
const filepath = join(Bun.main, '..', `output-eval-${Date.now()}.js`)
if (inspectedOutput.length > 1000) {
writeFileSync(filepath, inspectedOutput)
files.push(filepath)
embed.addFields({
name: 'Result',
value: '```js\n// (output too long, file uploaded)```',
})
} else
embed.addFields({
name: 'Result',
value: `\`\`\`js\n${inspectedOutput}\`\`\``,
})
await trigger.reply({
embeds: [embed],
flags: MessageFlags.Ephemeral,
files,
})
if (files.length) unlinkSync(filepath)
}, },
}) })

View File

@@ -1,5 +1,4 @@
import { ApplicationCommandOptionType } from 'discord.js' import { ApplicationCommandOptionType } from 'discord.js'
import { AdminCommand } from '$/classes/Command' import { AdminCommand } from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
@@ -11,7 +10,10 @@ export default new AdminCommand({
description: 'The type of exception to throw', description: 'The type of exception to throw',
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
required: true, required: true,
choices: Object.keys(CommandErrorType).map(k => ({ name: k, value: k })), choices: [
{ name: 'Process', value: 'Process' },
...Object.keys(CommandErrorType).map(k => ({ name: k, value: k })),
],
}, },
}, },
async execute(_, __, { type }) { async execute(_, __, { type }) {

View File

@@ -0,0 +1,39 @@
import { type CommandInteraction, MessageFlags } from 'discord.js'
import { AdminCommand } from '$/classes/Command'
export default new AdminCommand({
name: 'reload',
description: 'Reload configuration',
async execute(context, trigger) {
const { api, logger, discord } = context
logger.info(`Reload triggered by ${context.executor.tag} (${context.executor.id})`)
logger.debug('Invalidating previous config...')
context.config.invalidate()
if ((trigger as CommandInteraction).deferReply)
await (trigger as CommandInteraction).deferReply({ flags: MessageFlags.Ephemeral })
logger.info('Reinitializing API client to reload configuration...')
await api.client.ws.setOptions(
{
url: context.config.api.url,
},
false,
)
api.intentionallyDisconnecting = true
api.client.disconnect(true)
api.disconnectCount = 0
api.intentionallyDisconnecting = false
api.client.connect()
logger.info('Reinitializing Discord client to reload configuration...')
await discord.client.destroy()
// discord.client.token only gets set whenever a new Client is intialized
// so that's why we need to provide the token here :/
await discord.client.login(process.env['DISCORD_TOKEN'])
// @ts-expect-error: TypeScript dum
await trigger['deferReply' in trigger ? 'editReply' : 'reply']({ content: 'Reloaded configuration' })
},
})

View File

@@ -1,8 +1,6 @@
import { ApplicationCommandOptionType, Routes } from 'discord.js' import { ApplicationCommandOptionType, Routes } from 'discord.js'
import { AdminCommand } from '$/classes/Command' import { AdminCommand } from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { createSuccessEmbed } from '$/utils/discord/embeds' import { createSuccessEmbed } from '$/utils/discord/embeds'
const SubcommandOptions = { const SubcommandOptions = {
@@ -42,7 +40,7 @@ export default new AdminCommand({
const { global: globalCommands, guild: guildCommands } = Object.groupBy( const { global: globalCommands, guild: guildCommands } = Object.groupBy(
Object.values(context.discord.commands), Object.values(context.discord.commands),
cmd => (cmd.global ? 'global' : 'guild'), cmd => (cmd.isGuildSpecific() ? 'guild' : 'global'),
) )
const { const {

View File

@@ -1,3 +1,4 @@
import { MessageFlags } from 'discord.js'
import { AdminCommand } from '$/classes/Command' import { AdminCommand } from '$/classes/Command'
export default new AdminCommand({ export default new AdminCommand({
@@ -9,7 +10,7 @@ export default new AdminCommand({
logger.fatal('Stopping bot...') logger.fatal('Stopping bot...')
trigger.reply({ trigger.reply({
content: 'Stopping... (I will go offline once done)', content: 'Stopping... (I will go offline once done)',
ephemeral: true, flags: MessageFlags.Ephemeral,
}) })
if (!api.client.disconnected) api.client.disconnect() if (!api.client.disconnected) api.client.disconnect()

View File

@@ -1,12 +1,11 @@
import { EmbedBuilder } from 'discord.js' import { EmbedBuilder, MessageFlags } from 'discord.js'
import Command from '$/classes/Command' import Command from '$/classes/Command'
import { applyCommonEmbedStyles } from '$/utils/discord/embeds' import { applyCommonEmbedStyles } from '$/utils/discord/embeds'
export default new Command({ export default new Command({
name: 'coinflip', name: 'coinflip',
description: 'Do a coinflip!', description: 'Do a coinflip!',
global: true, type: Command.Type.ChatGlobal,
requirements: { requirements: {
defaultCondition: 'pass', defaultCondition: 'pass',
}, },
@@ -18,6 +17,7 @@ export default new Command({
const reply = await trigger const reply = await trigger
.reply({ .reply({
embeds: [embed.toJSON()], embeds: [embed.toJSON()],
flags: MessageFlags.Ephemeral,
}) })
.then(it => it.fetch()) .then(it => it.fetch())

View File

@@ -1,5 +1,5 @@
import { ApplicationCommandOptionType, Message, MessageFlags } from 'discord.js'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { ApplicationCommandOptionType, Message } from 'discord.js'
import { ModerationCommand } from '../../classes/Command' import { ModerationCommand } from '../../classes/Command'
export default new ModerationCommand({ export default new ModerationCommand({
@@ -40,7 +40,7 @@ export default new ModerationCommand({
await trigger.reply({ await trigger.reply({
content: 'OK!', content: 'OK!',
ephemeral: true, flags: MessageFlags.Ephemeral,
}) })
}, },
}) })

View File

@@ -18,13 +18,14 @@ export default new ModerationCommand({
required: false, required: false,
type: ModerationCommand.OptionType.String, type: ModerationCommand.OptionType.String,
}, },
dmd: { dmt: {
description: 'Duration to delete messages (must be from 0 to 7 days)', description:
'Time duration to delete messages (default time unit is days, must be from 0s to 7d, default is 0s)',
required: false, required: false,
type: ModerationCommand.OptionType.String, type: ModerationCommand.OptionType.String,
}, },
}, },
async execute({ logger, executor }, interaction, { user, reason, dmd }) { async execute({ logger, executor }, interaction, { user, reason, dmt }) {
const guild = await interaction.client.guilds.fetch(interaction.guildId) const guild = await interaction.client.guilds.fetch(interaction.guildId)
const member = await guild.members.fetch(user).catch(() => {}) const member = await guild.members.fetch(user).catch(() => {})
const moderator = await guild.members.fetch(executor.user) const moderator = await guild.members.fetch(executor.user)
@@ -35,12 +36,12 @@ export default new ModerationCommand({
if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0) if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidUser, CommandErrorType.InvalidArgument,
'You cannot ban a user with a role equal to or higher than yours.', 'You cannot ban a user with a role equal to or higher than yours.',
) )
} }
const dms = Math.floor(dmd ? parseDuration(dmd) : 0 / 1000) const dms = Math.floor((dmt ? parseDuration(dmt, 'd') : 0) / 1000)
await interaction.guild!.members.ban(user, { await interaction.guild!.members.ban(user, {
reason: `Banned by moderator ${executor.user.tag} (${executor.id}): ${reason}`, reason: `Banned by moderator ${executor.user.tag} (${executor.id}): ${reason}`,
deleteMessageSeconds: dms, deleteMessageSeconds: dms,

View File

@@ -1,3 +1,4 @@
import { MessageFlags } from 'discord.js'
import { ModerationCommand } from '$/classes/Command' import { ModerationCommand } from '$/classes/Command'
import { createSuccessEmbed } from '$/utils/discord/embeds' import { createSuccessEmbed } from '$/utils/discord/embeds'
import { cureNickname } from '$/utils/discord/moderation' import { cureNickname } from '$/utils/discord/moderation'
@@ -18,7 +19,7 @@ export default new ModerationCommand({
await cureNickname(member) await cureNickname(member)
await interaction.reply({ await interaction.reply({
embeds: [createSuccessEmbed(null, `Cured nickname for ${member.toString()}`)], embeds: [createSuccessEmbed(null, `Cured nickname for ${member.toString()}`)],
ephemeral: true, flags: MessageFlags.Ephemeral,
}) })
}, },
}) })

View File

@@ -3,7 +3,7 @@ import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { createModerationActionEmbed } from '$/utils/discord/embeds' import { createModerationActionEmbed } from '$/utils/discord/embeds'
import { sendModerationReplyAndLogs } from '$/utils/discord/moderation' import { sendModerationReplyAndLogs } from '$/utils/discord/moderation'
import { applyRolePreset, removeRolePreset } from '$/utils/discord/rolePresets' import { applyRolePreset, removeRolePreset } from '$/utils/discord/rolePresets'
import { parseDuration } from '$/utils/duration' import { isSafeTimeoutDuration, parseDuration } from '$/utils/duration'
export default new ModerationCommand({ export default new ModerationCommand({
name: 'mute', name: 'mute',
@@ -14,13 +14,13 @@ export default new ModerationCommand({
required: true, required: true,
type: ModerationCommand.OptionType.User, type: ModerationCommand.OptionType.User,
}, },
reason: { duration: {
description: 'The reason for muting the member', description: 'The duration of the mute (default time unit is minutes)',
required: false, required: false,
type: ModerationCommand.OptionType.String, type: ModerationCommand.OptionType.String,
}, },
duration: { reason: {
description: 'The duration of the mute', description: 'The reason for muting the member',
required: false, required: false,
type: ModerationCommand.OptionType.String, type: ModerationCommand.OptionType.String,
}, },
@@ -33,18 +33,18 @@ export default new ModerationCommand({
const guild = await interaction.client.guilds.fetch(interaction.guildId) const guild = await interaction.client.guilds.fetch(interaction.guildId)
const member = await guild.members.fetch(user.id) const member = await guild.members.fetch(user.id)
const moderator = await guild.members.fetch(executor.id) const moderator = await guild.members.fetch(executor.id)
const duration = durationInput ? parseDuration(durationInput) : Infinity const duration = durationInput ? parseDuration(durationInput, 'm') : Infinity
if (Number.isInteger(duration) && duration! < 1) if (Number.isInteger(duration) && duration! < 1)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidDuration, CommandErrorType.InvalidArgument,
'The duration must be at least 1 millisecond long.', 'The duration must be at least 1 millisecond long.',
) )
const expires = Math.max(duration, Date.now() + duration) const expires = Math.max(duration, Date.now() + duration)
if (!member) if (!member)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidUser, CommandErrorType.InvalidArgument,
'The provided member is not in the server or does not exist.', 'The provided member is not in the server or does not exist.',
) )
@@ -53,17 +53,17 @@ export default new ModerationCommand({
if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0) if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidUser, CommandErrorType.InvalidArgument,
'You cannot mute a user with a role equal to or higher than yours.', 'You cannot mute a user with a role equal to or higher than yours.',
) )
await applyRolePreset(member, 'mute', expires) await applyRolePreset(member, 'mute', expires)
await sendModerationReplyAndLogs( await sendModerationReplyAndLogs(
interaction, interaction,
createModerationActionEmbed('Muted', user, executor.user, reason, duration), createModerationActionEmbed('Muted', user, executor.user, reason, Math.ceil(expires / 1000)),
) )
if (duration) if (isSafeTimeoutDuration(duration))
setTimeout(() => { setTimeout(() => {
removeRolePreset(member, 'mute') removeRolePreset(member, 'mute')
}, duration) }, duration)

View File

@@ -1,5 +1,4 @@
import { EmbedBuilder, GuildChannel } from 'discord.js' import { EmbedBuilder } from 'discord.js'
import { ModerationCommand } from '$/classes/Command' import { ModerationCommand } from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { applyCommonEmbedStyles } from '$/utils/discord/embeds' import { applyCommonEmbedStyles } from '$/utils/discord/embeds'
@@ -31,8 +30,8 @@ export default new ModerationCommand({
throw new CommandError(CommandErrorType.MissingArgument, 'Either `amount` or `until` must be provided.') throw new CommandError(CommandErrorType.MissingArgument, 'Either `amount` or `until` must be provided.')
const channel = interaction.channel! const channel = interaction.channel!
if (!(channel.isTextBased() && channel instanceof GuildChannel)) if (!channel.isTextBased())
throw new CommandError(CommandErrorType.InvalidChannel, 'The supplied channel is not a text channel.') throw new CommandError(CommandErrorType.InvalidArgument, 'The supplied channel is not a text channel.')
const embed = applyCommonEmbedStyles( const embed = applyCommonEmbedStyles(
new EmbedBuilder({ new EmbedBuilder({

View File

@@ -2,7 +2,7 @@ import { ModerationCommand } from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { sendPresetReplyAndLogs } from '$/utils/discord/moderation' import { sendPresetReplyAndLogs } from '$/utils/discord/moderation'
import { applyRolePreset, removeRolePreset } from '$/utils/discord/rolePresets' import { applyRolePreset, removeRolePreset } from '$/utils/discord/rolePresets'
import { parseDuration } from '$/utils/duration' import { isSafeTimeoutDuration, parseDuration } from '$/utils/duration'
const SubcommandOptions = { const SubcommandOptions = {
member: { member: {
@@ -11,12 +11,12 @@ const SubcommandOptions = {
type: ModerationCommand.OptionType.User, type: ModerationCommand.OptionType.User,
}, },
preset: { preset: {
description: 'The preset to apply or remove', description: 'The preset to manage',
required: true, required: true,
type: ModerationCommand.OptionType.String, type: ModerationCommand.OptionType.String,
}, },
duration: { duration: {
description: 'The duration to apply the preset for (only for apply action)', description: 'The duration to apply the preset for (only for apply action, default time unit is minutes)',
required: false, required: false,
type: ModerationCommand.OptionType.String, type: ModerationCommand.OptionType.String,
}, },
@@ -45,7 +45,7 @@ export default new ModerationCommand({
if (!member) if (!member)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidUser, CommandErrorType.InvalidArgument,
'The provided member is not in the server or does not exist.', 'The provided member is not in the server or does not exist.',
) )
@@ -53,16 +53,16 @@ export default new ModerationCommand({
throw new CommandError(CommandErrorType.Generic, 'This user cannot be managed by the bot.') throw new CommandError(CommandErrorType.Generic, 'This user cannot be managed by the bot.')
if (apply) { if (apply) {
const duration = durationInput ? parseDuration(durationInput) : Infinity const duration = durationInput ? parseDuration(durationInput, 'm') : Infinity
if (Number.isInteger(duration) && duration! < 1) if (Number.isInteger(duration) && duration! < 1)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidDuration, CommandErrorType.InvalidArgument,
'The duration must be at least 1 millisecond long.', 'The duration must be at least 1 millisecond long.',
) )
if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0) if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidUser, CommandErrorType.InvalidArgument,
'You cannot apply a role preset to a user with a role equal to or higher than yours.', 'You cannot apply a role preset to a user with a role equal to or higher than yours.',
) )
@@ -78,11 +78,18 @@ export default new ModerationCommand({
) )
} }
if (expires) if (expires && isSafeTimeoutDuration(expires))
setTimeout(() => { setTimeout(() => {
removeRolePreset(member, preset) removeRolePreset(member, preset)
}, expires) }, expires)
await sendPresetReplyAndLogs(apply ? 'apply' : 'remove', trigger, executor, user, preset, expires) await sendPresetReplyAndLogs(
apply ? 'apply' : 'remove',
trigger,
executor,
user,
preset,
expires ? Math.ceil(expires / 1000) : undefined,
)
}, },
}) })

View File

@@ -1,16 +1,15 @@
import { createSuccessEmbed } from '$/utils/discord/embeds' import { ChannelType } from 'discord.js'
import { durationToString, parseDuration } from '$/utils/duration'
import { ModerationCommand } from '$/classes/Command' import { ModerationCommand } from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { ChannelType } from 'discord.js' import { createSuccessEmbed } from '$/utils/discord/embeds'
import { durationToString, parseDuration } from '$/utils/duration'
export default new ModerationCommand({ export default new ModerationCommand({
name: 'slowmode', name: 'slowmode',
description: 'Set a slowmode for a channel', description: 'Set a slowmode for a channel',
options: { options: {
duration: { duration: {
description: 'The duration to set', description: 'The duration to set (default time unit is seconds)',
required: true, required: true,
type: ModerationCommand.OptionType.String, type: ModerationCommand.OptionType.String,
}, },
@@ -23,18 +22,15 @@ export default new ModerationCommand({
}, },
async execute({ logger, executor }, interaction, { duration: durationInput, channel: channelInput }) { async execute({ logger, executor }, interaction, { duration: durationInput, channel: channelInput }) {
const channel = channelInput ?? (await interaction.guild!.channels.fetch(interaction.channelId)) const channel = channelInput ?? (await interaction.guild!.channels.fetch(interaction.channelId))
const duration = parseDuration(durationInput) const duration = parseDuration(durationInput, 's')
if (!channel?.isTextBased() || channel.isDMBased()) if (!channel?.isTextBased() || channel.isDMBased())
throw new CommandError( throw new CommandError(CommandErrorType.InvalidArgument, 'The supplied channel is not a text channel.')
CommandErrorType.InvalidChannel,
'The supplied channel is not a text channel or does not exist.',
)
if (Number.isNaN(duration)) throw new CommandError(CommandErrorType.InvalidDuration, 'Invalid duration.') if (Number.isNaN(duration)) throw new CommandError(CommandErrorType.InvalidArgument, 'Invalid duration.')
if (duration < 0 || duration > 36e4) if (duration < 0 || duration > 36e4)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidDuration, CommandErrorType.InvalidArgument,
'Duration out of range, must be between 0s and 6h.', 'Duration out of range, must be between 0s and 6h.',
) )

View File

@@ -1,10 +1,10 @@
import { and, eq } from 'drizzle-orm'
import { ModerationCommand } from '$/classes/Command' import { ModerationCommand } from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError' import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { appliedPresets } from '$/database/schemas' import { appliedPresets } from '$/database/schemas'
import { createModerationActionEmbed } from '$/utils/discord/embeds' import { createModerationActionEmbed } from '$/utils/discord/embeds'
import { sendModerationReplyAndLogs } from '$/utils/discord/moderation' import { sendModerationReplyAndLogs } from '$/utils/discord/moderation'
import { removeRolePreset } from '$/utils/discord/rolePresets' import { removeRolePreset } from '$/utils/discord/rolePresets'
import { and, eq } from 'drizzle-orm'
export default new ModerationCommand({ export default new ModerationCommand({
name: 'unmute', name: 'unmute',
@@ -20,7 +20,7 @@ export default new ModerationCommand({
const member = await interaction.guild!.members.fetch(user.id) const member = await interaction.guild!.members.fetch(user.id)
if (!member) if (!member)
throw new CommandError( throw new CommandError(
CommandErrorType.InvalidUser, CommandErrorType.InvalidArgument,
'The provided member is not in the server or does not exist.', 'The provided member is not in the server or does not exist.',
) )

View File

@@ -0,0 +1,76 @@
import { type FetchMessageOptions, MessageFlags, type MessageResolvable } from 'discord.js'
import Command from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { createSuccessEmbed } from '$/utils/discord/embeds'
import { config } from '../../../context'
import type { ConfigMessageScanResponseLabelConfig } from 'config.schema'
const msRcConfig = config.messageScan?.humanCorrections?.allow
export default new Command({
name: 'train',
description: 'Train a specific message to a specific label',
type: Command.Type.ChatGuild,
requirements: {
users: msRcConfig?.users,
roles: msRcConfig?.members?.roles,
permissions: msRcConfig?.members?.permissions,
mode: 'any',
memberRequirementsForUsers: 'fail',
defaultCondition: 'fail',
},
options: {
message: {
description: 'The message to train (use `latest` to train the latest message)',
type: Command.OptionType.String,
required: true,
},
label: {
description: 'The label to train the message as (leave empty for out of scope)',
type: Command.OptionType.String,
required: false,
},
},
allowMessageCommand: true,
async execute(context, trigger, { label, message: ref }) {
const { logger, config } = context
const { messageScan: msConfig } = config
// If there's no config, we can't do anything
if (!msConfig?.humanCorrections) throw new CommandError(CommandErrorType.Generic, 'Response correction is off.')
const labels = msConfig.responses?.flatMap(r =>
r.triggers.text!.filter((t): t is ConfigMessageScanResponseLabelConfig => 'label' in t).map(t => t.label),
)
const channel = await trigger.guild!.channels.fetch(trigger.channelId)
if (!channel?.isTextBased())
throw new CommandError(
CommandErrorType.InvalidArgument,
'This command can only be used in or on text channels',
)
if (label && !labels.includes(label))
throw new CommandError(
CommandErrorType.InvalidArgument,
`The provided label is invalid.\nValid labels are:${labels.map(l => `\n- \`${l}\``).join('')}`,
)
const refMsg = await channel.messages.fetch(
(ref.startsWith('latest') ? { limit: 1 } : ref) as MessageResolvable | FetchMessageOptions,
)
if (!refMsg) throw new CommandError(CommandErrorType.InvalidArgument, 'The provided message does not exist.')
logger.debug(`User ${context.executor.id} is training message ${refMsg?.id} as ${label ?? 'out of scope'}`)
await context.api.client.trainMessage(refMsg.content, label)
await trigger.reply({
embeds: [
createSuccessEmbed(
'Message trained',
`The provided message has been trained as ${label ? `\`${label}\`` : 'out of scope'}. Thank you for your contribution!`,
),
],
flags: MessageFlags.Ephemeral,
})
},
})

View File

@@ -0,0 +1,55 @@
import { type APIStringSelectComponent, ComponentType, MessageFlags } from 'discord.js'
import Command from '$/classes/Command'
import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { config } from '../../../context'
import type { ConfigMessageScanResponseLabelConfig } from 'config.schema'
const msRcConfig = config.messageScan?.humanCorrections?.allow
export default new Command({
name: 'Train Message',
type: Command.Type.ContextMenuGuildMessage,
requirements: {
users: msRcConfig?.users,
roles: msRcConfig?.members?.roles,
permissions: msRcConfig?.members?.permissions,
mode: 'any',
memberRequirementsForUsers: 'fail',
defaultCondition: 'fail',
},
async execute(context, trigger) {
const { logger, config } = context
const { messageScan: msConfig } = config
// If there's no config, we can't do anything
if (!msConfig?.humanCorrections) throw new CommandError(CommandErrorType.Generic, 'Response correction is off.')
logger.debug(`User ${context.executor.id} is training message ${trigger.targetId}`)
const labels = msConfig.responses.flatMap(r =>
r.triggers.text!.filter((t): t is ConfigMessageScanResponseLabelConfig => 'label' in t).map(t => t.label),
)
await trigger.reply({
content: 'Select a label to train this message as:',
components: [
{
components: [
{
custom_id: `tr_${trigger.targetMessage.channelId}_${trigger.targetId}`,
options: [
...labels.map(label => ({ label, value: label })),
{ label: 'Out of scope', value: OutOfScopeLabel, emoji: { name: '❌' } },
],
type: ComponentType.StringSelect,
} satisfies APIStringSelectComponent,
],
type: ComponentType.ActionRow,
},
],
flags: MessageFlags.Ephemeral,
})
},
})
export const OutOfScopeLabel = '<out of scope>'

View File

@@ -0,0 +1,28 @@
import { dirname, join } from 'path'
import _firstConfig from '../config.js'
let currentConfig = _firstConfig
// Other parts of the code will access properties of this proxy, they don't care what the target looks like
export const config = new Proxy(
{
INSPECTION_WARNING: 'Run `context.__getConfig()` to inspect the latest config.',
} as unknown as typeof currentConfig,
{
get(_, p, receiver) {
if (p === 'invalidate')
return async () => {
const path = join(dirname(Bun.main), '..', 'config.js')
Loader.registry.delete(path)
currentConfig = (await import(path)).default
}
return Reflect.get(currentConfig, p, receiver)
},
set(_, p, newValue, receiver) {
return Reflect.set(currentConfig, p, newValue, receiver)
},
},
) as typeof _firstConfig & { invalidate(): void }
export const __getConfig = () => currentConfig

View File

@@ -4,12 +4,6 @@ export const MessageScanLabeledResponseReactions = {
delete: '❌', delete: '❌',
} as const } as const
export const MessageScanHumanizedMode = {
ocr: 'image recognition',
nlp: 'text analysis',
match: 'pattern matching',
} as const
export const DefaultEmbedColor = '#4E98F0' export const DefaultEmbedColor = '#4E98F0'
export const ReVancedLogoURL = export const ReVancedLogoURL =
'https://media.discordapp.net/attachments/1095487869923119144/1115436493050224660/revanced-logo.png' 'https://media.discordapp.net/attachments/1095487869923119144/1115436493050224660/revanced-logo.png'

View File

@@ -1,23 +1,21 @@
import { Database } from 'bun:sqlite' import { Database } from 'bun:sqlite'
import { existsSync, readFileSync, readdirSync } from 'fs'
import { join } from 'path'
import { Client as APIClient } from '@revanced/bot-api' import { Client as APIClient } from '@revanced/bot-api'
import { createLogger } from '@revanced/bot-shared' import { createLogger } from '@revanced/bot-shared'
import { Client as DiscordClient, Partials } from 'discord.js' import { Client as DiscordClient, type Message, Options, Partials } from 'discord.js'
import { drizzle } from 'drizzle-orm/bun-sqlite' import { drizzle } from 'drizzle-orm/bun-sqlite'
import { existsSync, readdirSync, readFileSync } from 'fs'
// Export some things first, as commands require them import { join } from 'path'
import config from '../config.js' import { __getConfig, config } from './config'
export { config } import * as schemas from './database/schemas'
import type { default as Command, CommandOptionsOptions, CommandType } from './classes/Command'
export { config, __getConfig }
export const logger = createLogger({ export const logger = createLogger({
level: config.logLevel === 'none' ? Number.MAX_SAFE_INTEGER : config.logLevel, level: config.logLevel === 'none' ? Number.MAX_SAFE_INTEGER : config.logLevel,
}) })
// Export a few things before we initialize commands
import * as commands from './commands' import * as commands from './commands'
import * as schemas from './database/schemas'
import type { default as Command, CommandOptionsOptions } from './classes/Command'
export const api = { export const api = {
client: new APIClient({ client: new APIClient({
@@ -56,7 +54,7 @@ if (DatabasePath && !existsSync(DatabasePath)) {
} }
} }
const db = new Database(DatabasePath) const db = new Database(DatabasePath, { readwrite: true, create: true })
if (dbSchemaFileName) db.run(readFileSync(join(DatabaseSchemaDir, dbSchemaFileName)).toString()) if (dbSchemaFileName) db.run(readFileSync(join(DatabaseSchemaDir, dbSchemaFileName)).toString())
export const database = drizzle(db, { export const database = drizzle(db, {
@@ -79,10 +77,77 @@ export const discord = {
parse: ['users'], parse: ['users'],
repliedUser: true, repliedUser: true,
}, },
partials: [Partials.Message, Partials.Reaction], sweepers: {
...Options.DefaultSweeperSettings,
messages: {
interval: 1_800, // Every 30m
lifetime: 3_600, // Remove messages older than 1h
},
},
makeCache: Options.cacheWithLimits({
...Options.DefaultMakeCacheSettings,
UserManager: 50,
GuildMemberManager: {
maxSize: 50,
// Always keep client guild member in cache
keepOverLimit: member => member.id === member.client.user.id,
},
ThreadManager: {
maxSize: 0,
// Always keep threads that are used for moderation logging
keepOverLimit: thread => config.moderation?.log?.thread === thread.id,
},
GuildMessageManager: {
maxSize: 0,
// Always keep messages posted by the client in cache
keepOverLimit: message => message.author?.id === message.client.user.id,
},
// Unneeded cache
MessageManager: 0,
ReactionManager: 0,
VoiceStateManager: 0,
ThreadMemberManager: 0,
StageInstanceManager: 0,
ReactionUserManager: 0,
PresenceManager: 0,
GuildTextThreadManager: 0,
GuildStickerManager: 0,
DMMessageManager: 0,
GuildEmojiManager: 0,
GuildBanManager: 0,
GuildScheduledEventManager: 0,
EntitlementManager: 0,
AutoModerationRuleManager: 0,
GuildForumThreadManager: 0,
BaseGuildEmojiManager: 0,
GuildInviteManager: 0,
}),
partials: [Partials.Message, Partials.Reaction, Partials.GuildMember, Partials.User],
}), }),
commands: Object.fromEntries(Object.values(commands).map(cmd => [cmd.name, cmd])) as Record< commands: Object.fromEntries(Object.values(commands).map(cmd => [cmd.name, cmd])) as Record<
string, string,
Command<boolean, CommandOptionsOptions | undefined, boolean> Command<CommandType, CommandOptionsOptions | undefined, boolean>
>,
stickyMessages: {} as Record<
string,
Record<
string,
{
/**
* Chat is active, so force send timer is also active
*/
forceTimerActive: boolean
/**
* There was a message sent, so the timer is active
*/
timerActive: boolean
timerMs: number
forceTimerMs?: number
send: () => Promise<void>
currentMessage?: Message<true>
timer?: NodeJS.Timeout
forceTimer?: NodeJS.Timeout
}
>
>, >,
} as const } as const

View File

@@ -1,5 +1,5 @@
import type { InferSelectModel } from 'drizzle-orm'
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core' import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import type { InferSelectModel } from 'drizzle-orm'
export const responses = sqliteTable('responses', { export const responses = sqliteTable('responses', {
replyId: text('reply').primaryKey().notNull(), replyId: text('reply').primaryKey().notNull(),
@@ -16,12 +16,11 @@ export const appliedPresets = sqliteTable(
{ {
memberId: text('member').notNull(), memberId: text('member').notNull(),
guildId: text('guild').notNull(), guildId: text('guild').notNull(),
removedRoles: text('roles', { mode: 'json' }).notNull().$type<string[]>().default([]),
preset: text('preset').notNull(), preset: text('preset').notNull(),
until: integer('until'), until: integer('until'),
}, },
table => ({ table => [uniqueIndex('unique_composite').on(table.memberId, table.preset, table.guildId)],
uniqueComposite: uniqueIndex('unique_composite').on(table.memberId, table.preset, table.guildId),
}),
) )
export type Response = InferSelectModel<typeof responses> export type Response = InferSelectModel<typeof responses>

View File

@@ -1,5 +1,5 @@
import { on, withContext } from '$utils/api/events'
import { DisconnectReason, HumanizedDisconnectReason } from '@revanced/bot-shared' import { DisconnectReason, HumanizedDisconnectReason } from '@revanced/bot-shared'
import { on, withContext } from '$utils/api/events'
withContext(on, 'disconnect', ({ api, config, logger }, reason, msg) => { withContext(on, 'disconnect', ({ api, config, logger }, reason, msg) => {
if (reason === DisconnectReason.PlannedDisconnect && api.intentionallyDisconnecting) return if (reason === DisconnectReason.PlannedDisconnect && api.intentionallyDisconnecting) return

View File

@@ -1,9 +1,9 @@
import { on } from '$/utils/discord/events' import { on } from '$/utils/discord/events'
import { cureNickname } from '$/utils/discord/moderation' import { cureNickname } from '$/utils/discord/moderation'
on('guildMemberUpdate', async (oldMember, newMember) => { on('guildMemberUpdate', (_, newMember) => {
if (newMember.user.bot) return if (newMember.user.bot) return
if (oldMember.displayName !== newMember.displayName) await cureNickname(newMember) cureNickname(newMember)
}) })
on('guildMemberAdd', member => { on('guildMemberAdd', member => {
@@ -11,7 +11,7 @@ on('guildMemberAdd', member => {
cureNickname(member) cureNickname(member)
}) })
on('messageCreate', async msg => { on('messageCreate', msg => {
if (msg.author.bot || !msg.member) return if (msg.author.bot || !msg.member) return
await cureNickname(msg.member) cureNickname(msg.member)
}) })

View File

@@ -1,7 +1,7 @@
import { and, eq, gt } from 'drizzle-orm'
import { appliedPresets } from '$/database/schemas' import { appliedPresets } from '$/database/schemas'
import { on, withContext } from '$/utils/discord/events' import { on, withContext } from '$/utils/discord/events'
import { applyRolesUsingPreset } from '$/utils/discord/rolePresets' import { applyRolesUsingPreset } from '$/utils/discord/rolePresets'
import { and, eq, gt } from 'drizzle-orm'
withContext(on, 'guildMemberAdd', async ({ database }, member) => { withContext(on, 'guildMemberAdd', async ({ database }, member) => {
const applieds = await database.query.appliedPresets.findMany({ const applieds = await database.query.appliedPresets.findMany({
@@ -12,5 +12,5 @@ withContext(on, 'guildMemberAdd', async ({ database }, member) => {
), ),
}) })
for (const { preset } of applieds) await applyRolesUsingPreset(preset, member, true) for (const { preset } of applieds) await applyRolesUsingPreset(preset, member)
}) })

View File

@@ -1,3 +1,4 @@
import { MessageFlags } from 'discord.js'
import CommandError from '$/classes/CommandError' import CommandError from '$/classes/CommandError'
import { createStackTraceEmbed } from '$utils/discord/embeds' import { createStackTraceEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events' import { on, withContext } from '$utils/discord/events'
@@ -8,18 +9,23 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
const { logger, discord } = context const { logger, discord } = context
const command = discord.commands[interaction.commandName] const command = discord.commands[interaction.commandName]
logger.debug(`Command ${interaction.commandName} being invoked by ${interaction.user.tag}`) logger.debug(`Command ${interaction.commandName} being invoked by ${interaction.user.tag} via chat`)
if (!command) return void logger.error(`Command ${interaction.commandName} not implemented but registered!!!`) if (!command) return void logger.error(`Chat command ${interaction.commandName} not implemented but registered!!!`)
try { try {
logger.debug(`Command ${interaction.commandName} being executed`) logger.debug(`Command ${interaction.commandName} being executed via chat`)
await command.onInteraction(context, interaction) await command.onInteraction(context, interaction)
} catch (err) { } catch (err) {
if (!(err instanceof CommandError)) if (!(err instanceof CommandError))
logger.error(`Error while executing command ${interaction.commandName}:`, err) logger.error(`Error while executing command ${interaction.commandName}:`, err)
await interaction[interaction.replied ? 'followUp' : 'reply']({ await interaction[interaction.replied ? 'followUp' : 'reply']({
embeds: [err instanceof CommandError ? err.toEmbed() : createStackTraceEmbed(err)], embeds: [err instanceof CommandError ? err.toEmbed() : createStackTraceEmbed(err)],
ephemeral: true, flags: MessageFlags.Ephemeral,
}) })
// 100 and up are user errors
if (err instanceof CommandError && err.type < 100)
logger.error(`Command ${interaction.commandName} internally failed with error:`, err)
} }
}) })

View File

@@ -0,0 +1,27 @@
import { MessageFlags } from 'discord.js'
import CommandError from '$/classes/CommandError'
import { createStackTraceEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events'
withContext(on, 'interactionCreate', async (context, interaction) => {
if (!interaction.isContextMenuCommand()) return
const { logger, discord } = context
const command = discord.commands[interaction.commandName]
logger.debug(`Command ${interaction.commandName} being invoked by ${interaction.user.tag} via context menu`)
if (!command)
return void logger.error(`Context menu command ${interaction.commandName} not implemented but registered!!!`)
try {
logger.debug(`Command ${interaction.commandName} being executed via context menu`)
await command.onContextMenuInteraction(context, interaction)
} catch (err) {
if (!(err instanceof CommandError))
logger.error(`Error while executing command ${interaction.commandName}:`, err)
await interaction[interaction.replied ? 'followUp' : 'reply']({
embeds: [err instanceof CommandError ? err.toEmbed() : createStackTraceEmbed(err)],
flags: MessageFlags.Ephemeral,
})
}
})

View File

@@ -1,11 +1,15 @@
import {
type ButtonInteraction,
MessageFlags,
type StringSelectMenuInteraction,
type TextBasedChannel,
} from 'discord.js'
import { eq } from 'drizzle-orm'
import { responses } from '$/database/schemas' import { responses } from '$/database/schemas'
import { handleUserResponseCorrection } from '$/utils/discord/messageScan' import { handleUserResponseCorrection } from '$/utils/discord/messageScan'
import { createErrorEmbed, createStackTraceEmbed, createSuccessEmbed } from '$utils/discord/embeds' import { createErrorEmbed, createStackTraceEmbed, createSuccessEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events' import { on, withContext } from '$utils/discord/events'
import type { ButtonInteraction, StringSelectMenuInteraction, TextBasedChannel } from 'discord.js'
import { eq } from 'drizzle-orm'
// No permission check required as it is already done when the user reacts to a bot response // No permission check required as it is already done when the user reacts to a bot response
withContext(on, 'interactionCreate', async (context, interaction) => { withContext(on, 'interactionCreate', async (context, interaction) => {
const { const {
@@ -18,25 +22,25 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
if (!interaction.isStringSelectMenu() && !interaction.isButton()) return if (!interaction.isStringSelectMenu() && !interaction.isButton()) return
if (!interaction.customId.startsWith('cr_')) return if (!interaction.customId.startsWith('cr_')) return
const [, key, action] = interaction.customId.split('_') as ['cr', string, 'select' | 'cancel' | 'delete'] const [, msgId, action] = interaction.customId.split('_') as ['cr', string, 'select' | 'cancel' | 'delete']
if (!key || !action) return if (!msgId || !action) return
const response = await db.query.responses.findFirst({ where: eq(responses.replyId, key) }) const response = await db.query.responses.findFirst({ where: eq(responses.replyId, msgId) })
// If the message isn't saved in my DB (unrelated message) // If the message isn't saved in my DB (unrelated message)
if (!response) if (!response)
return void (await interaction.reply({ return void (await interaction.reply({
content: "I don't recall having sent this response, so I cannot correct it.", content: "I don't recall having sent this response, so I cannot correct it.",
ephemeral: true, flags: MessageFlags.Ephemeral,
})) }))
try { try {
// We're gonna pretend reactionChannel is a text-based channel, but it can be many more // We're gonna pretend reactionChannel is a text-based channel, but it can be many more
// But `messages` should always exist as a property // But `messages` should always exist as a property
const reactionGuild = await interaction.client.guilds.fetch(response.guildId) const guild = await interaction.client.guilds.fetch(response.guildId)
const reactionChannel = (await reactionGuild.channels.fetch(response.channelId)) as TextBasedChannel | null const channel = (await guild.channels.fetch(response.channelId)) as TextBasedChannel | null
const reactionMessage = await reactionChannel?.messages.fetch(key) const msg = await channel?.messages.fetch(msgId)
if (!reactionMessage) { if (!msg) {
await interaction.deferUpdate() await interaction.deferUpdate()
await interaction.message.edit({ await interaction.message.edit({
content: null, content: null,
@@ -53,9 +57,9 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
} }
const editMessage = (content: string, description?: string) => const editMessage = (content: string, description?: string) =>
editInteractionMessage(interaction, reactionMessage.url, content, description) editInteractionMessage(interaction, msg.url, content, description)
const handleCorrection = (label: string) => const handleCorrection = (label?: string) =>
handleUserResponseCorrection(context, response, reactionMessage, label, interaction.user) handleUserResponseCorrection(context, response, msg, interaction.user, label)
if (response.correctedById) if (response.correctedById)
return await editMessage( return await editMessage(
@@ -78,7 +82,7 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
await editMessage('Canceled', 'You canceled this interaction. 😞') await editMessage('Canceled', 'You canceled this interaction. 😞')
break break
case 'delete': case 'delete':
await handleCorrection(msConfig.humanCorrections.falsePositiveLabel) await handleCorrection()
await editMessage( await editMessage(
'Marked as false positive', 'Marked as false positive',
'The response has been deleted and marked as a false positive. Thank you for your feedback. 🎉', 'The response has been deleted and marked as a false positive. Thank you for your feedback. 🎉',
@@ -91,7 +95,7 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
logger.error('Failed to handle correct response interaction:', e) logger.error('Failed to handle correct response interaction:', e)
await interaction.reply({ await interaction.reply({
embeds: [createStackTraceEmbed(e)], embeds: [createStackTraceEmbed(e)],
ephemeral: true, flags: MessageFlags.Ephemeral,
}) })
} }
}) })

View File

@@ -0,0 +1,55 @@
import { MessageFlags, type TextBasedChannel } from 'discord.js'
import { OutOfScopeLabel } from '$/commands/support/train/context-menu'
import { createErrorEmbed, createStackTraceEmbed, createSuccessEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events'
withContext(on, 'interactionCreate', async (context, interaction) => {
const {
logger,
config: { messageScan: msConfig },
} = context
if (!msConfig?.humanCorrections) return
if (!interaction.isStringSelectMenu()) return
if (!interaction.customId.startsWith('tr_')) return
const [, channelId, msgId] = interaction.customId.split('_') as ['tr', string, string]
if (!channelId || !msgId) return
try {
const channel = (await interaction.client.channels.fetch(channelId)) as TextBasedChannel | null
const msg = await channel?.messages.fetch(msgId)
if (!msg)
return void (await interaction.reply({
embeds: [
createErrorEmbed(
'Message not found',
'Thank you for your contribution! Unfortunately, the message could not be found.',
),
],
flags: MessageFlags.Ephemeral,
}))
const selectedLabel = interaction.values[0]
await context.api.client.trainMessage(
msg.content,
selectedLabel === OutOfScopeLabel ? undefined : selectedLabel,
)
await interaction.reply({
embeds: [
createSuccessEmbed(
'Message being trained',
`Thank you for your contribution! The selected message is being trained as \`${selectedLabel}\`. 🎉`,
),
],
flags: MessageFlags.Ephemeral,
})
} catch (e) {
logger.error('Failed to handle train message interaction:', e)
await interaction.reply({
embeds: [createStackTraceEmbed(e)],
flags: MessageFlags.Ephemeral,
})
}
})

View File

@@ -18,8 +18,8 @@ withContext(on, 'messageCreate', async (context, msg) => {
if (!commandName) return if (!commandName) return
const command = discord.commands[commandName] const command = discord.commands[commandName]
logger.debug(`Command ${commandName} being invoked by ${msg.author.id}`) logger.debug(`Command ${commandName} being invoked by ${msg.author.id} via message`)
if (!command) return void logger.error(`Command ${commandName} not implemented`) if (!command) return void logger.debug(`Message command ${commandName} not implemented`)
const argsRegex: RegExp = /[^\s"]+|"([^"]*)"/g const argsRegex: RegExp = /[^\s"]+|"([^"]*)"/g
const args: CommandArguments = [] const args: CommandArguments = []
@@ -46,7 +46,7 @@ withContext(on, 'messageCreate', async (context, msg) => {
} }
try { try {
logger.debug(`Command ${commandName} being executed`) logger.debug(`Command ${commandName} being executed via message`)
await command.onMessage(context, msg, args) await command.onMessage(context, msg, args)
} catch (err) { } catch (err) {
if (!(err instanceof CommandError)) logger.error(`Error while executing command ${commandName}:`, err) if (!(err instanceof CommandError)) logger.error(`Error while executing command ${commandName}:`, err)

View File

@@ -0,0 +1,114 @@
import { MessageScanLabeledResponseReactions } from '$/constants'
import { responses } from '$/database/schemas'
import { getResponseFromText, messageMatchesFilter } from '$/utils/discord/messageScan'
import { createMessageScanResponseEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events'
withContext(on, 'messageCreate', async (context, msg) => {
const {
api,
config: { messageScan: config },
database: db,
logger,
} = context
if (!config || !config.responses) return
if (msg.author.bot && !config.scanBots) return
if (!msg.inGuild() && !config.scanOutsideGuilds) return
if (msg.inGuild() && msg.member?.partial) await msg.member.fetch()
const filteredResponses = config.responses.filter(x => messageMatchesFilter(msg, x.filterOverride ?? config.filter))
if (!filteredResponses.length) return
if (msg.content.length) {
try {
logger.debug(`Classifying message ${msg.id}, possible responses is ${filteredResponses.length}`)
const { response, label, respondToReply } = await getResponseFromText(
msg.content,
filteredResponses,
context,
)
if (response) {
logger.debug('Response found')
const toReply =
msg.reference?.messageId &&
(respondToReply === true ||
(label === undefined ? respondToReply === 'only_regex' : respondToReply === 'only_labeled'))
? await msg.fetchReference()
: msg
const reply = await toReply.reply({
...response,
embeds: response.embeds?.map(createMessageScanResponseEmbed),
})
if (label) {
await db.insert(responses).values({
replyId: reply.id,
channelId: reply.channel.id,
guildId: reply.guild!.id,
referenceId: msg.id,
label,
content: msg.content,
})
await Promise.all(Object.values(MessageScanLabeledResponseReactions).map(name => reply.react(name)))
}
}
} catch (e) {
logger.error('Failed to classify message:', e)
}
}
if (msg.attachments.size && config.attachments?.scanAttachments) {
logger.debug(`Classifying message attachments for ${msg.id}, possible responses is ${filteredResponses.length}`)
for (const attachment of msg.attachments.values()) {
const mimeType = attachment.contentType?.split(';')?.[0]
if (!mimeType) return void logger.warn(`No MIME type for attachment: ${attachment.url}`)
if (config.attachments.allowedMimeTypes && !config.attachments.allowedMimeTypes.includes(mimeType)) {
logger.debug(`Disallowed MIME type for attachment: ${attachment.url}, ${mimeType}`)
continue
}
const isTextFile = mimeType.startsWith('text/')
if (isTextFile && attachment.size > (config.attachments.maxTextFileSize ?? 512 * 1000)) {
logger.debug(`Attachment ${attachment.url} is too large be to scanned, size is ${attachment.size}`)
continue
}
try {
let response: Awaited<ReturnType<typeof getResponseFromText>>['response'] | undefined
if (isTextFile) {
const content = await (await fetch(attachment.url)).text()
response = await getResponseFromText(content, filteredResponses, context, {
textRegexesOnly: true,
}).then(it => it.response)
} else {
const { text: content } = await api.client.parseImage(attachment.url)
response = await getResponseFromText(content, filteredResponses, context, {
imageTriggersOnly: true,
}).then(it => it.response)
}
if (response) {
logger.debug(`Response found for attachment: ${attachment.url}`)
await msg.reply({
...response,
embeds: response.embeds?.map(createMessageScanResponseEmbed),
})
break
}
} catch (e) {
logger.error(`Failed to parse attachment: ${attachment.url}`, e)
}
}
}
})

View File

@@ -1,87 +0,0 @@
import { MessageScanLabeledResponseReactions } from '$/constants'
import { responses } from '$/database/schemas'
import { getResponseFromText, messageMatchesFilter } from '$/utils/discord/messageScan'
import { createMessageScanResponseEmbed } from '$utils/discord/embeds'
import { on, withContext } from '$utils/discord/events'
withContext(on, 'messageCreate', async (context, msg) => {
const {
api,
config: { messageScan: config },
database: db,
logger,
} = context
if (!config || !config.responses) return
if (msg.author.bot && !config.scanBots) return
if (!msg.inGuild() && !config.scanOutsideGuilds) return
if (msg.inGuild() && msg.member?.partial) await msg.member.fetch()
const filteredResponses = config.responses.filter(x => messageMatchesFilter(msg, x.filterOverride ?? config.filter))
if (!filteredResponses.length) return
if (msg.content.length) {
try {
logger.debug(`Classifying message ${msg.id}`)
const { response, label, replyToReplied } = await getResponseFromText(
msg.content,
filteredResponses,
context,
)
if (response) {
logger.debug('Response found')
const toReply = replyToReplied ? await msg.fetchReference() : msg
const reply = await toReply.reply({
...response,
embeds: response.embeds?.map(it => createMessageScanResponseEmbed(it, label ? 'nlp' : 'match')),
})
if (label)
db.insert(responses).values({
replyId: reply.id,
channelId: reply.channel.id,
guildId: reply.guild!.id,
referenceId: msg.id,
label,
content: msg.content,
})
if (label) {
for (const reaction of Object.values(MessageScanLabeledResponseReactions)) {
await reply.react(reaction)
}
}
}
} catch (e) {
logger.error('Failed to classify message:', e)
}
}
if (msg.attachments.size > 0) {
logger.debug(`Classifying message attachments for ${msg.id}`)
for (const attachment of msg.attachments.values()) {
if (attachment.contentType && !config.allowedAttachmentMimeTypes.includes(attachment.contentType)) continue
try {
const { text: content } = await api.client.parseImage(attachment.url)
const { response } = await getResponseFromText(content, filteredResponses, context, true)
if (response) {
logger.debug(`Response found for attachment: ${attachment.url}`)
await msg.reply({
...response,
embeds: response.embeds?.map(it => createMessageScanResponseEmbed(it, 'ocr')),
})
break
}
} catch {
logger.error(`Failed to parse image: ${attachment.url}`)
}
}
}
})

View File

@@ -0,0 +1,28 @@
import { on, withContext } from '$utils/discord/events'
withContext(on, 'messageCreate', async ({ discord, logger }, msg) => {
if (!msg.inGuild()) return
if (msg.author.id === msg.client.user.id) return
const store = discord.stickyMessages[msg.guildId]?.[msg.channelId]
if (!store) return
// Timer is already active from previous event, and force timer isn't active, so we start the latter
if (store.timerActive && store.forceTimerMs && !store.forceTimerActive) {
logger.debug(
`Channel ${msg.channelId} in guild ${msg.guildId} is very active, starting sticky message force timer`,
)
// (Re)start the force timer
store.forceTimerActive = true
if (store.forceTimer) store.forceTimer.refresh()
else store.forceTimer = setTimeout(store.send, store.forceTimerMs)
}
logger.debug(`Channel ${msg.channelId} in guild ${msg.guildId} is active, starting sticky message timer`)
// (Re)start the timer
store.timerActive = true
if (store.timer) store.timer.refresh()
else store.timer = setTimeout(store.send, store.timerMs) as NodeJS.Timeout
})

View File

@@ -1,7 +1,3 @@
import { MessageScanLabeledResponseReactions as Reactions } from '$/constants'
import { createErrorEmbed, createStackTraceEmbed, createSuccessEmbed } from '$/utils/discord/embeds'
import { on, withContext } from '$/utils/discord/events'
import { import {
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
@@ -9,12 +5,14 @@ import {
StringSelectMenuBuilder, StringSelectMenuBuilder,
StringSelectMenuOptionBuilder, StringSelectMenuOptionBuilder,
} from 'discord.js' } from 'discord.js'
import { eq } from 'drizzle-orm'
import type { ConfigMessageScanResponseLabelConfig } from '$/../config.schema' import { MessageScanLabeledResponseReactions as Reactions } from '$/constants'
import { responses } from '$/database/schemas' import { responses } from '$/database/schemas'
import { createErrorEmbed, createStackTraceEmbed, createSuccessEmbed } from '$/utils/discord/embeds'
import { on, withContext } from '$/utils/discord/events'
import { handleUserResponseCorrection } from '$/utils/discord/messageScan' import { handleUserResponseCorrection } from '$/utils/discord/messageScan'
import { isAdmin } from '$/utils/discord/permissions' import { isAdmin } from '$/utils/discord/permissions'
import { eq } from 'drizzle-orm' import type { ConfigMessageScanResponseLabelConfig } from '$/../config.schema'
const PossibleReactions = Object.values(Reactions) as string[] const PossibleReactions = Object.values(Reactions) as string[]
@@ -32,13 +30,11 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
if (reactionMessage.author.id !== reaction.client.user!.id) return if (reactionMessage.author.id !== reaction.client.user!.id) return
if (!PossibleReactions.includes(reaction.emoji.name!)) return if (!PossibleReactions.includes(reaction.emoji.name!)) return
await rct.users.remove(user.id)
if (!isAdmin(reactionMessage.member || reactionMessage.author)) { if (!isAdmin(reactionMessage.member || reactionMessage.author)) {
// User is in guild, and config has member requirements // User is in guild, and config has member requirements
if ( if (reactionMessage.inGuild() && msConfig.humanCorrections.allow) {
reactionMessage.inGuild() &&
(msConfig.humanCorrections.allow?.members || msConfig.humanCorrections.allow?.users)
) {
const { const {
allow: { users: allowedUsers, members: allowedMembers }, allow: { users: allowedUsers, members: allowedMembers },
} = msConfig.humanCorrections } = msConfig.humanCorrections
@@ -54,22 +50,21 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
) )
) )
return return
} else if (allowedUsers) { } else if (!allowedUsers?.includes(user.id)) return
if (!allowedUsers.includes(user.id)) return } else
} else {
return void logger.warn( return void logger.warn(
'No member or user requirements set for human corrections, all requests will be ignored', 'No member or user requirements set for human corrections, all requests will be ignored',
) )
} }
}
}
// Sanity check // Sanity check
const response = await db.query.responses.findFirst({ where: eq(responses.replyId, rct.message.id) }) const response = await db.query.responses.findFirst({ where: eq(responses.replyId, rct.message.id) })
if (!response || response.correctedById) return if (!response || response.correctedById) return
const handleCorrection = (label: string) => logger.debug(`User ${user.id} is trying to correct the response ${rct.message.id}`)
handleUserResponseCorrection(context, response, reactionMessage, label, user)
const handleCorrection = (label?: string) =>
handleUserResponseCorrection(context, response, reactionMessage, user, label)
try { try {
if (reaction.emoji.name === Reactions.train) { if (reaction.emoji.name === Reactions.train) {
@@ -111,7 +106,7 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
.setCustomId(`${componentPrefix}_cancel`), .setCustomId(`${componentPrefix}_cancel`),
new ButtonBuilder() new ButtonBuilder()
.setEmoji(Reactions.delete) .setEmoji(Reactions.delete)
.setLabel('Delete (mark as false positive)') .setLabel('Delete (mark as out of scope)')
.setStyle(ButtonStyle.Danger) .setStyle(ButtonStyle.Danger)
.setCustomId(`${componentPrefix}_delete`), .setCustomId(`${componentPrefix}_delete`),
), ),
@@ -122,8 +117,8 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => {
components: rows, components: rows,
}) })
} else if (reaction.emoji.name === Reactions.delete) { } else if (reaction.emoji.name === Reactions.delete) {
await handleCorrection(msConfig.humanCorrections.falsePositiveLabel) await handleCorrection()
await user.send({ content: 'The response has been deleted and marked as a false positive.' }) await user.send({ content: 'The response has been deleted and marked as out of scope.' })
} }
} catch (e) { } catch (e) {
logger.error('Failed to correct response:', e) logger.error('Failed to correct response:', e)

View File

@@ -1,35 +0,0 @@
import { database, logger } from '$/context'
import { appliedPresets } from '$/database/schemas'
import { removeRolePreset } from '$/utils/discord/rolePresets'
import type { Client } from 'discord.js'
import { lt } from 'drizzle-orm'
import { on, withContext } from 'src/utils/discord/events'
export default withContext(on, 'ready', ({ config, logger }, client) => {
logger.info(`Connected to Discord API, logged in as ${client.user.displayName} (@${client.user.tag})!`)
logger.info(`Bot is in ${client.guilds.cache.size} guilds`)
if (config.rolePresets) {
removeExpiredPresets(client)
setTimeout(() => removeExpiredPresets(client), config.rolePresets.checkExpiredEvery)
}
})
const removeExpiredPresets = async (client: Client) => {
logger.debug('Checking for expired role presets...')
const expireds = await database.query.appliedPresets.findMany({
where: lt(appliedPresets.until, Math.floor(Date.now() / 1000)),
})
for (const expired of expireds)
try {
const guild = await client.guilds.fetch(expired.guildId)
const member = await guild.members.fetch(expired.memberId)
logger.debug(`Removing role preset for ${expired.memberId} in ${expired.guildId}`)
await removeRolePreset(member, expired.preset)
} catch (e) {
logger.error(`Error while removing role preset for ${expired.memberId} in ${expired.guildId}: ${e}`)
}
}

View File

@@ -0,0 +1,6 @@
import { on, withContext } from '$/utils/discord/events'
export default withContext(on, 'ready', async ({ logger }, client) => {
logger.info(`Connected to Discord API, logged in as ${client.user.displayName} (@${client.user.tag})!`)
logger.info(`Bot is in ${client.guilds.cache.size} guilds`)
})

View File

@@ -0,0 +1,42 @@
import { type Client, DiscordAPIError } from 'discord.js'
import { and, eq, lt } from 'drizzle-orm'
import { database, logger } from '$/context'
import { appliedPresets } from '$/database/schemas'
import { on, withContext } from '$/utils/discord/events'
import { removeRolePreset } from '$/utils/discord/rolePresets'
export default withContext(on, 'ready', async ({ config }, client) => {
if (config.rolePresets) {
removeExpiredPresets(client)
setInterval(() => removeExpiredPresets(client), config.rolePresets.checkExpiredEvery)
}
})
async function removeExpiredPresets(client: Client) {
logger.debug('Checking for expired role presets...')
const expireds = await database.query.appliedPresets.findMany({
where: lt(appliedPresets.until, Math.floor(Date.now() / 1000)),
})
for (const expired of expireds) {
try {
logger.debug(`Removing role preset for ${expired.memberId} in ${expired.guildId}`)
const guild = await client.guilds.fetch(expired.guildId)
const member = await guild.members.fetch(expired.memberId)
await removeRolePreset(member, expired.preset)
} catch (e) {
// Unknown Member: https://discord.com/developers/docs/topics/opcodes-and-status-codes#json
if (!(e instanceof DiscordAPIError) || e.code !== 10007) {
logger.error(`Error while removing role preset for ${expired.memberId} in ${expired.guildId}: ${e}`)
continue
}
}
await database
.delete(appliedPresets)
.where(and(eq(appliedPresets.guildId, expired.guildId), eq(appliedPresets.memberId, expired.memberId)))
}
}

View File

@@ -0,0 +1,68 @@
import { applyCommonEmbedStyles } from '$/utils/discord/embeds'
import { on, withContext } from '$/utils/discord/events'
export default withContext(on, 'ready', async ({ config, discord, logger }, client) => {
if (config.stickyMessages)
for (const [guildId, channels] of Object.entries(config.stickyMessages)) {
const guild = await client.guilds.fetch(guildId)
// In case of configuration refresh, this will not be nullable
const oldStore = discord.stickyMessages[guildId]
discord.stickyMessages[guildId] = {}
for (const [channelId, { message, timeout, forceSendTimeout }] of Object.entries(channels)) {
const channel = await guild.channels.fetch(channelId)
if (!channel?.isTextBased())
return void logger.warn(
`Channel ${channelId} in guild ${guildId} is not a text channel, sticky messages will not be sent`,
)
// Set up the store
// biome-ignore lint/suspicious/noAssignInExpressions: don't care
const store = (discord.stickyMessages[guildId]![channelId] = {
forceTimerActive: false,
timerActive: false,
forceTimerMs: forceSendTimeout,
timerMs: timeout,
async send() {
try {
await Promise.all([
channel
.send({
...message,
embeds: message.embeds?.map(it => applyCommonEmbedStyles(it, true, true, true)),
})
.then(msg => {
this.currentMessage = msg
logger.debug(`Sent sticky message to channel ${channelId} in guild ${guildId}`)
}),
this.currentMessage
?.delete()
?.then(() =>
logger.debug(
`Deleted old sticky message from channel ${channelId} in guild ${guildId}`,
),
),
])
} catch (e) {
logger.error(
`Error while managing sticky message of channel ${channelId} in guild ${guildId}:`,
e,
)
} finally {
// Clear any remaining timers
clearTimeout(this.timer)
clearTimeout(this.forceTimer)
this.forceTimerActive = this.timerActive = false
logger.debug(`Cleared sticky message timer for channel ${channelId} in guild ${guildId}`)
}
},
// If the store exists before the configuration refresh, take its current message
currentMessage: oldStore?.[channelId]?.currentMessage,
})
// Send a new sticky message immediately, as well as deleting the old/outdated message, if it exists
await store.send()
}
}
})

View File

@@ -1,5 +1,5 @@
import { api, discord, logger } from '$/context'
import { getMissingEnvironmentVariables } from '@revanced/bot-shared' import { getMissingEnvironmentVariables } from '@revanced/bot-shared'
import { api, discord, logger } from '$/context'
import './events/register' import './events/register'
@@ -10,5 +10,10 @@ if (missingEnvs.length) {
process.exit(1) process.exit(1)
} }
// Handle uncaught exceptions
process.on('uncaughtException', error => console.error('Uncaught exception:', error))
process.on('unhandledRejection', reason => console.error('Unhandled rejection:', reason))
api.client.connect() api.client.connect()
discord.client.login() discord.client.login()

View File

@@ -1,5 +1,5 @@
import type { ClientWebSocketEvents } from '@revanced/bot-api'
import * as context from '../../context' import * as context from '../../context'
import type { ClientWebSocketEvents } from '@revanced/bot-api'
const { client } = context.api const { client } = context.api

View File

@@ -1,5 +1,5 @@
import { DefaultEmbedColor, MessageScanHumanizedMode, ReVancedLogoURL } from '$/constants' import { type APIEmbed, EmbedBuilder, type EmbedField, type JSONEncodable, type User } from 'discord.js'
import { EmbedBuilder, type EmbedField, type User } from 'discord.js' import { DefaultEmbedColor, ReVancedLogoURL } from '$/constants'
import type { ConfigMessageScanResponseMessage } from '../../../config.schema' import type { ConfigMessageScanResponseMessage } from '../../../config.schema'
export const createErrorEmbed = (title: string | null, description?: string) => export const createErrorEmbed = (title: string | null, description?: string) =>
@@ -25,23 +25,7 @@ export const createSuccessEmbed = (title: string | null, description?: string) =
export const createMessageScanResponseEmbed = ( export const createMessageScanResponseEmbed = (
response: NonNullable<ConfigMessageScanResponseMessage['embeds']>[number], response: NonNullable<ConfigMessageScanResponseMessage['embeds']>[number],
mode: 'ocr' | 'nlp' | 'match', ) => applyCommonEmbedStyles(response, true, true, true)
) => {
// biome-ignore lint/style/noParameterAssign: While this is confusing, it is fine for this purpose
if ('toJSON' in response) response = response.toJSON()
const embed = new EmbedBuilder().setTitle(response.title ?? null)
if (response.description) embed.setDescription(response.description)
if (response.fields) embed.addFields(response.fields)
embed.setFooter({
text: `ReVanced • Via ${MessageScanHumanizedMode[mode]}`,
iconURL: ReVancedLogoURL,
})
return applyCommonEmbedStyles(embed, true, true, true)
}
export const createModerationActionEmbed = ( export const createModerationActionEmbed = (
action: string, action: string,
@@ -77,19 +61,23 @@ export const applyReferenceToModerationActionEmbed = (embed: EmbedBuilder, refer
} }
export const applyCommonEmbedStyles = ( export const applyCommonEmbedStyles = (
embed: EmbedBuilder, embed: EmbedBuilder | JSONEncodable<APIEmbed> | APIEmbed,
setThumbnail = false, setThumbnail = false,
setFooter = false, setFooter = false,
setColor = false, setColor = false,
) => { ) => {
// biome-ignore lint/style/noParameterAssign: While this is confusing, it is fine for this purpose
if ('toJSON' in embed) embed = embed.toJSON()
const builder = new EmbedBuilder(embed)
if (setFooter) if (setFooter)
embed.setFooter({ builder.setFooter({
text: 'ReVanced', text: 'ReVanced',
iconURL: ReVancedLogoURL, iconURL: ReVancedLogoURL,
}) })
if (setColor) embed.setColor(DefaultEmbedColor) if (setColor) builder.setColor(DefaultEmbedColor)
if (setThumbnail) embed.setThumbnail(ReVancedLogoURL) if (setThumbnail) builder.setThumbnail(ReVancedLogoURL)
return embed return builder
} }

View File

@@ -1,19 +1,23 @@
import { type Response, responses } from '$/database/schemas' import { ButtonStyle, ComponentType } from 'discord.js'
import type { Config, ConfigMessageScanResponse, ConfigMessageScanResponseLabelConfig } from 'config.schema'
import type { Message, PartialUser, User } from 'discord.js'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { type Response, responses } from '$/database/schemas'
import { createMessageScanResponseEmbed } from './embeds' import { createMessageScanResponseEmbed } from './embeds'
import type { Config, ConfigMessageScanResponse, ConfigMessageScanResponseLabelConfig } from 'config.schema'
import type { APIActionRowComponent, APIButtonComponent, Message, PartialUser, User } from 'discord.js'
export const getResponseFromText = async ( export const getResponseFromText = async (
content: string, content: string,
responses: ConfigMessageScanResponse[], responses: ConfigMessageScanResponse[],
// Just to be safe that we will never use data from the context parameter // Just to be safe that we will never use data from the context parameter
{ api, logger }: Omit<typeof import('src/context'), 'config'>, { api, logger }: Omit<typeof import('src/context'), 'config'>,
ocrMode = false, flags: { imageTriggersOnly?: boolean; textRegexesOnly?: boolean } = {},
): Promise<ConfigMessageScanResponse & { label?: string }> => { ): Promise<
let responseConfig: Awaited<ReturnType<typeof getResponseFromText>> = { Omit<ConfigMessageScanResponse, 'triggers'> & { label?: string; triggers?: ConfigMessageScanResponse['triggers'] }
triggers: {}, > => {
response: null, type ResponseConfig = Awaited<ReturnType<typeof getResponseFromText>>
let responseConfig: Omit<ResponseConfig, 'triggers'> & { triggers?: ResponseConfig['triggers'] } = {
triggers: undefined,
response: null!,
} }
const firstLabelIndexes: number[] = [] const firstLabelIndexes: number[] = []
@@ -27,9 +31,8 @@ export const getResponseFromText = async (
const { const {
triggers: { text: textTriggers, image: imageTriggers }, triggers: { text: textTriggers, image: imageTriggers },
} = trigger } = trigger
if (responseConfig) break
if (ocrMode) { if (flags.imageTriggersOnly) {
if (imageTriggers) if (imageTriggers)
for (const regex of imageTriggers) for (const regex of imageTriggers)
if (regex.test(content)) { if (regex.test(content)) {
@@ -37,7 +40,7 @@ export const getResponseFromText = async (
responseConfig = trigger responseConfig = trigger
break break
} }
} else } else {
for (let j = 0; j < textTriggers!.length; j++) { for (let j = 0; j < textTriggers!.length; j++) {
const regex = textTriggers![j]! const regex = textTriggers![j]!
@@ -53,38 +56,40 @@ export const getResponseFromText = async (
} }
} }
} }
}
// If none of the regexes match, we can search for labels immediately // If none of the regexes match, we can search for labels immediately
if (!responseConfig && !ocrMode) { if (!responseConfig.triggers && !flags.textRegexesOnly) {
logger.debug('No match from before regexes, doing NLP') logger.debug('No match from before regexes, doing NLP')
const scan = await api.client.parseText(content) const scan = await api.client.parseText(content)
if (scan.labels.length) { if (scan.labels.length) {
const matchedLabel = scan.labels[0]! const matchedLabel = scan.labels[0]!
logger.debug(`Message matched label with confidence: ${matchedLabel.name}, ${matchedLabel.confidence}`) logger.debug(`Message matched label with confidence: ${matchedLabel.name}, ${matchedLabel.confidence}`)
let triggerConfig: ConfigMessageScanResponseLabelConfig | undefined let trigger: ConfigMessageScanResponseLabelConfig | undefined
const labelConfig = responses.find(x => { const response = responses.find(x => {
const config = x.triggers.text!.find( const config = x.triggers.text!.find(
(x): x is ConfigMessageScanResponseLabelConfig => 'label' in x && x.label === matchedLabel.name, (x): x is ConfigMessageScanResponseLabelConfig => 'label' in x && x.label === matchedLabel.name,
) )
if (config) triggerConfig = config if (config) trigger = config
return config return config
}) })
if (!labelConfig) { if (!response) {
logger.warn(`No label config found for label ${matchedLabel.name}`) logger.warn(`No response config found for label ${matchedLabel.name}`)
// This returns the default value set in line 17, which means no response matched
return responseConfig return responseConfig
} }
if (matchedLabel.confidence >= triggerConfig!.threshold) { if (matchedLabel.confidence >= trigger!.threshold) {
logger.debug('Label confidence is enough') logger.debug('Label confidence is enough')
responseConfig = labelConfig responseConfig = { ...responseConfig, ...response, label: trigger!.label }
} }
} }
} }
// If we still don't have a response config, we can match all regexes after the initial label trigger // If we still don't have a response config, we can match all regexes after the initial label trigger
if (!responseConfig) { if (!responseConfig.triggers && !flags.imageTriggersOnly) {
logger.debug('No match from NLP, doing after regexes') logger.debug('No match from NLP, doing after regexes')
for (let i = 0; i < responses.length; i++) { for (let i = 0; i < responses.length; i++) {
const { const {
@@ -92,8 +97,8 @@ export const getResponseFromText = async (
} = responses[i]! } = responses[i]!
const firstLabelIndex = firstLabelIndexes[i] ?? -1 const firstLabelIndex = firstLabelIndexes[i] ?? -1
for (let i = firstLabelIndex + 1; i < textTriggers!.length; i++) { for (let j = firstLabelIndex + 1; j < textTriggers!.length; j++) {
const trigger = textTriggers![i]! const trigger = textTriggers![j]!
if (trigger instanceof RegExp) { if (trigger instanceof RegExp) {
if (trigger.test(content)) { if (trigger.test(content)) {
@@ -138,15 +143,22 @@ export const handleUserResponseCorrection = async (
{ api, database: db, config: { messageScan: msConfig }, logger }: typeof import('$/context'), { api, database: db, config: { messageScan: msConfig }, logger }: typeof import('$/context'),
response: Response, response: Response,
reply: Message, reply: Message,
label: string,
user: User | PartialUser, user: User | PartialUser,
label?: string,
) => { ) => {
if (!label) {
await Promise.all([reply.delete(), api.client.trainMessage(response.content, label)]).finally(() =>
logger.debug(`User ${user.id} trained message ${response.replyId} as out of scope`),
)
return
}
const correctLabelResponse = msConfig!.responses!.find(r => const correctLabelResponse = msConfig!.responses!.find(r =>
r.triggers.text!.some(t => 'label' in t && t.label === label), r.triggers.text!.some(t => 'label' in t && t.label === label),
) )
if (!correctLabelResponse) throw new Error('Cannot find label config for the selected label') if (!correctLabelResponse) throw new Error('Cannot find label config for the selected label')
if (!correctLabelResponse.response) return void (await reply.delete())
if (response.label !== label) { if (response.label !== label) {
db.update(responses) db.update(responses)
@@ -156,14 +168,41 @@ export const handleUserResponseCorrection = async (
}) })
.where(eq(responses.replyId, response.replyId)) .where(eq(responses.replyId, response.replyId))
await reply.edit({ return void (await reply.edit({
...correctLabelResponse.response, ...correctLabelResponse.response,
embeds: correctLabelResponse.response.embeds?.map(it => createMessageScanResponseEmbed(it, 'nlp')), embeds: correctLabelResponse.response.embeds?.map(createMessageScanResponseEmbed),
}) components: [],
}))
} }
await api.client.trainMessage(response.content, label) await Promise.all([
logger.debug(`User ${user.id} trained message ${response.replyId} as ${label} (positive)`) api.client.trainMessage(response.content, label),
reply.edit({
await reply.reactions.removeAll() components: [],
}),
]).finally(() => logger.debug(`User ${user.id} trained message ${response.replyId} as ${label}`))
} }
export const createMessageScanResponseComponents = (reply: Message<true>) => [
{
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.Button,
style: ButtonStyle.Secondary,
emoji: {
id: '👍',
},
custom_id: `train:${reply.id}`,
},
{
type: ComponentType.Button,
style: ButtonStyle.Secondary,
emoji: {
id: '🔧',
},
custom_id: `edit:${reply.id}`,
},
],
} as APIActionRowComponent<APIButtonComponent>,
]

View File

@@ -1,7 +1,7 @@
import { config, logger } from '$/context'
import decancer from 'decancer' import decancer from 'decancer'
import type { ChatInputCommandInteraction, EmbedBuilder, Guild, GuildMember, Message, User } from 'discord.js' import { config, logger } from '$/context'
import { applyReferenceToModerationActionEmbed, createModerationActionEmbed } from './embeds' import { applyReferenceToModerationActionEmbed, createModerationActionEmbed } from './embeds'
import type { CommandInteraction, EmbedBuilder, Guild, GuildMember, Message, User } from 'discord.js'
const PresetLogAction = { const PresetLogAction = {
apply: 'Applied role preset to', apply: 'Applied role preset to',
@@ -10,7 +10,7 @@ const PresetLogAction = {
export const sendPresetReplyAndLogs = ( export const sendPresetReplyAndLogs = (
action: keyof typeof PresetLogAction, action: keyof typeof PresetLogAction,
interaction: ChatInputCommandInteraction | Message, interaction: CommandInteraction | Message,
executor: GuildMember, executor: GuildMember,
user: User, user: User,
preset: string, preset: string,
@@ -23,10 +23,7 @@ export const sendPresetReplyAndLogs = (
]), ]),
) )
export const sendModerationReplyAndLogs = async ( export const sendModerationReplyAndLogs = async (interaction: CommandInteraction | Message, embed: EmbedBuilder) => {
interaction: ChatInputCommandInteraction | Message,
embed: EmbedBuilder,
) => {
const reply = await interaction.reply({ embeds: [embed] }).then(it => it.fetch()) const reply = await interaction.reply({ embeds: [embed] }).then(it => it.fetch())
const logChannel = await getLogChannel(interaction.guild!) const logChannel = await getLogChannel(interaction.guild!)
await logChannel?.send({ embeds: [applyReferenceToModerationActionEmbed(embed, reply.url)] }) await logChannel?.send({ embeds: [applyReferenceToModerationActionEmbed(embed, reply.url)] })
@@ -38,7 +35,7 @@ export const getLogChannel = async (guild: Guild) => {
try { try {
const channel = await guild.channels.fetch(logConfig.thread ?? logConfig.channel) const channel = await guild.channels.fetch(logConfig.thread ?? logConfig.channel)
if (!channel || !channel.isTextBased()) if (!channel?.isTextBased())
return void logger.warn('The moderation log channel does not exist, skipping logging') return void logger.warn('The moderation log channel does not exist, skipping logging')
return channel return channel
@@ -54,13 +51,13 @@ export const cureNickname = async (member: GuildMember) => {
const name = member.displayName const name = member.displayName
let cured = decancer(name) let cured = decancer(name)
.toString() .toString()
.replace(/[^a-zA-Z0-9]/g, '') .replace(new RegExp(config.moderation?.cure?.removeCharactersRegex ?? '[^a-zA-Z0-9 \\-_]', 'g'), '')
if (cured.length < 3 || !/^[a-zA-Z]/.test(cured)) if (cured.length < (config?.moderation?.cure?.minimumNameLength ?? 3))
cured = cured =
member.user.username.length >= 3 member.user.username.length >= 3
? member.user.username ? member.user.username
: config.moderation?.cure?.defaultName ?? 'Server member' : (config.moderation?.cure?.defaultName ?? 'Server member')
if (cured.toLowerCase() === name.toLowerCase()) return if (cured.toLowerCase() === name.toLowerCase()) return

View File

@@ -1,5 +1,5 @@
import { GuildMember, type User } from 'discord.js' import { GuildMember, type User } from 'discord.js'
import config from '../../../config' import { config } from '../../context'
export const isAdmin = (userOrMember: User | GuildMember) => { export const isAdmin = (userOrMember: User | GuildMember) => {
return ( return (

View File

@@ -1,14 +1,14 @@
import { and, eq } from 'drizzle-orm'
import { config, database } from '$/context' import { config, database } from '$/context'
import { appliedPresets } from '$/database/schemas' import { appliedPresets } from '$/database/schemas'
import type { GuildMember } from 'discord.js' import type { GuildMember } from 'discord.js'
import { and, eq } from 'drizzle-orm'
// TODO: Fix this type // TODO: Fix this type
type PresetKey = string type PresetKey = string
export const applyRolePreset = async (member: GuildMember, presetName: PresetKey, untilMs: number) => { export const applyRolePreset = async (member: GuildMember, presetName: PresetKey, expires: number) => {
const afterInsert = await applyRolesUsingPreset(presetName, member, true) const { removed, callback } = await applyRolesUsingPreset(presetName, member)
const until = untilMs === Infinity ? null : Math.ceil(untilMs / 1000) const until = expires === Infinity ? null : Math.ceil(expires / 1000)
await database await database
.insert(appliedPresets) .insert(appliedPresets)
@@ -16,39 +16,60 @@ export const applyRolePreset = async (member: GuildMember, presetName: PresetKey
memberId: member.id, memberId: member.id,
guildId: member.guild.id, guildId: member.guild.id,
preset: presetName, preset: presetName,
removedRoles: removed,
until, until,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: [appliedPresets.memberId, appliedPresets.preset, appliedPresets.guildId], target: [appliedPresets.memberId, appliedPresets.preset, appliedPresets.guildId],
set: { until }, set: { until },
}) })
.then(afterInsert) .then(callback)
} }
export const removeRolePreset = async (member: GuildMember, presetName: PresetKey) => { export const removeRolePreset = async (member: GuildMember, presetName: PresetKey) => {
const afterDelete = await applyRolesUsingPreset(presetName, member, false) const where = and(
await database
.delete(appliedPresets)
.where(
and(
eq(appliedPresets.memberId, member.id), eq(appliedPresets.memberId, member.id),
eq(appliedPresets.preset, presetName), eq(appliedPresets.preset, presetName),
eq(appliedPresets.guildId, member.guild.id), eq(appliedPresets.guildId, member.guild.id),
),
) )
.execute()
.then(afterDelete) const data = await database.query.appliedPresets.findFirst({ where })
if (!data) return false
const { callback } = await applyRolesUsingPreset(presetName, member, data.removedRoles)
await database.delete(appliedPresets).where(where).execute().then(callback)
return true
} }
export const applyRolesUsingPreset = async (presetName: string, member: GuildMember, applying: boolean) => { export const applyRolesUsingPreset = async (
presetName: string,
member: GuildMember,
removePresetGiveRoles?: string[],
) => {
const preset = config.rolePresets?.guilds[member.guild.id]?.[presetName] const preset = config.rolePresets?.guilds[member.guild.id]?.[presetName]
if (!preset) throw new Error(`The preset "${presetName}" does not exist for this server`) if (!preset) throw new Error(`The preset "${presetName}" does not exist for this server`)
const roles = new Set(member.roles.cache.keys()) const roles = new Set(member.roles.cache.keys())
const removed: string[] = []
for (const role of preset.give) roles[applying ? 'add' : 'delete'](role) // If removePresetGiveRoles is not provided, we're applying a preset
for (const role of preset.take) roles[applying ? 'delete' : 'add'](role) if (!removePresetGiveRoles) {
for (const role of preset.give) roles.add(role)
return () => member.roles.set(Array.from(roles)) for (const role of preset.take) {
if (roles.has(role)) {
roles.delete(role)
removed.push(role)
}
}
} else {
const guildRoles = await member.guild.roles.fetch()
for (const role of preset.give) roles.delete(role)
for (const role of removePresetGiveRoles) if (guildRoles.has(role)) roles.add(role)
}
return {
removed,
callback: () => member.roles.set(Array.from(roles)),
}
} }

View File

@@ -1,25 +1,44 @@
export const parseDuration = (duration: string) => { import { Duration, DurationFormatter } from '@sapphire/duration'
if (!duration.length) return Number.NaN
const matches = duration.match(/(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?/)!
const [, days, hours, minutes, seconds] = matches.map(Number) const fmt = new DurationFormatter({
return ( year: {
(days || 0) * 24 * 60 * 60 * 1000 + DEFAULT: 'y',
(hours || 0) * 60 * 60 * 1000 + },
(minutes || 0) * 60 * 1000 + month: {
(seconds || 0) * 1000 DEFAULT: 'M',
) },
week: {
DEFAULT: 'w',
},
day: {
DEFAULT: 'd',
},
hour: {
DEFAULT: 'h',
},
minute: {
DEFAULT: 'm',
},
second: {
DEFAULT: 's',
},
})
export const parseDuration = (duration: string, defaultUnit = 's') => {
// adds default unit to the end of the string if it doesn't have a unit
// 100 -> 100s
// 10m100 -> 10m100s
// biome-ignore lint/style/noParameterAssign: this is fine
if (/\d$/.test(duration)) duration += defaultUnit
return new Duration(duration).offset
} }
export const durationToString = (duration: number) => { export const durationToString = (duration: number) => {
if (duration === 0) return '0s' return fmt.format(duration, undefined, {
left: '',
const days = Math.floor(duration / (24 * 60 * 60 * 1000)) })
const hours = Math.floor((duration % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)) }
const minutes = Math.floor((duration % (60 * 60 * 1000)) / (60 * 1000))
const seconds = Math.floor((duration % (60 * 1000)) / 1000) export function isSafeTimeoutDuration(duration: number) {
return duration > 0 && duration < 2 ** 31 - 1
return `${days ? `${days}d` : ''}${hours ? `${hours}h` : ''}${minutes ? `${minutes}m` : ''}${
seconds ? `${seconds}s` : ''
}`
} }

2446
bun.lock Normal file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,8 +11,8 @@ To start developing, you'll need to set up the development environment first.
2. Clone the mono-repository 2. Clone the mono-repository
```sh ```sh
git clone https://github.com/revanced/revanced-helper.git && git clone https://github.com/revanced/revanced-bots.git &&
cd revanced-helper cd revanced-bots
``` ```
3. Install dependencies 3. Install dependencies

View File

@@ -1,12 +1,16 @@
{ {
"name": "revanced-helper", "name": "revanced-bots",
"description": "🤖 Bots assisting ReVanced on multiple platforms", "description": "🤖 Bots assisting ReVanced on multiple platforms",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"type": "module", "type": "module",
"author": "Palm <contact@palmdevs.me> (https://palmdevs.me)", "author": "Palm <contact@palmdevs.me> (https://palmdevs.me)",
"workspaces": ["packages/*", "apis/*", "bots/*"], "workspaces": [
"packages/*",
"apis/*",
"bots/*"
],
"scripts": { "scripts": {
"build:all": "turbo run build", "build:all": "turbo run build",
"build:packages": "turbo build --filter=\"./packages/*\"", "build:packages": "turbo build --filter=\"./packages/*\"",
@@ -15,37 +19,37 @@
"flint:check": "biome check .", "flint:check": "biome check .",
"clint": "commitlint --edit" "clint": "commitlint --edit"
}, },
"homepage": "https://github.com/revanced/revanced-helper#readme", "homepage": "https://github.com/revanced/revanced-bots#readme",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/revanced/revanced-helper.git" "url": "git+https://github.com/revanced/revanced-bots.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/revanced/revanced-helper/issues" "url": "https://github.com/revanced/revanced-bots/issues"
}, },
"contributors": [ "contributors": [
"Palm <contact@palmdevs.me> (https://palmdevs.me)", "Palm <contact@palmdevs.me> (https://palmdevs.me)",
"ReVanced <nosupport@revanced.app> (https://revanced.app)" "ReVanced <nosupport@revanced.app> (https://revanced.app)"
], ],
"packageManager": "bun@1.1.20", "packageManager": "bun@1.2.17",
"devDependencies": { "devDependencies": {
"@anolilab/multi-semantic-release": "^1.1.3", "@anolilab/multi-semantic-release": "^2.0.3",
"@biomejs/biome": "^1.8.3", "@biomejs/biome": "^2.2.4",
"@codedependant/semantic-release-docker": "^5.0.3", "@codedependant/semantic-release-docker": "^5.1.1",
"@commitlint/cli": "^19.3.0", "@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.2.2", "@commitlint/config-conventional": "^19.8.1",
"@saithodev/semantic-release-backmerge": "^4.0.1", "@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3", "@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@tsconfig/strictest": "^2.0.5", "@tsconfig/strictest": "^2.0.5",
"@types/bun": "^1.1.6", "@types/bun": "^1.2.21",
"conventional-changelog-conventionalcommits": "^7.0.2", "conventional-changelog-conventionalcommits": "^9.1.0",
"lefthook": "^1.7.5", "lefthook": "^1.13.0",
"portainer-service-webhook": "https://github.com/newarifrh/portainer-service-webhook#v1", "portainer-service-webhook": "https://github.com/newarifrh/portainer-service-webhook#v1",
"semantic-release": "^24.0.0", "semantic-release": "^24.2.8",
"turbo": "^2.0.9", "turbo": "^2.5.6",
"typescript": "^5.5.4" "typescript": "^5.9.2"
}, },
"trustedDependencies": [ "trustedDependencies": [
"@biomejs/biome", "@biomejs/biome",
@@ -54,8 +58,6 @@
"lefthook" "lefthook"
], ],
"patchedDependencies": { "patchedDependencies": {
"@semantic-release/npm@12.0.1": "patches/@semantic-release%2Fnpm@12.0.1.patch", "@semantic-release/npm@12.0.2": "patches/@semantic-release%2Fnpm@12.0.2.patch"
"drizzle-kit@0.22.8": "patches/drizzle-kit@0.22.8.patch",
"decancer@3.2.3": "patches/decancer@3.2.3.patch"
} }
} }

View File

@@ -13,7 +13,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/revanced/revanced-helper.git", "url": "git+https://github.com/revanced/revanced-bots.git",
"directory": "packages/api" "directory": "packages/api"
}, },
"author": "Palm <contact@palmdevs.me> (https://palmdevs.me)", "author": "Palm <contact@palmdevs.me> (https://palmdevs.me)",
@@ -23,15 +23,15 @@
], ],
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/revanced/revanced-helper/issues" "url": "https://github.com/revanced/revanced-bots/issues"
}, },
"homepage": "https://github.com/revanced/revanced-helper#readme", "homepage": "https://github.com/revanced/revanced-bots#readme",
"dependencies": { "dependencies": {
"@revanced/bot-shared": "workspace:*", "@revanced/bot-shared": "workspace:*",
"ws": "^8.17.1" "ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/ws": "^8.5.10", "@types/ws": "^8.18.1",
"typed-emitter": "^2.1.0" "typed-emitter": "^2.1.0"
} }
} }

View File

@@ -52,17 +52,20 @@ export default class Client {
// But if we add anything similar, this will cause another race condition // But if we add anything similar, this will cause another race condition
// To fix this, we can try adding a instanced function that would return the currentSequence // To fix this, we can try adding a instanced function that would return the currentSequence
// and it would be updated every time a "heartbeat ack" packet is received // and it would be updated every time a "heartbeat ack" packet is received
return Promise.race([ const packet = await Promise.race([
this.#awaiter.await(ServerOperation.ParsedText, this.ws.currentSequence), this.#awaiter.await(ServerOperation.ParsedText, this.ws.currentSequence),
this.#awaiter.await(ServerOperation.ParseTextFailed, this.ws.timeout + 5000), this.#awaiter.await(ServerOperation.ParseTextFailed, this.ws.currentSequence, this.ws.timeout + 5000),
]) ])
.then(pkt => { .then(pkt => {
if (pkt.op === ServerOperation.ParsedText) return pkt.d if (pkt.op === ServerOperation.ParsedText) return pkt.d
throw new Error('Failed to parse text, the API encountered an error') return null
}) })
.catch(() => { .catch(() => {
throw new Error('Failed to parse text, the API did not respond in time') throw new Error('Failed to parse text, the API did not respond in time')
}) })
if (!packet) throw new Error('Failed to parse text, the API encountered an error')
return packet
} }
/** /**
@@ -82,20 +85,23 @@ export default class Client {
// See line 50 // See line 50
return Promise.race([ const packet = await Promise.race([
this.#awaiter.await(ServerOperation.ParsedImage, this.ws.currentSequence), this.#awaiter.await(ServerOperation.ParsedImage, this.ws.currentSequence),
this.#awaiter.await(ServerOperation.ParseImageFailed, this.ws.timeout + 5000), this.#awaiter.await(ServerOperation.ParseImageFailed, this.ws.currentSequence, this.ws.timeout + 5000),
]) ])
.then(pkt => { .then(pkt => {
if (pkt.op === ServerOperation.ParsedImage) return pkt.d if (pkt.op === ServerOperation.ParsedImage) return pkt.d
throw new Error('Failed to parse image, the API encountered an error') return null
}) })
.catch(() => { .catch(() => {
throw new Error('Failed to parse image, the API did not respond in time') throw new Error('Failed to parse image, the API did not respond in time')
}) })
if (!packet) throw new Error('Failed to parse image, the API encountered an error')
return packet
} }
async trainMessage(text: string, label: string) { async trainMessage(text: string, label?: string) {
this.#throwIfNotReady() this.#throwIfNotReady()
this.ws.send({ this.ws.send({
@@ -107,17 +113,20 @@ export default class Client {
}) })
// See line 50 // See line 50
return Promise.race([ const packet = await Promise.race([
this.#awaiter.await(ServerOperation.TrainedMessage, this.ws.currentSequence), this.#awaiter.await(ServerOperation.TrainedMessage, this.ws.currentSequence),
this.#awaiter.await(ServerOperation.TrainMessageFailed, this.ws.timeout + 5000), this.#awaiter.await(ServerOperation.TrainMessageFailed, this.ws.currentSequence, this.ws.timeout + 5000),
]) ])
.then(pkt => { .then(pkt => {
if (pkt.op === ServerOperation.TrainedMessage) return pkt.d if (pkt.op === ServerOperation.TrainedMessage) return pkt.d
throw new Error('Failed to train message, the API encountered an error') return null
}) })
.catch(() => { .catch(() => {
throw new Error('Failed to train message, the API did not respond in time') throw new Error('Failed to train message, the API did not respond in time')
}) })
if (!packet) throw new Error('Failed to train message, the API encountered an error')
return packet
} }
/** /**

View File

@@ -1,23 +1,23 @@
import { EventEmitter } from 'events'
import { import {
type ClientOperation, type ClientOperation,
DisconnectReason, DisconnectReason,
type Packet,
ServerOperation,
deserializePacket, deserializePacket,
isServerPacket, isServerPacket,
type Packet,
ServerOperation,
serializePacket, serializePacket,
uncapitalize, uncapitalize,
} from '@revanced/bot-shared' } from '@revanced/bot-shared'
import type TypedEmitter from 'typed-emitter' import { EventEmitter } from 'events'
import { type RawData, WebSocket } from 'ws' import { type RawData, WebSocket } from 'ws'
import type TypedEmitter from 'typed-emitter'
/** /**
* The class that handles the WebSocket connection to the server. * The class that handles the WebSocket connection to the server.
* This is the only relevant class for the time being. But in the future, there may be more classes to handle different protocols of the API. * This is the only relevant class for the time being. But in the future, there may be more classes to handle different protocols of the API.
*/ */
export class ClientWebSocketManager { export class ClientWebSocketManager {
readonly url: string url: string
timeout: number timeout: number
connecting = false connecting = false
@@ -33,6 +33,21 @@ export class ClientWebSocketManager {
this.timeout = options.timeout ?? 10000 this.timeout = options.timeout ?? 10000
} }
/**
* Sets the URL to connect to
*
* **Requires a reconnect to take effect**
*/
async setOptions({ url, timeout }: Partial<ClientWebSocketManagerOptions>, autoReconnect = true) {
if (url) this.url = url
this.timeout = timeout ?? this.timeout
if (autoReconnect) {
this.disconnect(true)
await this.connect()
}
}
/** /**
* Connects to the WebSocket API * Connects to the WebSocket API
* @returns A promise that resolves when the client is ready * @returns A promise that resolves when the client is ready
@@ -77,7 +92,11 @@ export class ClientWebSocketManager {
} catch (e) { } catch (e) {
rj(e) rj(e)
} }
}).finally(() => { })
.then(() => {
this.#socket.on('close', (code, reason) => this._handleDisconnect(code, reason.toString()))
})
.finally(() => {
this.connecting = false this.connecting = false
}) })
} }
@@ -123,7 +142,7 @@ export class ClientWebSocketManager {
this.currentSequence++ this.currentSequence++
this.#socket.send(serializePacket(packet), err => { this.#socket.send(serializePacket(packet), err => {
throw err if (err) throw err
}) })
} }
@@ -188,7 +207,7 @@ export class ClientWebSocketManager {
protected _toBuffer(data: RawData) { protected _toBuffer(data: RawData) {
if (data instanceof Buffer) return data if (data instanceof Buffer) return data
if (data instanceof ArrayBuffer) return Buffer.from(data) if (data instanceof ArrayBuffer) return Buffer.from(data)
return Buffer.concat(data) return Buffer.concat(data as Uint8Array[])
} }
} }

View File

@@ -1,3 +1,3 @@
export { default as Client } from './Client'
export * from './Client' export * from './Client'
export { default as Client } from './Client'
export * from './ClientWebSocket' export * from './ClientWebSocket'

View File

@@ -16,7 +16,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/revanced/revanced-helper.git", "url": "git+https://github.com/revanced/revanced-bots.git",
"directory": "packages/shared" "directory": "packages/shared"
}, },
"author": "Palm <contact@palmdevs.me> (https://palmdevs.me)", "author": "Palm <contact@palmdevs.me> (https://palmdevs.me)",
@@ -26,13 +26,13 @@
], ],
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"bugs": { "bugs": {
"url": "https://github.com/revanced/revanced-helper/issues" "url": "https://github.com/revanced/revanced-bots/issues"
}, },
"homepage": "https://github.com/revanced/revanced-helper#readme", "homepage": "https://github.com/revanced/revanced-bots#readme",
"dependencies": { "dependencies": {
"bson": "^6.7.0", "bson": "^6.10.4",
"chalk": "^5.3.0", "chalk": "^5.6.2",
"tracer": "^1.3.0", "tracer": "^1.3.0",
"valibot": "^0.30.0" "valibot": "^1.1.0"
} }
} }

View File

@@ -1,16 +1,16 @@
import { import {
url,
type AnySchema,
type NullSchema,
type ObjectSchema,
type Output,
array, array,
boolean,
custom,
enum_, enum_,
type InferOutput,
null_, null_,
object, object,
optional,
parse, parse,
special, pipe,
string, string,
url,
// merge // merge
} from 'valibot' } from 'valibot'
import DisconnectReason from '../constants/DisconnectReason' import DisconnectReason from '../constants/DisconnectReason'
@@ -19,15 +19,14 @@ import { ClientOperation, Operation, ServerOperation } from '../constants/Operat
/** /**
* Schema to validate packets * Schema to validate packets
*/ */
export const PacketSchema = special<Packet>(input => { export const PacketSchema = custom<Packet>(input => {
if ( if (
typeof input === 'object' && typeof input === 'object' &&
input && input &&
'op' in input && 'op' in input &&
typeof input.op === 'number' && typeof input.op === 'number' &&
input.op in Operation && input.op in Operation &&
'd' in input && 'd' in input
typeof input.d === 'object'
) { ) {
if (input.op in ServerOperation && !('s' in input && typeof input.s === 'number')) return false if (input.op in ServerOperation && !('s' in input && typeof input.s === 'number')) return false
@@ -50,7 +49,7 @@ export const PacketDataSchemas = {
labels: array( labels: array(
object({ object({
name: string(), name: string(),
confidence: special<number>(input => typeof input === 'number' && input >= 0 && input <= 1), confidence: custom<number>(input => typeof input === 'number' && input >= 0 && input <= 1),
}), }),
), ),
}), }),
@@ -62,24 +61,20 @@ export const PacketDataSchemas = {
[ServerOperation.Disconnect]: object({ [ServerOperation.Disconnect]: object({
reason: enum_(DisconnectReason), reason: enum_(DisconnectReason),
}), }),
[ServerOperation.TrainedMessage]: null_(), [ServerOperation.TrainedMessage]: boolean(),
[ServerOperation.TrainMessageFailed]: null_(), [ServerOperation.TrainMessageFailed]: null_(),
[ClientOperation.ParseText]: object({ [ClientOperation.ParseText]: object({
text: string(), text: string(),
}), }),
[ClientOperation.ParseImage]: object({ [ClientOperation.ParseImage]: object({
image_url: string([url()]), image_url: pipe(string(), url()),
}), }),
[ClientOperation.TrainMessage]: object({ [ClientOperation.TrainMessage]: object({
text: string(), text: string(),
label: string(), label: optional(string()),
}), }),
} as const satisfies Record< } as const
Operation,
// biome-ignore lint/suspicious/noExplicitAny: This is a schema, it's not possible to type it
ObjectSchema<any> | AnySchema | NullSchema
>
export type Packet<TOp extends Operation = Operation> = TOp extends ServerOperation export type Packet<TOp extends Operation = Operation> = TOp extends ServerOperation
? PacketWithSequenceNumber<TOp> ? PacketWithSequenceNumber<TOp>
@@ -87,6 +82,6 @@ export type Packet<TOp extends Operation = Operation> = TOp extends ServerOperat
type PacketWithSequenceNumber<TOp extends Operation> = { type PacketWithSequenceNumber<TOp extends Operation> = {
op: TOp op: TOp
d: Output<(typeof PacketDataSchemas)[TOp]> d: InferOutput<(typeof PacketDataSchemas)[TOp]>
s: number s: number
} }

View File

@@ -1,5 +1,5 @@
import { Chalk, supportsColor, supportsColorStderr } from 'chalk' import { Chalk, supportsColor, supportsColorStderr } from 'chalk'
import { type Tracer, colorConsole, console as uncoloredConsole } from 'tracer' import { colorConsole, type Tracer, console as uncoloredConsole } from 'tracer'
const chalk = new Chalk() const chalk = new Chalk()
const DefaultConfig = { const DefaultConfig = {

View File

@@ -1,7 +1,7 @@
import * as BSON from 'bson' import * as BSON from 'bson'
import { parse } from 'valibot' import { parse } from 'valibot'
import type { Operation } from '../constants'
import { type Packet, PacketSchema } from '../schemas' import { type Packet, PacketSchema } from '../schemas'
import type { Operation } from '../constants'
/** /**
* Compresses a packet into a buffer * Compresses a packet into a buffer
@@ -18,6 +18,6 @@ export function serializePacket<TOp extends Operation>(packet: Packet<TOp>) {
* @returns A packet * @returns A packet
*/ */
export function deserializePacket(buffer: Buffer) { export function deserializePacket(buffer: Buffer) {
const data = BSON.deserialize(buffer) const data = BSON.deserialize(buffer as Uint8Array)
return parse(PacketSchema, data) as Packet return parse(PacketSchema, data) as Packet
} }

View File

@@ -1,12 +1,3 @@
diff --git a/node_modules/@semantic-release/npm/.bun-tag-3853154e196b7721 b/.bun-tag-3853154e196b7721
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/node_modules/@semantic-release/npm/.bun-tag-550461f23a8ec245 b/.bun-tag-550461f23a8ec245
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/node_modules/@semantic-release/npm/.bun-tag-c9c8130945517add b/.bun-tag-c9c8130945517add
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/lib/prepare.js b/lib/prepare.js diff --git a/lib/prepare.js b/lib/prepare.js
index 3e76bec44cf595a1b4141728336bed904d4d518d..4b25ca64879bbee2a600f2b23b738c86136ad9c6 100644 index 3e76bec44cf595a1b4141728336bed904d4d518d..4b25ca64879bbee2a600f2b23b738c86136ad9c6 100644
--- a/lib/prepare.js --- a/lib/prepare.js

View File

@@ -1,13 +0,0 @@
diff --git a/src/lib.js b/src/lib.js
index de45d7dbe82975b09eff3742d0718accae2107fc..0575daa03dfabdd5c96928458ff4270cb8f7188a 100644
--- a/src/lib.js
+++ b/src/lib.js
@@ -42,7 +42,7 @@ function isMusl() {
}
function getBinding(name) {
- const path = join(__dirname, '..', `decancer.${name}.node`)
+ const path = join(import.meta.dir, '..', `decancer.${name}.node`)
return require(existsSync(path) ? path : `@vierofernando/decancer-${name}`)
}

View File

@@ -1,21 +0,0 @@
diff --git a/bin.cjs b/bin.cjs
index 142ed9c20f28dc1080bebfb52325fa308c6cb771..9d3bea0787f6c05df11567c6821bc85743286340 100644
--- a/bin.cjs
+++ b/bin.cjs
@@ -22053,7 +22053,7 @@ var init_sqliteImports = __esm({
const { unregister } = await safeRegister();
for (let i2 = 0; i2 < imports.length; i2++) {
const it = imports[i2];
- const i0 = require(`${it}`);
+ const i0 = await import(`${it}`);
const prepared = prepareFromExports3(i0);
tables.push(...prepared.tables);
}
@@ -129572,6 +129572,7 @@ var generateCommand = new Command("generate").option("--dialect <dialect>", "Dat
} else {
assertUnreachable(dialect7);
}
+ process.exit(0);
});
var migrateCommand = new Command("migrate").option(
"--config <config>",

View File

@@ -11,9 +11,7 @@ const Options = {
prerelease: true, prerelease: true,
}, },
], ],
plugins: plugins: [
process.env['RELEASE_WORKFLOW_STEP'] !== 'publish'
? [
[ [
'@semantic-release/commit-analyzer', '@semantic-release/commit-analyzer',
{ {
@@ -58,8 +56,7 @@ const Options = {
clearWorkspace: true, clearWorkspace: true,
}, },
], ],
] ],
: [],
} }
/** /**
@@ -70,6 +67,6 @@ export default function defineSubprojectReleaseConfig(subprojectOptions) {
return { return {
...Options, ...Options,
...subprojectOptions, ...subprojectOptions,
plugins: [...(subprojectOptions.plugins || []), ...(Options.plugins || [])], plugins: [...(Options.plugins || []), ...(subprojectOptions.plugins || [])],
} }
} }