Commit 63b67403 authored by nanahira's avatar nanahira

migrate to 4.0.0

parent 48036c61
......@@ -9,7 +9,6 @@
"version": "1.4.4",
"license": "UNLICENSED",
"dependencies": {
"@koishijs/plugin-adapter-onebot": "^4.0.0-rc.0",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.3",
"@nestjs/core": "^8.0.0",
......@@ -18,8 +17,9 @@
"@nestjs/websockets": "^8.1.2",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"koishi": "^4.0.0-rc.1",
"koishi-nestjs": "^4.0.0",
"koishi": "^4.0.0",
"koishi-nestjs": "^4.3.0",
"qface": "^1.2.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
......@@ -1348,54 +1348,22 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/@koishijs/core": {
"version": "4.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0-rc.1.tgz",
"integrity": "sha512-5T2+DUvm2TUpNjWgLD6mlh8+2pNN1edF/80A1eKy5sXyrA86dzR9VkHWysY+ZZpEVedzpVPkc9RXlLwN3byQ2g==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0.tgz",
"integrity": "sha512-xonpueYfWhcTw2eJVc/SG7EwutFgzqtvdpuCGoBFvfRRSZlHOWAQfWsRw5he06+qn7rVOj3cyxrtFMP0fAthNA==",
"dependencies": {
"@koishijs/utils": "^5.0.0-rc.0",
"@koishijs/utils": "^5.0.0",
"fastest-levenshtein": "^1.0.12",
"schemastery": "^2.1.2"
"schemastery": "^2.1.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@koishijs/plugin-adapter-onebot": {
"version": "4.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@koishijs/plugin-adapter-onebot/-/plugin-adapter-onebot-4.0.0-rc.0.tgz",
"integrity": "sha512-IU7EQGXt35V73qGRHPn1WBGmdgaMkVky/OestbLAtZq07hQmBCbnZ8HTSakOMx34day+TS5Qn04QSUm/kfVIGA==",
"dependencies": {
"qface": "^1.2.0",
"ws": "^8.2.1"
},
"peerDependencies": {
"koishi": "^4.0.0-rc.0"
}
},
"node_modules/@koishijs/plugin-adapter-onebot/node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@koishijs/utils": {
"version": "5.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0-rc.0.tgz",
"integrity": "sha512-aG1FjFB9NKiSqGS/tjjsSnmSfrW5yXQK7aZAR7DhsSaiF+/CdWZ0sQ9XS830xZ6PV1UwKib9alluVMULyWvrbA==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0.tgz",
"integrity": "sha512-3ng7VkQZAP+EHdRLSbio5H36LDKRxy4OfooRKgol+gef2Yd0KOKYbVmGVud7/mZZExCPkaZVCwocKB/bKA4Xvw==",
"dependencies": {
"supports-color": "^8.1.0"
}
......@@ -6888,13 +6856,13 @@
}
},
"node_modules/koishi": {
"version": "4.0.0-rc.1",
"resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0-rc.1.tgz",
"integrity": "sha512-liXILmB7yHaUUOqNqEZ4/Q/bZwsBVsQ8aIwQ7JolZfxQjlZtmwE69B3AEYmY2dWcO3+lnJhjCiBkvKGj01kpSg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0.tgz",
"integrity": "sha512-7m9kv+8EOJlOOrwnk3KolU+Dt+tjd8fp2xNQviHj7+Z97kQlc6AnS+gXExikiiQHJpPshSgkBzbxbP1HuV668w==",
"dependencies": {
"@koa/router": "^10.1.1",
"@koishijs/core": "^4.0.0-rc.1",
"@koishijs/utils": "^5.0.0-rc.0",
"@koishijs/core": "^4.0.0",
"@koishijs/utils": "^5.0.0",
"@types/koa": "*",
"@types/koa__router": "*",
"@types/ws": "^7.4.7",
......@@ -6912,9 +6880,9 @@
}
},
"node_modules/koishi-nestjs": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.0.0.tgz",
"integrity": "sha512-kYWOwYkJSmrZuX6fPA8cTa7tCt+F/sVWKdw2KSEaqRIsLvv9J+d8kjh3GLs9SJIm99S43yaFhNntWxyT2XKUcA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.3.0.tgz",
"integrity": "sha512-E7rmi08mlJsUAOaMxEFcCMsffzYHXwON2aQTD87M5/fj+eDhuU3lUBZBidXeuEE4YLlbjIS0MRImqV+SR1tnAQ==",
"dependencies": {
"@nestjs/platform-ws": "^8.1.2",
"@nestjs/websockets": "^8.1.2",
......@@ -6927,7 +6895,7 @@
"peerDependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0",
"koishi": "^4.0.0-rc.1",
"koishi": "^4.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.4.0"
}
......@@ -8399,9 +8367,9 @@
"dev": true
},
"node_modules/schemastery": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.2.tgz",
"integrity": "sha512-iHwWfKxWaTFgZmKNULNtgyo8VDpdEWx31b6+j0tGTST8dBIYU7VYAHmq5qdYKYZ0uNSM5u57c09cuN75Yf7WwQ=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.3.tgz",
"integrity": "sha512-AH6dgucxUSkuK/LvIJtPor8/6KCEq5L9weD5JQJ6/HEDFqD4KbO7NqQsJmbW4TKubZ4FtPj7eXSEaz5nie+y+A=="
},
"node_modules/semver": {
"version": "7.3.5",
......@@ -11046,36 +11014,19 @@
}
},
"@koishijs/core": {
"version": "4.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0-rc.1.tgz",
"integrity": "sha512-5T2+DUvm2TUpNjWgLD6mlh8+2pNN1edF/80A1eKy5sXyrA86dzR9VkHWysY+ZZpEVedzpVPkc9RXlLwN3byQ2g==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0.tgz",
"integrity": "sha512-xonpueYfWhcTw2eJVc/SG7EwutFgzqtvdpuCGoBFvfRRSZlHOWAQfWsRw5he06+qn7rVOj3cyxrtFMP0fAthNA==",
"requires": {
"@koishijs/utils": "^5.0.0-rc.0",
"@koishijs/utils": "^5.0.0",
"fastest-levenshtein": "^1.0.12",
"schemastery": "^2.1.2"
}
},
"@koishijs/plugin-adapter-onebot": {
"version": "4.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@koishijs/plugin-adapter-onebot/-/plugin-adapter-onebot-4.0.0-rc.0.tgz",
"integrity": "sha512-IU7EQGXt35V73qGRHPn1WBGmdgaMkVky/OestbLAtZq07hQmBCbnZ8HTSakOMx34day+TS5Qn04QSUm/kfVIGA==",
"requires": {
"qface": "^1.2.0",
"ws": "^8.2.1"
},
"dependencies": {
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
}
"schemastery": "^2.1.3"
}
},
"@koishijs/utils": {
"version": "5.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0-rc.0.tgz",
"integrity": "sha512-aG1FjFB9NKiSqGS/tjjsSnmSfrW5yXQK7aZAR7DhsSaiF+/CdWZ0sQ9XS830xZ6PV1UwKib9alluVMULyWvrbA==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0.tgz",
"integrity": "sha512-3ng7VkQZAP+EHdRLSbio5H36LDKRxy4OfooRKgol+gef2Yd0KOKYbVmGVud7/mZZExCPkaZVCwocKB/bKA4Xvw==",
"requires": {
"supports-color": "^8.1.0"
},
......@@ -15269,13 +15220,13 @@
}
},
"koishi": {
"version": "4.0.0-rc.1",
"resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0-rc.1.tgz",
"integrity": "sha512-liXILmB7yHaUUOqNqEZ4/Q/bZwsBVsQ8aIwQ7JolZfxQjlZtmwE69B3AEYmY2dWcO3+lnJhjCiBkvKGj01kpSg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0.tgz",
"integrity": "sha512-7m9kv+8EOJlOOrwnk3KolU+Dt+tjd8fp2xNQviHj7+Z97kQlc6AnS+gXExikiiQHJpPshSgkBzbxbP1HuV668w==",
"requires": {
"@koa/router": "^10.1.1",
"@koishijs/core": "^4.0.0-rc.1",
"@koishijs/utils": "^5.0.0-rc.0",
"@koishijs/core": "^4.0.0",
"@koishijs/utils": "^5.0.0",
"@types/koa": "*",
"@types/koa__router": "*",
"@types/ws": "^7.4.7",
......@@ -15311,9 +15262,9 @@
}
},
"koishi-nestjs": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.0.0.tgz",
"integrity": "sha512-kYWOwYkJSmrZuX6fPA8cTa7tCt+F/sVWKdw2KSEaqRIsLvv9J+d8kjh3GLs9SJIm99S43yaFhNntWxyT2XKUcA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.3.0.tgz",
"integrity": "sha512-E7rmi08mlJsUAOaMxEFcCMsffzYHXwON2aQTD87M5/fj+eDhuU3lUBZBidXeuEE4YLlbjIS0MRImqV+SR1tnAQ==",
"requires": {
"@nestjs/platform-ws": "^8.1.2",
"@nestjs/websockets": "^8.1.2",
......@@ -16399,9 +16350,9 @@
}
},
"schemastery": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.2.tgz",
"integrity": "sha512-iHwWfKxWaTFgZmKNULNtgyo8VDpdEWx31b6+j0tGTST8dBIYU7VYAHmq5qdYKYZ0uNSM5u57c09cuN75Yf7WwQ=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.3.tgz",
"integrity": "sha512-AH6dgucxUSkuK/LvIJtPor8/6KCEq5L9weD5JQJ6/HEDFqD4KbO7NqQsJmbW4TKubZ4FtPj7eXSEaz5nie+y+A=="
},
"semver": {
"version": "7.3.5",
......
......@@ -21,7 +21,6 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@koishijs/plugin-adapter-onebot": "^4.0.0-rc.0",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.3",
"@nestjs/core": "^8.0.0",
......@@ -30,8 +29,9 @@
"@nestjs/websockets": "^8.1.2",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"koishi": "^4.0.0-rc.1",
"koishi-nestjs": "^4.0.0",
"koishi": "^4.0.0",
"koishi-nestjs": "^4.3.0",
"qface": "^1.2.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
......
This diff is collapsed.
import {
Adapter,
Logger,
assertProperty,
Schema,
Quester,
omit,
Context,
} from 'koishi';
import { BotConfig, OneBotBot } from './bot';
import { dispatchSession, AdapterConfig } from './utils';
import { createHmac } from 'crypto';
const logger = new Logger('onebot');
export class HttpServer extends Adapter<BotConfig, AdapterConfig> {
static schema: Schema<BotConfig> = Schema.object({
selfId: Schema.string().description('机器人的账号。').required(),
token: Schema.string().description(
'发送信息时用于验证的字段,应与 OneBot 配置文件中的 access_token 保持一致。',
),
endpoint: Schema.string()
.description('要连接的 OneBot 服务器地址。')
.required(),
...omit(Quester.Config.dict, ['endpoint']),
});
public bots: OneBotBot[];
constructor(ctx: Context, config: AdapterConfig = {}) {
super(ctx, config);
assertProperty(ctx.app.options, 'port');
this.http = ctx.http.extend(config.request);
}
async connect(bot: OneBotBot) {
const { endpoint, token } = bot.config;
if (!endpoint) return;
const http = this.http.extend(bot.config).extend({
headers: {
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
});
bot.internal._request = async (action, params) => {
return http.post('/' + action, params);
};
return bot.initialize();
}
async start() {
const { secret, path = '/onebot' } = this.config;
this.ctx.router.post(path, (ctx) => {
if (secret) {
// no signature
const signature = ctx.headers['x-signature'];
if (!signature) return (ctx.status = 401);
// invalid signature
const sig = createHmac('sha1', secret)
.update(ctx.request.rawBody)
.digest('hex');
if (signature !== `sha1=${sig}`) return (ctx.status = 403);
}
const selfId = ctx.headers['x-self-id'].toString();
const bot = this.bots.find((bot) => bot.selfId === selfId);
if (!bot) return (ctx.status = 403);
logger.debug('receive %o', ctx.request.body);
dispatchSession(bot, ctx.request.body);
});
}
stop() {
logger.debug('http server closing');
}
}
import { Adapter } from 'koishi';
import { OneBotBot } from './bot';
import { WebSocketClient, WebSocketServer } from './ws';
import { HttpServer } from './http';
import * as OneBot from './types';
declare module 'koishi' {
interface Modules {
'adapter-onebot': typeof import('.');
}
interface Session {
onebot?: OneBot.Payload & OneBot.Internal;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Session {
interface Events {
onebot: {
// eslint-disable-next-line @typescript-eslint/ban-types
'message-reactions-updated': {};
// eslint-disable-next-line @typescript-eslint/ban-types
'channel-updated': {};
// eslint-disable-next-line @typescript-eslint/ban-types
'channel-created': {};
// eslint-disable-next-line @typescript-eslint/ban-types
'channel-destroyed': {};
};
}
}
}
export { OneBot };
export * from './bot';
export * from './ws';
export * from './http';
export default Adapter.define(
'OneBot',
OneBotBot,
{
http: HttpServer,
ws: WebSocketClient,
'ws-reverse': WebSocketServer,
},
({ endpoint }) => {
return !endpoint ? 'ws-reverse' : endpoint.startsWith('ws') ? 'ws' : 'http';
},
);
This diff is collapsed.
import {
Adapter,
Bot,
Session,
paramCase,
segment,
Schema,
App,
defineProperty,
} from 'koishi';
import * as qface from 'qface';
import { OneBotBot } from './bot';
import * as OneBot from './types';
export * from './types';
export interface AdapterConfig
extends Adapter.WebSocketClient.Config,
App.Config.Request {
path?: string;
secret?: string;
responseTimeout?: number;
}
export const AdapterConfig: Schema<AdapterConfig> = Schema.intersect([
Schema.object({
path: Schema.string()
.description('服务器监听的路径,用于 http 和 ws-reverse 协议。')
.default('/onebot'),
secret: Schema.string().description(
'接收事件推送时用于验证的字段,应该与 OneBot 的 secret 配置保持一致。',
),
}),
Adapter.WebSocketClient.Config,
App.Config.Request,
]);
export const adaptUser = (user: OneBot.AccountInfo): Bot.User => ({
userId: user.tiny_id || user.user_id.toString(),
avatar: user.user_id
? `http://q.qlogo.cn/headimg_dl?dst_uin=${user.user_id}&spec=640`
: undefined,
username: user.nickname,
});
export const adaptGuildMember = (user: OneBot.SenderInfo): Bot.GuildMember => ({
...adaptUser(user),
nickname: user.card,
roles: [user.role],
});
export const adaptQQGuildMember = (
user: OneBot.GuildMemberInfo,
presetRole?: string,
): Bot.GuildMember => ({
userId: user.tiny_id,
username: user.nickname,
nickname: user.nickname,
roles: [...(presetRole ? [presetRole] : []), user.role.toString()],
isBot: presetRole === 'bot',
});
export const adaptAuthor = (
user: OneBot.SenderInfo,
anonymous?: OneBot.AnonymousInfo,
): Bot.Author => ({
...adaptUser(user),
nickname: anonymous?.name || user.card,
anonymous: anonymous?.flag,
roles: [user.role],
});
export function adaptMessage(message: OneBot.Message): Bot.Message {
const author = adaptAuthor(message.sender, message.anonymous);
const result: Bot.Message = {
author,
userId: author.userId,
messageId: message.message_id.toString(),
timestamp: message.time * 1000,
content: segment.transform(message.message, {
at({ qq }) {
if (qq !== 'all') return segment.at(qq);
return segment('at', { type: 'all' });
},
face: ({ id }) => segment('face', { id, url: qface.getUrl(id) }),
reply: (data) => segment('quote', data),
}),
};
if (message.guild_id) {
result.guildId = message.guild_id;
result.channelId = message.channel_id;
} else if (message.group_id) {
result.guildId = result.channelId = message.group_id.toString();
} else {
result.channelId = 'private:' + author.userId;
}
return result;
}
export const adaptGuild = (
info: OneBot.GroupInfo | OneBot.GuildBaseInfo,
): Bot.Guild => {
if ((info as OneBot.GuildBaseInfo).guild_id) {
const guild = info as OneBot.GuildBaseInfo;
return {
guildId: guild.guild_id,
guildName: guild.guild_name,
};
} else {
const group = info as OneBot.GroupInfo;
return {
guildId: group.group_id.toString(),
guildName: group.group_name,
};
}
};
export const adaptChannel = (
info: OneBot.GroupInfo | OneBot.ChannelInfo,
): Bot.Channel => {
if ((info as OneBot.ChannelInfo).channel_id) {
const channel = info as OneBot.ChannelInfo;
return {
channelId: channel.channel_id.toString(),
channelName: channel.channel_name,
};
} else {
const group = info as OneBot.GroupInfo;
return {
channelId: group.group_id.toString(),
channelName: group.group_name,
};
}
};
export function dispatchSession(bot: OneBotBot, data: OneBot.Payload) {
/*
if (data.self_tiny_id) {
// don't dispatch any guild message without guild initialization
if (!bot.guildBot) return;
bot = bot.guildBot;
}
*/
const payload = adaptSession(data);
if (!payload) return;
const session = new Session(bot, payload);
defineProperty(session, 'onebot', Object.create(bot.internal));
Object.assign(session.onebot, data);
bot.adapter.dispatch(session);
}
export function adaptSession(data: OneBot.Payload) {
const session: Partial<Session> = {};
session.selfId = data.self_tiny_id ? data.self_tiny_id : '' + data.self_id;
session.type = data.post_type;
if (data.post_type === 'message') {
Object.assign(session, adaptMessage(data));
session.subtype =
data.message_type === 'guild' ? 'group' : data.message_type;
session.subsubtype = data.message_type;
return session;
}
session.subtype = data.sub_type;
if (data.user_id) session.userId = '' + data.user_id;
if (data.group_id) session.guildId = session.channelId = '' + data.group_id;
if (data.guild_id) session.guildId = '' + data.guild_id;
if (data.channel_id) session.channelId = '' + data.channel_id;
if (data.target_id) session.targetId = '' + data.target_id;
if (data.operator_id) session.operatorId = '' + data.operator_id;
if (data.message_id) session.messageId = '' + data.message_id;
if (data.post_type === 'request') {
session.content = data.comment;
session.messageId = data.flag;
if (data.request_type === 'friend') {
session.type = 'friend-request';
session.channelId = `private:${session.userId}`;
} else if (data.sub_type === 'add') {
session.type = 'guild-member-request';
} else {
session.type = 'guild-request';
}
} else if (data.post_type === 'notice') {
switch (data.notice_type) {
case 'group_recall':
session.type = 'message-deleted';
session.subtype = 'group';
break;
case 'friend_recall':
session.type = 'message-deleted';
session.subtype = 'private';
session.channelId = `private:${session.userId}`;
break;
case 'friend_add':
session.type = 'friend-added';
break;
case 'group_upload':
session.type = 'group-file-added';
break;
case 'group_admin':
session.type = 'group-member';
session.subtype = 'role';
break;
case 'group_ban':
session.type = 'group-member';
session.subtype = 'ban';
break;
case 'group_decrease':
session.type =
session.userId === session.selfId
? 'group-deleted'
: 'group-member-deleted';
session.subtype =
session.userId === session.operatorId ? 'active' : 'passive';
break;
case 'group_increase':
session.type =
session.userId === session.selfId
? 'group-added'
: 'group-member-added';
session.subtype =
session.userId === session.operatorId ? 'active' : 'passive';
break;
case 'group_card':
session.type = 'group-member';
session.subtype = 'nickname';
break;
case 'notify':
session.type = 'notice';
session.subtype = paramCase(data.sub_type) as any;
if (session.subtype === 'poke') {
session.channelId ||= `private:${session.userId}`;
} else if (session.subtype === 'honor') {
session.subsubtype = paramCase(data.honor_type) as any;
}
break;
case 'message_reactions_updated':
session.type = 'onebot';
session.subtype = 'message-reactions-updated';
break;
case 'channel_created':
session.type = 'onebot';
session.subtype = 'channel-created';
break;
case 'channel_updated':
session.type = 'onebot';
session.subtype = 'channel-updated';
break;
case 'channel_destroyed':
session.type = 'onebot';
session.subtype = 'channel-destroyed';
break;
}
}
return session;
}
import {
Adapter,
Logger,
assertProperty,
Time,
Schema,
Context,
WebSocketLayer,
} from 'koishi';
import { BotConfig, OneBotBot } from './bot';
import { AdapterConfig, dispatchSession, Response } from './utils';
import WebSocket from 'ws';
const logger = new Logger('onebot');
export class WebSocketClient extends Adapter.WebSocketClient<
BotConfig,
AdapterConfig
> {
static schema: Schema<BotConfig> = Schema.object({
selfId: Schema.string().description('机器人的账号。').required(),
token: Schema.string().description(
'发送信息时用于验证的字段,应与 OneBot 的 access_token 配置保持一致。',
),
endpoint: Schema.string()
.description('要连接的 OneBot 服务器地址。')
.required(),
});
protected accept = accept;
prepare(bot: OneBotBot) {
const { endpoint, token } = bot.config;
const headers: Record<string, string> = {};
if (token) headers.Authorization = `Bearer ${token}`;
return new WebSocket(endpoint, { headers });
}
}
export class WebSocketServer extends Adapter<BotConfig, AdapterConfig> {
static schema: Schema<BotConfig> = Schema.object({
selfId: Schema.string().description('机器人的账号。').required(),
});
public wsServer?: WebSocketLayer;
protected accept = accept;
constructor(ctx: Context, config: AdapterConfig) {
super(ctx, config);
assertProperty(ctx.app.options, 'port');
const { path = '/onebot' } = config;
this.wsServer = ctx.router.ws(path, (socket, { headers }) => {
logger.debug('connected with', headers);
if (headers['x-client-role'] !== 'Universal') {
return socket.close(1008, 'invalid x-client-role');
}
const selfId = headers['x-self-id'].toString();
const bot = this.bots.find((bot) => bot.selfId === selfId);
if (!bot) return socket.close(1008, 'invalid x-self-id');
bot.socket = socket;
this.accept(bot as OneBotBot);
});
}
connect() {}
start() {}
stop() {
logger.debug('ws server closing');
this.wsServer.close();
for (const bot of this.bots) {
bot.socket = null;
}
}
}
let counter = 0;
const listeners: Record<number, (response: Response) => void> = {};
export function accept(
this: Adapter<BotConfig, AdapterConfig>,
bot: OneBotBot,
) {
bot.socket.on('message', (data) => {
data = data.toString();
let parsed: any;
try {
parsed = JSON.parse(data);
} catch (error) {
return logger.warn('cannot parse message', data);
}
if ('post_type' in parsed) {
logger.debug('receive %o', parsed);
dispatchSession(bot, parsed);
} else if (parsed.echo in listeners) {
listeners[parsed.echo](parsed);
delete listeners[parsed.echo];
}
});
bot.socket.on('close', () => {
delete bot.internal._request;
});
bot.internal._request = (action, params) => {
const data = { action, params, echo: ++counter };
data.echo = ++counter;
return new Promise((resolve, reject) => {
listeners[data.echo] = resolve;
setTimeout(() => {
delete listeners[data.echo];
reject(new Error('response timeout'));
}, this.config.responseTimeout || Time.minute);
bot.socket.send(JSON.stringify(data), (error) => {
if (error) reject(error);
});
});
};
bot.initialize();
}
......@@ -20,6 +20,7 @@ import { BotRegistryService } from './bot-registry/bot-registry.service';
}),
KoishiModule.register({
prefix: '__never_prefix',
help: false,
minSimilarity: 1,
useWs: true,
}),
......
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Adapter, Context, Session } from 'koishi';
import PluginOnebot from '../adapter-onebot';
import { ConfigService } from '@nestjs/config';
import { InjectContext, PluginDef, UsePlugin } from 'koishi-nestjs';
import { BotConfig } from '../adapter-onebot';
import { AdapterConfig } from '../adapter-onebot/utils';
declare module 'koishi' {
interface EventMap {
......@@ -14,24 +19,14 @@ Adapter.prototype.dispatch = function (this: Adapter, session: Session) {
this.ctx.emit(session, 'dispatch', session);
};
import PluginOnebot from '@koishijs/plugin-adapter-onebot';
import { ConfigService } from '@nestjs/config';
import { InjectContext, PluginDef, UsePlugin } from 'koishi-nestjs';
import { BotConfig } from '@koishijs/plugin-adapter-onebot/lib/bot';
import { AdapterConfig } from '@koishijs/plugin-adapter-onebot/lib/utils';
@Injectable()
export class BotLoaderService implements OnModuleInit {
constructor(
private config: ConfigService,
@InjectContext() private ctx: Context,
) {}
export class BotLoaderService {
constructor(private config: ConfigService) {}
@UsePlugin()
loadBots() {
const onebotConfig = this.config.get<
Adapter.PluginConfig<AdapterConfig, BotConfig>
>('onebot');
const onebotConfig =
this.config.get<Adapter.PluginConfig<AdapterConfig, BotConfig>>('onebot');
if (onebotConfig.selfId) {
onebotConfig.selfId = onebotConfig.selfId.toString();
}
......@@ -42,13 +37,4 @@ export class BotLoaderService implements OnModuleInit {
}
return PluginDef(PluginOnebot, onebotConfig);
}
onModuleInit() {
const helpCommand = this.ctx.command('help');
if (!helpCommand) {
return;
}
const helpCtx = helpCommand.context;
helpCommand.context = helpCtx.never();
}
}
import { Injectable } from '@nestjs/common';
import { WireContextService } from 'koishi-nestjs';
import { OneBotBot } from '@koishijs/plugin-adapter-onebot/lib/bot';
import { OneBotBot } from '../adapter-onebot';
import { Adapter } from 'koishi';
@Injectable()
export class BotRegistryService {
@WireContextService('bots')
private bots: OneBotBot[];
private bots: Adapter.BotList;
private botMap = new Map<string, OneBotBot>();
getBotWithId(selfId: string) {
if (!this.botMap.has(selfId)) {
const bot = this.bots.find((bot) => bot.selfId === selfId);
if (bot) {
this.botMap.set(selfId, bot);
}
}
return this.botMap.get(selfId);
getBotWithId(selfId: string): OneBotBot {
return this.bots.get(`onebot:${selfId}`) as OneBotBot;
}
getAllBots() {
return this.bots;
getAllBots(): OneBotBot[] {
return this.bots as unknown as OneBotBot[];
}
}
......@@ -5,6 +5,7 @@ export class HealthInfoDto {
name: string;
@ApiProperty({ description: '是否健康' })
healthy: boolean;
constructor(name: string, healthy: boolean) {
this.name = name;
this.healthy = healthy;
......
......@@ -19,6 +19,7 @@ export class BlankReturnMessageDto implements BlankReturnMessage {
message: string;
@ApiProperty({ description: '是否成功' })
success: boolean;
constructor(statusCode: number, message?: string) {
this.statusCode = statusCode;
this.message = message || 'success';
......@@ -32,9 +33,11 @@ export class BlankReturnMessageDto implements BlankReturnMessage {
export class ReturnMessageDto<T>
extends BlankReturnMessageDto
implements ReturnMessage<T> {
implements ReturnMessage<T>
{
@ApiProperty({ description: '返回内容' })
data?: T;
constructor(statusCode: number, message?: string, data?: T) {
super(statusCode, message);
this.data = data;
......
import { Injectable } from '@nestjs/common';
import { RouteService } from '../route/route.service';
import { InjectContext } from 'koishi-nestjs';
import { Context } from 'koishi';
import { HealthInfoDto } from '../dto/HealthInfo.dto';
import { BotRegistryService } from '../bot-registry/bot-registry.service';
......
......@@ -28,4 +28,5 @@ async function bootstrap() {
config.get<string>('host') || '::',
);
}
bootstrap();
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { InjectContext } from 'koishi-nestjs';
import { Context } from 'koishi';
import WebSocket from 'ws';
import { Route } from '../route/Route';
import { genMetaEvent } from '../utility/oicq';
import {
OnebotProtocol,
OnebotAsyncResponseWithEcho,
OnebotProtocol,
} from '../utility/onebot-protocol';
import { OneBotBot } from '@koishijs/plugin-adapter-onebot/lib/bot';
import { OneBotBot } from '../adapter-onebot';
import { WaitBotService } from '../wait-bot/wait-bot.service';
import { BotRegistryService } from '../bot-registry/bot-registry.service';
......
......@@ -2,7 +2,6 @@ import {
OnGatewayConnection,
OnGatewayDisconnect,
WebSocketGateway,
WsException,
} from '@nestjs/websockets';
import { IncomingMessage } from 'http';
import { RouteService } from './route/route.service';
......@@ -21,15 +20,18 @@ interface ClientInfo {
@WebSocketGateway({ path: '^/route/(.+?)/?$' })
export class OnebotGateway
extends ConsoleLogger
implements OnGatewayConnection, OnGatewayDisconnect {
implements OnGatewayConnection, OnGatewayDisconnect
{
constructor(
private routeService: RouteService,
private messageService: MessageService,
) {
super('ws');
}
private clientRouteMap = new Map<WebSocket, ClientInfo>();
private matchingRegex = new RegExp('^/route/(.+?)/?$');
handleConnection(client: WebSocket, request: IncomingMessage) {
const baseUrl = 'ws://' + request.headers.host + '/';
const url = new URL(request.url, baseUrl);
......@@ -62,6 +64,7 @@ export class OnebotGateway
`Client ${clientInfo.ip} of route ${clientInfo.routeName} connected.`,
);
}
handleDisconnect(client: WebSocket) {
const clientInfo = this.clientRouteMap.get(client);
if (!clientInfo) {
......
......@@ -9,6 +9,7 @@ export class ReverseWsService extends ConsoleLogger {
constructor(private meesageService: MessageService) {
super('reverse-ws');
}
initializeReverseWs(route: Route, revConfig: ReverseWsConfig) {
const headers: OutgoingHttpHeaders = {
'X-Self-ID': route.selfId,
......
import type WebSocket from 'ws';
import { Context, Session, Selection } from 'koishi';
import { Random, remove } from 'koishi';
import { Context, Random, remove, Selection, Session } from 'koishi';
import { createHash } from 'crypto';
import { SendTask } from '../message/message.service';
import { HealthInfoDto } from '../dto/HealthInfo.dto';
......@@ -26,6 +25,7 @@ export interface RouteConfig {
bufferAppMessage?: boolean;
bufferBotMessage?: boolean;
}
export class Route implements RouteConfig {
private connections: WebSocket[] = [];
private roundCount = 0;
......@@ -43,6 +43,7 @@ export class Route implements RouteConfig {
bufferAppMessage?: boolean;
bufferBotMessage?: boolean;
preMessages: { data: any; session: Session }[] = [];
constructor(routeConfig: RouteConfig, ctx: Context) {
Object.assign(this, routeConfig);
this.balancePolicy ||= 'hash';
......@@ -61,12 +62,15 @@ export class Route implements RouteConfig {
}, this.heartbeat);
}
}
isHealthy() {
return this.connections.length > 0;
}
getHealthyInfo() {
return new HealthInfoDto(this.name, this.isHealthy());
}
send(data: any, session: Session, allConns = this.connections) {
if (!allConns.length) {
if (this.bufferAppMessage) {
......@@ -96,6 +100,7 @@ export class Route implements RouteConfig {
});
}
}
broadcast(data: any) {
const message = JSON.stringify(data);
for (const conn of this.connections) {
......@@ -108,6 +113,7 @@ export class Route implements RouteConfig {
});
}
}
getFilteredContext(ctx: Context) {
const idCtx = ctx.self(this.selfId);
if (!this.select) {
......@@ -115,6 +121,7 @@ export class Route implements RouteConfig {
}
return idCtx.select(this.select);
}
static sessionKeys: (keyof Session)[] = [
'selfId',
'guildId',
......@@ -125,6 +132,7 @@ export class Route implements RouteConfig {
'subtype',
'subsubtype',
];
private getSequenceFromSession(sess: Session) {
const hash = createHash('md5');
for (const key of Route.sessionKeys) {
......@@ -135,6 +143,7 @@ export class Route implements RouteConfig {
}
return parseInt(hash.digest('hex'), 16) % 4294967295;
}
getRelatedConnections(
sess: Session,
allConns = this.connections,
......@@ -159,6 +168,7 @@ export class Route implements RouteConfig {
return [];
}
}
addConnection(conn: WebSocket) {
this.connections.push(conn);
if (!this.bufferAppMessage) {
......@@ -170,12 +180,15 @@ export class Route implements RouteConfig {
this.send(message.data, message.session);
}
}
removeConnection(conn: WebSocket) {
remove(this.connections, conn);
}
addSendTask(task: SendTask) {
this.sendQueue.push(task);
}
fetchSendTask() {
if (!this.sendQueue.length) {
return;
......
......@@ -13,8 +13,10 @@ import { MessageService } from '../message/message.service';
@Injectable()
export class RouteService
extends ConsoleLogger
implements OnApplicationBootstrap {
implements OnApplicationBootstrap
{
private routes = new Map<string, Route>();
constructor(
config: ConfigService,
@InjectContextPlatform('onebot') private ctx: Context,
......
......@@ -2,8 +2,8 @@ import yaml from 'yaml';
import * as fs from 'fs';
import { RouteConfig } from '../route/Route';
import { Adapter } from 'koishi';
import { AdapterConfig } from '@koishijs/plugin-adapter-onebot/lib/utils';
import { BotConfig } from '@koishijs/plugin-adapter-onebot/lib/bot';
import { AdapterConfig } from '../adapter-onebot/utils';
import { BotConfig } from '../adapter-onebot';
export interface LbConfig {
host: string;
......
......@@ -14,6 +14,7 @@ export const BOOLS = [
'approve',
'block',
];
export function toBool(v: any) {
if (v === '0' || v === 'false') v = false;
return Boolean(v);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment