From 6582ffe9646728a5fc4af6a92fc3d5adebe9b1bc Mon Sep 17 00:00:00 2001 From: idranme Date: Tue, 13 Aug 2024 19:29:22 +0800 Subject: [PATCH] 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'