Compare commits

...

107 Commits

Author SHA1 Message Date
github-actions[bot]
504cdee7b2 chore(main): release 3.2.1 (#344)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-21 12:49:42 -06:00
Jacob Nguyen
c7661f272c chore: bump version 2023-12-21 12:47:24 -06:00
Jacob Nguyen
daac37c288 fix: logger swap failing 2023-12-21 12:47:02 -06:00
ysf
a579e272d0 revolutionary (#342) 2023-12-15 17:03:23 -06:00
github-actions[bot]
2051aa1ac0 chore(main): release 3.2.0 (#341)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-15 16:23:39 -06:00
Jacob Nguyen
237c8537c6 chore: release 3.2.0
Release-As: 3.2.0
2023-12-15 16:19:38 -06:00
Jacob Nguyen
77fb00d386 feat/abstractiti (#340)
* progress on better error handling

* wiring onError callback through module loader and resolver

* fix error callbacks not being stored

* update onError to be record

* type alias

* wiring

* seems to work

* update error handling contract and wire more

* add command error builder

* fix merge

* progress on error handling

* naive onError handling, not tested

* progres

* proress

* progress on abstracting away iti

* seems to work

* fix tests

* better typings

* add doc

* abstracting iti

* remove onerror for this pr

* feat: better way to add dependencies

* fix tests
2023-12-15 16:09:13 -06:00
renovate[bot]
89f6bbb975 chore(deps): update google-github-actions/release-please-action digest to db8f2c6 (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-21 10:18:07 -06:00
github-actions[bot]
8ef4ee87e9 chore(main): release 3.1.1 (#338)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-05 21:25:18 -06:00
Neo
fd39858636 fix: queuing events (#332) @Benzo-Fury (#333)
fix: queuing events

Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-11-05 21:23:27 -06:00
Jacob Nguyen
132b625070 refactor: rm redudant fns and formatting 2023-11-04 16:57:13 -05:00
renovate[bot]
03439fec43 chore(deps): update google-github-actions/release-please-action digest to 4c5670f (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-21 22:30:40 -05:00
Jacob Nguyen
fc87e99ed0 Update README.md 2023-09-09 01:08:16 -05:00
renovate[bot]
a08541a8e7 chore(deps): update actions/checkout digest to f43a0e5 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 17:06:17 -05:00
renovate[bot]
8bd5eb4949 chore(deps): lock file maintenance (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 17:05:33 -05:00
github-actions[bot]
e1059f93f7 chore(main): release 3.1.0 (#330)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-04 15:43:47 -05:00
renovate[bot]
800531453f chore(deps): pin dependencies (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-22 11:37:22 -05:00
Jacob Nguyen
c9f2d75665 deprecate: mode (#325)
* test: add tests for context

* deprecate: mode

* revert docs for deprecated option
2023-08-19 07:07:47 +05:30
Jacob Nguyen
e59e0b9d40 test: add tests for context (#324) 2023-08-18 10:46:46 -05:00
Jacob Nguyen
26ccd118ff feat: dispose hooks (deprecate useContainerRaw) (#323)
* feat: dispose hooks

* build: unminify, add source map, deprecate useContainerRaw

* fix regression of context and fix tsup build
2023-08-17 12:51:24 -05:00
Jacob Nguyen
4b97d86908 chore: upgrade ts-results-es (#322) 2023-08-13 10:55:39 -05:00
xxDeveloper
b1c82448bd chore: Create FUNDING.yml (#321)
* chore: Create FUNDING.yml

* Rename FUNDING.yml to .github/FUNDING.yml
2023-08-08 10:29:40 +03:00
Jacob Nguyen
d80081384a Update README.md 2023-08-07 17:23:58 -05:00
mina
50253ca322 feat: add guaranteed channelId and userId getters to Context (#320) 2023-08-06 15:28:38 -05:00
github-actions[bot]
215aca2f46 chore(main): release 3.0.2 (#319)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-06 10:45:17 -05:00
Jacob Nguyen
a7f5ea269f fix: invalid id for cts, mts, cjs, mjs files, node paths (#318)
* better error messages

* fix: invalid id for cts, mts, cjs, mjs files
2023-08-06 10:43:34 -05:00
Jacob Nguyen
52d6368440 Delete codeql-analysis.yml 2023-08-06 00:36:50 -05:00
Jacob Nguyen
1e723a4154 Update npm-publish.yml 2023-08-06 00:34:43 -05:00
Jacob Nguyen
5fe13f43d2 better npm-publish.yml 2023-08-06 00:33:40 -05:00
Jacob Nguyen
ab9d39306a Create test.yml (#317)
* Create test.yml

* Update test.yml

* Update test.yml
2023-08-06 00:29:43 -05:00
github-actions[bot]
d429f3adbf chore(main): release 3.0.1 (#316)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-04 19:05:48 -05:00
Jacob Nguyen
5e011b471e remove warning 2023-08-04 19:01:27 -05:00
Jacob Nguyen
41344608c6 fix: collectors 2023-08-04 19:00:50 -05:00
github-actions[bot]
7a72cc4fe3 chore(main): release 3.0.0 (#315)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-29 17:13:42 -05:00
Jacob Nguyen
70cca0dbb0 chore: release 3.0.0
Release-As: 3.0.0
2023-07-29 17:11:51 -05:00
Jacob Nguyen
7798e36458 feat!: v3 (#294)
* refactor: move things to core, imports not fixed yet

* work on strategy and lifted Context

* remove id from lifted Context

* refactor: remove dependence on discord.js for module stoore

* moving and fixing imports

* chore: move operators into core

* chore: fix paths

* add wrapper platform field

* add deprecation warning

* chore:update paths

* chore:remove const function

* chore: remove deprecated symbols

* docs: add documentation to internal function

* chore: remove deprecated support for plugins

* chore: remove dependence on discord.js Awaitable type

* chore: update typings

* lift requiredDependencyKeys out of makeFetcher

* move strategy to index.ts and add adapters

* chore: fix typings

* chore: move command args matrix as binding

* feat: make Context platform specific, CoreContext as Core

* chore: remove extra file

* chore: move prettier into package.json

* chore(core): update imports and operators

* chore(core): add DefaultWrapper as sern classic

* move eslint and prettier configs to json

* chore: remove utils folder in favor of single file

* chore: remove redundant directories for single files

* chore: remove redundant directories for single files

* refactor: move and update things

* chore: move commands into seperate file

* chore: serverless work

* chore: remove redundant directories for single files

* chore: rename, wip refactoring

* chore: redundant directory

* refactor: internalize operators

* feat!: new module resolution algorithm

* chore: refactor and move things

* chore: refactor and add multiplatform typings

* chore: remove leaky import

* chore: add agnostic predicates

* chore: add old context here until i figure out what to do

* chore: update Proccessed typing to ./core

* chore: add tweetnacl

* revert: multiplatform

* revert: multiplatform

* chore: modularize and split typings

* chore: revert multiplatform

* chore: revert multi and mov sernEmitter

* chore: revert multi and clean up code

* refactor: add createGenericHandler

* refactor: remove unneeded signatures and fix imports

* feat: add getPublishableCommands to ModuleManager

* chore: remove bad imports

* style: pretty

* revert: remove AnyDependencies type

* refactor: fold switch case

* docs: specifics

* chore: change all file names to camel case

* refactor: change all files to camelcase and refactor

* revert: remove cloudflare typings

* feat: SernEmitter now captures promise rejections

* chore: fix InitArgs missing

* chore: move typings

* chore: move and clean

* chore: delete plugins dir

* chore: cleanup dispatchers subdirectory for single file

* chore: move context into structures directory

* refactor: cleaning up code and renaming variables

* chore: update name of function to reflect use

* revert: multiple entry points

* revert: readd discordEvent

* refactor: rename, format, move things

* feat: types organization and cleaning up code base

* fix: unaliased modules would throw error

* build: speed up build

* revert: readd module store and add contract

* add separate id for id processing

* chore: progress of globalizing dependencies type

* chore: update container and init hook progress

* style: format & lint

* feat: dev and prod mode

* fix: directories ignoring incorrectly

* refactor: move metadata outside of module declarations

* revert: re export command executable and event executable

* refactor: a lot

* fix: plugins for class modules and module loader

* style: pretty

* fix class based module loading

* feat: globalize dependencies type

* revert: internal name

* feat: add new sern emitter event

* refactor: remove cast

* refactor: add better typings for sern event modules

* test: add tests

* test: add more tests

* feat: change error handling contract

* chore: make changes in codebase after error contract change

* docs: add purpose of d.ts file

* revert removal of crash method and mark deprecated

* fix: typings for options- have access to all properties now

* refactor: npx knip

* 3.0.0-rc1

* chore: fix for version 3 and reexport old types

* fix: reexport payload and button modules

* fix: component commands incorrectly aligned and ordered

* chore: bump version

* test: add id generation testing

* refactor: algorithm for module resolution

* chore: bump vers

* test: add eventDispatcher test

* *.test.ts

* fix: autocomplete nested option

* chore: bump vers

* add npmignore .yarn

* feat: experimental loading sern.config.json

* refactor: simplify build

* chore: bump vers

* chore: add documentation for service api

* add since

* feat: add possible mode option in file loading mode

* refactor: remove two unneeded functions and refactor to throw early

* refactor: clean up handler code

* fix: undefined this binding

* refactor: clean up signatures and types

* refactor: make evident the internal api and move around stuff

* refactor: remove circular dependencies

* fix circulars and imports

* oops, moving around mroe stuff

* refresh lock

* chore: import type and prettier

* style: prettier

* feat: solidify init logic

* fix module-loading.ts

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-07-29 17:10:19 -05:00
Peter-MJ-Parker
9144485c39 My bot uses sern! (#313)
feat: My bot uses sern!
2023-07-03 21:24:05 -05:00
Evo
cf15b67ede chore: bless seren (#310) 2023-06-19 07:20:24 -05:00
jacob
57cc94ff81 Empty-Commit 2023-06-18 09:59:38 -05:00
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
112 changed files with 8063 additions and 3808 deletions

View File

@@ -1,13 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"extends": ["plugin:@typescript-eslint/recommended"],
"parserOptions": { "ecmaVersion": "latest", "sourceType": "script" },
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": ["error", "always"],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": "off"
}
}

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
open_collective: sern

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

@@ -1,37 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '37 20 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -3,13 +3,14 @@ name: Continuous Integration
on:
# Trigger the workflow on push or pull request or custom
push:
branches:
main
branches: [main]
paths:
- '**.ts'
- '*.ts'
pull_request_target:
branches:
main
paths:
- '*ts'
workflow_dispatch:
jobs:
@@ -19,7 +20,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Set up Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
@@ -27,18 +28,18 @@ jobs:
node-version: 17
- name: Install pnpm
run: npm i -g pnpm
run: npm i -g yarn
# Prettier must be in `package.json`
- name: Install Node.js dependencies
run: pnpm i
run: yarn --immutable
- name: Run Prettier
run: pnpm run pretty
run: yarn pretty
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4
with:
commit-message: "style: pretty please"
branch: prettier

View File

@@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2
uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2

View File

@@ -1,27 +1,22 @@
name: NPM / Publish
on:
workflow_run:
workflows: ["release-please"]
types: [completed]
workflow_dispatch:
# We only publish if the version of sern handler is different. workflow automatically cancels if verson is the same
push:
branches:
- 'main'
jobs:
test-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
with:
node-version: 17
- uses: pnpm/action-setup@v2
with:
run_install: |
- recursive: true
args: [--strict-peer-dependencies]
- run: pnpm install
- run: pnpm test
- run: pnpm build
- uses: JS-DevTools/npm-publish@v1
- run: yarn --immutable
- run: yarn build:prod
- uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: "public"
access: "public"

View File

@@ -6,7 +6,7 @@ jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
- uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3
with:
release-type: node
package-name: release-please-action

29
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 19.x, 20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install -g yarn
- run: yarn install
- run: yarn test

10
.gitignore vendored
View File

@@ -87,3 +87,13 @@ dist
# IntelliJ IDEA Config file
.idea/
# Yarn files
.yarn/install-state.gz
.yarn/build-state.yml
.yalc
yalc.lock
*.svg

View File

@@ -8,6 +8,7 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.yarn
# Runtime data
pids
*.pid
@@ -52,6 +53,7 @@ typings/
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
@@ -79,9 +81,7 @@ typings/
# FuseBox cache
.fusebox/
# TypeScript build output
dist
test
# VisualStudio Config file
.vs
@@ -108,6 +108,8 @@ tsup.config.js
tsconfig-base.json
tsconfig.cjs.json
tsconfig-cjs.json
tsconfig.esm.json
tsconfig-esm.json
renovate.json

View File

@@ -1,8 +0,0 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"arrowParens": "avoid"
}

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
## [3.2.1](https://github.com/sern-handler/handler/compare/v3.2.0...v3.2.1) (2023-12-21)
### Bug Fixes
* logger swap failing ([daac37c](https://github.com/sern-handler/handler/commit/daac37c28858c42b21042bdcb8141239db634e7d))
## [3.2.0](https://github.com/sern-handler/handler/compare/v3.1.1...v3.2.0) (2023-12-15)
### Miscellaneous Chores
* release 3.2.0 ([237c853](https://github.com/sern-handler/handler/commit/237c8537c66052309d7e13a7e6e0a4f7995c2558))
## [3.1.1](https://github.com/sern-handler/handler/compare/v3.1.0...v3.1.1) (2023-11-06)
### Bug Fixes
* queuing events ([fd39858](https://github.com/sern-handler/handler/commit/fd39858636d3038abb6d91021b65c99c488a3d6e))
* queuing events ([#332](https://github.com/sern-handler/handler/issues/332)) @Benzo-Fury ([#333](https://github.com/sern-handler/handler/issues/333)) ([fd39858](https://github.com/sern-handler/handler/commit/fd39858636d3038abb6d91021b65c99c488a3d6e))
## [3.1.0](https://github.com/sern-handler/handler/compare/v3.0.2...v3.1.0) (2023-09-04)
### Features
* add guaranteed `channelId` and `userId` getters to `Context` ([#320](https://github.com/sern-handler/handler/issues/320)) ([50253ca](https://github.com/sern-handler/handler/commit/50253ca322e7d6dbd2313139c0187a1028f71109))
* dispose hooks (deprecate useContainerRaw) ([#323](https://github.com/sern-handler/handler/issues/323)) ([26ccd11](https://github.com/sern-handler/handler/commit/26ccd118ff8cbcde94158a4d09fc0df18da9f254))
## [3.0.2](https://github.com/sern-handler/handler/compare/v3.0.1...v3.0.2) (2023-08-06)
### Bug Fixes
* invalid id for cts, mts, cjs, mjs files, node paths ([#318](https://github.com/sern-handler/handler/issues/318)) ([a7f5ea2](https://github.com/sern-handler/handler/commit/a7f5ea269fb344e221d10dbdc26a1611ffc8138f))
## [3.0.1](https://github.com/sern-handler/handler/compare/v3.0.0...v3.0.1) (2023-08-05)
### Bug Fixes
* collectors ([4134460](https://github.com/sern-handler/handler/commit/41344608c677b6069c46412f5f16e4337182ca7d))
## [3.0.0](https://github.com/sern-handler/handler/compare/v2.6.3...v3.0.0) (2023-07-29)
### ⚠ BREAKING CHANGES
* v3 ([#294](https://github.com/sern-handler/handler/issues/294))
### Features
* v3 ([#294](https://github.com/sern-handler/handler/issues/294)) ([7798e36](https://github.com/sern-handler/handler/commit/7798e36458c7f555d2bcb8a5857a6db47b7211da))
### Miscellaneous Chores
* release 3.0.0 ([70cca0d](https://github.com/sern-handler/handler/commit/70cca0dbb01e70b47a8c899b1fc4f43dee5ed8ed))
## [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)

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.

104
README.md
View File

@@ -15,19 +15,15 @@
</div>
## Why?
- Most handlers don't support discord.js 14.7+
- Customizable commands
- Plug and play or customize to your liking
- Embraces reactive programming for consistent and reliable backend
- Customizable logger, error handling, and more
- Active development and growing [community](https://sern.dev/discord)
## 👀 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
- 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.
- Start quickly. Plug and play or customize to your liking.
- Switch and customize how errors are handled, logging, and more.
- works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
- 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
@@ -44,27 +40,52 @@ pnpm add @sern/handler
```
## 👶 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, single } = require('@sern/handler');
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 } from '@sern/handler';
//client has been declared previously
//Version 3
await makeDependencies({
build: root => root
.add({ '@sern/client': single(() => client) })
.upsert({ '@sern/logger': single(() => new DefaultLogging()) })
});
//View docs for all options
@@ -72,35 +93,20 @@ Sern.init({
defaultPrefix: '!', // removing defaultPrefix will shut down text commands
commands: 'src/commands',
// events: 'src/events' (optional),
containerConfig : {
get: useContainer
}
});
client.login("YOUR_BOT_TOKEN_HERE");
```
#### ` ping.js (CommonJS)`
```js
const { CommandType, commandModule } = require('@sern/handler');
exports.default = commandModule({
type: CommandType.Slash,
description: 'A ping pong command',
execute(ctx) {
ctx.reply('pong!');
}
});
```
</details>
## 🤖 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.
- [ALMA (WIP)](https://github.com/Benzo-Fury/ALMA), Using AI to unleash the power in your server.
- [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.
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud), A fun bot for a small - but growing - server.
## 💻 CLI
@@ -112,9 +118,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](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.

0
fortnite Normal file
View File

View File

@@ -1,25 +1,29 @@
{
"name": "@sern/handler",
"packageManager": "pnpm@7.27.0",
"version": "2.5.2",
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
"types": "dist/index.d.ts",
"packageManager": "yarn@3.5.0",
"version": "3.2.1",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"watch": "tsup --dts --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"watch": "tsup --watch",
"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",
"pretty": "prettier --write ."
"build:dev": "tsup --metafile",
"build:prod": "tsup ",
"prepare": "npm run build:prod",
"pretty": "prettier --write .",
"tdd": "vitest",
"test": "vitest --run",
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
},
"keywords": [
"sern-handler",
@@ -35,17 +39,56 @@
"dependencies": {
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-pattern": "^4.1.4",
"ts-results-es": "^3.5.0"
"ts-results-es": "^4.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.48.0",
"discord.js": "^14.7.1",
"eslint": "8.30.0",
"prettier": "2.8.4",
"tsup": "^6.6.3",
"typescript": "4.9.4"
"@faker-js/faker": "^8.0.1",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"discord.js": "^14.11.0",
"esbuild": "^0.17.0",
"eslint": "8.39.0",
"prettier": "2.8.8",
"tsup": "^6.7.0",
"typescript": "5.0.2",
"vitest": "latest"
},
"prettier": {
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"arrowParens": "avoid"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "script"
},
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"quotes": [
2,
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"semi": [
"error",
"always"
],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": "off"
}
},
"repository": {
"type": "git",

1788
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,9 @@
{
"extends": [
"config:base",
"helpers:pinGitHubActionDigests",
"group:allNonMajor"
],
"major": {
"dependencyDashboardApproval": true,
"reviewers": ["EvolutionX-10", "jacoobes", "Murtatrxx"]
@@ -9,6 +14,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' }));
}
}

10
src/core/_internal.ts Normal file
View File

@@ -0,0 +1,10 @@
export * as Id from './id';
export * from './operators';
export * from './predicates';
export * as Files from './module-loading';
export * from './functions';
export type { VoidResult } from '../types/core-plugin';
export { SernError } from './structures/enums';
export { ModuleStore } from './structures/module-store';
export * as DefaultServices from './structures/services';
export { useContainerRaw } from './ioc/base'

View File

@@ -0,0 +1,9 @@
import type { Awaitable } from '../../types/utility';
/**
* Represents a Disposable contract.
* Let dependencies implement this to dispose and cleanup.
*/
export interface Disposable {
dispose(): Awaitable<unknown>;
}

View File

@@ -0,0 +1,7 @@
import type { AnyFunction } from '../../types/utility';
export interface Emitter {
addListener(eventName: string | symbol, listener: AnyFunction): this;
removeListener(eventName: string | symbol, listener: AnyFunction): this;
emit(eventName: string | symbol, ...payload: any[]): boolean;
}

View File

@@ -0,0 +1,18 @@
import type { CommandModule,Processed, EventModule } from "../../types/core-modules";
/**
* @since 2.0.0
*/
export interface ErrorHandling {
/**
* @deprecated
* Version 4 will remove this method
*/
crash(err: Error): never;
/**
* A function that is called on every throw.
* @param error
*/
updateAlive(error: Error): void;
}

View File

@@ -0,0 +1,7 @@
export * from './error-handling';
export * from './logging';
export * from './module-manager';
export * from './module-store';
export * from './init';
export * from './emitter';
export * from './disposable'

View File

@@ -0,0 +1,9 @@
import type { Awaitable } from '../../types/utility';
/**
* Represents an initialization contract.
* Let dependencies implement this to initiate some logic.
*/
export interface Init {
init(): Awaitable<unknown>;
}

View File

@@ -0,0 +1,11 @@
/**
* @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;
}
export type LogPayload<T = unknown> = { message: T };

View File

@@ -0,0 +1,27 @@
import type {
CommandMeta,
CommandModule,
CommandModuleDefs,
Module,
} from '../../types/core-modules';
import { CommandType } from '../structures';
interface MetadataAccess {
getMetadata(m: Module): CommandMeta | undefined;
setMetadata(m: Module, c: CommandMeta): void;
}
/**
* @since 2.0.0
* @internal - direct access to the module manager will be removed in version 4
*/
export interface ModuleManager extends MetadataAccess {
get(id: string): string | undefined;
set(id: string, path: string): void;
getPublishableCommands(): Promise<CommandModule[]>;
getByNameCommandType<T extends CommandType>(
name: string,
commandType: T,
): Promise<CommandModuleDefs[T]> | undefined;
}

View File

@@ -0,0 +1,9 @@
import type { CommandMeta, Module } from '../../types/core-modules';
/**
* Represents a core module store that stores IDs mapped to file paths.
*/
export interface CoreModuleStore {
commands: Map<string, string>;
metadata: WeakMap<Module, CommandMeta>;
}

View File

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

83
src/core/functions.ts Normal file
View File

@@ -0,0 +1,83 @@
import { Err, Ok } from 'ts-results-es';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../types/core-modules';
import type { AnyCommandPlugin, AnyEventPlugin, Plugin } from '../types/core-plugin';
import { PluginType } from './structures';
import assert from 'assert';
//function wrappers for empty ok / err
export const ok = /* @__PURE__*/ () => Ok.EMPTY;
export const err = /* @__PURE__*/ () => Err.EMPTY;
export function partitionPlugins(
arr: (AnyEventPlugin | AnyCommandPlugin)[] = [],
): [Plugin[], Plugin[]] {
const controlPlugins = [];
const initPlugins = [];
for (const el of arr) {
switch (el.type) {
case PluginType.Control:
controlPlugins.push(el);
break;
case PluginType.Init:
initPlugins.push(el);
break;
}
}
return [controlPlugins, initPlugins];
}
/**
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
* @param iAutocomplete
* @param options
*/
export function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return 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:
{
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);
}
break;
default:
{
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, parent };
}
} else {
if (cur.name === choice.name) {
return { ...cur, parent: undefined };
}
}
}
}
break;
}
}
}

63
src/core/id.ts Normal file
View File

@@ -0,0 +1,63 @@
import { ApplicationCommandType, ComponentType, Interaction, InteractionType } from 'discord.js';
import { CommandType, EventType } from './structures';
/**
* Construct unique ID for a given interaction object.
* @param event The interaction object for which to create an ID.
* @returns A unique string ID based on the type and properties of the interaction object.
*/
export function reconstruct<T extends Interaction>(event: T) {
switch (event.type) {
case InteractionType.MessageComponent: {
return `${event.customId}_C${event.componentType}`;
}
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete: {
return `${event.commandName}_A${event.commandType}`;
}
//Modal interactions are classified as components for sern
case InteractionType.ModalSubmit: {
return `${event.customId}_C1`;
}
}
}
/**
*
* A magic number to represent any commandtype that is an ApplicationCommand.
*/
const appBitField = 0b000000001111;
// Each index represents the exponent of a CommandType.
// Every CommandType is a power of two.
export const CommandTypeDiscordApi = [
1, // CommandType.Text
ApplicationCommandType.ChatInput,
ApplicationCommandType.User,
ApplicationCommandType.Message,
ComponentType.Button,
ComponentType.StringSelect,
1, // CommandType.Modal
ComponentType.UserSelect,
ComponentType.RoleSelect,
ComponentType.MentionableSelect,
ComponentType.ChannelSelect,
];
/*
* Generates a number based on CommandType.
* This corresponds to an ApplicationCommandType or ComponentType
* TextCommands are 0 as they aren't either or.
*/
function apiType(t: CommandType | EventType) {
if (t === CommandType.Both || t === CommandType.Modal) return 1;
return CommandTypeDiscordApi[Math.log2(t)];
}
/*
* Generates an id based on name and CommandType.
* A is for any ApplicationCommand. C is for any ComponentCommand
* Then, another number generated by apiType function is appended
*/
export function create(name: string, type: CommandType | EventType) {
const am = (appBitField & type) !== 0 ? 'A' : 'C';
return name + '_' + am + apiType(type);
}

4
src/core/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './contracts';
export * from './create-plugins';
export * from './structures';
export * from './ioc';

104
src/core/ioc/base.ts Normal file
View File

@@ -0,0 +1,104 @@
import * as assert from 'assert';
import { composeRoot, useContainer } from './dependency-injection';
import type { DependencyConfiguration } from '../../types/ioc';
import { CoreContainer } from './container';
import { Result } from 'ts-results-es'
import { DefaultServices } from '../_internal';
import { AnyFunction } from '../../types/utility';
//SIDE EFFECT: GLOBAL DI
let containerSubject: CoreContainer<Partial<Dependencies>>;
/**
* @deprecated
* Returns the underlying data structure holding all dependencies.
* Exposes methods from iti
* Use the Service API. The container should be readonly
*/
export function useContainerRaw() {
assert.ok(
containerSubject && containerSubject.isReady(),
"Could not find container or container wasn't ready. Did you call makeDependencies?",
);
return containerSubject;
}
const dependencyBuilder = (container: any, excluded: string[]) => {
type Insertable =
| ((container: CoreContainer<Dependencies>) => unknown )
| Record<PropertyKey, unknown>
return {
/**
* Insert a dependency into your container.
* Supply the correct key and dependency
*/
add(key: keyof Dependencies, v: Insertable) {
Result
.wrap(() => container.add({ [key]: v}))
.expect("Failed to add " + key);
},
/**
* Exclude any dependencies from being added.
* Warning: this could lead to bad errors if not used correctly
*/
exclude(...keys: (keyof Dependencies)[]) {
keys.forEach(key => excluded.push(key));
},
/**
* @param key the key of the dependency
* @param v The dependency to swap out.
* Swap out a preexisting dependency.
*/
swap(key: keyof Dependencies, v: Insertable) {
Result
.wrap(() => container.upsert({ [key]: v }))
.expect("Failed to update " + key);
},
/**
* @param key the key of the dependency
* @param cleanup Provide cleanup for the dependency at key. First parameter is the dependency itself
* @example
* ```ts
* addDisposer('dbConnection', (dbConnection) => dbConnection.end())
* ```
* Swap out a preexisting dependency.
*/
addDisposer(key: keyof Dependencies, cleanup: AnyFunction) {
Result
.wrap(() => container.addDisposer({ [key] : cleanup }))
.expect("Failed to addDisposer for" + key);
}
};
};
type CallbackBuilder = (c: ReturnType<typeof dependencyBuilder>) => any
type ValidDependencyConfig =
| CallbackBuilder
| DependencyConfiguration;
export const insertLogger = (containerSubject: CoreContainer<any>) => {
containerSubject
.upsert({'@sern/logger': () => new DefaultServices.DefaultLogging});
}
export async function makeDependencies<const T extends Dependencies>
(conf: ValidDependencyConfig) {
//Until there are more optional dependencies, just check if the logger exists
//SIDE EFFECT
containerSubject = new CoreContainer();
if(typeof conf === 'function') {
const excluded: string[] = [];
conf(dependencyBuilder(containerSubject, excluded));
if(!excluded.includes('@sern/logger')
&& !containerSubject.getTokens()['@sern/logger']) {
insertLogger(containerSubject);
}
containerSubject.ready();
} else {
composeRoot(containerSubject, conf);
}
return useContainer<T>();
}

54
src/core/ioc/container.ts Normal file
View File

@@ -0,0 +1,54 @@
import { Container } from 'iti';
import { Disposable, SernEmitter } from '../';
import * as assert from 'node:assert';
import { Subject } from 'rxjs';
import { DefaultServices, ModuleStore } from '../_internal';
import * as Hooks from './hooks'
/**
* A semi-generic container that provides error handling, emitter, and module store.
* For the handler to operate correctly, The only user provided dependency needs to be @sern/client
*/
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
private ready$ = new Subject<void>();
constructor() {
super();
assert.ok(!this.isReady(), 'Listening for dispose & init should occur prior to sern being ready.');
const { unsubscribe } = Hooks.createInitListener(this);
this.ready$
.subscribe({ complete: unsubscribe });
(this as Container<{}, {}>)
.add({ '@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter(),
'@sern/store': () => new ModuleStore() })
.add(ctx => {
return {
'@sern/modules': () =>
new DefaultServices.DefaultModuleManager(ctx['@sern/store']),
};
});
}
isReady() {
return this.ready$.closed;
}
override async disposeAll() {
const otherDisposables = Object
.entries(this._context)
.flatMap(([key, value]) =>
'dispose' in value ? [key] : []);
for(const key of otherDisposables) {
this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never);
}
await super.disposeAll()
}
ready() {
this.ready$.complete();
this.ready$.unsubscribe();
}
}

View File

@@ -0,0 +1,77 @@
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
import { insertLogger, useContainerRaw } from './base';
import { CoreContainer } from './container';
/**
* @__PURE__
* @since 2.0.0.
* Creates a singleton object.
* @param cb
*/
export function single<T>(cb: () => T) {
return cb;
}
/**
* @__PURE__
* @since 2.0.0
* Creates a transient object
* @param cb
*/
export function transient<T>(cb: () => () => T) {
return cb;
}
/**
* The new Service api, a cleaner alternative to useContainer
* To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
* Usually our scaffolding tool takes care of this.
* @since 3.0.0
* @example
* ```ts
* const client = Service('@sern/client');
* ```
* @param key a key that corresponds to a dependency registered.
*
*/
export function Service<const T extends keyof Dependencies>(key: T) {
return useContainerRaw().get(key)!;
}
/**
* @since 3.0.0
* The plural version of {@link Service}
* @returns array of dependencies, in the same order of keys provided
*/
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
const container = useContainerRaw();
return keys.map(k => container.get(k)!) as IntoDependencies<T>;
}
/**
* 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(
container: CoreContainer<Partial<Dependencies>>,
conf: DependencyConfiguration,
) {
//container should have no client or logger yet.
const hasLogger = conf.exclude?.has('@sern/logger');
if (!hasLogger) {
insertLogger(container);
}
//Build the container based on the callback provided by the user
conf.build(container as CoreContainer<Omit<CoreDependencies, '@sern/client'>>);
if (!hasLogger) {
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
container.ready();
}
export function useContainer<const T extends Dependencies>() {
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => useContainerRaw().get(key as keyof Dependencies)) as IntoDependencies<V>;
}

40
src/core/ioc/hooks.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { CoreContainer } from "./container"
interface HookEvent {
key : PropertyKey
newContainer: any
}
type HookName = 'init';
export const createInitListener = (coreContainer : CoreContainer<any>) => {
const initCalled = new Set<PropertyKey>();
const hasCallableMethod = createPredicate(initCalled);
const unsubscribe = coreContainer.on('containerUpserted', async (event) => {
if(isNotHookable(event)) {
return;
}
if(hasCallableMethod('init', event)) {
await event.newContainer?.init();
initCalled.add(event.key);
}
});
return { unsubscribe };
}
const isNotHookable = (hk: HookEvent) => {
return typeof hk.newContainer !== 'object'
|| Array.isArray(hk.newContainer)
|| hk.newContainer === null;
}
const createPredicate = <T extends HookEvent>(called: Set<PropertyKey>) => {
return (hookName: HookName, event: T) => {
const hasMethod = Reflect.has(event.newContainer!, hookName);
const beenCalledOnce = !called.has(event.key)
return hasMethod && beenCalledOnce
}
}

2
src/core/ioc/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { makeDependencies } from './base';
export { Service, Services, single, transient } from './dependency-injection';

127
src/core/module-loading.ts Normal file
View File

@@ -0,0 +1,127 @@
import { Result } from 'ts-results-es';
import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
import { readdir, stat } from 'fs/promises';
import { basename, extname, join, resolve, parse } from 'path';
import assert from 'assert';
import { createRequire } from 'node:module';
import type { ImportPayload, Wrapper } from '../types/core';
import type { Module } from '../types/core-modules';
export type ModuleResult<T> = Promise<ImportPayload<T>>;
/**
* Import any module based on the absolute path.
* This can accept four types of exported modules
* commonjs, javascript :
* ```js
* exports = commandModule({ })
*
* //or
* exports.default = commandModule({ })
* ```
* esm javascript, typescript, and commonjs typescript
* export default commandModule({})
*/
export async function importModule<T>(absPath: string) {
let fileModule = await import(absPath);
let commandModule = fileModule.default;
assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in commandModule ) {
commandModule = commandModule.default;
}
return Result
.wrap(() => ({ module: commandModule.getInstance() }))
.unwrapOr({ module: commandModule }) as T;
}
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
let { module } = await importModule<{ module: T }>(absPath);
assert(module, `Found an undefined module: ${absPath}`);
return { module, absPath };
}
export const fmtFileName = (fileName: string) => parse(fileName).name;
/**
* 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 extends Module>(
input: ObservableInput<string>,
): Observable<ImportPayload<T>> {
return from(input)
.pipe(mergeMap(defaultModuleLoader<T>));
}
export const getFullPathTree = (dir: string) => readPaths(resolve(dir));
export const filename = (path: string) => fmtFileName(basename(path));
const isSkippable = (filename: string) => {
//empty string is for non extension files (directories)
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
};
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file),
};
}
async function* readPaths(dir: string): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
if (fileStats.isDirectory()) {
//Todo: refactor so that i dont repeat myself for files (line 71)
if (!isSkippable(base)) {
yield* readPaths(fullPath);
}
} else {
if (!isSkippable(base)) {
yield 'file:///' + fullPath;
}
}
}
} catch (err) {
throw err;
}
}
const requir = createRequire(import.meta.url);
export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
if (wrapper !== 'file') {
return wrapper;
}
console.log('Experimental loading of sern.config.json');
const config = requir(resolve('sern.config.json'));
const makePath = (dir: PropertyKey) =>
config.language === 'typescript'
? join('dist', config.paths[dir]!)
: join(config.paths[dir]!);
console.log('Loading config: ', config);
const commandsPath = makePath('commands');
console.log('Commands path is set to', commandsPath);
let eventsPath: string | undefined;
if (config.paths.events) {
eventsPath = makePath('events');
console.log('Events path is set to', eventsPath);
}
return {
defaultPrefix: config.defaultPrefix,
commands: commandsPath,
events: eventsPath,
};
}

112
src/core/modules.ts Normal file
View File

@@ -0,0 +1,112 @@
import { ClientEvents } from 'discord.js';
import { CommandType, EventType, PluginType } from '../core/structures';
import type {
AnyCommandPlugin,
AnyEventPlugin,
CommandArgs,
ControlPlugin,
EventArgs,
InitPlugin,
} from '../types/core-plugin';
import type {
CommandModule,
EventModule,
InputCommand,
InputEvent,
Module,
} from '../types/core-modules';
import { partitionPlugins } from './_internal';
import type { Awaitable } from '../types/utility';
/**
* @since 1.0.0 The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommand): CommandModule {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
return {
...mod,
onEvent,
plugins,
} as CommandModule;
}
/**
* @since 1.0.0
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEvent): EventModule {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
return {
...mod,
plugins,
onEvent,
} as EventModule;
}
/** Create event modules from discord.js client events,
* This is an {@link eventModule} for discord events,
* where typings can be very bad.
* @Experimental
* @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,
});
}
function prepareClassPlugins(c: Module) {
const [onEvent, initPlugins] = partitionPlugins(c.plugins);
c.plugins = initPlugins as InitPlugin[];
c.onEvent = onEvent as ControlPlugin[];
}
//
// Class modules:
// Can be refactored.
// Both implement singleton, could I make them inherit a singleton parent class?
/**
* @Experimental
* Will be refactored / changed in future
*/
export abstract class CommandExecutable<const Type extends CommandType = CommandType> {
abstract type: Type;
plugins: AnyCommandPlugin[] = [];
private static _instance: CommandModule;
static getInstance() {
if (!CommandExecutable._instance) {
//@ts-ignore
CommandExecutable._instance = new this();
prepareClassPlugins(CommandExecutable._instance);
}
return CommandExecutable._instance;
}
abstract execute(...args: CommandArgs<Type, PluginType.Control>): Awaitable<unknown>;
}
/**
* @Experimental
* Will be refactored in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: AnyEventPlugin[] = [];
private static _instance: EventModule;
static getInstance() {
if (!EventExecutable._instance) {
//@ts-ignore
EventExecutable._instance = new this();
prepareClassPlugins(EventExecutable._instance);
}
return EventExecutable._instance;
}
abstract execute(...args: EventArgs<Type, PluginType.Control>): Awaitable<unknown>;
}

86
src/core/operators.ts Normal file
View File

@@ -0,0 +1,86 @@
/**
* 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,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
share,
} from 'rxjs';
import { Emitter, ErrorHandling, Logging } from './contracts';
import util from 'node:util';
import type { PluginResult, VoidResult } from '../types/core-plugin';
import type { Result } from 'ts-results-es'
/**
* 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 => {
if (Array.isArray(args)) {
return plugin.execute(...args);
}
return plugin.execute(args);
});
}
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
/**
* Checks if the stream of results is all ok.
*/
export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
every(result => result.isOk()),
defaultIfEmpty(true),
);
export const sharedEventStream = <T>(e: Emitter, eventName: string) => {
return (fromEvent(e, eventName) as Observable<T>).pipe(share());
};
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.inspect(pload, { colors: true }));
//formatted payload
logging?.error({ message: util.inspect(pload) });
crashHandler.updateAlive(err);
return caught;
};
}
// Temporary until i get rxjs operators working on ts-results-es
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
pipe(
concatMap(result => {
if(result.isOk()) {
return of(result.value)
}
onErr(result.error);
return EMPTY
})
)

34
src/core/predicates.ts Normal file
View File

@@ -0,0 +1,34 @@
import type {
AnySelectMenuInteraction,
AutocompleteInteraction,
ButtonInteraction,
ChatInputCommandInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import { InteractionType } from 'discord.js';
interface InteractionTypable {
type: InteractionType;
}
//discord.js pls fix ur typings or i will >:(
type AnyMessageComponentInteraction = AnySelectMenuInteraction | ButtonInteraction;
type AnyCommandInteraction =
| ChatInputCommandInteraction
| MessageContextMenuCommandInteraction
| UserContextMenuCommandInteraction;
export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction {
return i.type === InteractionType.MessageComponent;
}
export function isCommand(i: InteractionTypable): i is AnyCommandInteraction {
return i.type === InteractionType.ApplicationCommand;
}
export function isAutocomplete(i: InteractionTypable): i is AutocompleteInteraction {
return i.type === InteractionType.ApplicationCommandAutocomplete;
}
export function isModal(i: InteractionTypable): i is ModalSubmitInteraction {
return i.type === InteractionType.ModalSubmit;
}

View File

@@ -0,0 +1,41 @@
import type { ReplyOptions } from "../../types/utility";
import type { Logging } from "../contracts";
export interface Response {
type: 'fail' | 'continue';
body?: ReplyOptions;
log?: { type: keyof Logging; message: unknown }
}
export const of = () => {
const payload = {
type: 'fail',
body: undefined,
log : undefined
} as Record<PropertyKey, unknown>
return {
/**
* @param {'fail' | 'continue'} p a status to determine if the error will
* terminate your application or continue. Warning and
*/
status: (p: 'fail' | 'continue') => {
payload.type = p;
return payload;
},
/**
* @param {keyof Logging} type Determine to log to logger[type].
* @param {T} message the message to log
*
* Log this error with the logger.
*/
log: <T=string>(type: keyof Logging, message: T) => {
payload.log = { type, message };
return payload;
},
reply: (bodyContent: ReplyOptions) => {
payload.body = bodyContent;
return payload;
}
};
}

View File

@@ -0,0 +1,127 @@
import {
BaseInteraction,
ChatInputCommandInteraction,
Client,
InteractionReplyOptions,
Message,
MessageReplyOptions,
Snowflake,
User,
} from 'discord.js';
import { CoreContext } from '../structures/core-context';
import { Result, Ok, Err } from 'ts-results-es';
import * as assert from 'assert';
import { ReplyOptions } from '../../types/utility';
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
/*
* @Experimental
*/
get options() {
return this.interaction.options;
}
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>) {
super(ctx);
}
public get id(): Snowflake {
return safeUnwrap(this.ctx
.map(m => m.id)
.mapErr(i => i.id));
}
public get channel() {
return safeUnwrap(this.ctx
.map(m => m.channel)
.mapErr(i => i.channel));
}
public get channelId(): Snowflake {
return safeUnwrap(this.ctx
.map(m => m.channelId)
.mapErr(i => i.channelId));
}
/**
* 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));
}
public get userId(): Snowflake {
return this.user.id;
}
public get createdTimestamp(): number {
return safeUnwrap(this.ctx
.map(m => m.createdTimestamp)
.mapErr(i => i.createdTimestamp));
}
public get guild() {
return safeUnwrap(this.ctx
.map(m => m.guild)
.mapErr(i => i.guild));
}
public get guildId() {
return safeUnwrap(this.ctx
.map(m => m.guildId)
.mapErr(i => i.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));
}
public get client(): Client {
return safeUnwrap(this.ctx
.map(m => m.client)
.mapErr(i => i.client));
}
public get inGuild(): boolean {
return safeUnwrap(this.ctx
.map(m => m.inGuild())
.mapErr(i => i.inGuild()));
}
public async reply(content: ReplyOptions) {
return safeUnwrap(
this.ctx
.map(m => m.reply(content as MessageReplyOptions))
.mapErr(i =>
i.reply(content as InteractionReplyOptions).then(() => i.fetchReply()),
),
);
}
static override wrap(wrappable: BaseInteraction | Message): Context {
if ('interaction' in wrappable) {
return new Context(Ok(wrappable));
}
assert.ok(wrappable.isChatInputCommand());
return new Context(Err(wrappable));
}
}
function safeUnwrap<T>(res: Result<T, T>) {
if(res.isOk()) {
return res.expect("Tried unwrapping message field: " + res)
}
return res.expectErr("Tried unwrapping interaction field" + res)
}

View File

@@ -0,0 +1,32 @@
import { Result as Either } from 'ts-results-es';
import { SernError } from '../_internal';
import * as assert from 'node:assert';
/**
* @since 3.0.0
*/
export abstract class CoreContext<M, I> {
protected constructor(protected ctx: Either<M, I>) {
assert.ok(typeof ctx === 'object' && ctx != null);
}
get message(): M {
return this.ctx.expect(SernError.MismatchEvent);
}
get interaction(): I {
return this.ctx.expectErr(SernError.MismatchEvent);
}
public isMessage(): this is CoreContext<M, never> {
return this.ctx.isOk();
}
public isSlash(): this is CoreContext<never, I> {
return !this.isMessage();
}
//todo: add agnostic options resolver for Context
abstract get options(): unknown;
static wrap(_: unknown): unknown {
throw Error('You need to override this method; cannot wrap an abstract class');
}
}

View File

@@ -1,4 +1,5 @@
/**
* @since 1.0.0
* A bitfield that discriminates command modules
* @enum { number }
* @example
@@ -14,45 +15,18 @@
* ```
*/
export enum CommandType {
/**
* The CommandType for text commands
*/
Text = 1,
/**
* The CommandType for slash commands
*/
Slash = 2,
/**
* The CommandType for hybrid commands, text and slash
*/
Text = 1 << 0,
Slash = 1 << 1,
Both = 3,
/**
* The CommandType for UserContextMenuInteraction commands
*/
CtxUser = 4,
/**
* The CommandType for MessageContextMenuInteraction commands
*/
CtxMsg = 8,
/**
* The CommandType for ButtonInteraction commands
*/
Button = 16,
/**
* The CommandType for StringSelectMenuInteraction commands
*/
StringSelect = 32,
/**
* The CommandType for ModalSubmitInteraction commands
*/
Modal = 64,
/**
* The CommandType for the other SelectMenuInteractions
*/
ChannelSelect = 256,
MentionableSelect = 512,
RoleSelect = 1024,
UserSelect = 2048,
CtxUser = 1 << 2,
CtxMsg = 1 << 3,
Button = 1 << 4,
StringSelect = 1 << 5,
Modal = 1 << 6,
UserSelect = 1 << 7,
RoleSelect = 1 << 8,
MentionableSelect = 1 << 9,
ChannelSelect = 1 << 10,
}
/**
@@ -105,16 +79,6 @@ export enum PluginType {
* The PluginType for InitPlugins
*/
Init = 1,
/**
* @deprecated
* Use PluginType.Init instead
*/
Command = 1,
/**
* @deprecated
* Use PluginType.Control instead
*/
Event = 2,
/**
* The PluginType for EventPlugins
*/
@@ -137,3 +101,42 @@ export enum PayloadType {
*/
Warning = 'warning',
}
/**
* @enum { string }
*/
export const enum SernError {
/**
* Throws when registering an invalid module.
* This means it is undefined or an invalid command type was provided
*/
InvalidModuleType = 'Detected an unknown module type',
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
UndefinedModule = `A module could not be detected`,
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
MismatchModule = `A module type mismatched with event emitted!`,
/**
* Unsupported interaction at this moment.
*/
NotSupportedInteraction = `This interaction is not supported.`,
/**
* One plugin called `controller.stop()` (end command execution / loading)
*/
PluginFailure = `A plugin failed to call controller.next()`,
/**
* A crash that occurs when accessing an invalid property of Context
*/
MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
/**
* Unsupported feature attempted to access at this time
*/
NotSupportedYet = `This feature is not supported yet`,
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`,
}

View File

@@ -0,0 +1,6 @@
export { CommandType, PluginType, PayloadType, EventType } from './enums';
export * from './context';
export * from './sern-emitter';
export * from './services';
export * from './module-store';
export * as CommandError from './command-error';

View File

@@ -0,0 +1,12 @@
import { CommandMeta, Module } from '../../types/core-modules';
import { CoreModuleStore } from '../contracts';
/*
* @internal
* Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
* For interacting with modules, use the ModuleManager instead.
*/
export class ModuleStore implements CoreModuleStore {
metadata = new WeakMap<Module, CommandMeta>();
commands = new Map<string, string>();
}

View File

@@ -1,9 +1,15 @@
import { EventEmitter } from 'events';
import type { Payload, SernEventsMapping } from '../types/handler';
import { PayloadType } from './structures';
import type { Module } from '../types/module';
import { EventEmitter } from 'node:events';
import { PayloadType } from '../../core/structures';
import { Module } from '../../types/core-modules';
import { SernEventsMapping, Payload } from '../../types/utility';
class SernEmitter extends EventEmitter {
/**
* @since 1.0.0
*/
export class SernEmitter extends EventEmitter {
constructor() {
super({ captureRejections: true });
}
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
@@ -81,5 +87,3 @@ class SernEmitter extends EventEmitter {
);
}
}
export default SernEmitter;

View File

@@ -0,0 +1,21 @@
import { ErrorHandling } from '../../contracts';
/**
* @internal
* @since 2.0.0
* Version 4.0.0 will internalize this api. Please refrain from using the defaults!
*/
export class DefaultErrorHandling implements ErrorHandling {
crash(err: Error): never {
throw err;
}
#keepAlive = 5;
updateAlive(err: Error) {
this.#keepAlive--;
if (this.#keepAlive === 0) {
throw err;
}
}
}

View File

@@ -0,0 +1,3 @@
export * from './error-handling';
export * from './logger';
export * from './module-manager';

View File

@@ -1,12 +1,10 @@
import type { LogPayload } from '../../types/handler';
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;
}
import { LogPayload, Logging } from '../../contracts';
/**
* @internal
* @since 2.0.0
* Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
*/
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {

View File

@@ -0,0 +1,51 @@
import * as Id from '../../../core/id';
import { CoreModuleStore, ModuleManager } from '../../contracts';
import { Files } from '../../_internal';
import { CommandMeta, CommandModule, CommandModuleDefs, Module } from '../../../types/core-modules';
import { CommandType } from '../enums';
/**
* @internal
* @since 2.0.0
* Version 4.0.0 will internalize this api. Please refrain from using DefaultModuleManager!
*/
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: CoreModuleStore) {}
getByNameCommandType<T extends CommandType>(name: string, commandType: T) {
const id = this.get(Id.create(name, commandType));
if (!id) {
return undefined;
}
return Files.importModule<CommandModuleDefs[T]>(id);
}
setMetadata(m: Module, c: CommandMeta): void {
this.moduleStore.metadata.set(m, c);
}
getMetadata(m: Module): CommandMeta {
const maybeModule = this.moduleStore.metadata.get(m);
if (!maybeModule) {
throw Error('Could not find metadata in store for ' + m);
}
return maybeModule;
}
get(id: string) {
return this.moduleStore.commands.get(id);
}
set(id: string, path: string): void {
this.moduleStore.commands.set(id, path);
}
//not tested
getPublishableCommands(): Promise<CommandModule[]> {
const entries = this.moduleStore.commands.entries();
const publishable = 0b000000110;
return Promise.all(
Array.from(entries)
.filter(([id]) => !(Number.parseInt(id.at(-1)!) & publishable))
.map(([, path]) => Files.importModule<CommandModule>(path)),
);
}
}

View File

@@ -1,45 +0,0 @@
import type { Observable } from 'rxjs';
import type { Logging } from './logging';
import util from 'util';
export interface ErrorHandling {
/**
* Number of times the process should throw an error until crashing and exiting
*/
keepAlive: number;
/**
* Utility function to crash
* @param error
*/
crash(error: Error): never;
/**
* A function that is called on every crash. Updates keepAlive
* @param error
*/
updateAlive(error: Error): void;
}
export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5;
crash(error: Error): never {
throw error;
}
updateAlive(e: Error) {
this.keepAlive--;
}
}
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) {
crashHandler.crash(err);
}
//formatted payload
logging?.error({ message: util.format(pload) });
crashHandler.updateAlive(err);
return caught;
};
}

View File

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

View File

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

View File

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

View File

@@ -1,57 +0,0 @@
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
* @param cb
*/
export function single<T extends () => unknown>(cb: T): T;
/**
* 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;
/**
* 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;
}
/**
* @deprecated
* @param value
* Please use the transient function instead
*/
// prettier-ignore
export const many = <T>(value: T) => () => _const(value);

View File

@@ -1,73 +0,0 @@
import type { Container } from 'iti';
import { SernError } from '../structures/errors';
import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
import SernEmitter from '../sernEmitter';
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
import { ModuleStore } from '../structures/moduleStore';
import { Result } from 'ts-results-es';
import { BehaviorSubject } from 'rxjs';
import { createContainer } from 'iti';
export const containerSubject = new BehaviorSubject(defaultContainer());
/**
* 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 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<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'>,
{}
>;
}

View File

@@ -1,64 +0,0 @@
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 { callPlugin } from '../operators';
import { createResultResolver } from '../observableHandling';
export function dispatchCommand(module: Processed<CommandModule>, createArgs: () => unknown[]) {
const args = createArgs();
return {
module,
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`);
/**
* Sometimes fromEvent emits a single parameter, which is not an Array. This
* operator function flattens events into an array
* @param src
*/
const arrayify = pipe(
map(event => (Array.isArray(event) ? (event as unknown[]) : [event])),
map(args => ({ module, args })),
);
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
unknown[]
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
onSuccess: ({ args }) => args,
});
const execute: OperatorFunction<unknown[], unknown> = pipe(
concatMap(async args => module.execute(...args)),
);
return fromEvent(source, module.name).pipe(arrayify, 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

@@ -1,23 +0,0 @@
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,34 +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';
/**
* why did I make this, definitely going to be changed in the future
*/
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,137 +0,0 @@
import type { Interaction } from 'discord.js';
import { catchError, concatMap, finalize, fromEvent, map, Observable } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { EventsHandler } from './eventsHandler';
import { CommandType, SernError, type ModuleStore } from '../structures';
import { match, P } from 'ts-pattern';
import { contextArgs, interactionArg, dispatchAutocomplete, dispatchCommand } from './dispatchers';
import { executeModule, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
export default class InteractionHandler extends EventsHandler<{
event: Interaction;
module: Processed<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.createDispatcher),
makeModuleExecutor(module => {
this.emitter.emit(
'module.activate',
SernEmitter.failure(module, SernError.PluginFailure),
);
}),
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({
message: 'interactionCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
override init() {
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return this.modules.get(cb);
};
/**
* Module retrieval:
* ModuleStores are mapped by Discord API values and modules mapped
* by customId or command name.
*/
this.discordEvent.subscribe({
next: event => {
if (event.isMessageComponent()) {
const module = get(ms =>
ms.InteractionHandlers[event.componentType].get(event.customId),
);
this.setState({ event, module });
} else if (event.isCommand() || event.isAutocomplete()) {
const module = get(
ms =>
/**
* try to fetch from ApplicationCommands, if nothing, try BothCommands
* map. If nothing again,this means a slash command
* exists on the API but not sern
*/
ms.ApplicationCommands[event.commandType].get(event.commandName) ??
ms.BothCommands.get(event.commandName),
);
this.setState({ event, module });
} else if (event.isModalSubmit()) {
const module = get(ms => ms.ModalSubmit.get(event.customId));
this.setState({ event, module });
} else {
throw Error('This interaction is not supported yet');
}
},
error: reason => {
this.emitter.emit('error', SernEmitter.failure(undefined, reason));
},
});
}
protected setState(state: { event: Interaction; module: CommandModule | undefined }): void {
if (state.module === undefined) {
this.emitter.emit(
'warning',
SernEmitter.warning('Found no module for this interaction'),
);
} else {
//if statement above checks already, safe cast
this.payloadSubject.next(
state as { event: Interaction; module: Processed<CommandModule> },
);
}
}
protected createDispatcher({
module,
event,
}: {
event: Interaction;
module: Processed<CommandModule>;
}) {
return (
match(module)
.with({ type: CommandType.Text }, () =>
this.crashHandler.crash(Error(SernError.MismatchEvent)),
)
//P.union = either CommandType.Slash or CommandType.Both
.with({ type: P.union(CommandType.Slash, CommandType.Both) }, module => {
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));
}
})
/**
* Every other command module takes a one argument parameter, its corresponding interaction
* this makes this usage safe
*/
.otherwise(mod => dispatchCommand(mod, interactionArg(event)))
);
}
}

View File

@@ -1,84 +0,0 @@
import { EventsHandler } from './eventsHandler';
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of } from 'rxjs';
import { type Wrapper, type ModuleStore, SernError } from '../structures';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import { fmt } from '../utilities/messageHelpers';
import type { CommandModule, Module, TextCommand } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import { contextArgs, dispatchCommand } from './dispatchers';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
export default class MessageHandler extends EventsHandler<{
module: Processed<Module>;
args: unknown[];
}> {
protected discordEvent: Observable<Message>;
public constructor(protected wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Message>>fromEvent(this.client, 'messageCreate');
this.init();
this.payloadSubject
.pipe(
makeModuleExecutor(module => {
this.emitter.emit(
'module.activate',
SernEmitter.failure(module, SernError.PluginFailure),
);
}),
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({
message: 'messageCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.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) => Processed<CommandModule> | undefined) => {
return this.modules.get(cb);
};
this.discordEvent
.pipe(
ignoreNonBot(this.wrapper.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, 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)),
)
.subscribe({
next: value => this.setState(value),
error: reason => this.emitter.emit('error', SernEmitter.failure(reason)),
});
}
protected setState(state: { module: Processed<Module>; args: unknown[] }) {
this.payloadSubject.next(state);
}
}

View File

@@ -1,155 +0,0 @@
import type { Awaitable, Message } from 'discord.js';
import { concatMap, EMPTY, from, Observable, of, pipe, tap, throwError } from 'rxjs';
import type { SernError } from '../structures';
import { Result } from 'ts-results-es';
import type { AnyModule, CommandModule, EventModule, Module } from '../../types/module';
import { _const as i } from '../utilities/functions';
import SernEmitter from '../sernEmitter';
import { callPlugin, everyPluginOk, filterMapTo } from './operators';
import type { Processed } from '../../types/handler';
import type { ControlPlugin, VoidResult } from '../../types/plugin';
/**
* Ignores messages from any person / bot except itself
* @param prefix
*/
export function ignoreNonBot<T extends Message>(prefix: string) {
return (src: Observable<T>) =>
new Observable<T>(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);
}
},
});
});
}
/**
* 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<T extends AnyModule>(cb: (err: SernError) => void) {
return (src: Observable<Result<{ module: T; absPath: string }, SernError>>) =>
new Observable<{ module: T; absPath: string }>(subscriber => {
return src.subscribe({
next(value) {
if (value.err) {
cb(value.val);
} else {
subscriber.next(value.val);
}
},
});
});
}
/**
* 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(
emitter: SernEmitter,
{
module,
task,
}: {
module: Processed<Module>;
task: () => Awaitable<unknown>;
},
) {
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(i(SernEmitter.failure(module, result.val)));
}
}),
);
}
/**
* 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: {
onFailure?: (module: T) => unknown;
onSuccess: (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.onFailure?.(args.module);
}
}),
everyPluginOk(),
filterMapTo(() => config.onSuccess(args)),
);
};
}
/**
* Calls a module's init plugins and checks for Err. If so, call { onFailure } and
* ignore the module
*/
export function scanModule<
T extends Processed<CommandModule | EventModule>,
Args extends { module: T; absPath: string },
>(config: { onFailure?: (module: T) => unknown; onSuccess: (module: Args) => T }) {
return pipe(
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 onFailure emits a failure response to the SernEmitter
*/
export function makeModuleExecutor<
M extends Processed<Module>,
Args extends { module: M; args: unknown[] },
>(onFailure: (m: M) => unknown) {
const onSuccess = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
return pipe(
concatMap(
createResultResolver({
onFailure,
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
onSuccess,
}),
),
);
}

View File

@@ -1,70 +0,0 @@
/**
* 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';
/**
* if {src} is true, mapTo V, else ignore
* @param item
*/
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return pipe(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 pipe(
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);
}
}),
);
}
/**
* operator function that fill the defaults for a module
*/
export function defineAllFields<T extends AnyModule>() {
const fillFields = ({ absPath, module }: { absPath: string; module: T }) => ({
absPath,
module: {
name: nameOrFilename(module.name, absPath),
description: module.description ?? '...',
...module,
},
});
return pipe(map(fillFields));
}
/**
* Checks if the stream of results is all ok.
*/
export function everyPluginOk(): OperatorFunction<VoidResult, boolean> {
return pipe(
every(result => result.ok),
defaultIfEmpty(true),
);
}

View File

@@ -1,119 +0,0 @@
import { EventsHandler } from './eventsHandler';
import type Wrapper from '../structures/wrapper';
import { concatMap, fromEvent, type Observable, take } from 'rxjs';
import * as Files from '../utilities/readFile';
import { errTap, scanModule } from './observableHandling';
import { CommandType, SernError, type ModuleStore } from '../structures';
import { match } from 'ts-pattern';
import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module';
import type { Processed } from '../../types/handler';
import type { ModuleManager } from '../contracts';
import { _const, err, ok } from '../utilities/functions';
import { defineAllFields } from './operators';
import SernEmitter from '../sernEmitter';
export default class ReadyHandler extends EventsHandler<{
module: Processed<CommandModule>;
absPath: string;
}> {
protected discordEvent!: Observable<{ module: 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',
SernEmitter.failure(undefined, reason),
);
}),
),
),
);
this.init();
this.payloadSubject
.pipe(
scanModule({
onFailure: module => {
this.emitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onSuccess: ({ module }) => {
this.emitter.emit('module.register', SernEmitter.success(module));
return module;
},
}),
)
.subscribe(module => {
const res = registerModule(this.modules, module as Processed<CommandModule>);
if (res.err) {
this.crashHandler.crash(Error(SernError.InvalidModuleType));
}
});
}
protected init() {
this.discordEvent.pipe(defineAllFields()).subscribe({
next: value => this.setState(value),
complete: () => this.payloadSubject.unsubscribe(),
});
}
protected setState(state: { absPath: string; module: Processed<CommandModule> }): void {
this.payloadSubject.next(state);
}
}
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)));
return set.ok ? ok() : err();
};
return match(mod as Processed<CommandModule>)
.with({ type: CommandType.Text }, mod => {
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 => {
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);
}

View File

@@ -1,72 +0,0 @@
import { catchError, finalize, map, tap } from 'rxjs';
import { buildData } from '../utilities/readFile';
import type { Dependencies, Processed } from '../../types/handler';
import { errTap, scanModule } from './observableHandling';
import type { CommandModule, EventModule } from '../../types/module';
import type { EventEmitter } from 'events';
import SernEmitter from '../sernEmitter';
import { match } from 'ts-pattern';
import type { ErrorHandling, Logging } from '../contracts';
import { SernError, EventType, type Wrapper } from '../structures';
import { eventDispatcher } from './dispatchers';
import { handleError } from '../contracts/errorHandling';
import { defineAllFields } from './operators';
import { useContainerRaw } from '../dependencies';
export function processEvents({ containerConfig, events }: Wrapper) {
const [client, errorHandling, sernEmitter, logger] = 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 eventCreation$ = eventStream$.pipe(
defineAllFields(),
scanModule({
onFailure: module => sernEmitter.emit('module.register', SernEmitter.success(module)),
onSuccess: ({ module }) => {
sernEmitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
return module;
},
}),
);
const intoDispatcher = (e: Processed<EventModule | CommandModule>) =>
match(e)
.with({ type: EventType.Sern }, m => eventDispatcher(m, sernEmitter))
.with({ type: EventType.Discord }, m => eventDispatcher(m, client))
.with({ type: EventType.External }, m => eventDispatcher(m, lazy(m.emitter)))
.otherwise(() => errorHandling.crash(Error(SernError.InvalidModuleType)));
eventCreation$
.pipe(
map(intoDispatcher),
/**
* Where all events are turned on
*/
tap(dispatcher => dispatcher.subscribe()),
catchError(handleError(errorHandling, logger)),
finalize(() => {
logger?.info({ message: 'an event module reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => {
logger?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
function eventObservable$(events: string, emitter: SernEmitter) {
return buildData<EventModule>(events).pipe(
errTap(reason => {
emitter.emit('module.register', SernEmitter.failure(undefined, reason));
}),
);
}

View File

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

View File

@@ -1,131 +0,0 @@
import type Wrapper from './structures/wrapper';
import { processEvents } from './events/userDefinedEventsHandling';
import { CommandType, EventType, PluginType } from './structures/enums';
import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin';
import InteractionHandler from './events/interactionHandler';
import ReadyHandler from './events/readyHandler';
import MessageHandler from './events/messageHandler';
import type {
CommandModule,
CommandModuleDefs,
EventModule,
EventModuleDefs,
InputCommand,
InputEvent,
} from '../types/module';
import type { Dependencies, DependencyConfiguration } from '../types/handler';
import { composeRoot, useContainer } from './dependencies/provider';
import type { Logging } from './contracts';
import { err, ok, partition } from './utilities/functions';
import type { Awaitable, ClientEvents } from 'discord.js';
/**
*
* @param wrapper Options to pass into sern.
* Function to start the handler up
* @example
* ```ts title="src/index.ts"
* Sern.init({
* defaultPrefix: '!',
* commands: 'dist/commands',
* events: 'dist/events',
* containerConfig : {
* get: useContainer
* }
* })
* ```
*/
export function init(wrapper: Wrapper) {
const logger = wrapper.containerConfig.get('@sern/logger')[0] as Logging | undefined;
const startTime = performance.now();
const { events } = wrapper;
if (events !== undefined) {
processEvents(wrapper);
}
new ReadyHandler(wrapper);
new MessageHandler(wrapper);
new InteractionHandler(wrapper);
const endTime = performance.now();
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
}
/**
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: ok,
stop: err,
};
/**
* The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommand): CommandModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
onEvent,
plugins,
} as CommandModule;
}
/**
* The wrapper function to define event modules for sern
* @param mod
*/
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 });
}
/**
* @param conf a configuration for creating your project 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: 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: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: EventModuleDefs[Type]['execute'];
}

View File

@@ -1,106 +0,0 @@
import type {
ChatInputCommandInteraction,
Client,
InteractionReplyOptions,
Message,
Snowflake,
MessageReplyOptions,
User,
} from 'discord.js';
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;
}
/**
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export default class Context {
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() {
return this.ctx.expect(SernError.MismatchEvent);
}
/**
* Getting the ChatInputCommandInteraction object. Crashes if module type is
* CommandType.Text or the event fired in a Both command was
* Message
*/
public get interaction() {
return this.ctx.expectErr(SernError.MismatchEvent);
}
public get id(): Snowflake {
return this.ctx.val.id;
}
public get 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));
}
public get createdTimestamp(): number {
return this.ctx.val.createdTimestamp;
}
public get guild() {
return this.ctx.val.guild;
}
public get guildId() {
return this.ctx.val.guildId;
}
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
public get member() {
return this.ctx.val.member;
}
public get client(): Client {
return this.ctx.val.client;
}
public get inGuild(): boolean {
return this.ctx.val.inGuild();
}
public isMessage() {
return this.ctx.map(() => true).unwrapOr(false);
}
public isSlash() {
return !this.isMessage();
}
static wrap(wrappable: ChatInputCommandInteraction | Message): Context {
if ('token' in wrappable) {
return new Context(Right(wrappable));
}
return new Context(Left(wrappable));
}
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()),
),
);
}
}

View File

@@ -1,38 +0,0 @@
/**
* @enum { string }
*/
export enum SernError {
/**
* Throws when registering an invalid module.
* This means it is undefined or an invalid command type was provided
*/
InvalidModuleType = 'Detected an unknown module type',
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
UndefinedModule = `A module could not be detected`,
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
MismatchModule = `A module type mismatched with event emitted!`,
/**
* Unsupported interaction at this moment.
*/
NotSupportedInteraction = `This interaction is not supported.`,
/**
* One plugin called `controller.stop()` (end command execution / loading)
*/
PluginFailure = `A plugin failed to call controller.next()`,
/**
* A crash that occurs when accessing an invalid property of Context
*/
MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
/**
* Unsupported feature attempted to access at this time
*/
NotSupportedYet = `This feature is not supported yet`,
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`,
}

View File

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

View File

@@ -1,26 +0,0 @@
import type { CommandModule } from '../../types/module';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { Processed } from '../../types/handler';
/**
* Storing all command modules
* This dependency is usually injected into ModuleManager
*/
export class ModuleStore {
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, Processed<CommandModule>>();
readonly TextCommands = new Map<string, Processed<CommandModule>>();
readonly InteractionHandlers = {
[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 +0,0 @@
import type { Dependencies } from '../../types/handler';
/**
* An object to be passed into Sern#init() function.
* @typedef {object} Wrapper
*/
interface Wrapper {
readonly defaultPrefix?: string;
readonly commands: string;
readonly events?: string;
readonly containerConfig: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
};
}
export default Wrapper;

View File

@@ -1,35 +0,0 @@
import * as Files from './readFile';
import { basename } from 'path';
import { Err, Ok } from 'ts-results-es';
/**
* A function that returns whatever value is provided.
* Warning: this evaluates { @param value }. It does not defer a value.
* @param value
*/
// prettier-ignore
export const _const = <T>(value: T) => () => value;
/**
*
* @param modName
* @param absPath
*/
export function nameOrFilename(modName: string | undefined, absPath: string) {
return modName ?? Files.fmtFileName(basename(absPath));
}
//function wrappers for empty ok / err
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)) {
t.push(el as T);
} else {
v.push(el as V);
}
}
return [t, v];
}

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,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);
/**
* 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 buildData<T>(commandDir: string): Observable<
Result<
{
module: T;
absPath: string;
},
SernError
>
> {
const commands = getCommands(commandDir);
return from(
Promise.all(
commands.map(async absPath => {
let module: T | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
module = require(absPath).default;
} catch {
module = (await import(`file:///` + absPath)).default;
}
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
}),
),
).pipe(concatAll());
}
export function getCommands(dir: string): string[] {
return readPath(join(process.cwd(), dir));
}

View File

@@ -1,47 +0,0 @@
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
/**
* 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;
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand:
{
for (const option of cur.options ?? []) {
_options.push(option);
}
}
break;
case ApplicationCommandOptionType.SubcommandGroup:
{
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;
}
}
}
break;
}
}
return autocompleteData;
}

View File

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

100
src/handlers/dispatchers.ts Normal file
View File

@@ -0,0 +1,100 @@
import { EventEmitter } from 'node:events';
import * as assert from 'node:assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
import {
arrayifySource,
callPlugin,
isAutocomplete,
treeSearch,
SernError,
} from '../core/_internal';
import { createResultResolver } from './event-utils';
import { BaseInteraction, Message } from 'discord.js';
import { CommandType, Context } from '../core';
import type { AnyFunction, Args } from '../types/utility';
import type { CommandModule, Module, Processed } from '../types/core-modules';
//TODO: refactor dispatchers so that it implements a strategy for each different type of payload?
export function dispatchMessage(module: Processed<CommandModule>, args: [Context, Args]) {
return {
module,
args,
};
}
export function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
const ctx = Context.wrap(wrappable);
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options];
return [ctx, args] as [Context, 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 createDispatcher(payload: {
module: Processed<CommandModule>;
event: BaseInteraction;
}) {
assert.ok(
CommandType.Text !== payload.module.type,
SernError.MismatchEvent + 'Found text command in interaction stream',
);
switch (payload.module.type) {
case CommandType.Slash:
case CommandType.Both: {
if (isAutocomplete(payload.event)) {
const option = treeSearch(payload.event, payload.module.options);
assert.ok(
option,
Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`),
);
const { command, name, parent } = option;
return {
...payload,
module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [payload.event],
};
}
return {
module: payload.module,
args: contextArgs(payload.event),
};
}
default: return {
module: payload.module,
args: [payload.event],
};
}
}

265
src/handlers/event-utils.ts Normal file
View File

@@ -0,0 +1,265 @@
import { Interaction, Message } from 'discord.js';
import {
EMPTY,
Observable,
concatMap,
filter,
from,
of,
throwError,
tap,
MonoTypeOperatorFunction,
catchError,
finalize,
} from 'rxjs';
import {
Files,
Id,
callPlugin,
everyPluginOk,
filterMapTo,
handleError,
SernError,
VoidResult,
useContainerRaw,
} from '../core/_internal';
import { Emitter, ErrorHandling, Logging, ModuleManager } from '../core';
import { contextArgs, createDispatcher } from './dispatchers';
import { ObservableInput, pipe } from 'rxjs';
import { SernEmitter } from '../core';
import { Err, Ok, Result } from 'ts-results-es';
import type { AnyFunction, Awaitable } from '../types/utility';
import type { ControlPlugin } from '../types/core-plugin';
import type { AnyModule, CommandModule, Module, Processed } from '../types/core-modules';
import type { ImportPayload } from '../types/core';
function createGenericHandler<Source, Narrowed extends Source, Output>(
source: Observable<Source>,
makeModule: (event: Narrowed) => Promise<Output>,
) {
return (pred: (i: Source) => i is Narrowed) =>
source.pipe(
filter(pred),
concatMap(makeModule));
}
/**
* 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);
}
/**
*
* Creates an RxJS observable that filters and maps incoming interactions to their respective modules.
* @param i An RxJS observable of interactions.
* @param mg The module manager instance used to retrieve the module path for each interaction.
* @returns A handler to create a RxJS observable of dispatchers that take incoming interactions and execute their corresponding modules.
*/
export function createInteractionHandler<T extends Interaction>(
source: Observable<Interaction>,
mg: ModuleManager,
) {
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
source,
async event => {
const fullPath = mg.get(Id.reconstruct(event));
if(!fullPath) {
return Err.EMPTY
}
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then(payload =>
Ok(createDispatcher({
module: payload.module,
event,
})));
},
);
}
export function createMessageHandler(
source: Observable<Message>,
defaultPrefix: string,
mg: ModuleManager,
) {
return createGenericHandler(source, async event => {
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
const fullPath = mg.get(`${prefix}_A1`);
if(!fullPath) {
return Err('Possibly undefined behavior: could not find a static id to resolve')
}
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then((payload)=> {
const args = contextArgs(event, rest);
return Ok({ args, ...payload });
});
});
}
/**
* IMPURE SIDE EFFECT
* This function assigns remaining, incomplete data to each imported module.
*/
function assignDefaults<T extends Module>(
moduleManager: ModuleManager,
): MonoTypeOperatorFunction<ImportPayload<T>> {
return tap(({ module, absPath }) => {
module.name ??= Files.filename(absPath);
module.description ??= '...';
moduleManager.setMetadata(module, {
isClass: module.constructor.name === 'Function',
fullPath: absPath,
id: Id.create(module.name, module.type),
});
});
}
export function buildModules<T extends AnyModule>(
input: ObservableInput<string>,
moduleManager: ModuleManager,
) {
return Files
.buildModuleStream<Processed<T>>(input)
.pipe(assignDefaults(moduleManager));
}
interface ExecutePayload {
module: Processed<Module>;
task: () => Awaitable<unknown>;
args: unknown[]
}
/**
* 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(
emitter: Emitter,
logger: Logging|undefined,
errHandler: ErrorHandling,
{
module,
task,
args
}: ExecutePayload,
) {
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.isOk()) {
emitter.emit('module.activate', SernEmitter.success(module));
return EMPTY;
}
return throwError(() => SernEmitter.failure(module, result.error));
}),
);
}
/**
* 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.onNext }
* 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 => {
result.isErr() && 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<AnyModule>>(sernEmitter: Emitter) {
return concatMap(
createResultResolver({
createStream: args => from(args.module.plugins).pipe(callPlugin(args)),
onStop: (module: T) => {
sernEmitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onNext: ({ module }) => {
sernEmitter.emit('module.register', SernEmitter.success(module));
return { module };
},
}),
);
}
/**
* 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,
args
});
return concatMap(
createResultResolver({
onStop,
createStream: ({ args, module }) =>
from(module.onEvent)
.pipe(callPlugin(args)),
onNext,
}),
);
}
export const handleCrash = (err: ErrorHandling, log?: Logging) =>
pipe(
catchError(handleError(err, log)),
finalize(() => {
log?.info({
message: 'A stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
);

View File

@@ -0,0 +1,33 @@
import { Interaction } from 'discord.js';
import { mergeMap, merge } from 'rxjs';
import { SernEmitter } from '../core';
import {
isAutocomplete,
isCommand,
isMessageComponent,
isModal,
sharedEventStream,
SernError,
filterTap,
} from '../core/_internal';
import { createInteractionHandler, executeModule, makeModuleExecutor } from './_internal';
import type { DependencyList } from '../types/ioc';
export function interactionHandler([emitter, err, log, modules, client]: DependencyList) {
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler(interactionStream$, modules);
const interactionHandler$ = merge(
handle(isMessageComponent),
handle(isAutocomplete),
handle(isCommand),
handle(isModal),
);
return interactionHandler$
.pipe(
filterTap(e => emitter.emit('warning', SernEmitter.warning(e))),
makeModuleExecutor(module =>
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure))),
mergeMap(payload => executeModule(emitter, log, err, payload)),
);
}

View File

@@ -0,0 +1,47 @@
import { mergeMap, EMPTY } from 'rxjs';
import type { Message } from 'discord.js';
import { SernEmitter } from '../core';
import { sharedEventStream, SernError, filterTap } from '../core/_internal';
import { createMessageHandler, executeModule, makeModuleExecutor } from './_internal';
import type { DependencyList } from '../types/ioc';
/**
* Ignores messages from any person / bot except itself
* @param prefix
*/
function isNonBot(prefix: string) {
return (msg: Message): msg is Message => !msg.author.bot && hasPrefix(prefix, msg.content);
}
function hasPrefix(prefix: string, content: string) {
const prefixInContent = content.slice(0, prefix.length);
return (
prefixInContent.localeCompare(prefix, undefined, {
sensitivity: 'accent',
}) === 0
);
}
export function messageHandler(
[emitter, err, log, modules, client]: DependencyList,
defaultPrefix: string | undefined,
) {
if (!defaultPrefix) {
log?.debug({
message: 'No prefix found. message handler shutting down',
});
return EMPTY;
}
const messageStream$ = sharedEventStream<Message>(client, 'messageCreate');
const handle = createMessageHandler(messageStream$, defaultPrefix, modules);
const msgCommands$ = handle(isNonBot(defaultPrefix));
return msgCommands$.pipe(
filterTap((e) => emitter.emit('warning', SernEmitter.warning(e))),
makeModuleExecutor(module => {
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
mergeMap(payload => executeModule(emitter, log, err, payload)),
);
}

View File

@@ -0,0 +1,47 @@
import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe } from 'rxjs';
import { CommandType } from '../core/structures';
import { SernError } from '../core/_internal';
import { Result } from 'ts-results-es';
import { ModuleManager } from '../core/contracts';
import { buildModules, callInitPlugins } from './_internal';
import * as assert from 'node:assert';
import * as util from 'node:util';
import type { DependencyList } from '../types/ioc';
import type { AnyModule, Processed } from '../types/core-modules';
export function startReadyEvent(
[sEmitter, , , moduleManager, client]: DependencyList,
allPaths: ObservableInput<string>,
) {
const ready$ = fromEvent(client!, 'ready').pipe(once());
return concat(ready$, buildModules<AnyModule>(allPaths, moduleManager))
.pipe(callInitPlugins(sEmitter))
.subscribe(({ module }) => {
register(moduleManager, module)
.expect(SernError.InvalidModuleType + ' ' + util.inspect(module));
});
}
const once = () => pipe(
first(),
ignoreElements()
)
function register<T extends Processed<AnyModule>>(
manager: ModuleManager,
module: T,
): Result<void, void> {
const { id, fullPath } = manager.getMetadata(module)!;
const validModuleType = module.type >= 0 && module.type <= 1 << 10;
assert.ok(
validModuleType,
`Found ${module.name} at ${fullPath}, which does not have a valid type`,
);
if (module.type === CommandType.Both || module.type === CommandType.Text) {
module.alias?.forEach(a => manager.set(`${a}_A1`, fullPath));
}
return Result.wrap(() => manager.set(id, fullPath));
}

View File

@@ -0,0 +1,36 @@
import { ObservableInput, map, mergeAll } from 'rxjs';
import { EventType } from '../core/structures';
import { SernError } from '../core/_internal';
import { buildModules, callInitPlugins, handleCrash, eventDispatcher } from './_internal';
import { Service } from '../core/ioc';
import type { DependencyList } from '../types/ioc';
import type { EventModule, Processed } from '../types/core-modules';
export function eventsHandler(
[emitter, err, log, moduleManager, client]: DependencyList,
allPaths: ObservableInput<string>,
) {
//code smell
const intoDispatcher = (e: { module: Processed<EventModule> }) => {
switch (e.module.type) {
case EventType.Sern:
return eventDispatcher(e.module, emitter);
case EventType.Discord:
return eventDispatcher(e.module, client);
case EventType.External:
return eventDispatcher(e.module, Service(e.module.emitter));
default:
throw Error(SernError.InvalidModuleType + ' while creating event handler');
}
};
buildModules<EventModule>(allPaths, moduleManager)
.pipe(
callInitPlugins(emitter),
map(intoDispatcher),
/**
* Where all events are turned on
*/
mergeAll(),
handleCrash(err, log))
.subscribe();
}

View File

@@ -1,18 +1,56 @@
import SernEmitter from './handler/sernEmitter';
export * as Sern from './sern';
export * from './core';
export type {
CommandModule,
EventModule,
BothCommand,
ContextMenuMsg,
ContextMenuUser,
SlashCommand,
TextCommand,
ButtonCommand,
StringSelectCommand,
MentionableSelectCommand,
UserSelectCommand,
ChannelSelectCommand,
RoleSelectCommand,
ModalSubmitCommand,
DiscordEventCommand,
SernEventCommand,
ExternalEventCommand,
CommandModuleDefs,
EventModuleDefs,
SernAutocompleteData,
SernOptionsData,
SernSubCommandData,
SernSubCommandGroupData,
} from './types/core-modules';
export type {
Controller,
PluginResult,
InitPlugin,
ControlPlugin,
Plugin,
AnyEventPlugin,
AnyCommandPlugin,
} from './types/core-plugin';
export type { Wrapper } from './types/core';
export type { Args, SlashOptions, Payload, SernEventsMapping } from './types/utility';
export type { Singleton, Transient, CoreDependencies, Initializable } from './types/ioc';
export {
eventModule,
commandModule,
eventModule,
discordEvent,
EventExecutable,
CommandExecutable,
controller,
discordEvent,
} from './handler/sern';
export * as Sern from './handler/sern';
export * from './types/handler';
export * from './types/module';
export * from './types/plugin';
export * from './handler/structures';
export * from './handler/plugins';
export * from './handler/contracts';
export { SernEmitter };
export * from './handler/dependencies';
} from './core/modules';
export {
useContainerRaw
} from './core/_internal'
export { controller } from './sern';

66
src/sern.ts Normal file
View File

@@ -0,0 +1,66 @@
import { handleCrash } from './handlers/_internal';
import { err, ok, Files } from './core/_internal';
import { merge } from 'rxjs';
import { Services } from './core/ioc';
import { Wrapper } from './types/core';
import { eventsHandler } from './handlers/user-defined-events';
import { startReadyEvent } from './handlers/ready-event';
import { messageHandler } from './handlers/message-event';
import { interactionHandler } from './handlers/interaction-event';
/**
* @since 1.0.0
* @param wrapper Options to pass into sern.
* Function to start the handler up
* @example
* ```ts title="src/index.ts"
* Sern.init({
* commands: 'dist/commands',
* events: 'dist/events',
* })
* ```
*/
export function init(maybeWrapper: Wrapper | 'file') {
const startTime = performance.now();
const wrapper = Files.loadConfig(maybeWrapper);
const dependencies = useDependencies();
const logger = dependencies[2],
errorHandler = dependencies[1];
if (wrapper.events !== undefined) {
eventsHandler(dependencies, Files.getFullPathTree(wrapper.events));
}
//Ready event: load all modules and when finished, time should be taken and logged
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands)).add(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
dependencies[0].emit('modulesLoaded');
logger?.info({
message: `sern: registered all modules in ${time} s`,
});
});
const messages$ = messageHandler(dependencies, wrapper.defaultPrefix);
const interactions$ = interactionHandler(dependencies);
// listening to the message stream and interaction stream
merge(messages$, interactions$).pipe(handleCrash(errorHandler, logger)).subscribe();
}
function useDependencies() {
return Services(
'@sern/emitter',
'@sern/errors',
'@sern/logger',
'@sern/modules',
'@sern/client',
);
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: ok,
stop: err,
};

View File

@@ -1,34 +1,33 @@
import type {
ApplicationCommandAttachmentOption,
ApplicationCommandChannelOptionData,
ApplicationCommandChoicesData,
ApplicationCommandNonOptionsData,
ApplicationCommandNumericOptionData,
ApplicationCommandOptionData,
APIApplicationCommandBasicOption,
APIApplicationCommandOptionBase,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
AutocompleteInteraction,
Awaitable,
BaseApplicationCommandOptionsData,
AutocompleteInteraction,
ButtonInteraction,
ChannelSelectMenuInteraction,
ClientEvents,
MentionableSelectMenuInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { CommandType } from '../handler/structures/enums';
import type { Args, SlashOptions } from './handler';
import type Context from '../handler/structures/context';
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';
import { CommandType, Context, EventType } from '../../src/core';
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './core-plugin';
import { Awaitable, Args, SlashOptions, SernEventsMapping } from './utility';
export interface CommandMeta {
fullPath: string;
id: string;
isClass: boolean;
}
export type Processed<T> = T & { name: string; description: string };
export interface Module {
type: CommandType | EventType;
@@ -36,28 +35,20 @@ export interface Module {
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
execute: (...args: any[]) => Awaitable<any>;
execute(...args: any[]): Awaitable<any>;
}
export interface TextCommand extends Module {
type: CommandType.Text;
alias?: string[];
execute: (ctx: Context, args: ['text', string[]]) => 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 SlashCommand extends Module {
type: CommandType.Slash;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: ['slash', SlashOptions]) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
alias?: string[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
export interface ExternalEventCommand extends Module {
name?: string;
emitter: keyof Dependencies;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
@@ -111,25 +102,31 @@ export interface AutocompleteCommand
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 TextCommand extends Module {
type: CommandType.Text;
alias?: string[];
execute: (ctx: Context, args: ['text', string[]]) => Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: string;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
export interface SlashCommand extends Module {
type: CommandType.Slash;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: ['slash', SlashOptions]) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
alias?: string[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
@@ -148,10 +145,9 @@ export type CommandModule =
| ModalSubmitCommand;
export type AnyModule = CommandModule | EventModule;
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union
// Explicit Module Definitions for mapping
export type CommandModuleDefs = {
export interface CommandModuleDefs {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
@@ -164,13 +160,13 @@ export type CommandModuleDefs = {
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
}
export type EventModuleDefs = {
export interface EventModuleDefs {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
};
}
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
@@ -182,10 +178,10 @@ export interface SernAutocompleteData
command: AutocompleteCommand;
}
export type CommandModuleNoPlugins = {
type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type EventModulesNoPlugins = {
type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
};
@@ -194,35 +190,27 @@ export type InputEvent = {
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: AnyCommandPlugin[] };
[T in CommandType]: CommandModuleNoPlugins[T] & {
plugins?: AnyCommandPlugin[];
};
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
export type BaseOptions =
| ApplicationCommandChoicesData
| ApplicationCommandNonOptionsData
| ApplicationCommandChannelOptionData
| ApplicationCommandNumericOptionData
| ApplicationCommandAttachmentOption
export type SernOptionsData =
| SernSubCommandData
| SernSubCommandGroupData
| APIApplicationCommandBasicOption
| SernAutocompleteData;
export interface SernSubCommandData extends BaseApplicationCommandOptionsData {
export interface SernSubCommandData
extends APIApplicationCommandOptionBase<ApplicationCommandOptionType.Subcommand> {
type: ApplicationCommandOptionType.Subcommand;
required?: never;
options?: BaseOptions[];
options?: SernOptionsData[];
}
export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.SubcommandGroup;
required?: never;
options?: SernSubCommandData[];
}
export type SernOptionsData<U extends ApplicationCommandOptionData = ApplicationCommandOptionData> =
U extends ApplicationCommandSubCommandData
? SernSubCommandData
: U extends ApplicationCommandSubGroupData
? SernSubCommandGroupData
: BaseOptions;

View File

@@ -1,42 +1,92 @@
import type { CommandType } from '../structures/enums';
import type { PluginType } from '../structures/enums';
import type { ClientEvents } from 'discord.js';
/*
* 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 { Err, Ok, Result } from 'ts-results-es';
import type {
BothCommand,
ButtonCommand,
ChannelSelectCommand,
CommandModule,
ContextMenuMsg,
ContextMenuUser,
DiscordEventCommand,
EventModule,
ExternalEventCommand,
MentionableSelectCommand,
ModalSubmitCommand,
Module,
Processed,
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 {
} from './core-modules';
import { Args, Awaitable, Payload, SlashOptions } from './utility';
import { CommandType, Context, EventType, PluginType } from '../core';
import {
ButtonInteraction,
ChannelSelectMenuInteraction,
ClientEvents,
MentionableSelectMenuInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import type {
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
ModalSubmitInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { EventType } from '../structures/enums';
type CommandArgsMatrix = {
export type PluginResult = Awaitable<VoidResult>;
export type VoidResult = Result<void, void>;
export interface InitArgs<T extends Processed<Module>> {
module: T;
absPath: string;
}
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>>]>;
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];
interface CommandArgsMatrix {
[CommandType.Text]: {
[PluginType.Control]: [Context, ['text', string[]]];
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
@@ -85,9 +135,9 @@ type CommandArgsMatrix = {
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
};
};
}
type EventArgsMatrix = {
interface EventArgsMatrix {
[EventType.Discord]: {
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
@@ -100,18 +150,4 @@ type EventArgsMatrix = {
[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];

23
src/types/core.ts Normal file
View File

@@ -0,0 +1,23 @@
export interface ImportPayload<T> {
module: T;
absPath: string;
[key: string]: unknown;
}
export interface Wrapper {
commands: string;
defaultPrefix?: string;
events?: string;
/**
* Overload to enable mode in case developer does not use a .env file.
* @deprecated - https://github.com/sern-handler/handler/pull/325
*/
mode?: string
/*
* @deprecated
*/
containerConfig?: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
};
}

12
src/types/dependencies.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
// This file serves an the interface for developers to augment the Dependencies interface
// Developers will have to create a new file dependencies.d.ts in the root directory, augmenting
// this type
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { CoreDependencies } from './ioc';
declare global {
interface Dependencies extends CoreDependencies {}
}

View File

@@ -1,69 +0,0 @@
import type { CommandInteractionOptionResolver } from 'discord.js';
import type { PayloadType } from '../handler/structures/enums';
import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { EventEmitter } from 'events';
import type { CommandModule, EventModule, AnyModule } from './module';
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;
}[keyof T];
export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
/**
* After modules are transformed, name and description are given default values if none
* are provided to Module. This type represents that transformation
*/
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 };
export type SernEventsMapping = {
'module.register': [Payload];
'module.activate': [Payload];
error: [Payload];
warning: [Payload];
};
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/errors': Singleton<ErrorHandling>;
}
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 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, {}>;
}

48
src/types/ioc.ts Normal file
View File

@@ -0,0 +1,48 @@
import { Container, UnpackFunction } from 'iti';
import * as Contracts from '../core/contracts';
/**
* Type to annotate that something is a singleton.
* T is created once and lazily.
*/
export type Singleton<T> = () => T;
/**
* Type to annotate that something is transient.
* Every time this is called, a new object is created
*/
export type Transient<T> = () => () => T;
/**
* Type to annotate that something is initializable.
* If T has an init method, this will be called.
*/
export type Initializable<T extends Contracts.Init> = T
export type DependencyList = [
Contracts.Emitter,
Contracts.ErrorHandling,
Contracts.Logging | undefined,
Contracts.ModuleManager,
Contracts.Emitter,
];
export interface CoreDependencies {
'@sern/client': () => Contracts.Emitter;
'@sern/logger'?: () => Contracts.Logging;
'@sern/emitter': () => Contracts.Emitter;
'@sern/store': () => Contracts.CoreModuleStore;
'@sern/modules': () => Contracts.ModuleManager;
'@sern/errors': () => Contracts.ErrorHandling;
}
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
export type IntoDependencies<Tuple extends [...any[]]> = {
[Index in keyof Tuple]: UnpackFunction<NonNullable<DependencyFromKey<Tuple[Index]>>>; //Unpack and make NonNullable
} & { length: Tuple['length'] };
export interface DependencyConfiguration {
//@deprecated. Loggers will always be included in the future
exclude?: Set<'@sern/logger'>;
build: (
root: Container<Omit<CoreDependencies, '@sern/client'>, {}>,
) => Container<Dependencies, {}>;
}

View File

@@ -1,74 +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 { 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: '>;

33
src/types/utility.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { CommandInteractionOptionResolver, InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { PayloadType } from '../core';
import type { AnyModule } from './core-modules';
export type Awaitable<T> = PromiseLike<T> | T;
export type AnyFunction = (...args: unknown[]) => unknown;
// Thanks to @kelsny
type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
}[keyof T];
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
export interface SernEventsMapping {
'module.register': [Payload];
'module.activate': [Payload];
error: [Payload];
warning: [Payload];
modulesLoaded: [never?];
}
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string };
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;

85
test/core/context.test.ts Normal file
View File

@@ -0,0 +1,85 @@
import { describe, vi, it, expect } from'vitest'
import { Context } from '../../src';
import { faker } from '@faker-js/faker'
describe('Context', () => {
// Mocked message and interaction objects for testing
const mockMessage = {
id: 'messageId',
channel: 'channelId',
channelId: 'channelId',
interaction: {
id: faker.string.uuid()
},
author: { id: 'userId' },
createdTimestamp: 1234567890,
guild: 'guildId',
guildId: 'guildId',
member: { id: 'memberId' },
client: { id: 'clientId' },
inGuild: vi.fn().mockReturnValue(true),
reply: vi.fn(),
};
const mockInteraction = {
id: 'interactionId',
user: { id: 'userId' },
channel: 'channelId',
channelId: 'channelId',
createdTimestamp: 1234567890,
guild: 'guildId',
guildId: 'guildId',
fetchReply: vi.fn().mockResolvedValue({}),
member: { id: 'memberId' },
client: { id: 'clientId' },
isChatInputCommand: vi.fn().mockResolvedValue(true),
inGuild: vi.fn().mockReturnValue(true),
reply: vi.fn().mockResolvedValue({}),
};
it('should create a context from a message', () => {
//@ts-ignore
const context = Context.wrap(mockMessage);
expect(context).toBeDefined();
expect(context.id).toBe('messageId');
});
it('should throw error if accessing interaction as message', () => {
//@ts-ignore
const context = Context.wrap(mockMessage);
expect(context).toBeDefined();
expect(() => context.interaction)
.toThrowError('You cannot use message when an interaction fired or vice versa');
})
it('should throw error if accessing message as interaction', () => {
//@ts-ignore
const context = Context.wrap(mockInteraction);
expect(context).toBeDefined();
expect(() => context.message)
.toThrowError('You cannot use message when an interaction fired or vice versa');
})
it('should create a context from an interaction', () => {
//@ts-ignore
const context = Context.wrap(mockInteraction);
expect(context).toBeDefined();
expect(context.id).toBe('interactionId');
});
it('should reply to a context with a message', async () => {
//@ts-ignore
const context = Context.wrap(mockMessage);
const replyOptions = { content: 'Hello, world!' };
await context.reply(replyOptions);
expect(mockMessage.reply).toHaveBeenCalledWith(replyOptions);
});
it('should reply to a context with an interaction', async () => {
//@ts-ignore
const context = Context.wrap(mockInteraction);
const replyOptions = { content: 'Hello, world!' };
await context.reply(replyOptions);
expect(mockInteraction.reply).toHaveBeenCalledWith(replyOptions);
});
});

View File

@@ -0,0 +1,16 @@
import { assertType, describe, it } from 'vitest';
import { ModuleStore } from '../../src';
import * as DefaultContracts from '../../src/core/structures/services';
import * as Contracts from '../../src/core/contracts/index.js';
describe('default contracts', () => {
it('should satisfy contracts', () => {
assertType<Contracts.Logging>(new DefaultContracts.DefaultLogging());
assertType<Contracts.ErrorHandling>(new DefaultContracts.DefaultErrorHandling());
assertType<Contracts.ModuleManager>(
new DefaultContracts.DefaultModuleManager(new ModuleStore()),
);
assertType<Contracts.CoreModuleStore>(new ModuleStore());
});
});

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