From f8e231b8b80d4a5f823611f14339c16040b80ff6 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Tue, 13 Aug 2024 19:09:13 +0800 Subject: [PATCH 01/23] =?UTF-8?q?chore:=20v3.28.7=20fix:=20CPU=E5=8D=A0?= =?UTF-8?q?=E7=94=A8=E8=BF=87=E9=AB=98=20fix:=20=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=8F=98=E5=8A=A8hook=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.json | 2 +- src/common/data.ts | 2 +- src/main/main.ts | 9 ++++--- src/ntqqapi/hook.ts | 54 ++++++++++++++++++++++++++++----------- src/ntqqapi/types/user.ts | 2 +- src/version.ts | 2 +- 6 files changed, 48 insertions(+), 23 deletions(-) diff --git a/manifest.json b/manifest.json index f218c4e..f296992 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.28.6", + "version": "3.28.7", "icon": "./icon.webp", "authors": [ { diff --git a/src/common/data.ts b/src/common/data.ts index 3f8f85a..cc559d3 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -127,4 +127,4 @@ export function getSelfUid() { export function getSelfUin() { return selfInfo['uin'] -} \ No newline at end of file +} diff --git a/src/main/main.ts b/src/main/main.ts index cf4e8aa..173f62c 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -401,8 +401,9 @@ function onLoad() { dbUtil.init(uin) log('start activate group member info') - NTQQGroupApi.activateMemberInfoChange().then().catch(log) - NTQQGroupApi.activateMemberListChange().then().catch(log) + // 下面两个会导致CPU占用过高,QQ卡死 + // NTQQGroupApi.activateMemberInfoChange().then().catch(log) + // NTQQGroupApi.activateMemberListChange().then().catch(log) startReceiveHook().then() if (config.ob11.enableHttp) { @@ -432,13 +433,13 @@ function onLoad() { } //log('self info', selfInfo, globalThis.authData) if (current.uin) { - start(current.uid, current.uin) + await start(current.uid, current.uin) } else { setTimeout(init, 1000) } } - init() + init().then() } // 创建窗口时触发 diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 781f04d..651dcda 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,7 +1,16 @@ import type { BrowserWindow } from 'electron' import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQMsgApi } from './api/msg' -import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types' +import { + CategoryFriend, + ChatType, + FriendV2, + Group, + GroupMember, + GroupMemberRole, + RawMessage, + SimpleInfo, User, +} from './types' import { deleteGroup, friends, @@ -103,8 +112,8 @@ export function hookNTQQApiReceive(window: BrowserWindow) { if (hook.hookFunc.constructor.name === 'AsyncFunction') { ; (_ as Promise).then() } - } catch (e) { - log('hook error', e, receiveData.payload) + } catch (e: any) { + log('hook error', ntQQApiMethodName, e.stack.toString()) } }).then() } @@ -332,7 +341,7 @@ export async function startHook() { }) registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => { // updateType 3是群列表变动,2是群成员变动 - // log("群列表变动", payload.updateType, payload.groupList) + // log("群列表变动, store", payload.updateType, payload.groupList) if (payload.updateType != 2) { updateGroups(payload.groupList).then() } @@ -391,17 +400,32 @@ export async function startHook() { registerReceiveHook<{ data: CategoryFriend[] }>(ReceiveCmdS.FRIENDS, (payload) => { - for (const fData of payload.data) { - const _friends = fData.buddyList - for (let friend of _friends) { - NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() - let existFriend = friends.find((f) => f.uin == friend.uin) - if (!existFriend) { - friends.push(friend) - } - else { - Object.assign(existFriend, friend) + // log("onBuddyListChange", payload) + // let friendListV2: {userSimpleInfos: Map} = [] + type V2data = {userSimpleInfos: Map} + let friendList: User[] = []; + if ((payload as any).userSimpleInfos) { + // friendListV2 = payload as any + friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => { + return { + ...v.coreInfo, } + }) + } + else{ + for (const fData of payload.data) { + friendList.push(...fData.buddyList) + } + } + log('好友列表变动', friendList) + for (let friend of friendList) { + NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() + let existFriend = friends.find((f) => f.uin == friend.uin) + if (!existFriend) { + friends.push(friend) + } + else { + Object.assign(existFriend, friend) } } }) @@ -508,4 +532,4 @@ export async function startHook() { log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) }) }) -} \ No newline at end of file +} diff --git a/src/ntqqapi/types/user.ts b/src/ntqqapi/types/user.ts index e109794..d687734 100644 --- a/src/ntqqapi/types/user.ts +++ b/src/ntqqapi/types/user.ts @@ -340,4 +340,4 @@ export interface UserDetailInfoByUin { pendantId: string vipNameColorId: string } -} \ No newline at end of file +} diff --git a/src/version.ts b/src/version.ts index b13a226..0efbc45 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.28.6' +export const version = '3.28.7' From 6582ffe9646728a5fc4af6a92fc3d5adebe9b1bc Mon Sep 17 00:00:00 2001 From: idranme Date: Tue, 13 Aug 2024 19:29:22 +0800 Subject: [PATCH 02/23] fix: msg --- CHANGELOG | 14 - README.md | 2 +- electron.vite.config.ts | 9 +- manifest.json | 2 +- package.json | 4 +- src/common/data.ts | 6 +- src/common/db.ts | 253 ------------------ src/common/utils/EventTask.ts | 3 + src/common/utils/MessageUnique.ts | 133 +++++++++ src/common/utils/file.ts | 8 - src/common/utils/helper.ts | 23 ++ src/common/utils/index.ts | 10 +- src/common/utils/table.ts | 1 + src/main/main.ts | 63 ++--- src/ntqqapi/api/file.ts | 117 ++++++++ src/ntqqapi/api/msg.ts | 20 ++ src/ntqqapi/hook.ts | 68 +++-- .../services/NodeIKernelSearchService.ts | 128 +++++++++ src/ntqqapi/services/index.ts | 3 +- src/ntqqapi/types/user.ts | 2 +- src/ntqqapi/wrapper.ts | 4 +- src/onebot11/action/file/GetFile.ts | 161 ++++++----- .../action/go-cqhttp/DelEssenceMsg.ts | 19 +- src/onebot11/action/go-cqhttp/DownloadFile.ts | 19 +- .../action/go-cqhttp/GetForwardMsg.ts | 32 +-- .../action/go-cqhttp/GetGroupMsgHistory.ts | 47 ++-- .../action/go-cqhttp/SetEssenceMsg.ts | 23 +- src/onebot11/action/msg/DeleteMsg.ts | 19 +- src/onebot11/action/msg/ForwardSingleMsg.ts | 18 +- src/onebot11/action/msg/GetMsg.ts | 28 +- src/onebot11/action/msg/SendMsg.ts | 213 +++++++-------- src/onebot11/action/msg/SetMsgEmojiLike.ts | 28 +- src/onebot11/action/quick-operation.ts | 13 +- src/onebot11/constructor.ts | 169 ++++++------ src/onebot11/server/ws/reply.ts | 4 +- src/onebot11/types.ts | 2 +- src/version.ts | 2 +- 37 files changed, 932 insertions(+), 738 deletions(-) delete mode 100644 CHANGELOG delete mode 100644 src/common/db.ts create mode 100644 src/common/utils/MessageUnique.ts create mode 100644 src/ntqqapi/services/NodeIKernelSearchService.ts diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index d6dc7d8..0000000 --- a/CHANGELOG +++ /dev/null @@ -1,14 +0,0 @@ -# 3.24.0 - -## 修复 - -* 修复图片rkey导致链接失效的问题 -* 修复/get_image, /get_file 无法获取图片的问题 -* 修复上报他人管理员被取消通知 - -## 新增 - -* 新增表情回应发送和上报 -* 新增商城表情发送,和上报 url -* 新增转发单条消息接口 `forward_friend_single_msg`, `forward_group_single_msg` -* 新增新增好友事件 \ No newline at end of file diff --git a/README.md b/README.md index 71d88f2..b08ecd5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发 > [!CAUTION]\ -> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** +> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** TG群: diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 1259f35..ea0928a 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -5,14 +5,7 @@ import './scripts/gen-manifest' const external = [ 'silk-wasm', 'ws', - 'level', - 'classic-level', - 'abstract-level', - 'level-supports', - 'level-transcoder', - 'module-error', - 'catering', - 'node-gyp-build', + '@minatojs/sql.js', ] function genCpModule(module: string) { diff --git a/manifest.json b/manifest.json index f218c4e..f296992 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.28.6", + "version": "3.28.7", "icon": "./icon.webp", "authors": [ { diff --git a/package.json b/package.json index af164bd..aa74344 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,15 @@ "author": "", "license": "MIT", "dependencies": { + "@minatojs/driver-sqlite": "^4.4.1", "compressing": "^1.10.1", + "cordis": "^3.17.9", "cors": "^2.8.5", "express": "^4.19.2", "fast-xml-parser": "^4.4.1", "file-type": "^19.4.0", "fluent-ffmpeg": "^2.1.3", - "level": "^8.0.1", + "minato": "^3.4.3", "silk-wasm": "^3.6.1", "ws": "^8.18.0" }, diff --git a/src/common/data.ts b/src/common/data.ts index 3f8f85a..c551fa0 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -16,7 +16,7 @@ export const llonebotError: LLOneBotError = { ffmpegError: '', httpServerError: '', wsServerError: '', - otherError: 'LLOnebot未能正常启动,请检查日志查看错误', + otherError: 'LLOnebot 未能正常启动,请检查日志查看错误', } // 群号 -> 群成员map(uid=>GroupMember) export const groupMembers: Map> = new Map>() @@ -102,7 +102,7 @@ const selfInfo: SelfInfo = { } export async function getSelfNick(force = false): Promise { - if (!selfInfo.nick || force) { + if ((!selfInfo.nick || force) && selfInfo.uid) { const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid) if (userInfo) { selfInfo.nick = userInfo.nick @@ -127,4 +127,4 @@ export function getSelfUid() { export function getSelfUin() { return selfInfo['uin'] -} \ No newline at end of file +} diff --git a/src/common/db.ts b/src/common/db.ts deleted file mode 100644 index b27b7a3..0000000 --- a/src/common/db.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { Level } from 'level' -import { type GroupNotify, RawMessage } from '../ntqqapi/types' -import { DATA_DIR } from './utils' -import { FileCache } from './types' -import { log } from './utils/log' - -type ReceiveTempUinMap = Record - -class DBUtil { - public readonly DB_KEY_PREFIX_MSG_ID = 'msg_id_' - public readonly DB_KEY_PREFIX_MSG_SHORT_ID = 'msg_short_id_' - public readonly DB_KEY_PREFIX_MSG_SEQ_ID = 'msg_seq_id_' - public readonly DB_KEY_PREFIX_FILE = 'file_' - public readonly DB_KEY_PREFIX_GROUP_NOTIFY = 'group_notify_' - private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = 'received_temp_uin_map' - public db: Level | undefined - public cache: Record = {} // : RawMessage - private currentShortId: number | undefined - - /* - * 数据库结构 - * msg_id_101231230999: {} // 长id: RawMessage - * msg_short_id_1: 101231230999 // 短id: 长id - * msg_seq_id_1: 101231230999 // 序列id: 长id - * file_7827DBAFJFW2323.png: {} // 文件名: FileCache - * */ - - constructor() { - } - - init(uin: string) { - const DB_PATH = DATA_DIR + `/msg_${uin}` - this.db = new Level(DB_PATH, { valueEncoding: 'json' }) - const expiredMilliSecond = 1000 * 60 * 60 - setInterval(() => { - // this.cache = {} - // 清理时间较久的缓存 - const now = Date.now() - for (let key in this.cache) { - let message: RawMessage = this.cache[key] as RawMessage - if (message?.msgTime) { - if (now - parseInt(message.msgTime) * 1000 > expiredMilliSecond) { - delete this.cache[key] - // log("clear cache", key, message.msgTime); - } - } - } - }, expiredMilliSecond) - } - - public async getReceivedTempUinMap(): Promise { - try { - this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db?.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)!) - } catch (e) { } - return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap - } - public setReceivedTempUinMap(data: ReceiveTempUinMap) { - this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data - this.db?.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then() - } - private addCache(msg: RawMessage) { - const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId - const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId - const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq - this.cache[longIdKey] = this.cache[shortIdKey] = msg - } - - public clearCache() { - this.cache = {} - } - - async getMsgByShortId(shortMsgId: number): Promise { - const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId - if (this.cache[shortMsgIdKey]) { - // log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey]) - return this.cache[shortMsgIdKey] as RawMessage - } - try { - const longId = await this.db?.get(shortMsgIdKey) - const msg = await this.getMsgByLongId(longId!) - this.addCache(msg!) - return msg - } catch (e: any) { - log('getMsgByShortId db error', e.stack.toString()) - } - } - - async getMsgByLongId(longId: string): Promise { - const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId - if (this.cache[longIdKey]) { - return this.cache[longIdKey] as RawMessage - } - try { - const data = await this.db?.get(longIdKey) - const msg = JSON.parse(data!) - this.addCache(msg) - return msg - } catch (e) { - // log("getMsgByLongId db error", e.stack.toString()) - } - } - - async getMsgBySeqId(seqId: string): Promise { - const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId - if (this.cache[seqIdKey]) { - return this.cache[seqIdKey] as RawMessage - } - try { - const longId = await this.db?.get(seqIdKey) - const msg = await this.getMsgByLongId(longId!) - this.addCache(msg!) - return msg - } catch (e: any) { - log('getMsgBySeqId db error', e.stack.toString()) - } - } - - async addMsg(msg: RawMessage) { - // 有则更新,无则添加 - // log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId); - const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId - let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage - if (!existMsg) { - try { - existMsg = await this.getMsgByLongId(msg.msgId) - } catch (e) { - // log("addMsg getMsgByLongId error", e.stack.toString()) - } - } - if (existMsg) { - // log("消息已存在", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId) - this.updateMsg(msg).then() - return existMsg.msgShortId - } - - const shortMsgId = await this.genMsgShortId() - const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId - const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq - msg.msgShortId = shortMsgId - this.addCache(msg) - // log("新增消息记录", msg.msgId) - this.db?.put(shortIdKey, msg.msgId).then().catch() - this.db?.put(longIdKey, JSON.stringify(msg)).then().catch() - try { - await this.db?.get(seqIdKey) - } catch (e) { - // log("新的seqId", seqIdKey) - this.db?.put(seqIdKey, msg.msgId).then().catch() - } - if (!this.cache[seqIdKey]) { - this.cache[seqIdKey] = msg - } - return shortMsgId - // log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`); - } - - async updateMsg(msg: RawMessage) { - const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId - let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage - if (!existMsg) { - try { - existMsg = await this.getMsgByLongId(msg.msgId) - } catch (e) { - existMsg = msg - } - } - - Object.assign(existMsg!, msg) - this.db?.put(longIdKey, JSON.stringify(existMsg)).then().catch() - const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg?.msgShortId - const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq - if (!this.cache[seqIdKey]) { - this.cache[seqIdKey] = existMsg! - } - this.db?.put(shortIdKey, msg.msgId).then().catch() - try { - await this.db?.get(seqIdKey) - } catch (e) { - this.db?.put(seqIdKey, msg.msgId).then().catch() - // log("更新seqId error", e.stack, seqIdKey); - } - // log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId); - } - - private async genMsgShortId(): Promise { - const key = 'msg_current_short_id' - if (this.currentShortId === undefined) { - try { - const id = await this.db?.get(key) - this.currentShortId = parseInt(id!) - } catch (e) { - this.currentShortId = -2147483640 - } - } - - this.currentShortId++ - this.db?.put(key, this.currentShortId.toString()).then().catch() - return this.currentShortId - } - - async addFileCache(fileNameOrUuid: string, data: FileCache) { - const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid - if (this.cache[key]) { - return - } - let cacheDBData = { ...data } - delete cacheDBData['downloadFunc'] - this.cache[fileNameOrUuid] = data - try { - await this.db?.put(key, JSON.stringify(cacheDBData)) - } catch (e: any) { - log('addFileCache db error', e.stack.toString()) - } - } - - async getFileCache(fileNameOrUuid: string): Promise { - const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid - if (this.cache[key]) { - return this.cache[key] as FileCache - } - try { - const data = await this.db?.get(key) - return JSON.parse(data!) - } catch (e) { - // log("getFileCache db error", e.stack.toString()) - } - } - - async addGroupNotify(notify: GroupNotify) { - const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + notify.seq - let existNotify = this.cache[key] as GroupNotify - if (existNotify) { - return - } - this.cache[key] = notify - this.db?.put(key, JSON.stringify(notify)).then().catch() - } - - async getGroupNotify(seq: string): Promise { - const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + seq - if (this.cache[key]) { - return this.cache[key] as GroupNotify - } - try { - const data = await this.db?.get(key) - return JSON.parse(data!) - } catch (e) { - // log("getGroupNotify db error", e.stack.toString()) - } - } -} - -export const dbUtil = new DBUtil() diff --git a/src/common/utils/EventTask.ts b/src/common/utils/EventTask.ts index 28d9a8e..1099752 100644 --- a/src/common/utils/EventTask.ts +++ b/src/common/utils/EventTask.ts @@ -16,6 +16,7 @@ export interface ListenerIBase { new(listener: any): ListenerClassBase } +// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20 export class NTEventWrapper { private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数 private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession @@ -68,6 +69,8 @@ export class NTEventWrapper { } } + createEventFunction = this.CreatEventFunction + CreatListenerFunction(listenerMainName: string, uniqueCode: string = ''): T { const ListenerType = this.ListenerMap![listenerMainName] let Listener = this.ListenerManger.get(listenerMainName + uniqueCode) diff --git a/src/common/utils/MessageUnique.ts b/src/common/utils/MessageUnique.ts new file mode 100644 index 0000000..f300f14 --- /dev/null +++ b/src/common/utils/MessageUnique.ts @@ -0,0 +1,133 @@ +import { Peer } from '@/ntqqapi/types' +import { createHash } from 'node:crypto' +import { LimitedHashTable } from './table' +import { DATA_DIR } from './index' +import Database, { Tables } from 'minato' +import SQLite from '@minatojs/driver-sqlite' +import fsPromise from 'node:fs/promises' +import fs from 'node:fs' +import path from 'node:path' + +interface SQLiteTables extends Tables { + message: { + shortId: number + msgId: string + chatType: number + peerUid: string + } +} + +interface MsgIdAndPeerByShortId { + MsgId: string + Peer: Peer +} + +// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84 +class MessageUniqueWrapper { + private msgDataMap: LimitedHashTable + private msgIdMap: LimitedHashTable + private db: Database | undefined + + constructor(maxMap: number = 1000) { + this.msgIdMap = new LimitedHashTable(maxMap) + this.msgDataMap = new LimitedHashTable(maxMap) + } + + async init(uin: string) { + const dbDir = path.join(DATA_DIR, 'database') + if (!fs.existsSync(dbDir)) { + await fsPromise.mkdir(dbDir) + } + const database = new Database() + await database.connect(SQLite, { + path: path.join(dbDir, `${uin}.db`) + }) + database.extend('message', { + shortId: 'integer(10)', + chatType: 'unsigned', + msgId: 'string(24)', + peerUid: 'string(24)' + }, { + primary: 'shortId' + }) + this.db = database + } + + async getRecentMsgIds(Peer: Peer, size: number): Promise { + const heads = this.msgIdMap.getHeads(size) + if (!heads) { + return [] + } + const data: (MsgIdAndPeerByShortId | undefined)[] = [] + for (const t of heads) { + data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value)) + } + const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid) + return ret.map((t) => t?.MsgId).filter((t) => t !== undefined) + } + + createMsg(peer: Peer, msgId: string): number | undefined { + const key = `${msgId}|${peer.chatType}|${peer.peerUid}` + const hash = createHash('md5').update(key).digest() + //设置第一个bit为0 保证shortId为正数 + hash[0] &= 0x7f + const shortId = hash.readInt32BE(0) + //减少性能损耗 + // const isExist = this.msgIdMap.getKey(shortId) + // if (isExist && isExist === msgId) { + // return shortId + // } + this.msgIdMap.set(msgId, shortId) + this.msgDataMap.set(key, shortId) + this.db?.upsert('message', [{ + msgId, + shortId, + chatType: peer.chatType, + peerUid: peer.peerUid + }], 'shortId').then() + return shortId + } + + async getMsgIdAndPeerByShortId(shortId: number): Promise { + const data = this.msgDataMap.getKey(shortId) + if (data) { + const [msgId, chatTypeStr, peerUid] = data.split('|') + const peer: Peer = { + chatType: parseInt(chatTypeStr), + peerUid, + guildId: '', + } + return { MsgId: msgId, Peer: peer } + } + const items = await this.db?.get('message', { shortId }) + if (items?.length) { + const { msgId, chatType, peerUid } = items[0] + return { + MsgId: msgId, + Peer: { + chatType, + peerUid, + guildId: '', + } + } + } + return undefined + } + + getShortIdByMsgId(msgId: string): number | undefined { + return this.msgIdMap.getValue(msgId) + } + + async getPeerByMsgId(msgId: string) { + const shortId = this.msgIdMap.getValue(msgId) + if (!shortId) return undefined + return await this.getMsgIdAndPeerByShortId(shortId) + } + + resize(maxSize: number): void { + this.msgIdMap.resize(maxSize) + this.msgDataMap.resize(maxSize) + } +} + +export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper() \ No newline at end of file diff --git a/src/common/utils/file.ts b/src/common/utils/file.ts index 2e60cd9..7c349e9 100644 --- a/src/common/utils/file.ts +++ b/src/common/utils/file.ts @@ -2,7 +2,6 @@ import fs from 'node:fs' import fsPromise from 'node:fs/promises' import path from 'node:path' import { log, TEMP_DIR } from './index' -import { dbUtil } from '../db' import * as fileType from 'file-type' import { randomUUID, createHash } from 'node:crypto' @@ -187,13 +186,6 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr } else { filePath = pathname } - } else { - const cache = await dbUtil.getFileCache(uri) - if (cache) { - filePath = cache.filePath - } else { - filePath = uri - } } res.isLocal = true diff --git a/src/common/utils/helper.ts b/src/common/utils/helper.ts index 10388b8..d35d309 100644 --- a/src/common/utils/helper.ts +++ b/src/common/utils/helper.ts @@ -143,4 +143,27 @@ export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: } } return logExecutionTime +} + +// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14 +export class UUIDConverter { + static encode(highStr: string, lowStr: string): string { + const high = BigInt(highStr) + const low = BigInt(lowStr) + const highHex = high.toString(16).padStart(16, '0') + const lowHex = low.toString(16).padStart(16, '0') + const combinedHex = highHex + lowHex + const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring( + 12, + 16, + )}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}` + return uuid + } + + static decode(uuid: string): { high: string; low: string } { + const hex = uuid.replace(/-/g, '') + const high = BigInt('0x' + hex.substring(0, 16)) + const low = BigInt('0x' + hex.substring(16)) + return { high: high.toString(), low: low.toString() } + } } \ No newline at end of file diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index fd55490..22f1c3c 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,5 +1,4 @@ import path from 'node:path' -import fs from 'fs' export * from './file' export * from './helper' @@ -7,12 +6,9 @@ export * from './log' export * from './qqlevel' export * from './QQBasicInfo' export * from './upgrade' -export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data -export const TEMP_DIR = path.join(DATA_DIR, 'temp') -export const PLUGIN_DIR = global.LiteLoader.plugins['LLOneBot'].path.plugin -if (!fs.existsSync(TEMP_DIR)) { - fs.mkdirSync(TEMP_DIR, { recursive: true }) -} +export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data +export const TEMP_DIR: string = path.join(DATA_DIR, 'temp') +export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin export { getVideoInfo } from './video' export { checkFfmpeg } from './video' export { encodeSilk } from './audio' \ No newline at end of file diff --git a/src/common/utils/table.ts b/src/common/utils/table.ts index bdb9e02..bd2a6e0 100644 --- a/src/common/utils/table.ts +++ b/src/common/utils/table.ts @@ -1,3 +1,4 @@ +// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5 export class LimitedHashTable { private keyToValue: Map = new Map() private valueToKey: Map = new Map() diff --git a/src/main/main.ts b/src/main/main.ts index cf4e8aa..448095b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,6 +1,7 @@ // 运行在 Electron 主进程 下的插件入口 import { BrowserWindow, dialog, ipcMain } from 'electron' +import path from 'node:path' import fs from 'node:fs' import { Config } from '../common/types' import { @@ -13,7 +14,7 @@ import { CHANNEL_UPDATE, } from '../common/channels' import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' -import { DATA_DIR } from '../common/utils' +import { DATA_DIR, TEMP_DIR } from '../common/utils' import { getGroupMember, llonebotError, @@ -36,8 +37,7 @@ import { postOb11Event } from '../onebot11/server/post-ob11-event' import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket' import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest' import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest' -import path from 'node:path' -import { dbUtil } from '../common/db' +import { MessageUnique } from '../common/utils/MessageUnique' import { setConfig } from './setConfig' import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' @@ -48,6 +48,7 @@ import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/ import '../ntqqapi/wrapper' import { NTEventDispatch } from '../common/utils/EventTask' import { wrapperConstructor, getSession } from '../ntqqapi/wrapper' +import { Peer } from '../ntqqapi/types' let mainWindow: BrowserWindow | null = null @@ -153,9 +154,11 @@ function onLoad() { continue } // log("收到新消息", message.msgId, message.msgSeq) - // if (message.senderUin !== selfInfo.uin){ - message.msgShortId = await dbUtil.addMsg(message) - // } + const peer: Peer = { + chatType: message.chatType, + peerUid: message.peerUid + } + message.msgShortId = MessageUnique.createMsg(peer, message.msgId) OB11Constructor.message(message) .then((msg) => { @@ -210,29 +213,22 @@ function onLoad() { const recallMsgIds: string[] = [] // 避免重复上报 registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { for (const message of payload.msgList) { - log('message update', message.msgId, message) if (message.recallTime != '0') { if (recallMsgIds.includes(message.msgId)) { continue } recallMsgIds.push(message.msgId) - const oriMessage = await dbUtil.getMsgByLongId(message.msgId) - if (!oriMessage) { + const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId) + if (!oriMessageId) { continue } - oriMessage.recallTime = message.recallTime - dbUtil.updateMsg(oriMessage).then() - message.msgShortId = oriMessage.msgShortId - OB11Constructor.RecallEvent(message).then((recallEvent) => { + OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => { if (recallEvent) { - log('post recall event', recallEvent) + //log('post recall event', recallEvent) postOb11Event(recallEvent) } }) - // 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了 - continue } - dbUtil.updateMsg(message).then() } }) registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => { @@ -268,17 +264,11 @@ function onLoad() { for (const notify of notifies) { try { notify.time = Date.now() - // const notifyTime = parseInt(notify.seq) / 1000 - // log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`); - // if (notifyTime < startTime) { - // continue; - // } - let existNotify = await dbUtil.getGroupNotify(notify.seq) - if (existNotify) { + const notifyTime = parseInt(notify.seq) / 1000 + if (notifyTime < startTime) { continue } log('收到群通知', notify) - await dbUtil.addGroupNotify(notify) const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { log('有成员退出通知', notify) @@ -392,17 +382,22 @@ function onLoad() { log('llonebot pid', process.pid) const config = getConfigUtil().getConfig() if (!config.enableLLOB) { + llonebotError.otherError = 'LLOnebot 未启动' log('LLOneBot 开关设置为关闭,不启动LLOneBot') return } + if (!fs.existsSync(TEMP_DIR)) { + fs.mkdirSync(TEMP_DIR, { recursive: true }) + } llonebotError.otherError = '' startTime = Date.now() NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! }) - dbUtil.init(uin) + MessageUnique.init(uin) log('start activate group member info') - NTQQGroupApi.activateMemberInfoChange().then().catch(log) - NTQQGroupApi.activateMemberListChange().then().catch(log) + // 下面两个会导致CPU占用过高,QQ卡死 + // NTQQGroupApi.activateMemberInfoChange().then().catch(log) + // NTQQGroupApi.activateMemberListChange().then().catch(log) startReceiveHook().then() if (config.ob11.enableHttp) { @@ -421,7 +416,7 @@ function onLoad() { log('LLOneBot start') } - const init = async () => { + const intervalId = setInterval(() => { const current = getSelfInfo() if (!current.uin) { setSelfInfo({ @@ -430,15 +425,11 @@ function onLoad() { nick: current.uin, }) } - //log('self info', selfInfo, globalThis.authData) - if (current.uin) { + if (current.uin && getSession()) { + clearInterval(intervalId) start(current.uid, current.uin) } - else { - setTimeout(init, 1000) - } - } - init() + }, 600) } // 创建窗口时触发 diff --git a/src/ntqqapi/api/file.ts b/src/ntqqapi/api/file.ts index 6023958..80bf555 100644 --- a/src/ntqqapi/api/file.ts +++ b/src/ntqqapi/api/file.ts @@ -24,6 +24,7 @@ import { fileTypeFromFile } from 'file-type' import fsPromise from 'node:fs/promises' import { NTEventDispatch } from '@/common/utils/EventTask' import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' +import { NodeIKernelSearchService } from '@/ntqqapi/services' export class NTQQFileApi { static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise { @@ -203,6 +204,122 @@ export class NTQQFileApi { log('图片url获取失败', element) return '' } + + // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/core/src/apis/file.ts#L149 + static async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) { + let GroupData: any[] | undefined + let BuddyData: any[] | undefined + if (peer.chatType === ChatType.group) { + GroupData = + [{ + groupCode: peer.peerUid, + isConf: false, + hasModifyConfGroupFace: true, + hasModifyConfGroupName: true, + groupName: 'LLOneBot.Cached', + remark: 'LLOneBot.Cached', + }]; + } else if (peer.chatType === ChatType.friend) { + BuddyData = [{ + category_name: 'LLOneBot.Cached', + peerUid: peer.peerUid, + peerUin: peer.peerUid, + remark: 'LLOneBot.Cached', + }] + } else { + return undefined + } + + const session = getSession() + return session?.getSearchService().addSearchHistory({ + type: 4, + contactList: [], + id: -1, + groupInfos: [], + msgs: [], + fileInfos: [ + { + chatType: peer.chatType, + buddyChatInfo: BuddyData || [], + discussChatInfo: [], + groupChatInfo: GroupData || [], + dataLineChatInfo: [], + tmpChatInfo: [], + msgId: msgId, + msgSeq: msgSeq, + msgTime: Math.floor(Date.now() / 1000).toString(), + senderUid: senderUid, + senderNick: 'LLOneBot.Cached', + senderRemark: 'LLOneBot.Cached', + senderCard: 'LLOneBot.Cached', + elemId: elemId, + elemType: elemType, + fileSize: fileSize, + filePath: '', + fileName: fileName, + hits: [{ + start: 12, + end: 14, + }], + }, + ], + }) + } + + static async searchfile(keys: string[]) { + type EventType = NodeIKernelSearchService['searchFileWithKeywords'] + + interface OnListener { + searchId: string, + hasMore: boolean, + resultItems: { + chatType: ChatType, + buddyChatInfo: any[], + discussChatInfo: any[], + groupChatInfo: + { + groupCode: string, + isConf: boolean, + hasModifyConfGroupFace: boolean, + hasModifyConfGroupName: boolean, + groupName: string, + remark: string + }[], + dataLineChatInfo: any[], + tmpChatInfo: any[], + msgId: string, + msgSeq: string, + msgTime: string, + senderUid: string, + senderNick: string, + senderRemark: string, + senderCard: string, + elemId: string, + elemType: number, + fileSize: string, + filePath: string, + fileName: string, + hits: + { + start: number, + end: number + }[] + }[] + } + + const Event = NTEventDispatch.createEventFunction('NodeIKernelSearchService/searchFileWithKeywords') + let id = '' + const Listener = NTEventDispatch.RegisterListen<(params: OnListener) => void> + ( + 'NodeIKernelSearchListener/onSearchFileKeywordsResult', + 1, + 20000, + (params) => id !== '' && params.searchId == id, + ) + id = await Event!(keys, 12) + const [ret] = await Listener + return ret + } } export class NTQQFileCacheApi { diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts index de26ba1..c581e50 100644 --- a/src/ntqqapi/api/msg.ts +++ b/src/ntqqapi/api/msg.ts @@ -270,4 +270,24 @@ export class NTQQMsgApi { const session = getSession() return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)! } + + static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) { + const session = getSession() + const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { + chatInfo: peer, + filterMsgType: [], + filterSendersUid: [], + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息 + isIncludeCurrent: true, + pageLimit: count, + }) + return ret! + } + + static async getSingleMsg(peer: Peer, seq: string) { + const session = getSession() + return await session?.getMsgService().getSingleMsg(peer, seq)! + } } diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 781f04d..5b54f62 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,7 +1,16 @@ import type { BrowserWindow } from 'electron' import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQMsgApi } from './api/msg' -import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types' +import { + CategoryFriend, + ChatType, + FriendV2, + Group, + GroupMember, + GroupMemberRole, + RawMessage, + SimpleInfo, User, +} from './types' import { deleteGroup, friends, @@ -14,15 +23,15 @@ import { import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { postOb11Event } from '../onebot11/server/post-ob11-event' import { getConfigUtil, HOOK_LOG } from '@/common/config' -import fs from 'fs' -import { dbUtil } from '@/common/db' +import fs from 'node:fs' import { NTQQGroupApi } from './api/group' import { log } from '@/common/utils' +import { randomUUID } from 'node:crypto' +import { MessageUnique } from '../common/utils/MessageUnique' import { isNumeric, sleep } from '@/common/utils' import { OB11Constructor } from '../onebot11/constructor' import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent' import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent' -import { randomUUID } from 'node:crypto' export let hookApiCallbacks: Record void> = {} @@ -103,8 +112,8 @@ export function hookNTQQApiReceive(window: BrowserWindow) { if (hook.hookFunc.constructor.name === 'AsyncFunction') { ; (_ as Promise).then() } - } catch (e) { - log('hook error', e, receiveData.payload) + } catch (e: any) { + log('hook error', ntQQApiMethodName, e.stack.toString()) } }).then() } @@ -332,7 +341,7 @@ export async function startHook() { }) registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => { // updateType 3是群列表变动,2是群成员变动 - // log("群列表变动", payload.updateType, payload.groupList) + // log("群列表变动, store", payload.updateType, payload.groupList) if (payload.updateType != 2) { updateGroups(payload.groupList).then() } @@ -391,17 +400,32 @@ export async function startHook() { registerReceiveHook<{ data: CategoryFriend[] }>(ReceiveCmdS.FRIENDS, (payload) => { - for (const fData of payload.data) { - const _friends = fData.buddyList - for (let friend of _friends) { - NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() - let existFriend = friends.find((f) => f.uin == friend.uin) - if (!existFriend) { - friends.push(friend) - } - else { - Object.assign(existFriend, friend) + // log("onBuddyListChange", payload) + // let friendListV2: {userSimpleInfos: Map} = [] + type V2data = {userSimpleInfos: Map} + let friendList: User[] = []; + if ((payload as any).userSimpleInfos) { + // friendListV2 = payload as any + friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => { + return { + ...v.coreInfo, } + }) + } + else{ + for (const fData of payload.data) { + friendList.push(...fData.buddyList) + } + } + log('好友列表变动', friendList) + for (let friend of friendList) { + NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() + let existFriend = friends.find((f) => f.uin == friend.uin) + if (!existFriend) { + friends.push(friend) + } + else { + Object.assign(existFriend, friend) } } }) @@ -444,8 +468,12 @@ export async function startHook() { }) registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { - const message = msgRecord - dbUtil.addMsg(message).then() + const { msgId, chatType, peerUid } = msgRecord + const peer = { + chatType, + peerUid + } + MessageUnique.createMsg(peer, msgId) }) registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { @@ -508,4 +536,4 @@ export async function startHook() { log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) }) }) -} \ No newline at end of file +} diff --git a/src/ntqqapi/services/NodeIKernelSearchService.ts b/src/ntqqapi/services/NodeIKernelSearchService.ts new file mode 100644 index 0000000..17bb8a6 --- /dev/null +++ b/src/ntqqapi/services/NodeIKernelSearchService.ts @@ -0,0 +1,128 @@ +import { ChatType } from '../types' + +export interface NodeIKernelSearchService { + addKernelSearchListener(...args: any[]): unknown// needs 1 arguments + + removeKernelSearchListener(...args: any[]): unknown// needs 1 arguments + + searchStranger(...args: any[]): unknown// needs 3 arguments + + searchGroup(...args: any[]): unknown// needs 1 arguments + + searchLocalInfo(keywords: string, unknown: number/*4*/): unknown + + cancelSearchLocalInfo(...args: any[]): unknown// needs 3 arguments + + searchBuddyChatInfo(...args: any[]): unknown// needs 2 arguments + + searchMoreBuddyChatInfo(...args: any[]): unknown// needs 1 arguments + + cancelSearchBuddyChatInfo(...args: any[]): unknown// needs 3 arguments + + searchContact(...args: any[]): unknown// needs 2 arguments + + searchMoreContact(...args: any[]): unknown// needs 1 arguments + + cancelSearchContact(...args: any[]): unknown// needs 3 arguments + + searchGroupChatInfo(...args: any[]): unknown// needs 3 arguments + + resetSearchGroupChatInfoSortType(...args: any[]): unknown// needs 3 arguments + + resetSearchGroupChatInfoFilterMembers(...args: any[]): unknown// needs 3 arguments + + searchMoreGroupChatInfo(...args: any[]): unknown// needs 1 arguments + + cancelSearchGroupChatInfo(...args: any[]): unknown// needs 3 arguments + + searchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments + + searchMoreChatsWithKeywords(...args: any[]): unknown// needs 1 arguments + + cancelSearchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments + + searchChatMsgs(...args: any[]): unknown// needs 2 arguments + + searchMoreChatMsgs(...args: any[]): unknown// needs 1 arguments + + cancelSearchChatMsgs(...args: any[]): unknown// needs 3 arguments + + searchMsgWithKeywords(...args: any[]): unknown// needs 2 arguments + + searchMoreMsgWithKeywords(...args: any[]): unknown// needs 1 arguments + + cancelSearchMsgWithKeywords(...args: any[]): unknown// needs 3 arguments + + searchFileWithKeywords(keywords: string[], source: number): Promise// needs 2 arguments + + searchMoreFileWithKeywords(...args: any[]): unknown// needs 1 arguments + + cancelSearchFileWithKeywords(...args: any[]): unknown// needs 3 arguments + + searchAtMeChats(...args: any[]): unknown// needs 3 arguments + + searchMoreAtMeChats(...args: any[]): unknown// needs 1 arguments + + cancelSearchAtMeChats(...args: any[]): unknown// needs 3 arguments + + searchChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments + + searchMoreChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments + + cancelSearchChatAtMeMsgs(...args: any[]): unknown// needs 3 arguments + + addSearchHistory(param: { + type: number,//4 + contactList: [], + id: number,//-1 + groupInfos: [], + msgs: [], + fileInfos: [ + { + chatType: ChatType, + buddyChatInfo: Array<{ category_name: string, peerUid: string, peerUin: string, remark: string }>, + discussChatInfo: [], + groupChatInfo: Array< + { + groupCode: string, + isConf: boolean, + hasModifyConfGroupFace: boolean, + hasModifyConfGroupName: boolean, + groupName: string, + remark: string + }>, + dataLineChatInfo: [], + tmpChatInfo: [], + msgId: string, + msgSeq: string, + msgTime: string, + senderUid: string, + senderNick: string, + senderRemark: string, + senderCard: string, + elemId: string, + elemType: string,//3 + fileSize: string, + filePath: string, + fileName: string, + hits: Array< + { + start: 12, + end: 14 + } + > + } + ] + + }): Promise<{ + result: number, + errMsg: string, + id?: number + }> + + removeSearchHistory(...args: any[]): unknown// needs 1 arguments + + searchCache(...args: any[]): unknown// needs 3 arguments + + clearSearchCache(...args: any[]): unknown// needs 1 arguments +} \ No newline at end of file diff --git a/src/ntqqapi/services/index.ts b/src/ntqqapi/services/index.ts index 8d518da..e7997a3 100644 --- a/src/ntqqapi/services/index.ts +++ b/src/ntqqapi/services/index.ts @@ -7,4 +7,5 @@ export * from './NodeIKernelMSFService' export * from './NodeIKernelUixConvertService' export * from './NodeIKernelRichMediaService' export * from './NodeIKernelTicketService' -export * from './NodeIKernelTipOffService' \ No newline at end of file +export * from './NodeIKernelTipOffService' +export * from './NodeIKernelSearchService' \ No newline at end of file diff --git a/src/ntqqapi/types/user.ts b/src/ntqqapi/types/user.ts index e109794..d687734 100644 --- a/src/ntqqapi/types/user.ts +++ b/src/ntqqapi/types/user.ts @@ -340,4 +340,4 @@ export interface UserDetailInfoByUin { pendantId: string vipNameColorId: string } -} \ No newline at end of file +} diff --git a/src/ntqqapi/wrapper.ts b/src/ntqqapi/wrapper.ts index a96fcaa..05d2bc2 100644 --- a/src/ntqqapi/wrapper.ts +++ b/src/ntqqapi/wrapper.ts @@ -8,7 +8,8 @@ import { NodeIKernelUixConvertService, NodeIKernelRichMediaService, NodeIKernelTicketService, - NodeIKernelTipOffService + NodeIKernelTipOffService, + NodeIKernelSearchService } from './services' import os from 'node:os' const Process = require('node:process') @@ -25,6 +26,7 @@ export interface NodeIQQNTWrapperSession { getRichMediaService(): NodeIKernelRichMediaService getTicketService(): NodeIKernelTicketService getTipOffService(): NodeIKernelTipOffService + getSearchService(): NodeIKernelSearchService } export interface WrapperApi { diff --git a/src/onebot11/action/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index f59d7c8..d741bfc 100644 --- a/src/onebot11/action/file/GetFile.ts +++ b/src/onebot11/action/file/GetFile.ts @@ -1,12 +1,11 @@ import BaseAction from '../BaseAction' -import fs from 'fs/promises' -import { dbUtil } from '@/common/db' +import fsPromise from 'node:fs/promises' import { getConfigUtil } from '@/common/config' -import { checkFileReceived, log, sleep, uri2local } from '@/common/utils' -import { NTQQFileApi } from '@/ntqqapi/api' +import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api' import { ActionName } from '../types' -import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types' -import { FileCache } from '@/common/types' +import { RawMessage } from '@/ntqqapi/types' +import { UUIDConverter } from '@/common/utils/helper' +import { Peer, ChatType, ElementType } from '@/ntqqapi/types' export interface GetFilePayload { file: string // 文件名或者fileUuid @@ -21,79 +20,105 @@ export interface GetFileResponse { } export abstract class GetFileBase extends BaseAction { - private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement { - let element = msg.elements.find((e) => e.elementId === elementId) - if (!element) { - throw new Error('element not found') - } - return element.fileElement - } - private async download(cache: FileCache, file: string) { - log('需要调用 NTQQ 下载文件api') - if (cache.msgId) { - let msg = await dbUtil.getMsgByLongId(cache.msgId) - if (msg) { - log('找到了文件 msg', msg) - let element = this.getElement(msg, cache.elementId) - log('找到了文件 element', element) - // 构建下载函数 - await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '') - // 等待文件下载完成 - msg = await dbUtil.getMsgByLongId(cache.msgId) - log('下载完成后的msg', msg) - cache.filePath = this.getElement(msg!, cache.elementId).filePath - await checkFileReceived(cache.filePath, 10 * 1000) - dbUtil.addFileCache(file, cache).then() - } - } - } + // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44 protected async _handle(payload: GetFilePayload): Promise { - let cache = await dbUtil.getFileCache(payload.file) - if (!cache) { - throw new Error('file not found') - } - const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig() - if (cache.downloadFunc) { - await cache.downloadFunc() - } + const { enableLocalFile2Url } = getConfigUtil().getConfig() + let UuidData: { + high: string + low: string + } | undefined try { - await fs.access(cache.filePath, fs.constants.F_OK) - } catch (e) { - // log("file not found", e) - if (cache.url) { - const downloadResult = await uri2local(cache.url) - if (downloadResult.success) { - cache.filePath = downloadResult.path - dbUtil.addFileCache(payload.file, cache).then() - } else { - await this.download(cache, payload.file) + UuidData = UUIDConverter.decode(payload.file) + if (UuidData) { + const peerUin = UuidData.high + const msgId = UuidData.low + const isGroup: boolean = !!(await NTQQGroupApi.getGroups(false)).find(e => e.groupCode == peerUin) + let peer: Peer | undefined + //识别Peer + if (isGroup) { + peer = { chatType: ChatType.group, peerUid: peerUin } } - } else { - // 没有url的可能是私聊文件或者群文件,需要自己下载 - await this.download(cache, payload.file) + const PeerUid = await NTQQUserApi.getUidByUinV2(peerUin) + if (PeerUid) { + const isBuddy = await NTQQFriendApi.isBuddy(PeerUid) + if (isBuddy) { + peer = { chatType: ChatType.friend, peerUid: PeerUid } + } else { + peer = { chatType: ChatType.temp, peerUid: PeerUid } + } + } + if (!peer) { + throw new Error('chattype not support') + } + const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId]) + if (msgList.msgList.length == 0) { + throw new Error('msg not found') + } + const msg = msgList.msgList[0]; + const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT) + if (!findEle) { + throw new Error('element not found') + } + const downloadPath = await NTQQFileApi.downloadMedia(msgId, msg.chatType, msg.peerUid, findEle.elementId, '', '') + const fileSize = findEle?.videoElement?.fileSize || findEle?.fileElement?.fileSize || findEle?.pttElement?.fileSize || '0' + const fileName = findEle?.videoElement?.fileName || findEle?.fileElement?.fileName || findEle?.pttElement?.fileName || '' + const res: GetFileResponse = { + file: downloadPath, + url: downloadPath, + file_size: fileSize, + file_name: fileName, + } + if (enableLocalFile2Url) { + try { + res.base64 = await fsPromise.readFile(downloadPath, 'base64') + } catch (e) { + throw new Error('文件下载失败. ' + e) + } + } + //不手动删除?文件持久化了 + return res } + } catch { + } - let res: GetFileResponse = { - file: cache.filePath, - url: cache.url, - file_size: cache.fileSize, - file_name: cache.fileName, - } - if (enableLocalFile2Url) { - if (!cache.url) { + + const NTSearchNameResult = (await NTQQFileApi.searchfile([payload.file])).resultItems + if (NTSearchNameResult.length !== 0) { + const MsgId = NTSearchNameResult[0].msgId + let peer: Peer | undefined = undefined + if (NTSearchNameResult[0].chatType == ChatType.group) { + peer = { chatType: ChatType.group, peerUid: NTSearchNameResult[0].groupChatInfo[0].groupCode } + } + if (!peer) { + throw new Error('chattype not support') + } + const msgList: RawMessage[] = (await NTQQMsgApi.getMsgsByMsgId(peer, [MsgId]))?.msgList + if (!msgList || msgList.length == 0) { + throw new Error('msg not found') + } + const msg = msgList[0] + const file = msg.elements.filter(e => e.elementType == NTSearchNameResult[0].elemType) + if (file.length == 0) { + throw new Error('file not found') + } + const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, file[0].elementId, '', '') + const res: GetFileResponse = { + file: downloadPath, + url: downloadPath, + file_size: NTSearchNameResult[0].fileSize.toString(), + file_name: NTSearchNameResult[0].fileName, + } + if (enableLocalFile2Url) { try { - res.base64 = await fs.readFile(cache.filePath, 'base64') + res.base64 = await fsPromise.readFile(downloadPath, 'base64') } catch (e) { throw new Error('文件下载失败. ' + e) } } + //不手动删除?文件持久化了 + return res } - // if (autoDeleteFile) { - // setTimeout(() => { - // fs.unlink(cache.filePath) - // }, autoDeleteFileSecond * 1000) - // } - return res + throw new Error('file not found') } } diff --git a/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts b/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts index 2bc5c3b..561c032 100644 --- a/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts +++ b/src/onebot11/action/go-cqhttp/DelEssenceMsg.ts @@ -1,24 +1,27 @@ import BaseAction from '../BaseAction'; import { ActionName } from '../types'; -import { NTQQGroupApi } from '../../../ntqqapi/api/group' -import { dbUtil } from '@/common/db'; +import { NTQQGroupApi } from '@/ntqqapi/api/group' +import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { - message_id: number | string; + message_id: number | string } export default class GoCQHTTPDelEssenceMsg extends BaseAction { actionName = ActionName.GoCQHTTP_DelEssenceMsg; protected async _handle(payload: Payload): Promise { - const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); + if (!payload.message_id) { + throw Error('message_id不能为空') + } + const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) if (!msg) { - throw new Error('msg not found'); + throw new Error('msg not found') } return await NTQQGroupApi.removeGroupEssence( - msg.peerUid, - msg.msgId - ); + msg.Peer.peerUid, + msg.MsgId, + ) } } diff --git a/src/onebot11/action/go-cqhttp/DownloadFile.ts b/src/onebot11/action/go-cqhttp/DownloadFile.ts index fd43ef8..611b60f 100644 --- a/src/onebot11/action/go-cqhttp/DownloadFile.ts +++ b/src/onebot11/action/go-cqhttp/DownloadFile.ts @@ -1,8 +1,9 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' import fs from 'fs' -import { join as joinPath } from 'node:path' -import { calculateFileMD5, httpDownload, TEMP_DIR } from '../../../common/utils' +import fsPromise from 'fs/promises' +import path from 'node:path' +import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils' import { randomUUID } from 'node:crypto' interface Payload { @@ -22,15 +23,15 @@ export default class GoCQHTTPDownloadFile extends BaseAction { const isRandomName = !payload.name - let name = payload.name || randomUUID() - const filePath = joinPath(TEMP_DIR, name) + const name = payload.name ? path.basename(payload.name) : randomUUID() + const filePath = path.join(TEMP_DIR, name) if (payload.base64) { - fs.writeFileSync(filePath, payload.base64, 'base64') + await fsPromise.writeFile(filePath, payload.base64, 'base64') } else if (payload.url) { const headers = this.getHeaders(payload.headers) - let buffer = await httpDownload({ url: payload.url, headers: headers }) - fs.writeFileSync(filePath, Buffer.from(buffer), 'binary') + const buffer = await httpDownload({ url: payload.url, headers: headers }) + await fsPromise.writeFile(filePath, buffer) } else { throw new Error('不存在任何文件, 无法下载') } @@ -38,8 +39,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction { +export class GoCQHTTGetForwardMsgAction extends BaseAction { actionName = ActionName.GoCQHTTP_GetForwardMsg protected async _handle(payload: Payload): Promise { - const message_id = payload.id || payload.message_id - if (!message_id) { + const msgId = payload.id || payload.message_id + if (!msgId) { throw Error('message_id不能为空') } - const rootMsg = await dbUtil.getMsgByLongId(message_id) + const rootMsgId = MessageUnique.getShortIdByMsgId(msgId) + const rootMsg = await MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || +msgId) if (!rootMsg) { throw Error('msg not found') } - let data = await NTQQMsgApi.getMultiMsg( - { chatType: rootMsg.chatType, peerUid: rootMsg.peerUid }, - rootMsg.msgId, - rootMsg.msgId, - ) - if (data.result !== 0) { - throw Error('找不到相关的聊天记录' + data.errMsg) + const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId) + if (data?.result !== 0) { + throw Error('找不到相关的聊天记录' + data?.errMsg) } - let msgList = data.msgList - let messages = await Promise.all( + const msgList = data.msgList + const messages = await Promise.all( msgList.map(async (msg) => { - let resMsg = await OB11Constructor.message(msg) - resMsg.message_id = (await dbUtil.addMsg(msg))! + const resMsg = await OB11Constructor.message(msg) + resMsg.message_id = MessageUnique.createMsg({ + chatType: msg.chatType, + peerUid: msg.peerUid, + }, msg.msgId)! return resMsg }), ) diff --git a/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts b/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts index dc0b469..3e7efc3 100644 --- a/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts +++ b/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts @@ -1,16 +1,17 @@ import BaseAction from '../BaseAction' -import { OB11Message, OB11User } from '../../types' -import { groups } from '../../../common/data' +import { OB11Message } from '../../types' import { ActionName } from '../types' -import { ChatType } from '../../../ntqqapi/types' -import { dbUtil } from '../../../common/db' -import { NTQQMsgApi } from '../../../ntqqapi/api/msg' +import { ChatType } from '@/ntqqapi/types' +import { NTQQMsgApi } from '@/ntqqapi/api/msg' import { OB11Constructor } from '../../constructor' +import { RawMessage } from '@/ntqqapi/types' +import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { - group_id: number - message_seq: number - count: number + group_id: number | string + message_seq?: number + count?: number + reverseOrder?: boolean } interface Response { @@ -21,23 +22,23 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction { - const group = groups.find((group) => group.groupCode === payload.group_id.toString()) - if (!group) { - throw `群${payload.group_id}不存在` + const count = payload.count || 20 + const isReverseOrder = payload.reverseOrder || true + const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() } + let msgList: RawMessage[] + // 包含 message_seq 0 + if (!payload.message_seq) { + msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count)).msgList + } else { + const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId + if (!startMsgId) throw `消息${payload.message_seq}不存在` + msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList } - const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0' - // log("startMsgId", startMsgId) - let msgList = ( - await NTQQMsgApi.getMsgHistory( - { chatType: ChatType.group, peerUid: group.groupCode }, - startMsgId, - parseInt(payload.count?.toString()) || 20, - ) - ).msgList + if (isReverseOrder) msgList.reverse() await Promise.all( - msgList.map(async (msg) => { - msg.msgShortId = await dbUtil.addMsg(msg) - }), + msgList.map(async msg => { + msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId) + }) ) const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg))) return { messages: ob11MsgList } diff --git a/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts b/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts index 682eddd..56eef28 100644 --- a/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts +++ b/src/onebot11/action/go-cqhttp/SetEssenceMsg.ts @@ -1,23 +1,26 @@ -import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { NTQQGroupApi } from '../../../ntqqapi/api/group' -import { dbUtil } from '@/common/db'; +import BaseAction from '../BaseAction' +import { ActionName } from '../types' +import { NTQQGroupApi } from '@/ntqqapi/api/group' +import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { - message_id: number | string; + message_id: number | string } export default class GoCQHTTPSetEssenceMsg extends BaseAction { actionName = ActionName.GoCQHTTP_SetEssenceMsg; protected async _handle(payload: Payload): Promise { - const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); + if (!payload.message_id) { + throw Error('message_id不能为空') + } + const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) if (!msg) { - throw new Error('msg not found'); + throw new Error('msg not found') } return await NTQQGroupApi.addGroupEssence( - msg.peerUid, - msg.msgId - ); + msg.Peer.peerUid, + msg.MsgId + ) } } diff --git a/src/onebot11/action/msg/DeleteMsg.ts b/src/onebot11/action/msg/DeleteMsg.ts index d1bc1c8..831c5dc 100644 --- a/src/onebot11/action/msg/DeleteMsg.ts +++ b/src/onebot11/action/msg/DeleteMsg.ts @@ -1,27 +1,24 @@ import { ActionName } from '../types' import BaseAction from '../BaseAction' -import { dbUtil } from '../../../common/db' -import { NTQQMsgApi } from '../../../ntqqapi/api/msg' +import { NTQQMsgApi } from '@/ntqqapi/api/msg' +import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { - message_id: number + message_id: number | string } class DeleteMsg extends BaseAction { actionName = ActionName.DeleteMsg protected async _handle(payload: Payload) { - let msg = await dbUtil.getMsgByShortId(payload.message_id) + if (!payload.message_id) { + throw Error('message_id不能为空') + } + const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) if (!msg) { throw `消息${payload.message_id}不存在` } - await NTQQMsgApi.recallMsg( - { - chatType: msg.chatType, - peerUid: msg.peerUid, - }, - [msg.msgId], - ) + await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId]) } } diff --git a/src/onebot11/action/msg/ForwardSingleMsg.ts b/src/onebot11/action/msg/ForwardSingleMsg.ts index 1c37f47..42a4f23 100644 --- a/src/onebot11/action/msg/ForwardSingleMsg.ts +++ b/src/onebot11/action/msg/ForwardSingleMsg.ts @@ -1,12 +1,12 @@ import BaseAction from '../BaseAction' import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api' import { ChatType } from '@/ntqqapi/types' -import { dbUtil } from '@/common/db' import { ActionName } from '../types' import { Peer } from '@/ntqqapi/types' +import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { - message_id: number + message_id: number | string group_id: number | string user_id?: number | string } @@ -24,19 +24,15 @@ abstract class ForwardSingleMsg extends BaseAction { } protected async _handle(payload: Payload): Promise { - const msg = await dbUtil.getMsgByShortId(payload.message_id) + if (!payload.message_id) { + throw Error('message_id不能为空') + } + const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) if (!msg) { throw new Error(`无法找到消息${payload.message_id}`) } const peer = await this.getTargetPeer(payload) - const ret = await NTQQMsgApi.forwardMsg( - { - chatType: msg.chatType, - peerUid: msg.peerUid, - }, - peer, - [msg.msgId], - ) + const ret = await NTQQMsgApi.forwardMsg(msg.Peer, peer, [msg.MsgId]) if (ret.result !== 0) { throw new Error(`转发消息失败 ${ret.errMsg}`) } diff --git a/src/onebot11/action/msg/GetMsg.ts b/src/onebot11/action/msg/GetMsg.ts index 543ef05..5f53294 100644 --- a/src/onebot11/action/msg/GetMsg.ts +++ b/src/onebot11/action/msg/GetMsg.ts @@ -2,10 +2,11 @@ import { OB11Message } from '../../types' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { dbUtil } from '../../../common/db' +import { NTQQMsgApi } from '@/ntqqapi/api' +import { MessageUnique } from '@/common/utils/MessageUnique' export interface PayloadType { - message_id: number + message_id: number | string } export type ReturnDataType = OB11Message @@ -18,14 +19,25 @@ class GetMsg extends BaseAction { if (!payload.message_id) { throw '参数message_id不能为空' } - let msg = await dbUtil.getMsgByShortId(payload.message_id) - if (!msg) { - msg = await dbUtil.getMsgByLongId(payload.message_id.toString()) + const msgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString()) + const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(msgShortId || +payload.message_id) + if (!msgIdWithPeer) { + throw ('消息不存在') } - if (!msg) { - throw '消息不存在' + const peer = { + guildId: '', + peerUid: msgIdWithPeer.Peer.peerUid, + chatType: msgIdWithPeer.Peer.chatType } - return await OB11Constructor.message(msg) + const msg = await NTQQMsgApi.getMsgsByMsgId( + peer, + [msgIdWithPeer?.MsgId || payload.message_id.toString()] + ) + const retMsg = await OB11Constructor.message(msg.msgList[0]) + retMsg.message_id = MessageUnique.createMsg(peer, msg.msgList[0].msgId)! + retMsg.message_seq = retMsg.message_id + retMsg.real_id = retMsg.message_id + return retMsg } } diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index d0c1c5c..cae7707 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -3,35 +3,35 @@ import { ChatType, ElementType, GroupMemberRole, - PicSubType, RawMessage, SendMessageElement, -} from '../../../ntqqapi/types' -import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '../../../common/data' +} from '@/ntqqapi/types' +import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '@/common/data' import { OB11MessageCustomMusic, OB11MessageData, OB11MessageDataType, - OB11MessageFile, OB11MessageJson, OB11MessageMixType, OB11MessageMusic, OB11MessageNode, OB11PostSendMsg, } from '../../types' -import { SendMsgElementConstructor } from '../../../ntqqapi/constructor' +import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import BaseAction from '../BaseAction' import { ActionName, BaseCheckResult } from '../types' import fs from 'node:fs' +import fsPromise from 'node:fs/promises' import { decodeCQCode } from '../../cqcode' -import { dbUtil } from '../../../common/db' -import { getConfigUtil } from '../../../common/config' -import { log } from '../../../common/utils/log' -import { sleep } from '../../../common/utils/helper' -import { uri2local } from '../../../common/utils' +import { getConfigUtil } from '@/common/config' +import { log } from '@/common/utils/log' +import { sleep } from '@/common/utils/helper' +import { uri2local } from '@/common/utils' import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign' import { Peer } from '@/ntqqapi/types/msg' +import { MessageUnique } from '@/common/utils/MessageUnique' +import { OB11MessageFileBase } from '../../types' export interface ReturnDataType { message_id: number @@ -43,6 +43,11 @@ export enum ContextMode { Group = 2 } +interface MessageContext { + deleteAfterSentFiles: string[] + peer: Peer +} + export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) { if (typeof message === 'string') { if (autoEscape === true) { @@ -65,6 +70,32 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa return message } +// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26 +async function handleOb11FileLikeMessage( + { data: inputdata }: OB11MessageFileBase, + { deleteAfterSentFiles }: Pick, +) { + //有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa + const { + path, + isLocal, + fileName, + errMsg, + success, + } = (await uri2local(inputdata?.url || inputdata.file)) + + if (!success) { + log('文件下载失败', errMsg) + throw Error('文件下载失败' + errMsg) + } + + if (!isLocal) { // 只删除http和base64转过来的文件 + deleteAfterSentFiles.push(path) + } + + return { path, fileName: inputdata.name || fileName } +} + export async function createSendElements( messageData: OB11MessageData[], peer: Peer, @@ -130,9 +161,16 @@ export async function createSendElements( } break case OB11MessageDataType.reply: { - let replyMsgId = sendMsg.data.id - if (replyMsgId) { - const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId)) + if (sendMsg.data.id) { + const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id) + if (!replyMsgId) { + log('回复消息不存在', replyMsgId) + continue + } + const replyMsg = (await NTQQMsgApi.getMsgsByMsgId( + replyMsgId.Peer, + [replyMsgId.MsgId!] + )).msgList[0] if (replyMsg) { sendElements.push( SendMsgElementConstructor.reply( @@ -164,66 +202,36 @@ export async function createSendElements( ) } break - case OB11MessageDataType.image: - case OB11MessageDataType.file: - case OB11MessageDataType.video: - case OB11MessageDataType.voice: { - const data = (sendMsg as OB11MessageFile).data - let file = data.file - const payloadFileName = data?.name - if (file) { - const cache = await dbUtil.getFileCache(file) - if (cache) { - if (fs.existsSync(cache.filePath)) { - file = 'file://' + cache.filePath - } - else if (cache.downloadFunc) { - await cache.downloadFunc() - file = cache.filePath - } - else if (cache.url) { - file = cache.url - } - log('找到文件缓存', file) - } - const { path, isLocal, fileName, errMsg } = await uri2local(file) - if (errMsg) { - throw errMsg - } - if (path) { - if (!isLocal) { - // 只删除http和base64转过来的文件 - deleteAfterSentFiles.push(path) - } - if (sendMsg.type === OB11MessageDataType.file) { - log('发送文件', path, payloadFileName || fileName) - sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName)) - } - else if (sendMsg.type === OB11MessageDataType.video) { - log('发送视频', path, payloadFileName || fileName) - let thumb = sendMsg.data?.thumb - if (thumb) { - let uri2LocalRes = await uri2local(thumb) - if (uri2LocalRes.success) { - thumb = uri2LocalRes.path - } - } - sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb)) - } - else if (sendMsg.type === OB11MessageDataType.voice) { - sendElements.push(await SendMsgElementConstructor.ptt(path)) - } - else if (sendMsg.type === OB11MessageDataType.image) { - sendElements.push( - await SendMsgElementConstructor.pic( - path, - sendMsg.data.summary || '', - parseInt(sendMsg.data?.subType?.toString()!) || 0, - ), - ) - } - } + case OB11MessageDataType.image: { + const res = await SendMsgElementConstructor.pic( + (await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })).path, + sendMsg.data.summary || '', + sendMsg.data.subType || 0 + ) + deleteAfterSentFiles.push(res.picElement.sourcePath) + sendElements.push(res) + } + break + case OB11MessageDataType.file: { + const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) + sendElements.push(await SendMsgElementConstructor.file(path, fileName)) + } + break + case OB11MessageDataType.video: { + const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) + let thumb = sendMsg.data.thumb + if (thumb) { + const uri2LocalRes = await uri2local(thumb) + if (uri2LocalRes.success) thumb = uri2LocalRes.path } + const res = await SendMsgElementConstructor.video(path, fileName, thumb) + deleteAfterSentFiles.push(res.videoElement.filePath) + sendElements.push(res) + } + break + case OB11MessageDataType.voice: { + const { path } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles }) + sendElements.push(await SendMsgElementConstructor.ptt(path)) } break case OB11MessageDataType.json: { @@ -287,9 +295,8 @@ export async function sendMsg( log('设置消息超时时间', timeout) const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) log('消息发送结果', returnMsg) - returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) - deleteAfterSentFiles.map((f) => fs.unlink(f, () => { - })) + returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) + deleteAfterSentFiles.map(path => fsPromise.unlink(path)) return returnMsg } @@ -428,8 +435,6 @@ export class SendMsg extends BaseAction { } } const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles) - deleteAfterSentFiles.map((f) => fs.unlink(f, () => { - })) return { message_id: returnMsg.msgShortId! } } @@ -462,7 +467,7 @@ export class SendMsg extends BaseAction { sendElements, true, ) - await sleep(500) + await sleep(400) return nodeMsg } catch (e) { log(e, '克隆转发消息失败,将忽略本条消息', msg) @@ -477,25 +482,17 @@ export class SendMsg extends BaseAction { } let nodeMsgIds: string[] = [] // 先判断一遍是不是id和自定义混用 - let needClone = - messageNodes.filter((node) => node.data.id).length && messageNodes.filter((node) => !node.data.id).length for (const messageNode of messageNodes) { // 一个node表示一个人的消息 let nodeId = messageNode.data.id // 有nodeId表示一个子转发消息卡片 if (nodeId) { - let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId)) - if (!needClone) { - nodeMsgIds.push(nodeMsg?.msgId!) - } - else { - if (nodeMsg?.peerUid !== selfPeer.peerUid) { - const cloneMsg = await this.cloneMsg(nodeMsg!) - if (cloneMsg) { - nodeMsgIds.push(cloneMsg.msgId) - } - } + const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId) + if (!nodeMsg) { + log('转发消息失败,未找到消息', nodeId) + continue } + nodeMsgIds.push(nodeMsg.MsgId) } else { // 自定义的消息 @@ -529,7 +526,7 @@ export class SendMsg extends BaseAction { for (const eles of sendElementsSplit) { const nodeMsg = await sendMsg(selfPeer, eles, [], true) nodeMsgIds.push(nodeMsg.msgId) - await sleep(500) + await sleep(400) log('转发节点生成成功', nodeMsg.msgId) } deleteAfterSentFiles.map((f) => fs.unlink(f, () => { @@ -541,33 +538,25 @@ export class SendMsg extends BaseAction { } // 检查srcPeer是否一致,不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的,使其保持一致才能够转发 - let nodeMsgArray: Array = [] + const nodeMsgArray: RawMessage[] = [] let srcPeer: Peer | null = null let needSendSelf = false - for (const [index, msgId] of nodeMsgIds.entries()) { - const nodeMsg = await dbUtil.getMsgByLongId(msgId) - if (nodeMsg) { - nodeMsgArray.push(nodeMsg) - if (!srcPeer) { - srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid } - } - else if (srcPeer.peerUid !== nodeMsg.peerUid) { + for (const msgId of nodeMsgIds) { + const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId) + if (nodeMsgPeer) { + const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0] + srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid } + if (srcPeer.peerUid !== nodeMsg.peerUid) { needSendSelf = true - srcPeer = selfPeer } + nodeMsgArray.push(nodeMsg) } } - log('nodeMsgArray', nodeMsgArray) nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId) if (needSendSelf) { - log('需要克隆转发消息') - for (const [index, msg] of nodeMsgArray.entries()) { - if (msg.peerUid !== selfPeer.peerUid) { - const cloneMsg = await this.cloneMsg(msg) - if (cloneMsg) { - nodeMsgIds[index] = cloneMsg.msgId - } - } + for (const msg of nodeMsgArray) { + if (msg.peerUid === selfPeer.peerUid) continue + await this.cloneMsg(msg) } } // elements之间用换行符分隔 @@ -584,7 +573,7 @@ export class SendMsg extends BaseAction { throw Error('转发消息失败,节点为空') } const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) - returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) + returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId) return returnMsg } } diff --git a/src/onebot11/action/msg/SetMsgEmojiLike.ts b/src/onebot11/action/msg/SetMsgEmojiLike.ts index eaa5ffb..0cc7436 100644 --- a/src/onebot11/action/msg/SetMsgEmojiLike.ts +++ b/src/onebot11/action/msg/SetMsgEmojiLike.ts @@ -1,32 +1,36 @@ import { ActionName } from '../types' import BaseAction from '../BaseAction' -import { dbUtil } from '../../../common/db' -import { NTQQMsgApi } from '../../../ntqqapi/api/msg' +import { NTQQMsgApi } from '@/ntqqapi/api/msg' +import { MessageUnique } from '@/common/utils/MessageUnique' interface Payload { - message_id: number - emoji_id: string + message_id: number | string + emoji_id: number | string } export class SetMsgEmojiLike extends BaseAction { actionName = ActionName.SetMsgEmojiLike protected async _handle(payload: Payload) { - let msg = await dbUtil.getMsgByShortId(payload.message_id) + if (!payload.message_id) { + throw Error('message_id不能为空') + } + const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id) if (!msg) { throw new Error('msg not found') } if (!payload.emoji_id) { throw new Error('emojiId not found') } + const msgData = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList + if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) { + throw new Error('find msg by msgid error') + } return await NTQQMsgApi.setEmojiLike( - { - chatType: msg.chatType, - peerUid: msg.peerUid, - }, - msg.msgSeq, - payload.emoji_id, - true, + msg.Peer, + msgData[0].msgSeq, + payload.emoji_id.toString(), + true ) } } diff --git a/src/onebot11/action/quick-operation.ts b/src/onebot11/action/quick-operation.ts index ad6883b..0000441 100644 --- a/src/onebot11/action/quick-operation.ts +++ b/src/onebot11/action/quick-operation.ts @@ -4,12 +4,12 @@ import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' -import { dbUtil } from '@/common/db' import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api' import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types' import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg' import { isNull, log } from '@/common/utils' import { getConfigUtil } from '@/common/config' +import { MessageUnique } from '@/common/utils/MessageUnique' interface QuickOperationPrivateMessage { @@ -61,7 +61,6 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc } async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { - const rawMessage = await dbUtil.getMsgByShortId(msg.message_id) const reply = quickAction.reply const ob11Config = getConfigUtil().getConfig().ob11 const peer: Peer = { @@ -105,17 +104,21 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes } if (msg.message_type === 'group') { const groupMsgQuickAction = quickAction as QuickOperationGroupMessage + const rawMessage = await MessageUnique.getMsgIdAndPeerByShortId(+(msg.message_id ?? 0)) + if (!rawMessage) return // handle group msg if (groupMsgQuickAction.delete) { - NTQQMsgApi.recallMsg(peer, [rawMessage?.msgId!]).then().catch(log) + NTQQMsgApi.recallMsg(peer, [rawMessage.MsgId]).then().catch(log) } if (groupMsgQuickAction.kick) { - NTQQGroupApi.kickMember(peer.peerUid, [rawMessage?.senderUid!]).then().catch(log) + const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) + NTQQGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).then().catch(log) } if (groupMsgQuickAction.ban) { + const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId]) NTQQGroupApi.banMember(peer.peerUid, [ { - uid: rawMessage?.senderUid!, + uid: msgList[0].senderUid, timeStamp: groupMsgQuickAction.ban_duration || 60 * 30, }, ]).then().catch(log) diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index 9566fec..1f605f7 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -23,14 +23,14 @@ import { Sex, TipGroupElementType, User, - VideoElement, FriendV2, ChatType2 } from '../ntqqapi/types' import { deleteGroup, getGroupMember, getSelfUin } from '../common/data' import { EventType } from './event/OB11BaseEvent' import { encodeCQCode } from './cqcode' -import { dbUtil } from '../common/db' +import { MessageUnique } from '../common/utils/MessageUnique' +import { UUIDConverter } from '../common/utils/helper' import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent' import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' @@ -152,55 +152,47 @@ export class OB11Constructor { } else if (element.replyElement) { message_data['type'] = OB11MessageDataType.reply - // log("收到回复消息", element.replyElement.replayMsgSeq) try { - const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq) - // log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId) - if (replyMsg) { - message_data['data']['id'] = replyMsg.msgShortId?.toString() + const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords) + if (!records) throw new Error('找不到回复消息') + let replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({ + peerUid: msg.peerUid, + guildId: '', + chatType: msg.chatType, + }, element.replyElement.replayMsgSeq, 1, true, true)).msgList[0] + if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { + const peer = { + chatType: msg.chatType, + peerUid: msg.peerUid, + guildId: '', + } + replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq)).msgList[0] } - else { - continue + if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { + throw new Error('回复消息消息验证失败') } + message_data['data']['id'] = MessageUnique.createMsg({ + peerUid: msg.peerUid, + guildId: '', + chatType: msg.chatType, + }, replyMsg.msgId)?.toString() } catch (e: any) { log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) + continue } } else if (element.picElement) { message_data['type'] = OB11MessageDataType.image - // message_data["data"]["file"] = element.picElement.sourcePath let fileName = element.picElement.fileName - const sourcePath = element.picElement.sourcePath const isGif = element.picElement.picType === PicType.gif if (isGif && !fileName.endsWith('.gif')) { fileName += '.gif' } message_data['data']['file'] = fileName message_data['data']['subType'] = element.picElement.picSubType - // message_data["data"]["path"] = element.picElement.sourcePath - // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" - + message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement) - // message_data["data"]["file_id"] = element.picElement.fileUuid message_data['data']['file_size'] = element.picElement.fileSize - dbUtil - .addFileCache(fileName, { - fileName, - elementId: element.elementId, - filePath: sourcePath, - fileSize: element.picElement.fileSize.toString(), - url: message_data['data']['url'], - downloadFunc: async () => { - await NTQQFileApi.downloadMedia( - msg.msgId, - msg.chatType, - msg.peerUid, - element.elementId, - element.picElement.thumbPath?.get(0) || '', - element.picElement.sourcePath, - ) - }, - }).then() } else if (element.videoElement || element.fileElement) { const videoOrFileElement = element.videoElement || element.fileElement @@ -208,7 +200,7 @@ export class OB11Constructor { message_data['type'] = ob11MessageDataType message_data['data']['file'] = videoOrFileElement.fileName message_data['data']['path'] = videoOrFileElement.filePath - message_data['data']['file_id'] = videoOrFileElement.fileUuid + message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) message_data['data']['file_size'] = videoOrFileElement.fileSize if (element.videoElement) { message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ @@ -217,50 +209,40 @@ export class OB11Constructor { }, msg.msgId, element.elementId, ) } - dbUtil - .addFileCache(videoOrFileElement.fileUuid!, { - msgId: msg.msgId, - elementId: element.elementId, - fileName: videoOrFileElement.fileName, - filePath: videoOrFileElement.filePath, - fileSize: videoOrFileElement.fileSize!, - downloadFunc: async () => { - await NTQQFileApi.downloadMedia( - msg.msgId, - msg.chatType, - msg.peerUid, - element.elementId, - ob11MessageDataType == OB11MessageDataType.video - ? (videoOrFileElement as VideoElement).thumbPath?.get(0) - : null, - videoOrFileElement.filePath, - ) - }, - }) - .then() - // 怎么拿到url呢 + NTQQFileApi.addFileCache( + { + peerUid: msg.peerUid, + chatType: msg.chatType, + guildId: '', + }, + msg.msgId, + msg.msgSeq, + msg.senderUid, + element.elementId, + element.elementType.toString(), + videoOrFileElement.fileSize || '0', + videoOrFileElement.fileName, + ) } else if (element.pttElement) { message_data['type'] = OB11MessageDataType.voice message_data['data']['file'] = element.pttElement.fileName message_data['data']['path'] = element.pttElement.filePath - // message_data["data"]["file_id"] = element.pttElement.fileUuid + message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) message_data['data']['file_size'] = element.pttElement.fileSize - dbUtil - .addFileCache(element.pttElement.fileName, { - elementId: element.elementId, - fileName: element.pttElement.fileName, - filePath: element.pttElement.filePath, - fileSize: element.pttElement.fileSize, - }) - .then() - - // log("收到语音消息", msg) - // window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { - // console.log("语音转文字结果", text) - // }).catch(err => { - // console.log("语音转文字失败", err) - // }) + NTQQFileApi.addFileCache({ + peerUid: msg.peerUid, + chatType: msg.chatType, + guildId: '', + }, + msg.msgId, + msg.msgSeq, + msg.senderUid, + element.elementId, + element.elementType.toString(), + element.pttElement.fileSize || '0', + element.pttElement.fileUuid || '', + ) } else if (element.arkElement) { message_data['type'] = OB11MessageDataType.json @@ -474,16 +456,27 @@ export class OB11Constructor { const senderUin = emojiLikeData.gtip.qq.jp const msgSeq = emojiLikeData.gtip.url.msgseq const emojiId = emojiLikeData.gtip.face.id - const msg = await dbUtil.getMsgBySeqId(msgSeq) - if (!msg) { + const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({ + chatType: ChatType.group, + guildId: '', + peerUid: msg.peerUid, + }, msgSeq, 1, true, true)).msgList + if (replyMsgList.length < 1) { return } - return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId!, [ + const likes = [ { emoji_id: emojiId, count: 1, }, - ]) + ] + const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId) + return new OB11GroupMsgEmojiLikeEvent( + parseInt(msg.peerUid), + parseInt(senderUin), + shortId!, + likes + ) } catch (e: any) { log('解析表情回应消息失败', e.stack) } @@ -556,20 +549,23 @@ export class OB11Constructor { const searchParams = new URL(json.items[0].jp).searchParams const msgSeq = searchParams.get('msgSeq')! const Group = searchParams.get('groupCode') - const Businessid = searchParams.get('businessid') const Peer: Peer = { guildId: '', chatType: ChatType.group, peerUid: Group! } - let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList - const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId) - const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg + const { msgList } = await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true) + //const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId) + //const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg // 如果 senderUin 为 0,可能是 历史消息 或 自身消息 - if (msgList[0].senderUin === '0') { - msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin() - } - return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin!)) + //if (msgList[0].senderUin === '0') { + //msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin() + //} + return new OB11GroupEssenceEvent( + parseInt(msg.peerUid), + MessageUnique.getShortIdByMsgId(msgList[0].msgId)!, + parseInt(msgList[0].senderUin!) + ) // 获取MsgSeq+Peer可获取具体消息 } if (grayTipElement.jsonGrayTipElement.busiId == 2407) { @@ -590,6 +586,7 @@ export class OB11Constructor { static async RecallEvent( msg: RawMessage, + shortId: number ): Promise { let msgElement = msg.elements.find( (element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL, @@ -606,11 +603,11 @@ export class OB11Constructor { parseInt(msg.peerUid), parseInt(sender?.uin!), parseInt(operator?.uin!), - msg.msgShortId!, + shortId, ) } else { - return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!) + return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), shortId) } } diff --git a/src/onebot11/server/ws/reply.ts b/src/onebot11/server/ws/reply.ts index 98263e9..7837a2c 100644 --- a/src/onebot11/server/ws/reply.ts +++ b/src/onebot11/server/ws/reply.ts @@ -6,12 +6,12 @@ import { isNull } from '../../../common/utils/helper' export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) { try { - let packet = Object.assign({}, data) + const packet = Object.assign({}, data) if (isNull(packet['echo'])) { delete packet['echo'] } wsClient.send(JSON.stringify(packet)) - log('ws 消息上报', wsClient.url || '', data) + //log('ws 消息上报', wsClient.url || '', data) } catch (e: any) { log('websocket 回复失败', e.stack, data) } diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index 37a1b5b..789e64f 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -164,7 +164,7 @@ export interface OB11MessagePoke { } } -interface OB11MessageFileBase { +export interface OB11MessageFileBase { data: { thumb?: string name?: string diff --git a/src/version.ts b/src/version.ts index b13a226..0efbc45 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.28.6' +export const version = '3.28.7' From 8ed0e6c1bebdcb21923e33c091ba771c70ad9a1b Mon Sep 17 00:00:00 2001 From: idranme Date: Tue, 13 Aug 2024 21:59:13 +0800 Subject: [PATCH 03/23] fix --- src/common/data.ts | 2 +- src/main/main.ts | 2 +- src/ntqqapi/api/friend.ts | 1 + src/ntqqapi/api/user.ts | 29 ++++++++------------------- src/ntqqapi/hook.ts | 4 ++-- src/onebot11/action/llonebot/Debug.ts | 2 +- 6 files changed, 14 insertions(+), 26 deletions(-) diff --git a/src/common/data.ts b/src/common/data.ts index c551fa0..ccd36cc 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -16,7 +16,7 @@ export const llonebotError: LLOneBotError = { ffmpegError: '', httpServerError: '', wsServerError: '', - otherError: 'LLOnebot 未能正常启动,请检查日志查看错误', + otherError: 'LLOneBot 未能正常启动,请检查日志查看错误', } // 群号 -> 群成员map(uid=>GroupMember) export const groupMembers: Map> = new Map>() diff --git a/src/main/main.ts b/src/main/main.ts index 448095b..5ba38f5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -382,7 +382,7 @@ function onLoad() { log('llonebot pid', process.pid) const config = getConfigUtil().getConfig() if (!config.enableLLOB) { - llonebotError.otherError = 'LLOnebot 未启动' + llonebotError.otherError = 'LLOneBot 未启动' log('LLOneBot 开关设置为关闭,不启动LLOneBot') return } diff --git a/src/ntqqapi/api/friend.ts b/src/ntqqapi/api/friend.ts index 48d6f12..cb51f3e 100644 --- a/src/ntqqapi/api/friend.ts +++ b/src/ntqqapi/api/friend.ts @@ -8,6 +8,7 @@ import { CacheClassFuncAsyncExtend } from '@/common/utils/helper' import { LimitedHashTable } from '@/common/utils/table' export class NTQQFriendApi { + /** >=26702 应使用 getBuddyV2 */ static async getFriends(forced = false) { const data = await callNTQQApi<{ data: { diff --git a/src/ntqqapi/api/user.ts b/src/ntqqapi/api/user.ts index 1bdd5fe..1d6c6b5 100644 --- a/src/ntqqapi/api/user.ts +++ b/src/ntqqapi/api/user.ts @@ -52,20 +52,11 @@ export class NTQQUserApi { 'NodeIKernelProfileListener/onUserDetailInfoChanged', 1, 5000, - (profile) => { - if (profile.uid === uid) { - return true - } - return false - }, + (profile) => profile.uid === uid, 'BuddyProfileStore', - [ - uid - ], + [uid], UserDetailSource.KSERVER, - [ - ProfileBizType.KALL - ] + [ProfileBizType.KALL] ) const RetUser: User = { ...profile.simpleInfo.coreInfo, @@ -81,7 +72,7 @@ export class NTQQUserApi { static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { if (getBuildVersion() >= 26702) { - return this.fetchUserDetailInfo(uid) + return NTQQUserApi.fetchUserDetailInfo(uid) } type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'] @@ -92,12 +83,7 @@ export class NTQQUserApi { 'NodeIKernelProfileListener/onProfileDetailInfoChanged', 2, 5000, - (profile: User) => { - if (profile.uid === uid) { - return true - } - return false - }, + (profile) => profile.uid === uid, uid, [0] ) @@ -119,7 +105,7 @@ export class NTQQUserApi { static async getQzoneCookies() { const uin = getSelfUin() - const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + uin + '%2Finfocenter&keyindex=19%27' + const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + (await NTQQUserApi.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + uin + '%2Finfocenter&keyindex=19%27' let cookies: { [key: string]: string } = {} try { cookies = await RequestUtil.HttpsGetCookies(requestUrl) @@ -129,8 +115,9 @@ export class NTQQUserApi { } return cookies } + static async getSkey(): Promise { - const clientKeyData = await this.getClientKey() + const clientKeyData = await NTQQUserApi.getClientKey() if (clientKeyData.result !== 0) { throw new Error('获取clientKey失败') } diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 5b54f62..6acd5e3 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -246,12 +246,12 @@ let activatedGroups: string[] = [] async function updateGroups(_groups: Group[], needUpdate: boolean = true) { for (let group of _groups) { - log('update group', group) + log('update group', group.groupCode) if (group.privilegeFlag === 0) { deleteGroup(group.groupCode) continue } - log('update group', group) + //log('update group', group) NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().catch(log) let existGroup = groups.find((g) => g.groupCode == group.groupCode) if (existGroup) { diff --git a/src/onebot11/action/llonebot/Debug.ts b/src/onebot11/action/llonebot/Debug.ts index 70a74c9..a184626 100644 --- a/src/onebot11/action/llonebot/Debug.ts +++ b/src/onebot11/action/llonebot/Debug.ts @@ -24,7 +24,7 @@ export default class Debug extends BaseAction { log('debug call ntqq api', payload) const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi] for (const ntqqApiClass of ntqqApi) { - log('ntqqApiClass', ntqqApiClass) + //log('ntqqApiClass', ntqqApiClass) const method = ntqqApiClass[payload.method] if (method) { const result = method(...payload.args) From 2715552814f4d34a0cceef32cb7e03353a2b4409 Mon Sep 17 00:00:00 2001 From: idranme Date: Tue, 13 Aug 2024 22:08:36 +0800 Subject: [PATCH 04/23] chore: v3.29.0 --- manifest.json | 2 +- package.json | 2 +- src/onebot11/constructor.ts | 1 + src/version.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index f296992..8b31b6b 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.28.7", + "version": "3.29.0", "icon": "./icon.webp", "authors": [ { diff --git a/package.json b/package.json index aa74344..f963f1e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "llonebot", "version": "1.0.0", "type": "module", - "description": "NTQQLiteLoaderOneBotApi", + "description": "", "main": "dist/main.js", "scripts": { "build": "electron-vite build", diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index 1f605f7..a9440b8 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -168,6 +168,7 @@ export class OB11Constructor { } replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq)).msgList[0] } + // 284840486: 合并消息内侧 消息具体定位不到 if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { throw new Error('回复消息消息验证失败') } diff --git a/src/version.ts b/src/version.ts index 0efbc45..f1fc764 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.28.7' +export const version = '3.29.0' From 6aa44bdd79db42dca88278436ffa673e2974555c Mon Sep 17 00:00:00 2001 From: idranme Date: Wed, 14 Aug 2024 18:20:39 +0800 Subject: [PATCH 05/23] fix: `/get_image` --- src/common/types.ts | 9 ++-- src/common/utils/EventTask.ts | 20 ++++---- src/common/utils/MessageUnique.ts | 21 ++++++++ src/ntqqapi/api/file.ts | 1 - src/onebot11/action/file/GetFile.ts | 64 ++++++++++++++---------- src/onebot11/action/file/GetImage.ts | 7 +++ src/onebot11/constructor.ts | 75 ++++++++++++++-------------- 7 files changed, 116 insertions(+), 81 deletions(-) diff --git a/src/common/types.ts b/src/common/types.ts index b4d722d..d4c6986 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -41,11 +41,10 @@ export interface LLOneBotError { export interface FileCache { fileName: string - filePath: string fileSize: string - fileUuid?: string - url?: string - msgId?: string + msgId: string + peerUid: string + chatType: number elementId: string - downloadFunc?: () => Promise + elementType: number } diff --git a/src/common/utils/EventTask.ts b/src/common/utils/EventTask.ts index 1099752..bc32199 100644 --- a/src/common/utils/EventTask.ts +++ b/src/common/utils/EventTask.ts @@ -34,7 +34,7 @@ export class NTEventWrapper { if (typeof target[prop] === 'undefined') { // 如果方法不存在,返回一个函数,这个函数调用existentMethod return (...args: any[]) => { - current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then() + current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then() } } // 如果方法存在,正常返回 @@ -48,7 +48,7 @@ export class NTEventWrapper { this.WrapperSession = WrapperSession } - CreatEventFunction any>(eventName: string): T | undefined { + createEventFunction any>(eventName: string): T | undefined { const eventNameArr = eventName.split('/') type eventType = { [key: string]: () => { [key: string]: (...params: Parameters) => Promise> } @@ -69,16 +69,14 @@ export class NTEventWrapper { } } - createEventFunction = this.CreatEventFunction - - CreatListenerFunction(listenerMainName: string, uniqueCode: string = ''): T { + createListenerFunction(listenerMainName: string, uniqueCode: string = ''): T { const ListenerType = this.ListenerMap![listenerMainName] let Listener = this.ListenerManger.get(listenerMainName + uniqueCode) if (!Listener && ListenerType) { Listener = new ListenerType(this.createProxyDispatch(listenerMainName)) const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1] const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener' - const addfunc = this.CreatEventFunction<(listener: T) => number>(Service) + const addfunc = this.createEventFunction<(listener: T) => number>(Service) addfunc!(Listener as T) //console.log(addfunc!(Listener as T)) this.ListenerManger.set(listenerMainName + uniqueCode, Listener) @@ -87,7 +85,7 @@ export class NTEventWrapper { } //统一回调清理事件 - async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) { + async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) { //console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args) this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => { //console.log(task.func, uuid, task.createtime, task.timeout) @@ -103,7 +101,7 @@ export class NTEventWrapper { async CallNoListenerEvent Promise | any>(EventName = '', timeout: number = 3000, ...args: Parameters) { return new Promise>>(async (resolve, reject) => { - const EventFunc = this.CreatEventFunction(EventName) + const EventFunc = this.createEventFunction(EventName) let complete = false const Timeouter = setTimeout(() => { if (!complete) { @@ -152,7 +150,7 @@ export class NTEventWrapper { this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()) } this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak) - this.CreatListenerFunction(ListenerMainName) + this.createListenerFunction(ListenerMainName) }) } @@ -198,8 +196,8 @@ export class NTEventWrapper { this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()) } this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak) - this.CreatListenerFunction(ListenerMainName) - const EventFunc = this.CreatEventFunction(EventName) + this.createListenerFunction(ListenerMainName) + const EventFunc = this.createEventFunction(EventName) retEvent = await EventFunc!(...(args as any[])) }) } diff --git a/src/common/utils/MessageUnique.ts b/src/common/utils/MessageUnique.ts index f300f14..9fd3873 100644 --- a/src/common/utils/MessageUnique.ts +++ b/src/common/utils/MessageUnique.ts @@ -7,6 +7,7 @@ import SQLite from '@minatojs/driver-sqlite' import fsPromise from 'node:fs/promises' import fs from 'node:fs' import path from 'node:path' +import { FileCache } from '../types' interface SQLiteTables extends Tables { message: { @@ -15,6 +16,7 @@ interface SQLiteTables extends Tables { chatType: number peerUid: string } + file: FileCache } interface MsgIdAndPeerByShortId { @@ -50,6 +52,17 @@ class MessageUniqueWrapper { }, { primary: 'shortId' }) + database.extend('file', { + fileName: 'string', + fileSize: 'string', + msgId: 'string(24)', + peerUid: 'string(24)', + chatType: 'unsigned', + elementId: 'string(24)', + elementType: 'unsigned', + }, { + primary: 'fileName' + }) this.db = database } @@ -128,6 +141,14 @@ class MessageUniqueWrapper { this.msgIdMap.resize(maxSize) this.msgDataMap.resize(maxSize) } + + addFileCache(data: FileCache) { + return this.db?.upsert('file', [data], 'fileName') + } + + getFileCache(fileName: string) { + return this.db?.get('file', { fileName }) + } } export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper() \ No newline at end of file diff --git a/src/ntqqapi/api/file.ts b/src/ntqqapi/api/file.ts index 80bf555..e4d51c6 100644 --- a/src/ntqqapi/api/file.ts +++ b/src/ntqqapi/api/file.ts @@ -179,7 +179,6 @@ export class NTQQFileApi { const url: string = element.originImageUrl! // 没有域名 const md5HexStr = element.md5HexStr const fileMd5 = element.md5HexStr - const fileUuid = element.fileUuid if (url) { const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 diff --git a/src/onebot11/action/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index d741bfc..7e53e4f 100644 --- a/src/onebot11/action/file/GetFile.ts +++ b/src/onebot11/action/file/GetFile.ts @@ -3,9 +3,10 @@ import fsPromise from 'node:fs/promises' import { getConfigUtil } from '@/common/config' import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api' import { ActionName } from '../types' -import { RawMessage } from '@/ntqqapi/types' +import { PicElement } from '@/ntqqapi/types' import { UUIDConverter } from '@/common/utils/helper' import { Peer, ChatType, ElementType } from '@/ntqqapi/types' +import { MessageUnique } from '@/common/utils/MessageUnique' export interface GetFilePayload { file: string // 文件名或者fileUuid @@ -51,10 +52,10 @@ export abstract class GetFileBase extends BaseAction e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT) if (!findEle) { throw new Error('element not found') @@ -68,7 +69,7 @@ export abstract class GetFileBase extends BaseAction e.elementType == NTSearchNameResult[0].elemType) - if (file.length == 0) { - throw new Error('file not found') - } - const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, file[0].elementId, '', '') + const fileCache = await MessageUnique.getFileCache(String(payload.file)) + if (fileCache?.length) { + const downloadPath = await NTQQFileApi.downloadMedia( + fileCache[0].msgId, + fileCache[0].chatType, + fileCache[0].peerUid, + fileCache[0].elementId, + '', + '' + ) const res: GetFileResponse = { file: downloadPath, url: downloadPath, - file_size: NTSearchNameResult[0].fileSize.toString(), - file_name: NTSearchNameResult[0].fileName, + file_size: fileCache[0].fileSize, + file_name: fileCache[0].fileName, } - if (enableLocalFile2Url) { + const peer: Peer = { + chatType: fileCache[0].chatType, + peerUid: fileCache[0].peerUid, + guildId: '' + } + if (fileCache[0].elementType === ElementType.PIC) { + const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId]) + if (msgList.msgList.length === 0) { + throw new Error('msg not found') + } + const msg = msgList.msgList[0] + const findEle = msg.elements.find(e => e.elementId === fileCache[0].elementId) + if (!findEle) { + throw new Error('element not found') + } + res.url = await NTQQFileApi.getImageUrl(findEle.picElement) + } else if (fileCache[0].elementType === ElementType.VIDEO) { + res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId) + } + if (enableLocalFile2Url && downloadPath && res.file === res.url) { try { res.base64 = await fsPromise.readFile(downloadPath, 'base64') } catch (e) { diff --git a/src/onebot11/action/file/GetImage.ts b/src/onebot11/action/file/GetImage.ts index d51ba67..56f0288 100644 --- a/src/onebot11/action/file/GetImage.ts +++ b/src/onebot11/action/file/GetImage.ts @@ -3,4 +3,11 @@ import { ActionName } from '../types' export default class GetImage extends GetFileBase { actionName = ActionName.GetImage + + protected async _handle(payload: { file: string }) { + if (!payload.file) { + throw new Error('参数 file 不能为空') + } + return super._handle(payload) + } } diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index a9440b8..2bc85a8 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -184,21 +184,30 @@ export class OB11Constructor { } else if (element.picElement) { message_data['type'] = OB11MessageDataType.image - let fileName = element.picElement.fileName - const isGif = element.picElement.picType === PicType.gif + const { picElement } = element + /*let fileName = picElement.fileName + const isGif = picElement.picType === PicType.gif if (isGif && !fileName.endsWith('.gif')) { fileName += '.gif' - } - message_data['data']['file'] = fileName - message_data['data']['subType'] = element.picElement.picSubType + }*/ + message_data['data']['file'] = picElement.fileName + message_data['data']['subType'] = picElement.picSubType message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) - message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement) - message_data['data']['file_size'] = element.picElement.fileSize + message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement) + message_data['data']['file_size'] = picElement.fileSize + MessageUnique.addFileCache({ + peerUid: msg.peerUid, + msgId: msg.msgId, + chatType: msg.chatType, + elementId: element.elementId, + elementType: element.elementType, + fileName: picElement.fileName, + fileSize: String(picElement.fileSize || '0'), + }) } else if (element.videoElement || element.fileElement) { const videoOrFileElement = element.videoElement || element.fileElement - const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file - message_data['type'] = ob11MessageDataType + message_data['type'] = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file message_data['data']['file'] = videoOrFileElement.fileName message_data['data']['path'] = videoOrFileElement.filePath message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) @@ -210,40 +219,32 @@ export class OB11Constructor { }, msg.msgId, element.elementId, ) } - NTQQFileApi.addFileCache( - { - peerUid: msg.peerUid, - chatType: msg.chatType, - guildId: '', - }, - msg.msgId, - msg.msgSeq, - msg.senderUid, - element.elementId, - element.elementType.toString(), - videoOrFileElement.fileSize || '0', - videoOrFileElement.fileName, - ) + MessageUnique.addFileCache({ + peerUid: msg.peerUid, + msgId: msg.msgId, + chatType: msg.chatType, + elementId: element.elementId, + elementType: element.elementType, + fileName: videoOrFileElement.fileName, + fileSize: String(videoOrFileElement.fileSize || '0') + }) } else if (element.pttElement) { message_data['type'] = OB11MessageDataType.voice - message_data['data']['file'] = element.pttElement.fileName - message_data['data']['path'] = element.pttElement.filePath + const { pttElement } = element + message_data['data']['file'] = pttElement.fileName + message_data['data']['path'] = pttElement.filePath message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) - message_data['data']['file_size'] = element.pttElement.fileSize - NTQQFileApi.addFileCache({ + message_data['data']['file_size'] = pttElement.fileSize + MessageUnique.addFileCache({ peerUid: msg.peerUid, + msgId: msg.msgId, chatType: msg.chatType, - guildId: '', - }, - msg.msgId, - msg.msgSeq, - msg.senderUid, - element.elementId, - element.elementType.toString(), - element.pttElement.fileSize || '0', - element.pttElement.fileUuid || '', - ) + elementId: element.elementId, + elementType: element.elementType, + fileName: pttElement.fileName, + fileSize: String(pttElement.fileSize || '0') + }) } else if (element.arkElement) { message_data['type'] = OB11MessageDataType.json From c1d7aa7aedba765eb0b598e9879116b9ee73418e Mon Sep 17 00:00:00 2001 From: idranme Date: Wed, 14 Aug 2024 18:59:27 +0800 Subject: [PATCH 06/23] chore: v3.29.1 --- manifest.json | 2 +- src/onebot11/action/file/GetFile.ts | 1 - src/version.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 8b31b6b..749de07 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.29.0", + "version": "3.29.1", "icon": "./icon.webp", "authors": [ { diff --git a/src/onebot11/action/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index 7e53e4f..11fff91 100644 --- a/src/onebot11/action/file/GetFile.ts +++ b/src/onebot11/action/file/GetFile.ts @@ -3,7 +3,6 @@ import fsPromise from 'node:fs/promises' import { getConfigUtil } from '@/common/config' import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api' import { ActionName } from '../types' -import { PicElement } from '@/ntqqapi/types' import { UUIDConverter } from '@/common/utils/helper' import { Peer, ChatType, ElementType } from '@/ntqqapi/types' import { MessageUnique } from '@/common/utils/MessageUnique' diff --git a/src/version.ts b/src/version.ts index f1fc764..b7db38f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.29.0' +export const version = '3.29.1' From 3f4b0b44cf84e27fe2d1b61854f14bdee7f76a5b Mon Sep 17 00:00:00 2001 From: idranme Date: Wed, 14 Aug 2024 23:04:15 +0800 Subject: [PATCH 07/23] feat: cache recalled message content --- src/common/config.ts | 1 + src/common/data.ts | 20 ++++++++++++++++++++ src/common/types.ts | 1 + src/main/main.ts | 4 +++- src/onebot11/action/msg/GetMsg.ts | 10 ++++------ src/onebot11/action/msg/SendMsg.ts | 7 +++---- src/onebot11/constructor.ts | 10 ++++------ src/renderer/index.ts | 5 +++++ 8 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 3ef929c..5fb5ee5 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -52,6 +52,7 @@ export class ConfigUtil { autoDeleteFile: false, autoDeleteFileSecond: 60, musicSignUrl: '', + msgCacheExpire: 8000 } if (!fs.existsSync(this.configPath)) { diff --git a/src/common/data.ts b/src/common/data.ts index ccd36cc..3f9770c 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -9,6 +9,8 @@ import { NTQQGroupApi } from '../ntqqapi/api/group' import { log } from './utils/log' import { isNumeric } from './utils/helper' import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api' +import { RawMessage } from '../ntqqapi/types' +import { getConfigUtil } from './config' export let groups: Group[] = [] export let friends: Friend[] = [] @@ -128,3 +130,21 @@ export function getSelfUid() { export function getSelfUin() { return selfInfo['uin'] } + +const messages: Map = new Map() +let expire: number + +/** 缓存已撤回消息内容 */ +export function addMsgCache(msg: RawMessage) { + expire ??= getConfigUtil().getConfig().msgCacheExpire! + const id = msg.msgId + messages.set(id, msg) + setTimeout(() => { + messages.delete(id) + }, expire) +} + +/** 获取已撤回消息内容 */ +export function getMsgCache(msgId: string) { + return messages.get(msgId) +} \ No newline at end of file diff --git a/src/common/types.ts b/src/common/types.ts index d4c6986..4705dd0 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -30,6 +30,7 @@ export interface Config { ffmpeg?: string // ffmpeg路径 musicSignUrl?: string ignoreBeforeLoginMsg?: boolean + msgCacheExpire?: number } export interface LLOneBotError { diff --git a/src/main/main.ts b/src/main/main.ts index 5ba38f5..49868cd 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -21,7 +21,8 @@ import { setSelfInfo, getSelfInfo, getSelfUid, - getSelfUin + getSelfUin, + addMsgCache } from '../common/data' import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook' import { OB11Constructor } from '../onebot11/constructor' @@ -222,6 +223,7 @@ function onLoad() { if (!oriMessageId) { continue } + addMsgCache(message) OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => { if (recallEvent) { //log('post recall event', recallEvent) diff --git a/src/onebot11/action/msg/GetMsg.ts b/src/onebot11/action/msg/GetMsg.ts index 5f53294..e3a0e2b 100644 --- a/src/onebot11/action/msg/GetMsg.ts +++ b/src/onebot11/action/msg/GetMsg.ts @@ -4,6 +4,7 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' import { NTQQMsgApi } from '@/ntqqapi/api' import { MessageUnique } from '@/common/utils/MessageUnique' +import { getMsgCache } from '@/common/data' export interface PayloadType { message_id: number | string @@ -29,12 +30,9 @@ class GetMsg extends BaseAction { peerUid: msgIdWithPeer.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType } - const msg = await NTQQMsgApi.getMsgsByMsgId( - peer, - [msgIdWithPeer?.MsgId || payload.message_id.toString()] - ) - const retMsg = await OB11Constructor.message(msg.msgList[0]) - retMsg.message_id = MessageUnique.createMsg(peer, msg.msgList[0].msgId)! + const msg = getMsgCache(msgIdWithPeer.MsgId) ?? (await NTQQMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0] + const retMsg = await OB11Constructor.message(msg) + retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)! retMsg.message_seq = retMsg.message_id retMsg.real_id = retMsg.message_id return retMsg diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index cae7707..a08c3d5 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -119,9 +119,8 @@ export async function createSendElements( if (!peer) { continue } - let atQQ = sendMsg.data?.qq - if (atQQ) { - atQQ = atQQ.toString() + if (sendMsg.data?.qq) { + const atQQ = String(sendMsg.data.qq) if (atQQ === 'all') { // todo:查询剩余的at全体次数 const groupCode = peer.peerUid @@ -161,7 +160,7 @@ export async function createSendElements( } break case OB11MessageDataType.reply: { - if (sendMsg.data.id) { + if (sendMsg.data?.id) { const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id) if (!replyMsgId) { log('回复消息不存在', replyMsgId) diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index 2bc85a8..c94335f 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -590,21 +590,19 @@ export class OB11Constructor { msg: RawMessage, shortId: number ): Promise { - let msgElement = msg.elements.find( + const msgElement = msg.elements.find( (element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL, ) if (!msgElement) { return } - const isGroup = msg.chatType === ChatType.group const revokeElement = msgElement.grayTipElement.revokeElement - if (isGroup) { + if (msg.chatType === ChatType.group) { const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid) - const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!) return new OB11GroupRecallNoticeEvent( parseInt(msg.peerUid), - parseInt(sender?.uin!), - parseInt(operator?.uin!), + parseInt(msg.senderUin!), + parseInt(operator?.uin || msg.senderUin!), shortId, ) } diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 70d9083..5015d46 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -219,6 +219,11 @@ async function onSettingWindowCreated(view: Element) { `${window.LiteLoader.plugins['LLOneBot'].path.data}/logs`, SettingButton('打开', 'config-open-log-path'), ), + SettingItem( + '已撤回消息内容缓存时长', + '单位为毫秒', + `
`, + ), ]), SettingList([ SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')), From d143dc043c43e67b2738a78c0aac38745175218c Mon Sep 17 00:00:00 2001 From: idranme Date: Thu, 15 Aug 2024 10:31:51 +0800 Subject: [PATCH 08/23] fix --- src/common/config.ts | 2 +- src/common/data.ts | 11 +++++++---- src/common/types.ts | 1 + src/main/main.ts | 6 +++--- src/renderer/index.ts | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 5fb5ee5..ce2250f 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -52,7 +52,7 @@ export class ConfigUtil { autoDeleteFile: false, autoDeleteFileSecond: 60, musicSignUrl: '', - msgCacheExpire: 8000 + msgCacheExpire: 120 } if (!fs.existsSync(this.configPath)) { diff --git a/src/common/data.ts b/src/common/data.ts index 3f9770c..a875fc4 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -134,9 +134,12 @@ export function getSelfUin() { const messages: Map = new Map() let expire: number -/** 缓存已撤回消息内容 */ -export function addMsgCache(msg: RawMessage) { - expire ??= getConfigUtil().getConfig().msgCacheExpire! +/** 缓存近期消息内容 */ +export async function addMsgCache(msg: RawMessage) { + expire ??= getConfigUtil().getConfig().msgCacheExpire! * 1000 + if (expire === 0) { + return + } const id = msg.msgId messages.set(id, msg) setTimeout(() => { @@ -144,7 +147,7 @@ export function addMsgCache(msg: RawMessage) { }, expire) } -/** 获取已撤回消息内容 */ +/** 获取近期消息内容 */ export function getMsgCache(msgId: string) { return messages.get(msgId) } \ No newline at end of file diff --git a/src/common/types.ts b/src/common/types.ts index 4705dd0..e7612b4 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -30,6 +30,7 @@ export interface Config { ffmpeg?: string // ffmpeg路径 musicSignUrl?: string ignoreBeforeLoginMsg?: boolean + /** 单位为秒 */ msgCacheExpire?: number } diff --git a/src/main/main.ts b/src/main/main.ts index 49868cd..7d758be 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -96,7 +96,7 @@ function onLoad() { } ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) - llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常' + llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常' let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` error = error.replace('\n\n', '\n') @@ -160,6 +160,7 @@ function onLoad() { peerUid: message.peerUid } message.msgShortId = MessageUnique.createMsg(peer, message.msgId) + addMsgCache(message) OB11Constructor.message(message) .then((msg) => { @@ -184,7 +185,7 @@ function onLoad() { } }) OB11Constructor.PrivateEvent(message).then((privateEvent) => { - log(message) + //log(message) if (privateEvent) { // log("post private event", privateEvent); postOb11Event(privateEvent) @@ -223,7 +224,6 @@ function onLoad() { if (!oriMessageId) { continue } - addMsgCache(message) OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => { if (recallEvent) { //log('post recall event', recallEvent) diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 5015d46..7077b42 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -220,8 +220,8 @@ async function onSettingWindowCreated(view: Element) { SettingButton('打开', 'config-open-log-path'), ), SettingItem( - '已撤回消息内容缓存时长', - '单位为毫秒', + '消息内容缓存时长', + '单位为秒', `
`, ), ]), From 94c1aea6dfb5eec1ef0c66db27cb86494e3afaaf Mon Sep 17 00:00:00 2001 From: idranme Date: Thu, 15 Aug 2024 10:57:15 +0800 Subject: [PATCH 09/23] chore: v3.29.2 --- manifest.json | 2 +- src/renderer/index.ts | 2 +- src/version.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 749de07..b9fccf0 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.29.1", + "version": "3.29.2", "icon": "./icon.webp", "authors": [ { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 7077b42..fa88e6a 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -221,7 +221,7 @@ async function onSettingWindowCreated(view: Element) { ), SettingItem( '消息内容缓存时长', - '单位为秒', + '单位为秒,可用于获取撤回的消息', `
`, ), ]), diff --git a/src/version.ts b/src/version.ts index b7db38f..d453805 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.29.1' +export const version = '3.29.2' From a47ee4c3e404a4a2bafeb99c653ba1125720c74d Mon Sep 17 00:00:00 2001 From: idranme Date: Fri, 16 Aug 2024 09:53:23 +0800 Subject: [PATCH 10/23] fix --- package.json | 10 +-- src/common/data.ts | 53 +++++----------- src/main/main.ts | 35 +++++------ src/ntqqapi/api/friend.ts | 2 +- src/ntqqapi/api/group.ts | 20 +++++- src/ntqqapi/hook.ts | 21 +++---- src/onebot11/action/go-cqhttp/UploadFile.ts | 12 ++-- src/onebot11/action/group/GetGroupInfo.ts | 12 ++-- src/onebot11/action/group/GetGroupList.ts | 12 +--- .../action/group/GetGroupMemberList.ts | 62 ++++++++++++++----- src/onebot11/action/msg/SendMsg.ts | 13 +--- src/onebot11/constructor.ts | 26 ++++---- src/onebot11/types.ts | 3 +- 13 files changed, 138 insertions(+), 143 deletions(-) diff --git a/package.json b/package.json index f963f1e..c269e75 100644 --- a/package.json +++ b/package.json @@ -16,15 +16,15 @@ "author": "", "license": "MIT", "dependencies": { - "@minatojs/driver-sqlite": "^4.4.1", + "@minatojs/driver-sqlite": "^4.5.0", "compressing": "^1.10.1", - "cordis": "^3.17.9", + "cordis": "^3.18.0", "cors": "^2.8.5", "express": "^4.19.2", "fast-xml-parser": "^4.4.1", - "file-type": "^19.4.0", + "file-type": "^19.4.1", "fluent-ffmpeg": "^2.1.3", - "minato": "^3.4.3", + "minato": "^3.5.0", "silk-wasm": "^3.6.1", "ws": "^8.18.0" }, @@ -37,7 +37,7 @@ "electron": "^29.1.4", "electron-vite": "^2.3.0", "typescript": "^5.5.4", - "vite": "^5.4.0", + "vite": "^5.4.1", "vite-plugin-cp": "^4.0.8" }, "packageManager": "yarn@4.4.0" diff --git a/src/common/data.ts b/src/common/data.ts index a875fc4..60b031e 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -1,6 +1,5 @@ import { type Friend, - type Group, type GroupMember, type SelfInfo, } from '../ntqqapi/types' @@ -11,8 +10,8 @@ import { isNumeric } from './utils/helper' import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api' import { RawMessage } from '../ntqqapi/types' import { getConfigUtil } from './config' +import { getBuildVersion } from './utils/QQBasicInfo' -export let groups: Group[] = [] export let friends: Friend[] = [] export const llonebotError: LLOneBotError = { ffmpegError: '', @@ -24,10 +23,10 @@ export const llonebotError: LLOneBotError = { export const groupMembers: Map> = new Map>() export async function getFriend(uinOrUid: string): Promise { - let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid' - let filterValue = uinOrUid + const filterKey: 'uin' | 'uid' = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid' + const filterValue = uinOrUid let friend = friends.find((friend) => friend[filterKey] === filterValue.toString()) - if (!friend) { + if (!friend && getBuildVersion() < 26702) { try { const _friends = await NTQQFriendApi.getFriends(true) friend = _friends.find((friend) => friend[filterKey] === filterValue.toString()) @@ -41,39 +40,15 @@ export async function getFriend(uinOrUid: string): Promise { return friend } -export async function getGroup(qq: string): Promise { - let group = groups.find((group) => group.groupCode === qq.toString()) - if (!group) { - try { - const _groups = await NTQQGroupApi.getGroups(true) - group = _groups.find((group) => group.groupCode === qq.toString()) - if (group) { - groups.push(group) - } - } catch (e) { - } - } - return group -} - -export function deleteGroup(groupCode: string) { - const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString()) - // log(groups, groupCode, groupIndex); - if (groupIndex !== -1) { - log('删除群', groupCode) - groups.splice(groupIndex, 1) - } -} - -export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) { - groupQQ = groupQQ.toString() - memberUinOrUid = memberUinOrUid.toString() - let members = groupMembers.get(groupQQ) +export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { + const groupCodeStr = groupCode.toString() + const memberUinOrUidStr = memberUinOrUid.toString() + let members = groupMembers.get(groupCodeStr) if (!members) { try { - members = await NTQQGroupApi.getGroupMembers(groupQQ) + members = await NTQQGroupApi.getGroupMembers(groupCodeStr) // 更新群成员列表 - groupMembers.set(groupQQ, members) + groupMembers.set(groupCodeStr, members) } catch (e) { return null @@ -81,16 +56,16 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s } const getMember = () => { let member: GroupMember | undefined = undefined - if (isNumeric(memberUinOrUid)) { - member = Array.from(members!.values()).find(member => member.uin === memberUinOrUid) + if (isNumeric(memberUinOrUidStr)) { + member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr) } else { - member = members!.get(memberUinOrUid) + member = members!.get(memberUinOrUidStr) } return member } let member = getMember() if (!member) { - members = await NTQQGroupApi.getGroupMembers(groupQQ) + members = await NTQQGroupApi.getGroupMembers(groupCodeStr) member = getMember() } return member diff --git a/src/main/main.ts b/src/main/main.ts index 7d758be..1ad1773 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -16,7 +16,6 @@ import { import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' import { DATA_DIR, TEMP_DIR } from '../common/utils' import { - getGroupMember, llonebotError, setSelfInfo, getSelfInfo, @@ -274,26 +273,24 @@ function onLoad() { const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { log('有成员退出通知', notify) - try { - const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid) - let operatorId = member1.uin - let subType: GroupDecreaseSubType = 'leave' - if (notify.user2.uid) { - // 是被踢的 - const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid) - operatorId = member2?.uin! - subType = 'kick' + const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))! + let operatorId = member1Uin + let subType: GroupDecreaseSubType = 'leave' + if (notify.user2.uid) { + // 是被踢的 + const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid) + if (member2Uin) { + operatorId = member2Uin } - let groupDecreaseEvent = new OB11GroupDecreaseEvent( - parseInt(notify.group.groupCode), - parseInt(member1.uin), - parseInt(operatorId), - subType, - ) - postOb11Event(groupDecreaseEvent, true) - } catch (e: any) { - log('获取群通知的成员信息失败', notify, e.stack.toString()) + subType = 'kick' } + const groupDecreaseEvent = new OB11GroupDecreaseEvent( + parseInt(notify.group.groupCode), + parseInt(member1Uin), + parseInt(operatorId), + subType, + ) + postOb11Event(groupDecreaseEvent, true) } else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { log('有加群请求') diff --git a/src/ntqqapi/api/friend.ts b/src/ntqqapi/api/friend.ts index cb51f3e..fcab865 100644 --- a/src/ntqqapi/api/friend.ts +++ b/src/ntqqapi/api/friend.ts @@ -8,7 +8,7 @@ import { CacheClassFuncAsyncExtend } from '@/common/utils/helper' import { LimitedHashTable } from '@/common/utils/table' export class NTQQFriendApi { - /** >=26702 应使用 getBuddyV2 */ + /** 大于或等于 26702 应使用 getBuddyV2 */ static async getFriends(forced = false) { const data = await callNTQQApi<{ data: { diff --git a/src/ntqqapi/api/group.ts b/src/ntqqapi/api/group.ts index a1d6b2f..3567716 100644 --- a/src/ntqqapi/api/group.ts +++ b/src/ntqqapi/api/group.ts @@ -5,6 +5,7 @@ import { NTQQWindowApi, NTQQWindows } from './window' import { getSession } from '../wrapper' import { NTEventDispatch } from '@/common/utils/EventTask' import { NodeIKernelGroupListener } from '../listeners' +import { NodeIKernelGroupService } from '../services' export class NTQQGroupApi { static async activateMemberListChange() { @@ -45,12 +46,29 @@ export class NTQQGroupApi { 'NodeIKernelGroupListener/onGroupListUpdate', 1, 5000, - (updateType) => true, + () => true, forced ) return groupList } + async getGroupMemberV2(GroupCode: string, uid: string, forced = false) { + type ListenerType = NodeIKernelGroupListener['onMemberInfoChange'] + type EventType = NodeIKernelGroupService['getMemberInfo'] + const [, , , _members] = await NTEventDispatch.CallNormalEvent + ( + 'NodeIKernelGroupService/getMemberInfo', + 'NodeIKernelGroupListener/onMemberInfoChange', + 1, + 5000, + (groupCode: string, changeType: number, members: Map) => { + return groupCode == GroupCode && members.has(uid) + }, + GroupCode, [uid], forced, + ) + return _members.get(uid) + } + static async getGroupMembers(groupQQ: string, num = 3000): Promise> { const session = getSession() const groupService = session?.getGroupService() diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 6acd5e3..aa107cc 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -4,27 +4,20 @@ import { NTQQMsgApi } from './api/msg' import { CategoryFriend, ChatType, - FriendV2, - Group, GroupMember, GroupMemberRole, RawMessage, SimpleInfo, User, } from './types' import { - deleteGroup, friends, getFriend, getGroupMember, - groups, - getSelfUin, setSelfInfo } from '@/common/data' -import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { postOb11Event } from '../onebot11/server/post-ob11-event' import { getConfigUtil, HOOK_LOG } from '@/common/config' import fs from 'node:fs' -import { NTQQGroupApi } from './api/group' import { log } from '@/common/utils' import { randomUUID } from 'node:crypto' import { MessageUnique } from '../common/utils/MessageUnique' @@ -242,9 +235,9 @@ export function removeReceiveHook(id: string) { receiveHooks.splice(index, 1) } -let activatedGroups: string[] = [] +//let activatedGroups: string[] = [] -async function updateGroups(_groups: Group[], needUpdate: boolean = true) { +/*async function updateGroups(_groups: Group[], needUpdate: boolean = true) { for (let group of _groups) { log('update group', group.groupCode) if (group.privilegeFlag === 0) { @@ -269,9 +262,9 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) { } } } -} +}*/ -async function processGroupEvent(payload: { groupList: Group[] }) { +/*async function processGroupEvent(payload: { groupList: Group[] }) { try { const newGroupList = payload.groupList for (const group of newGroupList) { @@ -322,12 +315,12 @@ async function processGroupEvent(payload: { groupList: Group[] }) { updateGroups(payload.groupList).then() log('更新群信息错误', e.stack.toString()) } -} +}*/ export async function startHook() { // 群列表变动 - registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { + /*registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { // updateType 3是群列表变动,2是群成员变动 // log("群列表变动", payload.updateType, payload.groupList) if (payload.updateType != 2) { @@ -350,7 +343,7 @@ export async function startHook() { processGroupEvent(payload).then() } } - }) + })*/ registerReceiveHook<{ groupCode: string diff --git a/src/onebot11/action/go-cqhttp/UploadFile.ts b/src/onebot11/action/go-cqhttp/UploadFile.ts index 08a0b69..c7cdd99 100644 --- a/src/onebot11/action/go-cqhttp/UploadFile.ts +++ b/src/onebot11/action/go-cqhttp/UploadFile.ts @@ -1,6 +1,5 @@ import fs from 'node:fs' import BaseAction from '../BaseAction' -import { getGroup } from '@/common/data' import { ActionName } from '../types' import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import { ChatType, SendFileElement } from '@/ntqqapi/types' @@ -22,10 +21,6 @@ export class GoCQHTTPUploadGroupFile extends BaseAction { actionName = ActionName.GoCQHTTP_UploadGroupFile protected async _handle(payload: Payload): Promise { - const group = await getGroup(payload.group_id?.toString()!) - if (!group) { - throw new Error(`群组${payload.group_id}不存在`) - } let file = payload.file if (fs.existsSync(file)) { file = `file://${file}` @@ -34,8 +29,11 @@ export class GoCQHTTPUploadGroupFile extends BaseAction { if (!downloadResult.success) { throw new Error(downloadResult.errMsg) } - const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id) - await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true) + const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id) + await sendMsg({ + chatType: ChatType.group, + peerUid: payload.group_id?.toString()!, + }, [sendFileEle], [], true) return null } } diff --git a/src/onebot11/action/group/GetGroupInfo.ts b/src/onebot11/action/group/GetGroupInfo.ts index 8bfc219..d251514 100644 --- a/src/onebot11/action/group/GetGroupInfo.ts +++ b/src/onebot11/action/group/GetGroupInfo.ts @@ -1,18 +1,18 @@ import { OB11Group } from '../../types' -import { getGroup } from '../../../common/data' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' +import { NTQQGroupApi } from '@/ntqqapi/api' -interface PayloadType { - group_id: number +interface Payload { + group_id: number | string } -class GetGroupInfo extends BaseAction { +class GetGroupInfo extends BaseAction { actionName = ActionName.GetGroupInfo - protected async _handle(payload: PayloadType) { - const group = await getGroup(payload.group_id.toString()) + protected async _handle(payload: Payload) { + const group = (await NTQQGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString()) if (group) { return OB11Constructor.group(group) } else { diff --git a/src/onebot11/action/group/GetGroupList.ts b/src/onebot11/action/group/GetGroupList.ts index 8d1e0be..b071085 100644 --- a/src/onebot11/action/group/GetGroupList.ts +++ b/src/onebot11/action/group/GetGroupList.ts @@ -1,10 +1,8 @@ import { OB11Group } from '../../types' import { OB11Constructor } from '../../constructor' -import { groups } from '../../../common/data' import BaseAction from '../BaseAction' import { ActionName } from '../types' import { NTQQGroupApi } from '../../../ntqqapi/api' -import { log } from '../../../common/utils' interface Payload { no_cache: boolean | string @@ -14,14 +12,8 @@ class GetGroupList extends BaseAction { actionName = ActionName.GetGroupList protected async _handle(payload: Payload) { - if (groups.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { - try { - const groups = await NTQQGroupApi.getGroups(true) - log('强制刷新群列表, 数量:', groups.length) - return OB11Constructor.groups(groups) - } catch (e) {} - } - return OB11Constructor.groups(groups) + const groupList = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload.no_cache === 'true') + return OB11Constructor.groups(groupList) } } diff --git a/src/onebot11/action/group/GetGroupMemberList.ts b/src/onebot11/action/group/GetGroupMemberList.ts index 86e4456..507fb67 100644 --- a/src/onebot11/action/group/GetGroupMemberList.ts +++ b/src/onebot11/action/group/GetGroupMemberList.ts @@ -1,31 +1,59 @@ import { OB11GroupMember } from '../../types' -import { getGroup } from '../../../common/data' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQGroupApi } from '../../../ntqqapi/api/group' -import { log } from '../../../common/utils' +import { NTQQGroupApi, WebApi } from '@/ntqqapi/api' +import { getSelfUid } from '@/common/data' -export interface PayloadType { - group_id: number +interface Payload { + group_id: number | string no_cache: boolean | string } -class GetGroupMemberList extends BaseAction { +class GetGroupMemberList extends BaseAction { actionName = ActionName.GetGroupMemberList - protected async _handle(payload: PayloadType) { - const group = await getGroup(payload.group_id.toString()) - if (group) { - if (!group.members?.length || payload.no_cache === true || payload.no_cache === 'true') { - const members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) - group.members = Array.from(members.values()) - log('强制刷新群成员列表, 数量: ', group.members.length) - } - return OB11Constructor.groupMembers(group) - } else { - throw `群${payload.group_id}不存在` + protected async _handle(payload: Payload) { + //const isNocache = payload.no_cache == true || payload.no_cache === 'true' //已强制无缓存 + const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) + const groupMembersArr = Array.from(groupMembers.values()) + + let _groupMembers = groupMembersArr.map(item => { + return OB11Constructor.groupMember(payload.group_id.toString(), item) + }) + + const MemberMap: Map = new Map() + const date = Math.round(Date.now() / 1000) + + for (let i = 0, len = _groupMembers.length; i < len; i++) { + // 保证基础数据有这个 同时避免群管插件过于依赖这个杀了 + _groupMembers[i].join_time = date + _groupMembers[i].last_sent_time = date + MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]) } + + const selfRole = groupMembers.get(getSelfUid())?.role + const isPrivilege = selfRole === 3 || selfRole === 4 + + if (isPrivilege) { + const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()) + for (let i = 0, len = webGroupMembers.length; i < len; i++) { + if (!webGroupMembers[i]?.uin) { + continue + } + const MemberData = MemberMap.get(webGroupMembers[i]?.uin) + if (MemberData) { + MemberData.join_time = webGroupMembers[i]?.join_time + MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time + MemberData.qage = webGroupMembers[i]?.qage + MemberData.level = webGroupMembers[i]?.lv.level.toString() + MemberMap.set(webGroupMembers[i]?.uin, MemberData) + } + } + } + + _groupMembers = Array.from(MemberMap.values()) + return _groupMembers } } diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index a08c3d5..f7f9b7d 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -6,7 +6,7 @@ import { RawMessage, SendMessageElement, } from '@/ntqqapi/types' -import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '@/common/data' +import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data' import { OB11MessageCustomMusic, OB11MessageData, @@ -305,10 +305,9 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode) // This redundant design of Ob11 here should be blamed. if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) { - const group = (await getGroup(payload.group_id))! // checked before return { chatType: ChatType.group, - peerUid: group.groupCode + peerUid: payload.group_id.toString(), } } if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) { @@ -318,7 +317,7 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode) return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid: Uid!, - guildId: payload.group_id || ''//临时主动发起时需要传入群号 + guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号 } } throw '请指定 group_id 或 user_id' @@ -343,12 +342,6 @@ export class SendMsg extends BaseAction { message: '音乐消息不可以和其他消息混在一起发送', } } - if (payload.message_type !== 'private' && payload.group_id && !(await getGroup(payload.group_id))) { - return { - valid: false, - message: `群${payload.group_id}不存在`, - } - } if (payload.user_id && payload.message_type !== 'group') { const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) const isBuddy = await NTQQFriendApi.isBuddy(uid!) diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index c94335f..e7982de 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -17,7 +17,6 @@ import { Group, Peer, GroupMember, - PicType, RawMessage, SelfInfo, Sex, @@ -26,7 +25,7 @@ import { FriendV2, ChatType2 } from '../ntqqapi/types' -import { deleteGroup, getGroupMember, getSelfUin } from '../common/data' +import { getGroupMember, getSelfUin } from '../common/data' import { EventType } from './event/OB11BaseEvent' import { encodeCQCode } from './cqcode' import { MessageUnique } from '../common/utils/MessageUnique' @@ -358,7 +357,7 @@ export class OB11Constructor { const groupElement = grayTipElement?.groupElement if (groupElement) { // log("收到群提示消息", groupElement) - if (groupElement.type == TipGroupElementType.memberIncrease) { + if (groupElement.type === TipGroupElementType.memberIncrease) { log('收到群成员增加消息', groupElement) await sleep(1000) const member = await getGroupMember(msg.peerUid, groupElement.memberUid) @@ -406,25 +405,26 @@ export class OB11Constructor { ) } } - else if (groupElement.type == TipGroupElementType.kicked) { + else if (groupElement.type === TipGroupElementType.kicked) { log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) - deleteGroup(msg.peerUid) NTQQGroupApi.quitGroup(msg.peerUid).then() - const selfUin = getSelfUin() try { - const adminUin = - (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || - (await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin + const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await NTQQUserApi.getUidByUin(groupElement.adminUid)) if (adminUin) { return new OB11GroupDecreaseEvent( parseInt(msg.peerUid), - parseInt(selfUin), + parseInt(getSelfUin()), parseInt(adminUin), - 'kick_me', + 'kick_me' ) } } catch (e) { - return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfUin), 0, 'leave') + return new OB11GroupDecreaseEvent( + parseInt(msg.peerUid), + parseInt(getSelfUin()), + 0, + 'leave' + ) } } } @@ -677,7 +677,7 @@ export class OB11Constructor { sex: OB11Constructor.sex(member.sex!), age: 0, area: '', - level: 0, + level: '0', qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0, join_time: 0, // 暂时没法获取 last_sent_time: 0, // 暂时没法获取 diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index 789e64f..a771f42 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -36,7 +36,7 @@ export interface OB11GroupMember { age?: number join_time?: number last_sent_time?: number - level?: number + level?: string qq_level?: number role?: OB11GroupMemberRole title?: string @@ -48,6 +48,7 @@ export interface OB11GroupMember { shut_up_timestamp?: number // 以下为扩展字段 is_robot?: boolean + qage?: number } export interface OB11Group { From 0d7aa9bd2cac84f27ee890bb631eff15b627e4e3 Mon Sep 17 00:00:00 2001 From: idranme Date: Fri, 16 Aug 2024 21:28:43 +0800 Subject: [PATCH 11/23] fix --- src/common/types.ts | 12 ++++ src/common/utils/MessageUnique.ts | 25 +++++--- src/main/main.ts | 16 +++-- src/ntqqapi/api/group.ts | 34 ++++++++++- src/onebot11/action/file/GetFile.ts | 60 +----------------- src/onebot11/action/go-cqhttp/DelGroupFile.ts | 17 ++++++ src/onebot11/action/index.ts | 6 +- src/onebot11/action/types.ts | 1 + src/onebot11/constructor.ts | 61 +++++++++++++------ 9 files changed, 134 insertions(+), 98 deletions(-) create mode 100644 src/onebot11/action/go-cqhttp/DelGroupFile.ts diff --git a/src/common/types.ts b/src/common/types.ts index e7612b4..94987de 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -50,3 +50,15 @@ export interface FileCache { elementId: string elementType: number } + +export interface FileCacheV2 { + fileName: string + fileSize: string + fileUuid: string + msgId: string + msgTime: number + peerUid: string + chatType: number + elementId: string + elementType: number +} \ No newline at end of file diff --git a/src/common/utils/MessageUnique.ts b/src/common/utils/MessageUnique.ts index 9fd3873..27b882e 100644 --- a/src/common/utils/MessageUnique.ts +++ b/src/common/utils/MessageUnique.ts @@ -7,7 +7,7 @@ import SQLite from '@minatojs/driver-sqlite' import fsPromise from 'node:fs/promises' import fs from 'node:fs' import path from 'node:path' -import { FileCache } from '../types' +import { FileCacheV2 } from '../types' interface SQLiteTables extends Tables { message: { @@ -16,7 +16,7 @@ interface SQLiteTables extends Tables { chatType: number peerUid: string } - file: FileCache + file_v2: FileCacheV2 } interface MsgIdAndPeerByShortId { @@ -52,16 +52,19 @@ class MessageUniqueWrapper { }, { primary: 'shortId' }) - database.extend('file', { + database.extend('file_v2', { fileName: 'string', fileSize: 'string', + fileUuid: 'string(128)', msgId: 'string(24)', + msgTime: 'unsigned(10)', peerUid: 'string(24)', chatType: 'unsigned', elementId: 'string(24)', elementType: 'unsigned', }, { - primary: 'fileName' + primary: 'fileUuid', + indexes: ['fileName'] }) this.db = database } @@ -142,12 +145,18 @@ class MessageUniqueWrapper { this.msgDataMap.resize(maxSize) } - addFileCache(data: FileCache) { - return this.db?.upsert('file', [data], 'fileName') + addFileCache(data: FileCacheV2) { + return this.db?.upsert('file_v2', [data], 'fileUuid') } - getFileCache(fileName: string) { - return this.db?.get('file', { fileName }) + getFileCacheByName(fileName: string) { + return this.db?.get('file_v2', { fileName }, { + sort: { msgTime: 'desc' } + }) + } + + getFileCacheById(fileUuid: string) { + return this.db?.get('file_v2', { fileUuid }) } } diff --git a/src/main/main.ts b/src/main/main.ts index 1ad1773..5bf644f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -27,7 +27,7 @@ import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, import { OB11Constructor } from '../onebot11/constructor' import { FriendRequestNotify, - GroupNotifies, + GroupNotify, GroupNotifyTypes, RawMessage, BuddyReqType, @@ -244,6 +244,7 @@ function onLoad() { log('report self message error: ', e.stack.toString()) } }) + const processedGroupNotify: string[] = [] registerReceiveHook<{ doubt: boolean oldestUnreadSeq: string @@ -251,26 +252,23 @@ function onLoad() { }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => { if (payload.unreadCount) { // log("开始获取群通知详情") - let notify: GroupNotifies + let notifies: GroupNotify[] try { - notify = await NTQQGroupApi.getGroupNotifies() + notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount) } catch (e) { // log("获取群通知详情失败", e); return } - const notifies = notify.notifies.slice(0, payload.unreadCount) - // log("获取群通知详情完成", notifies, payload); - for (const notify of notifies) { try { notify.time = Date.now() const notifyTime = parseInt(notify.seq) / 1000 - if (notifyTime < startTime) { + const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type + if (notifyTime < startTime || processedGroupNotify.includes(flag)) { continue } - log('收到群通知', notify) - const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type + processedGroupNotify.push(flag) if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { log('有成员退出通知', notify) const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))! diff --git a/src/ntqqapi/api/group.ts b/src/ntqqapi/api/group.ts index 3567716..2e39155 100644 --- a/src/ntqqapi/api/group.ts +++ b/src/ntqqapi/api/group.ts @@ -1,5 +1,5 @@ import { ReceiveCmdS } from '../hook' -import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } from '../types' +import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { NTQQWindowApi, NTQQWindows } from './window' import { getSession } from '../wrapper' @@ -52,7 +52,7 @@ export class NTQQGroupApi { return groupList } - async getGroupMemberV2(GroupCode: string, uid: string, forced = false) { + static async getGroupMemberV2(GroupCode: string, uid: string, forced = false) { type ListenerType = NodeIKernelGroupListener['onMemberInfoChange'] type EventType = NodeIKernelGroupService['getMemberInfo'] const [, , , _members] = await NTEventDispatch.CallNormalEvent @@ -118,6 +118,36 @@ export class NTQQGroupApi { ) } + static async getSingleScreenNotifies(num: number) { + const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent + <(arg1: boolean, arg2: string, arg3: number) => Promise, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void> + ( + 'NodeIKernelGroupService/getSingleScreenNotifies', + 'NodeIKernelGroupListener/onGroupSingleScreenNotifies', + 1, + 5000, + () => true, + false, + '', + num, + ) + return notifies + } + + static async delGroupFile(groupCode: string, files: string[]) { + const session = getSession() + return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)! + } + + static DelGroupFile = NTQQGroupApi.delGroupFile + + static async delGroupFileFolder(groupCode: string, folderId: string) { + const session = getSession() + return session?.getRichMediaService().deleteGroupFolder(groupCode, folderId)! + } + + static DelGroupFileFolder = NTQQGroupApi.delGroupFileFolder + static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { const flagitem = flag.split('|') const groupCode = flagitem[0] diff --git a/src/onebot11/action/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index 11fff91..fbbda7d 100644 --- a/src/onebot11/action/file/GetFile.ts +++ b/src/onebot11/action/file/GetFile.ts @@ -23,66 +23,12 @@ export abstract class GetFileBase extends BaseAction { const { enableLocalFile2Url } = getConfigUtil().getConfig() - let UuidData: { - high: string - low: string - } | undefined - try { - UuidData = UUIDConverter.decode(payload.file) - if (UuidData) { - const peerUin = UuidData.high - const msgId = UuidData.low - const isGroup: boolean = !!(await NTQQGroupApi.getGroups(false)).find(e => e.groupCode == peerUin) - let peer: Peer | undefined - //识别Peer - if (isGroup) { - peer = { chatType: ChatType.group, peerUid: peerUin } - } - const PeerUid = await NTQQUserApi.getUidByUinV2(peerUin) - if (PeerUid) { - const isBuddy = await NTQQFriendApi.isBuddy(PeerUid) - if (isBuddy) { - peer = { chatType: ChatType.friend, peerUid: PeerUid } - } else { - peer = { chatType: ChatType.temp, peerUid: PeerUid } - } - } - if (!peer) { - throw new Error('chattype not support') - } - const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId]) - if (msgList.msgList.length === 0) { - throw new Error('msg not found') - } - const msg = msgList.msgList[0] - const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT) - if (!findEle) { - throw new Error('element not found') - } - const downloadPath = await NTQQFileApi.downloadMedia(msgId, msg.chatType, msg.peerUid, findEle.elementId, '', '') - const fileSize = findEle?.videoElement?.fileSize || findEle?.fileElement?.fileSize || findEle?.pttElement?.fileSize || '0' - const fileName = findEle?.videoElement?.fileName || findEle?.fileElement?.fileName || findEle?.pttElement?.fileName || '' - const res: GetFileResponse = { - file: downloadPath, - url: downloadPath, - file_size: fileSize, - file_name: fileName, - } - if (enableLocalFile2Url && downloadPath) { - try { - res.base64 = await fsPromise.readFile(downloadPath, 'base64') - } catch (e) { - throw new Error('文件下载失败. ' + e) - } - } - //不手动删除?文件持久化了 - return res - } - } catch { + let fileCache = await MessageUnique.getFileCacheById(String(payload.file)) + if (!fileCache?.length) { + fileCache = await MessageUnique.getFileCacheByName(String(payload.file)) } - const fileCache = await MessageUnique.getFileCache(String(payload.file)) if (fileCache?.length) { const downloadPath = await NTQQFileApi.downloadMedia( fileCache[0].msgId, diff --git a/src/onebot11/action/go-cqhttp/DelGroupFile.ts b/src/onebot11/action/go-cqhttp/DelGroupFile.ts new file mode 100644 index 0000000..581ff4f --- /dev/null +++ b/src/onebot11/action/go-cqhttp/DelGroupFile.ts @@ -0,0 +1,17 @@ +import BaseAction from '../BaseAction' +import { ActionName } from '../types' +import { NTQQGroupApi } from '@/ntqqapi/api' + +interface Payload { + group_id: string | number + file_id: string + busid?: number +} + +export class GoCQHTTPDelGroupFile extends BaseAction { + actionName = ActionName.GoCQHTTP_DelGroupFile + + async _handle(payload: Payload) { + await NTQQGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id]) + } +} \ No newline at end of file diff --git a/src/onebot11/action/index.ts b/src/onebot11/action/index.ts index a0a8b02..6b6d8d9 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -1,6 +1,6 @@ import GetMsg from './msg/GetMsg' import GetLoginInfo from './system/GetLoginInfo' -import { GetFriendList, GetFriendWithCategory} from './user/GetFriendList' +import { GetFriendList, GetFriendWithCategory } from './user/GetFriendList' import GetGroupList from './group/GetGroupList' import GetGroupInfo from './group/GetGroupInfo' import GetGroupMemberList from './group/GetGroupMemberList' @@ -53,6 +53,7 @@ import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation' import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg' import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg' import GetEvent from './llonebot/GetEvent' +import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile' export const actionHandlers = [ @@ -113,7 +114,8 @@ export const actionHandlers = [ new GoCQHTTGetForwardMsgAction(), new GoCQHTTHandleQuickOperation(), new GoCQHTTPSetEssenceMsg(), - new GoCQHTTPDelEssenceMsg() + new GoCQHTTPDelEssenceMsg(), + new GoCQHTTPDelGroupFile() ] function initActionMap() { diff --git a/src/onebot11/action/types.ts b/src/onebot11/action/types.ts index 88cdd0f..71730ea 100644 --- a/src/onebot11/action/types.ts +++ b/src/onebot11/action/types.ts @@ -73,4 +73,5 @@ export enum ActionName { GetGroupHonorInfo = "get_group_honor_info", GoCQHTTP_SetEssenceMsg = 'set_essence_msg', GoCQHTTP_DelEssenceMsg = 'delete_essence_msg', + GoCQHTTP_DelGroupFile = 'delete_group_file', } diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index e7982de..2c399cb 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -29,7 +29,6 @@ import { getGroupMember, getSelfUin } from '../common/data' import { EventType } from './event/OB11BaseEvent' import { encodeCQCode } from './cqcode' import { MessageUnique } from '../common/utils/MessageUnique' -import { UUIDConverter } from '../common/utils/helper' import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent' import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' @@ -191,41 +190,61 @@ export class OB11Constructor { }*/ message_data['data']['file'] = picElement.fileName message_data['data']['subType'] = picElement.picSubType - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) + //message_data['data']['file_id'] = picElement.fileUuid message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement) message_data['data']['file_size'] = picElement.fileSize MessageUnique.addFileCache({ peerUid: msg.peerUid, msgId: msg.msgId, + msgTime: +msg.msgTime, chatType: msg.chatType, elementId: element.elementId, elementType: element.elementType, fileName: picElement.fileName, fileSize: String(picElement.fileSize || '0'), + fileUuid: picElement.fileUuid }) } - else if (element.videoElement || element.fileElement) { - const videoOrFileElement = element.videoElement || element.fileElement - message_data['type'] = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file - message_data['data']['file'] = videoOrFileElement.fileName - message_data['data']['path'] = videoOrFileElement.filePath - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) - message_data['data']['file_size'] = videoOrFileElement.fileSize - if (element.videoElement) { - message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ - chatType: msg.chatType, - peerUid: msg.peerUid, - }, msg.msgId, element.elementId, - ) - } + else if (element.videoElement) { + message_data['type'] = OB11MessageDataType.video + const { videoElement } = element + message_data['data']['file'] = videoElement.fileName + message_data['data']['path'] = videoElement.filePath + //message_data['data']['file_id'] = videoElement.fileUuid + message_data['data']['file_size'] = videoElement.fileSize + message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ + chatType: msg.chatType, + peerUid: msg.peerUid, + }, msg.msgId, element.elementId) MessageUnique.addFileCache({ peerUid: msg.peerUid, msgId: msg.msgId, + msgTime: +msg.msgTime, chatType: msg.chatType, elementId: element.elementId, elementType: element.elementType, - fileName: videoOrFileElement.fileName, - fileSize: String(videoOrFileElement.fileSize || '0') + fileName: videoElement.fileName, + fileSize: String(videoElement.fileSize || '0'), + fileUuid: videoElement.fileUuid! + }) + } + else if (element.fileElement) { + message_data['type'] = OB11MessageDataType.file + const { fileElement } = element + message_data['data']['file'] = fileElement.fileName + message_data['data']['path'] = fileElement.filePath + message_data['data']['file_id'] = fileElement.fileUuid + message_data['data']['file_size'] = fileElement.fileSize + MessageUnique.addFileCache({ + peerUid: msg.peerUid, + msgId: msg.msgId, + msgTime: +msg.msgTime, + chatType: msg.chatType, + elementId: element.elementId, + elementType: element.elementType, + fileName: fileElement.fileName, + fileSize: String(fileElement.fileSize || '0'), + fileUuid: fileElement.fileUuid! }) } else if (element.pttElement) { @@ -233,16 +252,18 @@ export class OB11Constructor { const { pttElement } = element message_data['data']['file'] = pttElement.fileName message_data['data']['path'] = pttElement.filePath - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) + //message_data['data']['file_id'] = pttElement.fileUuid message_data['data']['file_size'] = pttElement.fileSize MessageUnique.addFileCache({ peerUid: msg.peerUid, msgId: msg.msgId, + msgTime: +msg.msgTime, chatType: msg.chatType, elementId: element.elementId, elementType: element.elementType, fileName: pttElement.fileName, - fileSize: String(pttElement.fileSize || '0') + fileSize: String(pttElement.fileSize || '0'), + fileUuid: pttElement.fileUuid }) } else if (element.arkElement) { From 8542594181aa08fa3632618c233e936602da7f2a Mon Sep 17 00:00:00 2001 From: idranme Date: Fri, 16 Aug 2024 21:58:05 +0800 Subject: [PATCH 12/23] fix --- src/ntqqapi/types/group.ts | 5 ++++- src/onebot11/action/group/GetGroupMemberInfo.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ntqqapi/types/group.ts b/src/ntqqapi/types/group.ts index 4ba3e07..6512575 100644 --- a/src/ntqqapi/types/group.ts +++ b/src/ntqqapi/types/group.ts @@ -45,7 +45,7 @@ export enum GroupMemberRole { } export interface GroupMember { - memberSpecialTitle: string + memberSpecialTitle?: string avatarPath: string cardName: string cardType: number @@ -60,4 +60,7 @@ export interface GroupMember { isRobot: boolean sex?: Sex qqLevel?: QQLevel + isChangeRole: boolean + joinTime: string + lastSpeakTime: string } diff --git a/src/onebot11/action/group/GetGroupMemberInfo.ts b/src/onebot11/action/group/GetGroupMemberInfo.ts index cdd4a7d..2273f23 100644 --- a/src/onebot11/action/group/GetGroupMemberInfo.ts +++ b/src/onebot11/action/group/GetGroupMemberInfo.ts @@ -24,7 +24,11 @@ class GetGroupMemberInfo extends BaseAction { log('群成员详细信息结果', info) Object.assign(member, info) } - return OB11Constructor.groupMember(payload.group_id.toString(), member) + const ret = OB11Constructor.groupMember(payload.group_id.toString(), member) + const date = Math.round(Date.now() / 1000) + ret.last_sent_time = Number(member.lastSpeakTime || date) + ret.join_time = Number(member.joinTime || date) + return ret } else { throw `群成员${payload.user_id}不存在` } From 46b1e8e67d8658c711438dc95786f80d1aa5fcd7 Mon Sep 17 00:00:00 2001 From: idranme Date: Fri, 16 Aug 2024 22:25:17 +0800 Subject: [PATCH 13/23] chore: v3.29.3 --- manifest.json | 2 +- src/onebot11/action/go-cqhttp/DelGroupFile.ts | 2 +- src/onebot11/action/group/GetGroupMemberList.ts | 1 - src/version.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index b9fccf0..3009867 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.29.2", + "version": "3.29.3", "icon": "./icon.webp", "authors": [ { diff --git a/src/onebot11/action/go-cqhttp/DelGroupFile.ts b/src/onebot11/action/go-cqhttp/DelGroupFile.ts index 581ff4f..eddca67 100644 --- a/src/onebot11/action/go-cqhttp/DelGroupFile.ts +++ b/src/onebot11/action/go-cqhttp/DelGroupFile.ts @@ -5,7 +5,7 @@ import { NTQQGroupApi } from '@/ntqqapi/api' interface Payload { group_id: string | number file_id: string - busid?: number + busid?: 102 } export class GoCQHTTPDelGroupFile extends BaseAction { diff --git a/src/onebot11/action/group/GetGroupMemberList.ts b/src/onebot11/action/group/GetGroupMemberList.ts index 507fb67..9edd6f4 100644 --- a/src/onebot11/action/group/GetGroupMemberList.ts +++ b/src/onebot11/action/group/GetGroupMemberList.ts @@ -14,7 +14,6 @@ class GetGroupMemberList extends BaseAction { actionName = ActionName.GetGroupMemberList protected async _handle(payload: Payload) { - //const isNocache = payload.no_cache == true || payload.no_cache === 'true' //已强制无缓存 const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) const groupMembersArr = Array.from(groupMembers.values()) diff --git a/src/version.ts b/src/version.ts index d453805..34cd654 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.29.2' +export const version = '3.29.3' From 9353cb0432403c9e233146437431c4741c8f9e0d Mon Sep 17 00:00:00 2001 From: lin <102898587+gfhdhytghd@users.noreply.github.com> Date: Sat, 17 Aug 2024 14:21:27 +0800 Subject: [PATCH 14/23] Update LICENSE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改许可证以在法律层面上禁止宣传 --- LICENSE | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c4a1c29..15d641c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License +MIT Without Public Sicial Media Promotion License Copyright (c) 2024 LLOneBot @@ -19,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +You may use this software in accordance with the above terms, but you are not +allowed to promote this project or your projects based on this project on any +public social media. From f4c77f3e20436917f055a79e18549187efde31b3 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Sat, 17 Aug 2024 23:45:41 +0800 Subject: [PATCH 15/23] Fix: typo --- manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 3009867..5719fd8 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "type": "extension", "name": "LLOneBot", "slug": "LLOneBot", - "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", + "description": "实现 OneBot 11 协议,用于 QQ 机器人开发", "version": "3.29.3", "icon": "./icon.webp", "authors": [ @@ -30,4 +30,4 @@ "main": "./main/main.cjs", "preload": "./preload/preload.cjs" } -} \ No newline at end of file +} From 2245d0d3dec3f82c418937eb0173dffabbf7cba2 Mon Sep 17 00:00:00 2001 From: idranme Date: Sun, 18 Aug 2024 20:58:26 +0800 Subject: [PATCH 16/23] fix --- README.md | 2 +- scripts/gen-manifest.ts | 2 +- src/common/data.ts | 1 + src/common/utils/request.ts | 4 +- src/ntqqapi/api/webapi.ts | 72 ++++++++++--------- src/onebot11/action/OB11Response.ts | 2 +- .../action/group/GetGroupMemberInfo.ts | 38 ++++++---- src/onebot11/action/msg/SendMsg.ts | 8 +-- src/onebot11/server/post-ob11-event.ts | 6 +- src/onebot11/server/ws/reply.ts | 17 ++--- 10 files changed, 83 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index b08ecd5..353dbc7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LLOneBot -LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发 +LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发 > [!CAUTION]\ > **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** diff --git a/scripts/gen-manifest.ts b/scripts/gen-manifest.ts index fe48785..2794a65 100644 --- a/scripts/gen-manifest.ts +++ b/scripts/gen-manifest.ts @@ -6,7 +6,7 @@ const manifest = { type: 'extension', name: 'LLOneBot', slug: 'LLOneBot', - description: '实现 OneBot 11 协议,用以 QQ 机器人开发', + description: '实现 OneBot 11 协议,用于 QQ 机器人开发', version, icon: './icon.webp', authors: [ diff --git a/src/common/data.ts b/src/common/data.ts index 60b031e..ea9fcd2 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -66,6 +66,7 @@ export async function getGroupMember(groupCode: string | number, memberUinOrUid: let member = getMember() if (!member) { members = await NTQQGroupApi.getGroupMembers(groupCodeStr) + groupMembers.set(groupCodeStr, members) member = getMember() } return member diff --git a/src/common/utils/request.ts b/src/common/utils/request.ts index 0487ecc..0c471d4 100644 --- a/src/common/utils/request.ts +++ b/src/common/utils/request.ts @@ -16,7 +16,7 @@ export class RequestUtil { const redirectUrl = new URL(res.headers.location, url); RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => { // 合并重定向过程中的cookies - log('redirectCookies', redirectCookies) + //log('redirectCookies', redirectCookies) cookies = { ...cookies, ...redirectCookies }; resolve(cookies); }); @@ -33,7 +33,7 @@ export class RequestUtil { }); if (res.headers['set-cookie']) { // console.log(res.headers['set-cookie']); - log('set-cookie', url, res.headers['set-cookie']); + //log('set-cookie', url, res.headers['set-cookie']); res.headers['set-cookie'].forEach((cookie) => { const parts = cookie.split(';')[0].split('='); const key = parts[0]; diff --git a/src/ntqqapi/api/webapi.ts b/src/ntqqapi/api/webapi.ts index d3f1aaf..e3d2623 100644 --- a/src/ntqqapi/api/webapi.ts +++ b/src/ntqqapi/api/webapi.ts @@ -138,45 +138,47 @@ export class WebApi { return ret } - @CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members') static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise { - //logDebug('webapi 获取群成员', GroupCode) - let MemberData: Array = new Array() - try { - const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com') - const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') - const Bkn = WebApi.genBkn(CookiesObject.skey) - const retList: Promise[] = [] - const fastRet = await RequestUtil.HttpGetJson('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); - if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { - return [] - } else { - for (const key in fastRet.mems) { - MemberData.push(fastRet.mems[key]) - } + const memberData: Array = new Array() + const cookieObject = await NTQQUserApi.getCookies('qun.qq.com') + const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ') + const retList: Promise[] = [] + const params = new URLSearchParams({ + st: '0', + end: '40', + sort: '1', + gc: GroupCode, + bkn: WebApi.genBkn(cookieObject.skey) + }) + const fastRet = await RequestUtil.HttpGetJson(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr }) + if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { + return [] + } else { + for (const member of fastRet.mems) { + memberData.push(member) } - //初始化获取PageNum - const PageNum = Math.ceil(fastRet.count / 40) - //遍历批量请求 - for (let i = 2; i <= PageNum; i++) { - const ret: Promise = RequestUtil.HttpGetJson('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); - retList.push(ret) - } - //批量等待 - for (let i = 1; i <= PageNum; i++) { - const ret = await (retList[i]) - if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) { - continue - } - for (const key in ret.mems) { - MemberData.push(ret.mems[key]) - } - } - } catch { - return MemberData } - return MemberData + const pageNum = Math.ceil(fastRet.count / 40) + //遍历批量请求 + for (let i = 2; i <= pageNum; i++) { + params.set('st', String((i - 1) * 40)) + params.set('end', String(i * 40)) + const ret = RequestUtil.HttpGetJson(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr }) + retList.push(ret) + } + //批量等待 + for (let i = 1; i <= pageNum; i++) { + const ret = await (retList[i]) + if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) { + continue + } + for (const member of ret.mems) { + memberData.push(member) + } + } + return memberData } + // public static async addGroupDigest(groupCode: string, msgSeq: string) { // const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`; // const res = await this.request(url); diff --git a/src/onebot11/action/OB11Response.ts b/src/onebot11/action/OB11Response.ts index 25a394e..0ce6b81 100644 --- a/src/onebot11/action/OB11Response.ts +++ b/src/onebot11/action/OB11Response.ts @@ -10,7 +10,7 @@ export class OB11Response { data: data, message: message, wording: message, - echo: null, + echo: undefined, } } diff --git a/src/onebot11/action/group/GetGroupMemberInfo.ts b/src/onebot11/action/group/GetGroupMemberInfo.ts index 2273f23..e3cafd3 100644 --- a/src/onebot11/action/group/GetGroupMemberInfo.ts +++ b/src/onebot11/action/group/GetGroupMemberInfo.ts @@ -1,33 +1,45 @@ import { OB11GroupMember } from '../../types' -import { getGroupMember } from '../../../common/data' +import { getGroupMember, getSelfUid } from '@/common/data' import { OB11Constructor } from '../../constructor' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { NTQQUserApi } from '../../../ntqqapi/api/user' +import { NTQQUserApi, WebApi } from '@/ntqqapi/api' +import { isNull } from '@/common/utils/helper' import { log } from '../../../common/utils/log' -import { isNull } from '../../../common/utils/helper' -export interface PayloadType { - group_id: number - user_id: number +interface Payload { + group_id: number | string + user_id: number | string } -class GetGroupMemberInfo extends BaseAction { +class GetGroupMemberInfo extends BaseAction { actionName = ActionName.GetGroupMemberInfo - protected async _handle(payload: PayloadType) { + protected async _handle(payload: Payload) { const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) if (member) { if (isNull(member.sex)) { - log('获取群成员详细信息') - let info = await NTQQUserApi.getUserDetailInfo(member.uid, true) - log('群成员详细信息结果', info) + //log('获取群成员详细信息') + const info = await NTQQUserApi.getUserDetailInfo(member.uid, true) + //log('群成员详细信息结果', info) Object.assign(member, info) } const ret = OB11Constructor.groupMember(payload.group_id.toString(), member) + const self = await getGroupMember(payload.group_id.toString(), getSelfUid()) + if (self?.role === 3 || self?.role === 4) { + const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()) + const target = webGroupMembers.find(e => e?.uin && e.uin === ret.user_id) + log(target) + if (target) { + ret.join_time = target.join_time + ret.last_sent_time = target.last_speak_time + ret.qage = target.qage + ret.level = target.lv.level.toString() + } + } const date = Math.round(Date.now() / 1000) - ret.last_sent_time = Number(member.lastSpeakTime || date) - ret.join_time = Number(member.joinTime || date) + ret.last_sent_time ||= Number(member.lastSpeakTime || date) + ret.join_time ||= Number(member.joinTime || date) return ret } else { throw `群成员${payload.user_id}不存在` diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index f7f9b7d..a03d04b 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -289,12 +289,12 @@ export async function sendMsg( log('文件大小计算失败', e, fileElement) } } - log('发送消息总大小', totalSize, 'bytes') - let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s - log('设置消息超时时间', timeout) + //log('发送消息总大小', totalSize, 'bytes') + const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s ) + //log('设置消息超时时间', timeout) const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) - log('消息发送结果', returnMsg) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) + log('消息发送', returnMsg.msgShortId) deleteAfterSentFiles.map(path => fsPromise.unlink(path)) return returnMsg } diff --git a/src/onebot11/server/post-ob11-event.ts b/src/onebot11/server/post-ob11-event.ts index f979dd4..7ebc1d4 100644 --- a/src/onebot11/server/post-ob11-event.ts +++ b/src/onebot11/server/post-ob11-event.ts @@ -61,13 +61,15 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t body: msgStr, }).then( async (res) => { - log(`新消息事件HTTP上报成功: ${host} `, msgStr) + if (msg.post_type) { + log(`HTTP 事件上报: ${host} `, msg.post_type) + } try { const resJson = await res.json() log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson)) handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log); } catch (e) { - log(`新消息事件HTTP上报没有返回快速操作,不需要处理`) + //log(`新消息事件HTTP上报没有返回快速操作,不需要处理`) return } }, diff --git a/src/onebot11/server/ws/reply.ts b/src/onebot11/server/ws/reply.ts index 7837a2c..2be847b 100644 --- a/src/onebot11/server/ws/reply.ts +++ b/src/onebot11/server/ws/reply.ts @@ -1,18 +1,15 @@ import { WebSocket as WebSocketClass } from 'ws' -import { OB11Response } from '../../action/OB11Response' import { PostEventType } from '../post-ob11-event' -import { log } from '../../../common/utils/log' -import { isNull } from '../../../common/utils/helper' +import { log } from '@/common/utils/log' +import { OB11Return } from '../../types' -export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) { +export function wsReply(wsClient: WebSocketClass, data: OB11Return | PostEventType) { try { - const packet = Object.assign({}, data) - if (isNull(packet['echo'])) { - delete packet['echo'] + wsClient.send(JSON.stringify(data)) + if (data['post_type']) { + log('WebSocket 事件上报', wsClient.url ?? '', data['post_type']) } - wsClient.send(JSON.stringify(packet)) - //log('ws 消息上报', wsClient.url || '', data) } catch (e: any) { - log('websocket 回复失败', e.stack, data) + log('WebSocket 上报失败', e.stack, data) } } From e912911dd89dbe1be520cbdf51498dd81accce51 Mon Sep 17 00:00:00 2001 From: idranme Date: Sun, 18 Aug 2024 21:04:30 +0800 Subject: [PATCH 17/23] chore: v3.29.4 --- manifest.json | 4 ++-- src/onebot11/action/group/GetGroupMemberInfo.ts | 2 -- src/version.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/manifest.json b/manifest.json index 5719fd8..0330437 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发", - "version": "3.29.3", + "version": "3.29.4", "icon": "./icon.webp", "authors": [ { @@ -30,4 +30,4 @@ "main": "./main/main.cjs", "preload": "./preload/preload.cjs" } -} +} \ No newline at end of file diff --git a/src/onebot11/action/group/GetGroupMemberInfo.ts b/src/onebot11/action/group/GetGroupMemberInfo.ts index e3cafd3..d7b2a10 100644 --- a/src/onebot11/action/group/GetGroupMemberInfo.ts +++ b/src/onebot11/action/group/GetGroupMemberInfo.ts @@ -5,7 +5,6 @@ import BaseAction from '../BaseAction' import { ActionName } from '../types' import { NTQQUserApi, WebApi } from '@/ntqqapi/api' import { isNull } from '@/common/utils/helper' -import { log } from '../../../common/utils/log' interface Payload { group_id: number | string @@ -29,7 +28,6 @@ class GetGroupMemberInfo extends BaseAction { if (self?.role === 3 || self?.role === 4) { const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()) const target = webGroupMembers.find(e => e?.uin && e.uin === ret.user_id) - log(target) if (target) { ret.join_time = target.join_time ret.last_sent_time = target.last_speak_time diff --git a/src/version.ts b/src/version.ts index 34cd654..8bf09f6 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.29.3' +export const version = '3.29.4' From 1e579858b8011cf424969984b29cdc06cd06de54 Mon Sep 17 00:00:00 2001 From: yota <610991622@qq.com> Date: Mon, 19 Aug 2024 09:47:24 +0800 Subject: [PATCH 18/23] =?UTF-8?q?ws=E4=BF=AE=E5=A4=8D=E5=BF=85=E9=A1=BBno?= =?UTF-8?q?=5Fcache=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/onebot11/action/group/GetGroupList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onebot11/action/group/GetGroupList.ts b/src/onebot11/action/group/GetGroupList.ts index b071085..08c5775 100644 --- a/src/onebot11/action/group/GetGroupList.ts +++ b/src/onebot11/action/group/GetGroupList.ts @@ -12,7 +12,7 @@ class GetGroupList extends BaseAction { actionName = ActionName.GetGroupList protected async _handle(payload: Payload) { - const groupList = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload.no_cache === 'true') + const groupList = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true') return OB11Constructor.groups(groupList) } } From ff189378283b9cebadef3688d4b642f25f49c4bb Mon Sep 17 00:00:00 2001 From: idranme Date: Mon, 19 Aug 2024 17:29:58 +0800 Subject: [PATCH 19/23] fix --- package.json | 2 +- src/common/config.ts | 4 +--- src/ntqqapi/hook.ts | 16 ++++++++-------- src/ntqqapi/ntcall.ts | 27 +++------------------------ 4 files changed, 13 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index c269e75..2898750 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/fluent-ffmpeg": "^2.1.25", "@types/node": "^20.14.15", "@types/ws": "^8.5.12", - "electron": "^29.1.4", + "electron": "^31.4.0", "electron-vite": "^2.3.0", "typescript": "^5.5.4", "vite": "^5.4.1", diff --git a/src/common/config.ts b/src/common/config.ts index ce2250f..df571ff 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -5,9 +5,7 @@ import path from 'node:path' import { getSelfUin } from './data' import { DATA_DIR } from './utils' -export const HOOK_LOG = false - -export const ALLOW_SEND_TEMP_MSG = false +//export const HOOK_LOG = false export class ConfigUtil { private readonly configPath: string diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index aa107cc..deccbb6 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -16,7 +16,7 @@ import { setSelfInfo } from '@/common/data' import { postOb11Event } from '../onebot11/server/post-ob11-event' -import { getConfigUtil, HOOK_LOG } from '@/common/config' +import { getConfigUtil } from '@/common/config' import fs from 'node:fs' import { log } from '@/common/utils' import { randomUUID } from 'node:crypto' @@ -81,7 +81,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) { const originalSend = window.webContents.send const patchSend = (channel: string, ...args: NTQQApiReturnData) => { // console.log("hookNTQQApiReceive", channel, args) - let isLogger = false + /*let isLogger = false try { isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') } catch (e) { } @@ -91,7 +91,7 @@ export function hookNTQQApiReceive(window: BrowserWindow) { } catch (e) { log('hook log error', e, args) } - } + }*/ try { if (args?.[1] instanceof Array) { for (let receiveData of args?.[1]) { @@ -145,9 +145,9 @@ export function hookNTQQApiCall(window: BrowserWindow) { isLogger = args[3][0].eventName.startsWith('ns-LoggerApi') } catch (e) { } if (!isLogger) { - try { + /*try { HOOK_LOG && log('call NTQQ api', thisArg, args) - } catch (e) { } + } catch (e) { }*/ try { const _args: unknown[] = args[3][1] const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod @@ -181,16 +181,16 @@ export function hookNTQQApiCall(window: BrowserWindow) { const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { apply(target, thisArg, args) { // console.log(args); - HOOK_LOG && log('call NTQQ invoke api', thisArg, args) + //HOOK_LOG && log('call NTQQ invoke api', thisArg, args) args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], { apply(sendtarget, sendthisArg, sendargs) { sendtarget.apply(sendthisArg, sendargs) }, }) let ret = target.apply(thisArg, args) - try { + /*try { HOOK_LOG && log('call NTQQ invoke api return', ret) - } catch (e) { } + } catch (e) { }*/ return ret }, }) diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index aed5651..9fec4d2 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -1,7 +1,6 @@ import { ipcMain } from 'electron' import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook' import { log } from '../common/utils/log' -import { HOOK_LOG } from '../common/config' import { randomUUID } from 'node:crypto' export enum NTQQApiClass { @@ -15,6 +14,7 @@ export enum NTQQApiClass { SKEY_API = 'ns-SkeyApi', GROUP_HOME_WORK = 'ns-GroupHomeWork', GROUP_ESSENCE = 'ns-GroupEssence', + NODE_STORE_API = 'ns-NodeStoreApi' } export enum NTQQApiMethod { @@ -129,7 +129,7 @@ export function callNTQQApi(params: NTQQApiParams) { timeout = timeout ?? 5 afterFirstCmd = afterFirstCmd ?? true const uuid = randomUUID() - HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid) + //HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid) return new Promise((resolve: (data: ReturnType) => void, reject) => { // log("callNTQQApiPromise", channel, className, methodName, args, uuid) const _timeout = timeout * 1000 @@ -202,25 +202,4 @@ export function callNTQQApi(params: NTQQApiParams) { export interface GeneralCallResult { result: number // 0: success errMsg: string -} - -export class NTQQApi { - static async call(className: NTQQApiClass, cmdName: string, args: any[]) { - return await callNTQQApi({ - className, - methodName: cmdName, - args: [...args], - }) - } - - static async fetchUnitedCommendConfig() { - return await callNTQQApi({ - methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG, - args: [ - { - groups: ['100243'], - }, - ], - }) - } -} +} \ No newline at end of file From 3b3795c946256dc81de3382c5ed74c4a18193ded Mon Sep 17 00:00:00 2001 From: idranme Date: Mon, 19 Aug 2024 17:38:42 +0800 Subject: [PATCH 20/23] chore: v3.29.5 --- manifest.json | 2 +- scripts/test/test_db.ts | 17 ----------------- src/version.ts | 2 +- 3 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 scripts/test/test_db.ts diff --git a/manifest.json b/manifest.json index 0330437..7609d6d 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发", - "version": "3.29.4", + "version": "3.29.5", "icon": "./icon.webp", "authors": [ { diff --git a/scripts/test/test_db.ts b/scripts/test/test_db.ts deleted file mode 100644 index 5b7444c..0000000 --- a/scripts/test/test_db.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Level } from 'level' - -const db = new Level(process.env['level_db_path'] as string, { valueEncoding: 'json' }) - -async function getGroupNotify() { - let keys = await db.keys().all() - let result: string[] = [] - for (const key of keys) { - // console.log(key) - if (key.startsWith('group_notify_')) { - result.push(key) - } - } - return result -} - -getGroupNotify().then(console.log) diff --git a/src/version.ts b/src/version.ts index 8bf09f6..44fcdcd 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.29.4' +export const version = '3.29.5' From c37858e2f94d366536d6d78f2584ac7321e9ec0a Mon Sep 17 00:00:00 2001 From: idranme Date: Tue, 20 Aug 2024 21:13:27 +0800 Subject: [PATCH 21/23] opt --- src/common/data.ts | 3 +- src/common/server/http.ts | 2 +- src/main/main.ts | 12 +---- src/ntqqapi/hook.ts | 75 +++++++++++--------------- src/onebot11/action/msg/SendMsg.ts | 8 ++- src/onebot11/server/post-ob11-event.ts | 5 +- 6 files changed, 46 insertions(+), 59 deletions(-) diff --git a/src/common/data.ts b/src/common/data.ts index ea9fcd2..06aa569 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -108,11 +108,10 @@ export function getSelfUin() { } const messages: Map = new Map() -let expire: number /** 缓存近期消息内容 */ export async function addMsgCache(msg: RawMessage) { - expire ??= getConfigUtil().getConfig().msgCacheExpire! * 1000 + const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000 if (expire === 0) { return } diff --git a/src/common/server/http.ts b/src/common/server/http.ts index 550522f..3e5ba65 100644 --- a/src/common/server/http.ts +++ b/src/common/server/http.ts @@ -100,7 +100,7 @@ export abstract class HttpServerBase { } else if (req.query) { payload = { ...req.query, ...req.body } } - log('收到http请求', url, payload) + log('收到 HTTP 请求', url, payload) try { res.send(await handler(res, payload)) } catch (e: any) { diff --git a/src/main/main.ts b/src/main/main.ts index 5bf644f..dc14897 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -149,7 +149,6 @@ function onLoad() { const { debug, reportSelfMessage } = getConfigUtil().getConfig() for (let message of msgList) { // 过滤启动之前的消息 - // log('收到新消息', message); if (parseInt(message.msgTime) < startTime / 1000) { continue } @@ -190,13 +189,6 @@ function onLoad() { postOb11Event(privateEvent) } }) - // OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => { - // log(message) - // if (friendAddEvent) { - // // log("post friend add event", friendAddEvent); - // postOb11Event(friendAddEvent) - // } - // }) } } @@ -376,7 +368,7 @@ function onLoad() { let startTime = 0 // 毫秒 async function start(uid: string, uin: string) { - log('llonebot pid', process.pid) + log('process pid', process.pid) const config = getConfigUtil().getConfig() if (!config.enableLLOB) { llonebotError.otherError = 'LLOneBot 未启动' @@ -391,7 +383,7 @@ function onLoad() { NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! }) MessageUnique.init(uin) - log('start activate group member info') + //log('start activate group member info') // 下面两个会导致CPU占用过高,QQ卡死 // NTQQGroupApi.activateMemberInfoChange().then().catch(log) // NTQQGroupApi.activateMemberListChange().then().catch(log) diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index deccbb6..cef1672 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -80,52 +80,41 @@ let callHooks: Array<{ export function hookNTQQApiReceive(window: BrowserWindow) { const originalSend = window.webContents.send const patchSend = (channel: string, ...args: NTQQApiReturnData) => { - // console.log("hookNTQQApiReceive", channel, args) - /*let isLogger = false - try { - isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') - } catch (e) { } - if (!isLogger) { - try { - HOOK_LOG && log(`received ntqq api message: ${channel}`, args) - } catch (e) { - log('hook log error', e, args) + /*try { + const isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') + if (!isLogger) { + log(`received ntqq api message: ${channel}`, args) } - }*/ - try { - if (args?.[1] instanceof Array) { - for (let receiveData of args?.[1]) { - const ntQQApiMethodName = receiveData.cmdName - // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) - for (let hook of receiveHooks) { - if (hook.method.includes(ntQQApiMethodName)) { - new Promise((resolve, reject) => { - try { - let _ = hook.hookFunc(receiveData.payload) - if (hook.hookFunc.constructor.name === 'AsyncFunction') { - ; (_ as Promise).then() - } - } catch (e: any) { - log('hook error', ntQQApiMethodName, e.stack.toString()) - } - }).then() - } + } catch { }*/ + if (args?.[1] instanceof Array) { + for (const receiveData of args?.[1]) { + const ntQQApiMethodName = receiveData.cmdName + // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) + for (const hook of receiveHooks) { + if (hook.method.includes(ntQQApiMethodName)) { + new Promise((resolve, reject) => { + try { + hook.hookFunc(receiveData.payload) + } catch (e: any) { + log('hook error', ntQQApiMethodName, e.stack.toString()) + } + resolve(undefined) + }).then() } } } - if (args[0]?.callbackId) { - // log("hookApiCallback", hookApiCallbacks, args) - const callbackId = args[0].callbackId - if (hookApiCallbacks[callbackId]) { - // log("callback found") - new Promise((resolve, reject) => { - hookApiCallbacks[callbackId](args[1]) - }).then() - delete hookApiCallbacks[callbackId] - } + } + if (args[0]?.callbackId) { + // log("hookApiCallback", hookApiCallbacks, args) + const callbackId = args[0].callbackId + if (hookApiCallbacks[callbackId]) { + // log("callback found") + new Promise((resolve, reject) => { + hookApiCallbacks[callbackId](args[1]) + resolve(undefined) + }).then() + delete hookApiCallbacks[callbackId] } - } catch (e: any) { - log('hookNTQQApiReceive error', e.stack.toString(), args) } originalSend.call(window.webContents, channel, ...args) } @@ -395,7 +384,7 @@ export async function startHook() { }>(ReceiveCmdS.FRIENDS, (payload) => { // log("onBuddyListChange", payload) // let friendListV2: {userSimpleInfos: Map} = [] - type V2data = {userSimpleInfos: Map} + type V2data = { userSimpleInfos: Map } let friendList: User[] = []; if ((payload as any).userSimpleInfos) { // friendListV2 = payload as any @@ -405,7 +394,7 @@ export async function startHook() { } }) } - else{ + else { for (const fData of payload.data) { friendList.push(...fData.buddyList) } diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index a03d04b..e62638c 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -356,7 +356,13 @@ export class SendMsg extends BaseAction { } protected async _handle(payload: OB11PostSendMsg) { - const peer = await createContext(payload, ContextMode.Normal) + let contextMode = ContextMode.Normal + if (payload.message_type === 'group') { + contextMode = ContextMode.Group + } else if (payload.message_type === 'private') { + contextMode = ContextMode.Private + } + const peer = await createContext(payload, contextMode) const messages = convertMessage2List( payload.message, payload.auto_escape === true || payload.auto_escape === 'true', diff --git a/src/onebot11/server/post-ob11-event.ts b/src/onebot11/server/post-ob11-event.ts index 7ebc1d4..c7f8257 100644 --- a/src/onebot11/server/post-ob11-event.ts +++ b/src/onebot11/server/post-ob11-event.ts @@ -27,9 +27,10 @@ export function unregisterWsEventSender(ws: WebSocketClass) { export function postWsEvent(event: PostEventType) { for (const ws of eventWSList) { - new Promise(() => { + new Promise((resolve) => { wsReply(ws, event) - }).then().catch(log) + resolve(undefined) + }).then() } } From 2792fa4776e7e438d195e46b4e446b2f3a2519fa Mon Sep 17 00:00:00 2001 From: idranme Date: Wed, 21 Aug 2024 00:14:15 +0800 Subject: [PATCH 22/23] fix --- src/common/server/websocket.ts | 93 --------- src/common/utils/file.ts | 211 ++++++++++----------- src/onebot11/server/ws/ReverseWebsocket.ts | 4 + src/onebot11/server/ws/WebsocketServer.ts | 115 ++++++++--- 4 files changed, 188 insertions(+), 235 deletions(-) delete mode 100644 src/common/server/websocket.ts diff --git a/src/common/server/websocket.ts b/src/common/server/websocket.ts deleted file mode 100644 index dc44980..0000000 --- a/src/common/server/websocket.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { WebSocket, WebSocketServer } from 'ws' -import urlParse from 'url' -import { IncomingMessage } from 'node:http' -import { log } from '../utils/log' -import { getConfigUtil } from '../config' -import { llonebotError } from '../data' - -class WebsocketClientBase { - private wsClient: WebSocket | undefined - - constructor() { } - - send(msg: string) { - if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) { - this.wsClient.send(msg) - } - } - - onMessage(msg: string) { } -} - -export class WebsocketServerBase { - private ws: WebSocketServer | null = null - - constructor() { - console.log(`llonebot websocket service started`) - } - - start(port: number) { - try { - this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 }) - llonebotError.wsServerError = '' - } catch (e: any) { - llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString() - } - this.ws?.on('connection', (wsClient, req) => { - const url = req.url?.split('?').shift() - this.authorize(wsClient, req) - this.onConnect(wsClient, url!, req) - wsClient.on('message', async (msg) => { - this.onMessage(wsClient, url!, msg.toString()) - }) - }) - } - - stop() { - llonebotError.wsServerError = '' - this.ws?.close((err) => { - log('ws server close failed!', err) - }) - this.ws = null - } - - restart(port: number) { - this.stop() - this.start(port) - } - - authorize(wsClient: WebSocket, req) { - let token = getConfigUtil().getConfig().token - const url = req.url.split('?').shift() - log('ws connect', url) - let clientToken: string = '' - const authHeader = req.headers['authorization'] - if (authHeader) { - clientToken = authHeader.split('Bearer ').pop() - log('receive ws header token', clientToken) - } else { - const parsedUrl = urlParse.parse(req.url, true) - const urlToken = parsedUrl.query.access_token - if (urlToken) { - if (Array.isArray(urlToken)) { - clientToken = urlToken[0] - } else { - clientToken = urlToken - } - log('receive ws url token', clientToken) - } - } - if (token && clientToken != token) { - this.authorizeFailed(wsClient) - return wsClient.close() - } - } - - authorizeFailed(wsClient: WebSocket) { } - - onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { } - - onMessage(wsClient: WebSocket, url: string, msg: string) { } - - sendHeart() { } -} diff --git a/src/common/utils/file.ts b/src/common/utils/file.ts index 7c349e9..752ebfa 100644 --- a/src/common/utils/file.ts +++ b/src/common/utils/file.ts @@ -1,9 +1,9 @@ import fs from 'node:fs' import fsPromise from 'node:fs/promises' import path from 'node:path' -import { log, TEMP_DIR } from './index' -import * as fileType from 'file-type' +import { TEMP_DIR } from './index' import { randomUUID, createHash } from 'node:crypto' +import { fileURLToPath } from 'node:url' export function isGIF(path: string) { const buffer = Buffer.alloc(4) @@ -32,31 +32,6 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise }) } -export async function file2base64(path: string) { - let result = { - err: '', - data: '', - } - try { - // 读取文件内容 - // if (!fs.existsSync(path)){ - // path = path.replace("\\Ori\\", "\\Thumb\\"); - // } - try { - await checkFileReceived(path, 5000) - } catch (e: any) { - result.err = e.toString() - return result - } - const data = await fsPromise.readFile(path) - // 转换为Base64编码 - result.data = data.toString('base64') - } catch (err: any) { - result.err = err.toString() - } - return result -} - export function calculateFileMD5(filePath: string): Promise { return new Promise((resolve, reject) => { // 创建一个流式读取器 @@ -109,112 +84,118 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi return Buffer.from(await fetchRes.arrayBuffer()) } +export enum FileUriType { + Unknown = 0, + FileURL = 1, + RemoteURL = 2, + OneBotBase64 = 3, + DataURL = 4, + Path = 5 +} + +export function checkUriType(uri: string): { type: FileUriType } { + if (uri.startsWith('base64://')) { + return { type: FileUriType.OneBotBase64 } + } + if (uri.startsWith('data:')) { + return { type: FileUriType.DataURL } + } + if (uri.startsWith('http://') || uri.startsWith('https://')) { + return { type: FileUriType.RemoteURL } + } + if (uri.startsWith('file://')) { + return { type: FileUriType.FileURL } + } + try { + if (fs.existsSync(uri)) return { type: FileUriType.Path } + } catch { } + return { type: FileUriType.Unknown } +} + +interface FetchFileRes { + data: Buffer + url: string +} + +async function fetchFile(url: string): Promise { + const headers: Record = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36', + 'Host': new URL(url).hostname + } + const raw = await fetch(url, { headers }).catch((err) => { + if (err.cause) { + throw err.cause + } + throw err + }) + if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`) + return { + data: Buffer.from(await raw.arrayBuffer()), + url: raw.url + } +} + type Uri2LocalRes = { success: boolean errMsg: string fileName: string - ext: string path: string isLocal: boolean } -export async function uri2local(uri: string, fileName: string | null = null): Promise { - let res = { - success: false, - errMsg: '', - fileName: '', - ext: '', - path: '', - isLocal: false, - } - if (!fileName) { - fileName = randomUUID() - } - let filePath = path.join(TEMP_DIR, fileName) - let url: URL | null = null - try { - url = new URL(uri) - } catch (e: any) { - res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` - return res +export async function uri2local(uri: string, filename?: string): Promise { + const { type } = checkUriType(uri) + + if (type === FileUriType.FileURL) { + const filePath = fileURLToPath(uri) + const fileName = path.basename(filePath) + return { success: true, errMsg: '', fileName, path: filePath, isLocal: true } } - // log("uri protocol", url.protocol, uri); - if (url.protocol == 'base64:') { - // base64转成文件 - let base64Data = uri.split('base64://')[1] + if (type === FileUriType.Path) { + const fileName = path.basename(uri) + return { success: true, errMsg: '', fileName, path: uri, isLocal: true } + } + + if (type === FileUriType.RemoteURL) { try { - const buffer = Buffer.from(base64Data, 'base64') - await fsPromise.writeFile(filePath, buffer) - } catch (e: any) { - res.errMsg = `base64文件下载失败,` + e.toString() - return res - } - } else if (url.protocol == 'http:' || url.protocol == 'https:') { - // 下载文件 - let buffer: Buffer | null = null - try { - buffer = await httpDownload(uri) - } catch (e: any) { - res.errMsg = `${url}下载失败,` + e.toString() - return res - } - try { - const pathInfo = path.parse(decodeURIComponent(url.pathname)) - if (pathInfo.name) { - fileName = pathInfo.name - if (pathInfo.ext) { - fileName += pathInfo.ext - // res.ext = pathInfo.ext - } - } - fileName = fileName.replace(/[/\\:*?"<>|]/g, '_') - res.fileName = fileName - filePath = path.join(TEMP_DIR, randomUUID() + fileName) - await fsPromise.writeFile(filePath, buffer) - } catch (e: any) { - res.errMsg = `${url}下载失败,` + e.toString() - return res - } - } else { - let pathname: string - if (url.protocol === 'file:') { - // await fs.copyFile(url.pathname, filePath); - pathname = decodeURIComponent(url.pathname) - if (process.platform === 'win32') { - filePath = pathname.slice(1) + const res = await fetchFile(uri) + const match = res.url.match(/.+\/([^/?]*)(?=\?)?/) + if (match?.[1]) { + filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_') } else { - filePath = pathname + filename ??= randomUUID() } + const filePath = path.join(TEMP_DIR, filename) + await fsPromise.writeFile(filePath, res.data) + return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } + } catch (e: any) { + const errMsg = `${uri}下载失败,` + e.toString() + return { success: false, errMsg, fileName: '', path: '', isLocal: false } } + } - res.isLocal = true + if (type === FileUriType.OneBotBase64) { + filename ??= randomUUID() + const filePath = path.join(TEMP_DIR, filename) + const base64 = uri.replace(/^base64:\/\//, '') + await fsPromise.writeFile(filePath, base64, 'base64') + return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } } - // else{ - // res.errMsg = `不支持的file协议,` + url.protocol - // return res - // } - // if (isGIF(filePath) && !res.isLocal) { - // await fs.rename(filePath, filePath + ".gif"); - // filePath += ".gif"; - // } - if (!res.isLocal && !res.ext) { - try { - const ext = (await fileType.fileTypeFromFile(filePath))?.ext - if (ext) { - log('获取文件类型', ext, filePath) - await fsPromise.rename(filePath, filePath + `.${ext}`) - filePath += `.${ext}` - res.fileName += `.${ext}` - res.ext = ext - } - } catch (e) { - // log("获取文件类型失败", filePath,e.stack) + + if (type === FileUriType.DataURL) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri) + if (capture) { + filename ??= randomUUID() + const [, _type, base64] = capture + const filePath = path.join(TEMP_DIR, filename) + await fsPromise.writeFile(filePath, base64, 'base64') + return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false } } } - res.success = true - res.path = filePath - return res + + return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false } } export async function copyFolder(sourcePath: string, destPath: string) { diff --git a/src/onebot11/server/ws/ReverseWebsocket.ts b/src/onebot11/server/ws/ReverseWebsocket.ts index c7c0462..8272bf9 100644 --- a/src/onebot11/server/ws/ReverseWebsocket.ts +++ b/src/onebot11/server/ws/ReverseWebsocket.ts @@ -104,6 +104,10 @@ export class ReverseWebsocket { this.websocket.on('error', log) + this.websocket.on('ping',()=>{ + this.websocket?.pong() + }) + const wsClientInterval = setInterval(() => { postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!)) }, heartInterval) // 心跳包 diff --git a/src/onebot11/server/ws/WebsocketServer.ts b/src/onebot11/server/ws/WebsocketServer.ts index 4cff47e..ee6560f 100644 --- a/src/onebot11/server/ws/WebsocketServer.ts +++ b/src/onebot11/server/ws/WebsocketServer.ts @@ -1,70 +1,131 @@ -import { WebSocket } from 'ws' +import BaseAction from '../../action/BaseAction' +import { WebSocket, WebSocketServer } from 'ws' import { actionMap } from '../../action' import { OB11Response } from '../../action/OB11Response' import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../post-ob11-event' import { ActionName } from '../../action/types' -import BaseAction from '../../action/BaseAction' import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent' -import { WebsocketServerBase } from '../../../common/server/websocket' import { IncomingMessage } from 'node:http' import { wsReply } from './reply' -import { getSelfInfo } from '../../../common/data' -import { log } from '../../../common/utils/log' -import { getConfigUtil } from '../../../common/config' +import { getSelfInfo } from '@/common/data' +import { log } from '@/common/utils/log' +import { getConfigUtil } from '@/common/config' +import { llonebotError } from '@/common/data' -class OB11WebsocketServer extends WebsocketServerBase { - authorizeFailed(wsClient: WebSocket) { - wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败'))) +export class OB11WebsocketServer { + private ws?: WebSocketServer + + constructor() { + log(`llonebot websocket service started`) } - async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) { + start(port: number) { + try { + this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 }) + llonebotError.wsServerError = '' + } catch (e: any) { + llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString() + return + } + this.ws?.on('connection', (socket, req) => { + const url = req.url?.split('?').shift() + this.authorize(socket, req) + this.onConnect(socket, url!) + }) + } + + stop() { + llonebotError.wsServerError = '' + this.ws?.close(err => { + log('ws server close failed!', err) + }) + this.ws = undefined + } + + restart(port: number) { + this.stop() + this.start(port) + } + + private authorize(socket: WebSocket, req: IncomingMessage) { + const { token } = getConfigUtil().getConfig() + const url = req.url?.split('?').shift() + log('ws connect', url) + let clientToken = '' + const authHeader = req.headers['authorization'] + if (authHeader) { + clientToken = authHeader.split('Bearer ').pop()! + log('receive ws header token', clientToken) + } else { + const { searchParams } = new URL(`http://localhost${req.url}`) + const urlToken = searchParams.get('access_token') + if (urlToken) { + if (Array.isArray(urlToken)) { + clientToken = urlToken[0] + } else { + clientToken = urlToken + } + log('receive ws url token', clientToken) + } + } + if (token && clientToken !== token) { + this.authorizeFailed(socket) + return socket.close() + } + } + + private authorizeFailed(socket: WebSocket) { + socket.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败'))) + } + + private async handleAction(socket: WebSocket, actionName: string, params: any, echo?: any) { const action: BaseAction = actionMap.get(actionName)! if (!action) { - return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo)) + return wsReply(socket, OB11Response.error('不支持的api ' + actionName, 1404, echo)) } try { - let handleResult = await action.websocketHandle(params, echo) + const handleResult = await action.websocketHandle(params, echo) handleResult.echo = echo - wsReply(wsClient, handleResult) + wsReply(socket, handleResult) } catch (e: any) { - wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) + wsReply(socket, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) } } - onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { - if (url == '/api' || url == '/api/' || url == '/') { - wsClient.on('message', async (msg) => { + private onConnect(socket: WebSocket, url: string) { + if (['/api', '/api/', '/'].includes(url)) { + socket.on('message', async (msg) => { let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} } - let echo = null + let echo: any try { receiveData = JSON.parse(msg.toString()) echo = receiveData.echo log('收到正向Websocket消息', receiveData) } catch (e) { - return wsReply(wsClient, OB11Response.error('json解析失败,请检查数据格式', 1400, echo)) + return wsReply(socket, OB11Response.error('json解析失败,请检查数据格式', 1400, echo)) } - this.handleAction(wsClient, receiveData.action!, receiveData.params, receiveData.echo).then() + this.handleAction(socket, receiveData.action!, receiveData.params, receiveData.echo) }) } - if (url == '/event' || url == '/event/' || url == '/') { - registerWsEventSender(wsClient) + if (['/event', '/event/', '/'].includes(url)) { + registerWsEventSender(socket) log('event上报ws客户端已连接') try { - wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) + wsReply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) } catch (e) { log('发送生命周期失败', e) } const { heartInterval } = getConfigUtil().getConfig() - const wsClientInterval = setInterval(() => { + const intervalId = setInterval(() => { postWsEvent(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!)) }, heartInterval) // 心跳包 - wsClient.on('close', () => { + socket.on('close', () => { log('event上报ws客户端已断开') - clearInterval(wsClientInterval) - unregisterWsEventSender(wsClient) + clearInterval(intervalId) + unregisterWsEventSender(socket) }) } } From ddefb4c1945b8e77ca504c98b6fb0093521fdc24 Mon Sep 17 00:00:00 2001 From: idranme Date: Wed, 21 Aug 2024 00:27:47 +0800 Subject: [PATCH 23/23] chore: v3.29.6 --- manifest.json | 2 +- package.json | 2 +- src/version.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 7609d6d..5b9ffb6 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用于 QQ 机器人开发", - "version": "3.29.5", + "version": "3.29.6", "icon": "./icon.webp", "authors": [ { diff --git a/package.json b/package.json index 2898750..8e3ac6c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "electron": "^31.4.0", "electron-vite": "^2.3.0", "typescript": "^5.5.4", - "vite": "^5.4.1", + "vite": "^5.4.2", "vite-plugin-cp": "^4.0.8" }, "packageManager": "yarn@4.4.0" diff --git a/src/version.ts b/src/version.ts index 44fcdcd..80dadbf 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.29.5' +export const version = '3.29.6'