Compare commits

..

1 Commits

Author SHA1 Message Date
Jacob Nguyen
395f75bcda fix: tsresults 2023-08-21 21:39:30 -05:00
158 changed files with 9845 additions and 9502 deletions

450
.dependency-cruiser.js Normal file
View File

@@ -0,0 +1,450 @@
/** @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

1
.github/FUNDING.yml vendored
View File

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

View File

@@ -0,0 +1,50 @@
name: Continuous Integration
on:
# Trigger the workflow on push or pull request or custom
push:
branches: [main]
paths:
- '*.ts'
pull_request_target:
branches:
main
paths:
- '*ts'
workflow_dispatch:
jobs:
Prettier:
name: Run Prettier
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
- name: Set up Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
- name: Install pnpm
run: npm i -g yarn
# Prettier must be in `package.json`
- name: Install Node.js dependencies
run: yarn --immutable
- name: Run Prettier
run: yarn pretty
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
commit-message: "style: pretty please"
branch: prettier
delete-branch: true
branch-suffix: short-commit-hash
title: "style: pretty please"
body: "pretty pretty prettier"
reviewers: EvolutionX-10

View File

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

View File

@@ -1,34 +0,0 @@
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

@@ -10,13 +10,13 @@ jobs:
test-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm i
- run: npm run build:prod
- uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
node-version: 17
- run: yarn --immutable
- run: yarn build:prod
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: "public"

View File

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

View File

@@ -18,11 +18,12 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: npm run test
- run: npm install -g yarn
- run: yarn install
- run: yarn test

2
.gitignore vendored
View File

@@ -95,5 +95,3 @@ dist
.yalc
yalc.lock
*.svg

View File

@@ -113,4 +113,3 @@ tsconfig-cjs.json
tsconfig-esm.json
renovate.json
fortnite

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,166 +1,5 @@
# Changelog
## [4.2.4](https://github.com/sern-handler/handler/compare/v4.2.3...v4.2.4) (2025-03-06)
### Bug Fixes
* flat autocomplete ([#395](https://github.com/sern-handler/handler/issues/395)) ([89d7409](https://github.com/sern-handler/handler/commit/89d74095363befddc3222b9e5c89c35e7c6457b9))
## [4.2.3](https://github.com/sern-handler/handler/compare/v4.2.2...v4.2.3) (2025-03-04)
### Bug Fixes
* autocomplete sdt.module not present ([#393](https://github.com/sern-handler/handler/issues/393)) ([2414992](https://github.com/sern-handler/handler/commit/2414992b73a40065464b20f2d53826c78fcd3a5f))
## [4.2.2](https://github.com/sern-handler/handler/compare/v4.2.1...v4.2.2) (2025-02-03)
### Bug Fixes
* faster autocomplete lookup ([#387](https://github.com/sern-handler/handler/issues/387)) ([974c30f](https://github.com/sern-handler/handler/commit/974c30fa6cccaae7b1c2c3246ffa9eecb6bc7bf9))
## [4.2.1](https://github.com/sern-handler/handler/compare/v4.2.0...v4.2.1) (2025-01-24)
### Bug Fixes
* context-interactions error ([#382](https://github.com/sern-handler/handler/issues/382)) ([a52ad27](https://github.com/sern-handler/handler/commit/a52ad270d843e92db5bf2049d07527eed59d428c))
## [4.2.0](https://github.com/sern-handler/handler/compare/v4.1.1...v4.2.0) (2025-01-18)
### Features
* 4.2.0 load multiple directories & `handleModuleErrors` ([#378](https://github.com/sern-handler/handler/issues/378)) ([f9e7eaf](https://github.com/sern-handler/handler/commit/f9e7eaf92d22b76d3d02a1bbe8324ca6813f48f8))
## [4.1.1](https://github.com/sern-handler/handler/compare/v4.1.0...v4.1.1) (2025-01-13)
### Bug Fixes
* remove rxjs ([#376](https://github.com/sern-handler/handler/issues/376)) ([59d08ef](https://github.com/sern-handler/handler/commit/59d08ef207c486ce1cf0aba267e6f862838e0dfb))
* This puts the light back into lightweight (\- 4.1 MB)
## [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)

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 sern
Copyright (c) 2023 sern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -7,7 +7,6 @@
<div align="center" styles="margin-top: 10px">
<img src="https://img.shields.io/badge/open-source-brightgreen" />
<img src="https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev"/>
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/v/@sern/handler?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/dt/@sern/handler?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="License MIT" /></a>
@@ -19,17 +18,30 @@
- For you. A framework that's tailored to your exact needs.
- Lightweight. Does a lot while being small.
- Latest features. Support for discord.js v14 and all of its interactions.
- Hybrid, customizable and composable commands. Create them just how you like.
- Start quickly. Plug and play or customize to your liking.
- Works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
- Embraces reactive programming. For consistent and reliable backend.
- Switch and customize how errors are handled, logging, and more.
- Use it with TypeScript or JavaScript. CommonJS and ESM supported.
- Active and growing community, always here to help. [Join us](https://sern.dev/discord)
- Unleash its full potential with a powerful CLI and awesome plugins.
## 📜 Installation
[Start here!!](https://sern.dev/v4/reference/getting-started)
```sh
npm install @sern/handler
```
```sh
yarn add @sern/handler
```
```sh
pnpm add @sern/handler
```
## 👶 Basic Usage
<details><summary>ping.ts</summary>
<details open><summary>ping.ts</summary>
```ts
export default commandModule({
@@ -43,30 +55,69 @@ export default commandModule({
});
```
</details>
<details open><summary>modal.ts</summary>
# Show off your sern Discord Bot!
```ts
export default commandModule({
type: CommandType.Modal,
//Installed a plugin to make sure modal fields pass a validation.
plugins : [
assertFields({
fields: {
name: /^([^0-9]*)$/
},
failure: (errors, modal) => modal.reply('your submission did not pass the validations')
})
],
execute : (modal) => {
modal.reply('thanks for the submission!');
}
})
```
</details>
<details open><summary>index.ts</summary>
## Badge
- Copy this and add it to your [README.md](https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev)
<img src="https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev">
```ts
import { Client, GatewayIntentBits } from 'discord.js';
import { Sern, single, type Dependencies } from '@sern/handler';
//client has been declared previously
interface MyDependencies extends Dependencies {
'@sern/client': Singleton<Client>;
}
export const useContainer = Sern.makeDependencies<MyDependencies>({
build: root => root
.add({ '@sern/client': single(() => client) })
});
//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");
```
</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 to your favorite artists on Discord.
- [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.
- [ZenithBot](https://github.com/CodeCraftersHaven/ZenithBot) - A versatile bot coded in TypeScript, designed to enhance server management and user interaction through its robust features.
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).
- [Vinci](https://github.com/SrIzan10/vinci), the bot for Mara Turing.
- [Bask](https://github.com/baskbotml/bask), Listen your favorite artists on Discord.
- [ava](https://github.com/SrIzan10/ava), A discord bot that plays KNGI and Gensokyo Radio.
- [Murayama](https://github.com/murayamabot/murayama), :pepega:
- [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud), A fun bot for a small - but growing - server.
## 💻 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.
## 🔗 Links
- [Official Documentation and Guide](https://sern.dev)

4
bot/.gitignore vendored
View File

@@ -1,4 +0,0 @@
/node_modules
/dist
.env
.sern

View File

@@ -1,6 +0,0 @@
# Test bot
## add .env
DISCORD_TOKEN=<token>
NODE_ENV=<production|development>

View File

@@ -1,12 +0,0 @@
{
"command/ping": {
"name": "ping",
"description": "yeth",
"options": {
"asdfs": {
"name": "shidenglish",
"description": "yeah"
}
}
}
}

View File

@@ -1,12 +0,0 @@
{
"command/ping": {
"name": "ping",
"description": "hola",
"options": {
"asdfs": {
"name": "shidspnaol",
"description": "si"
}
}
}
}

View File

@@ -1 +0,0 @@
{ "sdfasdfas": "asdf" }

418
bot/package-lock.json generated
View File

@@ -1,418 +0,0 @@
{
"name": "plugtest",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "plugtest",
"version": "1.0.0",
"license": "UNLICENSED",
"dependencies": {
"@sern/handler": "file:../",
"@sern/localizer": "^1.1.3",
"@sern/publisher": "^1.1.2",
"discord.js": "^14.15.0",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@types/node": "^17.0.25",
"typescript": "latest"
}
},
"..": {
"name": "@sern/handler",
"version": "4.2.4",
"license": "MIT",
"dependencies": {
"@sern/ioc": "^1.1.2",
"callsites": "^3.1.0",
"cron": "^3.1.7",
"deepmerge": "^4.3.1"
},
"devDependencies": {
"@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.14.1",
"eslint": "8.39.0",
"typescript": "5.0.2",
"vitest": "^1.6.0"
},
"engines": {
"node": ">= 20.0.x"
}
},
"../../tools/packages/builder": {
"name": "@sern/builder",
"version": "1.0.0-rc1",
"extraneous": true,
"license": "ISC",
"dependencies": {
"discord-api-types": "latest"
},
"devDependencies": {
"@types/node": "^20.1.0"
}
},
"../handler": {
"name": "@sern/handler",
"version": "4.2.3",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@sern/ioc": "^1.1.2",
"callsites": "^3.1.0",
"cron": "^3.1.7",
"deepmerge": "^4.3.1"
},
"devDependencies": {
"@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.14.1",
"eslint": "8.39.0",
"typescript": "5.0.2",
"vitest": "^1.6.0"
},
"engines": {
"node": ">= 20.0.x"
}
},
"../tools/packages/builder": {
"name": "@sern/builder",
"version": "1.0.0-rc1",
"extraneous": true,
"license": "ISC",
"dependencies": {
"discord-api-types": "latest"
},
"devDependencies": {
"@types/node": "^20.1.0"
}
},
"../tools/packages/localizer": {
"name": "@sern/localizer",
"version": "1.1.1",
"extraneous": true,
"license": "ISC",
"dependencies": {
"shrimple-locales": "^0.2.1"
},
"devDependencies": {
"@sern/handler": "^4.0.0",
"discord.js": "^14.15.3",
"vitest": "^1.2.2"
}
},
"node_modules/@discordjs/builders": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz",
"integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==",
"dependencies": {
"@discordjs/formatters": "^0.6.0",
"@discordjs/util": "^1.1.1",
"@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.37.114",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/collection": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
"engines": {
"node": ">=16.11.0"
}
},
"node_modules/@discordjs/formatters": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz",
"integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==",
"dependencies": {
"discord-api-types": "^0.37.114"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz",
"integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==",
"dependencies": {
"@discordjs/collection": "^2.1.1",
"@discordjs/util": "^1.1.1",
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.3",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.37.114",
"magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3",
"undici": "6.19.8"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/util": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz",
"integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==",
"dependencies": {
"@discordjs/collection": "^2.1.0",
"@discordjs/rest": "^2.4.1",
"@discordjs/util": "^1.1.0",
"@sapphire/async-queue": "^1.5.2",
"@types/ws": "^8.5.10",
"@vladfrangu/async_event_emitter": "^2.2.4",
"discord-api-types": "^0.37.114",
"tslib": "^2.6.2",
"ws": "^8.17.0"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@sapphire/shapeshift": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v16"
}
},
"node_modules/@sapphire/snowflake": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@sern/handler": {
"resolved": "..",
"link": true
},
"node_modules/@sern/localizer": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@sern/localizer/-/localizer-1.1.3.tgz",
"integrity": "sha512-hTn0DtiAzIWSuokqMsvnVuFqU+P776p/Yv5etlrq+CWDgw332Hwuj3geyqN1C0yEjwF+ceyXJE/kGu2/inkEyg==",
"dependencies": {
"shrimple-locales": "^0.2.1"
}
},
"node_modules/@sern/publisher": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@sern/publisher/-/publisher-1.1.2.tgz",
"integrity": "sha512-1zh99JZykKUhqHhE75ZXfiLsBtf1WI+NnDCojv8UlpnGBEyzO8xyI1X7PNf6cPKRs4W9XqY3PqTJ+hrqzIsMkg=="
},
"node_modules/@types/node": {
"version": "17.0.45",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
},
"node_modules/@types/ws": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz",
"integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/discord-api-types": {
"version": "0.37.119",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz",
"integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg=="
},
"node_modules/discord.js": {
"version": "14.17.3",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.3.tgz",
"integrity": "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==",
"dependencies": {
"@discordjs/builders": "^1.10.0",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.0",
"@discordjs/rest": "^2.4.2",
"@discordjs/util": "^1.1.1",
"@discordjs/ws": "^1.2.0",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.37.114",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"tslib": "^2.6.3",
"undici": "6.19.8"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
},
"node_modules/magic-bytes.js": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
"integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="
},
"node_modules/shrimple-locales": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/shrimple-locales/-/shrimple-locales-0.2.1.tgz",
"integrity": "sha512-j2vNBDXJgED3XqGXCD/vqXBSqwlDXP1iGkseVos8mCtZqHp3R+0FImx8xwtjeYufJcYfhjBMkaBTWgsBi8eJZw=="
},
"node_modules/ts-mixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
"engines": {
"node": ">=18.17"
}
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -1,30 +0,0 @@
{
"name": "plugtest",
"version": "1.0.0",
"description": "a descriptiuon",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "sern build",
"start": "node ./dist/index.js",
"run": "node ./dist/index.js"
},
"keywords": [
"typescript",
"sern",
"discord.js"
],
"license": "UNLICENSED",
"dependencies": {
"@sern/handler": "file:../",
"@sern/localizer": "^1.1.3",
"@sern/publisher": "^1.1.2",
"discord.js": "^14.15.0",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@types/node": "^17.0.25",
"typescript": "latest"
},
"type": "module"
}

View File

@@ -1,11 +0,0 @@
import os
for root, dirs, files in os.walk('.'):
for filename in files:
if filename.endswith('.js'):
file_path = os.path.join(root, filename)
try:
os.remove(file_path)
print(f'Successfully deleted: {file_path}')
except OSError as e:
print(f'Error deleting {file_path}: {e.strerror}')

View File

@@ -1,13 +0,0 @@
{
"language": "typescript",
"defaultPrefix": "!",
"paths": {
"base": "src",
"commands": "commands",
"events": "events"
},
"app": {
"tags": ["Nice ass bot"],
"description": "A bot"
}
}

View File

@@ -1,37 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
import { ApplicationCommandOptionType } from "discord.js";
export default commandModule({
name: 'add',
type: CommandType.Slash,
description: 'Adds numbers together',
options: [
{
type: ApplicationCommandOptionType.String,
name: 'numbers',
description: 'Numbers to add together separated by a space.',
required: true,
min_length: 3,
},
],
execute: async (ctx) => {
let numbers = ctx.options.getString('numbers')?.split(' ')!;
numbers = numbers.filter((num) => num !== '');
if (!numbers.every((num) => !isNaN(parseFloat(num)))) {
return ctx.reply({
content: 'You can only input numbers.',
ephemeral: true,
});
}
const sum = numbers.reduce((acc, num) => acc + parseFloat(num), 0);
return ctx.reply({
content: `The sum is ${sum}`,
ephemeral: true,
});
},
});

View File

@@ -1,8 +0,0 @@
import { filter, hasRole } from "../../plugins/filter.js";
import { ownerOnly } from "../../plugins/ownerOnly.js";
import { ADMIN } from '../../constants.js'
export default [
ownerOnly(),
filter({ condition: [hasRole(ADMIN)] })
]

View File

@@ -1,11 +0,0 @@
import { commandModule, CommandType } from '@sern/handler'
export default commandModule({
type: CommandType.Slash,
description: "A",
execute: (ctx, args) => {
}
})

View File

@@ -1,14 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
import { json } from "../plugins/json-params.js";
export default commandModule({
type: CommandType.Button,
plugins: [json],
execute(ctx, args) {
console.log(args.state['json/data'])
//@ts-ignore
ctx.reply(args.state['json/data'].uid)
}
})

View File

@@ -1,9 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
export default commandModule({
type: CommandType.ChannelSelect,
execute: (s) => {
s.reply('clicked channel');
}
});

View File

@@ -1,25 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
import { publishConfig } from "@sern/publisher";
import { PermissionFlagsBits } from "discord.js";
export default commandModule({
type: CommandType.Slash,
plugins: [
publishConfig({
integrationTypes: ['User'],
contexts: [0,1,2],
defaultMemberPermissions:
PermissionFlagsBits.Speak
| PermissionFlagsBits.Connect
| PermissionFlagsBits.BanMembers
})
],
description: "yo",
execute:(ctx) => {
ctx.reply("hello");
}
})

View File

@@ -1,103 +0,0 @@
import { CommandType, Context, commandModule } from "@sern/handler";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
export default commandModule ({
type: CommandType.Slash,
description: 'collectors',
execute: async (ctx) => {
//await close(ctx)
await testCollect(ctx)
}
})
const testCollect = async (ctx: Context) => {
const msgcmpt = ctx.interaction.channel?.createMessageComponentCollector()
const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId("closeyes").setLabel("Yes").setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId("closeno").setLabel("No").setStyle(ButtonStyle.Danger)
);
ctx.reply({ components: [buttonRow] })
msgcmpt?.on('collect', async button => {
await button.deferUpdate();
if (button.customId === "closeyes") {
try {
await button.editReply('closing')
} catch (e) {
await button.editReply({ content: "An error has occurred and I could not close the ticket...", components: [] });
}
} else {
await button.editReply({ content: "This ticket will remain open.", components: [] });
msgcmpt.stop();
}
});
}
//export const quiz = async(client: Client, ctx: Context) => {
// try {
// const pokemon = Math.round(Math.random() * 890)
// const question = `https://cdn.dagpi.xyz/wtp/pokemon/${pokemon}q.png`;
// const answer = `https://cdn.dagpi.xyz/wtp/pokemon/${pokemon}a.png`;
//
// const correctPokemon = await (await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)).json();
// const allPokemon = await (await fetch("https://pokeapi.co/api/v2/pokemon?limit=899")).json();
//
// const options: string[] = [];
// //client.utils.log("WARNING", "INFO", `Correct answer is ${correctPokemon.name}`);
//
// while (options.length < 9) {
// let option = allPokemon.results[pokemon];
// if (options.includes(option.name)) continue;
// options.push(option.name);
// }
//
// if (!options.includes(correctPokemon.name)) {
// options.splice(client.utils.randomRange(0, 10), 0, correctPokemon.name.toLowerCase());
// } else {
// while (options.length < 10) {
// let option = allPokemon.results[client.utils.randomRange(1, 890)];
// if (options.includes(option.name)) continue;
// options.push(option.name);
// }
// }
//
// const msgEmbed = (await client.utils.CustomEmbed({ userID: ctx.user.id })).setTitle("Who's that Pokémon?").setImage(question);
// const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
// new StringSelectMenuBuilder().setCustomId("pokequiz").addOptions(
// options.sort().map((opt) => {
// return { label: client.utils.titleCase(opt), value: opt.toLowerCase() };
// })
// )
// );
//
// const msg = await client.utils.fetchReply(ctx.interaction, { embeds: [msgEmbed], components: [row] });
// const filter = (i: StringSelectMenuInteraction) => i.user.id === ctx.user.id && i.message.id === msg.id;
//
// const collector = msg.createMessageComponentCollector({ filter, componentType: ComponentType.StringSelect, time: 1000 * 20 });
// collector.on("collect", async (i) => {
// const guess = i.values[0].toLowerCase();
//
// msgEmbed.setImage(answer).setTitle(`It's ${client.utils.titleCase(correctPokemon.name)}!`);
//
// if (guess === correctPokemon.name.toLowerCase()) msgEmbed.setColor("Green").setFooter({ text: "You're correct!" });
// else msgEmbed.setColor("Red").setFooter({ text: `You guessed ${client.utils.titleCase(guess)}.` });
//
// await i.update({ embeds: [msgEmbed], components: [] });
// collector.stop("Guessed");
// });
//
// collector.on("end", async (i, reason) => {
// if (reason === "Guessed") return;
//
// msgEmbed
// .setImage(answer)
// .setTitle(`It's ${client.utils.titleCase(correctPokemon.name)}!`)
// .setColor("Red")
// .setFooter({ text: "You did not guess in time." });
//
// await ctx.interaction.editReply({ embeds: [msgEmbed], components: [] });
// });
// } catch (e) {
// client.utils.log("ERROR", __filename, `${e}`);
// return ctx.interaction.reply({ content: "An error has occurred. Please try again.", ephemeral: true });
// }
//};

View File

@@ -1,6 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
export default commandModule({
type: CommandType.Modal,
execute: (modal) => modal.reply('thanks')
});

View File

@@ -1,24 +0,0 @@
import { commandModule, CommandType } from "@sern/handler";
import { ApplicationCommandOptionType } from "discord.js";
export default commandModule({
description: "testing",
type: CommandType.Slash,
options: [
{
name: "option",
description: "option desc",
type: ApplicationCommandOptionType.String,
required: true,
autocomplete: true,
command: {
execute: (i) => {
i.respond([{ name: "rah", value: "rah" }]);
},
},
},
],
execute: (ctx) => {
return ctx.reply("rah");
},
});

View File

@@ -1,52 +0,0 @@
import { commandModule, CommandType } from "@sern/handler";
import { ActionRowBuilder, ApplicationCommandOptionType, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
export default commandModule({
type: CommandType.Slash,
description : 'a ping command',
options: [
{
name: "nest",
description: "testing nested",
type: ApplicationCommandOptionType.SubcommandGroup,
options : [
{
name: "nest",
description: "testing nested",
type: ApplicationCommandOptionType.Subcommand,
options : [
{
name: "sdfasd",
description: "testing autocomplete",
autocomplete: true,
type: ApplicationCommandOptionType.String,
command : {
onEvent : [],
async execute(autocmp, sdt) {
//console.log(autocmp)
const choices = ['butt', 'deez', 'lmao', 'lmfao', 'nuts', 'chicken'];
await autocmp.respond(choices.map((e,i) => ({ name : e, value: i.toString()})));
}
}
}
]
},
]
},
],
async execute ({ interaction }) {
const modal = new ModalBuilder()
.setCustomId('dmMe')
.setTitle('send something to my dm (nothing bad pls)');
const input = new TextInputBuilder()
.setCustomId('message')
.setLabel("Send something to me")
.setStyle(TextInputStyle.Short);
const firstActionRow = new ActionRowBuilder<TextInputBuilder>().addComponents([input]);
modal.addComponents([firstActionRow]);
await interaction.showModal(modal);
}
});

View File

@@ -1,8 +0,0 @@
import { CommandType, commandModule } from '@sern/handler'
export default commandModule({
type: CommandType.CtxMsg,
execute: (i, sdt) => {
i.reply('pong msg')
}
})

View File

@@ -1,9 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
export default commandModule({
type: CommandType.CtxUser,
execute: (i, sdt) => {
i.reply('pong')
}
})

View File

@@ -1,84 +0,0 @@
import {commandModule, CommandType, controller, CommandInitPlugin, CommandControlPlugin } from '@sern/handler';
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ChannelSelectMenuBuilder,
RoleSelectMenuBuilder,
UserSelectMenuBuilder,
} from "discord.js";
import { localize } from '@sern/localizer';
const plugin = CommandControlPlugin(() => {
return controller.next({ a: 'from plugin1' });
});
const plugin2 = CommandControlPlugin(() => {
return controller.next({ a: 'from plugin2' });
})
const updateDescription = (description: string) => {
return CommandInitPlugin(() => {
if(description.length > 100) {
console.error("Description is invalid")
return controller.stop("From updateDescription: description is invalid");
}
return controller.next({ description }); // continue to next plugin
});
};
export default commandModule({
type: CommandType.Slash,
plugins: [localize()],
description: 'A ping command I just updated',
options: [
str(name("asdfs"),
description("sdfds"))
],
execute: async (ctx, sdt) => {
ctx.interaction
const btn = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setLabel("Click me")
.setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ&pp=ygUIcmlja3JvbGw%3D')
const editButton = new ButtonBuilder({
customId: `btn/{"uid":"1061421834341462036"}`,
label: "click me also",
emoji: "🛠",
style: ButtonStyle.Primary,
});
ctx.reply({ components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(btn, editButton),
new ActionRowBuilder<UserSelectMenuBuilder>({
components: [
new UserSelectMenuBuilder({
custom_id: "userselect",
placeholder: "select channel",
minValues: 1,
}),
],
}),
new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
new ChannelSelectMenuBuilder({
custom_id: "channelselect",
placeholder: "select channel",
minValues: 1,
}),
),
new ActionRowBuilder<RoleSelectMenuBuilder>({
components: [
new RoleSelectMenuBuilder({
custom_id: "roleselect",
placeholder: "select role",
minValues: 1,
}),
],
})
]})
},
});

View File

@@ -1,9 +0,0 @@
import { CommandType, commandModule } from "@sern/handler"
export default commandModule( {
type: CommandType.RoleSelect,
execute: (s) => {
s.reply('selected role')
}
})

View File

@@ -1,12 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
export default commandModule({
type: CommandType.Slash,
description: 'shid',
execute({ interaction }) {
interaction.reply('hello')
}
})

View File

@@ -1,62 +0,0 @@
import { commandModule, CommandType } from '@sern/handler';
import { ApplicationCommandOptionType } from 'discord.js';
export default commandModule({
type: CommandType.Slash,
description: 'A ping command',
options: [
{
name: "art",
type: ApplicationCommandOptionType.Subcommand,
description: "Lists out information about an Animal Crossing artwork.",
options: [
{
name: "name",
description: "The name of the artwork to lookup.",
type: ApplicationCommandOptionType.String,
autocomplete: true,
required: true,
command: {
async execute(ctx) {
await ctx.respond([{ name: 'art', value: 'first' }])
},
},
},
],
},
{
name: "villager",
type: ApplicationCommandOptionType.Subcommand,
description: "Lists out information about an Animal Crossing villager.",
options: [
{
name: "name",
description: "The name of the villager to lookup.",
type: ApplicationCommandOptionType.String,
autocomplete: true,
required: true,
command: {
onEvent: [],
async execute(ctx) {
await ctx.respond([{ name: 'villager', value: 'second' } ])
},
},
},
],
},
],
execute: async (ctx) => {
const command = ctx.options.getSubcommand();
switch (command) {
case "art": {
ctx.reply('art');
break;
}
case "villager": {
ctx.reply('vil');
break;
}
}
},
});

View File

@@ -1,40 +0,0 @@
import { ApplicationCommandOptionType } from "discord.js";
import { Service, commandModule, CommandType } from "@sern/handler";
export const config = {
guildIds: ['941002690211766332']
}
export default commandModule({
type: CommandType.Both,
description: 'tests context',
options: [
{
type: ApplicationCommandOptionType.String,
name: "hello",
description: "wassup",
required: false,
}
],
async execute(ctx) {
const logger = Service('@sern/logger');
if(ctx.isMessage()) {
logger?.info({ message : ctx.message.content })
logger?.info({ message : ctx.prefix })
} else {
logger?.info({ message : ctx.interaction.toString() })
}
logger?.info({ message: ctx.id })
logger?.info({ message: ctx.channel?.toString()! })
logger?.info({ message: ctx.user.toString()! })
logger?.info({ message: ctx.createdTimestamp.toString() })
logger?.info({ message: ctx.guild?.toString() })
logger?.info({ message: ctx.member?.toString() })
logger?.info({ message: ctx.client })
logger?.info({ message: ctx.inGuild })
await ctx.reply("guayin bodishivatta")
}
})

View File

@@ -1,33 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
import { ActionRowBuilder, ModalActionRowComponentBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
const informationRequestModal = new ModalBuilder()
.setCustomId("information-request")
.setTitle("More Information")
.addComponents(
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
new TextInputBuilder()
.setCustomId("command-name")
.setLabel("Command Name")
.setPlaceholder("The name of the command that this bug occurred on.")
.setStyle(TextInputStyle.Short)
.setMinLength(4)
.setMaxLength(20)
.setRequired(true)));
export default commandModule({
type: CommandType.Slash,
plugins: [],
description: "A random test command.",
execute: async (ctx) => {
await ctx.interaction.showModal(informationRequestModal);
await ctx.interaction
.awaitModalSubmit({ time: 300_000 })
.then(async (modal) => {
modal.reply("thanks brody")
})
.catch(() => null);
},
});

View File

@@ -1,8 +0,0 @@
import { CommandType, commandModule } from "@sern/handler";
export default commandModule( {
type: CommandType.UserSelect,
execute: (s) => {
s.reply('selected user')
}
})

View File

@@ -1 +0,0 @@
export const ADMIN = '983754333944434712'

View File

@@ -1,18 +0,0 @@
import type {
Logging,
ErrorHandling,
CoreDependencies
} from '@sern/handler'
import type { Publisher } from '@sern/publisher';
import type { Localizer } from '@sern/localizer';
declare global {
interface Dependencies extends CoreDependencies {
localizer: Localizer;
publisher: Publisher
}
}
export {}

View File

@@ -1,10 +0,0 @@
import { EventType, eventModule } from "@sern/handler";
export default eventModule({
name: 'error',
type: EventType.Sern,
execute: (e) => {
console.log(e)
}
})

View File

@@ -1,10 +0,0 @@
import { discordEvent } from "@sern/handler";
const execute = (...args: any[]) => {
console.log(args[0].content)
}
export default discordEvent({
name: 'messageCreate',
once: true,
execute
})

View File

@@ -1,10 +0,0 @@
import {eventModule, EventType} from "@sern/handler";
export default eventModule({
type: EventType.Sern,
name: 'module.activate',
execute(args) {
}
})

View File

@@ -1,9 +0,0 @@
import { CommandType, EventType, Service, eventModule } from "@sern/handler";
export default eventModule({
type: EventType.Sern,
execute: async () => {
console.log('eventmodule: all loaded');
}
})

View File

@@ -1,8 +0,0 @@
import { discordEvent } from "@sern/handler";
export default discordEvent({
name: 'threadCreate',
execute(thread) {
console.log(thread)
}
})

View File

@@ -1,41 +0,0 @@
import 'dotenv/config';
import { makeDependencies, Sern, Service } from '@sern/handler'
import { Client, GatewayIntentBits, Partials } from 'discord.js';
import { Publisher } from '@sern/publisher'
import { Localization } from '@sern/localizer'
__DEV__: console.log(1);
const intents = GatewayIntentBits.Guilds |
GatewayIntentBits.GuildMembers |
GatewayIntentBits.GuildMessageReactions |
GatewayIntentBits.GuildMessages |
GatewayIntentBits.DirectMessages |
GatewayIntentBits.MessageContent;
const partials = [
Partials.Channel
];
async function init() {
await makeDependencies(({ add }) => {
add('@sern/client', new Client({ intents, partials }));
add('localizer', Localization());
add('publisher', deps => {
return new Publisher(deps['@sern/modules'],
deps['@sern/emitter'],
deps['@sern/logger']!)
})
})
Sern.init({
commands : "./dist/commands",
events: "./dist/events",
tasks: "./dist/tasks",
defaultPrefix: "!"
})
}
init().then(() => {
Service('@sern/client').login()
})
//View docs for all options

View File

@@ -1,156 +0,0 @@
/**
* @author HighArcs
* @version 1.0.0
* @description converts array of argument strings to an object (and maps them)
* @license null
* @example
* ```ts
* import { parsedCommandModule, args } from "../plugins/args";
* import { CommandType } from "@sern/handler";
*
* interface Arg {
* value: number;
* }
*
* export default parsedCommandModule({
* type : CommandType.Text
* plugins: [args({ value: Number })],
* execute: (ctx, args) => {
* console.log(ctx.args.value);
* }
* })
*/
import {
commandModule,
CommandType,
Context, ControlPlugin,
Plugin, CommandControlPlugin, controller
} from "@sern/handler";
import type { Awaitable } from "discord.js";
type Converter<T> = (value?: string) => Awaitable<T>;
type Struct = Record<string, any>;
type ConverterList<T extends Struct> = {
[K in keyof T]: Converter<T[K]>;
};
type Ctx<T> = Context & { _args: T };
interface Err {
key: string;
error: string;
given: string;
index: number;
}
type OnError<T> = (context: Ctx<T>, error: Err) => any;
type SpecialEvt<T> = {
readonly "@@plugin": symbol
} & ControlPlugin<[Ctx<T>, ]>
async function convert<T extends Struct>(
args: Array<string>,
struct: ConverterList<T>
) {
const entries = Object.entries(struct);
const result = {} as T;
for (let i = 0; i < entries.length; i++) {
const value = args[i];
const [key, converter] = entries[i]!;
try {
result[key as keyof T] = await converter(value);
} catch (error) {
throw { key, error: String(error), given: value, index: i };
}
}
return result;
}
interface ParsedInputCommandModule<T extends Struct> {
name?: string;
description: string;
type: CommandType.Both | CommandType.Text | CommandType.Slash;
execute: (context: Ctx<T>, args: Array<string>) => any;
plugins: () =>
| [SpecialEvt<T>, ...Array<Plugin>]
| []
| undefined;
}
export const Structs = {
string: (value: string) => String(value),
number: (value: string) => Number(value),
boolean: (value: string) => value === "true" || value === "1",
date: (value: string) => new Date(value),
integer: (value: string) => Number.parseInt(value),
};
export function parsedCommandModule<T extends Struct>(
a: ParsedInputCommandModule<T>
) {
const plugins = (a.plugins() ?? []);
return commandModule({ ...a, plugins } as never);
}
export namespace Checks {
export function choices<K extends string>(
choices: K[],
value?: string
): asserts value is K {
if (!choices.includes(value as unknown as K)) {
throw "value is not in choices";
}
}
export function required(value?: string): asserts value is string {
if (value === undefined) {
throw "value is required";
}
}
export function limit(min: number, max: number, value?: string) {
required(value);
const val = Structs.number(value);
if (val < min) {
throw `value must be higher than ${min}`;
}
if (val > max) {
throw `value must be lower than ${max}`;
}
return val;
}
}
export function args<T extends Struct>(
struct: ConverterList<T>,
onError?: OnError<T>
): SpecialEvt<T> {
const plugin = CommandControlPlugin<CommandType.Both>(async (ctx, args) => {
switch(args.type) {
case "slash": {
let result: T;
} break;
case "text" : {
let result: T;
try {
result = await convert(args, struct)
} catch (e) {
if (onError) {
onError(ctx as Ctx<T>, e as Err);
}
return controller.stop();
}
//@warn - mutable assignment!
(ctx as Ctx<T>)._args = result;
return controller.next();
}
}
return controller.next()
})
Object.defineProperty(plugin, "@@plugin", { value: Symbol("args") })
return plugin as SpecialEvt<T>;
}

View File

@@ -1,57 +0,0 @@
/**
* This plugin checks the fields of a ModalSubmitInteraction
* with regex or a custom callback
*
* @author @jacoobes [<@182326315813306368>]
* @version 1.0.0
* @example
* ```ts
* export default commandModule({
* type: CommandType.Modal,
* plugins: [
* assertFields({
* fields: {
* // check the modal field "mcUsernameInput" with the regex /a+b+c/
* mcUsernameInput: /a+b+c+/
* },
* failure: (errors, interaction) => {
* interaction.reply(errors.join("\n"))
* }
* }),
* ],
* execute: ctx => {
* ctx.reply("nice!")
* }
* })
* ```
*/
import { CommandControlPlugin, CommandType, controller } from "@sern/handler";
import type { ModalSubmitInteraction } from "discord.js";
type Assertion =
| RegExp
| ((value : string) => boolean);
export function assertFields(config: {
fields: Record<string, Assertion>,
failure: (errors: string[], interaction: ModalSubmitInteraction) => any
}) {
return CommandControlPlugin<CommandType.Modal>(modal => {
const pairs = Object.entries(config.fields);
const errors = [];
for(const [ field, assertion ] of pairs) {
// Keep in mind this doesn't check for typos!
// feel free to add more checks.
const input = modal.fields.getTextInputValue(field)
const resolvedAssertion = assertion instanceof RegExp ? (value: string) => assertion.test(value) : assertion;
if(!resolvedAssertion(input)) {
errors.push(input + " failed to pass assertion " + resolvedAssertion.toString() )
}
}
if(errors.length > 0) {
config.failure(errors, modal);
return controller.stop();
}
return controller.next();
})
}

View File

@@ -1,39 +0,0 @@
/**
* This plugin checks if a channel is the specified type
*
* @author @Benzo-Fury [<@762918086349029386>]
* @version 1.0.0
* @example
* ```ts
* import { channelType } from "../plugins/channelType";
* import { ChannelType } from "discord.js"
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [ channelType([ChannelType.GuildText], 'This cannot be used here') ],
* execute: (ctx) => {
* //your code here
* }
* })
* ```
*/
import { ChannelType } from "discord.js";
import {CommandControlPlugin, CommandType, controller } from "@sern/handler";
export function channelType(
channelType: ChannelType[],
onFail?: string
){
return CommandControlPlugin<CommandType.Both>(async (ctx) => {
let channel = ctx.channel?.type;
//for some reason the dm channel type was returning undefined at some points
if (channel === undefined) {
channel = ChannelType.DM;
}
if (channelType.includes(channel)) {
return controller.next();
}
if (onFail) {
await ctx.reply(onFail);
}
return controller.stop();
})
}

View File

@@ -1,106 +0,0 @@
/**
* This is buttonConfirmation plugin, it runs confirmation prompt in the form of buttons.
* Note that you need to use edit/editReply in the command itself because we are already replying in the plugin!
* Credits to original plugin of confirmation using reactions and its author!
*
* @author @EvolutionX-10 [<@697795666373640213>]
* @version 1.0.0
* @example
* ```ts
* import { buttonConfirmation } from "../plugins/buttonConfirmation";
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [ buttonConfirmation() ],
* execute: (ctx) => {
* //your code here
* }
* })
* ```
*/
import {CommandControlPlugin, CommandType, controller} from "@sern/handler";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
} from "discord.js";
export function confirmation(
options?: Partial<ConfirmationOptions>
) {
return CommandControlPlugin<CommandType.Both>(async (ctx, args) => {
options = {
content: "Do you want to proceed?",
denialMessage: "Cancelled",
labels: ["No", "Yes"],
time: 60_000,
wrongUserResponse: "Not for you!",
...options,
};
const buttons = options.labels!.map((l, i) => {
return new ButtonBuilder()
.setCustomId(l)
.setLabel(l)
.setStyle( i === 0 ? ButtonStyle.Danger : ButtonStyle.Success
);
});
const sent = await ctx.reply({
content: options.content,
components: [
new ActionRowBuilder<ButtonBuilder>().setComponents(
buttons
),
],
});
const collector = sent.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: (i) => i.user.id === ctx.user.id,
time: options.time,
});
return new Promise((resolve) => {
collector.on("collect", async (i) => {
await i.update({ components: [] });
collector.stop();
if (i.customId === options!.labels![1]) {
resolve(controller.next());
return;
}
await i.editReply({
content: options?.denialMessage,
});
resolve(controller.stop());
});
collector.on("end", async (c) => {
if (c.size) return;
buttons.forEach((b) => b.setDisabled());
await sent.edit({
components: [
new ActionRowBuilder<ButtonBuilder>().setComponents(
buttons
),
],
});
});
collector.on("ignore", async (i) => {
await i.reply({
content: options?.wrongUserResponse,
ephemeral: true,
});
});
});
});
}
interface ConfirmationOptions {
content: string;
denialMessage: string;
time: number;
labels: [string, string];
wrongUserResponse: string;
}

View File

@@ -1,6 +0,0 @@
import {CommandInitPlugin, controller} from "@sern/handler";
export const correctFile = CommandInitPlugin(() => {
return controller.stop()
})

View File

@@ -1,38 +0,0 @@
// @ts-nocheck
/**
* Disables a command entirely, for whatever reasons you may need.
*
* @author @jacoobes [<@182326315813306368>]
* @version 1.0.0
* @example
* ```ts
* import { disable } from "../plugins/disable";
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [ disable() ],
* execute: (ctx) => {
* //your code here
* }
* })
* ```
*/
import { CommandType, EventPlugin, PluginType } from "@sern/handler";
import { InteractionReplyOptions, ReplyMessageOptions } from "discord.js";
export function disable(
onFail?:
| string
| Omit<InteractionReplyOptions, "fetchReply">
| ReplyMessageOptions
): EventPlugin<CommandType.Both> {
return {
type: PluginType.Event,
description: "Disables command from responding",
async execute([ctx], controller) {
if (onFail !== undefined) {
await ctx.reply(onFail);
}
return controller.stop();
},
};
}

View File

@@ -1,29 +0,0 @@
/**
* This is dmOnly plugin, it allows commands to be run only in DMs.
* For discord.js you should have the Partials.Channel and DirectMessages intent enabled.
* @author @EvolutionX-10 [<@697795666373640213>]
* @version 1.0.0
* @example
* ```ts
* import { dmOnly } from "../plugins/dmOnly";
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [dmOnly()],
* execute: (ctx) => {
* //your code here
* }
* })
* ```
*/
import {CommandControlPlugin, CommandType, controller } from "@sern/handler";
export function dmOnly(
content?: string,
ephemeral?: boolean
) {
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
if (ctx.channel?.isDMBased()) return controller.next();
if (content) await ctx.reply({ content, ephemeral }); // Change this if you want or remove it for silent deny
return controller.stop();
})
}

View File

@@ -1,637 +0,0 @@
import {
CommandControlPlugin,
type CommandType,
type Context,
controller,
} from "@sern/handler";
import {
GuildMember,
GuildMemberRoleManager,
PermissionResolvable,
PermissionsBitField,
User,
} from "discord.js";
export type Test = (context: Context) => boolean;
export class Criteria {
public constructor(
public readonly name: string,
public readonly execute: Test,
public readonly children: Array<Criteria>
) {}
toString() {
return this.name + ' ' + this.children.map(c => c.name).join(', ')
}
}
export const or = (...filters: Array<FilterImpl>): FilterImpl => {
function execute(context: Context): boolean {
let pass = false;
tests: for (const filter of filters) {
if (filter.test(context)) {
pass = true;
break tests;
}
}
return pass;
}
const children: Array<Criteria> = filters.map((x) => x.criteria);
return new FilterImpl(
new Criteria("or", execute, children),
`or(${filters.map((x) => x.message).join(", ")})`
);
}
export const and = (...filters: Array<FilterImpl>): FilterImpl => {
function execute(context: Context): boolean {
for (const filter of filters) {
if (!filter.test(context)) {
return false;
}
}
return true;
}
const children: Array<Criteria> = filters.map((x) => x.criteria);
return new FilterImpl(
new Criteria("and", execute, children),
`and(${filters.map((x) => x.message).join(", ")})`
);
}
export const not = (filter: FilterImpl): FilterImpl => {
function execute(context: Context): boolean {
return !filter.test(context);
}
return new FilterImpl(
new Criteria("not", execute, [filter.criteria]),
`not(${filter.criteria})`
);
}
export const custom =(execute: Test, message?: string): FilterImpl => {
return new FilterImpl(new Criteria("custom", execute, []), message);
}
export const withCustomMessage = (
filter: FilterImpl,
message?: string
): FilterImpl => {
return new FilterImpl(filter.criteria, message);
}
export const hasGuildPermission = (
permission: PermissionResolvable
): FilterImpl => {
const b = PermissionsBitField.resolve(permission);
const field = Object.entries(PermissionsBitField.Flags).find(
([, v]) => v === b
);
if (field === undefined) {
throw new Error(
`unknown permission \`${permission}\` in filter \`hasGuildPermission\``
);
}
const [name] = field;
function execute(context: Context): boolean {
if (context.member !== null) {
if (typeof context.member.permissions === "string") {
return new PermissionsBitField(BigInt(context.member.permissions)).has(b);
}
return context.member.permissions.has(b);
}
return true;
}
return new FilterImpl(
new Criteria("hasGuildPermission", execute, []),
`has guild permission: ${name}`
);
}
export const hasChannelPermission = (
permission: PermissionResolvable,
channelId?: string
): FilterImpl => {
const b = PermissionsBitField.resolve(permission);
const field = Object.entries(PermissionsBitField.Flags).find(
([, v]) => v === b
);
if (field === undefined) {
throw new Error(
`unknown permission \`${permission}\` in filter \`hasChannelPermission\``
);
}
const [name] = field;
function execute(context: Context): boolean {
if (context.member !== null) {
const channel =
channelId !== undefined
? context.guild?.channels.cache.get(channelId)
: context.channel;
// ?
if (channel == undefined || channel === null) {
return false;
}
if (channel.isDMBased()) {
return true;
}
const field2 = channel.permissionsFor(context.user);
// assume we have no permission overrides
if (field2 === null) {
if (context.member !== null) {
if (typeof context.member.permissions === "string") {
return new PermissionsBitField(
BigInt(context.member.permissions)
).has(b);
}
return context.member.permissions.has(b);
}
return false;
}
return field2.has(b);
}
return true;
}
return new FilterImpl(
new Criteria("hasChannelPermission", execute, []),
channelId !== undefined
? `has channel permission ${name} in <#${channelId}>`
: `has channel permission ${name}`
);
}
export const canAddReactions =(channelId?: string): FilterImpl => {
return hasChannelPermission("AddReactions", channelId);
}
export const canAttachFiles =(channelId?: string): FilterImpl => {
return hasChannelPermission("AttachFiles", channelId);
}
export const canBanMembers = (): FilterImpl => {
return hasGuildPermission("BanMembers");
}
export const canChangeNickname = (): FilterImpl => {
return hasGuildPermission("ChangeNickname");
}
export const canConnect = (channelId?: string): FilterImpl => {
return hasChannelPermission("Connect", channelId);
}
export const canCreateInstantInvite =(channelId?: string): FilterImpl => {
return hasChannelPermission("CreateInstantInvite", channelId);
}
export const canDeafenMembers =(channelId?: string): FilterImpl => {
return hasChannelPermission("DeafenMembers", channelId);
}
export const canEmbedLinks =(channelId?: string): FilterImpl => {
return hasChannelPermission("EmbedLinks", channelId);
}
export const canKickMembers =(): FilterImpl => {
return hasGuildPermission("KickMembers");
}
export const canManageChannelWebhooks =(channelId?: string): FilterImpl => {
return hasChannelPermission("ManageWebhooks", channelId);
}
export const canManageChannels =(channelId?: string): FilterImpl => {
return hasChannelPermission("ManageChannels", channelId);
}
export const canManageEmojisAndStickers =(): FilterImpl => {
return hasGuildPermission("ManageEmojisAndStickers");
}
export const canManageGuild =(): FilterImpl => {
return hasGuildPermission("ManageGuild");
}
export const canManageGuildWebhooks =(): FilterImpl => {
return hasGuildPermission("ManageWebhooks");
}
export const canManageMessages =(channelId?: string): FilterImpl => {
return hasChannelPermission("ManageMessages", channelId);
}
export const canManageNicknames = (): FilterImpl => {
return hasGuildPermission("ManageNicknames");
}
export const canManageRoles = (): FilterImpl => {
return hasGuildPermission("ManageRoles");
}
export const canMentionEveryone = (channelId?: string): FilterImpl => {
return hasChannelPermission("MentionEveryone", channelId);
}
export const canMoveMembers = (channelId?: string): FilterImpl => {
return hasChannelPermission("MoveMembers", channelId);
}
export const canMuteMembers = (channelId?: string): FilterImpl => {
return hasChannelPermission("MuteMembers", channelId);
}
export const canPrioritySpeaker = (channelId?: string): FilterImpl => {
return hasChannelPermission("PrioritySpeaker", channelId);
}
export const canReadMessageHistory = (channelId?: string): FilterImpl => {
return hasChannelPermission("ReadMessageHistory", channelId);
}
export const canViewChannel = (channelId: string): FilterImpl => {
return hasChannelPermission("ViewChannel", channelId);
}
export const canSendMessages = (channelId: string): FilterImpl => {
return hasChannelPermission("SendMessages", channelId);
}
export const canSendTtsMessages = (channelId?: string): FilterImpl => {
return hasChannelPermission("SendTTSMessages", channelId);
}
export const canSpeak = (channelId?: string): FilterImpl => {
return hasChannelPermission("Speak", channelId);
}
export const canStream = (channelId?: string): FilterImpl => {
return hasChannelPermission("Stream", channelId);
}
export const canUseExternalEmojis = (channelId?: string): FilterImpl => {
return hasChannelPermission("UseExternalEmojis", channelId);
}
export const canUseVoiceActivity = (channelId?: string): FilterImpl => {
return hasChannelPermission("UseVAD", channelId);
}
export const canViewAuditLog = (): FilterImpl => {
return hasGuildPermission("ViewAuditLog");
}
export const canViewGuildInsights = (): FilterImpl => {
return hasGuildPermission("ViewGuildInsights");
}
export const channelIdIn = (channelIds: Array<string>): FilterImpl => {
function execute(context: Context): boolean {
return channelIds.includes(
context.isMessage()
? context.message.channelId
: context.interaction.channelId
);
}
return new FilterImpl(
new Criteria("channelIdIn", execute, []),
`channel is one of: ${channelIds.map((v) => `<#${v}>`).join(", ")}`
);
}
export const hasEveryRole = (roles: Array<string>): FilterImpl => {
return withCustomMessage(
and(...roles.map((v) => hasRole(v))),
`has all of: ${roles.map((v) => `<@&${v}>`).join(", ")}`
);
}
export const hasMentionableRole = (): FilterImpl => {
function execute(context: Context): boolean {
if (context.member !== null) {
if (context.member.roles instanceof GuildMemberRoleManager) {
return (
context.member.roles.cache.filter((x) => x.mentionable === true)
.size > 0
);
}
if (context.guild === null) {
return false;
}
return context.member.roles
.map((roleId) => context.guild!.roles.cache.get(roleId))
.filter((x) => x !== undefined)
.some((x) => x!.mentionable);
}
return false;
}
return new FilterImpl(
new Criteria("hasMentionableRole", execute, []),
"has a mentionable role"
);
}
export const hasNickname = (nickname?: string): FilterImpl => {
function execute(context: Context): boolean {
if (context.member !== null) {
if (context.member instanceof GuildMember) {
if (nickname !== null) {
return context.member.nickname === nickname;
}
return context.member.nickname !== null;
}
if (nickname !== null) {
return context.member.nick === nickname;
}
return (
context.member.nick !== null && context.member.nick !== undefined
);
}
// dm members can technically have nicknames but they're per-user, so this should never be true.
return false;
}
return new FilterImpl(new Criteria("hasNickname", execute, []), "has a nickname");
}
export const hasParentId = (parentId: string): FilterImpl => {
function execute(context: Context): boolean {
if (context.channel !== null) {
if (context.channel.isDMBased()) {
return false;
}
return context.channel.parentId === parentId;
}
return false;
}
return new FilterImpl(
new Criteria("hasParentId", execute, []),
`has channel parent <#${parentId}>`
);
}
export const hasRole = (roleId: string): FilterImpl => {
function execute(context: Context): boolean {
if (context.member !== null) {
if (context.member.roles instanceof GuildMemberRoleManager) {
return context.member.roles.cache.has(roleId);
}
if (context.guild === null) {
return false;
}
return context.member.roles.includes(roleId);
}
// assume dm members have every role ever.
return true;
}
return new FilterImpl(
new Criteria("hasRole", execute, []),
`has role <@&${roleId}>`
);
}
export const hasSomeRole = (roles: Array<string>): FilterImpl => {
return withCustomMessage(
or(...roles.map((role) => hasRole(role))),
`has any of: ${roles.map((v) => `<@&${v}>`).join(", ")}`
);
}
export const isAdministator = (): FilterImpl => {
return hasGuildPermission("Administrator");
}
export const isChannelId = (channelId: string): FilterImpl => {
function execute(context: Context): boolean {
if (context.isMessage()) {
return context.message.channelId === channelId;
}
return context.interaction.channelId === channelId;
}
return new FilterImpl(
new Criteria("isChannelId", execute, []),
`is channel <#${channelId}>`
);
}
export const isChannelNsfw = (): FilterImpl => {
function execute(context: Context): boolean {
if (context.channel !== null) {
if (context.channel.isDMBased() || context.channel.isThread()) {
return false;
}
return context.channel.nsfw;
}
return false;
}
return new FilterImpl(
new Criteria("isChannelNsfw", execute, []),
"channel marked as nsfw"
);
}
export const isGuildOwner = (): FilterImpl => {
function execute(context: Context): boolean {
if (context.guild !== null) {
return context.guild.ownerId === context.user.id;
}
return true;
}
return new FilterImpl(
new Criteria("isGuildOwner", execute, []),
"is guild owner"
);
}
export const isBotOwner = (): FilterImpl => {
function execute(context: Context): boolean {
if (context.client.application !== null) {
if (context.client.application.owner !== null) {
if (context.client.application.owner instanceof User) {
return context.user.id === context.client.application.owner.id;
}
return context.client.application.owner.members.has(context.user.id);
}
}
// nope
return false;
}
return new FilterImpl(new Criteria("isBotOwner", execute, []), "is bot owner");
}
export const isUserId = (userId: string): FilterImpl => {
function execute(context: Context): boolean {
return context.user.id === userId;
}
return new FilterImpl(
new Criteria("isUserId", execute, []),
`is user: <@${userId}>`
);
}
export const parentIdIn = (parentIds: Array<string>): FilterImpl => {
return withCustomMessage(
or(...parentIds.map((v) => hasParentId(v))),
`channel parent is one of: ${parentIds.map((v) => `<#${v}>`).join(", ")}`
);
}
export const userIdIn = (userIds: Array<string>): FilterImpl => {
return withCustomMessage(
or(...userIds.map((v) => isUserId(v))),
`user is one of: ${userIds.map((v) => `<@${v}>`).join(", ")}`
);
}
export const isInGuild = (): FilterImpl => {
function execute(context: Context): boolean {
return context.guildId !== null;
}
return new FilterImpl(new Criteria("isInGuild", execute, []), "is in guild");
}
export const isInDm = (): FilterImpl => {
const notInGuild = compose(not, isInGuild);
return withCustomMessage(notInGuild(), "is in dm");
}
export const never = (): FilterImpl => {
function execute(context: Context): boolean {
void context;
return false;
}
return new FilterImpl(new Criteria("never", execute, []), "never");
}
export const always = (): FilterImpl => {
function execute(context: Context): boolean {
void context;
return true;
}
return new FilterImpl(new Criteria("always", execute, []), "always");
}
type CtxMap<T> = (arg: T) => FilterImpl;
/**
* Call FilterImpls in right to left order.
* @example
* import { compose, isUserId, not } from '../plugins/filter'
* const isNotUserId = compose(not, isUserId)
*
*/
export const compose = <T = void>(...funcs: CtxMap<any>[]): CtxMap<T> => {
return (arg: T): FilterImpl =>
//@ts-ignore
funcs.reduceRight((result, func) => func(result), arg);
}
export class FilterImpl {
public readonly test: Test;
public constructor(
public readonly criteria: Criteria,
public message?: string
) {
this.test = this.criteria.execute;
}
}
export type FilterOptions = {
condition: Array<FilterImpl> | FilterImpl,
onFailed?: (context: Context, filters: Array<FilterImpl>) => unknown
};
/**
* Generalized `filter` plugin. revised by jacoobes, all credit to original author.
* Perform declarative conditionals as plugins.
* @author @trueharuu [<@504698587221852172>]
* @version 2.0.0
* @example
* import { filter, not, isGuildOwner, canMentionEveryone } from '../plugins/filter';
* import { commandModule } from '@sern/handler';
*
* export default commandModule({
* plugins: filter({ condition: [not(isGuildOwner()), canMentionEveryone()] }),
* async execute(context) {
* // your code here
* }
* });
*/
export const filter =
(options: FilterOptions) => {
return CommandControlPlugin<CommandType.Both>(async (context) => {
const arrayifiedCondition = Array.isArray(options.condition) ? options.condition : [options.condition]
const value = and(...arrayifiedCondition).test(context);
if (value) {
return controller.next();
}
if (options.onFailed !== undefined) {
await options.onFailed(context, arrayifiedCondition);
} else {
await context.reply({
ephemeral: true,
content: `you do not match the criteria for this command:\n${arrayifiedCondition
.map((x) => x.message)
.filter((x) => x !== undefined)
.join("\n")}`,
allowedMentions: {
repliedUser: false,
parse: [],
},
});
}
return controller.stop();
});
};

View File

@@ -1,40 +0,0 @@
import { PluginType, makePlugin, controller, ControlPlugin } from "@sern/handler";
import type { AutocompleteInteraction } from 'discord.js'
/**
* @plugin
* filters autocomplete interaction that pass the criteria
* @author @jacoobes [<@182326315813306368>]
* @version 1.0.0
* @example
* ```ts
* import { CommandType, commandModule } from "@sern/handler";
* import { filterA } from '../plugins/filterA.js'
* export default commandModule({
* type : CommandType.Slash,
* options: [
* {
* autocomplete: true,
* command : {
* //only accept autocomplete interactions that include 'poo' in the text
* onEvent: [filterA(s => s.includes('poo'))],
* execute: (autocomplete) => {
* let data = [{ name: 'pooba', value: 'first' }, { name: 'pooga', value: 'second' }]
* autocomplete.respond(data)
* }
* }
* }
* ],
* execute: (ctx, args) => {}
* })
* @end
*/
export const filterA = (pred: (value: string) => boolean) => {
return makePlugin(PluginType.Control, (a: AutocompleteInteraction) => {
if(pred(a.options.getFocused())) {
return controller.next();
}
return controller.stop();
}) as ControlPlugin;
}

View File

@@ -1,36 +0,0 @@
//@ts-nocheck
/**
* @plugin
* fromCallback turns a callback into a plugin result.
* if the callback returns truthy value, plugin continues.
* This control plugin works for every command type. The arguments of the callback
* mirror the execute method on the current module.
* @author @jacoobes [<@182326315813306368>]
* @version 1.0.0
* @example
* ```ts
* const myServer = "941002690211766332";
* export default commandModule({
* type: CommandType.Both,
* plugins: [
* fromCallback((ctx, args) => ctx.guildId == myServer)
* ],
* execute: ctx => {
* ctx.reply("I only respond in myServer!");
* }
* })
* ```
* @end
*/
import { PluginType, makePlugin, controller } from "@sern/handler";
export const fromCallback = (cb: (...args: any[]) => boolean) =>
makePlugin(PluginType.Control, (...args) => {
console.log(args)
if(cb.apply(null, args)) {
return controller.next();
}
return controller.stop();
});

View File

@@ -1,5 +0,0 @@
import { CommandControlPlugin, CommandType, controller } from "@sern/handler";
export const json = CommandControlPlugin<CommandType.Button>((ctx, args) => {
return controller.next({ 'json/data': JSON.parse(args.params!) });
})

View File

@@ -1,48 +0,0 @@
/**
* This plugin checks if the channel is nsfw and responds to user with a specified response if not nsfw
*
* @author @Benzo-Fury [<@762918086349029386>]
* @version 1.0.0
* @example
* ```ts
* import { nsfwOnly } from "../plugins/nsfwOnly";
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [ nsfwOnly('response', true) ],
* execute: (ctx) => {
* //your code here
* }
* })
* ```
*/
import {
ChannelType,
GuildTextBasedChannel,
TextBasedChannel,
TextChannel,
} from "discord.js";
import {CommandControlPlugin, CommandType, controller } from "@sern/handler";
function isGuildText(channel: TextBasedChannel|null): channel is GuildTextBasedChannel {
return (channel?.type == ChannelType.GuildPublicThread ||
channel?.type == ChannelType.GuildPrivateThread);
}
export function nsfwOnly(onFail: string, ephemeral: boolean) {
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
if (ctx.guild === null) {
await ctx.reply({ content: onFail, ephemeral });
return controller.stop();
}
//channel is thread (not supported by nsfw)
if (isGuildText(ctx.channel) == true) {
await ctx.reply({ content: onFail, ephemeral });
return controller.stop();
}
if (!(ctx.channel! as TextChannel).nsfw) {
//channel is not nsfw
await ctx.reply({ content: onFail, ephemeral });
return controller.stop();
}
//continues to command if nsfw
return controller.next();
});
}

View File

@@ -1,30 +0,0 @@
// @ts-nocheck
/**
* This is OwnerOnly plugin, it allows only bot owners to run the command, like eval.
*
* @author @EvolutionX-10 [<@697795666373640213>]
* @version 1.2.0
* @example
* ```ts
* import { ownerOnly } from "../plugins/ownerOnly";
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [ ownerOnly() ], // can also pass array of IDs to override default owner IDs
* execute: (ctx) => {
* //your code here
* }
* })
* ```
*/
import { CommandType, CommandControlPlugin, controller } from "@sern/handler";
const ownerIDs = ["182326315813306368"]; //! Fill your ID
export function ownerOnly(override?: string[]) {
return CommandControlPlugin<CommandType.Both>((ctx) => {
if ((override ?? ownerIDs).includes(ctx.user.id))
return controller.next();
//* If you want to reply when the command fails due to user not being owner, you can use following
// await ctx.reply("Only owner can run it!!!");
return controller.stop(); //! Important: It stops the execution of command!
});
}

View File

@@ -1,39 +0,0 @@
// @ts-nocheck
/**
* @plugin
* This is perm check, it allows users to parse the permission you want and let the plugin do the rest. (check user for that perm).
*
* @author @Benzo-Fury [<@762918086349029386>]
* @version 1.0.1
* @example
* ```ts
* import { permCheck } from "../plugins/permCheck";
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [ permCheck('permission', 'No permission response') ],
* execute: (ctx) => {
* //your code here
* }
* })
* ```
* @end
*/
import type { GuildMember, PermissionResolvable } from "discord.js";
import { CommandControlPlugin, CommandType, controller } from "@sern/handler";
export function permCheck(perm: PermissionResolvable, response: string) {
return CommandControlPlugin<CommandType.Both>(async (ctx, args) => {
if (ctx.guild === null) {
await ctx.reply("This command cannot be used here");
console.warn(
"PermCheck > A command stopped because we couldn't check a users permissions (was used in dms)",
); //delete this line if you dont want to be notified when a command is used outside of a guild/server
return controller.stop();
}
if (!(ctx.member! as GuildMember).permissions.has(perm)) {
await ctx.reply(response);
return controller.stop();
}
return controller.next();
});
}

View File

@@ -1,215 +0,0 @@
// @ts-nocheck
/**
* @plugin
* [DEPRECATED] It allows you to publish your application commands using the discord.js library with ease.
*
* @author @EvolutionX-10 [<@697795666373640213>]
* @version 2.0.0
* @example
* ```ts
* import { publish } from "../plugins/publish";
* import { commandModule } from "@sern/handler";
* export default commandModule({
* plugins: [ publish() ], // put an object containing permissions, ids for guild commands, boolean for dmPermission
* // plugins: [ publish({ guildIds: ['guildId'], defaultMemberPermissions: 'Administrator'})]
* execute: (ctx) => {
* //your code here
* }
* })
* ```
* @end
*/
import {
CommandInitPlugin,
CommandType,
controller,
SernOptionsData,
SlashCommand,
Service,
} from "@sern/handler";
import {
ApplicationCommandData,
ApplicationCommandType,
ApplicationCommandOptionType,
PermissionResolvable,
} from "discord.js";
export const CommandTypeRaw = {
[CommandType.Both]: ApplicationCommandType.ChatInput,
[CommandType.CtxUser]: ApplicationCommandType.User,
[CommandType.CtxMsg]: ApplicationCommandType.Message,
[CommandType.Slash]: ApplicationCommandType.ChatInput,
} as const;
export function publish<
T extends
| CommandType.Both
| CommandType.Slash
| CommandType.CtxMsg
| CommandType.CtxUser,
>(options?: PublishOptions) {
return CommandInitPlugin<T>(async ({ module }) => {
// Users need to provide their own useContainer function.
let client;
try {
client = (await import("@sern/handler")).Service("@sern/client");
} catch {
const { useContainer } = await import("../index.js");
client = useContainer("@sern/client")[0];
}
const defaultOptions = {
guildIds: [],
dmPermission: undefined,
defaultMemberPermissions: null,
};
options = { ...defaultOptions, ...options } as PublishOptions &
ValidPublishOptions;
let { defaultMemberPermissions, dmPermission, guildIds } =
options as unknown as ValidPublishOptions;
function c(e: unknown) {
console.error("publish command didnt work for", module.name);
console.error(e);
}
const log =
(...message: any[]) =>
() =>
console.log(...message);
const logged = (...message: any[]) => log(message);
/**
* a local function that returns either one value or the other,
* depending on {t}'s CommandType. If the commandtype of
* this module is CommandType.Both or CommandType.Text or CommandType.Slash,
* return 'is', else return 'els'
* @param t
* @returns S | T
*/
const appCmd = <V extends CommandType, S, T>(t: V) => {
return (is: S, els: T) => ((t & CommandType.Both) !== 0 ? is : els);
};
const curAppType = CommandTypeRaw[module.type];
const createCommandData = () => {
const cmd = appCmd(module.type);
return {
name: module.name,
type: curAppType,
description: cmd(module.description, ""),
options: cmd(
optionsTransformer((module as SlashCommand).options ?? []),
[],
),
defaultMemberPermissions,
dmPermission,
} as ApplicationCommandData;
};
try {
const commandData = createCommandData();
if (!guildIds.length) {
const cmd = (await client.application!.commands.fetch()).find(
(c) => c.name === module.name && c.type === curAppType,
);
if (cmd) {
if (!cmd.equals(commandData, true)) {
logged(
`Found differences in global command ${module.name}`,
);
cmd.edit(commandData).then(
log(
`${module.name} updated with new data successfully!`,
),
);
}
return controller.next();
}
client
.application!.commands.create(commandData)
.then(log("Command created", module.name))
.catch(c);
return controller.next();
}
for (const id of guildIds) {
const guild = await client.guilds.fetch(id).catch(c);
if (!guild) continue;
const guildCmd = (await guild.commands.fetch()).find(
(c) => c.name === module.name && c.type === curAppType,
);
if (guildCmd) {
if (!guildCmd.equals(commandData, true)) {
logged(`Found differences in command ${module.name}`);
guildCmd
.edit(commandData)
.then(
log(
`${module.name} updated with new data successfully!`,
),
)
.catch(c);
continue;
}
continue;
}
guild.commands
.create(commandData)
.then(log("Guild Command created", module.name, guild.name))
.catch(c);
}
return controller.next();
} catch (e) {
logged("Command did not register" + module.name);
logged(e);
return controller.stop();
}
});
}
export function optionsTransformer(ops: Array<SernOptionsData>) {
return ops.map((el) => {
switch (el.type) {
case ApplicationCommandOptionType.String:
case ApplicationCommandOptionType.Number:
case ApplicationCommandOptionType.Integer: {
return el.autocomplete && "command" in el
? (({ command, ...el }) => el)(el)
: el;
}
default:
return el;
}
});
}
export type NonEmptyArray<T extends `${number}` = `${number}`> = [T, ...T[]];
export interface ValidPublishOptions {
guildIds: string[];
dmPermission: boolean;
defaultMemberPermissions: PermissionResolvable;
}
interface GuildPublishOptions {
guildIds?: NonEmptyArray;
defaultMemberPermissions?: PermissionResolvable;
dmPermission?: never;
}
interface GlobalPublishOptions {
defaultMemberPermissions?: PermissionResolvable;
dmPermission?: false;
guildIds?: never;
}
type BasePublishOptions = GuildPublishOptions | GlobalPublishOptions;
export type PublishOptions = BasePublishOptions &
(
| Required<Pick<BasePublishOptions, "defaultMemberPermissions">>
| (
| Required<Pick<BasePublishOptions, "dmPermission">>
| Required<Pick<BasePublishOptions, "guildIds">>
)
);

View File

@@ -1,95 +0,0 @@
/**
* This is perm check, it allows users to parse the permission you want and let the plugin do the rest. (check bot or user for that perm).
*
* @author @Benzo-Fury [<@762918086349029386>]
* @author @needhamgary [<@342314924804014081>]
* @version 1.2.0
* @example
* ```ts
* import { requirePermission } from "../plugins/myPermCheck";
* import { commandModule, CommandType } from "@sern/handler";
* export default commandModule({
* plugins: [ requirePermission<CommandType>('target', 'permission', 'No response (optional)') ],
* execute: (ctx) => {
* //your code here
* }
* })
* ```
*/
import type { GuildMember, PermissionResolvable } from "discord.js";
import {
CommandType, CommandControlPlugin, controller,
} from "@sern/handler";
function payload(resp?: string) {
return {
fetchReply: true,
content: resp,
allowedMentions: { repliedUser: false },
} as const;
}
export function requirePermission(
target: "user" | "bot" | "both",
perm: PermissionResolvable[],
response?: string
) {
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
if (ctx.guild === null) {
ctx.reply(payload("This command cannot be used here"));
console.warn(
"PermCheck > A command stopped because we couldn't check a users permissions (was used in dms)"
); //delete this line if you dont want to be notified when a command is used outside of a guild/server
return controller.stop();
}
const bot = (await ctx.guild.members.fetchMe({
cache: false,
})!) as GuildMember; const memm = ctx.member! as GuildMember;
switch (target) {
//*********************************************************************************************************************//
case "bot":
if (!bot.permissions.has(perm)) {
if (!response)
response = `I cannot use this command, please give me \`${perm.join(
", "
)}\` permission(s).`;
await ctx.reply(payload(response));
return controller.stop();
}
return controller.next();
//*********************************************************************************************************************//
case "user":
if (!memm.permissions.has(perm)) {
if (!response)
response = `You cannot use this command because you are missing \`${perm.join(
", "
)}\` permission(s).`;
await ctx.reply(payload(response));
return controller.stop();
}
return controller.next();
//*********************************************************************************************************************//
case "both":
if (
!bot.permissions.has(perm) ||
!memm.permissions.has(perm)
) {
if (!response)
response = `Please ensure <@${bot.user.id}> and <@${
memm.user.id
}> both have \`${perm.join(", ")}\` permission(s).`;
await ctx.reply(payload(response));
return controller.stop();
}
return controller.next();
//*********************************************************************************************************************//
default:
console.warn(
"Perm Check >>> You didn't specify user or bot."
);
ctx.reply(payload("User or Bot was not specified."));
return controller.stop();
}
});
}

View File

@@ -1,38 +0,0 @@
/**
* Checks if a command is available in a specific server.
*
* @author @Peter-MJ-Parker [<@1017182455926624316>]
* @version 1.0.0
* @example
* ```ts
* import { commandModule, CommandType } from "@sern/handler";
* import { serverOnly } from "../plugins/serverOnly";
* export default commandModule({
* type: CommandType.Both,
* plugins: [serverOnly(["guildId"], failMessage)], // fail message is the message you will see when the command is ran in the wrong server.
* description: "command description",
* execute: async (ctx, args) => {
* // your code here
* },
* });
* ```
*/
import { CommandType, controller, CommandControlPlugin } from "@sern/handler";
export function serverOnly(
guildId: string[],
failMessage = "This command is not available in this guild. \nFor permission to use in your server, please contact my developer."
) {
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
if (!guildId.includes(ctx.guildId!)) {
ctx.reply(failMessage).then(async (m) => {
setTimeout(async () => {
await m.delete();
}, 3000);
});
return controller.stop();
}
return controller.next();
})
}

View File

@@ -1,34 +0,0 @@
import { Presence } from '@sern/handler'
import { ActivityType, ClientPresenceStatus } from 'discord.js';
function shuffleArray<T>(array: T[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return [...array];
}
const statuses = [[ActivityType.Watching, "the sern community", "online"],
[ActivityType.Listening, "Evo", "dnd"],
[ActivityType.Playing, "with @sern/cli", "idle"],
[ActivityType.Watching, "sern bots", "dnd"],
[ActivityType.Watching, "github stars go brrr", "online"],
[ActivityType.Listening, "Spotify", "dnd"],
[ActivityType.Listening, "what's bofa", "idle"]] satisfies
[ActivityType, string, ClientPresenceStatus][];
export default Presence.module({
execute: () => {
const [type, name, status] = statuses.at(0)!;
return Presence
.of({ activities: [ { type, name } ], status }) //start your presence with this.
.repeated(() => {
const [type, name, status] = [...shuffleArray(statuses)].shift()!;
return {
status,
activities: [{ type, name }]
};
}, 60_000); //repeat and setPresence with returned result every minute
}
})

View File

@@ -1,9 +0,0 @@
import { scheduledTask } from "@sern/handler";
export default scheduledTask({
trigger: "* * * * *",
execute: (args, { deps }) => {
console.log("hello")
}
})

View File

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

1484
dependency-graph.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -1 +0,0 @@

3614
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,28 @@
{
"name": "@sern/handler",
"packageManager": "yarn@3.5.0",
"version": "4.2.4",
"version": "3.0.2",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "./dist/index.js",
"module": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"watch": "tsc --watch",
"watch": "tsup --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"build:dev": "tsc",
"build:prod": "tsc",
"prepare": "tsc",
"build:dev": "tsup --metafile",
"build:prod": "tsup --minify",
"prepare": "npm run build:prod",
"pretty": "prettier --write .",
"tdd": "vitest",
"benchmark": "vitest bench",
"test": "vitest --run",
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
},
@@ -36,21 +38,31 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"@sern/ioc": "^1.1.2",
"callsites": "^3.1.0",
"cron": "^3.1.7",
"deepmerge": "^4.3.1"
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-results-es": "^3.6.1"
},
"devDependencies": {
"@faker-js/faker": "^8.0.1",
"@types/node": "^20.0.0",
"@types/node-cron": "^3.0.11",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"discord.js": "^14.14.1",
"dependency-cruiser": "^13.0.5",
"discord.js": "14.11.0",
"esbuild": "^0.17.0",
"eslint": "8.39.0",
"prettier": "2.8.8",
"tsup": "^6.7.0",
"typescript": "5.0.2",
"vitest": "^1.6.0"
"vitest": "latest"
},
"prettier": {
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"arrowParens": "avoid"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
@@ -84,14 +96,5 @@
"type": "git",
"url": "git+https://github.com/sern-handler/handler.git"
},
"engines": {
"node": ">= 20.0.x"
},
"homepage": "https://sern.dev",
"overrides": {
"ws": "8.17.1"
},
"resolutions": {
"ws": "8.17.1"
}
"homepage": "https://sern.dev"
}

View File

@@ -1,9 +1,5 @@
{
"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,111 +0,0 @@
// It's this package but without default console log / error https://github.com/trevorr/async-cleanup
/** A possibly asynchronous function invoked with the process is about to exit. */
export type CleanupListener = () => void | Promise<void>;
let cleanupListeners: Set<CleanupListener> | undefined;
/** Registers a new cleanup listener. Adding the same listener more than once has no effect. */
export function addCleanupListener(listener: CleanupListener): void {
// Install exit listeners on initial cleanup listener
if (!cleanupListeners) {
installExitListeners();
cleanupListeners = new Set();
}
cleanupListeners.add(listener);
}
/** Removes an existing cleanup listener, and returns whether the listener was registered. */
export function removeCleanupListener(listener: CleanupListener): boolean {
return cleanupListeners != null && cleanupListeners.delete(listener);
}
/** Executes all cleanup listeners and then exits the process. Call this instead of `process.exit` to ensure all listeners are fully executed. */
export async function exitAfterCleanup(code = 0): Promise<never> {
await executeCleanupListeners();
process.exit(code);
}
/** Executes all cleanup listeners and then kills the process with the given signal. */
export async function killAfterCleanup(signal: ExitSignal): Promise<void> {
await executeCleanupListeners();
process.kill(process.pid, signal);
}
async function executeCleanupListeners(): Promise<void> {
if (cleanupListeners) {
// Remove exit listeners to restore normal event handling
uninstallExitListeners();
// Clear cleanup listeners to reset state for testing
const listeners = cleanupListeners;
cleanupListeners = undefined;
// Call listeners in order added with async listeners running concurrently
const promises: Promise<void>[] = [];
for (const listener of listeners) {
try {
const promise = listener();
if (promise) promises.push(promise);
} catch (err) {
// console.error("Uncaught exception during cleanup", err);
}
}
// Wait for all listeners to complete and log any rejections
const results = await Promise.allSettled(promises);
for (const result of results) {
if (result.status === "rejected") {
console.error("Unhandled rejection during cleanup", result.reason);
}
}
}
}
function beforeExitListener(code: number): void {
// console.log(`Exiting with code ${code} due to empty event loop`);
void exitAfterCleanup(code);
}
function uncaughtExceptionListener(error: Error): void {
// console.error("Exiting with code 1 due to uncaught exception", error);
void exitAfterCleanup(1);
}
function signalListener(signal: ExitSignal): void {
// console.log(`Exiting due to signal ${signal}`);
void killAfterCleanup(signal);
}
// Listenable signals that terminate the process by default
// (except SIGQUIT, which generates a core dump and should not trigger cleanup)
// See https://nodejs.org/api/process.html#signal-events
const listenedSignals = [
"SIGBREAK", // Ctrl-Break on Windows
"SIGHUP", // Parent terminal closed
"SIGINT", // Terminal interrupt, usually by Ctrl-C
"SIGTERM", // Graceful termination
"SIGUSR2", // Used by Nodemon
] as const;
/** Signals that can terminate the process. */
export type ExitSignal =
| typeof listenedSignals[number]
| "SIGKILL"
| "SIGQUIT"
| "SIGSTOP";
function installExitListeners(): void {
process.on("beforeExit", beforeExitListener);
process.on("uncaughtException", uncaughtExceptionListener);
listenedSignals.forEach((signal) => process.on(signal, signalListener));
}
function uninstallExitListeners(): void {
process.removeListener("beforeExit", beforeExitListener);
process.removeListener("uncaughtException", uncaughtExceptionListener);
listenedSignals.forEach((signal) =>
process.removeListener(signal, signalListener)
);
}

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

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

View File

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

View File

@@ -0,0 +1,21 @@
/**
* @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.
* @param error
*/
updateAlive(error: Error): void;
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
/**
* @since 2.0.0
*/
export interface Logging<T = unknown> {
error(payload: LogPayload<T>): void;
warning(payload: LogPayload<T>): void;
info(payload: LogPayload<T>): void;
debug(payload: LogPayload<T>): void;
}
export type LogPayload<T = unknown> = { message: T };

View File

@@ -0,0 +1,22 @@
import type {
CommandMeta,
CommandModule,
CommandModuleDefs,
Module,
} from '../../types/core-modules';
import { CommandType } from '../structures';
/**
* @since 2.0.0
*/
export interface ModuleManager {
get(id: string): string | undefined;
getMetadata(m: Module): CommandMeta | undefined;
setMetadata(m: Module, c: CommandMeta): void;
set(id: string, path: string): void;
getPublishableCommands(): Promise<CommandModule[]>;
getByNameCommandType<T extends CommandType>(
name: string,
commandType: T,
): Promise<CommandModuleDefs[T]> | undefined;
}

View File

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

View File

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

View File

@@ -1,124 +1,83 @@
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 type { Payload, UnpackedDependencies } from '../types/utility';
import path from 'node:path'
import { Err, Ok } from 'ts-results-es';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../types/core-modules';
import type { AnyCommandPlugin, AnyEventPlugin, Plugin } from '../types/core-plugin';
import { PluginType } from './structures';
import assert from 'assert';
export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
return {
state: {},
deps,
params,
type: module.type,
module: {
name: module.name,
description: module.description,
locals: module.locals,
meta: module.meta
//function wrappers for empty ok / err
export const ok = /* @__PURE__*/ () => Ok.EMPTY;
export const err = /* @__PURE__*/ () => Err.EMPTY;
export function partitionPlugins(
arr: (AnyEventPlugin | AnyCommandPlugin)[] = [],
): [Plugin[], Plugin[]] {
const controlPlugins = [];
const initPlugins = [];
for (const el of arr) {
switch (el.type) {
case PluginType.Control:
controlPlugins.push(el);
break;
case PluginType.Init:
initPlugins.push(el);
break;
}
}
return [controlPlugins, initPlugins];
}
/**
* 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' ]
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
* @param iAutocomplete
* @param options
*/
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;
export function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
let subcommands = new Set();
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand:
{
subcommands.add(cur.name);
for (const option of cur.options ?? []) _options.push(option);
}
break;
case ApplicationCommandOptionType.SubcommandGroup:
{
for (const command of cur.options ?? []) _options.push(command);
}
break;
default:
{
if ('autocomplete' in cur && cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
assert(
'command' in cur,
'No command property found for autocomplete option',
);
if (subcommands.size > 0) {
const parent = iAutocomplete.options.getSubcommand();
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return cur;
}
} else {
if (cur.name === choice.name) {
return cur;
}
}
}
}
break;
}
}
return [controlPlugins, initPlugins] as [T[], V[]];
}
export const createLookupTable = (options: SernOptionsData[]): Map<string, SernAutocompleteData> => {
const table = new Map<string, SernAutocompleteData>();
_createLookupTable(table, options, "<parent>");
return table;
}
const _createLookupTable = (table: Map<string, SernAutocompleteData>, options: SernOptionsData[], parent: string) => {
for (const opt of options) {
const name = path.posix.join(parent, opt.name)
switch(opt.type) {
case ApplicationCommandOptionType.Subcommand: {
_createLookupTable(table, opt.options ?? [], name);
} break;
case ApplicationCommandOptionType.SubcommandGroup: {
_createLookupTable(table, opt.options ?? [], name);
} break;
default: {
if(Reflect.get(opt, 'autocomplete') === true) {
table.set(name, opt as SernAutocompleteData)
}
} 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 isContextCommand(i: AnyCommandInteraction): i is MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction {
return i.isContextMenuCommand();
}
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;
}

View File

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

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

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

View File

@@ -1,55 +0,0 @@
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 };

View File

@@ -1,163 +0,0 @@
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({ captureRejections: true }))
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
}

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

@@ -0,0 +1,34 @@
import * as assert from 'assert';
import { composeRoot, useContainer } from './dependency-injection';
import type { DependencyConfiguration } from '../../types/ioc';
import { CoreContainer } from './container';
//SIDE EFFECT: GLOBAL DI
let containerSubject: CoreContainer<Partial<Dependencies>>;
/**
* Returns the underlying data structure holding all dependencies.
* Exposes methods from iti
*/
export function useContainerRaw() {
assert.ok(
containerSubject && containerSubject.isReady(),
"Could not find container or container wasn't ready. Did you call makeDependencies?",
);
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
containerSubject = new CoreContainer();
await composeRoot(containerSubject, conf);
return useContainer<T>();
}

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

@@ -0,0 +1,67 @@
import { Container } from 'iti';
import { SernEmitter } from '../';
import { isAsyncFunction } from 'node:util/types';
import * as assert from 'node:assert';
import { Subject } from 'rxjs';
import { DefaultServices, ModuleStore } from '../_internal';
/**
* Provides all the defaults for sern to function properly.
* The only user provided dependency needs to be @sern/client
*/
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
private ready$ = new Subject<never>();
private beenCalled = new Set<PropertyKey>();
constructor() {
super();
this.listenForInsertions();
(this as Container<{}, {}>)
.add({
'@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter(),
'@sern/store': () => new ModuleStore(),
})
.add(ctx => {
return {
'@sern/modules': () =>
new DefaultServices.DefaultModuleManager(ctx['@sern/store']),
};
});
}
private listenForInsertions() {
assert.ok(
!this.isReady(),
'listening for init functions should only occur prior to sern being ready.',
);
const unsubscriber = this.on('containerUpserted', e => this.callInitHooks(e));
this.ready$.subscribe({
complete: unsubscriber,
});
}
private async callInitHooks(e: { key: keyof T; newContainer: T[keyof T] | null }) {
const dep = e.newContainer;
assert.ok(dep);
//Ignore any dependencies that are not objects or array
if (typeof dep !== 'object' || Array.isArray(dep)) {
return;
}
if ('init' in dep && typeof dep.init === 'function' && !this.beenCalled.has(e.key)) {
isAsyncFunction(dep.init) ? await dep.init() : dep.init();
this.beenCalled.add(e.key);
}
}
isReady() {
return this.ready$.closed;
}
ready() {
this.ready$.unsubscribe();
}
}

View File

@@ -0,0 +1,85 @@
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
import { SernError, DefaultServices } from '../_internal';
import { useContainerRaw } from './base';
import { CoreContainer } from './container';
/**
* @__PURE__
* @since 2.0.0.
* Creates a singleton object.
* @param cb
*/
export function single<T>(cb: () => T) {
return cb;
}
/**
* @__PURE__
* @since 2.0.0
* Creates a transient object
* @param cb
*/
export function transient<T>(cb: () => () => T) {
return cb;
}
/**
* The new Service api, a cleaner alternative to useContainer
* To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
* Usually our scaffolding tool takes care of this.
* @since 3.0.0
* @example
* ```ts
* const client = Service('@sern/client');
* ```
* @param key a key that corresponds to a dependency registered.
*
*/
export function Service<const T extends keyof Dependencies>(key: T) {
return useContainerRaw().get(key)!;
}
/**
* @since 3.0.0
* The plural version of {@link Service}
* @returns array of dependencies, in the same order of keys provided
*/
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
const container = useContainerRaw();
return keys.map(k => container.get(k)!) as IntoDependencies<T>;
}
/**
* Given the user's conf, check for any excluded dependency keys.
* Then, call conf.build to get the rest of the users' dependencies.
* Finally, update the containerSubject with the new container state
* @param conf
*/
export async 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(),
});
}
//Build the container based on the callback provided by the user
conf.build(container as CoreContainer<Omit<CoreDependencies, '@sern/client'>>);
try {
container.get('@sern/client');
} catch {
throw new Error(SernError.MissingRequired + ' No client was provided');
}
if (!hasLogger) {
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
container.ready();
}
export function useContainer<const T extends Dependencies>() {
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => useContainerRaw().get(key as keyof Dependencies)) as IntoDependencies<V>;
}

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

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

View File

@@ -1,26 +1,13 @@
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 { Result } from 'ts-results-es';
import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
import { readdir, stat } from 'fs/promises';
import { basename, extname, join, resolve, parse } from 'path';
import assert from 'assert';
import { createRequire } from 'node:module';
import type { ImportPayload, Wrapper } from '../types/core';
import type { Module } from '../types/core-modules';
export type ModuleResult<T> = Promise<ImportPayload<T>>;
/**
* Import any module based on the absolute path.
@@ -28,6 +15,7 @@ export const shouldHandle = (pth: string, filenam: string) => {
* commonjs, javascript :
* ```js
* exports = commandModule({ })
*
* //or
* exports.default = commandModule({ })
* ```
@@ -35,36 +23,113 @@ export const shouldHandle = (pth: string, filenam: string) => {
* export default commandModule({})
*/
export async function importModule<T>(absPath: string) {
let fileModule = await import(absPath);
let module = await import(absPath).then(esm => esm.default);
let commandModule: Module = fileModule.default;
assert(commandModule , `No default export @ ${absPath}`);
if ('default' in commandModule) {
commandModule = commandModule.default as Module;
assert(module, `Found no export for module at ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in module) {
module = module.default;
}
const p = path.parse(absPath)
commandModule.name ??= p.name; commandModule.description ??= "...";
commandModule.meta = {
id: Id.create(commandModule.name, commandModule.type),
absPath,
return Result
.wrap(() => module.getInstance())
.unwrapOr(module) as T;
}
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
let module = await importModule<T>(absPath);
assert(module, `Found an undefined module: ${absPath}`);
return { module, absPath };
}
export const fmtFileName = (fileName: string) => parse(fileName).name;
/**
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildModuleStream<T extends Module>(
input: ObservableInput<string>,
): Observable<ImportPayload<T>> {
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
}
export const getFullPathTree = (dir: string, mode: boolean) => readPaths(resolve(dir), mode);
export const filename = (path: string) => fmtFileName(basename(path));
const isSkippable = (filename: string) => {
//empty string is for non extension files (directories)
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
};
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file),
};
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);
async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
if (fileStats.isDirectory()) {
//Todo: refactor so that i dont repeat myself for files (line 71)
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
} else {
yield* readPaths(fullPath, shouldDebug);
}
} else {
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored: ${fullPath}`);
} else {
yield 'file:///' + fullPath;
}
}
} else if (!file.name.startsWith('!')) {
yield "file:///"+path.resolve(fullPath);
}
} catch (err) {
throw err;
}
}
const requir = createRequire(import.meta.url);
export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
if (wrapper === 'file') {
console.log('Experimental loading of sern.config.json');
const config = requir(resolve('sern.config.json')) as {
language: string;
defaultPrefix?: string;
mode?: 'PROD' | 'DEV';
paths: {
base: string;
commands: string;
events?: string;
};
};
const makePath = (dir: keyof typeof config.paths) =>
config.language === 'typescript'
? join('dist', config.paths[dir]!)
: join(config.paths[dir]!);
console.log('Loading config: ', config);
const commandsPath = makePath('commands');
console.log('Commands path is set to', commandsPath);
let eventsPath: string | undefined;
if (config.paths.events) {
eventsPath = makePath('events');
console.log('Events path is set to', eventsPath);
}
return {
defaultPrefix: config.defaultPrefix,
commands: commandsPath,
events: eventsPath,
mode: config.mode,
};
}
return wrapper;
}

View File

@@ -1,135 +1,112 @@
import type { ClientEvents } from 'discord.js';
import { EventType } from '../core/structures/enums';
import { ClientEvents } from 'discord.js';
import { CommandType, EventType, PluginType } from '../core/structures';
import type {
AnyCommandPlugin,
AnyEventPlugin,
CommandArgs,
ControlPlugin,
EventArgs,
InitPlugin,
} from '../types/core-plugin';
import type {
CommandModule,
EventModule,
InputCommand,
InputEvent,
Module,
ScheduledTask,
} from '../types/core-modules';
import { partitionPlugins } from './functions'
import { partitionPlugins } from './_internal';
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)]
* });
* }
* });
* @since 1.0.0 The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommand): Module {
export function commandModule(mod: InputCommand): CommandModule {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
return { ...mod,
onEvent,
plugins,
locals: {} } as Module;
return {
...mod,
onEvent,
plugins,
} as CommandModule;
}
/**
* 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
* }
* });
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule<T extends keyof ClientEvents = keyof ClientEvents>(mod: InputEvent<T>): Module {
export function eventModule(mod: InputEvent): EventModule {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
if(onEvent.length !== 0) throw Error("Event modules cannot have ControlPlugins");
return { ...mod,
plugins,
locals: {} } as Module;
return {
...mod,
plugins,
onEvent,
} as EventModule;
}
/** 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
* This is an {@link eventModule} for discord events,
* where typings can be very bad.
* @Experimental
* @param mod
*/
export function discordEvent<T extends keyof ClientEvents>(mod: {
name: T;
once?: boolean;
plugins?: AnyEventPlugin[];
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
}) {
return eventModule({ type: EventType.Discord, ...mod, });
return eventModule({
type: EventType.Discord,
...mod,
});
}
function prepareClassPlugins(c: Module) {
const [onEvent, initPlugins] = partitionPlugins(c.plugins);
c.plugins = initPlugins as InitPlugin[];
c.onEvent = onEvent as ControlPlugin[];
}
//
// Class modules:
// Can be refactored.
// Both implement singleton, could I make them inherit a singleton parent class?
/**
* @Experimental
* Will be refactored / changed in future
*/
export abstract class CommandExecutable<const Type extends CommandType = CommandType> {
abstract type: Type;
plugins: AnyCommandPlugin[] = [];
private static _instance: CommandModule;
static getInstance() {
if (!CommandExecutable._instance) {
//@ts-ignore
CommandExecutable._instance = new this();
prepareClassPlugins(CommandExecutable._instance);
}
return CommandExecutable._instance;
}
abstract execute(...args: CommandArgs<Type, PluginType.Control>): Awaitable<unknown>;
}
/**
* Creates a scheduled task that can be executed at specified intervals using cron patterns
*
* @param {ScheduledTask} ism - The scheduled task configuration object
* @param {string} ism.trigger - A cron pattern that determines when the task should execute
* Format: "* * * * *" (minute hour day month day-of-week)
* @param {Function} ism.execute - The function to execute when the task is triggered
* @param {Object} ism.execute.context - The execution context passed to the task
*
* @returns {ScheduledTask} The configured scheduled task
*
* @example
* // Create a task that runs every minute
* export default scheduledTask({
* trigger: "* * * * *",
* execute: (context) => {
* console.log("Task executed!");
* }
* });
*
* @remarks
* - Tasks must be placed in the 'tasks' directory specified in your config
* - The file name serves as a unique identifier for the task
* - Tasks can be cancelled using deps['@sern/scheduler'].kill(uuid)
*
* @see {@link https://crontab.guru/} for testing and creating cron patterns
* @Experimental
* Will be refactored in future
*/
export function scheduledTask(ism: ScheduledTask): ScheduledTask {
return ism
}
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: AnyEventPlugin[] = [];
private static _instance: EventModule;
static getInstance() {
if (!EventExecutable._instance) {
//@ts-ignore
EventExecutable._instance = new this();
prepareClassPlugins(EventExecutable._instance);
}
return EventExecutable._instance;
}
abstract execute(...args: EventArgs<Type, PluginType.Control>): Awaitable<unknown>;
}

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

@@ -0,0 +1,86 @@
/**
* This file holds sern's rxjs operators used for processing data.
* Each function should be modular and testable, not bound to discord / sern
* and independent of each other.
*/
import {
concatMap,
defaultIfEmpty,
EMPTY,
every,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
share,
} from 'rxjs';
import { Emitter, ErrorHandling, Logging } from './contracts';
import util from 'node:util';
import type { PluginResult, VoidResult } from '../types/core-plugin';
import type { Result } from 'ts-results-es'
/**
* if {src} is true, mapTo V, else ignore
* @param item
*/
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
}
/**
* Calls any plugin with {args}.
* @param args if an array, its spread and plugin called.
*/
export function callPlugin(args: unknown): OperatorFunction<
{
execute: (...args: unknown[]) => PluginResult;
},
VoidResult
> {
return concatMap(async plugin => {
if (Array.isArray(args)) {
return plugin.execute(...args);
}
return plugin.execute(args);
});
}
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
/**
* Checks if the stream of results is all ok.
*/
export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
every(result => result.ok),
defaultIfEmpty(true),
);
export const sharedEventStream = <T>(e: Emitter, eventName: string) => {
return (fromEvent(e, eventName) as Observable<T>).pipe(share());
};
export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
return (pload: unknown, caught: Observable<C>) => {
// This is done to fit the ErrorHandling contract
const err = pload instanceof Error ? pload : Error(util.inspect(pload, { colors: true }));
//formatted payload
logging?.error({ message: util.inspect(pload) });
crashHandler.updateAlive(err);
return caught;
};
}
// Temporary until i get rxjs operators working on ts-results-es
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
pipe(
concatMap(result => {
if(result.ok) {
return of(result.val)
}
onErr(result.val);
return EMPTY
})
)

View File

@@ -1,137 +0,0 @@
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);
}
/**
* Creates an initialization plugin for command preprocessing and modification
*
* @since 2.5.0
* @template I - Extends CommandType to enforce type safety for command modules
*
* @param {function} execute - Function to execute during command initialization
* @param {InitArgs<T>} execute.args - The initialization arguments
* @param {T} execute.args.module - The command module being initialized
* @param {string} execute.args.absPath - The absolute path to the module file
* @param {Dependencies} execute.args.deps - Dependency injection container
*
* @returns {Plugin} A plugin that runs during command initialization
*
* @example
* // Plugin to update command description
* export const updateDescription = (description: string) => {
* return CommandInitPlugin(({ deps }) => {
* if(description.length > 100) {
* deps.logger?.info({ message: "Invalid description" })
* return controller.stop("From updateDescription: description is invalid");
* }
* module.description = description;
* return controller.next();
* });
* };
*
* @example
* // Plugin to store registration date in module locals
* export const dateRegistered = () => {
* return CommandInitPlugin(({ module }) => {
* module.locals.registered = Date.now()
* return controller.next();
* });
* };
*
* @remarks
* - Init plugins can modify how commands are loaded and perform preprocessing
* - The module.locals object can be used to store custom plugin-specific data
* - Be careful when modifying module fields as multiple plugins may interact with them
* - Use controller.next() to continue to the next plugin
* - Use controller.stop(reason) to halt plugin execution
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (args: InitArgs) => PluginResult
): Plugin {
return makePlugin(PluginType.Init, execute);
}
/**
* Creates a control plugin for command preprocessing, filtering, and state management
*
* @since 2.5.0
* @template I - Extends CommandType to enforce type safety for command modules
*
* @param {function} execute - Function to execute during command control flow
* @param {CommandArgs<I>} execute.args - The command arguments array
* @param {Context} execute.args[0] - The discord context (e.g., guild, channel, user info, interaction)
* @param {SDT} execute.args[1] - The State, Dependencies, Params, Module, and Type object
*
* @returns {Plugin} A plugin that runs during command execution flow
*
* @example
* // Plugin to restrict command to specific guild
* export const inGuild = (guildId: string) => {
* return CommandControlPlugin((ctx, sdt) => {
* if(ctx.guild.id !== guildId) {
* return controller.stop();
* }
* return controller.next();
* });
* };
*
* @example
* // Plugins passing state through the chain
* const plugin1 = CommandControlPlugin((ctx, sdt) => {
* return controller.next({ 'plugin1/data': 'from plugin1' });
* });
*
* const plugin2 = CommandControlPlugin((ctx, sdt) => {
* return controller.next({ 'plugin2/data': ctx.user.id });
* });
*
* export default commandModule({
* type: CommandType.Slash,
* plugins: [plugin1, plugin2],
* execute: (ctx, sdt) => {
* console.log(sdt.state); // Access accumulated state
* }
* });
*
* @remarks
* - Control plugins are executed in order when a discord.js event is emitted
* - Use controller.next() to continue to next plugin or controller.stop() to halt execution
* - State can be passed between plugins using controller.next({ key: value })
* - State keys should be namespaced to avoid collisions (e.g., 'plugin-name/key')
* - Final accumulated state is passed to the command's execute function
* - All plugins must succeed for the command to execute
* - Plugins have access to dependencies through the sdt.deps object
* - Useful for implementing preconditions, filters, and command preprocessing
*/
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;

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