Compare commits

...

45 Commits

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

* more refactors and name changes

* feat: update name usage and update dispatchers.ts

* fix:naming

* feat: slightly safer typings than any[]

* fix: forgot to destructure arguments

* feat: add special function

* fix: typings

* feat: SUPER SIMPLIFY!!!

* refactor: move promisifiedPlugins closer to call site

* refactor: typings

* refactor: typings

* refactor: consolidate resolving initplugins into one function

* refactor: better types

* revert: remove unneeded function

* revert: remove unneeded function

* feat: dispatch work, simplify

* feat: move some observableHandling function to operators for clarity

* feat: simplify and document

* feat: simplifying sern and docs

* fix: typings

* docs: clarity of function name

* docs: add documentation for executeModule

* feat: contextArgs overloads

* docs: found out why

* fix: typings

* feat: shorten operators signature

* refactor: switch to correct convention

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

* refactor: revert

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

* style: prettier and short type aliases

* fix: typings

* fix: typings

* docs: add deprecations

* refactor: organization and moving stuff

* pretty: prettey

* docs: describe file

* chore: update dependencies and version

* docs: fix link for docasaurus

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

* fix!: changing single and many

* refactor: typings and simplifying composeRoot

* fix: re-add logger into handleError

* docs: comment

* docs: new section

* feat: help mitigate breaking changes

* feat: help mitigate breaking changes

* feat: help mitigate breaking changes and function overloads

* feat: deprecate instead of remove

* feat: partial remove and deprecate old symbols

* revert: trying to accommodate old plugins is too difficult

* docs: add many as deprecated

* docs: update

* feat: partial backwards compatability

* refactor: renaming, docs, and exports more clean

* refactor: context got a lot simpler

* refactor: imports

* docs: explain methods
2023-01-26 12:06:04 -06:00
renovate[bot]
cb95105c1c chore(deps): update dependency prettier to v2.8.3 (#196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-17 12:54:57 -06:00
renovate[bot]
845b82feef chore(deps): update actions/checkout digest to ac59398 (#184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-14 22:38:44 -06:00
renovate[bot]
833a323f3c chore(deps): update actions/setup-node digest to 64ed1c7 (#185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-14 22:36:47 -06:00
renovate[bot]
42e5f20425 chore(deps): update pnpm to v7.25.0 (#195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-14 22:36:09 -06:00
renovate[bot]
2b25e6bfbb chore(deps): update pnpm to v7.22.0 (#179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-07 19:43:59 -06:00
renovate[bot]
1a27341092 chore(deps): update dependency prettier to v2.8.2 (#189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-07 16:30:48 -06:00
renovate[bot]
4680e451bb chore(deps): update dependency @typescript-eslint/parser to v5.48.0 (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-07 16:09:49 -06:00
renovate[bot]
917d8b0d1b chore(deps): update dependency iti to ^0.6.0 (#178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-31 14:01:56 -06:00
47 changed files with 2923 additions and 3508 deletions

View File

@@ -5,6 +5,8 @@ on:
push:
branches:
main
paths:
- '**.ts'
pull_request_target:
branches:
main
@@ -17,10 +19,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17

View File

@@ -1,30 +1,27 @@
name: NPM / Publish
on:
release:
types: [created]
workflow_run:
workflows: ["release-please"]
types: [completed]
jobs:
build:
test-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
- run: npm test
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
node-version: 17
- uses: pnpm/action-setup@v2
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
run_install: |
- recursive: true
args: [--strict-peer-dependencies]
- run: pnpm install
- run: pnpm test
- run: pnpm build
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: "public"

View File

@@ -1,5 +1,53 @@
# Changelog
## [2.5.3](https://github.com/sern-handler/handler/compare/v2.5.2...v2.5.3) (2023-02-16)
### Miscellaneous Chores
* release 2.5.3 ([ce9a083](https://github.com/sern-handler/handler/commit/ce9a0831a6e47dd38648f34653f0bd89b1d2e48e))
## [2.5.2](https://github.com/sern-handler/handler/compare/v2.5.1...v2.5.2) (2023-02-16)
### Reverts
* version ([facee79](https://github.com/sern-handler/handler/commit/facee79c904ad663d3c57ce56fb825419fcc12f9))
## [2.5.1](https://github.com/sern-handler/handler/compare/v2.5.0...v2.5.1) (2023-02-12)
### Features
* Adding my bot to readme ([#210](https://github.com/sern-handler/handler/issues/210)) ([96f4281](https://github.com/sern-handler/handler/commit/96f42811218e4898a47e75a8138ccd452ae2c5c2))
* Adding the WIP to my bot ([86fa531](https://github.com/sern-handler/handler/commit/86fa531eb620d2ac649bad6decb29d5c55a25445))
### Bug Fixes
* autocomplete ([1860b89](https://github.com/sern-handler/handler/commit/1860b898f3a231840e2a8b781e007ef9d6f587a4))
### Miscellaneous Chores
* release 2.5.1 ([c78936a](https://github.com/sern-handler/handler/commit/c78936a22574da4af71826f5b5f72f354a4eb06a))
## [2.5.0](https://github.com/sern-handler/handler/compare/v2.1.1...v2.5.0) (2023-01-30)
### ⚠ BREAKING CHANGES
* simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193))
### Features
* simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193)) ([33f1446](https://github.com/sern-handler/handler/commit/33f14467ec413e003a82503c8a77cb42a6194281))
### Miscellaneous Chores
* release 2.5.0 ([b4b195d](https://github.com/sern-handler/handler/commit/b4b195dc9586736760d0b78caa8589f3d6131f8a))
## [2.1.1](https://github.com/sern-handler/handler/compare/v2.1.0...v2.1.1) (2022-12-31)

View File

@@ -3,7 +3,7 @@
</div>
<h1 align="center">Handlers. Redefined.</h1>
<h4 align="center">A customizable, batteries-included, powerful discord.js framework to streamline bot development.</h4>
<h4 align="center">A complete, customizable, typesafe, & reactive framework for discord bots</h4>
<div align="center" styles="margin-top: 10px">
<img src="https://img.shields.io/badge/open-source-brightgreen" />
@@ -14,6 +14,20 @@
<img alt="Lines of code" src="https://img.shields.io/badge/total%20lines-2k-blue" />
</div>
## Why?
- Most handlers don't support discord.js 14.7+
- Customizable commands
- Plug and play or customize to your liking
- Embraces reactive programming for consistent and reliable backend
- Customizable logger, error handling, and more
- Active development and growing [community](https://sern.dev/discord)
## 👀 Quick Look
* Support for discord.js v14 and all interactions
* Hybrid commands
* Lightweight and customizable
* ESM, CommonJS and TypeScript support
* A powerful CLI and awesome community-made plugins
## 📜 Installation
@@ -29,14 +43,6 @@ yarn add @sern/handler
pnpm add @sern/handler
```
## 👀 Quick Look
* Support for discord.js v14 and all interactions
* Hybrid commands
* Lightweight and customizable
* ESM, CommonJS and TypeScript support
* A powerful CLI and awesome community-made plugins
## 👶 Basic Usage
#### ` index.js (CommonJS)`
@@ -46,10 +52,7 @@ pnpm add @sern/handler
const { Client, GatewayIntentBits } = require('discord.js');
// Import Sern namespace
const { Sern } = require('@sern/handler');
// Our configuration file
const { defaultPrefix, token } = require('./config.json');
const { Sern, single } = require('@sern/handler');
const client = new Client({
intents: [
@@ -60,8 +63,8 @@ const client = new Client({
});
export const useContainer = Sern.makeDependencies({
build: root => root
.add({ '@sern/client': single(client) })
.add({ '@sern/logger': single(new DefaultLogging()) })
.add({ '@sern/client': single(() => client) })
.upsert({ '@sern/logger': single(() => new DefaultLogging()) })
});
//View docs for all options
@@ -74,7 +77,7 @@ Sern.init({
}
});
client.login(token);
client.login("YOUR_BOT_TOKEN_HERE");
```
#### ` ping.js (CommonJS)`
@@ -83,16 +86,21 @@ client.login(token);
const { CommandType, commandModule } = require('@sern/handler');
exports.default = commandModule({
name: 'ping',
description: 'A ping pong command',
type: CommandType.Slash,
description: 'A ping pong command',
execute(ctx) {
ctx.reply('pong!');
}
});
```
See our [templates](https://github.com/sern-handler/templates) for TypeScript examples and more.
## 🤖 Bots Using sern
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).
- [Vinci](https://github.com/SrIzan10/vinci), the bot for Mara Turing.
- [Bask](https://github.com/baskbotml/bask), Listen your favorite artists on Discord.
- [ava](https://github.com/SrIzan10/ava), A discord bot that plays KNGI and Gensokyo Radio.
- [ALMA (WIP)](https://github.com/Benzo-Fury/ALMA), Using AI to unleash the power in your server.
- [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
## 💻 CLI
@@ -105,10 +113,8 @@ It is **highly encouraged** to use the [command line interface](https://github.c
## 👋 Contribute
- Read our contribution [guidelines](./.github/CONTRIBUTING.md) carefully
- Read our contribution [guidelines](https://github.com/sern-handler/handler/blob/main/.github/CONTRIBUTING.md) carefully
- Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs
- All kinds of contributions are welcomed.
## 🚈 Roadmap
You can check our [roadmap](https://github.com/sern-handler/roadmap) to see what's going to be added or patched in the future.

View File

@@ -1,7 +1,7 @@
{
"name": "@sern/handler",
"packageManager": "pnpm@7.18.2",
"version": "2.1.1",
"packageManager": "pnpm@7.27.0",
"version": "2.5.3",
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
@@ -13,7 +13,7 @@
}
},
"scripts": {
"watch": "tsup --watch --dts",
"watch": "tsup --dts --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
@@ -33,19 +33,19 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"iti": "^0.5.0",
"rxjs": "^7.5.6",
"ts-pattern": "^4.0.2",
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-pattern": "^4.1.4",
"ts-results-es": "^3.5.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.47.1",
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.48.0",
"discord.js": "^14.7.1",
"eslint": "8.30.0",
"prettier": "2.8.1",
"tsup": "^6.1.3",
"typescript": "4.9.4",
"discord.js": ">= ^14.7.x"
"prettier": "2.8.4",
"tsup": "^6.6.3",
"typescript": "4.9.4"
},
"repository": {
"type": "git",

4153
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import type { Observable } from 'rxjs';
import type { Logging } from './logging';
import { useContainerRaw } from '../dependencies/provider';
import util from 'util';
export interface ErrorHandling {
/**
@@ -36,11 +35,6 @@ export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
// This is done to fit the ErrorHandling contract
const err = pload instanceof Error ? pload : Error(util.format(pload));
if (crashHandler.keepAlive == 0) {
useContainerRaw()
?.disposeAll()
.then(() => {
logging?.info({ message: 'Cleaning container and crashing' });
});
crashHandler.crash(err);
}
//formatted payload

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ import type { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
/**
* why did I make this, definitely going to be changed in the future
*/
export abstract class EventsHandler<T> {
protected payloadSubject = new Subject<T>();
protected abstract discordEvent: Observable<unknown>;

View File

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

View File

@@ -1,39 +1,46 @@
import { EventsHandler } from './eventsHandler';
import { catchError, concatMap, from, fromEvent, map, Observable, of, switchMap } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of } from 'rxjs';
import { type Wrapper, type ModuleStore, SernError } from '../structures';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, isOneOfCorrectModules } from './observableHandling';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import { fmt } from '../utilities/messageHelpers';
import Context from '../structures/context';
import { CommandType, PayloadType } from '../structures/enums';
import { arrAsync } from '../utilities/arrAsync';
import { controller } from '../sern';
import type { CommandModule, TextCommand } from '../../types/module';
import type { CommandModule, Module, TextCommand } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import type { ModuleStore } from '../structures/moduleStore';
import { contextArgs, dispatchCommand } from './dispatchers';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
export default class MessageHandler extends EventsHandler<{
ctx: Context;
args: ['text', string[]];
mod: TextCommand;
module: Processed<Module>;
args: unknown[];
}> {
protected discordEvent: Observable<Message>;
public constructor(protected wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Message>>fromEvent(this.client, 'messageCreate');
this.init();
this.payloadSubject
.pipe(
switchMap(({ mod, ctx, args }) => {
const res = arrAsync(
mod.onEvent.map(ep => ep.execute([ctx, args], controller)),
makeModuleExecutor(module => {
this.emitter.emit(
'module.activate',
SernEmitter.failure(module, SernError.PluginFailure),
);
const execute = () => mod.execute(ctx, args);
//resolves the promise and re-emits it back into source
return from(res).pipe(map(res => ({ mod, execute, res })));
}),
concatMap(payload => executeModule(wrapper, payload)),
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({
message: 'messageCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
@@ -41,34 +48,37 @@ export default class MessageHandler extends EventsHandler<{
protected init(): void {
if (this.wrapper.defaultPrefix === undefined) return; //for now, just ignore if prefix doesn't exist
const { defaultPrefix } = this.wrapper;
const get = (cb: (ms: ModuleStore) => CommandModule | undefined) => {
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return this.modules.get(cb);
};
this.discordEvent
.pipe(
ignoreNonBot(this.wrapper.defaultPrefix),
map(message => {
//This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
return {
ctx: Context.wrap(message),
args: <['text', string[]]>['text', rest],
mod: get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix)),
const module = get(
ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix),
);
if (module === undefined) {
return EMPTY;
}
const payload = {
args: contextArgs(message, rest),
module,
};
return of(payload);
}),
concatMap(element =>
of(element.mod).pipe(
isOneOfCorrectModules(CommandType.Text),
map(mod => ({ ...element, mod })),
),
),
map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)),
)
.subscribe({
next: value => this.setState(value),
error: reason => this.emitter.emit('error', { type: PayloadType.Failure, reason }),
error: reason => this.emitter.emit('error', SernEmitter.failure(reason)),
});
}
protected setState(state: { ctx: Context; args: ['text', string[]]; mod: TextCommand }) {
protected setState(state: { module: Processed<Module>; args: unknown[] }) {
this.payloadSubject.next(state);
}
}

View File

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

View File

@@ -0,0 +1,70 @@
/**
* This file holds sern's rxjs operators used for processing data.
* Each function should be modular and testable, not bound to discord / sern
* and independent of each other
*/
import { concatMap, defaultIfEmpty, EMPTY, every, map, of, OperatorFunction, pipe } from 'rxjs';
import type { AnyModule } from '../../../types/module';
import { nameOrFilename } from '../../utilities/functions';
import type { PluginResult, VoidResult } from '../../../types/plugin';
import { guayin } from '../../plugins';
import { controller } from '../../sern';
/**
* if {src} is true, mapTo V, else ignore
* @param item
*/
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return pipe(concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY)));
}
/**
* Calls any plugin with {args}.
* @param args if an array, its spread and plugin called.
*/
export function callPlugin(args: unknown): OperatorFunction<
{
execute: (...args: unknown[]) => PluginResult;
},
VoidResult
> {
return pipe(
concatMap(async plugin => {
const isNewPlugin = Reflect.has(plugin, guayin);
if (isNewPlugin) {
if (Array.isArray(args)) {
return plugin.execute(...args);
}
return plugin.execute(args);
} else {
return plugin.execute(args, controller);
}
}),
);
}
/**
* operator function that fill the defaults for a module
*/
export function defineAllFields<T extends AnyModule>() {
const fillFields = ({ absPath, module }: { absPath: string; module: T }) => ({
absPath,
module: {
name: nameOrFilename(module.name, absPath),
description: module.description ?? '...',
...module,
},
});
return pipe(map(fillFields));
}
/**
* Checks if the stream of results is all ok.
*/
export function everyPluginOk(): OperatorFunction<VoidResult, boolean> {
return pipe(
every(result => result.ok),
defaultIfEmpty(true),
);
}

View File

@@ -1,98 +1,84 @@
import { EventsHandler } from './eventsHandler';
import type Wrapper from '../structures/wrapper';
import { concatMap, fromEvent, Observable, map, take } from 'rxjs';
import { concatMap, fromEvent, type Observable, take } from 'rxjs';
import * as Files from '../utilities/readFile';
import { errTap, processPlugins, resolvePlugins } from './observableHandling';
import { CommandType, PayloadType } from '../structures/enums';
import { SernError } from '../structures/errors';
import { errTap, scanModule } from './observableHandling';
import { CommandType, SernError, type ModuleStore } from '../structures';
import { match } from 'ts-pattern';
import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module';
import type { DefinedCommandModule, DefinedEventModule } from '../../types/handler';
import type { Processed } from '../../types/handler';
import type { ModuleManager } from '../contracts';
import type { ModuleStore } from '../structures/moduleStore';
import { _const, err, nameOrFilename, ok } from '../utilities/functions';
import { _const, err, ok } from '../utilities/functions';
import { defineAllFields } from './operators';
import SernEmitter from '../sernEmitter';
export default class ReadyHandler extends EventsHandler<{
mod: DefinedCommandModule;
module: Processed<CommandModule>;
absPath: string;
}> {
protected discordEvent!: Observable<{ mod: CommandModule; absPath: string }>;
protected discordEvent!: Observable<{ module: CommandModule; absPath: string }>;
constructor(wrapper: Wrapper) {
super(wrapper);
const ready$ = fromEvent(this.client, 'ready').pipe(take(1));
this.discordEvent = ready$.pipe(
concatMap(() =>
Files.buildData<CommandModule>(wrapper.commands).pipe(
errTap(reason =>
this.emitter.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
}),
),
errTap(reason => {
this.emitter.emit(
'module.register',
SernEmitter.failure(undefined, reason),
);
}),
),
),
);
this.init();
this.payloadSubject
.pipe(concatMap(processPlugins), concatMap(resolvePlugins))
.subscribe(payload => {
const allPluginsSuccessful = payload.pluginRes.every(({ execute }) => execute.ok);
if (allPluginsSuccessful) {
const res = registerModule(this.modules, payload.mod);
if (res.err) {
this.crashHandler.crash(Error(SernError.InvalidModuleType));
}
this.emitter.emit('module.register', {
type: PayloadType.Success,
module: payload.mod,
});
} else {
this.emitter.emit('module.register', {
type: PayloadType.Failure,
module: payload.mod,
reason: SernError.PluginFailure,
});
.pipe(
scanModule({
onFailure: module => {
this.emitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onSuccess: ({ module }) => {
this.emitter.emit('module.register', SernEmitter.success(module));
return module;
},
}),
)
.subscribe(module => {
const res = registerModule(this.modules, module as Processed<CommandModule>);
if (res.err) {
this.crashHandler.crash(Error(SernError.InvalidModuleType));
}
});
}
private static intoDefinedModule({ absPath, mod }: { absPath: string; mod: CommandModule }): {
absPath: string;
mod: DefinedCommandModule;
} {
return {
absPath,
mod: {
name: nameOrFilename(mod.name, absPath),
description: mod?.description ?? '...',
...mod,
},
};
}
protected init() {
this.discordEvent.pipe(map(ReadyHandler.intoDefinedModule)).subscribe({
this.discordEvent.pipe(defineAllFields()).subscribe({
next: value => this.setState(value),
complete: () => this.payloadSubject.unsubscribe(),
});
}
protected setState(state: { absPath: string; mod: DefinedCommandModule }): void {
protected setState(state: { absPath: string; module: Processed<CommandModule> }): void {
this.payloadSubject.next(state);
}
}
function registerModule(
function registerModule<T extends Processed<CommandModule>>(
manager: ModuleManager,
mod: DefinedCommandModule | DefinedEventModule,
mod: T,
): Result<void, void> {
const name = mod.name;
const insert = (cb: (ms: ModuleStore) => void) => {
const set = Result.wrap(_const(manager.set(cb)));
return set.ok ? ok() : err();
};
return match<DefinedCommandModule | DefinedEventModule>(mod)
return match(mod as Processed<CommandModule>)
.with({ type: CommandType.Text }, mod => {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod));

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,7 @@
import type Wrapper from './structures/wrapper';
import { processEvents } from './events/userDefinedEventsHandling';
import { CommandType, EventType, PluginType } from './structures/enums';
import type {
Plugin,
CommandPlugin,
EventModuleCommandPluginDefs,
EventModuleEventPluginDefs,
EventPlugin,
InputCommandModule,
InputEventModule,
} from './plugins/plugin';
import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin';
import InteractionHandler from './events/interactionHandler';
import ReadyHandler from './events/readyHandler';
import MessageHandler from './events/messageHandler';
@@ -18,12 +10,15 @@ import type {
CommandModuleDefs,
EventModule,
EventModuleDefs,
InputCommand,
InputEvent,
} from '../types/module';
import { Container, createContainer } from 'iti';
import type { Dependencies, OptionalDependencies } from '../types/handler';
import { composeRoot, containerSubject, useContainer } from './dependencies/provider';
import type { Dependencies, DependencyConfiguration } from '../types/handler';
import { composeRoot, useContainer } from './dependencies/provider';
import type { Logging } from './contracts';
import { err, ok, partition } from './utilities/functions';
import type { Awaitable, ClientEvents } from 'discord.js';
/**
*
* @param wrapper Options to pass into sern.
@@ -66,10 +61,10 @@ export const controller = {
* The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommandModule): CommandModule {
export function commandModule(mod: InputCommand): CommandModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Event,
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
@@ -81,10 +76,10 @@ export function commandModule(mod: InputCommandModule): CommandModule {
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEventModule): EventModule {
export function eventModule(mod: InputEvent): EventModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Event,
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
@@ -92,29 +87,45 @@ export function eventModule(mod: InputEventModule): EventModule {
plugins,
} as EventModule;
}
/**
* Create event modules from discord.js client events,
* This is an {@link eventModule} for discord events,
* where typings can be very bad.
* @param mod
*/
export function discordEvent<T extends keyof ClientEvents>(mod: {
name: T;
plugins?: AnyEventPlugin[];
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
}) {
return eventModule({ type: EventType.Discord, ...mod });
}
/**
* @param conf a configuration for creating your project dependencies
*/
export function makeDependencies<T extends Dependencies>(conf: {
exclude?: Set<OptionalDependencies>;
build: (root: Container<Record<string, any>, {}>) => Container<Partial<T>, T>;
}) {
const container = conf.build(createContainer());
composeRoot(container, conf.exclude ?? new Set());
containerSubject.next(container as unknown as Container<Dependencies, {}>);
export function makeDependencies<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Until there are more optional dependencies, just check if the logger exists
composeRoot(conf);
return useContainer<T>();
}
/**
* @Experimental
* Will be refactored / changed in future
*/
export abstract class CommandExecutable<Type extends CommandType> {
abstract type: Type;
plugins: CommandPlugin<Type>[] = [];
onEvent: EventPlugin<Type>[] = [];
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: CommandModuleDefs[Type]['execute'];
}
/**@Experimental
* Will be refactored in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: EventModuleCommandPluginDefs[Type][] = [];
onEvent: EventModuleEventPluginDefs[Type][] = [];
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: EventModuleDefs[Type]['execute'];
}

View File

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

View File

@@ -39,11 +39,11 @@ export default class Context {
}
public get id(): Snowflake {
return safeUnwrap(this.ctx.map(m => m.id).mapErr(i => i.id));
return this.ctx.val.id;
}
public get channel() {
return safeUnwrap(this.ctx.map(m => m.channel).mapErr(i => i.channel));
return this.ctx.val.channel;
}
/**
* If context is holding a message, message.author
@@ -54,30 +54,30 @@ export default class Context {
}
public get createdTimestamp(): number {
return safeUnwrap(this.ctx.map(m => m.createdTimestamp).mapErr(i => i.createdTimestamp));
return this.ctx.val.createdTimestamp;
}
public get guild() {
return safeUnwrap(this.ctx.map(m => m.guild).mapErr(i => i.guild));
return this.ctx.val.guild;
}
public get guildId() {
return safeUnwrap(this.ctx.map(m => m.guildId).mapErr(i => i.guildId));
return this.ctx.val.guildId;
}
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
public get member() {
return safeUnwrap(this.ctx.map(m => m.member).mapErr(i => i.member));
return this.ctx.val.member;
}
public get client(): Client {
return safeUnwrap(this.ctx.map(m => m.client).mapErr(i => i.client));
return this.ctx.val.client;
}
public get inGuild(): boolean {
return safeUnwrap(this.ctx.map(m => m.inGuild()).mapErr(i => i.inGuild()));
return this.ctx.val.inGuild();
}
public isMessage() {
return this.ctx.map(() => true).unwrapOr(false);

View File

@@ -102,13 +102,23 @@ export enum EventType {
*/
export enum PluginType {
/**
* The PluginType for CommandPlugins
* The PluginType for InitPlugins
*/
Init = 1,
/**
* @deprecated
* Use PluginType.Init instead
*/
Command = 1,
/**
* The PluginType for EventPlugins
* @deprecated
* Use PluginType.Control instead
*/
Event = 2,
/**
* The PluginType for EventPlugins
*/
Control = 2,
}
/**
* @enum { string }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,8 @@ function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/**
*
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
@@ -30,7 +31,7 @@ export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
export function buildData<T>(commandDir: string): Observable<
Result<
{
mod: T;
module: T;
absPath: string;
},
SernError
@@ -40,20 +41,20 @@ export function buildData<T>(commandDir: string): Observable<
return from(
Promise.all(
commands.map(async absPath => {
let mod: T | undefined;
let module: T | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
mod = require(absPath).default;
module = require(absPath).default;
} catch {
mod = (await import(`file:///` + absPath)).default;
module = (await import(`file:///` + absPath)).default;
}
if (mod === undefined) {
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
mod = new (mod as unknown as new () => T)();
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ mod, absPath });
return Ok({ module, absPath });
}),
),
).pipe(concatAll());

View File

@@ -1,6 +1,11 @@
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
/**
* Uses an iterative DFS to check if an autocomplete node exists
* @param iAutocomplete
* @param options
*/
export default function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,

View File

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

View File

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

View File

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

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

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

View File

View File

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

View File

View File

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

View File

@@ -5,6 +5,7 @@ const shared = {
platform: 'node',
clean: true,
sourcemap: false,
minify: true
};
export default defineConfig([
{