From d2d507104edf6ed17e4a9edb6aebcad2e50ebd79 Mon Sep 17 00:00:00 2001 From: Hotaru Date: Mon, 29 Jul 2024 16:46:06 +0800 Subject: [PATCH] feat: message elements encode and decode --- TODO.md | 1 + modules/access/src/index.ts | 7 +- packages/core/src/app/message.ts | 7 - packages/core/src/components/adapter.ts | 63 ++++---- packages/core/src/components/elements.ts | 84 ++++++----- packages/core/src/components/messages.ts | 178 ++++++++++++++++++++++- packages/core/src/components/session.ts | 14 +- packages/core/src/global/constants.ts | 6 - packages/core/src/global/symbols.ts | 2 + packages/core/src/index.ts | 1 - packages/core/src/types/message.ts | 19 ++- packages/core/src/utils/container.ts | 18 --- packages/kotori/src/cli.ts | 10 +- packages/kotori/src/index.ts | 9 +- packages/kotori/src/utils/container.ts | 17 +++ packages/kotori/src/utils/env.ts | 3 +- packages/loader/src/class/loader.ts | 32 ++-- packages/loader/src/class/runner.ts | 15 +- packages/loader/src/constants.ts | 6 + packages/loader/src/decorators/index.ts | 33 +++-- packages/loader/src/decorators/utils.ts | 168 +++++++++++---------- packages/loader/src/service/database.ts | 14 +- packages/loader/src/service/server.ts | 44 +++--- pnpm-workspace.yaml | 4 +- 24 files changed, 477 insertions(+), 278 deletions(-) delete mode 100644 packages/core/src/utils/container.ts create mode 100644 packages/kotori/src/utils/container.ts diff --git a/TODO.md b/TODO.md index 4cee599c..127643bf 100644 --- a/TODO.md +++ b/TODO.md @@ -45,3 +45,4 @@ - [x] Kotori cli start - [x] kotori-plugin-request: onGroupMsg and onPrivateMsg - [ ] webui load tips twice +- [ ] symbols props inject and reality context diff --git a/modules/access/src/index.ts b/modules/access/src/index.ts index 8b09bd60..7eccae85 100644 --- a/modules/access/src/index.ts +++ b/modules/access/src/index.ts @@ -1,4 +1,4 @@ -import { UserAccess, Context, MessageScope } from 'kotori-bot' +import { UserAccess, type Context, MessageScope, EventsList } from 'kotori-bot' export const lang = [__dirname, '../locales'] @@ -10,13 +10,12 @@ export function main(ctx: Context) { const load = (bot: string) => ctx.file.load(`${bot}.json`, 'json', {}) const save = (bot: string, data: Data) => ctx.file.save(`${bot}.json`, data) - ctx.on('before_parse', (data) => { - const d = data + ctx.on('before_parse', (d) => { if (d.session.type !== MessageScope.GROUP || !d.session.groupId) return const list = load(d.session.api.adapter.identity) if (!(String(d.session.groupId) in list)) return if (!list[String(d.session.groupId)].includes(String(d.session.userId))) return - d.session.sender.role = 'admin' + if (d.session.type === MessageScope.GROUP) d.session.sender.role = 'admin' }) ctx diff --git a/packages/core/src/app/message.ts b/packages/core/src/app/message.ts index cd2e735b..71fe0d5a 100644 --- a/packages/core/src/app/message.ts +++ b/packages/core/src/app/message.ts @@ -143,13 +143,6 @@ export class Message { }) } - /** - * ss - * - * @param callback - * @param priority - * @returns - */ public midware(callback: MidwareCallback, priority = 100) { setMidwareMeta(callback, { identity: this.ctx.identity, priority }) const data = { callback, priority } diff --git a/packages/core/src/components/adapter.ts b/packages/core/src/components/adapter.ts index 81c8ebb4..de78935e 100644 --- a/packages/core/src/components/adapter.ts +++ b/packages/core/src/components/adapter.ts @@ -106,41 +106,6 @@ export interface Adapter(api: T, ctx: Context): T { - // TODO: modify them - api.sendPrivateMsg = new Proxy(api.sendPrivateMsg, { - apply(_, __, argArray) { - const [message, targetId] = argArray - const cancel = cancelFactory() - ctx.emit('before_send', { - api, - message, - messageType: MessageScope.PRIVATE, - targetId, - cancel: cancel.get() - }) - if (cancel.value) return - Reflect.apply(message, targetId, argArray[2]) - } - }) - api.sendGroupMsg = new Proxy(api.sendGroupMsg, { - apply(_, __, argArray) { - const [message, targetId] = argArray - const cancel = cancelFactory() - ctx.emit('before_send', { - api, - message, - messageType: MessageScope.PRIVATE, - targetId, - cancel: cancel.get() - }) - if (cancel.value) return - Reflect.apply(message, targetId, argArray[2]) - } - }) - return api -} - abstract class AdapterOrigin< A extends Api = Api, C extends AdapterConfig = AdapterConfig, @@ -207,7 +172,33 @@ abstract class AdapterOrigin< export const Adapter = new Proxy(AdapterOrigin, { construct: (target, args, newTarget) => { const bot = Reflect.construct(target, args, newTarget) - bot.api = setProxy(bot.api, bot.ctx) + bot.api = new Proxy(bot.api, { + get: (target, prop, receiver) => { + const value = Reflect.get(target, prop, receiver) + if (typeof prop !== 'string' || !['sendPrivateMsg', 'sendGroupMsg', 'sendChannelMsg'].includes(prop)) { + return value + } + return new Proxy(value as () => void, { + apply(target, thisArg, argArray) { + const [message, id1, id2] = argArray + const cancel = cancelFactory() + bot.ctx.emit('before_send', { + api: bot.api, + message, + cancel: cancel.get(), + target: + prop === 'sendPrivateMsg' + ? { type: MessageScope.PRIVATE, userId: id1 } + : prop === 'sendGroupMsg' + ? { type: MessageScope.GROUP, groupId: id1 } + : { type: MessageScope.CHANNEL, guildId: id1, channelId: id2 } + }) + if (cancel.value) return + Reflect.apply(target, thisArg, argArray) + } + }) + } + }) return bot } }) diff --git a/packages/core/src/components/elements.ts b/packages/core/src/components/elements.ts index 1bfd508c..e36a1222 100644 --- a/packages/core/src/components/elements.ts +++ b/packages/core/src/components/elements.ts @@ -1,49 +1,61 @@ -import { none } from '@kotori-bot/tools' import type { Adapter } from './adapter' import type { Api } from './api' -import type { AdapterConfig } from '../types' +import type { AdapterConfig, Message, MessageMapping } from '../types' +import { none } from '@kotori-bot/tools' +/** + * Elements handler base class. + * + * @abstract + */ export abstract class Elements { + /** + * Adapter instance. + * + * @readonly + */ public readonly adapter: Adapter + /** + * Create a elements handler. + * + * @param adapter - Adapter instance + */ public constructor(adapter: Adapter) { this.adapter = adapter as Adapter } - private default(...args: unknown[]) { - none(this, args) - return '' - } - - public at(target: string, ...extra: unknown[]) { - return this.default(target, extra) - } - - public image(url: string, ...extra: unknown[]) { - return this.default(url, extra) - } - - public voice(url: string, ...extra: unknown[]) { - return this.default(url, extra) - } - - public video(url: string, ...extra: unknown[]) { - return this.default(url, extra) - } - - public face(id: number | string, ...extra: unknown[]) { - return this.default(id, extra) - } - - public file(data: unknown, ...extra: unknown[]) { - return this.default(data, extra) - } - - public getSupports() { - return (['at', 'image', 'voice', 'video', 'face', 'file'] as const).filter( - (key) => this[key] && this[key] !== Object.getPrototypeOf(Object.getPrototypeOf(key))[key] - ) - } + /** + * Encode raw message string to `Message`. + * + * @param raw - Raw message + * @param meta - Message metadata + * @returns Encoded message + */ + public encode(raw: string, meta: object = {}): Message { + none(meta) + return raw + } + + /** + * Decode `Message` elements to string. + * + * @param message - Message to decode + * @returns Decoded message + */ + // biome-ignore lint: + public decode(message: Message): any { + return message.toString() + } + + /** + * Get supported elements. + * + * @returns Supported elements + * + * @abstract + */ + public abstract getSupportsElements(): (keyof MessageMapping)[] } export default Elements diff --git a/packages/core/src/components/messages.ts b/packages/core/src/components/messages.ts index 1413d59f..7360977f 100644 --- a/packages/core/src/components/messages.ts +++ b/packages/core/src/components/messages.ts @@ -1,12 +1,33 @@ import type { Message, MessageMapping } from '../types' +/** + * Single message class. + * + * @template T - Message type + */ export class MessageSingle extends String { private data: MessageMapping[T] - public type: T + /** + * Message type. + * + * @readonly + */ + public readonly type: T - public length: number + /** + * Message text length. + * + * @readonly + */ + public readonly length: number + /** + * Create a single message + * + * @param type - Message type + * @param data - Message data + */ public constructor(type: T, data: MessageMapping[T]) { super(Messages.stringify(type, data, false)) this.type = type @@ -14,18 +35,39 @@ export class MessageSingle extends String { this.length = this.isText() ? this.toString().length : 0 } + /** + * Convert message to string. + * + * @param isStrict - Whether to strictly convert message, strict convert will ignore the elements else text + * @returns Message string + */ public toString(isStrict = true) { return Messages.stringify(this.type, this.data, isStrict) } + /** + * Check whether the message is text. + * + * @returns Whether the message is text + */ public isText() { return this.type === 'text' } } class MessageListOrigin extends Array> { - public length: number + /** + * Message list length. + * + * @readonly + */ + public readonly length: number + /** + * Create a message list. + * + * @param messages - Message list + */ public constructor(...messages: Message[]) { const handleMessage: MessageSingle[] = [] for (const value of messages) { @@ -37,33 +79,74 @@ class MessageListOrigin extends Array value.toString()) + .map((value) => value.toString(isStrict)) .join('') } + /** + * Check whether the message list is pure. + * + * @param keys - Message type list + * + * @returns Whether the message list is pure + */ public isPure(...keys: T[]) { return this.every((value) => (keys as string[]).includes(value.type)) } + /** + * Check whether the message list is text. + * + * @returns Whether the message list is text + */ public isText(): boolean { return this.isPure('text') } + /** + * Fetch all text from message list. + * + * @returns Text string + */ public fetchText() { return this.map((value) => (value.isText() ? value.toString() : ' ')).join('') } + /** + * Pick message list. + * + * @param keys - Message type list + * @returns Message list + */ public pick(...keys: T[]): MessageList { return Messages(...this.filter((value) => (keys as string[]).includes(value.type))) as unknown as MessageList } + /** + * Omit message list. + * + * @param keys - Message type list + * @returns Message list + */ public omit(...keys: T[]): MessageList { return Messages(...this.filter((value) => !(keys as string[]).includes(value.type))) as unknown as MessageList } } +/** + * Message list class. + * + * @template T - Message type + * @class + */ export type MessageList = string & MessageListOrigin export const MessageList = new Proxy(MessageListOrigin, { construct: (target, argArray, newTarget) => @@ -75,55 +158,136 @@ export const MessageList = new Proxy(MessageListOrigin, { return undefined } }) -}) as new ( +}) as unknown as new ( ...args: ConstructorParameters -) => MessageList +) => MessageList +/** + * Create a message list. + * + * @param messages - Message list + * @returns Message list + */ export function Messages(...messages: Message[]) { return new MessageList(...messages) } +/** + * Messages namespace + * + * @namespace + */ export namespace Messages { + /** + * Create a text message. + * + * @param text - Text string + * @returns Text message + */ export function text(text: string) { return new MessageSingle('text', { text }) } + /** + * Create a mention message. + * + * @param userId - User id + * @returns Mention message + */ export function mention(userId: string) { return new MessageSingle('mention', { userId }) } + /** + * Create a mention all message. + * + * @returns Mention all message + */ export function mentionAll() { return new MessageSingle('mentionAll', {}) } + /** + * Create an image message. + * + * @param content - Image content + * @returns Image message + */ export function image(content: string) { return new MessageSingle('image', { content }) } + /** + * Create a voice message. + * + * @param content - Voice content + * @returns Voice message + */ export function voice(content: string) { return new MessageSingle('voice', { content }) } + /** + * Create an audio message. + * + * @param content - Audio content + * @returns Audio message + */ export function audio(content: string) { return new MessageSingle('audio', { content }) } + /** + * Create a video message. + * + * @param content - Video content + * @returns Video message + */ export function video(content: string) { return new MessageSingle('video', { content }) } + /** + * Create a file message. + * + * @param content - File content + * @returns File message + */ export function file(content: string) { return new MessageSingle('file', { content }) } + /** + * Create a location message. + * + * @param latitude - Latitude + * @param longitude - Longitude + * @param title - Title + * @param content - Content + * @returns Location message + */ export function location(latitude: number, longitude: number, title: string, content: string) { return new MessageSingle('location', { latitude, longitude, title, content }) } + /** + * Create a reply message. + * + * @param messageId - Message id + * @returns Reply message + */ export function reply(messageId: string) { return new MessageSingle('reply', { messageId }) } + /** + * Stringify message. + * + * @param type - Message type + * @param data - Message data + * @param isStrict - Whether to strictly convert message, strict convert will ignore the elements else text + * @returns Message string + */ export function stringify(type: T, data: MessageMapping[T], isStrict = true) { if (isStrict) { if (type === 'text') return (data as MessageMapping['text']).text diff --git a/packages/core/src/components/session.ts b/packages/core/src/components/session.ts index 52adb637..d079a75f 100644 --- a/packages/core/src/components/session.ts +++ b/packages/core/src/components/session.ts @@ -16,13 +16,7 @@ import type { I18n } from '@kotori-bot/i18n' import { Symbols } from '../global' import { MessageList, MessageSingle } from './messages' -/** - * Session event. - * - * @class - * @template T - Session event data type - */ -export class SessionOrigin implements EventDataApiBase { +class SessionOrigin implements EventDataApiBase { private isSameSender(session: SessionOrigin) { return ( this.api.adapter.identity === session.api.adapter.identity && @@ -270,6 +264,12 @@ export class SessionOrigin implem */ export type Session = SessionOrigin & T +/** + * Session event. + * + * @class + * @template T - Session event data type + */ export const Session = new Proxy(SessionOrigin, { construct: (target, args, newTarget) => Object.assign(args[0], Reflect.construct(target, args, newTarget)) }) as new ( diff --git a/packages/core/src/global/constants.ts b/packages/core/src/global/constants.ts index f9e7ebab..4a57a9ff 100644 --- a/packages/core/src/global/constants.ts +++ b/packages/core/src/global/constants.ts @@ -1,11 +1,5 @@ import { DEFAULT_LANG } from '@kotori-bot/i18n' -export const PLUGIN_PREFIX = 'kotori-plugin-' - -export const DATABASE_PREFIX = `${PLUGIN_PREFIX}database-` - -export const ADAPTER_PREFIX = `${PLUGIN_PREFIX}adapter-` - export const DEFAULT_CORE_CONFIG = { global: { lang: DEFAULT_LANG, diff --git a/packages/core/src/global/symbols.ts b/packages/core/src/global/symbols.ts index f8759d25..cb682659 100644 --- a/packages/core/src/global/symbols.ts +++ b/packages/core/src/global/symbols.ts @@ -8,6 +8,8 @@ export namespace Symbols { export const filter = Symbol.for('kotori.core.filter') export const promise = Symbol.for('kotori.core.promise') export const modules = Symbol.for('kotori.loader.module') + export const getInstance = Symbol.for('kotori.cli.get_instance') + export const setInstance = Symbol.for('kotori.cli.set_instance') } export default Symbols diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 586e5a0f..fc91bf98 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,7 +11,6 @@ export * from 'fluoro' export * from './app' export * from './components' export * from './utils/error' -export * from './utils/container' export * from './utils/factory' export * from './global' export * from './types' diff --git a/packages/core/src/types/message.ts b/packages/core/src/types/message.ts index 5175cc0b..fcffb8f3 100644 --- a/packages/core/src/types/message.ts +++ b/packages/core/src/types/message.ts @@ -196,10 +196,21 @@ interface EventDataBeforeSend { api: Api /** Message to send */ message: Message - /** Message type */ - messageType: MessageScope - /** Target id */ - targetId: string + /** Target user or group or channel */ + target: + | { + type: MessageScope.PRIVATE + userId: string + } + | { + type: MessageScope.GROUP + groupId: string + } + | { + type: MessageScope.CHANNEL + channelId: string + guildId: string + } /** Cancel the message sending */ cancel(): void } diff --git a/packages/core/src/utils/container.ts b/packages/core/src/utils/container.ts deleted file mode 100644 index 7ca26c6a..00000000 --- a/packages/core/src/utils/container.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Context } from 'fluoro' -import { KotoriError } from './error' - -export namespace Container { - let instance: Context - - export function setInstance(ctx: Context) { - if (instance) throw new KotoriError('Default context instance is already set') - instance = ctx - } - - export function getInstance() { - if (!instance) throw new KotoriError('Default context instance is not set') - return instance - } -} - -export default Container diff --git a/packages/kotori/src/cli.ts b/packages/kotori/src/cli.ts index d550bbaa..a9ae282e 100644 --- a/packages/kotori/src/cli.ts +++ b/packages/kotori/src/cli.ts @@ -2,8 +2,9 @@ import cac from 'cac' import { BUILD_MODE, DEV_MODE, Loader, Logger } from '@kotori-bot/loader' import { resolve } from 'node:path' import env from './utils/env' -import { type Context, executeCommand, supportTs } from '@kotori-bot/core' +import { executeCommand, supportTs, Symbols } from '@kotori-bot/core' import daemon from './daemon' +import { Container } from './utils/container' const program = cac() @@ -56,9 +57,10 @@ program } if (options.daemon) return daemon(virtualEnv) - const kotori = new Loader(loaderOptions) - ;(kotori as unknown as { ctx: Context }).ctx.meta.version = require(resolve(__dirname, '../package.json')).version - kotori.run() + const loader = new Loader(loaderOptions) + loader.ctx.meta.version = require(resolve(__dirname, '../package.json')).version + Container[Symbols.setInstance](loader.ctx) + loader.run() }) program diff --git a/packages/kotori/src/index.ts b/packages/kotori/src/index.ts index 4e89fd1b..5c1736a1 100644 --- a/packages/kotori/src/index.ts +++ b/packages/kotori/src/index.ts @@ -1,4 +1,5 @@ -import { Container, type Context } from '@kotori-bot/core' +import { Symbols, type Context } from '@kotori-bot/core' +import { Container } from './utils/container' export * from '@kotori-bot/core' export * from '@kotori-bot/loader' @@ -6,11 +7,7 @@ export * from '@kotori-bot/loader' export const Kotori = new Proxy( {}, { - get: (_, prop) => { - const target = Container.getInstance() - if (prop === undefined) return target - return target[prop as keyof Context] - } + get: (_, prop) => Container[Symbols.getInstance]()[prop as keyof Context] } ) as Context diff --git a/packages/kotori/src/utils/container.ts b/packages/kotori/src/utils/container.ts new file mode 100644 index 00000000..66e2f0a1 --- /dev/null +++ b/packages/kotori/src/utils/container.ts @@ -0,0 +1,17 @@ +import { type Context, KotoriError, Symbols } from '@kotori-bot/core' + +export class Container { + private constructor() {} + + private static instance: Context + + public static [Symbols.setInstance](ctx: Context) { + if (Container.instance) throw new KotoriError('Default context instance is already set') + Container.instance = ctx + } + + public static [Symbols.getInstance]() { + if (!Container.instance) throw new KotoriError('Default context instance is not set') + return Container.instance + } +} diff --git a/packages/kotori/src/utils/env.ts b/packages/kotori/src/utils/env.ts index 689da3c3..5dc2de75 100644 --- a/packages/kotori/src/utils/env.ts +++ b/packages/kotori/src/utils/env.ts @@ -9,5 +9,6 @@ export default { config: process.env.CONFIG, port: process.env.PORT ? Number(process.env.PORT) : undefined, level: process.env.LEVEL ? Number(process.env.LEVEL) : undefined, - noColor: process.env.NO_COLOR === 'on' + noColor: process.env.NO_COLOR === 'on', + isDaemon: !!process.env.IS_DAEMON } diff --git a/packages/loader/src/class/loader.ts b/packages/loader/src/class/loader.ts index a1457c19..1a226f8d 100644 --- a/packages/loader/src/class/loader.ts +++ b/packages/loader/src/class/loader.ts @@ -3,11 +3,10 @@ * @Blog: https://hotaru.icu * @Date: 2023-06-24 15:12:55 * @LastEditors: Hotaru biyuehuya@gmail.com - * @LastEditTime: 2024-07-25 21:18:32 + * @LastEditTime: 2024-07-29 15:47:36 */ import { KotoriError, - Container, Tsu, type AdapterConfig, type CoreConfig, @@ -66,6 +65,7 @@ declare module '@kotori-bot/core' { interface LoaderOptions { mode: typeof DEV_MODE | typeof BUILD_MODE + isDaemon?: boolean dir?: string config?: string port?: number @@ -173,23 +173,25 @@ function getConfig(baseDir: Runner['baseDir'], loaderOptions?: LoaderOptions) { } export class Loader { - private readonly ctx: Context - private loadCount = 0 + public readonly ctx: Context + public constructor(loaderOptions?: LoaderOptions) { const baseDir = getBaseDir(loaderOptions?.config || CONFIG_NAME, loaderOptions?.dir) - const options = { mode: loaderOptions?.mode || BUILD_MODE } - - const ctx = new Core(getConfig(baseDir, options)) - ctx.root.meta.loaderVersion = require(path.resolve(__dirname, '../../package.json')).version - ctx.provide('runner', new Runner(ctx, { baseDir, options })) - ctx.mixin('runner', ['baseDir', 'options']) - ctx.provide('loader-tools', { format: formatFactory(ctx.i18n), locale: ctx.i18n.locale.bind(ctx.i18n) }) - ctx.mixin('loader-tools', ['locale', 'format']) - ctx.i18n.use(path.resolve(__dirname, '../../locales')) - Container.setInstance(ctx) - this.ctx = Container.getInstance() + const options = { mode: loaderOptions?.mode || BUILD_MODE, isDaemon: !!loaderOptions?.isDaemon } + + this.ctx = new Core(getConfig(baseDir, options)) + this.ctx.root.meta.loaderVersion = require(path.resolve(__dirname, '../../package.json')).version + this.ctx.provide('runner', new Runner(this.ctx, { baseDir, options })) + this.ctx.mixin('runner', ['baseDir', 'options']) + this.ctx.provide('loader-tools', { + format: formatFactory(this.ctx.i18n), + locale: this.ctx.i18n.locale.bind(this.ctx.i18n) + }) + this.ctx.mixin('loader-tools', ['locale', 'format']) + this.ctx.i18n.use(path.resolve(__dirname, '../../locales')) + this.ctx.logger.trace('loaderOptions:', options) this.ctx.logger.trace('baseDir:', this.ctx.baseDir) this.ctx.logger.trace('options:', this.ctx.options) diff --git a/packages/loader/src/class/runner.ts b/packages/loader/src/class/runner.ts index fe8f5d2d..ea751e59 100644 --- a/packages/loader/src/class/runner.ts +++ b/packages/loader/src/class/runner.ts @@ -1,15 +1,12 @@ import fs from 'node:fs' import path from 'node:path' import { - ADAPTER_PREFIX, Adapter, type Context, - DATABASE_PREFIX, DevError, type LocaleType, type ModuleConfig, ModuleError, - PLUGIN_PREFIX, Parser, Service, Symbols, @@ -17,7 +14,15 @@ import { TsuError } from '@kotori-bot/core' import { ConsoleTransport, FileTransport, LoggerLevel } from '@kotori-bot/logger' -import { type BUILD_MODE, CORE_MODULES, DEV_MODE } from '../constants' +import { + type BUILD_MODE, + CORE_MODULES, + DEV_MODE, + INTERNAL_PACKAGES, + DATABASE_PREFIX, + ADAPTER_PREFIX, + PLUGIN_PREFIX +} from '../constants' import '../types/internal' import KotoriLogger from '../utils/logger' import './loader' @@ -31,6 +36,7 @@ interface BaseDir { interface Options { mode: typeof BUILD_MODE | typeof DEV_MODE + isDaemon: boolean } interface RunnerConfig { @@ -169,6 +175,7 @@ export class Runner { try { pkg = modulePackageSchema.parse(JSON.parse(fs.readFileSync(pkgPath).toString())) + if (INTERNAL_PACKAGES.includes(pkg.name)) return } catch (e) { if (e instanceof TsuError) throw new DevError(this.ctx.format('error.dev.package.missing', [pkgPath, e.message])) throw new DevError(this.ctx.format('error.dev.package.illegal', [pkgPath])) diff --git a/packages/loader/src/constants.ts b/packages/loader/src/constants.ts index 8d20ee14..0fa948b8 100644 --- a/packages/loader/src/constants.ts +++ b/packages/loader/src/constants.ts @@ -1,5 +1,11 @@ import { LoggerLevel } from '@kotori-bot/logger' +export const PLUGIN_PREFIX = 'kotori-plugin-' + +export const DATABASE_PREFIX = `${PLUGIN_PREFIX}database-` + +export const ADAPTER_PREFIX = `${PLUGIN_PREFIX}adapter-` + export const CONFIG_NAME = 'kotori.toml' export const SUPPORTS_VERSION = /(1\.1\.0)|(1\.2\.0)|(1\.(3|4|5|6|7|8|9)\.(.*))/ diff --git a/packages/loader/src/decorators/index.ts b/packages/loader/src/decorators/index.ts index 665c96a5..9571054c 100644 --- a/packages/loader/src/decorators/index.ts +++ b/packages/loader/src/decorators/index.ts @@ -1,21 +1,22 @@ -import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { Container } from '@kotori-bot/core'; -import Decorators from './utils'; +// import { readFileSync } from 'node:fs' +// import { resolve } from 'node:path' +import { none } from '@kotori-bot/core' +import Decorators from './utils' -export * from './plugin'; +export * from './plugin' export function plugins(plugin: string | string[] | { name: string }) { - let pkgName: string; - if (!Array.isArray(plugin) && typeof plugin === 'object') { - pkgName = plugin.name; - } else { - pkgName = JSON.parse( - readFileSync(resolve(...(Array.isArray(plugin) ? plugin : [plugin]), 'package.json')).toString() - ).name; - } - const ctx = Container.getInstance().extends(undefined, pkgName); - return new Decorators(ctx); + // let pkgName: string; + // if (!Array.isArray(plugin) && typeof plugin === 'object') { + // pkgName = plugin.name + // } else { + // pkgName = JSON.parse( + // readFileSync(resolve(...(Array.isArray(plugin) ? plugin : [plugin]), 'package.json')).toString() + // ).name + // } + // const ctx = Container.getInstance().extends(undefined, pkgName); + none(plugin) + return new Decorators() } -export default plugins; +export default plugins diff --git a/packages/loader/src/decorators/utils.ts b/packages/loader/src/decorators/utils.ts index 122e7b75..b159b01f 100644 --- a/packages/loader/src/decorators/utils.ts +++ b/packages/loader/src/decorators/utils.ts @@ -1,104 +1,111 @@ -import { resolve } from 'node:path' +// import { resolve } from 'node:path' import { - Symbols, + // Symbols, type Context, type EventsList, - Service, + // Service, type UserAccess, type MessageScope, - type Parser, - type ModuleConfig, - ModuleError, - Tokens + none + // type Parser, + // type ModuleConfig, + // ModuleError, + // Tokens } from '@kotori-bot/core' import '../types/internal' -type Fn = (...args: unknown[]) => void +// type Fn = (...args: unknown[]) => void export class Decorators { - private readonly ctx: Context + // public readonly ctx: object - private isCreated = false + // private isCreated = false - private object?: object + // private object?: object // biome-ignore lint: - private register(callback: (...args: any[]) => void) { - // biome-ignore lint: - return (...args: any[]) => - this.ctx.root.once('ready_module_decorators', (pkgName) => { - if (pkgName === this.ctx.identity) { - callback(...args) - return - } - this.register(callback) - }) - } - - public constructor(ctx: Context) { - this.ctx = ctx - if (!this.ctx.root.get('decorators')) this.ctx.root.provide('decorators', { registers: [] }) - if (this.ctx.identity) this.ctx.root.get<{ registers: string[] }>('decorators').registers.push(this.ctx.identity) - } + // private register(callback: (...args: any[]) => void) { + // // biome-ignore lint: + // return (...args: any[]) => + // this.ctx.root.once('ready_module_decorators', (pkgName) => { + // if (pkgName === this.ctx.identity) { + // callback(...args) + // return + // } + // this.register(callback) + // }) + // } + + // public constructor(/* ctx: Context */) { + // // this.ctx = ctx + // // if (!this.ctx.root.get('decorators')) this.ctx.root.provide('decorators', { registers: [] }) + // // if (this.ctx.identity) this.ctx.root.get<{ registers: string[] }>('decorators').registers.push(this.ctx.identity) + // } public readonly import = (Target: object) => { - this.register(() => { - if (!this.isCreated) this.schema(Target, undefined as keyof object) - })() + none(Target) + // this.register(() => { + // if (!this.isCreated) this.schema(Target, undefined as keyof object) + // })() } public readonly lang = (target: T, property: keyof T) => { - this.register(() => { - const lang = target[property] as string | string[] - ;(this.ctx.parent as Context).i18n.use(resolve(...(Array.isArray(lang) ? lang : [lang]))) - })() + none(target, property) + // this.register(() => { + // const lang = target[property] as string | string[] + // ;(this.ctx.parent as Context).i18n.use(resolve(...(Array.isArray(lang) ? lang : [lang]))) + // })() } public readonly inject = (target: T, property: keyof T) => { - this.register(() => { - const inject = target[property] as string[] - for (const identity of inject) { - this.ctx[Tokens.container].forEach((service, name) => { - if (!(service instanceof Service) || service.identity !== identity) return - this.ctx.inject(name as Exclude) - }) - } - })() + none(target, property) + // this.register(() => { + // const inject = target[property] as string[] + // for (const identity of inject) { + // this.ctx[Tokens.container].forEach((service, name) => { + // if (!(service instanceof Service) || service.identity !== identity) return + // this.ctx.inject(name as Parameters[0]) + // }) + // } + // })() } public readonly schema = (Target: T, property: keyof T) => { - this.register(() => { - let config = (this.ctx[Symbols.modules].get(this.ctx.identity as string) ?? [])[1] - if (Target[property]) { - const result = (Target[property] as unknown as Parser).parseSafe(config) - if (!result.value) - throw new ModuleError(`Config format of module ${this.ctx.identity} is error: ${result.error.message}`) - config = result.data - } - - if (this.isCreated) return - this.isCreated = true - this.object = new (Target as new (...args: unknown[]) => object)(this.ctx, config) - this.ctx.root.emit('ready_module', { instance: { name: this.ctx.identity as string, config } }) - })() + none(Target, property) + // this.register(() => { + // let config = (this.ctx[Symbols.modules].get(this.ctx.identity as string) ?? [])[1] + // if (Target[property]) { + // const result = (Target[property] as unknown as Parser).parseSafe(config) + // if (!result.value) + // throw new ModuleError(`Config format of module ${this.ctx.identity} is error: ${result.error.message}`) + // config = result.data + // } + // if (this.isCreated) return + // this.isCreated = true + // this.object = new (Target as new (...args: unknown[]) => object)(this.ctx, config) + // this.ctx.root.emit('ready_module', { instance: { name: this.ctx.identity as string, config } }) + // })() } public on(meta: { type: T }) { - return this.register((target: T, property: keyof T) => - this.ctx.on(meta.type, (...args: unknown[]) => (target[property] as Fn).bind(this.object)(...args)) - ) + none(meta) + // return this.register((target: T, property: keyof T) => + // this.ctx.on(meta.type, (...args: unknown[]) => (target[property] as Fn).bind(this.object)(...args)) + // ) } public once(meta: { type: T }) { - return this.register((target: T, property: keyof T) => - this.ctx.once(meta.type, (...args: unknown[]) => (target[property] as Fn).bind(this.object)(...args)) - ) + none(meta) + // return this.register((target: T, property: keyof T) => + // this.ctx.once(meta.type, (...args: unknown[]) => (target[property] as Fn).bind(this.object)(...args)) + // ) } public midware(meta?: { priority: number }) { - return this.register((target: T, property: keyof T) => - this.ctx.midware((next, session) => (target[property] as Fn).bind(this.object)(next, session), meta?.priority) - ) + none(meta) + // return this.register((target: T, property: keyof T) => + // this.ctx.midware((next, session) => (target[property] as Fn).bind(this.object)(next, session), meta?.priority) + // ) } public command(meta: { @@ -110,24 +117,27 @@ export class Decorators { access?: UserAccess options?: [string, string][] }) { - return this.register((target: T, property: keyof T) => { - const command = this.ctx - .command(meta.template, meta) - .action((data, session) => (target[property] as Fn).bind(this.object)(data, session)) - if (meta.options) for (const [name, template] of Array.from(meta.options ?? [])) command.option(name, template) - }) + none(meta) + // return this.register((target: T, property: keyof T) => { + // const command = this.ctx + // .command(meta.template, meta) + // .action((data, session) => (target[property] as Fn).bind(this.object)(data, session)) + // if (meta.options) for (const [name, template] of Array.from(meta.options ?? [])) command.option(name, template) + // }) } public regexp(meta: { match: RegExp }) { - return this.register((target: T, property: keyof T) => - this.ctx.regexp(meta.match, (match, session) => (target[property] as Fn).bind(this.object)(match, session)) - ) + none(meta) + // return this.register((target: T, property: keyof T) => + // this.ctx.regexp(meta.match, (match, session) => (target[property] as Fn).bind(this.object)(match, session)) + // ) } public task(meta: Exclude[0], string>) { - return this.register((target: T, property: keyof T) => - this.ctx.task(meta, (ctx) => (target[property] as Fn).bind(this.object)(ctx)) - ) + none(meta) + // return this.register((target: T, property: keyof T) => + // this.ctx.task(meta, (ctx) => (target[property] as Fn).bind(this.object)(ctx)) + // ) } } diff --git a/packages/loader/src/service/database.ts b/packages/loader/src/service/database.ts index 33546ff3..c9878bba 100644 --- a/packages/loader/src/service/database.ts +++ b/packages/loader/src/service/database.ts @@ -1,13 +1,13 @@ import type { Service } from '@kotori-bot/core' -// import type knex from 'knex' +import type knex from 'knex' export interface Database extends Service { - // internal: ReturnType - // select: ReturnType['select'] - // delete: ReturnType['delete'] - // update: ReturnType['update'] - // insert: ReturnType['insert'] - // schema: ReturnType['schema'] + internal: ReturnType + select: ReturnType['select'] + delete: ReturnType['delete'] + update: ReturnType['update'] + insert: ReturnType['insert'] + schema: ReturnType['schema'] } export default Database diff --git a/packages/loader/src/service/server.ts b/packages/loader/src/service/server.ts index e1274cfb..68fde3e3 100644 --- a/packages/loader/src/service/server.ts +++ b/packages/loader/src/service/server.ts @@ -79,28 +79,36 @@ export class Server extends Service implements HttpRoutes { }) this.server = createServer(this.app) - this.wsServer = new Ws.Server({ noServer: true }) - this.server.on('upgrade', (req, socket, head) => { - this.wsServer.handleUpgrade(req, socket, head, (ws, req) => { - this.wsServer.emit('connection', ws, req) - }) - }) + this.wsServer = new Ws.Server({ server: this.server }) this.wsServer.on('connection', (ws, req) => { - let triggered = false - for (const [template, list] of this.wsRoutes.entries()) { - if (!req.url) continue - const result = match(template, { decode: decodeURIComponent })(req.url) - if (!result) continue - triggered = true - - for (const callback of list) { - callback(ws, Object.assign(req, { params: result.params as Record })) + ws.on('open', () => { + let triggered = false + for (const [template, list] of this.wsRoutes.entries()) { + if (!req.url) continue + const result = match(template, { decode: decodeURIComponent })(req.url) + if (!result) continue + triggered = true + + for (const callback of list) { + callback(ws, Object.assign(req, { params: result.params as Record })) + } } - } - if (!triggered) { + if (triggered) return ws.close(1003) req.destroy() - } + }) + ws.on('message', (mes) => { + mes.toString() + }) + ws.on('error', (error) => { + this.ctx.logger.label('server').error(`WebSocket client error: ${error.message}`) + }) + ws.on('close', (code, reason) => { + this.ctx.logger.label('server').info(`WebSocket connection closed: ${code} - ${reason}`) + }) + }) + this.wsServer.on('error', (error) => { + this.ctx.logger.label('server').error(`WebSocket server error: ${error.message}`) }) } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0e74d77f..b9ce8c95 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - 'packages/*' - - 'modules/*' + - "packages/*" + # - 'modules/*'