Compare commits

...

51 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
github-actions[bot]
2fd7697300 chore(main): release 2.6.1 (#258)
* chore(main): release 2.6.1

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-17 16:37:20 -05:00
Jacob Nguyen
f9609ce6cd chore: release 2.6.1
Release-As: 2.6.1
2023-03-17 16:33:46 -05:00
Jacob Nguyen
a3064aa915 chore: audit & remove tspattern (#256)
* chore: move import

* build: remove ts pattern

* fix: forgot to convert to switch

* fix workflow

* refactor: lift function out of readyHandler

* refactor: clean up errTap signature

* fix: sern emitter emitting wrong payload

* wa

* style: space

* chore: remove old errTap

* chore:bump discord.js

* chore: eslint format
2023-03-17 16:30:27 -05:00
github-actions[bot]
0a53a48521 style: pretty please (#255)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2023-03-15 21:16:58 -05:00
Jacob Nguyen
05037b5315 build: prettier ignore 2023-03-15 21:15:33 -05:00
Jacob Nguyen
06a3e69210 feat: prettier ignore 2023-03-15 21:13:42 -05:00
Jacob Nguyen
74c4b77d4b build: refactor/building (#252)
* refactor: conditional compilation of loading esm/cjs modules

* refactor: move file loading file

* refactor: add conditional compilation for building modules

* refactor: add conditional compilation for building modules

* perf: decrease build times

* test

* revert: typo and clean code

* build: smaller build

* chore:cleanscripts

* chore:refactor readme

* build:automerge lockfile

* chore: remove build and upgrade readme

* fix: dropdown

* chore: fix

* chore: more docs

---------

Co-authored-by: jacoobes <jacobnguyend@gmail.com>
2023-03-15 21:08:27 -05:00
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
45 changed files with 4804 additions and 2474 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 | | 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 ## 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. 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: on:
push: push:
branches: [ main ] branches: [ main ]
paths: ["src/**/*"]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
paths: ["src/**/*"]
schedule: schedule:
- cron: '37 20 * * 4' - cron: '37 20 * * 4'

View File

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

View File

@@ -1,10 +1,7 @@
name: NPM / Publish name: NPM / Publish
on: on:
workflow_run: workflow_dispatch:
workflows: ["release-please"]
types: [completed]
jobs: jobs:
test-and-publish: test-and-publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -13,15 +10,9 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 17 node-version: 17
- uses: pnpm/action-setup@v2 - run: yarn --immutable
with: - run: yarn build:prod
run_install: |
- recursive: true
args: [--strict-peer-dependencies]
- run: pnpm install
- run: pnpm test
- run: pnpm build
- uses: JS-DevTools/npm-publish@v1 - uses: JS-DevTools/npm-publish@v1
with: with:
token: ${{ secrets.NPM_TOKEN }} token: ${{ secrets.NPM_TOKEN }}
access: "public" access: "public"

4
.gitignore vendored
View File

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

View File

@@ -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

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,33 @@
# Changelog # 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)
### 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)
### 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) ## [2.5.2](https://github.com/sern-handler/handler/compare/v2.5.1...v2.5.2) (2023-02-16)

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

@@ -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,33 +100,20 @@ 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).
- [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.
- [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. - [Protector (WIP)](https://github.com/needhamgary/Protector), Just a simple bot to help enhance a private minecraft server.
## 💻 CLI ## 💻 CLI
@@ -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.

View File

@@ -1,8 +1,8 @@
{ {
"name": "@sern/handler", "name": "@sern/handler",
"packageManager": "pnpm@7.27.0", "packageManager": "yarn@3.5.0",
"version": "2.5.2", "version": "2.6.2",
"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",
"pretty": "prettier --write ." "pretty": "prettier --write ."
}, },
"keywords": [ "keywords": [
@@ -35,17 +36,19 @@
"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.6.0"
"ts-results-es": "^3.5.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "5.51.0", "@types/node": "^18.15.11",
"@typescript-eslint/parser": "5.48.0", "@typescript-eslint/eslint-plugin": "5.58.0",
"discord.js": "^14.7.1", "@typescript-eslint/parser": "5.58.0",
"eslint": "8.30.0", "discord.js": "^14.9.0",
"prettier": "2.8.4", "esbuild": "^0.15.2",
"tsup": "^6.6.3", "esbuild-ifdef": "^0.2.0",
"typescript": "4.9.4" "eslint": "8.38.0",
"prettier": "2.8.7",
"tsup": "^6.7.0",
"typescript": "5.0.2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

1788
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,6 @@
"schedule": ["every weekend"], "schedule": ["every weekend"],
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true, "enabled": true,
"automerge": false "automerge": true
} }
} }

View File

@@ -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' }));
}
}

View File

@@ -1,6 +1,9 @@
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';
/**
* @since 2.0.0
*/
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
@@ -19,13 +22,15 @@ export interface ErrorHandling {
*/ */
updateAlive(error: Error): void; updateAlive(error: Error): void;
} }
/**
* @since 2.0.0
*/
export class DefaultErrorHandling implements ErrorHandling { export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5; keepAlive = 5;
crash(error: Error): never { crash(error: Error): never {
throw error; throw error;
} }
updateAlive(e: Error) { updateAlive(_: Error) {
this.keepAlive--; this.keepAlive--;
} }
} }

View File

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

View File

@@ -1,14 +1,18 @@
import type { CommandModuleDefs } from '../../types/module'; import type { CommandModuleDefs } from '../../types/module';
import type { CommandType, ModuleStore } from '../structures'; import type { CommandType, ModuleStore } from '../structures';
import type { Processed } from '../../types/handler'; import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
*/
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;
} }
/**
* @since 2.0.0
*/
export class DefaultModuleManager implements ModuleManager { export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: ModuleStore) {} constructor(private moduleStore: ModuleStore) {}
get<T extends CommandType>( get<T extends CommandType>(

View File

@@ -18,10 +18,13 @@ type NotFunction =
export function single<T extends NotFunction>(cb: T): () => T; export function single<T extends NotFunction>(cb: T): () => T;
/** /**
* New signature * New signature
* @since 2.0.0
* @param cb * @param cb
*/ */
export function single<T extends () => unknown>(cb: T): T; 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 * 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 +42,8 @@ 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__
* @since 2.0.0
* 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 +54,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

@@ -6,7 +6,7 @@ import type { BothCommand, CommandModule, Module, SlashCommand } from '../../../
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as assert from 'assert'; import * as assert from 'assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs'; import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
import { callPlugin } from '../operators'; import { arrayifySource, callPlugin } from '../operators';
import { createResultResolver } from '../observableHandling'; import { createResultResolver } from '../observableHandling';
export function dispatchCommand(module: Processed<CommandModule>, createArgs: () => unknown[]) { 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 } * Creates an observable from { source }
* @param module * @param module
@@ -24,27 +39,15 @@ export function dispatchCommand(module: Processed<CommandModule>, createArgs: ()
*/ */
export function eventDispatcher(module: Processed<Module>, source: unknown) { export function eventDispatcher(module: Processed<Module>, source: unknown) {
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`); assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
/**
* Sometimes fromEvent emits a single parameter, which is not an Array. This const execute: OperatorFunction<unknown[], unknown> = concatMap(async args =>
* operator function flattens events into an array module.execute(...args),
* @param src
*/
const arrayify = pipe(
map(event => (Array.isArray(event) ? (event as unknown[]) : [event])),
map(args => ({ module, args })),
); );
const createResult = createResultResolver< return fromEvent(source, module.name).pipe(
Processed<Module>, intoPayload(module),
{ module: Processed<Module>; args: unknown[] }, concatMap(createResult),
unknown[] execute,
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
onSuccess: ({ args }) => args,
});
const execute: OperatorFunction<unknown[], unknown> = pipe(
concatMap(async args => module.execute(...args)),
); );
return fromEvent(source, module.name).pipe(arrayify, concatMap(createResult), execute);
} }
export function dispatchAutocomplete( export function dispatchAutocomplete(

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,118 @@
import type { Interaction } from 'discord.js'; import { 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,
import { match, P } from 'ts-pattern'; filter,
import { contextArgs, interactionArg, dispatchAutocomplete, dispatchCommand } from './dispatchers'; finalize,
fromEvent,
map,
Observable,
of,
OperatorFunction,
pipe,
} from 'rxjs';
import { CommandType, type ModuleStore, SernError } from '../structures';
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 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;
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(commandName) ??
ms.BothCommands.get(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>; switch (module.type) {
constructor(wrapper: Wrapper) { case CommandType.Text:
super(wrapper); throw Error(SernError.MismatchEvent);
this.discordEvent = <Observable<Interaction>>fromEvent(this.client, 'interactionCreate'); case CommandType.Slash:
this.init(); case CommandType.Both: {
if (event.isAutocomplete()) {
this.payloadSubject
.pipe(
map(this.createDispatcher),
makeModuleExecutor(module => {
this.emitter.emit(
'module.activate',
SernEmitter.failure(module, SernError.PluginFailure),
);
}),
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({
message: 'interactionCreate stream closed or reached end of lifetime',
});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
}),
)
.subscribe();
}
override init() {
const get = (cb: (ms: ModuleStore) => 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 {
throw Error('This interaction is not supported yet');
}
},
error: reason => {
this.emitter.emit('error', SernEmitter.failure(undefined, reason));
},
});
}
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 * Autocomplete is a special case that
* this makes this usage safe * must be handled separately, since it's
* too different from regular command modules
*/ */
.otherwise(mod => dispatchCommand(mod, interactionArg(event))) return dispatchAutocomplete(module, event);
); } else {
return dispatchCommand(module, contextArgs(event));
}
}
default:
return dispatchCommand(module, interactionArg(event));
} }
} }

View File

@@ -1,84 +1,92 @@
import { EventsHandler } from './eventsHandler'; import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, 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 type { CommandModule, TextCommand } from '../../types/module';
import type { CommandModule, Module, TextCommand } from '../../types/module'; import { ErrorHandling, handleError } from '../contracts/errorHandling';
import { 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>; * Removes the first character(s) _[depending on prefix length]_ of the message
args: unknown[]; * @param msg
}> { * @param prefix The prefix to remove
protected discordEvent: Observable<Message>; * @returns The message without the prefix
* @example
public constructor(protected wrapper: Wrapper) { * message.content = '!ping';
super(wrapper); * console.log(fmt(message, '!'));
this.discordEvent = <Observable<Message>>fromEvent(this.client, 'messageCreate'); * // [ 'ping' ]
this.init(); */
this.payloadSubject export function fmt(msg: string, prefix: string): string[] {
.pipe( return msg.slice(prefix.length).trim().split(/\s+/g);
makeModuleExecutor(module => { }
this.emitter.emit(
'module.activate', /**
SernEmitter.failure(module, SernError.PluginFailure), * An operator function that processes a message to fetch a command module and prepares context payload.
); * @param defaultPrefix
}), * @param get
concatMap(payload => executeModule(this.emitter, payload)), */
catchError(handleError(this.crashHandler, this.logger)), const createMessageProcessor = (
finalize(() => { defaultPrefix: string,
this.logger?.info({ get: (
message: 'messageCreate stream closed or reached end of lifetime', cb: (ms: ModuleStore) => Processed<CommandModule> | undefined,
}); ) => CommandModule | undefined,
useContainerRaw() ) =>
?.disposeAll() pipe(
.then(() => { ignoreNonBot(defaultPrefix),
this.logger?.info({ message: 'Cleaning container and crashing' }); //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.content, defaultPrefix);
.subscribe(); const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix));
} if (module === undefined) {
return EMPTY;
protected init(): void { }
if (this.wrapper.defaultPrefix === undefined) return; //for now, just ignore if prefix doesn't exist const payload = {
const { defaultPrefix } = this.wrapper; args: contextArgs(message, rest),
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => { module,
return this.modules.get(cb); };
}; return of(payload);
this.discordEvent }),
.pipe( map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)),
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 export function makeMessageCreate(
concatMap(message => { [s, client, err, log, modules]: [
const [prefix, ...rest] = fmt(message, defaultPrefix); SernEmitter,
const module = get( EventEmitter,
ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix), ErrorHandling,
); Logging | undefined,
if (module === undefined) { ModuleManager,
return EMPTY; ],
} defaultPrefix?: string,
const payload = { ) {
args: contextArgs(message, rest), if (!defaultPrefix) {
module, return EMPTY.subscribe();
}; }
return of(payload); const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
}), return modules.get(cb);
map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)), };
) const messageStream$ = fromEvent(client, 'messageCreate') as Observable<Message>;
.subscribe({ const messageProcessor = createMessageProcessor(defaultPrefix, get);
next: value => this.setState(value), return messageStream$
error: reason => this.emitter.emit('error', SernEmitter.failure(reason)), .pipe(
}); messageProcessor,
} makeModuleExecutor(module => {
s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
protected setState(state: { module: Processed<Module>; args: unknown[] }) { }),
this.payloadSubject.next(state); 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

@@ -1,55 +1,25 @@
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, filter, from, Observable, of, 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 { ImportPayload, Processed } from '../../types/handler';
import type { ControlPlugin, VoidResult } from '../../types/plugin'; 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 * Ignores messages from any person / bot except itself
* @param prefix * @param prefix
*/ */
export function ignoreNonBot<T extends Message>(prefix: string) { export function ignoreNonBot(prefix: string) {
return (src: Observable<T>) => const messageFromHumanAndHasPrefix = ({ author, content }: Message) =>
new Observable<T>(subscriber => { !author.bot && hasPrefix(prefix, content);
return src.subscribe({ return filter(messageFromHumanAndHasPrefix);
next(m) {
const messageFromHumanAndHasPrefix =
!m.author.bot &&
m.content
.slice(0, prefix.length)
.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
if (messageFromHumanAndHasPrefix) {
subscriber.next(m);
}
},
});
});
}
/**
* 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);
}
},
});
});
} }
/** /**
@@ -78,7 +48,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));
} }
}), }),
); );
@@ -98,8 +68,8 @@ export function createResultResolver<
Args extends { module: T; [key: string]: unknown }, Args extends { module: T; [key: string]: unknown },
Output, Output,
>(config: { >(config: {
onFailure?: (module: T) => unknown; onStop?: (module: T) => unknown;
onSuccess: (args: Args) => Output; onNext: (args: Args) => Output;
createStream: (args: Args) => Observable<VoidResult>; createStream: (args: Args) => Observable<VoidResult>;
}) { }) {
return (args: Args) => { return (args: Args) => {
@@ -107,49 +77,45 @@ export function createResultResolver<
return task$.pipe( return task$.pipe(
tap(result => { tap(result => {
if (result.err) { if (result.err) {
config.onFailure?.(args.module); config.onStop?.(args.module);
} }
}), }),
everyPluginOk(), everyPluginOk,
filterMapTo(() => config.onSuccess(args)), 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 * 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 ImportPayload<T>,
>(config: { onFailure?: (module: T) => unknown; onSuccess: (module: Args) => T }) { >(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) {
return pipe( return concatMap(
concatMap( createResultResolver({
createResultResolver({ createStream: args => from(args.module.plugins).pipe(callPlugin(args)),
createStream: args => from(args.module.plugins).pipe(callPlugin(args)), ...config,
...config, }),
}),
),
); );
} }
/** /**
* Creates an executable task ( execute the command ) if all control plugins are successful * 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< export function makeModuleExecutor<
M extends Processed<Module>, M extends Processed<Module>,
Args extends { module: M; args: unknown[] }, Args extends { module: M; args: unknown[] },
>(onFailure: (m: M) => unknown) { >(onStop: (m: M) => unknown) {
const onSuccess = ({ args, module }: Args) => ({ task: () => module.execute(...args), module }); const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
return pipe( return concatMap(
concatMap( createResultResolver({
createResultResolver({ onStop,
onFailure, createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)), onNext,
onSuccess, }),
}),
),
); );
} }

View File

@@ -10,13 +10,14 @@ 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 { Result } from 'ts-results-es';
import { ImportPayload, Processed } from '../../../types/handler';
/** /**
* if {src} is true, mapTo V, else ignore * if {src} is true, mapTo V, else ignore
* @param item * @param item
*/ */
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> { export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return pipe(concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY))); return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
} }
/** /**
@@ -29,42 +30,53 @@ export function callPlugin(args: unknown): OperatorFunction<
}, },
VoidResult VoidResult
> { > {
return pipe( return concatMap(async plugin => {
concatMap(async plugin => { const isNewPlugin = Reflect.has(plugin, guayin);
const isNewPlugin = Reflect.has(plugin, guayin); if (isNewPlugin) {
if (isNewPlugin) { if (Array.isArray(args)) {
if (Array.isArray(args)) { return plugin.execute(...args);
return plugin.execute(...args);
}
return plugin.execute(args);
} else {
return plugin.execute(args, controller);
} }
}), return plugin.execute(args);
); } else {
return plugin.execute(args, controller);
}
});
} }
/** export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
* operator function that fill the defaults for a module
*/ export const fillDefaults = <T extends AnyModule>({ module, absPath }: ImportPayload<T>) => {
export function defineAllFields<T extends AnyModule>() { return {
const fillFields = ({ absPath, module }: { absPath: string; module: T }) => ({
absPath, absPath,
module: { module: {
name: nameOrFilename(module.name, absPath), name: nameOrFilename(module?.name, absPath),
description: module.description ?? '...', description: module?.description ?? '...',
...module, ...module,
}, },
};
};
/**
* 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<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 as Err);
return EMPTY;
}
}); });
return pipe(map(fillFields));
} }
/** /**
* Checks if the stream of results is all ok. * Checks if the stream of results is all ok.
*/ */
export function everyPluginOk(): OperatorFunction<VoidResult, boolean> { export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
return pipe( every(result => result.ok),
every(result => result.ok), defaultIfEmpty(true),
defaultIfEmpty(true), );
);
}

View File

@@ -1,72 +1,59 @@
import { EventsHandler } from './eventsHandler'; import { fromEvent, map, pipe, switchMap, take } from 'rxjs';
import type Wrapper from '../structures/wrapper'; import * as Files from '../module-loading/readFile';
import { concatMap, fromEvent, type Observable, take } from 'rxjs'; import { callInitPlugins } from './observableHandling';
import * as Files from '../utilities/readFile'; import { CommandType, type ModuleStore, SernError } from '../structures';
import { errTap, scanModule } from './observableHandling';
import { CommandType, SernError, type ModuleStore } 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 { 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 { errTap, fillDefaults } from './operators';
import SernEmitter from '../sernEmitter'; import SernEmitter from '../sernEmitter';
import type { EventEmitter } from 'node:events';
export default class ReadyHandler extends EventsHandler<{ function buildCommandModules(commandDir: string, sernEmitter: SernEmitter) {
module: Processed<CommandModule>; return pipe(
absPath: string; switchMap(() => Files.buildModuleStream<CommandModule>(commandDir)),
}> { errTap(error => {
protected discordEvent!: Observable<{ module: CommandModule; absPath: string }>; sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
constructor(wrapper: Wrapper) { }),
super(wrapper); map(fillDefaults),
const ready$ = fromEvent(this.client, 'ready').pipe(take(1)); );
this.discordEvent = ready$.pipe( }
concatMap(() => export function makeReadyEvent(
Files.buildData<CommandModule>(wrapper.commands).pipe( [sEmitter, client, errorHandler, , moduleManager]: [
errTap(reason => { SernEmitter,
this.emitter.emit( EventEmitter,
'module.register', ErrorHandling,
SernEmitter.failure(undefined, reason), Logging | undefined,
); ModuleManager,
}), ],
), commandDir: string,
), ) {
); const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
this.init(); return readyOnce$
this.payloadSubject .pipe(
.pipe( buildCommandModules(commandDir, sEmitter),
scanModule({ callInitPlugins({
onFailure: module => { onStop: module => {
this.emitter.emit( sEmitter.emit(
'module.register', 'module.register',
SernEmitter.failure(module, SernError.PluginFailure), SernEmitter.failure(module, SernError.PluginFailure),
); );
}, },
onSuccess: ({ module }) => { onNext: ({ module }) => {
this.emitter.emit('module.register', SernEmitter.success(module)); sEmitter.emit('module.register', SernEmitter.success(module));
return module; return module;
}, },
}), }),
) )
.subscribe(module => { .subscribe(module => {
const res = registerModule(this.modules, module as Processed<CommandModule>); const result = registerModule(moduleManager, module as Processed<CommandModule>);
if (res.err) { if (result.err) {
this.crashHandler.crash(Error(SernError.InvalidModuleType)); errorHandler.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>>(
@@ -75,45 +62,45 @@ 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),
.with({ type: CommandType.Both }, mod => { );
case CommandType.Both: {
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 =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod)), ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod),
) );
.with({ type: CommandType.Button }, mod => case CommandType.Button:
insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod)), return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
) case CommandType.StringSelect:
.with({ type: CommandType.StringSelect }, mod => return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod)), case CommandType.MentionableSelect:
) return insert(ms =>
.with({ type: CommandType.MentionableSelect }, mod => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod),
insert(ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod)), );
) case CommandType.UserSelect:
.with({ type: CommandType.ChannelSelect }, mod => return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod)), case CommandType.ChannelSelect:
) return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
.with({ type: CommandType.UserSelect }, mod => case CommandType.RoleSelect:
insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod)), return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
) case CommandType.Modal:
.with({ type: CommandType.RoleSelect }, mod => return insert(ms => ms.ModalSubmit.set(name, mod));
insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod)), default:
) return err();
.with({ type: CommandType.Modal }, mod => insert(ms => ms.ModalSubmit.set(name, mod))) }
.otherwise(err);
} }

View File

@@ -1,70 +1,72 @@
import { catchError, finalize, map, tap } from 'rxjs'; import { catchError, finalize, map, mergeAll } from 'rxjs';
import { buildData } from '../utilities/readFile'; import * as Files 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 { errTap, fillDefaults } 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(), map(fillDefaults),
scanModule({ callInitPlugins({
onFailure: module => sernEmitter.emit('module.register', SernEmitter.success(module)), onStop: module =>
onSuccess: ({ module }) => { s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
sernEmitter.emit( onNext: ({ module }) => {
'module.register', s.emit('module.register', SernEmitter.success(module));
SernEmitter.failure(module, SernError.PluginFailure),
);
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, sernEmitter)) case EventType.Sern:
.with({ type: EventType.Discord }, m => eventDispatcher(m, client)) return eventDispatcher(e, s);
.with({ type: EventType.External }, m => eventDispatcher(m, lazy(m.emitter))) case EventType.Discord:
.otherwise(() => errorHandling.crash(Error(SernError.InvalidModuleType))); 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$ 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 Files.buildModuleStream<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

@@ -0,0 +1,64 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
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[] {
try {
const files = readdirSync(dir);
for (const file of files) {
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
else arrayOfFiles.push(join(dir, '/', file));
}
} catch (err) {
throw err;
}
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
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildModuleStream<T>(
commandDir: string,
): Observable<Result<ImportPayload<T>, SernError>> {
const commands = getCommands(commandDir);
return from(commands).pipe(mergeMap(defaultModuleLoader<T>));
}
export function fullPathFrom(dir: string) {
return join(process.cwd(), dir);
}
export function getCommands(dir: string): string[] {
return readPath(fullPathFrom(dir));
}

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

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,13 +14,13 @@ 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';
/** /**
* * @since 1.0.0
* @param wrapper Options to pass into sern. * @param wrapper Options to pass into sern.
* Function to start the handler up * Function to start the handler up
* @example * @example
@@ -37,19 +37,22 @@ 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); const dependencies = requiredDependenciesAnd(['@sern/modules']);
new MessageHandler(wrapper); makeReadyEvent(dependencies, wrapper.commands);
new InteractionHandler(wrapper); makeMessageCreate(dependencies, wrapper.defaultPrefix);
makeInteractionCreate(dependencies);
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` });
} }
/** /**
* @since 1.0.0
* The object passed into every plugin to control a command's behavior * The object passed into every plugin to control a command's behavior
*/ */
export const controller = { export const controller = {
@@ -58,6 +61,7 @@ export const controller = {
}; };
/** /**
* @since 1.0.0
* The wrapper function to define command modules for sern * The wrapper function to define command modules for sern
* @param mod * @param mod
*/ */
@@ -73,6 +77,7 @@ export function commandModule(mod: InputCommand): CommandModule {
} as CommandModule; } as CommandModule;
} }
/** /**
* @since 1.0.0
* The wrapper function to define event modules for sern * The wrapper function to define event modules for sern
* @param mod * @param mod
*/ */
@@ -102,6 +107,7 @@ export function discordEvent<T extends keyof ClientEvents>(mod: {
return eventModule({ type: EventType.Discord, ...mod }); return eventModule({ type: EventType.Discord, ...mod });
} }
/** /**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies * @param conf a configuration for creating your project dependencies
*/ */
export function makeDependencies<T extends Dependencies>(conf: DependencyConfiguration<T>) { export function makeDependencies<T extends Dependencies>(conf: DependencyConfiguration<T>) {
@@ -120,7 +126,8 @@ export abstract class CommandExecutable<Type extends CommandType> {
onEvent: ControlPlugin[] = []; onEvent: ControlPlugin[] = [];
abstract execute: CommandModuleDefs[Type]['execute']; abstract execute: CommandModuleDefs[Type]['execute'];
} }
/**@Experimental /**
* @Experimental
* Will be refactored in future * Will be refactored in future
*/ */
export abstract class EventExecutable<Type extends EventType> { 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 { PayloadType } from './structures';
import type { Module } from '../types/module'; import type { Module } from '../types/module';
/**
* @since 1.0.0
*/
class SernEmitter extends EventEmitter { class SernEmitter extends EventEmitter {
/** /**
* Listening to sern events with on. This event stays on until a crash or a normal exit * 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; return res.val;
} }
/** /**
* @since 1.0.0
* Provides values shared between * Provides values shared between
* Message and ChatInputCommandInteraction * Message and ChatInputCommandInteraction
*/ */

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
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';
/** /**
* 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

@@ -1,15 +0,0 @@
import type { Message } from 'discord.js';
/**
* 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: Message, prefix: string): string[] {
return msg.content.slice(prefix.length).trim().split(/\s+/g);
}

View File

@@ -1,65 +0,0 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { type Observable, from, concatAll } from 'rxjs';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {
const files = readdirSync(dir);
for (const file of files) {
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
else arrayOfFiles.push(join(dir, '/', file));
}
} catch (err) {
throw err;
}
return arrayOfFiles;
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/**
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildData<T>(commandDir: string): Observable<
Result<
{
module: T;
absPath: string;
},
SernError
>
> {
const commands = getCommands(commandDir);
return from(
Promise.all(
commands.map(async absPath => {
let module: T | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
module = require(absPath).default;
} catch {
module = (await import(`file:///` + absPath)).default;
}
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
}),
),
).pipe(concatAll());
}
export function getCommands(dir: string): string[] {
return readPath(join(process.cwd(), dir));
}

View File

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

View File

@@ -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';

View File

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

3
tsconfig.json Normal file
View File

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

View File

@@ -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([
{ {
@@ -13,19 +14,23 @@ 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,
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,
external: ['discord.js'],
target: 'node16', target: 'node16',
tsconfig: './tsconfig-cjs.json', tsconfig: './tsconfig-cjs.json',
outDir: './dist/cjs', outDir: './dist/cjs',
@@ -34,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,
}, },
]); ]);

3171
yarn.lock Normal file

File diff suppressed because it is too large Load Diff