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/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. diff --git a/README.md b/README.md index 71d88f2..353dbc7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # LLOneBot -LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发 +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..5b9ffb6 100644 --- a/manifest.json +++ b/manifest.json @@ -3,8 +3,8 @@ "type": "extension", "name": "LLOneBot", "slug": "LLOneBot", - "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.28.6", + "description": "实现 OneBot 11 协议,用于 QQ 机器人开发", + "version": "3.29.6", "icon": "./icon.webp", "authors": [ { diff --git a/package.json b/package.json index af164bd..8e3ac6c 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", @@ -16,13 +16,15 @@ "author": "", "license": "MIT", "dependencies": { + "@minatojs/driver-sqlite": "^4.5.0", "compressing": "^1.10.1", + "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", - "level": "^8.0.1", + "minato": "^3.5.0", "silk-wasm": "^3.6.1", "ws": "^8.18.0" }, @@ -32,10 +34,10 @@ "@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.0", + "vite": "^5.4.2", "vite-plugin-cp": "^4.0.8" }, "packageManager": "yarn@4.4.0" 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/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/common/config.ts b/src/common/config.ts index 3ef929c..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 @@ -52,6 +50,7 @@ export class ConfigUtil { autoDeleteFile: false, autoDeleteFileSecond: 60, musicSignUrl: '', + msgCacheExpire: 120 } if (!fs.existsSync(this.configPath)) { diff --git a/src/common/data.ts b/src/common/data.ts index 3f8f85a..06aa569 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' @@ -9,23 +8,25 @@ 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' +import { getBuildVersion } from './utils/QQBasicInfo' -export let groups: Group[] = [] export let friends: Friend[] = [] export const llonebotError: LLOneBotError = { ffmpegError: '', httpServerError: '', wsServerError: '', - otherError: 'LLOnebot未能正常启动,请检查日志查看错误', + otherError: 'LLOneBot 未能正常启动,请检查日志查看错误', } // 群号 -> 群成员map(uid=>GroupMember) 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()) @@ -39,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 @@ -79,16 +56,17 @@ 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) + groupMembers.set(groupCodeStr, members) member = getMember() } return member @@ -102,7 +80,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 +105,24 @@ export function getSelfUid() { export function getSelfUin() { return selfInfo['uin'] +} + +const messages: Map = new Map() + +/** 缓存近期消息内容 */ +export async function addMsgCache(msg: RawMessage) { + const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000 + if (expire === 0) { + return + } + 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/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/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/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/types.ts b/src/common/types.ts index b4d722d..94987de 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -30,6 +30,8 @@ export interface Config { ffmpeg?: string // ffmpeg路径 musicSignUrl?: string ignoreBeforeLoginMsg?: boolean + /** 单位为秒 */ + msgCacheExpire?: number } export interface LLOneBotError { @@ -41,11 +43,22 @@ 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 } + +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/EventTask.ts b/src/common/utils/EventTask.ts index 28d9a8e..bc32199 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 @@ -33,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() } } // 如果方法存在,正常返回 @@ -47,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> } @@ -68,14 +69,14 @@ export class NTEventWrapper { } } - 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) @@ -84,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) @@ -100,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) { @@ -149,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) }) } @@ -195,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 new file mode 100644 index 0000000..27b882e --- /dev/null +++ b/src/common/utils/MessageUnique.ts @@ -0,0 +1,163 @@ +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' +import { FileCacheV2 } from '../types' + +interface SQLiteTables extends Tables { + message: { + shortId: number + msgId: string + chatType: number + peerUid: string + } + file_v2: FileCacheV2 +} + +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' + }) + 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: 'fileUuid', + indexes: ['fileName'] + }) + 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) + } + + addFileCache(data: FileCacheV2) { + return this.db?.upsert('file_v2', [data], 'fileUuid') + } + + getFileCacheByName(fileName: string) { + return this.db?.get('file_v2', { fileName }, { + sort: { msgTime: 'desc' } + }) + } + + getFileCacheById(fileUuid: string) { + return this.db?.get('file_v2', { fileUuid }) + } +} + +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..752ebfa 100644 --- a/src/common/utils/file.ts +++ b/src/common/utils/file.ts @@ -1,10 +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 { dbUtil } from '../db' -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) @@ -33,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) => { // 创建一个流式读取器 @@ -110,119 +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] - 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) - } else { - filePath = pathname - } - } else { - const cache = await dbUtil.getFileCache(uri) - if (cache) { - filePath = cache.filePath - } else { - filePath = uri - } - } + if (type === FileUriType.Path) { + const fileName = path.basename(uri) + return { success: true, errMsg: '', fileName, path: uri, isLocal: true } + } - res.isLocal = true - } - // 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) { + if (type === FileUriType.RemoteURL) { 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 + const res = await fetchFile(uri) + const match = res.url.match(/.+\/([^/?]*)(?=\?)?/) + if (match?.[1]) { + filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_') + } else { + filename ??= randomUUID() } - } catch (e) { - // log("获取文件类型失败", filePath,e.stack) + 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.success = true - res.path = filePath - return res + + 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 } + } + + 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 } + } + } + + return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false } } export async function copyFolder(sourcePath: string, destPath: string) { 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/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/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..dc14897 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,20 +14,20 @@ 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, setSelfInfo, getSelfInfo, getSelfUid, - getSelfUin + getSelfUin, + addMsgCache } from '../common/data' import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook' import { OB11Constructor } from '../onebot11/constructor' import { FriendRequestNotify, - GroupNotifies, + GroupNotify, GroupNotifyTypes, RawMessage, BuddyReqType, @@ -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 @@ -94,7 +95,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') @@ -148,14 +149,16 @@ function onLoad() { const { debug, reportSelfMessage } = getConfigUtil().getConfig() for (let message of msgList) { // 过滤启动之前的消息 - // log('收到新消息', message); if (parseInt(message.msgTime) < startTime / 1000) { 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) + addMsgCache(message) OB11Constructor.message(message) .then((msg) => { @@ -180,19 +183,12 @@ function onLoad() { } }) OB11Constructor.PrivateEvent(message).then((privateEvent) => { - log(message) + //log(message) if (privateEvent) { // log("post private event", privateEvent); postOb11Event(privateEvent) } }) - // OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => { - // log(message) - // if (friendAddEvent) { - // // log("post friend add event", friendAddEvent); - // postOb11Event(friendAddEvent) - // } - // }) } } @@ -210,29 +206,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) => { @@ -247,6 +236,7 @@ function onLoad() { log('report self message error: ', e.stack.toString()) } }) + const processedGroupNotify: string[] = [] registerReceiveHook<{ doubt: boolean oldestUnreadSeq: string @@ -254,54 +244,43 @@ 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 - // log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`); - // if (notifyTime < startTime) { - // continue; - // } - let existNotify = await dbUtil.getGroupNotify(notify.seq) - if (existNotify) { + const notifyTime = parseInt(notify.seq) / 1000 + const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type + if (notifyTime < startTime || processedGroupNotify.includes(flag)) { continue } - log('收到群通知', notify) - await dbUtil.addGroupNotify(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) - 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('有加群请求') @@ -389,20 +368,25 @@ 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 未启动' 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) + //log('start activate group member info') + // 下面两个会导致CPU占用过高,QQ卡死 + // NTQQGroupApi.activateMemberInfoChange().then().catch(log) + // NTQQGroupApi.activateMemberListChange().then().catch(log) startReceiveHook().then() if (config.ob11.enableHttp) { @@ -421,7 +405,7 @@ function onLoad() { log('LLOneBot start') } - const init = async () => { + const intervalId = setInterval(() => { const current = getSelfInfo() if (!current.uin) { setSelfInfo({ @@ -430,15 +414,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..e4d51c6 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 { @@ -178,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) //临时解析拼接 @@ -203,6 +203,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/friend.ts b/src/ntqqapi/api/friend.ts index 48d6f12..fcab865 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/group.ts b/src/ntqqapi/api/group.ts index a1d6b2f..2e39155 100644 --- a/src/ntqqapi/api/group.ts +++ b/src/ntqqapi/api/group.ts @@ -1,10 +1,11 @@ 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' 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 } + static 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() @@ -100,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/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/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/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/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 781f04d..cef1672 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,28 +1,30 @@ 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 { - deleteGroup, + CategoryFriend, + ChatType, + GroupMember, + GroupMemberRole, + RawMessage, + SimpleInfo, User, +} from './types' +import { 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 'fs' -import { dbUtil } from '@/common/db' -import { NTQQGroupApi } from './api/group' +import { getConfigUtil } from '@/common/config' +import fs from 'node:fs' 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> = {} @@ -78,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) { - log('hook error', e, receiveData.payload) - } - }).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) } @@ -143,9 +134,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 @@ -179,16 +170,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 }, }) @@ -233,16 +224,16 @@ 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) + 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) { @@ -260,9 +251,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) { @@ -313,12 +304,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) { @@ -332,7 +323,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() } @@ -341,7 +332,7 @@ export async function startHook() { processGroupEvent(payload).then() } } - }) + })*/ registerReceiveHook<{ groupCode: string @@ -391,17 +382,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 +450,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 +518,4 @@ export async function startHook() { log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) }) }) -} \ No newline at end of file +} 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 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/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/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/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/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index f59d7c8..fbbda7d 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 { UUIDConverter } from '@/common/utils/helper' +import { Peer, ChatType, ElementType } from '@/ntqqapi/types' +import { MessageUnique } from '@/common/utils/MessageUnique' export interface GetFilePayload { file: string // 文件名或者fileUuid @@ -21,79 +20,60 @@ 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 { enableLocalFile2Url } = getConfigUtil().getConfig() + + let fileCache = await MessageUnique.getFileCacheById(String(payload.file)) + if (!fileCache?.length) { + fileCache = await MessageUnique.getFileCacheByName(String(payload.file)) } - const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig() - if (cache.downloadFunc) { - await cache.downloadFunc() - } - 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) - } - } else { - // 没有url的可能是私聊文件或者群文件,需要自己下载 - await this.download(cache, 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: fileCache[0].fileSize, + file_name: fileCache[0].fileName, } - } - let res: GetFileResponse = { - file: cache.filePath, - url: cache.url, - file_size: cache.fileSize, - file_name: cache.fileName, - } - if (enableLocalFile2Url) { - if (!cache.url) { + 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 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/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/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/DelGroupFile.ts b/src/onebot11/action/go-cqhttp/DelGroupFile.ts new file mode 100644 index 0000000..eddca67 --- /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?: 102 +} + +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/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/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..08c5775 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/GetGroupMemberInfo.ts b/src/onebot11/action/group/GetGroupMemberInfo.ts index cdd4a7d..d7b2a10 100644 --- a/src/onebot11/action/group/GetGroupMemberInfo.ts +++ b/src/onebot11/action/group/GetGroupMemberInfo.ts @@ -1,30 +1,44 @@ 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 { log } from '../../../common/utils/log' -import { isNull } from '../../../common/utils/helper' +import { NTQQUserApi, WebApi } from '@/ntqqapi/api' +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) } - return OB11Constructor.groupMember(payload.group_id.toString(), member) + 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) + 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) + return ret } else { throw `群成员${payload.user_id}不存在` } diff --git a/src/onebot11/action/group/GetGroupMemberList.ts b/src/onebot11/action/group/GetGroupMemberList.ts index 86e4456..9edd6f4 100644 --- a/src/onebot11/action/group/GetGroupMemberList.ts +++ b/src/onebot11/action/group/GetGroupMemberList.ts @@ -1,31 +1,58 @@ 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 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/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/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) 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..e3a0e2b 100644 --- a/src/onebot11/action/msg/GetMsg.ts +++ b/src/onebot11/action/msg/GetMsg.ts @@ -2,10 +2,12 @@ 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' +import { getMsgCache } from '@/common/data' export interface PayloadType { - message_id: number + message_id: number | string } export type ReturnDataType = OB11Message @@ -18,14 +20,22 @@ 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 = 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 d0c1c5c..e62638c 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 { 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, @@ -88,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 @@ -130,9 +160,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 +201,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: { @@ -282,14 +289,13 @@ 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 = await dbUtil.addMsg(returnMsg) - deleteAfterSentFiles.map((f) => fs.unlink(f, () => { - })) + returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) + log('消息发送', returnMsg.msgShortId) + deleteAfterSentFiles.map(path => fsPromise.unlink(path)) return returnMsg } @@ -299,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) { @@ -312,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' @@ -337,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!) @@ -357,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', @@ -428,8 +433,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 +465,7 @@ export class SendMsg extends BaseAction { sendElements, true, ) - await sleep(500) + await sleep(400) return nodeMsg } catch (e) { log(e, '克隆转发消息失败,将忽略本条消息', msg) @@ -477,25 +480,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 +524,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 +536,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 +571,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/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 9566fec..2c399cb 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -17,20 +17,18 @@ import { Group, Peer, GroupMember, - PicType, RawMessage, SelfInfo, Sex, TipGroupElementType, User, - VideoElement, 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 { dbUtil } from '../common/db' +import { MessageUnique } from '../common/utils/MessageUnique' import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent' import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' @@ -152,115 +150,121 @@ 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 + // 284840486: 合并消息内侧 消息具体定位不到 + 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 + 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"]["path"] = element.picElement.sourcePath - // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" - - 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() + }*/ + message_data['data']['file'] = picElement.fileName + message_data['data']['subType'] = picElement.picSubType + //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 - const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file - 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_size'] = videoOrFileElement.fileSize - if (element.videoElement) { - message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ - chatType: msg.chatType, - peerUid: msg.peerUid, - }, 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呢 + 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: 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) { 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_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) - // }) + const { pttElement } = element + message_data['data']['file'] = pttElement.fileName + message_data['data']['path'] = pttElement.filePath + //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'), + fileUuid: pttElement.fileUuid + }) } else if (element.arkElement) { message_data['type'] = OB11MessageDataType.json @@ -374,7 +378,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) @@ -422,25 +426,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' + ) } } } @@ -474,16 +479,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 +572,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,27 +609,26 @@ export class OB11Constructor { static async RecallEvent( 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!), - msg.msgShortId!, + parseInt(msg.senderUin!), + parseInt(operator?.uin || msg.senderUin!), + shortId, ) } else { - return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!) + return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), shortId) } } @@ -680,7 +698,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/server/post-ob11-event.ts b/src/onebot11/server/post-ob11-event.ts index f979dd4..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() } } @@ -61,13 +62,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/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) }) } } diff --git a/src/onebot11/server/ws/reply.ts b/src/onebot11/server/ws/reply.ts index 98263e9..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 { - let 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) } } diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index 37a1b5b..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 { @@ -164,7 +165,7 @@ export interface OB11MessagePoke { } } -interface OB11MessageFileBase { +export interface OB11MessageFileBase { data: { thumb?: string name?: string diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 70d9083..fa88e6a 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')), diff --git a/src/version.ts b/src/version.ts index b13a226..80dadbf 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.28.6' +export const version = '3.29.6'