mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb6e8c2cfc | ||
|
|
0eecb08e87 | ||
|
|
c67748c7df | ||
|
|
efee0fdbe2 | ||
|
|
797442ece3 | ||
|
|
513ac8edf4 | ||
|
|
81a0180d05 | ||
|
|
89d7409536 | ||
|
|
aa802f761e | ||
|
|
2414992b73 | ||
|
|
70c6236802 | ||
|
|
1f25aa64b9 | ||
|
|
7cddee30aa | ||
|
|
e7286eee9f | ||
|
|
a67450328e | ||
|
|
47401f46a3 | ||
|
|
1059065980 | ||
|
|
974c30fa6c | ||
|
|
3a569726d8 | ||
|
|
1b7f2a49a8 | ||
|
|
97fa2a2d78 | ||
|
|
a52ad270d8 |
6
.github/workflows/npm-publish.yml
vendored
6
.github/workflows/npm-publish.yml
vendored
@@ -13,9 +13,9 @@ jobs:
|
|||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
- uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # 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 }}
|
||||||
|
|||||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -24,6 +24,5 @@ jobs:
|
|||||||
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
|
|
||||||
|
|||||||
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
|
|
||||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,5 +1,47 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [4.2.6](https://github.com/sern-handler/handler/compare/v4.2.5...v4.2.6) (2025-09-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Chores
|
||||||
|
|
||||||
|
* release 4.2.6 ([#402](https://github.com/sern-handler/handler/issues/402)) ([0eecb08](https://github.com/sern-handler/handler/commit/0eecb08e87e26102030ccf6ef38ddd81051ec373))
|
||||||
|
|
||||||
|
## [4.2.5](https://github.com/sern-handler/handler/compare/v4.2.4...v4.2.5) (2025-08-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* make message module warn rather than throwing ([#399](https://github.com/sern-handler/handler/issues/399)) ([797442e](https://github.com/sern-handler/handler/commit/797442ece3999bf2cb6b5ba0688ce0177e72a22f))
|
||||||
|
|
||||||
|
## [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)
|
## [4.2.0](https://github.com/sern-handler/handler/compare/v4.1.1...v4.2.0) (2025-01-18)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
28
README.md
28
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,7 +20,7 @@
|
|||||||
- 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.
|
||||||
- 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.
|
||||||
@@ -43,20 +44,29 @@ export default commandModule({
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
# Show off your sern Discord Bot!
|
||||||
|
|
||||||
|
## Badge
|
||||||
|
- 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)
|
||||||
|
<img src="https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev">
|
||||||
|
|
||||||
## 🤖 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.
|
||||||
- [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](https://github.com/GlitchApotamus/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.
|
- [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.
|
- [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)
|
||||||
|
|||||||
3897
package-lock.json
generated
Normal file
3897
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@sern/handler",
|
"name": "@sern/handler",
|
||||||
"packageManager": "yarn@3.5.0",
|
"packageManager": "yarn@3.5.0",
|
||||||
"version": "4.2.0",
|
"version": "4.2.6",
|
||||||
"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.js",
|
"module": "./dist/index.js",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"prepare": "tsc",
|
"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"
|
||||||
},
|
},
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
"author": "SernDevs",
|
"author": "SernDevs",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sern/ioc": "^1.1.0",
|
"@sern/ioc": "^1.1.2",
|
||||||
"callsites": "^3.1.0",
|
"callsites": "^3.1.0",
|
||||||
"cron": "^3.1.7",
|
"cron": "^3.1.7",
|
||||||
"deepmerge": "^4.3.1"
|
"deepmerge": "^4.3.1"
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
"@types/node-cron": "^3.0.11",
|
"@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.15.3",
|
"discord.js": "^14.22.1",
|
||||||
"eslint": "8.39.0",
|
"eslint": "8.39.0",
|
||||||
"typescript": "5.0.2",
|
"typescript": "5.0.2",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import type {
|
|||||||
MessageContextMenuCommandInteraction,
|
MessageContextMenuCommandInteraction,
|
||||||
ModalSubmitInteraction,
|
ModalSubmitInteraction,
|
||||||
UserContextMenuCommandInteraction,
|
UserContextMenuCommandInteraction,
|
||||||
AutocompleteInteraction
|
AutocompleteInteraction,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
|
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
|
||||||
import { PluginType } from './structures/enums';
|
import { PluginType } from './structures/enums';
|
||||||
import assert from 'assert';
|
|
||||||
import type { Payload, UnpackedDependencies } from '../types/utility';
|
import type { Payload, UnpackedDependencies } from '../types/utility';
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
|
export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
|
||||||
return {
|
return {
|
||||||
@@ -57,51 +57,31 @@ export function partitionPlugins<T,V>
|
|||||||
return [controlPlugins, initPlugins] as [T[], V[]];
|
return [controlPlugins, initPlugins] as [T[], V[]];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const createLookupTable = (options: SernOptionsData[]): Map<string, SernAutocompleteData> => {
|
||||||
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
|
const table = new Map<string, SernAutocompleteData>();
|
||||||
* @param iAutocomplete
|
_createLookupTable(table, options, "<parent>");
|
||||||
* @param options
|
return table;
|
||||||
*/
|
}
|
||||||
export function treeSearch(
|
|
||||||
iAutocomplete: AutocompleteInteraction,
|
const _createLookupTable = (table: Map<string, SernAutocompleteData>, options: SernOptionsData[], parent: string) => {
|
||||||
options: SernOptionsData[] | undefined,
|
for (const opt of options) {
|
||||||
): SernAutocompleteData & { parent?: string } | undefined {
|
const name = path.posix.join(parent, opt.name)
|
||||||
if (options === undefined) return undefined;
|
switch(opt.type) {
|
||||||
//clone to prevent mutation of original command module
|
|
||||||
const _options = options.map(a => ({ ...a }));
|
|
||||||
const subcommands = new Set();
|
|
||||||
while (_options.length > 0) {
|
|
||||||
const cur = _options.pop()!;
|
|
||||||
switch (cur.type) {
|
|
||||||
case ApplicationCommandOptionType.Subcommand: {
|
case ApplicationCommandOptionType.Subcommand: {
|
||||||
subcommands.add(cur.name);
|
_createLookupTable(table, opt.options ?? [], name);
|
||||||
for (const option of cur.options ?? []) _options.push(option);
|
} break;
|
||||||
} break;
|
|
||||||
case ApplicationCommandOptionType.SubcommandGroup: {
|
case ApplicationCommandOptionType.SubcommandGroup: {
|
||||||
for (const command of cur.options ?? []) _options.push(command);
|
_createLookupTable(table, opt.options ?? [], name);
|
||||||
} break;
|
} break;
|
||||||
default: {
|
default: {
|
||||||
if ('autocomplete' in cur && cur.autocomplete) {
|
if(Reflect.get(opt, 'autocomplete') === true) {
|
||||||
const choice = iAutocomplete.options.getFocused(true);
|
table.set(name, opt as SernAutocompleteData)
|
||||||
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
|
|
||||||
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;
|
} break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
interface InteractionTypable {
|
interface InteractionTypable {
|
||||||
type: InteractionType;
|
type: InteractionType;
|
||||||
@@ -119,6 +99,9 @@ export function isMessageComponent(i: InteractionTypable): i is AnyMessageCompon
|
|||||||
export function isCommand(i: InteractionTypable): i is AnyCommandInteraction {
|
export function isCommand(i: InteractionTypable): i is AnyCommandInteraction {
|
||||||
return i.type === InteractionType.ApplicationCommand;
|
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 {
|
export function isAutocomplete(i: InteractionTypable): i is AutocompleteInteraction {
|
||||||
return i.type === InteractionType.ApplicationCommandAutocomplete;
|
return i.type === InteractionType.ApplicationCommandAutocomplete;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Module } from '../types/core-modules'
|
import type { Module, SernAutocompleteData } from '../types/core-modules'
|
||||||
import { callPlugins, executeModule } from './event-utils';
|
import { callPlugins, executeModule } from './event-utils';
|
||||||
import { SernError } from '../core/structures/enums'
|
import { SernError } from '../core/structures/enums'
|
||||||
import { createSDT, isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload, treeSearch } from '../core/functions'
|
import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload } from '../core/functions'
|
||||||
import type { UnpackedDependencies } from '../types/utility';
|
import type { UnpackedDependencies } from '../types/utility';
|
||||||
import * as Id from '../core/id'
|
import * as Id from '../core/id'
|
||||||
import { Context } from '../core/structures/context';
|
import { Context } from '../core/structures/context';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -29,13 +30,28 @@ export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: s
|
|||||||
}
|
}
|
||||||
const { module, params } = modules.at(0)!;
|
const { module, params } = modules.at(0)!;
|
||||||
let payload;
|
let payload;
|
||||||
|
// handles autocomplete
|
||||||
if(isAutocomplete(event)) {
|
if(isAutocomplete(event)) {
|
||||||
//@ts-ignore stfu
|
const lookupTable = module.locals['@sern/lookup-table'] as Map<string, SernAutocompleteData>
|
||||||
const { command } = treeSearch(event, module.options);
|
const subCommandGroup = event.options.getSubcommandGroup(false) ?? "",
|
||||||
payload= { module: command as Module, //autocomplete is not a true "module" warning cast!
|
subCommand = event.options.getSubcommand(false) ?? "",
|
||||||
args: [event, createSDT(command, deps, params)] };
|
option = event.options.getFocused(true),
|
||||||
|
fullPath = path.posix.join("<parent>", subCommandGroup, subCommand, option.name)
|
||||||
|
|
||||||
|
const resolvedModule = (lookupTable.get(fullPath)!.command) as Module
|
||||||
|
payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast!
|
||||||
|
args: [event, createSDT(module, deps, params)] };
|
||||||
|
// either CommandTypes Slash | ContextMessage | ContextUesr
|
||||||
} else if(isCommand(event)) {
|
} else if(isCommand(event)) {
|
||||||
payload= { module, args: [Context.wrap(event, defaultPrefix), createSDT(module, deps, params)] };
|
const sdt = createSDT(module, deps, params)
|
||||||
|
// handle CommandType.CtxUser || CommandType.CtxMsg
|
||||||
|
if(isContextCommand(event)) {
|
||||||
|
payload= { module, args: [event, sdt] };
|
||||||
|
} else {
|
||||||
|
// handle CommandType.Slash || CommandType.Both
|
||||||
|
payload= { module, args: [Context.wrap(event, defaultPrefix), sdt] };
|
||||||
|
}
|
||||||
|
// handles modals or components
|
||||||
} else if (isModal(event) || isMessageComponent(event)) {
|
} else if (isModal(event) || isMessageComponent(event)) {
|
||||||
payload= { module, args: [event, createSDT(module, deps, params)] }
|
payload= { module, args: [event, createSDT(module, deps, params)] }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function messageHandler (deps: UnpackedDependencies, defaultPrefix?: stri
|
|||||||
const [prefix] = fmt(message.content, defaultPrefix);
|
const [prefix] = fmt(message.content, defaultPrefix);
|
||||||
let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
|
let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
|
||||||
if(!module) {
|
if(!module) {
|
||||||
throw Error('Possibly undefined behavior: could not find a static id to resolve')
|
log?.warning({ message: 'Possibly undefined behavior: could not find a static id to resolve' });
|
||||||
}
|
}
|
||||||
const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] }
|
const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] }
|
||||||
const result = await callPlugins(payload)
|
const result = await callPlugins(payload)
|
||||||
|
|||||||
@@ -1,37 +1,58 @@
|
|||||||
import * as Files from '../core/module-loading'
|
import * as Files from "../core/module-loading";
|
||||||
import { once } from 'node:events';
|
import { once } from "node:events";
|
||||||
import { resultPayload } from '../core/functions';
|
import { createLookupTable, resultPayload } from "../core/functions";
|
||||||
import { CommandType } from '../core/structures/enums';
|
import { CommandType } from "../core/structures/enums";
|
||||||
import { Module } from '../types/core-modules';
|
import { Module, SernOptionsData } from "../types/core-modules";
|
||||||
import type { UnpackedDependencies, Wrapper } from '../types/utility';
|
import type { UnpackedDependencies, Wrapper } from "../types/utility";
|
||||||
import { callInitPlugins } from './event-utils';
|
import { callInitPlugins } from "./event-utils";
|
||||||
|
import { SernAutocompleteData } from "..";
|
||||||
|
import { Events } from "discord.js";
|
||||||
|
|
||||||
export default async function(dirs: string | string[], deps : UnpackedDependencies) {
|
export default async function (
|
||||||
const { '@sern/client': client,
|
dirs: string | string[],
|
||||||
'@sern/logger': log,
|
deps: UnpackedDependencies,
|
||||||
'@sern/emitter': sEmitter,
|
) {
|
||||||
'@sern/modules': commands } = deps;
|
const {
|
||||||
log?.info({ message: "Waiting on discord client to be ready..." })
|
"@sern/client": client,
|
||||||
await once(client, "ready");
|
"@sern/logger": log,
|
||||||
log?.info({ message: "Client signaled ready, registering modules" });
|
"@sern/emitter": sEmitter,
|
||||||
|
"@sern/modules": commands,
|
||||||
|
} = deps;
|
||||||
|
log?.info({ message: "Waiting on discord client to be ready..." });
|
||||||
|
await once(client, Events.ClientReady);
|
||||||
|
log?.info({ message: "Client signaled ready, registering modules" });
|
||||||
|
|
||||||
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
|
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
|
||||||
// possibly optimize to concurrently import modules
|
// possibly optimize to concurrently import modules
|
||||||
|
|
||||||
const directories = Array.isArray(dirs) ? dirs : [dirs];
|
const directories = Array.isArray(dirs) ? dirs : [dirs];
|
||||||
|
|
||||||
for (const dir of directories) {
|
for (const dir of directories) {
|
||||||
for await (const path of Files.readRecursive(dir)) {
|
for await (const path of Files.readRecursive(dir)) {
|
||||||
let { module } = await Files.importModule<Module>(path);
|
const { module } = await Files.importModule<Module>(path);
|
||||||
const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect;
|
const validType =
|
||||||
if(!validType) {
|
module.type >= CommandType.Text &&
|
||||||
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
|
module.type <= CommandType.ChannelSelect;
|
||||||
}
|
if (!validType) {
|
||||||
const resultModule = await callInitPlugins(module, deps, true);
|
throw Error(
|
||||||
// FREEZE! no more writing!!
|
`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``,
|
||||||
commands.set(resultModule.meta.id, Object.freeze(resultModule));
|
);
|
||||||
sEmitter.emit('module.register', resultPayload('success', resultModule));
|
}
|
||||||
}
|
const resultModule = await callInitPlugins(module, deps, true);
|
||||||
|
|
||||||
|
if (
|
||||||
|
module.type === CommandType.Both ||
|
||||||
|
module.type === CommandType.Slash
|
||||||
|
) {
|
||||||
|
const options = (Reflect.get(module, "options") ??
|
||||||
|
[]) as SernOptionsData[];
|
||||||
|
const lookupTable = createLookupTable(options);
|
||||||
|
module.locals["@sern/lookup-table"] = lookupTable;
|
||||||
|
}
|
||||||
|
// FREEZE! no more writing!!
|
||||||
|
commands.set(resultModule.meta.id, Object.freeze(resultModule));
|
||||||
|
sEmitter.emit("module.register", resultPayload("success", resultModule));
|
||||||
}
|
}
|
||||||
sEmitter.emit('modulesLoaded');
|
}
|
||||||
|
sEmitter.emit("modulesLoaded");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import { Awaitable, SernEventsMapping, UnpackedDependencies, Dictionary } from '
|
|||||||
*
|
*
|
||||||
* @see {@link CommandControlPlugin} for plugin implementation
|
* @see {@link CommandControlPlugin} for plugin implementation
|
||||||
* @see {@link CommandType} for available command types
|
* @see {@link CommandType} for available command types
|
||||||
* @see {@link Dependencies} for dependency injection interface
|
* @see {@link Dependencies} for [dependency injection](https://sern.dev/v4/reference/dependencies/) interface
|
||||||
*/
|
*/
|
||||||
export type SDT = {
|
export type SDT = {
|
||||||
/**
|
/**
|
||||||
@@ -114,6 +114,10 @@ export type SDT = {
|
|||||||
|
|
||||||
export type Processed<T> = T & { name: string; description: string };
|
export type Processed<T> = T & { name: string; description: string };
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface Module {
|
export interface Module {
|
||||||
type: CommandType | EventType;
|
type: CommandType | EventType;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -196,13 +200,18 @@ export interface Module {
|
|||||||
execute(...args: any[]): Awaitable<any>;
|
execute(...args: any[]): Awaitable<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
|
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
|
||||||
extends Module {
|
extends Module {
|
||||||
name?: T;
|
name?: T;
|
||||||
type: EventType.Sern;
|
type: EventType.Sern;
|
||||||
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
|
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface ExternalEventCommand extends Module {
|
export interface ExternalEventCommand extends Module {
|
||||||
name?: string;
|
name?: string;
|
||||||
emitter: keyof Dependencies;
|
emitter: keyof Dependencies;
|
||||||
@@ -210,83 +219,121 @@ export interface ExternalEventCommand extends Module {
|
|||||||
execute(...args: unknown[]): Awaitable<unknown>;
|
execute(...args: unknown[]): Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface ContextMenuUser extends Module {
|
export interface ContextMenuUser extends Module {
|
||||||
type: CommandType.CtxUser;
|
type: CommandType.CtxUser;
|
||||||
execute: (ctx: UserContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: UserContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface ContextMenuMsg extends Module {
|
export interface ContextMenuMsg extends Module {
|
||||||
type: CommandType.CtxMsg;
|
type: CommandType.CtxMsg;
|
||||||
execute: (ctx: MessageContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: MessageContextMenuCommandInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface ButtonCommand extends Module {
|
export interface ButtonCommand extends Module {
|
||||||
type: CommandType.Button;
|
type: CommandType.Button;
|
||||||
execute: (ctx: ButtonInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: ButtonInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface StringSelectCommand extends Module {
|
export interface StringSelectCommand extends Module {
|
||||||
type: CommandType.StringSelect;
|
type: CommandType.StringSelect;
|
||||||
execute: (ctx: StringSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: StringSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface ChannelSelectCommand extends Module {
|
export interface ChannelSelectCommand extends Module {
|
||||||
type: CommandType.ChannelSelect;
|
type: CommandType.ChannelSelect;
|
||||||
execute: (ctx: ChannelSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: ChannelSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface RoleSelectCommand extends Module {
|
export interface RoleSelectCommand extends Module {
|
||||||
type: CommandType.RoleSelect;
|
type: CommandType.RoleSelect;
|
||||||
execute: (ctx: RoleSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: RoleSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface MentionableSelectCommand extends Module {
|
export interface MentionableSelectCommand extends Module {
|
||||||
type: CommandType.MentionableSelect;
|
type: CommandType.MentionableSelect;
|
||||||
execute: (ctx: MentionableSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: MentionableSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface UserSelectCommand extends Module {
|
export interface UserSelectCommand extends Module {
|
||||||
type: CommandType.UserSelect;
|
type: CommandType.UserSelect;
|
||||||
execute: (ctx: UserSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: UserSelectMenuInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface ModalSubmitCommand extends Module {
|
export interface ModalSubmitCommand extends Module {
|
||||||
type: CommandType.Modal;
|
type: CommandType.Modal;
|
||||||
execute: (ctx: ModalSubmitInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: ModalSubmitInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface AutocompleteCommand {
|
export interface AutocompleteCommand {
|
||||||
onEvent?: ControlPlugin[];
|
onEvent?: ControlPlugin[];
|
||||||
execute: (ctx: AutocompleteInteraction, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: AutocompleteInteraction, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
|
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
|
||||||
extends Module {
|
extends Module {
|
||||||
name?: T;
|
name?: T;
|
||||||
type: EventType.Discord;
|
type: EventType.Discord;
|
||||||
execute(...args: ClientEvents[T]): Awaitable<unknown>;
|
execute(...args: ClientEvents[T]): Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
* @see @link {commandModule} to create a text command
|
||||||
|
*/
|
||||||
export interface TextCommand extends Module {
|
export interface TextCommand extends Module {
|
||||||
type: CommandType.Text;
|
type: CommandType.Text;
|
||||||
execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
* @see @link {commandModule} to create a slash command
|
||||||
|
*/
|
||||||
export interface SlashCommand extends Module {
|
export interface SlashCommand extends Module {
|
||||||
type: CommandType.Slash;
|
type: CommandType.Slash;
|
||||||
description: string;
|
description: string;
|
||||||
options?: SernOptionsData[];
|
options?: SernOptionsData[];
|
||||||
execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
* @see @link {commandModule} to create a both command
|
||||||
|
*/
|
||||||
export interface BothCommand extends Module {
|
export interface BothCommand extends Module {
|
||||||
type: CommandType.Both;
|
type: CommandType.Both;
|
||||||
description: string;
|
description: string;
|
||||||
options?: SernOptionsData[];
|
options?: SernOptionsData[];
|
||||||
execute: (ctx: Context, tbd: SDT) => Awaitable<unknown>;
|
execute: (ctx: Context, tbd: SDT) => Awaitable<unknown>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
|
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
export type CommandModule =
|
export type CommandModule =
|
||||||
| TextCommand
|
| TextCommand
|
||||||
| SlashCommand
|
| SlashCommand
|
||||||
@@ -355,6 +402,7 @@ export type InputCommand = {
|
|||||||
}[CommandType];
|
}[CommandType];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @see @link {https://sern.dev/v4/reference/autocomplete/}
|
||||||
* Type that replaces autocomplete with {@link SernAutocompleteData}
|
* Type that replaces autocomplete with {@link SernAutocompleteData}
|
||||||
*/
|
*/
|
||||||
export type SernOptionsData =
|
export type SernOptionsData =
|
||||||
@@ -374,7 +422,9 @@ export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsDa
|
|||||||
options?: SernSubCommandData[];
|
options?: SernSubCommandData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.0.0
|
||||||
|
*/
|
||||||
export interface ScheduledTaskContext {
|
export interface ScheduledTaskContext {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,7 +448,9 @@ interface TaskAttrs {
|
|||||||
*/
|
*/
|
||||||
deps: UnpackedDependencies
|
deps: UnpackedDependencies
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @since 4.0.0
|
||||||
|
*/
|
||||||
export interface ScheduledTask {
|
export interface ScheduledTask {
|
||||||
name?: string;
|
name?: string;
|
||||||
trigger: string | Date;
|
trigger: string | Date;
|
||||||
|
|||||||
84
test/autocomp.bench.ts
Normal file
84
test/autocomp.bench.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe } from 'node:test'
|
||||||
|
import { bench } from 'vitest'
|
||||||
|
import { SernAutocompleteData, SernOptionsData } from '../src'
|
||||||
|
import { createRandomChoice } from './setup/util'
|
||||||
|
import { ApplicationCommandOptionType, AutocompleteFocusedOption, AutocompleteInteraction } from 'discord.js'
|
||||||
|
import { createLookupTable } from '../src/core/functions'
|
||||||
|
import assert from 'node:assert'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses an iterative DFS to check if an autocomplete node exists on the option tree
|
||||||
|
* This is the old internal method that sern used to resolve autocomplete
|
||||||
|
* @param iAutocomplete
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
function treeSearch(
|
||||||
|
choice: AutocompleteFocusedOption,
|
||||||
|
parent: string|undefined,
|
||||||
|
options: SernOptionsData[] | undefined,
|
||||||
|
): SernAutocompleteData & { parent?: string } | undefined {
|
||||||
|
if (options === undefined) return undefined;
|
||||||
|
//clone to prevent mutation of original command module
|
||||||
|
const _options = options.map(a => ({ ...a }));
|
||||||
|
const subcommands = new Set();
|
||||||
|
while (_options.length > 0) {
|
||||||
|
const cur = _options.pop()!;
|
||||||
|
switch (cur.type) {
|
||||||
|
case ApplicationCommandOptionType.Subcommand: {
|
||||||
|
subcommands.add(cur.name);
|
||||||
|
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) {
|
||||||
|
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
|
||||||
|
if (subcommands.size > 0) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: SernOptionsData[] = [
|
||||||
|
createRandomChoice(),
|
||||||
|
createRandomChoice(),
|
||||||
|
createRandomChoice(),
|
||||||
|
{
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
name: 'autocomplete',
|
||||||
|
description: 'here',
|
||||||
|
autocomplete: true,
|
||||||
|
command: { onEvent: [], execute: () => {} },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const table = createLookupTable(options)
|
||||||
|
|
||||||
|
|
||||||
|
describe('autocomplete lookup', () => {
|
||||||
|
|
||||||
|
bench('lookup table', () => {
|
||||||
|
table.get('<parent>/autocomplete')
|
||||||
|
}, { time: 500 })
|
||||||
|
|
||||||
|
|
||||||
|
bench('naive treeSearch', () => {
|
||||||
|
treeSearch({ focused: true,
|
||||||
|
name: 'autocomplete',
|
||||||
|
value: 'autocomplete',
|
||||||
|
type: ApplicationCommandOptionType.String }, undefined, options)
|
||||||
|
}, { time: 500 })
|
||||||
|
})
|
||||||
@@ -1,29 +1,17 @@
|
|||||||
//@ts-nocheck
|
//@ts-nocheck
|
||||||
|
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { PluginType, SernOptionsData, controller } from '../../src/index';
|
import { PluginType, SernOptionsData, controller } from '../../src/index';
|
||||||
import { partitionPlugins, treeSearch } from '../../src/core/functions';
|
import { createLookupTable, partitionPlugins, treeSearch } from '../../src/core/functions';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
|
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
|
||||||
|
import { createRandomChoice, createRandomPlugins } from '../setup/util';
|
||||||
|
|
||||||
describe('functions', () => {
|
describe('functions', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
function createRandomPlugins(len: number) {
|
|
||||||
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
|
|
||||||
return Array.from({ length: len }, () => ({
|
|
||||||
type: random(),
|
|
||||||
execute: () => (random() === 1 ? controller.next() : controller.stop()),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
function createRandomChoice() {
|
|
||||||
return {
|
|
||||||
type: faker.number.int({ min: 1, max: 11 }),
|
|
||||||
name: faker.word.noun(),
|
|
||||||
description: faker.word.adjective(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
it('should partition plugins correctly', () => {
|
it('should partition plugins correctly', () => {
|
||||||
const plugins = createRandomPlugins(100);
|
const plugins = createRandomPlugins(100);
|
||||||
const [onEvent, init] = partitionPlugins(plugins);
|
const [onEvent, init] = partitionPlugins(plugins);
|
||||||
@@ -32,308 +20,275 @@ describe('functions', () => {
|
|||||||
for (const el of init) expect(el.type).to.equal(PluginType.Init);
|
for (const el of init) expect(el.type).to.equal(PluginType.Init);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tree search options tree depth 1', () => {
|
describe('autocomplete', ( ) => {
|
||||||
//@ts-expect-error mocking
|
|
||||||
let autocmpInteraction = new AutocompleteInteraction('autocomplete');
|
it('should tree search options tree depth 1', () => {
|
||||||
const options: SernOptionsData[] = [
|
const options: SernOptionsData[] = [
|
||||||
createRandomChoice(),
|
createRandomChoice(),
|
||||||
createRandomChoice(),
|
{
|
||||||
createRandomChoice(),
|
type: ApplicationCommandOptionType.String,
|
||||||
{
|
name: 'autocomplete',
|
||||||
type: ApplicationCommandOptionType.String,
|
description: 'here',
|
||||||
name: 'autocomplete',
|
autocomplete: true,
|
||||||
description: 'here',
|
command: { onEvent: [], execute: vi.fn() },
|
||||||
autocomplete: true,
|
},
|
||||||
command: { onEvent: [], execute: vi.fn() },
|
];
|
||||||
},
|
const table = createLookupTable(options)
|
||||||
];
|
const result = table.get('<parent>/autocomplete')
|
||||||
autocmpInteraction.options.getFocused.mockReturnValue({
|
expect(result == undefined).to.be.false;
|
||||||
name: 'autocomplete',
|
expect(result.name).to.be.eq('autocomplete');
|
||||||
value: faker.string.alpha(),
|
expect(result.command).to.be.not.undefined;
|
||||||
focused: true,
|
}),
|
||||||
});
|
it('should tree search depth 2', () => {
|
||||||
const result = treeSearch(autocmpInteraction, options);
|
const subcommandName = faker.string.alpha();
|
||||||
expect(result == undefined).to.be.false;
|
const options: SernOptionsData[] = [
|
||||||
expect(result.name).to.be.eq('autocomplete');
|
{
|
||||||
expect(result.command).to.be.not.undefined;
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
}),
|
name: subcommandName,
|
||||||
it('should tree search depth 2', () => {
|
description: faker.string.alpha(),
|
||||||
//@ts-expect-error mocking
|
options: [
|
||||||
let autocmpInteraction = new AutocompleteInteraction('nested');
|
createRandomChoice(),
|
||||||
|
createRandomChoice(),
|
||||||
|
createRandomChoice(),
|
||||||
|
{
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
name: 'nested',
|
||||||
|
description: faker.string.alpha(),
|
||||||
|
autocomplete: true,
|
||||||
|
command: {
|
||||||
|
onEvent: [],
|
||||||
|
execute: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const table = createLookupTable(options)
|
||||||
|
const result = table.get(`<parent>/${subcommandName}/nested`)
|
||||||
|
expect(result == undefined).to.be.false;
|
||||||
|
expect(result.name).to.be.eq('nested');
|
||||||
|
expect(result.command).to.be.not.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should tree search depth n > 2', () => {
|
||||||
|
const subgroupName = faker.string.alpha()
|
||||||
const subcommandName = faker.string.alpha();
|
const subcommandName = faker.string.alpha();
|
||||||
const options: SernOptionsData[] = [
|
const options: SernOptionsData[] = [
|
||||||
{
|
{
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
name: subcommandName,
|
name: subgroupName,
|
||||||
description: faker.string.alpha(),
|
description: faker.string.alpha(),
|
||||||
options: [
|
options: [
|
||||||
createRandomChoice(),
|
|
||||||
createRandomChoice(),
|
|
||||||
createRandomChoice(),
|
|
||||||
{
|
{
|
||||||
type: ApplicationCommandOptionType.String,
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
name: 'nested',
|
name: subcommandName,
|
||||||
description: faker.string.alpha(),
|
description: faker.string.alpha(),
|
||||||
autocomplete: true,
|
options: [
|
||||||
command: {
|
createRandomChoice(),
|
||||||
onEvent: [],
|
createRandomChoice(),
|
||||||
execute: () => {},
|
{
|
||||||
},
|
type: ApplicationCommandOptionType.String,
|
||||||
|
name: 'nested',
|
||||||
|
description: faker.string.alpha(),
|
||||||
|
autocomplete: true,
|
||||||
|
command: {
|
||||||
|
onEvent: [],
|
||||||
|
execute: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createRandomChoice(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
|
const table = createLookupTable(options)
|
||||||
autocmpInteraction.options.getFocused.mockReturnValue({
|
const result = table.get(`<parent>/${subgroupName}/${subcommandName}/nested`)
|
||||||
name: 'nested',
|
|
||||||
value: faker.string.alpha(),
|
|
||||||
focused: true,
|
|
||||||
});
|
|
||||||
const result = treeSearch(autocmpInteraction, options);
|
|
||||||
expect(result == undefined).to.be.false;
|
expect(result == undefined).to.be.false;
|
||||||
expect(result.name).to.be.eq('nested');
|
expect(result.name).to.be.eq('nested');
|
||||||
expect(result.command).to.be.not.undefined;
|
expect(result.command).to.be.not.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tree search depth n > 2', () => {
|
it('should correctly resolve suboption of the same name given two subcommands ', () => {
|
||||||
//@ts-expect-error mocking
|
const subcommandName = faker.string.alpha();
|
||||||
let autocmpInteraction = new AutocompleteInteraction('nested');
|
const groupname = faker.string.alpha()
|
||||||
const subcommandName = faker.string.alpha();
|
const options: SernOptionsData[] = [
|
||||||
const options: SernOptionsData[] = [
|
{
|
||||||
{
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
name: groupname,
|
||||||
name: faker.string.alpha(),
|
description: faker.string.alpha(),
|
||||||
description: faker.string.alpha(),
|
options: [
|
||||||
options: [
|
{
|
||||||
{
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
name: subcommandName,
|
||||||
name: subcommandName,
|
description: faker.string.alpha(),
|
||||||
description: faker.string.alpha(),
|
options: [
|
||||||
options: [
|
createRandomChoice(),
|
||||||
createRandomChoice(),
|
createRandomChoice(),
|
||||||
createRandomChoice(),
|
{
|
||||||
{
|
type: ApplicationCommandOptionType.String,
|
||||||
type: ApplicationCommandOptionType.String,
|
name: 'nested',
|
||||||
name: 'nested',
|
description: faker.string.alpha(),
|
||||||
description: faker.string.alpha(),
|
autocomplete: true,
|
||||||
autocomplete: true,
|
command: {
|
||||||
command: {
|
onEvent: [],
|
||||||
onEvent: [],
|
execute: () => {},
|
||||||
execute: () => {},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
createRandomChoice(),
|
},
|
||||||
],
|
{
|
||||||
},
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
],
|
name: subcommandName + 'a',
|
||||||
},
|
description: faker.string.alpha(),
|
||||||
];
|
options: [
|
||||||
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
|
createRandomChoice(),
|
||||||
autocmpInteraction.options.getFocused.mockReturnValue({
|
{
|
||||||
name: 'nested',
|
type: ApplicationCommandOptionType.String,
|
||||||
value: faker.string.alpha(),
|
name: 'nested',
|
||||||
focused: true,
|
description: faker.string.alpha(),
|
||||||
|
autocomplete: true,
|
||||||
|
command: {
|
||||||
|
onEvent: [],
|
||||||
|
execute: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const table = createLookupTable(options)
|
||||||
|
const result = table.get(`<parent>/${groupname}/${subcommandName}/nested`);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result.name).to.be.eq('nested');
|
||||||
|
expect(result.command).to.be.not.undefined;
|
||||||
});
|
});
|
||||||
const result = treeSearch(autocmpInteraction, options);
|
it('two subcommands with an option of the same name', () => {
|
||||||
expect(result == undefined).to.be.false;
|
const groupName = faker.string.alpha()
|
||||||
expect(result.name).to.be.eq('nested');
|
const subcommandName = faker.string.alpha();
|
||||||
expect(result.command).to.be.not.undefined;
|
const options: SernOptionsData[] = [
|
||||||
});
|
{
|
||||||
it('should correctly resolve suboption of the same name given two subcommands ', () => {
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
let autocmpInteraction = new AutocompleteInteraction('nested');
|
name: groupName,
|
||||||
const subcommandName = faker.string.alpha();
|
description: faker.string.alpha(),
|
||||||
const options: SernOptionsData[] = [
|
options: [
|
||||||
{
|
{
|
||||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
name: faker.string.alpha(),
|
name: subcommandName,
|
||||||
description: faker.string.alpha(),
|
description: faker.string.alpha(),
|
||||||
options: [
|
options: [
|
||||||
{
|
createRandomChoice(),
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
createRandomChoice(),
|
||||||
name: subcommandName,
|
{
|
||||||
description: faker.string.alpha(),
|
type: ApplicationCommandOptionType.String,
|
||||||
options: [
|
name: 'nested',
|
||||||
createRandomChoice(),
|
description: faker.string.alpha(),
|
||||||
createRandomChoice(),
|
autocomplete: true,
|
||||||
{
|
command: {
|
||||||
type: ApplicationCommandOptionType.String,
|
onEvent: [],
|
||||||
name: 'nested',
|
execute: () => {},
|
||||||
description: faker.string.alpha(),
|
},
|
||||||
autocomplete: true,
|
|
||||||
command: {
|
|
||||||
onEvent: [],
|
|
||||||
execute: () => {},
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
name: subcommandName + 'anothera',
|
||||||
name: subcommandName + 'a',
|
description: faker.string.alpha(),
|
||||||
description: faker.string.alpha(),
|
options: [
|
||||||
options: [
|
createRandomChoice(),
|
||||||
createRandomChoice(),
|
{
|
||||||
{
|
type: ApplicationCommandOptionType.String,
|
||||||
type: ApplicationCommandOptionType.String,
|
name: 'nested',
|
||||||
name: 'nested',
|
description: faker.string.alpha(),
|
||||||
description: faker.string.alpha(),
|
autocomplete: true,
|
||||||
autocomplete: true,
|
command: {
|
||||||
command: {
|
onEvent: [],
|
||||||
onEvent: [],
|
execute: () => {},
|
||||||
execute: () => {},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
|
const table = createLookupTable(options)
|
||||||
autocmpInteraction.options.getFocused.mockReturnValue({
|
const result = table.get(`<parent>/${groupName}/${subcommandName}/nested`);
|
||||||
name: 'nested',
|
expect(result).toBeTruthy();
|
||||||
value: faker.string.alpha(),
|
expect(result.name).to.be.eq('nested');
|
||||||
focused: true,
|
expect(result.command).to.be.not.undefined;
|
||||||
});
|
|
||||||
const result = treeSearch(autocmpInteraction, options);
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result.name).to.be.eq('nested');
|
|
||||||
expect(result.command).to.be.not.undefined;
|
|
||||||
});
|
|
||||||
it('two subcommands with an option of the same name', () => {
|
|
||||||
let autocmpInteraction = new AutocompleteInteraction('nested');
|
|
||||||
const subcommandName = faker.string.alpha();
|
|
||||||
const options: SernOptionsData[] = [
|
|
||||||
{
|
|
||||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
|
||||||
name: faker.string.alpha(),
|
|
||||||
description: faker.string.alpha(),
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
|
||||||
name: subcommandName,
|
|
||||||
description: faker.string.alpha(),
|
|
||||||
options: [
|
|
||||||
createRandomChoice(),
|
|
||||||
createRandomChoice(),
|
|
||||||
{
|
|
||||||
type: ApplicationCommandOptionType.String,
|
|
||||||
name: 'nested',
|
|
||||||
description: faker.string.alpha(),
|
|
||||||
autocomplete: true,
|
|
||||||
command: {
|
|
||||||
onEvent: [],
|
|
||||||
execute: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
|
||||||
name: subcommandName + 'a',
|
|
||||||
description: faker.string.alpha(),
|
|
||||||
options: [
|
|
||||||
createRandomChoice(),
|
|
||||||
{
|
|
||||||
type: ApplicationCommandOptionType.String,
|
|
||||||
name: 'nested',
|
|
||||||
description: faker.string.alpha(),
|
|
||||||
autocomplete: true,
|
|
||||||
command: {
|
|
||||||
onEvent: [],
|
|
||||||
execute: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
autocmpInteraction.options.getSubcommand.mockReturnValue(subcommandName);
|
|
||||||
autocmpInteraction.options.getFocused.mockReturnValue({
|
|
||||||
name: 'nested',
|
|
||||||
value: faker.string.alpha(),
|
|
||||||
focused: true,
|
|
||||||
});
|
|
||||||
const result = treeSearch(autocmpInteraction, options);
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result.name).to.be.eq('nested');
|
|
||||||
expect(result.command).to.be.not.undefined;
|
|
||||||
|
|
||||||
let autocmpInteraction2 = new AutocompleteInteraction('nested');
|
|
||||||
autocmpInteraction2.options.getSubcommand.mockReturnValue(subcommandName + 'a');
|
|
||||||
autocmpInteraction2.options.getFocused.mockReturnValue({
|
|
||||||
name: 'nested',
|
|
||||||
value: faker.string.alpha(),
|
|
||||||
focused: true,
|
|
||||||
});
|
});
|
||||||
const result2 = treeSearch(autocmpInteraction2, options);
|
|
||||||
expect(result2).toBeTruthy();
|
|
||||||
expect(result2?.name).toEqual('nested');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('simulates autocomplete typing and resolution', () => {
|
it('simulates autocomplete typing and resolution', () => {
|
||||||
const subcommandName = faker.string.alpha();
|
const subcommandGroupName = faker.string.alpha()
|
||||||
const optionName = faker.word.noun();
|
const subcommandName = faker.string.alpha();
|
||||||
const options: SernOptionsData[] = [
|
const optionName = faker.word.noun();
|
||||||
{
|
const options: SernOptionsData[] = [
|
||||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
{
|
||||||
name: faker.string.alpha(),
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
description: faker.string.alpha(),
|
name: subcommandGroupName,
|
||||||
options: [
|
description: faker.string.alpha(),
|
||||||
{
|
options: [
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
{
|
||||||
name: subcommandName,
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
description: faker.string.alpha(),
|
name: subcommandName,
|
||||||
options: [
|
description: faker.string.alpha(),
|
||||||
createRandomChoice(),
|
options: [
|
||||||
createRandomChoice(),
|
createRandomChoice(),
|
||||||
{
|
createRandomChoice(),
|
||||||
type: ApplicationCommandOptionType.String,
|
{
|
||||||
name: optionName,
|
type: ApplicationCommandOptionType.String,
|
||||||
description: faker.string.alpha(),
|
name: optionName,
|
||||||
autocomplete: true,
|
description: faker.string.alpha(),
|
||||||
command: {
|
autocomplete: true,
|
||||||
onEvent: [],
|
command: {
|
||||||
execute: vi.fn(),
|
onEvent: [],
|
||||||
|
execute: vi.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
name: subcommandName + 'a',
|
||||||
name: subcommandName + 'a',
|
description: faker.string.alpha(),
|
||||||
description: faker.string.alpha(),
|
options: [
|
||||||
options: [
|
createRandomChoice(),
|
||||||
createRandomChoice(),
|
{
|
||||||
{
|
type: ApplicationCommandOptionType.String,
|
||||||
type: ApplicationCommandOptionType.String,
|
name: optionName,
|
||||||
name: optionName,
|
description: faker.string.alpha(),
|
||||||
description: faker.string.alpha(),
|
autocomplete: true,
|
||||||
autocomplete: true,
|
command: {
|
||||||
command: {
|
onEvent: [],
|
||||||
onEvent: [],
|
execute: vi.fn(),
|
||||||
execute: vi.fn(),
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
];
|
||||||
];
|
let accumulator = '';
|
||||||
let accumulator = '';
|
let result: unknown;
|
||||||
let result: unknown;
|
const table = createLookupTable(options)
|
||||||
for (const char of optionName) {
|
for (const char of optionName) {
|
||||||
accumulator += char;
|
accumulator += char;
|
||||||
|
|
||||||
|
const focusedValue = {
|
||||||
|
name: accumulator,
|
||||||
|
value: faker.string.alpha(),
|
||||||
|
focused: true,
|
||||||
|
};
|
||||||
|
result = table.get(`<parent>/${subcommandGroupName}/${subcommandName}/${focusedValue.name}` );
|
||||||
|
}
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const autocomplete = new AutocompleteInteraction(accumulator);
|
|
||||||
autocomplete.options.getSubcommand.mockReturnValue(subcommandName);
|
|
||||||
autocomplete.options.getFocused.mockReturnValue({
|
|
||||||
name: accumulator,
|
|
||||||
value: faker.string.alpha(),
|
|
||||||
focused: true,
|
|
||||||
});
|
|
||||||
result = treeSearch(autocomplete, options);
|
|
||||||
}
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,3 +28,21 @@ export function createRandomModule(plugins: any[]): Processed<Module> {
|
|||||||
execute: vi.fn(),
|
execute: vi.fn(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function createRandomChoice() {
|
||||||
|
return {
|
||||||
|
type: faker.number.int({ min: 1, max: 11 }),
|
||||||
|
name: faker.word.noun(),
|
||||||
|
description: faker.word.adjective(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function createRandomPlugins(len: number) {
|
||||||
|
const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum
|
||||||
|
return Array.from({ length: len }, () => ({
|
||||||
|
type: random(),
|
||||||
|
execute: () => (random() === 1 ? controller.next() : controller.stop()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user