Commit 63b67403 authored by nanahira's avatar nanahira

migrate to 4.0.0

parent 48036c61
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
"version": "1.4.4", "version": "1.4.4",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@koishijs/plugin-adapter-onebot": "^4.0.0-rc.0",
"@nestjs/common": "^8.0.0", "@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.3", "@nestjs/config": "^1.0.3",
"@nestjs/core": "^8.0.0", "@nestjs/core": "^8.0.0",
...@@ -18,8 +17,9 @@ ...@@ -18,8 +17,9 @@
"@nestjs/websockets": "^8.1.2", "@nestjs/websockets": "^8.1.2",
"class-transformer": "^0.4.0", "class-transformer": "^0.4.0",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"koishi": "^4.0.0-rc.1", "koishi": "^4.0.0",
"koishi-nestjs": "^4.0.0", "koishi-nestjs": "^4.3.0",
"qface": "^1.2.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
...@@ -1348,54 +1348,22 @@ ...@@ -1348,54 +1348,22 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
}, },
"node_modules/@koishijs/core": { "node_modules/@koishijs/core": {
"version": "4.0.0-rc.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0-rc.1.tgz", "resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0.tgz",
"integrity": "sha512-5T2+DUvm2TUpNjWgLD6mlh8+2pNN1edF/80A1eKy5sXyrA86dzR9VkHWysY+ZZpEVedzpVPkc9RXlLwN3byQ2g==", "integrity": "sha512-xonpueYfWhcTw2eJVc/SG7EwutFgzqtvdpuCGoBFvfRRSZlHOWAQfWsRw5he06+qn7rVOj3cyxrtFMP0fAthNA==",
"dependencies": { "dependencies": {
"@koishijs/utils": "^5.0.0-rc.0", "@koishijs/utils": "^5.0.0",
"fastest-levenshtein": "^1.0.12", "fastest-levenshtein": "^1.0.12",
"schemastery": "^2.1.2" "schemastery": "^2.1.3"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "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": { "node_modules/@koishijs/utils": {
"version": "5.0.0-rc.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0-rc.0.tgz", "resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0.tgz",
"integrity": "sha512-aG1FjFB9NKiSqGS/tjjsSnmSfrW5yXQK7aZAR7DhsSaiF+/CdWZ0sQ9XS830xZ6PV1UwKib9alluVMULyWvrbA==", "integrity": "sha512-3ng7VkQZAP+EHdRLSbio5H36LDKRxy4OfooRKgol+gef2Yd0KOKYbVmGVud7/mZZExCPkaZVCwocKB/bKA4Xvw==",
"dependencies": { "dependencies": {
"supports-color": "^8.1.0" "supports-color": "^8.1.0"
} }
...@@ -6888,13 +6856,13 @@ ...@@ -6888,13 +6856,13 @@
} }
}, },
"node_modules/koishi": { "node_modules/koishi": {
"version": "4.0.0-rc.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0-rc.1.tgz", "resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0.tgz",
"integrity": "sha512-liXILmB7yHaUUOqNqEZ4/Q/bZwsBVsQ8aIwQ7JolZfxQjlZtmwE69B3AEYmY2dWcO3+lnJhjCiBkvKGj01kpSg==", "integrity": "sha512-7m9kv+8EOJlOOrwnk3KolU+Dt+tjd8fp2xNQviHj7+Z97kQlc6AnS+gXExikiiQHJpPshSgkBzbxbP1HuV668w==",
"dependencies": { "dependencies": {
"@koa/router": "^10.1.1", "@koa/router": "^10.1.1",
"@koishijs/core": "^4.0.0-rc.1", "@koishijs/core": "^4.0.0",
"@koishijs/utils": "^5.0.0-rc.0", "@koishijs/utils": "^5.0.0",
"@types/koa": "*", "@types/koa": "*",
"@types/koa__router": "*", "@types/koa__router": "*",
"@types/ws": "^7.4.7", "@types/ws": "^7.4.7",
...@@ -6912,9 +6880,9 @@ ...@@ -6912,9 +6880,9 @@
} }
}, },
"node_modules/koishi-nestjs": { "node_modules/koishi-nestjs": {
"version": "4.0.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.0.0.tgz", "resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.3.0.tgz",
"integrity": "sha512-kYWOwYkJSmrZuX6fPA8cTa7tCt+F/sVWKdw2KSEaqRIsLvv9J+d8kjh3GLs9SJIm99S43yaFhNntWxyT2XKUcA==", "integrity": "sha512-E7rmi08mlJsUAOaMxEFcCMsffzYHXwON2aQTD87M5/fj+eDhuU3lUBZBidXeuEE4YLlbjIS0MRImqV+SR1tnAQ==",
"dependencies": { "dependencies": {
"@nestjs/platform-ws": "^8.1.2", "@nestjs/platform-ws": "^8.1.2",
"@nestjs/websockets": "^8.1.2", "@nestjs/websockets": "^8.1.2",
...@@ -6927,7 +6895,7 @@ ...@@ -6927,7 +6895,7 @@
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^8.0.0", "@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0", "@nestjs/core": "^8.0.0",
"koishi": "^4.0.0-rc.1", "koishi": "^4.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.4.0" "rxjs": "^7.4.0"
} }
...@@ -8399,9 +8367,9 @@ ...@@ -8399,9 +8367,9 @@
"dev": true "dev": true
}, },
"node_modules/schemastery": { "node_modules/schemastery": {
"version": "2.1.2", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.2.tgz", "resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.3.tgz",
"integrity": "sha512-iHwWfKxWaTFgZmKNULNtgyo8VDpdEWx31b6+j0tGTST8dBIYU7VYAHmq5qdYKYZ0uNSM5u57c09cuN75Yf7WwQ==" "integrity": "sha512-AH6dgucxUSkuK/LvIJtPor8/6KCEq5L9weD5JQJ6/HEDFqD4KbO7NqQsJmbW4TKubZ4FtPj7eXSEaz5nie+y+A=="
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.3.5", "version": "7.3.5",
...@@ -11046,36 +11014,19 @@ ...@@ -11046,36 +11014,19 @@
} }
}, },
"@koishijs/core": { "@koishijs/core": {
"version": "4.0.0-rc.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0-rc.1.tgz", "resolved": "https://registry.npmjs.org/@koishijs/core/-/core-4.0.0.tgz",
"integrity": "sha512-5T2+DUvm2TUpNjWgLD6mlh8+2pNN1edF/80A1eKy5sXyrA86dzR9VkHWysY+ZZpEVedzpVPkc9RXlLwN3byQ2g==", "integrity": "sha512-xonpueYfWhcTw2eJVc/SG7EwutFgzqtvdpuCGoBFvfRRSZlHOWAQfWsRw5he06+qn7rVOj3cyxrtFMP0fAthNA==",
"requires": { "requires": {
"@koishijs/utils": "^5.0.0-rc.0", "@koishijs/utils": "^5.0.0",
"fastest-levenshtein": "^1.0.12", "fastest-levenshtein": "^1.0.12",
"schemastery": "^2.1.2" "schemastery": "^2.1.3"
}
},
"@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": {}
}
} }
}, },
"@koishijs/utils": { "@koishijs/utils": {
"version": "5.0.0-rc.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0-rc.0.tgz", "resolved": "https://registry.npmjs.org/@koishijs/utils/-/utils-5.0.0.tgz",
"integrity": "sha512-aG1FjFB9NKiSqGS/tjjsSnmSfrW5yXQK7aZAR7DhsSaiF+/CdWZ0sQ9XS830xZ6PV1UwKib9alluVMULyWvrbA==", "integrity": "sha512-3ng7VkQZAP+EHdRLSbio5H36LDKRxy4OfooRKgol+gef2Yd0KOKYbVmGVud7/mZZExCPkaZVCwocKB/bKA4Xvw==",
"requires": { "requires": {
"supports-color": "^8.1.0" "supports-color": "^8.1.0"
}, },
...@@ -15269,13 +15220,13 @@ ...@@ -15269,13 +15220,13 @@
} }
}, },
"koishi": { "koishi": {
"version": "4.0.0-rc.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0-rc.1.tgz", "resolved": "https://registry.npmjs.org/koishi/-/koishi-4.0.0.tgz",
"integrity": "sha512-liXILmB7yHaUUOqNqEZ4/Q/bZwsBVsQ8aIwQ7JolZfxQjlZtmwE69B3AEYmY2dWcO3+lnJhjCiBkvKGj01kpSg==", "integrity": "sha512-7m9kv+8EOJlOOrwnk3KolU+Dt+tjd8fp2xNQviHj7+Z97kQlc6AnS+gXExikiiQHJpPshSgkBzbxbP1HuV668w==",
"requires": { "requires": {
"@koa/router": "^10.1.1", "@koa/router": "^10.1.1",
"@koishijs/core": "^4.0.0-rc.1", "@koishijs/core": "^4.0.0",
"@koishijs/utils": "^5.0.0-rc.0", "@koishijs/utils": "^5.0.0",
"@types/koa": "*", "@types/koa": "*",
"@types/koa__router": "*", "@types/koa__router": "*",
"@types/ws": "^7.4.7", "@types/ws": "^7.4.7",
...@@ -15311,9 +15262,9 @@ ...@@ -15311,9 +15262,9 @@
} }
}, },
"koishi-nestjs": { "koishi-nestjs": {
"version": "4.0.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.0.0.tgz", "resolved": "https://registry.npmjs.org/koishi-nestjs/-/koishi-nestjs-4.3.0.tgz",
"integrity": "sha512-kYWOwYkJSmrZuX6fPA8cTa7tCt+F/sVWKdw2KSEaqRIsLvv9J+d8kjh3GLs9SJIm99S43yaFhNntWxyT2XKUcA==", "integrity": "sha512-E7rmi08mlJsUAOaMxEFcCMsffzYHXwON2aQTD87M5/fj+eDhuU3lUBZBidXeuEE4YLlbjIS0MRImqV+SR1tnAQ==",
"requires": { "requires": {
"@nestjs/platform-ws": "^8.1.2", "@nestjs/platform-ws": "^8.1.2",
"@nestjs/websockets": "^8.1.2", "@nestjs/websockets": "^8.1.2",
...@@ -16399,9 +16350,9 @@ ...@@ -16399,9 +16350,9 @@
} }
}, },
"schemastery": { "schemastery": {
"version": "2.1.2", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.2.tgz", "resolved": "https://registry.npmjs.org/schemastery/-/schemastery-2.1.3.tgz",
"integrity": "sha512-iHwWfKxWaTFgZmKNULNtgyo8VDpdEWx31b6+j0tGTST8dBIYU7VYAHmq5qdYKYZ0uNSM5u57c09cuN75Yf7WwQ==" "integrity": "sha512-AH6dgucxUSkuK/LvIJtPor8/6KCEq5L9weD5JQJ6/HEDFqD4KbO7NqQsJmbW4TKubZ4FtPj7eXSEaz5nie+y+A=="
}, },
"semver": { "semver": {
"version": "7.3.5", "version": "7.3.5",
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@koishijs/plugin-adapter-onebot": "^4.0.0-rc.0",
"@nestjs/common": "^8.0.0", "@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.3", "@nestjs/config": "^1.0.3",
"@nestjs/core": "^8.0.0", "@nestjs/core": "^8.0.0",
...@@ -30,8 +29,9 @@ ...@@ -30,8 +29,9 @@
"@nestjs/websockets": "^8.1.2", "@nestjs/websockets": "^8.1.2",
"class-transformer": "^0.4.0", "class-transformer": "^0.4.0",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"koishi": "^4.0.0-rc.1", "koishi": "^4.0.0",
"koishi-nestjs": "^4.0.0", "koishi-nestjs": "^4.3.0",
"qface": "^1.2.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
......
import {
Bot,
segment,
Adapter,
Dict,
Schema,
Quester,
Logger,
camelize,
noop,
} from 'koishi';
import * as OneBot from './utils';
export function renderText(source: string) {
return segment.parse(source).reduce((prev, { type, data }) => {
if (type === 'at') {
if (data.type === 'all') return prev + '[CQ:at,qq=all]';
return prev + `[CQ:at,qq=${data.id}]`;
} else if (['video', 'audio', 'image'].includes(type)) {
if (type === 'audio') type = 'record';
if (!data.file) data.file = data.url;
} else if (type === 'quote') {
type = 'reply';
}
return prev + segment(type, data);
}, '');
}
export interface BotConfig extends Bot.BaseConfig, Quester.Config {
selfId?: string;
token?: string;
}
export const BotConfig: Schema<BotConfig> = Schema.intersect([
Schema.object({
selfId: Schema.string(),
token: Schema.string(),
}),
Quester.Config,
]);
export class OneBotBot extends Bot<BotConfig> {
static schema = OneBot.AdapterConfig;
public internal = new Internal();
public guildBot: QQGuildBot;
constructor(adapter: Adapter, config: BotConfig) {
super(adapter, config);
this.selfId = config.selfId;
this.avatar = `http://q.qlogo.cn/headimg_dl?dst_uin=${config.selfId}&spec=640`;
}
get status() {
return super.status;
}
set status(status) {
super.status = status;
if (this.guildBot && this.app.bots.includes(this.guildBot)) {
this.app.emit('bot-status-updated', this.guildBot);
}
}
async stop() {
if (this.guildBot) {
// QQGuild stub bot should also be removed
await this.app.bots.remove(this.guildBot.sid);
}
await super.stop();
}
async initialize() {
await Promise.all([
this.getSelf().then((data) => Object.assign(this, data)),
// this.setupGuildService().catch(noop),
]).then(
() => this.resolve(),
(error) => this.reject(error),
);
}
async setupGuildService() {
const profile = await this.internal.getGuildServiceProfile();
// guild service is not supported in this account
if (!profile?.tiny_id || profile.tiny_id === '0') return;
this.guildBot = this.app.bots.create('onebot', this.config, QQGuildBot);
this.guildBot.internal = this.internal;
this.guildBot.parentBot = this;
this.guildBot.platform = 'qqguild';
this.guildBot.selfId = profile.tiny_id;
this.guildBot.avatar = profile.avatar_url;
this.guildBot.username = profile.nickname;
}
sendMessage(channelId: string, content: string, guildId?: string) {
content = renderText(content);
return channelId.startsWith('private:')
? this.sendPrivateMessage(channelId.slice(8), content)
: this.sendGuildMessage(guildId, channelId, content);
}
async getMessage(channelId: string, messageId: string) {
const data = await this.internal.getMsg(messageId);
return OneBot.adaptMessage(data);
}
async deleteMessage(channelId: string, messageId: string) {
await this.internal.deleteMsg(messageId);
}
async getSelf() {
const data = await this.internal.getLoginInfo();
return OneBot.adaptUser(data);
}
async getUser(userId: string) {
const data = await this.internal.getStrangerInfo(userId);
return OneBot.adaptUser(data);
}
async getFriendList() {
const data = await this.internal.getFriendList();
return data.map(OneBot.adaptUser);
}
async getChannel(channelId: string) {
const data = await this.internal.getGroupInfo(channelId);
return OneBot.adaptChannel(data);
}
async getGuild(guildId: string) {
const data = await this.internal.getGroupInfo(guildId);
return OneBot.adaptGuild(data);
}
async getGuildList() {
const data = await this.internal.getGroupList();
return data.map(OneBot.adaptGuild);
}
async getGuildMember(guildId: string, userId: string) {
const data = await this.internal.getGroupMemberInfo(guildId, userId);
return OneBot.adaptGuildMember(data);
}
async getGuildMemberList(guildId: string) {
const data = await this.internal.getGroupMemberList(guildId);
return data.map(OneBot.adaptGuildMember);
}
protected async sendGuildMessage(
guildId: string,
channelId: string,
content: string,
) {
if (!content) return;
const session = this.createSession({
content,
subtype: 'group',
guildId,
channelId,
});
if (this.app.bail(session, 'before-send', session)) return;
session.messageId =
'' + (await this.internal.sendGroupMsg(channelId, content));
this.app.emit(session, 'send', session);
return [session.messageId];
}
async sendPrivateMessage(userId: string, content: string) {
if (!content) return;
const session = this.createSession({
content,
subtype: 'private',
userId,
channelId: 'private:' + userId,
});
if (this.app.bail(session, 'before-send', session)) return;
session.messageId =
'' + (await this.internal.sendPrivateMsg(userId, content));
this.app.emit(session, 'send', session);
return [session.messageId];
}
async handleFriendRequest(
messageId: string,
approve: boolean,
comment?: string,
) {
await this.internal.setFriendAddRequest(messageId, approve, comment);
}
async handleGuildRequest(
messageId: string,
approve: boolean,
comment?: string,
) {
await this.internal.setGroupAddRequest(
messageId,
'invite',
approve,
comment,
);
}
async handleGuildMemberRequest(
messageId: string,
approve: boolean,
comment?: string,
) {
await this.internal.setGroupAddRequest(messageId, 'add', approve, comment);
}
async deleteFriend(userId: string) {
await this.internal.deleteFriend(userId);
}
}
export class QQGuildBot extends OneBotBot {
parentBot: OneBotBot;
get status() {
if (!this.parentBot) {
return 'offline';
}
return this.parentBot.status;
}
set status(status) {
// cannot change status here
}
async start() {
await this.app.parallel('bot-connect', this);
}
async stop() {
// Don't stop this bot twice
if (!this.parentBot) return;
// prevent circular reference and use this as already disposed
this.parentBot = undefined;
await this.app.parallel('bot-disconnect', this);
}
async sendGuildMessage(guildId: string, channelId: string, content: string) {
if (!content) return;
const session = this.createSession({
content,
subtype: 'group',
guildId,
channelId,
});
if (this.app.bail(session, 'before-send', session)) return;
session.messageId =
'' +
(await this.internal.sendGuildChannelMsg(guildId, channelId, content));
this.app.emit(session, 'send', session);
return [session.messageId];
}
async getChannel(channelId: string, guildId?: string) {
const channels = await this.getChannelList(guildId);
return channels.find((channel) => channel.channelId === channelId);
}
async getChannelList(guildId: string) {
const data = await this.internal.getGuildChannelList(guildId, false);
return (data || []).map(OneBot.adaptChannel);
}
async getGuild(guildId: string) {
const data = await this.internal.getGuildMetaByGuest(guildId);
return OneBot.adaptGuild(data);
}
async getGuildList() {
const data = await this.internal.getGuildList();
return data.map(OneBot.adaptGuild);
}
async getGuildMember(guildId: string, userId: string) {
const memberList = await this.getGuildMemberList(guildId);
return memberList.find((member) => member.userId === userId);
}
async getGuildMemberList(guildId: string) {
const { members, bots, admins } = await this.internal.getGuildMembers(
guildId,
);
return [
...(members || []).map((member) =>
OneBot.adaptQQGuildMember(member, 'member'),
),
...(bots || []).map((member) => OneBot.adaptQQGuildMember(member, 'bot')),
...(admins || []).map((member) =>
OneBot.adaptQQGuildMember(member, 'admin'),
),
];
}
}
class SenderError extends Error {
constructor(args: Dict, url: string, retcode: number) {
super(
`Error when trying to send to ${url}, args: ${JSON.stringify(
args,
)}, retcode: ${retcode}`,
);
Object.defineProperties(this, {
name: { value: 'SenderError' },
code: { value: retcode },
args: { value: args },
url: { value: url },
});
}
}
const logger = new Logger('onebot');
export interface Internal extends OneBot.Internal {
noop: never;
}
export class Internal {
_request?(action: string, params: Dict): Promise<OneBot.Response>;
private async _get<T = any>(action: string, params = {}): Promise<T> {
logger.debug('[request] %s %o', action, params);
const response = await this._request(action, params);
logger.debug('[response] %o', response);
const { data, retcode } = response;
if (retcode === 0) return data;
throw new SenderError(params, action, retcode);
}
async setGroupAnonymousBan(
group_id: string,
meta: string | object,
duration?: number,
) {
const args = { group_id, duration } as any;
args[typeof meta === 'string' ? 'flag' : 'anonymous'] = meta;
await this._get('set_group_anonymous_ban', args);
}
async setGroupAnonymousBanAsync(
group_id: string,
meta: string | object,
duration?: number,
) {
const args = { group_id, duration } as any;
args[typeof meta === 'string' ? 'flag' : 'anonymous'] = meta;
await this._get('set_group_anonymous_ban_async', args);
}
private static asyncPrefixes = ['set', 'send', 'delete', 'create', 'upload'];
private static prepareMethod(name: string) {
const prop = camelize(name.replace(/^[_.]/, ''));
const isAsync = Internal.asyncPrefixes.some((prefix) =>
prop.startsWith(prefix),
);
return [prop, isAsync] as const;
}
static define(name: string, ...params: string[]) {
const [prop, isAsync] = Internal.prepareMethod(name);
Internal.prototype[prop] = async function (this: Internal, ...args: any[]) {
const data = await this._get(
name,
Object.fromEntries(params.map((name, index) => [name, args[index]])),
);
if (!isAsync) return data;
};
isAsync &&
(Internal.prototype[prop + 'Async'] = async function (
this: Internal,
...args: any[]
) {
await this._get(
name + '_async',
Object.fromEntries(params.map((name, index) => [name, args[index]])),
);
});
}
static defineExtract(name: string, key: string, ...params: string[]) {
const [prop, isAsync] = Internal.prepareMethod(name);
Internal.prototype[prop] = async function (this: Internal, ...args: any[]) {
const data = await this._get(
name,
Object.fromEntries(params.map((name, index) => [name, args[index]])),
);
return data[key];
};
isAsync &&
(Internal.prototype[prop + 'Async'] = async function (
this: Internal,
...args: any[]
) {
await this._get(
name + '_async',
Object.fromEntries(params.map((name, index) => [name, args[index]])),
);
});
}
}
Internal.defineExtract(
'send_private_msg',
'message_id',
'user_id',
'message',
'auto_escape',
);
Internal.defineExtract(
'send_group_msg',
'message_id',
'group_id',
'message',
'auto_escape',
);
Internal.defineExtract(
'send_group_forward_msg',
'message_id',
'group_id',
'messages',
);
Internal.define('delete_msg', 'message_id');
Internal.define('set_essence_msg', 'message_id');
Internal.define('delete_essence_msg', 'message_id');
Internal.define('send_like', 'user_id', 'times');
Internal.define('get_msg', 'message_id');
Internal.define('get_essence_msg_list', 'group_id');
Internal.define('ocr_image', 'image');
Internal.defineExtract('get_forward_msg', 'messages', 'message_id');
Internal.defineExtract('.get_word_slices', 'slices', 'content');
Internal.define('get_group_msg_history', 'group_id', 'message_seq');
Internal.define('set_friend_add_request', 'flag', 'approve', 'remark');
Internal.define(
'set_group_add_request',
'flag',
'sub_type',
'approve',
'reason',
);
Internal.defineExtract('_get_model_show', 'variants', 'model');
Internal.define('_set_model_show', 'model', 'model_show');
Internal.define('set_group_kick', 'group_id', 'user_id', 'reject_add_request');
Internal.define('set_group_ban', 'group_id', 'user_id', 'duration');
Internal.define('set_group_whole_ban', 'group_id', 'enable');
Internal.define('set_group_admin', 'group_id', 'user_id', 'enable');
Internal.define('set_group_anonymous', 'group_id', 'enable');
Internal.define('set_group_card', 'group_id', 'user_id', 'card');
Internal.define('set_group_leave', 'group_id', 'is_dismiss');
Internal.define(
'set_group_special_title',
'group_id',
'user_id',
'special_title',
'duration',
);
Internal.define('set_group_name', 'group_id', 'group_name');
Internal.define('set_group_portrait', 'group_id', 'file', 'cache');
Internal.define('_send_group_notice', 'group_id', 'content');
Internal.define('get_group_at_all_remain', 'group_id');
Internal.define('get_login_info');
Internal.define('get_stranger_info', 'user_id', 'no_cache');
Internal.define('_get_vip_info', 'user_id');
Internal.define('get_friend_list');
Internal.define('get_group_info', 'group_id', 'no_cache');
Internal.define('get_group_list');
Internal.define('get_group_member_info', 'group_id', 'user_id', 'no_cache');
Internal.define('get_group_member_list', 'group_id');
Internal.define('get_group_honor_info', 'group_id', 'type');
Internal.define('get_group_system_msg');
Internal.define('get_group_file_system_info', 'group_id');
Internal.define('get_group_root_files', 'group_id');
Internal.define('get_group_files_by_folder', 'group_id', 'folder_id');
Internal.define('upload_group_file', 'group_id', 'file', 'name', 'folder');
Internal.define('create_group_file_folder', 'group_id', 'folder_id', 'name');
Internal.define('delete_group_folder', 'group_id', 'folder_id');
Internal.define(
'delete_group_file',
'group_id',
'folder_id',
'file_id',
'busid',
);
Internal.defineExtract(
'get_group_file_url',
'url',
'group_id',
'file_id',
'busid',
);
Internal.defineExtract(
'download_file',
'file',
'url',
'headers',
'thread_count',
);
Internal.defineExtract('get_online_clients', 'clients', 'no_cache');
Internal.defineExtract('check_url_safely', 'level', 'url');
Internal.define('delete_friend', 'user_id');
Internal.defineExtract('get_cookies', 'cookies', 'domain');
Internal.defineExtract('get_csrf_token', 'token');
Internal.define('get_credentials', 'domain');
Internal.define('get_record', 'file', 'out_format', 'full_path');
Internal.define('get_image', 'file');
Internal.defineExtract('can_send_image', 'yes');
Internal.defineExtract('can_send_record', 'yes');
Internal.define('get_status');
Internal.define('get_version_info');
Internal.define('set_restart', 'delay');
Internal.define('reload_event_filter');
Internal.define('get_guild_service_profile');
Internal.define('get_guild_list');
Internal.define('get_guild_meta_by_guest', 'guild_id');
Internal.define('get_guild_channel_list', 'guild_id', 'no_cache');
Internal.define('get_guild_members', 'guild_id');
Internal.defineExtract(
'send_guild_channel_msg',
'message_id',
'guild_id',
'channel_id',
'message',
);
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';
},
);
export interface Response {
status: string;
retcode: number;
data: any;
echo?: number;
}
export interface MessageId {
message_id: number;
}
export interface AccountInfo {
user_id: string;
tiny_id?: string;
nickname: string;
}
export interface StrangerInfo extends AccountInfo {
sex: 'male' | 'female' | 'unknown';
age: number;
}
export interface TalkativeMemberInfo extends AccountInfo {
avatar: string;
day_count: number;
}
export type GroupRole = 'member' | 'admin' | 'owner';
export type HonorType =
| 'talkative'
| 'performer'
| 'legend'
| 'strong_newbie'
| 'emotion';
export interface HonoredMemberInfo {
avatar: string;
description: string;
}
export interface HonorInfo {
current_talkative: TalkativeMemberInfo;
talkative_list: HonoredMemberInfo[];
performer_list: HonoredMemberInfo[];
legend_list: HonoredMemberInfo[];
strong_newbie_list: HonoredMemberInfo[];
emotion_list: HonoredMemberInfo[];
}
export interface SenderInfo extends StrangerInfo {
area?: string;
level?: string;
title?: string;
role?: GroupRole;
card?: string;
}
export interface Message extends MessageId {
real_id?: number;
time: number;
message_type: 'private' | 'group' | 'guild';
sender: SenderInfo;
group_id?: number;
guild_id?: string;
channel_id?: string;
message: string | any[];
anonymous?: AnonymousInfo;
}
export interface AnonymousInfo {
id: number;
name: string;
flag: string;
}
export type RecordFormat =
| 'mp3'
| 'amr'
| 'wma'
| 'm4a'
| 'spx'
| 'ogg'
| 'wav'
| 'flac';
export type DataDirectory = 'image' | 'record' | 'show' | 'bface';
export interface FriendInfo extends AccountInfo {
remark: string;
}
export interface GroupBase {
group_id: number;
group_name: string;
}
export interface GroupInfo extends GroupBase {
member_count: number;
max_member_count: number;
}
export interface GroupMemberInfo extends SenderInfo {
card_changeable: boolean;
group_id: number;
join_time: number;
last_sent_time: number;
title_expire_time: number;
unfriendly: boolean;
}
export interface Credentials {
cookies: string;
csrf_token: number;
}
export interface ImageInfo {
file: string;
}
export interface RecordInfo {
file: string;
}
export interface VersionInfo {
coolq_directory: string;
coolq_edition: 'air' | 'pro';
plugin_version: string;
plugin_build_number: number;
plugin_build_configuration: 'debug' | 'release';
version?: string;
go_cqhttp?: boolean;
runtime_version?: string;
runtime_os?: string;
}
export interface ImageInfo {
size?: number;
filename?: string;
url?: string;
}
export interface ForwardMessage {
sender: AccountInfo;
time: number;
content: string;
}
export interface EssenceMessage extends MessageId {
sender_id: number;
sender_nick: string;
sender_time: number;
operator_id: number;
operator_nick: string;
operator_time: number;
}
interface CQNode {
type: 'node';
data:
| {
id: number;
}
| {
name: string;
uin: number;
content: string;
};
}
export interface VipInfo extends AccountInfo {
level: number;
level_speed: number;
vip_level: number;
vip_growth_speed: number;
vip_growth_total: string;
}
export interface GroupNotice {
cn: number;
fid: string;
fn: number;
msg: {
text: string;
text_face: string;
title: string;
};
pubt: number;
read_num: number;
settings: {
is_show_edit_card: number;
remind_ts: number;
};
u: number;
vn: number;
}
export interface Statistics {
packet_received: number;
packet_sent: number;
packet_lost: number;
message_received: number;
message_sent: number;
disconnect_times: number;
lost_times: number;
}
export interface StatusInfo {
app_initialized: boolean;
app_enabled: boolean;
plugins_good: boolean;
app_good: boolean;
online: boolean;
good: boolean;
stat: Statistics;
}
export interface TextDetection {
text: string;
confidence: string;
coordinates: any;
}
export interface OcrResult {
language: string;
texts: TextDetection[];
}
export interface GroupRequest extends GroupBase {
request_id: number;
invitor_uin: number;
invitor_nick: string;
checked: boolean;
actor: number;
}
export type InvitedRequest = GroupRequest;
export interface JoinRequest extends GroupRequest {
message: string;
}
export interface GroupSystemMessageInfo {
invited_qequests: InvitedRequest[];
join_requests: JoinRequest[];
}
export interface GroupFileSystemInfo {
file_count: number;
limit_count: number;
used_space: number;
total_space: number;
}
export interface GroupFile {
file_id: string;
file_name: string;
busid: number;
file_size: number;
upload_time: number;
dead_time: number;
modify_time: number;
download_time: number;
uploader: number;
uploader_name: string;
}
export interface GroupFolder {
folder_id: string;
folder_name: string;
create_time: number;
creator: number;
creator_name: string;
total_file_count: number;
}
export interface GroupFileList {
files: GroupFile[];
folders: GroupFolder[];
}
export interface AtAllRemain {
can_at_all: boolean;
remain_at_all_count_for_group: number;
remain_at_all_count_for_uin: number;
}
export interface Device {
app_id: number;
device_name: string;
device_kind: string;
}
export interface ModelVariant {
model_show: string;
need_pay: boolean;
}
export enum SafetyLevel {
safe,
unknown,
danger,
}
export interface GuildServiceProfile {
nickname: string;
tiny_id: string;
avatar_url: string;
}
export interface GuildBaseInfo {
guild_id: string;
guild_name: string;
}
export interface GuildInfo extends GuildBaseInfo {
guild_display_id: string;
}
export interface GuildMeta extends GuildBaseInfo {
guild_profile: string;
create_time: number;
max_member_count: number;
max_robot_count: number;
max_admin_count: number;
member_count: number;
owner_id: string;
}
export interface ChannelInfo {
owner_guild_id: string;
channel_id: string;
channel_type: number;
channel_name: string;
create_time: number;
creator_id: string;
creator_tiny_id: string;
talk_permission: number;
visible_type: number;
current_slow_mode: number;
slow_modes: SlowModeInfo[];
}
export interface SlowModeInfo {
slow_mode_key: number;
slow_mode_text: string;
speak_frequency: number;
slow_mode_circle: number;
}
export interface GuildMemberInfo {
tiny_id: string;
title: string;
nickname: string;
role: number;
}
export interface GuildMembers {
members: GuildMemberInfo[];
bots: GuildMemberInfo[];
admins: GuildMemberInfo[];
}
export interface ReactionInfo {
emoji_id: string;
emoji_index: number;
emoji_type: number;
emoji_name: string;
count: number;
clicked: boolean;
}
export interface Payload extends Message {
time: number;
self_id: number;
self_tiny_id?: string;
post_type: string;
request_type: string;
notice_type: string;
meta_event_type: string;
honor_type: string;
sub_type: string;
message_id: number;
user_id: number;
target_id: number;
operator_id: number;
raw_message: string;
font: number;
comment: string;
flag: string;
old_info: ChannelInfo;
new_info: ChannelInfo;
channel_info: ChannelInfo;
current_reactions: ReactionInfo[];
}
type id = string | number;
export interface Internal {
sendPrivateMsg(
user_id: id,
message: string,
auto_escape?: boolean,
): Promise<number>;
sendPrivateMsgAsync(
user_id: id,
message: string,
auto_escape?: boolean,
): Promise<void>;
sendGroupMsg(
group_id: id,
message: string,
auto_escape?: boolean,
): Promise<number>;
sendGroupMsgAsync(
group_id: id,
message: string,
auto_escape?: boolean,
): Promise<void>;
sendGroupForwardMsg(
group_id: id,
messages: readonly CQNode[],
): Promise<number>;
sendGroupForwardMsgAsync(
group_id: id,
messages: readonly CQNode[],
): Promise<void>;
deleteMsg(message_id: id): Promise<void>;
deleteMsgAsync(message_id: id): Promise<void>;
setEssenceMsg(message_id: id): Promise<void>;
setEssenceMsgAsync(message_id: id): Promise<void>;
deleteEssenceMsg(message_id: id): Promise<void>;
deleteEssenceMsgAsync(message_id: id): Promise<void>;
sendLike(user_id: id, times?: number): Promise<void>;
sendLikeAsync(user_id: id, times?: number): Promise<void>;
getMsg(message_id: id): Promise<Message>;
getForwardMsg(message_id: id): Promise<ForwardMessage[]>;
getEssenceMsgList(group_id: id): Promise<EssenceMessage[]>;
getWordSlices(content: string): Promise<string[]>;
ocrImage(image: string): Promise<OcrResult>;
getGroupMsgHistory(group_id: id, message_seq: id): Promise<Message[]>;
deleteFriend(user_id: id): Promise<void>;
deleteFriendAsync(user_id: id): Promise<void>;
setFriendAddRequest(
flag: string,
approve: boolean,
remark?: string,
): Promise<void>;
setFriendAddRequestAsync(
flag: string,
approve: boolean,
remark?: string,
): Promise<void>;
setGroupAddRequest(
flag: string,
subType: 'add' | 'invite',
approve: boolean,
reason?: string,
): Promise<void>;
setGroupAddRequestAsync(
flag: string,
subType: 'add' | 'invite',
approve: boolean,
reason?: string,
): Promise<void>;
setGroupKick(
group_id: id,
user_id: id,
reject_add_request?: boolean,
): Promise<void>;
setGroupKickAsync(
group_id: id,
user_id: id,
reject_add_request?: boolean,
): Promise<void>;
setGroupBan(group_id: id, user_id: id, duration?: number): Promise<void>;
setGroupBanAsync(group_id: id, user_id: id, duration?: number): Promise<void>;
setGroupWholeBan(group_id: id, enable?: boolean): Promise<void>;
setGroupWholeBanAsync(group_id: id, enable?: boolean): Promise<void>;
setGroupAdmin(group_id: id, user_id: id, enable?: boolean): Promise<void>;
setGroupAdminAsync(
group_id: id,
user_id: id,
enable?: boolean,
): Promise<void>;
setGroupAnonymous(group_id: id, enable?: boolean): Promise<void>;
setGroupAnonymousAsync(group_id: id, enable?: boolean): Promise<void>;
setGroupCard(group_id: id, user_id: id, card?: string): Promise<void>;
setGroupCardAsync(group_id: id, user_id: id, card?: string): Promise<void>;
setGroupLeave(group_id: id, is_dismiss?: boolean): Promise<void>;
setGroupLeaveAsync(group_id: id, is_dismiss?: boolean): Promise<void>;
setGroupSpecialTitle(
group_id: id,
user_id: id,
special_title?: string,
duration?: number,
): Promise<void>;
setGroupSpecialTitleAsync(
group_id: id,
user_id: id,
special_title?: string,
duration?: number,
): Promise<void>;
setGroupName(group_id: id, name: string): Promise<void>;
setGroupNameAsync(group_id: id, name: string): Promise<void>;
setGroupPortrait(group_id: id, file: string, cache?: boolean): Promise<void>;
setGroupPortraitAsync(
group_id: id,
file: string,
cache?: boolean,
): Promise<void>;
getGroupAtAllRemain(group_id: id): Promise<AtAllRemain>;
sendGroupNotice(group_id: id, content: string): Promise<void>;
sendGroupNoticeAsync(group_id: id, content: string): Promise<void>;
getLoginInfo(): Promise<AccountInfo>;
getVipInfo(): Promise<VipInfo>;
getStrangerInfo(user_id: id, no_cache?: boolean): Promise<StrangerInfo>;
getFriendList(): Promise<FriendInfo[]>;
getGroupInfo(group_id: id, no_cache?: boolean): Promise<GroupInfo>;
getGroupList(): Promise<GroupInfo[]>;
getGroupMemberInfo(
group_id: id,
user_id: id,
no_cache?: boolean,
): Promise<GroupMemberInfo>;
getGroupMemberList(
group_id: id,
no_cache?: boolean,
): Promise<GroupMemberInfo[]>;
getGroupHonorInfo(group_id: id, type: HonorType): Promise<HonorInfo>;
getGroupSystemMsg(): Promise<GroupSystemMessageInfo>;
getGroupFileSystemInfo(group_id: id): Promise<GroupFileSystemInfo>;
getGroupRootFiles(group_id: id): Promise<GroupFileList>;
getGroupFilesByFolder(
group_id: id,
folder_id: string,
): Promise<GroupFileList>;
getGroupFileUrl(
group_id: id,
file_id: string,
busid: number,
): Promise<string>;
downloadFile(
url: string,
headers?: string | readonly string[],
thread_count?: number,
): Promise<string>;
uploadGroupFile(
group_id: id,
file: string,
name: string,
folder?: string,
): Promise<void>;
createGroupFileFolder(
group_id: id,
folder_id: string,
name: string,
): Promise<void>;
deleteGroupFolder(group_id: id, folder_id: string): Promise<void>;
deleteGroupFile(
group_id: id,
folder_id: string,
file_id: string,
busid: number,
): Promise<void>;
getOnlineClients(no_cache?: boolean): Promise<Device[]>;
checkUrlSafely(url: string): Promise<SafetyLevel>;
getModelShow(model: string): Promise<ModelVariant[]>;
setModelShow(model: string, model_show: string): Promise<void>;
getCookies(domain?: string): Promise<string>;
getCsrfToken(): Promise<number>;
getCredentials(domain?: string): Promise<Credentials>;
getRecord(
file: string,
out_format: RecordFormat,
full_path?: boolean,
): Promise<RecordInfo>;
getImage(file: string): Promise<ImageInfo>;
canSendImage(): Promise<boolean>;
canSendRecord(): Promise<boolean>;
getStatus(): Promise<StatusInfo>;
getVersionInfo(): Promise<VersionInfo>;
setRestart(delay?: number): Promise<void>;
reloadEventFilter(): Promise<void>;
getGuildServiceProfile(): Promise<GuildServiceProfile>;
getGuildList(): Promise<GuildInfo[]>;
getGuildMetaByGuest(guild_id: id): Promise<GuildMeta>;
getGuildChannelList(guild_id: id, no_cache: boolean): Promise<ChannelInfo[]>;
getGuildMembers(guild_id: id): Promise<GuildMembers>;
sendGuildChannelMsg(
guild_id: id,
channel_id: id,
message: string,
): Promise<number>;
}
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'; ...@@ -20,6 +20,7 @@ import { BotRegistryService } from './bot-registry/bot-registry.service';
}), }),
KoishiModule.register({ KoishiModule.register({
prefix: '__never_prefix', prefix: '__never_prefix',
help: false,
minSimilarity: 1, minSimilarity: 1,
useWs: true, useWs: true,
}), }),
......
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { Adapter, Context, Session } from 'koishi'; 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' { declare module 'koishi' {
interface EventMap { interface EventMap {
...@@ -14,24 +19,14 @@ Adapter.prototype.dispatch = function (this: Adapter, session: Session) { ...@@ -14,24 +19,14 @@ Adapter.prototype.dispatch = function (this: Adapter, session: Session) {
this.ctx.emit(session, 'dispatch', 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() @Injectable()
export class BotLoaderService implements OnModuleInit { export class BotLoaderService {
constructor( constructor(private config: ConfigService) {}
private config: ConfigService,
@InjectContext() private ctx: Context,
) {}
@UsePlugin() @UsePlugin()
loadBots() { loadBots() {
const onebotConfig = this.config.get< const onebotConfig =
Adapter.PluginConfig<AdapterConfig, BotConfig> this.config.get<Adapter.PluginConfig<AdapterConfig, BotConfig>>('onebot');
>('onebot');
if (onebotConfig.selfId) { if (onebotConfig.selfId) {
onebotConfig.selfId = onebotConfig.selfId.toString(); onebotConfig.selfId = onebotConfig.selfId.toString();
} }
...@@ -42,13 +37,4 @@ export class BotLoaderService implements OnModuleInit { ...@@ -42,13 +37,4 @@ export class BotLoaderService implements OnModuleInit {
} }
return PluginDef(PluginOnebot, onebotConfig); 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 { Injectable } from '@nestjs/common';
import { WireContextService } from 'koishi-nestjs'; import { WireContextService } from 'koishi-nestjs';
import { OneBotBot } from '@koishijs/plugin-adapter-onebot/lib/bot'; import { OneBotBot } from '../adapter-onebot';
import { Adapter } from 'koishi';
@Injectable() @Injectable()
export class BotRegistryService { export class BotRegistryService {
@WireContextService('bots') @WireContextService('bots')
private bots: OneBotBot[]; private bots: Adapter.BotList;
private botMap = new Map<string, OneBotBot>(); getBotWithId(selfId: string): OneBotBot {
return this.bots.get(`onebot:${selfId}`) as 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);
} }
getAllBots() { getAllBots(): OneBotBot[] {
return this.bots; return this.bots as unknown as OneBotBot[];
} }
} }
...@@ -5,6 +5,7 @@ export class HealthInfoDto { ...@@ -5,6 +5,7 @@ export class HealthInfoDto {
name: string; name: string;
@ApiProperty({ description: '是否健康' }) @ApiProperty({ description: '是否健康' })
healthy: boolean; healthy: boolean;
constructor(name: string, healthy: boolean) { constructor(name: string, healthy: boolean) {
this.name = name; this.name = name;
this.healthy = healthy; this.healthy = healthy;
......
...@@ -19,6 +19,7 @@ export class BlankReturnMessageDto implements BlankReturnMessage { ...@@ -19,6 +19,7 @@ export class BlankReturnMessageDto implements BlankReturnMessage {
message: string; message: string;
@ApiProperty({ description: '是否成功' }) @ApiProperty({ description: '是否成功' })
success: boolean; success: boolean;
constructor(statusCode: number, message?: string) { constructor(statusCode: number, message?: string) {
this.statusCode = statusCode; this.statusCode = statusCode;
this.message = message || 'success'; this.message = message || 'success';
...@@ -32,9 +33,11 @@ export class BlankReturnMessageDto implements BlankReturnMessage { ...@@ -32,9 +33,11 @@ export class BlankReturnMessageDto implements BlankReturnMessage {
export class ReturnMessageDto<T> export class ReturnMessageDto<T>
extends BlankReturnMessageDto extends BlankReturnMessageDto
implements ReturnMessage<T> { implements ReturnMessage<T>
{
@ApiProperty({ description: '返回内容' }) @ApiProperty({ description: '返回内容' })
data?: T; data?: T;
constructor(statusCode: number, message?: string, data?: T) { constructor(statusCode: number, message?: string, data?: T) {
super(statusCode, message); super(statusCode, message);
this.data = data; this.data = data;
......
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { RouteService } from '../route/route.service'; import { RouteService } from '../route/route.service';
import { InjectContext } from 'koishi-nestjs';
import { Context } from 'koishi';
import { HealthInfoDto } from '../dto/HealthInfo.dto'; import { HealthInfoDto } from '../dto/HealthInfo.dto';
import { BotRegistryService } from '../bot-registry/bot-registry.service'; import { BotRegistryService } from '../bot-registry/bot-registry.service';
......
...@@ -28,4 +28,5 @@ async function bootstrap() { ...@@ -28,4 +28,5 @@ async function bootstrap() {
config.get<string>('host') || '::', config.get<string>('host') || '::',
); );
} }
bootstrap(); bootstrap();
import { ConsoleLogger, Injectable } from '@nestjs/common'; import { ConsoleLogger, Injectable } from '@nestjs/common';
import { InjectContext } from 'koishi-nestjs';
import { Context } from 'koishi';
import WebSocket from 'ws'; import WebSocket from 'ws';
import { Route } from '../route/Route'; import { Route } from '../route/Route';
import { genMetaEvent } from '../utility/oicq'; import { genMetaEvent } from '../utility/oicq';
import { import {
OnebotProtocol,
OnebotAsyncResponseWithEcho, OnebotAsyncResponseWithEcho,
OnebotProtocol,
} from '../utility/onebot-protocol'; } 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 { WaitBotService } from '../wait-bot/wait-bot.service';
import { BotRegistryService } from '../bot-registry/bot-registry.service'; import { BotRegistryService } from '../bot-registry/bot-registry.service';
......
...@@ -2,7 +2,6 @@ import { ...@@ -2,7 +2,6 @@ import {
OnGatewayConnection, OnGatewayConnection,
OnGatewayDisconnect, OnGatewayDisconnect,
WebSocketGateway, WebSocketGateway,
WsException,
} from '@nestjs/websockets'; } from '@nestjs/websockets';
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
import { RouteService } from './route/route.service'; import { RouteService } from './route/route.service';
...@@ -21,15 +20,18 @@ interface ClientInfo { ...@@ -21,15 +20,18 @@ interface ClientInfo {
@WebSocketGateway({ path: '^/route/(.+?)/?$' }) @WebSocketGateway({ path: '^/route/(.+?)/?$' })
export class OnebotGateway export class OnebotGateway
extends ConsoleLogger extends ConsoleLogger
implements OnGatewayConnection, OnGatewayDisconnect { implements OnGatewayConnection, OnGatewayDisconnect
{
constructor( constructor(
private routeService: RouteService, private routeService: RouteService,
private messageService: MessageService, private messageService: MessageService,
) { ) {
super('ws'); super('ws');
} }
private clientRouteMap = new Map<WebSocket, ClientInfo>(); private clientRouteMap = new Map<WebSocket, ClientInfo>();
private matchingRegex = new RegExp('^/route/(.+?)/?$'); private matchingRegex = new RegExp('^/route/(.+?)/?$');
handleConnection(client: WebSocket, request: IncomingMessage) { handleConnection(client: WebSocket, request: IncomingMessage) {
const baseUrl = 'ws://' + request.headers.host + '/'; const baseUrl = 'ws://' + request.headers.host + '/';
const url = new URL(request.url, baseUrl); const url = new URL(request.url, baseUrl);
...@@ -62,6 +64,7 @@ export class OnebotGateway ...@@ -62,6 +64,7 @@ export class OnebotGateway
`Client ${clientInfo.ip} of route ${clientInfo.routeName} connected.`, `Client ${clientInfo.ip} of route ${clientInfo.routeName} connected.`,
); );
} }
handleDisconnect(client: WebSocket) { handleDisconnect(client: WebSocket) {
const clientInfo = this.clientRouteMap.get(client); const clientInfo = this.clientRouteMap.get(client);
if (!clientInfo) { if (!clientInfo) {
......
...@@ -9,6 +9,7 @@ export class ReverseWsService extends ConsoleLogger { ...@@ -9,6 +9,7 @@ export class ReverseWsService extends ConsoleLogger {
constructor(private meesageService: MessageService) { constructor(private meesageService: MessageService) {
super('reverse-ws'); super('reverse-ws');
} }
initializeReverseWs(route: Route, revConfig: ReverseWsConfig) { initializeReverseWs(route: Route, revConfig: ReverseWsConfig) {
const headers: OutgoingHttpHeaders = { const headers: OutgoingHttpHeaders = {
'X-Self-ID': route.selfId, 'X-Self-ID': route.selfId,
......
import type WebSocket from 'ws'; import type WebSocket from 'ws';
import { Context, Session, Selection } from 'koishi'; import { Context, Random, remove, Selection, Session } from 'koishi';
import { Random, remove } from 'koishi';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { SendTask } from '../message/message.service'; import { SendTask } from '../message/message.service';
import { HealthInfoDto } from '../dto/HealthInfo.dto'; import { HealthInfoDto } from '../dto/HealthInfo.dto';
...@@ -26,6 +25,7 @@ export interface RouteConfig { ...@@ -26,6 +25,7 @@ export interface RouteConfig {
bufferAppMessage?: boolean; bufferAppMessage?: boolean;
bufferBotMessage?: boolean; bufferBotMessage?: boolean;
} }
export class Route implements RouteConfig { export class Route implements RouteConfig {
private connections: WebSocket[] = []; private connections: WebSocket[] = [];
private roundCount = 0; private roundCount = 0;
...@@ -43,6 +43,7 @@ export class Route implements RouteConfig { ...@@ -43,6 +43,7 @@ export class Route implements RouteConfig {
bufferAppMessage?: boolean; bufferAppMessage?: boolean;
bufferBotMessage?: boolean; bufferBotMessage?: boolean;
preMessages: { data: any; session: Session }[] = []; preMessages: { data: any; session: Session }[] = [];
constructor(routeConfig: RouteConfig, ctx: Context) { constructor(routeConfig: RouteConfig, ctx: Context) {
Object.assign(this, routeConfig); Object.assign(this, routeConfig);
this.balancePolicy ||= 'hash'; this.balancePolicy ||= 'hash';
...@@ -61,12 +62,15 @@ export class Route implements RouteConfig { ...@@ -61,12 +62,15 @@ export class Route implements RouteConfig {
}, this.heartbeat); }, this.heartbeat);
} }
} }
isHealthy() { isHealthy() {
return this.connections.length > 0; return this.connections.length > 0;
} }
getHealthyInfo() { getHealthyInfo() {
return new HealthInfoDto(this.name, this.isHealthy()); return new HealthInfoDto(this.name, this.isHealthy());
} }
send(data: any, session: Session, allConns = this.connections) { send(data: any, session: Session, allConns = this.connections) {
if (!allConns.length) { if (!allConns.length) {
if (this.bufferAppMessage) { if (this.bufferAppMessage) {
...@@ -96,6 +100,7 @@ export class Route implements RouteConfig { ...@@ -96,6 +100,7 @@ export class Route implements RouteConfig {
}); });
} }
} }
broadcast(data: any) { broadcast(data: any) {
const message = JSON.stringify(data); const message = JSON.stringify(data);
for (const conn of this.connections) { for (const conn of this.connections) {
...@@ -108,6 +113,7 @@ export class Route implements RouteConfig { ...@@ -108,6 +113,7 @@ export class Route implements RouteConfig {
}); });
} }
} }
getFilteredContext(ctx: Context) { getFilteredContext(ctx: Context) {
const idCtx = ctx.self(this.selfId); const idCtx = ctx.self(this.selfId);
if (!this.select) { if (!this.select) {
...@@ -115,6 +121,7 @@ export class Route implements RouteConfig { ...@@ -115,6 +121,7 @@ export class Route implements RouteConfig {
} }
return idCtx.select(this.select); return idCtx.select(this.select);
} }
static sessionKeys: (keyof Session)[] = [ static sessionKeys: (keyof Session)[] = [
'selfId', 'selfId',
'guildId', 'guildId',
...@@ -125,6 +132,7 @@ export class Route implements RouteConfig { ...@@ -125,6 +132,7 @@ export class Route implements RouteConfig {
'subtype', 'subtype',
'subsubtype', 'subsubtype',
]; ];
private getSequenceFromSession(sess: Session) { private getSequenceFromSession(sess: Session) {
const hash = createHash('md5'); const hash = createHash('md5');
for (const key of Route.sessionKeys) { for (const key of Route.sessionKeys) {
...@@ -135,6 +143,7 @@ export class Route implements RouteConfig { ...@@ -135,6 +143,7 @@ export class Route implements RouteConfig {
} }
return parseInt(hash.digest('hex'), 16) % 4294967295; return parseInt(hash.digest('hex'), 16) % 4294967295;
} }
getRelatedConnections( getRelatedConnections(
sess: Session, sess: Session,
allConns = this.connections, allConns = this.connections,
...@@ -159,6 +168,7 @@ export class Route implements RouteConfig { ...@@ -159,6 +168,7 @@ export class Route implements RouteConfig {
return []; return [];
} }
} }
addConnection(conn: WebSocket) { addConnection(conn: WebSocket) {
this.connections.push(conn); this.connections.push(conn);
if (!this.bufferAppMessage) { if (!this.bufferAppMessage) {
...@@ -170,12 +180,15 @@ export class Route implements RouteConfig { ...@@ -170,12 +180,15 @@ export class Route implements RouteConfig {
this.send(message.data, message.session); this.send(message.data, message.session);
} }
} }
removeConnection(conn: WebSocket) { removeConnection(conn: WebSocket) {
remove(this.connections, conn); remove(this.connections, conn);
} }
addSendTask(task: SendTask) { addSendTask(task: SendTask) {
this.sendQueue.push(task); this.sendQueue.push(task);
} }
fetchSendTask() { fetchSendTask() {
if (!this.sendQueue.length) { if (!this.sendQueue.length) {
return; return;
......
...@@ -13,8 +13,10 @@ import { MessageService } from '../message/message.service'; ...@@ -13,8 +13,10 @@ import { MessageService } from '../message/message.service';
@Injectable() @Injectable()
export class RouteService export class RouteService
extends ConsoleLogger extends ConsoleLogger
implements OnApplicationBootstrap { implements OnApplicationBootstrap
{
private routes = new Map<string, Route>(); private routes = new Map<string, Route>();
constructor( constructor(
config: ConfigService, config: ConfigService,
@InjectContextPlatform('onebot') private ctx: Context, @InjectContextPlatform('onebot') private ctx: Context,
......
...@@ -2,8 +2,8 @@ import yaml from 'yaml'; ...@@ -2,8 +2,8 @@ import yaml from 'yaml';
import * as fs from 'fs'; import * as fs from 'fs';
import { RouteConfig } from '../route/Route'; import { RouteConfig } from '../route/Route';
import { Adapter } from 'koishi'; import { Adapter } from 'koishi';
import { AdapterConfig } from '@koishijs/plugin-adapter-onebot/lib/utils'; import { AdapterConfig } from '../adapter-onebot/utils';
import { BotConfig } from '@koishijs/plugin-adapter-onebot/lib/bot'; import { BotConfig } from '../adapter-onebot';
export interface LbConfig { export interface LbConfig {
host: string; host: string;
......
...@@ -14,6 +14,7 @@ export const BOOLS = [ ...@@ -14,6 +14,7 @@ export const BOOLS = [
'approve', 'approve',
'block', 'block',
]; ];
export function toBool(v: any) { export function toBool(v: any) {
if (v === '0' || v === 'false') v = false; if (v === '0' || v === 'false') v = false;
return Boolean(v); 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