Compare commits

...

22 Commits

Author SHA1 Message Date
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
43 changed files with 733 additions and 1326 deletions

View File

@@ -1,450 +0,0 @@
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
forbidden: [
/* rules from the 'recommended' preset: */
{
name: 'no-circular',
severity: 'warn',
comment:
'This dependency is part of a circular relationship. You might want to revise ' +
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
from: {},
to: {
circular: true,
},
},
{
name: 'no-orphans',
comment:
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
'add an exception for it in your dependency-cruiser configuration. By default ' +
'this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration ' +
'files (.d.ts), tsconfig.json and some of the babel and webpack configs.',
severity: 'warn',
from: {
orphan: true,
pathNot: [
'(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files
'\\.d\\.ts$', // TypeScript declaration files
'(^|/)tsconfig\\.json$', // TypeScript config
'(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$', // other configs
],
},
to: {},
},
{
name: 'no-deprecated-core',
comment:
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
"bound to exist - node doesn't deprecate lightly.",
severity: 'warn',
from: {},
to: {
dependencyTypes: ['core'],
path: [
'^(v8/tools/codemap)$',
'^(v8/tools/consarray)$',
'^(v8/tools/csvparser)$',
'^(v8/tools/logreader)$',
'^(v8/tools/profile_view)$',
'^(v8/tools/profile)$',
'^(v8/tools/SourceMap)$',
'^(v8/tools/splaytree)$',
'^(v8/tools/tickprocessor-driver)$',
'^(v8/tools/tickprocessor)$',
'^(node-inspect/lib/_inspect)$',
'^(node-inspect/lib/internal/inspect_client)$',
'^(node-inspect/lib/internal/inspect_repl)$',
'^(async_hooks)$',
'^(punycode)$',
'^(domain)$',
'^(constants)$',
'^(sys)$',
'^(_linklist)$',
'^(_stream_wrap)$',
],
},
},
{
name: 'not-to-deprecated',
comment:
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
'version of that module, or find an alternative. Deprecated modules are a security risk.',
severity: 'warn',
from: {},
to: {
dependencyTypes: ['deprecated'],
},
},
{
name: 'no-non-package-json',
severity: 'error',
comment:
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
'available on live with an non-guaranteed version. Fix it by adding the package to the dependencies ' +
'in your package.json.',
from: {},
to: {
dependencyTypes: ['npm-no-pkg', 'npm-unknown'],
},
},
{
name: 'not-to-unresolvable',
comment:
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
'module: add it to your package.json. In all other cases you likely already know what to do.',
severity: 'error',
from: {},
to: {
couldNotResolve: true,
},
},
{
name: 'no-duplicate-dep-types',
comment:
"Likely this module depends on an external ('npm') package that occurs more than once " +
'in your package.json i.e. bot as a devDependencies and in dependencies. This will cause ' +
'maintenance problems later on.',
severity: 'warn',
from: {},
to: {
moreThanOneDependencyType: true,
// as it's pretty common to have a type import be a type only import
// _and_ (e.g.) a devDependency - don't consider type-only dependency
// types for this rule
dependencyTypesNot: ['type-only'],
},
},
/* rules you might want to tweak for your specific situation: */
{
name: 'not-to-test',
comment:
"This module depends on code within a folder that should only contain tests. As tests don't " +
"implement functionality this is odd. Either you're writing a test outside the test folder " +
"or there's something in the test folder that isn't a test.",
severity: 'error',
from: {
pathNot: '^(test)',
},
to: {
path: '^(test)',
},
},
{
name: 'not-to-spec',
comment:
'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' +
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
severity: 'error',
from: {},
to: {
path: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$',
},
},
{
name: 'not-to-dev-dep',
severity: 'error',
comment:
"This module depends on an npm package from the 'devDependencies' section of your " +
'package.json. It looks like something that ships to production, though. To prevent problems ' +
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
'section of your package.json. If this module is development only - add it to the ' +
'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration',
from: {
path: '^(src)',
pathNot: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$',
},
to: {
dependencyTypes: ['npm-dev'],
},
},
{
name: 'optional-deps-used',
severity: 'info',
comment:
'This module depends on an npm package that is declared as an optional dependency ' +
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
"If you're using an optional dependency here by design - add an exception to your" +
'dependency-cruiser configuration.',
from: {},
to: {
dependencyTypes: ['npm-optional'],
},
},
{
name: 'peer-deps-used',
comment:
'This module depends on an npm package that is declared as a peer dependency ' +
'in your package.json. This makes sense if your package is e.g. a plugin, but in ' +
'other cases - maybe not so much. If the use of a peer dependency is intentional ' +
'add an exception to your dependency-cruiser configuration.',
severity: 'warn',
from: {},
to: {
dependencyTypes: ['npm-peer'],
},
},
],
options: {
/* conditions specifying which files not to follow further when encountered:
- path: a regular expression to match
- dependencyTypes: see https://github.com/sverweij/dependency-cruiser/blob/main/doc/rules-reference.md#dependencytypes-and-dependencytypesnot
for a complete list
*/
doNotFollow: {
path: 'node_modules',
},
/* conditions specifying which dependencies to exclude
- path: a regular expression to match
- dynamic: a boolean indicating whether to ignore dynamic (true) or static (false) dependencies.
leave out if you want to exclude neither (recommended!)
*/
// exclude : {
// path: '',
// dynamic: true
// },
/* pattern specifying which files to include (regular expression)
dependency-cruiser will skip everything not matching this pattern
*/
// includeOnly : '',
/* dependency-cruiser will include modules matching against the focus
regular expression in its output, as well as their neighbours (direct
dependencies and dependents)
*/
// focus : '',
/* list of module systems to cruise */
// moduleSystems: ['amd', 'cjs', 'es6', 'tsd'],
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/develop/'
to open it on your online repo or `vscode://file/${process.cwd()}/` to
open it in visual studio code),
*/
// prefix: '',
/* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
true: also detect dependencies that only exist before typescript-to-javascript compilation
"specify": for each dependency identify whether it only exists before compilation or also after
*/
tsPreCompilationDeps: true,
/*
list of extensions to scan that aren't javascript or compile-to-javascript.
Empty by default. Only put extensions in here that you want to take into
account that are _not_ parsable.
*/
// extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
/* if true combines the package.jsons found from the module up to the base
folder the cruise is initiated from. Useful for how (some) mono-repos
manage dependencies & dependency definitions.
*/
// combinedDependencies: false,
/* if true leave symlinks untouched, otherwise use the realpath */
// preserveSymlinks: false,
/* TypeScript project file ('tsconfig.json') to use for
(1) compilation and
(2) resolution (e.g. with the paths property)
The (optional) fileName attribute specifies which file to take (relative to
dependency-cruiser's current working directory). When not provided
defaults to './tsconfig.json'.
*/
tsConfig: {
fileName: 'tsconfig.json',
},
/* Webpack configuration to use to get resolve options from.
The (optional) fileName attribute specifies which file to take (relative
to dependency-cruiser's current working directory. When not provided defaults
to './webpack.conf.js'.
The (optional) `env` and `arguments` attributes contain the parameters to be passed if
your webpack config is a function and takes them (see webpack documentation
for details)
*/
// webpackConfig: {
// fileName: './webpack.config.js',
// env: {},
// arguments: {},
// },
/* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
for compilation (and whatever other naughty things babel plugins do to
source code). This feature is well tested and usable, but might change
behavior a bit over time (e.g. more precise results for used module
systems) without dependency-cruiser getting a major version bump.
*/
// babelConfig: {
// fileName: './.babelrc'
// },
/* List of strings you have in use in addition to cjs/ es6 requires
& imports to declare module dependencies. Use this e.g. if you've
re-declared require, use a require-wrapper or use window.require as
a hack.
*/
// exoticRequireStrings: [],
/* options to pass on to enhanced-resolve, the package dependency-cruiser
uses to resolve module references to disk. You can set most of these
options in a webpack.conf.js - this section is here for those
projects that don't have a separate webpack config file.
Note: settings in webpack.conf.js override the ones specified here.
*/
enhancedResolveOptions: {
/* List of strings to consider as 'exports' fields in package.json. Use
['exports'] when you use packages that use such a field and your environment
supports it (e.g. node ^12.19 || >=14.7 or recent versions of webpack).
If you have an `exportsFields` attribute in your webpack config, that one
will have precedence over the one specified here.
*/
exportsFields: ['exports'],
/* List of conditions to check for in the exports field. e.g. use ['imports']
if you're only interested in exposed es6 modules, ['require'] for commonjs,
or all conditions at once `(['import', 'require', 'node', 'default']`)
if anything goes for you. Only works when the 'exportsFields' array is
non-empty.
If you have a 'conditionNames' attribute in your webpack config, that one will
have precedence over the one specified here.
*/
conditionNames: ['import', 'require', 'node', 'default'],
/*
The extensions, by default are the same as the ones dependency-cruiser
can access (run `npx depcruise --info` to see which ones that are in
_your_ environment. If that list is larger than what you need (e.g.
it contains .js, .jsx, .ts, .tsx, .cts, .mts - but you don't use
TypeScript you can pass just the extensions you actually use (e.g.
[".js", ".jsx"]). This can speed up the most expensive step in
dependency cruising (module resolution) quite a bit.
*/
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
/*
If your TypeScript project makes use of types specified in 'types'
fields in package.jsons of external dependencies, specify "types"
in addition to "main" in here, so enhanced-resolve (the resolver
dependency-cruiser uses) knows to also look there. You can also do
this if you're not sure, but still use TypeScript. In a future version
of dependency-cruiser this will likely become the default.
*/
mainFields: ['main', 'types'],
},
reporterOptions: {
dot: {
/* pattern of modules that can be consolidated in the detailed
graphical dependency graph. The default pattern in this configuration
collapses everything in node_modules to one folder deep so you see
the external modules, but not the innards your app depends upon.
*/
collapsePattern: 'node_modules/(@[^/]+/[^/]+|[^/]+)',
/* Options to tweak the appearance of your graph.See
https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
for details and some examples. If you don't specify a theme
don't worry - dependency-cruiser will fall back to the default one.
*/
// theme: {
// graph: {
// /* use splines: "ortho" for straight lines. Be aware though
// graphviz might take a long time calculating ortho(gonal)
// routings.
// */
// splines: "true"
// },
// modules: [
// {
// criteria: { matchesFocus: true },
// attributes: {
// fillcolor: "lime",
// penwidth: 2,
// },
// },
// {
// criteria: { matchesFocus: false },
// attributes: {
// fillcolor: "lightgrey",
// },
// },
// {
// criteria: { matchesReaches: true },
// attributes: {
// fillcolor: "lime",
// penwidth: 2,
// },
// },
// {
// criteria: { matchesReaches: false },
// attributes: {
// fillcolor: "lightgrey",
// },
// },
// {
// criteria: { source: "^src/model" },
// attributes: { fillcolor: "#ccccff" }
// },
// {
// criteria: { source: "^src/view" },
// attributes: { fillcolor: "#ccffcc" }
// },
// ],
// dependencies: [
// {
// criteria: { "rules[0].severity": "error" },
// attributes: { fontcolor: "red", color: "red" }
// },
// {
// criteria: { "rules[0].severity": "warn" },
// attributes: { fontcolor: "orange", color: "orange" }
// },
// {
// criteria: { "rules[0].severity": "info" },
// attributes: { fontcolor: "blue", color: "blue" }
// },
// {
// criteria: { resolved: "^src/model" },
// attributes: { color: "#0000ff77" }
// },
// {
// criteria: { resolved: "^src/view" },
// attributes: { color: "#00770077" }
// }
// ]
// }
},
archi: {
/* pattern of modules that can be consolidated in the high level
graphical dependency graph. If you use the high level graphical
dependency graph reporter (`archi`) you probably want to tweak
this collapsePattern to your situation.
*/
collapsePattern:
'^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/(@[^/]+/[^/]+|[^/]+)',
/* Options to tweak the appearance of your graph.See
https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
for details and some examples. If you don't specify a theme
for 'archi' dependency-cruiser will use the one specified in the
dot section (see above), if any, and otherwise use the default one.
*/
// theme: {
// },
},
text: {
highlightFocused: true,
},
},
},
};
// generated: dependency-cruiser@13.0.5 on 2023-07-08T03:48:00.632Z

View File

@@ -23,7 +23,7 @@ jobs:
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

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
- uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: 17
- run: yarn --immutable

View File

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

View File

@@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -1,5 +1,46 @@
# Changelog
## [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)

View File

@@ -19,28 +19,16 @@
- Lightweight. Does a lot while being small.
- Latest features. Support for discord.js v14 and all of its interactions.
- Start quickly. Plug and play or customize to your liking.
- Switch and customize how errors are handled, logging, and more.
- works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
- Use it with TypeScript or JavaScript. CommonJS and ESM supported.
- Active and growing community, always here to help. [Join us](https://sern.dev/discord)
- Unleash its full potential with a powerful CLI and awesome plugins.
## 📜 Installation
```sh
npm install @sern/handler
```
```sh
yarn add @sern/handler
```
```sh
pnpm add @sern/handler
```
[Start here!!](https://sern.dev/docs/guide/walkthrough/new-project)
## 👶 Basic Usage
<details open><summary>ping.ts</summary>
<details><summary>ping.ts</summary>
```ts
export default commandModule({
@@ -54,7 +42,7 @@ export default commandModule({
});
```
</details>
<details open><summary>modal.ts</summary>
<details><summary>modal.ts</summary>
```ts
export default commandModule({
@@ -74,30 +62,7 @@ export default commandModule({
})
```
</details>
<details open><summary>index.ts</summary>
```ts
import { Client, GatewayIntentBits } from 'discord.js';
import { Sern, single } from '@sern/handler';
//client has been declared previously
//Version 3
await makeDependencies({
build: root => root
.add({ '@sern/client': single(() => client) })
});
//View docs for all options
Sern.init({
defaultPrefix: '!', // removing defaultPrefix will shut down text commands
commands: 'src/commands',
// events: 'src/events' (optional),
});
client.login("YOUR_BOT_TOKEN_HERE");
```
</details>
## 🤖 Bots Using sern
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).

1
fortnite Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,7 +1,7 @@
{
"name": "@sern/handler",
"packageManager": "yarn@3.5.0",
"version": "3.1.0",
"version": "3.3.1",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -15,7 +15,6 @@
},
"scripts": {
"watch": "tsup --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"build:dev": "tsup --metafile",
@@ -38,6 +37,7 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"callsites": "^3.1.0",
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-results-es": "^4.0.0"
@@ -47,8 +47,7 @@
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"dependency-cruiser": "^13.0.5",
"discord.js": "14.11.0",
"discord.js": "^14.11.0",
"esbuild": "^0.17.0",
"eslint": "8.39.0",
"prettier": "2.8.8",

View File

@@ -1,5 +1,9 @@
{
"extends": ["config:base", "helpers:pinGitHubActionDigests", "group:allNonMajor"],
"extends": [
"config:base",
"helpers:pinGitHubActionDigests",
"group:allNonMajor"
],
"major": {
"dependencyDashboardApproval": true,
"reviewers": ["EvolutionX-10", "jacoobes", "Murtatrxx"]

View File

@@ -1,21 +1,18 @@
import type { CommandModule,Processed, EventModule } from "../../types/core-modules";
/**
* @since 2.0.0
*/
export interface ErrorHandling {
/**
* Number of times the process should throw an error until crashing and exiting
*/
keepAlive: number;
/**
* @deprecated
* Version 4 will remove this method
*/
crash(err: Error): never;
/**
* A function that is called on every crash. Updates keepAlive.
* If keepAlive is 0, the process crashes.
* A function that is called on every throw.
* @param error
*/
updateAlive(error: Error): void;
}

View File

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

View File

@@ -1,6 +1,7 @@
import { CommandType, EventType, PluginType } from './structures';
import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/core-plugin';
import type { ClientEvents } from 'discord.js';
import { err, ok } from './functions';
export function makePlugin<V extends unknown[]>(
type: PluginType,
@@ -60,3 +61,12 @@ export function DiscordEventControlPlugin<T extends keyof ClientEvents>(
) {
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: ok,
stop: err,
};

View File

@@ -36,7 +36,7 @@ export function partitionPlugins(
export function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData | undefined {
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
@@ -61,18 +61,18 @@ export function treeSearch(
const choice = iAutocomplete.options.getFocused(true);
assert(
'command' in cur,
'No command property found for autocomplete option',
'No `command` property found for autocomplete option',
);
if (subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return cur;
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return cur;
return { ...cur, parent: undefined };
}
}
}

View File

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

View File

@@ -2,7 +2,10 @@ import * as assert from 'assert';
import { composeRoot, useContainer } from './dependency-injection';
import type { DependencyConfiguration } from '../../types/ioc';
import { CoreContainer } from './container';
import { Result } from 'ts-results-es'
import { DefaultServices } from '../_internal';
import { AnyFunction } from '../../types/utility';
import type { Logging } from '../contracts/logging';
//SIDE EFFECT: GLOBAL DI
let containerSubject: CoreContainer<Partial<Dependencies>>;
@@ -20,17 +23,89 @@ export function useContainerRaw() {
return containerSubject;
}
/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export async function makeDependencies<const T extends Dependencies>(
conf: DependencyConfiguration,
) {
//Until there are more optional dependencies, just check if the logger exists
//SIDE EFFECT
export function disposeAll(logger: Logging|undefined) {
containerSubject
?.disposeAll()
.then(() => logger?.info({ message: 'Cleaning container and crashing' }));
}
const dependencyBuilder = (container: any, excluded: string[]) => {
type Insertable =
| ((container: CoreContainer<Dependencies>) => unknown )
| Record<PropertyKey, unknown>
return {
/**
* Insert a dependency into your container.
* Supply the correct key and dependency
*/
add(key: keyof Dependencies, v: Insertable) {
Result
.wrap(() => container.add({ [key]: v}))
.expect("Failed to add " + key);
},
/**
* Exclude any dependencies from being added.
* Warning: this could lead to bad errors if not used correctly
*/
exclude(...keys: (keyof Dependencies)[]) {
keys.forEach(key => excluded.push(key));
},
/**
* @param key the key of the dependency
* @param v The dependency to swap out.
* Swap out a preexisting dependency.
*/
swap(key: keyof Dependencies, v: Insertable) {
Result
.wrap(() => container.upsert({ [key]: v }))
.expect("Failed to update " + key);
},
/**
* @param key the key of the dependency
* @param cleanup Provide cleanup for the dependency at key. First parameter is the dependency itself
* @example
* ```ts
* addDisposer('dbConnection', (dbConnection) => dbConnection.end())
* ```
* Swap out a preexisting dependency.
*/
addDisposer(key: keyof Dependencies, cleanup: AnyFunction) {
Result
.wrap(() => container.addDisposer({ [key] : cleanup }))
.expect("Failed to addDisposer for" + key);
}
};
};
type CallbackBuilder = (c: ReturnType<typeof dependencyBuilder>) => any
type ValidDependencyConfig =
| CallbackBuilder
| DependencyConfiguration;
export const insertLogger = (containerSubject: CoreContainer<any>) => {
containerSubject
.upsert({'@sern/logger': () => new DefaultServices.DefaultLogging});
}
export async function makeDependencies<const T extends Dependencies>
(conf: ValidDependencyConfig) {
containerSubject = new CoreContainer();
await composeRoot(containerSubject, conf);
if(typeof conf === 'function') {
const excluded: string[] = [];
conf(dependencyBuilder(containerSubject, excluded));
if(!excluded.includes('@sern/logger')
&& !containerSubject.getTokens()['@sern/logger']) {
insertLogger(containerSubject);
}
containerSubject.ready();
} else {
composeRoot(containerSubject, conf);
}
return useContainer<T>();
}

View File

@@ -21,32 +21,25 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
.subscribe({ complete: unsubscribe });
(this as Container<{}, {}>)
.add({
'@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter(),
'@sern/store': () => new ModuleStore(),
})
.add({ '@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter,
'@sern/store': () => new ModuleStore })
.add(ctx => {
return {
'@sern/modules': () =>
new DefaultServices.DefaultModuleManager(ctx['@sern/store']),
};
return { '@sern/modules': () =>
new DefaultServices.DefaultModuleManager(ctx['@sern/store']) };
});
}
isReady() {
return this.ready$.closed;
}
override async disposeAll() {
const otherDisposables = Object
.entries(this._context)
.flatMap(([key, value]) =>
'dispose' in value
? [key]
: []);
'dispose' in value ? [key] : []);
for(const key of otherDisposables) {
this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never);

View File

@@ -1,6 +1,5 @@
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
import { DefaultServices } from '../_internal';
import { useContainerRaw } from './base';
import { insertLogger, useContainerRaw } from './base';
import { CoreContainer } from './container';
/**
@@ -53,16 +52,14 @@ export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]
* Finally, update the containerSubject with the new container state
* @param conf
*/
export async function composeRoot(
export function composeRoot(
container: CoreContainer<Partial<Dependencies>>,
conf: DependencyConfiguration,
) {
//container should have no client or logger yet.
const hasLogger = conf.exclude?.has('@sern/logger');
if (!hasLogger) {
container.upsert({
'@sern/logger': () => new DefaultServices.DefaultLogging(),
});
insertLogger(container);
}
//Build the container based on the callback provided by the user
conf.build(container as CoreContainer<Omit<CoreDependencies, '@sern/client'>>);

View File

@@ -6,9 +6,19 @@ import assert from 'assert';
import { createRequire } from 'node:module';
import type { ImportPayload, Wrapper } from '../types/core';
import type { Module } from '../types/core-modules';
import { existsSync } from 'fs';
import { fileURLToPath } from 'node:url';
export const shouldHandle = (path: string, fpath: string) => {
const newPath = new URL(fpath+extname(path), path).href;
return {
exists: existsSync(fileURLToPath(newPath)),
path: newPath
}
}
export type ModuleResult<T> = Promise<ImportPayload<T>>;
/**
* Import any module based on the absolute path.
* This can accept four types of exported modules
@@ -23,19 +33,21 @@ export type ModuleResult<T> = Promise<ImportPayload<T>>;
* export default commandModule({})
*/
export async function importModule<T>(absPath: string) {
let module = await import(absPath).then(esm => esm.default);
let fileModule = await import(absPath);
assert(module, `Found no export for module at ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in module) {
module = module.default;
let commandModule = fileModule.default;
assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in commandModule ) {
commandModule = commandModule.default;
}
return Result
.wrap(() => module.getInstance())
.unwrapOr(module) as T;
.wrap(() => ({ module: commandModule.getInstance() }))
.unwrapOr({ module: commandModule }) as T;
}
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
let module = await importModule<T>(absPath);
let { module } = await importModule<{ module: T }>(absPath);
assert(module, `Found an undefined module: ${absPath}`);
return { module, absPath };
}
@@ -51,7 +63,8 @@ export const fmtFileName = (fileName: string) => parse(fileName).name;
export function buildModuleStream<T extends Module>(
input: ObservableInput<string>,
): Observable<ImportPayload<T>> {
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
return from(input)
.pipe(mergeMap(defaultModuleLoader<T>));
}
export const getFullPathTree = (dir: string) => readPaths(resolve(dir));
@@ -63,6 +76,7 @@ const isSkippable = (filename: string) => {
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
};
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
@@ -71,6 +85,7 @@ async function deriveFileInfo(dir: string, file: string) {
base: basename(file),
};
}
async function* readPaths(dir: string): AsyncGenerator<string> {
try {
const files = await readdir(dir);
@@ -115,6 +130,7 @@ export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
eventsPath = makePath('events');
console.log('Events path is set to', eventsPath);
}
return {
defaultPrefix: config.defaultPrefix,
commands: commandsPath,

View File

@@ -61,18 +61,19 @@ export function discordEvent<T extends keyof ClientEvents>(mod: {
});
}
/**
* @deprecated
*/
function prepareClassPlugins(c: Module) {
const [onEvent, initPlugins] = partitionPlugins(c.plugins);
c.plugins = initPlugins as InitPlugin[];
c.onEvent = onEvent as ControlPlugin[];
}
//
// Class modules:
// Can be refactored.
// Both implement singleton, could I make them inherit a singleton parent class?
/**
* @Experimental
* Will be refactored / changed in future
* @deprecated
* Will be removed in future
*/
export abstract class CommandExecutable<const Type extends CommandType = CommandType> {
abstract type: Type;
@@ -92,9 +93,9 @@ export abstract class CommandExecutable<const Type extends CommandType = Command
}
/**
* @Experimental
* Will be refactored in future
*/
* @deprecated
* Will be removed in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: AnyEventPlugin[] = [];

View File

@@ -28,16 +28,15 @@ export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
}
interface PluginExecutable {
execute: (...args: unknown[]) => PluginResult;
};
/**
* 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
> {
export function callPlugin(args: unknown): OperatorFunction<PluginExecutable, VoidResult>
{
return concatMap(async plugin => {
if (Array.isArray(args)) {
return plugin.execute(...args);
@@ -79,8 +78,6 @@ export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<
}
onErr(result.error);
return EMPTY
})
)
}))

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

@@ -0,0 +1,70 @@
import type { ActivitiesOptions } from "discord.js";
import type { IntoDependencies } from "../types/ioc";
import type { Emitter } from "./contracts/emitter";
type Status = 'online' | 'idle' | 'invisible' | 'dnd'
type PresenceReduce = (previous: Result) => Result;
export interface Result {
status?: Status;
afk?: boolean;
activities?: ActivitiesOptions[];
shardId?: number[];
repeat?: number | [Emitter, string];
onRepeat?: (previous: Result) => Result;
}
export type Config <T extends (keyof Dependencies)[]> =
{
inject?: [...T]
execute: (...v: IntoDependencies<T>) => Result;
};
/**
* 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.
*/
export function module<T extends (keyof Dependencies)[]>
(conf: Config<T>) {
return 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.
*/
export function of(root: Omit<Result, 'repeat' | 'onRepeat'>) {
return {
/**
* @example
* Presence
* .of({
* activities: [{ name: "deez nuts" }]
* }) //starts the 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 returned one.
*/
repeated: (onRepeat: PresenceReduce, repeat: number | [Emitter, string]) => {
return { repeat, onRepeat, ...root }
},
/**
* @example
* Presence
* .of({
* activities: [
* { name: "Chilling out" }
* ]
* })
* .once() // Sets the presence once, with what's provided in '.of()'
*/
once: () => root
};
}

View File

@@ -11,8 +11,8 @@ import {
import { CoreContext } from '../structures/core-context';
import { Result, Ok, Err } from 'ts-results-es';
import * as assert from 'assert';
import { ReplyOptions } from '../../types/utility';
type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
/**
* @since 1.0.0
@@ -103,9 +103,9 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
public async reply(content: ReplyOptions) {
return safeUnwrap(
this.ctx
.map(m => m.reply(content as string | MessageReplyOptions))
.map(m => m.reply(content as MessageReplyOptions))
.mapErr(i =>
i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()),
i.reply(content as InteractionReplyOptions).then(() => i.fetchReply()),
),
);
}
@@ -114,7 +114,7 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
if ('interaction' in wrappable) {
return new Context(Ok(wrappable));
}
assert.ok(wrappable.isChatInputCommand());
assert.ok(wrappable.isChatInputCommand(), "Context created with bad interaction.");
return new Context(Err(wrappable));
}
}

View File

@@ -7,7 +7,7 @@ import * as assert from 'node:assert';
*/
export abstract class CoreContext<M, I> {
protected constructor(protected ctx: Either<M, I>) {
assert.ok(typeof ctx === 'object' && ctx != null);
assert.ok(typeof ctx === 'object' && ctx != null, "Context was nonobject or null");
}
get message(): M {
return this.ctx.expect(SernError.MismatchEvent);

View File

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

View File

@@ -11,6 +11,7 @@ import { CommandType } from '../enums';
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: CoreModuleStore) {}
getByNameCommandType<T extends CommandType>(name: string, commandType: T) {
const id = this.get(Id.create(name, commandType));
if (!id) {
@@ -43,7 +44,10 @@ export class DefaultModuleManager implements ModuleManager {
const publishable = 0b000000110;
return Promise.all(
Array.from(entries)
.filter(([id]) => !(Number.parseInt(id.at(-1)!) & publishable))
.filter(([id]) => {
const last_entry = id.at(-1);
return last_entry == 'B' || !(publishable & Number.parseInt(last_entry!));
})
.map(([, path]) => Files.importModule<CommandModule>(path)),
);
}

View File

@@ -9,20 +9,12 @@ import {
SernError,
} from '../core/_internal';
import { createResultResolver } from './event-utils';
import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js';
import { BaseInteraction, Message } from 'discord.js';
import { CommandType, Context } from '../core';
import type { Args } from '../types/utility';
import type { BothCommand, CommandModule, Module, Processed } from '../types/core-modules';
import { inspect } from 'node:util'
import type { CommandModule, Module, Processed } from '../types/core-modules';
function dispatchInteraction<T extends CommandModule, V extends BaseInteraction | Message>(
payload: { module: Processed<T>; event: V },
createArgs: (m: typeof payload.event) => unknown[],
) {
return {
module: payload.module,
args: createArgs(payload.event),
};
}
//TODO: refactor dispatchers so that it implements a strategy for each different type of payload?
export function dispatchMessage(module: Processed<CommandModule>, args: [Context, Args]) {
return {
@@ -31,41 +23,23 @@ export function dispatchMessage(module: Processed<CommandModule>, args: [Context
};
}
function dispatchAutocomplete(payload: {
module: Processed<BothCommand>;
event: AutocompleteInteraction;
}) {
const option = treeSearch(payload.event, payload.module.options);
assert.ok(
option,
Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`),
);
return {
module: option.command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [payload.event],
};
}
export function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
const ctx = Context.wrap(wrappable);
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options];
return [ctx, args] as [Context, Args];
}
function interactionArg<T extends BaseInteraction>(interaction: T) {
return [interaction] as [T];
}
function intoPayload(module: Processed<Module>) {
function intoPayload(module: Processed<Module>, ) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
map(args => ({ module, args, })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
{ module: Processed<Module>; args: unknown[] },
unknown[]
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
@@ -76,7 +50,7 @@ const createResult = createResultResolver<
* @param module
* @param source
*/
export function eventDispatcher(module: Processed<Module>, source: unknown) {
export function eventDispatcher(module: Processed<Module>, source: unknown) {
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
const execute: OperatorFunction<unknown[], unknown> = concatMap(async args =>
@@ -101,17 +75,24 @@ export function createDispatcher(payload: {
case CommandType.Slash:
case CommandType.Both: {
if (isAutocomplete(payload.event)) {
/**
* Autocomplete is a special case that
* must be handled separately, since it's
* too different from regular command modules
* CAST SAFETY: payload is already guaranteed to be a slash command or both command
*/
return dispatchAutocomplete(payload as never);
const option = treeSearch(payload.event, payload.module.options);
assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(payload.module));
const { command } = option;
return {
...payload,
module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [payload.event],
};
}
return dispatchInteraction(payload, contextArgs);
return {
module: payload.module,
args: contextArgs(payload.event),
};
}
default:
return dispatchInteraction(payload, interactionArg);
default: return {
module: payload.module,
args: [payload.event],
};
}
}

View File

@@ -21,10 +21,9 @@ import {
handleError,
SernError,
VoidResult,
useContainerRaw,
} from '../core/_internal';
import { Emitter, ErrorHandling, Logging, ModuleManager } from '../core';
import { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
import { contextArgs, createDispatcher } from './dispatchers';
import { ObservableInput, pipe } from 'rxjs';
import { SernEmitter } from '../core';
import { Err, Ok, Result } from 'ts-results-es';
@@ -32,6 +31,7 @@ import type { Awaitable } from '../types/utility';
import type { ControlPlugin } from '../types/core-plugin';
import type { AnyModule, CommandModule, Module, Processed } from '../types/core-modules';
import type { ImportPayload } from '../types/core';
import { disposeAll } from '../core/ioc/base';
function createGenericHandler<Source, Narrowed extends Source, Output>(
source: Observable<Source>,
@@ -71,17 +71,23 @@ export function createInteractionHandler<T extends Interaction>(
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
source,
async event => {
const fullPath = mg.get(Id.reconstruct(event));
if(!fullPath) {
return Err.EMPTY
const possibleIds = Id.reconstruct(event);
let fullPaths= possibleIds
.map(id => mg.get(id))
.filter((id): id is string => id !== undefined);
if(fullPaths.length == 0) {
return Err.EMPTY;
}
const [ path ] = fullPaths;
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then(payload =>
Ok(createDispatcher({ module: payload.module, event }))
);
},
);
.defaultModuleLoader<Processed<CommandModule>>(path)
.then(payload => Ok(createDispatcher({
module: payload.module,
event,
})));
},
);
}
export function createMessageHandler(
@@ -91,16 +97,18 @@ export function createMessageHandler(
) {
return createGenericHandler(source, async event => {
const [prefix, ...rest] = fmt(event.content, defaultPrefix);
const fullPath = mg.get(`${prefix}_A1`);
let fullPath = mg.get(`${prefix}_T`);
if(!fullPath) {
return Err('Possibly undefined behavior: could not find a static id to resolve ')
fullPath = mg.get(`${prefix}_B`);
if(!fullPath) {
return Err('Possibly undefined behavior: could not find a static id to resolve');
}
}
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then(payload => {
const args = contextArgs(event, rest);
return Ok(dispatchMessage(payload.module, args));
return Ok({ args, ...payload });
});
});
}
@@ -126,9 +134,17 @@ export function buildModules<T extends AnyModule>(
input: ObservableInput<string>,
moduleManager: ModuleManager,
) {
return Files.buildModuleStream<Processed<T>>(input).pipe(assignDefaults(moduleManager));
return Files
.buildModuleStream<Processed<T>>(input)
.pipe(assignDefaults(moduleManager));
}
interface ExecutePayload {
module: Processed<Module>;
task: () => Awaitable<unknown>;
args: unknown[]
}
/**
* Wraps the task in a Result as a try / catch.
* if the task is ok, an event is emitted and the stream becomes empty
@@ -139,13 +155,13 @@ export function buildModules<T extends AnyModule>(
*/
export function executeModule(
emitter: Emitter,
logger: Logging|undefined,
errHandler: ErrorHandling,
{
module,
task,
}: {
module: Processed<Module>;
task: () => Awaitable<unknown>;
},
args
}: ExecutePayload,
) {
return of(module).pipe(
//converting the task into a promise so rxjs can resolve the Awaitable properly
@@ -154,13 +170,15 @@ export function executeModule(
if (result.isOk()) {
emitter.emit('module.activate', SernEmitter.success(module));
return EMPTY;
} else {
return throwError(() => SernEmitter.failure(module, result.error));
}
}
return throwError(() => SernEmitter.failure(module, result.error));
}),
);
}
/**
* A higher order function that
* - creates a stream of {@link VoidResult} { config.createStream }
@@ -207,7 +225,7 @@ export function callInitPlugins<T extends Processed<AnyModule>>(sernEmitter: Emi
},
onNext: ({ module }) => {
sernEmitter.emit('module.register', SernEmitter.success(module));
return module;
return { module };
},
}),
);
@@ -219,16 +237,22 @@ export function callInitPlugins<T extends Processed<AnyModule>>(sernEmitter: Emi
*/
export function makeModuleExecutor<
M extends Processed<Module>,
Args extends { module: M; args: unknown[] },
Args extends {
module: M;
args: unknown[];
},
>(onStop: (m: M) => unknown) {
const onNext = ({ args, module }: Args) => ({
task: () => module.execute(...args),
module,
args
});
return concatMap(
createResultResolver({
onStop,
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
createStream: ({ args, module }) =>
from(module.onEvent)
.pipe(callPlugin(args)),
onNext,
}),
);
@@ -241,8 +265,5 @@ export const handleCrash = (err: ErrorHandling, log?: Logging) =>
log?.info({
message: 'A stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
);
disposeAll(log);
}));

View File

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

View File

@@ -1,4 +1,4 @@
import { concatMap, EMPTY } from 'rxjs';
import { mergeMap, EMPTY } from 'rxjs';
import type { Message } from 'discord.js';
import { SernEmitter } from '../core';
import { sharedEventStream, SernError, filterTap } from '../core/_internal';
@@ -23,7 +23,7 @@ function hasPrefix(prefix: string, content: string) {
}
export function messageHandler(
[emitter, , log, modules, client]: DependencyList,
[emitter, err, log, modules, client]: DependencyList,
defaultPrefix: string | undefined,
) {
if (!defaultPrefix) {
@@ -42,6 +42,5 @@ export function messageHandler(
makeModuleExecutor(module => {
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(emitter, payload)),
);
mergeMap(payload => executeModule(emitter, log, err, payload)));
}

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

@@ -0,0 +1,46 @@
import { concatMap, from, interval, of, map, scan, startWith, fromEvent, take } from "rxjs"
import { Files } from "../core/_internal";
import * as Presence from "../core/presences";
import { Services } from "../core/ioc";
import assert from "node:assert";
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(scan(onRepeat, s),
startWith(s));
}
//take 1?
return of(s).pipe(take(1));
})
};
export const presenceHandler = (path: string, setPresence: SetPresence) => {
interface PresenceModule {
module: Presence.Config<(keyof Dependencies)[]>
}
const presence = Files
.importModule<PresenceModule>(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))));
}

View File

@@ -17,17 +17,15 @@ export function startReadyEvent(
return concat(ready$, buildModules<AnyModule>(allPaths, moduleManager))
.pipe(callInitPlugins(sEmitter))
.subscribe(module => {
register(moduleManager, module).expect(
SernError.InvalidModuleType + ' ' + util.inspect(module),
);
.subscribe(({ module }) => {
register(moduleManager, module)
.expect(SernError.InvalidModuleType + ' ' + util.inspect(module));
});
}
const once = () => pipe(
first(),
ignoreElements()
)
ignoreElements())
function register<T extends Processed<AnyModule>>(
@@ -41,8 +39,12 @@ function register<T extends Processed<AnyModule>>(
validModuleType,
`Found ${module.name} at ${fullPath}, which does not have a valid type`,
);
if (module.type === CommandType.Both || module.type === CommandType.Text) {
module.alias?.forEach(a => manager.set(`${a}_A1`, fullPath));
if (module.type === CommandType.Both) {
module.alias?.forEach(a => manager.set(`${a}_B`, fullPath));
} else {
if(module.type === CommandType.Text){
module.alias?.forEach(a => manager.set(`${a}_T`, fullPath));
}
}
return Result.wrap(() => manager.set(id, fullPath));
}

View File

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

View File

@@ -50,7 +50,8 @@ export {
CommandExecutable,
} from './core/modules';
export * as Presence from './core/presences'
export {
useContainerRaw
} from './core/_internal'
export { controller } from './sern';

View File

@@ -1,4 +1,5 @@
import { handleCrash } from './handlers/_internal';
import callsites from 'callsites';
import { err, ok, Files } from './core/_internal';
import { merge } from 'rxjs';
import { Services } from './core/ioc';
@@ -7,6 +8,8 @@ import { eventsHandler } from './handlers/user-defined-events';
import { startReadyEvent } from './handlers/ready-event';
import { messageHandler } from './handlers/message-event';
import { interactionHandler } from './handlers/interaction-event';
import { presenceHandler } from './handlers/presence';
import { Client } from 'discord.js';
/**
* @since 1.0.0
@@ -31,14 +34,21 @@ export function init(maybeWrapper: Wrapper | 'file') {
if (wrapper.events !== undefined) {
eventsHandler(dependencies, Files.getFullPathTree(wrapper.events));
}
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
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands)).add(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
dependencies[0].emit('modulesLoaded');
logger?.info({
message: `sern: registered all modules in ${time} s`,
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands))
.add(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
dependencies[0].emit('modulesLoaded');
logger?.info({ message: `sern: registered all modules in ${time} s`, });
if(presencePath.exists) {
const setPresence = async (p: any) => {
return (dependencies[4] as Client).user?.setPresence(p);
}
presenceHandler(presencePath.path, setPresence).subscribe();
}
});
});
const messages$ = messageHandler(dependencies, wrapper.defaultPrefix);
const interactions$ = interactionHandler(dependencies);
@@ -56,11 +66,4 @@ function useDependencies() {
);
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
next: ok,
stop: err,
};

View File

@@ -19,6 +19,8 @@ import { CommandType, Context, EventType } from '../../src/core';
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './core-plugin';
import { Awaitable, Args, SlashOptions, SernEventsMapping } from './utility';
export interface CommandMeta {
fullPath: string;
id: string;

View File

@@ -1,3 +1,4 @@
export interface ImportPayload<T> {
module: T;
absPath: string;

View File

@@ -1,6 +1,6 @@
import { CommandInteractionOptionResolver } from 'discord.js';
import { PayloadType } from '../core';
import { AnyModule } from './core-modules';
import type { CommandInteractionOptionResolver, InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { PayloadType } from '../core';
import type { AnyModule } from './core-modules';
export type Awaitable<T> = PromiseLike<T> | T;
@@ -27,3 +27,7 @@ export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string };
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;

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.Config<['dependency1', 'dependency2']> = 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' }],
});
});
})

View File

@@ -29,14 +29,14 @@ describe('eventDispatcher standard', () => {
});
it('should throw', () => {
expect(() => eventDispatcher(m, 'not event emitter')).toThrowError();
expect(() => eventDispatcher(m, 'not event emitter')).toThrowError();
});
it("Shouldn't throw", () => {
expect(() => eventDispatcher(m, ee)).not.toThrowError();
});
it('Should be called once', () => {
const s = eventDispatcher(m, ee);
const s = eventDispatcher(m, ee);
s.subscribe();
ee.emit(m.name, faker.string.alpha());

View File

@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from 'vitest';
import * as Id from '../../src/core/id';
import { faker } from '@faker-js/faker';
import { CommandModule, CommandType, commandModule } from '../../src';
import { CommandTypeDiscordApi } from '../../src/core/id';
function createRandomCommandModules() {
const randomCommandType = [
@@ -41,32 +40,8 @@ describe('id resolution', () => {
const metadata = modules.map(createMetadata);
metadata.forEach((meta, idx) => {
const associatedModule = modules[idx];
const am = (appBitField & associatedModule.type) !== 0 ? 'A' : 'C';
let uid = 0;
if (
associatedModule.type === CommandType.Both ||
associatedModule.type === CommandType.Modal
) {
uid = 1;
} else {
uid = CommandTypeDiscordApi[Math.log2(associatedModule.type)];
}
expect(meta.id).toBe(associatedModule.name + '_' + am + uid);
const uid = Id.create(associatedModule.name!, associatedModule.type!);
expect(meta.id).toBe(uid);
});
});
it('maps commands type to discord components or application commands', () => {
expect(CommandTypeDiscordApi[Math.log2(CommandType.Text)]).toBe(1);
expect(CommandTypeDiscordApi[1]).toBe(1);
expect(CommandTypeDiscordApi[Math.log2(CommandType.CtxUser)]).toBe(2);
expect(CommandTypeDiscordApi[Math.log2(CommandType.CtxMsg)]).toBe(3);
expect(CommandTypeDiscordApi[Math.log2(CommandType.Button)]).toBe(2);
expect(CommandTypeDiscordApi[Math.log2(CommandType.StringSelect)]).toBe(3);
expect(CommandTypeDiscordApi[Math.log2(CommandType.UserSelect)]).toBe(5);
expect(CommandTypeDiscordApi[Math.log2(CommandType.RoleSelect)]).toBe(6);
expect(CommandTypeDiscordApi[Math.log2(CommandType.MentionableSelect)]).toBe(7);
expect(CommandTypeDiscordApi[Math.log2(CommandType.ChannelSelect)]).toBe(8);
expect(CommandTypeDiscordApi[6]).toBe(1);
});
});

View File

@@ -3,8 +3,6 @@
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"moduleResolution": "node",
"skipLibCheck": true,

735
yarn.lock

File diff suppressed because it is too large Load Diff