mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
Compare commits
83 Commits
revert-342
...
fix/dispos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4722849f56 | ||
|
|
513ac8edf4 | ||
|
|
3673263f3a | ||
|
|
24ec4d6ad6 | ||
|
|
27d13f1ea5 | ||
|
|
81a0180d05 | ||
|
|
89d7409536 | ||
|
|
aa802f761e | ||
|
|
2414992b73 | ||
|
|
70c6236802 | ||
|
|
ed5bb20c60 | ||
|
|
0d77878dfb | ||
|
|
8ac7b60b59 | ||
|
|
1f25aa64b9 | ||
|
|
7cddee30aa | ||
|
|
e7286eee9f | ||
|
|
a67450328e | ||
|
|
926d531863 | ||
|
|
ef3d3d71a0 | ||
|
|
47401f46a3 | ||
|
|
1059065980 | ||
|
|
974c30fa6c | ||
|
|
3a569726d8 | ||
|
|
1b7f2a49a8 | ||
|
|
97fa2a2d78 | ||
|
|
a52ad270d8 | ||
|
|
3f703c17b8 | ||
|
|
f9e7eaf92d | ||
|
|
52e145600d | ||
|
|
59d08ef207 | ||
|
|
7deb79e907 | ||
|
|
f2d4b5bda1 | ||
|
|
a575b3ed74 | ||
|
|
2042559b4d | ||
|
|
220a60ecf8 | ||
|
|
55715d5659 | ||
|
|
d0c3b7469e | ||
|
|
eabfb81819 | ||
|
|
1789ccb2f2 | ||
|
|
25c5891ade | ||
|
|
2106cdc1d0 | ||
|
|
61e82fdc7b | ||
|
|
3755b95b1a | ||
|
|
06807ea77f | ||
|
|
3fd3f1c236 | ||
|
|
92623d2914 | ||
|
|
a91f260a86 | ||
|
|
dda0e3395b | ||
|
|
9a8904f5ae | ||
|
|
04c4625bfa | ||
|
|
91b3768e37 | ||
|
|
d6f49d1d97 | ||
|
|
8ecd30cf18 | ||
|
|
a19edaf883 | ||
|
|
90e55dfa14 | ||
|
|
2106522812 | ||
|
|
ce8c4bf649 | ||
|
|
e89b918390 | ||
|
|
f8b69ae542 | ||
|
|
48f9f6ec16 | ||
|
|
86ebb221ed | ||
|
|
4efdbb21fb | ||
|
|
07b11b357b | ||
|
|
ac7f47c590 | ||
| 45cbda7b42 | |||
|
|
5cad432589 | ||
|
|
044a10dace | ||
|
|
9d5c6c714f | ||
|
|
4f2387119a | ||
|
|
a6fa4e3dcb | ||
|
|
c281832db2 | ||
|
|
a359f73fa2 | ||
| 655bb8d358 | |||
| e8d5029834 | |||
|
|
b0399f9507 | ||
|
|
b962dae36c | ||
|
|
c73cf96cb2 | ||
|
|
7458befe8a | ||
|
|
efe49391e8 | ||
|
|
3140f80c10 | ||
|
|
504cdee7b2 | ||
|
|
c7661f272c | ||
|
|
daac37c288 |
50
.github/workflows/continuous-integration.yml
vendored
50
.github/workflows/continuous-integration.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
name: Continuous Integration
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Trigger the workflow on push or pull request or custom
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- '*.ts'
|
|
||||||
pull_request_target:
|
|
||||||
branches:
|
|
||||||
main
|
|
||||||
paths:
|
|
||||||
- '*ts'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Prettier:
|
|
||||||
name: Run Prettier
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
|
||||||
with:
|
|
||||||
node-version: 17
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
run: npm i -g yarn
|
|
||||||
|
|
||||||
# Prettier must be in `package.json`
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: yarn --immutable
|
|
||||||
|
|
||||||
- name: Run Prettier
|
|
||||||
run: yarn pretty
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
id: cpr
|
|
||||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4
|
|
||||||
with:
|
|
||||||
commit-message: "style: pretty please"
|
|
||||||
branch: prettier
|
|
||||||
delete-branch: true
|
|
||||||
branch-suffix: short-commit-hash
|
|
||||||
title: "style: pretty please"
|
|
||||||
body: "pretty pretty prettier"
|
|
||||||
reviewers: EvolutionX-10
|
|
||||||
34
.github/workflows/npm-publish-dev.yml
vendored
Normal file
34
.github/workflows/npm-publish-dev.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Continuous Delivery
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'package.json'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Publish:
|
||||||
|
name: Publishing Dev
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm i && npm run build:dev
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: |
|
||||||
|
npm version premajor --preid "dev.$(git rev-parse --verify --short HEAD)" --git-tag-version=false
|
||||||
|
npm publish --tag dev
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
8
.github/workflows/npm-publish.yml
vendored
8
.github/workflows/npm-publish.yml
vendored
@@ -11,11 +11,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
|
- uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
|
||||||
with:
|
with:
|
||||||
node-version: 17
|
node-version: 18
|
||||||
- run: yarn --immutable
|
- run: npm i
|
||||||
- run: yarn build:prod
|
- run: npm run build:prod
|
||||||
- uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
|
- uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
token: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@@ -20,10 +20,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
|
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm install -g yarn
|
- run: npm install
|
||||||
- run: yarn install
|
- run: npm run test
|
||||||
- run: yarn test
|
|
||||||
|
|||||||
@@ -113,3 +113,4 @@ tsconfig-cjs.json
|
|||||||
tsconfig-esm.json
|
tsconfig-esm.json
|
||||||
|
|
||||||
renovate.json
|
renovate.json
|
||||||
|
fortnite
|
||||||
|
|||||||
873
.yarn/releases/yarn-3.5.1.cjs
vendored
873
.yarn/releases/yarn-3.5.1.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
|
|||||||
enableGlobalCache: true
|
|
||||||
|
|
||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.5.1.cjs
|
|
||||||
138
CHANGELOG.md
138
CHANGELOG.md
@@ -1,5 +1,143 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [4.2.4](https://github.com/sern-handler/handler/compare/v4.2.3...v4.2.4) (2025-03-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* flat autocomplete ([#395](https://github.com/sern-handler/handler/issues/395)) ([89d7409](https://github.com/sern-handler/handler/commit/89d74095363befddc3222b9e5c89c35e7c6457b9))
|
||||||
|
|
||||||
|
## [4.2.3](https://github.com/sern-handler/handler/compare/v4.2.2...v4.2.3) (2025-03-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* autocomplete sdt.module not present ([#393](https://github.com/sern-handler/handler/issues/393)) ([2414992](https://github.com/sern-handler/handler/commit/2414992b73a40065464b20f2d53826c78fcd3a5f))
|
||||||
|
|
||||||
|
## [4.2.2](https://github.com/sern-handler/handler/compare/v4.2.1...v4.2.2) (2025-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* faster autocomplete lookup ([#387](https://github.com/sern-handler/handler/issues/387)) ([974c30f](https://github.com/sern-handler/handler/commit/974c30fa6cccaae7b1c2c3246ffa9eecb6bc7bf9))
|
||||||
|
|
||||||
|
## [4.2.1](https://github.com/sern-handler/handler/compare/v4.2.0...v4.2.1) (2025-01-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* context-interactions error ([#382](https://github.com/sern-handler/handler/issues/382)) ([a52ad27](https://github.com/sern-handler/handler/commit/a52ad270d843e92db5bf2049d07527eed59d428c))
|
||||||
|
|
||||||
|
## [4.2.0](https://github.com/sern-handler/handler/compare/v4.1.1...v4.2.0) (2025-01-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* 4.2.0 load multiple directories & `handleModuleErrors` ([#378](https://github.com/sern-handler/handler/issues/378)) ([f9e7eaf](https://github.com/sern-handler/handler/commit/f9e7eaf92d22b76d3d02a1bbe8324ca6813f48f8))
|
||||||
|
|
||||||
|
## [4.1.1](https://github.com/sern-handler/handler/compare/v4.1.0...v4.1.1) (2025-01-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove rxjs ([#376](https://github.com/sern-handler/handler/issues/376)) ([59d08ef](https://github.com/sern-handler/handler/commit/59d08ef207c486ce1cf0aba267e6f862838e0dfb))
|
||||||
|
* This puts the light back into lightweight (\- 4.1 MB)
|
||||||
|
|
||||||
|
## [4.1.0](https://github.com/sern-handler/handler/compare/v4.0.3...v4.1.0) (2025-01-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* moduleinfo-in-eventplugins ([#373](https://github.com/sern-handler/handler/issues/373)) ([220a60e](https://github.com/sern-handler/handler/commit/220a60ecf853df8d288de2533c669562a430c3f9))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update github username ([#371](https://github.com/sern-handler/handler/issues/371)) ([55715d5](https://github.com/sern-handler/handler/commit/55715d565990fe686159f3c1eda3754d1262c72c))
|
||||||
|
|
||||||
|
## [4.0.3](https://github.com/sern-handler/handler/compare/v4.0.2...v4.0.3) (2024-10-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* async presence ([#369](https://github.com/sern-handler/handler/issues/369)) ([eabfb81](https://github.com/sern-handler/handler/commit/eabfb81819b53a4656d8eac6e21cfb488b724a42))
|
||||||
|
* fix eventModule typing for Discord events ([#368](https://github.com/sern-handler/handler/issues/368)) ([1789ccb](https://github.com/sern-handler/handler/commit/1789ccb2f22f502f87538fecdb07106ff7110434))
|
||||||
|
|
||||||
|
## [4.0.2](https://github.com/sern-handler/handler/compare/v4.0.1...v4.0.2) (2024-08-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* type issue ([2106cdc](https://github.com/sern-handler/handler/commit/2106cdc1d033f88b6ee4ccca6754fe7a595a9328))
|
||||||
|
|
||||||
|
## [4.0.1](https://github.com/sern-handler/handler/compare/v4.0.0...v4.0.1) (2024-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add SDT typings to autocomplete commands ([#363](https://github.com/sern-handler/handler/issues/363)) ([92623d2](https://github.com/sern-handler/handler/commit/92623d2914fb80e31365f06cf896bb37f36fc814))
|
||||||
|
|
||||||
|
## [4.0.0](https://github.com/sern-handler/handler/compare/v3.3.4...v4.0.0) (2024-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* v4 ([#361](https://github.com/sern-handler/handler/issues/361)) ([9a8904f](https://github.com/sern-handler/handler/commit/9a8904f5aed4fa36b018ad73bbe58049bae33274))
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Chores
|
||||||
|
|
||||||
|
* release 4.0.0 ([dda0e33](https://github.com/sern-handler/handler/commit/dda0e3395b6704862bfd3fda2a201e2cb9b45d2f))
|
||||||
|
|
||||||
|
## [3.3.4](https://github.com/sern-handler/handler/compare/v3.3.3...v3.3.4) (2024-03-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* sern emitter err ([#358](https://github.com/sern-handler/handler/issues/358)) ([90e55df](https://github.com/sern-handler/handler/commit/90e55dfa1466c91e5da48922251309331921b1ef))
|
||||||
|
|
||||||
|
## [3.3.3](https://github.com/sern-handler/handler/compare/v3.3.2...v3.3.3) (2024-02-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* rm deprecated class modules, clean up, rm indirection ([#355](https://github.com/sern-handler/handler/issues/355)) ([48f9f6e](https://github.com/sern-handler/handler/commit/48f9f6ec16e650d574bd24dcbb0ed176933bfe17))
|
||||||
|
* singleton init not being fired when inserting function ([07b11b3](https://github.com/sern-handler/handler/commit/07b11b357baac0c3c7055c022bc353995c80f766))
|
||||||
|
* typings and cleanup ([#356](https://github.com/sern-handler/handler/issues/356)) ([ce8c4bf](https://github.com/sern-handler/handler/commit/ce8c4bf6492b9680fb1c1a530d3e0028f214ad2f))
|
||||||
|
|
||||||
|
## [3.3.2](https://github.com/sern-handler/handler/compare/v3.3.1...v3.3.2) (2024-01-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* presence feature not working on cjs applications ([#351](https://github.com/sern-handler/handler/issues/351)) ([4f23871](https://github.com/sern-handler/handler/commit/4f2387119acfde036d0d1626553e9050f55627d1))
|
||||||
|
|
||||||
|
## [3.3.1](https://github.com/sern-handler/handler/compare/v3.3.0...v3.3.1) (2024-01-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* crashing when slash command is used as text command ([#349](https://github.com/sern-handler/handler/issues/349)) ([a359f73](https://github.com/sern-handler/handler/commit/a359f73fa24127a4964d411c8c1c0dfea5edc0f1))
|
||||||
|
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* the last commit ([655bb8d](https://github.com/sern-handler/handler/commit/655bb8d35815fe0ce9797d8b169310a07b284ae0))
|
||||||
|
|
||||||
|
## [3.3.0](https://github.com/sern-handler/handler/compare/v3.2.1...v3.3.0) (2023-12-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* presence ([#345](https://github.com/sern-handler/handler/issues/345)) ([7458bef](https://github.com/sern-handler/handler/commit/7458befe8a5900480cd71900df02a8364837dc00))
|
||||||
|
|
||||||
|
## [3.2.1](https://github.com/sern-handler/handler/compare/v3.2.0...v3.2.1) (2023-12-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* logger swap failing ([daac37c](https://github.com/sern-handler/handler/commit/daac37c28858c42b21042bdcb8141239db634e7d))
|
||||||
|
|
||||||
## [3.2.0](https://github.com/sern-handler/handler/compare/v3.1.1...v3.2.0) (2023-12-15)
|
## [3.2.0](https://github.com/sern-handler/handler/compare/v3.1.1...v3.2.0) (2023-12-15)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 sern
|
Copyright (c) 2025 sern
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
83
README.md
83
README.md
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<div align="center" styles="margin-top: 10px">
|
<div align="center" styles="margin-top: 10px">
|
||||||
<img src="https://img.shields.io/badge/open-source-brightgreen" />
|
<img src="https://img.shields.io/badge/open-source-brightgreen" />
|
||||||
|
<img src="https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev"/>
|
||||||
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/v/@sern/handler?maxAge=3600" alt="NPM version" /></a>
|
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/v/@sern/handler?maxAge=3600" alt="NPM version" /></a>
|
||||||
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/dt/@sern/handler?maxAge=3600" alt="NPM downloads" /></a>
|
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/dt/@sern/handler?maxAge=3600" alt="NPM downloads" /></a>
|
||||||
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="License MIT" /></a>
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="License MIT" /></a>
|
||||||
@@ -19,28 +20,16 @@
|
|||||||
- Lightweight. Does a lot while being small.
|
- Lightweight. Does a lot while being small.
|
||||||
- Latest features. Support for discord.js v14 and all of its interactions.
|
- Latest features. Support for discord.js v14 and all of its interactions.
|
||||||
- Start quickly. Plug and play or customize to your liking.
|
- Start quickly. Plug and play or customize to your liking.
|
||||||
- Switch and customize how errors are handled, logging, and more.
|
- Works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
|
||||||
- works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
|
|
||||||
- Use it with TypeScript or JavaScript. CommonJS and ESM supported.
|
- Use it with TypeScript or JavaScript. CommonJS and ESM supported.
|
||||||
- Active and growing community, always here to help. [Join us](https://sern.dev/discord)
|
- Active and growing community, always here to help. [Join us](https://sern.dev/discord)
|
||||||
- Unleash its full potential with a powerful CLI and awesome plugins.
|
- Unleash its full potential with a powerful CLI and awesome plugins.
|
||||||
|
|
||||||
## 📜 Installation
|
## 📜 Installation
|
||||||
|
[Start here!!](https://sern.dev/v4/reference/getting-started)
|
||||||
```sh
|
|
||||||
npm install @sern/handler
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
yarn add @sern/handler
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm add @sern/handler
|
|
||||||
```
|
|
||||||
|
|
||||||
## 👶 Basic Usage
|
## 👶 Basic Usage
|
||||||
<details open><summary>ping.ts</summary>
|
<details><summary>ping.ts</summary>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export default commandModule({
|
export default commandModule({
|
||||||
@@ -54,64 +43,30 @@ export default commandModule({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
<details open><summary>modal.ts</summary>
|
|
||||||
|
|
||||||
```ts
|
# Show off your sern Discord Bot!
|
||||||
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>
|
|
||||||
|
|
||||||
|
## Badge
|
||||||
```ts
|
- Copy this and add it to your [README.md](https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev)
|
||||||
import { Client, GatewayIntentBits } from 'discord.js';
|
<img src="https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev">
|
||||||
import { Sern, single } from '@sern/handler';
|
|
||||||
|
|
||||||
//client has been declared previously
|
|
||||||
//Version 3
|
|
||||||
await makeDependencies({
|
|
||||||
build: root => root
|
|
||||||
.add({ '@sern/client': single(() => client) })
|
|
||||||
});
|
|
||||||
|
|
||||||
//View docs for all options
|
|
||||||
Sern.init({
|
|
||||||
defaultPrefix: '!', // removing defaultPrefix will shut down text commands
|
|
||||||
commands: 'src/commands',
|
|
||||||
// events: 'src/events' (optional),
|
|
||||||
});
|
|
||||||
|
|
||||||
client.login("YOUR_BOT_TOKEN_HERE");
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 🤖 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 to your favorite artists on Discord.
|
||||||
- [ava](https://github.com/SrIzan10/ava), A discord bot that plays KNGI and Gensokyo Radio.
|
- [Murayama](https://github.com/murayamabot/murayama) - :pepega:
|
||||||
- [Murayama](https://github.com/murayamabot/murayama), :pepega:
|
- [Protector](https://github.com/GlitchApotamus/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.
|
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud) - A fun bot for a small, but growing server.
|
||||||
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud), A fun bot for a small - but growing - server.
|
- [Man Nomic](https://github.com/jacoobes/man-nomic) - A simple information bot to provide information to the nomic-ai Discord community.
|
||||||
|
- [Linear-Discord](https://github.com/sern-handler/linear-discord) - Display and manage a linear dashboard.
|
||||||
|
- [ZenithBot](https://github.com/CodeCraftersHaven/ZenithBot) - A versatile bot coded in TypeScript, designed to enhance server management and user interaction through its robust features.
|
||||||
|
|
||||||
## 💻 CLI
|
## 💻 CLI
|
||||||
|
|
||||||
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.
|
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🔗 Links
|
## 🔗 Links
|
||||||
|
|
||||||
- [Official Documentation and Guide](https://sern.dev)
|
- [Official Documentation and Guide](https://sern.dev)
|
||||||
|
|||||||
4
bot/.gitignore
vendored
Normal file
4
bot/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
|
.env
|
||||||
|
.sern
|
||||||
6
bot/README.md
Normal file
6
bot/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Test bot
|
||||||
|
|
||||||
|
|
||||||
|
## add .env
|
||||||
|
DISCORD_TOKEN=<token>
|
||||||
|
NODE_ENV=<production|development>
|
||||||
12
bot/assets/locals/en-US.json
Normal file
12
bot/assets/locals/en-US.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"command/ping": {
|
||||||
|
"name": "ping",
|
||||||
|
"description": "yeth",
|
||||||
|
"options": {
|
||||||
|
"asdfs": {
|
||||||
|
"name": "shidenglish",
|
||||||
|
"description": "yeah"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
bot/assets/locals/es-ES.json
Normal file
12
bot/assets/locals/es-ES.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"command/ping": {
|
||||||
|
"name": "ping",
|
||||||
|
"description": "hola",
|
||||||
|
"options": {
|
||||||
|
"asdfs": {
|
||||||
|
"name": "shidspnaol",
|
||||||
|
"description": "si"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
bot/assets/test.txt
Normal file
1
bot/assets/test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "sdfasdfas": "asdf" }
|
||||||
418
bot/package-lock.json
generated
Normal file
418
bot/package-lock.json
generated
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
{
|
||||||
|
"name": "plugtest",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "plugtest",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"dependencies": {
|
||||||
|
"@sern/handler": "file:../",
|
||||||
|
"@sern/localizer": "^1.1.3",
|
||||||
|
"@sern/publisher": "^1.1.2",
|
||||||
|
"discord.js": "^14.15.0",
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^17.0.25",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"..": {
|
||||||
|
"name": "@sern/handler",
|
||||||
|
"version": "4.2.4",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sern/ioc": "^1.1.2",
|
||||||
|
"callsites": "^3.1.0",
|
||||||
|
"cron": "^3.1.7",
|
||||||
|
"deepmerge": "^4.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^8.0.1",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||||
|
"@typescript-eslint/parser": "5.59.1",
|
||||||
|
"discord.js": "^14.14.1",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"typescript": "5.0.2",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.0.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../tools/packages/builder": {
|
||||||
|
"name": "@sern/builder",
|
||||||
|
"version": "1.0.0-rc1",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../handler": {
|
||||||
|
"name": "@sern/handler",
|
||||||
|
"version": "4.2.3",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sern/ioc": "^1.1.2",
|
||||||
|
"callsites": "^3.1.0",
|
||||||
|
"cron": "^3.1.7",
|
||||||
|
"deepmerge": "^4.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^8.0.1",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||||
|
"@typescript-eslint/parser": "5.59.1",
|
||||||
|
"discord.js": "^14.14.1",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"typescript": "5.0.2",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.0.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../tools/packages/builder": {
|
||||||
|
"name": "@sern/builder",
|
||||||
|
"version": "1.0.0-rc1",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../tools/packages/localizer": {
|
||||||
|
"name": "@sern/localizer",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"shrimple-locales": "^0.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sern/handler": "^4.0.0",
|
||||||
|
"discord.js": "^14.15.3",
|
||||||
|
"vitest": "^1.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/builders": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/formatters": "^0.6.0",
|
||||||
|
"@discordjs/util": "^1.1.1",
|
||||||
|
"@sapphire/shapeshift": "^4.0.0",
|
||||||
|
"discord-api-types": "^0.37.114",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"ts-mixer": "^6.0.4",
|
||||||
|
"tslib": "^2.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/collection": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/formatters": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.37.114"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.1",
|
||||||
|
"@discordjs/util": "^1.1.1",
|
||||||
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
|
"@sapphire/snowflake": "^3.5.3",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||||
|
"discord-api-types": "^0.37.114",
|
||||||
|
"magic-bytes.js": "^1.10.0",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.19.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/util": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.0",
|
||||||
|
"@discordjs/rest": "^2.4.1",
|
||||||
|
"@discordjs/util": "^1.1.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.2",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||||
|
"discord-api-types": "^0.37.114",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"ws": "^8.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/async-queue": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/shapeshift": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/snowflake": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sern/handler": {
|
||||||
|
"resolved": "..",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@sern/localizer": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sern/localizer/-/localizer-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-hTn0DtiAzIWSuokqMsvnVuFqU+P776p/Yv5etlrq+CWDgw332Hwuj3geyqN1C0yEjwF+ceyXJE/kGu2/inkEyg==",
|
||||||
|
"dependencies": {
|
||||||
|
"shrimple-locales": "^0.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sern/publisher": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sern/publisher/-/publisher-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-1zh99JZykKUhqHhE75ZXfiLsBtf1WI+NnDCojv8UlpnGBEyzO8xyI1X7PNf6cPKRs4W9XqY3PqTJ+hrqzIsMkg=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "17.0.45",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
|
||||||
|
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.5.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
||||||
|
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vladfrangu/async_event_emitter": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/discord-api-types": {
|
||||||
|
"version": "0.37.119",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz",
|
||||||
|
"integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg=="
|
||||||
|
},
|
||||||
|
"node_modules/discord.js": {
|
||||||
|
"version": "14.17.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.3.tgz",
|
||||||
|
"integrity": "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/builders": "^1.10.0",
|
||||||
|
"@discordjs/collection": "1.5.3",
|
||||||
|
"@discordjs/formatters": "^0.6.0",
|
||||||
|
"@discordjs/rest": "^2.4.2",
|
||||||
|
"@discordjs/util": "^1.1.1",
|
||||||
|
"@discordjs/ws": "^1.2.0",
|
||||||
|
"@sapphire/snowflake": "3.5.3",
|
||||||
|
"discord-api-types": "^0.37.114",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
|
"lodash.snakecase": "4.1.1",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.19.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
|
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.snakecase": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
|
||||||
|
},
|
||||||
|
"node_modules/magic-bytes.js": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="
|
||||||
|
},
|
||||||
|
"node_modules/shrimple-locales": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/shrimple-locales/-/shrimple-locales-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-j2vNBDXJgED3XqGXCD/vqXBSqwlDXP1iGkseVos8mCtZqHp3R+0FImx8xwtjeYufJcYfhjBMkaBTWgsBi8eJZw=="
|
||||||
|
},
|
||||||
|
"node_modules/ts-mixer": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "6.19.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
|
||||||
|
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
bot/package.json
Normal file
30
bot/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "plugtest",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "a descriptiuon",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "sern build",
|
||||||
|
"start": "node ./dist/index.js",
|
||||||
|
"run": "node ./dist/index.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"typescript",
|
||||||
|
"sern",
|
||||||
|
"discord.js"
|
||||||
|
],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"dependencies": {
|
||||||
|
"@sern/handler": "file:../",
|
||||||
|
"@sern/localizer": "^1.1.3",
|
||||||
|
"@sern/publisher": "^1.1.2",
|
||||||
|
"discord.js": "^14.15.0",
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^17.0.25",
|
||||||
|
"typescript": "latest"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
11
bot/rm.py
Normal file
11
bot/rm.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk('.'):
|
||||||
|
for filename in files:
|
||||||
|
if filename.endswith('.js'):
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
print(f'Successfully deleted: {file_path}')
|
||||||
|
except OSError as e:
|
||||||
|
print(f'Error deleting {file_path}: {e.strerror}')
|
||||||
13
bot/sern.config.json
Normal file
13
bot/sern.config.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"language": "typescript",
|
||||||
|
"defaultPrefix": "!",
|
||||||
|
"paths": {
|
||||||
|
"base": "src",
|
||||||
|
"commands": "commands",
|
||||||
|
"events": "events"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"tags": ["Nice ass bot"],
|
||||||
|
"description": "A bot"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
bot/src/commands/!plugins.ts
Normal file
0
bot/src/commands/!plugins.ts
Normal file
37
bot/src/commands/add.ts
Normal file
37
bot/src/commands/add.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
import { ApplicationCommandOptionType } from "discord.js";
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
name: 'add',
|
||||||
|
type: CommandType.Slash,
|
||||||
|
description: 'Adds numbers together',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
name: 'numbers',
|
||||||
|
description: 'Numbers to add together separated by a space.',
|
||||||
|
required: true,
|
||||||
|
min_length: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
execute: async (ctx) => {
|
||||||
|
let numbers = ctx.options.getString('numbers')?.split(' ')!;
|
||||||
|
|
||||||
|
numbers = numbers.filter((num) => num !== '');
|
||||||
|
|
||||||
|
if (!numbers.every((num) => !isNaN(parseFloat(num)))) {
|
||||||
|
return ctx.reply({
|
||||||
|
content: 'You can only input numbers.',
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sum = numbers.reduce((acc, num) => acc + parseFloat(num), 0);
|
||||||
|
|
||||||
|
|
||||||
|
return ctx.reply({
|
||||||
|
content: `The sum is ${sum}`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
8
bot/src/commands/admin/!plugins.ts
Normal file
8
bot/src/commands/admin/!plugins.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { filter, hasRole } from "../../plugins/filter.js";
|
||||||
|
import { ownerOnly } from "../../plugins/ownerOnly.js";
|
||||||
|
import { ADMIN } from '../../constants.js'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
ownerOnly(),
|
||||||
|
filter({ condition: [hasRole(ADMIN)] })
|
||||||
|
]
|
||||||
11
bot/src/commands/admin/admin.ts
Normal file
11
bot/src/commands/admin/admin.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { commandModule, CommandType } from '@sern/handler'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
description: "A",
|
||||||
|
execute: (ctx, args) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
14
bot/src/commands/btn.ts
Normal file
14
bot/src/commands/btn.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
import { json } from "../plugins/json-params.js";
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Button,
|
||||||
|
plugins: [json],
|
||||||
|
execute(ctx, args) {
|
||||||
|
console.log(args.state['json/data'])
|
||||||
|
//@ts-ignore
|
||||||
|
ctx.reply(args.state['json/data'].uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
9
bot/src/commands/channelselect.ts
Normal file
9
bot/src/commands/channelselect.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.ChannelSelect,
|
||||||
|
execute: (s) => {
|
||||||
|
s.reply('clicked channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
25
bot/src/commands/chicken.ts
Normal file
25
bot/src/commands/chicken.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
import { publishConfig } from "@sern/publisher";
|
||||||
|
import { PermissionFlagsBits } from "discord.js";
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
plugins: [
|
||||||
|
publishConfig({
|
||||||
|
integrationTypes: ['User'],
|
||||||
|
contexts: [0,1,2],
|
||||||
|
defaultMemberPermissions:
|
||||||
|
PermissionFlagsBits.Speak
|
||||||
|
| PermissionFlagsBits.Connect
|
||||||
|
| PermissionFlagsBits.BanMembers
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
],
|
||||||
|
description: "yo",
|
||||||
|
execute:(ctx) => {
|
||||||
|
ctx.reply("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
103
bot/src/commands/collectors.ts
Normal file
103
bot/src/commands/collectors.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { CommandType, Context, commandModule } from "@sern/handler";
|
||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||||
|
|
||||||
|
export default commandModule ({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
description: 'collectors',
|
||||||
|
execute: async (ctx) => {
|
||||||
|
//await close(ctx)
|
||||||
|
await testCollect(ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const testCollect = async (ctx: Context) => {
|
||||||
|
const msgcmpt = ctx.interaction.channel?.createMessageComponentCollector()
|
||||||
|
const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("closeyes").setLabel("Yes").setStyle(ButtonStyle.Success),
|
||||||
|
new ButtonBuilder().setCustomId("closeno").setLabel("No").setStyle(ButtonStyle.Danger)
|
||||||
|
);
|
||||||
|
ctx.reply({ components: [buttonRow] })
|
||||||
|
msgcmpt?.on('collect', async button => {
|
||||||
|
|
||||||
|
await button.deferUpdate();
|
||||||
|
if (button.customId === "closeyes") {
|
||||||
|
try {
|
||||||
|
await button.editReply('closing')
|
||||||
|
} catch (e) {
|
||||||
|
await button.editReply({ content: "An error has occurred and I could not close the ticket...", components: [] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await button.editReply({ content: "This ticket will remain open.", components: [] });
|
||||||
|
msgcmpt.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//export const quiz = async(client: Client, ctx: Context) => {
|
||||||
|
// try {
|
||||||
|
// const pokemon = Math.round(Math.random() * 890)
|
||||||
|
// const question = `https://cdn.dagpi.xyz/wtp/pokemon/${pokemon}q.png`;
|
||||||
|
// const answer = `https://cdn.dagpi.xyz/wtp/pokemon/${pokemon}a.png`;
|
||||||
|
//
|
||||||
|
// const correctPokemon = await (await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)).json();
|
||||||
|
// const allPokemon = await (await fetch("https://pokeapi.co/api/v2/pokemon?limit=899")).json();
|
||||||
|
//
|
||||||
|
// const options: string[] = [];
|
||||||
|
// //client.utils.log("WARNING", "INFO", `Correct answer is ${correctPokemon.name}`);
|
||||||
|
//
|
||||||
|
// while (options.length < 9) {
|
||||||
|
// let option = allPokemon.results[pokemon];
|
||||||
|
// if (options.includes(option.name)) continue;
|
||||||
|
// options.push(option.name);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!options.includes(correctPokemon.name)) {
|
||||||
|
// options.splice(client.utils.randomRange(0, 10), 0, correctPokemon.name.toLowerCase());
|
||||||
|
// } else {
|
||||||
|
// while (options.length < 10) {
|
||||||
|
// let option = allPokemon.results[client.utils.randomRange(1, 890)];
|
||||||
|
// if (options.includes(option.name)) continue;
|
||||||
|
// options.push(option.name);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const msgEmbed = (await client.utils.CustomEmbed({ userID: ctx.user.id })).setTitle("Who's that Pokémon?").setImage(question);
|
||||||
|
// const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||||
|
// new StringSelectMenuBuilder().setCustomId("pokequiz").addOptions(
|
||||||
|
// options.sort().map((opt) => {
|
||||||
|
// return { label: client.utils.titleCase(opt), value: opt.toLowerCase() };
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const msg = await client.utils.fetchReply(ctx.interaction, { embeds: [msgEmbed], components: [row] });
|
||||||
|
// const filter = (i: StringSelectMenuInteraction) => i.user.id === ctx.user.id && i.message.id === msg.id;
|
||||||
|
//
|
||||||
|
// const collector = msg.createMessageComponentCollector({ filter, componentType: ComponentType.StringSelect, time: 1000 * 20 });
|
||||||
|
// collector.on("collect", async (i) => {
|
||||||
|
// const guess = i.values[0].toLowerCase();
|
||||||
|
//
|
||||||
|
// msgEmbed.setImage(answer).setTitle(`It's ${client.utils.titleCase(correctPokemon.name)}!`);
|
||||||
|
//
|
||||||
|
// if (guess === correctPokemon.name.toLowerCase()) msgEmbed.setColor("Green").setFooter({ text: "You're correct!" });
|
||||||
|
// else msgEmbed.setColor("Red").setFooter({ text: `You guessed ${client.utils.titleCase(guess)}.` });
|
||||||
|
//
|
||||||
|
// await i.update({ embeds: [msgEmbed], components: [] });
|
||||||
|
// collector.stop("Guessed");
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// collector.on("end", async (i, reason) => {
|
||||||
|
// if (reason === "Guessed") return;
|
||||||
|
//
|
||||||
|
// msgEmbed
|
||||||
|
// .setImage(answer)
|
||||||
|
// .setTitle(`It's ${client.utils.titleCase(correctPokemon.name)}!`)
|
||||||
|
// .setColor("Red")
|
||||||
|
// .setFooter({ text: "You did not guess in time." });
|
||||||
|
//
|
||||||
|
// await ctx.interaction.editReply({ embeds: [msgEmbed], components: [] });
|
||||||
|
// });
|
||||||
|
// } catch (e) {
|
||||||
|
// client.utils.log("ERROR", __filename, `${e}`);
|
||||||
|
// return ctx.interaction.reply({ content: "An error has occurred. Please try again.", ephemeral: true });
|
||||||
|
// }
|
||||||
|
//};
|
||||||
6
bot/src/commands/dmMe.ts
Normal file
6
bot/src/commands/dmMe.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Modal,
|
||||||
|
execute: (modal) => modal.reply('thanks')
|
||||||
|
});
|
||||||
24
bot/src/commands/flat-autocmp.ts
Normal file
24
bot/src/commands/flat-autocmp.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { commandModule, CommandType } from "@sern/handler";
|
||||||
|
import { ApplicationCommandOptionType } from "discord.js";
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
description: "testing",
|
||||||
|
type: CommandType.Slash,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "option",
|
||||||
|
description: "option desc",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: true,
|
||||||
|
autocomplete: true,
|
||||||
|
command: {
|
||||||
|
execute: (i) => {
|
||||||
|
i.respond([{ name: "rah", value: "rah" }]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
execute: (ctx) => {
|
||||||
|
return ctx.reply("rah");
|
||||||
|
},
|
||||||
|
});
|
||||||
52
bot/src/commands/nested.ts
Normal file
52
bot/src/commands/nested.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { commandModule, CommandType } from "@sern/handler";
|
||||||
|
import { ActionRowBuilder, ApplicationCommandOptionType, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
description : 'a ping command',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "nest",
|
||||||
|
description: "testing nested",
|
||||||
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
|
options : [
|
||||||
|
{
|
||||||
|
name: "nest",
|
||||||
|
description: "testing nested",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options : [
|
||||||
|
{
|
||||||
|
name: "sdfasd",
|
||||||
|
description: "testing autocomplete",
|
||||||
|
autocomplete: true,
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
command : {
|
||||||
|
onEvent : [],
|
||||||
|
async execute(autocmp, sdt) {
|
||||||
|
//console.log(autocmp)
|
||||||
|
const choices = ['butt', 'deez', 'lmao', 'lmfao', 'nuts', 'chicken'];
|
||||||
|
await autocmp.respond(choices.map((e,i) => ({ name : e, value: i.toString()})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
async execute ({ interaction }) {
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId('dmMe')
|
||||||
|
.setTitle('send something to my dm (nothing bad pls)');
|
||||||
|
|
||||||
|
const input = new TextInputBuilder()
|
||||||
|
.setCustomId('message')
|
||||||
|
.setLabel("Send something to me")
|
||||||
|
.setStyle(TextInputStyle.Short);
|
||||||
|
|
||||||
|
const firstActionRow = new ActionRowBuilder<TextInputBuilder>().addComponents([input]);
|
||||||
|
modal.addComponents([firstActionRow]);
|
||||||
|
await interaction.showModal(modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
8
bot/src/commands/ping-ctx-msg.ts
Normal file
8
bot/src/commands/ping-ctx-msg.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { CommandType, commandModule } from '@sern/handler'
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.CtxMsg,
|
||||||
|
execute: (i, sdt) => {
|
||||||
|
i.reply('pong msg')
|
||||||
|
}
|
||||||
|
})
|
||||||
9
bot/src/commands/ping-ctx.ts
Normal file
9
bot/src/commands/ping-ctx.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.CtxUser,
|
||||||
|
execute: (i, sdt) => {
|
||||||
|
i.reply('pong')
|
||||||
|
}
|
||||||
|
})
|
||||||
84
bot/src/commands/ping.ts
Normal file
84
bot/src/commands/ping.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {commandModule, CommandType, controller, CommandInitPlugin, CommandControlPlugin } from '@sern/handler';
|
||||||
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
ChannelSelectMenuBuilder,
|
||||||
|
RoleSelectMenuBuilder,
|
||||||
|
UserSelectMenuBuilder,
|
||||||
|
} from "discord.js";
|
||||||
|
import { localize } from '@sern/localizer';
|
||||||
|
|
||||||
|
const plugin = CommandControlPlugin(() => {
|
||||||
|
return controller.next({ a: 'from plugin1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
const plugin2 = CommandControlPlugin(() => {
|
||||||
|
return controller.next({ a: 'from plugin2' });
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateDescription = (description: string) => {
|
||||||
|
return CommandInitPlugin(() => {
|
||||||
|
if(description.length > 100) {
|
||||||
|
console.error("Description is invalid")
|
||||||
|
return controller.stop("From updateDescription: description is invalid");
|
||||||
|
}
|
||||||
|
return controller.next({ description }); // continue to next plugin
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
plugins: [localize()],
|
||||||
|
description: 'A ping command I just updated',
|
||||||
|
options: [
|
||||||
|
str(name("asdfs"),
|
||||||
|
description("sdfds"))
|
||||||
|
],
|
||||||
|
execute: async (ctx, sdt) => {
|
||||||
|
ctx.interaction
|
||||||
|
const btn = new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Link)
|
||||||
|
.setLabel("Click me")
|
||||||
|
.setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ&pp=ygUIcmlja3JvbGw%3D')
|
||||||
|
|
||||||
|
const editButton = new ButtonBuilder({
|
||||||
|
customId: `btn/{"uid":"1061421834341462036"}`,
|
||||||
|
label: "click me also",
|
||||||
|
emoji: "🛠",
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.reply({ components: [
|
||||||
|
new ActionRowBuilder<ButtonBuilder>().addComponents(btn, editButton),
|
||||||
|
new ActionRowBuilder<UserSelectMenuBuilder>({
|
||||||
|
components: [
|
||||||
|
new UserSelectMenuBuilder({
|
||||||
|
custom_id: "userselect",
|
||||||
|
placeholder: "select channel",
|
||||||
|
minValues: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
|
||||||
|
new ChannelSelectMenuBuilder({
|
||||||
|
custom_id: "channelselect",
|
||||||
|
placeholder: "select channel",
|
||||||
|
minValues: 1,
|
||||||
|
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
new ActionRowBuilder<RoleSelectMenuBuilder>({
|
||||||
|
components: [
|
||||||
|
new RoleSelectMenuBuilder({
|
||||||
|
custom_id: "roleselect",
|
||||||
|
placeholder: "select role",
|
||||||
|
minValues: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
]})
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
9
bot/src/commands/roleselect.ts
Normal file
9
bot/src/commands/roleselect.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler"
|
||||||
|
|
||||||
|
export default commandModule( {
|
||||||
|
type: CommandType.RoleSelect,
|
||||||
|
execute: (s) => {
|
||||||
|
s.reply('selected role')
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
12
bot/src/commands/sernoptions.ts
Normal file
12
bot/src/commands/sernoptions.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
description: 'shid',
|
||||||
|
execute({ interaction }) {
|
||||||
|
interaction.reply('hello')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
62
bot/src/commands/subcommandoption.ts
Normal file
62
bot/src/commands/subcommandoption.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { commandModule, CommandType } from '@sern/handler';
|
||||||
|
import { ApplicationCommandOptionType } from 'discord.js';
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
description: 'A ping command',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "art",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
description: "Lists out information about an Animal Crossing artwork.",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
description: "The name of the artwork to lookup.",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
autocomplete: true,
|
||||||
|
required: true,
|
||||||
|
command: {
|
||||||
|
async execute(ctx) {
|
||||||
|
await ctx.respond([{ name: 'art', value: 'first' }])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "villager",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
description: "Lists out information about an Animal Crossing villager.",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
description: "The name of the villager to lookup.",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
autocomplete: true,
|
||||||
|
required: true,
|
||||||
|
command: {
|
||||||
|
onEvent: [],
|
||||||
|
async execute(ctx) {
|
||||||
|
await ctx.respond([{ name: 'villager', value: 'second' } ])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
execute: async (ctx) => {
|
||||||
|
const command = ctx.options.getSubcommand();
|
||||||
|
switch (command) {
|
||||||
|
case "art": {
|
||||||
|
|
||||||
|
ctx.reply('art');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "villager": {
|
||||||
|
ctx.reply('vil');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
40
bot/src/commands/test-ctx.ts
Normal file
40
bot/src/commands/test-ctx.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { ApplicationCommandOptionType } from "discord.js";
|
||||||
|
import { Service, commandModule, CommandType } from "@sern/handler";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
guildIds: ['941002690211766332']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Both,
|
||||||
|
description: 'tests context',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
name: "hello",
|
||||||
|
description: "wassup",
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
async execute(ctx) {
|
||||||
|
const logger = Service('@sern/logger');
|
||||||
|
|
||||||
|
if(ctx.isMessage()) {
|
||||||
|
logger?.info({ message : ctx.message.content })
|
||||||
|
logger?.info({ message : ctx.prefix })
|
||||||
|
} else {
|
||||||
|
logger?.info({ message : ctx.interaction.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
logger?.info({ message: ctx.id })
|
||||||
|
logger?.info({ message: ctx.channel?.toString()! })
|
||||||
|
logger?.info({ message: ctx.user.toString()! })
|
||||||
|
logger?.info({ message: ctx.createdTimestamp.toString() })
|
||||||
|
logger?.info({ message: ctx.guild?.toString() })
|
||||||
|
logger?.info({ message: ctx.member?.toString() })
|
||||||
|
logger?.info({ message: ctx.client })
|
||||||
|
logger?.info({ message: ctx.inGuild })
|
||||||
|
await ctx.reply("guayin bodishivatta")
|
||||||
|
}
|
||||||
|
})
|
||||||
33
bot/src/commands/testing-neo.ts
Normal file
33
bot/src/commands/testing-neo.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
import { ActionRowBuilder, ModalActionRowComponentBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
|
||||||
|
|
||||||
|
const informationRequestModal = new ModalBuilder()
|
||||||
|
.setCustomId("information-request")
|
||||||
|
.setTitle("More Information")
|
||||||
|
.addComponents(
|
||||||
|
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId("command-name")
|
||||||
|
.setLabel("Command Name")
|
||||||
|
.setPlaceholder("The name of the command that this bug occurred on.")
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setMinLength(4)
|
||||||
|
.setMaxLength(20)
|
||||||
|
.setRequired(true)));
|
||||||
|
|
||||||
|
export default commandModule({
|
||||||
|
type: CommandType.Slash,
|
||||||
|
plugins: [],
|
||||||
|
description: "A random test command.",
|
||||||
|
execute: async (ctx) => {
|
||||||
|
await ctx.interaction.showModal(informationRequestModal);
|
||||||
|
|
||||||
|
await ctx.interaction
|
||||||
|
.awaitModalSubmit({ time: 300_000 })
|
||||||
|
.then(async (modal) => {
|
||||||
|
modal.reply("thanks brody")
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
8
bot/src/commands/userselect.ts
Normal file
8
bot/src/commands/userselect.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
|
||||||
|
export default commandModule( {
|
||||||
|
type: CommandType.UserSelect,
|
||||||
|
execute: (s) => {
|
||||||
|
s.reply('selected user')
|
||||||
|
}
|
||||||
|
})
|
||||||
1
bot/src/constants.ts
Normal file
1
bot/src/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const ADMIN = '983754333944434712'
|
||||||
18
bot/src/dependencies.d.ts
vendored
Normal file
18
bot/src/dependencies.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type {
|
||||||
|
Logging,
|
||||||
|
ErrorHandling,
|
||||||
|
CoreDependencies
|
||||||
|
} from '@sern/handler'
|
||||||
|
import type { Publisher } from '@sern/publisher';
|
||||||
|
import type { Localizer } from '@sern/localizer';
|
||||||
|
declare global {
|
||||||
|
interface Dependencies extends CoreDependencies {
|
||||||
|
localizer: Localizer;
|
||||||
|
publisher: Publisher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
10
bot/src/events/error.ts
Normal file
10
bot/src/events/error.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { EventType, eventModule } from "@sern/handler";
|
||||||
|
|
||||||
|
|
||||||
|
export default eventModule({
|
||||||
|
name: 'error',
|
||||||
|
type: EventType.Sern,
|
||||||
|
execute: (e) => {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
10
bot/src/events/messageCreate.ts
Normal file
10
bot/src/events/messageCreate.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { discordEvent } from "@sern/handler";
|
||||||
|
|
||||||
|
const execute = (...args: any[]) => {
|
||||||
|
console.log(args[0].content)
|
||||||
|
}
|
||||||
|
export default discordEvent({
|
||||||
|
name: 'messageCreate',
|
||||||
|
once: true,
|
||||||
|
execute
|
||||||
|
})
|
||||||
10
bot/src/events/module.activate.ts
Normal file
10
bot/src/events/module.activate.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {eventModule, EventType} from "@sern/handler";
|
||||||
|
|
||||||
|
|
||||||
|
export default eventModule({
|
||||||
|
type: EventType.Sern,
|
||||||
|
name: 'module.activate',
|
||||||
|
execute(args) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
9
bot/src/events/modulesLoaded.ts
Normal file
9
bot/src/events/modulesLoaded.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { CommandType, EventType, Service, eventModule } from "@sern/handler";
|
||||||
|
|
||||||
|
|
||||||
|
export default eventModule({
|
||||||
|
type: EventType.Sern,
|
||||||
|
execute: async () => {
|
||||||
|
console.log('eventmodule: all loaded');
|
||||||
|
}
|
||||||
|
})
|
||||||
8
bot/src/events/threadCreate.ts
Normal file
8
bot/src/events/threadCreate.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { discordEvent } from "@sern/handler";
|
||||||
|
|
||||||
|
export default discordEvent({
|
||||||
|
name: 'threadCreate',
|
||||||
|
execute(thread) {
|
||||||
|
console.log(thread)
|
||||||
|
}
|
||||||
|
})
|
||||||
41
bot/src/index.ts
Normal file
41
bot/src/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { makeDependencies, Sern, Service } from '@sern/handler'
|
||||||
|
import { Client, GatewayIntentBits, Partials } from 'discord.js';
|
||||||
|
import { Publisher } from '@sern/publisher'
|
||||||
|
import { Localization } from '@sern/localizer'
|
||||||
|
|
||||||
|
__DEV__: console.log(1);
|
||||||
|
|
||||||
|
const intents = GatewayIntentBits.Guilds |
|
||||||
|
GatewayIntentBits.GuildMembers |
|
||||||
|
GatewayIntentBits.GuildMessageReactions |
|
||||||
|
GatewayIntentBits.GuildMessages |
|
||||||
|
GatewayIntentBits.DirectMessages |
|
||||||
|
GatewayIntentBits.MessageContent;
|
||||||
|
|
||||||
|
const partials = [
|
||||||
|
Partials.Channel
|
||||||
|
];
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await makeDependencies(({ add }) => {
|
||||||
|
add('@sern/client', new Client({ intents, partials }));
|
||||||
|
add('localizer', Localization());
|
||||||
|
add('publisher', deps => {
|
||||||
|
return new Publisher(deps['@sern/modules'],
|
||||||
|
deps['@sern/emitter'],
|
||||||
|
deps['@sern/logger']!)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Sern.init({
|
||||||
|
commands : "./dist/commands",
|
||||||
|
events: "./dist/events",
|
||||||
|
tasks: "./dist/tasks",
|
||||||
|
defaultPrefix: "!"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
init().then(() => {
|
||||||
|
Service('@sern/client').login()
|
||||||
|
})
|
||||||
|
//View docs for all options
|
||||||
|
|
||||||
156
bot/src/plugins/args.ts
Normal file
156
bot/src/plugins/args.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* @author HighArcs
|
||||||
|
* @version 1.0.0
|
||||||
|
* @description converts array of argument strings to an object (and maps them)
|
||||||
|
* @license null
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { parsedCommandModule, args } from "../plugins/args";
|
||||||
|
* import { CommandType } from "@sern/handler";
|
||||||
|
*
|
||||||
|
* interface Arg {
|
||||||
|
* value: number;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* export default parsedCommandModule({
|
||||||
|
* type : CommandType.Text
|
||||||
|
* plugins: [args({ value: Number })],
|
||||||
|
* execute: (ctx, args) => {
|
||||||
|
* console.log(ctx.args.value);
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
commandModule,
|
||||||
|
CommandType,
|
||||||
|
Context, ControlPlugin,
|
||||||
|
Plugin, CommandControlPlugin, controller
|
||||||
|
} from "@sern/handler";
|
||||||
|
import type { Awaitable } from "discord.js";
|
||||||
|
|
||||||
|
type Converter<T> = (value?: string) => Awaitable<T>;
|
||||||
|
type Struct = Record<string, any>;
|
||||||
|
type ConverterList<T extends Struct> = {
|
||||||
|
[K in keyof T]: Converter<T[K]>;
|
||||||
|
};
|
||||||
|
type Ctx<T> = Context & { _args: T };
|
||||||
|
|
||||||
|
interface Err {
|
||||||
|
key: string;
|
||||||
|
error: string;
|
||||||
|
given: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnError<T> = (context: Ctx<T>, error: Err) => any;
|
||||||
|
|
||||||
|
type SpecialEvt<T> = {
|
||||||
|
readonly "@@plugin": symbol
|
||||||
|
} & ControlPlugin<[Ctx<T>, ]>
|
||||||
|
|
||||||
|
async function convert<T extends Struct>(
|
||||||
|
args: Array<string>,
|
||||||
|
struct: ConverterList<T>
|
||||||
|
) {
|
||||||
|
const entries = Object.entries(struct);
|
||||||
|
const result = {} as T;
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const value = args[i];
|
||||||
|
const [key, converter] = entries[i]!;
|
||||||
|
try {
|
||||||
|
result[key as keyof T] = await converter(value);
|
||||||
|
} catch (error) {
|
||||||
|
throw { key, error: String(error), given: value, index: i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParsedInputCommandModule<T extends Struct> {
|
||||||
|
name?: string;
|
||||||
|
description: string;
|
||||||
|
type: CommandType.Both | CommandType.Text | CommandType.Slash;
|
||||||
|
execute: (context: Ctx<T>, args: Array<string>) => any;
|
||||||
|
plugins: () =>
|
||||||
|
| [SpecialEvt<T>, ...Array<Plugin>]
|
||||||
|
| []
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Structs = {
|
||||||
|
string: (value: string) => String(value),
|
||||||
|
number: (value: string) => Number(value),
|
||||||
|
boolean: (value: string) => value === "true" || value === "1",
|
||||||
|
date: (value: string) => new Date(value),
|
||||||
|
integer: (value: string) => Number.parseInt(value),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parsedCommandModule<T extends Struct>(
|
||||||
|
a: ParsedInputCommandModule<T>
|
||||||
|
) {
|
||||||
|
const plugins = (a.plugins() ?? []);
|
||||||
|
return commandModule({ ...a, plugins } as never);
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Checks {
|
||||||
|
export function choices<K extends string>(
|
||||||
|
choices: K[],
|
||||||
|
value?: string
|
||||||
|
): asserts value is K {
|
||||||
|
if (!choices.includes(value as unknown as K)) {
|
||||||
|
throw "value is not in choices";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function required(value?: string): asserts value is string {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw "value is required";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function limit(min: number, max: number, value?: string) {
|
||||||
|
required(value);
|
||||||
|
const val = Structs.number(value);
|
||||||
|
if (val < min) {
|
||||||
|
throw `value must be higher than ${min}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val > max) {
|
||||||
|
throw `value must be lower than ${max}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function args<T extends Struct>(
|
||||||
|
struct: ConverterList<T>,
|
||||||
|
onError?: OnError<T>
|
||||||
|
): SpecialEvt<T> {
|
||||||
|
const plugin = CommandControlPlugin<CommandType.Both>(async (ctx, args) => {
|
||||||
|
switch(args.type) {
|
||||||
|
case "slash": {
|
||||||
|
let result: T;
|
||||||
|
} break;
|
||||||
|
case "text" : {
|
||||||
|
let result: T;
|
||||||
|
try {
|
||||||
|
result = await convert(args, struct)
|
||||||
|
} catch (e) {
|
||||||
|
if (onError) {
|
||||||
|
onError(ctx as Ctx<T>, e as Err);
|
||||||
|
}
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
//@warn - mutable assignment!
|
||||||
|
(ctx as Ctx<T>)._args = result;
|
||||||
|
return controller.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return controller.next()
|
||||||
|
})
|
||||||
|
Object.defineProperty(plugin, "@@plugin", { value: Symbol("args") })
|
||||||
|
return plugin as SpecialEvt<T>;
|
||||||
|
}
|
||||||
57
bot/src/plugins/assertFields.ts
Normal file
57
bot/src/plugins/assertFields.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* This plugin checks the fields of a ModalSubmitInteraction
|
||||||
|
* with regex or a custom callback
|
||||||
|
*
|
||||||
|
* @author @jacoobes [<@182326315813306368>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* export default commandModule({
|
||||||
|
* type: CommandType.Modal,
|
||||||
|
* plugins: [
|
||||||
|
* assertFields({
|
||||||
|
* fields: {
|
||||||
|
* // check the modal field "mcUsernameInput" with the regex /a+b+c/
|
||||||
|
* mcUsernameInput: /a+b+c+/
|
||||||
|
* },
|
||||||
|
* failure: (errors, interaction) => {
|
||||||
|
* interaction.reply(errors.join("\n"))
|
||||||
|
* }
|
||||||
|
* }),
|
||||||
|
* ],
|
||||||
|
* execute: ctx => {
|
||||||
|
* ctx.reply("nice!")
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import { CommandControlPlugin, CommandType, controller } from "@sern/handler";
|
||||||
|
import type { ModalSubmitInteraction } from "discord.js";
|
||||||
|
|
||||||
|
type Assertion =
|
||||||
|
| RegExp
|
||||||
|
| ((value : string) => boolean);
|
||||||
|
|
||||||
|
export function assertFields(config: {
|
||||||
|
fields: Record<string, Assertion>,
|
||||||
|
failure: (errors: string[], interaction: ModalSubmitInteraction) => any
|
||||||
|
}) {
|
||||||
|
return CommandControlPlugin<CommandType.Modal>(modal => {
|
||||||
|
const pairs = Object.entries(config.fields);
|
||||||
|
const errors = [];
|
||||||
|
for(const [ field, assertion ] of pairs) {
|
||||||
|
// Keep in mind this doesn't check for typos!
|
||||||
|
// feel free to add more checks.
|
||||||
|
const input = modal.fields.getTextInputValue(field)
|
||||||
|
const resolvedAssertion = assertion instanceof RegExp ? (value: string) => assertion.test(value) : assertion;
|
||||||
|
if(!resolvedAssertion(input)) {
|
||||||
|
errors.push(input + " failed to pass assertion " + resolvedAssertion.toString() )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(errors.length > 0) {
|
||||||
|
config.failure(errors, modal);
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
})
|
||||||
|
}
|
||||||
39
bot/src/plugins/channelType.ts
Normal file
39
bot/src/plugins/channelType.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* This plugin checks if a channel is the specified type
|
||||||
|
*
|
||||||
|
* @author @Benzo-Fury [<@762918086349029386>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { channelType } from "../plugins/channelType";
|
||||||
|
* import { ChannelType } from "discord.js"
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ channelType([ChannelType.GuildText], 'This cannot be used here') ],
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import { ChannelType } from "discord.js";
|
||||||
|
import {CommandControlPlugin, CommandType, controller } from "@sern/handler";
|
||||||
|
export function channelType(
|
||||||
|
channelType: ChannelType[],
|
||||||
|
onFail?: string
|
||||||
|
){
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (ctx) => {
|
||||||
|
let channel = ctx.channel?.type;
|
||||||
|
//for some reason the dm channel type was returning undefined at some points
|
||||||
|
if (channel === undefined) {
|
||||||
|
channel = ChannelType.DM;
|
||||||
|
}
|
||||||
|
if (channelType.includes(channel)) {
|
||||||
|
return controller.next();
|
||||||
|
}
|
||||||
|
if (onFail) {
|
||||||
|
await ctx.reply(onFail);
|
||||||
|
}
|
||||||
|
return controller.stop();
|
||||||
|
})
|
||||||
|
}
|
||||||
106
bot/src/plugins/confirmation.ts
Normal file
106
bot/src/plugins/confirmation.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* This is buttonConfirmation plugin, it runs confirmation prompt in the form of buttons.
|
||||||
|
* Note that you need to use edit/editReply in the command itself because we are already replying in the plugin!
|
||||||
|
* Credits to original plugin of confirmation using reactions and its author!
|
||||||
|
*
|
||||||
|
* @author @EvolutionX-10 [<@697795666373640213>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { buttonConfirmation } from "../plugins/buttonConfirmation";
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ buttonConfirmation() ],
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {CommandControlPlugin, CommandType, controller} from "@sern/handler";
|
||||||
|
import {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
ComponentType,
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
export function confirmation(
|
||||||
|
options?: Partial<ConfirmationOptions>
|
||||||
|
) {
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (ctx, args) => {
|
||||||
|
options = {
|
||||||
|
content: "Do you want to proceed?",
|
||||||
|
denialMessage: "Cancelled",
|
||||||
|
labels: ["No", "Yes"],
|
||||||
|
time: 60_000,
|
||||||
|
wrongUserResponse: "Not for you!",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons = options.labels!.map((l, i) => {
|
||||||
|
return new ButtonBuilder()
|
||||||
|
.setCustomId(l)
|
||||||
|
.setLabel(l)
|
||||||
|
.setStyle( i === 0 ? ButtonStyle.Danger : ButtonStyle.Success
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const sent = await ctx.reply({
|
||||||
|
content: options.content,
|
||||||
|
components: [
|
||||||
|
new ActionRowBuilder<ButtonBuilder>().setComponents(
|
||||||
|
buttons
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const collector = sent.createMessageComponentCollector({
|
||||||
|
componentType: ComponentType.Button,
|
||||||
|
filter: (i) => i.user.id === ctx.user.id,
|
||||||
|
time: options.time,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
collector.on("collect", async (i) => {
|
||||||
|
await i.update({ components: [] });
|
||||||
|
collector.stop();
|
||||||
|
if (i.customId === options!.labels![1]) {
|
||||||
|
resolve(controller.next());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await i.editReply({
|
||||||
|
content: options?.denialMessage,
|
||||||
|
});
|
||||||
|
resolve(controller.stop());
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("end", async (c) => {
|
||||||
|
if (c.size) return;
|
||||||
|
buttons.forEach((b) => b.setDisabled());
|
||||||
|
await sent.edit({
|
||||||
|
components: [
|
||||||
|
new ActionRowBuilder<ButtonBuilder>().setComponents(
|
||||||
|
buttons
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on("ignore", async (i) => {
|
||||||
|
await i.reply({
|
||||||
|
content: options?.wrongUserResponse,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfirmationOptions {
|
||||||
|
content: string;
|
||||||
|
denialMessage: string;
|
||||||
|
time: number;
|
||||||
|
labels: [string, string];
|
||||||
|
wrongUserResponse: string;
|
||||||
|
}
|
||||||
6
bot/src/plugins/correctFile.ts
Normal file
6
bot/src/plugins/correctFile.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import {CommandInitPlugin, controller} from "@sern/handler";
|
||||||
|
|
||||||
|
|
||||||
|
export const correctFile = CommandInitPlugin(() => {
|
||||||
|
return controller.stop()
|
||||||
|
})
|
||||||
38
bot/src/plugins/disable.ts
Normal file
38
bot/src/plugins/disable.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* Disables a command entirely, for whatever reasons you may need.
|
||||||
|
*
|
||||||
|
* @author @jacoobes [<@182326315813306368>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { disable } from "../plugins/disable";
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ disable() ],
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import { CommandType, EventPlugin, PluginType } from "@sern/handler";
|
||||||
|
import { InteractionReplyOptions, ReplyMessageOptions } from "discord.js";
|
||||||
|
|
||||||
|
export function disable(
|
||||||
|
onFail?:
|
||||||
|
| string
|
||||||
|
| Omit<InteractionReplyOptions, "fetchReply">
|
||||||
|
| ReplyMessageOptions
|
||||||
|
): EventPlugin<CommandType.Both> {
|
||||||
|
return {
|
||||||
|
type: PluginType.Event,
|
||||||
|
description: "Disables command from responding",
|
||||||
|
async execute([ctx], controller) {
|
||||||
|
if (onFail !== undefined) {
|
||||||
|
await ctx.reply(onFail);
|
||||||
|
}
|
||||||
|
return controller.stop();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
29
bot/src/plugins/dmOnly.ts
Normal file
29
bot/src/plugins/dmOnly.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* This is dmOnly plugin, it allows commands to be run only in DMs.
|
||||||
|
* For discord.js you should have the Partials.Channel and DirectMessages intent enabled.
|
||||||
|
* @author @EvolutionX-10 [<@697795666373640213>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { dmOnly } from "../plugins/dmOnly";
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [dmOnly()],
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import {CommandControlPlugin, CommandType, controller } from "@sern/handler";
|
||||||
|
export function dmOnly(
|
||||||
|
content?: string,
|
||||||
|
ephemeral?: boolean
|
||||||
|
) {
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
|
||||||
|
if (ctx.channel?.isDMBased()) return controller.next();
|
||||||
|
|
||||||
|
if (content) await ctx.reply({ content, ephemeral }); // Change this if you want or remove it for silent deny
|
||||||
|
return controller.stop();
|
||||||
|
})
|
||||||
|
}
|
||||||
637
bot/src/plugins/filter.ts
Normal file
637
bot/src/plugins/filter.ts
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
import {
|
||||||
|
CommandControlPlugin,
|
||||||
|
type CommandType,
|
||||||
|
type Context,
|
||||||
|
controller,
|
||||||
|
} from "@sern/handler";
|
||||||
|
import {
|
||||||
|
GuildMember,
|
||||||
|
GuildMemberRoleManager,
|
||||||
|
PermissionResolvable,
|
||||||
|
PermissionsBitField,
|
||||||
|
User,
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
|
||||||
|
export type Test = (context: Context) => boolean;
|
||||||
|
|
||||||
|
export class Criteria {
|
||||||
|
public constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly execute: Test,
|
||||||
|
public readonly children: Array<Criteria>
|
||||||
|
) {}
|
||||||
|
toString() {
|
||||||
|
return this.name + ' ' + this.children.map(c => c.name).join(', ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const or = (...filters: Array<FilterImpl>): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
let pass = false;
|
||||||
|
tests: for (const filter of filters) {
|
||||||
|
if (filter.test(context)) {
|
||||||
|
pass = true;
|
||||||
|
break tests;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children: Array<Criteria> = filters.map((x) => x.criteria);
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("or", execute, children),
|
||||||
|
`or(${filters.map((x) => x.message).join(", ")})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const and = (...filters: Array<FilterImpl>): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (!filter.test(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children: Array<Criteria> = filters.map((x) => x.criteria);
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("and", execute, children),
|
||||||
|
`and(${filters.map((x) => x.message).join(", ")})`
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const not = (filter: FilterImpl): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
return !filter.test(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("not", execute, [filter.criteria]),
|
||||||
|
`not(${filter.criteria})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const custom =(execute: Test, message?: string): FilterImpl => {
|
||||||
|
return new FilterImpl(new Criteria("custom", execute, []), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withCustomMessage = (
|
||||||
|
filter: FilterImpl,
|
||||||
|
message?: string
|
||||||
|
): FilterImpl => {
|
||||||
|
return new FilterImpl(filter.criteria, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasGuildPermission = (
|
||||||
|
permission: PermissionResolvable
|
||||||
|
): FilterImpl => {
|
||||||
|
const b = PermissionsBitField.resolve(permission);
|
||||||
|
const field = Object.entries(PermissionsBitField.Flags).find(
|
||||||
|
([, v]) => v === b
|
||||||
|
);
|
||||||
|
|
||||||
|
if (field === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`unknown permission \`${permission}\` in filter \`hasGuildPermission\``
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [name] = field;
|
||||||
|
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.member !== null) {
|
||||||
|
if (typeof context.member.permissions === "string") {
|
||||||
|
return new PermissionsBitField(BigInt(context.member.permissions)).has(b);
|
||||||
|
}
|
||||||
|
return context.member.permissions.has(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("hasGuildPermission", execute, []),
|
||||||
|
`has guild permission: ${name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const hasChannelPermission = (
|
||||||
|
permission: PermissionResolvable,
|
||||||
|
channelId?: string
|
||||||
|
): FilterImpl => {
|
||||||
|
const b = PermissionsBitField.resolve(permission);
|
||||||
|
const field = Object.entries(PermissionsBitField.Flags).find(
|
||||||
|
([, v]) => v === b
|
||||||
|
);
|
||||||
|
|
||||||
|
if (field === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`unknown permission \`${permission}\` in filter \`hasChannelPermission\``
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [name] = field;
|
||||||
|
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.member !== null) {
|
||||||
|
const channel =
|
||||||
|
channelId !== undefined
|
||||||
|
? context.guild?.channels.cache.get(channelId)
|
||||||
|
: context.channel;
|
||||||
|
|
||||||
|
// ?
|
||||||
|
if (channel == undefined || channel === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.isDMBased()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field2 = channel.permissionsFor(context.user);
|
||||||
|
|
||||||
|
// assume we have no permission overrides
|
||||||
|
if (field2 === null) {
|
||||||
|
if (context.member !== null) {
|
||||||
|
if (typeof context.member.permissions === "string") {
|
||||||
|
return new PermissionsBitField(
|
||||||
|
BigInt(context.member.permissions)
|
||||||
|
).has(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.member.permissions.has(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field2.has(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("hasChannelPermission", execute, []),
|
||||||
|
channelId !== undefined
|
||||||
|
? `has channel permission ${name} in <#${channelId}>`
|
||||||
|
: `has channel permission ${name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canAddReactions =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("AddReactions", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canAttachFiles =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("AttachFiles", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canBanMembers = (): FilterImpl => {
|
||||||
|
return hasGuildPermission("BanMembers");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canChangeNickname = (): FilterImpl => {
|
||||||
|
return hasGuildPermission("ChangeNickname");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canConnect = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("Connect", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canCreateInstantInvite =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("CreateInstantInvite", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canDeafenMembers =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("DeafenMembers", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canEmbedLinks =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("EmbedLinks", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canKickMembers =(): FilterImpl => {
|
||||||
|
return hasGuildPermission("KickMembers");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageChannelWebhooks =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("ManageWebhooks", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageChannels =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("ManageChannels", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageEmojisAndStickers =(): FilterImpl => {
|
||||||
|
return hasGuildPermission("ManageEmojisAndStickers");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageGuild =(): FilterImpl => {
|
||||||
|
return hasGuildPermission("ManageGuild");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageGuildWebhooks =(): FilterImpl => {
|
||||||
|
return hasGuildPermission("ManageWebhooks");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageMessages =(channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("ManageMessages", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageNicknames = (): FilterImpl => {
|
||||||
|
return hasGuildPermission("ManageNicknames");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canManageRoles = (): FilterImpl => {
|
||||||
|
return hasGuildPermission("ManageRoles");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canMentionEveryone = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("MentionEveryone", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canMoveMembers = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("MoveMembers", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canMuteMembers = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("MuteMembers", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canPrioritySpeaker = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("PrioritySpeaker", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canReadMessageHistory = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("ReadMessageHistory", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canViewChannel = (channelId: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("ViewChannel", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canSendMessages = (channelId: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("SendMessages", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canSendTtsMessages = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("SendTTSMessages", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canSpeak = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("Speak", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canStream = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("Stream", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canUseExternalEmojis = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("UseExternalEmojis", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canUseVoiceActivity = (channelId?: string): FilterImpl => {
|
||||||
|
return hasChannelPermission("UseVAD", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canViewAuditLog = (): FilterImpl => {
|
||||||
|
return hasGuildPermission("ViewAuditLog");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canViewGuildInsights = (): FilterImpl => {
|
||||||
|
return hasGuildPermission("ViewGuildInsights");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const channelIdIn = (channelIds: Array<string>): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
return channelIds.includes(
|
||||||
|
context.isMessage()
|
||||||
|
? context.message.channelId
|
||||||
|
: context.interaction.channelId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("channelIdIn", execute, []),
|
||||||
|
`channel is one of: ${channelIds.map((v) => `<#${v}>`).join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasEveryRole = (roles: Array<string>): FilterImpl => {
|
||||||
|
return withCustomMessage(
|
||||||
|
and(...roles.map((v) => hasRole(v))),
|
||||||
|
`has all of: ${roles.map((v) => `<@&${v}>`).join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasMentionableRole = (): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.member !== null) {
|
||||||
|
if (context.member.roles instanceof GuildMemberRoleManager) {
|
||||||
|
return (
|
||||||
|
context.member.roles.cache.filter((x) => x.mentionable === true)
|
||||||
|
.size > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.guild === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.member.roles
|
||||||
|
.map((roleId) => context.guild!.roles.cache.get(roleId))
|
||||||
|
.filter((x) => x !== undefined)
|
||||||
|
.some((x) => x!.mentionable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("hasMentionableRole", execute, []),
|
||||||
|
"has a mentionable role"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasNickname = (nickname?: string): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.member !== null) {
|
||||||
|
if (context.member instanceof GuildMember) {
|
||||||
|
if (nickname !== null) {
|
||||||
|
return context.member.nickname === nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.member.nickname !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nickname !== null) {
|
||||||
|
return context.member.nick === nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
context.member.nick !== null && context.member.nick !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dm members can technically have nicknames but they're per-user, so this should never be true.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new FilterImpl(new Criteria("hasNickname", execute, []), "has a nickname");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasParentId = (parentId: string): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.channel !== null) {
|
||||||
|
if (context.channel.isDMBased()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.channel.parentId === parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("hasParentId", execute, []),
|
||||||
|
`has channel parent <#${parentId}>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasRole = (roleId: string): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.member !== null) {
|
||||||
|
if (context.member.roles instanceof GuildMemberRoleManager) {
|
||||||
|
return context.member.roles.cache.has(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.guild === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.member.roles.includes(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// assume dm members have every role ever.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("hasRole", execute, []),
|
||||||
|
`has role <@&${roleId}>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasSomeRole = (roles: Array<string>): FilterImpl => {
|
||||||
|
return withCustomMessage(
|
||||||
|
or(...roles.map((role) => hasRole(role))),
|
||||||
|
`has any of: ${roles.map((v) => `<@&${v}>`).join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isAdministator = (): FilterImpl => {
|
||||||
|
return hasGuildPermission("Administrator");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isChannelId = (channelId: string): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.isMessage()) {
|
||||||
|
return context.message.channelId === channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.interaction.channelId === channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("isChannelId", execute, []),
|
||||||
|
`is channel <#${channelId}>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isChannelNsfw = (): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.channel !== null) {
|
||||||
|
if (context.channel.isDMBased() || context.channel.isThread()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.channel.nsfw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("isChannelNsfw", execute, []),
|
||||||
|
"channel marked as nsfw"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isGuildOwner = (): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.guild !== null) {
|
||||||
|
return context.guild.ownerId === context.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("isGuildOwner", execute, []),
|
||||||
|
"is guild owner"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isBotOwner = (): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
if (context.client.application !== null) {
|
||||||
|
if (context.client.application.owner !== null) {
|
||||||
|
if (context.client.application.owner instanceof User) {
|
||||||
|
return context.user.id === context.client.application.owner.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.client.application.owner.members.has(context.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nope
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new FilterImpl(new Criteria("isBotOwner", execute, []), "is bot owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isUserId = (userId: string): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
return context.user.id === userId;
|
||||||
|
}
|
||||||
|
return new FilterImpl(
|
||||||
|
new Criteria("isUserId", execute, []),
|
||||||
|
`is user: <@${userId}>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parentIdIn = (parentIds: Array<string>): FilterImpl => {
|
||||||
|
return withCustomMessage(
|
||||||
|
or(...parentIds.map((v) => hasParentId(v))),
|
||||||
|
`channel parent is one of: ${parentIds.map((v) => `<#${v}>`).join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userIdIn = (userIds: Array<string>): FilterImpl => {
|
||||||
|
return withCustomMessage(
|
||||||
|
or(...userIds.map((v) => isUserId(v))),
|
||||||
|
`user is one of: ${userIds.map((v) => `<@${v}>`).join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isInGuild = (): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
return context.guildId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FilterImpl(new Criteria("isInGuild", execute, []), "is in guild");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isInDm = (): FilterImpl => {
|
||||||
|
const notInGuild = compose(not, isInGuild);
|
||||||
|
return withCustomMessage(notInGuild(), "is in dm");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const never = (): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
void context;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new FilterImpl(new Criteria("never", execute, []), "never");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const always = (): FilterImpl => {
|
||||||
|
function execute(context: Context): boolean {
|
||||||
|
void context;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return new FilterImpl(new Criteria("always", execute, []), "always");
|
||||||
|
}
|
||||||
|
type CtxMap<T> = (arg: T) => FilterImpl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call FilterImpls in right to left order.
|
||||||
|
* @example
|
||||||
|
* import { compose, isUserId, not } from '../plugins/filter'
|
||||||
|
* const isNotUserId = compose(not, isUserId)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const compose = <T = void>(...funcs: CtxMap<any>[]): CtxMap<T> => {
|
||||||
|
return (arg: T): FilterImpl =>
|
||||||
|
//@ts-ignore
|
||||||
|
funcs.reduceRight((result, func) => func(result), arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class FilterImpl {
|
||||||
|
public readonly test: Test;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly criteria: Criteria,
|
||||||
|
public message?: string
|
||||||
|
) {
|
||||||
|
this.test = this.criteria.execute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type FilterOptions = {
|
||||||
|
condition: Array<FilterImpl> | FilterImpl,
|
||||||
|
onFailed?: (context: Context, filters: Array<FilterImpl>) => unknown
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Generalized `filter` plugin. revised by jacoobes, all credit to original author.
|
||||||
|
* Perform declarative conditionals as plugins.
|
||||||
|
* @author @trueharuu [<@504698587221852172>]
|
||||||
|
* @version 2.0.0
|
||||||
|
* @example
|
||||||
|
* import { filter, not, isGuildOwner, canMentionEveryone } from '../plugins/filter';
|
||||||
|
* import { commandModule } from '@sern/handler';
|
||||||
|
*
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: filter({ condition: [not(isGuildOwner()), canMentionEveryone()] }),
|
||||||
|
* async execute(context) {
|
||||||
|
* // your code here
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const filter =
|
||||||
|
(options: FilterOptions) => {
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (context) => {
|
||||||
|
const arrayifiedCondition = Array.isArray(options.condition) ? options.condition : [options.condition]
|
||||||
|
const value = and(...arrayifiedCondition).test(context);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
return controller.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.onFailed !== undefined) {
|
||||||
|
await options.onFailed(context, arrayifiedCondition);
|
||||||
|
} else {
|
||||||
|
await context.reply({
|
||||||
|
ephemeral: true,
|
||||||
|
content: `you do not match the criteria for this command:\n${arrayifiedCondition
|
||||||
|
.map((x) => x.message)
|
||||||
|
.filter((x) => x !== undefined)
|
||||||
|
.join("\n")}`,
|
||||||
|
allowedMentions: {
|
||||||
|
repliedUser: false,
|
||||||
|
parse: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return controller.stop();
|
||||||
|
});
|
||||||
|
};
|
||||||
40
bot/src/plugins/filterA.ts
Normal file
40
bot/src/plugins/filterA.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { PluginType, makePlugin, controller, ControlPlugin } from "@sern/handler";
|
||||||
|
import type { AutocompleteInteraction } from 'discord.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @plugin
|
||||||
|
* filters autocomplete interaction that pass the criteria
|
||||||
|
* @author @jacoobes [<@182326315813306368>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { CommandType, commandModule } from "@sern/handler";
|
||||||
|
* import { filterA } from '../plugins/filterA.js'
|
||||||
|
* export default commandModule({
|
||||||
|
* type : CommandType.Slash,
|
||||||
|
* options: [
|
||||||
|
* {
|
||||||
|
* autocomplete: true,
|
||||||
|
* command : {
|
||||||
|
* //only accept autocomplete interactions that include 'poo' in the text
|
||||||
|
* onEvent: [filterA(s => s.includes('poo'))],
|
||||||
|
* execute: (autocomplete) => {
|
||||||
|
* let data = [{ name: 'pooba', value: 'first' }, { name: 'pooga', value: 'second' }]
|
||||||
|
* autocomplete.respond(data)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* execute: (ctx, args) => {}
|
||||||
|
* })
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const filterA = (pred: (value: string) => boolean) => {
|
||||||
|
return makePlugin(PluginType.Control, (a: AutocompleteInteraction) => {
|
||||||
|
if(pred(a.options.getFocused())) {
|
||||||
|
return controller.next();
|
||||||
|
}
|
||||||
|
return controller.stop();
|
||||||
|
}) as ControlPlugin;
|
||||||
|
}
|
||||||
36
bot/src/plugins/fromCallback.ts
Normal file
36
bot/src/plugins/fromCallback.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//@ts-nocheck
|
||||||
|
/**
|
||||||
|
* @plugin
|
||||||
|
* fromCallback turns a callback into a plugin result.
|
||||||
|
* if the callback returns truthy value, plugin continues.
|
||||||
|
* This control plugin works for every command type. The arguments of the callback
|
||||||
|
* mirror the execute method on the current module.
|
||||||
|
* @author @jacoobes [<@182326315813306368>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const myServer = "941002690211766332";
|
||||||
|
* export default commandModule({
|
||||||
|
* type: CommandType.Both,
|
||||||
|
* plugins: [
|
||||||
|
* fromCallback((ctx, args) => ctx.guildId == myServer)
|
||||||
|
* ],
|
||||||
|
* execute: ctx => {
|
||||||
|
* ctx.reply("I only respond in myServer!");
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { PluginType, makePlugin, controller } from "@sern/handler";
|
||||||
|
|
||||||
|
export const fromCallback = (cb: (...args: any[]) => boolean) =>
|
||||||
|
makePlugin(PluginType.Control, (...args) => {
|
||||||
|
console.log(args)
|
||||||
|
if(cb.apply(null, args)) {
|
||||||
|
return controller.next();
|
||||||
|
}
|
||||||
|
return controller.stop();
|
||||||
|
});
|
||||||
5
bot/src/plugins/json-params.ts
Normal file
5
bot/src/plugins/json-params.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { CommandControlPlugin, CommandType, controller } from "@sern/handler";
|
||||||
|
|
||||||
|
export const json = CommandControlPlugin<CommandType.Button>((ctx, args) => {
|
||||||
|
return controller.next({ 'json/data': JSON.parse(args.params!) });
|
||||||
|
})
|
||||||
48
bot/src/plugins/nsfwOnly.ts
Normal file
48
bot/src/plugins/nsfwOnly.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* This plugin checks if the channel is nsfw and responds to user with a specified response if not nsfw
|
||||||
|
*
|
||||||
|
* @author @Benzo-Fury [<@762918086349029386>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { nsfwOnly } from "../plugins/nsfwOnly";
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ nsfwOnly('response', true) ],
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
ChannelType,
|
||||||
|
GuildTextBasedChannel,
|
||||||
|
TextBasedChannel,
|
||||||
|
TextChannel,
|
||||||
|
} from "discord.js";
|
||||||
|
import {CommandControlPlugin, CommandType, controller } from "@sern/handler";
|
||||||
|
function isGuildText(channel: TextBasedChannel|null): channel is GuildTextBasedChannel {
|
||||||
|
return (channel?.type == ChannelType.GuildPublicThread ||
|
||||||
|
channel?.type == ChannelType.GuildPrivateThread);
|
||||||
|
}
|
||||||
|
export function nsfwOnly(onFail: string, ephemeral: boolean) {
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
|
||||||
|
if (ctx.guild === null) {
|
||||||
|
await ctx.reply({ content: onFail, ephemeral });
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
//channel is thread (not supported by nsfw)
|
||||||
|
if (isGuildText(ctx.channel) == true) {
|
||||||
|
await ctx.reply({ content: onFail, ephemeral });
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
if (!(ctx.channel! as TextChannel).nsfw) {
|
||||||
|
//channel is not nsfw
|
||||||
|
await ctx.reply({ content: onFail, ephemeral });
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
//continues to command if nsfw
|
||||||
|
return controller.next();
|
||||||
|
});
|
||||||
|
}
|
||||||
30
bot/src/plugins/ownerOnly.ts
Normal file
30
bot/src/plugins/ownerOnly.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* This is OwnerOnly plugin, it allows only bot owners to run the command, like eval.
|
||||||
|
*
|
||||||
|
* @author @EvolutionX-10 [<@697795666373640213>]
|
||||||
|
* @version 1.2.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { ownerOnly } from "../plugins/ownerOnly";
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ ownerOnly() ], // can also pass array of IDs to override default owner IDs
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CommandType, CommandControlPlugin, controller } from "@sern/handler";
|
||||||
|
const ownerIDs = ["182326315813306368"]; //! Fill your ID
|
||||||
|
export function ownerOnly(override?: string[]) {
|
||||||
|
return CommandControlPlugin<CommandType.Both>((ctx) => {
|
||||||
|
if ((override ?? ownerIDs).includes(ctx.user.id))
|
||||||
|
return controller.next();
|
||||||
|
//* If you want to reply when the command fails due to user not being owner, you can use following
|
||||||
|
// await ctx.reply("Only owner can run it!!!");
|
||||||
|
return controller.stop(); //! Important: It stops the execution of command!
|
||||||
|
});
|
||||||
|
}
|
||||||
39
bot/src/plugins/permCheck.ts
Normal file
39
bot/src/plugins/permCheck.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* @plugin
|
||||||
|
* This is perm check, it allows users to parse the permission you want and let the plugin do the rest. (check user for that perm).
|
||||||
|
*
|
||||||
|
* @author @Benzo-Fury [<@762918086349029386>]
|
||||||
|
* @version 1.0.1
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { permCheck } from "../plugins/permCheck";
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ permCheck('permission', 'No permission response') ],
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { GuildMember, PermissionResolvable } from "discord.js";
|
||||||
|
import { CommandControlPlugin, CommandType, controller } from "@sern/handler";
|
||||||
|
export function permCheck(perm: PermissionResolvable, response: string) {
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (ctx, args) => {
|
||||||
|
if (ctx.guild === null) {
|
||||||
|
await ctx.reply("This command cannot be used here");
|
||||||
|
console.warn(
|
||||||
|
"PermCheck > A command stopped because we couldn't check a users permissions (was used in dms)",
|
||||||
|
); //delete this line if you dont want to be notified when a command is used outside of a guild/server
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
if (!(ctx.member! as GuildMember).permissions.has(perm)) {
|
||||||
|
await ctx.reply(response);
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
});
|
||||||
|
}
|
||||||
215
bot/src/plugins/publish.ts
Normal file
215
bot/src/plugins/publish.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* @plugin
|
||||||
|
* [DEPRECATED] It allows you to publish your application commands using the discord.js library with ease.
|
||||||
|
*
|
||||||
|
* @author @EvolutionX-10 [<@697795666373640213>]
|
||||||
|
* @version 2.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { publish } from "../plugins/publish";
|
||||||
|
* import { commandModule } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ publish() ], // put an object containing permissions, ids for guild commands, boolean for dmPermission
|
||||||
|
* // plugins: [ publish({ guildIds: ['guildId'], defaultMemberPermissions: 'Administrator'})]
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
* @end
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
CommandInitPlugin,
|
||||||
|
CommandType,
|
||||||
|
controller,
|
||||||
|
SernOptionsData,
|
||||||
|
SlashCommand,
|
||||||
|
Service,
|
||||||
|
} from "@sern/handler";
|
||||||
|
import {
|
||||||
|
ApplicationCommandData,
|
||||||
|
ApplicationCommandType,
|
||||||
|
ApplicationCommandOptionType,
|
||||||
|
PermissionResolvable,
|
||||||
|
} from "discord.js";
|
||||||
|
|
||||||
|
export const CommandTypeRaw = {
|
||||||
|
[CommandType.Both]: ApplicationCommandType.ChatInput,
|
||||||
|
[CommandType.CtxUser]: ApplicationCommandType.User,
|
||||||
|
[CommandType.CtxMsg]: ApplicationCommandType.Message,
|
||||||
|
[CommandType.Slash]: ApplicationCommandType.ChatInput,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function publish<
|
||||||
|
T extends
|
||||||
|
| CommandType.Both
|
||||||
|
| CommandType.Slash
|
||||||
|
| CommandType.CtxMsg
|
||||||
|
| CommandType.CtxUser,
|
||||||
|
>(options?: PublishOptions) {
|
||||||
|
return CommandInitPlugin<T>(async ({ module }) => {
|
||||||
|
// Users need to provide their own useContainer function.
|
||||||
|
let client;
|
||||||
|
try {
|
||||||
|
client = (await import("@sern/handler")).Service("@sern/client");
|
||||||
|
} catch {
|
||||||
|
const { useContainer } = await import("../index.js");
|
||||||
|
client = useContainer("@sern/client")[0];
|
||||||
|
}
|
||||||
|
const defaultOptions = {
|
||||||
|
guildIds: [],
|
||||||
|
dmPermission: undefined,
|
||||||
|
defaultMemberPermissions: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
options = { ...defaultOptions, ...options } as PublishOptions &
|
||||||
|
ValidPublishOptions;
|
||||||
|
let { defaultMemberPermissions, dmPermission, guildIds } =
|
||||||
|
options as unknown as ValidPublishOptions;
|
||||||
|
|
||||||
|
function c(e: unknown) {
|
||||||
|
console.error("publish command didnt work for", module.name);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const log =
|
||||||
|
(...message: any[]) =>
|
||||||
|
() =>
|
||||||
|
console.log(...message);
|
||||||
|
const logged = (...message: any[]) => log(message);
|
||||||
|
/**
|
||||||
|
* a local function that returns either one value or the other,
|
||||||
|
* depending on {t}'s CommandType. If the commandtype of
|
||||||
|
* this module is CommandType.Both or CommandType.Text or CommandType.Slash,
|
||||||
|
* return 'is', else return 'els'
|
||||||
|
* @param t
|
||||||
|
* @returns S | T
|
||||||
|
*/
|
||||||
|
const appCmd = <V extends CommandType, S, T>(t: V) => {
|
||||||
|
return (is: S, els: T) => ((t & CommandType.Both) !== 0 ? is : els);
|
||||||
|
};
|
||||||
|
const curAppType = CommandTypeRaw[module.type];
|
||||||
|
const createCommandData = () => {
|
||||||
|
const cmd = appCmd(module.type);
|
||||||
|
return {
|
||||||
|
name: module.name,
|
||||||
|
type: curAppType,
|
||||||
|
description: cmd(module.description, ""),
|
||||||
|
options: cmd(
|
||||||
|
optionsTransformer((module as SlashCommand).options ?? []),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
defaultMemberPermissions,
|
||||||
|
dmPermission,
|
||||||
|
} as ApplicationCommandData;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const commandData = createCommandData();
|
||||||
|
|
||||||
|
if (!guildIds.length) {
|
||||||
|
const cmd = (await client.application!.commands.fetch()).find(
|
||||||
|
(c) => c.name === module.name && c.type === curAppType,
|
||||||
|
);
|
||||||
|
if (cmd) {
|
||||||
|
if (!cmd.equals(commandData, true)) {
|
||||||
|
logged(
|
||||||
|
`Found differences in global command ${module.name}`,
|
||||||
|
);
|
||||||
|
cmd.edit(commandData).then(
|
||||||
|
log(
|
||||||
|
`${module.name} updated with new data successfully!`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
}
|
||||||
|
client
|
||||||
|
.application!.commands.create(commandData)
|
||||||
|
.then(log("Command created", module.name))
|
||||||
|
.catch(c);
|
||||||
|
return controller.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of guildIds) {
|
||||||
|
const guild = await client.guilds.fetch(id).catch(c);
|
||||||
|
if (!guild) continue;
|
||||||
|
const guildCmd = (await guild.commands.fetch()).find(
|
||||||
|
(c) => c.name === module.name && c.type === curAppType,
|
||||||
|
);
|
||||||
|
if (guildCmd) {
|
||||||
|
if (!guildCmd.equals(commandData, true)) {
|
||||||
|
logged(`Found differences in command ${module.name}`);
|
||||||
|
guildCmd
|
||||||
|
.edit(commandData)
|
||||||
|
.then(
|
||||||
|
log(
|
||||||
|
`${module.name} updated with new data successfully!`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.catch(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
guild.commands
|
||||||
|
.create(commandData)
|
||||||
|
.then(log("Guild Command created", module.name, guild.name))
|
||||||
|
.catch(c);
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
} catch (e) {
|
||||||
|
logged("Command did not register" + module.name);
|
||||||
|
logged(e);
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function optionsTransformer(ops: Array<SernOptionsData>) {
|
||||||
|
return ops.map((el) => {
|
||||||
|
switch (el.type) {
|
||||||
|
case ApplicationCommandOptionType.String:
|
||||||
|
case ApplicationCommandOptionType.Number:
|
||||||
|
case ApplicationCommandOptionType.Integer: {
|
||||||
|
return el.autocomplete && "command" in el
|
||||||
|
? (({ command, ...el }) => el)(el)
|
||||||
|
: el;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NonEmptyArray<T extends `${number}` = `${number}`> = [T, ...T[]];
|
||||||
|
|
||||||
|
export interface ValidPublishOptions {
|
||||||
|
guildIds: string[];
|
||||||
|
dmPermission: boolean;
|
||||||
|
defaultMemberPermissions: PermissionResolvable;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildPublishOptions {
|
||||||
|
guildIds?: NonEmptyArray;
|
||||||
|
defaultMemberPermissions?: PermissionResolvable;
|
||||||
|
dmPermission?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalPublishOptions {
|
||||||
|
defaultMemberPermissions?: PermissionResolvable;
|
||||||
|
dmPermission?: false;
|
||||||
|
guildIds?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BasePublishOptions = GuildPublishOptions | GlobalPublishOptions;
|
||||||
|
|
||||||
|
export type PublishOptions = BasePublishOptions &
|
||||||
|
(
|
||||||
|
| Required<Pick<BasePublishOptions, "defaultMemberPermissions">>
|
||||||
|
| (
|
||||||
|
| Required<Pick<BasePublishOptions, "dmPermission">>
|
||||||
|
| Required<Pick<BasePublishOptions, "guildIds">>
|
||||||
|
)
|
||||||
|
);
|
||||||
95
bot/src/plugins/requirePermission.ts
Normal file
95
bot/src/plugins/requirePermission.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* This is perm check, it allows users to parse the permission you want and let the plugin do the rest. (check bot or user for that perm).
|
||||||
|
*
|
||||||
|
* @author @Benzo-Fury [<@762918086349029386>]
|
||||||
|
* @author @needhamgary [<@342314924804014081>]
|
||||||
|
* @version 1.2.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { requirePermission } from "../plugins/myPermCheck";
|
||||||
|
* import { commandModule, CommandType } from "@sern/handler";
|
||||||
|
* export default commandModule({
|
||||||
|
* plugins: [ requirePermission<CommandType>('target', 'permission', 'No response (optional)') ],
|
||||||
|
* execute: (ctx) => {
|
||||||
|
* //your code here
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { GuildMember, PermissionResolvable } from "discord.js";
|
||||||
|
import {
|
||||||
|
CommandType, CommandControlPlugin, controller,
|
||||||
|
} from "@sern/handler";
|
||||||
|
|
||||||
|
function payload(resp?: string) {
|
||||||
|
return {
|
||||||
|
fetchReply: true,
|
||||||
|
content: resp,
|
||||||
|
allowedMentions: { repliedUser: false },
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requirePermission(
|
||||||
|
target: "user" | "bot" | "both",
|
||||||
|
perm: PermissionResolvable[],
|
||||||
|
response?: string
|
||||||
|
) {
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
|
||||||
|
if (ctx.guild === null) {
|
||||||
|
ctx.reply(payload("This command cannot be used here"));
|
||||||
|
console.warn(
|
||||||
|
"PermCheck > A command stopped because we couldn't check a users permissions (was used in dms)"
|
||||||
|
); //delete this line if you dont want to be notified when a command is used outside of a guild/server
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
const bot = (await ctx.guild.members.fetchMe({
|
||||||
|
cache: false,
|
||||||
|
})!) as GuildMember; const memm = ctx.member! as GuildMember;
|
||||||
|
switch (target) {
|
||||||
|
//*********************************************************************************************************************//
|
||||||
|
case "bot":
|
||||||
|
if (!bot.permissions.has(perm)) {
|
||||||
|
if (!response)
|
||||||
|
response = `I cannot use this command, please give me \`${perm.join(
|
||||||
|
", "
|
||||||
|
)}\` permission(s).`;
|
||||||
|
await ctx.reply(payload(response));
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
//*********************************************************************************************************************//
|
||||||
|
case "user":
|
||||||
|
if (!memm.permissions.has(perm)) {
|
||||||
|
if (!response)
|
||||||
|
response = `You cannot use this command because you are missing \`${perm.join(
|
||||||
|
", "
|
||||||
|
)}\` permission(s).`;
|
||||||
|
await ctx.reply(payload(response));
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
//*********************************************************************************************************************//
|
||||||
|
case "both":
|
||||||
|
if (
|
||||||
|
!bot.permissions.has(perm) ||
|
||||||
|
!memm.permissions.has(perm)
|
||||||
|
) {
|
||||||
|
if (!response)
|
||||||
|
response = `Please ensure <@${bot.user.id}> and <@${
|
||||||
|
memm.user.id
|
||||||
|
}> both have \`${perm.join(", ")}\` permission(s).`;
|
||||||
|
await ctx.reply(payload(response));
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
//*********************************************************************************************************************//
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
"Perm Check >>> You didn't specify user or bot."
|
||||||
|
);
|
||||||
|
ctx.reply(payload("User or Bot was not specified."));
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
38
bot/src/plugins/serverOnly.ts
Normal file
38
bot/src/plugins/serverOnly.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Checks if a command is available in a specific server.
|
||||||
|
*
|
||||||
|
* @author @Peter-MJ-Parker [<@1017182455926624316>]
|
||||||
|
* @version 1.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { commandModule, CommandType } from "@sern/handler";
|
||||||
|
* import { serverOnly } from "../plugins/serverOnly";
|
||||||
|
* export default commandModule({
|
||||||
|
* type: CommandType.Both,
|
||||||
|
* plugins: [serverOnly(["guildId"], failMessage)], // fail message is the message you will see when the command is ran in the wrong server.
|
||||||
|
* description: "command description",
|
||||||
|
* execute: async (ctx, args) => {
|
||||||
|
* // your code here
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CommandType, controller, CommandControlPlugin } from "@sern/handler";
|
||||||
|
|
||||||
|
export function serverOnly(
|
||||||
|
guildId: string[],
|
||||||
|
failMessage = "This command is not available in this guild. \nFor permission to use in your server, please contact my developer."
|
||||||
|
) {
|
||||||
|
return CommandControlPlugin<CommandType.Both>(async (ctx, _) => {
|
||||||
|
if (!guildId.includes(ctx.guildId!)) {
|
||||||
|
ctx.reply(failMessage).then(async (m) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await m.delete();
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
return controller.stop();
|
||||||
|
}
|
||||||
|
return controller.next();
|
||||||
|
})
|
||||||
|
}
|
||||||
34
bot/src/presence.ts
Normal file
34
bot/src/presence.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Presence } from '@sern/handler'
|
||||||
|
import { ActivityType, ClientPresenceStatus } from 'discord.js';
|
||||||
|
|
||||||
|
function shuffleArray<T>(array: T[]) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
return [...array];
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses = [[ActivityType.Watching, "the sern community", "online"],
|
||||||
|
[ActivityType.Listening, "Evo", "dnd"],
|
||||||
|
[ActivityType.Playing, "with @sern/cli", "idle"],
|
||||||
|
[ActivityType.Watching, "sern bots", "dnd"],
|
||||||
|
[ActivityType.Watching, "github stars go brrr", "online"],
|
||||||
|
[ActivityType.Listening, "Spotify", "dnd"],
|
||||||
|
[ActivityType.Listening, "what's bofa", "idle"]] satisfies
|
||||||
|
[ActivityType, string, ClientPresenceStatus][];
|
||||||
|
|
||||||
|
export default Presence.module({
|
||||||
|
execute: () => {
|
||||||
|
const [type, name, status] = statuses.at(0)!;
|
||||||
|
return Presence
|
||||||
|
.of({ activities: [ { type, name } ], status }) //start your presence with this.
|
||||||
|
.repeated(() => {
|
||||||
|
const [type, name, status] = [...shuffleArray(statuses)].shift()!;
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
activities: [{ type, name }]
|
||||||
|
};
|
||||||
|
}, 60_000); //repeat and setPresence with returned result every minute
|
||||||
|
}
|
||||||
|
})
|
||||||
9
bot/src/tasks/dbbackup.ts
Normal file
9
bot/src/tasks/dbbackup.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { scheduledTask } from "@sern/handler";
|
||||||
|
|
||||||
|
|
||||||
|
export default scheduledTask({
|
||||||
|
trigger: "* * * * *",
|
||||||
|
execute: (args, { deps }) => {
|
||||||
|
console.log("hello")
|
||||||
|
}
|
||||||
|
})
|
||||||
3
bot/tsconfig.json
Normal file
3
bot/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.sern/tsconfig.json"
|
||||||
|
}
|
||||||
3614
package-lock.json
generated
Normal file
3614
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -1,27 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "@sern/handler",
|
"name": "@sern/handler",
|
||||||
"packageManager": "yarn@3.5.0",
|
"packageManager": "yarn@3.5.0",
|
||||||
"version": "3.2.0",
|
"version": "4.2.4",
|
||||||
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
|
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.js",
|
"require": "./dist/index.js"
|
||||||
"types": "./dist/index.d.ts"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsup --watch",
|
"watch": "tsc --watch",
|
||||||
"lint": "eslint src/**/*.ts",
|
"lint": "eslint src/**/*.ts",
|
||||||
"format": "eslint src/**/*.ts --fix",
|
"format": "eslint src/**/*.ts --fix",
|
||||||
"build:dev": "tsup --metafile",
|
"build:dev": "tsc",
|
||||||
"build:prod": "tsup ",
|
"build:prod": "tsc",
|
||||||
"prepare": "npm run build:prod",
|
"prepare": "tsc",
|
||||||
"pretty": "prettier --write .",
|
"pretty": "prettier --write .",
|
||||||
"tdd": "vitest",
|
"tdd": "vitest",
|
||||||
|
"benchmark": "vitest bench",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
|
"analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
|
||||||
},
|
},
|
||||||
@@ -37,30 +36,21 @@
|
|||||||
"author": "SernDevs",
|
"author": "SernDevs",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iti": "^0.6.0",
|
"@sern/ioc": "^1.1.2",
|
||||||
"rxjs": "^7.8.0",
|
"callsites": "^3.1.0",
|
||||||
"ts-results-es": "^4.0.0"
|
"cron": "^3.1.7",
|
||||||
|
"deepmerge": "^4.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.0.1",
|
"@faker-js/faker": "^8.0.1",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||||
"@typescript-eslint/parser": "5.59.1",
|
"@typescript-eslint/parser": "5.59.1",
|
||||||
"discord.js": "^14.11.0",
|
"discord.js": "^14.14.1",
|
||||||
"esbuild": "^0.17.0",
|
|
||||||
"eslint": "8.39.0",
|
"eslint": "8.39.0",
|
||||||
"prettier": "2.8.8",
|
|
||||||
"tsup": "^6.7.0",
|
|
||||||
"typescript": "5.0.2",
|
"typescript": "5.0.2",
|
||||||
"vitest": "latest"
|
"vitest": "^1.6.0"
|
||||||
},
|
|
||||||
"prettier": {
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 100,
|
|
||||||
"tabWidth": 4,
|
|
||||||
"arrowParens": "avoid"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
@@ -94,5 +84,14 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/sern-handler/handler.git"
|
"url": "git+https://github.com/sern-handler/handler.git"
|
||||||
},
|
},
|
||||||
"homepage": "https://sern.dev"
|
"engines": {
|
||||||
|
"node": ">= 20.0.x"
|
||||||
|
},
|
||||||
|
"homepage": "https://sern.dev",
|
||||||
|
"overrides": {
|
||||||
|
"ws": "8.17.1"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"ws": "8.17.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/cleanup.ts
Normal file
111
src/cleanup.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// It's this package but without default console log / error https://github.com/trevorr/async-cleanup
|
||||||
|
|
||||||
|
/** A possibly asynchronous function invoked with the process is about to exit. */
|
||||||
|
export type CleanupListener = () => void | Promise<void>;
|
||||||
|
|
||||||
|
let cleanupListeners: Set<CleanupListener> | undefined;
|
||||||
|
|
||||||
|
/** Registers a new cleanup listener. Adding the same listener more than once has no effect. */
|
||||||
|
export function addCleanupListener(listener: CleanupListener): void {
|
||||||
|
// Install exit listeners on initial cleanup listener
|
||||||
|
if (!cleanupListeners) {
|
||||||
|
installExitListeners();
|
||||||
|
cleanupListeners = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes an existing cleanup listener, and returns whether the listener was registered. */
|
||||||
|
export function removeCleanupListener(listener: CleanupListener): boolean {
|
||||||
|
return cleanupListeners != null && cleanupListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Executes all cleanup listeners and then exits the process. Call this instead of `process.exit` to ensure all listeners are fully executed. */
|
||||||
|
export async function exitAfterCleanup(code = 0): Promise<never> {
|
||||||
|
await executeCleanupListeners();
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Executes all cleanup listeners and then kills the process with the given signal. */
|
||||||
|
export async function killAfterCleanup(signal: ExitSignal): Promise<void> {
|
||||||
|
await executeCleanupListeners();
|
||||||
|
process.kill(process.pid, signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCleanupListeners(): Promise<void> {
|
||||||
|
if (cleanupListeners) {
|
||||||
|
// Remove exit listeners to restore normal event handling
|
||||||
|
uninstallExitListeners();
|
||||||
|
|
||||||
|
// Clear cleanup listeners to reset state for testing
|
||||||
|
const listeners = cleanupListeners;
|
||||||
|
cleanupListeners = undefined;
|
||||||
|
|
||||||
|
// Call listeners in order added with async listeners running concurrently
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
for (const listener of listeners) {
|
||||||
|
try {
|
||||||
|
const promise = listener();
|
||||||
|
if (promise) promises.push(promise);
|
||||||
|
} catch (err) {
|
||||||
|
// console.error("Uncaught exception during cleanup", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all listeners to complete and log any rejections
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
console.error("Unhandled rejection during cleanup", result.reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeExitListener(code: number): void {
|
||||||
|
// console.log(`Exiting with code ${code} due to empty event loop`);
|
||||||
|
void exitAfterCleanup(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uncaughtExceptionListener(error: Error): void {
|
||||||
|
// console.error("Exiting with code 1 due to uncaught exception", error);
|
||||||
|
void exitAfterCleanup(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signalListener(signal: ExitSignal): void {
|
||||||
|
// console.log(`Exiting due to signal ${signal}`);
|
||||||
|
void killAfterCleanup(signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listenable signals that terminate the process by default
|
||||||
|
// (except SIGQUIT, which generates a core dump and should not trigger cleanup)
|
||||||
|
// See https://nodejs.org/api/process.html#signal-events
|
||||||
|
const listenedSignals = [
|
||||||
|
"SIGBREAK", // Ctrl-Break on Windows
|
||||||
|
"SIGHUP", // Parent terminal closed
|
||||||
|
"SIGINT", // Terminal interrupt, usually by Ctrl-C
|
||||||
|
"SIGTERM", // Graceful termination
|
||||||
|
"SIGUSR2", // Used by Nodemon
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Signals that can terminate the process. */
|
||||||
|
export type ExitSignal =
|
||||||
|
| typeof listenedSignals[number]
|
||||||
|
| "SIGKILL"
|
||||||
|
| "SIGQUIT"
|
||||||
|
| "SIGSTOP";
|
||||||
|
|
||||||
|
function installExitListeners(): void {
|
||||||
|
process.on("beforeExit", beforeExitListener);
|
||||||
|
process.on("uncaughtException", uncaughtExceptionListener);
|
||||||
|
listenedSignals.forEach((signal) => process.on(signal, signalListener));
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninstallExitListeners(): void {
|
||||||
|
process.removeListener("beforeExit", beforeExitListener);
|
||||||
|
process.removeListener("uncaughtException", uncaughtExceptionListener);
|
||||||
|
listenedSignals.forEach((signal) =>
|
||||||
|
process.removeListener(signal, signalListener)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export * as Id from './id';
|
|
||||||
export * from './operators';
|
|
||||||
export * from './predicates';
|
|
||||||
export * as Files from './module-loading';
|
|
||||||
export * from './functions';
|
|
||||||
export type { VoidResult } from '../types/core-plugin';
|
|
||||||
export { SernError } from './structures/enums';
|
|
||||||
export { ModuleStore } from './structures/module-store';
|
|
||||||
export * as DefaultServices from './structures/services';
|
|
||||||
export { useContainerRaw } from './ioc/base'
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { Awaitable } from '../../types/utility';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a Disposable contract.
|
|
||||||
* Let dependencies implement this to dispose and cleanup.
|
|
||||||
*/
|
|
||||||
export interface Disposable {
|
|
||||||
dispose(): Awaitable<unknown>;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { AnyFunction } from '../../types/utility';
|
|
||||||
|
|
||||||
export interface Emitter {
|
|
||||||
addListener(eventName: string | symbol, listener: AnyFunction): this;
|
|
||||||
removeListener(eventName: string | symbol, listener: AnyFunction): this;
|
|
||||||
emit(eventName: string | symbol, ...payload: any[]): boolean;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { CommandModule,Processed, EventModule } from "../../types/core-modules";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
export interface ErrorHandling {
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Version 4 will remove this method
|
|
||||||
*/
|
|
||||||
crash(err: Error): never;
|
|
||||||
/**
|
|
||||||
* A function that is called on every throw.
|
|
||||||
* @param error
|
|
||||||
*/
|
|
||||||
updateAlive(error: Error): void;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export * from './error-handling';
|
|
||||||
export * from './logging';
|
|
||||||
export * from './module-manager';
|
|
||||||
export * from './module-store';
|
|
||||||
export * from './init';
|
|
||||||
export * from './emitter';
|
|
||||||
export * from './disposable'
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { Awaitable } from '../../types/utility';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an initialization contract.
|
|
||||||
* Let dependencies implement this to initiate some logic.
|
|
||||||
*/
|
|
||||||
export interface Init {
|
|
||||||
init(): Awaitable<unknown>;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LogPayload<T = unknown> = { message: T };
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type {
|
|
||||||
CommandMeta,
|
|
||||||
CommandModule,
|
|
||||||
CommandModuleDefs,
|
|
||||||
Module,
|
|
||||||
} from '../../types/core-modules';
|
|
||||||
import { CommandType } from '../structures';
|
|
||||||
|
|
||||||
interface MetadataAccess {
|
|
||||||
getMetadata(m: Module): CommandMeta | undefined;
|
|
||||||
setMetadata(m: Module, c: CommandMeta): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @since 2.0.0
|
|
||||||
* @internal - direct access to the module manager will be removed in version 4
|
|
||||||
*/
|
|
||||||
export interface ModuleManager extends MetadataAccess {
|
|
||||||
get(id: string): string | undefined;
|
|
||||||
|
|
||||||
set(id: string, path: string): void;
|
|
||||||
getPublishableCommands(): Promise<CommandModule[]>;
|
|
||||||
getByNameCommandType<T extends CommandType>(
|
|
||||||
name: string,
|
|
||||||
commandType: T,
|
|
||||||
): Promise<CommandModuleDefs[T]> | undefined;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { CommandMeta, Module } from '../../types/core-modules';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a core module store that stores IDs mapped to file paths.
|
|
||||||
*/
|
|
||||||
export interface CoreModuleStore {
|
|
||||||
commands: Map<string, string>;
|
|
||||||
metadata: WeakMap<Module, CommandMeta>;
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { CommandType, EventType, PluginType } from './structures';
|
|
||||||
import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/core-plugin';
|
|
||||||
import type { ClientEvents } from 'discord.js';
|
|
||||||
|
|
||||||
export function makePlugin<V extends unknown[]>(
|
|
||||||
type: PluginType,
|
|
||||||
execute: (...args: any[]) => any,
|
|
||||||
): Plugin<V> {
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
execute,
|
|
||||||
} as Plugin<V>;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @since 2.5.0
|
|
||||||
* @__PURE__
|
|
||||||
*/
|
|
||||||
export function EventInitPlugin<I extends EventType>(
|
|
||||||
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
|
|
||||||
) {
|
|
||||||
return makePlugin(PluginType.Init, execute);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @since 2.5.0
|
|
||||||
* @__PURE__
|
|
||||||
*/
|
|
||||||
export function CommandInitPlugin<I extends CommandType>(
|
|
||||||
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
|
|
||||||
) {
|
|
||||||
return makePlugin(PluginType.Init, execute);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @since 2.5.0
|
|
||||||
* @__PURE__
|
|
||||||
*/
|
|
||||||
export function CommandControlPlugin<I extends CommandType>(
|
|
||||||
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
|
|
||||||
) {
|
|
||||||
return makePlugin(PluginType.Control, execute);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @since 2.5.0
|
|
||||||
* @__PURE__
|
|
||||||
*/
|
|
||||||
export function EventControlPlugin<I extends EventType>(
|
|
||||||
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
|
|
||||||
) {
|
|
||||||
return makePlugin(PluginType.Control, execute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @since 2.5.0
|
|
||||||
* @Experimental
|
|
||||||
* A specialized function for creating control plugins with discord.js ClientEvents.
|
|
||||||
* Will probably be moved one day!
|
|
||||||
*/
|
|
||||||
export function DiscordEventControlPlugin<T extends keyof ClientEvents>(
|
|
||||||
name: T,
|
|
||||||
execute: (...args: ClientEvents[T]) => PluginResult,
|
|
||||||
) {
|
|
||||||
return makePlugin(PluginType.Control, execute);
|
|
||||||
}
|
|
||||||
@@ -1,83 +1,124 @@
|
|||||||
import { Err, Ok } from 'ts-results-es';
|
import type { Module, SernAutocompleteData, SernOptionsData } from '../types/core-modules';
|
||||||
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
|
import type {
|
||||||
import type { SernAutocompleteData, SernOptionsData } from '../types/core-modules';
|
AnySelectMenuInteraction,
|
||||||
import type { AnyCommandPlugin, AnyEventPlugin, Plugin } from '../types/core-plugin';
|
ButtonInteraction,
|
||||||
import { PluginType } from './structures';
|
ChatInputCommandInteraction,
|
||||||
import assert from 'assert';
|
MessageContextMenuCommandInteraction,
|
||||||
|
ModalSubmitInteraction,
|
||||||
|
UserContextMenuCommandInteraction,
|
||||||
|
AutocompleteInteraction,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
|
||||||
|
import { PluginType } from './structures/enums';
|
||||||
|
import type { Payload, UnpackedDependencies } from '../types/utility';
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
//function wrappers for empty ok / err
|
export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
|
||||||
export const ok = /* @__PURE__*/ () => Ok.EMPTY;
|
return {
|
||||||
export const err = /* @__PURE__*/ () => Err.EMPTY;
|
state: {},
|
||||||
|
deps,
|
||||||
export function partitionPlugins(
|
params,
|
||||||
arr: (AnyEventPlugin | AnyCommandPlugin)[] = [],
|
type: module.type,
|
||||||
): [Plugin[], Plugin[]] {
|
module: {
|
||||||
const controlPlugins = [];
|
name: module.name,
|
||||||
const initPlugins = [];
|
description: module.description,
|
||||||
|
locals: module.locals,
|
||||||
for (const el of arr) {
|
meta: module.meta
|
||||||
switch (el.type) {
|
|
||||||
case PluginType.Control:
|
|
||||||
controlPlugins.push(el);
|
|
||||||
break;
|
|
||||||
case PluginType.Init:
|
|
||||||
initPlugins.push(el);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [controlPlugins, initPlugins];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
|
* Removes the first character(s) _[depending on prefix length]_ of the message
|
||||||
* @param iAutocomplete
|
* @param msg
|
||||||
* @param options
|
* @param prefix The prefix to remove
|
||||||
|
* @returns The message without the prefix
|
||||||
|
* @example
|
||||||
|
* message.content = '!ping';
|
||||||
|
* console.log(fmt(message.content, '!'));
|
||||||
|
* // [ 'ping' ]
|
||||||
*/
|
*/
|
||||||
export function treeSearch(
|
export function fmt(msg: string, prefix?: string): string[] {
|
||||||
iAutocomplete: AutocompleteInteraction,
|
if(!prefix) throw Error("Unable to parse message without prefix");
|
||||||
options: SernOptionsData[] | undefined,
|
return msg.slice(prefix.length).trim().split(/\s+/g);
|
||||||
): SernAutocompleteData & { parent?: string } | undefined {
|
}
|
||||||
if (options === undefined) return undefined;
|
|
||||||
//clone to prevent mutation of original command module
|
|
||||||
const _options = options.map(a => ({ ...a }));
|
export function partitionPlugins<T,V>
|
||||||
let subcommands = new Set();
|
(arr: Array<{ type: PluginType }> = []): [T[], V[]] {
|
||||||
while (_options.length > 0) {
|
const controlPlugins = [];
|
||||||
const cur = _options.pop()!;
|
const initPlugins = [];
|
||||||
switch (cur.type) {
|
for (const el of arr) {
|
||||||
case ApplicationCommandOptionType.Subcommand:
|
switch (el.type) {
|
||||||
{
|
case PluginType.Control: controlPlugins.push(el); break;
|
||||||
subcommands.add(cur.name);
|
case PluginType.Init: initPlugins.push(el); break;
|
||||||
for (const option of cur.options ?? []) _options.push(option);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ApplicationCommandOptionType.SubcommandGroup:
|
|
||||||
{
|
|
||||||
for (const command of cur.options ?? []) _options.push(command);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
if ('autocomplete' in cur && cur.autocomplete) {
|
|
||||||
const choice = iAutocomplete.options.getFocused(true);
|
|
||||||
assert(
|
|
||||||
'command' in cur,
|
|
||||||
'No command property found for autocomplete option',
|
|
||||||
);
|
|
||||||
if (subcommands.size > 0) {
|
|
||||||
const parent = iAutocomplete.options.getSubcommand();
|
|
||||||
const parentAndOptionMatches =
|
|
||||||
subcommands.has(parent) && cur.name === choice.name;
|
|
||||||
if (parentAndOptionMatches) {
|
|
||||||
return { ...cur, parent };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (cur.name === choice.name) {
|
|
||||||
return { ...cur, parent: undefined };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return [controlPlugins, initPlugins] as [T[], V[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLookupTable = (options: SernOptionsData[]): Map<string, SernAutocompleteData> => {
|
||||||
|
const table = new Map<string, SernAutocompleteData>();
|
||||||
|
_createLookupTable(table, options, "<parent>");
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _createLookupTable = (table: Map<string, SernAutocompleteData>, options: SernOptionsData[], parent: string) => {
|
||||||
|
for (const opt of options) {
|
||||||
|
const name = path.posix.join(parent, opt.name)
|
||||||
|
switch(opt.type) {
|
||||||
|
case ApplicationCommandOptionType.Subcommand: {
|
||||||
|
_createLookupTable(table, opt.options ?? [], name);
|
||||||
|
} break;
|
||||||
|
case ApplicationCommandOptionType.SubcommandGroup: {
|
||||||
|
_createLookupTable(table, opt.options ?? [], name);
|
||||||
|
} break;
|
||||||
|
default: {
|
||||||
|
if(Reflect.get(opt, 'autocomplete') === true) {
|
||||||
|
table.set(name, opt as SernAutocompleteData)
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InteractionTypable {
|
||||||
|
type: InteractionType;
|
||||||
|
}
|
||||||
|
//discord.js pls fix ur typings or i will >:(
|
||||||
|
type AnyMessageComponentInteraction = AnySelectMenuInteraction | ButtonInteraction;
|
||||||
|
type AnyCommandInteraction =
|
||||||
|
| ChatInputCommandInteraction
|
||||||
|
| MessageContextMenuCommandInteraction
|
||||||
|
| UserContextMenuCommandInteraction;
|
||||||
|
|
||||||
|
export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction {
|
||||||
|
return i.type === InteractionType.MessageComponent;
|
||||||
|
}
|
||||||
|
export function isCommand(i: InteractionTypable): i is AnyCommandInteraction {
|
||||||
|
return i.type === InteractionType.ApplicationCommand;
|
||||||
|
}
|
||||||
|
export function isContextCommand(i: AnyCommandInteraction): i is MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction {
|
||||||
|
return i.isContextMenuCommand();
|
||||||
|
}
|
||||||
|
export function isAutocomplete(i: InteractionTypable): i is AutocompleteInteraction {
|
||||||
|
return i.type === InteractionType.ApplicationCommandAutocomplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isModal(i: InteractionTypable): i is ModalSubmitInteraction {
|
||||||
|
return i.type === InteractionType.ModalSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resultPayload<T extends 'success'|'warning'|'failure'>
|
||||||
|
(type: T, module?: Module, reason?: unknown) {
|
||||||
|
return { type, module, reason } as Payload & { type : T };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pipe<T>(arg: unknown, firstFn: Function, ...fns: Function[]): T {
|
||||||
|
let result = firstFn(arg);
|
||||||
|
for (let fn of fns) {
|
||||||
|
result = fn(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
import { ApplicationCommandType, ComponentType, Interaction, InteractionType } from 'discord.js';
|
import { ApplicationCommandType, ComponentType, type Interaction, InteractionType } from 'discord.js';
|
||||||
import { CommandType, EventType } from './structures';
|
import { CommandType, EventType } from './structures/enums';
|
||||||
|
|
||||||
|
const parseParams = (event: { customId: string }, append: string) => {
|
||||||
|
const hasSlash = event.customId.indexOf('/')
|
||||||
|
if(hasSlash === -1) {
|
||||||
|
return { id:event.customId+append };
|
||||||
|
}
|
||||||
|
const baseid = event.customId.substring(0, hasSlash);
|
||||||
|
const params = event.customId.substring(hasSlash+1);
|
||||||
|
return { id: baseid+append, params }
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Construct unique ID for a given interaction object.
|
* Construct unique ID for a given interaction object.
|
||||||
* @param event The interaction object for which to create an ID.
|
* @param event The interaction object for which to create an ID.
|
||||||
* @returns A unique string ID based on the type and properties of the interaction object.
|
* @returns An array of unique string IDs based on the type and properties of the interaction object.
|
||||||
*/
|
*/
|
||||||
export function reconstruct<T extends Interaction>(event: T) {
|
export function reconstruct<T extends Interaction>(event: T) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case InteractionType.MessageComponent: {
|
case InteractionType.MessageComponent: {
|
||||||
return `${event.customId}_C${event.componentType}`;
|
const data = parseParams(event, `_C${event.componentType}`)
|
||||||
|
return [data];
|
||||||
}
|
}
|
||||||
case InteractionType.ApplicationCommand:
|
case InteractionType.ApplicationCommand:
|
||||||
case InteractionType.ApplicationCommandAutocomplete: {
|
case InteractionType.ApplicationCommandAutocomplete:
|
||||||
return `${event.commandName}_A${event.commandType}`;
|
return [{ id: `${event.commandName}_A${event.commandType}` }, { id: `${event.commandName}_B` }];
|
||||||
}
|
|
||||||
//Modal interactions are classified as components for sern
|
//Modal interactions are classified as components for sern
|
||||||
case InteractionType.ModalSubmit: {
|
case InteractionType.ModalSubmit: {
|
||||||
return `${event.customId}_C1`;
|
const data = parseParams(event, '_M');
|
||||||
|
return [data];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,39 +35,40 @@ export function reconstruct<T extends Interaction>(event: T) {
|
|||||||
*
|
*
|
||||||
* A magic number to represent any commandtype that is an ApplicationCommand.
|
* A magic number to represent any commandtype that is an ApplicationCommand.
|
||||||
*/
|
*/
|
||||||
const appBitField = 0b000000001111;
|
const PUBLISHABLE = 0b000000001111;
|
||||||
|
|
||||||
// Each index represents the exponent of a CommandType.
|
|
||||||
// Every CommandType is a power of two.
|
const TypeMap = new Map<number, number>([[CommandType.Text, 0],
|
||||||
export const CommandTypeDiscordApi = [
|
[CommandType.Both, 0],
|
||||||
1, // CommandType.Text
|
[CommandType.Slash, ApplicationCommandType.ChatInput],
|
||||||
ApplicationCommandType.ChatInput,
|
[CommandType.CtxUser, ApplicationCommandType.User],
|
||||||
ApplicationCommandType.User,
|
[CommandType.CtxMsg, ApplicationCommandType.Message],
|
||||||
ApplicationCommandType.Message,
|
[CommandType.Button, ComponentType.Button],
|
||||||
ComponentType.Button,
|
[CommandType.StringSelect, ComponentType.StringSelect],
|
||||||
ComponentType.StringSelect,
|
[CommandType.Modal, InteractionType.ModalSubmit],
|
||||||
1, // CommandType.Modal
|
[CommandType.UserSelect, ComponentType.UserSelect],
|
||||||
ComponentType.UserSelect,
|
[CommandType.MentionableSelect, ComponentType.MentionableSelect],
|
||||||
ComponentType.RoleSelect,
|
[CommandType.RoleSelect, ComponentType.RoleSelect],
|
||||||
ComponentType.MentionableSelect,
|
[CommandType.ChannelSelect, ComponentType.ChannelSelect]]);
|
||||||
ComponentType.ChannelSelect,
|
|
||||||
];
|
|
||||||
/*
|
|
||||||
* Generates a number based on CommandType.
|
|
||||||
* This corresponds to an ApplicationCommandType or ComponentType
|
|
||||||
* TextCommands are 0 as they aren't either or.
|
|
||||||
*/
|
|
||||||
function apiType(t: CommandType | EventType) {
|
|
||||||
if (t === CommandType.Both || t === CommandType.Modal) return 1;
|
|
||||||
return CommandTypeDiscordApi[Math.log2(t)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Generates an id based on name and CommandType.
|
* Generates an id based on name and CommandType.
|
||||||
* A is for any ApplicationCommand. C is for any ComponentCommand
|
* A is for any ApplicationCommand. C is for any ComponentCommand
|
||||||
* Then, another number generated by apiType function is appended
|
* Then, another number fetched from TypeMap
|
||||||
*/
|
*/
|
||||||
export function create(name: string, type: CommandType | EventType) {
|
export function create(name: string, type: CommandType | EventType) {
|
||||||
const am = (appBitField & type) !== 0 ? 'A' : 'C';
|
if(type == CommandType.Text) {
|
||||||
return name + '_' + am + apiType(type);
|
return `${name}_T`;
|
||||||
|
}
|
||||||
|
if(type == CommandType.Both) {
|
||||||
|
return `${name}_B`;
|
||||||
|
}
|
||||||
|
if(type == CommandType.Modal) {
|
||||||
|
return `${name}_M`;
|
||||||
|
}
|
||||||
|
const am = (PUBLISHABLE & type) !== 0 ? 'A' : 'C';
|
||||||
|
return `${name}_${am}${TypeMap.get(type)!}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './contracts';
|
|
||||||
export * from './create-plugins';
|
|
||||||
export * from './structures';
|
|
||||||
export * from './ioc';
|
|
||||||
55
src/core/interfaces.ts
Normal file
55
src/core/interfaces.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { AnyFunction } from '../types/utility';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an initialization contract.
|
||||||
|
* Let dependencies implement this to initiate some logic.
|
||||||
|
*/
|
||||||
|
export interface Init {
|
||||||
|
init(): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Disposable contract.
|
||||||
|
* Let dependencies implement this to dispose and cleanup.
|
||||||
|
*/
|
||||||
|
export interface Disposable {
|
||||||
|
dispose(): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Emitter {
|
||||||
|
addListener(eventName: string | symbol, listener: AnyFunction): this;
|
||||||
|
removeListener(eventName: string | symbol, listener: AnyFunction): this;
|
||||||
|
emit(eventName: string | symbol, ...payload: any[]): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
export interface ErrorHandling {
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* Version 4 will remove this method
|
||||||
|
*/
|
||||||
|
crash(err: Error): never;
|
||||||
|
/**
|
||||||
|
* A function that is called on every throw.
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
updateAlive(error: Error): void;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogPayload<T = unknown> = { message: T };
|
||||||
163
src/core/ioc.ts
Normal file
163
src/core/ioc.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { Service as $Service, Services as $Services } from '@sern/ioc/global'
|
||||||
|
import { Container } from '@sern/ioc';
|
||||||
|
import * as Contracts from './interfaces';
|
||||||
|
import * as __Services from './structures/default-services';
|
||||||
|
import type { Logging } from './interfaces';
|
||||||
|
import { __init_container, useContainerRaw } from '@sern/ioc/global';
|
||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import { Client } from 'discord.js';
|
||||||
|
import { Module } from '../types/core-modules';
|
||||||
|
import { UnpackFunction } from '../types/utility';
|
||||||
|
|
||||||
|
export function disposeAll(logger: Logging|undefined) {
|
||||||
|
useContainerRaw()
|
||||||
|
?.disposeAll()
|
||||||
|
.then(() => logger?.info({ message: 'Cleaning container and crashing' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
type Insertable = | ((container: Dependencies) => object)
|
||||||
|
| object
|
||||||
|
|
||||||
|
const dependencyBuilder = (container: Container) => {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Insert a dependency into your container.
|
||||||
|
* Supply the correct key and dependency
|
||||||
|
*/
|
||||||
|
add(key: keyof Dependencies, v: Insertable) {
|
||||||
|
if(typeof v !== 'function') {
|
||||||
|
container.addSingleton(key, v)
|
||||||
|
} else {
|
||||||
|
//@ts-ignore
|
||||||
|
container.addWiredSingleton(key, (cntr) => v(cntr))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param key the key of the dependency
|
||||||
|
* @param v The dependency to swap out.
|
||||||
|
* Swap out a preexisting dependency.
|
||||||
|
*/
|
||||||
|
swap(key: keyof Dependencies, v: Insertable) {
|
||||||
|
if(typeof v !== 'function') {
|
||||||
|
container.swap(key, v);
|
||||||
|
} else {
|
||||||
|
container.swap(key, v(container.deps()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidDependencyConfig =
|
||||||
|
(c: ReturnType<typeof dependencyBuilder>) => any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* makeDependencies constructs a dependency injection container for sern handler to use.
|
||||||
|
* This is required to start the handler, and is to be called before Sern.init.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* await makeDependencies(({ add }) => {
|
||||||
|
* add('@sern/client', new Client({ intents, partials })
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function makeDependencies (conf: ValidDependencyConfig) {
|
||||||
|
const container = await __init_container({ autowire: false });
|
||||||
|
//We only include logger if it does not exist
|
||||||
|
const includeLogger = !container.hasKey('@sern/logger');
|
||||||
|
|
||||||
|
if(includeLogger) {
|
||||||
|
container.addSingleton('@sern/logger', new __Services.DefaultLogging);
|
||||||
|
}
|
||||||
|
container.addSingleton('@sern/errors', new __Services.DefaultErrorHandling);
|
||||||
|
container.addSingleton('@sern/modules', new Map);
|
||||||
|
container.addSingleton('@sern/emitter', new EventEmitter({ captureRejections: true }))
|
||||||
|
container.addSingleton('@sern/scheduler', new __Services.TaskScheduler)
|
||||||
|
conf(dependencyBuilder(container));
|
||||||
|
await container.ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Service api, which allows users to access dependencies in places IOC cannot reach.
|
||||||
|
* To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
|
||||||
|
* Our scaffolding tool takes care of this.
|
||||||
|
* Note: this method only works AFTER your container has been initiated
|
||||||
|
* @since 3.0.0
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const client = Service('@sern/client');
|
||||||
|
* ```
|
||||||
|
* @param key a key that corresponds to a dependency registered.
|
||||||
|
* @throws if container is absent or not present
|
||||||
|
*/
|
||||||
|
export function Service<const T extends keyof Dependencies>(key: T) {
|
||||||
|
return $Service(key) as Dependencies[T]
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @since 3.0.0
|
||||||
|
* The plural version of {@link Service}
|
||||||
|
* @throws if container is absent or not present
|
||||||
|
* @returns array of dependencies, in the same order of keys provided
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
|
||||||
|
return $Services<T, IntoDependencies<T>>(...keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* Creates a singleton object.
|
||||||
|
* @param cb
|
||||||
|
*/
|
||||||
|
export function single<T>(cb: () => T) {
|
||||||
|
console.log('The `single` function is deprecated and has no effect')
|
||||||
|
return cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* @since 2.0.0
|
||||||
|
* Creates a transient object
|
||||||
|
* @param cb
|
||||||
|
*/
|
||||||
|
export function transient<T>(cb: () => () => T) {
|
||||||
|
console.log('The `transient` function is deprecated and has no effect')
|
||||||
|
return cb()();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type IntoDependencies<Tuple extends [...any[]]> = {
|
||||||
|
[Index in keyof Tuple]: UnpackFunction<NonNullable<DependencyFromKey<Tuple[Index]>>>; //Unpack and make NonNullable
|
||||||
|
} & { length: Tuple['length'] };
|
||||||
|
|
||||||
|
export interface CoreDependencies {
|
||||||
|
/**
|
||||||
|
* discord.js client.
|
||||||
|
*/
|
||||||
|
'@sern/client': Client;
|
||||||
|
/**
|
||||||
|
* sern emitter listens to events that happen throughout
|
||||||
|
* the handler. some include module.register, module.activate.
|
||||||
|
*/
|
||||||
|
'@sern/emitter': Contracts.Emitter;
|
||||||
|
/**
|
||||||
|
* An error handler which is the final step before
|
||||||
|
* the sern process actually crashes.
|
||||||
|
*/
|
||||||
|
'@sern/errors': Contracts.ErrorHandling;
|
||||||
|
/**
|
||||||
|
* Optional logger. Performs ... logging
|
||||||
|
*/
|
||||||
|
'@sern/logger'?: Contracts.Logging;
|
||||||
|
/**
|
||||||
|
* Readonly module store. sern stores these
|
||||||
|
* by module.meta.id -> Module
|
||||||
|
*/
|
||||||
|
'@sern/modules': Map<string, Module>;
|
||||||
|
|
||||||
|
'@sern/scheduler': __Services.TaskScheduler
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import * as assert from 'assert';
|
|
||||||
import { composeRoot, useContainer } from './dependency-injection';
|
|
||||||
import type { DependencyConfiguration } from '../../types/ioc';
|
|
||||||
import { CoreContainer } from './container';
|
|
||||||
import { Result } from 'ts-results-es'
|
|
||||||
import { DefaultServices } from '../_internal';
|
|
||||||
import { AnyFunction } from '../../types/utility';
|
|
||||||
//SIDE EFFECT: GLOBAL DI
|
|
||||||
let containerSubject: CoreContainer<Partial<Dependencies>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Returns the underlying data structure holding all dependencies.
|
|
||||||
* Exposes methods from iti
|
|
||||||
* Use the Service API. The container should be readonly
|
|
||||||
*/
|
|
||||||
export function useContainerRaw() {
|
|
||||||
assert.ok(
|
|
||||||
containerSubject && containerSubject.isReady(),
|
|
||||||
"Could not find container or container wasn't ready. Did you call makeDependencies?",
|
|
||||||
);
|
|
||||||
return containerSubject;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dependencyBuilder = (container: any, excluded: string[]) => {
|
|
||||||
type Insertable =
|
|
||||||
| ((container: CoreContainer<Dependencies>) => unknown )
|
|
||||||
| Record<PropertyKey, unknown>
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Insert a dependency into your container.
|
|
||||||
* Supply the correct key and dependency
|
|
||||||
*/
|
|
||||||
add(key: keyof Dependencies, v: Insertable) {
|
|
||||||
Result
|
|
||||||
.wrap(() => container.add({ [key]: v}))
|
|
||||||
.expect("Failed to add " + key);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Exclude any dependencies from being added.
|
|
||||||
* Warning: this could lead to bad errors if not used correctly
|
|
||||||
*/
|
|
||||||
exclude(...keys: (keyof Dependencies)[]) {
|
|
||||||
keys.forEach(key => excluded.push(key));
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @param key the key of the dependency
|
|
||||||
* @param v The dependency to swap out.
|
|
||||||
* Swap out a preexisting dependency.
|
|
||||||
*/
|
|
||||||
swap(key: keyof Dependencies, v: Insertable) {
|
|
||||||
Result
|
|
||||||
.wrap(() => container.upsert({ [key]: v }))
|
|
||||||
.expect("Failed to update " + key);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @param key the key of the dependency
|
|
||||||
* @param cleanup Provide cleanup for the dependency at key. First parameter is the dependency itself
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* addDisposer('dbConnection', (dbConnection) => dbConnection.end())
|
|
||||||
* ```
|
|
||||||
* Swap out a preexisting dependency.
|
|
||||||
*/
|
|
||||||
addDisposer(key: keyof Dependencies, cleanup: AnyFunction) {
|
|
||||||
Result
|
|
||||||
.wrap(() => container.addDisposer({ [key] : cleanup }))
|
|
||||||
.expect("Failed to addDisposer for" + key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type CallbackBuilder = (c: ReturnType<typeof dependencyBuilder>) => any
|
|
||||||
|
|
||||||
type ValidDependencyConfig =
|
|
||||||
| CallbackBuilder
|
|
||||||
| DependencyConfiguration;
|
|
||||||
|
|
||||||
export const insertLogger = (containerSubject: CoreContainer<any>) => {
|
|
||||||
containerSubject
|
|
||||||
.upsert({'@sern/logger': () => new DefaultServices.DefaultLogging});
|
|
||||||
}
|
|
||||||
export async function makeDependencies<const T extends Dependencies>
|
|
||||||
(conf: ValidDependencyConfig) {
|
|
||||||
//Until there are more optional dependencies, just check if the logger exists
|
|
||||||
//SIDE EFFECT
|
|
||||||
containerSubject = new CoreContainer();
|
|
||||||
if(typeof conf === 'function') {
|
|
||||||
const excluded: string[] = [];
|
|
||||||
conf(dependencyBuilder(containerSubject, excluded));
|
|
||||||
if(!excluded.includes('@sern/logger')) {
|
|
||||||
assert.ok(!containerSubject.getTokens()['@sern/logger'])
|
|
||||||
insertLogger(containerSubject);
|
|
||||||
}
|
|
||||||
containerSubject.ready();
|
|
||||||
} else {
|
|
||||||
composeRoot(containerSubject, conf);
|
|
||||||
}
|
|
||||||
|
|
||||||
return useContainer<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Container } from 'iti';
|
|
||||||
import { Disposable, SernEmitter } from '../';
|
|
||||||
import * as assert from 'node:assert';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { DefaultServices, ModuleStore } from '../_internal';
|
|
||||||
import * as Hooks from './hooks'
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A semi-generic container that provides error handling, emitter, and module store.
|
|
||||||
* For the handler to operate correctly, The only user provided dependency needs to be @sern/client
|
|
||||||
*/
|
|
||||||
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
|
|
||||||
private ready$ = new Subject<void>();
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
assert.ok(!this.isReady(), 'Listening for dispose & init should occur prior to sern being ready.');
|
|
||||||
|
|
||||||
const { unsubscribe } = Hooks.createInitListener(this);
|
|
||||||
this.ready$
|
|
||||||
.subscribe({ complete: unsubscribe });
|
|
||||||
|
|
||||||
(this as Container<{}, {}>)
|
|
||||||
.add({ '@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
|
|
||||||
'@sern/emitter': () => new SernEmitter(),
|
|
||||||
'@sern/store': () => new ModuleStore() })
|
|
||||||
.add(ctx => {
|
|
||||||
return {
|
|
||||||
'@sern/modules': () =>
|
|
||||||
new DefaultServices.DefaultModuleManager(ctx['@sern/store']),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isReady() {
|
|
||||||
return this.ready$.closed;
|
|
||||||
}
|
|
||||||
override async disposeAll() {
|
|
||||||
|
|
||||||
const otherDisposables = Object
|
|
||||||
.entries(this._context)
|
|
||||||
.flatMap(([key, value]) =>
|
|
||||||
'dispose' in value ? [key] : []);
|
|
||||||
|
|
||||||
for(const key of otherDisposables) {
|
|
||||||
this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never);
|
|
||||||
}
|
|
||||||
await super.disposeAll()
|
|
||||||
}
|
|
||||||
ready() {
|
|
||||||
this.ready$.complete();
|
|
||||||
this.ready$.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
|
|
||||||
import { insertLogger, useContainerRaw } from './base';
|
|
||||||
import { CoreContainer } from './container';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @__PURE__
|
|
||||||
* @since 2.0.0.
|
|
||||||
* Creates a singleton object.
|
|
||||||
* @param cb
|
|
||||||
*/
|
|
||||||
export function single<T>(cb: () => T) {
|
|
||||||
return cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @__PURE__
|
|
||||||
* @since 2.0.0
|
|
||||||
* Creates a transient object
|
|
||||||
* @param cb
|
|
||||||
*/
|
|
||||||
export function transient<T>(cb: () => () => T) {
|
|
||||||
return cb;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The new Service api, a cleaner alternative to useContainer
|
|
||||||
* To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
|
|
||||||
* Usually our scaffolding tool takes care of this.
|
|
||||||
* @since 3.0.0
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const client = Service('@sern/client');
|
|
||||||
* ```
|
|
||||||
* @param key a key that corresponds to a dependency registered.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function Service<const T extends keyof Dependencies>(key: T) {
|
|
||||||
return useContainerRaw().get(key)!;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @since 3.0.0
|
|
||||||
* The plural version of {@link Service}
|
|
||||||
* @returns array of dependencies, in the same order of keys provided
|
|
||||||
*/
|
|
||||||
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
|
|
||||||
const container = useContainerRaw();
|
|
||||||
return keys.map(k => container.get(k)!) as IntoDependencies<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given the user's conf, check for any excluded dependency keys.
|
|
||||||
* Then, call conf.build to get the rest of the users' dependencies.
|
|
||||||
* Finally, update the containerSubject with the new container state
|
|
||||||
* @param conf
|
|
||||||
*/
|
|
||||||
export function composeRoot(
|
|
||||||
container: CoreContainer<Partial<Dependencies>>,
|
|
||||||
conf: DependencyConfiguration,
|
|
||||||
) {
|
|
||||||
//container should have no client or logger yet.
|
|
||||||
const hasLogger = conf.exclude?.has('@sern/logger');
|
|
||||||
if (!hasLogger) {
|
|
||||||
insertLogger(container);
|
|
||||||
}
|
|
||||||
//Build the container based on the callback provided by the user
|
|
||||||
conf.build(container as CoreContainer<Omit<CoreDependencies, '@sern/client'>>);
|
|
||||||
|
|
||||||
if (!hasLogger) {
|
|
||||||
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
container.ready();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContainer<const T extends Dependencies>() {
|
|
||||||
return <V extends (keyof T)[]>(...keys: [...V]) =>
|
|
||||||
keys.map(key => useContainerRaw().get(key as keyof Dependencies)) as IntoDependencies<V>;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import type { CoreContainer } from "./container"
|
|
||||||
|
|
||||||
interface HookEvent {
|
|
||||||
key : PropertyKey
|
|
||||||
newContainer: any
|
|
||||||
}
|
|
||||||
type HookName = 'init';
|
|
||||||
|
|
||||||
export const createInitListener = (coreContainer : CoreContainer<any>) => {
|
|
||||||
const initCalled = new Set<PropertyKey>();
|
|
||||||
const hasCallableMethod = createPredicate(initCalled);
|
|
||||||
const unsubscribe = coreContainer.on('containerUpserted', async (event) => {
|
|
||||||
|
|
||||||
if(isNotHookable(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(hasCallableMethod('init', event)) {
|
|
||||||
await event.newContainer?.init();
|
|
||||||
initCalled.add(event.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
return { unsubscribe };
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNotHookable = (hk: HookEvent) => {
|
|
||||||
return typeof hk.newContainer !== 'object'
|
|
||||||
|| Array.isArray(hk.newContainer)
|
|
||||||
|| hk.newContainer === null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createPredicate = <T extends HookEvent>(called: Set<PropertyKey>) => {
|
|
||||||
return (hookName: HookName, event: T) => {
|
|
||||||
const hasMethod = Reflect.has(event.newContainer!, hookName);
|
|
||||||
const beenCalledOnce = !called.has(event.key)
|
|
||||||
|
|
||||||
return hasMethod && beenCalledOnce
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { makeDependencies } from './base';
|
|
||||||
export { Service, Services, single, transient } from './dependency-injection';
|
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
import { Result } from 'ts-results-es';
|
import path from 'node:path';
|
||||||
import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
|
import { existsSync } from 'node:fs';
|
||||||
import { readdir, stat } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { basename, extname, join, resolve, parse } from 'path';
|
import assert from 'node:assert';
|
||||||
import assert from 'assert';
|
import * as Id from './id'
|
||||||
import { createRequire } from 'node:module';
|
import { Module } from '../types/core-modules';
|
||||||
import type { ImportPayload, Wrapper } from '../types/core';
|
|
||||||
import type { Module } from '../types/core-modules';
|
export const parseCallsite = (site: string) => {
|
||||||
|
const pathobj = path.posix.parse(site.replace(/file:\\?/, "")
|
||||||
|
.split(path.sep)
|
||||||
|
.join(path.posix.sep))
|
||||||
|
return { name: pathobj.name,
|
||||||
|
absPath : path.posix.format(pathobj) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shouldHandle = (pth: string, filenam: string) => {
|
||||||
|
const file_name = filenam+path.extname(pth);
|
||||||
|
let newPath = path.join(path.dirname(pth), file_name)
|
||||||
|
.replace(/file:\\?/, "");
|
||||||
|
return { exists: existsSync(newPath),
|
||||||
|
path: 'file://'+newPath };
|
||||||
|
}
|
||||||
|
|
||||||
export type ModuleResult<T> = Promise<ImportPayload<T>>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import any module based on the absolute path.
|
* Import any module based on the absolute path.
|
||||||
@@ -15,7 +28,6 @@ export type ModuleResult<T> = Promise<ImportPayload<T>>;
|
|||||||
* commonjs, javascript :
|
* commonjs, javascript :
|
||||||
* ```js
|
* ```js
|
||||||
* exports = commandModule({ })
|
* exports = commandModule({ })
|
||||||
*
|
|
||||||
* //or
|
* //or
|
||||||
* exports.default = commandModule({ })
|
* exports.default = commandModule({ })
|
||||||
* ```
|
* ```
|
||||||
@@ -25,103 +37,34 @@ export type ModuleResult<T> = Promise<ImportPayload<T>>;
|
|||||||
export async function importModule<T>(absPath: string) {
|
export async function importModule<T>(absPath: string) {
|
||||||
let fileModule = await import(absPath);
|
let fileModule = await import(absPath);
|
||||||
|
|
||||||
let commandModule = fileModule.default;
|
let commandModule: Module = fileModule.default;
|
||||||
|
|
||||||
assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
|
assert(commandModule , `No default export @ ${absPath}`);
|
||||||
if ('default' in commandModule ) {
|
if ('default' in commandModule) {
|
||||||
commandModule = commandModule.default;
|
commandModule = commandModule.default as Module;
|
||||||
}
|
}
|
||||||
return Result
|
const p = path.parse(absPath)
|
||||||
.wrap(() => ({ module: commandModule.getInstance() }))
|
commandModule.name ??= p.name; commandModule.description ??= "...";
|
||||||
.unwrapOr({ module: commandModule }) as T;
|
commandModule.meta = {
|
||||||
}
|
id: Id.create(commandModule.name, commandModule.type),
|
||||||
|
absPath,
|
||||||
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
|
|
||||||
let { module } = await importModule<{ module: T }>(absPath);
|
|
||||||
assert(module, `Found an undefined module: ${absPath}`);
|
|
||||||
return { module, absPath };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fmtFileName = (fileName: string) => parse(fileName).name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 extends Module>(
|
|
||||||
input: ObservableInput<string>,
|
|
||||||
): Observable<ImportPayload<T>> {
|
|
||||||
return from(input)
|
|
||||||
.pipe(mergeMap(defaultModuleLoader<T>));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getFullPathTree = (dir: string) => readPaths(resolve(dir));
|
|
||||||
|
|
||||||
export const filename = (path: string) => fmtFileName(basename(path));
|
|
||||||
|
|
||||||
const isSkippable = (filename: string) => {
|
|
||||||
//empty string is for non extension files (directories)
|
|
||||||
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
|
|
||||||
return filename[0] === '!' || !validExtensions.includes(extname(filename));
|
|
||||||
};
|
|
||||||
async function deriveFileInfo(dir: string, file: string) {
|
|
||||||
const fullPath = join(dir, file);
|
|
||||||
return {
|
|
||||||
fullPath,
|
|
||||||
fileStats: await stat(fullPath),
|
|
||||||
base: basename(file),
|
|
||||||
};
|
};
|
||||||
|
return { module: commandModule as T };
|
||||||
}
|
}
|
||||||
async function* readPaths(dir: string): AsyncGenerator<string> {
|
|
||||||
try {
|
|
||||||
const files = await readdir(dir);
|
export async function* readRecursive(dir: string): AsyncGenerator<string> {
|
||||||
for (const file of files) {
|
const files = await readdir(dir, { withFileTypes: true });
|
||||||
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
|
for (const file of files) {
|
||||||
if (fileStats.isDirectory()) {
|
const fullPath = path.posix.join(dir, file.name);
|
||||||
//Todo: refactor so that i dont repeat myself for files (line 71)
|
if (file.isDirectory()) {
|
||||||
if (!isSkippable(base)) {
|
if (!file.name.startsWith('!')) {
|
||||||
yield* readPaths(fullPath);
|
yield* readRecursive(fullPath);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!isSkippable(base)) {
|
|
||||||
yield 'file:///' + fullPath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (!file.name.startsWith('!')) {
|
||||||
|
yield "file:///"+path.resolve(fullPath);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requir = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
|
|
||||||
if (wrapper !== 'file') {
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
console.log('Experimental loading of sern.config.json');
|
|
||||||
const config = requir(resolve('sern.config.json'));
|
|
||||||
|
|
||||||
const makePath = (dir: PropertyKey) =>
|
|
||||||
config.language === 'typescript'
|
|
||||||
? join('dist', config.paths[dir]!)
|
|
||||||
: join(config.paths[dir]!);
|
|
||||||
|
|
||||||
console.log('Loading config: ', config);
|
|
||||||
const commandsPath = makePath('commands');
|
|
||||||
|
|
||||||
console.log('Commands path is set to', commandsPath);
|
|
||||||
let eventsPath: string | undefined;
|
|
||||||
if (config.paths.events) {
|
|
||||||
eventsPath = makePath('events');
|
|
||||||
console.log('Events path is set to', eventsPath);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
defaultPrefix: config.defaultPrefix,
|
|
||||||
commands: commandsPath,
|
|
||||||
events: eventsPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,112 +1,135 @@
|
|||||||
import { ClientEvents } from 'discord.js';
|
import type { ClientEvents } from 'discord.js';
|
||||||
import { CommandType, EventType, PluginType } from '../core/structures';
|
import { EventType } from '../core/structures/enums';
|
||||||
import type {
|
import type {
|
||||||
AnyCommandPlugin,
|
|
||||||
AnyEventPlugin,
|
|
||||||
CommandArgs,
|
|
||||||
ControlPlugin,
|
|
||||||
EventArgs,
|
|
||||||
InitPlugin,
|
|
||||||
} from '../types/core-plugin';
|
|
||||||
import type {
|
|
||||||
CommandModule,
|
|
||||||
EventModule,
|
|
||||||
InputCommand,
|
InputCommand,
|
||||||
InputEvent,
|
InputEvent,
|
||||||
Module,
|
Module,
|
||||||
|
ScheduledTask,
|
||||||
} from '../types/core-modules';
|
} from '../types/core-modules';
|
||||||
import { partitionPlugins } from './_internal';
|
import { partitionPlugins } from './functions'
|
||||||
import type { Awaitable } from '../types/utility';
|
import type { Awaitable } from '../types/utility';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 1.0.0 The wrapper function to define command modules for sern
|
* Creates a command module with standardized structure and plugin support.
|
||||||
* @param mod
|
*
|
||||||
*/
|
|
||||||
export function commandModule(mod: InputCommand): CommandModule {
|
|
||||||
const [onEvent, plugins] = partitionPlugins(mod.plugins);
|
|
||||||
return {
|
|
||||||
...mod,
|
|
||||||
onEvent,
|
|
||||||
plugins,
|
|
||||||
} as CommandModule;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* The wrapper function to define event modules for sern
|
* @param {InputCommand} mod - Command module configuration
|
||||||
* @param mod
|
* @returns {Module} Processed command module ready for registration
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic slash command
|
||||||
|
* export default commandModule({
|
||||||
|
* type: CommandType.Slash,
|
||||||
|
* description: "Ping command",
|
||||||
|
* execute: async (ctx) => {
|
||||||
|
* await ctx.reply("Pong! 🏓");
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Command with component interaction
|
||||||
|
* export default commandModule({
|
||||||
|
* type: CommandType.Slash,
|
||||||
|
* description: "Interactive command",
|
||||||
|
* execute: async (ctx) => {
|
||||||
|
* const button = new ButtonBuilder({
|
||||||
|
* customId: "btn/someData",
|
||||||
|
* label: "Click me",
|
||||||
|
* style: ButtonStyle.Primary
|
||||||
|
* });
|
||||||
|
* await ctx.reply({
|
||||||
|
* content: "Interactive message",
|
||||||
|
* components: [new ActionRowBuilder().addComponents(button)]
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* });
|
||||||
*/
|
*/
|
||||||
export function eventModule(mod: InputEvent): EventModule {
|
export function commandModule(mod: InputCommand): Module {
|
||||||
const [onEvent, plugins] = partitionPlugins(mod.plugins);
|
const [onEvent, plugins] = partitionPlugins(mod.plugins);
|
||||||
return {
|
return { ...mod,
|
||||||
...mod,
|
onEvent,
|
||||||
plugins,
|
plugins,
|
||||||
onEvent,
|
locals: {} } as Module;
|
||||||
} as EventModule;
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an event module for handling Discord.js or custom events.
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @template T - Event name from ClientEvents
|
||||||
|
* @param {InputEvent<T>} mod - Event module configuration
|
||||||
|
* @returns {Module} Processed event module ready for registration
|
||||||
|
* @throws {Error} If ControlPlugins are used in event modules
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Discord event listener
|
||||||
|
* export default eventModule({
|
||||||
|
* type: EventType.Discord,
|
||||||
|
* execute: async (message) => {
|
||||||
|
* console.log(`${message.author.tag}: ${message.content}`);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Custom sern event
|
||||||
|
* export default eventModule({
|
||||||
|
* type: EventType.Sern,
|
||||||
|
* execute: async (eventData) => {
|
||||||
|
* // Handle sern-specific event
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function eventModule<T extends keyof ClientEvents = keyof ClientEvents>(mod: InputEvent<T>): Module {
|
||||||
|
const [onEvent, plugins] = partitionPlugins(mod.plugins);
|
||||||
|
if(onEvent.length !== 0) throw Error("Event modules cannot have ControlPlugins");
|
||||||
|
return { ...mod,
|
||||||
|
plugins,
|
||||||
|
locals: {} } as Module;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create event modules from discord.js client events,
|
/** Create event modules from discord.js client events,
|
||||||
* This is an {@link eventModule} for discord events,
|
* This was an {@link eventModule} for discord events,
|
||||||
* where typings can be very bad.
|
* where typings were bad.
|
||||||
* @Experimental
|
* @deprecated Use {@link eventModule} instead
|
||||||
* @param mod
|
* @param mod
|
||||||
*/
|
*/
|
||||||
export function discordEvent<T extends keyof ClientEvents>(mod: {
|
export function discordEvent<T extends keyof ClientEvents>(mod: {
|
||||||
name: T;
|
name: T;
|
||||||
plugins?: AnyEventPlugin[];
|
once?: boolean;
|
||||||
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
|
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
|
||||||
}) {
|
}) {
|
||||||
return eventModule({
|
return eventModule({ type: EventType.Discord, ...mod, });
|
||||||
type: EventType.Discord,
|
|
||||||
...mod,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareClassPlugins(c: Module) {
|
|
||||||
const [onEvent, initPlugins] = partitionPlugins(c.plugins);
|
|
||||||
c.plugins = initPlugins as InitPlugin[];
|
|
||||||
c.onEvent = onEvent as ControlPlugin[];
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// Class modules:
|
|
||||||
// Can be refactored.
|
|
||||||
// Both implement singleton, could I make them inherit a singleton parent class?
|
|
||||||
/**
|
|
||||||
* @Experimental
|
|
||||||
* Will be refactored / changed in future
|
|
||||||
*/
|
|
||||||
export abstract class CommandExecutable<const Type extends CommandType = CommandType> {
|
|
||||||
abstract type: Type;
|
|
||||||
plugins: AnyCommandPlugin[] = [];
|
|
||||||
private static _instance: CommandModule;
|
|
||||||
|
|
||||||
static getInstance() {
|
|
||||||
if (!CommandExecutable._instance) {
|
|
||||||
//@ts-ignore
|
|
||||||
CommandExecutable._instance = new this();
|
|
||||||
prepareClassPlugins(CommandExecutable._instance);
|
|
||||||
}
|
|
||||||
return CommandExecutable._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract execute(...args: CommandArgs<Type, PluginType.Control>): Awaitable<unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Experimental
|
* Creates a scheduled task that can be executed at specified intervals using cron patterns
|
||||||
* Will be refactored in future
|
*
|
||||||
|
* @param {ScheduledTask} ism - The scheduled task configuration object
|
||||||
|
* @param {string} ism.trigger - A cron pattern that determines when the task should execute
|
||||||
|
* Format: "* * * * *" (minute hour day month day-of-week)
|
||||||
|
* @param {Function} ism.execute - The function to execute when the task is triggered
|
||||||
|
* @param {Object} ism.execute.context - The execution context passed to the task
|
||||||
|
*
|
||||||
|
* @returns {ScheduledTask} The configured scheduled task
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Create a task that runs every minute
|
||||||
|
* export default scheduledTask({
|
||||||
|
* trigger: "* * * * *",
|
||||||
|
* execute: (context) => {
|
||||||
|
* console.log("Task executed!");
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Tasks must be placed in the 'tasks' directory specified in your config
|
||||||
|
* - The file name serves as a unique identifier for the task
|
||||||
|
* - Tasks can be cancelled using deps['@sern/scheduler'].kill(uuid)
|
||||||
|
*
|
||||||
|
* @see {@link https://crontab.guru/} for testing and creating cron patterns
|
||||||
*/
|
*/
|
||||||
export abstract class EventExecutable<Type extends EventType> {
|
export function scheduledTask(ism: ScheduledTask): ScheduledTask {
|
||||||
abstract type: Type;
|
return ism
|
||||||
plugins: AnyEventPlugin[] = [];
|
|
||||||
|
|
||||||
private static _instance: EventModule;
|
|
||||||
static getInstance() {
|
|
||||||
if (!EventExecutable._instance) {
|
|
||||||
//@ts-ignore
|
|
||||||
EventExecutable._instance = new this();
|
|
||||||
prepareClassPlugins(EventExecutable._instance);
|
|
||||||
}
|
|
||||||
return EventExecutable._instance;
|
|
||||||
}
|
|
||||||
abstract execute(...args: EventArgs<Type, PluginType.Control>): Awaitable<unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file holds sern's rxjs operators used for processing data.
|
|
||||||
* Each function should be modular and testable, not bound to discord / sern
|
|
||||||
* and independent of each other.
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
concatMap,
|
|
||||||
defaultIfEmpty,
|
|
||||||
EMPTY,
|
|
||||||
every,
|
|
||||||
fromEvent,
|
|
||||||
map,
|
|
||||||
Observable,
|
|
||||||
of,
|
|
||||||
OperatorFunction,
|
|
||||||
pipe,
|
|
||||||
share,
|
|
||||||
} from 'rxjs';
|
|
||||||
import { Emitter, ErrorHandling, Logging } from './contracts';
|
|
||||||
import util from 'node:util';
|
|
||||||
import type { PluginResult, VoidResult } from '../types/core-plugin';
|
|
||||||
import type { Result } from 'ts-results-es'
|
|
||||||
/**
|
|
||||||
* if {src} is true, mapTo V, else ignore
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
|
|
||||||
return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls any plugin with {args}.
|
|
||||||
* @param args if an array, its spread and plugin called.
|
|
||||||
*/
|
|
||||||
export function callPlugin(args: unknown): OperatorFunction<
|
|
||||||
{
|
|
||||||
execute: (...args: unknown[]) => PluginResult;
|
|
||||||
},
|
|
||||||
VoidResult
|
|
||||||
> {
|
|
||||||
return concatMap(async plugin => {
|
|
||||||
if (Array.isArray(args)) {
|
|
||||||
return plugin.execute(...args);
|
|
||||||
}
|
|
||||||
return plugin.execute(args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the stream of results is all ok.
|
|
||||||
*/
|
|
||||||
export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
|
|
||||||
every(result => result.isOk()),
|
|
||||||
defaultIfEmpty(true),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sharedEventStream = <T>(e: Emitter, eventName: string) => {
|
|
||||||
return (fromEvent(e, eventName) as Observable<T>).pipe(share());
|
|
||||||
};
|
|
||||||
|
|
||||||
export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
|
|
||||||
return (pload: unknown, caught: Observable<C>) => {
|
|
||||||
// This is done to fit the ErrorHandling contract
|
|
||||||
const err = pload instanceof Error ? pload : Error(util.inspect(pload, { colors: true }));
|
|
||||||
//formatted payload
|
|
||||||
logging?.error({ message: util.inspect(pload) });
|
|
||||||
crashHandler.updateAlive(err);
|
|
||||||
return caught;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Temporary until i get rxjs operators working on ts-results-es
|
|
||||||
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
|
|
||||||
pipe(
|
|
||||||
concatMap(result => {
|
|
||||||
if(result.isOk()) {
|
|
||||||
return of(result.value)
|
|
||||||
}
|
|
||||||
onErr(result.error);
|
|
||||||
return EMPTY
|
|
||||||
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
137
src/core/plugin.ts
Normal file
137
src/core/plugin.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { CommandType, PluginType } from './structures/enums';
|
||||||
|
import type { Plugin, PluginResult, CommandArgs, InitArgs } from '../types/core-plugin';
|
||||||
|
import { Err, Ok } from './structures/result';
|
||||||
|
import type { Dictionary } from '../types/utility';
|
||||||
|
|
||||||
|
export function makePlugin<V extends unknown[]>(
|
||||||
|
type: PluginType,
|
||||||
|
execute: (...args: any[]) => any,
|
||||||
|
): Plugin<V> {
|
||||||
|
return { type, execute } as Plugin<V>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @since 2.5.0
|
||||||
|
*/
|
||||||
|
export function EventInitPlugin(execute: (args: InitArgs) => PluginResult) {
|
||||||
|
return makePlugin(PluginType.Init, execute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an initialization plugin for command preprocessing and modification
|
||||||
|
*
|
||||||
|
* @since 2.5.0
|
||||||
|
* @template I - Extends CommandType to enforce type safety for command modules
|
||||||
|
*
|
||||||
|
* @param {function} execute - Function to execute during command initialization
|
||||||
|
* @param {InitArgs<T>} execute.args - The initialization arguments
|
||||||
|
* @param {T} execute.args.module - The command module being initialized
|
||||||
|
* @param {string} execute.args.absPath - The absolute path to the module file
|
||||||
|
* @param {Dependencies} execute.args.deps - Dependency injection container
|
||||||
|
*
|
||||||
|
* @returns {Plugin} A plugin that runs during command initialization
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Plugin to update command description
|
||||||
|
* export const updateDescription = (description: string) => {
|
||||||
|
* return CommandInitPlugin(({ deps }) => {
|
||||||
|
* if(description.length > 100) {
|
||||||
|
* deps.logger?.info({ message: "Invalid description" })
|
||||||
|
* return controller.stop("From updateDescription: description is invalid");
|
||||||
|
* }
|
||||||
|
* module.description = description;
|
||||||
|
* return controller.next();
|
||||||
|
* });
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Plugin to store registration date in module locals
|
||||||
|
* export const dateRegistered = () => {
|
||||||
|
* return CommandInitPlugin(({ module }) => {
|
||||||
|
* module.locals.registered = Date.now()
|
||||||
|
* return controller.next();
|
||||||
|
* });
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Init plugins can modify how commands are loaded and perform preprocessing
|
||||||
|
* - The module.locals object can be used to store custom plugin-specific data
|
||||||
|
* - Be careful when modifying module fields as multiple plugins may interact with them
|
||||||
|
* - Use controller.next() to continue to the next plugin
|
||||||
|
* - Use controller.stop(reason) to halt plugin execution
|
||||||
|
*/
|
||||||
|
export function CommandInitPlugin<I extends CommandType>(
|
||||||
|
execute: (args: InitArgs) => PluginResult
|
||||||
|
): Plugin {
|
||||||
|
return makePlugin(PluginType.Init, execute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a control plugin for command preprocessing, filtering, and state management
|
||||||
|
*
|
||||||
|
* @since 2.5.0
|
||||||
|
* @template I - Extends CommandType to enforce type safety for command modules
|
||||||
|
*
|
||||||
|
* @param {function} execute - Function to execute during command control flow
|
||||||
|
* @param {CommandArgs<I>} execute.args - The command arguments array
|
||||||
|
* @param {Context} execute.args[0] - The discord context (e.g., guild, channel, user info, interaction)
|
||||||
|
* @param {SDT} execute.args[1] - The State, Dependencies, Params, Module, and Type object
|
||||||
|
*
|
||||||
|
* @returns {Plugin} A plugin that runs during command execution flow
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Plugin to restrict command to specific guild
|
||||||
|
* export const inGuild = (guildId: string) => {
|
||||||
|
* return CommandControlPlugin((ctx, sdt) => {
|
||||||
|
* if(ctx.guild.id !== guildId) {
|
||||||
|
* return controller.stop();
|
||||||
|
* }
|
||||||
|
* return controller.next();
|
||||||
|
* });
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Plugins passing state through the chain
|
||||||
|
* const plugin1 = CommandControlPlugin((ctx, sdt) => {
|
||||||
|
* return controller.next({ 'plugin1/data': 'from plugin1' });
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* const plugin2 = CommandControlPlugin((ctx, sdt) => {
|
||||||
|
* return controller.next({ 'plugin2/data': ctx.user.id });
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* export default commandModule({
|
||||||
|
* type: CommandType.Slash,
|
||||||
|
* plugins: [plugin1, plugin2],
|
||||||
|
* execute: (ctx, sdt) => {
|
||||||
|
* console.log(sdt.state); // Access accumulated state
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Control plugins are executed in order when a discord.js event is emitted
|
||||||
|
* - Use controller.next() to continue to next plugin or controller.stop() to halt execution
|
||||||
|
* - State can be passed between plugins using controller.next({ key: value })
|
||||||
|
* - State keys should be namespaced to avoid collisions (e.g., 'plugin-name/key')
|
||||||
|
* - Final accumulated state is passed to the command's execute function
|
||||||
|
* - All plugins must succeed for the command to execute
|
||||||
|
* - Plugins have access to dependencies through the sdt.deps object
|
||||||
|
* - Useful for implementing preconditions, filters, and command preprocessing
|
||||||
|
*/
|
||||||
|
export function CommandControlPlugin<I extends CommandType>(
|
||||||
|
execute: (...args: CommandArgs<I>) => PluginResult,
|
||||||
|
) {
|
||||||
|
return makePlugin(PluginType.Control, execute);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
* The object passed into every plugin to control a command's behavior
|
||||||
|
*/
|
||||||
|
export const controller = {
|
||||||
|
next: (val?: Dictionary) => Ok(val),
|
||||||
|
stop: (val?: string) => Err(val),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type Controller = typeof controller;
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import type {
|
|
||||||
AnySelectMenuInteraction,
|
|
||||||
AutocompleteInteraction,
|
|
||||||
ButtonInteraction,
|
|
||||||
ChatInputCommandInteraction,
|
|
||||||
MessageContextMenuCommandInteraction,
|
|
||||||
ModalSubmitInteraction,
|
|
||||||
UserContextMenuCommandInteraction,
|
|
||||||
} from 'discord.js';
|
|
||||||
import { InteractionType } from 'discord.js';
|
|
||||||
|
|
||||||
interface InteractionTypable {
|
|
||||||
type: InteractionType;
|
|
||||||
}
|
|
||||||
//discord.js pls fix ur typings or i will >:(
|
|
||||||
type AnyMessageComponentInteraction = AnySelectMenuInteraction | ButtonInteraction;
|
|
||||||
type AnyCommandInteraction =
|
|
||||||
| ChatInputCommandInteraction
|
|
||||||
| MessageContextMenuCommandInteraction
|
|
||||||
| UserContextMenuCommandInteraction;
|
|
||||||
|
|
||||||
export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction {
|
|
||||||
return i.type === InteractionType.MessageComponent;
|
|
||||||
}
|
|
||||||
export function isCommand(i: InteractionTypable): i is AnyCommandInteraction {
|
|
||||||
return i.type === InteractionType.ApplicationCommand;
|
|
||||||
}
|
|
||||||
export function isAutocomplete(i: InteractionTypable): i is AutocompleteInteraction {
|
|
||||||
return i.type === InteractionType.ApplicationCommandAutocomplete;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isModal(i: InteractionTypable): i is ModalSubmitInteraction {
|
|
||||||
return i.type === InteractionType.ModalSubmit;
|
|
||||||
}
|
|
||||||
65
src/core/presences.ts
Normal file
65
src/core/presences.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import type { ActivitiesOptions } from "discord.js";
|
||||||
|
import type { IntoDependencies } from "./ioc";
|
||||||
|
import type { Emitter } from "./interfaces";
|
||||||
|
import { Awaitable } from "../types/utility";
|
||||||
|
|
||||||
|
type Status = 'online' | 'idle' | 'invisible' | 'dnd'
|
||||||
|
type PresenceReduce = (previous: Presence.Result) => Awaitable<Presence.Result>;
|
||||||
|
|
||||||
|
export const Presence = {
|
||||||
|
/**
|
||||||
|
* A small wrapper to provide type inference.
|
||||||
|
* Create a Presence module which **MUST** be put in a file called presence.(language-extension)
|
||||||
|
* adjacent to the file where **Sern.init** is CALLED.
|
||||||
|
*/
|
||||||
|
module : <T extends (keyof Dependencies)[]>(conf: Presence.Config<T>) => conf,
|
||||||
|
/**
|
||||||
|
* Create a Presence body which can be either:
|
||||||
|
* - once, the presence is activated only once.
|
||||||
|
* - repeated, per cycle or event, the presence can be changed.
|
||||||
|
*/
|
||||||
|
of : (root: Omit<Presence.Result, 'repeat' | 'onRepeat'>) => {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* Presence
|
||||||
|
* .of({ activities: [{ name: "deez nuts" }] }) //starts presence with "deez nuts".
|
||||||
|
* .repeated(prev => {
|
||||||
|
* return {
|
||||||
|
* afk: true,
|
||||||
|
* activities: prev.activities?.map(s => ({ ...s, name: s.name+"s" }))
|
||||||
|
* };
|
||||||
|
* }, 10000)) //every 10 s, the callback sets the presence to the value returned.
|
||||||
|
*/
|
||||||
|
repeated: (onRepeat: PresenceReduce, repeat: number | [Emitter, string]) => {
|
||||||
|
return { repeat, onRepeat, ...root }
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* Presence.of({
|
||||||
|
* activities: [{ name: "Chilling out" }]
|
||||||
|
* }).once() // Sets the presence once, with what's provided in '.of()'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
once: () => root
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export declare namespace Presence {
|
||||||
|
export type Config<T extends (keyof Dependencies)[]> = {
|
||||||
|
inject?: [...T]
|
||||||
|
execute: (...v: IntoDependencies<T>) => Awaitable<Presence.Result>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
status?: Status;
|
||||||
|
afk?: boolean;
|
||||||
|
activities?: ActivitiesOptions[];
|
||||||
|
shardId?: number[];
|
||||||
|
repeat?: number | [Emitter, string];
|
||||||
|
onRepeat?: PresenceReduce
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import type { ReplyOptions } from "../../types/utility";
|
|
||||||
import type { Logging } from "../contracts";
|
|
||||||
|
|
||||||
export interface Response {
|
|
||||||
type: 'fail' | 'continue';
|
|
||||||
body?: ReplyOptions;
|
|
||||||
log?: { type: keyof Logging; message: unknown }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const of = () => {
|
|
||||||
const payload = {
|
|
||||||
type: 'fail',
|
|
||||||
body: undefined,
|
|
||||||
log : undefined
|
|
||||||
} as Record<PropertyKey, unknown>
|
|
||||||
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* @param {'fail' | 'continue'} p a status to determine if the error will
|
|
||||||
* terminate your application or continue. Warning and
|
|
||||||
*/
|
|
||||||
status: (p: 'fail' | 'continue') => {
|
|
||||||
payload.type = p;
|
|
||||||
return payload;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @param {keyof Logging} type Determine to log to logger[type].
|
|
||||||
* @param {T} message the message to log
|
|
||||||
*
|
|
||||||
* Log this error with the logger.
|
|
||||||
*/
|
|
||||||
log: <T=string>(type: keyof Logging, message: T) => {
|
|
||||||
payload.log = { type, message };
|
|
||||||
return payload;
|
|
||||||
},
|
|
||||||
reply: (bodyContent: ReplyOptions) => {
|
|
||||||
payload.body = bodyContent;
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import type {
|
||||||
BaseInteraction,
|
BaseInteraction,
|
||||||
ChatInputCommandInteraction,
|
ChatInputCommandInteraction,
|
||||||
Client,
|
Client,
|
||||||
@@ -8,10 +8,11 @@ import {
|
|||||||
Snowflake,
|
Snowflake,
|
||||||
User,
|
User,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { CoreContext } from '../structures/core-context';
|
import { Result, Ok, Err, val } from './result';
|
||||||
import { Result, Ok, Err } from 'ts-results-es';
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { ReplyOptions } from '../../types/utility';
|
import type { ReplyOptions } from '../../types/utility';
|
||||||
|
import { fmt } from '../functions'
|
||||||
|
import { SernError } from './enums';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,33 +20,32 @@ import { ReplyOptions } from '../../types/utility';
|
|||||||
* Provides values shared between
|
* Provides values shared between
|
||||||
* Message and ChatInputCommandInteraction
|
* Message and ChatInputCommandInteraction
|
||||||
*/
|
*/
|
||||||
export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
export class Context {
|
||||||
/*
|
|
||||||
* @Experimental
|
|
||||||
*/
|
|
||||||
get options() {
|
get options() {
|
||||||
|
if(this.isMessage()) {
|
||||||
|
const [, ...rest] = fmt(this.message.content, this.prefix);
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
return this.interaction.options;
|
return this.interaction.options;
|
||||||
}
|
}
|
||||||
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>) {
|
|
||||||
super(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
protected constructor(protected ctx: Result<Message, ChatInputCommandInteraction>,
|
||||||
|
private __prefix?: string) { }
|
||||||
|
public get prefix() {
|
||||||
|
return this.__prefix;
|
||||||
|
}
|
||||||
public get id(): Snowflake {
|
public get id(): Snowflake {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).id
|
||||||
.map(m => m.id)
|
|
||||||
.mapErr(i => i.id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get channel() {
|
public get channel() {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).channel;
|
||||||
.map(m => m.channel)
|
|
||||||
.mapErr(i => i.channel));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get channelId(): Snowflake {
|
public get channelId(): Snowflake {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).channelId;
|
||||||
.map(m => m.channelId)
|
|
||||||
.mapErr(i => i.channelId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,9 +53,11 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
|||||||
* else, interaction.user
|
* else, interaction.user
|
||||||
*/
|
*/
|
||||||
public get user(): User {
|
public get user(): User {
|
||||||
return safeUnwrap(this.ctx
|
if(this.ctx.ok) {
|
||||||
.map(m => m.author)
|
return this.ctx.value.author;
|
||||||
.mapErr(i => i.user));
|
}
|
||||||
|
return this.ctx.error.user;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get userId(): Snowflake {
|
public get userId(): Snowflake {
|
||||||
@@ -63,65 +65,67 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get createdTimestamp(): number {
|
public get createdTimestamp(): number {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).createdTimestamp;
|
||||||
.map(m => m.createdTimestamp)
|
|
||||||
.mapErr(i => i.createdTimestamp));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get guild() {
|
public get guild() {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).guild;
|
||||||
.map(m => m.guild)
|
|
||||||
.mapErr(i => i.guild));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get guildId() {
|
public get guildId() {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).guildId;
|
||||||
.map(m => m.guildId)
|
|
||||||
.mapErr(i => i.guildId));
|
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* interactions can return APIGuildMember if the guild it is emitted from is not cached
|
* interactions can return APIGuildMember if the guild it is emitted from is not cached
|
||||||
*/
|
*/
|
||||||
public get member() {
|
public get member() {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).member;
|
||||||
.map(m => m.member)
|
|
||||||
.mapErr(i => i.member));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get message(): Message {
|
||||||
|
if(this.ctx.ok) {
|
||||||
|
return this.ctx.value;
|
||||||
|
}
|
||||||
|
throw Error(SernError.MismatchEvent);
|
||||||
|
}
|
||||||
|
public isMessage(): this is Context & { ctx: Result<Message, never> } {
|
||||||
|
return this.ctx.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSlash(): this is Context & { ctx: Result<never, ChatInputCommandInteraction> } {
|
||||||
|
return !this.isMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
get interaction(): ChatInputCommandInteraction {
|
||||||
|
if(!this.ctx.ok) {
|
||||||
|
return this.ctx.error;
|
||||||
|
}
|
||||||
|
throw Error(SernError.MismatchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public get client(): Client {
|
public get client(): Client {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).client;
|
||||||
.map(m => m.client)
|
|
||||||
.mapErr(i => i.client));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get inGuild(): boolean {
|
public get inGuild(): boolean {
|
||||||
return safeUnwrap(this.ctx
|
return val(this.ctx).inGuild()
|
||||||
.map(m => m.inGuild())
|
|
||||||
.mapErr(i => i.inGuild()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reply(content: ReplyOptions) {
|
public async reply(content: ReplyOptions) {
|
||||||
return safeUnwrap(
|
if(this.ctx.ok) {
|
||||||
this.ctx
|
return this.ctx.value.reply(content as MessageReplyOptions)
|
||||||
.map(m => m.reply(content as MessageReplyOptions))
|
}
|
||||||
.mapErr(i =>
|
interface FetchReply { fetchReply: true };
|
||||||
i.reply(content as InteractionReplyOptions).then(() => i.fetchReply()),
|
return this.ctx.error.reply(content as InteractionReplyOptions & FetchReply)
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static override wrap(wrappable: BaseInteraction | Message): Context {
|
static wrap(wrappable: BaseInteraction | Message, prefix?: string): Context {
|
||||||
if ('interaction' in wrappable) {
|
if ('interaction' in wrappable) {
|
||||||
return new Context(Ok(wrappable));
|
return new Context(Ok(wrappable), prefix);
|
||||||
}
|
}
|
||||||
assert.ok(wrappable.isChatInputCommand());
|
assert.ok(wrappable.isChatInputCommand(), "Context created with bad interaction.");
|
||||||
return new Context(Err(wrappable));
|
return new Context(Err(wrappable), prefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeUnwrap<T>(res: Result<T, T>) {
|
|
||||||
if(res.isOk()) {
|
|
||||||
return res.expect("Tried unwrapping message field: " + res)
|
|
||||||
}
|
|
||||||
return res.expectErr("Tried unwrapping interaction field" + res)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Result as Either } from 'ts-results-es';
|
|
||||||
import { SernError } from '../_internal';
|
|
||||||
import * as assert from 'node:assert';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @since 3.0.0
|
|
||||||
*/
|
|
||||||
export abstract class CoreContext<M, I> {
|
|
||||||
protected constructor(protected ctx: Either<M, I>) {
|
|
||||||
assert.ok(typeof ctx === 'object' && ctx != null);
|
|
||||||
}
|
|
||||||
get message(): M {
|
|
||||||
return this.ctx.expect(SernError.MismatchEvent);
|
|
||||||
}
|
|
||||||
get interaction(): I {
|
|
||||||
return this.ctx.expectErr(SernError.MismatchEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMessage(): this is CoreContext<M, never> {
|
|
||||||
return this.ctx.isOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isSlash(): this is CoreContext<never, I> {
|
|
||||||
return !this.isMessage();
|
|
||||||
}
|
|
||||||
//todo: add agnostic options resolver for Context
|
|
||||||
abstract get options(): unknown;
|
|
||||||
|
|
||||||
static wrap(_: unknown): unknown {
|
|
||||||
throw Error('You need to override this method; cannot wrap an abstract class');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user