Compare commits

..

26 Commits

Author SHA1 Message Date
github-actions[bot]
d983f95906 chore(main): release 2.6.2 (#281)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 12:42:59 -05:00
Jacob Nguyen
c1f690633c chore: release 2.6.2
Release-As: 2.6.2
2023-04-15 12:40:17 -05:00
Jacob Nguyen
8544d301ef bump version 2023-04-15 12:19:12 -05:00
Jacob Nguyen
52bcba9cfc docs: add deprecation warning 2023-04-15 12:16:35 -05:00
xxDeveloper
21febd2c90 chore: Update SECURITY.md (#276)
Semantic security file

Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 22:46:02 -05:00
xxDeveloper
11daebb30a chore: Update LICENSE (#275)
We're in 2023
We're sern, not Sern

Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 22:45:45 -05:00
github-actions[bot]
b817f98c10 style: pretty please (#277)
Co-authored-by: EvolutionX-10 <EvolutionX-10@users.noreply.github.com>
2023-04-11 22:45:30 -05:00
Evo
563f583318 chore: switch to yarn (#273)
* chore: switch to yarn

* chore: pointless limitation

permalink: http://whatthecommit.com/468a491808723d12de48b079d9092b44

* chore: i can't believe it took so long to fix this.

permalink: http://whatthecommit.com/b298fe6d3375ab953abfdb0f1f737826
2023-04-11 12:45:16 -05:00
EvolutionX
e4c7bfe686 chore: ok work pls 2023-04-11 22:32:31 +05:30
EvolutionX
69fa4908c3 chore: refresh lockfile 2023-04-11 22:09:32 +05:30
renovate[bot]
4fa28d605f chore(deps): lock file maintenance (#245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:32:46 -05:00
Jacob Nguyen
079b554f8b Update continuous-integration.yml 2023-04-11 10:31:15 -05:00
Jacob Nguyen
dec56335b9 Update codeql-analysis.yml 2023-04-11 10:30:41 -05:00
Jacob Nguyen
50be972d4f Update continuous-integration.yml 2023-04-11 10:29:06 -05:00
Jacob Nguyen
507d183970 Update codeql-analysis.yml 2023-04-11 10:28:29 -05:00
renovate[bot]
90edd4f91e chore(deps): update dependency eslint to v8.38.0 (#180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:24:32 -05:00
renovate[bot]
5f11142599 chore(deps): update dependency @typescript-eslint/parser to v5.58.0 (#250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 10:22:27 -05:00
renovate[bot]
7a635f9978 chore(deps): update actions/checkout digest to 8f4b7f8 (#261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 10:21:59 -05:00
renovate[bot]
a17aeac558 chore(deps): update pnpm to v7.32.0 (#262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-04-11 10:21:39 -05:00
renovate[bot]
af6ebed348 chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.58.0 (#249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:18:46 -05:00
renovate[bot]
2f96b7634d chore(deps): update dependency prettier to v2.8.7 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 10:18:17 -05:00
EvolutionX
97741faa69 chore: refresh lockfile 2023-04-11 16:02:13 +05:30
Jacob Nguyen
94070d99e8 refactor/decoupling (#265)
* fix npm script for workflows

* filter lazy modules

* lift inline function for readability

* perf: use one instance of operator instead of creating instances

* chore: move fmt closer to call site

* refactor: inline function lifting and readability

* add import payload type

* refactor: remove redundant pipe for single function operators

* refactor: clearer naming for resultResolver

* refactor: no unused variable warning for updateAlive

* style: pretty

* refactor: remove redundant getter

* style: pretty

* fix: typescript needs explicit definition for defineAllFields

* add LazyPaths map

* chore: update tsup and typescript

* chore: revert lazy module work and work on decoupling core

* fix npm script for workflows

* chore: fix typings

* refactor: inline function `defineAllFields`

* docs: add @since annotation

* style: prettier

* docs: add since annotations

* fix: typings

* chore: update dependencies

* chore: remove unused import

* style: pretty

* merge on home pc

* refactor: use dependencies less

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-04-10 22:12:26 -05:00
Jacob Nguyen
473be775f0 Update README.md 2023-03-29 15:12:26 -05:00
Neo
36af102251 docs: removed ALMA (#264)
Not working on it anymore, also not running it.
2023-03-29 12:55:16 -05:00
github-actions[bot]
cee740ea3f style: pretty please (#260)
Co-authored-by: renovate[bot] <renovate[bot]@users.noreply.github.com>
2023-03-17 17:01:20 -05:00
37 changed files with 4406 additions and 2237 deletions

36
.github/SECURITY.md vendored
View File

@@ -6,13 +6,43 @@ Project is currently under heavy development but you can try out our [npm packag
| Version | Supported |
| ------- | ------------------ |
| 0.1.0 @ dev | :white_check_mark: |
| 2.6.1 | YES |
| 2.6.0 | YES |
| 2.5.3 | YES |
| 2.5.2 | YES |
| 2.5.1 | YES |
| 2.5.0 | YES |
| 2.1.1 | NO |
| 2.1.0 | NO |
| 2.0.0 | NO |
| 1.2.1 | NO |
| 1.2.0 | NO |
| 1.1.0 | NO |
1.0.1 | NO
1.0.0 | NO
1.1.9 @ beta | NO
1.1.8 @ beta | NO
1.1.7 @ beta | NO
1.1.6 @ beta | NO
1.1.5 @ beta | NO
1.1.4 @ beta | NO
1.1.3 @ beta | NO
1.1.2 @ beta | NO
1.1.1 @ beta | NO
1.1.0 @ beta | NO
1.0.4 @ beta | NO
1.0.3 @ beta | NO
1.0.2 @ beta | NO
1.0.1 @ beta | NO
1.0.0 @ beta | NO
0.0.1 @ dev | NO (TRY IT)
* Dev versions might include bugs, use it with your own risk.
* Dev versions might include bugs and not supported use stable versions.
## Reporting a Vulnerability
You can report a vulnerability by opening an issue on the [project's GitHub](https://github.com/SernHandler/Sern/issues) repository.
You can report a vulnerability by opening an issue on the [project's GitHub](https://github.com/sern-handler/handler/issues) repository.
Please provide as much information as possible when reporting a vulnerability. We are looking information for, the affected version, and the steps to reproduce the vulnerability.

View File

@@ -3,8 +3,10 @@ name: "CodeQL"
on:
push:
branches: [ main ]
paths: ["src/**/*"]
pull_request:
branches: [ main ]
paths: ["src/**/*"]
schedule:
- cron: '37 20 * * 4'

View File

@@ -3,13 +3,14 @@ name: Continuous Integration
on:
# Trigger the workflow on push or pull request or custom
push:
branches:
main
branches: [main]
paths:
- '**.ts'
- '*.ts'
pull_request_target:
branches:
main
paths:
- '*ts'
workflow_dispatch:
jobs:
@@ -19,7 +20,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
- name: Set up Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
@@ -27,14 +28,14 @@ jobs:
node-version: 17
- name: Install pnpm
run: npm i -g pnpm
run: npm i -g yarn
# Prettier must be in `package.json`
- name: Install Node.js dependencies
run: pnpm i
run: yarn --immutable
- name: Run Prettier
run: pnpm run pretty
run: yarn pretty
- name: Create Pull Request
id: cpr

View File

@@ -10,13 +10,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 17
- uses: pnpm/action-setup@v2
with:
run_install: |
- recursive: true
args: [--strict-peer-dependencies]
- run: pnpm install
- run: pnpm build:prod
- run: yarn --immutable
- run: yarn build:prod
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}

4
.gitignore vendored
View File

@@ -87,3 +87,7 @@ dist
# IntelliJ IDEA Config file
.idea/
# Yarn files
.yarn/install-state.gz
.yarn/build-state.yml

873
.yarn/releases/yarn-3.5.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

5
.yarnrc.yml Normal file
View File

@@ -0,0 +1,5 @@
enableGlobalCache: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.5.0.cjs

View File

@@ -1,5 +1,12 @@
# Changelog
## [2.6.2](https://github.com/sern-handler/handler/compare/v2.6.1...v2.6.2) (2023-04-15)
### Miscellaneous Chores
* release 2.6.2 ([c1f6906](https://github.com/sern-handler/handler/commit/c1f690633c55ba41db1e035b7c16f9e19c70b385))
## [2.6.1](https://github.com/sern-handler/handler/compare/v2.6.0...v2.6.1) (2023-03-17)

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 sern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -113,7 +113,7 @@ client.login("YOUR_BOT_TOKEN_HERE");
- [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.
- [Murayama](https://github.com/murayamabot/murayama), :pepega:
- [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
## 💻 CLI

View File

@@ -1,7 +1,7 @@
{
"name": "@sern/handler",
"packageManager": "pnpm@7.28.0",
"version": "2.6.1",
"packageManager": "yarn@3.5.0",
"version": "2.6.2",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
@@ -19,7 +19,7 @@
"format": "eslint src/**/*.ts --fix",
"build:dev": "tsup && tsup --dts-only --outDir dist",
"build:prod": "tsup --minify && tsup --dts-only --outDir dist",
"publish": "npm run build:prod && npm publish",
"publish": "npm run build:prod",
"pretty": "prettier --write ."
},
"keywords": [
@@ -36,17 +36,19 @@
"dependencies": {
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-results-es": "^3.5.0"
"ts-results-es": "^3.6.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.54.0",
"discord.js": "^14.8.0",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.58.0",
"discord.js": "^14.9.0",
"esbuild": "^0.15.2",
"esbuild-ifdef": "^0.2.0",
"eslint": "8.30.0",
"prettier": "2.8.4",
"typescript": "4.9.5",
"tsup": "^6.6.3"
"eslint": "8.38.0",
"prettier": "2.8.7",
"tsup": "^6.7.0",
"typescript": "5.0.2"
},
"repository": {
"type": "git",

1987
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,9 @@
import type { Observable } from 'rxjs';
import type { Logging } from './logging';
import util from 'util';
/**
* @since 2.0.0
*/
export interface ErrorHandling {
/**
* Number of times the process should throw an error until crashing and exiting
@@ -20,13 +22,15 @@ export interface ErrorHandling {
*/
updateAlive(error: Error): void;
}
/**
* @since 2.0.0
*/
export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5;
crash(error: Error): never {
throw error;
}
updateAlive(e: Error) {
updateAlive(_: Error) {
this.keepAlive--;
}
}

View File

@@ -1,12 +1,16 @@
import type { LogPayload } from '../../types/handler';
/**
* @since 2.0.0
*/
export interface Logging<T = unknown> {
error(payload: LogPayload<T>): void;
warning(payload: LogPayload<T>): void;
info(payload: LogPayload<T>): void;
debug(payload: LogPayload<T>): void;
}
/**
* @since 2.0.0
*/
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {

View File

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

View File

@@ -18,11 +18,13 @@ type NotFunction =
export function single<T extends NotFunction>(cb: T): () => T;
/**
* New signature
* @since 2.0.0
* @param cb
*/
export function single<T extends () => unknown>(cb: T): T;
/**
* @__PURE__
* @since 2.0.0.
* 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!!**
@@ -41,6 +43,7 @@ export function transient<T extends NotFunction>(cb: T): () => () => T;
export function transient<T extends () => () => unknown>(cb: T): T;
/**
* @__PURE__
* @since 2.0.0
* Following iti's singleton and transient implementation,
* use transient if you want a new dependency every time your container getter is called
* @param cb

View File

@@ -6,7 +6,7 @@ import type { BothCommand, CommandModule, Module, SlashCommand } from '../../../
import { EventEmitter } from 'events';
import * as assert from 'assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
import { callPlugin } from '../operators';
import { arrayifySource, callPlugin } from '../operators';
import { createResultResolver } from '../observableHandling';
export function dispatchCommand(module: Processed<CommandModule>, createArgs: () => unknown[]) {
@@ -17,6 +17,21 @@ export function dispatchCommand(module: Processed<CommandModule>, createArgs: ()
};
}
function intoPayload(module: Processed<Module>) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
unknown[]
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
onNext: ({ args }) => args,
});
/**
* Creates an observable from { source }
* @param module
@@ -24,27 +39,15 @@ export function dispatchCommand(module: Processed<CommandModule>, createArgs: ()
*/
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 execute: OperatorFunction<unknown[], unknown> = concatMap(async args =>
module.execute(...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(
intoPayload(module),
concatMap(createResult),
execute,
);
return fromEvent(source, module.name).pipe(arrayify, concatMap(createResult), execute);
}
export function dispatchAutocomplete(

View File

@@ -1,4 +1,4 @@
import type { Interaction } from 'discord.js';
import { Interaction } from 'discord.js';
import {
catchError,
concatMap,
@@ -32,9 +32,10 @@ function makeInteractionProcessor(
return pipe(
concatMap(event => {
if (event.isMessageComponent()) {
const module = get(ms =>
ms.InteractionHandlers[event.componentType].get(event.customId),
);
const customId = event.customId;
const module = get(ms => {
return ms.InteractionHandlers[event.componentType].get(customId);
});
return of({ module, event });
} else if (event.isCommand() || event.isAutocomplete()) {
const commandName = event.commandName;
@@ -95,21 +96,23 @@ function createDispatcher({
event: Interaction;
module: Processed<CommandModule>;
}) {
switch(module.type) {
case CommandType.Text:
switch (module.type) {
case CommandType.Text:
throw Error(SernError.MismatchEvent);
case CommandType.Slash: case CommandType.Both : {
if(event.isAutocomplete()) {
case CommandType.Slash:
case CommandType.Both: {
if (event.isAutocomplete()) {
/**
* Autocomplete is a special case that
* 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
* too different from regular command modules
*/
return dispatchAutocomplete(module, event);
} else {
return dispatchCommand(module, contextArgs(event));
}
}
default : return dispatchCommand(module, interactionArg(event));
default:
return dispatchCommand(module, interactionArg(event));
}
}

View File

@@ -1,8 +1,7 @@
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, of, pipe } from 'rxjs';
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of, pipe } from 'rxjs';
import { type ModuleStore, SernError } from '../structures';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import { fmt } from '../utilities/messageHelpers';
import type { CommandModule, TextCommand } from '../../types/module';
import { ErrorHandling, handleError } from '../contracts/errorHandling';
import { contextArgs, dispatchCommand } from './dispatchers';
@@ -12,6 +11,20 @@ import { useContainerRaw } from '../dependencies';
import type { Logging, ModuleManager } from '../contracts';
import type { EventEmitter } from 'node:events';
/**
* Removes the first character(s) _[depending on prefix length]_ of the message
* @param msg
* @param prefix The prefix to remove
* @returns The message without the prefix
* @example
* message.content = '!ping';
* console.log(fmt(message, '!'));
* // [ 'ping' ]
*/
export function fmt(msg: string, prefix: string): string[] {
return msg.slice(prefix.length).trim().split(/\s+/g);
}
/**
* An operator function that processes a message to fetch a command module and prepares context payload.
* @param defaultPrefix
@@ -58,7 +71,7 @@ export function makeMessageCreate(
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return modules.get(cb);
};
const messageStream$ = fromEvent<Message>(client, 'messageCreate');
const messageStream$ = fromEvent(client, 'messageCreate') as Observable<Message>;
const messageProcessor = createMessageProcessor(defaultPrefix, get);
return messageStream$
.pipe(

View File

@@ -1,32 +1,25 @@
import type { Awaitable, Message } from 'discord.js';
import { concatMap, EMPTY, from, Observable, of, pipe, tap, throwError } from 'rxjs';
import { concatMap, EMPTY, filter, from, Observable, of, tap, throwError } from 'rxjs';
import { Result } from 'ts-results-es';
import type { CommandModule, EventModule, Module } from '../../types/module';
import SernEmitter from '../sernEmitter';
import { callPlugin, everyPluginOk, filterMapTo } from './operators';
import type { Processed } from '../../types/handler';
import type { ImportPayload, Processed } from '../../types/handler';
import type { ControlPlugin, VoidResult } from '../../types/plugin';
function hasPrefix(prefix: string, content: string) {
const prefixInContent = content.slice(0, prefix.length);
return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
}
/**
* 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 =
!m.author.bot &&
m.content
.slice(0, prefix.length)
.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
if (messageFromHumanAndHasPrefix) {
subscriber.next(m);
}
},
});
});
export function ignoreNonBot(prefix: string) {
const messageFromHumanAndHasPrefix = ({ author, content }: Message) =>
!author.bot && hasPrefix(prefix, content);
return filter(messageFromHumanAndHasPrefix);
}
/**
@@ -75,8 +68,8 @@ export function createResultResolver<
Args extends { module: T; [key: string]: unknown },
Output,
>(config: {
onFailure?: (module: T) => unknown;
onSuccess: (args: Args) => Output;
onStop?: (module: T) => unknown;
onNext: (args: Args) => Output;
createStream: (args: Args) => Observable<VoidResult>;
}) {
return (args: Args) => {
@@ -84,49 +77,45 @@ export function createResultResolver<
return task$.pipe(
tap(result => {
if (result.err) {
config.onFailure?.(args.module);
config.onStop?.(args.module);
}
}),
everyPluginOk(),
filterMapTo(() => config.onSuccess(args)),
everyPluginOk,
filterMapTo(() => config.onNext(args)),
);
};
}
/**
* 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 { onStop } and
* ignore the module
*/
export function callInitPlugins<
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,
}),
),
Args extends ImportPayload<T>,
>(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) {
return 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
* @param onStop 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,
}),
),
>(onStop: (m: M) => unknown) {
const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
return concatMap(
createResultResolver({
onStop,
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
onNext,
}),
);
}

View File

@@ -10,15 +10,14 @@ import { nameOrFilename } from '../../utilities/functions';
import type { PluginResult, VoidResult } from '../../../types/plugin';
import { guayin } from '../../plugins';
import { controller } from '../../sern';
import { SernError } from '../../structures';
import { Result } from 'ts-results-es';
import { ImportPayload, Processed } from '../../../types/handler';
/**
* 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)));
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
}
/**
@@ -31,35 +30,31 @@ export function callPlugin(args: unknown): OperatorFunction<
},
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);
return 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 = ({ module, absPath }: { module: T; absPath: string }) => ({
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
export const fillDefaults = <T extends AnyModule>({ module, absPath }: ImportPayload<T>) => {
return {
absPath,
module: {
name: nameOrFilename(module.name, absPath),
description: module.description ?? '...',
name: nameOrFilename(module?.name, absPath),
description: module?.description ?? '...',
...module,
},
});
return pipe(map(fillFields));
}
};
};
/**
* If the current value in Result stream is an error, calls callback.
@@ -67,27 +62,21 @@ export function defineAllFields<T extends AnyModule>() {
* @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) {
export function errTap<Ok, Err>(cb: (err: Err) => void): OperatorFunction<Result<Ok, Err>, Ok> {
return concatMap(result => {
if (result.ok) {
return of(result.val);
} else {
cb(result.val);
return EMPTY;
}
})
);
} else {
cb(result.val as Err);
return EMPTY;
}
});
}
/**
* Checks if the stream of results is all ok.
*/
export function everyPluginOk(): OperatorFunction<VoidResult, boolean> {
return pipe(
every(result => result.ok),
defaultIfEmpty(true),
);
}
export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
every(result => result.ok),
defaultIfEmpty(true),
);

View File

@@ -1,4 +1,4 @@
import { fromEvent, pipe, switchMap, take } from 'rxjs';
import { fromEvent, map, pipe, switchMap, take } from 'rxjs';
import * as Files from '../module-loading/readFile';
import { callInitPlugins } from './observableHandling';
import { CommandType, type ModuleStore, SernError } from '../structures';
@@ -8,21 +8,17 @@ import type { CommandModule } from '../../types/module';
import type { Processed } from '../../types/handler';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
import { err, ok } from '../utilities/functions';
import { defineAllFields, errTap } from './operators';
import { errTap, fillDefaults } from './operators';
import SernEmitter from '../sernEmitter';
import type { EventEmitter } from 'node:events';
function buildCommandModules(
commandDir: string,
sernEmitter: SernEmitter
) {
function buildCommandModules(commandDir: string, sernEmitter: SernEmitter) {
return pipe(
switchMap(() => Files.buildData<CommandModule>(commandDir)),
switchMap(() => Files.buildModuleStream<CommandModule>(commandDir)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
defineAllFields(),
map(fillDefaults),
);
}
export function makeReadyEvent(
@@ -40,13 +36,13 @@ export function makeReadyEvent(
.pipe(
buildCommandModules(commandDir, sEmitter),
callInitPlugins({
onFailure: module => {
onStop: module => {
sEmitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onSuccess: ({ module }) => {
onNext: ({ module }) => {
sEmitter.emit('module.register', SernEmitter.success(module));
return module;
},
@@ -69,28 +65,34 @@ function registerModule<T extends Processed<CommandModule>>(
const set = Result.wrap(() => manager.set(cb));
return set.ok ? ok() : err();
};
switch(mod.type) {
switch (mod.type) {
case CommandType.Text: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod));
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod));
}
case CommandType.Slash:
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod));
case CommandType.Slash:
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod),
);
case CommandType.Both: {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.BothCommands.set(name, mod));
}
case CommandType.CtxUser:
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
case CommandType.CtxUser:
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
case CommandType.CtxMsg:
return insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod));
return insert(ms =>
ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod),
);
case CommandType.Button:
return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
case CommandType.StringSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
case CommandType.MentionableSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod));
case CommandType.UserSelect:
return insert(ms =>
ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod),
);
case CommandType.UserSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
case CommandType.ChannelSelect:
return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
@@ -98,6 +100,7 @@ function registerModule<T extends Processed<CommandModule>>(
return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
case CommandType.Modal:
return insert(ms => ms.ModalSubmit.set(name, mod));
default: return err();
default:
return err();
}
}

View File

@@ -1,5 +1,5 @@
import { catchError, finalize, map, mergeAll } from 'rxjs';
import { buildData } from '../module-loading/readFile';
import * as Files from '../module-loading/readFile';
import type { Dependencies, Processed } from '../../types/handler';
import { callInitPlugins } from './observableHandling';
import type { CommandModule, EventModule } from '../../types/module';
@@ -9,7 +9,7 @@ import type { ErrorHandling, Logging } from '../contracts';
import { SernError, EventType, type Wrapper } from '../structures';
import { eventDispatcher } from './dispatchers';
import { handleError } from '../contracts/errorHandling';
import { defineAllFields, errTap } from './operators';
import { errTap, fillDefaults } from './operators';
import { useContainerRaw } from '../dependencies';
export function makeEventsHandler(
@@ -21,21 +21,28 @@ export function makeEventsHandler(
const eventStream$ = eventObservable(eventsPath, s);
const eventCreation$ = eventStream$.pipe(
defineAllFields(),
map(fillDefaults),
callInitPlugins({
onFailure: module => s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onSuccess: ({ module }) => {
onStop: module =>
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
onNext: ({ module }) => {
s.emit('module.register', SernEmitter.success(module));
return module;
},
}),
);
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
switch(e.type) {
case EventType.Sern: return eventDispatcher(e, s);
case EventType.Discord: return eventDispatcher(e, client);
case EventType.External: return eventDispatcher(e, lazy(e.emitter));
default: err.crash(Error(SernError.InvalidModuleType + ' while creating event handler'));
switch (e.type) {
case EventType.Sern:
return eventDispatcher(e, s);
case EventType.Discord:
return eventDispatcher(e, client);
case EventType.External:
return eventDispatcher(e, lazy(e.emitter));
default:
return err.crash(
Error(SernError.InvalidModuleType + ' while creating event handler'),
);
}
};
eventCreation$
@@ -59,7 +66,7 @@ export function makeEventsHandler(
}
function eventObservable(events: string, emitter: SernEmitter) {
return buildData<EventModule>(events).pipe(
return Files.buildModuleStream<EventModule>(events).pipe(
errTap(reason => {
emitter.emit('module.register', SernEmitter.failure(undefined, reason));
}),

View File

@@ -1,8 +1,10 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { type Observable, from, mergeAll } from 'rxjs';
import { type Observable, from, mergeMap } from 'rxjs';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import { ImportPayload } from '../../types/handler';
import { pathToFileURL } from 'node:url';
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
@@ -19,45 +21,44 @@ function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
return arrayOfFiles;
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
// export const isLazy = (n: string) => n.indexOf(".lazy.", n.length-9) !== -1;
export async function defaultModuleLoader<T>(
absPath: string,
): Promise<Result<ImportPayload<T>, SernError>> {
// prettier-ignore
let module: T | undefined
/// #if MODE === 'esm'
= (await import(pathToFileURL(absPath).toString())).default
/// #elif MODE === 'cjs'
= require(absPath).default; // eslint-disable-line
/// #endif
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
}
/**
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildData<T>(commandDir: string): Observable<
Result<
{
module: T;
absPath: string;
},
SernError
>
> {
export function buildModuleStream<T>(
commandDir: string,
): Observable<Result<ImportPayload<T>, SernError>> {
const commands = getCommands(commandDir);
return from(
Promise.all(
commands.map(async absPath => {
// prettier-ignore
let module: T | undefined
/// #if MODE === 'esm'
= (await import(`file:///` + absPath)).default
/// #elif MODE === 'cjs'
= require(absPath).default; // eslint-disable-line
/// #endif
return from(commands).pipe(mergeMap(defaultModuleLoader<T>));
}
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
}),
),
).pipe(mergeAll());
export function fullPathFrom(dir: string) {
return join(process.cwd(), dir);
}
export function getCommands(dir: string): string[] {
return readPath(join(process.cwd(), dir));
return readPath(fullPathFrom(dir));
}

View File

@@ -13,25 +13,37 @@ export function makePlugin<V extends unknown[]>(
[guayin]: undefined,
} as Plugin<V>;
}
/**
* @since 2.5.0
*
*/
export function EventInitPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 2.5.0
*
*/
export function EventControlPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
) {
@@ -39,6 +51,7 @@ export function EventControlPlugin<I extends EventType>(
}
/**
* @since 2.5.0
* @Experimental
* A specialized function for creating control plugins with discord.js ClientEvents.
* Will probably be moved one day!

View File

@@ -20,7 +20,7 @@ import { err, ok, partition } from './utilities/functions';
import type { Awaitable, ClientEvents } from 'discord.js';
/**
*
* @since 1.0.0
* @param wrapper Options to pass into sern.
* Function to start the handler up
* @example
@@ -43,14 +43,16 @@ export function init(wrapper: Wrapper) {
if (events !== undefined) {
makeEventsHandler(requiredDependenciesAnd([]), events, wrapper.containerConfig);
}
makeReadyEvent(requiredDependenciesAnd(['@sern/modules']), wrapper.commands);
makeMessageCreate(requiredDependenciesAnd(['@sern/modules']), wrapper.defaultPrefix);
makeInteractionCreate(requiredDependenciesAnd(['@sern/modules']));
const dependencies = requiredDependenciesAnd(['@sern/modules']);
makeReadyEvent(dependencies, wrapper.commands);
makeMessageCreate(dependencies, wrapper.defaultPrefix);
makeInteractionCreate(dependencies);
const endTime = performance.now();
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
}
/**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior
*/
export const controller = {
@@ -59,6 +61,7 @@ export const controller = {
};
/**
* @since 1.0.0
* The wrapper function to define command modules for sern
* @param mod
*/
@@ -74,6 +77,7 @@ export function commandModule(mod: InputCommand): CommandModule {
} as CommandModule;
}
/**
* @since 1.0.0
* The wrapper function to define event modules for sern
* @param mod
*/
@@ -103,6 +107,7 @@ export function discordEvent<T extends keyof ClientEvents>(mod: {
return eventModule({ type: EventType.Discord, ...mod });
}
/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export function makeDependencies<T extends Dependencies>(conf: DependencyConfiguration<T>) {
@@ -121,7 +126,8 @@ export abstract class CommandExecutable<Type extends CommandType> {
onEvent: ControlPlugin[] = [];
abstract execute: CommandModuleDefs[Type]['execute'];
}
/**@Experimental
/**
* @Experimental
* Will be refactored in future
*/
export abstract class EventExecutable<Type extends EventType> {

View File

@@ -3,6 +3,9 @@ import type { Payload, SernEventsMapping } from '../types/handler';
import { PayloadType } from './structures';
import type { Module } from '../types/module';
/**
* @since 1.0.0
*/
class SernEmitter extends EventEmitter {
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit

View File

@@ -15,6 +15,7 @@ function safeUnwrap<T>(res: Either<T, T>) {
return res.val;
}
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/

View File

@@ -1,4 +1,5 @@
/**
* @since 1.0.0
* A bitfield that discriminates command modules
* @enum { number }
* @example

View File

@@ -3,6 +3,7 @@ import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
* Storing all command modules
* This dependency is usually injected into ModuleManager
*/

View File

@@ -1,10 +1,15 @@
import type { Dependencies } from '../../types/handler';
/**
* @since 1.0.0
* An object to be passed into Sern#init() function.
* @typedef {object} Wrapper
*/
interface Wrapper {
/**
* @deprecated
* This will be moved to a new field in 3.0.0
*/
readonly defaultPrefix?: string;
readonly commands: string;
readonly events?: string;

View File

@@ -1,13 +0,0 @@
/**
* Removes the first character(s) _[depending on prefix length]_ of the message
* @param msg
* @param prefix The prefix to remove
* @returns The message without the prefix
* @example
* message.content = '!ping';
* console.log(fmt(message, '!'));
* // [ 'ping' ]
*/
export function fmt(msg: string, prefix: string): string[] {
return msg.slice(prefix.length).trim().split(/\s+/g);
}

View File

@@ -67,3 +67,5 @@ export interface DependencyConfiguration<T extends Dependencies> {
exclude?: Set<OptionalDependencies>;
build: (root: Container<Omit<Dependencies, '@sern/client'>, {}>) => Container<T, {}>;
}
export type ImportPayload<T> = { module: T; absPath: string };

View File

@@ -19,7 +19,7 @@ import type {
MentionableSelectMenuInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserSelectMenuInteraction
UserSelectMenuInteraction,
} from 'discord.js';
import { CommandType } from '../handler/structures/enums';
import type { Args, SlashOptions } from './handler';

View File

@@ -6,10 +6,10 @@
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"preserveSymlinks": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},

3
tsconfig.json Normal file
View File

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

3171
yarn.lock Normal file

File diff suppressed because it is too large Load Diff