Skip to content

Commit

Permalink
feat: support more config file type
Browse files Browse the repository at this point in the history
  • Loading branch information
lc-cn committed Feb 12, 2025
1 parent 7ef7581 commit 48da6cb
Show file tree
Hide file tree
Showing 20 changed files with 145 additions and 126 deletions.
3 changes: 2 additions & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"jiti": "^1.21.0",
"log4js": "^6.9.1",
"tsconfig-paths": "^4.2.0",
"yaml": "^2.4.5"
"yaml": "^2.4.5",
"smol-toml": "^1.3.1"
},
"peerDependencies": {
"@zhinjs/shared": "workspace:^"
Expand Down
67 changes: 36 additions & 31 deletions core/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export type Element = {
data: Dict;
};
export class Adapter<P extends keyof App.Adapters> extends EventEmitter {
bots: Adapter.Bot<P>[] = [];
elements: Element[] = [];
private [adapterKey] = true;
#is_started: boolean = false;
Expand All @@ -21,6 +20,18 @@ export class Adapter<P extends keyof App.Adapters> extends EventEmitter {
static isAdapter(obj: any): obj is Adapter {
return typeof obj === 'object' && !!obj[adapterKey];
}
get bots(): Adapter.Bot<P>[] {
return (this.app?.bots.filter(bot => bot.adapter === this) || []) as unknown as Adapter.Bot<P>[];
}
set bots(bots: Adapter.Bot<P>[]) {
for (const bot of bots) {
if (bot.adapter !== this) throw new Error(`bot ${bot.unique_id} not belongs to adapter ${this.name}`);
const hasBot = (bot: Adapter.Bot<P>) => {
return this.app?.bots.some(b => b.unique_id === bot.unique_id);
};
if (!hasBot(bot)) this.app?.bots.push(bot as any);
}
}
start(app: App, config: Adapter.BotConfig<P>[]) {
this.app = app;
this.emit('start', config);
Expand Down Expand Up @@ -89,71 +100,59 @@ export class Adapter<P extends keyof App.Adapters> extends EventEmitter {
export interface Adapter<P extends keyof App.Adapters = keyof App.Adapters> {
on<T extends keyof Adapter.EventMap<P>>(event: T, listener: Adapter.EventMap<P>[T]): this;

on<S extends string | symbol>(
event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>,
listener: (...args: any[]) => any,
): this;
on<S extends string>(event: S & Exclude<S, keyof Adapter.EventMap<P>>, listener: (...args: any[]) => any): this;

off<T extends keyof Adapter.EventMap<P>>(event: T, callback?: Adapter.EventMap<P>[T]): this;
off<T extends keyof Adapter.EventMap<P>>(event: T, listener?: Adapter.EventMap<P>[T]): this;

off<S extends string | symbol>(
event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>,
callback?: (...args: any[]) => void,
): this;
off<S extends string>(event: S & Exclude<S, keyof Adapter.EventMap<P>>, callback?: (...args: any[]) => void): this;

once<T extends keyof Adapter.EventMap<P>>(event: T, listener: Adapter.EventMap<P>[T]): this;

once<S extends string | symbol>(
event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>,
listener: (...args: any[]) => any,
): this;
once<S extends string>(event: S & Exclude<S, keyof Adapter.EventMap<P>>, listener: (...args: any[]) => any): this;

emit<T extends keyof Adapter.EventMap<P>>(event: T, ...args: Parameters<Adapter.EventMap<P>[T]>): boolean;

emit<S extends string | symbol>(
event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>,
...args: any[]
): boolean;
emit<S extends string>(event: S & Exclude<S, keyof Adapter.EventMap<P>>, ...args: any[]): boolean;

addListener<T extends keyof Adapter.EventMap<P>>(event: T, listener: Adapter.EventMap<P>[T]): this;

addListener<S extends string | symbol>(
event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>,
addListener<S extends string>(
event: S & Exclude<S, keyof Adapter.EventMap<P>>,
listener: (...args: any[]) => any,
): this;

addListenerOnce<T extends keyof Adapter.EventMap<P>>(event: T, callback: Adapter.EventMap<P>[T]): this;

addListenerOnce<S extends string | symbol>(
event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>,
addListenerOnce<S extends string>(
event: S & Exclude<S, keyof Adapter.EventMap<P>>,
callback: (...args: any[]) => void,
): this;

removeListener<T extends keyof Adapter.EventMap<P>>(event: T, callback?: Adapter.EventMap<P>[T]): this;

removeListener<S extends string | symbol>(
event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>,
removeListener<S extends string>(
event: S & Exclude<S, keyof Adapter.EventMap<P>>,
callback?: (...args: any[]) => void,
): this;

removeAllListeners<T extends keyof Adapter.EventMap<P>>(event: T): this;

removeAllListeners<S extends string | symbol>(event: S & Exclude<string | symbol, keyof Adapter.EventMap<P>>): this;
removeAllListeners<S extends string>(event: S & Exclude<S, keyof Adapter.EventMap<P>>): this;
}
export namespace Adapter {
export interface EventMap<P extends keyof App.Adapters> {
'bot-ready'(bot: Bot<P>): void;
'start'(configs: BotConfig<P>[]): void;
'stop'(): void;
}
export type Bot<P extends Adapters = Adapters> = BaseBot<P> & App.Clients[P];
export abstract class BaseBot<P extends Adapters = Adapters> {
protected constructor(
public adapter: Adapter<P>,
public unique_id: string,
public client: App.Clients[P],
) {
const _this = this;
const oldEmit = client.emit;
client.emit = function (event: string, ...args: any[]) {
const result = oldEmit.apply(client, [event, ...args] as any);
_this.adapter.app?.emit(`${_this.adapter.name}.${event}`, _this, ...args);
return result;
} as typeof oldEmit;
return new Proxy(_this, {
get(target, prop, receiver) {
if (prop in target) return Reflect.get(target, prop, receiver);
Expand All @@ -176,6 +175,12 @@ export namespace Adapter {
return this.adapter.botConfig(this.unique_id)?.forward_length;
}
}
export type Bot<P extends Adapters = Adapters> = BaseBot<P> & App.Clients[P];
export interface EventMap<P extends Adapters = Adapters> {
'bot-ready': (bot: Bot<P>) => void;
'start': (configs: BotConfig<P>[]) => void;
'stop': () => void;
}
export function load(name: string) {
const maybePath = [
path.join(WORK_DIR, 'node_modules', `@zhinjs`, name), // 官方适配器
Expand Down
9 changes: 5 additions & 4 deletions core/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Plugin, PluginMap } from './plugin';
import { LogLevel } from './types';
import { loadModule } from './utils';
import { remove, sleep } from '@zhinjs/shared';
import { APP_KEY, CONFIG_DIR, serviceCallbacksKey, WORK_DIR } from './constans';
import { APP_KEY, CONFIG_DIR, WORK_DIR } from './constans';
import path from 'path';
import { Adapter } from './adapter';
import { Message } from './message';
Expand All @@ -29,6 +29,7 @@ export class App extends EventEmitter {
middlewares: Middleware<Adapters>[] = [];
plugins: PluginMap = new PluginMap();
renders: Message.Render[] = [];
bots: Adapter.Bot[] = [];
get adapters() {
return App.adapters;
}
Expand Down Expand Up @@ -178,6 +179,7 @@ export class App extends EventEmitter {

emit(event: string, ...args: any[]) {
const result = super.emit(event, ...args);
this.logger.debug(`emit event: ${event}`);
for (const plugin of this.pluginList) {
plugin.emit(event, ...args);
}
Expand Down Expand Up @@ -274,12 +276,11 @@ export class App extends EventEmitter {
for (const loadPath of maybePath) {
if (loaded) break;
try {
this.logger.debug(`try load plugin(${name}) from ${loadPath}`);
this.mount(loadPath);
loaded = true;
} catch (e) {
if (!error || String(Reflect.get(error, 'message')).startsWith('Cannot find')) error = e as Error;
this.logger.debug(`try load plugin(${name}) failed. (from: ${loadPath})`, (e as Error)?.message || e);
this.logger.trace(`try load plugin(${name}) failed. (from: ${loadPath})`, (e as Error)?.message || e);
}
}
if (!loaded) this.logger.warn(`load plugin "${name}" failed`, error?.message || error);
Expand Down Expand Up @@ -394,7 +395,7 @@ export namespace App {
title: string;
};
}
export interface Clients {
export interface Clients extends Record<keyof Adapters, EventEmitter> {
process: NodeJS.Process;
}
export interface Services {}
Expand Down
46 changes: 43 additions & 3 deletions core/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as fs from 'fs';
import * as path from 'path';
import smolToml from 'smol-toml';
import * as yaml from 'yaml';
import { CONFIG_DIR } from './constans';
import { App } from './app';
export class Config<T extends object = object> {
public static exts: string[] = ['.json', '.yaml', '.yml'];
public static exts: string[] = ['.cts', '.mts', '.ts', '.cjs', '.mjs', '.js', '.json', '.yaml', '.yml', '.toml'];
filename: string = '';
#type: Config.Type = Config.Type.YAML;
private _data: T;
Expand All @@ -20,6 +21,7 @@ export class Config<T extends object = object> {
if (!Config.exts.includes(ext)) this.filename = path.join(CONFIG_DIR, `${name}${this.#resolveExt()}`);
this.#saveConfig(defaultValue);
}
this.#type = Config.resolveType(path.extname(this.filename));
this._data = this.#loadConfig();
return new Proxy<T>(this._data, {
get: (target, p, receiver) => {
Expand Down Expand Up @@ -59,8 +61,6 @@ export class Config<T extends object = object> {
if (!fs.existsSync(name)) {
throw new Error(`未找到配置文件${name}`);
}
const ext = path.extname(name);
this.#type = ['.yaml', '.yml'].includes(ext) ? Config.Type.YAML : Config.Type.JSON;
return name;
}
#resolveExt() {
Expand All @@ -69,6 +69,12 @@ export class Config<T extends object = object> {
return '.json';
case Config.Type.YAML:
return '.yml';
case Config.Type.TOML:
return '.toml';
case Config.Type.JS:
return '.js';
case Config.Type.TS:
return '.ts';
default:
throw new Error(`不支持的配置文件类型${this.#type}`);
}
Expand All @@ -80,6 +86,11 @@ export class Config<T extends object = object> {
return JSON.parse(content);
case Config.Type.YAML:
return yaml.parse(content);
case Config.Type.TOML:
return smolToml.parse(content);
case Config.Type.JS:
case Config.Type.TS:
return require(this.filename).default;
default:
throw new Error(`不支持的配置文件类型${this.#type}`);
}
Expand All @@ -90,6 +101,11 @@ export class Config<T extends object = object> {
return fs.writeFileSync(this.filename, JSON.stringify(data, null, 2));
case Config.Type.YAML:
return fs.writeFileSync(this.filename, yaml.stringify(data));
case Config.Type.TOML:
return fs.writeFileSync(this.filename, smolToml.stringify(data));
case Config.Type.JS:
case Config.Type.TS:
return fs.writeFileSync(this.filename, `export default ${JSON.stringify(data, null, 2)}`);
default:
throw new Error(`不支持的配置文件类型${this.#type}`);
}
Expand Down Expand Up @@ -124,6 +140,30 @@ export namespace Config {
export enum Type {
JSON = 'json',
YAML = 'yaml',
TOML = 'toml',
TS = 'ts',
JS = 'js',
}
export function resolveType(ext: string): Config.Type {
switch (ext) {
case '.json':
return Config.Type.JSON;
case '.yaml':
case '.yml':
return Config.Type.YAML;
case '.toml':
return Config.Type.TOML;
case '.ts':
case '.cts':
case '.mts':
return Config.Type.TS;
case '.js':
case '.cjs':
case '.mjs':
return Config.Type.JS;
default:
throw new Error(`不支持的配置文件类型${ext}`);
}
}
}
export interface Config extends App.Config {}
8 changes: 8 additions & 0 deletions core/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export class Message<P extends keyof App.Adapters> {
message_base: MessageBase,
) {
Object.assign(this, message_base);
const permissions: Set<string> = new Set<string>(this.sender.permissions);
if (adapter.botConfig(bot.unique_id)?.master === this.sender.user_id) {
permissions.add('master');
}
if (adapter.botConfig(bot.unique_id)?.admins?.includes(this.sender.user_id!)) {
permissions.add('admin');
}
this.sender.permissions = [...permissions];
}
get group_id() {
if (this.message_type !== 'group') return undefined;
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/com-wechat/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class Client extends EventEmitter {
if (['path', 'event_path'].includes(key))
server.on('connection', (ws, req) => {
this.logger.info(`已连接到协议端:${req.socket.remoteAddress}`);
this.adapter.emit('bot-ready', this);
this.emit('ready', this);
ws.on('error', err => {
this.logger.error('连接出错:', err);
});
Expand All @@ -113,7 +113,7 @@ export class Client extends EventEmitter {
});
this.ws.on('open', () => {
this.logger.mark(`connected to ${config.url}`);
this.adapter.emit('bot-ready', this);
this.emit('ready', this);
this.reTryCount = 0;
});
this.ws.on('message', this.dispatch);
Expand Down
11 changes: 4 additions & 7 deletions packages/adapters/com-wechat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ adapter.schema({
class WechatClient extends Adapter.BaseBot<'com-wechat'> {
constructor(config: Adapter.BotConfig<'com-wechat'>) {
super(adapter, config.unique_id, new Client(adapter, config, adapter.app!.router));
this.on('ready', () => {
this.adapter.emit('bot-ready', this);
});
}
async handleSendMessage(
channel: Message.Channel,
Expand Down Expand Up @@ -59,18 +62,12 @@ const startBots = (configs: Adapter.BotConfig<'com-wechat'>[]) => {
}
};
const messageHandler = (bot: WechatClient, event: ClientMessage) => {
const master = bot.config?.master;
const admins = bot.config.admins?.filter(Boolean) || [];
const message = Message.from(adapter, bot, {
channel: `${event.detail_type}:${event.user_id}`,
sender: {
user_id: event.user_id,
user_name: event.nickname || '',
permissions: [
master && event.user_id === master && 'master',
admins && admins.includes(event.user_id) && 'admins',
...(event.permissions || []),
].filter(Boolean) as string[],
permissions: [...(event.permissions || [])].filter(Boolean) as string[],
},
raw_message: ClientMessage.formatToString(event.message),
message_type: event.detail_type as any,
Expand Down
7 changes: 1 addition & 6 deletions packages/adapters/dingtalk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,13 @@ const startBots = (configs: Adapter.BotConfig<'dingtalk'>[]) => {
}
};
const messageHandler = (bot: Adapter.Bot<'dingtalk'>, event: DingMsgEvent) => {
const master = dingTalkAdapter.botConfig(bot.unique_id)?.master;
const admins = dingTalkAdapter.botConfig(bot.unique_id)?.admins?.filter(Boolean) || [];
const message = Message.from(dingTalkAdapter, bot, {
raw_message: sendableToString(event.message).trim(),
channel: `${event.message_type}:${event instanceof PrivateMessageEvent ? event.user_id : event.group_id}`,
message_type: event.message_type,
sender: {
...event.sender,
permissions: [
master && event.user_id === master && 'master',
admins && admins.includes(event.user_id) && 'admins',
].filter(Boolean) as string[],
permissions: [],
},
});
dingTalkAdapter.app!.emit('message', dingTalkAdapter, bot, message);
Expand Down
8 changes: 1 addition & 7 deletions packages/adapters/discord/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,12 @@ const startBots = (configs: Adapter.BotConfig<'discord'>[]) => {
}
};
const messageHandler = (bot: Adapter.Bot<'discord'>, event: DiscordMessageEvent) => {
const master = discordAdapter.botConfig(bot.unique_id)?.master;
const admins = discordAdapter.botConfig(bot.unique_id)?.admins?.filter(Boolean) || [];
const message = Message.from(discordAdapter, bot, {
channel: `${event.message_type}:${event instanceof DirectMessageEvent ? event.user_id : event.channel_id}`,
sender: {
user_id: event.sender.user_id,
user_name: event.sender.user_name,
permissions: [
...(event.sender?.permissions as unknown as string[]),
master && event.sender?.user_id === master && 'master',
admins && admins.includes(event.sender.user_id) && 'admins',
].filter(Boolean) as string[],
permissions: [...(event.sender?.permissions as unknown as string[])].filter(Boolean) as string[],
},
raw_message: sendableToString(event.message).trim(),
message_type: event.message_type,
Expand Down
Loading

0 comments on commit 48da6cb

Please sign in to comment.