diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts new file mode 100644 index 0000000..e3a6a1c --- /dev/null +++ b/src/adapter/adapter.ts @@ -0,0 +1,572 @@ +/** + * KarinAdapter + * + * @description KarinAdapter 适配器,所有适配器都应该继承它。用户直接看到的应该是这个继承器 + * @todo 补全方法 + * @class KarinAdapter + */ + + +export class KarinAdapter { + constructor () { + this.index = 0 + this.self_id = '' + this.account = { uid: '', uin: '', name: '' } + this.version = { name: '', app_name: '', version: '' } + this.adapter = { id: '', name: '', type: '', sub_type: '', url: '', start_time: Date.now() } + } + + /** + * @type {number} 重连次数 + */ + index + + /** + * @type {string} 机器人id 一般情况为QQ号 + */ + self_id + + /** + * @type {account} 账号信息 + * @typedef account 账号信息 + * @property {string} account.uid - 账号uid + * @property {string} account.uin - 账号uin + * @property {string} account.name - 账号名 + */ + account + + /** + * @type {adapter} 适配器信息 + * @typedef {object} adapter 适配器信息 + * @property {'QQ' | 'WeChat' | 'Telegram' | string} adapter.id - 适配器ID + * @property {'icqq'|'onebot11'|'ontbot12'|'kritor'|string} adapter.name - 适配器名称 + * @property {'internal'|'websocket'|'grpc'|'http'|'render'} adapter.type - 适配器类型 + * @property {'server'|'client'|undefined} adapter.sub_type - 适配器子类型 + * @property {string|undefined} adapter.url - 适配器连接地址 internal和render下为undefined + * @property {number} adapter.start_time - 适配器连接时间 + */ + adapter + + /** + * @type {version} 适配器版本信息 + * @typedef {object} version 适配器版本信息 + * @property {string} version.name - 适配器名称 + * @property {string} version.app_name - 适配器名称 + * @property {string} version.version - 适配器版本 + */ + version + + /** + * 获取头像url + * @param {number} _size 头像大小,默认`0` + * @param {string|number} _uid 用户qq,默认为机器人QQ + * @returns {string} 头像的url地址 + */ + + getAvatarUrl (_uid = this.#id, _size = 0) { + throw new Error('Not implemented') + } + + /** + * 获取群头像 + * @param {string} _group_id - 群号 + * @param {number?} _size - 头像大小,默认`0` + * @param {number?} _history - 历史头像记录,默认`0`,若要获取历史群头像则填写1,2,3... + * @returns {string} - 群头像的url地址 + */ + + getGroupAvatar (_group_id, _size = 0, _history = 0) { + throw new Error('Not implemented') + } + + /** + * 发送私聊消息 + * @param {number} user_id - 用户ID + * @param {Array} message - 要发送的内容 + * @returns {Promise<{message_id:string}>} - 消息ID + */ + + async send_private_msg (user_id, message) { + throw new Error('Not implemented') + } + + /** + * 发送群消息 + * @param {number} group_id - 群号 + * @param {Array} message - 要发送的内容 + * @returns {Promise<{message_id:string}>} - 消息ID + */ + + async send_group_msg (group_id, message) { + throw new Error('Not implemented') + } + + /** + * 发送消息 + * + * @param {KarinContact} contact + * @param {Array} elements + * @returns {Promise<{message_id:string}>} - 消息ID + */ + + async SendMessage (contact, elements) { + throw new Error('Not implemented') + } + + /** + * 上传合并转发消息 + * @param {KarinContact} contact - 联系人信息 + * @param {KarinNodeElement[] | KarinNodeElement} elements - nodes + * @returns {Promise} - 资源id + * */ + + async UploadForwardMessage (contact, elements) { + throw new Error('Not implemented') + } + + /** + * 通过资源id发送转发消息 + * @param {KarinContact} contact - 联系人信息 + * @param {string} id - 资源id + * @return {Promise} + * */ + + async SendMessageByResId (contact, id) { + throw new Error('Not implemented') + } + + /** + * 撤回消息 + * @param {string?} _contact - ob11无需提供contact参数 + * @param {number} _message_id - 消息ID + * @returns {Promise} + */ + + async RecallMessage (_contact, _message_id) { + throw new Error('Not implemented') + } + + /** + * 获取消息 + * @param {string?} _contact - ob11无需提供contact参数 + * @param {number} _message_id - 消息ID + * @returns {Promise} - 消息内容 + */ + + async GetMessage (_contact, _message_id) { + throw new Error('Not implemented') + } + + /** + * 获取msg_id获取历史消息 + * @param {KarinContact} contact - 联系人信息 + * @param {string} start_message_id - 起始消息ID + * @param {number} count - 获取消息数量 + * @return {Promise>} - 消息内容 + * */ + + async GetHistoryMessage (contact, start_message_id, count) { + throw new Error('Not implemented') + } + + /** + * todo + * 获取合并转发消息 + * @param {string} id - 合并转发 ID + * @returns {Promise} - 消息内容,使用消息的数组格式表示,数组中的消息段全部为 node 消息段 + */ + + async get_forward_msg (id) { + throw new Error('Not implemented') + } + + /** + * 发送好友赞 + * @param {{ + * target_uid?: string, + * target_uin?: string, + * times: number + * }} options + * @param options.target_uid - 好友 QQ 号 任选其一 + * @param options.target_uin - 好友 QQ 号 任选其一 + * @param options.times - 赞的次数,默认为 10 + */ + + async VoteUser (options) { + throw new Error('Not implemented') + } + + /** + * 群组踢人 + * @param {{ + * group_id:string, + * target_uid?:string, + * target_uin?:string, + * reject_add_request?:boolean, + * kick_reason?:string + * }} options + * @param options.group_id - 群组ID + * @param options.target_uid - 被踢出目标的 uid 任选其一 + * @param options.target_uin - 被踢出目标的 uin 任选其一 + * @param options.reject_add_request - 是否拒绝再次申请,默认为false + * @param options.kick_reason - 踢出原因,可选 + * @returns {Promise} - 踢出操作的响应 + */ + + async KickMember (options) { + throw new Error('Not implemented') + } + + /** + * 禁言用户 + * @param {{ + * group_id:string, + * target_uid?:string, + * target_uin?:string, + * duration:number + * }} options + * @param options.group_id - 群组ID + * @param options.target_uid - 被禁言目标的uin 任选其一 + * @param options.target_uin - 被禁言目标的uid 任选其一 + * @param options.duration - 禁言时长(单位:秒) + * @returns {Promise} - 禁言操作的响应 + */ + + async BanMember (options) { + throw new Error('Not implemented') + } + + /** + * 群组匿名用户禁言 + * @param {number} group_id - 群号 + * @param {object} anonymous - 要禁言的匿名用户对象(群消息上报的 anonymous 字段) + * @param {string} [anonymous_flag] - 要禁言的匿名用户的 flag(需从群消息上报的数据中获得) + * @param {number} [duration=1800] - 禁言时长,单位秒,无法取消匿名用户禁言 + */ + + async set_group_anonymous_ban (group_id, anonymous, anonymous_flag, duration = 1800) { + throw new Error('Not implemented') + } + + /** + * 群组全员禁言 + * @param {number} group_id - 群号 + * @param {boolean} [enable=true] - 是否全员禁言 + */ + + async SetGroupWholeBan (group_id, enable = true) { + throw new Error('Not implemented') + } + + /** + * 设置群管理员 + * @param {{ + * group_id:string, + * target_uid?:string, + * target_uin?:string, + * is_admin:boolean + * }} options - 设置管理员选项 + * @param options.group_id - 群组ID + * @param options.target_uid - 要设置为管理员的用户uid + * @param options.target_uin - 要设置为管理员的用户uin + * @param options.is_admin - 是否设置为管理员 + * @returns {Promise} - 设置群管理员操作的响应 + */ + + async SetGroupAdmin (options) { + throw new Error('Not implemented') + } + + /** + * 群组匿名 + * @param {number} group_id - 群号 + * @param {boolean} [enable=true] - 是否允许匿名聊天 + */ + + async set_group_anonymous (group_id, enable = true) { + throw new Error('Not implemented') + } + + /** + * 修改群名片 + * @param {{ + * group_id:string, + * target_uid?:string, + * target_uin?:string, + * card:string + * }} options + * @param {number} options.group_id - 群组ID + * @param {string|number} options.target_uid - 目标用户的 uid 任选其一 + * @param {string|number} options.target_uin - 目标用户的 uin 任选其一 + * @param {string} options.card - 新的群名片 + * @returns {Promise} - 修改群名片操作的响应 + */ + + async ModifyMemberCard (options) { + throw new Error('Not implemented') + } + + /** + * 设置群名 + * @param {number} group_id - 群号 + * @param {string} group_name - 新群名 + */ + + async ModifyGroupName (group_id, group_name) { + throw new Error('Not implemented') + } + + /** + * 退出群组 + * @param {number} group_id - 群号 + * @param {boolean} [is_dismiss=false] - 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 + */ + + async LeaveGroup (group_id, is_dismiss = false) { + throw new Error('Not implemented') + } + + /** + * 设置群专属头衔 + * @param {{ + * group_id:string, + * target_uid?:string, + * target_uin?:string, + * unique_title:string + * }} options - 设置头衔选项 + * @param options.group_id - 群组ID + * @param options.target_uid - 目标用户的uid + * @param options.target_uin - 目标用户的uin + * @param options.unique_title - 新的群头衔 + * @returns {Promise} - 设置群头衔操作的响应 + */ + + async SetGroupUniqueTitle (options) { + throw new Error('Not implemented') + } + + /** + * 处理加好友请求 + * @param {string} flag - 加好友请求的 flag(需从上报的数据中获得) + * @param {boolean} [approve=true] - 是否同意请求 + * @param {string} [remark=''] - 添加后的好友备注(仅在同意时有效) + */ + + async set_friend_add_request (flag, approve = true, remark = '') { + throw new Error('Not implemented') + } + + /** + * 处理加群请求/邀请 + * @param {string} flag - 加群请求的 flag(需从上报的数据中获得) + * @param {string} sub_event - add 或 invite,请求类型(需要和上报消息中的 sub_event 字段相符) + * @param {boolean} [approve=true] - 是否同意请求/邀请 + * @param {string} [reason=''] - 拒绝理由(仅在拒绝时有效) + */ + + async set_group_add_request (flag, sub_event, approve = true, reason = '') { + throw new Error('Not implemented') + } + + /** + * 获取登录号信息 + * @returns {Promise<{account_uid:string, account_uin:string, account_name:number}>} - 登录号信息 + */ + + async GetCurrentAccount () { + throw new Error('Not implemented') + } + + /** + * 获取陌生人信息 不支持批量获取 + * @param {Object} [options] - 陌生人信息选项 + * @param {Array} [options.target_uids] - 目标用户的 uid 数组 可选 + * @param {Array} [options.target_uins] - 目标用户的 uin 数组 可选 + * @param {boolean} [options.no_cache=false] - 是否不使用缓存 + * @returns {Promise} - 获取到的陌生人信息 + */ + + async GetStrangerProfileCard (options) { + throw new Error('Not implemented') + } + + /** + * 获取好友列表 + * @returns {Promise>} - 好友列表 + */ + + async GetFriendList () { + throw new Error('Not implemented') + } + + /** + * 获取群信息 + * @param {number} group_id - 群号 + * @param {boolean} [no_cache=false] - 是否不使用缓存 + * @returns {Promise} - 群信息 + */ + + async GetGroupInfo (group_id, no_cache = false) { + throw new Error('Not implemented') + } + + /** + * 获取群列表 + * @returns {Promise>} - 群列表 + */ + + async GetGroupList () { + throw new Error('Not implemented') + } + + /** + * 获取群成员信息 + * @param {{ + * group_id:string, + * target_uid?:string, + * target_uin?:string, + * refresh?:boolean + * }} options - 获取成员信息选项 + * @param options.group_id - 群组ID + * @param options.target_uid - 目标用户的uid + * @param options.target_uin - 目标用户的uin + * @param options.refresh - 是否刷新缓存,默认为 false + * @returns {Promise} - 获取群成员信息操作的响应 + */ + + async GetGroupMemberInfo (options) { + throw new Error('Not implemented') + } + + /** + * 获取群成员列表 + * @param {Object} options - 获取成员列表选项 + * @param {number} options.group_id - 群组ID + * @param {boolean} [options.refresh] - 是否刷新缓存 + * @returns {Promise} - 获取群成员列表操作的响应 + */ + + async GetGroupMemberList (options) { + throw new Error('Not implemented') + } + + /** + * 获取群荣誉信息 + * @param {Object} options - 获取群荣誉信息选项 + * @param {number} options.group_id - 群号 + * @param {boolean} [options.refresh] - 是否刷新缓存 + * @returns {Promise} - 获取群荣誉信息操作的响应 + */ + + async get_group_honor_info (options) { + throw new Error('Not implemented') + } + + /** + * 获取 Cookies + * @param {string} domain - 需要获取 cookies 的域名 + * @returns {Promise<{string}>} - Cookies + */ + + async get_cookies (domain) { + throw new Error('Not implemented') + } + + /** + * 获取 CSRF Token + * @returns {Promise} - CSRF Token + */ + + async get_csrf_token () { + throw new Error('Not implemented') + } + + /** + * 获取 QQ 相关接口凭证 + * @param {string} domain - 需要获取 cookies 的域名 + * @returns {Promise<{ + * cookies: string, + * cstf_token: number + * }>} - QQ 相关接口凭证 + */ + + async get_credentials (domain) { + throw new Error('Not implemented') + } + + /** + * 获取语音 + * @param {string} file - 收到的语音文件名 + * @param {string} out_format - 要转换到的格式 + * @returns {Promise<{ + * file: string, + * out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' + * }>} - 转换后的语音文件路径 + */ + + async get_record (file, out_format) { + throw new Error('Not implemented') + } + + /** + * 获取图片 + * @param {string} file - 收到的图片文件名 + * @returns {Promise<{ + * size: number, + * filename: string, + * url: string + * }>} - 下载后的图片文件路径 + */ + + async get_image (file) { + throw new Error('Not implemented') + } + + /** + * 检查是否可以发送图片 + * @returns {Promise} - 是否可以发送图片 + */ + + async can_send_image () { + throw new Error('Not implemented') + } + + /** + * 检查是否可以发送语音 + * @returns {Promise} - 是否可以发送语音 + */ + + async can_send_record () { + throw new Error('Not implemented') + } + + /** + * 获取运行状态 + * @returns {Promise} - 运行状态 + */ + + async get_status () { + throw new Error('Not implemented') + } + + /** + * 获取版本信息 + * @returns {Promise} - 版本信息 + */ + + async GetVersion () { + throw new Error('Not implemented') + } + + /** + * 发送合并转发消息 + * @param {KarinContact} contact + * @param {Array} elements + * @return {Promise} + */ + async sendForwardMessage (contact, elements) { + let resId = await this.UploadForwardMessage(contact, elements) + if (this.version.name === 'Lagrange.OneBot') resId = await this.SendMessageByResId(contact, resId) + return resId + } +} diff --git a/src/adapter/onebot/onebot11.ts b/src/adapter/onebot/onebot11.ts new file mode 100644 index 0000000..2fc39b8 --- /dev/null +++ b/src/adapter/onebot/onebot11.ts @@ -0,0 +1,1359 @@ +import { KarinMessage } from '../../event/message' +import { KarinNotice } from '../../event/notice' +import { KarinRequest } from '../../event/request' +import WebSocket from 'ws' +import { IncomingMessage } from 'http' +import { randomUUID } from 'crypto' +import { KarinAdapter } from '../../types/adapter' +import logger from '../../utils/logger' +import common from '../../utils/common' +import listener from '../../core/listener' +import config from '../../utils/config' +import { ByPostType, CustomNodeSegment, OneBot11Api, OneBot11ApiParamsType, OneBot11Event, OneBot11Segment } from '../../types/onebots11' +import { Role, Scene, contact } from '../../types/types' +import segment from '../../utils/segment' +import { KarinElement } from '../../types/element' + +/** + * @typedef OneBotSegmentNode + * @property {'node'} type - 节点类型 + * @property {object} data - 节点数据 + * @property data.uin - 用户QQ号 + * @property data.name - 用户昵称 + * @property {Array} data.content - 节点内容 + */ + +/** + * @typedef {object} version 适配器版本信息 + * @property version.app_name - 适配器名称 + * @property version.version - 适配器版本 + */ + +/** + * @class OneBot11 + * @extends KarinAdapter + */ +export class OneBot11 implements KarinAdapter { + /** + * 是否初始化 + */ + #init = false + /** + * 机器人QQ号 + */ + self_id: string + /** + * - 重连次数 仅正向ws使用 + */ + index: number + socket!: WebSocket.WebSocket + account: KarinAdapter['account'] + adapter: KarinAdapter['adapter'] + version: KarinAdapter['version'] + + constructor() { + this.self_id = '' + this.index = 0 + this.account = { uid: '', uin: '', name: '' } + this.adapter = { id: 'QQ', name: 'OneBot11', type: 'ws', sub_type: 'internal', start_time: Date.now(), connect: '' } + this.version = { name: '', app_name: '', version: '' } + } + + /** + * 反向ws初始化 + */ + async server(socket: WebSocket, request: IncomingMessage) { + this.socket = socket + + const self_id = String(request.headers['x-self-id']) as string + const connect = `ws://` + (request.headers.host as String) + (request.url as String) + + this.account.uin = self_id + this.account.uid = self_id + this.adapter.connect = connect + this.adapter.sub_type = 'server' + this.logger('info', `[反向WS][onebot11-${request.headers.upgrade}][${self_id}] ` + logger.green(connect)) + await this.#initListener(connect) + } + + /** + * 正向ws初始化 + * @param connect - WebSocket连接地址 + */ + async client(connect: string) { + /** 创建连接 */ + this.socket = new WebSocket(connect) + + this.socket.on('open', async () => { + this.adapter.sub_type = 'client' + this.adapter.connect = connect + + logger.info('[正向WS][连接成功][onebot11] ' + logger.green(connect)) + this.index = 0 + this.#initListener(connect) + }) + + /** 监听断开 */ + this.socket.on('close', async () => { + this.index++ + logger.warn(`[正向WS][重连次数:${this.index}] 连接断开,将在5秒后重连:${connect}`) + /** 停止全部监听 */ + this.socket.removeAllListeners() + await common.sleep(5000) + this.client(connect) + }) + } + + /** + * 初始化监听事件 + * @param connect - WebSocket连接地址 + */ + async #initListener(connect: string) { + /** 监听事件 */ + this.socket.on('message', data => { + this.logger('debug', `[收到事件]:${data}`) + const event = data.toString().trim() || '{"post_type":"error","error":"空事件"}' + const json = JSON.parse(event) + if (json.echo) return this.socket.emit(json.echo, json) + /** 未初始化 */ + if (!this.#init) return + this.#init && this.#event(json) + }) + + /** 监听错误 */ + this.socket.on('error', error => { + this.logger('debug', '[正向WS] 发生错误', error) + }) + + /** 监听断开 */ + this.socket.once('close', async () => { + const type = this.adapter.sub_type === 'server' ? '反向WS' : '正向WS' + this.logger('warn', `[${type}] 连接断开:${connect}`) + /** 停止全部监听 */ + this.socket.removeAllListeners() + + /** 正向ws需要重连 */ + if (this.adapter.sub_type === 'client') { + this.index++ + this.logger('warn', `[正向WS][重连次数:${this.index}] 连接断开,将在5秒后重连:${connect}`) + await common.sleep(5000) + this.client(connect) + } + }) + await this.getSelf() + this.#init = true + } + + /** + * 获取当前登录号信息 + */ + async getSelf() { + const data = await this.GetCurrentAccount() + try { + const { app_name, app_version: version } = await this.GetVersion() + this.version.name = app_name + this.version.app_name = app_name + this.version.version = version + } catch (e) { + /** 兼容onebots */ + const { app_name, app_version: version } = await this.SendApi('get_version') + this.version.name = app_name + this.version.app_name = app_name + this.version.version = version + } + + this.account.uid = data.account_uid + this.account.uin = data.account_uin + this.account.name = data.account_name + this.logger('info', `[加载完成][app_name:${this.version.name}][version:${this.version.version}] ` + logger.green(this.adapter.connect as string)) + /** 注册bot */ + listener.emit('bot', { type: 'websocket', bot: this }) + } + + /** 是否初始化 */ + get isInit() { + return new Promise(resolve => { + const timer = setInterval(() => { + if (this.account.name) { + const { name, version } = this.version + this.logger('info', `建立连接成功:[${name}(${version})] ${this.adapter.connect}`) + clearInterval(timer) + resolve(true) + } + }, 100) + }) + } + + /** 处理事件 */ + #event(data: OneBot11Event) { + switch (data.post_type) { + case 'meta_event': { + switch (data.meta_event_type) { + case 'heartbeat': + this.logger('trace', `[心跳]:${JSON.stringify(data.status)}`) + break + case 'lifecycle': { + const typeMap = { + enable: 'OneBot启用', + disable: 'OneBot停用', + connect: 'WebSocket连接成功', + } + const sub_type = data.sub_type + this.logger('debug', `[生命周期]:${typeMap[sub_type]}`) + break + } + } + listener.emit('meta_event', data) + return + } + case 'message': + case 'message_sent': { + const message = { + event: (data.post_type + '') as 'message' | 'message_sent', + self_id: data.self_id + '', + user_id: data.sender.user_id + '', + time: data.time, + message_id: data.message_id + '', + message_seq: data.message_id + '', + sender: { + ...data.sender, + uid: data.sender.user_id + '', + uin: data.sender.user_id + '', + nick: data.sender.nickname || '', + role: ('role' in data.sender ? data.sender.role || '' : '') as Role, + }, + elements: this.AdapterConvertKarin(data.message), + contact: { + scene: (data.message_type === 'private' ? 'private' : 'group') as 'private' | 'group', + peer: data.message_type === 'private' ? data.sender.user_id : data.group_id, + sub_peer: '', + }, + group_id: '', + raw_message: '', + } + + const e = new KarinMessage(message) + e.bot = this + /** + * 快速回复 开发者不应该使用这个方法,应该使用由karin封装过后的reply方法 + */ + e.replyCallback = async elements => { + if (data.message_type === 'private') { + return this.send_private_msg(data.user_id, elements) + } else { + return this.send_group_msg(data.group_id, elements) + } + } + + listener.emit('message', e) + return + } + case 'notice': + this.#notice_event(data) + break + case 'request': + this.#request_event(data) + break + default: + this.logger('info', `未知事件:${JSON.stringify(data)}`) + } + } + + /** + * 通知事件 + */ + #notice_event(data: ByPostType<'notice'>) { + const time = data.time + const self_id = data.self_id + '' + let notice = {} + + const user_id = data.user_id + '' + const event_id = `notice.${time}` + const sender = { + uid: data.user_id + '', + uin: data.user_id + '', + nick: '', + role: '' as Role, + } + + const contact = { + scene: ('group_id' in data ? 'group' : 'private') as Scene, + peer: 'group_id' in data ? data.group_id : data.user_id, + sub_peer: '', + } + + switch (data.notice_type) { + // 群文件上传 + case 'group_upload': { + const content = { + group_id: data.group_id + '', + operator_uid: data.user_id + '', + operator_uin: data.user_id + '', + file_id: data.file.id, + file_sub_id: '', + file_name: data.file.name, + file_size: data.file.size, + bus_id: 0, + expire_time: 0, + url: '', + } + + const options = { + time, + self_id, + user_id, + event_id, + content, + sender, + contact, + sub_event: 'group_file_uploaded' as 'group_file_uploaded', + } + notice = new KarinNotice(options) + break + } + // 群管理员变动 + case 'group_admin': { + const content = { + group_id: data.group_id + '', + target_uid: data.user_id + '', + target_uin: data.user_id + '', + is_admin: data.sub_type === 'set', + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_admin_changed' as 'group_admin_changed', + } + notice = new KarinNotice(options) + break + } + // 群成员减少 + case 'group_decrease': { + const content = { + group_id: data.group_id + '', + operator_uid: data.operator_id || '', + operator_uin: data.operator_id || '', + target_uid: data.user_id || '', + target_uin: data.user_id || '', + type: data.sub_type, + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_member_decrease' as 'group_member_decrease', + } + notice = new KarinNotice(options) + break + } + // 群成员增加 + case 'group_increase': { + const content = { + group_id: data.group_id + '', + operator_uid: (data.operator_id || '') + '', + operator_uin: (data.operator_id || '') + '', + target_uid: (data.user_id || '') + '', + target_uin: (data.user_id || '') + '', + type: data.sub_type, + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_member_increase' as 'group_member_increase', + } + notice = new KarinNotice(options) + break + } + // 群禁言事件 + case 'group_ban': { + const content = { + group_id: data.group_id, + operator_uid: data.operator_id || '', + operator_uin: data.operator_id || '', + target_uid: data.user_id || '', + target_uin: data.user_id || '', + duration: data.duration, + type: data.sub_type, + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_member_ban' as 'group_member_ban', + } + notice = new KarinNotice(options) + break + } + case 'friend_add': + // todo kritor没有这个事件 + this.logger('info', `[好友添加]:${JSON.stringify(data)}`) + break + case 'group_recall': { + const content = { + group_id: data.group_id, + operator_uid: data.operator_id || '', + operator_uin: data.operator_id || '', + target_uid: data.user_id || '', + target_uin: data.user_id || '', + message_id: data.message_id, + tip_text: '撤回了一条消息', + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_recall' as 'group_recall', + } + notice = new KarinNotice(options) + break + } + case 'friend_recall': { + const content = { + operator_uid: data.user_id || '', + operator_uin: data.user_id || '', + message_id: data.message_id, + tip_text: '撤回了一条消息', + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_recall' as 'group_recall', + } + notice = new KarinNotice(options) + break + } + case 'notify': + switch (data.sub_type) { + case 'poke': { + const content = { + group_id: data.group_id + '', + operator_uid: data.user_id + '', + operator_uin: data.user_id + '', + target_uid: data.target_id + '', + target_uin: data.target_id + '', + action: '戳了戳', + suffix: '', + action_image: '', + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_poke' as 'group_poke', + } + notice = new KarinNotice(options) + break + } + case 'lucky_king': + // todo kritor没有这个事件 + this.logger('info', `[运气王]:${JSON.stringify(data)}`) + break + case 'honor': + // todo kritor没有这个事件 + this.logger('info', `[群荣誉变更]:${JSON.stringify(data)}`) + break + } + break + case 'group_msg_emoji_like': { + const content = { + group_id: data.group_id + '', + message_id: data.message_id, + face_id: data.likes[0].emoji_id, + is_set: true, + } + + const options = { + time, + self_id, + user_id, + event_id, + sender, + contact, + content, + sub_event: 'group_message_reaction' as 'group_message_reaction', + } + notice = new KarinNotice(options) + break + } + default: { + return this.logger('error', '未知通知事件:', JSON.stringify(data)) + } + } + + listener.emit('notice', notice) + } + + /** 请求事件 */ + #request_event(data: ByPostType<'request'>) { + switch (data.request_type) { + case 'friend': { + const request = new KarinRequest({ + event_id: `request.${data.time}`, + self_id: data.self_id + '', + user_id: data.user_id + '', + time: data.time, + contact: { + scene: 'private', + peer: data.user_id + '', + sub_peer: '', + }, + sender: { + uid: data.user_id + '', + uin: data.user_id + '', + nick: '', + role: '' as Role, + }, + sub_event: 'friend_apply' as 'friend_apply', + content: { + applier_uid: data.user_id + '', + applier_uin: data.user_id + '', + message: data.comment, + }, + }) + listener.emit('request', request) + return + } + case 'group': { + const request = new KarinRequest({ + event_id: `request.${data.time}`, + self_id: data.self_id + '', + user_id: data.user_id + '', + time: data.time, + contact: { + scene: 'group', + peer: data.group_id + '', + sub_peer: '', + }, + sender: { + uid: data.user_id + '', + uin: data.user_id + '', + nick: '', + role: '' as Role, + }, + sub_event: data.sub_type === 'add' ? 'group_apply' : 'invited_group', + content: { + group_id: data.group_id + '', + applier_uid: data.user_id + '', + applier_uin: data.user_id + '', + inviter_uid: data.user_id + '', + inviter_uin: data.user_id + '', + message: data.comment, + }, + }) + listener.emit('request', request) + return + } + default: { + this.logger('info', `未知请求事件:${JSON.stringify(data)}`) + } + } + } + + /** + * onebot11转karin + * @param {Array<{type: string, data: any}>} data onebot11格式消息 + * @return karin格式消息 + * */ + AdapterConvertKarin(data: Array) { + const elements = [] + for (const i of data) { + switch (i.type) { + case 'text': + elements.push(segment.text(i.data.text)) + break + case 'face': + elements.push(segment.face(Number(i.data.id))) + break + case 'image': + elements.push(segment.image(i.data.url || i.data.file, { file_type: i.data.type })) + break + case 'record': + elements.push(segment.voice(i.data.url || i.data.file, i.data.magic === 1)) + break + case 'video': + elements.push(segment.video(i.data.url || i.data.file)) + break + case 'at': + elements.push(segment.at(i.data.qq, i.data.qq)) + break + case 'poke': + elements.push(segment.poke(Number(i.data.id), Number(i.data.type))) + break + case 'contact': + elements.push(segment.contact(i.data.type === 'qq' ? 'friend' : 'group', i.data.id)) + break + case 'location': + elements.push(segment.location(Number(i.data.lat), Number(i.data.lon), i.data.title || '', i.data.content || '')) + break + case 'reply': + elements.push(segment.reply(i.data.id)) + break + case 'forward': + elements.push(segment.forward(i.data.id)) + break + case 'json': + elements.push(segment.json(i.data.data)) + break + case 'xml': + elements.push(segment.xml(i.data.data)) + break + default: { + elements.push(segment.text(JSON.stringify(i))) + } + } + } + return elements + } + + /** + * karin转onebot11 + * @param data karin格式消息 + * @return {Array<{type: string, data: any}>} onebot11格式消息 + * */ + KarinConvertAdapter(data: Array) { + const elements = [] + const selfUin = this.account.uin + const selfNick = this.account.name + + for (const i of data) { + switch (i.type) { + case 'text': + elements.push({ type: 'text', data: { text: i.text } }) + break + case 'face': + elements.push({ type: 'face', data: { id: i.id } }) + break + case 'at': + elements.push({ type: 'at', data: { qq: String(i.uid || i.uin) } }) + break + case 'reply': + elements.push({ type: 'reply', data: { id: i.message_id } }) + break + case 'image': + case 'video': + case 'file': { + elements.push({ type: i.type, data: { file: i.file } }) + break + } + case 'xml': + case 'json': { + elements.push({ type: i.type, data: { data: i.data } }) + break + } + // case 'node': { + // let { type, user_id = selfUin, nickname = selfNick, content } = i + // content = this.KarinConvertAdapter(content) + // elements.push({ type, data: { uin: user_id, name: nickname, content } }) + // break + // } + case 'forward': { + elements.push({ type: 'forward', data: { id: i.res_id } }) + break + } + case 'voice': { + elements.push({ type: 'record', data: { file: i.file, magic: i.magic || false } }) + break + } + case 'music': { + // if (i.platform) { + // elements.push({ type: 'music', data: { type: i.platform, id: i.id } }) + // } else { + // const { url, audio, title, content, image } = i + // elements.push({ type: 'music', data: { type: 'custom', url, audio, title, content, image } }) + // } + break + } + case 'button': { + // todo + // elements.push({ type: 'button', data: { buttons: i.buttons } }) + break + } + case 'markdown': { + const { type, ...data } = i + elements.push({ type, data: { ...data } }) + break + } + // case 'rows': { + // for (const val of i.rows) { + // elements.push({ type: 'button', data: { buttons: val.buttons } }) + // } + // break + // } + case 'poke': { + elements.push({ type: 'poke', data: { type: i.poke_type, id: i.id } }) + break + } + default: { + elements.push(i) + logger.info(i) + } + } + } + return elements + } + + /** + * 专属当前Bot的日志打印方法 + */ + logger(level: 'info' | 'error' | 'trace' | 'debug' | 'mark' | 'warn' | 'fatal', ...args: any[]) { + logger.bot(level, this.account.uid || this.account.uin, ...args) + } + + /** + * 获取头像url + * @param 头像大小,默认`0` + * @param 用户qq,默认为机器人QQ + * @returns 头像的url地址 + */ + getAvatarUrl(uid = this.account.uid || this.account.uin, size = 0) { + return Number(uid) ? `https://q1.qlogo.cn/g?b=qq&s=${size}&nk=${uid}` : `https://q.qlogo.cn/qqapp/${uid}/${uid}/${size}` + } + + /** + * 获取群头像 + * @param group_id - 群号 + * @param size - 头像大小,默认`0` + * @param history - 历史头像记录,默认`0`,若要获取历史群头像则填写1,2,3... + * @returns - 群头像的url地址 + */ + getGroupAvatar(group_id: string, size = 0, history = 0) { + return `https://p.qlogo.cn/gh/${group_id}/${group_id}${history ? '_' + history : ''}/` + size + } + + /** + * 发送私聊消息 + * @param user_id - 用户ID + * @param message - 要发送的内容 + * @returns - 消息ID + */ + async send_private_msg(user_id: string, message: Array): Promise<{ message_id?: string }> { + const obMessage = this.KarinConvertAdapter(message) + // this.logger(`${logger.green(`Send private ${user_id}: `)}${this.logSend(message)}`)) + return await this.SendApi('send_private_msg', { user_id, message: obMessage }) + } + + /** + * 发送群消息 + * @param group_id - 群号 + * @param message - 要发送的内容 + * @returns - 消息ID + */ + async send_group_msg(group_id: string, message: Array) { + const obMessages = this.KarinConvertAdapter(message) + return await this.SendApi('send_group_msg', { group_id, message: obMessages }) + } + + /** + * 发送消息 + * + * @param contact + * @param elements + * @returns - 消息ID + */ + async SendMessage(contact: contact, elements: Array) { + let { scene, peer } = contact + const message_type = scene === 'group' ? 'group' : 'private' + const key = scene === 'group' ? 'group_id' : 'user_id' + const message = this.KarinConvertAdapter(elements) + const params = { [key]: peer, message_type, message } + return await this.SendApi('send_msg', params) + } + + /** + * 上传合并转发消息 + * @param contact - 联系人信息 + * @param elements - nodes + * @returns - 资源id + * */ + async UploadForwardMessage(contact: { scene: Scene; peer: string }, elements: any[]) { + if (!Array.isArray(elements)) elements = [elements] + if (elements.some((element: { type: string }) => element.type !== 'node')) { + throw new Error('elements should be all node type') + } + const { scene, peer } = contact + const message_type = scene === 'group' ? 'group_id' : 'user_id' + const messages = this.KarinConvertAdapter(elements) + + const params = { [message_type]: String(peer), messages } + return await this.SendApi('send_forward_msg', params) + } + + /** + * 通过资源id发送转发消息 + * @param contact - 联系人信息 + * @param id - 资源id + * */ + async SendMessageByResId(contact: { scene: Scene; peer: string }, id: any) { + let { scene, peer } = contact + const message_type = scene === 'group' ? 'group' : 'private' + const key = scene === 'group' ? 'group_id' : 'user_id' + const message = [{ type: 'forward', data: { id } }] + const params = { [key]: peer, message_type, message } + const res = await this.SendApi('send_msg', params) + return { message_id: res.message_id, message_time: Date.now() } + } + + /** + * 撤回消息 + * @param {null} [_contact] - ob11无需提供contact参数 + * @param message_id - 消息ID + * @returns {Promise} + */ + + async RecallMessage(_contact: contact, message_id: string) { + return await this.SendApi('delete_msg', { message_id }) + } + + /** + * 获取消息 + * @param {null} [_contact] - ob11无需提供contact参数 + * @param message_id - 消息ID + * @returns {Promise} - 消息内容 + */ + + async GetMessage(_contact: any, message_id: any) { + let res = await this.SendApi('get_msg', { message_id }) + res = { + time: res.time, + message_id: res.message_id, + message_seq: res.message_id, + contact: { + scene: res.message_type === 'group' ? 'group' : 'private', + peer: res.sender.user_id, // 拿不到group_id... + }, + sender: { + uid: res.sender.user_id, + uin: res.sender.user_id, + nick: res.sender.nickname, + }, + elements: this.AdapterConvertKarin(res.message), + } + return res + } + + /** + * 获取msg_id获取历史消息 + * @description 此api各平台实现不同,暂时废弃 + */ + async GetHistoryMessage(contact: contact, start_message_id: string, count: number = 1) { + const type = contact.scene === 'group' ? 'group_id' : 'user_id' + const param = { [type]: contact.peer, message_id: start_message_id, count } + const api = contact.scene === 'group' ? 'get_group_msg_history' : 'get_friend_msg_history' + const res = await this.SendApi(api, param, 120) + const ret = [] + for (const i of res.messages) { + let { time = Date.now(), message_id, message_seq = message_id, sender, message } = i + const { user_id, nickname } = sender + sender = { uid: user_id, uin: user_id, nick: nickname } + const elements = this.AdapterConvertKarin(message) + ret.push({ time, message_id, message_seq, contact, sender, elements }) + } + return ret + } + + /** + * 获取合并转发消息 + */ + async get_forward_msg(id: string): Promise> { + return await this.SendApi('get_forward_msg', { id }) + } + + /** + * 发送好友赞 + * @param target_uid_or_uin - 用户ID + * @param vote_count - 赞的次数,默认为`10` + */ + async VoteUser(target_uid_or_uin: string, vote_count: number = 10) { + const user_id = Number(target_uid_or_uin) + await this.SendApi('send_like', { user_id, times: vote_count }) + } + + /** + * 群组踢人 + */ + async KickMember(group_id: string, target_uid_or_uin: string, reject_add_request: boolean = false, kick_reason: string = '') { + const user_id = Number(target_uid_or_uin) + await this.SendApi('set_group_kick', { group_id, user_id, reject_add_request }) + } + + /** + * 禁言用户 + * @param group_id - 群号 + * @param target_uid_or_uin - 用户ID + * @param duration - 禁言时长,单位秒,0 表示取消禁言 + */ + async BanMember(group_id: string, target_uid_or_uin: string, duration: number) { + const user_id = Number(target_uid_or_uin) + await this.SendApi('set_group_ban', { group_id, user_id, duration }) + } + + /** + * 群组全员禁言 + * @param group_id - 群号 + * @param enable - 是否全员禁言 + */ + async SetGroupWholeBan(group_id: string, enable = true) { + await this.SendApi('set_group_whole_ban', { group_id, enable }) + } + + /** + * 设置群管理员 + * @param {{ + * group_id:string, + * target_uid?:string, + * target_uin?:string, + * is_admin:boolean + * }} options - 设置管理员选项 + * @param options.group_id - 群组ID + * @param options.target_uid - 要设置为管理员的用户uid + * @param options.target_uin - 要设置为管理员的用户uin + * @param options.is_admin - 是否设置为管理员 + * @returns {Promise} - 设置群管理员操作的响应 + */ + async SetGroupAdmin(group_id: string, target_uid_or_uin: string, is_admin: boolean) { + const user_id = Number(target_uid_or_uin) + await this.SendApi('set_group_admin', { group_id, user_id, enable: is_admin }) + } + + /** + * 群组匿名 + * @param group_id - 群号 + * @param enable - 是否允许匿名聊天 + */ + async set_group_anonymous(group_id: string, enable = true) { + await this.SendApi('set_group_anonymous', { group_id, enable }) + } + + /** + * 修改群名片 + * @param group_id - 群号 + * @param target_uid_or_uin - 目标用户ID + * @param card - 新名片 + */ + async ModifyMemberCard(group_id: string, target_uid_or_uin: string, card: string) { + const user_id = Number(target_uid_or_uin) + await this.SendApi('set_group_card', { group_id, user_id, card }) + } + + /** + * 设置群名 + * @param group_id - 群号 + * @param group_name - 新群名 + */ + async ModifyGroupName(group_id: string, group_name: string) { + await this.SendApi('set_group_name', { group_id, group_name }) + } + + /** + * 退出群组 + * @param group_id - 群号 + * @param is_dismiss - 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 + */ + async LeaveGroup(group_id: string, is_dismiss = false) { + await this.SendApi('set_group_leave', { group_id, is_dismiss }) + } + + /** + * 设置群专属头衔 + * @param group_id - 群号 + * @param target_uid_or_uin - 目标用户ID + * @param special_title - 专属头衔 + */ + async SetGroupUniqueTitle(group_id: string, target_uid_or_uin: string, unique_title: string) { + const user_id = Number(target_uid_or_uin) + const special_title = unique_title + const duration = -1 + await this.SendApi('set_group_special_title', { group_id, user_id, special_title, duration }) + } + + // /** + // * 处理加好友请求 + // * @param flag - 加好友请求的 flag(需从上报的数据中获得) + // * @param {boolean} [approve=true] - 是否同意请求 + // * @param [remark=''] - 添加后的好友备注(仅在同意时有效) + // */ + // async set_friend_add_request(flag: string, approve = true, remark = '') { + // await this.SendApi('set_friend_add_request', { flag, approve, remark }) + // } + + // /** + // * 处理加群请求/邀请 + // * @param flag - 加群请求的 flag(需从上报的数据中获得) + // * @param sub_type - add 或 invite,请求类型(需要和上报消息中的 sub_type 字段相符) + // * @param {boolean} [approve=true] - 是否同意请求/邀请 + // * @param [reason=''] - 拒绝理由(仅在拒绝时有效) + // */ + // async set_group_add_request(flag: string, sub_type: string, approve = true, reason = '') { + // await this.SendApi('set_group_add_request', { flag, sub_type, approve, reason }) + // } + + /** + * 获取登录号信息 + */ + async GetCurrentAccount(): Promise<{ + account_uid: string + account_uin: string + account_name: string + }> { + const res = await this.SendApi('get_login_info') + return { + account_uid: res.user_id as string, + account_uin: res.user_id as string, + account_name: res.nickname as string, + } + } + + /** + * 获取陌生人信息 不支持批量获取 只支持一个 + * @param target_uid_or_uin - 目标用户ID + */ + async GetStrangerProfileCard(target_uid_or_uin: Array) { + const user_id = Number(target_uid_or_uin[0]) || String(target_uid_or_uin[0]) + const res = await this.SendApi('get_stranger_info', { user_id, no_cache: true }) + return [res] + } + + /** + * 获取好友列表 + * @returns {Promise>} - 好友列表 + */ + async GetFriendList() { + /** @type {{ + * user_id: number, + * user_name: string?, + * user_remark: string, + * remark: string?, + * nickname: string?, + }[]} **/ + const friendList = await this.SendApi('get_friend_list') + return friendList.map((friend: { user_id: any; nickname: any; user_name: any; remark: any; user_remark: any }) => { + return { + uin: friend.user_id, + uid: friend.user_id, + qid: '', + nick: friend.nickname || friend.user_name, + remark: friend.remark || friend.user_remark, + } + }) + } + + /** + * 获取群信息 + * @param group_id - 群号 + * @param no_cache - 是否不使用缓存 + * @returns {Promise} - 群信息 + */ + async GetGroupInfo(group_id: string, no_cache = false) { + /** + * @type {{ + * group_id: number, + * group_name: string, + * group_memo: string, + * group_remark: string, + * group_create_time: number, + * group_level: number, + * member_count: number, + * max_member_count: number, + * admins: number[] + * }} + */ + const groupInfo = await this.SendApi('get_group_info', { group_id, no_cache }) + return { + group_id: groupInfo.group_id, + group_name: groupInfo.group_name, + group_remark: groupInfo.group_memo || groupInfo.group_remark, + max_member_count: groupInfo.max_member_count, + member_count: groupInfo.member_count, + group_uin: groupInfo.group_id, + admins: groupInfo.admins, + owner: groupInfo.admins[0], + } + } + + /** + * 获取群列表 + */ + async GetGroupList() { + const groupList = await this.SendApi('get_group_list') + return groupList?.map((groupInfo: { group_id: any; group_name: any; group_memo: any; group_remark: any; max_member_count: any; member_count: any; admins: any }) => { + return { + group_id: groupInfo.group_id, + group_name: groupInfo.group_name, + group_remark: groupInfo.group_memo || groupInfo.group_remark, + max_member_count: groupInfo.max_member_count, + member_count: groupInfo.member_count, + group_uin: groupInfo.group_id, + admins: groupInfo.admins, + } + }) + } + + /** + * 获取群成员信息 + * @param group_id - 群号 + * @param target_uid_or_uin - 目标用户ID + * @param refresh - 是否刷新缓存,默认为 false + */ + async GetGroupMemberInfo(group_id: string, target_uid_or_uin: string, refresh = false) { + const user_id = Number(target_uid_or_uin) + /** + * @type {{ + * group_id: number, + * user_id: number, + * nickname: string, + * card: string, + * sex: string, + * age: number, + * area: string, + * join_time: number, + * last_sent_time: number, + * level: string, + * role: 'owner' | 'admin' | 'member', + * unfriendly: boolean, + * title: string, + * title_expire_time: number, + * card_changeable: boolean, + * shut_up_timestamp: number + * }} + */ + const groupMemberInfo = await this.SendApi('get_group_member_info', { group_id, user_id, no_cache: refresh }) + let level = 0 + try { + level = parseInt(groupMemberInfo.level) + } catch (e) {} + return { + uid: groupMemberInfo.user_id, + uin: groupMemberInfo.user_id, + nick: groupMemberInfo.nickname, + age: groupMemberInfo.age, + unique_title: groupMemberInfo.title, + unique_title_expire_time: groupMemberInfo.title_expire_time, + card: groupMemberInfo.card, + join_time: groupMemberInfo.join_time, + last_active_time: groupMemberInfo.last_sent_time, + shut_up_time: 0, + level, + shut_up_timestamp: groupMemberInfo.shut_up_timestamp, + unfriendly: groupMemberInfo.unfriendly, + card_changeable: groupMemberInfo.card_changeable, + } + } + + /** + * 获取群成员列表 + * @param group_id - 群号 + * @param refresh - 是否刷新缓存,默认为 false + */ + async GetGroupMemberList(group_id: string, refresh = false) { + const gl = await this.SendApi('get_group_member_list', { group_id, refresh }) + return gl.map((groupMemberInfo: { level: string; user_id: any; nickname: any; age: any; title: any; title_expire_time: any; card: any; join_time: any; last_sent_time: any; shut_up_timestamp: any; unfriendly: any; card_changeable: any }) => { + let level = 0 + try { + level = parseInt(groupMemberInfo.level) + } catch (e) {} + return { + uid: groupMemberInfo.user_id, + uin: groupMemberInfo.user_id, + nick: groupMemberInfo.nickname, + age: groupMemberInfo.age, + unique_title: groupMemberInfo.title, + unique_title_expire_time: groupMemberInfo.title_expire_time, + card: groupMemberInfo.card, + join_time: groupMemberInfo.join_time, + last_active_time: groupMemberInfo.last_sent_time, + level, + shut_up_timestamp: groupMemberInfo.shut_up_timestamp, + unfriendly: groupMemberInfo.unfriendly, + card_changeable: groupMemberInfo.card_changeable, + } + }) + } + + /** + * 获取群荣誉信息 + */ + async GetGroupHonor(group_id: string, refresh = false) { + /** + * @typedef {{user_id: number, nickname: string, avatar: string, description: string}} GroupHonor + */ + /** + * @type {{ + * group_id: number, + * current_talkative: {user_id: number, nickname: string, avatar: string, day_count: number}, + * talkative_list: Array, + * performer_list: Array, + * legend_list: Array, + * strong_newbie_list: Array, + * emotion_list: Array, + * }} + */ + const groupHonor = await this.SendApi('get_group_honor_info', { group_id, type: 'all' }) + + const result: { uin: string; uid: string; nick: string; honor_name: string; avatar: string; id: number; description: string }[] = [] + groupHonor.talkative_list.forEach((honor: { user_id: any; nickname: any; avatar: any; description: any }) => { + result.push({ + uin: honor.user_id, + uid: honor.user_id, + nick: honor.nickname, + honor_name: '历史龙王', + id: 0, + avatar: honor.avatar, + description: honor.description, + }) + }) + groupHonor.performer_list.forEach((honor: { user_id: any; nickname: any; avatar: any; description: any }) => { + result.push({ + uin: honor.user_id, + uid: honor.user_id, + nick: honor.nickname, + honor_name: '群聊之火', + avatar: honor.avatar, + id: 0, + description: honor.description, + }) + }) + groupHonor.legend_list.forEach((honor: { user_id: any; nickname: any; avatar: any; description: any }) => { + result.push({ + uin: honor.user_id, + uid: honor.user_id, + nick: honor.nickname, + honor_name: '群聊炽焰', + avatar: honor.avatar, + id: 0, + description: honor.description, + }) + }) + groupHonor.strong_newbie_list.forEach((honor: { user_id: any; nickname: any; avatar: any; description: any }) => { + result.push({ + uin: honor.user_id, + uid: honor.user_id, + nick: honor.nickname, + honor_name: '冒尖小春笋', + avatar: honor.avatar, + id: 0, + description: honor.description, + }) + }) + groupHonor.emotion_list.forEach((honor: { user_id: any; nickname: any; avatar: any; description: any }) => { + result.push({ + uin: honor.user_id, + uid: honor.user_id, + nick: honor.nickname, + honor_name: '快乐之源', + avatar: honor.avatar, + id: 0, + description: honor.description, + }) + }) + return result + } + + // /** + // * 对消息进行表情回应 + // * @param Contact - 联系人信息 + // * @param message_id - 消息ID + // * @param face_id - 表情ID + // */ + // async ReactMessageWithEmojiRequest(Contact: any, message_id: any, face_id: any, is_set = true) { + // return await this.SendApi('set_msg_emoji_like', { message_id, emoji_id: face_id, is_set }) + // } + + /** + * 获取版本信息 + */ + async GetVersion() { + return await this.SendApi('get_version_info') + } + + async DownloadForwardMessage() { + throw new Error('Method not implemented.') + } + + async GetEssenceMessageList() { + throw new Error('Method not implemented.') + } + + async SetEssenceMessage() {} + async DeleteEssenceMessage() {} + async SetFriendApplyResult() {} + async SetGroupApplyResultRequest() {} + async SetInvitedJoinGroupResult() {} + async ReactMessageWithEmojiRequest() {} + async UploadPrivateFile() {} + async UploadGroupFile() {} + async sendForwardMessage() { + return {} + } + + /** + * 发送API请求 + * @param action - API断点 + * @param {object} params - API参数 + * @returns {Promise} - API返回 + */ + async SendApi(action: OneBot11Api, params: OneBot11ApiParamsType[OneBot11Api] = {}, time = 0): Promise { + if (!time) time = config.timeout('ws') + const echo = randomUUID() + const request = JSON.stringify({ echo, action, params }) + logger.debug(`[API请求] ${action}: ${request}`) + return new Promise((resolve, reject) => { + this.socket.send(request) + this.socket.once(echo, data => { + if (data.status === 'ok') { + resolve(data.data) + } else { + this.logger('error', `[Api请求错误] ${action}: ${JSON.stringify(data, null, 2)}`) + reject(data) + } + }) + /** 设置一个超时计时器 */ + setTimeout(() => { + reject(new Error('API请求超时')) + }, time * 1000) + }) + } +} + +export default { + type: 'websocket', + path: '/onebot/v11/ws', + adapter: OneBot11, +} diff --git a/src/event/message.ts b/src/event/message.ts index 5509f91..9a16ea1 100644 --- a/src/event/message.ts +++ b/src/event/message.ts @@ -22,7 +22,7 @@ export class KarinMessage extends KarinEvent { /** * - 事件类型 */ - event: 'message' | 'meta_event' + event: 'message' | 'message_sent' /** * - 机器人ID 请尽量使用UID */ @@ -63,38 +63,6 @@ export class KarinMessage extends KarinEvent { * 群ID */ group_id: string - /** - * 框架处理后的文本 - */ - msg: string - /** - * 游戏类型 - */ - game: string - /** - * 图片数组 - */ - image: Array - /** - * AT数组 - */ - at: Array - /** - * 是否AT机器人 - */ - atBot: boolean - /** - * 是否AT全体 - */ - atAll: boolean - /** - * 文件元素 - */ - file: object - /** - * 引用消息ID - */ - reply_id?: string }) { super({ event, event_id: message_id, self_id, user_id, group_id, time, contact, sender, sub_event: contact.scene === 'group' ? 'group_message' : 'friend_message' }) this.message_id = message_id diff --git a/src/types/adapter.ts b/src/types/adapter.ts index 9b66af6..e25692c 100644 --- a/src/types/adapter.ts +++ b/src/types/adapter.ts @@ -63,6 +63,11 @@ export interface KarinAdapter { * - 连接时间 */ start_time: number + /** + * - 适配器连接地址 + * - 仅在`http`、`ws`、`grpc`有效 比如 ws://127.0.0.1:7000 + */ + connect?: string } /** @@ -70,6 +75,13 @@ export interface KarinAdapter { */ get self_id(): string + /** + * 专属当前Bot的日志打印方法 + * @param level - 日志等级 + * @param args - 日志内容 + */ + logger(level: 'info' | 'error' | 'trace' | 'debug' | 'mark' | 'warn' | 'fatal', ...args: any[]): void + /** * - 获取头像url */ @@ -212,7 +224,7 @@ export interface KarinAdapter { * - 获取消息数量 默认为1 */ count: number, - ): Promise> + ): Promise | void> /** * - 下载合并转发消息 @@ -222,7 +234,7 @@ export interface KarinAdapter { * - 资源ID */ res_id: string, - ): Promise> + ): Promise | void> /** * - 获取精华消息 @@ -240,7 +252,7 @@ export interface KarinAdapter { * - 每页数量 */ page_size: number, - ): Promise + ): Promise /** * - 设置精华消息 @@ -395,7 +407,7 @@ export interface KarinAdapter { /** * - 群ID */ - group_id: number, + group_id: string, /** * - 如果Bot是群主,是否解散群 * - 此项属于拓展选项,Kritor标准没有,仅在OneBot11中有效 @@ -436,7 +448,7 @@ export interface KarinAdapter { /** * - 登录账户名称 */ - account_name: number + account_name: string }> /** @@ -588,9 +600,34 @@ export interface KarinAdapter { /** * 发送合并转发消息 - * @param {contact} contact - * @param {Array} elements - * @return {Promise} + * @param contact 联系人信息 + * @param elements 消息元素 + * @return {Promise<{message_id?}>} */ sendForwardMessage(contact: contact, elements: Array): Promise<{ message_id?: string }> + + /** + * 对消息进行表情回应 + * @param Contact - 联系人信息 + * @param message_id - 消息ID + * @param face_id - 表情ID + */ + ReactMessageWithEmojiRequest(contact: contact, message_id: string, face_id: number, is_set: boolean): Promise + + /** + * 上传群文件 + * @param group_id - 群号 + * @param file - 本地文件绝对路径 + * @param name - 文件名称 必须提供 + * @param folder - 父目录ID 不提供则上传到根目录 + */ + UploadGroupFile(group_id: string, file: string, name: string, folder?: string): Promise + + /** + * 上传私聊文件 + * @param user_id - 用户ID + * @param file - 本地文件绝对路径 + * @param name - 文件名称 必须提供 + */ + UploadPrivateFile(user_id: string, file: string, name: string): Promise } diff --git a/src/types/element.ts b/src/types/element.ts index 52253dd..3851dcb 100644 --- a/src/types/element.ts +++ b/src/types/element.ts @@ -1,4 +1,4 @@ -export type ElementType = 'text' | 'at' | 'face' | 'bubble_face' | 'reply' | 'image' | 'voice' | 'video' | 'basketball' | 'dice' | 'rps' | 'poke' | 'music' | 'weather' | 'location' | 'share' | 'gift' | 'market_face' | 'forward' | 'contact' | 'json' | 'xml' | 'file' | 'markdown' | 'keyboard' | 'node' | 'rows' | 'record' +export type ElementType = 'text' | 'at' | 'face' | 'bubble_face' | 'reply' | 'image' | 'voice' | 'video' | 'basketball' | 'dice' | 'rps' | 'poke' | 'music' | 'weather' | 'location' | 'share' | 'gift' | 'market_face' | 'forward' | 'contact' | 'json' | 'xml' | 'file' | 'markdown' | 'keyboard' | 'node' | 'rows' | 'record' | 'long_msg' export interface Element { /** @@ -586,6 +586,17 @@ export interface RowElement extends Element { rows: Array } +/** + * - 长消息元素 + */ +export interface LongMsgElement extends Element { + type: 'long_msg' + /** + * - 消息ID + */ + id: string +} + /** * - 构建自定义转发节点 此元素仅可通过专用接口发送 不支持混合发送 */ @@ -596,4 +607,4 @@ export interface KarinNodeElement extends Element { content: KarinElement | Array } -export type KarinElement = TextElement | AtElement | FaceElement | BubbleFaceElement | ReplyElement | ImageElement | VoiceElement | VideoElement | BasketballElement | DiceElement | RpsElement | PokeElement | MusicElement | WeatherElement | LocationElement | ShareElement | GiftElement | MarketFaceElement | ForwardElement | ContactElement | JsonElement | XmlElement | FileElement | MarkdownElement | ButtonElement | RowElement | RecordElement +export type KarinElement = TextElement | AtElement | FaceElement | BubbleFaceElement | ReplyElement | ImageElement | VoiceElement | VideoElement | BasketballElement | DiceElement | RpsElement | PokeElement | MusicElement | WeatherElement | LocationElement | ShareElement | GiftElement | MarketFaceElement | ForwardElement | ContactElement | JsonElement | XmlElement | FileElement | MarkdownElement | ButtonElement | RowElement | RecordElement | LongMsgElement diff --git a/src/types/onebots11.ts b/src/types/onebots11.ts new file mode 100644 index 0000000..cf2fef2 --- /dev/null +++ b/src/types/onebots11.ts @@ -0,0 +1,1428 @@ +/** + * - OneBot 11 标准事件 + */ +export type PostType = 'message' | 'notice' | 'request' | 'meta_event' | 'message_sent' +/** + * - 消息事件类型 + */ +export type MessageType = 'private' | 'group' +/** + * - 消息子类型 + */ +export type MessageSubType = 'friend' | 'group' | 'other' | 'normal' | 'anonymous' | 'notice' +/** + * - 通知事件类型 + */ +export type NoticeType = 'group_upload' | 'group_admin' | 'group_decrease' | 'group_increase' | 'group_ban' | 'friend_add' | 'group_recall' | 'friend_recall' | 'notify' | 'group_msg_emoji_like' +/** + * - 请求类型 + */ +export type RequestType = 'friend' | 'group' + +/** + * - 消息事件映射 + */ +export interface MessageToSubType { + private: 'friend' | 'group' | 'other' + group: 'normal' | 'anonymous' | 'notice' +} + +/** + * - 消息子类型映射 + */ +export type MessageTypeToSubEvent = E extends keyof MessageToSubType ? MessageToSubType[E] : never + +/** + * - 事件基类 + */ +export interface OneBot11 { + /** + * - 事件发生的时间戳 + */ + time: number + /** + * - 事件类型 + */ + post_type: PostType + /** + * - 收到事件的机器人 QQ 号 + */ + self_id: string +} + +/** + * - 消息事件基类 + */ +export interface OneBot11Message extends OneBot11 { + /** + * - 事件类型 + */ + post_type: 'message' | 'message_sent' + /** + * - 消息类型 + */ + message_type: MessageType + /** + * - 消息子类型 + */ + sub_type: MessageTypeToSubEvent + /** + * - 消息 ID + */ + message_id: string + /** + * - 发送者 QQ 号 + */ + user_id: string + /** + * - 消息内容 + */ + message: OneBot11Segment[] + /** + * - 原始消息内容 + */ + raw_message: string + /** + * - 字体 + */ + font: number + /** + * - 发送人信息 + */ + sender: { + /** + * - 发送者 QQ 号 + */ + user_id: string + /** + * - 昵称 不存在则为空字符串 + */ + nickname: string + /** + * - 性别 + */ + sex?: 'male' | 'female' | 'unknown' + /** + * - 年龄 + */ + age?: number + } +} + +/** + * - 私聊消息事件 + */ +export interface OneBot11PrivateMessage extends OneBot11Message { + /** + * - 消息类型 + */ + message_type: 'private' + /** + * - 消息子类型 + */ + sub_type: 'friend' +} + +/** + * - 群消息事件 + */ +export interface OneBot11GroupMessage extends OneBot11Message { + /** + * - 消息类型 + */ + message_type: 'group' + /** + * - 消息子类型 + */ + sub_type: 'normal' | 'anonymous' | 'notice' + /** + * - 群号 + */ + group_id: string + /** + * - 匿名信息 + */ + anonymous?: { + /** + * - 匿名用户 ID + */ + id: string + /** + * - 匿名用户名称 + */ + name: string + /** + * - 匿名用户 flag,在调用禁言 API 时需要传入 + */ + flag: string + } + sender: { + /** + * - 发送者 QQ 号 + */ + user_id: string + /** + * - 昵称 不存在则为空字符串 + */ + nickname: string + /** + * - 性别 + */ + sex?: 'male' | 'female' | 'unknown' + /** + * - 年龄 + */ + age?: number + /** + * - 群名片/备注 + */ + card?: string + /** + * - 地区 + */ + area?: string + /** + * - 成员等级 + */ + level?: string + /** + * - 角色 不存在则为空字符串 + */ + role: 'owner' | 'admin' | 'member' | '' + /** + * - 专属头衔 + */ + title?: string + } +} + +/** + * - 通知事件基类 + */ +export interface OneBot11Notice extends OneBot11 { + /** + * - 事件类型 + */ + post_type: 'notice' + /** + * - 通知类型 + */ + notice_type: NoticeType +} + +/** + * - 群文件上传事件 + */ +export interface OneBot11GroupUpload extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'group_upload' + /** + * - 群号 + */ + group_id: string + /** + * - 发送者 QQ 号 + */ + user_id: string + /** + * - 文件信息 + */ + file: { + /** + * - 文件 ID + */ + id: string + /** + * - 文件名 + */ + name: string + /** + * - 文件大小(字节数) + */ + size: number + /** + * - busid(目前不清楚有什么作用) + */ + busid: number + } +} + +/** + * - 群管理员变动事件 + */ +export interface OneBot11GroupAdmin extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'group_admin' + /** + * - 事件子类型,分别表示设置和取消管理员 + */ + sub_type: 'set' | 'unset' + /** + * - 群号 + */ + group_id: string + /** + * - 管理员 QQ 号 + */ + user_id: string +} + +/** + * - 群减少事件 + */ +export interface OneBot11GroupDecrease extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'group_decrease' + /** + * - 事件子类型,分别表示主动退群、成员被踢、登录号被踢 + */ + sub_type: 'leave' | 'kick' | 'kick_me' + /** + * - 群号 + */ + group_id: string + /** + * - 操作者 QQ 号(如果是主动退群,则和 user_id 相同) + */ + operator_id: string + /** + * - 离开者 QQ 号 + */ + user_id: string +} + +/** + * - 群增加事件 + */ +export interface OneBot11GroupIncrease extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'group_increase' + /** + * - 事件子类型,分别表示管理员已同意入群、管理员邀请入群 + */ + sub_type: 'approve' | 'invite' + /** + * - 群号 + */ + group_id: string + /** + * - 操作者 QQ 号 + */ + operator_id: string + /** + * - 加入者 QQ 号 + */ + user_id: string +} + +/** + * - 群禁言事件 + */ +export interface OneBot11GroupBan extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'group_ban' + /** + * - 事件子类型,分别表示禁言、解除禁言 + */ + sub_type: 'ban' | 'lift_ban' + /** + * - 群号 + */ + group_id: string + /** + * - 操作者 QQ 号 + */ + operator_id: string + /** + * - 被禁言 QQ 号 + */ + user_id: string + /** + * - 禁言时长,单位秒 + */ + duration: number +} + +/** + * - 新添加好友事件 + */ +export interface OneBot11FriendAdd extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'friend_add' + /** + * - 新添加好友 QQ 号 + */ + user_id: string +} + +/** + * - 群撤回事件 + */ +export interface OneBot11GroupRecall extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'group_recall' + /** + * - 群号 + */ + group_id: string + /** + * - 消息发送者 QQ 号 + */ + user_id: string + /** + * - 操作者 QQ 号 + */ + operator_id: string + /** + * - 被撤回的消息 ID + */ + message_id: string +} + +/** + * - 好友消息撤回事件 + */ +export interface OneBot11FriendRecall extends OneBot11Notice { + /** + * - 通知类型 + */ + notice_type: 'friend_recall' + /** + * - 好友 QQ 号 + */ + user_id: string + /** + * - 被撤回的消息 ID + */ + message_id: string +} + +/** + * - 戳一戳事件 + */ +export interface OneBot11Poke extends OneBot11Notice { + /** + * - 消息类型 + */ + notice_type: 'notify' + /** + * - 提示类型 + */ + sub_type: 'poke' + /** + * - 群号 + */ + group_id: string + /** + * - 发送者 QQ 号 + */ + user_id: string + /** + * - 被戳者 QQ 号 + */ + target_id: string +} + +/** + * - 运气王事件 + */ +export interface OneBot11LuckyKing extends OneBot11Notice { + /** + * - 消息类型 + */ + notice_type: 'notify' + /** + * - 提示类型 + */ + sub_type: 'lucky_king' + /** + * - 群号 + */ + group_id: string + /** + * - 红包发送者 QQ 号 + */ + user_id: string + /** + * - 运气王 QQ 号 + */ + target_id: string +} + +/** + * - 荣誉变更事件 + */ +export interface OneBot11Honor extends OneBot11Notice { + /** + * - 消息类型 + */ + notice_type: 'notify' + /** + * - 提示类型 + */ + sub_type: 'honor' + /** + * - 群号 + */ + group_id: string + /** + * - 荣誉类型,分别表示龙王、群聊之火、快乐源泉 + */ + honor_type: 'talkative' | 'performer' | 'emotion' + /** + * - 成员 QQ 号 + */ + user_id: string +} + +/** + * - 群表情回应事件 + */ +export interface OneBot11GroupMessageReaction extends OneBot11Notice { + /** + * - 消息类型 + */ + notice_type: 'group_msg_emoji_like' + /** + * - 群号 + */ + group_id: string + /** + * - 发送者 QQ 号 + */ + user_id: string + /** + * - 消息 ID + */ + message_id: string + /** + * - 表情信息 此处目前只有llob有 + */ + likes: Array<{ + count: number + /** + * - 表情ID参考: https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType + */ + emoji_id: number + }> +} + +/** + * - 请求事件基类 + */ +export interface OneBot11Request extends OneBot11 { + /** + * - 事件发生的时间戳 + */ + time: number + /** + * - 事件类型 + */ + post_type: 'request' + /** + * - 收到事件的机器人 QQ 号 + */ + self_id: string + /** + * - 请求类型 + */ + request_type: 'friend' | 'group' + /** + * - 请求 flag,在调用处理请求的 API 时需要传入 + */ + flag: string + /** + * - 发送请求的 QQ 号 + */ + user_id: string + /** + * - 验证信息 + */ + comment: string +} + +/** + * - 好友请求事件 + */ +export interface OneBot11FriendRequest extends OneBot11Request { + /** + * - 请求类型 + */ + request_type: 'friend' +} + +/** + * - 群请求事件 + */ +export interface OneBot11GroupRequest extends OneBot11Request { + /** + * - 请求类型 + */ + request_type: 'group' + /** + * - 请求子类型,分别表示加群请求、邀请登录号入群 + */ + sub_type: 'add' | 'invite' + /** + * - 群号 + */ + group_id: string +} + +/** + * - 元事件基类 + */ +export interface OneBot11MetaEvent extends OneBot11 { + /** + * - 事件类型 + */ + post_type: 'meta_event' + /** + * - 元事件类型 + */ + meta_event_type: 'lifecycle' | 'heartbeat' +} + +/** + * - 生命周期元事件 + */ +export interface OneBot11Lifecycle extends OneBot11MetaEvent { + /** + * - 元事件类型 + */ + meta_event_type: 'lifecycle' + /** + * - 事件子类型,分别表示 OneBot 启用、停用、WebSocket 连接成功 + */ + sub_type: 'enable' | 'disable' | 'connect' +} + +/** + * - 心跳元事件 + */ +export interface OneBot11Heartbeat extends OneBot11MetaEvent { + /** + * - 元事件类型 + */ + meta_event_type: 'heartbeat' + /** + * - 状态信息 + */ + status: { + /** + * - 到下次心跳的间隔,单位毫秒 + */ + interval: number + } +} + +// 所有事件 +export type OneBot11Event = OneBot11GroupMessage | OneBot11PrivateMessage | OneBot11GroupUpload | OneBot11GroupAdmin | OneBot11GroupDecrease | OneBot11GroupIncrease | OneBot11GroupBan | OneBot11FriendAdd | OneBot11GroupRecall | OneBot11FriendRecall | OneBot11Poke | OneBot11LuckyKing | OneBot11Honor | OneBot11FriendRequest | OneBot11GroupRequest | OneBot11Lifecycle | OneBot11Heartbeat | OneBot11GroupMessageReaction +/** + * - 传入 post_type 返回对应的事件类型 + */ +export type ByPostType = Extract + +/** + * - OneBot11公开Api + */ +export type OneBot11Api = 'send_private_msg' | 'send_group_msg' | 'send_msg' | 'delete_msg' | 'get_msg' | 'get_forward_msg' | 'send_like' | 'set_group_kick' | 'set_group_ban' | 'set_group_anonymous_ban' | 'set_group_whole_ban' | 'set_group_admin' | 'set_group_anonymous' | 'set_group_card' | 'set_group_name' | 'set_group_leave' | 'set_group_special_title' | 'set_friend_add_request' | 'set_group_add_request' | 'get_login_info' | 'get_stranger_info' | 'get_friend_list' | 'get_group_info' | 'get_group_list' | 'get_group_member_info' | 'get_group_member_list' | 'get_group_honor_info' | 'get_cookies' | 'get_csrf_token' | 'get_credentials' | 'get_record' | 'get_image' | 'can_send_image' | 'can_send_record' | 'get_status' | 'get_version_info' | 'set_restart' | 'clean_cache' | 'get_version' | 'send_forward_msg' | 'get_friend_msg_history' | 'get_group_msg_history' + +/** + * - OneBot11公开Api参数 params + */ +export type OneBot11ApiParams = { + /** + * - 发送私聊消息 + */ + send_private_msg: { + /** + * - 对方 QQ 号 + */ + user_id: number + /** + * - 要发送的内容 + */ + message: string + /** + * - 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 + */ + auto_escape?: boolean + } + /** + * - 发送群消息 + */ + send_group_msg: { + /** + * - 群号 + */ + group_id: number + /** + * - 要发送的内容 + */ + message: string + /** + * - 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 + */ + auto_escape?: boolean + } + /** + * - 发送消息 + */ + send_msg: { + /** + * - 消息类型,可选值为 "private" 或 "group" + */ + message_type?: 'private' | 'group' + /** + * - 对方 QQ 号,当消息类型为 "private" 时有效 + */ + user_id?: number + /** + * - 群号,当消息类型为 "group" 时有效 + */ + group_id?: number + /** + * - 要发送的内容 + */ + message: string + /** + * - 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 + */ + auto_escape?: boolean + } + /** + * - 撤回消息 + */ + delete_msg: { + /** + * - 消息 ID + */ + message_id: number + } + /** + * - 获取消息 + */ + get_msg: { + /** + * - 消息 ID + */ + message_id: number + } + /** + * - 获取转发消息 + */ + get_forward_msg: { + /** + * - 转发消息 ID + */ + id: string + } + /** + * - 发送好友赞 + */ + send_like: { + /** + * - 对方 QQ 号 + */ + user_id: number + /** + * - 赞的次数,每个赞为一个好友赞,每个用户每天最多赞 10 次 + */ + times?: number + } + /** + * - 群组踢人 + */ + set_group_kick: { + /** + * - 群号 + */ + group_id: number + /** + * - 要踢的 QQ 号 + */ + user_id: number + /** + * - 拒绝此人的加群请求 + */ + reject_add_request?: boolean + } + /** + * - 群组禁言 + */ + set_group_ban: { + /** + * - 群号 + */ + group_id: number + /** + * - 要禁言的 QQ 号 + */ + user_id: number + /** + * - 禁言时长,单位秒,0 表示取消禁言 + */ + duration?: number + } + /** + * - 群组匿名用户禁言 + */ + set_group_anonymous_ban: { + /** + * - 群号 + */ + group_id: number + /** + * - 匿名用户对象 + */ + anonymous?: object + /** + * - 匿名用户标识,使用匿名用户对象时此参数无效 + */ + anonymous_flag?: string + /** + * - 禁言时长,单位秒,无法取消匿名用户禁言 + */ + duration?: number + } + /** + * - 群组全员禁言 + */ + set_group_whole_ban: { + /** + * - 群号 + */ + group_id: number + /** + * - 是否禁言,true 为开启,false 为关闭 + */ + enable?: boolean + } + /** + * - 设置群管理员 + */ + set_group_admin: { + /** + * - 群号 + */ + group_id: number + /** + * - 要设置管理员的 QQ 号 + */ + user_id: number + /** + * - 是否设置为管理员,true 为设置,false 为取消 + */ + enable?: boolean + } + /** + * - 设置群匿名聊天 + */ + set_group_anonymous: { + /** + * - 群号 + */ + group_id: number + /** + * - 是否允许匿名聊天,true 为开启,false 为关闭 + */ + enable?: boolean + } + /** + * - 设置群名片(群备注) + */ + set_group_card: { + /** + * - 群号 + */ + group_id: number + /** + * - 要设置的 QQ 号 + */ + user_id: number + /** + * - 名片内容,不填或空字符串表示删除群名片 + */ + card?: string + } + /** + * - 设置群名 + */ + set_group_name: { + /** + * - 群号 + */ + group_id: number + /** + * - 新群名 + */ + group_name: string + } + /** + * - 退出群组 + */ + set_group_leave: { + /** + * - 群号 + */ + group_id: number + /** + * - 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 + */ + is_dismiss?: boolean + } + /** + * - 设置群成员专属头衔 + */ + set_group_special_title: { + /** + * - 群号 + */ + group_id: number + /** + * - 要设置的 QQ 号 + */ + user_id: number + /** + * - 专属头衔,不填或空字符串表示删除专属头衔 + */ + special_title?: string + /** + * - 专属头衔有效期,单位秒,-1 表示永久,不过此项似乎没有效果 + */ + duration?: number + } + /** + * - 处理好友添加请求 + */ + set_friend_add_request: { + /** + * - 请求 flag,在调用处理请求的事件中返回 + */ + flag: string + /** + * - 是否同意请求 + */ + approve?: boolean + /** + * - 添加后的好友备注 + */ + remark?: string + } + /** + * - 处理群添加请求/邀请 + */ + set_group_add_request: { + /** + * - 请求 flag,在调用处理请求的事件中返回 + */ + flag: string + /** + * - 请求子类型,add 或 invite,请求子类型为 invite 时为邀请 + */ + sub_type?: 'add' | 'invite' + /** + * - 是否同意请求/邀请 + */ + approve?: boolean + /** + * - 拒绝理由,仅在拒绝时有效 + */ + reason?: string + } + /** + * - 获取登录号信息 + */ + get_login_info: {} + /** + * - 获取陌生人信息 + */ + get_stranger_info: { + /** + * - QQ 号 + */ + user_id: number + /** + * - 是否不使用缓存,true 表示不使用缓存,false 或留空表示使用缓存 + */ + no_cache?: boolean + } + /** + * - 获取好友列表 + */ + get_friend_list: {} + /** + * - 获取群信息 + */ + get_group_info: { + /** + * - 群号 + */ + group_id: number + /** + * - 是否不使用缓存,true 表示不使用缓存,false 或留空表示使用缓存 + */ + no_cache?: boolean + } + /** + * - 获取群列表 + */ + get_group_list: {} + /** + * - 获取群成员信息 + */ + get_group_member_info: { + /** + * - 群号 + */ + group_id: number + /** + * - QQ 号 + */ + user_id: number + /** + * - 是否不使用缓存,true 表示不使用缓存,false 或留空表示使用缓存 + */ + no_cache?: boolean + } + /** + * - 获取群成员列表 + */ + get_group_member_list: { + /** + * - 群号 + */ + group_id: number + } + /** + * - 获取群荣誉信息 + */ + get_group_honor_info: { + /** + * - 群号 + */ + group_id: number + /** + * - 荣誉类型,可选值为 "talkative"(龙王)、"performer"(群聊之火)、"legend"(群聊炽焰)、"strong_newbie"(新人王)、"emotion"(快乐源泉)、"all"(所有类型) + */ + type: 'talkative' | 'performer' | 'legend' | 'strong_newbie' | 'emotion' | 'all' + } + /** + * - 获取 Cookies + */ + get_cookies: { + /** + * - 指定域名,不填或空字符串表示获取当前域名下的 Cookies + */ + domain?: string + } + /** + * - 获取 CSRF Token + */ + get_csrf_token: {} + /** + * - 获取 QQ 相关接口凭证 + */ + get_credentials: { + /** + * - 指定域名,不填或空字符串表示获取当前域名下的凭证 + */ + domain?: string + } + /** + * - 获取语音 + */ + get_record: { + /** + * - 文件路径 + */ + file: string + /** + * - 输出格式,可选值为 "amr"、"silk"、"mp3"、"wav",默认为 "amr" + */ + out_format: string + } + /** + * - 获取图片 + */ + get_image: { + /** + * - 文件路径 + */ + file: string + } + /** + * - 是否可以发送图片 + */ + can_send_image: {} + /** + * - 是否可以发送语音 + */ + can_send_record: {} + /** + * - 获取插件运行状态 + */ + get_status: {} + /** + * - 获取插件版本信息 + */ + get_version_info: {} + /** + * - 获取插件版本信息 + */ + get_version: {} + /** + * - 重启插件 + */ + set_restart: { + /** + * - 延迟重启时间,单位毫秒,不填或留空表示立即重启 + */ + delay?: number + } + /** + * - 清理数据缓存 + */ + clean_cache: {} + + /** + * - 发送合并转发消息 + */ + send_forward_msg: { + /** + * - 对方 QQ 号,当消息类型为 "private" 时有效 + */ + user_id?: number + /** + * - 群号,当消息类型为 "group" 时有效 + */ + group_id?: number + /** + * - 要发送的内容 + */ + messages: Array<{ + /** + * - 消息段 + */ + message: OneBot11Segment[] + }> + } + + /** + * - 获取好友历史消息记录 + */ + get_friend_msg_history: { + /** + * - 对方 QQ 号 + */ + user_id: number + /** + * - 起始消息序号 + */ + message_seq: number + /** + * - 消息数量 + */ + message_count: number + } + + /** + * - 获取群组历史消息记录 + */ + get_group_msg_history: { + /** + * - 群号 + */ + group_id: number + /** + * - 起始消息序号 + */ + message_seq: number + /** + * - 消息数量 + */ + message_count: number + } +} + +/** + * - OneBot11公开Api与参数映射 + */ +export type OneBot11ApiParamsType = { + [K in OneBot11Api]: OneBot11ApiParams[K] +} + +/** + * - OneBot11消息类型 + */ +export type OneBot11SegmentType = 'text' | 'face' | 'image' | 'record' | 'video' | 'at' | 'rps' | 'dice' | 'shake' | 'poke' | 'anonymous' | 'share' | 'contact' | 'location' | 'music' | 'music_custom' | 'reply' | 'forward' | 'node' | 'node_custom' | 'xml' | 'json' + +export interface Segment { + type: OneBot11SegmentType +} + +/** + * - 纯文本 + */ +export interface TextSegment extends Segment { + type: 'text' + data: { + text: string + } +} + +/** + * - QQ表情 + */ +export interface FaceSegment extends Segment { + type: 'face' + data: { + id: string + } +} + +/* +/** + * - 图片消息段 + */ +export interface ImageSegment extends Segment { + type: 'image' + data: { + file: string + type?: 'flash' + url?: string + cache?: 0 | 1 + proxy?: 0 | 1 + timeout?: number + } +} + +/** + * - 语音消息段 + */ +export interface RecordSegment extends Segment { + type: 'record' + data: { + file: string + magic?: 0 | 1 + url?: string + cache?: 0 | 1 + proxy?: 0 | 1 + timeout?: number + } +} + +/** + * - 短视频消息段 + */ +export interface VideoSegment extends Segment { + type: 'video' + data: { + file: string + url?: string + cache?: 0 | 1 + proxy?: 0 | 1 + timeout?: number + } +} + +/** + * - @某人消息段 + */ +export interface AtSegment extends Segment { + type: 'at' + data: { + qq: string | 'all' + } +} + +/** + * - 猜拳魔法表情消息段 + */ +export interface RpsSegment extends Segment { + type: 'rps' + data: {} +} + +/** + * - 掷骰子魔法表情消息段 + */ +export interface DiceSegment extends Segment { + type: 'dice' + data: {} +} + +/** + * - 窗口抖动(戳一戳)消息段 + */ +export interface ShakeSegment extends Segment { + type: 'shake' + data: {} +} + +/** + * - 戳一戳消息段 + */ +export interface PokeSegment extends Segment { + type: 'poke' + data: { + type: string + id: string + name?: string + } +} + +/** + * - 匿名发消息消息段 + */ +export interface AnonymousSegment extends Segment { + type: 'anonymous' + data: { + ignore?: 0 | 1 + } +} + +/** + * - 链接分享消息段 + */ +export interface ShareSegment extends Segment { + type: 'share' + data: { + url: string + title: string + content?: string + image?: string + } +} + +/** + * - 推荐好友/群消息段 + */ +export interface ContactSegment extends Segment { + type: 'contact' + data: { + type: 'qq' | 'group' + id: string + } +} + +/** + * - 位置消息段 + */ +export interface LocationSegment extends Segment { + type: 'location' + data: { + lat: string + lon: string + title?: string + content?: string + } +} + +/** + * - 音乐分享消息段 + */ +export interface MusicSegment extends Segment { + type: 'music' + data: { + type: 'qq' | '163' | 'xm' + id: string + } +} + +/** + * - 音乐自定义分享消息段 + */ +export interface CustomMusicSegment extends Segment { + type: 'music' + data: { + type: 'custom' + url: string + audio: string + title: string + content?: string + image?: string + } +} + +/** + * - 回复消息段 + */ +export interface ReplySegment extends Segment { + type: 'reply' + data: { + id: string + } +} + +/** + * - 合并转发消息段 + */ +export interface ForwardSegment extends Segment { + type: 'forward' + data: { + id: string + } +} + +/** + * - 合并转发节点消息段 + */ +export interface NodeSegment extends Segment { + type: 'node' + data: { + id: string + } +} + +/** + * - 合并转发自定义节点消息段 + */ +export interface CustomNodeSegment extends Segment { + type: 'node' + data: { + user_id: string + nickname: string + content: string | Segment[] + } +} + +/** + * - XML消息段 + */ +export interface XmlSegment extends Segment { + type: 'xml' + data: { + data: string + } +} + +/** + * - JSON消息段 + */ +export interface JsonSegment extends Segment { + type: 'json' + data: { + data: string + } +} + +/** + * - OneBot11消息段 + */ +export type OneBot11Segment = TextSegment | FaceSegment | ImageSegment | RecordSegment | VideoSegment | AtSegment | RpsSegment | DiceSegment | ShakeSegment | PokeSegment | AnonymousSegment | ShareSegment | ContactSegment | LocationSegment | MusicSegment | CustomMusicSegment | ReplySegment | ForwardSegment | NodeSegment | CustomNodeSegment | XmlSegment | JsonSegment diff --git a/src/types/types.ts b/src/types/types.ts index 144dc79..5bb5de6 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -6,7 +6,7 @@ import { KarinRequest } from '../event/request' /** * - 事件类型 */ -export type Event = 'message' | 'notice' | 'request' | 'meta_event' +export type Event = 'message' | 'notice' | 'request' | 'meta_event' | 'message_sent' /** * - 事件来源 @@ -23,7 +23,7 @@ export type Sub_event = 'group_message' | 'friend_message' | 'guild_message' | ' */ export type EventToSubEvent = { message: 'group_message' | 'friend_message' | 'guild_message' | 'nearby' | 'stranger' | 'stranger_from_group' - notice: 'friend_poke' | 'friend_recall' | 'friend_file_uploaded' | 'group_poke' | 'group_card_changed' | 'group_member_unique_title_changed' | 'group_essence_changed' | 'group_recall' | 'group_member_increase' | 'group_member_decrease' | 'group_admin_changed' | 'group_member_ban' | 'group_sign' | 'group_whole_ban' | 'group_file_uploaded' + notice: 'friend_poke' | 'friend_recall' | 'friend_file_uploaded' | 'group_poke' | 'group_card_changed' | 'group_member_unique_title_changed' | 'group_essence_changed' | 'group_recall' | 'group_member_increase' | 'group_member_decrease' | 'group_admin_changed' | 'group_member_ban' | 'group_sign' | 'group_whole_ban' | 'group_file_uploaded' | 'group_message_reaction' request: 'friend_apply' | 'group_apply' | 'invited_group' meta_event: 'group_message' | 'friend_message' | 'guild_message' } @@ -38,6 +38,11 @@ export type SubEventForEvent = E extends keyof EventToSubEvent */ export type Permission = 'all' | 'master' | 'admin' | 'group.owner' | 'group.admin' +/** + * - 群角色 + */ +export type Role = 'owner' | 'admin' | 'member' | 'unknown' | '' + /** * - 事件联系人信息 */ @@ -75,7 +80,7 @@ export interface Sender { /** * - 发送者在群的角色身份 */ - role: 'owner' | 'admin' | 'member' | 'unknown' | '' + role: Role } /** @@ -374,7 +379,7 @@ export interface NoticeTytpe { /** * - 加入方式 APPROVE:管理员批准 INVITE:管理员邀请 */ - type: 'invite' | 'apply' + type: 'invite' | 'approve' } /** * - 群成员减少 @@ -518,7 +523,7 @@ export interface NoticeTytpe { */ message_id: string /** - * - 表情ID + * - 表情ID 参考: https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType */ face_id: number /** diff --git a/src/utils/common.ts b/src/utils/common.ts index dc1fe0b..788dd0b 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -380,6 +380,9 @@ export default new (class Common { case 'button': logs.push(`[button:${JSON.stringify(val.data)}]`) break + case 'long_msg': + logs.push(`[long_msg:${val.id}]`) + break default: logs.push(`[未知:${JSON.stringify(val)}]`) }