Compare commits

...

136 Commits

Author SHA1 Message Date
github-actions[bot]
6a2a5b4565 chore(main): release 2.6.3 (#309)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-17 16:21:26 -05:00
Jacob Nguyen
5fdc1eda7f fix: autocomplete nested option and merge main 2023-06-17 16:16:47 -05:00
renovate[bot]
e00d1df32e chore(deps): update dependency @types/node to v18.16.8 (#299)
chore(deps): update dependency @types/node to v18.16.7

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-06-16 00:13:10 -05:00
renovate[bot]
31c221bd5e chore(deps): update actions/checkout digest to c85c95e (#306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 00:12:42 -05:00
renovate[bot]
0aba4a6606 chore(deps): update dependency prettier to v2.8.8 (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-05-19 20:50:36 -05:00
renovate[bot]
e9c7661804 chore(deps): update dependency discord.js to v14.11.0 (#297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-05-12 21:15:49 -05:00
renovate[bot]
446417bfb9 chore(deps): update dependency @types/node to v18.16.7 (#291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-05-10 14:47:30 -05:00
renovate[bot]
6b58ef731b chore(deps): update yarn to v3.5.1 (#296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 13:45:24 -05:00
renovate[bot]
b62129bf04 chore(deps): update dependency rxjs to v7.8.1 (#292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-10 13:43:21 -05:00
renovate[bot]
3d121ff01c chore(deps): lock file maintenance 2023-04-25 21:24:03 +00:00
renovate[bot]
d201087d4f chore(deps): update dependency eslint to v8.39.0 (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-25 11:51:12 -05:00
renovate[bot]
1af4a2bed4 chore(deps): update dependency @typescript-eslint/parser to v5.59.1 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-25 11:50:50 -05:00
renovate[bot]
edcaed083e chore(deps): update dependency @types/node to v18.16.0 (#285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-23 23:49:08 -05:00
renovate[bot]
a4fe2c50df chore(deps): update dependency esbuild to ^0.17.0 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-23 23:48:12 -05:00
renovate[bot]
9ea991626d chore(deps): update actions/checkout digest to 8e5e7e5 (#278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-23 23:47:15 -05:00
Jacob Nguyen
64f20f1cf5 Delete non detected license 2023-04-21 16:35:15 -05:00
xxDeveloper
41cc72fe63 chore: README patch (#282)
* docs: Update README.md

It’s better IG

* chore: Update README.md

* chore: Update README.md

* chore; Update README.md

* chore: Update README.md

* chore: Update README.md

* chore: Update README.md

* chore: Update README.md

* chore: Update README.md

* chore: Final updates to README.md
2023-04-18 11:08:20 -05:00
github-actions[bot]
d983f95906 chore(main): release 2.6.2 (#281)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 12:42:59 -05:00
Jacob Nguyen
c1f690633c chore: release 2.6.2
Release-As: 2.6.2
2023-04-15 12:40:17 -05:00
Jacob Nguyen
8544d301ef bump version 2023-04-15 12:19:12 -05:00
Jacob Nguyen
52bcba9cfc docs: add deprecation warning 2023-04-15 12:16:35 -05:00
xxDeveloper
21febd2c90 chore: Update SECURITY.md (#276)
Semantic security file

Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 22:46:02 -05:00
xxDeveloper
11daebb30a chore: Update LICENSE (#275)
We're in 2023
We're sern, not Sern

Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 22:45:45 -05:00
github-actions[bot]
b817f98c10 style: pretty please (#277)
Co-authored-by: EvolutionX-10 <EvolutionX-10@users.noreply.github.com>
2023-04-11 22:45:30 -05:00
Evo
563f583318 chore: switch to yarn (#273)
* chore: switch to yarn

* chore: pointless limitation

permalink: http://whatthecommit.com/468a491808723d12de48b079d9092b44

* chore: i can't believe it took so long to fix this.

permalink: http://whatthecommit.com/b298fe6d3375ab953abfdb0f1f737826
2023-04-11 12:45:16 -05:00
EvolutionX
e4c7bfe686 chore: ok work pls 2023-04-11 22:32:31 +05:30
EvolutionX
69fa4908c3 chore: refresh lockfile 2023-04-11 22:09:32 +05:30
renovate[bot]
4fa28d605f chore(deps): lock file maintenance (#245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:32:46 -05:00
Jacob Nguyen
079b554f8b Update continuous-integration.yml 2023-04-11 10:31:15 -05:00
Jacob Nguyen
dec56335b9 Update codeql-analysis.yml 2023-04-11 10:30:41 -05:00
Jacob Nguyen
50be972d4f Update continuous-integration.yml 2023-04-11 10:29:06 -05:00
Jacob Nguyen
507d183970 Update codeql-analysis.yml 2023-04-11 10:28:29 -05:00
renovate[bot]
90edd4f91e chore(deps): update dependency eslint to v8.38.0 (#180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:24:32 -05:00
renovate[bot]
5f11142599 chore(deps): update dependency @typescript-eslint/parser to v5.58.0 (#250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 10:22:27 -05:00
renovate[bot]
7a635f9978 chore(deps): update actions/checkout digest to 8f4b7f8 (#261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 10:21:59 -05:00
renovate[bot]
a17aeac558 chore(deps): update pnpm to v7.32.0 (#262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 10:21:39 -05:00
renovate[bot]
af6ebed348 chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.58.0 (#249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:18:46 -05:00
renovate[bot]
2f96b7634d chore(deps): update dependency prettier to v2.8.7 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:18:17 -05:00
EvolutionX
97741faa69 chore: refresh lockfile 2023-04-11 16:02:13 +05:30
Jacob Nguyen
94070d99e8 refactor/decoupling (#265)
* fix npm script for workflows

* filter lazy modules

* lift inline function for readability

* perf: use one instance of operator instead of creating instances

* chore: move fmt closer to call site

* refactor: inline function lifting and readability

* add import payload type

* refactor: remove redundant pipe for single function operators

* refactor: clearer naming for resultResolver

* refactor: no unused variable warning for updateAlive

* style: pretty

* refactor: remove redundant getter

* style: pretty

* fix: typescript needs explicit definition for defineAllFields

* add LazyPaths map

* chore: update tsup and typescript

* chore: revert lazy module work and work on decoupling core

* fix npm script for workflows

* chore: fix typings

* refactor: inline function `defineAllFields`

* docs: add @since annotation

* style: prettier

* docs: add since annotations

* fix: typings

* chore: update dependencies

* chore: remove unused import

* style: pretty

* merge on home pc

* refactor: use dependencies less

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-04-10 22:12:26 -05:00
Jacob Nguyen
473be775f0 Update README.md 2023-03-29 15:12:26 -05:00
Neo
36af102251 docs: removed ALMA (#264)
Not working on it anymore, also not running it.
2023-03-29 12:55:16 -05:00
github-actions[bot]
cee740ea3f style: pretty please (#260)
Co-authored-by: renovate[bot] <renovate[bot]@users.noreply.github.com>
2023-03-17 17:01:20 -05:00
github-actions[bot]
2fd7697300 chore(main): release 2.6.1 (#258)
* chore(main): release 2.6.1

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-17 16:37:20 -05:00
Jacob Nguyen
f9609ce6cd chore: release 2.6.1
Release-As: 2.6.1
2023-03-17 16:33:46 -05:00
Jacob Nguyen
a3064aa915 chore: audit & remove tspattern (#256)
* chore: move import

* build: remove ts pattern

* fix: forgot to convert to switch

* fix workflow

* refactor: lift function out of readyHandler

* refactor: clean up errTap signature

* fix: sern emitter emitting wrong payload

* wa

* style: space

* chore: remove old errTap

* chore:bump discord.js

* chore: eslint format
2023-03-17 16:30:27 -05:00
github-actions[bot]
0a53a48521 style: pretty please (#255)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-03-15 21:16:58 -05:00
Jacob Nguyen
05037b5315 build: prettier ignore 2023-03-15 21:15:33 -05:00
Jacob Nguyen
06a3e69210 feat: prettier ignore 2023-03-15 21:13:42 -05:00
Jacob Nguyen
74c4b77d4b build: refactor/building (#252)
* refactor: conditional compilation of loading esm/cjs modules

* refactor: move file loading file

* refactor: add conditional compilation for building modules

* refactor: add conditional compilation for building modules

* perf: decrease build times

* test

* revert: typo and clean code

* build: smaller build

* chore:cleanscripts

* chore:refactor readme

* build:automerge lockfile

* chore: remove build and upgrade readme

* fix: dropdown

* chore: fix

* chore: more docs

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-03-15 21:08:27 -05:00
github-actions[bot]
d381ff568e chore(main): release 2.6.0 (#248)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-09 16:18:06 -06:00
Jacob Nguyen
6db5c71506 chore: update workflow 2023-03-09 16:15:29 -06:00
github-actions[bot]
507c9e7939 style: pretty please (#247)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-03-09 16:11:29 -06:00
Jacob Nguyen
09610d0501 refactor: eventhandlers (#246)
* refactor:import

* feat: save progress

* feat:progress

* refactor: event handlers

* fix: merge all subscriptions into event handler

* fix: remove duplicate minify key

* fix: leftover this

* docs: jsdoc

* chore: clean pnpm

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-03-09 16:09:35 -06:00
github-actions[bot]
0862bf92d0 style: pretty please (#244)
Co-authored-by: renovate[bot] <renovate[bot]@users.noreply.github.com>
2023-03-03 09:22:02 -06:00
renovate[bot]
62162f6b8c chore(deps): lock file maintenance (#240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-02 19:03:02 -06:00
renovate[bot]
eb501db09a chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.54.0 (#243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 19:01:18 -06:00
renovate[bot]
964848a4e2 chore(deps): update dependency @typescript-eslint/parser to v5.54.0 (#194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-02 18:48:56 -06:00
renovate[bot]
78dead1b49 chore(deps): update pnpm to v7.28.0 (#239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 18:48:32 -06:00
renovate[bot]
39e6d6d2f9 chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.52.0 (#238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-20 00:30:10 -06:00
renovate[bot]
5aff57ed6d chore(deps): update dependency typescript to v4.9.5 (#208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-20 00:27:36 -06:00
github-actions[bot]
4095471346 style: pretty please (#237)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-02-17 15:47:33 -06:00
Jacob Nguyen
f7b9c52df1 Merge remote-tracking branch 'origin/main' 2023-02-17 15:40:54 -06:00
Jacob Nguyen
d20d01524b feat: adding pure annotation for better tree shaking 2023-02-17 15:40:32 -06:00
renovate[bot]
5684c060bc chore(deps): lock file maintenance (#225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-02-17 13:13:15 -06:00
github-actions[bot]
ca8b31f280 chore(main): release 2.5.3 (#235)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-16 11:11:32 -06:00
jacoobes
ce9a0831a6 chore: release 2.5.3
Release-As: 2.5.3
2023-02-16 11:09:46 -06:00
jacoobes
3a32968a17 chore: bump version 2023-02-16 10:57:38 -06:00
github-actions[bot]
e549f8bc3e chore(main): release 2.5.2 (#234)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-16 10:52:14 -06:00
jacoobes
3ab73459ad bump version 2023-02-16 10:49:02 -06:00
jacoobes
16af2e996d chore: minifiy build output 2023-02-16 10:44:38 -06:00
Jacob Nguyen
4f9a842b0e update ci 2023-02-15 21:31:18 -06:00
Jacob Nguyen
5dfd1a1fc1 Update README.md 2023-02-15 21:28:48 -06:00
Jacob Nguyen
c45a10c950 Update README.md 2023-02-15 21:28:25 -06:00
Jacob Nguyen
b45ba34f3c Update README.md 2023-02-15 18:22:23 -06:00
Jacob Nguyen
facee79c90 revert: version 2023-02-14 17:54:31 -06:00
Jacob Nguyen
306eee071d chore: bump dependencies 2023-02-14 17:52:23 -06:00
Jacob Nguyen
00d55208a0 Merge remote-tracking branch 'origin/main' 2023-02-14 17:42:26 -06:00
Jacob Nguyen
49fad801a5 chore: bump dependencies 2023-02-14 17:41:54 -06:00
Neo
c9f44ce72b docs: changed name of Benzo-Fury's Bot (#226) 2023-02-13 11:46:28 -06:00
Jacob Nguyen
a9a2528faf Update README.md 2023-02-12 17:07:51 -06:00
github-actions[bot]
529edb7da5 style: pretty please (#224)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-02-12 12:59:34 -06:00
github-actions[bot]
f236dc05e2 chore(main): release 2.5.1 (#222)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-12 12:58:00 -06:00
Jacob Nguyen
c78936a225 chore: release 2.5.1
Release-As: 2.5.1
2023-02-12 12:54:18 -06:00
Jacob Nguyen
1860b898f3 fix: autocomplete 2023-02-12 12:51:49 -06:00
renovate[bot]
fd10772a9b chore(deps): lock file maintenance (#213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-12 11:47:37 -06:00
renovate[bot]
cd36bd8a47 chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.51.0 (#187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-12 11:33:00 -06:00
renovate[bot]
dda7f41231 chore(deps): update dependency prettier to v2.8.4 (#215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-02-12 11:29:38 -06:00
renovate[bot]
371a57194c chore(deps): update pnpm to v7.27.0 (#216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-12 11:28:54 -06:00
Neo
bfcc160a39 docs: change protectors WIP mark to its name (#214)
Idk I was bored and saw this was different to mine. Change mine instead or just close this pr, idm.
2023-02-07 20:16:23 -06:00
Neo
86fa531eb6 docs: Adding the WIP to my bot (#212)
feat: Adding the WIP to my bot
2023-02-04 10:37:03 -06:00
Gary
58052e94cb docs: add my bot to the ReadMe (#211) 2023-02-04 01:38:07 -06:00
Neo
96f4281121 feat: Adding my bot to readme (#210) 2023-02-03 23:55:26 -06:00
Jacob Nguyen
f9ae7c003b docs: clarify example 2023-02-03 23:46:19 -06:00
Jacob Nguyen
ec211d5a8d docs: do some updating on readme 2023-02-03 23:14:57 -06:00
github-actions[bot]
3faf83bbf7 style: pretty please (#209)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-02-03 23:04:13 -06:00
renovate[bot]
8eed099503 chore(deps): lock file maintenance (#183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 23:00:46 -06:00
renovate[bot]
e2874be4e7 chore(deps): update pnpm to v7.26.3 (#203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 22:56:17 -06:00
github-actions[bot]
1d6751a9cd style: pretty please (#207)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-01-30 12:35:37 -06:00
github-actions[bot]
f6afafa352 chore(main): release 2.5.0 (#205)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-30 12:30:28 -06:00
Jacob Nguyen
b4b195dc95 chore: release 2.5.0
Release-As: 2.5.0
2023-01-30 12:27:18 -06:00
Jacob Nguyen
33f14467ec feat!: simpler plugins (#193)
* feat: experimental plugin changes

* more refactors and name changes

* feat: update name usage and update dispatchers.ts

* fix:naming

* feat: slightly safer typings than any[]

* fix: forgot to destructure arguments

* feat: add special function

* fix: typings

* feat: SUPER SIMPLIFY!!!

* refactor: move promisifiedPlugins closer to call site

* refactor: typings

* refactor: typings

* refactor: consolidate resolving initplugins into one function

* refactor: better types

* revert: remove unneeded function

* revert: remove unneeded function

* feat: dispatch work, simplify

* feat: move some observableHandling function to operators for clarity

* feat: simplify and document

* feat: simplifying sern and docs

* fix: typings

* docs: clarity of function name

* docs: add documentation for executeModule

* feat: contextArgs overloads

* docs: found out why

* fix: typings

* feat: shorten operators signature

* refactor: switch to correct convention

* refactor: take(1) -> first()

* refactor: revert

* refactor: safer typings (less any) and more accurate typings

* style: prettier and short type aliases

* fix: typings

* fix: typings

* docs: add deprecations

* refactor: organization and moving stuff

* pretty: prettey

* docs: describe file

* chore: update dependencies and version

* docs: fix link for docasaurus

* refactor: using a more appropriate operator function for closing an observable on crash

* fix!: changing single and many

* refactor: typings and simplifying composeRoot

* fix: re-add logger into handleError

* docs: comment

* docs: new section

* feat: help mitigate breaking changes

* feat: help mitigate breaking changes

* feat: help mitigate breaking changes and function overloads

* feat: deprecate instead of remove

* feat: partial remove and deprecate old symbols

* revert: trying to accommodate old plugins is too difficult

* docs: add many as deprecated

* docs: update

* feat: partial backwards compatability

* refactor: renaming, docs, and exports more clean

* refactor: context got a lot simpler

* refactor: imports

* docs: explain methods
2023-01-26 12:06:04 -06:00
renovate[bot]
cb95105c1c chore(deps): update dependency prettier to v2.8.3 (#196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-17 12:54:57 -06:00
renovate[bot]
845b82feef chore(deps): update actions/checkout digest to ac59398 (#184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-14 22:38:44 -06:00
renovate[bot]
833a323f3c chore(deps): update actions/setup-node digest to 64ed1c7 (#185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-14 22:36:47 -06:00
renovate[bot]
42e5f20425 chore(deps): update pnpm to v7.25.0 (#195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-14 22:36:09 -06:00
renovate[bot]
2b25e6bfbb chore(deps): update pnpm to v7.22.0 (#179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-07 19:43:59 -06:00
renovate[bot]
1a27341092 chore(deps): update dependency prettier to v2.8.2 (#189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-07 16:30:48 -06:00
renovate[bot]
4680e451bb chore(deps): update dependency @typescript-eslint/parser to v5.48.0 (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-07 16:09:49 -06:00
renovate[bot]
917d8b0d1b chore(deps): update dependency iti to ^0.6.0 (#178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-31 14:01:56 -06:00
github-actions[bot]
b8492ee45d chore(main): release 2.1.1 (#181)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-12-31 13:50:44 -06:00
Jacob Nguyen
08aac1d67a chore: bump version 2022-12-31 13:47:50 -06:00
Jacob Nguyen
a13df6fb42 fix: modals remapping 2022-12-31 13:41:20 -06:00
Jacob Nguyen
7089f5c0dc chore: fix prettier wkflw holy shit 2022-12-30 11:30:38 -06:00
Jacob Nguyen
559c1a7a7b chore: fix prettier wkflw holy shit 2022-12-30 11:28:50 -06:00
Jacob Nguyen
ac27d168e2 chore: fix prettier wkflw holy shit 2022-12-30 11:27:00 -06:00
Jacob Nguyen
d1e6ec4589 chore: fix prettier wkflw holy shit 2022-12-30 11:25:42 -06:00
Jacob Nguyen
ff379d03be chore: fix prettier wkflw holy shit 2022-12-30 11:15:40 -06:00
Jacob Nguyen
1e4e933db2 chore: fix prettier wkflw holy shit 2022-12-30 11:13:38 -06:00
Jacob Nguyen
ce960f4c8d chore: fix prettier wkflw holy shit 2022-12-30 11:11:38 -06:00
Jacob Nguyen
1130456045 chore: fix prettier wkflw holy shit 2022-12-30 11:10:48 -06:00
Jacob Nguyen
1617d2dcc3 chore: fix prettier wkflw 2022-12-30 11:09:14 -06:00
Jacob Nguyen
ddacbd6e38 chore: fix prettier wkflw 2022-12-30 11:06:26 -06:00
Jacob Nguyen
d69819e9fc chore: fix prettier wkflw 2022-12-30 11:03:30 -06:00
github-actions[bot]
49e4ba623f chore(main): release 2.1.0 (#176)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-30 10:58:57 -06:00
github-actions[bot]
1b6c413fc2 style: pretty please (#175)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2022-12-30 10:56:23 -06:00
Jacob Nguyen
e986535935 fix: multi parameter events 2022-12-30 10:45:02 -06:00
Jacob Nguyen
c30aac476c feat: grammar 2022-12-28 15:37:35 -06:00
github-actions[bot]
f9622d3788 style: pretty please (#173)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2022-12-28 15:34:22 -06:00
renovate[bot]
f286a24686 chore(deps): update dependency prettier to v2.8.1 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:20:45 -06:00
renovate[bot]
166934d749 chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.47.1 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:18:51 -06:00
renovate[bot]
01d79177e8 chore(deps): lock file maintenance (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:15:12 -06:00
renovate[bot]
50dac7fb46 chore(deps): update dependency @typescript-eslint/parser to v5.47.1 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:10:17 -06:00
renovate[bot]
714d23d401 chore(deps): update actions/checkout digest to 755da8c (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:05:10 -06:00
renovate[bot]
565c4fc35a chore(deps): update dependency eslint to v8.30.0 (#152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-28 14:56:44 -06:00
github-actions[bot]
8d18c4b182 style: pretty please (#162)
* style: pretty please

* feat: no package.lock.json anymore

Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 14:40:19 -06:00
66 changed files with 5618 additions and 3252 deletions

View File

@@ -7,7 +7,7 @@
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": ["error", "always"],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/ban-types" : 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": "off"
}
}

36
.github/SECURITY.md vendored
View File

@@ -6,13 +6,43 @@ Project is currently under heavy development but you can try out our [npm packag
| Version | Supported |
| ------- | ------------------ |
| 0.1.0 @ dev | :white_check_mark: |
| 2.6.1 | YES |
| 2.6.0 | YES |
| 2.5.3 | YES |
| 2.5.2 | YES |
| 2.5.1 | YES |
| 2.5.0 | YES |
| 2.1.1 | NO |
| 2.1.0 | NO |
| 2.0.0 | NO |
| 1.2.1 | NO |
| 1.2.0 | NO |
| 1.1.0 | NO |
1.0.1 | NO
1.0.0 | NO
1.1.9 @ beta | NO
1.1.8 @ beta | NO
1.1.7 @ beta | NO
1.1.6 @ beta | NO
1.1.5 @ beta | NO
1.1.4 @ beta | NO
1.1.3 @ beta | NO
1.1.2 @ beta | NO
1.1.1 @ beta | NO
1.1.0 @ beta | NO
1.0.4 @ beta | NO
1.0.3 @ beta | NO
1.0.2 @ beta | NO
1.0.1 @ beta | NO
1.0.0 @ beta | NO
0.0.1 @ dev | NO (TRY IT)
* Dev versions might include bugs, use it with your own risk.
* Dev versions might include bugs and not supported use stable versions.
## Reporting a Vulnerability
You can report a vulnerability by opening an issue on the [project's GitHub](https://github.com/SernHandler/Sern/issues) repository.
You can report a vulnerability by opening an issue on the [project's GitHub](https://github.com/sern-handler/handler/issues) repository.
Please provide as much information as possible when reporting a vulnerability. We are looking information for, the affected version, and the steps to reproduce the vulnerability.

View File

@@ -3,8 +3,10 @@ name: "CodeQL"
on:
push:
branches: [ main ]
paths: ["src/**/*"]
pull_request:
branches: [ main ]
paths: ["src/**/*"]
schedule:
- cron: '37 20 * * 4'

View File

@@ -3,11 +3,14 @@ name: Continuous Integration
on:
# Trigger the workflow on push or pull request or custom
push:
branches:
main
branches: [main]
paths:
- '*.ts'
pull_request_target:
branches:
main
paths:
- '*ts'
workflow_dispatch:
jobs:
@@ -17,19 +20,22 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
- name: Set up Node.js
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
- name: Install pnpm
run: npm i -g yarn
# Prettier must be in `package.json`
- name: Install Node.js dependencies
run: npm i -g prettier && npm i
run: yarn --immutable
- name: Run Prettier
run: prettier --write .
run: yarn pretty
- name: Create Pull Request
id: cpr

View File

@@ -1,30 +1,18 @@
name: NPM / Publish
on:
release:
types: [created]
workflow_dispatch:
jobs:
build:
test-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
- run: npm test
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
node-version: 17
- run: yarn --immutable
- run: yarn build:prod
- uses: JS-DevTools/npm-publish@v1
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
token: ${{ secrets.NPM_TOKEN }}
access: "public"

4
.gitignore vendored
View File

@@ -87,3 +87,7 @@ dist
# IntelliJ IDEA Config file
.idea/
# Yarn files
.yarn/install-state.gz
.yarn/build-state.yml

View File

@@ -52,6 +52,7 @@ typings/
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
@@ -108,6 +109,8 @@ tsup.config.js
tsconfig-base.json
tsconfig.cjs.json
tsconfig-cjs.json
tsconfig.esm.json
tsconfig-esm.json
renovate.json

873
.yarn/releases/yarn-3.5.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

5
.yarnrc.yml Normal file
View File

@@ -0,0 +1,5 @@
enableGlobalCache: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.5.1.cjs

View File

@@ -1,5 +1,100 @@
# Changelog
## [2.6.3](https://github.com/sern-handler/handler/compare/v2.6.2...v2.6.3) (2023-06-17)
### Bug Fixes
* autocomplete nested option and merge main ([5fdc1ed](https://github.com/sern-handler/handler/commit/5fdc1eda7f4fcc1f94af7eca661660c0edeb3251))
## [2.6.2](https://github.com/sern-handler/handler/compare/v2.6.1...v2.6.2) (2023-04-15)
### Miscellaneous Chores
* release 2.6.2 ([c1f6906](https://github.com/sern-handler/handler/commit/c1f690633c55ba41db1e035b7c16f9e19c70b385))
## [2.6.1](https://github.com/sern-handler/handler/compare/v2.6.0...v2.6.1) (2023-03-17)
### Miscellaneous Chores
* release 2.6.1 ([f9609ce](https://github.com/sern-handler/handler/commit/f9609ce6cd777fa0eb595d8c48d57905bbce5966))
## [2.6.0](https://github.com/sern-handler/handler/compare/v2.5.3...v2.6.0) (2023-03-09)
### Features
* adding pure annotation for better tree shaking ([d20d015](https://github.com/sern-handler/handler/commit/d20d01524b872549da501e21feec147ab204f397))
## [2.5.3](https://github.com/sern-handler/handler/compare/v2.5.2...v2.5.3) (2023-02-16)
### Miscellaneous Chores
* release 2.5.3 ([ce9a083](https://github.com/sern-handler/handler/commit/ce9a0831a6e47dd38648f34653f0bd89b1d2e48e))
## [2.5.2](https://github.com/sern-handler/handler/compare/v2.5.1...v2.5.2) (2023-02-16)
### Reverts
* version ([facee79](https://github.com/sern-handler/handler/commit/facee79c904ad663d3c57ce56fb825419fcc12f9))
## [2.5.1](https://github.com/sern-handler/handler/compare/v2.5.0...v2.5.1) (2023-02-12)
### Features
* Adding my bot to readme ([#210](https://github.com/sern-handler/handler/issues/210)) ([96f4281](https://github.com/sern-handler/handler/commit/96f42811218e4898a47e75a8138ccd452ae2c5c2))
* Adding the WIP to my bot ([86fa531](https://github.com/sern-handler/handler/commit/86fa531eb620d2ac649bad6decb29d5c55a25445))
### Bug Fixes
* autocomplete ([1860b89](https://github.com/sern-handler/handler/commit/1860b898f3a231840e2a8b781e007ef9d6f587a4))
### Miscellaneous Chores
* release 2.5.1 ([c78936a](https://github.com/sern-handler/handler/commit/c78936a22574da4af71826f5b5f72f354a4eb06a))
## [2.5.0](https://github.com/sern-handler/handler/compare/v2.1.1...v2.5.0) (2023-01-30)
### ⚠ BREAKING CHANGES
* simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193))
### Features
* simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193)) ([33f1446](https://github.com/sern-handler/handler/commit/33f14467ec413e003a82503c8a77cb42a6194281))
### Miscellaneous Chores
* release 2.5.0 ([b4b195d](https://github.com/sern-handler/handler/commit/b4b195dc9586736760d0b78caa8589f3d6131f8a))
## [2.1.1](https://github.com/sern-handler/handler/compare/v2.1.0...v2.1.1) (2022-12-31)
### Bug Fixes
* modals remapping ([a13df6f](https://github.com/sern-handler/handler/commit/a13df6fb424d256476284da49024dbe56e82baab))
## [2.1.0](https://github.com/sern-handler/handler/compare/v2.0.0...v2.1.0) (2022-12-30)
### Features
* grammar ([c30aac4](https://github.com/sern-handler/handler/commit/c30aac476cdc2094de34f9f67b5805204cc5e4dd))
### Bug Fixes
* multi parameter events ([e986535](https://github.com/sern-handler/handler/commit/e98653593566ef4635493e0c997bd107a7a3a2a2))
## [2.0.0](https://github.com/sern-handler/handler/compare/v1.2.1...v2.0.0) (2022-12-28)

View File

@@ -1,6 +1,6 @@
MIT License
**Copyright (c) 2022 Sern**
Copyright (c) 2023 sern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,5 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
End license text.

121
README.md
View File

@@ -3,7 +3,7 @@
</div>
<h1 align="center">Handlers. Redefined.</h1>
<h4 align="center">A customizable, batteries-included, powerful discord.js framework to streamline bot development.</h4>
<h4 align="center">A complete, customizable, typesafe, & reactive framework for discord bots</h4>
<div align="center" styles="margin-top: 10px">
<img src="https://img.shields.io/badge/open-source-brightgreen" />
@@ -14,6 +14,17 @@
<img alt="Lines of code" src="https://img.shields.io/badge/total%20lines-2k-blue" />
</div>
## Why?
- For you. A framework that's tailored to your exact needs.
- Lightweight. Does a lot while being small.
- Latest features. Support for discord.js v14 and all of its interactions.
- Hybrid, customizable and composable commands. Create them just how you like.
- Start quickly. Plug and play or customize to your liking.
- Embraces reactive programming. For consistent and reliable backend.
- Switch and customize how errors are handled, logging, and more.
- Use it with TypeScript or JavaScript. CommonJS and ESM supported.
- Active and growing community, always here to help. [Join us](https://sern.dev/discord)
- Unleash its full potential with a powerful CLI and awesome plugins.
## 📜 Installation
@@ -29,39 +40,55 @@ yarn add @sern/handler
pnpm add @sern/handler
```
## 👀 Quick Look
* Support for discord.js v14 and all interactions
* Hybrid commands
* Lightweight and customizable
* ESM, CommonJS and TypeScript support
* A powerful CLI and awesome community-made plugins
## 👶 Basic Usage
<details open><summary>ping.ts</summary>
#### ` index.js (CommonJS)`
```js
// Import the discord.js Client and GatewayIntentBits
const { Client, GatewayIntentBits } = require('discord.js');
// Import Sern namespace
const { Sern } = require('@sern/handler');
// Our configuration file
const { defaultPrefix, token } = require('./config.json');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages
]
```ts
export default commandModule({
type: CommandType.Slash,
//Installed plugin to publish to discord api and allow access to owners only.
plugins: [publish(), ownerOnly()],
description: 'A ping pong command',
execute(ctx) {
ctx.reply('Hello owner of the bot');
}
});
export const useContainer = Sern.makeDependencies({
```
</details>
<details open><summary>modal.ts</summary>
```ts
export default commandModule({
type: CommandType.Modal,
//Installed a plugin to make sure modal fields pass a validation.
plugins : [
assertFields({
fields: {
name: /^([^0-9]*)$/
},
failure: (errors, modal) => modal.reply('your submission did not pass the validations')
})
],
execute : (modal) => {
modal.reply('thanks for the submission!');
}
})
```
</details>
<details open><summary>index.ts</summary>
```ts
import { Client, GatewayIntentBits } from 'discord.js';
import { Sern, single, type Dependencies } from '@sern/handler';
//client has been declared previously
interface MyDependencies extends Dependencies {
'@sern/client': Singleton<Client>;
}
export const useContainer = Sern.makeDependencies<MyDependencies>({
build: root => root
.add({ '@sern/client': single(client) })
.add({ '@sern/logger': single(new DefaultLogging()) })
.add({ '@sern/client': single(() => client) })
});
//View docs for all options
@@ -70,29 +97,21 @@ Sern.init({
commands: 'src/commands',
// events: 'src/events' (optional),
containerConfig : {
get: useContainer
get: useContainer
}
});
client.login(token);
client.login("YOUR_BOT_TOKEN_HERE");
```
</details>
#### ` ping.js (CommonJS)`
```js
const { CommandType, commandModule } = require('@sern/handler');
exports.default = commandModule({
name: 'ping',
description: 'A ping pong command',
type: CommandType.Slash,
execute(ctx) {
ctx.reply('pong!');
}
});
```
See our [templates](https://github.com/sern-handler/templates) for TypeScript examples and more.
## 🤖 Bots Using sern
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).
- [Vinci](https://github.com/SrIzan10/vinci), the bot for Mara Turing.
- [Bask](https://github.com/baskbotml/bask), Listen your favorite artists on Discord.
- [ava](https://github.com/SrIzan10/ava), A discord bot that plays KNGI and Gensokyo Radio.
- [Murayama](https://github.com/murayamabot/murayama), :pepega:
- [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
## 💻 CLI
@@ -104,11 +123,7 @@ It is **highly encouraged** to use the [command line interface](https://github.c
- [Support Server](https://sern.dev/discord)
## 👋 Contribute
- Read our contribution [guidelines](./.github/CONTRIBUTING.md) carefully
- Read our contribution [guidelines](https://github.com/sern-handler/handler/blob/main/.github/CONTRIBUTING.md) carefully
- Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs
- All kinds of contributions are welcomed.
## 🚈 Roadmap
You can check our [roadmap](https://github.com/sern-handler/roadmap) to see what's going to be added or patched in the future.

View File

@@ -1,8 +1,8 @@
{
"name": "@sern/handler",
"packageManager": "pnpm@7.18.2",
"version": "2.0.0",
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.",
"packageManager": "yarn@3.5.1",
"version": "2.6.3",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
"types": "dist/index.d.ts",
@@ -13,12 +13,13 @@
}
},
"scripts": {
"watch": "tsup --watch --dts",
"watch": "tsup --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"build": "tsup && node scripts/mkjson.mjs dist/cjs dist/esm && tsup --dts-only --outDir dist",
"publish": "npm run build && npm publish",
"build:dev": "tsup && tsup --dts-only --outDir dist",
"build:prod": "tsup --minify && tsup --dts-only --outDir dist",
"publish": "npm run build:prod",
"pretty": "prettier --write ."
},
"keywords": [
@@ -33,21 +34,21 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"iti": "^0.5.0",
"rxjs": "^7.5.6",
"ts-pattern": "^4.0.2",
"ts-results-es": "^3.5.0"
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-results-es": "^3.6.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"eslint": "8.25.0",
"prettier": "2.7.1",
"tsup": "^6.1.3",
"typescript": "4.9.4"
},
"peerDependencies": {
"discord.js": ">= ^14.7.x"
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"discord.js": "^14.9.0",
"esbuild": "^0.17.0",
"esbuild-ifdef": "^0.2.0",
"eslint": "8.39.0",
"prettier": "2.8.8",
"tsup": "^6.7.0",
"typescript": "5.0.2"
},
"repository": {
"type": "git",

1738
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,6 @@
"schedule": ["every weekend"],
"lockFileMaintenance": {
"enabled": true,
"automerge": false
"automerge": true
}
}

View File

@@ -1,13 +0,0 @@
import { writeFile } from 'fs/promises';
import { join } from 'path';
// A quick script to regenerate package.jsons for each cjs and esm after tsup cleans distributions
const locations = process.argv;
locations.shift();
locations.shift();
for (const loc of locations) {
if (loc.endsWith('cjs')) {
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'commonjs' }));
} else {
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'module' }));
}
}

View File

@@ -1,32 +1,36 @@
import type { Observable } from 'rxjs';
import type { Logging } from './logging';
import { useContainerRaw } from '../dependencies/provider';
import util from 'util';
/**
* @since 2.0.0
*/
export interface ErrorHandling {
/**
* Number of times the process should throw an error until crashing and exiting
*/
keepAlive : number
keepAlive: number;
/**
* Utility function to crash
* @param error
*/
crash(error : Error) : never
crash(error: Error): never;
/**
* A function that is called on every crash. Updates keepAlive
* @param error
*/
updateAlive(error: Error): void
updateAlive(error: Error): void;
}
/**
* @since 2.0.0
*/
export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5;
crash(error: Error): never {
throw error;
}
updateAlive(e: Error) {
updateAlive(_: Error) {
this.keepAlive--;
}
}
@@ -35,10 +39,7 @@ export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
return (pload: unknown, caught: Observable<C>) => {
// This is done to fit the ErrorHandling contract
const err = pload instanceof Error ? pload : Error(util.format(pload));
if(crashHandler.keepAlive == 0) {
useContainerRaw()?.disposeAll().then(() => {
logging?.info({ message: 'Cleaning container and crashing' });
});
if (crashHandler.keepAlive == 0) {
crashHandler.crash(err);
}
//formatted payload
@@ -46,4 +47,4 @@ export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
crashHandler.updateAlive(err);
return caught;
};
}
}

View File

@@ -1,3 +1,3 @@
export { ErrorHandling, DefaultErrorHandling } from './errorHandling';
export { Logging, DefaultLogging } from './logging';
export { ModuleManager, DefaultModuleManager } from './moduleManager';
export { ModuleManager, DefaultModuleManager } from './moduleManager';

View File

@@ -1,12 +1,16 @@
import type { LogPayload } from '../../types/handler';
/**
* @since 2.0.0
*/
export interface Logging<T = unknown> {
error(payload : LogPayload<T>) : void;
warning(payload : LogPayload<T>) : void;
info(payload : LogPayload<T>) : void
debug(payload : LogPayload<T>) : void
error(payload: LogPayload<T>): void;
warning(payload: LogPayload<T>): void;
info(payload: LogPayload<T>): void;
debug(payload: LogPayload<T>): void;
}
/**
* @since 2.0.0
*/
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {
@@ -24,5 +28,4 @@ export class DefaultLogging implements Logging {
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}

View File

@@ -1,16 +1,23 @@
import type { CommandModuleDefs } from '../../types/module';
import type { CommandType } from '../structures/enums';
import type { ModuleStore } from '../structures/moduleStore';
import type { CommandType, ModuleStore } from '../structures';
import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
*/
export interface ModuleManager {
get<T extends CommandType>(strat : (ms: ModuleStore) => CommandModuleDefs[T] | undefined) : CommandModuleDefs[T] | undefined
set(strat: (ms: ModuleStore) => void) : void
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
): Processed<CommandModuleDefs[T]> | undefined;
set(strat: (ms: ModuleStore) => void): void;
}
/**
* @since 2.0.0
*/
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: ModuleStore) {}
get<T extends CommandType>(strat: (ms: ModuleStore) => CommandModuleDefs[T] | undefined) {
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
) {
return strat(this.moduleStore);
}

View File

@@ -1,3 +1,2 @@
export {
useContainer
} from './provider';
export { single, transient, many } from './lifetimeFunctions';
export { useContainerRaw } from './provider';

View File

@@ -0,0 +1,63 @@
import { _const } from '../utilities/functions';
type NotFunction =
| string
| number
| boolean
| null
| undefined
| bigint
| readonly any[]
| { apply?: never; [k: string]: any }
| { call?: never; [k: string]: any };
/**
* @deprecated
* @param cb
*/
export function single<T extends NotFunction>(cb: T): () => T;
/**
* New signature
* @since 2.0.0
* @param cb
*/
export function single<T extends () => unknown>(cb: T): T;
/**
* @__PURE__
* @since 2.0.0.
* Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and
* very annoying).
* For future versions, ensure that single is being passed as a **callback!!**
* @param cb
*/
export function single<T>(cb: T) {
if (typeof cb === 'function') return cb;
return () => cb;
}
/**
* @deprecated
* @param cb
* Deprecated signature
*/
export function transient<T extends NotFunction>(cb: T): () => () => T;
export function transient<T extends () => () => unknown>(cb: T): T;
/**
* @__PURE__
* @since 2.0.0
* Following iti's singleton and transient implementation,
* use transient if you want a new dependency every time your container getter is called
* @param cb
*/
export function transient<T>(cb: (() => () => T) | T) {
if (typeof cb !== 'function') return () => () => cb;
return cb;
}
/**
* @__PURE__
* @deprecated
* @param value
* Please use the transient function instead
*/
// prettier-ignore
export const many = <T>(value: T) => () => _const(value);

View File

@@ -1,83 +1,86 @@
import type { Container } from 'iti';
import { SernError } from '../structures/errors';
import { BehaviorSubject } from 'rxjs';
import * as assert from 'assert';
import type { Dependencies, MapDeps } from '../../types/handler';
import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
import SernEmitter from '../sernEmitter';
import { _const, ok } from '../utilities/functions';
import { DefaultErrorHandling, DefaultModuleManager, Logging } from '../contracts';
import { ModuleStore } from '../structures/moduleStore';
import { Ok, Result } from 'ts-results-es';
import { DefaultLogging } from '../contracts';
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
import { Result } from 'ts-results-es';
import { BehaviorSubject } from 'rxjs';
import { createContainer } from 'iti';
import { type Wrapper, ModuleStore, SernError } from '../structures';
export const containerSubject = new BehaviorSubject<Container<Dependencies, Partial<Dependencies>> | null>(null);
export function composeRoot<T extends Dependencies>(root: Container<Partial<T>, Partial<Dependencies>>, exclusion: Set<keyof Dependencies>) {
const client = root.get('@sern/client');
assert.ok(client !== undefined, SernError.MissingRequired);
//A utility function checking if a dependency has been declared excluded
const excluded = (key: keyof Dependencies) => exclusion.has(key);
//Wraps a fetch to the container in a Result, deferring the action
const get = <T>(key : keyof Dependencies) => Result.wrap(() => root.get(key) as T);
const getOr = (key: keyof Dependencies, elseAction: () => unknown) => {
//Gets dependency but if an Err, map to a function that upserts.
const dep = get(key).mapErr(() => elseAction);
if(dep.err) {
//Defers upsert until final check here
return dep.val();
}
};
const xGetOr = (key: keyof Dependencies, action: () => unknown) => {
if(excluded(key)) {
get(key) //if dev created a dependency but excluded, deletes on root composition
.andThen(() => Ok(root.delete(key)))
.unwrapOr(ok());
} else {
getOr(key, action);
}
export const containerSubject = new BehaviorSubject(defaultContainer());
};
xGetOr('@sern/emitter', () => root.upsert({
'@sern/emitter' : _const(new SernEmitter())
})
);
//An "optional" dependency
xGetOr('@sern/logger', () => {
root.upsert({
'@sern/logger' : _const(new DefaultLogging())
});
}
);
xGetOr('@sern/store', () => root.upsert({
'@sern/store' : _const(new ModuleStore())
})
);
xGetOr('@sern/modules', () => root.upsert((ctx) => ({
'@sern/modules' : _const(new DefaultModuleManager(ctx['@sern/store'] as ModuleStore))
}))
);
xGetOr('@sern/errors', () => root.upsert({
'@sern/errors': _const(new DefaultErrorHandling())
})
);
//If logger exists, log info, else do nothing.
get<Logging>('@sern/logger')
.map((logger => logger.info({ message: 'All dependencies loaded successfully' })))
.unwrapOr(ok());
/**
* Given the user's conf, check for any excluded dependency keys.
* Then, call conf.build to get the rest of the users' dependencies.
* Finally, update the containerSubject with the new container state
* @param conf
*/
export function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Get the current container. This should have no client or possible logger yet.
const currentContainer = containerSubject.getValue();
const excludeLogger = conf.exclude?.has('@sern/logger');
if (!excludeLogger) {
currentContainer.add({
'@sern/logger': () => new DefaultLogging(),
});
}
//Build the container based on the callback provided by the user
const container = conf.build(currentContainer);
//Check if the built container contains @sern/client or throw
// a runtime exception
Result.wrap(() => container.get('@sern/client')).expect(SernError.MissingRequired);
if (!excludeLogger) {
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
//I'm sorry little one
containerSubject.next(container as any);
}
export function useContainer<T extends Dependencies>() {
const container = containerSubject.getValue()! as unknown as Container<T, {}>;
assert.ok(container !== null, 'useContainer was called before Sern#init');
//weird edge case, why can i not use _const here?
const container = containerSubject.getValue() as Container<T, {}>;
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => Result.wrap(() => container.get(key)).unwrapOr(undefined)) as MapDeps<T, V>;
}
/**
* Returns the underlying data structure holding all dependencies.
* Please be careful as this only gets the client's current state.
* Exposes some methods from iti
*/
export function useContainerRaw() {
return containerSubject.getValue();
}
export function useContainerRaw<T extends Dependencies>() {
return containerSubject.getValue() as Container<T, {}>;
}
/**
* Provides all the defaults for sern to function properly.
* The only user provided dependency needs to be @sern/client
*/
function defaultContainer() {
return createContainer()
.add({ '@sern/errors': () => new DefaultErrorHandling() })
.add({ '@sern/store': () => new ModuleStore() })
.add(ctx => {
return {
'@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']),
};
})
.add({ '@sern/emitter': () => new SernEmitter() }) as Container<
Omit<Dependencies, '@sern/client' | '@sern/logger'>,
{}
>;
}
export function makeFetcher(wrapper: Wrapper) {
const requiredDependencyKeys = [
'@sern/emitter',
'@sern/client',
'@sern/errors',
'@sern/logger',
] as ['@sern/emitter', '@sern/client', '@sern/errors', '@sern/logger'];
return <Keys extends (keyof Dependencies)[]>(otherKeys: [...Keys]) =>
wrapper.containerConfig.get(...requiredDependencyKeys, ...otherKeys) as MapDeps<
Dependencies,
[...typeof requiredDependencyKeys, ...Keys]
>;
}

View File

@@ -1,170 +0,0 @@
import Context from '../structures/context';
import type { Payload, SlashOptions } from '../../types/handler';
import { arrAsync } from '../utilities/arrAsync';
import { controller } from '../sern';
import type {
ButtonInteraction,
ModalSubmitInteraction,
AutocompleteInteraction,
ChatInputCommandInteraction,
Interaction,
UserContextMenuCommandInteraction,
MessageContextMenuCommandInteraction,
} from 'discord.js';
import { SernError } from '../structures/errors';
import treeSearch from '../utilities/treeSearch';
import type {
BothCommand,
ButtonCommand, ContextMenuMsg,
ContextMenuUser,
ModalSubmitCommand,
StringSelectCommand,
SlashCommand, UserSelectCommand, ChannelSelectCommand, MentionableSelectCommand, RoleSelectCommand,
} from '../../types/module';
import type SernEmitter from '../sernEmitter';
import { EventEmitter } from 'events';
import type { DiscordEventCommand, ExternalEventCommand, SernEventCommand } from '../structures/events';
import * as assert from 'assert';
import { reducePlugins } from '../utilities/functions';
import { concatMap, from, fromEvent, map, of } from 'rxjs';
import type { MessageComponentInteraction } from 'discord.js';
export function applicationCommandDispatcher(interaction: Interaction) {
if (interaction.isAutocomplete()) {
return dispatchAutocomplete(interaction);
} else {
const ctx = Context.wrap(interaction as ChatInputCommandInteraction);
const args: ['slash', SlashOptions] = ['slash', ctx.interaction.options];
return (mod: BothCommand | SlashCommand) => ({
mod,
execute: () => mod.execute(ctx, args),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([ctx, args], controller)),
),
});
}
}
export function dispatchAutocomplete(interaction: AutocompleteInteraction) {
return (mod: BothCommand | SlashCommand) => {
const selectedOption = treeSearch(interaction, mod.options);
if (selectedOption !== undefined) {
return {
mod,
execute: () => selectedOption.command.execute(interaction),
eventPluginRes: arrAsync(
selectedOption.command.onEvent.map(e => e.execute(interaction, controller)),
),
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
};
}
export function modalCommandDispatcher(interaction: ModalSubmitInteraction) {
return (mod: ModalSubmitCommand) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function buttonCommandDispatcher(interaction: ButtonInteraction) {
return (mod: ButtonCommand) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function selectMenuCommandDispatcher(interaction: MessageComponentInteraction) {
//safe casts because command type runtime check
return (mod: StringSelectCommand | UserSelectCommand | ChannelSelectCommand | MentionableSelectCommand | RoleSelectCommand) => ({
mod,
execute: () => mod.execute(interaction as never),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction as never], controller)),
),
});
}
export function ctxMenuUserDispatcher(interaction: UserContextMenuCommandInteraction) {
return (mod: ContextMenuUser) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function ctxMenuMsgDispatcher(interaction: MessageContextMenuCommandInteraction) {
return (mod: ContextMenuMsg) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function sernEmitterDispatcher(e: SernEmitter) {
return(cmd: SernEventCommand & { name: string }) => ({
source: e,
cmd,
execute: fromEvent(e, cmd.name)
.pipe( map( event => ({
event,
executeEvent: of(event).pipe(concatMap(event =>
reducePlugins(from(
arrAsync(
cmd.onEvent.map(plug => plug.execute([event as Payload], controller))
))
)))
})))
});
}
export function discordEventDispatcher(e: EventEmitter) {
return (cmd: DiscordEventCommand & { name: string }) => ({
source: e,
cmd,
execute: fromEvent(e, cmd.name)
.pipe( map( event => ({
event,
executeEvent: of(event).pipe(concatMap( event =>
reducePlugins(from(
arrAsync(
// god forbid I use any!!!
cmd.onEvent.map(plug => plug.execute([event as any], controller))
))
)))
})))
});
}
export function externalEventDispatcher(e: (e:ExternalEventCommand) => unknown) {
return (cmd: ExternalEventCommand & { name: string}) => {
const external = e(cmd);
assert.ok(external instanceof EventEmitter, `${e} is not an EventEmitter`);
return {
source: external,
cmd,
execute: fromEvent(external, cmd.name)
.pipe(map(event => ({
event,
executeEvent : of(event).pipe(concatMap(event =>
reducePlugins(from(arrAsync(
cmd.onEvent.map(plug => plug.execute([event], controller))
)))
)),
})))
};
};
}

View File

@@ -0,0 +1,67 @@
import type { Processed } from '../../../types/handler';
import type { AutocompleteInteraction } from 'discord.js';
import { SernError } from '../../structures';
import treeSearch from '../../utilities/treeSearch';
import type { BothCommand, CommandModule, Module, SlashCommand } from '../../../types/module';
import { EventEmitter } from 'events';
import * as assert from 'assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
import { arrayifySource, callPlugin } from '../operators';
import { createResultResolver } from '../observableHandling';
export function dispatchCommand(module: Processed<CommandModule>, createArgs: () => unknown[]) {
const args = createArgs();
return {
module,
args,
};
}
function intoPayload(module: Processed<Module>) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
unknown[]
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
onNext: ({ args }) => args,
});
/**
* Creates an observable from { source }
* @param module
* @param source
*/
export function eventDispatcher(module: Processed<Module>, source: unknown) {
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
const execute: OperatorFunction<unknown[], unknown> = concatMap(async args =>
module.execute(...args),
);
return fromEvent(source, module.name).pipe(
intoPayload(module),
concatMap(createResult),
execute,
);
}
export function dispatchAutocomplete(
module: Processed<BothCommand | SlashCommand>,
interaction: AutocompleteInteraction,
) {
const option = treeSearch(interaction, module.options);
if (option !== undefined) {
return {
module: option.command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [interaction],
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
}

View File

@@ -0,0 +1,2 @@
export * from './dispatchers';
export * from './provideArgs';

View File

@@ -0,0 +1,23 @@
import type { ChatInputCommandInteraction, Interaction, Message } from 'discord.js';
import { Context } from '../../structures';
import type { Args, SlashOptions } from '../../../types/handler';
/**
* function overloads to create an arguments list for Context
* @param wrap
* @param messageArgs
*/
export function contextArgs(
wrap: Message,
messageArgs?: string[],
): () => [Context, ['text', string[]]];
export function contextArgs(wrap: Interaction): () => [Context, ['slash', SlashOptions]];
export function contextArgs(wrap: Interaction | Message, messageArgs?: string[]) {
const ctx = Context.wrap(wrap as ChatInputCommandInteraction | Message);
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.interaction.options];
return () => [ctx, args] as [Context, Args];
}
export function interactionArg<T extends Interaction>(interaction: T) {
return () => [interaction] as [T];
}

View File

@@ -1,31 +0,0 @@
import type Wrapper from '../structures/wrapper';
import { Subject, type Observable } from 'rxjs';
import type { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
export abstract class EventsHandler<T> {
protected payloadSubject = new Subject<T>();
protected abstract discordEvent: Observable<unknown>;
protected client: EventEmitter;
protected emitter: SernEmitter;
protected crashHandler: ErrorHandling;
protected logger?: Logging;
protected modules: ModuleManager;
protected constructor({ containerConfig }: Wrapper) {
const [
client,
emitter,
crash,
modules,
logger,
] = containerConfig.get('@sern/client', '@sern/emitter', '@sern/errors', '@sern/modules', '@sern/logger');
this.logger = logger as Logging|undefined;
this.modules = modules as ModuleManager;
this.client = client as EventEmitter;
this.emitter = emitter as SernEmitter;
this.crashHandler = crash as ErrorHandling;
}
protected abstract init(): void;
protected abstract setState(state: T): void;
}

View File

@@ -1,119 +1,118 @@
import type { Interaction } from 'discord.js';
import { catchError, concatMap, from, fromEvent, map, Observable } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { EventsHandler } from './eventsHandler';
import { SernError } from '../structures/errors';
import { CommandType, PayloadType } from '../structures/enums';
import { match, P } from 'ts-pattern';
import { Interaction } from 'discord.js';
import {
applicationCommandDispatcher,
buttonCommandDispatcher,
ctxMenuMsgDispatcher,
ctxMenuUserDispatcher,
modalCommandDispatcher,
selectMenuCommandDispatcher,
} from './dispatchers';
import type {
ButtonInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
MessageContextMenuCommandInteraction,
} from 'discord.js';
import { executeModule } from './observableHandling';
catchError,
concatMap,
EMPTY,
filter,
finalize,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
} from 'rxjs';
import { CommandType, type ModuleStore, SernError } from '../structures';
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers';
import { executeModule, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import type { ModuleStore } from '../structures/moduleStore';
import type { MessageComponentInteraction } from 'discord.js';
import { ErrorHandling, handleError } from '../contracts/errorHandling';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
import type { Logging, ModuleManager } from '../contracts';
import type { EventEmitter } from 'node:events';
export default class InteractionHandler extends EventsHandler<{
function makeInteractionProcessor(
modules: ModuleManager,
): OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }> {
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return modules.get(cb);
};
return pipe(
concatMap(event => {
if (event.isMessageComponent()) {
const customId = event.customId;
const module = get(ms => {
return ms.InteractionHandlers[event.componentType].get(customId);
});
return of({ module, event });
} else if (event.isCommand() || event.isAutocomplete()) {
const commandName = event.commandName;
const module = get(
ms =>
/**
* try to fetch from ApplicationCommands, if nothing, try BothCommands
* exists on the API but not sern
*/
ms.ApplicationCommands[event.commandType].get(commandName) ??
ms.BothCommands.get(commandName),
);
return of({ module, event });
} else if (event.isModalSubmit()) {
const module = get(ms => ms.ModalSubmit.get(event.customId));
return of({ module, event });
} else return EMPTY;
}),
filter(m => m.module !== undefined),
) as OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }>;
}
export function makeInteractionCreate([s, client, err, log, modules]: [
SernEmitter,
EventEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
]) {
//map. If nothing again,this means a slash command
const interactionStream$ = fromEvent(client, 'interactionCreate') as Observable<Interaction>;
const interactionProcessor = makeInteractionProcessor(modules);
return interactionStream$
.pipe(
interactionProcessor,
map(createDispatcher),
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(module => executeModule(s, module)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({
message: 'interactionCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
)
.subscribe();
}
function createDispatcher({
module,
event,
}: {
event: Interaction;
mod: CommandModule;
}> {
protected override discordEvent: Observable<Interaction>;
constructor(wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Interaction>>fromEvent(this.client, 'interactionCreate');
this.init();
this.payloadSubject
.pipe(
map(this.processModules),
concatMap(({ mod, execute, eventPluginRes }) =>
from(eventPluginRes).pipe(map(res => ({ mod, res, execute }))) //resolve all the Results from event plugins
),
concatMap(payload => executeModule(wrapper, payload)),
catchError(handleError(this.crashHandler, this.logger)),
)
.subscribe();
}
override init() {
const get = (cb: (ms: ModuleStore) => CommandModule|undefined) => {
return this.modules.get(cb);
};
this.discordEvent.subscribe({
next: event => {
if (event.isMessageComponent()) {
const mod = get(ms =>
ms.InteractionHandlers[event.componentType].get(event.customId));
this.setState({ event, mod });
} else if (event.isCommand() || event.isAutocomplete()) {
const mod = get(ms =>
ms.ApplicationCommands[event.commandType].get(event.commandName) ??
ms.BothCommands.get(event.commandName)
);
this.setState({ event, mod });
} else if (event.isModalSubmit()) {
const mod = get((ms) => ms.InteractionHandlers[5].get(event.customId));
this.setState({ event, mod });
} else {
throw Error('This interaction is not supported yet');
}
},
error: reason => {
this.emitter.emit('error', { type: PayloadType.Failure, reason });
},
});
}
protected setState(state: { event: Interaction; mod: CommandModule | undefined }): void {
if (state.mod === undefined) {
this.emitter.emit('warning',{ type: PayloadType.Warning, reason: 'Found no module for this interaction' });
} else {
//if statement above checks already, safe cast
this.payloadSubject.next(state as { event: Interaction; mod: CommandModule });
module: Processed<CommandModule>;
}) {
switch (module.type) {
case CommandType.Text:
throw Error(SernError.MismatchEvent);
case CommandType.Slash:
case CommandType.Both: {
if (event.isAutocomplete()) {
/**
* Autocomplete is a special case that
* must be handled separately, since it's
* too different from regular command modules
*/
return dispatchAutocomplete(module, event);
} else {
return dispatchCommand(module, contextArgs(event));
}
}
}
protected processModules({ mod, event }: { event: Interaction; mod: CommandModule }) {
return match(mod)
.with(
{ type: P.union(CommandType.Slash, CommandType.Both) },
applicationCommandDispatcher(event),
)
.with(
{ type: CommandType.Modal },
modalCommandDispatcher(event as ModalSubmitInteraction),
)
.with({ type: CommandType.Button }, buttonCommandDispatcher(event as ButtonInteraction))
.with(
{ type: P.union(
CommandType.RoleSelect,
CommandType.StringSelect,
CommandType.UserSelect,
CommandType.MentionableSelect,
CommandType.ChannelSelect
) },
selectMenuCommandDispatcher(event as MessageComponentInteraction),
)
.with(
{ type: CommandType.CtxUser },
ctxMenuUserDispatcher(event as UserContextMenuCommandInteraction),
)
.with(
{ type: CommandType.CtxMsg },
ctxMenuMsgDispatcher(event as MessageContextMenuCommandInteraction),
)
.otherwise(() => this.crashHandler.crash(Error(SernError.MismatchModule)));
default:
return dispatchCommand(module, interactionArg(event));
}
}

View File

@@ -1,78 +1,92 @@
import { EventsHandler } from './eventsHandler';
import { catchError, concatMap, from, fromEvent, map, Observable, of, switchMap } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of, pipe } from 'rxjs';
import { type ModuleStore, SernError } from '../structures';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, isOneOfCorrectModules } from './observableHandling';
import { fmt } from '../utilities/messageHelpers';
import Context from '../structures/context';
import { CommandType, PayloadType } from '../structures/enums';
import { arrAsync } from '../utilities/arrAsync';
import { controller } from '../sern';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import type { CommandModule, TextCommand } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import type { ModuleStore } from '../structures/moduleStore';
import { ErrorHandling, handleError } from '../contracts/errorHandling';
import { contextArgs, dispatchCommand } from './dispatchers';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
import type { Logging, ModuleManager } from '../contracts';
import type { EventEmitter } from 'node:events';
export default class MessageHandler extends EventsHandler<{
ctx: Context;
args: ['text', string[]];
mod: TextCommand;
}> {
protected discordEvent: Observable<Message>;
public constructor(protected wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Message>>fromEvent(this.client, 'messageCreate');
this.init();
this.payloadSubject
.pipe(
switchMap(({ mod, ctx, args }) => {
const res = arrAsync(
mod.onEvent.map(ep => ep.execute([ctx, args], controller)),
);
const execute = () => mod.execute(ctx, args);
//resolves the promise and re-emits it back into source
return from(res).pipe(map(res => ({ mod, execute, res })));
}),
concatMap(payload => executeModule(wrapper, payload)),
catchError(handleError(this.crashHandler, this.logger)),
)
.subscribe();
}
protected init(): void {
if (this.wrapper.defaultPrefix === undefined) return; //for now, just ignore if prefix doesn't exist
const { defaultPrefix } = this.wrapper;
const get = (cb: (ms: ModuleStore) => CommandModule | undefined) => {
return this.modules.get(cb);
};
this.discordEvent
.pipe(
ignoreNonBot(this.wrapper.defaultPrefix),
map(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
return {
ctx: Context.wrap(message),
args: <['text', string[]]>['text', rest],
mod: get(ms =>
ms.TextCommands.get(prefix) ??
ms.BothCommands.get(prefix)
),
};
}),
concatMap(element =>
of(element.mod).pipe(
isOneOfCorrectModules(CommandType.Text),
map(mod => ({ ...element, mod })),
),
),
)
.subscribe({
next: value => this.setState(value),
error: reason =>
this.emitter.emit('error', { type: PayloadType.Failure, reason }),
});
}
protected setState(state: { ctx: Context; args: ['text', string[]]; mod: TextCommand }) {
this.payloadSubject.next(state);
}
/**
* Removes the first character(s) _[depending on prefix length]_ of the message
* @param msg
* @param prefix The prefix to remove
* @returns The message without the prefix
* @example
* message.content = '!ping';
* console.log(fmt(message, '!'));
* // [ 'ping' ]
*/
export function fmt(msg: string, prefix: string): string[] {
return msg.slice(prefix.length).trim().split(/\s+/g);
}
/**
* An operator function that processes a message to fetch a command module and prepares context payload.
* @param defaultPrefix
* @param get
*/
const createMessageProcessor = (
defaultPrefix: string,
get: (
cb: (ms: ModuleStore) => Processed<CommandModule> | undefined,
) => CommandModule | undefined,
) =>
pipe(
ignoreNonBot(defaultPrefix),
//This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => {
const [prefix, ...rest] = fmt(message.content, defaultPrefix);
const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix));
if (module === undefined) {
return EMPTY;
}
const payload = {
args: contextArgs(message, rest),
module,
};
return of(payload);
}),
map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)),
);
export function makeMessageCreate(
[s, client, err, log, modules]: [
SernEmitter,
EventEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
],
defaultPrefix?: string,
) {
if (!defaultPrefix) {
return EMPTY.subscribe();
}
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return modules.get(cb);
};
const messageStream$ = fromEvent(client, 'messageCreate') as Observable<Message>;
const messageProcessor = createMessageProcessor(defaultPrefix, get);
return messageStream$
.pipe(
messageProcessor,
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(s, payload)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'messageCreate stream closed or reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
)
.subscribe();
}

View File

@@ -1,143 +1,121 @@
import type { Message } from 'discord.js';
import { concatMap, from, map, Observable, of, switchMap, tap, throwError, toArray } from 'rxjs';
import { SernError } from '../structures/errors';
import type { Awaitable, Message } from 'discord.js';
import { concatMap, EMPTY, filter, from, Observable, of, tap, throwError } from 'rxjs';
import { Result } from 'ts-results-es';
import type { CommandType } from '../structures/enums';
import type Wrapper from '../structures/wrapper';
import { PayloadType, PluginType } from '../structures/enums';
import type { CommandModule, CommandModuleDefs, AnyModule } from '../../types/module';
import { _const } from '../utilities/functions';
import type SernEmitter from '../sernEmitter';
import type { DefinedCommandModule, DefinedEventModule } from '../../types/handler';
import type { Awaitable } from 'discord.js';
import { processCommandPlugins } from './userDefinedEventsHandling';
import type { CommandModule, EventModule, Module } from '../../types/module';
import SernEmitter from '../sernEmitter';
import { callPlugin, everyPluginOk, filterMapTo } from './operators';
import type { ImportPayload, Processed } from '../../types/handler';
import type { ControlPlugin, VoidResult } from '../../types/plugin';
export function ignoreNonBot(prefix: string) {
return (src: Observable<Message>) =>
new Observable<Message>(subscriber => {
return src.subscribe({
next(m) {
const messageFromHumanAndHasPrefix =
!m.author.bot &&
m.content
.slice(0, prefix.length)
.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
if (messageFromHumanAndHasPrefix) {
subscriber.next(m);
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
function hasPrefix(prefix: string, content: string) {
const prefixInContent = content.slice(0, prefix.length);
return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
}
/**
* If the current value in Result stream is an error, calls callback.
* @param cb
* Ignores messages from any person / bot except itself
* @param prefix
*/
export function errTap<T extends AnyModule>(cb: (err: SernError) => void) {
return (src: Observable<Result<{ mod: T; absPath: string }, SernError>>) =>
new Observable<{ mod: T; absPath: string }>(subscriber => {
return src.subscribe({
next(value) {
if (value.err) {
cb(value.val);
} else {
subscriber.next(value.val);
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
}
//POG
export function isOneOfCorrectModules<T extends readonly CommandType[]>(...inputs: [...T]) {
return (src: Observable<CommandModule | undefined>) => {
return new Observable<CommandModuleDefs[T[number]]>(subscriber => {
return src.subscribe({
next(mod) {
if (mod === undefined) {
return throwError(_const(SernError.UndefinedModule));
}
if (inputs.some(type => (mod.type & type) !== 0)) {
subscriber.next(mod as CommandModuleDefs[T[number]]);
} else {
return throwError(_const(SernError.MismatchModule));
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
};
export function ignoreNonBot(prefix: string) {
const messageFromHumanAndHasPrefix = ({ author, content }: Message) =>
!author.bot && hasPrefix(prefix, content);
return filter(messageFromHumanAndHasPrefix);
}
/**
* Wraps the task in a Result as a try / catch.
* if the task is ok, an event is emitted and the stream becomes empty
* if the task is an error, throw an error down the stream which will be handled by catchError
* @param emitter reference to SernEmitter that will emit a successful execution of module
* @param module the module that will be executed with task
* @param task the deferred execution which will be called
*/
export function executeModule(
wrapper: Wrapper,
payload: {
mod: CommandModule;
execute: () => unknown;
res: Result<void, void>[];
emitter: SernEmitter,
{
module,
task,
}: {
module: Processed<Module>;
task: () => Awaitable<unknown>;
},
) {
const emitter = wrapper.containerConfig.get('@sern/emitter')[0] as SernEmitter;
if (payload.res.every(el => el.ok)) {
const executeFn = Result.wrapAsync<unknown, Error | string>(() => Promise.resolve(payload.execute()));
return from(executeFn).pipe(
concatMap(res => {
if (res.err) {
return throwError(() => ({
type: PayloadType.Failure,
reason: res.val,
module: payload.mod,
}));
}
return of(res.val).pipe(
tap(() =>
emitter.emit('module.activate', {
type: PayloadType.Success,
module: payload.mod,
}),
),
);
}),
);
} else {
emitter.emit('module.activate', {
type: PayloadType.Failure,
module: payload.mod,
reason: SernError.PluginFailure,
});
return of(undefined);
}
}
export function resolvePlugins({ mod, cmdPluginRes }: {
mod: DefinedCommandModule | DefinedEventModule;
cmdPluginRes: {
execute: Awaitable<Result<void, void>>;
type: PluginType.Command;
}[];
}) {
if (mod.plugins.length === 0) {
return of({ mod, pluginRes: [] });
}
// modules with no event plugins are ignored in the previous
return from(cmdPluginRes).pipe(
switchMap(pl =>
from(pl.execute).pipe(
map(execute => ({ ...pl, execute })),
toArray(),
),
),
map(pluginRes => ({ mod, pluginRes })),
return of(module).pipe(
//converting the task into a promise so rxjs can resolve the Awaitable properly
concatMap(() => Result.wrapAsync(async () => task())),
concatMap(result => {
if (result.ok) {
emitter.emit('module.activate', SernEmitter.success(module));
return EMPTY;
} else {
return throwError(() => SernEmitter.failure(module, result.val));
}
}),
);
}
export function processPlugins(payload: { mod: DefinedCommandModule | DefinedEventModule; absPath: string }) {
const cmdPluginRes = processCommandPlugins(payload);
return of({ mod: payload.mod, cmdPluginRes });
}
/**
* A higher order function that
* - creates a stream of {@link VoidResult} { config.createStream }
* - any failures results to { config.onFailure } being called
* - if all results are ok, the stream is converted to { config.onSuccess }
* emit config.onSuccess Observable
* @param config
* @returns receiver function for flattening a stream of data
*/
export function createResultResolver<
T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] },
Args extends { module: T; [key: string]: unknown },
Output,
>(config: {
onStop?: (module: T) => unknown;
onNext: (args: Args) => Output;
createStream: (args: Args) => Observable<VoidResult>;
}) {
return (args: Args) => {
const task$ = config.createStream(args);
return task$.pipe(
tap(result => {
if (result.err) {
config.onStop?.(args.module);
}
}),
everyPluginOk,
filterMapTo(() => config.onNext(args)),
);
};
}
/**
* Calls a module's init plugins and checks for Err. If so, call { onStop } and
* ignore the module
*/
export function callInitPlugins<
T extends Processed<CommandModule | EventModule>,
Args extends ImportPayload<T>,
>(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) {
return concatMap(
createResultResolver({
createStream: args => from(args.module.plugins).pipe(callPlugin(args)),
...config,
}),
);
}
/**
* Creates an executable task ( execute the command ) if all control plugins are successful
* @param onStop emits a failure response to the SernEmitter
*/
export function makeModuleExecutor<
M extends Processed<Module>,
Args extends { module: M; args: unknown[] },
>(onStop: (m: M) => unknown) {
const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
return concatMap(
createResultResolver({
onStop,
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
onNext,
}),
);
}

View File

@@ -0,0 +1,82 @@
/**
* This file holds sern's rxjs operators used for processing data.
* Each function should be modular and testable, not bound to discord / sern
* and independent of each other
*/
import { concatMap, defaultIfEmpty, EMPTY, every, map, of, OperatorFunction, pipe } from 'rxjs';
import type { AnyModule } from '../../../types/module';
import { nameOrFilename } from '../../utilities/functions';
import type { PluginResult, VoidResult } from '../../../types/plugin';
import { guayin } from '../../plugins';
import { controller } from '../../sern';
import { Result } from 'ts-results-es';
import { ImportPayload, Processed } from '../../../types/handler';
/**
* if {src} is true, mapTo V, else ignore
* @param item
*/
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
}
/**
* Calls any plugin with {args}.
* @param args if an array, its spread and plugin called.
*/
export function callPlugin(args: unknown): OperatorFunction<
{
execute: (...args: unknown[]) => PluginResult;
},
VoidResult
> {
return concatMap(async plugin => {
const isNewPlugin = Reflect.has(plugin, guayin);
if (isNewPlugin) {
if (Array.isArray(args)) {
return plugin.execute(...args);
}
return plugin.execute(args);
} else {
return plugin.execute(args, controller);
}
});
}
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
export const fillDefaults = <T extends AnyModule>({ module, absPath }: ImportPayload<T>) => {
return {
absPath,
module: {
name: nameOrFilename(module?.name, absPath),
description: module?.description ?? '...',
...module,
},
};
};
/**
* If the current value in Result stream is an error, calls callback.
* This also extracts the Ok value from Result
* @param cb
* @returns Observable<{ module: T; absPath: string }>
*/
export function errTap<Ok, Err>(cb: (err: Err) => void): OperatorFunction<Result<Ok, Err>, Ok> {
return concatMap(result => {
if (result.ok) {
return of(result.val);
} else {
cb(result.val as Err);
return EMPTY;
}
});
}
/**
* Checks if the stream of results is all ok.
*/
export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
every(result => result.ok),
defaultIfEmpty(true),
);

View File

@@ -1,134 +1,106 @@
import { EventsHandler } from './eventsHandler';
import type Wrapper from '../structures/wrapper';
import { concatMap, fromEvent, Observable, map, take } from 'rxjs';
import * as Files from '../utilities/readFile';
import { errTap, processPlugins, resolvePlugins } from './observableHandling';
import { CommandType, PayloadType } from '../structures/enums';
import { SernError } from '../structures/errors';
import { match } from 'ts-pattern';
import { fromEvent, map, pipe, switchMap, take } from 'rxjs';
import * as Files from '../module-loading/readFile';
import { callInitPlugins } from './observableHandling';
import { CommandType, type ModuleStore, SernError } from '../structures';
import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module';
import type { DefinedCommandModule, DefinedEventModule } from '../../types/handler';
import type { ModuleManager } from '../contracts';
import type { ModuleStore } from '../structures/moduleStore';
import { _const, err, nameOrFilename, ok } from '../utilities/functions';
import type { Processed } from '../../types/handler';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
import { err, ok } from '../utilities/functions';
import { errTap, fillDefaults } from './operators';
import SernEmitter from '../sernEmitter';
import type { EventEmitter } from 'node:events';
export default class ReadyHandler extends EventsHandler<{
mod: DefinedCommandModule;
absPath: string;
}> {
protected discordEvent!: Observable<{ mod: CommandModule; absPath: string }>;
constructor(wrapper: Wrapper) {
super(wrapper);
const ready$ = fromEvent(this.client, 'ready').pipe(take(1));
this.discordEvent = ready$.pipe(
concatMap(() =>
Files.buildData<CommandModule>(wrapper.commands).pipe(
errTap(reason =>
this.emitter.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
}),
),
),
),
);
this.init();
this.payloadSubject
.pipe(
concatMap(processPlugins),
concatMap(resolvePlugins),
).subscribe(payload => {
const allPluginsSuccessful = payload.pluginRes.every(({ execute }) => execute.ok);
if (allPluginsSuccessful) {
const res = registerModule(this.modules, payload.mod);
if (res.err) {
this.crashHandler.crash(Error(SernError.InvalidModuleType));
}
this.emitter.emit('module.register', {
type: PayloadType.Success,
module: payload.mod,
});
} else {
this.emitter.emit('module.register', {
type: PayloadType.Failure,
module: payload.mod,
reason: SernError.PluginFailure,
});
}
});
}
private static intoDefinedModule({ absPath, mod }: { absPath: string; mod: CommandModule }): {
absPath: string;
mod: DefinedCommandModule;
} {
return {
absPath,
mod: {
name: nameOrFilename(mod.name, absPath),
description: mod?.description ?? '...',
...mod,
},
};
}
protected init() {
this.discordEvent.pipe(map(ReadyHandler.intoDefinedModule)).subscribe({
next: value => this.setState(value),
complete: () => this.payloadSubject.unsubscribe(),
function buildCommandModules(commandDir: string, sernEmitter: SernEmitter) {
return pipe(
switchMap(() => Files.buildModuleStream<CommandModule>(commandDir)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
map(fillDefaults),
);
}
export function makeReadyEvent(
[sEmitter, client, errorHandler, , moduleManager]: [
SernEmitter,
EventEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
],
commandDir: string,
) {
const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
return readyOnce$
.pipe(
buildCommandModules(commandDir, sEmitter),
callInitPlugins({
onStop: module => {
sEmitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onNext: ({ module }) => {
sEmitter.emit('module.register', SernEmitter.success(module));
return module;
},
}),
)
.subscribe(module => {
const result = registerModule(moduleManager, module as Processed<CommandModule>);
if (result.err) {
errorHandler.crash(Error(SernError.InvalidModuleType));
}
});
}
protected setState(state: { absPath: string; mod: DefinedCommandModule }): void {
this.payloadSubject.next(state);
}
}
function registerModule(manager: ModuleManager, mod: DefinedCommandModule | DefinedEventModule): Result<void, void> {
function registerModule<T extends Processed<CommandModule>>(
manager: ModuleManager,
mod: T,
): Result<void, void> {
const name = mod.name;
const insert = (cb: (ms: ModuleStore) => void) => {
const set = Result.wrap(_const(manager.set(cb)));
const set = Result.wrap(() => manager.set(cb));
return set.ok ? ok() : err();
};
return match<DefinedCommandModule | DefinedEventModule>(mod)
.with({ type: CommandType.Text }, mod => {
switch (mod.type) {
case CommandType.Text: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod));
})
.with({ type: CommandType.Slash }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod))
)
.with({ type: CommandType.Both }, mod => {
}
case CommandType.Slash:
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod),
);
case CommandType.Both: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert( ms => ms.BothCommands.set(name, mod));
})
.with({ type: CommandType.CtxUser }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod))
)
.with({ type: CommandType.CtxMsg }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod))
)
.with({ type: CommandType.Button }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod))
)
.with({ type: CommandType.StringSelect }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod))
)
.with( { type: CommandType.MentionableSelect }, mod =>
insert (ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod))
)
.with( { type: CommandType.ChannelSelect }, mod =>
insert ( ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod))
)
.with( { type: CommandType.UserSelect }, mod =>
insert ( ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod))
)
.with( { type: CommandType.RoleSelect}, mod =>
insert ( ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod))
)
.with({ type: CommandType.Modal }, mod =>
insert(ms => ms.ModalSubmit.set(name, mod))
)
.otherwise(err);
return insert(ms => ms.BothCommands.set(name, mod));
}
case CommandType.CtxUser:
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
case CommandType.CtxMsg:
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod),
);
case CommandType.Button:
return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
case CommandType.StringSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
case CommandType.MentionableSelect:
return insert(ms =>
ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod),
);
case CommandType.UserSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
case CommandType.ChannelSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
case CommandType.RoleSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
case CommandType.Modal:
return insert(ms => ms.ModalSubmit.set(name, mod));
default:
return err();
}
}

View File

@@ -1,108 +1,74 @@
import { catchError, concatMap, filter, from, iif, map, of, tap, toArray } from 'rxjs';
import { buildData } from '../utilities/readFile';
import { controller } from '../sern';
import type { DefinedCommandModule, DefinedEventModule, Dependencies } from '../../types/handler';
import { PayloadType, PluginType } from '../structures/enums';
import type Wrapper from '../structures/wrapper';
import { isDiscordEvent, isExternalEvent, isSernEvent } from '../utilities/predicates';
import { errTap, processPlugins, resolvePlugins } from './observableHandling';
import type { AnyModule, EventModule } from '../../types/module';
import { catchError, finalize, map, mergeAll } from 'rxjs';
import * as Files from '../module-loading/readFile';
import type { Dependencies, Processed } from '../../types/handler';
import { callInitPlugins } from './observableHandling';
import type { CommandModule, EventModule } from '../../types/module';
import type { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import { nameOrFilename, reducePlugins } from '../utilities/functions';
import { match } from 'ts-pattern';
import { discordEventDispatcher, externalEventDispatcher, sernEmitterDispatcher } from './dispatchers';
import SernEmitter from '../sernEmitter';
import type { ErrorHandling, Logging } from '../contracts';
import { SernError } from '../structures/errors';
import { SernError, EventType, type Wrapper } from '../structures';
import { eventDispatcher } from './dispatchers';
import { handleError } from '../contracts/errorHandling';
import type { Awaitable } from 'discord.js';
import type { Result } from 'ts-results-es';
import { errTap, fillDefaults } from './operators';
import { useContainerRaw } from '../dependencies';
/**
* Utility function to process command plugins for all Modules
* @param payload
*/
export function processCommandPlugins<T extends DefinedCommandModule | DefinedEventModule>(
payload: { mod: T; absPath: string },
): {type: PluginType.Command, execute: Awaitable<Result<void,void>>}[] {
return payload.mod.plugins.map(plug => ({
type: plug.type,
execute: plug.execute(payload as any, controller),
}));
}
export function makeEventsHandler(
[s, client, err, log]: [SernEmitter, EventEmitter, ErrorHandling, Logging | undefined],
eventsPath: string,
containerGetter: Wrapper['containerConfig'],
) {
const lazy = (k: string) => containerGetter.get(k as keyof Dependencies)[0];
const eventStream$ = eventObservable(eventsPath, s);
export function processEvents({ containerConfig, events }: Wrapper) {
const [
client,
error,
sernEmitter,
logging
] = containerConfig.get('@sern/client', '@sern/errors', '@sern/emitter', '@sern/logger') as [EventEmitter, ErrorHandling, SernEmitter, Logging?];
const lazy = (k: string) => containerConfig.get(k as keyof Dependencies)[0];
const eventStream$ = eventObservable$(events!, sernEmitter);
const emitSuccess$ = (mod: AnyModule) =>
of({ type: PayloadType.Failure, module: mod, reason: SernError.PluginFailure })
.pipe(tap( it => sernEmitter.emit('module.register', it)));
const emitFailure$ = (mod: AnyModule) =>
of({ type: PayloadType.Success, module: mod, } as const)
.pipe(tap(it => sernEmitter.emit('module.register', it)));
const eventCreation$ = eventStream$.pipe(
map(({ mod, absPath }) => ({
mod : {
name: nameOrFilename(mod.name, absPath),
...mod,
} as DefinedEventModule,
absPath
})),
concatMap(processPlugins),
concatMap(resolvePlugins),
//Reduces pluginRes (generated from above) into a single boolean
concatMap(({pluginRes, mod}) => from(pluginRes)
.pipe(
map(pl => pl.execute),
toArray(),
reducePlugins,
map(success => ({ success, mod }))
)),
concatMap(({ success, mod }) =>
iif(() => success,
emitFailure$(mod),
emitSuccess$(mod)
).pipe(
filter(res => res.type === PayloadType.Success),
map(() => mod)
)
),
map(fillDefaults),
callInitPlugins({
onStop: module =>
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onNext: ({ module }) => {
s.emit('module.register', SernEmitter.success(module));
return module;
},
}),
);
eventCreation$.subscribe(e => {
const payload = match(e)
.when(isSernEvent, sernEmitterDispatcher(sernEmitter))
.when(isDiscordEvent, discordEventDispatcher(client))
.when(isExternalEvent, externalEventDispatcher(e => lazy(e.emitter)))
.otherwise(() => error.crash(Error(SernError.InvalidModuleType)));
payload
.execute
.pipe(
concatMap(({event, executeEvent}) =>
executeEvent
.pipe( tap(success => {
if(success)
payload.cmd.execute(event as never);
}), catchError(handleError(error, logging)))
),
).subscribe();
});
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
switch (e.type) {
case EventType.Sern:
return eventDispatcher(e, s);
case EventType.Discord:
return eventDispatcher(e, client);
case EventType.External:
return eventDispatcher(e, lazy(e.emitter));
default:
return err.crash(
Error(SernError.InvalidModuleType + ' while creating event handler'),
);
}
};
eventCreation$
.pipe(
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'an event module reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => {
log?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
function eventObservable$(events: string, emitter: SernEmitter) {
return buildData<EventModule>(events)
.pipe(
errTap(reason =>
emitter.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
}),
),
);
function eventObservable(events: string, emitter: SernEmitter) {
return Files.buildModuleStream<EventModule>(events).pipe(
errTap(reason => {
emitter.emit('module.register', SernEmitter.failure(undefined, reason));
}),
);
}

View File

@@ -0,0 +1,64 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { type Observable, from, mergeMap } from 'rxjs';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import { ImportPayload } from '../../types/handler';
import { pathToFileURL } from 'node:url';
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {
const files = readdirSync(dir);
for (const file of files) {
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
else arrayOfFiles.push(join(dir, '/', file));
}
} catch (err) {
throw err;
}
return arrayOfFiles;
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
// export const isLazy = (n: string) => n.indexOf(".lazy.", n.length-9) !== -1;
export async function defaultModuleLoader<T>(
absPath: string,
): Promise<Result<ImportPayload<T>, SernError>> {
// prettier-ignore
let module: T | undefined
/// #if MODE === 'esm'
= (await import(pathToFileURL(absPath).toString())).default
/// #elif MODE === 'cjs'
= require(absPath).default; // eslint-disable-line
/// #endif
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
}
/**
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildModuleStream<T>(
commandDir: string,
): Observable<Result<ImportPayload<T>, SernError>> {
const commands = getCommands(commandDir);
return from(commands).pipe(mergeMap(defaultModuleLoader<T>));
}
export function fullPathFrom(dir: string) {
return join(process.cwd(), dir);
}
export function getCommands(dir: string): string[] {
return readPath(fullPathFrom(dir));
}

117
src/handler/plugins/args.ts Normal file
View File

@@ -0,0 +1,117 @@
import type { CommandType } from '../structures/enums';
import type { PluginType } from '../structures/enums';
import type { ClientEvents } from 'discord.js';
import type {
BothCommand,
ButtonCommand,
ChannelSelectCommand,
ContextMenuUser,
DiscordEventCommand,
ExternalEventCommand,
MentionableSelectCommand,
ModalSubmitCommand,
RoleSelectCommand,
SernEventCommand,
SlashCommand,
StringSelectCommand,
TextCommand,
UserSelectCommand,
ContextMenuMsg,
Module,
} from '../../types/module';
import type { Args, Payload, Processed, SlashOptions } from '../../types/handler';
import type Context from '../structures/context';
import type { MessageContextMenuCommandInteraction } from 'discord.js';
import type {
ButtonInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import type {
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
ModalSubmitInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { EventType } from '../structures/enums';
type CommandArgsMatrix = {
[CommandType.Text]: {
[PluginType.Control]: [Context, ['text', string[]]];
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
};
[CommandType.Slash]: {
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
[PluginType.Init]: [InitArgs<Processed<SlashCommand>>];
};
[CommandType.Both]: {
[PluginType.Control]: [Context, Args];
[PluginType.Init]: [InitArgs<Processed<BothCommand>>];
};
[CommandType.CtxMsg]: {
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuMsg>>];
};
[CommandType.CtxUser]: {
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuUser>>];
};
[CommandType.Button]: {
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
[PluginType.Init]: [InitArgs<Processed<ButtonCommand>>];
};
[CommandType.StringSelect]: {
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<StringSelectCommand>>];
};
[CommandType.RoleSelect]: {
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<RoleSelectCommand>>];
};
[CommandType.ChannelSelect]: {
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<ChannelSelectCommand>>];
};
[CommandType.MentionableSelect]: {
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<MentionableSelectCommand>>];
};
[CommandType.UserSelect]: {
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<UserSelectCommand>>];
};
[CommandType.Modal]: {
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
};
};
type EventArgsMatrix = {
[EventType.Discord]: {
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
};
[EventType.Sern]: {
[PluginType.Control]: [Payload];
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
};
[EventType.External]: {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
};
export interface InitArgs<T extends Processed<Module>> {
module: T;
absPath: string;
}
export type CommandArgs<
I extends CommandType = CommandType,
J extends PluginType = PluginType,
> = CommandArgsMatrix[I][J];
export type EventArgs<
I extends EventType = EventType,
J extends PluginType = PluginType,
> = EventArgsMatrix[I][J];

View File

@@ -0,0 +1,64 @@
import { CommandType, EventType, PluginType } from '../structures';
import type { Plugin, PluginResult } from '../../types/plugin';
import type { CommandArgs, EventArgs } from './args';
import type { ClientEvents } from 'discord.js';
export const guayin = Symbol('twice<3');
export function makePlugin<V extends unknown[]>(
type: PluginType,
execute: (...args: any[]) => any,
): Plugin<V> {
return {
type,
execute,
[guayin]: undefined,
} as Plugin<V>;
}
/**
* @since 2.5.0
*
*/
export function EventInitPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 2.5.0
*
*/
export function EventControlPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 2.5.0
* @Experimental
* A specialized function for creating control plugins with discord.js ClientEvents.
* Will probably be moved one day!
*/
export function DiscordEventControlPlugin<T extends keyof ClientEvents>(
name: T,
execute: (...args: ClientEvents[T]) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}

View File

@@ -0,0 +1,2 @@
export { EventArgs, InitArgs, CommandArgs } from './args';
export * from './createPlugin';

View File

@@ -1,147 +0,0 @@
/*
* Plugins can be inserted on all commands and are emitted
*
* 1. On ready event, where all commands are loaded.
* 2. On corresponding observable (when command triggers)
*
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { AutocompleteInteraction, Awaitable, ClientEvents } from 'discord.js';
import type { Result, Ok, Err } from 'ts-results-es';
import type { CommandType, SernEventsMapping } from '../../index';
import { EventType, PluginType } from '../../index';
import type { CommandModuleDefs, EventModuleDefs } from '../../types/module';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../structures/events';
export interface Controller {
next: () => Ok<void>;
stop: () => Err<void>;
}
export interface Plugin {
/** @deprecated will be removed in the next update */
name?: string;
/** @deprecated will be removed in the next update */
description?: string;
type: PluginType
}
export interface CommandPlugin<T extends keyof CommandModuleDefs = keyof CommandModuleDefs> extends Plugin {
type: PluginType.Command;
execute: (
payload: {
mod: CommandModuleDefs[T] & { name : string; description : string };
absPath: string;
},
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface DiscordEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod: DiscordEventCommand & { name: string }; absPath: string },
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface ExternalEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod: ExternalEventCommand & { name : string }; absPath: string } ,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface SernEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod : SernEventCommand & { name : string }; absPath: string },
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface AutocompletePlugin extends Plugin {
type: PluginType.Event;
execute: (
autocmp: AutocompleteInteraction,
controlller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface EventPlugin<K extends keyof CommandModuleDefs = keyof CommandModuleDefs> extends Plugin {
type: PluginType.Event;
execute: (
event: Parameters<CommandModuleDefs[K]['execute']>,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface SernEventPlugin<T extends keyof SernEventsMapping = keyof SernEventsMapping> extends Plugin {
name?: T;
type: PluginType.Event;
execute: (
args: SernEventsMapping[T],
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface ExternalEventPlugin extends Plugin {
type: PluginType.Event;
execute: (args: unknown[], controller: Controller) => Awaitable<Result<void, void>>;
}
export interface DiscordEventPlugin<T extends keyof ClientEvents = keyof ClientEvents> extends Plugin {
name?: T;
type: PluginType.Event;
execute: (args: ClientEvents[T], controller: Controller) => Awaitable<Result<void, void>>;
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
};
/**
* Event Module Event Plugins
*/
export type EventModuleEventPluginDefs = {
[EventType.Discord]: DiscordEventPlugin;
[EventType.Sern]: SernEventPlugin;
[EventType.External]: ExternalEventPlugin;
};
/**
* Event Module Command Plugins
*/
export type EventModuleCommandPluginDefs = {
[EventType.Discord]: DiscordEmitterPlugin;
[EventType.Sern]: SernEmitterPlugin;
[EventType.External]: ExternalEmitterPlugin;
};
export type EventModulePlugin<T extends EventType> =
| EventModuleEventPluginDefs[T]
| EventModuleCommandPluginDefs[T];
export type CommandModulePlugin<T extends CommandType> = EventPlugin<T> | CommandPlugin<T>;
/**
* User inputs this type. Sern processes behind the scenes for better usage
*/
export type InputCommandModule = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: CommandModulePlugin<T>[] };
}[CommandType];
export type InputEventModule = {
[T in EventType]: EventModulesNoPlugins[T] & { plugins?: EventModulePlugin<T>[] };
}[EventType];

View File

@@ -1,26 +1,26 @@
import type Wrapper from './structures/wrapper';
import { processEvents } from './events/userDefinedEventsHandling';
import { makeEventsHandler } from './events/userDefinedEventsHandling';
import { CommandType, EventType, PluginType } from './structures/enums';
import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin';
import { makeInteractionCreate } from './events/interactionHandler';
import { makeReadyEvent } from './events/readyHandler';
import { makeMessageCreate } from './events/messageHandler';
import type {
Plugin,
CommandPlugin,
EventModuleCommandPluginDefs,
EventModuleEventPluginDefs,
EventPlugin,
InputCommandModule,
InputEventModule,
} from './plugins/plugin';
import InteractionHandler from './events/interactionHandler';
import ReadyHandler from './events/readyHandler';
import MessageHandler from './events/messageHandler';
import type { CommandModule, CommandModuleDefs, EventModule, EventModuleDefs } from '../types/module';
import { Container, createContainer } from 'iti';
import type { Dependencies, OptionalDependencies } from '../types/handler';
import { composeRoot, containerSubject, useContainer } from './dependencies/provider';
CommandModule,
CommandModuleDefs,
EventModule,
EventModuleDefs,
InputCommand,
InputEvent,
} from '../types/module';
import type { Dependencies, DependencyConfiguration } from '../types/handler';
import { composeRoot, makeFetcher, useContainer } from './dependencies/provider';
import type { Logging } from './contracts';
import { err, ok, partition } from './utilities/functions';
import type { Awaitable, ClientEvents } from 'discord.js';
/**
*
* @since 1.0.0
* @param wrapper Options to pass into sern.
* Function to start the handler up
* @example
@@ -37,19 +37,22 @@ import { err, ok, partition } from './utilities/functions';
*/
export function init(wrapper: Wrapper) {
const logger = wrapper.containerConfig.get('@sern/logger')[0] as Logging | undefined;
const requiredDependenciesAnd = makeFetcher(wrapper);
const startTime = performance.now();
const { events } = wrapper;
if (events !== undefined) {
processEvents(wrapper);
makeEventsHandler(requiredDependenciesAnd([]), events, wrapper.containerConfig);
}
new ReadyHandler(wrapper);
new MessageHandler(wrapper);
new InteractionHandler(wrapper);
const dependencies = requiredDependenciesAnd(['@sern/modules']);
makeReadyEvent(dependencies, wrapper.commands);
makeMessageCreate(dependencies, wrapper.defaultPrefix);
makeInteractionCreate(dependencies);
const endTime = performance.now();
logger?.info({ message: `sern : ${(endTime-startTime).toFixed(2)} ms` });
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
@@ -58,11 +61,15 @@ export const controller = {
};
/**
* @since 1.0.0
* The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommandModule): CommandModule {
const [onEvent, plugins] = partition(mod.plugins ?? [], el => (el as Plugin).type === PluginType.Event);
export function commandModule(mod: InputCommand): CommandModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
onEvent,
@@ -70,40 +77,62 @@ export function commandModule(mod: InputCommandModule): CommandModule {
} as CommandModule;
}
/**
* @since 1.0.0
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEventModule): EventModule {
const [onEvent, plugins] = partition(mod.plugins ?? [], el => (el as Plugin).type === PluginType.Event);
return {
export function eventModule(mod: InputEvent): EventModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
onEvent,
plugins,
} as EventModule;
}
/**
* Create event modules from discord.js client events,
* This is an {@link eventModule} for discord events,
* where typings can be very bad.
* @param mod
*/
export function discordEvent<T extends keyof ClientEvents>(mod: {
name: T;
plugins?: AnyEventPlugin[];
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
}) {
return eventModule({ type: EventType.Discord, ...mod });
}
/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export function makeDependencies<T extends Dependencies>(conf: {
exclude?: Set<OptionalDependencies>,
build: (root: Container<Record<string,any>, {}>) => Container<Partial<T>, T>,
}) {
const container = conf.build(createContainer());
composeRoot(container, conf.exclude ?? new Set());
containerSubject.next(container as unknown as Container<Dependencies, {}>);
export function makeDependencies<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Until there are more optional dependencies, just check if the logger exists
composeRoot(conf);
return useContainer<T>();
}
/**
* @Experimental
* Will be refactored / changed in future
*/
export abstract class CommandExecutable<Type extends CommandType> {
abstract type: Type;
plugins: CommandPlugin<Type>[] = [];
onEvent: EventPlugin<Type>[] = [];
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: CommandModuleDefs[Type]['execute'];
}
/**
* @Experimental
* Will be refactored in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: EventModuleCommandPluginDefs[Type][] = [];
onEvent: EventModuleEventPluginDefs[Type][] = [];
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: EventModuleDefs[Type]['execute'];
}

View File

@@ -1,6 +1,11 @@
import { EventEmitter } from 'events';
import type { SernEventsMapping } from '../types/handler';
import type { Payload, SernEventsMapping } from '../types/handler';
import { PayloadType } from './structures';
import type { Module } from '../types/module';
/**
* @since 1.0.0
*/
class SernEmitter extends EventEmitter {
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
@@ -35,6 +40,49 @@ class SernEmitter extends EventEmitter {
): boolean {
return super.emit(eventName, ...args);
}
private static payload<T extends Payload>(
type: PayloadType,
module?: Module,
reason?: unknown,
) {
return { type, module, reason } as T;
}
/**
* Creates a compliant SernEmitter failure payload
* @param module
* @param reason
*/
static failure(module?: Module, reason?: unknown) {
//The generic cast Payload & { type : PayloadType.* } coerces the type to be a failure payload
// same goes to the other methods below
return SernEmitter.payload<Payload & { type: PayloadType.Failure }>(
PayloadType.Failure,
module,
reason,
);
}
/**
* Creates a compliant SernEmitter module success payload
* @param module
*/
static success(module: Module) {
return SernEmitter.payload<Payload & { type: PayloadType.Success }>(
PayloadType.Success,
module,
);
}
/**
* Creates a compliant SernEmitter module warning payload
* @param reason
*/
static warning(reason: unknown) {
return SernEmitter.payload<Payload & { type: PayloadType.Warning }>(
PayloadType.Warning,
undefined,
reason,
);
}
}
export default SernEmitter;

View File

@@ -11,25 +11,23 @@ import { Result as Either, Ok as Left, Err as Right } from 'ts-results-es';
import type { ReplyOptions } from '../../types/handler';
import { SernError } from './errors';
function safeUnwrap<T>(res: Either<T, T>) {
return res.val;
}
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export default class Context {
private constructor(
private ctx: Either<Message, ChatInputCommandInteraction>,
) {}
private constructor(private ctx: Either<Message, ChatInputCommandInteraction>) {}
/**
* Getting the Message object. Crashes if module type is
* CommandType.Slash or the event fired in a Both command was
* ChatInputCommandInteraction
*/
public get message() {
public get message() {
return this.ctx.expect(SernError.MismatchEvent);
}
/**
@@ -42,71 +40,45 @@ export default class Context {
}
public get id(): Snowflake {
return safeUnwrap(
this.ctx
.map(m => m.id)
.mapErr(i => i.id));
return this.ctx.val.id;
}
public get channel() {
return safeUnwrap(this.ctx
.map(m => m.channel)
.mapErr(i => i.channel));
return this.ctx.val.channel;
}
/**
* If context is holding a message, message.author
* else, interaction.user
*/
public get user(): User {
return safeUnwrap(this.ctx
.map(m => m.author)
.mapErr(i => i.user)
);
return safeUnwrap(this.ctx.map(m => m.author).mapErr(i => i.user));
}
public get createdTimestamp(): number {
return safeUnwrap(this.ctx
.map(m => m.createdTimestamp)
.mapErr(i => i.createdTimestamp)
);
return this.ctx.val.createdTimestamp;
}
public get guild() {
return safeUnwrap(this.ctx
.map(m => m.guild)
.mapErr(i => i.guild)
);
return this.ctx.val.guild;
}
public get guildId() {
return safeUnwrap(this.ctx
.map(m => m.guildId)
.mapErr(i => i.guildId)
);
return this.ctx.val.guildId;
}
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
public get member() {
return safeUnwrap(this.ctx
.map(m => m.member)
.mapErr(i => i.member)
);
return this.ctx.val.member;
}
public get client(): Client {
return safeUnwrap(this.ctx
.map(m => m.client)
.mapErr(i => i.client)
);
return this.ctx.val.client;
}
public get inGuild(): boolean {
return safeUnwrap(this.ctx
.map(m=>m.inGuild())
.mapErr(i => i.inGuild())
);
return this.ctx.val.inGuild();
}
public isMessage() {
return this.ctx.map(() => true).unwrapOr(false);
@@ -124,9 +96,12 @@ export default class Context {
}
public reply(content: ReplyOptions) {
return safeUnwrap(this.ctx
.map( m => m.reply(content as string | MessageReplyOptions))
.mapErr(i => i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()))
return safeUnwrap(
this.ctx
.map(m => m.reply(content as string | MessageReplyOptions))
.mapErr(i =>
i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()),
),
);
}
}

View File

@@ -1,4 +1,5 @@
/**
* @since 1.0.0
* A bitfield that discriminates command modules
* @enum { number }
* @example
@@ -51,8 +52,8 @@ export enum CommandType {
*/
ChannelSelect = 256,
MentionableSelect = 512,
RoleSelect= 1024,
UserSelect= 2048
RoleSelect = 1024,
UserSelect = 2048,
}
/**
@@ -102,13 +103,23 @@ export enum EventType {
*/
export enum PluginType {
/**
* The PluginType for CommandPlugins
* The PluginType for InitPlugins
*/
Init = 1,
/**
* @deprecated
* Use PluginType.Init instead
*/
Command = 1,
/**
* The PluginType for EventPlugins
* @deprecated
* Use PluginType.Control instead
*/
Event = 2,
/**
* The PluginType for EventPlugins
*/
Control = 2,
}
/**
* @enum { string }

View File

@@ -34,5 +34,5 @@ export enum SernError {
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`
MissingRequired = `@sern/client is required but was not found`,
}

View File

@@ -1,38 +0,0 @@
import type { SernEventsMapping } from '../../types/handler';
import type {
DiscordEmitterPlugin,
DiscordEventPlugin,
ExternalEmitterPlugin,
ExternalEventPlugin,
SernEmitterPlugin,
SernEventPlugin,
} from '../plugins/plugin';
import type { Awaitable, ClientEvents } from 'discord.js';
import type { EventType } from './enums';
import type { Module } from '../../types/module';
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping> extends Module {
name?: T;
type: EventType.Sern;
onEvent: SernEventPlugin[];
plugins: SernEmitterPlugin[];
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents> extends Module {
name?: T;
type: EventType.Discord;
onEvent: DiscordEventPlugin[];
plugins: DiscordEmitterPlugin[];
execute(...args: ClientEvents[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string
emitter: string;
type: EventType.External;
onEvent: ExternalEventPlugin[];
plugins: ExternalEmitterPlugin[];
execute(...args: unknown[]): Awaitable<unknown>;
}

View File

@@ -1,10 +1,6 @@
import Context from './context';
import type Wrapper from './wrapper';
import { ModuleStore } from './moduleStore';
export * from './errors';
export * from './enums';
export {
Context,
Wrapper,
ModuleStore
};
export { Context, Wrapper, ModuleStore };

View File

@@ -1,26 +1,27 @@
import type { CommandModule } from '../../types/module';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
* Storing all command modules
* This dependency is usually injected into ModuleManager
*/
export class ModuleStore {
readonly BothCommands = new Map<string, CommandModule>();
readonly ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, CommandModule>(),
[ApplicationCommandType.Message]: new Map<string, CommandModule>(),
[ApplicationCommandType.ChatInput]: new Map<string, CommandModule>(),
readonly BothCommands = new Map<string, Processed<CommandModule>>();
readonly ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.Message]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.ChatInput]: new Map<string, Processed<CommandModule>>(),
};
readonly ModalSubmit = new Map<string, CommandModule>();
readonly TextCommands = new Map<string, CommandModule>();
readonly ModalSubmit = new Map<string, Processed<CommandModule>>();
readonly TextCommands = new Map<string, Processed<CommandModule>>();
readonly InteractionHandlers = {
[ComponentType.Button]: new Map<string, CommandModule>(),
[ComponentType.StringSelect]: new Map<string, CommandModule>(),
[ComponentType.ChannelSelect] : new Map<string, CommandModule>(),
[ComponentType.MentionableSelect] : new Map<string, CommandModule>(),
[ComponentType.RoleSelect] : new Map<string, CommandModule>(),
[ComponentType.UserSelect] : new Map<string, CommandModule>(),
[ComponentType.Button]: new Map<string, Processed<CommandModule>>(),
[ComponentType.StringSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.ChannelSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.MentionableSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.RoleSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.UserSelect]: new Map<string, Processed<CommandModule>>(),
};
}

View File

@@ -1,15 +1,20 @@
import type { Dependencies } from '../../types/handler';
/**
* @since 1.0.0
* An object to be passed into Sern#init() function.
* @typedef {object} Wrapper
*/
interface Wrapper {
/**
* @deprecated
* This will be moved to a new field in 3.0.0
*/
readonly defaultPrefix?: string;
readonly commands: string;
readonly events?: string;
readonly containerConfig : {
readonly containerConfig: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
}
};
}
export default Wrapper;

View File

@@ -1,9 +0,0 @@
import type { Awaitable } from 'discord.js';
export async function arrAsync<T>(promiseLike: Awaitable<T>[]): Promise<T[]> {
const arr: T[] = [];
for await (const el of promiseLike) {
arr.push(el);
}
return arr;
}

View File

@@ -1,20 +1,19 @@
import * as Files from './readFile';
import * as Files from '../module-loading/readFile';
import { basename } from 'path';
import { Err, Ok, Result } from 'ts-results-es';
import { Observable, of, switchMap } from 'rxjs';
import { Err, Ok } from 'ts-results-es';
/**
* A function that returns whatever value is provided.
* Used for singleton in iti
* Warning: this evaluates { @param value }. It does not defer a value.
* @param value
* @__PURE__
*/
// prettier-ignore
export const _const = <T>(value: T) => () => value;
/**
* A function that returns another function
* Used for transient in iti
* @param value
*
* @param modName
* @param absPath
*/
export const transient = <T>( value : T) => () => _const(value);
export function nameOrFilename(modName: string | undefined, absPath: string) {
return modName ?? Files.fmtFileName(basename(absPath));
}
@@ -23,20 +22,15 @@ export function nameOrFilename(modName: string | undefined, absPath: string) {
export const ok = _const(Ok.EMPTY);
export const err = _const(Err.EMPTY);
export function partition<T, V>(arr: (T & V)[], condition: (e: (T & V)) => boolean) : [T[], V[]] {
const t : T[] = [];
const v : V[] = [];
for(const el of arr) {
if(condition(el)) {
export function partition<T, V>(arr: (T & V)[], condition: (e: T & V) => boolean): [T[], V[]] {
const t: T[] = [];
const v: V[] = [];
for (const el of arr) {
if (condition(el)) {
t.push(el as T);
} else {
v.push(el as V);
}
}
return [ t, v ];
return [t, v];
}
export function reducePlugins(src: Observable<Result<void, void>[]>) : Observable<boolean> {
return src.pipe(switchMap(s => of(s.every((a) => a.ok))));
}

View File

@@ -1,15 +0,0 @@
import type { Message } from 'discord.js';
/**
* Removes the first character(s) _[depending on prefix length]_ of the message
* @param msg
* @param prefix The prefix to remove
* @returns The message without the prefix
* @example
* message.content = '!ping';
* console.log(fmt(message, '!'));
* // [ 'ping' ]
*/
export function fmt(msg: Message, prefix: string): string[] {
return msg.content.slice(prefix.length).trim().split(/\s+/g);
}

View File

@@ -1,23 +0,0 @@
import type { DiscordEventCommand, ExternalEventCommand, SernEventCommand } from '../structures/events';
import { CommandModule, EventType } from '../..';
import type { AnyModule, CommandModuleDefs, EventModule } from '../../types/module';
export function correctModuleType<T extends keyof CommandModuleDefs>(
plug: AnyModule | undefined,
type: T,
): plug is CommandModuleDefs[T] {
// Another way to check if type is equivalent,
// It will check based on flag system instead
return plug !== undefined && (plug.type & type) !== 0;
}
export function isDiscordEvent(el: EventModule | CommandModule): el is DiscordEventCommand {
return el.type === EventType.Discord;
}
export function isSernEvent(el: EventModule | CommandModule): el is SernEventCommand {
return el.type === EventType.Sern;
}
export function isExternalEvent(el: EventModule | CommandModule): el is ExternalEventCommand {
return el.type === EventType.External && 'emitter' in el;
}

View File

@@ -1,65 +0,0 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { type Observable, from, concatAll } from 'rxjs';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {
const files = readdirSync(dir);
for (const file of files) {
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
else arrayOfFiles.push(join(dir, '/', file));
}
} catch (err) {
throw err;
}
return arrayOfFiles;
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/**
*
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildData<T>(commandDir: string): Observable<
Result<
{
mod: T;
absPath: string;
},
SernError
>
> {
const commands = getCommands(commandDir);
return from(
Promise.all(
commands.map(async absPath => {
let mod: T | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
mod = require(absPath).default;
} catch {
mod = (await import(`file:///` + absPath)).default;
}
if (mod === undefined) {
return Err(SernError.UndefinedModule);
}
try {
mod = new (mod as unknown as new () => T)();
} catch {}
return Ok({ mod, absPath });
}),
),
).pipe(concatAll());
}
export function getCommands(dir: string): string[] {
return readPath(join(process.cwd(), dir));
}

View File

@@ -1,42 +1,54 @@
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
import assert from 'assert';
/**
* Uses an iterative DFS to check if an autocomplete node exists
* @param iAutocomplete
* @param options
*/
export default function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData | undefined {
if (options === undefined) return undefined;
const _options = options.slice(); // required to prevent direct mutation of options
let autocompleteData: SernAutocompleteData | undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({...a}));
let subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand:
{
for (const option of cur.options ?? []) {
_options.push(option);
{
subcommands.add(cur.name);
for (const option of cur.options ?? [])
_options.push(option);
}
}
break;
case ApplicationCommandOptionType.SubcommandGroup:
{
for (const command of cur.options ?? []) {
_options.push(command);
{
for (const command of cur.options ?? [])
_options.push(command);
}
}
break;
default:
{
if (cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
if (cur.name === choice.name && cur.autocomplete) {
autocompleteData = cur;
{
if ('autocomplete' in cur && cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
assert('command' in cur, "No command property found for autocomplete option");
if(subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches = subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return cur;
}
} else {
if(cur.name === choice.name) {
return cur;
}
}
}
}
}
break;
}
}
return autocompleteData;
}
}}

View File

@@ -1,11 +1,18 @@
import SernEmitter from './handler/sernEmitter';
export { eventModule, commandModule, EventExecutable, CommandExecutable } from './handler/sern';
export {
eventModule,
commandModule,
EventExecutable,
CommandExecutable,
controller,
discordEvent,
} from './handler/sern';
export * as Sern from './handler/sern';
export * from './types/handler';
export * from './types/module';
export * from './handler/structures/structxports';
export * from './handler/plugins/plugin';
export * from './handler/contracts/index';
export * from './types/plugin';
export * from './handler/structures';
export * from './handler/plugins';
export * from './handler/contracts';
export { SernEmitter };
export { _const as single, transient as many } from './handler/utilities/functions';
export { useContainerRaw } from './handler/dependencies/provider';
export * from './handler/dependencies';

View File

@@ -7,6 +7,7 @@ import type { UnpackFunction } from 'iti';
import type { ErrorHandling, Logging, ModuleManager } from '../handler/contracts';
import type { ModuleStore } from '../handler/structures/moduleStore';
import type SernEmitter from '../handler/sernEmitter';
import type { Container } from 'iti';
// Thanks to @kelsny
export type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
@@ -20,38 +21,51 @@ export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' |
* After modules are transformed, name and description are given default values if none
* are provided to Module. This type represents that transformation
*/
export type DefinedCommandModule = CommandModule & { name: string; description: string };
export type DefinedEventModule = EventModule & { name: string };
export type AnyDefinedModule = DefinedCommandModule | DefinedEventModule
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string};
| { type: PayloadType.Warning; reason: string };
export type SernEventsMapping = {
'module.register': [Payload];
'module.activate': [Payload];
'error': [Payload];
'warning': [Payload];
error: [Payload];
warning: [Payload];
};
export type LogPayload<T = unknown> = { message: T }
export type Singleton<T> = () => T
export type LogPayload<T = unknown> = { message: T };
export type Singleton<T> = () => T;
export type Transient<T> = () => () => T;
export interface Dependencies {
'@sern/client': Singleton<EventEmitter>;
'@sern/logger'?: Singleton<Logging>;
'@sern/emitter': Singleton<SernEmitter>;
'@sern/store' : Singleton<ModuleStore>;
'@sern/modules' : Singleton<ModuleManager>;
'@sern/store': Singleton<ModuleStore>;
'@sern/modules': Singleton<ModuleManager>;
'@sern/errors': Singleton<ErrorHandling>;
}
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
export type MapDeps<
Deps extends Dependencies,
T extends readonly unknown[]
> = T extends [infer First extends keyof Deps, ...infer Rest extends readonly unknown[]]
? [ UnpackFunction<Deps[First]>, ...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps,Rest>)] : [never]
export type ReplyOptions =
| string
| Omit<InteractionReplyOptions, 'fetchReply'>
| MessageReplyOptions;
//prettier-ignore
export type MapDeps<Deps extends Dependencies, T extends readonly unknown[]> = T extends [
infer First extends keyof Deps,
...infer Rest extends readonly unknown[],
]
? [
UnpackFunction<Deps[First]>,
...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps, Rest>),
]
: [never];
//Basically, '@sern/client' | '@sern/store' | '@sern/modules' | '@sern/error' | '@sern/emitter' will be provided defaults, and you can exclude the rest
export type OptionalDependencies = '@sern/logger';
export type OptionalDependencies = '@sern/logger';
export type Processed<T> = T & { name: string; description: string };
export type Deprecated<Message extends string> = [never, Message];
export interface DependencyConfiguration<T extends Dependencies> {
exclude?: Set<OptionalDependencies>;
build: (root: Container<Omit<Dependencies, '@sern/client'>, {}>) => Container<T, {}>;
}
export type ImportPayload<T> = { module: T; absPath: string };

View File

@@ -18,123 +18,120 @@ import type {
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction
StringSelectMenuInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../handler/structures/events';
import { CommandType } from '../handler/structures/enums';
import type { Args, SlashOptions } from './handler';
import type Context from '../handler/structures/context';
import type { AutocompletePlugin, CommandPlugin, EventPlugin } from '../handler/plugins/plugin';
import type { InitPlugin, ControlPlugin } from './plugin';
import { EventType } from '../handler/structures/enums';
import type { UserSelectMenuInteraction } from 'discord.js';
import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
import type { SernEventsMapping } from './handler';
import type { ClientEvents } from 'discord.js';
export interface Module {
type?: CommandType | EventType;
type: CommandType | EventType;
name?: string;
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
execute: (...args: any[]) => any
execute: (...args: any[]) => Awaitable<any>;
}
export interface TextCommand extends Module {
type: CommandType.Text;
onEvent: EventPlugin<CommandType.Text>[];
plugins: CommandPlugin[];
alias?: string[];
execute: (ctx: Context, args: ['text', string[]]) => Awaitable<unknown>;
}
export interface SlashCommand extends Module {
type: CommandType.Slash;
onEvent: EventPlugin<CommandType.Slash>[];
plugins: CommandPlugin[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: ['slash', SlashOptions]) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
onEvent: EventPlugin<CommandType.Both>[];
plugins: CommandPlugin[];
alias?: string[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
type: CommandType.CtxUser;
onEvent: EventPlugin<CommandType.CtxUser>[];
plugins: CommandPlugin[];
execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg;
onEvent: EventPlugin<CommandType.CtxMsg>[];
plugins: CommandPlugin[];
execute: (ctx: MessageContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ButtonCommand extends Module {
type: CommandType.Button;
onEvent: EventPlugin<CommandType.Button>[];
plugins: CommandPlugin[];
execute: (ctx: ButtonInteraction) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
onEvent: EventPlugin<CommandType.StringSelect>[];
plugins: CommandPlugin[];
execute: (ctx: StringSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
onEvent: EventPlugin<CommandType.ChannelSelect>[];
plugins: CommandPlugin[];
execute: (ctx: ChannelSelectMenuInteraction) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
onEvent: EventPlugin<CommandType.RoleSelect>[];
plugins: CommandPlugin[];
execute: (ctx: RoleSelectMenuInteraction) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
onEvent: EventPlugin<CommandType.MentionableSelect>[];
plugins: CommandPlugin[];
execute: (ctx: MentionableSelectMenuInteraction) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
onEvent: EventPlugin<CommandType.UserSelect>[];
plugins: CommandPlugin[];
execute: (ctx: UserSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
onEvent: EventPlugin<CommandType.Modal>[];
plugins: CommandPlugin[];
execute: (ctx: ModalSubmitInteraction) => Awaitable<unknown>;
}
export interface AutocompleteCommand extends Module {
name?: never;
description?: never;
type?: never;
onEvent: AutocompletePlugin[];
export interface AutocompleteCommand
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description'> {
onEvent: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
extends Module {
name?: T;
type: EventType.Sern;
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
extends Module {
name?: T;
type: EventType.Discord;
execute(...args: ClientEvents[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: string;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
@@ -162,10 +159,10 @@ export type CommandModuleDefs = {
[CommandType.CtxUser]: ContextMenuUser;
[CommandType.Button]: ButtonCommand;
[CommandType.StringSelect]: StringSelectCommand;
[CommandType.RoleSelect] : RoleSelectCommand;
[CommandType.ChannelSelect] : ChannelSelectCommand;
[CommandType.MentionableSelect] : MentionableSelectCommand;
[CommandType.UserSelect] : UserSelectCommand;
[CommandType.RoleSelect]: RoleSelectCommand;
[CommandType.ChannelSelect]: ChannelSelectCommand;
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
@@ -175,7 +172,8 @@ export type EventModuleDefs = {
[EventType.External]: ExternalEventCommand;
};
export interface SernAutocompleteData extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
autocomplete: true;
type:
| ApplicationCommandOptionType.String
@@ -184,6 +182,21 @@ export interface SernAutocompleteData extends Omit<BaseApplicationCommandOptions
command: AutocompleteCommand;
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type InputEvent = {
[T in EventType]: EventModulesNoPlugins[T] & { plugins?: AnyEventPlugin[] };
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: AnyCommandPlugin[] };
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
@@ -212,4 +225,4 @@ export type SernOptionsData<U extends ApplicationCommandOptionData = Application
? SernSubCommandData
: U extends ApplicationCommandSubGroupData
? SernSubCommandGroupData
: BaseOptions;
: BaseOptions;

74
src/types/plugin.ts Normal file
View File

@@ -0,0 +1,74 @@
/*
* Plugins can be inserted on all commands and are emitted
*
* 1. On ready event, where all commands are loaded.
* 2. On corresponding observable (when command triggers)
*
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { Awaitable } from 'discord.js';
import type { Err, Ok, Result } from 'ts-results-es';
import type { PluginType } from '../handler/structures/enums';
import type { CommandModule, EventModule } from './module';
import type { CommandArgs, InitArgs } from '../handler/plugins';
import type { Deprecated, Processed } from './handler';
import type { CommandType } from '../handler/structures/enums';
export type PluginResult = Awaitable<VoidResult>;
export type VoidResult = Result<void, void>;
export interface Controller {
next: () => Ok<void>;
stop: () => Err<void>;
}
export interface Plugin<Args extends any[] = any[]> {
type: PluginType;
execute: (...args: Args) => PluginResult;
}
export interface InitPlugin<Args extends any[] = any[]> {
type: PluginType.Init;
execute: (...args: Args) => PluginResult;
}
export interface ControlPlugin<Args extends any[] = any[]> {
type: PluginType.Control;
execute: (...args: Args) => PluginResult;
}
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<CommandModule>>]>;
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<EventModule>>]>;
/**
* @deprecated
* Use the newer helper functions and import { controller } from '@sern/handler'
*/
export interface CommandPlugin<T extends CommandType = CommandType> {
name?: string;
description?: string;
type: PluginType.Command;
execute: (
m: InitArgs<Processed<CommandModule>>,
controller?: Deprecated<'Please import controller instead'>,
) => PluginResult;
}
/**
* @deprecated
* Use the newer helper functions
*/
export interface EventPlugin<T extends CommandType> {
name?: string;
description?: string;
type: PluginType.Event;
execute: (args: CommandArgs<T, PluginType.Event>, controller?: Controller) => PluginResult;
}
export type DiscordEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type ExternalEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type SernEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type AutocompletePlugin = Deprecated<'Please view alternatives: '>;
export type SernEventPlugin = Deprecated<'Please view alternatives: '>;
export type ExternalEventPlugin = Deprecated<'Please view alternatives: '>;
export type DiscordEventPlugin = Deprecated<'Please view alternatives: '>;

View File

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"baseUrl": ".",
"resolveJsonModule": true,
"moduleResolution": "node",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/secrets.json", "src"]
}

View File

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"baseUrl": ".",
"resolveJsonModule": true,
"moduleResolution": "node",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/secrets.json", "src"]
}

View File

@@ -6,10 +6,10 @@
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"preserveSymlinks": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},

3
tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "./tsconfig-esm.json"
}

View File

@@ -1,10 +1,12 @@
import { defineConfig } from 'tsup';
import { writeFile } from 'fs/promises';
import ifdefPlugin from 'esbuild-ifdef';
const shared = {
entry: ['src/index.ts'],
external: ['discord.js'],
platform: 'node',
clean: true,
sourcemap: false
sourcemap: false,
};
export default defineConfig([
{
@@ -12,19 +14,23 @@ export default defineConfig([
target: 'node16',
tsconfig: './tsconfig-esm.json',
outDir: './dist/esm',
external: ['discord.js'],
treeshake: true,
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'esm' }, verbose: true })],
outExtension() {
return {
js: '.mjs',
};
},
...shared
async onSuccess() {
console.log('writing json esm');
await writeFile('./dist/esm/package.json', JSON.stringify({ type: 'module' }));
},
...shared,
},
{
format: 'cjs',
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'cjs' }, verbose: true })],
splitting: false,
external: ['discord.js'],
target: 'node16',
tsconfig: './tsconfig-cjs.json',
outDir: './dist/cjs',
@@ -33,6 +39,10 @@ export default defineConfig([
js: '.cjs',
};
},
...shared
async onSuccess() {
console.log('writing json commonjs');
await writeFile('./dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }));
},
...shared,
},
]);

3017
yarn.lock Normal file

File diff suppressed because it is too large Load Diff