Compare commits

..

151 Commits

Author SHA1 Message Date
github-actions[bot]
2042559b4d chore(main): release 4.1.0 (#374)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-01-06 17:02:24 -06:00
Jacob Nguyen
220a60ecf8 feat: moduleinfo-in-eventplugins (#373) 2025-01-06 17:00:02 -06:00
Glitch
55715d5659 fix: update github username (#371)
Some checks failed
NPM / Publish / test-and-publish (push) Has been cancelled
2024-11-18 16:34:27 -06:00
github-actions[bot]
d0c3b7469e chore(main): release 4.0.3 (#370)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-06 11:53:30 -05:00
Jacob Nguyen
eabfb81819 fix: async presence (#369)
* fix: async presence

* fixes to typings
2024-10-06 11:51:07 -05:00
Duro
1789ccb2f2 fix: fix eventModule typing for Discord events (#368) 2024-08-19 11:18:13 -05:00
github-actions[bot]
25c5891ade chore(main): release 4.0.2 (#367)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-12 21:30:44 -05:00
jacob
2106cdc1d0 fix: type issue 2024-08-12 21:28:22 -05:00
Jacob Nguyen
61e82fdc7b refactor: remove ts-results-es (#366)
* remove tsresultses

* remove test since it uses external api

* opt in for simpler

* add more debug information

Signed-off-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>

* add more debug information

Signed-off-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>

* clean up if else

---------

Signed-off-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2024-08-11 11:07:44 -05:00
xxDeveloper
3755b95b1a chore: Update LICENSE year (#365) 2024-08-06 10:56:25 -05:00
Jacob Nguyen
06807ea77f Update README.md 2024-07-19 01:32:22 -05:00
github-actions[bot]
3fd3f1c236 chore(main): release 4.0.1 (#364)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-18 22:55:59 -05:00
Peter-MJ-Parker
92623d2914 fix: add SDT typings to autocomplete commands (#363) 2024-07-18 22:54:23 -05:00
github-actions[bot]
a91f260a86 chore(main): release 4.0.0 (#362)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-18 17:01:47 -05:00
Jacob Nguyen
dda0e3395b chore: release 4.0.0
Release-As: 4.0.0
2024-07-18 17:00:33 -05:00
Jacob Nguyen
9a8904f5ae feat: v4 (#361)
* step 1

* Refactorings

* command modules do not depend on anything but itself

* tearing it up

* Remove module store, manager, and Intializable type

* consolidate interfaces in single file

* consolidate default services in single file

* TEAR IT UP

* fix text compile

* the end of sern init??

* Presence namespaced types removed

* internal namespace

* clean up dependencies

* fix test

* fix circular dependency

* still broken but progress

* remove barrel for core/structs

* reffactor

* refactor allat

* more refactoring

* prototyping linking static handler

* cleanup tests, codegen, and importing handler

* some refactor

* generify partition

* for now copy paste new ioc system

* removeiti

* fdsfD

* ensure container is init'd

* fix absPath gen

* working on bun compat

* refactor and clean up and reenter v3 module loading

* dsfsd

* refactor, add cron types, reinstante module loader

* ready handler revamped so much cleaner

* fdssdf

* refactor deps list

* add more tests, polish up ioc

* up to speed with event modules

* i think cron works

* cron works now, poc

* ksdjkldsfld

* updating ioc api, experimenting with cron

* save b4 thunder and lightning

* plugin data reduction & args changes

* freeze module after plugins, updateModule, and more

* simplify plugin args and prepare for reduction among plugins

* add deps to plugin calls and execute

* plugin system loking better, tbd type

* porg

* initplugins inject deps, inconspicuos

* fix faiklling test

* fix initPlugins not reassigning

* parsingParams kinda

* proper mapping

* dynamic customIds

* handling customId params working

* testing n shi

* inlineinignsd

* consolidate fmt

* once on eventModules

* refact,simplf

* readd vitest and Asset fn

* fix typings

* assets fn complete

* more intuitive context.options and Asset typings

* add init hooks not firing

* -file,-updateModule,publish?

* fix: ioc deps not created correctly

* documentation, add json for Asset

* remove asset

* ss

* finish ioc transition

* nvm, now i did

* s

* update locals api, docs, tests

* fix tests

* fix up tests and cleanup

* fix

* Update src/core/functions.ts

Co-authored-by: Evo <85353424+EvolutionX-10@users.noreply.github.com>

* better documentation

* temp fix

* namespace presence types again

* revising cron modules and better error messages

* scheduler ids

* more descriptive errors

* refactor to not type leak and job cancellation

* refactor n better signatures for task scheduler

* documentation

* fix swap not accepting functions

* change task signature

---------

Co-authored-by: Evo <85353424+EvolutionX-10@users.noreply.github.com>
2024-07-18 16:54:55 -05:00
Duro
04c4625bfa docs: change @param wrapper -> @param maybeWrapper for Sern.init (#360)
change `@param wrapper` -> `@param maybeWrapper` for typedoc fix
2024-05-14 21:52:24 -05:00
Jacob Nguyen
91b3768e37 bump tsresults 2024-03-21 10:46:19 -05:00
renovate[bot]
d6f49d1d97 chore(deps): update actions/setup-node digest to 1a4442c (#354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2024-03-21 10:35:43 -05:00
renovate[bot]
8ecd30cf18 chore(deps): update actions/checkout digest to f43a0e5 (#353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2024-03-19 10:22:39 -05:00
github-actions[bot]
a19edaf883 chore(main): release 3.3.4 (#359)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-18 01:57:13 -05:00
Jacob Nguyen
90e55dfa14 fix: sern emitter err (#358)
* prep for fix

* fix ? (not tested

* fix error event not emitting payload
2024-03-18 01:47:14 -05:00
github-actions[bot]
2106522812 chore(main): release 3.3.3 (#357)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-24 18:35:24 -06:00
Jacob Nguyen
ce8c4bf649 fix: typings and cleanup (#356)
* fix typings and cleanup

* import type

* rm unused import
2024-02-18 09:34:53 -06:00
jacob
e89b918390 dfs 2024-02-17 11:40:19 -06:00
jacob
f8b69ae542 stuff 2024-02-17 11:38:37 -06:00
Jacob Nguyen
48f9f6ec16 fix: rm deprecated class modules, clean up, rm indirection (#355)
* refactor: rm deprecations, clean up, rm indirection

* fix: singleton init not being fired when inserting function

* refactor and generic internal add

* deprecate a few things that i impusively added lol
2024-02-17 11:35:53 -06:00
jacob
86ebb221ed deprecate a few things that i impusively added lol 2024-02-15 12:36:00 -06:00
jacob
4efdbb21fb refactor and generic internal add 2024-02-15 12:26:34 -06:00
jacob
07b11b357b fix: singleton init not being fired when inserting function 2024-02-14 15:58:50 -06:00
jacob
ac7f47c590 refactor: rm deprecations, clean up, rm indirection 2024-02-12 21:47:35 -06:00
45cbda7b42 refactor: cleanup (#348)
* some wip code

Co-authored-by: Jacob Nguyen <jacoobes@users.noreply.github.com>

* general idea

* style

* making shrimple truly optional

* got optional localizer working

* proposing api notation?

* prepare for localization map

* add localsFor

* merge some internals

* boss call

* add test for init functionality

* add documentation

* inline and cleanup

* feat: logging for experimental json loading

* loosen typings

* dev workflow and cleaning up comments

* cleaning up a bit more

* rename Localizer -> Localization

* more documentation, change dir for default localizer

* some tests

* "

* move stuff, refactor, deprecate

* yarnb

* Update index.ts

---------

Co-authored-by: Jacob Nguyen <jacoobes@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
Co-authored-by: jacob <jacoobes@sern.dev>
2024-02-09 17:46:16 -06:00
Jacob Nguyen
5cad432589 Update README.md 2024-01-08 14:31:49 -06:00
Jacob Nguyen
044a10dace Update README.md 2024-01-08 14:29:49 -06:00
github-actions[bot]
9d5c6c714f chore(main): release 3.3.2 (#352)
* chore(main): release 3.3.2

* 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>
2024-01-08 12:16:52 -06:00
Jacob Nguyen
4f2387119a fix: presence feature not workign on cjs applications (#351)
* start fix

* fix on unix

* better solution
2024-01-08 12:11:27 -06:00
Jacob Nguyen
a6fa4e3dcb fix docs build 2024-01-07 15:38:22 -06:00
github-actions[bot]
c281832db2 chore(main): release 3.3.1 (#350)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-07 15:27:43 -06:00
Jacob Nguyen
a359f73fa2 fix: crashing when slash command is used as text command (#349)
* progress on fix

* fix: ids
2024-01-07 15:26:08 -06:00
655bb8d358 revert: the last commit 2024-01-05 20:47:25 +01:00
e8d5029834 chore: update fortnite file 2024-01-05 20:46:38 +01:00
Jacob Nguyen
b0399f9507 refactor: minor (#347)
* some refactoring

* accidental merge

* refactor: ensure all asserts have error message to avoid cryptic messages

* general refactoring

* move controller to create-plugin
2024-01-02 13:04:59 -06:00
renovate[bot]
b962dae36c chore(deps): update actions/setup-node digest to 1a4442c (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 11:40:18 -06:00
github-actions[bot]
c73cf96cb2 chore(main): release 3.3.0 (#346)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-27 11:13:11 -06:00
Jacob Nguyen
7458befe8a feat: presence (#345)
* presence

* from event presence and refactoring

* refine presence api

* add tests and more comments

* sss

---------

Co-authored-by: SrIzan10 <66965250+SrIzan10@users.noreply.github.com>
2023-12-27 11:11:32 -06:00
Jacob Nguyen
efe49391e8 Update README.md 2023-12-27 01:51:41 -06:00
Jacob Nguyen
3140f80c10 Update README.md 2023-12-27 01:46:55 -06:00
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
101 changed files with 7692 additions and 4245 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,26 +20,26 @@ 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
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
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

34
.github/workflows/npm-publish-dev.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Continuous Delivery
on:
push:
branches:
- main
paths:
- 'src/**'
- 'package.json'
jobs:
Publish:
name: Publishing Dev
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Set up Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- name: Install Node.js dependencies
run: npm i && npm run build:dev
- name: Publish to npm
run: |
npm version premajor --preid "dev.$(git rev-parse --verify --short HEAD)" --git-tag-version=false
npm publish --tag dev
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

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@1a4442cacd436585916779262731d5b162bc6ec7 # 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@1a4442cacd436585916779262731d5b162bc6ec7 # 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,9 @@ tsup.config.js
tsconfig-base.json
tsconfig.cjs.json
tsconfig-cjs.json
tsconfig.esm.json
tsconfig-esm.json
renovate.json
fortnite

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,181 @@
# Changelog
## [4.1.0](https://github.com/sern-handler/handler/compare/v4.0.3...v4.1.0) (2025-01-06)
### Features
* moduleinfo-in-eventplugins ([#373](https://github.com/sern-handler/handler/issues/373)) ([220a60e](https://github.com/sern-handler/handler/commit/220a60ecf853df8d288de2533c669562a430c3f9))
### Bug Fixes
* update github username ([#371](https://github.com/sern-handler/handler/issues/371)) ([55715d5](https://github.com/sern-handler/handler/commit/55715d565990fe686159f3c1eda3754d1262c72c))
## [4.0.3](https://github.com/sern-handler/handler/compare/v4.0.2...v4.0.3) (2024-10-06)
### Bug Fixes
* async presence ([#369](https://github.com/sern-handler/handler/issues/369)) ([eabfb81](https://github.com/sern-handler/handler/commit/eabfb81819b53a4656d8eac6e21cfb488b724a42))
* fix eventModule typing for Discord events ([#368](https://github.com/sern-handler/handler/issues/368)) ([1789ccb](https://github.com/sern-handler/handler/commit/1789ccb2f22f502f87538fecdb07106ff7110434))
## [4.0.2](https://github.com/sern-handler/handler/compare/v4.0.1...v4.0.2) (2024-08-13)
### Bug Fixes
* type issue ([2106cdc](https://github.com/sern-handler/handler/commit/2106cdc1d033f88b6ee4ccca6754fe7a595a9328))
## [4.0.1](https://github.com/sern-handler/handler/compare/v4.0.0...v4.0.1) (2024-07-19)
### Bug Fixes
* add SDT typings to autocomplete commands ([#363](https://github.com/sern-handler/handler/issues/363)) ([92623d2](https://github.com/sern-handler/handler/commit/92623d2914fb80e31365f06cf896bb37f36fc814))
## [4.0.0](https://github.com/sern-handler/handler/compare/v3.3.4...v4.0.0) (2024-07-18)
### Features
* v4 ([#361](https://github.com/sern-handler/handler/issues/361)) ([9a8904f](https://github.com/sern-handler/handler/commit/9a8904f5aed4fa36b018ad73bbe58049bae33274))
### Miscellaneous Chores
* release 4.0.0 ([dda0e33](https://github.com/sern-handler/handler/commit/dda0e3395b6704862bfd3fda2a201e2cb9b45d2f))
## [3.3.4](https://github.com/sern-handler/handler/compare/v3.3.3...v3.3.4) (2024-03-18)
### Bug Fixes
* sern emitter err ([#358](https://github.com/sern-handler/handler/issues/358)) ([90e55df](https://github.com/sern-handler/handler/commit/90e55dfa1466c91e5da48922251309331921b1ef))
## [3.3.3](https://github.com/sern-handler/handler/compare/v3.3.2...v3.3.3) (2024-02-25)
### Bug Fixes
* rm deprecated class modules, clean up, rm indirection ([#355](https://github.com/sern-handler/handler/issues/355)) ([48f9f6e](https://github.com/sern-handler/handler/commit/48f9f6ec16e650d574bd24dcbb0ed176933bfe17))
* singleton init not being fired when inserting function ([07b11b3](https://github.com/sern-handler/handler/commit/07b11b357baac0c3c7055c022bc353995c80f766))
* typings and cleanup ([#356](https://github.com/sern-handler/handler/issues/356)) ([ce8c4bf](https://github.com/sern-handler/handler/commit/ce8c4bf6492b9680fb1c1a530d3e0028f214ad2f))
## [3.3.2](https://github.com/sern-handler/handler/compare/v3.3.1...v3.3.2) (2024-01-08)
### Bug Fixes
* presence feature not working on cjs applications ([#351](https://github.com/sern-handler/handler/issues/351)) ([4f23871](https://github.com/sern-handler/handler/commit/4f2387119acfde036d0d1626553e9050f55627d1))
## [3.3.1](https://github.com/sern-handler/handler/compare/v3.3.0...v3.3.1) (2024-01-07)
### Bug Fixes
* crashing when slash command is used as text command ([#349](https://github.com/sern-handler/handler/issues/349)) ([a359f73](https://github.com/sern-handler/handler/commit/a359f73fa24127a4964d411c8c1c0dfea5edc0f1))
### Reverts
* the last commit ([655bb8d](https://github.com/sern-handler/handler/commit/655bb8d35815fe0ce9797d8b169310a07b284ae0))
## [3.3.0](https://github.com/sern-handler/handler/compare/v3.2.1...v3.3.0) (2023-12-27)
### Features
* presence ([#345](https://github.com/sern-handler/handler/issues/345)) ([7458bef](https://github.com/sern-handler/handler/commit/7458befe8a5900480cd71900df02a8364837dc00))
## [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)

View File

@@ -1,6 +1,6 @@
MIT License
**Copyright (c) 2022 Sern**
Copyright (c) 2024 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.

View File

@@ -15,93 +15,44 @@
</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.
- 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
```sh
npm install @sern/handler
```
```sh
yarn add @sern/handler
```
```sh
pnpm add @sern/handler
```
[Start here!!](https://sern.dev/v4/reference/getting-started)
## 👶 Basic Usage
<details><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
]
});
export const useContainer = Sern.makeDependencies({
build: root => root
.add({ '@sern/client': single(() => client) })
.upsert({ '@sern/logger': single(() => new DefaultLogging()) })
});
//View docs for all options
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({
```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('pong!');
ctx.reply('Hello owner of the bot');
}
});
```
</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.
- [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
- [Murayama](https://github.com/murayamabot/murayama), :pepega:
- [Protector](https://github.com/GlitchApotamus/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.
- [Man Nomic](https://github.com/jacoobes/man-nomic), A simple information bot to provide information to the nomic-ai discord community.
- [Linear-Discord](https://github.com/sern-handler/linear-discord) Display and manage a linear dashboard.
## 💻 CLI
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.
@@ -112,9 +63,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.

1
fortnite Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,25 +1,27 @@
{
"name": "@sern/handler",
"packageManager": "pnpm@7.27.0",
"version": "2.5.3",
"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": "4.1.0",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "./dist/index.js",
"module": "./dist/index.js",
"exports": {
".": {
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"scripts": {
"watch": "tsup --dts --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"watch": "tsc --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": "tsc",
"build:prod": "tsc",
"prepare": "tsc",
"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",
@@ -33,23 +35,63 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-pattern": "^4.1.4",
"ts-results-es": "^3.5.0"
"@sern/ioc": "^1.1.0",
"callsites": "^3.1.0",
"cron": "^3.1.7",
"deepmerge": "^4.3.1",
"rxjs": "^7.8.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": "^20.0.0",
"@types/node-cron": "^3.0.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"discord.js": "^14.15.3",
"eslint": "8.39.0",
"typescript": "5.0.2",
"vitest": "^1.6.0"
},
"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",
"url": "git+https://github.com/sern-handler/handler.git"
},
"homepage": "https://sern.dev"
"engines": {
"node": ">= 20.0.x"
},
"homepage": "https://sern.dev",
"overrides": {
"ws": "8.17.1"
},
"resolutions": {
"ws": "8.17.1"
}
}

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' }));
}
}

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

@@ -0,0 +1,126 @@
import type { Module, SernAutocompleteData, SernOptionsData } from '../types/core-modules';
import type {
AnySelectMenuInteraction,
ButtonInteraction,
ChatInputCommandInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
AutocompleteInteraction
} from 'discord.js';
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
import { PluginType } from './structures/enums';
import assert from 'assert';
import type { Payload } from '../types/utility';
/**
* 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.content, '!'));
* // [ 'ping' ]
*/
export function fmt(msg: string, prefix?: string): string[] {
if(!prefix) throw Error("Unable to parse message without prefix");
return msg.slice(prefix.length).trim().split(/\s+/g);
}
export function partitionPlugins<T,V>
(arr: Array<{ type: PluginType }> = []): [T[], V[]] {
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] as [T[], V[]];
}
/**
* 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 }));
const 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 option ' + cur.name);
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;
}
}
}
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;
}
export function resultPayload<T extends 'success'|'warning'|'failure'>
(type: T, module?: Module, reason?: unknown) {
return { type, module, reason } as Payload & { type : T };
}
export function pipe<T>(arg: unknown, firstFn: Function, ...fns: Function[]): T {
let result = firstFn(arg);
for (let fn of fns) {
result = fn(result);
}
return result;
}

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

@@ -0,0 +1,74 @@
import { ApplicationCommandType, ComponentType, type Interaction, InteractionType } from 'discord.js';
import { CommandType, EventType } from './structures/enums';
const parseParams = (event: { customId: string }, append: string) => {
const hasSlash = event.customId.indexOf('/')
if(hasSlash === -1) {
return { id:event.customId+append };
}
const baseid = event.customId.substring(0, hasSlash);
const params = event.customId.substring(hasSlash+1);
return { id: baseid+append, params }
}
/**
* Construct unique ID for a given interaction object.
* @param event The interaction object for which to create an ID.
* @returns An array of unique string IDs based on the type and properties of the interaction object.
*/
export function reconstruct<T extends Interaction>(event: T) {
switch (event.type) {
case InteractionType.MessageComponent: {
const data = parseParams(event, `_C${event.componentType}`)
return [data];
}
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete:
return [{ id: `${event.commandName}_A${event.commandType}` }, { id: `${event.commandName}_B` }];
//Modal interactions are classified as components for sern
case InteractionType.ModalSubmit: {
const data = parseParams(event, '_M');
return [data];
}
}
}
/**
*
* A magic number to represent any commandtype that is an ApplicationCommand.
*/
const PUBLISHABLE = 0b000000001111;
const TypeMap = new Map<number, number>([[CommandType.Text, 0],
[CommandType.Both, 0],
[CommandType.Slash, ApplicationCommandType.ChatInput],
[CommandType.CtxUser, ApplicationCommandType.User],
[CommandType.CtxMsg, ApplicationCommandType.Message],
[CommandType.Button, ComponentType.Button],
[CommandType.StringSelect, ComponentType.StringSelect],
[CommandType.Modal, InteractionType.ModalSubmit],
[CommandType.UserSelect, ComponentType.UserSelect],
[CommandType.MentionableSelect, ComponentType.MentionableSelect],
[CommandType.RoleSelect, ComponentType.RoleSelect],
[CommandType.ChannelSelect, ComponentType.ChannelSelect]]);
/*
* Generates an id based on name and CommandType.
* A is for any ApplicationCommand. C is for any ComponentCommand
* Then, another number fetched from TypeMap
*/
export function create(name: string, type: CommandType | EventType) {
if(type == CommandType.Text) {
return `${name}_T`;
}
if(type == CommandType.Both) {
return `${name}_B`;
}
if(type == CommandType.Modal) {
return `${name}_M`;
}
const am = (PUBLISHABLE & type) !== 0 ? 'A' : 'C';
return `${name}_${am}${TypeMap.get(type)!}`
}

55
src/core/interfaces.ts Normal file
View File

@@ -0,0 +1,55 @@
import type { AnyFunction } from '../types/utility';
/**
* Represents an initialization contract.
* Let dependencies implement this to initiate some logic.
*/
export interface Init {
init(): unknown;
}
/**
* Represents a Disposable contract.
* Let dependencies implement this to dispose and cleanup.
*/
export interface Disposable {
dispose(): unknown;
}
export interface Emitter {
addListener(eventName: string | symbol, listener: AnyFunction): this;
removeListener(eventName: string | symbol, listener: AnyFunction): this;
emit(eventName: string | symbol, ...payload: any[]): boolean;
}
/**
* @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;
}
/**
* @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 };

163
src/core/ioc.ts Normal file
View File

@@ -0,0 +1,163 @@
import { Service as $Service, Services as $Services } from '@sern/ioc/global'
import { Container } from '@sern/ioc';
import * as Contracts from './interfaces';
import * as __Services from './structures/default-services';
import type { Logging } from './interfaces';
import { __init_container, useContainerRaw } from '@sern/ioc/global';
import { EventEmitter } from 'node:events';
import { Client } from 'discord.js';
import { Module } from '../types/core-modules';
import { UnpackFunction } from '../types/utility';
export function disposeAll(logger: Logging|undefined) {
useContainerRaw()
?.disposeAll()
.then(() => logger?.info({ message: 'Cleaning container and crashing' }));
}
type Insertable = | ((container: Dependencies) => object)
| object
const dependencyBuilder = (container: Container) => {
return {
/**
* Insert a dependency into your container.
* Supply the correct key and dependency
*/
add(key: keyof Dependencies, v: Insertable) {
if(typeof v !== 'function') {
container.addSingleton(key, v)
} else {
//@ts-ignore
container.addWiredSingleton(key, (cntr) => v(cntr))
}
},
/**
* @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) {
if(typeof v !== 'function') {
container.swap(key, v);
} else {
container.swap(key, v(container.deps()));
}
},
};
};
type ValidDependencyConfig =
(c: ReturnType<typeof dependencyBuilder>) => any
/**
* makeDependencies constructs a dependency injection container for sern handler to use.
* This is required to start the handler, and is to be called before Sern.init.
* @example
* ```ts
* await makeDependencies(({ add }) => {
* add('@sern/client', new Client({ intents, partials })
* })
* ```
*/
export async function makeDependencies (conf: ValidDependencyConfig) {
const container = await __init_container({ autowire: false });
//We only include logger if it does not exist
const includeLogger = !container.hasKey('@sern/logger');
if(includeLogger) {
container.addSingleton('@sern/logger', new __Services.DefaultLogging);
}
container.addSingleton('@sern/errors', new __Services.DefaultErrorHandling);
container.addSingleton('@sern/modules', new Map);
container.addSingleton('@sern/emitter', new EventEmitter)
container.addSingleton('@sern/scheduler', new __Services.TaskScheduler)
conf(dependencyBuilder(container));
await container.ready();
}
/**
* The Service api, which allows users to access dependencies in places IOC cannot reach.
* To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
* Our scaffolding tool takes care of this.
* Note: this method only works AFTER your container has been initiated
* @since 3.0.0
* @example
* ```ts
* const client = Service('@sern/client');
* ```
* @param key a key that corresponds to a dependency registered.
* @throws if container is absent or not present
*/
export function Service<const T extends keyof Dependencies>(key: T) {
return $Service(key) as Dependencies[T]
}
/**
* @since 3.0.0
* The plural version of {@link Service}
* @throws if container is absent or not present
* @returns array of dependencies, in the same order of keys provided
*
*/
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
return $Services<T, IntoDependencies<T>>(...keys)
}
/**
* @deprecated
* Creates a singleton object.
* @param cb
*/
export function single<T>(cb: () => T) {
console.log('The `single` function is deprecated and has no effect')
return cb();
}
/**
* @deprecated
* @since 2.0.0
* Creates a transient object
* @param cb
*/
export function transient<T>(cb: () => () => T) {
console.log('The `transient` function is deprecated and has no effect')
return cb()();
}
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 CoreDependencies {
/**
* discord.js client.
*/
'@sern/client': Client;
/**
* sern emitter listens to events that happen throughout
* the handler. some include module.register, module.activate.
*/
'@sern/emitter': Contracts.Emitter;
/**
* An error handler which is the final step before
* the sern process actually crashes.
*/
'@sern/errors': Contracts.ErrorHandling;
/**
* Optional logger. Performs ... logging
*/
'@sern/logger'?: Contracts.Logging;
/**
* Readonly module store. sern stores these
* by module.meta.id -> Module
*/
'@sern/modules': Map<string, Module>;
'@sern/scheduler': __Services.TaskScheduler
}

View File

@@ -0,0 +1,70 @@
import path from 'node:path';
import { existsSync } from 'node:fs';
import { readdir } from 'fs/promises';
import assert from 'node:assert';
import * as Id from './id'
import { Module } from '../types/core-modules';
export const parseCallsite = (site: string) => {
const pathobj = path.posix.parse(site.replace(/file:\\?/, "")
.split(path.sep)
.join(path.posix.sep))
return { name: pathobj.name,
absPath : path.posix.format(pathobj) }
}
export const shouldHandle = (pth: string, filenam: string) => {
const file_name = filenam+path.extname(pth);
let newPath = path.join(path.dirname(pth), file_name)
.replace(/file:\\?/, "");
return { exists: existsSync(newPath),
path: 'file://'+newPath };
}
/**
* 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: Module = fileModule.default;
assert(commandModule , `No default export @ ${absPath}`);
if ('default' in commandModule) {
commandModule = commandModule.default as Module;
}
const p = path.parse(absPath)
commandModule.name ??= p.name; commandModule.description ??= "...";
commandModule.meta = {
id: Id.create(commandModule.name, commandModule.type),
absPath,
};
return { module: commandModule as T };
}
export async function* readRecursive(dir: string): AsyncGenerator<string> {
const files = await readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.posix.join(dir, file.name);
if (file.isDirectory()) {
if (!file.name.startsWith('!')) {
yield* readRecursive(fullPath);
}
} else if (!file.name.startsWith('!')) {
yield "file:///"+path.resolve(fullPath);
}
}
}

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

@@ -0,0 +1,106 @@
import type { ClientEvents } from 'discord.js';
import { EventType } from '../core/structures/enums';
import type {
InputCommand,
InputEvent,
Module,
ScheduledTask,
} from '../types/core-modules';
import { partitionPlugins } from './functions'
import type { Awaitable } from '../types/utility';
/**
* Creates a command module with standardized structure and plugin support.
*
* @since 1.0.0
* @param {InputCommand} mod - Command module configuration
* @returns {Module} Processed command module ready for registration
*
* @example
* // Basic slash command
* export default commandModule({
* type: CommandType.Slash,
* description: "Ping command",
* execute: async (ctx) => {
* await ctx.reply("Pong! 🏓");
* }
* });
*
* @example
* // Command with component interaction
* export default commandModule({
* type: CommandType.Slash,
* description: "Interactive command",
* execute: async (ctx) => {
* const button = new ButtonBuilder({
* customId: "btn/someData",
* label: "Click me",
* style: ButtonStyle.Primary
* });
* await ctx.reply({
* content: "Interactive message",
* components: [new ActionRowBuilder().addComponents(button)]
* });
* }
* });
*/
export function commandModule(mod: InputCommand): Module {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
return { ...mod,
onEvent,
plugins,
locals: {} } as Module;
}
/**
* Creates an event module for handling Discord.js or custom events.
*
* @since 1.0.0
* @template T - Event name from ClientEvents
* @param {InputEvent<T>} mod - Event module configuration
* @returns {Module} Processed event module ready for registration
* @throws {Error} If ControlPlugins are used in event modules
*
* @example
* // Discord event listener
* export default eventModule({
* type: EventType.Discord,
* execute: async (message) => {
* console.log(`${message.author.tag}: ${message.content}`);
* }
* });
*
* @example
* // Custom sern event
* export default eventModule({
* type: EventType.Sern,
* execute: async (eventData) => {
* // Handle sern-specific event
* }
* });
*/
export function eventModule<T extends keyof ClientEvents = keyof ClientEvents>(mod: InputEvent<T>): Module {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
if(onEvent.length !== 0) throw Error("Event modules cannot have ControlPlugins");
return { ...mod,
plugins,
locals: {} } as Module;
}
/** Create event modules from discord.js client events,
* This was an {@link eventModule} for discord events,
* where typings were bad.
* @deprecated Use {@link eventModule} instead
* @param mod
*/
export function discordEvent<T extends keyof ClientEvents>(mod: {
name: T;
once?: boolean;
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
}) {
return eventModule({ type: EventType.Discord, ...mod, });
}
export function scheduledTask(ism: ScheduledTask) { return ism }

46
src/core/plugin.ts Normal file
View File

@@ -0,0 +1,46 @@
import { CommandType, PluginType } from './structures/enums';
import type { Plugin, PluginResult, CommandArgs, InitArgs } from '../types/core-plugin';
import { Err, Ok } from './structures/result';
import type { Dictionary } from '../types/utility';
export function makePlugin<V extends unknown[]>(
type: PluginType,
execute: (...args: any[]) => any,
): Plugin<V> {
return { type, execute } as Plugin<V>;
}
/**
* @since 2.5.0
*/
export function EventInitPlugin(execute: (args: InitArgs) => PluginResult) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (args: InitArgs) => PluginResult
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: (val?: Dictionary) => Ok(val),
stop: (val?: string) => Err(val),
};
export type Controller = typeof controller;

65
src/core/presences.ts Normal file
View File

@@ -0,0 +1,65 @@
import type { ActivitiesOptions } from "discord.js";
import type { IntoDependencies } from "./ioc";
import type { Emitter } from "./interfaces";
import { Awaitable } from "../types/utility";
type Status = 'online' | 'idle' | 'invisible' | 'dnd'
type PresenceReduce = (previous: Presence.Result) => Awaitable<Presence.Result>;
export const Presence = {
/**
* A small wrapper to provide type inference.
* Create a Presence module which **MUST** be put in a file called presence.(language-extension)
* adjacent to the file where **Sern.init** is CALLED.
*/
module : <T extends (keyof Dependencies)[]>(conf: Presence.Config<T>) => conf,
/**
* Create a Presence body which can be either:
* - once, the presence is activated only once.
* - repeated, per cycle or event, the presence can be changed.
*/
of : (root: Omit<Presence.Result, 'repeat' | 'onRepeat'>) => {
return {
/**
* @example
* Presence
* .of({ activities: [{ name: "deez nuts" }] }) //starts presence with "deez nuts".
* .repeated(prev => {
* return {
* afk: true,
* activities: prev.activities?.map(s => ({ ...s, name: s.name+"s" }))
* };
* }, 10000)) //every 10 s, the callback sets the presence to the value returned.
*/
repeated: (onRepeat: PresenceReduce, repeat: number | [Emitter, string]) => {
return { repeat, onRepeat, ...root }
},
/**
* @example
* ```ts
* Presence.of({
* activities: [{ name: "Chilling out" }]
* }).once() // Sets the presence once, with what's provided in '.of()'
* ```
*/
once: () => root
};
}
}
export declare namespace Presence {
export type Config<T extends (keyof Dependencies)[]> = {
inject?: [...T]
execute: (...v: IntoDependencies<T>) => Awaitable<Presence.Result>;
}
export interface Result {
status?: Status;
afk?: boolean;
activities?: ActivitiesOptions[];
shardId?: number[];
repeat?: number | [Emitter, string];
onRepeat?: PresenceReduce
}
}

View File

@@ -0,0 +1,131 @@
import type {
BaseInteraction,
ChatInputCommandInteraction,
Client,
InteractionReplyOptions,
Message,
MessageReplyOptions,
Snowflake,
User,
} from 'discord.js';
import { Result, Ok, Err, val } from './result';
import * as assert from 'assert';
import type { ReplyOptions } from '../../types/utility';
import { fmt } from '../functions'
import { SernError } from './enums';
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export class Context {
get options() {
if(this.isMessage()) {
const [, ...rest] = fmt(this.message.content, this.prefix);
return rest;
}
return this.interaction.options;
}
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>,
private __prefix?: string) { }
public get prefix() {
return this.__prefix;
}
public get id(): Snowflake {
return val(this.ctx).id
}
public get channel() {
return val(this.ctx).channel;
}
public get channelId(): Snowflake {
return val(this.ctx).channelId;
}
/**
* If context is holding a message, message.author
* else, interaction.user
*/
public get user(): User {
if(this.ctx.ok) {
return this.ctx.value.author;
}
return this.ctx.error.user;
}
public get userId(): Snowflake {
return this.user.id;
}
public get createdTimestamp(): number {
return val(this.ctx).createdTimestamp;
}
public get guild() {
return val(this.ctx).guild;
}
public get guildId() {
return val(this.ctx).guildId;
}
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
public get member() {
return val(this.ctx).member;
}
get message(): Message {
if(this.ctx.ok) {
return this.ctx.value;
}
throw Error(SernError.MismatchEvent);
}
public isMessage(): this is Context & { ctx: Result<Message, never> } {
return this.ctx.ok;
}
public isSlash(): this is Context & { ctx: Result<never, ChatInputCommandInteraction> } {
return !this.isMessage();
}
get interaction(): ChatInputCommandInteraction {
if(!this.ctx.ok) {
return this.ctx.error;
}
throw Error(SernError.MismatchEvent);
}
public get client(): Client {
return val(this.ctx).client;
}
public get inGuild(): boolean {
return val(this.ctx).inGuild()
}
public async reply(content: ReplyOptions) {
if(this.ctx.ok) {
return this.ctx.value.reply(content as MessageReplyOptions)
}
interface FetchReply { fetchReply: true };
return this.ctx.error.reply(content as InteractionReplyOptions & FetchReply)
}
static wrap(wrappable: BaseInteraction | Message, prefix?: string): Context {
if ('interaction' in wrappable) {
return new Context(Ok(wrappable), prefix);
}
assert.ok(wrappable.isChatInputCommand(), "Context created with bad interaction.");
return new Context(Err(wrappable), prefix);
}
}

View File

@@ -0,0 +1,89 @@
import { ScheduledTask } from '../../types/core-modules';
import type { LogPayload, Logging, ErrorHandling, Disposable } from '../interfaces';
import { CronJob } from 'cron';
/**
* @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;
}
updateAlive(err: Error) {
throw err;
}
}
/**
* @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() { return new Date() }
debug(payload: LogPayload): void {
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
}
error(payload: LogPayload): void {
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
}
info(payload: LogPayload): void {
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
}
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}
export class TaskScheduler implements Disposable {
private __tasks: Map<string, CronJob<any, any>> = new Map();
schedule(uuid: string, task: ScheduledTask, deps: Dependencies) {
if (this.__tasks.has(uuid)) {
throw Error("while scheduling a task \
found another task of same name. Not scheduling " +
uuid + "again." );
}
try {
const onTick = async function(this: CronJob) {
task.execute({ id: uuid,
lastTimeExecution: this.lastExecution,
nextTimeExecution: this.nextDate().toJSDate() }, { deps })
}
const job = CronJob.from({ cronTime: task.trigger, onTick, timeZone: task.timezone });
job.start();
this.__tasks.set(uuid, job);
} catch (error) {
throw Error(`while scheduling a task ${uuid} ` + error);
}
}
kill(taskName: string): boolean {
const job = this.__tasks.get(taskName);
if (job) {
job.stop();
this.__tasks.delete(taskName);
return true;
}
return false;
}
get tasks(): string[] {
return Array.from(this.__tasks.keys());
}
dispose() {
this.__tasks.forEach((_, id) => {
this.kill(id);
this.__tasks.delete(id);
})
}
}

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,
}
/**
@@ -74,16 +48,16 @@ export enum EventType {
/**
* The EventType for handling discord events
*/
Discord = 1,
Discord,
/**
* The EventType for handling sern events
*/
Sern = 2,
Sern,
/**
* The EventType for handling external events.
* Could be for example, `process` events, database events
*/
External = 3,
External,
}
/**
@@ -105,35 +79,56 @@ 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
*/
Control = 2,
}
/**
* @deprecated - Use strings 'success' | 'failure' | 'warning'
* @enum { string }
*/
export enum PayloadType {
/**
* The PayloadType for a SernEmitter success event
*/
Success = 'success',
/**
* The PayloadType for a SernEmitter failure event
*/
Failure = 'failure',
/**
* The PayloadType for a SernEmitter warning event
*/
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,20 @@
export type Result<Ok, Err> =
| { ok: true; value: Ok }
| { ok: false; error: Err };
export const Ok = <Ok>(value: Ok) => ({ ok: true, value } as const);
export const Err = <Err>(error: Err) => ({ ok: false, error } as const);
export const val = <O, E>(r: Result<O, E>) => r.ok ? r.value : r.error;
export const EMPTY_ERR = Err(undefined);
/**
* Wrap an async operation that may throw an Error (`try-catch` style) into checked exception style
* @param op The operation function
*/
export async function wrapAsync<T, E = unknown>(op: () => Promise<T>): Promise<Result<T, E>> {
try { return op()
.then(Ok)
.catch(Err); }
catch (e) { return Promise.resolve(Err(e as E)); }
}

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,27 +0,0 @@
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;
}
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
}
error(payload: LogPayload): void {
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
}
info(payload: LogPayload): void {
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
}
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}

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,2 +0,0 @@
export * from './dispatchers';
export * from './provideArgs';

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

View File

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

View File

@@ -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,85 +0,0 @@
import { EventEmitter } from 'events';
import type { Payload, SernEventsMapping } from '../types/handler';
import { PayloadType } from './structures';
import type { Module } from '../types/module';
class SernEmitter extends EventEmitter {
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param listener what to do with the data
*/
public override on<T extends keyof SernEventsMapping>(
eventName: T,
listener: (...args: SernEventsMapping[T][]) => void,
): this {
return super.on(eventName, listener);
}
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param listener what to do with the data
*/
public override once<T extends keyof SernEventsMapping>(
eventName: T,
listener: (...args: SernEventsMapping[T][]) => void,
): this {
return super.once(eventName, listener);
}
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param args the arguments for emitting the eventName
*/
public override emit<T extends keyof SernEventsMapping>(
eventName: T,
...args: SernEventsMapping[T]
): boolean {
return super.emit(eventName, ...args);
}
private static payload<T extends Payload>(
type: PayloadType,
module?: Module,
reason?: unknown,
) {
return { type, module, reason } as T;
}
/**
* Creates a compliant SernEmitter failure payload
* @param module
* @param reason
*/
static failure(module?: Module, reason?: unknown) {
//The generic cast Payload & { type : PayloadType.* } coerces the type to be a failure payload
// same goes to the other methods below
return SernEmitter.payload<Payload & { type: PayloadType.Failure }>(
PayloadType.Failure,
module,
reason,
);
}
/**
* Creates a compliant SernEmitter module success payload
* @param module
*/
static success(module: Module) {
return SernEmitter.payload<Payload & { type: PayloadType.Success }>(
PayloadType.Success,
module,
);
}
/**
* Creates a compliant SernEmitter module warning payload
* @param reason
*/
static warning(reason: unknown) {
return SernEmitter.payload<Payload & { type: PayloadType.Warning }>(
PayloadType.Warning,
undefined,
reason,
);
}
}
export default SernEmitter;

View File

@@ -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;
}

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

@@ -0,0 +1,294 @@
import type { Interaction, Message, BaseInteraction } from 'discord.js';
import util from 'node:util';
import {
EMPTY, type Observable, concatMap, filter,
throwError, fromEvent, map, type OperatorFunction,
catchError, finalize, pipe, from, take, share, of,
} from 'rxjs';
import * as Id from '../core/id'
import type { Emitter, ErrorHandling, Logging } from '../core/interfaces';
import { SernError } from '../core/structures/enums'
import { EMPTY_ERR, Err, Ok, Result, wrapAsync } from '../core/structures/result';
import type { UnpackedDependencies } from '../types/utility';
import type { CommandModule, Module, Processed } from '../types/core-modules';
import * as assert from 'node:assert';
import { Context } from '../core/structures/context';
import { CommandType } from '../core/structures/enums'
import { inspect } from 'node:util'
import { disposeAll } from '../core/ioc';
import { resultPayload, isAutocomplete, treeSearch, fmt } from '../core/functions'
import merge from 'deepmerge'
function handleError<C>(crashHandler: ErrorHandling, emitter: Emitter, logging?: Logging) {
return (pload: unknown, caught: Observable<C>) => {
// This is done to fit the ErrorHandling contract
if(!emitter.emit('error', pload)) {
const err = pload instanceof Error ? pload : Error(util.inspect(pload, { colors: true }));
logging?.error({ message: util.inspect(pload) });
crashHandler.updateAlive(err);
}
return caught;
};
}
const arrayify= <T>(src: T) =>
Array.isArray(src) ? src : [src];
interface ExecutePayload {
module: Module;
args: unknown[];
deps: Dependencies;
params?: string;
[key: string]: unknown
}
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
concatMap(result => {
if(result.ok){
return of(result.value)
}
onErr(result.error);
return EMPTY;
})
export const sharedEventStream = <T>(e: Emitter, eventName: string) =>
(fromEvent(e, eventName) as Observable<T>).pipe(share());
function intoPayload(module: Module, deps: Dependencies) {
return pipe(map(arrayify),
map(args => ({ module, args, deps })),
map(p => p.args));
}
/**
* Creates an observable from { source }
* @param module
* @param source
*/
export function eventDispatcher(deps: Dependencies, module: Module, source: unknown) {
assert.ok(source && typeof source === 'object',
`${source} cannot be constructed into an event listener`);
const execute: OperatorFunction<unknown[]|undefined, unknown> =
concatMap(async args => {
if(args) return Reflect.apply(module.execute, null, args);
});
//@ts-ignore
let ev = fromEvent(source ,module.name!);
//@ts-ignore
if(module['once']) {
ev = ev.pipe(take(1))
}
return ev.pipe(intoPayload(module, deps),
execute);
}
interface DispatchPayload {
module: Processed<CommandModule>;
event: BaseInteraction;
defaultPrefix?: string;
deps: Dependencies;
params?: string
};
export function createDispatcher({ module, event, defaultPrefix, deps, params }: DispatchPayload): ExecutePayload {
assert.ok(CommandType.Text !== module.type,
SernError.MismatchEvent + 'Found text command in interaction stream');
if(isAutocomplete(event)) {
assert.ok(module.type === CommandType.Slash
|| module.type === CommandType.Both, "Autocomplete option on non command interaction");
const option = treeSearch(event, module.options);
assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(module));
const { command } = option;
return { module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [event],
deps };
}
switch (module.type) {
case CommandType.Slash:
case CommandType.Both: {
return { module, args: [Context.wrap(event, defaultPrefix)], deps };
}
default: return { module, args: [event], deps, params };
}
}
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), // only handle this stream if it passes pred
concatMap(makeModule)); // create a payload, preparing to execute
}
/**
*
* 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>,
deps: Dependencies,
defaultPrefix?: string
) {
const mg = deps['@sern/modules'];
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
source,
async event => {
const possibleIds = Id.reconstruct(event);
let modules = possibleIds
.map(({ id, params }) => ({ module: mg.get(id), params }))
.filter(({ module }) => module !== undefined);
if(modules.length == 0) {
return EMPTY_ERR;
}
const [{module, params}] = modules;
return Ok(createDispatcher({
module: module as Processed<CommandModule>,
event, defaultPrefix, deps, params
}));
});
}
export function createMessageHandler(
source: Observable<Message>,
defaultPrefix: string,
deps: Dependencies,
) {
const mg = deps['@sern/modules'];
return createGenericHandler(source, async event => {
const [prefix] = fmt(event.content, defaultPrefix);
let module= mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
if(!module) {
return Err('Possibly undefined behavior: could not find a static id to resolve');
}
return Ok({ args: [Context.wrap(event, defaultPrefix)], module, deps })
});
}
/**
* 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
* thank u kingomes
* @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, { module, args }: ExecutePayload) {
return from(wrapAsync(async () => module.execute(...args)))
.pipe(concatMap(result => {
if (result.ok){
emitter.emit('module.activate', resultPayload('success', module));
return EMPTY;
}
return throwError(() => resultPayload('failure', module, result.error));
}))
};
/**
* A higher order function that
* - calls all control plugins.
* - any failures results to { config.onStop } being called
* - if all results are ok, the stream is converted to { config.onNext }
* config.onNext will be returned if everything is okay.
* @param config
* @returns function which calls all plugins and returns onNext or fail
*/
export function createResultResolver<Output>(config: {
onStop?: (module: Module, err?: string) => unknown;
onNext: (args: ExecutePayload, map: Record<string, unknown>) => Output;
}) {
const { onStop, onNext } = config;
return async (payload: ExecutePayload) => {
const task = await callPlugins(payload);
if (!task) throw Error("Plugin did not return anything.");
if(!task.ok) {
onStop?.(payload.module, String(task.error));
} else {
return onNext(payload, task.value) as Output;
}
};
};
function isObject(item: unknown) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
//_module is frozen, preventing from mutations
export async function callInitPlugins(_module: Module, deps: Dependencies, emit?: boolean) {
let module = _module;
const emitter = deps['@sern/emitter'];
for(const plugin of module.plugins ?? []) {
const result = await plugin.execute({ module, absPath: module.meta.absPath, deps });
if (!result) throw Error("Plugin did not return anything. " + inspect(plugin, false, Infinity, true));
if(!result.ok) {
if(emit) {
emitter?.emit('module.register',
resultPayload('failure', module, result.error ?? SernError.PluginFailure));
}
throw Error((result.error ?? SernError.PluginFailure) +
'on module ' + module.name + " " + module.meta.absPath);
}
}
return module
}
export async function callPlugins({ args, module, deps, params }: ExecutePayload) {
let state = {};
for(const plugin of module.onEvent??[]) {
const executionContext = {
state,
deps,
params,
type: module.type,
module: { name: module.name,
description: module.description,
locals: module.locals,
meta: module.meta }
};
const result = await plugin.execute(...args, executionContext);
if(!result.ok) {
return result;
}
if(isObject(result.value)) {
state = merge(state, result.value!);
}
}
return Ok(state);
}
/**
* Creates an executable task ( execute the command ) if all control plugins are successful
* this needs to go
* @param onStop emits a failure response to the SernEmitter
*/
export function intoTask(onStop: (m: Module) => unknown) {
const onNext = ({ args, module, deps, params }: ExecutePayload, state: Record<string, unknown>) => {
return {
module,
args: [...args, { state,
deps,
params,
type: module.type,
module: { name: module.name,
description: module.description,
locals: module.locals,
meta: module.meta } }],
deps
}
};
return createResultResolver({ onStop, onNext });
}
export const handleCrash = ({ "@sern/errors": err, '@sern/emitter': sem, '@sern/logger': log } : UnpackedDependencies, metadata: string) =>
pipe(catchError(handleError(err, sem, log)),
finalize(() => {
log?.info({ message: 'A stream closed: ' + metadata });
disposeAll(log);
}))

View File

@@ -0,0 +1,30 @@
import type { Interaction } from 'discord.js';
import { mergeMap, merge, concatMap, EMPTY } from 'rxjs';
import { createInteractionHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash } from './event-utils';
import { SernError } from '../core/structures/enums'
import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
import { UnpackedDependencies } from '../types/utility';
export default function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) {
//i wish javascript had clojure destructuring
const { '@sern/client': client,
'@sern/emitter': emitter } = deps
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler(interactionStream$, deps, defaultPrefix);
const interactionHandler$ = merge(handle(isMessageComponent),
handle(isAutocomplete),
handle(isCommand),
handle(isModal));
return interactionHandler$
.pipe(filterTap(e => emitter.emit('warning', resultPayload('warning', undefined, e))),
concatMap(intoTask(module => {
emitter.emit('module.activate', resultPayload('failure', module, SernError.PluginFailure))
})),
mergeMap(payload => {
if(payload)
return executeModule(emitter, payload)
return EMPTY;
}),
handleCrash(deps, "interaction handling"));
}

50
src/handlers/message.ts Normal file
View File

@@ -0,0 +1,50 @@
import { EMPTY, mergeMap, concatMap } from 'rxjs';
import type { Message } from 'discord.js';
import { createMessageHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash} from './event-utils';
import { SernError } from '../core/structures/enums'
import { resultPayload } from '../core/functions'
import { UnpackedDependencies } from '../types/utility';
import type { Emitter } from '../core/interfaces';
/**
* 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 default
function (deps: UnpackedDependencies, defaultPrefix?: string) {
const {"@sern/emitter": emitter,
'@sern/logger': log,
'@sern/client': client} = deps
if (!defaultPrefix) {
log?.debug({ message: 'No prefix found. message handler shutting down' });
return EMPTY;
}
const messageStream$ = sharedEventStream<Message>(client as unknown as Emitter, 'messageCreate');
const handle = createMessageHandler(messageStream$, defaultPrefix, deps);
const msgCommands$ = handle(isNonBot(defaultPrefix));
return msgCommands$.pipe(
filterTap(e => emitter.emit('warning', resultPayload('warning', undefined, e))),
concatMap(intoTask(module => {
const result = resultPayload('failure', module, SernError.PluginFailure);
emitter.emit('module.activate', result);
})),
mergeMap(payload => {
if(payload)
return executeModule(emitter, payload)
return EMPTY;
}),
handleCrash(deps, "message handling")
)
}

40
src/handlers/presence.ts Normal file
View File

@@ -0,0 +1,40 @@
import { concatMap, from, interval, of, map, startWith, fromEvent, take, mergeScan } from "rxjs"
import { Presence } from "../core/presences";
import { Services } from "../core/ioc";
import assert from "node:assert";
import * as Files from "../core/module-loading";
type SetPresence = (conf: Presence.Result) => Promise<unknown>
const parseConfig = async (conf: Promise<Presence.Result>) => {
return conf.then(s => {
if('repeat' in s) {
const { onRepeat, repeat } = s;
assert(repeat !== undefined, "repeat option is undefined");
assert(onRepeat !== undefined, "onRepeat callback is undefined, but repeat exists");
const src$ = typeof repeat === 'number'
? interval(repeat)
: fromEvent(...repeat);
return src$.pipe(mergeScan(async (args) => onRepeat(args), s),
startWith(s));
}
return of(s).pipe(take(1));
})
};
export const presenceHandler = (path: string, setPresence: SetPresence) => {
const presence = Files
.importModule<Presence.Config<(keyof Dependencies)[]>>(path)
.then(({ module }) => {
//fetch services with the order preserved, passing it to the execute fn
const fetchedServices = Services(...module.inject ?? []);
return async () => module.execute(...fetchedServices);
})
const module$ = from(presence);
return module$.pipe(
//compose:
//call the execute function, passing that result into parseConfig.
//concatMap resolves the promise, and passes it to the next concatMap.
concatMap(fn => parseConfig(fn())),
// subscribe to the observable parseConfig yields, and set the presence.
concatMap(conf => conf.pipe(map(setPresence))));
}

32
src/handlers/ready.ts Normal file
View File

@@ -0,0 +1,32 @@
import * as Files from '../core/module-loading'
import { once } from 'node:events';
import { resultPayload } from '../core/functions';
import { CommandType } from '../core/structures/enums';
import { Module } from '../types/core-modules';
import { UnpackedDependencies } from '../types/utility';
import { callInitPlugins } from './event-utils';
export default async function(dir: string, deps : UnpackedDependencies) {
const { '@sern/client': client,
'@sern/logger': log,
'@sern/emitter': sEmitter,
'@sern/modules': commands } = deps;
log?.info({ message: "Waiting on discord client to be ready..." })
await once(client, "ready");
log?.info({ message: "Client signaled ready, registering modules" });
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
// possibly optimize to concurrently import modules
for await (const path of Files.readRecursive(dir)) {
let { module } = await Files.importModule<Module>(path);
const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect;
if(!validType) {
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
}
const resultModule = await callInitPlugins(module, deps, true);
// FREEZE! no more writing!!
commands.set(resultModule.meta.id, Object.freeze(resultModule));
sEmitter.emit('module.register', resultPayload('success', resultModule));
}
sEmitter.emit('modulesLoaded');
}

16
src/handlers/tasks.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as Files from '../core/module-loading'
import { UnpackedDependencies } from "../types/utility";
import type { ScheduledTask } from "../types/core-modules";
import { relative } from "path";
import { fileURLToPath } from "url";
export const registerTasks = async (tasksPath: string, deps: UnpackedDependencies) => {
const taskManager = deps['@sern/scheduler']
for await (const f of Files.readRecursive(tasksPath)) {
let { module } = await Files.importModule<ScheduledTask>(f);
//module.name is assigned by Files.importModule<>
// the id created for the task is unique
const uuid = module.name+"/"+relative(tasksPath,fileURLToPath(f))
taskManager.schedule(uuid, module, deps)
}
}

View File

@@ -0,0 +1,33 @@
import { EventType, SernError } from '../core/structures/enums';
import { callInitPlugins, eventDispatcher, handleCrash } from './event-utils'
import { EventModule, Module } from '../types/core-modules';
import * as Files from '../core/module-loading'
import type { UnpackedDependencies } from '../types/utility';
import { from, map, mergeAll } from 'rxjs';
const intoDispatcher = (deps: UnpackedDependencies) =>
(module : EventModule) => {
switch (module.type) {
case EventType.Sern:
return eventDispatcher(deps, module, deps['@sern/emitter']);
case EventType.Discord:
return eventDispatcher(deps, module, deps['@sern/client']);
case EventType.External:
return eventDispatcher(deps, module, deps[module.emitter]);
default: throw Error(SernError.InvalidModuleType + ' while creating event handler');
}
};
export default async function(deps: UnpackedDependencies, eventDir: string) {
const eventModules: EventModule[] = [];
for await (const path of Files.readRecursive(eventDir)) {
let { module } = await Files.importModule<Module>(path);
await callInitPlugins(module, deps)
eventModules.push(module as EventModule);
}
from(eventModules)
.pipe(map(intoDispatcher(deps)),
mergeAll(), // all eventListeners are turned on
handleCrash(deps, "event modules"))
.subscribe();
}

View File

@@ -1,18 +1,72 @@
import SernEmitter from './handler/sernEmitter';
export * as Sern from './sern';
export type {
Module,
CommandModule,
EventModule,
BothCommand,
ContextMenuMsg,
ContextMenuUser,
SlashCommand,
TextCommand,
ButtonCommand,
StringSelectCommand,
MentionableSelectCommand,
UserSelectCommand,
ChannelSelectCommand,
RoleSelectCommand,
ModalSubmitCommand,
DiscordEventCommand,
SernEventCommand,
ExternalEventCommand,
CommandModuleDefs,
EventModuleDefs,
SernAutocompleteData,
SernOptionsData,
SernSubCommandData,
SernSubCommandGroupData,
SDT,
ScheduledTask
} from './types/core-modules';
export type {
PluginResult,
InitPlugin,
ControlPlugin,
Plugin,
AnyPlugin,
} from './types/core-plugin';
export type { Payload, SernEventsMapping } from './types/utility';
export {
eventModule,
commandModule,
EventExecutable,
CommandExecutable,
controller,
eventModule,
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';
scheduledTask
} from './core/modules';
export * from './core/presences'
export * from './core/interfaces'
export * from './core/plugin';
export { CommandType, PluginType, PayloadType, EventType } from './core/structures/enums';
export { Context } from './core/structures/context';
export { type CoreDependencies, makeDependencies, single, transient, Service, Services } from './core/ioc';
import type { Container } from '@sern/ioc';
/**
* @deprecated This old signature will be incompatible with future versions of sern >= 4.0.0. See {@link makeDependencies}
* @example
* ```ts
* To switch your old code:
await makeDependencies(({ add }) => {
add('@sern/client', new Client())
})
* ```
*/
export interface DependencyConfiguration {
build: (root: Container) => Container;
}

71
src/sern.ts Normal file
View File

@@ -0,0 +1,71 @@
//side effect: global container
import { useContainerRaw } from '@sern/ioc/global';
import callsites from 'callsites';
import * as Files from './core/module-loading';
import { merge } from 'rxjs';
import eventsHandler from './handlers/user-defined-events';
import ready from './handlers/ready';
import messageHandler from './handlers/message';
import interactionHandler from './handlers/interaction';
import { presenceHandler } from './handlers/presence';
import { UnpackedDependencies } from './types/utility';
import type { Presence} from './core/presences';
import { registerTasks } from './handlers/tasks';
interface Wrapper {
commands: string;
defaultPrefix?: string;
events?: string;
tasks?: string;
}
/**
* @since 1.0.0
* @param maybeWrapper 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 = { commands: "./dist/commands" }) {
const startTime = performance.now();
const deps = useContainerRaw().deps<UnpackedDependencies>();
if (maybeWrapper.events !== undefined) {
eventsHandler(deps, maybeWrapper.events)
.then(() => {
deps['@sern/logger']?.info({ message: "Events registered" });
});
} else {
deps['@sern/logger']?.info({ message: "No events registered" });
}
const initCallsite = callsites()[1].getFileName();
const presencePath = Files.shouldHandle(initCallsite!, "presence");
//Ready event: load all modules and when finished, time should be taken and logged
ready(maybeWrapper.commands, deps)
.then(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
deps['@sern/logger']?.info({ message: `sern: registered in ${time} s` });
if(presencePath.exists) {
const setPresence = async (p: Presence.Result) => {
return deps['@sern/client'].user?.setPresence(p);
}
presenceHandler(presencePath.path, setPresence).subscribe();
}
if(maybeWrapper.tasks) {
registerTasks(maybeWrapper.tasks, deps);
}
})
.catch(err => { throw err });
const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix);
const interactions$ = interactionHandler(deps, maybeWrapper.defaultPrefix);
// listening to the message stream and interaction stream
merge(messages$, interactions$).subscribe();
}

409
src/types/core-modules.ts Normal file
View File

@@ -0,0 +1,409 @@
import type {
APIApplicationCommandBasicOption,
APIApplicationCommandOptionBase,
ApplicationCommandOptionType,
BaseApplicationCommandOptionsData,
AutocompleteInteraction,
ButtonInteraction,
ChannelSelectMenuInteraction,
ClientEvents,
MentionableSelectMenuInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
UserSelectMenuInteraction,
ChatInputCommandInteraction,
} from 'discord.js';
import type { CommandType, EventType } from '../core/structures/enums';
import { Context } from '../core/structures/context'
import { ControlPlugin, InitPlugin, Plugin } from './core-plugin';
import { Awaitable, SernEventsMapping, UnpackedDependencies, Dictionary } from './utility';
/**
* SDT (State, Dependencies, Type) interface represents the core data structure
* passed through the plugin pipeline to command modules.
*
* @interface SDT
* @template TState - Type parameter for the state object's structure
* @template TDeps - Type parameter for dependencies interface
*
* @property {Record<string, unknown>} state - Accumulated state data passed between plugins
* @property {TDeps} deps - Instance of application dependencies
* @property {CommandType} type - Command type identifier
* @property {string} [params] - Optional parameters passed to the command
*
* @example
* // Example of a plugin using SDT
* const loggingPlugin = CommandControlPlugin((ctx, sdt: SDT) => {
* console.log(`User ${ctx.user.id} executed command`);
* return controller.next({ 'logging/timestamp': Date.now() });
* });
*
* @example
* // Example of state accumulation through multiple plugins
* const plugin1 = CommandControlPlugin((ctx, sdt: SDT) => {
* return controller.next({ 'plugin1/data': 'value1' });
* });
*
* const plugin2 = CommandControlPlugin((ctx, sdt: SDT) => {
* // Access previous state
* const prevData = sdt.state['plugin1/data'];
* return controller.next({ 'plugin2/data': 'value2' });
* });
*
* @remarks
* - State is immutable and accumulated through the plugin chain
* - Keys in state should be namespaced to avoid collisions
* - Dependencies are injected and available throughout the pipeline
* - Type information helps plugins make type-safe decisions
*
* @see {@link CommandControlPlugin} for plugin implementation
* @see {@link CommandType} for available command types
* @see {@link Dependencies} for dependency injection interface
*/
export type SDT = {
/**
* Accumulated state passed between plugins in the pipeline.
* Each plugin can add to or modify this state using controller.next().
*
* @type {Record<string, unknown>}
* @example
* // Good: Namespaced state key
* { 'myPlugin/userData': { id: '123', name: 'User' } }
*
* // Avoid: Non-namespaced keys that might collide
* { userData: { id: '123' } }
*/
state: Record<string, unknown>;
/**
* Application dependencies available to plugins and command modules.
* Typically includes services, configurations, and utilities.
*
* @type {Dependencies}
*/
deps: Dependencies;
/**
* Identifies the type of command being processed.
* Used by plugins to apply type-specific logic.
*
* @type {CommandType}
*/
type: CommandType;
/**
* Optional parameters passed to the command.
* May contain additional configuration or runtime data.
*
* @type {string}
* @optional
*/
params?: string;
/**
* A copy of the current module that the plugin is running in.
*/
module: { name: string;
description: string;
meta: Dictionary;
locals: Dictionary; }
};
export type Processed<T> = T & { name: string; description: string };
export interface Module {
type: CommandType | EventType;
name?: string;
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
meta: {
id: string;
absPath: string;
}
/**
* Custom data storage object for module-specific information.
* Plugins and module code can use this to store and retrieve metadata,
* configuration, or any other module-specific information.
*
* @type {Dictionary}
* @description A key-value store that allows plugins and module code to persist
* data at the module level. This is especially useful for InitPlugins that need
* to attach metadata or configuration to modules.
*
* @example
* // In a plugin
* module.locals.registrationDate = Date.now();
* module.locals.version = "1.0.0";
* module.locals.permissions = ["ADMIN", "MODERATE"];
*
* @example
* // In module execution
* console.log(`Command registered on: ${new Date(module.locals.registrationDate)}`);
*
* @example
* // Storing localization data
* module.locals.translations = {
* en: "Hello",
* es: "Hola",
* fr: "Bonjour"
* };
*
* @example
* // Storing command metadata
* module.locals.metadata = {
* category: "admin",
* cooldown: 5000,
* requiresPermissions: true
* };
*
* @remarks
* - The locals object is initialized as an empty object ({}) by default
* - Keys should be namespaced to avoid collisions between plugins
* - Values can be of any type
* - Data persists for the lifetime of the module
* - Commonly used by InitPlugins during module initialization
*
* @best-practices
* 1. Namespace your keys to avoid conflicts:
* ```typescript
* module.locals['myPlugin:data'] = value;
* ```
*
* 2. Document the data structure you're storing:
* ```typescript
* interface MyPluginData {
* version: string;
* timestamp: number;
* }
* module.locals['myPlugin:data'] = {
* version: '1.0.0',
* timestamp: Date.now()
* } as MyPluginData;
* ```
*
* 3. Use type-safe accessors when possible:
* ```typescript
* const getPluginData = (module: Module): MyPluginData =>
* module.locals['myPlugin:data'];
* ```
*/
locals: Dictionary;
execute(...args: any[]): Awaitable<any>;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
extends Module {
name?: T;
type: EventType.Sern;
execute(...args: SernEventsMapping[T]): 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 {
type: CommandType.CtxUser;
execute: (ctx: UserContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg;
execute: (ctx: MessageContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ButtonCommand extends Module {
type: CommandType.Button;
execute: (ctx: ButtonInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
execute: (ctx: StringSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
execute: (ctx: ChannelSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
execute: (ctx: RoleSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
execute: (ctx: MentionableSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
execute: (ctx: UserSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
execute: (ctx: ModalSubmitInteraction, tbd: SDT) => Awaitable<unknown>;
}
export interface AutocompleteCommand {
onEvent?: ControlPlugin[];
execute: (ctx: AutocompleteInteraction, tbd: SDT) => 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;
execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable<unknown>;
}
export interface SlashCommand extends Module {
type: CommandType.Slash;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, tbd: SDT) => Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
| SlashCommand
| BothCommand
| ContextMenuUser
| ContextMenuMsg
| ButtonCommand
| StringSelectCommand
| MentionableSelectCommand
| UserSelectCommand
| ChannelSelectCommand
| RoleSelectCommand
| ModalSubmitCommand;
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union
// Explicit Module Definitions for mapping
export interface CommandModuleDefs {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
[CommandType.CtxMsg]: ContextMenuMsg;
[CommandType.CtxUser]: ContextMenuUser;
[CommandType.Button]: ButtonCommand;
[CommandType.StringSelect]: StringSelectCommand;
[CommandType.RoleSelect]: RoleSelectCommand;
[CommandType.ChannelSelect]: ChannelSelectCommand;
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
}
export interface EventModuleDefs<T extends keyof ClientEvents = keyof ClientEvents> {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand<T>;
[EventType.External]: ExternalEventCommand;
}
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
autocomplete: true;
type:
| ApplicationCommandOptionType.String
| ApplicationCommandOptionType.Number
| ApplicationCommandOptionType.Integer;
command: AutocompleteCommand;
}
type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent' | 'meta' | 'locals'>;
};
type EventModulesNoPlugins<K extends keyof ClientEvents = keyof ClientEvents> = {
[T in EventType]: Omit<EventModuleDefs<K>[T], 'plugins' | 'onEvent' | 'meta' | 'locals'> ;
};
export type InputEvent<K extends keyof ClientEvents = keyof ClientEvents> = {
[T in EventType]: EventModulesNoPlugins<K>[T] & {
once?: boolean;
plugins?: InitPlugin[]
};
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & {
plugins?: Plugin[];
};
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
export type SernOptionsData =
| SernSubCommandData
| SernSubCommandGroupData
| APIApplicationCommandBasicOption
| SernAutocompleteData;
export interface SernSubCommandData
extends APIApplicationCommandOptionBase<ApplicationCommandOptionType.Subcommand> {
type: ApplicationCommandOptionType.Subcommand;
options?: SernOptionsData[];
}
export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.SubcommandGroup;
options?: SernSubCommandData[];
}
export interface ScheduledTaskContext {
/**
* the uuid of the current task being run
*/
id: string;
/**
* the last time this task was executed. If this is the first time, it is null.
*/
lastTimeExecution: Date | null;
/**
* The next time this task will be executed.
*/
nextTimeExecution: Date | null;
}
//name subject to change
interface TaskAttrs {
/**
* An object of dependencies configured in `makeDependencies`
*/
deps: UnpackedDependencies
}
export interface ScheduledTask {
name?: string;
trigger: string | Date;
timezone?: string;
execute(tasks: ScheduledTaskContext, sdt: TaskAttrs): Awaitable<void>
}

72
src/types/core-plugin.ts Normal file
View File

@@ -0,0 +1,72 @@
/*
* 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 {
Module,
Processed,
SDT,
} from './core-modules';
import type { Awaitable } from './utility';
import type { CommandType, PluginType } from '../core/structures/enums'
import type { Context } from '../core/structures/context'
import type {
ButtonInteraction,
ChannelSelectMenuInteraction,
ChatInputCommandInteraction,
MentionableSelectMenuInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { Result } from '../core/structures/result';
export type PluginResult = Awaitable<Result<Record<string,unknown>|undefined, string|undefined>>;
export interface InitArgs<T extends Processed<Module> = Processed<Module>> {
module: T;
absPath: string;
deps: Dependencies
}
export interface Plugin<Args extends any[] = any[]> {
type: PluginType;
execute: (...args: Args) => PluginResult;
}
export interface InitPlugin<Args extends any[] = any[]> extends Plugin<Args> {
type: PluginType.Init;
execute: (...args: Args) => PluginResult;
}
export interface ControlPlugin<Args extends any[] = any[]> extends Plugin<Args> {
type: PluginType.Control;
}
export type AnyPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<Module>>]>;
export type CommandArgs<I extends CommandType = CommandType> = CommandArgsMatrix[I]
interface CommandArgsMatrix {
[CommandType.Text]: [Context & { get options(): string[]}, SDT];
[CommandType.Slash]: [Context & { get options(): ChatInputCommandInteraction['options']}, SDT];
[CommandType.Both]: [Context, SDT];
[CommandType.CtxMsg]: [MessageContextMenuCommandInteraction, SDT];
[CommandType.CtxUser]: [UserContextMenuCommandInteraction, SDT];
[CommandType.Button]: [ButtonInteraction, SDT];
[CommandType.StringSelect]: [StringSelectMenuInteraction, SDT];
[CommandType.RoleSelect]: [RoleSelectMenuInteraction, SDT];
[CommandType.ChannelSelect]: [ChannelSelectMenuInteraction, SDT];
[CommandType.MentionableSelect]: [MentionableSelectMenuInteraction, SDT];
[CommandType.UserSelect]: [UserSelectMenuInteraction, SDT];
[CommandType.Modal]: [ModalSubmitInteraction, SDT];
}

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

@@ -0,0 +1,27 @@
// 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 '../core/ioc';
declare global {
/**
* discord.js client.
* '@sern/client': Client
* sern emitter listens to events that happen throughout
* the handler. some include module.register, module.activate.
* '@sern/emitter': Contracts.Emitter;
* An error handler which is the final step before
* the sern process actually crashes.
'@sern/errors': Contracts.ErrorHandling;
* Optional logger. Performs ... logging
* '@sern/logger'?: Contracts.Logging;
* Readonly module store. sern stores these
* by module.meta.id -> Module
* '@sern/modules': Map<string, Module>;
*/
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, {}>;
}

View File

@@ -1,228 +0,0 @@
import type {
ApplicationCommandAttachmentOption,
ApplicationCommandChannelOptionData,
ApplicationCommandChoicesData,
ApplicationCommandNonOptionsData,
ApplicationCommandNumericOptionData,
ApplicationCommandOptionData,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
AutocompleteInteraction,
Awaitable,
BaseApplicationCommandOptionsData,
ButtonInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
} 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';
export interface Module {
type: CommandType | EventType;
name?: string;
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
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 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 ContextMenuUser extends Module {
type: CommandType.CtxUser;
execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg;
execute: (ctx: MessageContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ButtonCommand extends Module {
type: CommandType.Button;
execute: (ctx: ButtonInteraction) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
execute: (ctx: StringSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
execute: (ctx: ChannelSelectMenuInteraction) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
execute: (ctx: RoleSelectMenuInteraction) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
execute: (ctx: MentionableSelectMenuInteraction) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
execute: (ctx: UserSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
execute: (ctx: ModalSubmitInteraction) => Awaitable<unknown>;
}
export interface AutocompleteCommand
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description'> {
onEvent: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
extends Module {
name?: T;
type: EventType.Sern;
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
extends Module {
name?: T;
type: EventType.Discord;
execute(...args: ClientEvents[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: string;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
| SlashCommand
| BothCommand
| ContextMenuUser
| ContextMenuMsg
| ButtonCommand
| StringSelectCommand
| MentionableSelectCommand
| UserSelectCommand
| ChannelSelectCommand
| RoleSelectCommand
| 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 = {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
[CommandType.CtxMsg]: ContextMenuMsg;
[CommandType.CtxUser]: ContextMenuUser;
[CommandType.Button]: ButtonCommand;
[CommandType.StringSelect]: StringSelectCommand;
[CommandType.RoleSelect]: RoleSelectCommand;
[CommandType.ChannelSelect]: ChannelSelectCommand;
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
export type EventModuleDefs = {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
};
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
autocomplete: true;
type:
| ApplicationCommandOptionType.String
| ApplicationCommandOptionType.Number
| ApplicationCommandOptionType.Integer;
command: AutocompleteCommand;
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type InputEvent = {
[T in EventType]: EventModulesNoPlugins[T] & { plugins?: AnyEventPlugin[] };
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: AnyCommandPlugin[] };
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
export type BaseOptions =
| ApplicationCommandChoicesData
| ApplicationCommandNonOptionsData
| ApplicationCommandChannelOptionData
| ApplicationCommandNumericOptionData
| ApplicationCommandAttachmentOption
| SernAutocompleteData;
export interface SernSubCommandData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.Subcommand;
required?: never;
options?: BaseOptions[];
}
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,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: '>;

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

@@ -0,0 +1,28 @@
import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { Module } from './core-modules';
import type { Result } from '../core/structures/result';
export type Awaitable<T> = PromiseLike<T> | T;
export type Dictionary = Record<string, unknown>
export type VoidResult = Result<void, void>;
export type AnyFunction = (...args: any[]) => unknown;
export interface SernEventsMapping {
'module.register': [Payload];
'module.activate': [Payload];
error: [{ type: 'failure'; module?: Module; reason: string | Error }];
warning: [Payload];
modulesLoaded: [never?];
}
export type Payload =
| { type: 'success'; module: Module }
| { type: 'failure'; module?: Module; reason: string | Error }
| { type: 'warning'; module: undefined; reason: string };
export type UnpackFunction<T> = T extends (...args: any) => infer U ? U : T
export type UnpackedDependencies = {
[K in keyof Dependencies]: UnpackFunction<Dependencies[K]>
}
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,11 @@
import { assertType, describe, it } from 'vitest';
import * as __Services from '../../src/core/structures/default-services';
import * as Contracts from '../../src/core/interfaces';
describe('default contracts', () => {
it('should satisfy contracts', () => {
assertType<Contracts.Logging>(new __Services.DefaultLogging());
assertType<Contracts.ErrorHandling>(new __Services.DefaultErrorHandling());
});
});

View File

@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest';
import {
CommandControlPlugin,
CommandInitPlugin,
EventInitPlugin,
} from '../../src';
import { PluginType, controller } from '../../src';
describe('create-plugins', () => {
it('should make proper control plugins', () => {
const pl2 = CommandControlPlugin(() => controller.next());
expect(pl2).to.have.all.keys(['type', 'execute']);
expect(pl2.type).toBe(PluginType.Control);
expect(pl2.execute).an('function');
});
it('should make proper init plugins', () => {
const pl = EventInitPlugin(() => controller.next());
expect(pl).to.have.all.keys(['type', 'execute']);
expect(pl.type).toBe(PluginType.Init);
expect(pl.execute).an('function');
const pl2 = CommandInitPlugin(() => controller.next());
expect(pl2).to.have.all.keys(['type', 'execute']);
expect(pl2.type).toBe(PluginType.Init);
expect(pl2.execute).an('function');
});
});

382
test/core/functions.test.ts Normal file
View File

@@ -0,0 +1,382 @@
//@ts-nocheck
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PluginType, SernOptionsData, controller } from '../../src/index';
import { partitionPlugins, treeSearch } from '../../src/core/functions';
import { faker } from '@faker-js/faker';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
vi.mock('discord.js', async (importOriginal) => {
const mod = await importOriginal()
const ModalSubmitInteraction = class {
customId;
type = 5;
isModalSubmit = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const ButtonInteraction = class {
customId;
type = 3;
componentType = 2;
isButton = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const AutocompleteInteraction = class {
type = 4;
option: string;
constructor(s: string) {
this.option = s;
}
options = {
getFocused: vi.fn(),
getSubcommand: vi.fn(),
};
};
return {
Collection: mod.Collection,
ComponentType: mod.ComponentType,
InteractionType: mod.InteractionType,
ApplicationCommandOptionType: mod.ApplicationCommandOptionType,
ApplicationCommandType: mod.ApplicationCommandType,
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction,
};
});
describe('functions', () => {
afterEach(() => {
vi.clearAllMocks();
});
function createRandomPlugins(len: number) {
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
return Array.from({ length: len }, () => ({
type: random(),
execute: () => (random() === 1 ? controller.next() : controller.stop()),
}));
}
function createRandomChoice() {
return {
type: faker.number.int({ min: 1, max: 11 }),
name: faker.word.noun(),
description: faker.word.adjective(),
};
}
it('should partition plugins correctly', () => {
const plugins = createRandomPlugins(100);
const [onEvent, init] = partitionPlugins(plugins);
for (const el of onEvent) expect(el.type).to.equal(PluginType.Control);
for (const el of init) expect(el.type).to.equal(PluginType.Init);
});
it('should tree search options tree depth 1', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('autocomplete');
const options: SernOptionsData[] = [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'autocomplete',
description: 'here',
autocomplete: true,
command: { onEvent: [], execute: vi.fn() },
},
];
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'autocomplete',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('autocomplete');
expect(result.command).to.be.not.undefined;
}),
it('should tree search depth 2', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('should tree search depth n > 2', () => {
//@ts-expect-error mocking
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
createRandomChoice(),
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result == undefined).to.be.false;
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('should correctly resolve suboption of the same name given two subcommands ', () => {
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
});
it('two subcommands with an option of the same name', () => {
let autocmpInteraction = new AutocompleteInteraction('nested');
const subcommandName = faker.string.alpha();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: 'nested',
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: () => {},
},
},
],
},
],
},
];
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
autocmpInteraction.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result = treeSearch(autocmpInteraction, options);
expect(result).toBeTruthy();
expect(result.name).to.be.eq('nested');
expect(result.command).to.be.not.undefined;
let autocmpInteraction2 = new AutocompleteInteraction('nested');
autocmpInteraction2.options.getSubcommand.mockReturnValue(subcommandName + 'a');
autocmpInteraction2.options.getFocused.mockReturnValue({
name: 'nested',
value: faker.string.alpha(),
focused: true,
});
const result2 = treeSearch(autocmpInteraction2, options);
expect(result2).toBeTruthy();
expect(result2?.name).toEqual('nested');
});
it('simulates autocomplete typing and resolution', () => {
const subcommandName = faker.string.alpha();
const optionName = faker.word.noun();
const options: SernOptionsData[] = [
{
type: ApplicationCommandOptionType.SubcommandGroup,
name: faker.string.alpha(),
description: faker.string.alpha(),
options: [
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName,
description: faker.string.alpha(),
options: [
createRandomChoice(),
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: optionName,
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: vi.fn(),
},
},
],
},
{
type: ApplicationCommandOptionType.Subcommand,
name: subcommandName + 'a',
description: faker.string.alpha(),
options: [
createRandomChoice(),
{
type: ApplicationCommandOptionType.String,
name: optionName,
description: faker.string.alpha(),
autocomplete: true,
command: {
onEvent: [],
execute: vi.fn(),
},
},
],
},
],
},
];
let accumulator = '';
let result: unknown;
for (const char of optionName) {
accumulator += char;
const autocomplete = new AutocompleteInteraction(accumulator);
autocomplete.options.getSubcommand.mockReturnValue(subcommandName);
autocomplete.options.getFocused.mockReturnValue({
name: accumulator,
value: faker.string.alpha(),
focused: true,
});
result = treeSearch(autocomplete, options);
}
expect(result).toBeTruthy();
});
});

135
test/core/id.test.ts Normal file
View File

@@ -0,0 +1,135 @@
//@ts-nocheck
import { expect, test, vi } from 'vitest'
import { CommandType } from '../../src/core/structures/enums';
import * as Id from '../../src/core/id'
import { ButtonInteraction, ModalSubmitInteraction } from 'discord.js';
vi.mock('discord.js', async (importOriginal) => {
const mod = await importOriginal()
const ModalSubmitInteraction = class {
customId;
type = 5;
isModalSubmit = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const ButtonInteraction = class {
customId;
type = 3;
componentType = 2;
isButton = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const AutocompleteInteraction = class {
type = 4;
option: string;
constructor(s: string) {
this.option = s;
}
options = {
getFocused: vi.fn(),
getSubcommand: vi.fn(),
};
};
return {
Collection: mod.Collection,
ComponentType: mod.ComponentType,
InteractionType: mod.InteractionType,
ApplicationCommandOptionType: mod.ApplicationCommandOptionType,
ApplicationCommandType: mod.ApplicationCommandType,
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction,
};
});
test('id -> Text', () => {
expect(Id.create("ping", CommandType.Text)).toBe("ping_T")
})
test('id -> Both', () => {
expect(Id.create("ping", CommandType.Both)).toBe("ping_B")
})
test('id -> CtxMsg', () => {
expect(Id.create("ping", CommandType.CtxMsg)).toBe("ping_A3")
})
test('id -> CtxUsr', () => {
expect(Id.create("ping", CommandType.CtxUser)).toBe("ping_A2")
})
test('id -> Modal', () => {
expect(Id.create("my-modal", CommandType.Modal)).toBe("my-modal_M");
})
test('id -> Button', () => {
expect(Id.create("my-button", CommandType.Button)).toBe("my-button_C2");
})
test('id -> Slash', () => {
expect(Id.create("myslash", CommandType.Slash)).toBe("myslash_A1");
})
test('id -> StringSelect', () => {
expect(Id.create("mystringselect", CommandType.StringSelect)).toBe("mystringselect_C3");
})
test('id -> UserSelect', () => {
expect(Id.create("myuserselect", CommandType.UserSelect)).toBe("myuserselect_C5");
})
test('id -> RoleSelect', () => {
expect(Id.create("myroleselect", CommandType.RoleSelect)).toBe("myroleselect_C6");
})
test('id -> MentionSelect', () => {
expect(Id.create("mymentionselect", CommandType.MentionableSelect)).toBe("mymentionselect_C7");
})
test('id -> ChannelSelect', () => {
const modal = Id.create("mychannelselect", CommandType.ChannelSelect)
expect(modal).toBe("mychannelselect_C8");
})
test('id reconstruct button', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn"))
expect(idload[0].id).toBe("btn_C2")
})
test('id reconstruct button with params', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn/asdf"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe("asdf")
})
test('id reconstruct modal with params', () => {
const idload = Id.reconstruct(new ModalSubmitInteraction("btn/asdf"))
expect(idload[0].id).toBe("btn_M")
expect(idload[0].params).toBe("asdf")
})
test('id reconstruct modal', () => {
const idload = Id.reconstruct(new ModalSubmitInteraction("btn"))
expect(idload[0].id).toBe("btn_M")
expect(idload[0].params).toBe(undefined)
})
test('id reconstruct button with empty params', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn/"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe("")
})
test('id reconstruct with multiple slashes', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn//"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe("/")
})
test('id reconstruct button', () => {
const idload = Id.reconstruct(new ButtonInteraction("btn"))
expect(idload[0].id).toBe("btn_C2")
expect(idload[0].params).toBe(undefined)
})

View File

@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest'
import path from 'node:path'
import * as Files from '../../src/core/module-loading'
import { Module } from '../../src/types/core-modules'
import { AssertionError } from 'node:assert'
//TODO: mock fs?
describe('module-loading', () => {
it('should get the filename of the commandmodule (linux, esm)', () => {
const fname = "///home/pooba/Projects/sern/halibu/dist/commands/ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toBe("ping")
})
it('should get filename of commandmodule (linux, cjs)', () => {
const fname = "file:///home/pooba/Projects/sern/halibu/dist/commands/ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toBe("ping")
})
it('should get the filename of the commandmodule (windows, cjs)', () => {
//this test case is impossible on linux.
if(process.platform == 'win32') {
const fname = "C:\\pooba\\Projects\\sern\\halibu\\dist\\commands\\ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toEqual("ping");
}
})
it('should get filename of commandmodule (windows, esm)', () => {
//this test case is impossible on linux.
if(process.platform == 'win32') {
const fname = "file:///C:\\pooba\\Projects\\sern\\halibu\\dist\\commands\\ping.js"
const callsiteinfo = Files.parseCallsite(fname)
expect(callsiteinfo.name).toEqual("ping");
}
})
it('should import a commandModule properly', async () => {
const { module } = await Files.importModule<Module>(path.resolve("test", 'mockules', "module.ts"));
expect(module.name).toBe('module')
})
it('should throw when failed commandModule import', async () => {
try {
await Files.importModule(path.resolve('test', 'mockules', 'failed.ts'))
} catch(e) {
expect(e instanceof AssertionError)
}
})
it('should throw when failed commandModule import', async () => {
try {
await Files.importModule(path.resolve('test', 'mockules', 'failed.ts'))
} catch(e) {
expect(e instanceof AssertionError)
}
})
it('reads all modules in mockules', async () => {
const ps = [] as string[]
for await (const fpath of Files.readRecursive(path.resolve('test', 'mockules'))) {
ps.push(fpath)
}
expect(ps.length === 4)
})
})

View File

@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest';
import { Presence } from '../../src';
// Example test suite for the module function
describe('module function', () => {
it('should return a valid configuration', () => {
const config = Presence.module({
inject: ['dependency1', 'dependency2'],
execute: vi.fn(),
});
expect(config).toBeDefined();
expect(config.inject).toEqual(['dependency1', 'dependency2']);
expect(typeof config.execute).toBe('function');
});
});
describe('of function', () => {
it('should return a valid presence configuration without repeat and onRepeat', () => {
const presenceConfig = Presence.of({
status: 'online',
afk: false,
activities: [{ name: 'Test Activity' }],
shardId: [1, 2, 3],
}).once();
expect(presenceConfig).toBeDefined();
//@ts-ignore Maybe fix?
expect(presenceConfig.repeat).toBeUndefined();
//@ts-ignore Maybe fix?
expect(presenceConfig.onRepeat).toBeUndefined();
expect(presenceConfig).toMatchObject({
status: 'online',
afk: false,
activities: [{ name: 'Test Activity' }],
shardId: [1, 2, 3],
});
});
it('should return a valid presence configuration with repeat and onRepeat', () => {
const onRepeatCallback = vi.fn();
const presenceConfig = Presence.of({
status: 'idle',
activities: [{ name: 'Another Test Activity' }],
}).repeated(onRepeatCallback, 5000);
expect(presenceConfig).toBeDefined();
expect(presenceConfig.repeat).toBe(5000);
expect(presenceConfig.onRepeat).toBe(onRepeatCallback);
expect(presenceConfig).toMatchObject({
status: 'idle',
activities: [{ name: 'Another Test Activity' }],
});
});
})

144
test/handlers.test.ts Normal file
View File

@@ -0,0 +1,144 @@
//@ts-nocheck
import { beforeEach, describe, expect, vi, it, test } from 'vitest';
import { callInitPlugins, eventDispatcher } from '../src/handlers/event-utils';
import { Client, ChatInputCommandInteraction } from 'discord.js'
import { faker } from '@faker-js/faker';
import { Module } from '../src/types/core-modules';
import { Processed } from '../src/types/core-modules';
import { EventEmitter } from 'events';
import { EventType } from '../src/core/structures/enums';
import { CommandControlPlugin, CommandInitPlugin, CommandType, controller } from '../src';
vi.mock('discord.js', async (importOriginal) => {
const mod = await importOriginal()
const ModalSubmitInteraction = class {
customId;
type = 5;
isModalSubmit = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const ButtonInteraction = class {
customId;
type = 3;
componentType = 2;
isButton = vi.fn();
constructor(customId) {
this.customId = customId;
}
};
const AutocompleteInteraction = class {
type = 4;
option: string;
constructor(s: string) {
this.option = s;
}
options = {
getFocused: vi.fn(),
getSubcommand: vi.fn(),
};
};
return {
Client : vi.fn(),
Collection: mod.Collection,
ComponentType: mod.ComponentType,
InteractionType: mod.InteractionType,
ApplicationCommandOptionType: mod.ApplicationCommandOptionType,
ApplicationCommandType: mod.ApplicationCommandType,
ModalSubmitInteraction,
ButtonInteraction,
AutocompleteInteraction,
ChatInputCommandInteraction: vi.fn()
};
});
function createRandomPlugin (s: 'go', mut?: Partial<Module>) {
return CommandInitPlugin(({ module }) => {
if(mut) {
Object.entries(mut).forEach(([k, v]) => {
module[k] = v
})
}
return s == 'go'
? controller.next()
: controller.stop()
})
}
function createRandomModule(plugins: any[]): Processed<Module> {
return {
type: EventType.Discord,
meta: { id:"", absPath: "" },
description: faker.string.alpha(),
plugins,
name: "cheese",
onEvent: [],
execute: vi.fn(),
};
}
function mockDeps() {
return {
'@sern/client': new Client(),
'@sern/emitter': new EventEmitter()
}
}
describe('eventDispatcher standard', () => {
let m: Processed<Module>;
let ee: EventEmitter;
beforeEach(() => {
ee = new EventEmitter();
m = createRandomModule();
});
it('should throw', () => {
expect(() => eventDispatcher(mockDeps(), m, 'not event emitter')).toThrowError();
});
it("Shouldn't throw", () => {
expect(() => eventDispatcher(mockDeps(), m, ee)).not.toThrowError();
});
});
test ('call init plugins', async () => {
const deps = mockDeps()
const plugins = createRandomPlugin('go', { name: "abc" })
const mod = createRandomModule([plugins])
const s = await callInitPlugins(mod, deps, false)
expect("abc").equal(s.name)
})
test('init plugins replace array', async () => {
const deps = mockDeps()
const plugins = createRandomPlugin('go', { opts: [] })
const plugins2 = createRandomPlugin('go', { opts: ['a'] })
const mod = createRandomModule([plugins, plugins2])
const s = await callInitPlugins(mod, deps, false)
expect(['a']).deep.equal(s.opts)
})
test('form sdt', async () => {
const expectedObject = {
"plugin/abc": faker.person.jobArea(),
"plugin2/abc": faker.git.branch(),
"plugin3/cheese": faker.person.jobArea()
}
const plugin = CommandControlPlugin<CommandType.Slash>((ctx,sdt) => {
return controller.next({ "plugin/abc": expectedObject['plugin/abc'] });
});
const plugin2 = CommandControlPlugin<CommandType.Slash>((ctx,sdt) => {
return controller.next({ "plugin2/abc": expectedObject['plugin2/abc'] });
});
const plugin3 = CommandControlPlugin<CommandType.Slash>((ctx,sdt) => {
return controller.next({ "plugin3/cheese": expectedObject['plugin3/cheese'] });
});
})

0
test/mockules/!ignd.ts Normal file
View File

View File

0
test/mockules/failed.ts Normal file
View File

6
test/mockules/module.ts Normal file
View File

@@ -0,0 +1,6 @@
import { CommandType, commandModule } from '../../src/'
export default commandModule({
type: CommandType.Both,
description: "",
execute: (Ctx, args) => {}
})

6
test/mockules/ug/pass.ts Normal file
View File

@@ -0,0 +1,6 @@
import { CommandType, commandModule } from '../../../src/'
export default commandModule({
type: CommandType.Both,
description: "",
execute: (Ctx, args) => {}
})

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["node_modules", "dist"],
"include": ["src"]
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist/cjs",
"target": "esnext"
}
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"target": "esnext"
}
}

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"strictNullChecks": true,
"moduleResolution": "node16",
"skipLibCheck": true,
"declaration": true,
"preserveSymlinks": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"outDir": "dist",
"module": "node16",
"target": "esnext",
"sourceMap": true
},
"exclude": ["node_modules", "dist"],
"include": ["./src", "./src/**/*.d.ts"]
}

View File

@@ -1,39 +0,0 @@
import { defineConfig } from 'tsup';
const shared = {
entry: ['src/index.ts'],
external: ['discord.js'],
platform: 'node',
clean: true,
sourcemap: false,
minify: true
};
export default defineConfig([
{
format: 'esm',
target: 'node16',
tsconfig: './tsconfig-esm.json',
outDir: './dist/esm',
external: ['discord.js'],
treeshake: true,
outExtension() {
return {
js: '.mjs',
};
},
...shared,
},
{
format: 'cjs',
splitting: false,
external: ['discord.js'],
target: 'node16',
tsconfig: './tsconfig-cjs.json',
outDir: './dist/cjs',
outExtension() {
return {
js: '.cjs',
};
},
...shared,
},
]);

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