mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd7697300 | ||
|
|
f9609ce6cd | ||
|
|
a3064aa915 | ||
|
|
0a53a48521 | ||
|
|
05037b5315 | ||
|
|
06a3e69210 | ||
|
|
74c4b77d4b |
2
.github/workflows/npm-publish.yml
vendored
2
.github/workflows/npm-publish.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- recursive: true
|
- recursive: true
|
||||||
args: [--strict-peer-dependencies]
|
args: [--strict-peer-dependencies]
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm build
|
- run: pnpm build:prod
|
||||||
- uses: JS-DevTools/npm-publish@v1
|
- uses: JS-DevTools/npm-publish@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
token: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ typings/
|
|||||||
.node_repl_history
|
.node_repl_history
|
||||||
|
|
||||||
# Output of 'npm pack'
|
# Output of 'npm pack'
|
||||||
|
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|
||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
@@ -108,6 +109,8 @@ tsup.config.js
|
|||||||
|
|
||||||
tsconfig-base.json
|
tsconfig-base.json
|
||||||
|
|
||||||
tsconfig.cjs.json
|
tsconfig-cjs.json
|
||||||
|
|
||||||
tsconfig.esm.json
|
tsconfig-esm.json
|
||||||
|
|
||||||
|
renovate.json
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [2.6.1](https://github.com/sern-handler/handler/compare/v2.6.0...v2.6.1) (2023-03-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Chores
|
||||||
|
|
||||||
|
* release 2.6.1 ([f9609ce](https://github.com/sern-handler/handler/commit/f9609ce6cd777fa0eb595d8c48d57905bbce5966))
|
||||||
|
|
||||||
## [2.6.0](https://github.com/sern-handler/handler/compare/v2.5.3...v2.6.0) (2023-03-09)
|
## [2.6.0](https://github.com/sern-handler/handler/compare/v2.5.3...v2.6.0) (2023-03-09)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
82
README.md
82
README.md
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
- Most handlers don't support discord.js 14.7+
|
- Most handlers don't support discord.js 14.7+
|
||||||
- Customizable commands
|
- Customizable, composable commands
|
||||||
- Plug and play or customize to your liking
|
- Plug and play or customize to your liking
|
||||||
- Embraces reactive programming for consistent and reliable backend
|
- Embraces reactive programming for consistent and reliable backend
|
||||||
- Customizable logger, error handling, and more
|
- Customizable logger, error handling, and more
|
||||||
@@ -44,27 +44,54 @@ pnpm add @sern/handler
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 👶 Basic Usage
|
## 👶 Basic Usage
|
||||||
|
<details open><summary>ping.ts</summary>
|
||||||
|
|
||||||
#### ` index.js (CommonJS)`
|
```ts
|
||||||
|
export default commandModule({
|
||||||
```js
|
type: CommandType.Slash,
|
||||||
// Import the discord.js Client and GatewayIntentBits
|
//Installed plugin to publish to discord api and allow access to owners only.
|
||||||
const { Client, GatewayIntentBits } = require('discord.js');
|
plugins: [publish(), ownerOnly()],
|
||||||
|
description: 'A ping pong command',
|
||||||
// Import Sern namespace
|
execute(ctx) {
|
||||||
const { Sern, single } = require('@sern/handler');
|
ctx.reply('Hello owner of the bot');
|
||||||
|
}
|
||||||
const client = new Client({
|
|
||||||
intents: [
|
|
||||||
GatewayIntentBits.Guilds,
|
|
||||||
GatewayIntentBits.GuildMembers,
|
|
||||||
GatewayIntentBits.GuildMessages
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
export const useContainer = Sern.makeDependencies({
|
```
|
||||||
|
</details>
|
||||||
|
<details open><summary>modal.ts</summary>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Modal,
|
||||||
|
//Installed a plugin to make sure modal fields pass a validation.
|
||||||
|
plugins : [
|
||||||
|
assertFields({
|
||||||
|
fields: {
|
||||||
|
name: /^([^0-9]*)$/
|
||||||
|
},
|
||||||
|
failure: (errors, modal) => modal.reply('your submission did not pass the validations')
|
||||||
|
})
|
||||||
|
],
|
||||||
|
execute : (modal) => {
|
||||||
|
modal.reply('thanks for the submission!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
<details open><summary>index.ts</summary>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Client, GatewayIntentBits } from 'discord.js';
|
||||||
|
import { Sern, single, type Dependencies } from '@sern/handler';
|
||||||
|
|
||||||
|
//client has been declared previously
|
||||||
|
|
||||||
|
interface MyDependencies extends Dependencies {
|
||||||
|
'@sern/client': Singleton<Client>;
|
||||||
|
}
|
||||||
|
export const useContainer = Sern.makeDependencies<MyDependencies>({
|
||||||
build: root => root
|
build: root => root
|
||||||
.add({ '@sern/client': single(() => client) })
|
.add({ '@sern/client': single(() => client) })
|
||||||
.upsert({ '@sern/logger': single(() => new DefaultLogging()) })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//View docs for all options
|
//View docs for all options
|
||||||
@@ -73,26 +100,13 @@ Sern.init({
|
|||||||
commands: 'src/commands',
|
commands: 'src/commands',
|
||||||
// events: 'src/events' (optional),
|
// events: 'src/events' (optional),
|
||||||
containerConfig : {
|
containerConfig : {
|
||||||
get: useContainer
|
get: useContainer
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.login("YOUR_BOT_TOKEN_HERE");
|
client.login("YOUR_BOT_TOKEN_HERE");
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
#### ` ping.js (CommonJS)`
|
|
||||||
|
|
||||||
```js
|
|
||||||
const { CommandType, commandModule } = require('@sern/handler');
|
|
||||||
|
|
||||||
exports.default = commandModule({
|
|
||||||
type: CommandType.Slash,
|
|
||||||
description: 'A ping pong command',
|
|
||||||
execute(ctx) {
|
|
||||||
ctx.reply('pong!');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🤖 Bots Using sern
|
## 🤖 Bots Using sern
|
||||||
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).
|
- [Community Bot](https://github.com/sern-handler/sern-community), the community bot for our [discord server](https://sern.dev/discord).
|
||||||
@@ -112,9 +126,7 @@ It is **highly encouraged** to use the [command line interface](https://github.c
|
|||||||
- [Support Server](https://sern.dev/discord)
|
- [Support Server](https://sern.dev/discord)
|
||||||
|
|
||||||
## 👋 Contribute
|
## 👋 Contribute
|
||||||
|
|
||||||
- Read our contribution [guidelines](https://github.com/sern-handler/handler/blob/main/.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
|
- Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs
|
||||||
- All kinds of contributions are welcomed.
|
- All kinds of contributions are welcomed.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@sern/handler",
|
"name": "@sern/handler",
|
||||||
"packageManager": "pnpm@7.28.0",
|
"packageManager": "pnpm@7.28.0",
|
||||||
"version": "2.6.0",
|
"version": "2.6.1",
|
||||||
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.",
|
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
|
||||||
"main": "dist/cjs/index.cjs",
|
"main": "dist/cjs/index.cjs",
|
||||||
"module": "dist/esm/index.mjs",
|
"module": "dist/esm/index.mjs",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@@ -13,12 +13,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsup --dts --watch",
|
"watch": "tsup --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",
|
||||||
"build": "tsup && node scripts/mkjson.mjs dist/cjs dist/esm && tsup --dts-only --outDir dist",
|
"build:dev": "tsup && tsup --dts-only --outDir dist",
|
||||||
"publish": "npm run build && npm publish",
|
"build:prod": "tsup --minify && tsup --dts-only --outDir dist",
|
||||||
|
"publish": "npm run build:prod && npm publish",
|
||||||
"pretty": "prettier --write ."
|
"pretty": "prettier --write ."
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -35,17 +36,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iti": "^0.6.0",
|
"iti": "^0.6.0",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"ts-pattern": "^4.1.4",
|
|
||||||
"ts-results-es": "^3.5.0"
|
"ts-results-es": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "5.54.0",
|
"@typescript-eslint/eslint-plugin": "5.54.0",
|
||||||
"@typescript-eslint/parser": "5.54.0",
|
"@typescript-eslint/parser": "5.54.0",
|
||||||
"discord.js": "^14.7.1",
|
"discord.js": "^14.8.0",
|
||||||
|
"esbuild-ifdef": "^0.2.0",
|
||||||
"eslint": "8.30.0",
|
"eslint": "8.30.0",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.4",
|
||||||
"tsup": "^6.6.3",
|
"typescript": "4.9.5",
|
||||||
"typescript": "4.9.5"
|
"tsup": "^6.6.3"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
4361
pnpm-lock.yaml
generated
4361
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,6 @@
|
|||||||
"schedule": ["every weekend"],
|
"schedule": ["every weekend"],
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"automerge": false
|
"automerge": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { writeFile } from 'fs/promises';
|
|
||||||
import { join } from 'path';
|
|
||||||
// A quick script to regenerate package.jsons for each cjs and esm after tsup cleans distributions
|
|
||||||
const locations = process.argv;
|
|
||||||
locations.shift();
|
|
||||||
locations.shift();
|
|
||||||
for (const loc of locations) {
|
|
||||||
if (loc.endsWith('cjs')) {
|
|
||||||
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'commonjs' }));
|
|
||||||
} else {
|
|
||||||
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'module' }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Observable } from 'rxjs';
|
import type { Observable } from 'rxjs';
|
||||||
import type { Logging } from './logging';
|
import type { Logging } from './logging';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
|
||||||
export interface ErrorHandling {
|
export interface ErrorHandling {
|
||||||
/**
|
/**
|
||||||
* Number of times the process should throw an error until crashing and exiting
|
* Number of times the process should throw an error until crashing and exiting
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
pipe,
|
pipe,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { CommandType, type ModuleStore, SernError } from '../structures';
|
import { CommandType, type ModuleStore, SernError } from '../structures';
|
||||||
import { match, P } from 'ts-pattern';
|
|
||||||
import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } 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';
|
||||||
@@ -38,14 +37,15 @@ function makeInteractionProcessor(
|
|||||||
);
|
);
|
||||||
return of({ module, event });
|
return of({ module, event });
|
||||||
} else if (event.isCommand() || event.isAutocomplete()) {
|
} else if (event.isCommand() || event.isAutocomplete()) {
|
||||||
|
const commandName = event.commandName;
|
||||||
const module = get(
|
const module = get(
|
||||||
ms =>
|
ms =>
|
||||||
/**
|
/**
|
||||||
* try to fetch from ApplicationCommands, if nothing, try BothCommands
|
* try to fetch from ApplicationCommands, if nothing, try BothCommands
|
||||||
* exists on the API but not sern
|
* exists on the API but not sern
|
||||||
*/
|
*/
|
||||||
ms.ApplicationCommands[event.commandType].get(event.commandName) ??
|
ms.ApplicationCommands[event.commandType].get(commandName) ??
|
||||||
ms.BothCommands.get(event.commandName),
|
ms.BothCommands.get(commandName),
|
||||||
);
|
);
|
||||||
return of({ module, event });
|
return of({ module, event });
|
||||||
} else if (event.isModalSubmit()) {
|
} else if (event.isModalSubmit()) {
|
||||||
@@ -95,28 +95,21 @@ function createDispatcher({
|
|||||||
event: Interaction;
|
event: Interaction;
|
||||||
module: Processed<CommandModule>;
|
module: Processed<CommandModule>;
|
||||||
}) {
|
}) {
|
||||||
return (
|
switch(module.type) {
|
||||||
match(module)
|
case CommandType.Text:
|
||||||
.with({ type: CommandType.Text }, () => {
|
throw Error(SernError.MismatchEvent);
|
||||||
throw Error(SernError.MismatchEvent);
|
case CommandType.Slash: case CommandType.Both : {
|
||||||
})
|
if(event.isAutocomplete()) {
|
||||||
//P.union = either CommandType.Slash or CommandType.Both
|
/**
|
||||||
.with({ type: P.union(CommandType.Slash, CommandType.Both) }, module => {
|
* Autocomplete is a special case that
|
||||||
if (event.isAutocomplete()) {
|
* must be handled separately, since it's
|
||||||
/**
|
* too different from regular command modules
|
||||||
* Autocomplete is a special case that
|
*/
|
||||||
* must be handled separately, since it's
|
return dispatchAutocomplete(module, event);
|
||||||
* too different from regular command modules
|
} else {
|
||||||
*/
|
return dispatchCommand(module, contextArgs(event));
|
||||||
return dispatchAutocomplete(module, event);
|
}
|
||||||
} else {
|
}
|
||||||
return dispatchCommand(module, contextArgs(event));
|
default : return dispatchCommand(module, interactionArg(event));
|
||||||
}
|
}
|
||||||
})
|
|
||||||
/**
|
|
||||||
* Every other command module takes a one argument parameter, its corresponding interaction
|
|
||||||
* this makes this usage safe
|
|
||||||
*/
|
|
||||||
.otherwise(mod => dispatchCommand(mod, interactionArg(event)))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const createMessageProcessor = (
|
|||||||
//This concatMap checks if module is undefined, and if it is, do not continue.
|
//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
|
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
|
||||||
concatMap(message => {
|
concatMap(message => {
|
||||||
const [prefix, ...rest] = fmt(message, defaultPrefix);
|
const [prefix, ...rest] = fmt(message.content, defaultPrefix);
|
||||||
const module = 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) {
|
if (module === undefined) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { Awaitable, Message } from 'discord.js';
|
import type { Awaitable, Message } from 'discord.js';
|
||||||
import { concatMap, EMPTY, from, Observable, of, pipe, tap, throwError } from 'rxjs';
|
import { concatMap, EMPTY, from, Observable, of, pipe, tap, throwError } from 'rxjs';
|
||||||
import type { SernError } from '../structures';
|
|
||||||
import { Result } from 'ts-results-es';
|
import { Result } from 'ts-results-es';
|
||||||
import type { AnyModule, CommandModule, EventModule, Module } from '../../types/module';
|
import type { CommandModule, EventModule, Module } from '../../types/module';
|
||||||
import { _const as i } from '../utilities/functions';
|
|
||||||
import SernEmitter from '../sernEmitter';
|
import SernEmitter from '../sernEmitter';
|
||||||
import { callPlugin, everyPluginOk, filterMapTo } from './operators';
|
import { callPlugin, everyPluginOk, filterMapTo } from './operators';
|
||||||
import type { Processed } from '../../types/handler';
|
import type { Processed } from '../../types/handler';
|
||||||
@@ -31,27 +29,6 @@ export function ignoreNonBot<T extends Message>(prefix: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If the current value in Result stream is an error, calls callback.
|
|
||||||
* This also extracts the Ok value from Result
|
|
||||||
* @param cb
|
|
||||||
* @returns Observable<{ module: T; absPath: string }>
|
|
||||||
*/
|
|
||||||
export function errTap<T extends AnyModule>(cb: (err: SernError) => void) {
|
|
||||||
return (src: Observable<Result<{ module: T; absPath: string }, SernError>>) =>
|
|
||||||
new Observable<{ module: T; absPath: string }>(subscriber => {
|
|
||||||
return src.subscribe({
|
|
||||||
next(value) {
|
|
||||||
if (value.err) {
|
|
||||||
cb(value.val);
|
|
||||||
} else {
|
|
||||||
subscriber.next(value.val);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the task in a Result as a try / catch.
|
* 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 ok, an event is emitted and the stream becomes empty
|
||||||
@@ -78,7 +55,7 @@ export function executeModule(
|
|||||||
emitter.emit('module.activate', SernEmitter.success(module));
|
emitter.emit('module.activate', SernEmitter.success(module));
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
} else {
|
} else {
|
||||||
return throwError(i(SernEmitter.failure(module, result.val)));
|
return throwError(() => SernEmitter.failure(module, result.val));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -120,7 +97,7 @@ export function createResultResolver<
|
|||||||
* Calls a module's init plugins and checks for Err. If so, call { onFailure } and
|
* Calls a module's init plugins and checks for Err. If so, call { onFailure } and
|
||||||
* ignore the module
|
* ignore the module
|
||||||
*/
|
*/
|
||||||
export function scanModule<
|
export function callInitPlugins<
|
||||||
T extends Processed<CommandModule | EventModule>,
|
T extends Processed<CommandModule | EventModule>,
|
||||||
Args extends { module: T; absPath: string },
|
Args extends { module: T; absPath: string },
|
||||||
>(config: { onFailure?: (module: T) => unknown; onSuccess: (module: Args) => T }) {
|
>(config: { onFailure?: (module: T) => unknown; onSuccess: (module: Args) => T }) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { nameOrFilename } from '../../utilities/functions';
|
|||||||
import type { PluginResult, VoidResult } from '../../../types/plugin';
|
import type { PluginResult, VoidResult } from '../../../types/plugin';
|
||||||
import { guayin } from '../../plugins';
|
import { guayin } from '../../plugins';
|
||||||
import { controller } from '../../sern';
|
import { controller } from '../../sern';
|
||||||
|
import { SernError } from '../../structures';
|
||||||
|
import { Result } from 'ts-results-es';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if {src} is true, mapTo V, else ignore
|
* if {src} is true, mapTo V, else ignore
|
||||||
@@ -48,7 +50,7 @@ export function callPlugin(args: unknown): OperatorFunction<
|
|||||||
* operator function that fill the defaults for a module
|
* operator function that fill the defaults for a module
|
||||||
*/
|
*/
|
||||||
export function defineAllFields<T extends AnyModule>() {
|
export function defineAllFields<T extends AnyModule>() {
|
||||||
const fillFields = ({ absPath, module }: { absPath: string; module: T }) => ({
|
const fillFields = ({ module, absPath }: { module: T; absPath: string }) => ({
|
||||||
absPath,
|
absPath,
|
||||||
module: {
|
module: {
|
||||||
name: nameOrFilename(module.name, absPath),
|
name: nameOrFilename(module.name, absPath),
|
||||||
@@ -59,6 +61,27 @@ export function defineAllFields<T extends AnyModule>() {
|
|||||||
return pipe(map(fillFields));
|
return pipe(map(fillFields));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
): OperatorFunction<Result<{ module: T; absPath: string}, SernError>, { module: T; absPath: string }> {
|
||||||
|
return pipe(
|
||||||
|
concatMap(result => {
|
||||||
|
if(result.ok) {
|
||||||
|
return of(result.val);
|
||||||
|
} else {
|
||||||
|
cb(result.val);
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the stream of results is all ok.
|
* Checks if the stream of results is all ok.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
import { fromEvent, pipe, switchMap, take } from 'rxjs';
|
import { fromEvent, pipe, switchMap, take } from 'rxjs';
|
||||||
import * as Files from '../utilities/readFile';
|
import * as Files from '../module-loading/readFile';
|
||||||
import { errTap, scanModule } from './observableHandling';
|
import { callInitPlugins } from './observableHandling';
|
||||||
import { CommandType, type ModuleStore, SernError } from '../structures';
|
import { CommandType, type ModuleStore, SernError } from '../structures';
|
||||||
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 { ErrorHandling, Logging, ModuleManager } from '../contracts';
|
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
|
||||||
import { _const, err, ok } from '../utilities/functions';
|
import { err, ok } from '../utilities/functions';
|
||||||
import { defineAllFields } from './operators';
|
import { defineAllFields, errTap } from './operators';
|
||||||
import SernEmitter from '../sernEmitter';
|
import SernEmitter from '../sernEmitter';
|
||||||
import type { EventEmitter } from 'node:events';
|
import type { EventEmitter } from 'node:events';
|
||||||
|
|
||||||
|
|
||||||
|
function buildCommandModules(
|
||||||
|
commandDir: string,
|
||||||
|
sernEmitter: SernEmitter
|
||||||
|
) {
|
||||||
|
return pipe(
|
||||||
|
switchMap(() => Files.buildData<CommandModule>(commandDir)),
|
||||||
|
errTap(error => {
|
||||||
|
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
|
||||||
|
}),
|
||||||
|
defineAllFields(),
|
||||||
|
);
|
||||||
|
}
|
||||||
export function makeReadyEvent(
|
export function makeReadyEvent(
|
||||||
[sEmitter, client, errorHandler, , moduleManager]: [
|
[sEmitter, client, errorHandler, , moduleManager]: [
|
||||||
SernEmitter,
|
SernEmitter,
|
||||||
@@ -24,24 +36,17 @@ export function makeReadyEvent(
|
|||||||
commandDir: string,
|
commandDir: string,
|
||||||
) {
|
) {
|
||||||
const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
|
const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
|
||||||
const parseCommandModules = pipe(
|
|
||||||
switchMap(() => Files.buildData<CommandModule>(commandDir)),
|
|
||||||
errTap(error => {
|
|
||||||
sEmitter.emit('module.register', SernEmitter.failure(undefined, error));
|
|
||||||
}),
|
|
||||||
defineAllFields(),
|
|
||||||
);
|
|
||||||
return readyOnce$
|
return readyOnce$
|
||||||
.pipe(
|
.pipe(
|
||||||
parseCommandModules,
|
buildCommandModules(commandDir, sEmitter),
|
||||||
scanModule({
|
callInitPlugins({
|
||||||
onFailure: module => {
|
onFailure: module => {
|
||||||
sEmitter.emit(
|
sEmitter.emit(
|
||||||
'module.register',
|
'module.register',
|
||||||
SernEmitter.failure(module, SernError.PluginFailure),
|
SernEmitter.failure(module, SernError.PluginFailure),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuccess: ({ module }) => {
|
onSuccess: ({ module }) => {
|
||||||
sEmitter.emit('module.register', SernEmitter.success(module));
|
sEmitter.emit('module.register', SernEmitter.success(module));
|
||||||
return module;
|
return module;
|
||||||
},
|
},
|
||||||
@@ -61,45 +66,38 @@ function registerModule<T extends Processed<CommandModule>>(
|
|||||||
): Result<void, void> {
|
): Result<void, void> {
|
||||||
const name = mod.name;
|
const name = mod.name;
|
||||||
const insert = (cb: (ms: ModuleStore) => void) => {
|
const insert = (cb: (ms: ModuleStore) => void) => {
|
||||||
const set = Result.wrap(_const(manager.set(cb)));
|
const set = Result.wrap(() => manager.set(cb));
|
||||||
return set.ok ? ok() : err();
|
return set.ok ? ok() : err();
|
||||||
};
|
};
|
||||||
return match(mod as Processed<CommandModule>)
|
switch(mod.type) {
|
||||||
.with({ type: CommandType.Text }, mod => {
|
case CommandType.Text: {
|
||||||
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
|
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
|
||||||
return insert(ms => ms.TextCommands.set(name, mod));
|
return insert(ms => ms.TextCommands.set(name, mod));
|
||||||
})
|
}
|
||||||
.with({ type: CommandType.Slash }, mod =>
|
case CommandType.Slash:
|
||||||
insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod)),
|
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod));
|
||||||
)
|
case CommandType.Both: {
|
||||||
.with({ type: CommandType.Both }, mod => {
|
|
||||||
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
|
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
|
||||||
return insert(ms => ms.BothCommands.set(name, mod));
|
return insert(ms => ms.BothCommands.set(name, mod));
|
||||||
})
|
}
|
||||||
.with({ type: CommandType.CtxUser }, mod =>
|
case CommandType.CtxUser:
|
||||||
insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod)),
|
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
|
||||||
)
|
case CommandType.CtxMsg:
|
||||||
.with({ type: CommandType.CtxMsg }, mod =>
|
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod));
|
||||||
insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod)),
|
case CommandType.Button:
|
||||||
)
|
return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
|
||||||
.with({ type: CommandType.Button }, mod =>
|
case CommandType.StringSelect:
|
||||||
insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod)),
|
return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
|
||||||
)
|
case CommandType.MentionableSelect:
|
||||||
.with({ type: CommandType.StringSelect }, mod =>
|
return insert(ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod));
|
||||||
insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod)),
|
case CommandType.UserSelect:
|
||||||
)
|
return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
|
||||||
.with({ type: CommandType.MentionableSelect }, mod =>
|
case CommandType.ChannelSelect:
|
||||||
insert(ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod)),
|
return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
|
||||||
)
|
case CommandType.RoleSelect:
|
||||||
.with({ type: CommandType.ChannelSelect }, mod =>
|
return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
|
||||||
insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod)),
|
case CommandType.Modal:
|
||||||
)
|
return insert(ms => ms.ModalSubmit.set(name, mod));
|
||||||
.with({ type: CommandType.UserSelect }, mod =>
|
default: return err();
|
||||||
insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod)),
|
}
|
||||||
)
|
|
||||||
.with({ type: CommandType.RoleSelect }, mod =>
|
|
||||||
insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod)),
|
|
||||||
)
|
|
||||||
.with({ type: CommandType.Modal }, mod => insert(ms => ms.ModalSubmit.set(name, mod)))
|
|
||||||
.otherwise(err);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { catchError, finalize, map, tap, mergeAll } from 'rxjs';
|
import { catchError, finalize, map, mergeAll } from 'rxjs';
|
||||||
import { buildData } from '../utilities/readFile';
|
import { buildData } from '../module-loading/readFile';
|
||||||
import type { Dependencies, Processed } from '../../types/handler';
|
import type { Dependencies, Processed } from '../../types/handler';
|
||||||
import { errTap, scanModule } from './observableHandling';
|
import { callInitPlugins } from './observableHandling';
|
||||||
import type { CommandModule, EventModule } from '../../types/module';
|
import type { CommandModule, EventModule } from '../../types/module';
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
import SernEmitter from '../sernEmitter';
|
import SernEmitter from '../sernEmitter';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
import type { ErrorHandling, Logging } from '../contracts';
|
import type { ErrorHandling, Logging } from '../contracts';
|
||||||
import { SernError, EventType, type Wrapper } from '../structures';
|
import { SernError, EventType, type Wrapper } from '../structures';
|
||||||
import { eventDispatcher } from './dispatchers';
|
import { eventDispatcher } from './dispatchers';
|
||||||
import { handleError } from '../contracts/errorHandling';
|
import { handleError } from '../contracts/errorHandling';
|
||||||
import { defineAllFields } from './operators';
|
import { defineAllFields, errTap } from './operators';
|
||||||
import { useContainerRaw } from '../dependencies';
|
import { useContainerRaw } from '../dependencies';
|
||||||
|
|
||||||
export function makeEventsHandler(
|
export function makeEventsHandler(
|
||||||
@@ -23,20 +22,22 @@ export function makeEventsHandler(
|
|||||||
|
|
||||||
const eventCreation$ = eventStream$.pipe(
|
const eventCreation$ = eventStream$.pipe(
|
||||||
defineAllFields(),
|
defineAllFields(),
|
||||||
scanModule({
|
callInitPlugins({
|
||||||
onFailure: module => s.emit('module.register', SernEmitter.success(module)),
|
onFailure: module => s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
|
||||||
onSuccess: ({ module }) => {
|
onSuccess: ({ module }) => {
|
||||||
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure));
|
s.emit('module.register', SernEmitter.success(module));
|
||||||
return module;
|
return module;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const intoDispatcher = (e: Processed<EventModule | CommandModule>) =>
|
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
|
||||||
match(e)
|
switch(e.type) {
|
||||||
.with({ type: EventType.Sern }, m => eventDispatcher(m, s))
|
case EventType.Sern: return eventDispatcher(e, s);
|
||||||
.with({ type: EventType.Discord }, m => eventDispatcher(m, client))
|
case EventType.Discord: return eventDispatcher(e, client);
|
||||||
.with({ type: EventType.External }, m => eventDispatcher(m, lazy(m.emitter)))
|
case EventType.External: return eventDispatcher(e, lazy(e.emitter));
|
||||||
.otherwise(() => err.crash(Error(SernError.InvalidModuleType)));
|
default: err.crash(Error(SernError.InvalidModuleType + ' while creating event handler'));
|
||||||
|
}
|
||||||
|
};
|
||||||
eventCreation$
|
eventCreation$
|
||||||
.pipe(
|
.pipe(
|
||||||
map(intoDispatcher),
|
map(intoDispatcher),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readdirSync, statSync } from 'fs';
|
import { readdirSync, statSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { type Observable, from, concatAll } from 'rxjs';
|
import { type Observable, from, mergeAll } from 'rxjs';
|
||||||
import { SernError } from '../structures/errors';
|
import { SernError } from '../structures/errors';
|
||||||
import { type Result, Err, Ok } from 'ts-results-es';
|
import { type Result, Err, Ok } from 'ts-results-es';
|
||||||
|
|
||||||
@@ -18,16 +18,13 @@ function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
|
|||||||
|
|
||||||
return arrayOfFiles;
|
return arrayOfFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
|
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a directory string is converted into a stream of modules.
|
* a directory string is converted into a stream of modules.
|
||||||
* starts the stream of modules that sern needs to process on init
|
* starts the stream of modules that sern needs to process on init
|
||||||
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
|
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
|
||||||
* @param commandDir
|
* @param commandDir
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function buildData<T>(commandDir: string): Observable<
|
export function buildData<T>(commandDir: string): Observable<
|
||||||
Result<
|
Result<
|
||||||
{
|
{
|
||||||
@@ -41,13 +38,14 @@ export function buildData<T>(commandDir: string): Observable<
|
|||||||
return from(
|
return from(
|
||||||
Promise.all(
|
Promise.all(
|
||||||
commands.map(async absPath => {
|
commands.map(async absPath => {
|
||||||
let module: T | undefined;
|
// prettier-ignore
|
||||||
try {
|
let module: T | undefined
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
/// #if MODE === 'esm'
|
||||||
module = require(absPath).default;
|
= (await import(`file:///` + absPath)).default
|
||||||
} catch {
|
/// #elif MODE === 'cjs'
|
||||||
module = (await import(`file:///` + absPath)).default;
|
= require(absPath).default; // eslint-disable-line
|
||||||
}
|
/// #endif
|
||||||
|
|
||||||
if (module === undefined) {
|
if (module === undefined) {
|
||||||
return Err(SernError.UndefinedModule);
|
return Err(SernError.UndefinedModule);
|
||||||
}
|
}
|
||||||
@@ -57,7 +55,7 @@ export function buildData<T>(commandDir: string): Observable<
|
|||||||
return Ok({ module, absPath });
|
return Ok({ module, absPath });
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).pipe(concatAll());
|
).pipe(mergeAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommands(dir: string): string[] {
|
export function getCommands(dir: string): string[] {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as Files from './readFile';
|
import * as Files from '../module-loading/readFile';
|
||||||
import { basename } from 'path';
|
import { basename } from 'path';
|
||||||
import { Err, Ok } from 'ts-results-es';
|
import { Err, Ok } from 'ts-results-es';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { Message } from 'discord.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the first character(s) _[depending on prefix length]_ of the message
|
* Removes the first character(s) _[depending on prefix length]_ of the message
|
||||||
* @param msg
|
* @param msg
|
||||||
@@ -10,6 +8,6 @@ import type { Message } from 'discord.js';
|
|||||||
* console.log(fmt(message, '!'));
|
* console.log(fmt(message, '!'));
|
||||||
* // [ 'ping' ]
|
* // [ 'ping' ]
|
||||||
*/
|
*/
|
||||||
export function fmt(msg: Message, prefix: string): string[] {
|
export function fmt(msg: string, prefix: string): string[] {
|
||||||
return msg.content.slice(prefix.length).trim().split(/\s+/g);
|
return msg.slice(prefix.length).trim().split(/\s+/g);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import type {
|
|||||||
MentionableSelectMenuInteraction,
|
MentionableSelectMenuInteraction,
|
||||||
RoleSelectMenuInteraction,
|
RoleSelectMenuInteraction,
|
||||||
StringSelectMenuInteraction,
|
StringSelectMenuInteraction,
|
||||||
|
UserSelectMenuInteraction
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { CommandType } from '../handler/structures/enums';
|
import { CommandType } from '../handler/structures/enums';
|
||||||
import type { Args, SlashOptions } from './handler';
|
import type { Args, SlashOptions } from './handler';
|
||||||
import type Context from '../handler/structures/context';
|
import type Context from '../handler/structures/context';
|
||||||
import type { InitPlugin, ControlPlugin } from './plugin';
|
import type { InitPlugin, ControlPlugin } from './plugin';
|
||||||
import { EventType } from '../handler/structures/enums';
|
import { EventType } from '../handler/structures/enums';
|
||||||
import type { UserSelectMenuInteraction } from 'discord.js';
|
|
||||||
import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
|
import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
|
||||||
import type { SernEventsMapping } from './handler';
|
import type { SernEventsMapping } from './handler';
|
||||||
import type { ClientEvents } from 'discord.js';
|
import type { ClientEvents } from 'discord.js';
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { defineConfig } from 'tsup';
|
import { defineConfig } from 'tsup';
|
||||||
|
import { writeFile } from 'fs/promises';
|
||||||
|
import ifdefPlugin from 'esbuild-ifdef';
|
||||||
const shared = {
|
const shared = {
|
||||||
entry: ['src/index.ts'],
|
entry: ['src/index.ts'],
|
||||||
external: ['discord.js'],
|
external: ['discord.js'],
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
clean: true,
|
clean: true,
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
minify: true,
|
|
||||||
};
|
};
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
@@ -14,15 +15,21 @@ export default defineConfig([
|
|||||||
tsconfig: './tsconfig-esm.json',
|
tsconfig: './tsconfig-esm.json',
|
||||||
outDir: './dist/esm',
|
outDir: './dist/esm',
|
||||||
treeshake: true,
|
treeshake: true,
|
||||||
|
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'esm' }, verbose: true })],
|
||||||
outExtension() {
|
outExtension() {
|
||||||
return {
|
return {
|
||||||
js: '.mjs',
|
js: '.mjs',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async onSuccess() {
|
||||||
|
console.log('writing json esm');
|
||||||
|
await writeFile('./dist/esm/package.json', JSON.stringify({ type: 'module' }));
|
||||||
|
},
|
||||||
...shared,
|
...shared,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
|
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'cjs' }, verbose: true })],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
target: 'node16',
|
target: 'node16',
|
||||||
tsconfig: './tsconfig-cjs.json',
|
tsconfig: './tsconfig-cjs.json',
|
||||||
@@ -32,6 +39,10 @@ export default defineConfig([
|
|||||||
js: '.cjs',
|
js: '.cjs',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async onSuccess() {
|
||||||
|
console.log('writing json commonjs');
|
||||||
|
await writeFile('./dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }));
|
||||||
|
},
|
||||||
...shared,
|
...shared,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user