diff --git a/manifest.json b/manifest.json index 1d1aaa5..6668488 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发", - "version": "3.31.6", + "version": "3.31.7", "icon": "./icon.webp", "authors": [ { diff --git a/package.json b/package.json index aa734f0..81a0bbb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "cors": "^2.8.5", "cosmokit": "^1.6.2", "express": "^4.19.2", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^4.5.0", "file-type": "^19.4.1", "fluent-ffmpeg": "^2.1.3", "minato": "^3.5.1", @@ -38,7 +38,7 @@ "electron": "^31.4.0", "electron-vite": "^2.3.0", "typescript": "^5.5.4", - "vite": "^5.4.2", + "vite": "^5.4.3", "vite-plugin-cp": "^4.0.8" }, "packageManager": "yarn@4.4.1" diff --git a/src/common/config.ts b/src/common/config.ts index f9586ef..1922aad 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,25 +1,8 @@ import fs from 'node:fs' -import { Config, OB11Config } from './types' import path from 'node:path' +import { Config, OB11Config } from './types' import { selfInfo, DATA_DIR } from './globalVars' - -// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 -function mergeNewProperties(newObj: any, oldObj: any) { - Object.keys(newObj).forEach((key) => { - // 如果老对象不存在当前属性,则直接复制 - if (!oldObj.hasOwnProperty(key)) { - oldObj[key] = newObj[key] - } else { - // 如果老对象和新对象的当前属性都是对象,则递归合并 - if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') { - mergeNewProperties(newObj[key], oldObj[key]) - } else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') { - // 属性冲突,有一方不是对象,直接覆盖 - oldObj[key] = newObj[key] - } - } - }) -} +import { mergeNewProperties } from './utils/misc' export class ConfigUtil { private readonly configPath: string @@ -50,7 +33,8 @@ export class ConfigUtil { enableWsReverse: false, messagePostFormat: 'array', enableHttpHeart: false, - enableQOAutoQuote: false + enableQOAutoQuote: false, + listenLocalhost: false } const defaultConfig: Config = { enableLLOB: true, diff --git a/src/common/types.ts b/src/common/types.ts index b00a879..7379733 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -11,6 +11,7 @@ export interface OB11Config { messagePostFormat?: 'array' | 'string' enableHttpHeart?: boolean enableQOAutoQuote: boolean // 快速操作回复自动引用原消息 + listenLocalhost: boolean } export interface CheckVersion { diff --git a/src/common/utils/misc.ts b/src/common/utils/misc.ts index 80c8a50..0b1e496 100644 --- a/src/common/utils/misc.ts +++ b/src/common/utils/misc.ts @@ -13,4 +13,22 @@ export function calcQQLevel(level: QQLevel) { export function getBuildVersion(): number { const version: string = globalThis.LiteLoader.versions.qqnt return +version.split('-')[1] +} + +/** 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 */ +export function mergeNewProperties(newObj: any, oldObj: any) { + Object.keys(newObj).forEach((key) => { + // 如果老对象不存在当前属性,则直接复制 + if (!oldObj.hasOwnProperty(key)) { + oldObj[key] = newObj[key] + } else { + // 如果老对象和新对象的当前属性都是对象,则递归合并 + if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') { + mergeNewProperties(newObj[key], oldObj[key]) + } else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') { + // 属性冲突,有一方不是对象,直接覆盖 + oldObj[key] = newObj[key] + } + } + }) } \ No newline at end of file diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts index 82a7716..ef0ace3 100644 --- a/src/ntqqapi/api/msg.ts +++ b/src/ntqqapi/api/msg.ts @@ -272,4 +272,40 @@ export class NTQQMsgApi extends Service { return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null]) } } + + async queryFirstMsgBySeq(peer: Peer, msgSeq: string) { + return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{ + msgId: '0', + msgTime: '0', + msgSeq, + params: { + chatInfo: peer, + filterMsgType: [], + filterSendersUid: [], + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: true, + isIncludeCurrent: true, + pageLimit: 1, + } + }, null]) + } + + async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[]) { + return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{ + msgId: '0', + msgTime: '0', + msgSeq, + params: { + chatInfo: peer, + filterMsgType: [], + filterSendersUid, + filterMsgToTime: filterMsgTime, + filterMsgFromTime: filterMsgTime, + isReverseOrder: true, + isIncludeCurrent: true, + pageLimit: 1, + } + }, null]) + } } diff --git a/src/ntqqapi/api/user.ts b/src/ntqqapi/api/user.ts index be61f29..3e4360b 100644 --- a/src/ntqqapi/api/user.ts +++ b/src/ntqqapi/api/user.ts @@ -334,4 +334,14 @@ export class NTQQUserApi extends Service { } return selfInfo.nick } + + async setSelfStatus(status: number, extStatus: number, batteryStatus: number) { + return await invoke('nodeIKernelMsgService/setStatus', [{ + statusReq: { + status, + extStatus, + batteryStatus, + } + }, null]) + } } diff --git a/src/ntqqapi/api/webapi.ts b/src/ntqqapi/api/webapi.ts index 3f8b930..aa4ad4b 100644 --- a/src/ntqqapi/api/webapi.ts +++ b/src/ntqqapi/api/webapi.ts @@ -1,5 +1,6 @@ import { RequestUtil } from '@/common/utils/request' import { Service, Context } from 'cordis' +import { Dict } from 'cosmokit' declare module 'cordis' { interface Context { @@ -49,56 +50,6 @@ interface WebApiGroupMemberRet { extmode: number } -export interface WebApiGroupNoticeFeed { - u: number//发送者 - fid: string//fid - pubt: number//时间 - msg: { - text: string - text_face: string - title: string, - pics?: { - id: string, - w: string, - h: string - }[] - } - type: number - fn: number - cn: number - vn: number - settings: { - is_show_edit_card: number - remind_ts: number - tip_window_type: number - confirm_required: number - } - read_num: number - is_read: number - is_all_confirm: number -} - -export interface WebApiGroupNoticeRet { - ec: number - em: string - ltsm: number - srv_code: number - read_only: number - role: number - feeds: WebApiGroupNoticeFeed[] - group: { - group_id: number - class_ext: number - } - sta: number, - gln: number - tst: number, - ui: any - server_time: number - svrt: number - ad: number -} - interface GroupEssenceMsg { group_code: string msg_seq: number @@ -124,6 +75,30 @@ export interface GroupEssenceMsgRet { } } +interface SetGroupNoticeParams { + groupCode: string + content: string + pinned: number + type: number + isShowEditCard: number + tipWindowType: number + confirmRequired: number + picId: string + imgWidth?: number + imgHeight?: number +} + +interface SetGroupNoticeRet { + ec: number + em: string + id: number + ltsm: number + new_fid: string + read_only: number + role: number + srv_code: number +} + export class NTQQWebApi extends Service { static inject = ['ntUserApi'] @@ -134,7 +109,7 @@ export class NTQQWebApi extends Service { async getGroupMembers(GroupCode: string, cached: boolean = true): Promise { const memberData: Array = new Array() const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com') - const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ') + const cookieStr = this.cookieToString(cookieObject) const retList: Promise[] = [] const params = new URLSearchParams({ st: '0', @@ -173,49 +148,47 @@ export class NTQQWebApi extends Service { } genBkn(sKey: string) { - sKey = sKey || ''; - let hash = 5381; - + sKey = sKey || '' + let hash = 5381 for (let i = 0; i < sKey.length; i++) { - const code = sKey.charCodeAt(i); - hash = hash + (hash << 5) + code; + const code = sKey.charCodeAt(i) + hash = hash + (hash << 5) + code } - - return (hash & 0x7FFFFFFF).toString(); + return (hash & 0x7FFFFFFF).toString() } //实现未缓存 考虑2h缓存 async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => { - let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString(); - let res = ''; - let resJson; + let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString() + let res = '' + let resJson try { - res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr }); - const match = res.match(/window\.__INITIAL_STATE__=(.*?);/); + res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr }) + const match = res.match(/window\.__INITIAL_STATE__=(.*?);/) if (match) { - resJson = JSON.parse(match[1].trim()); + resJson = JSON.parse(match[1].trim()) } if (Internal_type === 1) { - return resJson?.talkativeList; + return resJson?.talkativeList } else { - return resJson?.actorList; + return resJson?.actorList } } catch (e) { - this.ctx.logger.error('获取当前群荣耀失败', url, e); + this.ctx.logger.error('获取当前群荣耀失败', url, e) } - return undefined; + return undefined } - let HonorInfo: any = { group_id: groupCode }; + let HonorInfo: any = { group_id: groupCode } const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com') - const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ') + const cookieStr = this.cookieToString(cookieObject) if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { try { - let RetInternal = await getDataInternal(groupCode, 1); + let RetInternal = await getDataInternal(groupCode, 1) if (!RetInternal) { - throw new Error('获取龙王信息失败'); + throw new Error('获取龙王信息失败') } HonorInfo.current_talkative = { user_id: RetInternal[0]?.uin, @@ -232,17 +205,17 @@ export class NTQQWebApi extends Service { description: talkative_ele?.desc, day_count: 0, nickname: talkative_ele?.name - }); + }) } } catch (e) { - this.ctx.logger.error(e); + this.ctx.logger.error(e) } } if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { try { - let RetInternal = await getDataInternal(groupCode, 2); + let RetInternal = await getDataInternal(groupCode, 2) if (!RetInternal) { - throw new Error('获取群聊之火失败'); + throw new Error('获取群聊之火失败') } HonorInfo.performer_list = []; for (const performer_ele of RetInternal) { @@ -251,54 +224,86 @@ export class NTQQWebApi extends Service { nickname: performer_ele?.name, avatar: performer_ele?.avatar, description: performer_ele?.desc - }); + }) } } catch (e) { - this.ctx.logger.error(e); + this.ctx.logger.error(e) } } if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { try { - let RetInternal = await getDataInternal(groupCode, 3); + let RetInternal = await getDataInternal(groupCode, 3) if (!RetInternal) { - throw new Error('获取群聊炽焰失败'); + throw new Error('获取群聊炽焰失败') } - HonorInfo.legend_list = []; + HonorInfo.legend_list = [] for (const legend_ele of RetInternal) { HonorInfo.legend_list.push({ user_id: legend_ele?.uin, nickname: legend_ele?.name, avatar: legend_ele?.avatar, desc: legend_ele?.description - }); + }) } } catch (e) { - this.ctx.logger.error('获取群聊炽焰失败', e); + this.ctx.logger.error('获取群聊炽焰失败', e) } } if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { try { - let RetInternal = await getDataInternal(groupCode, 6); + let RetInternal = await getDataInternal(groupCode, 6) if (!RetInternal) { - throw new Error('获取快乐源泉失败'); + throw new Error('获取快乐源泉失败') } - HonorInfo.emotion_list = []; + HonorInfo.emotion_list = [] for (const emotion_ele of RetInternal) { HonorInfo.emotion_list.push({ user_id: emotion_ele?.uin, nickname: emotion_ele?.name, avatar: emotion_ele?.avatar, desc: emotion_ele?.description - }); + }) } } catch (e) { - this.ctx.logger.error('获取快乐源泉失败', e); + this.ctx.logger.error('获取快乐源泉失败', e) } } //冒尖小春笋好像已经被tx扬了 if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { - HonorInfo.strong_newbie_list = []; + HonorInfo.strong_newbie_list = [] } - return HonorInfo; + return HonorInfo + } + + async setGroupNotice(params: SetGroupNoticeParams): Promise { + const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com') + const settings = JSON.stringify({ + is_show_edit_card: params.isShowEditCard, + tip_window_type: params.tipWindowType, + confirm_required: params.confirmRequired + }) + + return await RequestUtil.HttpGetJson( + `https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({ + bkn: this.genBkn(cookieObject.skey), + qid: params.groupCode, + text: params.content, + pinned: params.pinned.toString(), + type: params.type.toString(), + settings: settings, + ...(params.picId !== '' && { + pic: params.picId, + imgWidth: params.imgWidth?.toString(), + imgHeight: params.imgHeight?.toString(), + }) + })}`, + 'POST', + '', + { 'Cookie': this.cookieToString(cookieObject) } + ) + } + + private cookieToString(cookieObject: Dict) { + return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ') } } diff --git a/src/ntqqapi/core.ts b/src/ntqqapi/core.ts index 883f52a..3ed93ad 100644 --- a/src/ntqqapi/core.ts +++ b/src/ntqqapi/core.ts @@ -37,7 +37,7 @@ declare module 'cordis' { } class Core extends Service { - static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi'] + static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi'] constructor(protected ctx: Context, public config: Core.Config) { super(ctx, 'app', true) diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 5eaf14c..edf76ba 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -26,7 +26,7 @@ export const ReceiveCmdS = { CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan', MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', SKEY_UPDATE: 'onSkeyUpdate', -} +} as const export type ReceiveCmd = string diff --git a/src/ntqqapi/types/group.ts b/src/ntqqapi/types/group.ts index 6512575..b7d860b 100644 --- a/src/ntqqapi/types/group.ts +++ b/src/ntqqapi/types/group.ts @@ -36,6 +36,7 @@ export interface Group { memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q" } members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 + createTime: string } export enum GroupMemberRole { diff --git a/src/ntqqapi/types/msg.ts b/src/ntqqapi/types/msg.ts index 8a7abfc..fb5387f 100644 --- a/src/ntqqapi/types/msg.ts +++ b/src/ntqqapi/types/msg.ts @@ -480,6 +480,8 @@ export interface RawMessage { sourceMsgIsIncPic: boolean // 原消息是否有图片 sourceMsgText: string replayMsgSeq: string // 源消息的msgSeq,可以通过这个找到源消息的msgId + senderUidStr: string + replyMsgTime: string } textElement: { atType: AtType diff --git a/src/onebot11/action/go-cqhttp/SendGroupNotice.ts b/src/onebot11/action/go-cqhttp/SendGroupNotice.ts new file mode 100644 index 0000000..1e54a82 --- /dev/null +++ b/src/onebot11/action/go-cqhttp/SendGroupNotice.ts @@ -0,0 +1,37 @@ +import BaseAction from '../BaseAction' +import { ActionName } from '../types' + +interface Payload { + group_id: number | string + content: string + image?: string + pinned?: number | string //扩展 + confirm_required?: number | string //扩展 +} + +export class SendGroupNotice extends BaseAction { + actionName = ActionName.GoCQHTTP_SendGroupNotice + + async _handle(payload: Payload) { + const type = 1 + const isShowEditCard = 0 + const tipWindowType = 0 + const pinned = Number(payload.pinned ?? 0) + const confirmRequired = Number(payload.confirm_required ?? 1) + + const result = await this.ctx.ntWebApi.setGroupNotice({ + groupCode: payload.group_id.toString(), + content: payload.content, + pinned, + type, + isShowEditCard, + tipWindowType, + confirmRequired, + picId: '' + }) + if (result.ec !== 0) { + throw new Error(`设置群公告失败, 错误信息: ${result.em}`) + } + return null + } +} \ No newline at end of file diff --git a/src/onebot11/action/go-cqhttp/UploadFile.ts b/src/onebot11/action/go-cqhttp/UploadFile.ts index e40ecd4..24428b9 100644 --- a/src/onebot11/action/go-cqhttp/UploadFile.ts +++ b/src/onebot11/action/go-cqhttp/UploadFile.ts @@ -2,24 +2,22 @@ import fs from 'node:fs' import BaseAction from '../BaseAction' import { ActionName } from '../types' import { SendElementEntities } from '@/ntqqapi/entities' -import { ChatType, SendFileElement } from '@/ntqqapi/types' +import { SendFileElement } from '@/ntqqapi/types' import { uri2local } from '@/common/utils' -import { Peer } from '@/ntqqapi/types' -import { sendMsg } from '../../helper/createMessage' +import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' -interface Payload { - user_id: number | string - group_id?: number | string +interface UploadGroupFilePayload { + group_id: number | string file: string name: string folder?: string folder_id?: string } -export class UploadGroupFile extends BaseAction { +export class UploadGroupFile extends BaseAction { actionName = ActionName.GoCQHTTP_UploadGroupFile - protected async _handle(payload: Payload): Promise { + protected async _handle(payload: UploadGroupFilePayload): Promise { let file = payload.file if (fs.existsSync(file)) { file = `file://${file}` @@ -29,31 +27,23 @@ export class UploadGroupFile extends BaseAction { throw new Error(downloadResult.errMsg) } const sendFileEle = await SendElementEntities.file(this.ctx, downloadResult.path, payload.name, payload.folder_id) - await sendMsg(this.ctx, { - chatType: ChatType.group, - peerUid: payload.group_id?.toString()!, - }, [sendFileEle], [], true) + const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group) + await sendMsg(this.ctx, peer, [sendFileEle], [], true) return null } } -export class UploadPrivateFile extends BaseAction { +interface UploadPrivateFilePayload { + user_id: number | string + file: string + name: string +} + +export class UploadPrivateFile extends BaseAction { actionName = ActionName.GoCQHTTP_UploadPrivateFile - async getPeer(payload: Payload): Promise { - if (payload.user_id) { - const peerUid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString()) - if (!peerUid) { - throw `私聊${payload.user_id}不存在` - } - const isBuddy = await this.ctx.ntFriendApi.isBuddy(peerUid) - return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid } - } - throw '缺少参数 user_id' - } - - protected async _handle(payload: Payload): Promise { - const peer = await this.getPeer(payload) + protected async _handle(payload: UploadPrivateFilePayload): Promise { + const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private) let file = payload.file if (fs.existsSync(file)) { file = `file://${file}` diff --git a/src/onebot11/action/index.ts b/src/onebot11/action/index.ts index e511196..4845f45 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -60,6 +60,8 @@ import { CreateGroupFileFolder } from './go-cqhttp/CreateGroupFileFolder' import { DelGroupFolder } from './go-cqhttp/DelGroupFolder' import { GetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain' import { GetGroupRootFiles } from './go-cqhttp/GetGroupRootFiles' +import { SetOnlineStatus } from './llonebot/SetOnlineStatus' +import { SendGroupNotice } from './go-cqhttp/SendGroupNotice' export function initActionMap(adapter: Adapter) { const actionHandlers = [ @@ -71,6 +73,7 @@ export function initActionMap(adapter: Adapter) { new SetQQAvatar(adapter), new GetFriendWithCategory(adapter), new GetEvent(adapter), + new SetOnlineStatus(adapter), // onebot11 new SendLike(adapter), new GetMsg(adapter), @@ -126,7 +129,8 @@ export function initActionMap(adapter: Adapter) { new CreateGroupFileFolder(adapter), new DelGroupFolder(adapter), new GetGroupAtAllRemain(adapter), - new GetGroupRootFiles(adapter) + new GetGroupRootFiles(adapter), + new SendGroupNotice(adapter) ] const actionMap = new Map>() for (const action of actionHandlers) { diff --git a/src/onebot11/action/llonebot/SetOnlineStatus.ts b/src/onebot11/action/llonebot/SetOnlineStatus.ts new file mode 100644 index 0000000..98e67e2 --- /dev/null +++ b/src/onebot11/action/llonebot/SetOnlineStatus.ts @@ -0,0 +1,25 @@ +import BaseAction from '../BaseAction' +import { ActionName } from '../types' + +interface Payload { + status: number | string + ext_status: number | string + battery_status: number | string +} + +export class SetOnlineStatus extends BaseAction { + actionName = ActionName.SetOnlineStatus + + async _handle(payload: Payload) { + const ret = await this.ctx.ntUserApi.setSelfStatus( + Number(payload.status), + Number(payload.ext_status), + Number(payload.battery_status), + ) + if (ret.result !== 0) { + this.ctx.logger.error(ret) + throw new Error('设置在线状态失败') + } + return null + } +} \ No newline at end of file diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index 87509e7..9ee583f 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -21,45 +21,15 @@ import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostD import { Peer } from '@/ntqqapi/types/msg' import { MessageUnique } from '@/common/utils/messageUnique' import { selfInfo } from '@/common/globalVars' -import { convertMessage2List, createSendElements, sendMsg } from '../../helper/createMessage' +import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' -export interface ReturnDataType { +interface ReturnData { message_id: number } -export enum ContextMode { - Normal = 0, - Private = 1, - Group = 2 -} - -export class SendMsg extends BaseAction { +export class SendMsg extends BaseAction { actionName = ActionName.SendMsg - private async createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise { - // This function determines the type of message by the existence of user_id / group_id, - // not message_type. - // This redundant design of Ob11 here should be blamed. - - if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) { - return { - chatType: ChatType.group, - peerUid: payload.group_id.toString(), - } - } - if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) { - const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString()) - if (!uid) throw new Error('无法获取用户信息') - const isBuddy = await this.ctx.ntFriendApi.isBuddy(uid) - return { - chatType: isBuddy ? ChatType.friend : ChatType.temp, - peerUid: uid, - guildId: isBuddy ? '' : payload.group_id?.toString() || '' - } - } - throw new Error('请指定 group_id 或 user_id') - } - protected async check(payload: OB11PostSendMsg): Promise { const messages = convertMessage2List(payload.message) const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node) @@ -84,13 +54,13 @@ export class SendMsg extends BaseAction { } protected async _handle(payload: OB11PostSendMsg) { - let contextMode = ContextMode.Normal + let contextMode = CreatePeerMode.Normal if (payload.message_type === 'group') { - contextMode = ContextMode.Group + contextMode = CreatePeerMode.Group } else if (payload.message_type === 'private') { - contextMode = ContextMode.Private + contextMode = CreatePeerMode.Private } - const peer = await this.createContext(payload, contextMode) + const peer = await createPeer(this.ctx, payload, contextMode) const messages = convertMessage2List( payload.message, payload.auto_escape === true || payload.auto_escape === 'true', diff --git a/src/onebot11/action/types.ts b/src/onebot11/action/types.ts index 75b6926..704c5b8 100644 --- a/src/onebot11/action/types.ts +++ b/src/onebot11/action/types.ts @@ -23,6 +23,7 @@ export enum ActionName { GetFile = 'get_file', GetFriendsWithCategory = 'get_friends_with_category', GetEvent = 'get_event', + SetOnlineStatus = 'set_online_status', // onebot 11 SendLike = 'send_like', GetLoginInfo = 'get_login_info', @@ -78,5 +79,6 @@ export enum ActionName { GoCQHTTP_CreateGroupFileFolder = 'create_group_file_folder', GoCQHTTP_DelGroupFolder = 'delete_group_folder', GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain', - GoCQHTTP_GetGroupRootFiles = 'get_group_root_files' + GoCQHTTP_GetGroupRootFiles = 'get_group_root_files', + GoCQHTTP_SendGroupNotice = '_send_group_notice', } diff --git a/src/onebot11/adapter.ts b/src/onebot11/adapter.ts index 4713df5..152b225 100644 --- a/src/onebot11/adapter.ts +++ b/src/onebot11/adapter.ts @@ -35,7 +35,7 @@ declare module 'cordis' { } class OneBot11Adapter extends Service { - static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi'] + static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi'] public messages: Map = new Map() public startTime = 0 @@ -50,7 +50,8 @@ class OneBot11Adapter extends Service { this.ob11Http = new OB11Http(ctx, { port: config.httpPort, token: config.token, - actionMap + actionMap, + listenLocalhost: config.listenLocalhost }) this.ob11HttpPost = new OB11HttpPost(ctx, { hosts: config.httpHosts, @@ -62,7 +63,8 @@ class OneBot11Adapter extends Service { port: config.wsPort, heartInterval: config.heartInterval, token: config.token, - actionMap + actionMap, + listenLocalhost: config.listenLocalhost }) this.ob11WebSocketReverseManager = new OB11WebSocketReverseManager(ctx, { hosts: config.wsHosts, @@ -110,10 +112,10 @@ class OneBot11Adapter extends Service { for (const notify of notifies) { try { const notifyTime = parseInt(notify.seq) / 1000 - const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type if (notifyTime < this.startTime) { continue } + const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) { this.ctx.logger.info('有成员退出通知', notify) const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) @@ -292,7 +294,7 @@ class OneBot11Adapter extends Service { } } // HTTP 端口变化,重启服务 - if (config.ob11.httpPort !== old.httpPort) { + if ((config.ob11.httpPort !== old.httpPort || config.ob11.listenLocalhost !== old.listenLocalhost) && config.ob11.enableHttp) { await this.ob11Http.stop() this.ob11Http.start() } @@ -305,7 +307,7 @@ class OneBot11Adapter extends Service { } } // 正向 WebSocket 端口变化,重启服务 - if (config.ob11.wsPort !== old.wsPort) { + if ((config.ob11.wsPort !== old.wsPort || config.ob11.listenLocalhost !== old.listenLocalhost) && config.ob11.enableWs) { await this.ob11WebSocket.stop() this.ob11WebSocket.start() llonebotError.wsServerError = '' diff --git a/src/onebot11/connect/http.ts b/src/onebot11/connect/http.ts index d24c0c6..75896e2 100644 --- a/src/onebot11/connect/http.ts +++ b/src/onebot11/connect/http.ts @@ -51,8 +51,9 @@ class OB11Http { this.expressAPP.get('/', (req: Request, res: Response) => { res.send(`LLOneBot server 已启动`) }) - this.server = this.expressAPP.listen(this.config.port, '0.0.0.0', () => { - this.ctx.logger.info(`HTTP server started 0.0.0.0:${this.config.port}`) + const host = this.config.listenLocalhost ? '127.0.0.1' : '0.0.0.0' + this.server = this.expressAPP.listen(this.config.port, host, () => { + this.ctx.logger.info(`HTTP server started ${host}:${this.config.port}`) }) llonebotError.httpServerError = '' } catch (e: any) { @@ -136,6 +137,7 @@ namespace OB11Http { port: number token?: string actionMap: Map> + listenLocalhost: boolean } } diff --git a/src/onebot11/connect/ws.ts b/src/onebot11/connect/ws.ts index 690ec8d..00c10d9 100644 --- a/src/onebot11/connect/ws.ts +++ b/src/onebot11/connect/ws.ts @@ -21,9 +21,14 @@ class OB11WebSocket { public start() { if (this.wsServer) return - this.ctx.logger.info(`WebSocket server started 0.0.0.0:${this.config.port}`) + const host = this.config.listenLocalhost ? '127.0.0.1' : '0.0.0.0' + this.ctx.logger.info(`WebSocket server started ${host}:${this.config.port}`) try { - this.wsServer = new WebSocketServer({ port: this.config.port, maxPayload: 1024 * 1024 * 1024 }) + this.wsServer = new WebSocketServer({ + host, + port: this.config.port, + maxPayload: 1024 * 1024 * 1024 + }) llonebotError.wsServerError = '' } catch (e: any) { llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString() @@ -165,6 +170,7 @@ namespace OB11WebSocket { heartInterval: number token?: string actionMap: Map> + listenLocalhost: boolean } } diff --git a/src/onebot11/entities.ts b/src/onebot11/entities.ts index dd7ab0a..3a1680b 100644 --- a/src/onebot11/entities.ts +++ b/src/onebot11/entities.ts @@ -43,7 +43,7 @@ import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNotice import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' -import { omit, isNullable } from 'cosmokit' +import { omit, isNullable, pick } from 'cosmokit' import { Context } from 'cordis' import { selfInfo } from '@/common/globalVars' import { pathToFileURL } from 'node:url' @@ -154,15 +154,18 @@ export namespace OB11Entities { guildId: '' } try { + const { replayMsgSeq, replyMsgTime, senderUidStr } = replyElement const records = msg.records.find(msgRecord => msgRecord.msgId === replyElement.sourceMsgIdInRecords) - if (!records) throw new Error('找不到回复消息') - let replyMsg = (await ctx.ntMsgApi.getMsgsBySeqAndCount(peer, replyElement.replayMsgSeq, 1, true, true)).msgList[0] - if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { - replyMsg = (await ctx.ntMsgApi.getSingleMsg(peer, replyElement.replayMsgSeq)).msgList[0] + if (!records || !replyMsgTime || !senderUidStr) { + throw new Error('找不到回复消息') } + const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, replayMsgSeq, replyMsgTime, [senderUidStr]) + const replyMsg = msgList.find(msg => msg.msgRandom === records.msgRandom) + // 284840486: 合并消息内侧 消息具体定位不到 - if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { - throw new Error('回复消息消息验证失败') + if (!replyMsg && msg.peerUin !== '284840486') { + ctx.logger.info('queryMsgs', msgList.map(e => pick(e, ['msgSeq', 'msgRandom']))) + throw new Error('回复消息验证失败') } messageSegment = { type: OB11MessageDataType.reply, @@ -171,7 +174,7 @@ export namespace OB11Entities { } } } catch (e: any) { - ctx.logger.error('获取不到引用的消息', replyElement.replayMsgSeq, e.stack) + ctx.logger.error('获取不到引用的消息', replyElement, e.stack) continue } } @@ -518,29 +521,27 @@ export namespace OB11Entities { }).parse(xmlElement.content) ctx.logger.info('收到表情回应我的消息', emojiLikeData) try { - const senderUin = emojiLikeData.gtip.qq.jp - const msgSeq = emojiLikeData.gtip.url.msgseq - const emojiId = emojiLikeData.gtip.face.id - const replyMsgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount({ + const senderUin: string = emojiLikeData.gtip.qq.jp + const msgSeq: string = emojiLikeData.gtip.url.msgseq + const emojiId: string = emojiLikeData.gtip.face.id + const peer = { chatType: ChatType.group, guildId: '', peerUid: msg.peerUid, - }, msgSeq, 1, true, true))?.msgList + } + const replyMsgList = (await ctx.ntMsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList if (!replyMsgList?.length) { return } - const likes = [ - { - emoji_id: emojiId, - count: 1, - }, - ] const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId) return new OB11GroupMsgEmojiLikeEvent( parseInt(msg.peerUid), parseInt(senderUin), shortId!, - likes + [{ + emoji_id: emojiId, + count: 1, + }] ) } catch (e: any) { ctx.logger.error('解析表情回应消息失败', e.stack) @@ -768,6 +769,8 @@ export namespace OB11Entities { return { group_id: parseInt(group.groupCode), group_name: group.groupName, + group_memo: group.remarkName, + group_create_time: +group.createTime, member_count: group.memberCount, max_member_count: group.maxMember, } diff --git a/src/onebot11/helper/createMessage.ts b/src/onebot11/helper/createMessage.ts index a96cf8a..678da5d 100644 --- a/src/onebot11/helper/createMessage.ts +++ b/src/onebot11/helper/createMessage.ts @@ -276,4 +276,34 @@ export async function sendMsg( deleteAfterSentFiles.map(path => fsPromise.unlink(path)) return returnMsg } +} + +export interface CreatePeerPayload { + group_id?: string | number + user_id?: string | number +} + +export enum CreatePeerMode { + Normal = 0, + Private = 1, + Group = 2 +} + +export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode: CreatePeerMode): Promise { + if ((mode === CreatePeerMode.Group || mode === CreatePeerMode.Normal) && payload.group_id) { + return { + chatType: ChatType.group, + peerUid: payload.group_id.toString(), + } + } + if ((mode === CreatePeerMode.Private || mode === CreatePeerMode.Normal) && payload.user_id) { + const uid = await ctx.ntUserApi.getUidByUin(payload.user_id.toString()) + if (!uid) throw new Error('无法获取用户信息') + const isBuddy = await ctx.ntFriendApi.isBuddy(uid) + return { + chatType: isBuddy ? ChatType.friend : ChatType.temp, + peerUid: uid, + } + } + throw new Error('请指定 group_id 或 user_id') } \ No newline at end of file diff --git a/src/onebot11/helper/quickOperation.ts b/src/onebot11/helper/quickOperation.ts index 651281c..c67d8f7 100644 --- a/src/onebot11/helper/quickOperation.ts +++ b/src/onebot11/helper/quickOperation.ts @@ -1,12 +1,12 @@ import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' -import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types' -import { convertMessage2List, createSendElements, sendMsg } from '../helper/createMessage' -import { getConfigUtil } from '@/common/config' +import { GroupRequestOperateTypes } from '@/ntqqapi/types' +import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../helper/createMessage' import { MessageUnique } from '@/common/utils/messageUnique' import { isNullable } from 'cosmokit' import { Context } from 'cordis' +import { OB11Config } from '@/common/types' interface QuickOperationPrivateMessage { reply?: string @@ -57,21 +57,14 @@ export async function handleQuickOperation(ctx: Context, event: QuickOperationEv async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { const reply = quickAction.reply - const ob11Config = getConfigUtil().getConfig().ob11 - const peer: Peer = { - chatType: ChatType.friend, - peerUid: msg.user_id.toString(), - } - if (msg.message_type == 'private') { - peer.peerUid = (await ctx.ntUserApi.getUidByUin(msg.user_id.toString()))! - if (msg.sub_type === 'group') { - peer.chatType = ChatType.temp - } - } - else { - peer.chatType = ChatType.group - peer.peerUid = msg.group_id?.toString()! + const ob11Config: OB11Config = ctx.config + let contextMode = CreatePeerMode.Normal + if (msg.message_type === 'group') { + contextMode = CreatePeerMode.Group + } else if (msg.message_type === 'private') { + contextMode = CreatePeerMode.Private } + const peer = await createPeer(ctx, msg, contextMode) if (reply) { let replyMessage: OB11MessageData[] = [] if (ob11Config.enableQOAutoQuote) { diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index 87c9b9d..2d737ee 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -54,8 +54,10 @@ export interface OB11GroupMember { export interface OB11Group { group_id: number group_name: string - member_count?: number - max_member_count?: number + group_memo: string + group_create_time: number + member_count: number + max_member_count: number } interface OB11Sender { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 4623977..f783a98 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -166,7 +166,12 @@ async function onSettingWindowCreated(view: Element) { SettingItem( '快速操作回复自动引用原消息', null, - SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote, { 'control-display-id': 'config-ob11-enableQOAutoQuote' }), + SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote), + ), + SettingItem( + 'HTTP、正向 WebSocket 服务仅监听 127.0.0.1', + '而不是 0.0.0.0', + SettingSwitch('ob11.listenLocalhost', config.ob11.listenLocalhost), ), SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')), ]), diff --git a/src/version.ts b/src/version.ts index d6a6bd7..e63cc64 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.31.6' +export const version = '3.31.7'