Compare commits

...

32 Commits

Author SHA1 Message Date
github-actions[bot]
d381ff568e chore(main): release 2.6.0 (#248)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-09 16:18:06 -06:00
Jacob Nguyen
6db5c71506 chore: update workflow 2023-03-09 16:15:29 -06:00
github-actions[bot]
507c9e7939 style: pretty please (#247)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-03-09 16:11:29 -06:00
Jacob Nguyen
09610d0501 refactor: eventhandlers (#246)
* refactor:import

* feat: save progress

* feat:progress

* refactor: event handlers

* fix: merge all subscriptions into event handler

* fix: remove duplicate minify key

* fix: leftover this

* docs: jsdoc

* chore: clean pnpm

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-03-09 16:09:35 -06:00
github-actions[bot]
0862bf92d0 style: pretty please (#244)
Co-authored-by: renovate[bot] <renovate[bot]@users.noreply.github.com>
2023-03-03 09:22:02 -06:00
renovate[bot]
62162f6b8c chore(deps): lock file maintenance (#240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-02 19:03:02 -06:00
renovate[bot]
eb501db09a chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.54.0 (#243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 19:01:18 -06:00
renovate[bot]
964848a4e2 chore(deps): update dependency @typescript-eslint/parser to v5.54.0 (#194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-02 18:48:56 -06:00
renovate[bot]
78dead1b49 chore(deps): update pnpm to v7.28.0 (#239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 18:48:32 -06:00
renovate[bot]
39e6d6d2f9 chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.52.0 (#238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-20 00:30:10 -06:00
renovate[bot]
5aff57ed6d chore(deps): update dependency typescript to v4.9.5 (#208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-20 00:27:36 -06:00
github-actions[bot]
4095471346 style: pretty please (#237)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-02-17 15:47:33 -06:00
Jacob Nguyen
f7b9c52df1 Merge remote-tracking branch 'origin/main' 2023-02-17 15:40:54 -06:00
Jacob Nguyen
d20d01524b feat: adding pure annotation for better tree shaking 2023-02-17 15:40:32 -06:00
renovate[bot]
5684c060bc chore(deps): lock file maintenance (#225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-02-17 13:13:15 -06:00
github-actions[bot]
ca8b31f280 chore(main): release 2.5.3 (#235)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-16 11:11:32 -06:00
jacoobes
ce9a0831a6 chore: release 2.5.3
Release-As: 2.5.3
2023-02-16 11:09:46 -06:00
jacoobes
3a32968a17 chore: bump version 2023-02-16 10:57:38 -06:00
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
19 changed files with 2743 additions and 2129 deletions

View File

@@ -5,6 +5,8 @@ on:
push: push:
branches: branches:
main main
paths:
- '**.ts'
pull_request_target: pull_request_target:
branches: branches:
main main

View File

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

View File

@@ -1,5 +1,26 @@
# Changelog # Changelog
## [2.6.0](https://github.com/sern-handler/handler/compare/v2.5.3...v2.6.0) (2023-03-09)
### Features
* adding pure annotation for better tree shaking ([d20d015](https://github.com/sern-handler/handler/commit/d20d01524b872549da501e21feec147ab204f397))
## [2.5.3](https://github.com/sern-handler/handler/compare/v2.5.2...v2.5.3) (2023-02-16)
### Miscellaneous Chores
* release 2.5.3 ([ce9a083](https://github.com/sern-handler/handler/commit/ce9a0831a6e47dd38648f34653f0bd89b1d2e48e))
## [2.5.2](https://github.com/sern-handler/handler/compare/v2.5.1...v2.5.2) (2023-02-16)
### 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) ## [2.5.1](https://github.com/sern-handler/handler/compare/v2.5.0...v2.5.1) (2023-02-12)

View File

@@ -3,7 +3,7 @@
</div> </div>
<h1 align="center">Handlers. Redefined.</h1> <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"> <div align="center" styles="margin-top: 10px">
<img src="https://img.shields.io/badge/open-source-brightgreen" /> <img src="https://img.shields.io/badge/open-source-brightgreen" />
@@ -14,20 +14,6 @@
<img alt="Lines of code" src="https://img.shields.io/badge/total%20lines-2k-blue" /> <img alt="Lines of code" src="https://img.shields.io/badge/total%20lines-2k-blue" />
</div> </div>
## 📜 Installation
```sh
npm install @sern/handler
```
```sh
yarn add @sern/handler
```
```sh
pnpm add @sern/handler
```
## Why? ## Why?
- Most handlers don't support discord.js 14.7+ - Most handlers don't support discord.js 14.7+
- Customizable commands - Customizable commands
@@ -43,6 +29,20 @@ pnpm add @sern/handler
* ESM, CommonJS and TypeScript support * ESM, CommonJS and TypeScript support
* A powerful CLI and awesome community-made plugins * A powerful CLI and awesome community-made plugins
## 📜 Installation
```sh
npm install @sern/handler
```
```sh
yarn add @sern/handler
```
```sh
pnpm add @sern/handler
```
## 👶 Basic Usage ## 👶 Basic Usage
#### ` index.js (CommonJS)` #### ` index.js (CommonJS)`
@@ -52,7 +52,7 @@ pnpm add @sern/handler
const { Client, GatewayIntentBits } = require('discord.js'); const { Client, GatewayIntentBits } = require('discord.js');
// Import Sern namespace // Import Sern namespace
const { Sern } = require('@sern/handler'); const { Sern, single } = require('@sern/handler');
const client = new Client({ const client = new Client({
intents: [ intents: [
@@ -63,8 +63,8 @@ const client = new Client({
}); });
export const useContainer = Sern.makeDependencies({ export const useContainer = Sern.makeDependencies({
build: root => root build: root => root
.add({ '@sern/client': single(client) }) .add({ '@sern/client': single(() => client) })
.upsert({ '@sern/logger': single(new DefaultLogging()) }) .upsert({ '@sern/logger': single(() => new DefaultLogging()) })
}); });
//View docs for all options //View docs for all options
@@ -86,9 +86,8 @@ client.login("YOUR_BOT_TOKEN_HERE");
const { CommandType, commandModule } = require('@sern/handler'); const { CommandType, commandModule } = require('@sern/handler');
exports.default = commandModule({ exports.default = commandModule({
name: 'ping',
description: 'A ping pong command',
type: CommandType.Slash, type: CommandType.Slash,
description: 'A ping pong command',
execute(ctx) { execute(ctx) {
ctx.reply('pong!'); ctx.reply('pong!');
} }
@@ -100,7 +99,7 @@ exports.default = commandModule({
- [Vinci](https://github.com/SrIzan10/vinci), the bot for Mara Turing. - [Vinci](https://github.com/SrIzan10/vinci), the bot for Mara Turing.
- [Bask](https://github.com/baskbotml/bask), Listen your favorite artists on Discord. - [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. - [ava](https://github.com/SrIzan10/ava), A discord bot that plays KNGI and Gensokyo Radio.
- [Siamese (WIP)](https://github.com/Benzo-Fury/Siamese-Bot), Using AI to unleash the power in your server. - [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. - [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
## 💻 CLI ## 💻 CLI

View File

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

4158
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ import type { Processed } from '../../types/handler';
export interface ModuleManager { export interface ModuleManager {
get<T extends CommandType>( get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined, strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
): CommandModuleDefs[T] | undefined; ): Processed<CommandModuleDefs[T]> | undefined;
set(strat: (ms: ModuleStore) => void): void; set(strat: (ms: ModuleStore) => void): void;
} }

View File

@@ -22,6 +22,7 @@ export function single<T extends NotFunction>(cb: T): () => T;
*/ */
export function single<T extends () => unknown>(cb: T): T; export function single<T extends () => unknown>(cb: T): T;
/** /**
* @__PURE__
* Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and * Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and
* very annoying). * very annoying).
* For future versions, ensure that single is being passed as a **callback!!** * For future versions, ensure that single is being passed as a **callback!!**
@@ -39,6 +40,7 @@ export function single<T>(cb: T) {
export function transient<T extends NotFunction>(cb: T): () => () => T; export function transient<T extends NotFunction>(cb: T): () => () => T;
export function transient<T extends () => () => unknown>(cb: T): T; export function transient<T extends () => () => unknown>(cb: T): T;
/** /**
* @__PURE__
* Following iti's singleton and transient implementation, * Following iti's singleton and transient implementation,
* use transient if you want a new dependency every time your container getter is called * use transient if you want a new dependency every time your container getter is called
* @param cb * @param cb
@@ -49,6 +51,7 @@ export function transient<T>(cb: (() => () => T) | T) {
} }
/** /**
* @__PURE__
* @deprecated * @deprecated
* @param value * @param value
* Please use the transient function instead * Please use the transient function instead

View File

@@ -1,12 +1,11 @@
import type { Container } from 'iti'; import type { Container } from 'iti';
import { SernError } from '../structures/errors';
import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler'; import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts'; import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
import { ModuleStore } from '../structures/moduleStore';
import { Result } from 'ts-results-es'; import { Result } from 'ts-results-es';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { createContainer } from 'iti'; import { createContainer } from 'iti';
import { type Wrapper, ModuleStore, SernError } from '../structures';
export const containerSubject = new BehaviorSubject(defaultContainer()); export const containerSubject = new BehaviorSubject(defaultContainer());
@@ -71,3 +70,17 @@ function defaultContainer() {
{} {}
>; >;
} }
export function makeFetcher(wrapper: Wrapper) {
const requiredDependencyKeys = [
'@sern/emitter',
'@sern/client',
'@sern/errors',
'@sern/logger',
] as ['@sern/emitter', '@sern/client', '@sern/errors', '@sern/logger'];
return <Keys extends (keyof Dependencies)[]>(otherKeys: [...Keys]) =>
wrapper.containerConfig.get(...requiredDependencyKeys, ...otherKeys) as MapDeps<
Dependencies,
[...typeof requiredDependencyKeys, ...Keys]
>;
}

View File

@@ -1,34 +0,0 @@
import type Wrapper from '../structures/wrapper';
import { Subject, type Observable } from 'rxjs';
import type { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
/**
* why did I make this, definitely going to be changed in the future
*/
export abstract class EventsHandler<T> {
protected payloadSubject = new Subject<T>();
protected abstract discordEvent: Observable<unknown>;
protected client: EventEmitter;
protected emitter: SernEmitter;
protected crashHandler: ErrorHandling;
protected logger?: Logging;
protected modules: ModuleManager;
protected constructor({ containerConfig }: Wrapper) {
const [client, emitter, crash, modules, logger] = containerConfig.get(
'@sern/client',
'@sern/emitter',
'@sern/errors',
'@sern/modules',
'@sern/logger',
);
this.logger = logger as Logging | undefined;
this.modules = modules as ModuleManager;
this.client = client as EventEmitter;
this.emitter = emitter as SernEmitter;
this.crashHandler = crash as ErrorHandling;
}
protected abstract init(): void;
protected abstract setState(state: T): void;
}

View File

@@ -1,137 +1,122 @@
import type { Interaction } from 'discord.js'; import type { Interaction } from 'discord.js';
import { catchError, concatMap, finalize, fromEvent, map, Observable } from 'rxjs'; import {
import type Wrapper from '../structures/wrapper'; catchError,
import { EventsHandler } from './eventsHandler'; concatMap,
import { CommandType, SernError, type ModuleStore } from '../structures'; EMPTY,
filter,
finalize,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
} from 'rxjs';
import { CommandType, type ModuleStore, SernError } from '../structures';
import { match, P } from 'ts-pattern'; import { match, P } from 'ts-pattern';
import { contextArgs, interactionArg, dispatchAutocomplete, dispatchCommand } from './dispatchers'; import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers';
import { executeModule, makeModuleExecutor } from './observableHandling'; import { executeModule, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module'; import type { CommandModule } from '../../types/module';
import { handleError } from '../contracts/errorHandling'; import { ErrorHandling, handleError } from '../contracts/errorHandling';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler'; import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies'; import { useContainerRaw } from '../dependencies';
import type { Logging, ModuleManager } from '../contracts';
import type { EventEmitter } from 'node:events';
export default class InteractionHandler extends EventsHandler<{ function makeInteractionProcessor(
modules: ModuleManager,
): OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }> {
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return modules.get(cb);
};
return pipe(
concatMap(event => {
if (event.isMessageComponent()) {
const module = get(ms =>
ms.InteractionHandlers[event.componentType].get(event.customId),
);
return of({ module, event });
} else if (event.isCommand() || event.isAutocomplete()) {
const module = get(
ms =>
/**
* try to fetch from ApplicationCommands, if nothing, try BothCommands
* exists on the API but not sern
*/
ms.ApplicationCommands[event.commandType].get(event.commandName) ??
ms.BothCommands.get(event.commandName),
);
return of({ module, event });
} else if (event.isModalSubmit()) {
const module = get(ms => ms.ModalSubmit.get(event.customId));
return of({ module, event });
} else return EMPTY;
}),
filter(m => m.module !== undefined),
) as OperatorFunction<Interaction, { module: Processed<CommandModule>; event: Interaction }>;
}
export function makeInteractionCreate([s, client, err, log, modules]: [
SernEmitter,
EventEmitter,
ErrorHandling,
Logging | undefined,
ModuleManager,
]) {
//map. If nothing again,this means a slash command
const interactionStream$ = fromEvent(client, 'interactionCreate') as Observable<Interaction>;
const interactionProcessor = makeInteractionProcessor(modules);
return interactionStream$
.pipe(
interactionProcessor,
map(createDispatcher),
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(module => executeModule(s, module)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({
message: 'interactionCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
)
.subscribe();
}
function createDispatcher({
module,
event,
}: {
event: Interaction; event: Interaction;
module: Processed<CommandModule>; module: Processed<CommandModule>;
}> { }) {
protected override discordEvent: Observable<Interaction>; return (
constructor(wrapper: Wrapper) { match(module)
super(wrapper); .with({ type: CommandType.Text }, () => {
this.discordEvent = <Observable<Interaction>>fromEvent(this.client, 'interactionCreate'); throw Error(SernError.MismatchEvent);
this.init(); })
//P.union = either CommandType.Slash or CommandType.Both
this.payloadSubject .with({ type: P.union(CommandType.Slash, CommandType.Both) }, module => {
.pipe( if (event.isAutocomplete()) {
map(this.createDispatcher), /**
makeModuleExecutor(module => { * Autocomplete is a special case that
this.emitter.emit( * must be handled separately, since it's
'module.activate', * too different from regular command modules
SernEmitter.failure(module, SernError.PluginFailure), */
); return dispatchAutocomplete(module, event);
}),
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({
message: 'interactionCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
override init() {
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return this.modules.get(cb);
};
/**
* Module retrieval:
* ModuleStores are mapped by Discord API values and modules mapped
* by customId or command name.
*/
this.discordEvent.subscribe({
next: event => {
if (event.isMessageComponent()) {
const module = get(ms =>
ms.InteractionHandlers[event.componentType].get(event.customId),
);
this.setState({ event, module });
} else if (event.isCommand() || event.isAutocomplete()) {
const module = get(
ms =>
/**
* try to fetch from ApplicationCommands, if nothing, try BothCommands
* map. If nothing again,this means a slash command
* exists on the API but not sern
*/
ms.ApplicationCommands[event.commandType].get(event.commandName) ??
ms.BothCommands.get(event.commandName),
);
this.setState({ event, module });
} else if (event.isModalSubmit()) {
const module = get(ms => ms.ModalSubmit.get(event.customId));
this.setState({ event, module });
} else { } else {
throw Error('This interaction is not supported yet'); return dispatchCommand(module, contextArgs(event));
} }
}, })
error: reason => { /**
this.emitter.emit('error', SernEmitter.failure(undefined, reason)); * Every other command module takes a one argument parameter, its corresponding interaction
}, * this makes this usage safe
}); */
} .otherwise(mod => dispatchCommand(mod, interactionArg(event)))
);
protected setState(state: { event: Interaction; module: CommandModule | undefined }): void {
if (state.module === undefined) {
this.emitter.emit(
'warning',
SernEmitter.warning('Found no module for this interaction'),
);
} else {
//if statement above checks already, safe cast
this.payloadSubject.next(
state as { event: Interaction; module: Processed<CommandModule> },
);
}
}
protected createDispatcher({
module,
event,
}: {
event: Interaction;
module: Processed<CommandModule>;
}) {
return (
match(module)
.with({ type: CommandType.Text }, () =>
this.crashHandler.crash(Error(SernError.MismatchEvent)),
)
//P.union = either CommandType.Slash or CommandType.Both
.with({ type: P.union(CommandType.Slash, CommandType.Both) }, module => {
if (event.isAutocomplete()) {
/**
* Autocomplete is a special case that
* must be handled separately, since it's
* too different from regular command modules
*/
return dispatchAutocomplete(module, event);
} else {
return dispatchCommand(module, contextArgs(event));
}
})
/**
* Every other command module takes a one argument parameter, its corresponding interaction
* this makes this usage safe
*/
.otherwise(mod => dispatchCommand(mod, interactionArg(event)))
);
}
} }

View File

@@ -1,84 +1,79 @@
import { EventsHandler } from './eventsHandler'; import { catchError, concatMap, EMPTY, finalize, fromEvent, map, of, pipe } from 'rxjs';
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of } from 'rxjs'; import { type ModuleStore, SernError } from '../structures';
import { type Wrapper, type ModuleStore, SernError } from '../structures';
import type { Message } from 'discord.js'; import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling'; import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import { fmt } from '../utilities/messageHelpers'; import { fmt } from '../utilities/messageHelpers';
import type { CommandModule, Module, TextCommand } from '../../types/module'; import type { CommandModule, TextCommand } from '../../types/module';
import { handleError } from '../contracts/errorHandling'; import { ErrorHandling, handleError } from '../contracts/errorHandling';
import { contextArgs, dispatchCommand } from './dispatchers'; import { contextArgs, dispatchCommand } from './dispatchers';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler'; import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies'; import { useContainerRaw } from '../dependencies';
import type { Logging, ModuleManager } from '../contracts';
import type { EventEmitter } from 'node:events';
export default class MessageHandler extends EventsHandler<{ /**
module: Processed<Module>; * An operator function that processes a message to fetch a command module and prepares context payload.
args: unknown[]; * @param defaultPrefix
}> { * @param get
protected discordEvent: Observable<Message>; */
const createMessageProcessor = (
defaultPrefix: string,
get: (
cb: (ms: ModuleStore) => Processed<CommandModule> | undefined,
) => CommandModule | undefined,
) =>
pipe(
ignoreNonBot(defaultPrefix),
//This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix));
if (module === undefined) {
return EMPTY;
}
const payload = {
args: contextArgs(message, rest),
module,
};
return of(payload);
}),
map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)),
);
public constructor(protected wrapper: Wrapper) { export function makeMessageCreate(
super(wrapper); [s, client, err, log, modules]: [
this.discordEvent = <Observable<Message>>fromEvent(this.client, 'messageCreate'); SernEmitter,
this.init(); EventEmitter,
this.payloadSubject ErrorHandling,
.pipe( Logging | undefined,
makeModuleExecutor(module => { ModuleManager,
this.emitter.emit( ],
'module.activate', defaultPrefix?: string,
SernEmitter.failure(module, SernError.PluginFailure), ) {
); if (!defaultPrefix) {
}), return EMPTY.subscribe();
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({
message: 'messageCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
protected init(): void {
if (this.wrapper.defaultPrefix === undefined) return; //for now, just ignore if prefix doesn't exist
const { defaultPrefix } = this.wrapper;
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return this.modules.get(cb);
};
this.discordEvent
.pipe(
ignoreNonBot(this.wrapper.defaultPrefix),
//This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
const module = get(
ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix),
);
if (module === undefined) {
return EMPTY;
}
const payload = {
args: contextArgs(message, rest),
module,
};
return of(payload);
}),
map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)),
)
.subscribe({
next: value => this.setState(value),
error: reason => this.emitter.emit('error', SernEmitter.failure(reason)),
});
}
protected setState(state: { module: Processed<Module>; args: unknown[] }) {
this.payloadSubject.next(state);
} }
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return modules.get(cb);
};
const messageStream$ = fromEvent<Message>(client, 'messageCreate');
const messageProcessor = createMessageProcessor(defaultPrefix, get);
return messageStream$
.pipe(
messageProcessor,
makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(s, payload)),
catchError(handleError(err, log)),
finalize(() => {
log?.info({ message: 'messageCreate stream closed or reached end of lifetime' });
useContainerRaw()
?.disposeAll()
.then(() => log?.info({ message: 'Cleaning container and crashing' }));
}),
)
.subscribe();
} }

View File

@@ -94,7 +94,7 @@ export function executeModule(
* @returns receiver function for flattening a stream of data * @returns receiver function for flattening a stream of data
*/ */
export function createResultResolver< export function createResultResolver<
T extends { execute: (...args: any[]) => any; onEvent : ControlPlugin[] }, T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] },
Args extends { module: T; [key: string]: unknown }, Args extends { module: T; [key: string]: unknown },
Output, Output,
>(config: { >(config: {

View File

@@ -1,72 +1,58 @@
import { EventsHandler } from './eventsHandler'; import { fromEvent, pipe, switchMap, take } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { concatMap, fromEvent, type Observable, take } from 'rxjs';
import * as Files from '../utilities/readFile'; import * as Files from '../utilities/readFile';
import { errTap, scanModule } from './observableHandling'; import { errTap, scanModule } from './observableHandling';
import { CommandType, SernError, type ModuleStore } from '../structures'; import { CommandType, type ModuleStore, SernError } from '../structures';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { Result } from 'ts-results-es'; import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js'; import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module'; import type { CommandModule } from '../../types/module';
import type { Processed } from '../../types/handler'; import type { Processed } from '../../types/handler';
import type { ModuleManager } from '../contracts'; import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
import { _const, err, ok } from '../utilities/functions'; import { _const, err, ok } from '../utilities/functions';
import { defineAllFields } from './operators'; import { defineAllFields } from './operators';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import type { EventEmitter } from 'node:events';
export default class ReadyHandler extends EventsHandler<{ export function makeReadyEvent(
module: Processed<CommandModule>; [sEmitter, client, errorHandler, , moduleManager]: [
absPath: string; SernEmitter,
}> { EventEmitter,
protected discordEvent!: Observable<{ module: CommandModule; absPath: string }>; ErrorHandling,
constructor(wrapper: Wrapper) { Logging | undefined,
super(wrapper); ModuleManager,
const ready$ = fromEvent(this.client, 'ready').pipe(take(1)); ],
this.discordEvent = ready$.pipe( commandDir: string,
concatMap(() => ) {
Files.buildData<CommandModule>(wrapper.commands).pipe( const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
errTap(reason => { const parseCommandModules = pipe(
this.emitter.emit( switchMap(() => Files.buildData<CommandModule>(commandDir)),
'module.register', errTap(error => {
SernEmitter.failure(undefined, reason), sEmitter.emit('module.register', SernEmitter.failure(undefined, error));
); }),
}), defineAllFields(),
), );
), return readyOnce$
); .pipe(
this.init(); parseCommandModules,
this.payloadSubject scanModule({
.pipe( onFailure: module => {
scanModule({ sEmitter.emit(
onFailure: module => { 'module.register',
this.emitter.emit( SernEmitter.failure(module, SernError.PluginFailure),
'module.register', );
SernEmitter.failure(module, SernError.PluginFailure), },
); onSuccess: ({ module }) => {
}, sEmitter.emit('module.register', SernEmitter.success(module));
onSuccess: ({ module }) => { return module;
this.emitter.emit('module.register', SernEmitter.success(module)); },
return module; }),
}, )
}), .subscribe(module => {
) const result = registerModule(moduleManager, module as Processed<CommandModule>);
.subscribe(module => { if (result.err) {
const res = registerModule(this.modules, module as Processed<CommandModule>); errorHandler.crash(Error(SernError.InvalidModuleType));
if (res.err) { }
this.crashHandler.crash(Error(SernError.InvalidModuleType));
}
});
}
protected init() {
this.discordEvent.pipe(defineAllFields()).subscribe({
next: value => this.setState(value),
complete: () => this.payloadSubject.unsubscribe(),
}); });
}
protected setState(state: { absPath: string; module: Processed<CommandModule> }): void {
this.payloadSubject.next(state);
}
} }
function registerModule<T extends Processed<CommandModule>>( function registerModule<T extends Processed<CommandModule>>(

View File

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

View File

@@ -1,4 +1,4 @@
import { CommandType, EventType, PluginType } from '../structures/enums'; import { CommandType, EventType, PluginType } from '../structures';
import type { Plugin, PluginResult } from '../../types/plugin'; import type { Plugin, PluginResult } from '../../types/plugin';
import type { CommandArgs, EventArgs } from './args'; import type { CommandArgs, EventArgs } from './args';
import type { ClientEvents } from 'discord.js'; import type { ClientEvents } from 'discord.js';

View File

@@ -1,10 +1,10 @@
import type Wrapper from './structures/wrapper'; import type Wrapper from './structures/wrapper';
import { processEvents } from './events/userDefinedEventsHandling'; import { makeEventsHandler } from './events/userDefinedEventsHandling';
import { CommandType, EventType, PluginType } from './structures/enums'; import { CommandType, EventType, PluginType } from './structures/enums';
import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin'; import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin';
import InteractionHandler from './events/interactionHandler'; import { makeInteractionCreate } from './events/interactionHandler';
import ReadyHandler from './events/readyHandler'; import { makeReadyEvent } from './events/readyHandler';
import MessageHandler from './events/messageHandler'; import { makeMessageCreate } from './events/messageHandler';
import type { import type {
CommandModule, CommandModule,
CommandModuleDefs, CommandModuleDefs,
@@ -14,7 +14,7 @@ import type {
InputEvent, InputEvent,
} from '../types/module'; } from '../types/module';
import type { Dependencies, DependencyConfiguration } from '../types/handler'; import type { Dependencies, DependencyConfiguration } from '../types/handler';
import { composeRoot, useContainer } from './dependencies/provider'; import { composeRoot, makeFetcher, useContainer } from './dependencies/provider';
import type { Logging } from './contracts'; import type { Logging } from './contracts';
import { err, ok, partition } from './utilities/functions'; import { err, ok, partition } from './utilities/functions';
import type { Awaitable, ClientEvents } from 'discord.js'; import type { Awaitable, ClientEvents } from 'discord.js';
@@ -37,14 +37,15 @@ import type { Awaitable, ClientEvents } from 'discord.js';
*/ */
export function init(wrapper: Wrapper) { export function init(wrapper: Wrapper) {
const logger = wrapper.containerConfig.get('@sern/logger')[0] as Logging | undefined; const logger = wrapper.containerConfig.get('@sern/logger')[0] as Logging | undefined;
const requiredDependenciesAnd = makeFetcher(wrapper);
const startTime = performance.now(); const startTime = performance.now();
const { events } = wrapper; const { events } = wrapper;
if (events !== undefined) { if (events !== undefined) {
processEvents(wrapper); makeEventsHandler(requiredDependenciesAnd([]), events, wrapper.containerConfig);
} }
new ReadyHandler(wrapper); makeReadyEvent(requiredDependenciesAnd(['@sern/modules']), wrapper.commands);
new MessageHandler(wrapper); makeMessageCreate(requiredDependenciesAnd(['@sern/modules']), wrapper.defaultPrefix);
new InteractionHandler(wrapper); makeInteractionCreate(requiredDependenciesAnd(['@sern/modules']));
const endTime = performance.now(); const endTime = performance.now();
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` }); logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
} }

View File

@@ -5,6 +5,7 @@ import { Err, Ok } from 'ts-results-es';
* A function that returns whatever value is provided. * A function that returns whatever value is provided.
* Warning: this evaluates { @param value }. It does not defer a value. * Warning: this evaluates { @param value }. It does not defer a value.
* @param value * @param value
* @__PURE__
*/ */
// prettier-ignore // prettier-ignore
export const _const = <T>(value: T) => () => value; export const _const = <T>(value: T) => () => value;

View File

@@ -5,6 +5,7 @@ const shared = {
platform: 'node', platform: 'node',
clean: true, clean: true,
sourcemap: false, sourcemap: false,
minify: true,
}; };
export default defineConfig([ export default defineConfig([
{ {
@@ -12,7 +13,6 @@ export default defineConfig([
target: 'node16', target: 'node16',
tsconfig: './tsconfig-esm.json', tsconfig: './tsconfig-esm.json',
outDir: './dist/esm', outDir: './dist/esm',
external: ['discord.js'],
treeshake: true, treeshake: true,
outExtension() { outExtension() {
return { return {
@@ -24,7 +24,6 @@ export default defineConfig([
{ {
format: 'cjs', format: 'cjs',
splitting: false, splitting: false,
external: ['discord.js'],
target: 'node16', target: 'node16',
tsconfig: './tsconfig-cjs.json', tsconfig: './tsconfig-cjs.json',
outDir: './dist/cjs', outDir: './dist/cjs',