diff --git a/README.md b/README.md index 7b988c0f..3e4ff3d1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ - [x] Line - [x] Mail - [x] Matrix - - [x] OneBot - [x] QQ Guild - [x] Slack - [x] Telegram diff --git a/adapters/onebot/.npmignore b/adapters/onebot/.npmignore deleted file mode 100644 index 7e5fcbc1..00000000 --- a/adapters/onebot/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -.DS_Store -tsconfig.tsbuildinfo diff --git a/adapters/onebot/package.json b/adapters/onebot/package.json deleted file mode 100644 index e2882554..00000000 --- a/adapters/onebot/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@satorijs/adapter-onebot", - "description": "OneBot Adapter for Satorijs", - "version": "6.0.2", - "main": "lib/index.js", - "typings": "lib/index.d.ts", - "files": [ - "lib" - ], - "author": "Shigma ", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/satorijs/satori.git", - "directory": "adapters/onebot" - }, - "bugs": { - "url": "https://github.com/satorijs/satori/issues" - }, - "homepage": "https://koishi.chat/plugins/adapter/onebot.html", - "keywords": [ - "bot", - "onebot", - "chatbot", - "satori", - "im", - "chat" - ], - "peerDependencies": { - "@satorijs/satori": "^3.0.0-rc.1" - }, - "dependencies": { - "qface": "^1.4.1" - } -} diff --git a/adapters/onebot/readme.md b/adapters/onebot/readme.md deleted file mode 100644 index 92b4f2e2..00000000 --- a/adapters/onebot/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -# [@satorijs/adapter-onebot](https://koishi.chat/plugins/adapter/onebot.html) - -OneBot adapter for [Satori](https://github.com/satorijs/satori). - -- [Documentation](https://koishi.chat/plugins/adapter/onebot.html) diff --git a/adapters/onebot/src/bot/base.ts b/adapters/onebot/src/bot/base.ts deleted file mode 100644 index f468b0d2..00000000 --- a/adapters/onebot/src/bot/base.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Bot, Fragment, Schema, Universal } from '@satorijs/satori' -import * as OneBot from '../utils' -import { OneBotMessageEncoder } from './message' - -export class BaseBot extends Bot { - static MessageEncoder = OneBotMessageEncoder - - public parent?: BaseBot - public internal: OneBot.Internal - - sendMessage(channelId: string, fragment: Fragment, guildId?: string, options?: Universal.SendOptions) { - if (!this.parent && !channelId.startsWith('private:')) { - guildId = channelId - } - return super.sendMessage(channelId, fragment, guildId, options) - } - - async createDirectChannel(userId: string) { - return { id: 'private:' + userId, type: Universal.Channel.Type.DIRECT } - } - - async getMessage(channelId: string, messageId: string) { - const data = await this.internal.getMsg(messageId) - return await OneBot.adaptMessage(this, data) - } - - async deleteMessage(channelId: string, messageId: string) { - await this.internal.deleteMsg(messageId) - } - - async getLogin() { - const data = await this.internal.getLoginInfo() - this.user = OneBot.decodeUser(data) - return this.toJSON() - } - - async getUser(userId: string) { - const data = await this.internal.getStrangerInfo(userId) - return OneBot.decodeUser(data) - } - - async getFriendList() { - const data = await this.internal.getFriendList() - return { data: data.map(OneBot.decodeUser) } - } - - 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) - } - - async getMessageList(channelId: string, before?: string) { - // include `before` message - let list: OneBot.Message[] - if (before) { - const msg = await this.internal.getMsg(before) - if (msg?.message_seq) { - list = (await this.internal.getGroupMsgHistory(Number(channelId), msg.message_seq)).messages - } - } else { - list = (await this.internal.getGroupMsgHistory(Number(channelId))).messages - } - - // 从旧到新 - return { data: await Promise.all(list.map(item => OneBot.adaptMessage(this, item))) } - } -} - -export namespace BaseBot { - export interface Config { - advanced?: AdvancedConfig - } - - export interface AdvancedConfig { - splitMixedContent?: boolean - } - - export const AdvancedConfig: Schema = Schema.object({ - splitMixedContent: Schema.boolean().description('是否自动在混合内容间插入空格。').default(true), - }).description('高级设置') -} diff --git a/adapters/onebot/src/bot/cqcode.ts b/adapters/onebot/src/bot/cqcode.ts deleted file mode 100644 index 4e7e2672..00000000 --- a/adapters/onebot/src/bot/cqcode.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Dict, h } from '@satorijs/satori' - -export function CQCode(type: string, attrs: Dict) { - if (type === 'text') return attrs.content - let output = '[CQ:' + type - for (const key in attrs) { - if (attrs[key]) output += `,${key}=${h.escape(attrs[key], true)}` - } - return output + ']' -} - -export interface CQCode { - type: string - data: Dict - capture?: RegExpExecArray -} - -export namespace CQCode { - export function escape(source: any, inline = false) { - const result = String(source) - .replace(/&/g, '&') - .replace(/\[/g, '[') - .replace(/\]/g, ']') - return inline - ? result.replace(/,/g, ',').replace(/(\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]/g, ' ') - : result - } - - export function unescape(source: string) { - return String(source) - .replace(/[/g, '[') - .replace(/]/g, ']') - .replace(/,/g, ',') - .replace(/&/g, '&') - } - - const pattern = /\[CQ:(\w+)((,\w+=[^,\]]*)*)\]/ - - export function from(source: string): CQCode { - const capture = pattern.exec(source) - if (!capture) return null - const [, type, attrs] = capture - const data: Dict = {} - attrs && attrs.slice(1).split(',').forEach((str) => { - const index = str.indexOf('=') - data[str.slice(0, index)] = unescape(str.slice(index + 1)) - }) - return { type, data, capture } - } - - export function parse(source: string | CQCode[]) { - if (typeof source !== 'string') { - return source.map(({ type, data }) => { - if (type === 'text') { - return h('text', { content: data.text }) - } else { - return h(type, data) - } - }) - } - const elements: h[] = [] - let result: ReturnType - while ((result = from(source))) { - const { type, data, capture } = result - if (capture.index) { - elements.push(h('text', { content: unescape(source.slice(0, capture.index)) })) - } - elements.push(h(type, data)) - source = source.slice(capture.index + capture[0].length) - } - if (source) elements.push(h('text', { content: unescape(source) })) - return elements - } -} diff --git a/adapters/onebot/src/bot/index.ts b/adapters/onebot/src/bot/index.ts deleted file mode 100644 index e88a2dac..00000000 --- a/adapters/onebot/src/bot/index.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Context, noop, Schema, Session } from '@satorijs/satori' -import { HttpServer } from '../http' -import { WsClient, WsServer } from '../ws' -import { QQGuildBot } from './qqguild' -import { BaseBot } from './base' -import * as OneBot from '../utils' - -export * from './base' -export * from './cqcode' -export * from './message' -export * from './qqguild' - -export class OneBotBot extends BaseBot { - public guildBot: QQGuildBot - - constructor(ctx: Context, config: T) { - super(ctx, config) - this.selfId = config.selfId - this.platform = 'onebot' - this.internal = new OneBot.Internal() - this.user.avatar = `http://q.qlogo.cn/headimg_dl?dst_uin=${config.selfId}&spec=640` - - if (config.protocol === 'http') { - ctx.plugin(HttpServer, this) - } else if (config.protocol === 'ws') { - ctx.plugin(WsClient, this as any) - } else if (config.protocol === 'ws-reverse') { - ctx.plugin(WsServer, this) - } - } - - async stop() { - if (this.guildBot) { - // QQGuild stub bot should also be removed - delete this.ctx.bots[this.guildBot.sid] - } - await super.stop() - } - - async initialize() { - await Promise.all([ - this.getLogin(), - this.setupGuildService().catch(noop), - ]).then(() => this.online(), error => this.offline(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.ctx.plugin(QQGuildBot, { - profile, - parent: this, - advanced: this.config.advanced, - }) - } - - 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: data.map(OneBot.adaptGuild) } - } - - async getChannelList(guildId: string) { - return { data: [await this.getChannel(guildId)] } - } - - async getGuildMember(guildId: string, userId: string) { - const data = await this.internal.getGroupMemberInfo(guildId, userId) - return OneBot.decodeGuildMember(data) - } - - async getGuildMemberList(guildId: string) { - const data = await this.internal.getGroupMemberList(guildId) - return { data: data.map(OneBot.decodeGuildMember) } - } - - async kickGuildMember(guildId: string, userId: string, permanent?: boolean) { - return this.internal.setGroupKick(guildId, userId, permanent) - } - - async muteGuildMember(guildId: string, userId: string, duration: number) { - return this.internal.setGroupBan(guildId, userId, duration / 1000) - } - - async muteChannel(channelId: string, guildId?: string, enable?: boolean) { - return this.internal.setGroupWholeBan(channelId, enable) - } - - async checkPermission(name: string, session: Partial) { - if (name === 'onebot.group.admin') { - return session.author?.roles?.[0] === 'admin' - } else if (name === 'onebot.group.owner') { - return session.author?.roles?.[0] === 'owner' - } - return super.checkPermission(name, session) - } -} - -export namespace OneBotBot { - export interface BaseConfig extends BaseBot.Config { - selfId: string - password?: string - token?: string - } - - export const BaseConfig: Schema = Schema.object({ - selfId: Schema.string().description('机器人的账号。').required(), - token: Schema.string().role('secret').description('发送信息时用于验证的字段,应与 OneBot 配置文件中的 `access_token` 保持一致。'), - protocol: process.env.KOISHI_ENV === 'browser' - ? Schema.const('ws').default('ws') - : Schema.union(['http', 'ws', 'ws-reverse']).description('选择要使用的协议。').default('ws-reverse'), - }) - - export type Config = BaseConfig & (HttpServer.Config | WsServer.Config | WsClient.Config) - - export const Config: Schema = Schema.intersect([ - BaseConfig, - Schema.union([ - HttpServer.Config, - WsClient.Config, - WsServer.Config, - ]), - Schema.object({ - advanced: BaseBot.AdvancedConfig, - }), - ]) -} diff --git a/adapters/onebot/src/bot/message.ts b/adapters/onebot/src/bot/message.ts deleted file mode 100644 index 6edcc46a..00000000 --- a/adapters/onebot/src/bot/message.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { h, MessageEncoder, pick, Universal } from '@satorijs/satori' -import { BaseBot } from './base' -import { CQCode } from './cqcode' - -export interface Author extends Universal.User { - time?: string | number - messageId?: string -} - -class State { - author: Partial = {} - children: CQCode[] = [] - - constructor(public type: 'message' | 'forward' | 'reply') {} -} - -export class OneBotMessageEncoder extends MessageEncoder { - stack: State[] = [new State('message')] - children: CQCode[] = [] - - async forward() { - if (!this.stack[0].children.length) return - const session = this.bot.session(this.session) - session.messageId = this.guildId - ? '' + await this.bot.internal.sendGroupForwardMsg(this.guildId, this.stack[0].children) - : '' + await this.bot.internal.sendPrivateForwardMsg(this.channelId.slice(8), this.stack[0].children) - session.app.emit(session, 'send', session) - this.results.push(session.event.message) - } - - async flush() { - // trim start - while (true) { - const first = this.children[0] - if (first?.type !== 'text') break - first.data.text = first.data.text.trimStart() - if (first.data.text) break - this.children.shift() - } - - // trim end - while (true) { - const last = this.children[this.children.length - 1] - if (last?.type !== 'text') break - last.data.text = last.data.text.trimEnd() - if (last.data.text) break - this.children.pop() - } - - // flush - const { type, author } = this.stack[0] - if (!this.children.length && !author.messageId) return - if (type === 'forward') { - if (author.messageId) { - this.stack[1].children.push({ - type: 'node', - data: { - id: author.messageId, - }, - }) - } else { - this.stack[1].children.push({ - type: 'node', - data: { - name: author.name || this.bot.user.name, - uin: author.id || this.bot.userId, - content: this.children as any, - time: `${Math.floor((+author.time || Date.now()) / 1000)}`, - }, - }) - } - - this.children = [] - return - } - - const session = this.bot.session(this.session) - session.messageId = this.bot.parent - ? '' + await this.bot.internal.sendGuildChannelMsg(this.guildId, this.channelId, this.children) - : this.guildId - ? '' + await this.bot.internal.sendGroupMsg(this.guildId, this.children) - : '' + await this.bot.internal.sendPrivateMsg(this.channelId.slice(8), this.children) - session.app.emit(session, 'send', session) - this.results.push(session.event.message) - this.children = [] - } - - private text(text: string) { - this.children.push({ type: 'text', data: { text } }) - } - - async visit(element: h) { - let { type, attrs, children } = element - if (type === 'text') { - this.text(attrs.content) - } else if (type === 'br') { - this.text('\n') - } else if (type === 'p') { - const prev = this.children[this.children.length - 1] - if (prev?.type === 'text') { - if (!prev.data.text.endsWith('\n')) { - prev.data.text += '\n' - } - } else { - this.text('\n') - } - await this.render(children) - this.text('\n') - } else if (type === 'at') { - if (attrs.type === 'all') { - this.children.push({ type: 'at', data: { qq: 'all' } }) - } else { - this.children.push({ type: 'at', data: { qq: attrs.id, name: attrs.name } }) - } - } else if (type === 'sharp') { - if (attrs.id) this.text(attrs.id) - } else if (type === 'face') { - if (attrs.platform && attrs.platform !== this.bot.platform) { - await this.render(children) - } else { - this.children.push({ type: 'face', data: { id: attrs.id } }) - } - } else if (type === 'a') { - await this.render(children) - if (attrs.href) this.text(` (${attrs.href}) `) - } else if (['video', 'audio', 'image'].includes(type)) { - if (type === 'audio') type = 'record' - attrs = { ...attrs } - attrs.file = attrs.url - delete attrs.url - if (attrs.cache) { - attrs.cache = 1 - } else { - attrs.cache = 0 - } - const cap = /^data:([\w/-]+);base64,/.exec(attrs.file) - if (cap) attrs.file = 'base64://' + attrs.file.slice(cap[0].length) - this.children.push({ type, data: attrs }) - } else if (type === 'onebot:music') { - await this.flush() - this.children.push({ type: 'music', data: attrs }) - } else if (type === 'onebot:tts') { - await this.flush() - this.children.push({ type: 'tts', data: attrs }) - } else if (type === 'onebot:poke') { - await this.flush() - this.children.push({ type: 'poke', data: attrs }) - } else if (type === 'onebot:gift') { - await this.flush() - this.children.push({ type: 'gift', data: attrs }) - } else if (type === 'onebot:share') { - await this.flush() - this.children.push({ type: 'share', data: attrs }) - } else if (type === 'onebot:json') { - await this.flush() - this.children.push({ type: 'json', data: attrs }) - } else if (type === 'onebot:xml') { - await this.flush() - this.children.push({ type: 'xml', data: attrs }) - } else if (type === 'onebot:cardimage') { - await this.flush() - this.children.push({ type: 'cardimage', data: attrs }) - } else if (type === 'author') { - Object.assign(this.stack[0].author, attrs) - } else if (type === 'figure' && !this.bot.parent) { - await this.flush() - this.stack.unshift(new State('forward')) - await this.render(children) - await this.flush() - this.stack.shift() - await this.forward() - } else if (type === 'figure') { - await this.render(children) - await this.flush() - } else if (type === 'quote') { - await this.flush() - this.children.push({ type: 'reply', data: attrs }) - } else if (type === 'message') { - await this.flush() - // qqguild does not support forward messages - if ('forward' in attrs && !this.bot.parent) { - this.stack.unshift(new State('forward')) - await this.render(children) - await this.flush() - this.stack.shift() - await this.forward() - } else if ('id' in attrs) { - this.stack[0].author.messageId = attrs.id.toString() - } else { - Object.assign(this.stack[0].author, pick(attrs, ['userId', 'username', 'nickname', 'time'])) - await this.render(children) - await this.flush() - } - } else { - await this.render(children) - } - } -} diff --git a/adapters/onebot/src/bot/qqguild.ts b/adapters/onebot/src/bot/qqguild.ts deleted file mode 100644 index e21ef301..00000000 --- a/adapters/onebot/src/bot/qqguild.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Context, Universal } from '@satorijs/satori' -import { BaseBot } from './base' -import { OneBotBot } from '.' -import * as OneBot from '../utils' - -export namespace QQGuildBot { - export interface Config extends BaseBot.Config { - parent: OneBotBot - profile: OneBot.GuildServiceProfile - } -} - -export class QQGuildBot extends BaseBot { - declare parent: OneBotBot - hidden = true - - constructor(ctx: Context, config: QQGuildBot.Config) { - super(ctx, config) - this.platform = 'qqguild' - this.selfId = config.profile.tiny_id - this.parent = config.parent - this.internal = config.parent.internal - this.user.name = config.profile.nickname - this.user.avatar = config.profile.avatar_url - this.parent.guildBot = this - } - - get status() { - return this.parent.status - } - - set status(status) { - this.parent.status = status - } - - async start() { - await this.ctx.parallel('bot-connect', this) - } - - async stop() { - // Don't stop this bot twice - if (!this.parent) return - // prevent circular reference and use this as already disposed - this.parent = undefined - await this.ctx.parallel('bot-disconnect', this) - } - - async getChannel(channelId: string, guildId?: string) { - const { data } = await this.getChannelList(guildId) - return data.find((channel) => channel.id === channelId) - } - - async getChannelList(guildId: string) { - const data = await this.internal.getGuildChannelList(guildId, false) - return { data: (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: data.map(OneBot.adaptGuild) } - } - - async getGuildMember(guildId: string, userId: string) { - const profile = await this.internal.getGuildMemberProfile(guildId, userId) - return OneBot.adaptQQGuildMemberProfile(profile) - } - - async getGuildMemberList(guildId: string) { - let nextToken: string | undefined - let list: Universal.GuildMember[] = [] - while (true) { - const data = await this.internal.getGuildMemberList(guildId, nextToken) - if (!data.members?.length) break - list = list.concat(data.members.map(OneBot.adaptQQGuildMemberInfo)) - if (data.finished) break - nextToken = data.next_token - } - return { data: list } - } -} diff --git a/adapters/onebot/src/http.ts b/adapters/onebot/src/http.ts deleted file mode 100644 index 0767a70e..00000000 --- a/adapters/onebot/src/http.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Adapter, Context, Logger, Quester, Schema } from '@satorijs/satori' -import { OneBotBot } from './bot' -import { dispatchSession } from './utils' -import { createHmac } from 'crypto' - -const logger = new Logger('onebot') - -export class HttpServer extends Adapter { - declare bots: OneBotBot[] - - async fork(ctx: Context, bot: OneBotBot) { - super.fork(ctx, bot) - const config = bot.config - const { endpoint, token } = config - if (!endpoint) return - - const http = ctx.http.extend(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 connect(bot: OneBotBot) { - const { secret, path = '/onebot' } = bot.config - bot.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) - }) - } -} - -export namespace HttpServer { - export interface Config extends Quester.Config { - protocol: 'http' - path?: string - secret?: string - } - - export const Config: Schema = Schema.intersect([ - Schema.object({ - protocol: Schema.const('http').required(), - path: Schema.string().description('服务器监听的路径。').default('/onebot'), - secret: Schema.string().description('接收事件推送时用于验证的字段,应该与 OneBot 的 secret 配置保持一致。').role('secret'), - }).description('连接设置'), - Quester.createConfig(true), - ]) -} diff --git a/adapters/onebot/src/index.ts b/adapters/onebot/src/index.ts deleted file mode 100644 index 393de685..00000000 --- a/adapters/onebot/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { OneBotBot } from './bot' -import * as OneBot from './utils' - -export { OneBot } - -export * from './bot' -export * from './http' -export * from './ws' - -export default OneBotBot - -declare module '@satorijs/core' { - interface Session { - onebot?: OneBot.Payload & OneBot.Internal - } - - interface Events { - 'onebot/message-reactions-updated'(session: Session): void - 'onebot/channel-updated'(session: Session): void - 'onebot/channel-created'(session: Session): void - 'onebot/channel-destroyed'(session: Session): void - } -} diff --git a/adapters/onebot/src/types.ts b/adapters/onebot/src/types.ts deleted file mode 100644 index 6a976fb4..00000000 --- a/adapters/onebot/src/types.ts +++ /dev/null @@ -1,685 +0,0 @@ -import { camelize, Dict, Logger } from '@satorijs/satori' -import { CQCode } from './bot' - -export interface Response { - status: string - retcode: number - data: any - echo?: number -} - -export interface MessageId { - message_id: number -} - -export interface AccountInfo { - user_id: number - tiny_id?: string - nickname: string -} - -export interface QidianAccountInfo { - master_id: number - ext_name: string - create_time: number -} - -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_seq: number - message_type: 'private' | 'group' | 'guild' - sender: SenderInfo - group_id?: number - guild_id?: string - channel_id?: string - message: string | CQCode[] - 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 UnidirectionalFriendInfo extends AccountInfo { - source: 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 { - app_name?: string - app_version?: string - app_full_name?: string - protocol_version?: string - coolq_edition?: 'air' | 'pro' - coolq_directory?: string - plugin_version?: string - plugin_build_number?: number - plugin_build_configuration?: 'debug' | 'release' - version?: string - go_cqhttp?: boolean - runtime_version?: string - runtime_os?: string - protocol?: 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 -} - -export interface VipInfo extends AccountInfo { - level: number - level_speed: number - vip_level: number - vip_growth_speed: number - vip_growth_total: string -} - -export interface GroupNotice { - notice_id: string - sender_id: number - publish_time: number - message: { - text: string - images: GroupNoticeImage[] - } -} - -export interface GroupNoticeImage { - height: string - width: string - id: string -} - -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 interface InvitedRequest extends 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 GuildMemberListData { - members: GuildMemberInfo[] - finished: boolean - next_token: string -} - -export interface GuildMemberRole { - role_id: string - role_name: string -} - -export interface GuildMemberInfo extends GuildMemberRole { - tiny_id: string - title: string - nickname: string - role: number -} - -export interface GuildMemberProfile { - tiny_id: string - nickname: string - avatar_url: string - join_time: number - roles: GuildMemberRole[] -} - -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[] - file: File -} - -export interface File { - name: string - size: number - url: string -} - -type id = string | number - -export interface Internal { - sendPrivateMsg(user_id: id, message: string | readonly CQCode[], auto_escape?: boolean): Promise - sendPrivateMsgAsync(user_id: id, message: string | readonly CQCode[], auto_escape?: boolean): Promise - sendGroupMsg(group_id: id, message: string | readonly CQCode[], auto_escape?: boolean): Promise - sendGroupMsgAsync(group_id: id, message: string | readonly CQCode[], auto_escape?: boolean): Promise - sendGroupForwardMsg(group_id: id, messages: readonly CQCode[]): Promise - sendGroupForwardMsgAsync(group_id: id, messages: readonly CQCode[]): Promise - sendPrivateForwardMsg(user_id: id, messages: readonly CQCode[]): Promise - sendPrivateForwardMsgAsync(user_id: id, messages: readonly CQCode[]): Promise - deleteMsg(message_id: id): Promise - deleteMsgAsync(message_id: id): Promise - setEssenceMsg(message_id: id): Promise - setEssenceMsgAsync(message_id: id): Promise - deleteEssenceMsg(message_id: id): Promise - deleteEssenceMsgAsync(message_id: id): Promise - markMsgAsRead(message_id: id): Promise - sendLike(user_id: id, times?: number): Promise - sendLikeAsync(user_id: id, times?: number): Promise - sendGroupSign(group_id: id): Promise - sendGroupSignAsync(group_id: id): Promise - getMsg(message_id: id): Promise - getForwardMsg(message_id: id): Promise - getEssenceMsgList(group_id: id): Promise - getWordSlices(content: string): Promise - ocrImage(image: string): Promise - getGroupMsgHistory(group_id: id, message_seq?: number): Promise<{ messages: Message[] }> - deleteFriend(user_id: id): Promise - deleteFriendAsync(user_id: id): Promise - deleteUnidirectionalFriend(user_id: id): Promise - deleteUnidirectionalFriendAsync(user_id: id): Promise - setFriendAddRequest(flag: string, approve: boolean, remark?: string): Promise - setFriendAddRequestAsync(flag: string, approve: boolean, remark?: string): Promise - setGroupAddRequest(flag: string, subType: 'add' | 'invite', approve: boolean, reason?: string): Promise - setGroupAddRequestAsync(flag: string, subType: 'add' | 'invite', approve: boolean, reason?: string): Promise - - setGroupKick(group_id: id, user_id: id, reject_add_request?: boolean): Promise - setGroupKickAsync(group_id: id, user_id: id, reject_add_request?: boolean): Promise - setGroupBan(group_id: id, user_id: id, duration?: number): Promise - setGroupBanAsync(group_id: id, user_id: id, duration?: number): Promise - setGroupWholeBan(group_id: id, enable?: boolean): Promise - setGroupWholeBanAsync(group_id: id, enable?: boolean): Promise - setGroupAdmin(group_id: id, user_id: id, enable?: boolean): Promise - setGroupAdminAsync(group_id: id, user_id: id, enable?: boolean): Promise - setGroupAnonymous(group_id: id, enable?: boolean): Promise - setGroupAnonymousAsync(group_id: id, enable?: boolean): Promise - setGroupCard(group_id: id, user_id: id, card?: string): Promise - setGroupCardAsync(group_id: id, user_id: id, card?: string): Promise - setGroupLeave(group_id: id, is_dismiss?: boolean): Promise - setGroupLeaveAsync(group_id: id, is_dismiss?: boolean): Promise - setGroupSpecialTitle(group_id: id, user_id: id, special_title?: string, duration?: number): Promise - setGroupSpecialTitleAsync(group_id: id, user_id: id, special_title?: string, duration?: number): Promise - setGroupName(group_id: id, name: string): Promise - setGroupNameAsync(group_id: id, name: string): Promise - setGroupPortrait(group_id: id, file: string, cache?: boolean): Promise - setGroupPortraitAsync(group_id: id, file: string, cache?: boolean): Promise - getGroupAtAllRemain(group_id: id): Promise - sendGroupNotice(group_id: id, content: string): Promise - sendGroupNoticeAsync(group_id: id, content: string): Promise - getGroupNotice(group_id: id): Promise - delGroupNotice(group_id: id, notice_id: id): Promise - - getLoginInfo(): Promise - qidianGetLoginInfo(): Promise - setQqProfile(nickname: string, company: string, email: string, college: string, personal_note: string): Promise - setQqProfileAsync(nickname: string, company: string, email: string, college: string, personal_note: string): Promise - getVipInfo(): Promise - getStrangerInfo(user_id: id, no_cache?: boolean): Promise - getFriendList(): Promise - getUnidirectionalFriendList(): Promise - getGroupInfo(group_id: id, no_cache?: boolean): Promise - getGroupList(): Promise - getGroupMemberInfo(group_id: id, user_id: id, no_cache?: boolean): Promise - getGroupMemberList(group_id: id, no_cache?: boolean): Promise - getGroupHonorInfo(group_id: id, type: HonorType): Promise - getGroupSystemMsg(): Promise - - // files - getGroupFileSystemInfo(group_id: id): Promise - getGroupRootFiles(group_id: id): Promise - getGroupFilesByFolder(group_id: id, folder_id: string): Promise - getGroupFileUrl(group_id: id, file_id: string, busid: number): Promise - downloadFile(url: string, headers?: string | readonly string[], thread_count?: number): Promise - uploadPrivateFile(user_id: id, file: string, name: string): Promise - uploadGroupFile(group_id: id, file: string, name: string, folder?: string): Promise - createGroupFileFolder(group_id: id, folder_id: string, name: string): Promise - deleteGroupFolder(group_id: id, folder_id: string): Promise - deleteGroupFile(group_id: id, folder_id: string, file_id: string, busid: number): Promise - - getOnlineClients(no_cache?: boolean): Promise - checkUrlSafely(url: string): Promise - getModelShow(model: string): Promise - setModelShow(model: string, model_show: string): Promise - - getCookies(domain?: string): Promise - getCsrfToken(): Promise - getCredentials(domain?: string): Promise - getRecord(file: string, out_format: RecordFormat, full_path?: boolean): Promise - getImage(file: string): Promise - canSendImage(): Promise - canSendRecord(): Promise - getStatus(): Promise - getVersionInfo(): Promise - setRestart(delay?: number): Promise - reloadEventFilter(): Promise - - getGuildServiceProfile(): Promise - getGuildList(): Promise - getGuildMetaByGuest(guild_id: id): Promise - getGuildChannelList(guild_id: id, no_cache: boolean): Promise - getGuildMemberList(guild_id: id, next_token?: string): Promise - getGuildMemberProfile(guild_id: id, user_id: id): Promise - sendGuildChannelMsg(guild_id: id, channel_id: id, message: string | readonly CQCode[]): Promise -} - -export class TimeoutError extends Error { - constructor(args: Dict, url: string) { - super(`Timeout with request ${url}, args: ${JSON.stringify(args)}`) - Object.defineProperties(this, { - args: { value: args }, - url: { value: url }, - }) - } -} - -class SenderError extends Error { - constructor(args: Dict, url: string, retcode: number) { - super(`Error with request ${url}, args: ${JSON.stringify(args)}, retcode: ${retcode}`) - Object.defineProperties(this, { - code: { value: retcode }, - args: { value: args }, - url: { value: url }, - }) - } -} - -const logger = new Logger('onebot') - -export class Internal { - _request?(action: string, params: Dict): Promise - - private async _get(action: string, params = {}): Promise { - 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]]))) - }) - } -} - -// messages -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.defineExtract('send_private_forward_msg', 'message_id', 'user_id', 'messages') -Internal.define('delete_msg', 'message_id') -Internal.define('mark_msg_as_read', 'message_id') -Internal.define('set_essence_msg', 'message_id') -Internal.define('delete_essence_msg', 'message_id') -Internal.define('send_group_sign', 'group_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') - -// group operations -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_notice', 'group_id') -Internal.define('_del_group_notice', 'group_id', 'notice_id') -Internal.define('get_group_at_all_remain', 'group_id') - -// accounts -Internal.define('get_login_info') -Internal.define('qidian_get_login_info') -Internal.define('set_qq_profile', 'nickname', 'company', 'email', 'college', 'personal_note') -Internal.define('get_stranger_info', 'user_id', 'no_cache') -Internal.define('_get_vip_info', 'user_id') -Internal.define('get_friend_list') -Internal.define('get_unidirectional_friend_list') -Internal.define('delete_friend', 'user_id') -Internal.define('delete_unidirectional_friend', 'user_id') - -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_private_file', 'user_id', 'file', 'name') -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.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_member_list', 'guild_id', 'next_token') -Internal.define('get_guild_member_profile', 'guild_id', 'user_id') -Internal.defineExtract('send_guild_channel_msg', 'message_id', 'guild_id', 'channel_id', 'message') diff --git a/adapters/onebot/src/utils.ts b/adapters/onebot/src/utils.ts deleted file mode 100644 index db362b20..00000000 --- a/adapters/onebot/src/utils.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { h, hyphenate, Logger, Universal } from '@satorijs/satori' -import * as qface from 'qface' -import { BaseBot, CQCode } from './bot' -import * as OneBot from './types' - -export * from './types' - -const logger = new Logger('onebot') - -export const decodeUser = (user: OneBot.AccountInfo): Universal.User => ({ - id: user.tiny_id || user.user_id.toString(), - name: user.nickname, - 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 decodeGuildMember = (user: OneBot.SenderInfo): Universal.GuildMember => ({ - user: decodeUser(user), - name: user.card, - roles: [user.role], -}) - -export const adaptQQGuildMemberInfo = (user: OneBot.GuildMemberInfo): Universal.GuildMember => ({ - user: { - id: user.tiny_id, - name: user.nickname, - isBot: user.role_name === '机器人', - }, - name: user.nickname, - roles: user.role_name ? [user.role_name] : [], -}) - -export const adaptQQGuildMemberProfile = (user: OneBot.GuildMemberProfile): Universal.GuildMember => ({ - user: { - id: user.tiny_id, - name: user.nickname, - isBot: user.roles?.some(r => r.role_name === '机器人'), - }, - name: user.nickname, - roles: user.roles?.map(r => r.role_name) || [], -}) - -export async function adaptMessage( - bot: BaseBot, - data: OneBot.Message, - message: Universal.Message = {}, - payload: Universal.MessageLike = message, -) { - message.id = message.messageId = data.message_id.toString() - - // message content - const chain = CQCode.parse(data.message) - if (bot.config.advanced.splitMixedContent) { - chain.forEach((item, index) => { - if (item.type !== 'image') return - const left = chain[index - 1] - if (left && left.type === 'text' && left.attrs.content.trimEnd() === left.attrs.content) { - left.attrs.content += ' ' - } - const right = chain[index + 1] - if (right && right.type === 'text' && right.attrs.content.trimStart() === right.attrs.content) { - right.attrs.content = ' ' + right.attrs.content - } - }) - } - - message.elements = h.transform(chain, { - at({ qq }) { - if (qq !== 'all') return h.at(qq) - return h('at', { type: 'all' }) - }, - face({ id }) { - const name = qface.get(id)?.QDes.slice(1) - return h('face', { id, name, platform: bot.platform }, [ - h.image(qface.getUrl(id)), - ]) - }, - record(attrs) { - return h('audio', attrs) - }, - }) - const [guildId, channelId] = decodeGuildChannelId(data) - if (message.elements[0]?.type === 'reply') { - const reply = message.elements.shift() - message.quote = await bot.getMessage(channelId, reply.attrs.id).catch((error) => { - logger.warn(error) - return undefined - }) - } - message.content = message.elements.join('') - - if (!payload) return message - payload.user = decodeUser(data.sender) - payload.member = decodeGuildMember(data.sender) - payload.timestamp = data.time * 1000 - payload.guild = guildId && { id: guildId } - payload.channel = channelId && { id: channelId, type: guildId ? Universal.Channel.Type.TEXT : Universal.Channel.Type.DIRECT } -} - -const decodeGuildChannelId = (data: OneBot.Message) => { - if (data.guild_id) { - return [data.guild_id, data.channel_id] - } else if (data.group_id) { - return [data.group_id.toString(), data.group_id.toString()] - } else { - return [undefined, 'private:' + data.sender.user_id] - } -} - -export const adaptGuild = (info: OneBot.GroupInfo | OneBot.GuildBaseInfo): Universal.Guild => { - if ((info as OneBot.GuildBaseInfo).guild_id) { - const guild = info as OneBot.GuildBaseInfo - return { - id: guild.guild_id, - name: guild.guild_name, - } - } else { - const group = info as OneBot.GroupInfo - return { - id: group.group_id.toString(), - name: group.group_name, - } - } -} - -export const adaptChannel = (info: OneBot.GroupInfo | OneBot.ChannelInfo): Universal.Channel => { - if ((info as OneBot.ChannelInfo).channel_id) { - const channel = info as OneBot.ChannelInfo - return { - id: channel.channel_id, - name: channel.channel_name, - type: Universal.Channel.Type.TEXT, - } - } else { - const group = info as OneBot.GroupInfo - return { - id: group.group_id.toString(), - name: group.group_name, - type: Universal.Channel.Type.TEXT, - } - } -} - -export async function dispatchSession(bot: BaseBot, data: OneBot.Payload) { - if (data.self_tiny_id) { - // don't dispatch any guild message without guild initialization - bot = bot['guildBot'] - if (!bot) return - } - - const session = await adaptSession(bot, data) - if (!session) return - session.setInternal('onebot', data) - bot.dispatch(session) -} - -export async function adaptSession(bot: BaseBot, data: OneBot.Payload) { - const session = bot.session() - session.selfId = data.self_tiny_id ? data.self_tiny_id : '' + data.self_id - session.type = data.post_type - - if (data.post_type === 'message' || data.post_type === 'message_sent') { - await adaptMessage(bot, data, session.event.message = {}, session.event) - if (data.post_type === 'message_sent' && !session.guildId) { - session.channelId = 'private:' + data.target_id - } - session.type = 'message' - session.subtype = data.message_type === 'guild' ? 'group' : data.message_type - session.isDirect = data.message_type === 'private' - 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 - // from go-cqhttp source code, but not mentioned in official docs - case 'guild_channel_recall': - session.type = 'message-deleted' - session.subtype = 'guild' - break - case 'friend_add': - session.type = 'friend-added' - break - case 'group_admin': - session.type = 'guild-member' - session.subtype = 'role' - break - case 'group_ban': - session.type = 'guild-member' - session.subtype = 'ban' - break - case 'group_decrease': - session.type = session.userId === session.selfId ? 'guild-deleted' : 'guild-member-deleted' - session.subtype = session.userId === session.operatorId ? 'active' : 'passive' - break - case 'group_increase': - session.type = session.userId === session.selfId ? 'guild-added' : 'guild-member-added' - session.subtype = session.userId === session.operatorId ? 'active' : 'passive' - break - case 'group_card': - session.type = 'guild-member' - session.subtype = 'nickname' - break - case 'notify': - session.type = 'notice' - session.subtype = hyphenate(data.sub_type) as any - if (session.subtype === 'poke') { - session.channelId ||= `private:${session.userId}` - } else if (session.subtype === 'honor') { - session.subsubtype = hyphenate(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 - case 'offline_file': - session.elements = [h('file', data.file)] - session.type = 'message' - session.subtype = 'private' - session.isDirect = true - session.subsubtype = 'offline-file-added' - break - case 'group_upload': - session.elements = [h('file', data.file)] - session.type = 'message' - session.subtype = 'group' - session.subsubtype = 'guild-file-added' - break - default: return - } - } else return - - return session -} diff --git a/adapters/onebot/src/ws.ts b/adapters/onebot/src/ws.ts deleted file mode 100644 index c87327b9..00000000 --- a/adapters/onebot/src/ws.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Adapter, Context, Logger, Quester, Schema, Time, Universal, WebSocketLayer } from '@satorijs/satori' -import { OneBotBot } from './bot' -import { dispatchSession, Response, TimeoutError } from './utils' - -const logger = new Logger('onebot') - -interface SharedConfig { - protocol: T - responseTimeout?: number -} - -export class WsClient extends Adapter.WsClient> { - accept(socket: Universal.WebSocket): void { - accept(socket, this.bot) - } - - prepare() { - const { token, endpoint } = this.bot.config - const http = this.ctx.http.extend(this.bot.config) - if (token) http.config.headers.Authorization = `Bearer ${token}` - return http.ws(endpoint) - } -} - -export namespace WsClient { - export interface Config extends SharedConfig<'ws'>, Quester.Config, Adapter.WsClientConfig {} - - export const Config: Schema = Schema.intersect([ - Schema.object({ - protocol: Schema.const('ws').required(process.env.KOISHI_ENV !== 'browser'), - responseTimeout: Schema.natural().role('time').default(Time.minute).description('等待响应的时间 (单位为毫秒)。'), - }).description('连接设置'), - Quester.createConfig(true), - Adapter.WsClientConfig, - ]) -} - -const kSocket = Symbol('socket') - -export class WsServer extends Adapter> { - public wsServer?: WebSocketLayer - - constructor(ctx: Context, bot: OneBotBot) { - super() - - const { path = '/onebot' } = bot.config as WsServer.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[kSocket] = socket - accept(socket, bot) - }) - - ctx.on('dispose', () => { - logger.debug('ws server closing') - this.wsServer.close() - }) - } - - async disconnect(bot: OneBotBot) { - bot[kSocket]?.close() - bot[kSocket] = null - } -} - -export namespace WsServer { - export interface Config extends SharedConfig<'ws-reverse'> { - path?: string - } - - export const Config: Schema = Schema.object({ - protocol: Schema.const('ws-reverse').required(process.env.KOISHI_ENV === 'browser'), - path: Schema.string().description('服务器监听的路径。').default('/onebot'), - responseTimeout: Schema.natural().role('time').default(Time.minute).description('等待响应的时间 (单位为毫秒)。'), - }).description('连接设置') -} - -let counter = 0 -const listeners: Record void> = {} - -export function accept(socket: Universal.WebSocket, bot: OneBotBot) { - socket.addEventListener('message', ({ data }) => { - let parsed: any - try { - parsed = JSON.parse(data.toString()) - } 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] - } - }) - - socket.addEventListener('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 TimeoutError(params, action)) - }, bot.config.responseTimeout) - socket.send(JSON.stringify(data)) - }) - } - - bot.initialize() -} diff --git a/adapters/onebot/tsconfig.json b/adapters/onebot/tsconfig.json deleted file mode 100644 index 74ac2c8d..00000000 --- a/adapters/onebot/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base", - "compilerOptions": { - "outDir": "lib", - "rootDir": "src", - }, - "include": [ - "src", - ], -} \ No newline at end of file