diff --git a/manifest.json b/manifest.json index b27b0ca..f218c4e 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发", - "version": "3.28.2", + "version": "3.28.6", "icon": "./icon.webp", "authors": [ { @@ -13,7 +13,7 @@ } ], "repository": { - "repo": "linyuchen/LiteLoaderQQNT-OneBotApi", + "repo": "LLOneBot/LLOneBot", "branch": "main", "release": { "tag": "latest", diff --git a/package.json b/package.json index ab8026b..af164bd 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,9 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/fluent-ffmpeg": "^2.1.25", - "@types/node": "^20.11.24", + "@types/node": "^20.14.15", "@types/ws": "^8.5.12", - "electron": "^29.0.1", + "electron": "^29.1.4", "electron-vite": "^2.3.0", "typescript": "^5.5.4", "vite": "^5.4.0", diff --git a/scripts/gen-manifest.ts b/scripts/gen-manifest.ts index a4b41a6..fe48785 100644 --- a/scripts/gen-manifest.ts +++ b/scripts/gen-manifest.ts @@ -16,7 +16,7 @@ const manifest = { } ], repository: { - repo: 'linyuchen/LiteLoaderQQNT-OneBotApi', + repo: 'LLOneBot/LLOneBot', branch: 'main', release: { tag: 'latest', diff --git a/src/common/config.ts b/src/common/config.ts index d2da049..3ef929c 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -2,7 +2,7 @@ import fs from 'node:fs' import { Config, OB11Config } from './types' import { mergeNewProperties } from './utils/helper' import path from 'node:path' -import { selfInfo } from './data' +import { getSelfUin } from './data' import { DATA_DIR } from './utils' export const HOOK_LOG = false @@ -97,6 +97,6 @@ export class ConfigUtil { } export function getConfigUtil() { - const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) + const configFilePath = path.join(DATA_DIR, `config_${getSelfUin()}.json`) return new ConfigUtil(configFilePath) } diff --git a/src/common/data.ts b/src/common/data.ts index 6ef4a1d..3f8f85a 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -1,32 +1,17 @@ import { - CategoryFriend, type Friend, - type FriendRequest, type Group, type GroupMember, type SelfInfo, - User, } from '../ntqqapi/types' -import { type FileCache, type LLOneBotError } from './types' +import { type LLOneBotError } from './types' import { NTQQGroupApi } from '../ntqqapi/api/group' import { log } from './utils/log' import { isNumeric } from './utils/helper' -import { NTQQFriendApi } from '../ntqqapi/api' -import { WebApiGroupMember } from '@/ntqqapi/api/webapi' +import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api' -export const selfInfo: SelfInfo = { - uid: '', - uin: '', - nick: '', - online: true, -} -export const WebGroupData = { - GroupData: new Map>(), - GroupTime: new Map(), -} export let groups: Group[] = [] export let friends: Friend[] = [] -export let friendRequests: Map = new Map() export const llonebotError: LLOneBotError = { ffmpegError: '', httpServerError: '', @@ -109,16 +94,37 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s return member } -export const uidMaps: Record = {} // 一串加密的字符串(uid) -> qq号 - -export function getUidByUin(uin: string) { - for (const uid in uidMaps) { - if (uidMaps[uid] === uin) { - return uid - } - } +const selfInfo: SelfInfo = { + uid: '', + uin: '', + nick: '', + online: true, } -export let tempGroupCodeMap: Record = {} // peerUid => 群号 +export async function getSelfNick(force = false): Promise { + if (!selfInfo.nick || force) { + const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid) + if (userInfo) { + selfInfo.nick = userInfo.nick + return userInfo.nick + } + } -export let rawFriends: CategoryFriend[] = [] \ No newline at end of file + return selfInfo.nick +} + +export function getSelfInfo() { + return selfInfo +} + +export function setSelfInfo(data: Partial) { + Object.assign(selfInfo, data) +} + +export function getSelfUid() { + return selfInfo['uid'] +} + +export function getSelfUin() { + return selfInfo['uin'] +} \ No newline at end of file diff --git a/src/common/db.ts b/src/common/db.ts index 9cdb597..b27b7a3 100644 --- a/src/common/db.ts +++ b/src/common/db.ts @@ -1,7 +1,6 @@ import { Level } from 'level' import { type GroupNotify, RawMessage } from '../ntqqapi/types' import { DATA_DIR } from './utils' -import { selfInfo } from './data' import { FileCache } from './types' import { log } from './utils/log' @@ -27,33 +26,12 @@ class DBUtil { * */ constructor() { - let initCount = 0 - new Promise((resolve, reject) => { - const initDB = () => { - initCount++ - // if (initCount > 50) { - // return reject("init db fail") - // } - - try { - if (!selfInfo.uin) { - setTimeout(initDB, 300) - return - } - const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}` - this.db = new Level(DB_PATH, { valueEncoding: 'json' }) - console.log('llonebot init db success') - resolve(null) - } catch (e: any) { - console.log('init db fail', e.stack.toString()) - setTimeout(initDB, 300) - } - } - setTimeout(initDB) - }).then() + } + 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 = {} // 清理时间较久的缓存 diff --git a/src/common/utils/QQBasicInfo.ts b/src/common/utils/QQBasicInfo.ts index 2e7c5e1..d0c8bb8 100644 --- a/src/common/utils/QQBasicInfo.ts +++ b/src/common/utils/QQBasicInfo.ts @@ -1,5 +1,4 @@ import path from 'node:path' -import fs from 'node:fs' import os from 'node:os' import { systemPlatform } from './system' @@ -38,32 +37,6 @@ type QQPkgInfo = { platform: string eleArch: string } -type QQVersionConfigInfo = { - baseVersion: string - curVersion: string - prevVersion: string - onErrorVersions: Array - buildId: string -} - -let _qqVersionConfigInfo: QQVersionConfigInfo = { - 'baseVersion': '9.9.9-23361', - 'curVersion': '9.9.9-23361', - 'prevVersion': '', - 'onErrorVersions': [], - 'buildId': '23361', -} - -if (fs.existsSync(configVersionInfoPath)) { - try { - const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString()) - _qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _) - } catch (e) { - console.error('Load QQ version config info failed, Use default version', e) - } -} - -export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath) // platform_type: 3, @@ -74,14 +47,6 @@ export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath) // platVer: '10.0.26100', // clientVer: '9.9.9-23159', -let _appid: string = '537213803' // 默认为 Windows 平台的 appid -if (systemPlatform === 'linux') { - _appid = '537213827' -} -// todo: mac 平台的 appid -export const appid = _appid -export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106' - export function getBuildVersion(): number { return +qqPkgInfo.buildVersion } \ No newline at end of file diff --git a/src/common/utils/file.ts b/src/common/utils/file.ts index 2acd233..2e60cd9 100644 --- a/src/common/utils/file.ts +++ b/src/common/utils/file.ts @@ -25,7 +25,7 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise } else if (Date.now() - startTime > timeout) { reject(new Error(`文件不存在: ${path}`)) } else { - setTimeout(check, 100) + setTimeout(check, 200) } } diff --git a/src/common/utils/helper.ts b/src/common/utils/helper.ts index 1dbbd0a..10388b8 100644 --- a/src/common/utils/helper.ts +++ b/src/common/utils/helper.ts @@ -96,6 +96,29 @@ export function cacheFunc(ttl: number, customKey: string = '') { } } +export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') { + function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) { + const cache = new Map() + const originalMethod = descriptor.value + descriptor.value = async function (...args: any[]) { + const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})` + cache.forEach((value, key) => { + if (value.expiry < Date.now()) { + cache.delete(key) + } + }) + const cachedValue = cache.get(key) + if (cachedValue && cachedValue.expiry > Date.now()) { + return cachedValue.value + } + const result = await originalMethod.apply(this, args) + cache.set(key, { expiry: Date.now() + ttl, value: result }) + return result + } + } + return logExecutionTime +} + export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) { function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) { const cache = new Map() diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index fa95572..fd55490 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -15,5 +15,4 @@ if (!fs.existsSync(TEMP_DIR)) { } export { getVideoInfo } from './video' export { checkFfmpeg } from './video' -export { encodeSilk } from './audio' -export { isQQ998 } from './QQBasicInfo' \ No newline at end of file +export { encodeSilk } from './audio' \ No newline at end of file diff --git a/src/common/utils/log.ts b/src/common/utils/log.ts index e06593f..537b3eb 100644 --- a/src/common/utils/log.ts +++ b/src/common/utils/log.ts @@ -1,4 +1,4 @@ -import { selfInfo } from '../data' +import { getSelfInfo } from '../data' import fs from 'fs' import path from 'node:path' import { DATA_DIR, truncateString } from './index' @@ -15,7 +15,7 @@ export function log(...msg: any[]) { if (!getConfigUtil().getConfig().log) { return //console.log(...msg); } - + const selfInfo = getSelfInfo() const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : '' let logMsg = '' for (let msgItem of msg) { @@ -31,5 +31,5 @@ export function log(...msg: any[]) { logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` // sendLog(...msg); // console.log(msg) - fs.appendFile(path.join(logDir, logFileName), logMsg, (err: any) => {}) + fs.appendFile(path.join(logDir, logFileName), logMsg, () => {}) } diff --git a/src/main/main.ts b/src/main/main.ts index c4ed253..cf4e8aa 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,7 +1,7 @@ // 运行在 Electron 主进程 下的插件入口 import { BrowserWindow, dialog, ipcMain } from 'electron' -import * as fs from 'node:fs' +import fs from 'node:fs' import { Config } from '../common/types' import { CHANNEL_CHECK_VERSION, @@ -15,11 +15,12 @@ import { import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' import { DATA_DIR } from '../common/utils' import { - friendRequests, getGroupMember, llonebotError, - selfInfo, - uidMaps, + setSelfInfo, + getSelfInfo, + getSelfUid, + getSelfUin } from '../common/data' import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook' import { OB11Constructor } from '../onebot11/constructor' @@ -28,24 +29,23 @@ import { GroupNotifies, GroupNotifyTypes, RawMessage, + BuddyReqType, } from '../ntqqapi/types' import { httpHeart, ob11HTTPServer } from '../onebot11/server/http' 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 * as path from 'node:path' +import path from 'node:path' import { dbUtil } from '../common/db' import { setConfig } from './setConfig' -import { NTQQUserApi } from '../ntqqapi/api/user' -import { NTQQGroupApi } from '../ntqqapi/api/group' +import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { log } from '../common/utils/log' import { getConfigUtil } from '../common/config' import { checkFfmpeg } from '../common/utils/video' import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import '../ntqqapi/wrapper' -import { sentMessages } from '@/ntqqapi/api' import { NTEventDispatch } from '../common/utils/EventTask' import { wrapperConstructor, getSession } from '../ntqqapi/wrapper' @@ -53,7 +53,6 @@ let mainWindow: BrowserWindow | null = null // 加载插件时触发 function onLoad() { - log('llonebot main onLoad') ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => { return checkNewVersion() }) @@ -163,7 +162,7 @@ function onLoad() { if (!debug && msg.message.length === 0) { return } - const isSelfMsg = msg.user_id.toString() == selfInfo.uin + const isSelfMsg = msg.user_id.toString() === getSelfUin() if (isSelfMsg && !reportSelfMessage) { return } @@ -211,10 +210,6 @@ function onLoad() { const recallMsgIds: string[] = [] // 避免重复上报 registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { for (const message of payload.msgList) { - const sentMessage = sentMessages[message.msgId] - if (sentMessage) { - Object.assign(sentMessage, message) - } log('message update', message.msgId, message) if (message.recallTime != '0') { if (recallMsgIds.includes(message.msgId)) { @@ -284,39 +279,7 @@ function onLoad() { } log('收到群通知', notify) await dbUtil.addGroupNotify(notify) - // let member2: GroupMember; - // if (notify.user2.uid) { - // member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid); - // } - // 原本的群管变更通知事件处理 - // if ( - // [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes( - // notify.type, - // ) - // ) { - // const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid) - // log('有管理员变动通知') - // refreshGroupMembers(notify.group.groupCode).then() - // let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent() - // groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode) - // log('开始获取变动的管理员') - // if (member1) { - // log('变动管理员获取成功') - // groupAdminNoticeEvent.user_id = parseInt(member1.uin) - // groupAdminNoticeEvent.sub_type = [ - // GroupNotifyTypes.ADMIN_UNSET, - // GroupNotifyTypes.ADMIN_UNSET_OTHER, - // ].includes(notify.type) - // ? 'unset' - // : 'set' - // // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal; - // postOb11Event(groupAdminNoticeEvent, true) - // } - // else { - // log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode)) - // } - // } - // else + const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { log('有成员退出通知', notify) try { @@ -342,48 +305,47 @@ function onLoad() { } else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { log('有加群请求') - let requestQQ = uidMaps[notify.user1.uid] - if (!requestQQ) { - try { + let requestQQ = '' + try { + // uid-->uin + requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid)) + if (isNaN(parseInt(requestQQ))) { requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin - } catch (e) { - log('获取加群人QQ号失败', e) } + } catch (e) { + log('获取加群人QQ号失败 Uid:', notify.user1.uid, e) } - let invitorId: number + let invitorId: string if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) { // groupRequestEvent.sub_type = 'invite' - let invitorQQ = uidMaps[notify.user2.uid] - if (!invitorQQ) { - try { - let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)) - invitorId = parseInt(invitor.uin) - } catch (e) { - invitorId = 0 - log('获取邀请人QQ号失败', e) + try { + // uid-->uin + invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) + if (isNaN(parseInt(invitorId))) { + invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin } + } catch (e) { + invitorId = '' + log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e) } } const groupRequestEvent = new OB11GroupRequestEvent( parseInt(notify.group.groupCode), parseInt(requestQQ) || 0, - notify.seq, + flag, notify.postscript, - invitorId!, + invitorId! === undefined ? undefined : +invitorId, 'add' ) postOb11Event(groupRequestEvent) } else if (notify.type == GroupNotifyTypes.INVITE_ME) { log('收到邀请我加群通知') - let userId = uidMaps[notify.user2.uid] - if (!userId) { - userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin - } + const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || '' const groupInviteEvent = new OB11GroupRequestEvent( parseInt(notify.group.groupCode), parseInt(userId), - notify.seq, + flag, undefined, undefined, 'invite' @@ -402,27 +364,31 @@ function onLoad() { registerReceiveHook(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { for (const req of payload.data.buddyReqs) { - const flag = req.friendUid + req.reqTime - if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) { - friendRequests[flag] = req - log('有新的好友请求', req) - let userId: number - try { - const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid) - userId = parseInt(requester.uin) - } catch (e) { - log('获取加好友者QQ号失败', e) - } - const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag) - postOb11Event(friendRequestEvent) + if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) { + continue } + let userId = 0 + try { + const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid) + userId = parseInt(requesterUin!) + } catch (e) { + log('获取加好友者QQ号失败', e) + } + const flag = req.friendUid + '|' + req.reqTime + const comment = req.extWords + const friendRequestEvent = new OB11FriendRequestEvent( + userId, + comment, + flag + ) + postOb11Event(friendRequestEvent) } }) } let startTime = 0 // 毫秒 - async function start() { + async function start(uid: string, uin: string) { log('llonebot pid', process.pid) const config = getConfigUtil().getConfig() if (!config.enableLLOB) { @@ -431,12 +397,9 @@ function onLoad() { } llonebotError.otherError = '' startTime = Date.now() - dbUtil.getReceivedTempUinMap().then((m) => { - for (const [key, value] of Object.entries(m)) { - uidMaps[value] = key - } - }) NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! }) + dbUtil.init(uin) + log('start activate group member info') NTQQGroupApi.activateMemberInfoChange().then().catch(log) NTQQGroupApi.activateMemberListChange().then().catch(log) @@ -458,54 +421,29 @@ function onLoad() { log('LLOneBot start') } - let getSelfNickCount = 0 const init = async () => { - try { - log('start get self info') - const _ = await NTQQUserApi.getSelfInfo() - log('get self info api result:', _) - Object.assign(selfInfo, _) - selfInfo.nick = selfInfo.uin - } catch (e) { - log('retry get self info', e) + const current = getSelfInfo() + if (!current.uin) { + setSelfInfo({ + uin: globalThis.authData?.uin, + uid: globalThis.authData?.uid, + nick: current.uin, + }) } - if (!selfInfo.uin) { - selfInfo.uin = globalThis.authData?.uin - selfInfo.uid = globalThis.authData?.uid - selfInfo.nick = selfInfo.uin - } - log('self info', selfInfo, globalThis.authData) - if (selfInfo.uin) { - async function getUserNick() { - try { - getSelfNickCount++ - const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid) - log('self info', userInfo) - if (userInfo) { - selfInfo.nick = userInfo.nick - return - } - } catch (e: any) { - log('get self nickname failed', e.stack) - } - if (getSelfNickCount < 10) { - return setTimeout(getUserNick, 1000) - } - } - - getUserNick().then() - start().then() + //log('self info', selfInfo, globalThis.authData) + if (current.uin) { + start(current.uid, current.uin) } else { setTimeout(init, 1000) } } - setTimeout(init, 1000) + init() } // 创建窗口时触发 function onBrowserWindowCreated(window: BrowserWindow) { - if (selfInfo.uid) { + if (getSelfUid()) { return } mainWindow = window diff --git a/src/ntqqapi/api/file.ts b/src/ntqqapi/api/file.ts index 19bc1b0..6023958 100644 --- a/src/ntqqapi/api/file.ts +++ b/src/ntqqapi/api/file.ts @@ -9,15 +9,21 @@ import { ChatType, ElementType, IMAGE_HTTP_HOST, - IMAGE_HTTP_HOST_NT, PicElement, + IMAGE_HTTP_HOST_NT, + PicElement, } from '../types' import path from 'node:path' import fs from 'node:fs' import { ReceiveCmdS } from '../hook' -import { log } from '@/common/utils' +import { log, TEMP_DIR } from '@/common/utils' import { rkeyManager } from '@/ntqqapi/api/rkey' import { getSession } from '@/ntqqapi/wrapper' import { Peer } from '@/ntqqapi/types/msg' +import { calculateFileMD5 } from '@/common/utils/file' +import { fileTypeFromFile } from 'file-type' +import fsPromise from 'node:fs/promises' +import { NTEventDispatch } from '@/common/utils/EventTask' +import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' export class NTQQFileApi { static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise { @@ -26,23 +32,11 @@ export class NTQQFileApi { msgId, elementId, 0, - { downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url; + { downSourceType: 1, triggerType: 1 }))?.urlResult?.domainUrl[0]?.url! } static async getFileType(filePath: string) { - return await callNTQQApi<{ ext: string }>({ - className: NTQQApiClass.FS_API, - methodName: NTQQApiMethod.FILE_TYPE, - args: [filePath], - }) - } - - static async getFileMd5(filePath: string) { - return await callNTQQApi({ - className: NTQQApiClass.FS_API, - methodName: NTQQApiMethod.FILE_MD5, - args: [filePath], - }) + return fileTypeFromFile(filePath) } static async copyFile(filePath: string, destPath: string) { @@ -67,44 +61,35 @@ export class NTQQFileApi { } // 上传文件到QQ的文件夹 - static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { - const md5 = await NTQQFileApi.getFileMd5(filePath) - let ext = (await NTQQFileApi.getFileType(filePath))?.ext + static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) { + const fileMd5 = await calculateFileMD5(filePath) + let ext = (await NTQQFileApi.getFileType(filePath))?.ext || '' if (ext) { ext = '.' + ext - } else { - ext = '' } let fileName = `${path.basename(filePath)}` if (fileName.indexOf('.') === -1) { fileName += ext } - const mediaPath = await callNTQQApi({ - methodName: NTQQApiMethod.MEDIA_FILE_PATH, - args: [ - { - path_info: { - md5HexStr: md5, - fileName: fileName, - elementType: elementType, - elementSubType, - thumbSize: 0, - needCreate: true, - downloadType: 1, - file_uuid: '', - }, - }, - ], + const session = getSession() + const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({ + md5HexStr: fileMd5, + fileName: fileName, + elementType: elementType, + elementSubType, + thumbSize: 0, + needCreate: true, + downloadType: 1, + file_uuid: '' }) - log('media path', mediaPath) - await NTQQFileApi.copyFile(filePath, mediaPath) - const fileSize = await NTQQFileApi.getFileSize(filePath) + await fsPromise.copyFile(filePath, mediaPath!) + const fileSize = (await fsPromise.stat(filePath)).size return { - md5, + md5: fileMd5, fileName, - path: mediaPath, + path: mediaPath!, fileSize, - ext, + ext } } @@ -115,44 +100,67 @@ export class NTQQFileApi { elementId: string, thumbPath: string, sourcePath: string, - force: boolean = false, + timeout = 1000 * 60 * 2, + force = false ) { // 用于下载收到的消息中的图片等 if (sourcePath && fs.existsSync(sourcePath)) { if (force) { - fs.unlinkSync(sourcePath) + try { + await fsPromise.unlink(sourcePath) + } catch (e) { + // + } } else { return sourcePath } } - const apiParams = [ + const data = await NTEventDispatch.CallNormalEvent< + ( + params: { + fileModelId: string, + downloadSourceType: number, + triggerType: number, + msgId: string, + chatType: ChatType, + peerUid: string, + elementId: string, + thumbSize: number, + downloadType: number, + filePath: string + }) => Promise, + (fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void + >( + 'NodeIKernelMsgService/downloadRichMedia', + 'NodeIKernelMsgListener/onRichMediaDownloadComplete', + 1, + timeout, + (arg: OnRichMediaDownloadCompleteParams) => { + if (arg.msgId === msgId) { + return true + } + return false + }, { - getReq: { - fileModelId: '0', - downloadSourceType: 0, - triggerType: 1, - msgId: msgId, - chatType: chatType, - peerUid: peerUid, - elementId: elementId, - thumbSize: 0, - downloadType: 1, - filePath: thumbPath, - }, - }, - null, - ] - // log("需要下载media", sourcePath); - await callNTQQApi({ - methodName: NTQQApiMethod.DOWNLOAD_MEDIA, - args: apiParams, - cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, - cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => { - log('media 下载完成判断', payload.notifyInfo.msgId, msgId) - return payload.notifyInfo.msgId == msgId - }, - }) - return sourcePath + fileModelId: '0', + downloadSourceType: 0, + triggerType: 1, + msgId: msgId, + chatType: chatType, + peerUid: peerUid, + elementId: elementId, + thumbSize: 0, + downloadType: 1, + filePath: thumbPath + } + ) + let filePath = data[1].filePath + if (filePath.startsWith('\\')) { + const downloadPath = TEMP_DIR + filePath = path.join(downloadPath, filePath) + // 下载路径是下载文件夹的相对路径 + } + return filePath } static async getImageSize(filePath: string) { @@ -163,22 +171,27 @@ export class NTQQFileApi { }) } - static async getImageUrl(picElement: PicElement, chatType: ChatType) { - const isPrivateImage = chatType !== ChatType.group - const url = picElement.originImageUrl // 没有域名 - const md5HexStr = picElement.md5HexStr - const fileMd5 = picElement.md5HexStr - const fileUuid = picElement.fileUuid + static async getImageUrl(element: PicElement) { + if (!element) { + return '' + } + const url: string = element.originImageUrl! // 没有域名 + const md5HexStr = element.md5HexStr + const fileMd5 = element.md5HexStr + const fileUuid = element.fileUuid + if (url) { - if (url.startsWith('/download')) { - // console.log('rkey', rkey); - if (url.includes('&rkey=')) { + const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 + const imageAppid = UrlParse.searchParams.get('appid') + const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid) + if (isNewPic) { + let UrlRkey = UrlParse.searchParams.get('rkey') + if (UrlRkey) { return IMAGE_HTTP_HOST_NT + url } - - const rkeyData = await rkeyManager.getRkey(); - const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey; - return IMAGE_HTTP_HOST_NT + url + `${existsRKey}` + const rkeyData = await rkeyManager.getRkey() + UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey + return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}` } else { // 老的图片url,不需要rkey return IMAGE_HTTP_HOST + url @@ -187,7 +200,7 @@ export class NTQQFileApi { // 没有url,需要自己拼接 return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` } - log('图片url获取失败', picElement) + log('图片url获取失败', element) return '' } } diff --git a/src/ntqqapi/api/friend.ts b/src/ntqqapi/api/friend.ts index 56efd3a..48d6f12 100644 --- a/src/ntqqapi/api/friend.ts +++ b/src/ntqqapi/api/friend.ts @@ -1,7 +1,6 @@ -import { Friend, FriendRequest, FriendV2 } from '../types' +import { Friend, FriendV2 } from '../types' import { ReceiveCmdS } from '../hook' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' -import { friendRequests } from '@/common/data' import { getSession } from '@/ntqqapi/wrapper' import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { NTEventDispatch } from '@/common/utils/EventTask' @@ -49,24 +48,18 @@ export class NTQQFriendApi { } static async handleFriendRequest(flag: string, accept: boolean) { - const request: FriendRequest = friendRequests[flag] - if (!request) { - throw `flat: ${flag}, 对应的好友请求不存在` + const data = flag.split('|') + if (data.length < 2) { + return } - const result = await callNTQQApi({ - methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, - args: [ - { - approvalInfo: { - friendUid: request.friendUid, - reqTime: request.reqTime, - accept, - }, - }, - ], + const friendUid = data[0] + const reqTime = data[1] + const session = getSession() + return session?.getBuddyService().approvalFriendRequest({ + friendUid, + reqTime, + accept }) - delete friendRequests[flag] - return result } static async getBuddyV2(refresh = false): Promise { @@ -103,6 +96,28 @@ export class NTQQFriendApi { return retMap } + static async getBuddyV2ExWithCate(refresh = false) { + const uids: string[] = [] + const categoryMap: Map = new Map() + const session = getSession() + const buddyService = session?.getBuddyService() + const buddyListV2 = refresh ? (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data : (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data + uids.push( + ...buddyListV2?.flatMap(item => { + item.buddyUids.forEach(uid => { + categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName }) + }) + return item.buddyUids + })!) + const data = await NTEventDispatch.CallNoListenerEvent( + 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids + ) + return Array.from(data).map(([key, value]) => { + const category = categoryMap.get(key) + return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value + }) + } + static async isBuddy(uid: string): Promise { const session = getSession() return session?.getBuddyService().isBuddy(uid)! diff --git a/src/ntqqapi/api/group.ts b/src/ntqqapi/api/group.ts index 5663862..a1d6b2f 100644 --- a/src/ntqqapi/api/group.ts +++ b/src/ntqqapi/api/group.ts @@ -1,11 +1,10 @@ import { ReceiveCmdS } from '../hook' -import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes } from '../types' -import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' -import { deleteGroup, uidMaps } from '../../common/data' -import { dbUtil } from '../../common/db' -import { log } from '../../common/utils/log' +import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } 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' export class NTQQGroupApi { static async activateMemberListChange() { @@ -37,22 +36,19 @@ export class NTQQGroupApi { }) } - static async getGroups(forced = false) { - // let cbCmd = ReceiveCmdS.GROUPS - // if (process.platform != 'win32') { - // cbCmd = ReceiveCmdS.GROUPS_STORE - // } - const result = await callNTQQApi<{ - updateType: number - groupList: Group[] - }>({ - methodName: NTQQApiMethod.GROUPS, - args: [{ force_update: forced }, undefined], - cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE], - afterFirstCmd: false, - }) - log('get groups result', result) - return result.groupList + static async getGroups(forced = false): Promise { + type ListenerType = NodeIKernelGroupListener['onGroupListUpdate'] + const [, , groupList] = await NTEventDispatch.CallNormalEvent + <(force: boolean) => Promise, ListenerType> + ( + 'NodeIKernelGroupService/getGroupList', + 'NodeIKernelGroupListener/onGroupListUpdate', + 1, + 5000, + (updateType) => true, + forced + ) + return groupList } static async getGroupMembers(groupQQ: string, num = 3000): Promise> { @@ -104,130 +100,64 @@ export class NTQQGroupApi { ) } - static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { - const notify = await dbUtil.getGroupNotify(seq) - if (!notify) { - throw `${seq}对应的加群通知不存在` - } - // delete groupNotifies[seq] - return await callNTQQApi({ - methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, - args: [ - { - doubt: false, - operateMsg: { - operateType: operateType, // 2 拒绝 - targetMsg: { - seq: seq, // 通知序列号 - type: notify.type, - groupCode: notify.group.groupCode, - postscript: reason, - }, - }, - }, - null, - ], - }) + static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { + const flagitem = flag.split('|') + const groupCode = flagitem[0] + const seq = flagitem[1] + const type = parseInt(flagitem[2]) + const session = getSession() + return session?.getGroupService().operateSysNotify( + false, + { + 'operateType': operateType, // 2 拒绝 + 'targetMsg': { + 'seq': seq, // 通知序列号 + 'type': type, + 'groupCode': groupCode, + 'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 + } + }) } static async quitGroup(groupQQ: string) { - const result = await callNTQQApi({ - methodName: NTQQApiMethod.QUIT_GROUP, - args: [{ groupCode: groupQQ }, null], - }) - if (result.result === 0) { - deleteGroup(groupQQ) - } - return result + const session = getSession() + return session?.getGroupService().quitGroup(groupQQ) } static async kickMember( groupQQ: string, kickUids: string[], - refuseForever: boolean = false, - kickReason: string = '', + refuseForever = false, + kickReason = '', ) { - return await callNTQQApi({ - methodName: NTQQApiMethod.KICK_MEMBER, - args: [ - { - groupCode: groupQQ, - kickUids, - refuseForever, - kickReason, - }, - ], - }) + const session = getSession() + return session?.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason) } static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { // timeStamp为秒数, 0为解除禁言 - return await callNTQQApi({ - methodName: NTQQApiMethod.MUTE_MEMBER, - args: [ - { - groupCode: groupQQ, - memList, - }, - ], - }) + const session = getSession() + return session?.getGroupService().setMemberShutUp(groupQQ, memList) } static async banGroup(groupQQ: string, shutUp: boolean) { - return await callNTQQApi({ - methodName: NTQQApiMethod.MUTE_GROUP, - args: [ - { - groupCode: groupQQ, - shutUp, - }, - null, - ], - }) + const session = getSession() + return session?.getGroupService().setGroupShutUp(groupQQ, shutUp) } static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { - NTQQGroupApi.activateMemberListChange().then().catch(log) - const res = await callNTQQApi({ - methodName: NTQQApiMethod.SET_MEMBER_CARD, - args: [ - { - groupCode: groupQQ, - uid: memberUid, - cardName, - }, - null, - ], - }) - NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log) - return res + const session = getSession() + return session?.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName) } static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_MEMBER_ROLE, - args: [ - { - groupCode: groupQQ, - uid: memberUid, - role, - }, - null, - ], - }) + const session = getSession() + return session?.getGroupService().modifyMemberRole(groupQQ, memberUid, role) } static async setGroupName(groupQQ: string, groupName: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_GROUP_NAME, - args: [ - { - groupCode: groupQQ, - groupName, - }, - null, - ], - }) + const session = getSession() + return session?.getGroupService().modifyGroupName(groupQQ, groupName, false) } static async getGroupAtAllRemainCount(groupCode: string) { @@ -252,19 +182,13 @@ export class NTQQGroupApi { }) } + static async getGroupRemainAtTimes(GroupCode: string) { + const session = getSession() + return session?.getGroupService().getGroupRemainAtTimes(GroupCode)! + } + // 头衔不可用 static async setGroupTitle(groupQQ: string, uid: string, title: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.SET_GROUP_TITLE, - args: [ - { - groupCode: groupQQ, - uid, - title, - }, - null, - ], - }) } static publishGroupBulletin(groupQQ: string, title: string, content: string) { } diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts index b35374c..de26ba1 100644 --- a/src/ntqqapi/api/msg.ts +++ b/src/ntqqapi/api/msg.ts @@ -1,136 +1,50 @@ import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' -import { ChatType, RawMessage, SendMessageElement, Peer } from '../types' -import { dbUtil } from '../../common/db' -import { selfInfo } from '../../common/data' -import { ReceiveCmdS, registerReceiveHook } from '../hook' -import { log } from '../../common/utils/log' -import { sleep } from '../../common/utils/helper' -import { isQQ998, getBuildVersion } from '../../common/utils' +import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' +import { getSelfNick, getSelfUid } from '../../common/data' +import { getBuildVersion } from '../../common/utils' import { getSession } from '@/ntqqapi/wrapper' import { NTEventDispatch } from '@/common/utils/EventTask' -export let sendMessagePool: Record void) | null> = {} // peerUid: callbackFunc - -export let sentMessages: Record = {} // msgId: RawMessage - -async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) { - // 等待上一个相同的peer发送完 - const peerUid = peer.peerUid - let checkLastSendUsingTime = 0 - const waitLastSend = async () => { - if (checkLastSendUsingTime > timeout) { - throw '发送超时' - } - let lastSending = sendMessagePool[peer.peerUid] - if (lastSending) { - // log("有正在发送的消息,等待中...") - await sleep(500) - checkLastSendUsingTime += 500 - return await waitLastSend() - } - else { - return - } - } - await waitLastSend() - - let sentMessage: RawMessage | null = null - sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { - delete sendMessagePool[peerUid] - sentMessage = rawMessage - sentMessages[rawMessage.msgId] = rawMessage - } - - let checkSendCompleteUsingTime = 0 - const checkSendComplete = async (): Promise => { - if (sentMessage) { - if (waitComplete) { - if (sentMessage.sendStatus == 2) { - delete sentMessages[sentMessage.msgId] - return sentMessage - } - } - else { - delete sentMessages[sentMessage.msgId] - return sentMessage - } - // log(`给${peerUid}发送消息成功`) - } - checkSendCompleteUsingTime += 500 - if (checkSendCompleteUsingTime > timeout) { - throw '发送超时' - } - await sleep(500) - return await checkSendComplete() - } - return checkSendComplete() -} - export class NTQQMsgApi { - static enterOrExitAIO(peer: Peer, enter: boolean) { - return callNTQQApi({ - methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO, - args: [ - { - "info_list": [ - { - peer, - "option": enter ? 1 : 2 - } - ] - }, - { - "send": true - }, - ], + static async getTempChatInfo(chatType: ChatType2, peerUid: string) { + const session = getSession() + return session?.getMsgService().getTempChatInfo(chatType, peerUid)! + } + + static async prepareTempChat(toUserUid: string, GroupCode: string, nickname: string) { + //By Jadx/Ida Mlikiowa + let TempGameSession = { + nickname: '', + gameAppId: '', + selfTinyId: '', + peerRoleId: '', + peerOpenId: '', + } + const session = getSession() + return session?.getMsgService().prepareTempChat({ + chatType: ChatType2.KCHATTYPETEMPC2CFROMGROUP, + peerUid: toUserUid, + peerNickname: nickname, + fromGroupCode: GroupCode, + sig: '', + selfPhone: '', + selfUid: getSelfUid(), + gameSession: TempGameSession }) } + static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType emojiId = emojiId.toString() - return await callNTQQApi({ - methodName: NTQQApiMethod.EMOJI_LIKE, - args: [ - { - peer, - msgSeq, - emojiId, - emojiType: emojiId.length > 3 ? '2' : '1', - setEmoji: set, - }, - null, - ], - }) + const session = getSession() + return session?.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set) } static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { - return await callNTQQApi({ - methodName: NTQQApiMethod.GET_MULTI_MSG, - args: [ - { - peer, - rootMsgId, - parentMsgId, - }, - null, - ], - }) - } - - static async getMsgBoxInfo(peer: Peer) { - return await callNTQQApi({ - methodName: NTQQApiMethod.GET_MSG_BOX_INFO, - args: [ - { - contacts: [ - peer - ], - }, - null, - ], - }) + const session = getSession() + return session?.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)! } static async activateChat(peer: Peer) { @@ -152,80 +66,80 @@ export class NTQQMsgApi { }) } - static async getMsgHistory(peer: Peer, msgId: string, count: number) { - // 消息时间从旧到新 - return await callNTQQApi({ - methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG, - args: [ - { - peer, - msgId, - cnt: count, - queryOrder: true, - }, - null, - ], - }) + static async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { + if (!peer) throw new Error('peer is not allowed') + if (!msgIds) throw new Error('msgIds is not allowed') + const session = getSession() + //Mlikiowa: 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得 + return await session?.getMsgService().getMsgsByMsgId(peer, msgIds)! } - static async fetchRecentContact() { - await callNTQQApi({ - methodName: NTQQApiMethod.RECENT_CONTACT, - args: [ - { - fetchParam: { - anchorPointContact: { - contactId: '', - sortField: '', - pos: 0, - }, - relativeMoveCount: 0, - listType: 2, // 1普通消息,2群助手内的消息 - count: 200, - fetchOld: true, - }, - }, - ], - }) + static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) { + const session = getSession() + // 消息时间从旧到新 + return session?.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)! } static async recallMsg(peer: Peer, msgIds: string[]) { - return await callNTQQApi({ - methodName: NTQQApiMethod.RECALL_MSG, - args: [ - { - peer, - msgIds, - }, - null, - ], - }) + const session = getSession() + return await session?.getMsgService().recallMsg({ + chatType: peer.chatType, + peerUid: peer.peerUid + }, msgIds) } static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { - if (getBuildVersion() >= 26702) { - return NTQQMsgApi.sendMsgV2(peer, msgElements, waitComplete, timeout) + function generateMsgId() { + const timestamp = Math.floor(Date.now() / 1000) + const random = Math.floor(Math.random() * Math.pow(2, 32)) + const buffer = Buffer.alloc(8) + buffer.writeUInt32BE(timestamp, 0) + buffer.writeUInt32BE(random, 4) + const msgId = BigInt("0x" + buffer.toString('hex')).toString() + return msgId } - const waiter = sendWaiter(peer, waitComplete, timeout) - callNTQQApi({ - methodName: NTQQApiMethod.SEND_MSG, - args: [ - { - msgId: '0', - peer, - msgElements, - msgAttributeInfos: new Map(), - }, - null, - ], - }).then() - return await waiter + // 此处有采用Hack方法 利用数据返回正确得到对应消息 + // 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同 + // 谨慎采用 目前测试暂无问题 Developer.Mlikiowa + let msgId: string + try { + msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime()) + } catch (error) { + //if (!napCatCore.session.getMsgService()['generateMsgUniqueId']) + //兜底识别策略V2 + msgId = generateMsgId() + } + peer.guildId = msgId + const data = await NTEventDispatch.CallNormalEvent< + (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map) => Promise, + (msgList: RawMessage[]) => void + >( + 'NodeIKernelMsgService/sendMsg', + 'NodeIKernelMsgListener/onMsgInfoListUpdate', + 1, + timeout, + (msgRecords: RawMessage[]) => { + for (let msgRecord of msgRecords) { + if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) { + return true + } + } + return false + }, + '0', + peer, + msgElements, + new Map() + ) + const retMsg = data[1].find(msgRecord => { + if (msgRecord.guildId === msgId) { + return true + } + }) + return retMsg! } static async sendMsgV2(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { - if (peer.chatType === ChatType.temp) { - //await NTQQMsgApi.PrepareTempChat().then().catch() - } function generateMsgId() { const timestamp = Math.floor(Date.now() / 1000) const random = Math.floor(Math.random() * Math.pow(2, 32)) @@ -289,74 +203,67 @@ export class NTQQMsgApi { } static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { - const waiter = sendWaiter(destPeer, true, 10000) - callNTQQApi({ - methodName: NTQQApiMethod.FORWARD_MSG, - args: [ - { - msgIds: msgIds, - srcContact: srcPeer, - dstContacts: [destPeer], - commentElements: [], - msgAttributeInfos: new Map(), - }, - null, - ], - }).then().catch(log) - return await waiter + const session = getSession() + return session?.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])! } - static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { - const msgInfos = msgIds.map((id) => { - return { msgId: id, senderShowName: selfInfo.nick } + static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise { + const senderShowName = await getSelfNick() + const msgInfos = msgIds.map(id => { + return { msgId: id, senderShowName } }) - const apiArgs = [ - { - msgInfos, - srcContact: srcPeer, - dstContact: destPeer, - commentElements: [], - msgAttributeInfos: new Map(), + const selfUid = getSelfUid() + let data = await NTEventDispatch.CallNormalEvent< + (msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array, attr: Map,) => Promise, + (msgList: RawMessage[]) => void + >( + 'NodeIKernelMsgService/multiForwardMsgWithComment', + 'NodeIKernelMsgListener/onMsgInfoListUpdate', + 1, + 5000, + (msgRecords: RawMessage[]) => { + for (let msgRecord of msgRecords) { + if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) { + return true + } + } + return false }, - null, - ] - return await new Promise((resolve, reject) => { - let complete = false - setTimeout(() => { - if (!complete) { - reject('转发消息超时') - } - }, 5000) - registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { - const msg = payload.msgRecord - // 需要判断它是转发的消息,并且识别到是当前转发的这一条 - const arkElement = msg.elements.find((ele) => ele.arkElement) - if (!arkElement) { - // log("收到的不是转发消息") - return - } - const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) - if (forwardData.app != 'com.tencent.multimsg') { - return - } - if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { - complete = true - await dbUtil.addMsg(msg) - resolve(msg) - log('转发消息成功:', payload) - } - }) - callNTQQApi({ - methodName: NTQQApiMethod.MULTI_FORWARD_MSG, - args: apiArgs, - }).then((result) => { - log('转发消息结果:', result, apiArgs) - if (result.result !== 0) { - complete = true - reject('转发消息失败,' + JSON.stringify(result)) - } - }) + msgInfos, + srcPeer, + destPeer, + [], + new Map() + ) + for (let msg of data[1]) { + const arkElement = msg.elements.find(ele => ele.arkElement) + if (!arkElement) { + continue + } + const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) + if (forwardData.app != 'com.tencent.multimsg') { + continue + } + if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) { + return msg + } + } + throw new Error('转发消息超时') + } + + static async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) { + const session = getSession() + const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, { + chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa + filterMsgType: [], + filterSendersUid: [], + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: false, + isIncludeCurrent: true, + pageLimit: 1, }) + return ret! } static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { diff --git a/src/ntqqapi/api/user.ts b/src/ntqqapi/api/user.ts index 4c2d7fa..1bdd5fe 100644 --- a/src/ntqqapi/api/user.ts +++ b/src/ntqqapi/api/user.ts @@ -1,8 +1,8 @@ import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types' import { ReceiveCmdS } from '../hook' -import { selfInfo, uidMaps, friends, groupMembers } from '@/common/data' -import { cacheFunc, isQQ998, log, sleep, getBuildVersion } from '@/common/utils' +import { friends, groupMembers, getSelfUin } from '@/common/data' +import { CacheClassFuncAsync, log, getBuildVersion } from '@/common/utils' import { getSession } from '@/ntqqapi/wrapper' import { RequestUtil } from '@/common/utils/request' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services' @@ -10,15 +10,6 @@ import { NodeIKernelProfileListener } from '../listeners' import { NTEventDispatch } from '@/common/utils/EventTask' import { NTQQFriendApi } from './friend' -const userInfoCache: Record = {} // uid: User - -export interface ClientKeyData extends GeneralCallResult { - url: string - keyIndex: string - clientKey: string - expireTime: string -} - export class NTQQUserApi { static async setQQAvatar(filePath: string) { return await callNTQQApi({ @@ -92,42 +83,25 @@ export class NTQQUserApi { if (getBuildVersion() >= 26702) { return this.fetchUserDetailInfo(uid) } - // this.getUserInfo(uid) - let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO - if (!withBizInfo) { - methodName = NTQQApiMethod.USER_DETAIL_INFO - } - const fetchInfo = async () => { - const result = await callNTQQApi<{ info: User }>({ - methodName, - cbCmd: ReceiveCmdS.USER_DETAIL_INFO, - afterFirstCmd: false, - cmdCB: (payload) => { - const success = payload.info.uid == uid - // log("get user detail info", success, uid, payload) - return success + type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] + type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'] + const [_retData, profile] = await NTEventDispatch.CallNormalEvent + + ( + 'NodeIKernelProfileService/getUserDetailInfoWithBizInfo', + 'NodeIKernelProfileListener/onProfileDetailInfoChanged', + 2, + 5000, + (profile: User) => { + if (profile.uid === uid) { + return true + } + return false }, - args: [ - { - uid, - }, - null, - ], - }) - const info = result.info - if (info?.uin) { - uidMaps[info.uid] = info.uin - } - return info - } - // 首次请求两次才能拿到的等级信息 - if (!userInfoCache[uid] && getLevel) { - await fetchInfo() - await sleep(1000) - } - const userInfo = await fetchInfo() - userInfoCache[uid] = userInfo - return userInfo + uid, + [0] + ) + return profile } // return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey=' @@ -144,7 +118,8 @@ export class NTQQUserApi { } static async getQzoneCookies() { - const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27' + 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' let cookies: { [key: string]: string } = {} try { cookies = await RequestUtil.HttpsGetCookies(requestUrl) @@ -159,27 +134,19 @@ export class NTQQUserApi { if (clientKeyData.result !== 0) { throw new Error('获取clientKey失败') } - const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin() + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex return (await RequestUtil.HttpsGetCookies(url))?.skey } - @cacheFunc(60 * 30 * 1000) + @CacheClassFuncAsync(1800 * 1000) static async getCookies(domain: string) { - if (domain.endsWith("qzone.qq.com")) { - let data = (await NTQQUserApi.getQzoneCookies()) - const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin - return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue } - } - const skey = await this.getSkey() - const pskey = (await this.getPSkey([domain])).get(domain) - if (!pskey || !skey) { - throw new Error('获取Cookies失败') - } - const bkn = NTQQUserApi.genBkn(skey) - const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}` - return { cookies, bkn } + const ClientKeyData = await NTQQUserApi.forceFetchClientKey() + const uin = getSelfUin() + const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27' + const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl) + return cookies } static genBkn(sKey: string) { @@ -197,15 +164,15 @@ export class NTQQUserApi { static async getPSkey(domains: string[]): Promise> { const session = getSession() const res = await session?.getTipOffService().getPskey(domains, true) - if (res.result !== 0) { - throw new Error(`获取Pskey失败: ${res.errMsg}`) + if (res?.result !== 0) { + throw new Error(`获取Pskey失败: ${res?.errMsg}`) } return res.domainPskeyMap } - static async getClientKey(): Promise { + static async getClientKey() { const session = getSession() - return await session?.getTicketService().forceFetchClientKey('') + return await session?.getTicketService().forceFetchClientKey('')! } static async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> { @@ -291,4 +258,55 @@ export class NTQQUserApi { Uin ) } + + static async getUinByUidV1(Uid: string) { + const ret = await NTEventDispatch.CallNoListenerEvent + <(Uin: string[]) => Promise<{ uinInfo: Map }>>( + 'NodeIKernelUixConvertService/getUin', + 5000, + [Uid] + ) + let uin = ret.uinInfo.get(Uid) + if (!uin) { + //从Buddy缓存获取Uin + friends.forEach((t) => { + if (t.uid == Uid) { + uin = t.uin + } + }) + } + if (!uin) { + uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换 + } + return uin + } + + static async getUinByUidV2(Uid: string) { + const session = getSession() + let uin = (await session?.getProfileService().getUinByUid('FriendsServiceImpl', [Uid]))?.get(Uid) + if (uin) return uin + uin = (await session?.getGroupService().getUinByUids([Uid]))?.uins.get(Uid) + if (uin) return uin + uin = (await session?.getUixConvertService().getUin([Uid]))?.uinInfo.get(Uid) + if (uin) return uin + uin = (await NTQQFriendApi.getBuddyIdMapCache(true)).getKey(Uid) //从Buddy缓存获取Uin + if (uin) return uin + uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(Uid) + if (uin) return uin + uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换 + return uin + } + + static async getUinByUid(Uid: string) { + if (getBuildVersion() >= 26702) { + return await NTQQUserApi.getUinByUidV2(Uid) + } + return await NTQQUserApi.getUinByUidV1(Uid) + } + + @CacheClassFuncAsync(3600 * 1000, 'ClientKey') + static async forceFetchClientKey() { + const session = getSession() + return await session?.getTicketService().forceFetchClientKey('')! + } } diff --git a/src/ntqqapi/api/webapi.ts b/src/ntqqapi/api/webapi.ts index 44c5063..d3f1aaf 100644 --- a/src/ntqqapi/api/webapi.ts +++ b/src/ntqqapi/api/webapi.ts @@ -1,7 +1,8 @@ -import { WebGroupData, groups, selfInfo } from '@/common/data' +import { getSelfUin } from '@/common/data' import { log } from '@/common/utils/log' import { NTQQUserApi } from './user' import { RequestUtil } from '@/common/utils/request' +import { CacheClassFuncAsync } from '@/common/utils/helper' export enum WebHonorType { ALL = 'all', @@ -137,56 +138,44 @@ export class WebApi { return ret } + @CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members') static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise { - log('webapi 获取群成员', GroupCode); - let MemberData: Array = new Array(); + //logDebug('webapi 获取群成员', GroupCode) + let MemberData: Array = new Array() try { - let cachedData = WebGroupData.GroupData.get(GroupCode); - let cachedTime = WebGroupData.GroupTime.get(GroupCode); - - if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) { - const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; - const _Skey = await NTQQUserApi.getSkey(); - const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; - if (!_Skey || !_Pskey) { - return MemberData; - } - const Bkn = WebApi.genBkn(_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]); - } - } - //初始化获取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]); - } - } - WebGroupData.GroupData.set(GroupCode, MemberData); - WebGroupData.GroupTime.set(GroupCode, Date.now()); + 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 { - MemberData = cachedData as Array; + for (const key in fastRet.mems) { + MemberData.push(fastRet.mems[key]) + } + } + //初始化获取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 } - return MemberData; + 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`; @@ -203,49 +192,47 @@ export class WebApi { static async setGroupNotice(GroupCode: string, Content: string = '') { //https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn} //qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1} - const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; - const _Skey = await NTQQUserApi.getSkey(); - const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; - let ret: any = undefined; - //console.log(CookieValue); + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'] + const _Skey = await NTQQUserApi.getSkey() + const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin() + let ret: any = undefined + //console.log(CookieValue) if (!_Skey || !_Pskey) { //获取Cookies失败 - return undefined; + return undefined } - const Bkn = WebApi.genBkn(_Skey); - const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}'; - const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn; + const Bkn = WebApi.genBkn(_Skey) + const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}' + const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn try { - ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); - return ret; + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }) + return ret } catch (e) { - return undefined; + return undefined } - return undefined; } static async getGrouptNotice(GroupCode: string): Promise { - const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; - const _Skey = await NTQQUserApi.getSkey(); - const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; - let ret: WebApiGroupNoticeRet | undefined = undefined; - //console.log(CookieValue); + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'] + const _Skey = await NTQQUserApi.getSkey() + const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin() + let ret: WebApiGroupNoticeRet | undefined = undefined + //console.log(CookieValue) if (!_Skey || !_Pskey) { //获取Cookies失败 - return undefined; + return undefined } - const Bkn = WebApi.genBkn(_Skey); - const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20'; + const Bkn = WebApi.genBkn(_Skey) + const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20' try { - ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }) if (ret?.ec !== 0) { - return undefined; + return undefined } - return ret; + return ret } catch (e) { - return undefined; + return undefined } - return undefined; } static genBkn(sKey: string) { diff --git a/src/ntqqapi/constructor.ts b/src/ntqqapi/constructor.ts index 4f1f5f6..949d6e2 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -77,7 +77,7 @@ export class SendMsgElementConstructor { throw '文件异常,大小为0' } const maxMB = 30; - if (fileSize > 1024 * 1024 * 30){ + if (fileSize > 1024 * 1024 * 30) { throw `图片过大,最大支持${maxMB}MB,当前文件大小${fileSize}B` } const imageSize = await NTQQFileApi.getImageSize(picPath) @@ -104,21 +104,21 @@ export class SendMsgElementConstructor { } } - static async file(filePath: string, fileName: string = ''): Promise { - const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) + static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise { + const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) if (fileSize === 0) { - throw '文件异常,大小为0' + throw '文件异常,大小为 0' } - let element: SendFileElement = { + const element: SendFileElement = { elementType: ElementType.FILE, elementId: '', fileElement: { fileName: fileName || _fileName, - filePath: path, + folderId: folderId, + filePath: path!, fileSize: fileSize.toString(), }, } - return element } @@ -175,7 +175,6 @@ export class SendMsgElementConstructor { setTimeout(useDefaultThumb, 5000) ffmpeg(filePath) - .on('end', () => {}) .on('error', (err) => { if (diyThumbPath) { fs.copyFile(diyThumbPath, thumbPath) @@ -280,10 +279,10 @@ export class SendMsgElementConstructor { faceId = parseInt(faceId.toString()) // let faceType = parseInt(faceId.toString().substring(0, 1)); let faceType = 1 - if (faceId >= 222){ + if (faceId >= 222) { faceType = 2 } - if (face?.AniStickerType){ + if (face?.AniStickerType) { faceType = 3; } return { diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index b8d651d..781f04d 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -1,16 +1,15 @@ import type { BrowserWindow } from 'electron' import { NTQQApiClass, NTQQApiMethod } from './ntcall' -import { NTQQMsgApi, sendMessagePool } from './api/msg' -import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types' +import { NTQQMsgApi } from './api/msg' +import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types' import { deleteGroup, friends, getFriend, getGroupMember, - groups, rawFriends, - selfInfo, - tempGroupCodeMap, - uidMaps, + groups, + getSelfUin, + setSelfInfo } from '@/common/data' import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { postOb11Event } from '../onebot11/server/post-ob11-event' @@ -244,18 +243,7 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) { continue } log('update group', group) - // if (!activatedGroups.includes(group.groupCode)) { - NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }) - .then((r) => { - // activatedGroups.push(group.groupCode); - // log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r) - // if (r.result !== 0) { - // setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500); - // }else { - // } - }) - .catch(log) - // } + NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().catch(log) let existGroup = groups.find((g) => g.groupCode == group.groupCode) if (existGroup) { Object.assign(existGroup, group) @@ -295,12 +283,13 @@ async function processGroupEvent(payload: { groupList: Group[] }) { } // 判断bot是否是管理员,如果是管理员不需要从这里得知有人退群,这里的退群无法得知是主动退群还是被踢 - let bot = await getGroupMember(group.groupCode, selfInfo.uin) + const selfUin = getSelfUin() + const bot = await getGroupMember(group.groupCode, selfUin) if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) { continue } for (const member of oldMembers) { - if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) { + if (!newMembersSet.has(member.uin) && member.uin != selfUin) { postOb11Event( new OB11GroupDecreaseEvent( parseInt(group.groupCode), @@ -402,8 +391,6 @@ export async function startHook() { registerReceiveHook<{ data: CategoryFriend[] }>(ReceiveCmdS.FRIENDS, (payload) => { - rawFriends.length = 0; - rawFriends.push(...payload.data); for (const fData of payload.data) { const _friends = fData.buddyList for (let friend of _friends) { @@ -420,23 +407,6 @@ export async function startHook() { }) registerReceiveHook<{ msgList: Array }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { - // 保存一下uid - for (const message of payload.msgList) { - const uid = message.senderUid - const uin = message.senderUin - if (uid && uin) { - if (message.chatType === ChatType.temp) { - dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => { - if (!receivedTempUinMap[uin]) { - receivedTempUinMap[uin] = uid - dbUtil.setReceivedTempUinMap(receivedTempUinMap) - } - }) - } - uidMaps[uid] = uin - } - } - // 自动清理新消息文件 const { autoDeleteFile } = getConfigUtil().getConfig() if (!autoDeleteFile) { @@ -459,10 +429,6 @@ export async function startHook() { if (msgElement.picElement) { pathList.push(...Object.values(msgElement.picElement.thumbPath)) } - const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement - if (aioOpGrayTipElement) { - tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat - } // log("需要清理的文件", pathList); for (const path of pathList) { @@ -479,22 +445,13 @@ export async function startHook() { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { const message = msgRecord - const peerUid = message.peerUid - // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); - // log("收到自己发送成功的消息", message.msgId, message.msgSeq); dbUtil.addMsg(message).then() - const sendCallback = sendMessagePool[peerUid] - if (sendCallback) { - try { - sendCallback(message) - } catch (e: any) { - log('receive self msg error', e.stack) - } - } }) registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { - selfInfo.online = info.info.status !== 20 + setSelfInfo({ + online: info.info.status !== 20 + }) }) let activatedPeerUids: string[] = [] diff --git a/src/ntqqapi/native/cpmodule.ts b/src/ntqqapi/native/cpmodule.ts deleted file mode 100644 index ab98263..0000000 --- a/src/ntqqapi/native/cpmodule.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as os from "os"; -import path from "node:path"; -import fs from "fs"; - -export function getModuleWithArchName(moduleName: string) { - const systemPlatform = os.platform() - const cpuArch = os.arch() - return `${moduleName}-${systemPlatform}-${cpuArch}.node` -} - -export function cpModule(moduleName: string) { - const currentDir = path.resolve(__dirname); - const fileName = `./${getModuleWithArchName(moduleName)}` - try { - fs.copyFileSync(path.join(currentDir, fileName), path.join(currentDir, `${moduleName}.node`)); - } catch (e) { - - } -} \ No newline at end of file diff --git a/src/ntqqapi/native/crychic/crychic-win32-x64.node b/src/ntqqapi/native/crychic/crychic-win32-x64.node deleted file mode 100644 index 2f3e843..0000000 Binary files a/src/ntqqapi/native/crychic/crychic-win32-x64.node and /dev/null differ diff --git a/src/ntqqapi/native/crychic/index.ts b/src/ntqqapi/native/crychic/index.ts deleted file mode 100644 index a5ffa25..0000000 --- a/src/ntqqapi/native/crychic/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { log } from '../../../common/utils' -import { NTQQApi } from '../../ntcall' -import { cpModule } from '../cpmodule' - -type PokeHandler = (id: string, isGroup: boolean) => void -type CrychicHandler = (event: string, id: string, isGroup: boolean) => void - -let pokeRecords: Record = {} - -class Crychic { - private crychic: any = undefined - - loadNode() { - if (!this.crychic) { - try { - cpModule('crychic') - this.crychic = require('./crychic.node') - this.crychic.init() - } catch (e) { - log('crychic加载失败', e) - } - } - } - - registerPokeHandler(fn: PokeHandler) { - this.registerHandler((event, id, isGroup) => { - if (event === 'poke') { - let existTime = pokeRecords[id] - if (existTime) { - if (Date.now() - existTime < 1500) { - return - } - } - pokeRecords[id] = Date.now() - fn(id, isGroup) - } - }) - } - - registerHandler(fn: CrychicHandler) { - if (!this.crychic) return - this.crychic.setCryHandler(fn) - } - - sendFriendPoke(friendUid: string) { - if (!this.crychic) return - this.crychic.sendFriendPoke(parseInt(friendUid)) - NTQQApi.fetchUnitedCommendConfig().then() - } - - sendGroupPoke(groupCode: string, memberUin: string) { - if (!this.crychic) return - this.crychic.sendGroupPoke(parseInt(memberUin), parseInt(groupCode)) - NTQQApi.fetchUnitedCommendConfig().then() - } -} - -export const crychic = new Crychic() \ No newline at end of file diff --git a/src/ntqqapi/native/moehook/hook.ts b/src/ntqqapi/native/moehook/hook.ts deleted file mode 100644 index 05392a6..0000000 --- a/src/ntqqapi/native/moehook/hook.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {cpModule} from "../cpmodule"; -import { qqPkgInfo } from '@/common/utils/QQBasicInfo' - -interface MoeHook { - GetRkey: () => string, // Return '&rkey=xxx' - HookRkey: (version: string) => string -} - - -class HookApi { - private readonly moeHook: MoeHook | null = null; - - constructor() { - cpModule('MoeHoo'); - try { - this.moeHook = require('./MoeHoo.node'); - console.log("hook rkey qq version", this.moeHook!.HookRkey(qqPkgInfo.version)); - console.log("hook rkey地址", this.moeHook!.HookRkey(qqPkgInfo.version)); - } catch (e) { - console.log('加载 moehoo 失败', e); - } - } - - getRKey(): string { - return this.moeHook?.GetRkey() || ''; - } - - isAvailable() { - return !!this.moeHook; - } -} - -// export const hookApi = new HookApi(); diff --git a/src/ntqqapi/services/NodeIKernelRichMediaService.ts b/src/ntqqapi/services/NodeIKernelRichMediaService.ts new file mode 100644 index 0000000..42dbc72 --- /dev/null +++ b/src/ntqqapi/services/NodeIKernelRichMediaService.ts @@ -0,0 +1,270 @@ +import { GetFileListParam, MessageElement, Peer } from '../types' +import { GeneralCallResult } from './common' + +export enum UrlFileDownloadType { + KUNKNOWN, + KURLFILEDOWNLOADPRIVILEGEICON, + KURLFILEDOWNLOADPHOTOWALL, + KURLFILEDOWNLOADQZONE, + KURLFILEDOWNLOADCOMMON, + KURLFILEDOWNLOADINSTALLAPP +} + +export enum RMBizTypeEnum { + KUNKNOWN, + KC2CFILE, + KGROUPFILE, + KC2CPIC, + KGROUPPIC, + KDISCPIC, + KC2CVIDEO, + KGROUPVIDEO, + KC2CPTT, + KGROUPPTT, + KFEEDCOMMENTPIC, + KGUILDFILE, + KGUILDPIC, + KGUILDPTT, + KGUILDVIDEO +} + +export interface CommonFileInfo { + bizType: number + chatType: number + elemId: string + favId: string + fileModelId: string + fileName: string + fileSize: string + md5: string + md510m: string + msgId: string + msgTime: string + parent: string + peerUid: string + picThumbPath: Array + sha: string + sha3: string + subId: string + uuid: string +} + +export interface NodeIKernelRichMediaService { + //getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb) + // public enum VideoCodecFormatType { + // KCODECFORMATH264, + // KCODECFORMATH265, + // KCODECFORMATH266, + // KCODECFORMATAV1 + // } + // public enum VideoRequestWay { + // KUNKNOW, + // KHAND, + // KAUTO + // } + getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise + + //exParams (RMReqExParams) + // this.downSourceType = i2 + // this.triggerType = i3 + //peer, msgId, elemId, videoCodecFormat, exParams + // 1 0 频道在用 + // 1 1 + // 0 2 + + // public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007 + // public static final int KDOWNSOURCETYPEAIOINNER = 1 + // public static final int KDOWNSOURCETYPEBIGSCREEN = 2 + // public static final int KDOWNSOURCETYPEHISTORY = 3 + // public static final int KDOWNSOURCETYPEUNKNOWN = 0 + + // public static final int KTRIGGERTYPEAUTO = 1 + // public static final int KTRIGGERTYPEMANUAL = 0 + + getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise, + videoCodecFormat: number + } + }> + + getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown + + // this.senderUid = "" + // this.peerUid = "" + // this.guildId = "" + // this.elem = new MsgElement() + // this.downloadType = i2 + // this.thumbSize = i3 + // this.msgId = j2 + // this.msgRandom = j3 + // this.msgSeq = j4 + // this.msgTime = j5 + // this.chatType = i4 + // this.senderUid = str + // this.peerUid = str2 + // this.guildId = str3 + // this.elem = msgElement + // this.useHttps = num + + getVideoPlayUrlInVisit(arg: { + downloadType: number, + thumbSize: number, + msgId: string, + msgRandom: string, + msgSeq: string, + msgTime: string, + chatType: number, + senderUid: string, + peerUid: string, + guildId: string, + ele: MessageElement, + useHttps: boolean + }): Promise + + //arg双端number + isFileExpired(arg: number): unknown + + deleteGroupFolder(GroupCode: string, FolderId: string): Promise + + //参数与getVideoPlayUrlInVisit一样 + downloadRichMediaInVisit(arg: { + downloadType: number, + thumbSize: number, + msgId: string, + msgRandom: string, + msgSeq: string, + msgTime: string, + chatType: number, + senderUid: string, + peerUid: string, + guildId: string, + ele: MessageElement, + useHttps: boolean + }): unknown + //arg3为“” + downloadFileForModelId(peer: Peer, ModelId: string[], arg3: string): unknown + //第三个参数 Array + // this.fileId = "" + // this.fileName = "" + // this.fileId = str + // this.fileName = str2 + // this.fileSize = j2 + // this.fileModelId = j3 + + downloadFileForFileUuid(peer: Peer, uuid: string, arg3: { + fileId: string, + fileName: string, + fileSize: string, + fileModelId: string + }[]): Promise + + downloadFileByUrlList(fileDownloadTyp: UrlFileDownloadType, urlList: Array): unknown + + downloadFileForFileInfo(fileInfo: CommonFileInfo[], savePath: string): unknown + + createGroupFolder(GroupCode: string, FolderName: string): Promise } }> + + downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown + + createGroupFolder(arg1: unknown, arg2: unknown): unknown + + downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown + + renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown + + deleteGroupFolder(arg1: unknown, arg2: unknown): unknown + + deleteTransferInfo(arg1: unknown, arg2: unknown): unknown + + cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown + + cancelUrlDownload(arg: unknown): unknown + + updateOnlineVideoElemStatus(arg: unknown): unknown + + getGroupSpace(arg: unknown): unknown + + getGroupFileList(groupCode: string, params: GetFileListParam): Promise + + getGroupFileInfo(arg1: unknown, arg2: unknown): unknown + + getGroupTransferList(arg1: unknown, arg2: unknown): unknown + + renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown + + moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown + + transGroupFile(arg1: unknown, arg2: unknown): unknown + + searchGroupFile( + keywords: Array, + param: { + groupIds: Array, + fileType: number, + context: string, + count: number, + sortType: number, + groupNames: Array + }): Promise + searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown + + deleteGroupFile(GroupCode: string, params: Array, Files: Array): Promise + failFileIdList: Array + } + }> + + translateEnWordToZn(words: string[]): Promise + + getScreenOCR(path: string): Promise + + batchGetGroupFileCount(Gids: Array): Promise, groupFileCounts: Array }> + + queryPicDownloadSize(arg: unknown): unknown + + searchGroupFile(arg1: unknown, arg2: unknown): unknown + + searchMoreGroupFile(arg: unknown): unknown + + cancelSearcheGroupFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown + + onlyDownloadFile(peer: Peer, arg2: unknown, arg3: Array<{ + fileId: string, + fileName: string, + fileSize: string, + fileModelId: string + } + >): unknown + + onlyUploadFile(arg1: unknown, arg2: unknown): unknown + + isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown + + uploadRMFileWithoutMsg(arg: { + bizType: RMBizTypeEnum, + filePath: string, + peerUid: string, + transferId: string + useNTV2: string + }): Promise + + isNull(): boolean +} \ No newline at end of file diff --git a/src/ntqqapi/services/NodeIKernelTicketService.ts b/src/ntqqapi/services/NodeIKernelTicketService.ts new file mode 100644 index 0000000..f1ab0e5 --- /dev/null +++ b/src/ntqqapi/services/NodeIKernelTicketService.ts @@ -0,0 +1,11 @@ +import { forceFetchClientKeyRetType } from './common' + +export interface NodeIKernelTicketService { + addKernelTicketListener(listener: unknown): void + + removeKernelTicketListener(listenerId: unknown): void + + forceFetchClientKey(arg: string): Promise + + isNull(): boolean +} \ No newline at end of file diff --git a/src/ntqqapi/services/NodeIKernelTipOffService.ts b/src/ntqqapi/services/NodeIKernelTipOffService.ts new file mode 100644 index 0000000..62f87ce --- /dev/null +++ b/src/ntqqapi/services/NodeIKernelTipOffService.ts @@ -0,0 +1,19 @@ +import { GeneralCallResult } from './common' + +export interface NodeIKernelTipOffService { + addKernelTipOffListener(listener: unknown): void + + removeKernelTipOffListener(listenerId: unknown): void + + tipOffSendJsData(args: unknown[]): Promise //2 + + getPskey(domainList: string[], nocache: boolean): Promise }> //2 + + tipOffSendJsData(args: unknown[]): Promise //2 + + tipOffMsgs(args: unknown[]): Promise //1 + + encodeUinAesInfo(args: unknown[]): Promise //2 + + isNull(): boolean +} \ No newline at end of file diff --git a/src/ntqqapi/services/index.ts b/src/ntqqapi/services/index.ts index 88079a8..8d518da 100644 --- a/src/ntqqapi/services/index.ts +++ b/src/ntqqapi/services/index.ts @@ -4,4 +4,7 @@ export * from './NodeIKernelGroupService' export * from './NodeIKernelProfileLikeService' export * from './NodeIKernelMsgService' export * from './NodeIKernelMSFService' -export * from './NodeIKernelUixConvertService' \ No newline at end of file +export * from './NodeIKernelUixConvertService' +export * from './NodeIKernelRichMediaService' +export * from './NodeIKernelTicketService' +export * from './NodeIKernelTipOffService' \ No newline at end of file diff --git a/src/ntqqapi/types/msg.ts b/src/ntqqapi/types/msg.ts index c7c7a02..ec70771 100644 --- a/src/ntqqapi/types/msg.ts +++ b/src/ntqqapi/types/msg.ts @@ -1,6 +1,15 @@ import { GroupMemberRole } from './group' +export interface GetFileListParam { + sortType: number + fileCount: number + startIndex: number + sortOrder: number + showOnlinedocFolder: number +} + export enum ElementType { + UNKNOWN = 0, TEXT = 1, PIC = 2, FILE = 3, @@ -8,8 +17,30 @@ export enum ElementType { VIDEO = 5, FACE = 6, REPLY = 7, + WALLET = 9, + GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字 ARK = 10, MFACE = 11, + LIVEGIFT = 12, + STRUCTLONGMSG = 13, + MARKDOWN = 14, + GIPHY = 15, + MULTIFORWARD = 16, + INLINEKEYBOARD = 17, + INTEXTGIFT = 18, + CALENDAR = 19, + YOLOGAMERESULT = 20, + AVRECORD = 21, + FEED = 22, + TOFURECORD = 23, + ACEBUBBLE = 24, + ACTIVITY = 25, + TOFU = 26, + FACEBUBBLE = 27, + SHARELOCATION = 28, + TASKTOPMSG = 29, + RECOMMENDEDMSG = 43, + ACTIONBAR = 44 } export interface SendTextElement { @@ -101,18 +132,19 @@ export interface ReplyElement { } export interface FileElement { - fileMd5?: '' + fileMd5?: string fileName: string filePath: string fileSize: string picHeight?: number picWidth?: number - picThumbPath?: {} - file10MMd5?: '' - fileSha?: '' - fileSha3?: '' - fileUuid?: '' - fileSubId?: '' + folderId?: string + picThumbPath?: Map + file10MMd5?: string + fileSha?: string + fileSha3?: string + fileUuid?: string + fileSubId?: string thumbFileSize?: number fileBizId?: number } @@ -158,6 +190,50 @@ export enum ChatType { temp = 100, } +// 来自Android分析 +export enum ChatType2 { + KCHATTYPEADELIE = 42, + KCHATTYPEBUDDYNOTIFY = 5, + KCHATTYPEC2C = 1, + KCHATTYPECIRCLE = 113, + KCHATTYPEDATALINE = 8, + KCHATTYPEDATALINEMQQ = 134, + KCHATTYPEDISC = 3, + KCHATTYPEFAV = 41, + KCHATTYPEGAMEMESSAGE = 105, + KCHATTYPEGAMEMESSAGEFOLDER = 116, + KCHATTYPEGROUP = 2, + KCHATTYPEGROUPBLESS = 133, + KCHATTYPEGROUPGUILD = 9, + KCHATTYPEGROUPHELPER = 7, + KCHATTYPEGROUPNOTIFY = 6, + KCHATTYPEGUILD = 4, + KCHATTYPEGUILDMETA = 16, + KCHATTYPEMATCHFRIEND = 104, + KCHATTYPEMATCHFRIENDFOLDER = 109, + KCHATTYPENEARBY = 106, + KCHATTYPENEARBYASSISTANT = 107, + KCHATTYPENEARBYFOLDER = 110, + KCHATTYPENEARBYHELLOFOLDER = 112, + KCHATTYPENEARBYINTERACT = 108, + KCHATTYPEQQNOTIFY = 132, + KCHATTYPERELATEACCOUNT = 131, + KCHATTYPESERVICEASSISTANT = 118, + KCHATTYPESERVICEASSISTANTSUB = 201, + KCHATTYPESQUAREPUBLIC = 115, + KCHATTYPESUBSCRIBEFOLDER = 30, + KCHATTYPETEMPADDRESSBOOK = 111, + KCHATTYPETEMPBUSSINESSCRM = 102, + KCHATTYPETEMPC2CFROMGROUP = 100, + KCHATTYPETEMPC2CFROMUNKNOWN = 99, + KCHATTYPETEMPFRIENDVERIFY = 101, + KCHATTYPETEMPNEARBYPRO = 119, + KCHATTYPETEMPPUBLICACCOUNT = 103, + KCHATTYPETEMPWPA = 117, + KCHATTYPEUNKNOWN = 0, + KCHATTYPEWEIYUN = 40, +} + export interface PttElement { canConvert2Text: boolean duration: number // 秒数 @@ -386,15 +462,18 @@ export interface RawMessage { senderUin?: string // 发送者QQ号 peerUid: string // 群号 或者 QQ uid peerUin: string // 群号 或者 发送者QQ号 + guildId: string sendNickName: string sendMemberName?: string // 发送者群名片 chatType: ChatType sendStatus?: number // 消息状态,别人发的2是已撤回,自己发的2是已发送 recallTime: string // 撤回时间, "0"是没有撤回 + records: RawMessage[] elements: { elementId: string elementType: ElementType replyElement: { + sourceMsgIdInRecords: string senderUid: string // 原消息发送者QQ号 sourceMsgIsIncPic: boolean // 原消息是否有图片 sourceMsgText: string diff --git a/src/ntqqapi/types/notify.ts b/src/ntqqapi/types/notify.ts index 0b8c05b..1310612 100644 --- a/src/ntqqapi/types/notify.ts +++ b/src/ntqqapi/types/notify.ts @@ -48,8 +48,28 @@ export enum GroupRequestOperateTypes { reject = 2, } +export enum BuddyReqType { + KMEINITIATOR, + KPEERINITIATOR, + KMEAGREED, + KMEAGREEDANDADDED, + KPEERAGREED, + KPEERAGREEDANDADDED, + KPEERREFUSED, + KMEREFUSED, + KMEIGNORED, + KMEAGREEANYONE, + KMESETQUESTION, + KMEAGREEANDADDFAILED, + KMSGINFO, + KMEINITIATORWAITPEERCONFIRM +} + export interface FriendRequest { + isInitiator?: boolean + isDecide: boolean friendUid: string + reqType: BuddyReqType reqTime: string // 时间戳,秒 extWords: string // 申请人填写的验证消息 isUnread: boolean diff --git a/src/ntqqapi/wrapper.ts b/src/ntqqapi/wrapper.ts index 40bfc8f..a96fcaa 100644 --- a/src/ntqqapi/wrapper.ts +++ b/src/ntqqapi/wrapper.ts @@ -5,7 +5,10 @@ import { NodeIKernelProfileLikeService, NodeIKernelMSFService, NodeIKernelMsgService, - NodeIKernelUixConvertService + NodeIKernelUixConvertService, + NodeIKernelRichMediaService, + NodeIKernelTicketService, + NodeIKernelTipOffService } from './services' import os from 'node:os' const Process = require('node:process') @@ -19,6 +22,9 @@ export interface NodeIQQNTWrapperSession { getMsgService(): NodeIKernelMsgService getMSFService(): NodeIKernelMSFService getUixConvertService(): NodeIKernelUixConvertService + getRichMediaService(): NodeIKernelRichMediaService + getTicketService(): NodeIKernelTicketService + getTipOffService(): NodeIKernelTipOffService } export interface WrapperApi { diff --git a/src/onebot11/action/file/GetFile.ts b/src/onebot11/action/file/GetFile.ts index a231de0..f59d7c8 100644 --- a/src/onebot11/action/file/GetFile.ts +++ b/src/onebot11/action/file/GetFile.ts @@ -37,7 +37,7 @@ export abstract class GetFileBase extends BaseAction { +interface Payload { + user_id: number | string +} + +export default class GoCQHTTPGetStrangerInfo extends BaseAction { actionName = ActionName.GoCQHTTP_GetStrangerInfo - protected async _handle(payload: { user_id: number }): Promise { - const user_id = payload.user_id.toString() - const uid = getUidByUin(user_id) - if (!uid) { - throw new Error('查无此人') + protected async _handle(payload: Payload): Promise { + if (!(getBuildVersion() >= 26702)) { + const user_id = payload.user_id.toString() + const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id) + const uid = (await NTQQUserApi.getUidByUin(user_id))! + if (!uid || uid.indexOf('*') != -1) { + const ret = { + ...extendData, + user_id: parseInt(extendData.info.uin) || 0, + nickname: extendData.info.nick, + sex: OB11UserSex.unknown, + age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year, + qid: extendData.info.qid, + level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0, + login_days: 0, + uid: '' + } + return ret + } + const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) } + return OB11Constructor.stranger(data) + } else { + const user_id = payload.user_id.toString() + const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id) + const uid = (await NTQQUserApi.getUidByUin(user_id))! + if (!uid || uid.indexOf('*') != -1) { + const ret = { + ...extendData, + user_id: parseInt(extendData.detail.uin) || 0, + nickname: extendData.detail.simpleInfo.coreInfo.nick, + sex: OB11UserSex.unknown, + age: 0, + level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0, + login_days: 0, + uid: '' + } + return ret + } + const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) } + return OB11Constructor.stranger(data) } - return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true)) } } diff --git a/src/onebot11/action/go-cqhttp/UploadFile.ts b/src/onebot11/action/go-cqhttp/UploadFile.ts index 2a9cc3a..08a0b69 100644 --- a/src/onebot11/action/go-cqhttp/UploadFile.ts +++ b/src/onebot11/action/go-cqhttp/UploadFile.ts @@ -1,50 +1,72 @@ +import fs from 'node:fs' import BaseAction from '../BaseAction' -import { getGroup, getUidByUin } from '@/common/data' +import { getGroup } from '@/common/data' import { ActionName } from '../types' import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import { ChatType, SendFileElement } from '@/ntqqapi/types' -import fs from 'fs' -import { NTQQMsgApi } from '@/ntqqapi/api/msg' import { uri2local } from '@/common/utils' import { Peer } from '@/ntqqapi/types' +import { sendMsg } from '../msg/SendMsg' +import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api' interface Payload { - user_id: number - group_id?: number + user_id: number | string + group_id?: number | string file: string name: string - folder: string + folder?: string + folder_id?: string } -class GoCQHTTPUploadFileBase extends BaseAction { +export class GoCQHTTPUploadGroupFile extends BaseAction { actionName = ActionName.GoCQHTTP_UploadGroupFile - getPeer(payload: Payload): Peer { - if (payload.user_id) { - return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! } - } - return { chatType: ChatType.group, peerUid: payload.group_id?.toString()! } - } - 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}` } const downloadResult = await uri2local(file) - if (downloadResult.errMsg) { + if (!downloadResult.success) { throw new Error(downloadResult.errMsg) } - let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name) - await NTQQMsgApi.sendMsg(this.getPeer(payload), [sendFileEle]) + const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id) + await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true) return null } } -export class GoCQHTTPUploadGroupFile extends GoCQHTTPUploadFileBase { - actionName = ActionName.GoCQHTTP_UploadGroupFile -} - -export class GoCQHTTPUploadPrivateFile extends GoCQHTTPUploadFileBase { +export class GoCQHTTPUploadPrivateFile extends BaseAction { actionName = ActionName.GoCQHTTP_UploadPrivateFile + + async getPeer(payload: Payload): Promise { + if (payload.user_id) { + const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) + if (!peerUid) { + throw `私聊${payload.user_id}不存在` + } + const isBuddy = await NTQQFriendApi.isBuddy(peerUid) + return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid } + } + throw '缺少参数 user_id' + } + + protected async _handle(payload: Payload): Promise { + const peer = await this.getPeer(payload) + let file = payload.file + if (fs.existsSync(file)) { + file = `file://${file}` + } + const downloadResult = await uri2local(file) + if (!downloadResult.success) { + throw new Error(downloadResult.errMsg) + } + const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name) + await sendMsg(peer, [sendFileEle], [], true) + return null + } } diff --git a/src/onebot11/action/group/SetGroupAddRequest.ts b/src/onebot11/action/group/SetGroupAddRequest.ts index 966c453..294fcb0 100644 --- a/src/onebot11/action/group/SetGroupAddRequest.ts +++ b/src/onebot11/action/group/SetGroupAddRequest.ts @@ -5,22 +5,19 @@ import { NTQQGroupApi } from '../../../ntqqapi/api/group' interface Payload { flag: string - // sub_type: "add" | "invite", - // type: "add" | "invite" - approve: boolean - reason: string + approve?: boolean | string + reason?: string } export default class SetGroupAddRequest extends BaseAction { actionName = ActionName.SetGroupAddRequest protected async _handle(payload: Payload): Promise { - const seq = payload.flag.toString() - const approve = payload.approve.toString() === 'true' - await NTQQGroupApi.handleGroupRequest( - seq, + const flag = payload.flag.toString() + const approve = payload.approve?.toString() !== 'false' + await NTQQGroupApi.handleGroupRequest(flag, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, - payload.reason, + payload.reason || '' ) return null } diff --git a/src/onebot11/action/llonebot/GetGroupAddRequest.ts b/src/onebot11/action/llonebot/GetGroupAddRequest.ts index 6f986aa..562a9b9 100644 --- a/src/onebot11/action/llonebot/GetGroupAddRequest.ts +++ b/src/onebot11/action/llonebot/GetGroupAddRequest.ts @@ -1,10 +1,8 @@ import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types' import BaseAction from '../BaseAction' import { ActionName } from '../types' -import { uidMaps } from '../../../common/data' import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQGroupApi } from '../../../ntqqapi/api/group' -import { log } from '../../../common/utils/log' interface OB11GroupRequestNotify { group_id: number @@ -17,11 +15,10 @@ export default class GetGroupAddRequest extends BaseAction { const data = await NTQQGroupApi.getGroupIgnoreNotifies() - log(data) - let notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) - let returnData: OB11GroupRequestNotify[] = [] + const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) + const returnData: OB11GroupRequestNotify[] = [] for (const notify of notifies) { - const uin = uidMaps[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin + const uin = await NTQQUserApi.getUinByUid(notify.user1.uid) returnData.push({ group_id: parseInt(notify.group.groupCode), user_id: parseInt(uin), diff --git a/src/onebot11/action/msg/ForwardSingleMsg.ts b/src/onebot11/action/msg/ForwardSingleMsg.ts index 01083cf..1c37f47 100644 --- a/src/onebot11/action/msg/ForwardSingleMsg.ts +++ b/src/onebot11/action/msg/ForwardSingleMsg.ts @@ -1,33 +1,35 @@ import BaseAction from '../BaseAction' -import { NTQQMsgApi } from '@/ntqqapi/api' -import { ChatType, RawMessage } from '@/ntqqapi/types' +import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api' +import { ChatType } from '@/ntqqapi/types' import { dbUtil } from '@/common/db' -import { getUidByUin } from '@/common/data' import { ActionName } from '../types' import { Peer } from '@/ntqqapi/types' interface Payload { message_id: number - group_id: number - user_id?: number + group_id: number | string + user_id?: number | string } -interface Response { - message_id: number -} - -abstract class ForwardSingleMsg extends BaseAction { +abstract class ForwardSingleMsg extends BaseAction { protected async getTargetPeer(payload: Payload): Promise { if (payload.user_id) { - return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! } + const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) + if (!peerUid) { + throw new Error(`无法找到私聊对象${payload.user_id}`) + } + return { chatType: ChatType.friend, peerUid } } - return { chatType: ChatType.group, peerUid: payload.group_id.toString() } + return { chatType: ChatType.group, peerUid: payload.group_id!.toString() } } - protected async _handle(payload: Payload): Promise { - const msg = (await dbUtil.getMsgByShortId(payload.message_id))! + protected async _handle(payload: Payload): Promise { + const msg = await dbUtil.getMsgByShortId(payload.message_id) + if (!msg) { + throw new Error(`无法找到消息${payload.message_id}`) + } const peer = await this.getTargetPeer(payload) - const sentMsg = await NTQQMsgApi.forwardMsg( + const ret = await NTQQMsgApi.forwardMsg( { chatType: msg.chatType, peerUid: msg.peerUid, @@ -35,8 +37,10 @@ abstract class ForwardSingleMsg extends BaseAction { peer, [msg.msgId], ) - const ob11MsgId = await dbUtil.addMsg(sentMsg) - return { message_id: ob11MsgId! } + if (ret.result !== 0) { + throw new Error(`转发消息失败 ${ret.errMsg}`) + } + return null } } diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index 2453e45..d0c1c5c 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -2,14 +2,12 @@ import { AtType, ChatType, ElementType, - Friend, - Group, GroupMemberRole, PicSubType, RawMessage, SendMessageElement, } from '../../../ntqqapi/types' -import { friends, getGroup, getGroupMember, getUidByUin, selfInfo } from '../../../common/data' +import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '../../../common/data' import { OB11MessageCustomMusic, OB11MessageData, @@ -27,7 +25,7 @@ import { ActionName, BaseCheckResult } from '../types' import fs from 'node:fs' import { decodeCQCode } from '../../cqcode' import { dbUtil } from '../../../common/db' -import { ALLOW_SEND_TEMP_MSG, getConfigUtil } from '../../../common/config' +import { getConfigUtil } from '../../../common/config' import { log } from '../../../common/utils/log' import { sleep } from '../../../common/utils/helper' import { uri2local } from '../../../common/utils' @@ -103,7 +101,7 @@ export async function createSendElements( remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo .RemainAtAllCountForUin log(`群${groupCode}剩余at全体次数`, remainAtAllCount) - const self = await getGroupMember(groupCode, selfInfo.uin) + const self = await getGroupMember(groupCode, getSelfUin()) isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner } catch (e) { } @@ -459,7 +457,7 @@ export class SendMsg extends BaseAction { const nodeMsg = await NTQQMsgApi.sendMsg( { chatType: ChatType.friend, - peerUid: selfInfo.uid, + peerUid: getSelfUid(), }, sendElements, true, @@ -475,7 +473,7 @@ export class SendMsg extends BaseAction { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) { const selfPeer = { chatType: ChatType.friend, - peerUid: selfInfo.uid, + peerUid: getSelfUid(), } let nodeMsgIds: string[] = [] // 先判断一遍是不是id和自定义混用 @@ -491,7 +489,7 @@ export class SendMsg extends BaseAction { nodeMsgIds.push(nodeMsg?.msgId!) } else { - if (nodeMsg?.peerUid !== selfInfo.uid) { + if (nodeMsg?.peerUid !== selfPeer.peerUid) { const cloneMsg = await this.cloneMsg(nodeMsg!) if (cloneMsg) { nodeMsgIds.push(cloneMsg.msgId) @@ -564,7 +562,7 @@ export class SendMsg extends BaseAction { if (needSendSelf) { log('需要克隆转发消息') for (const [index, msg] of nodeMsgArray.entries()) { - if (msg.peerUid !== selfInfo.uid) { + if (msg.peerUid !== selfPeer.peerUid) { const cloneMsg = await this.cloneMsg(msg) if (cloneMsg) { nodeMsgIds[index] = cloneMsg.msgId @@ -585,13 +583,9 @@ export class SendMsg extends BaseAction { if (nodeMsgIds.length === 0) { throw Error('转发消息失败,节点为空') } - try { - log('开发转发', nodeMsgIds) - return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) - } catch (e) { - log('forward failed', e) - return null - } + const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) + returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) + return returnMsg } } diff --git a/src/onebot11/action/quick-operation.ts b/src/onebot11/action/quick-operation.ts index a68f86e..ad6883b 100644 --- a/src/onebot11/action/quick-operation.ts +++ b/src/onebot11/action/quick-operation.ts @@ -5,9 +5,8 @@ import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' import { dbUtil } from '@/common/db' -import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/ntqqapi/api' -import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types' -import { getGroup, getUidByUin } from '@/common/data' +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' @@ -62,16 +61,15 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc } async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { - msg = msg as OB11Message const rawMessage = await dbUtil.getMsgByShortId(msg.message_id) const reply = quickAction.reply const ob11Config = getConfigUtil().getConfig().ob11 - let peer: Peer = { + const peer: Peer = { chatType: ChatType.friend, peerUid: msg.user_id.toString(), } if (msg.message_type == 'private') { - peer.peerUid = getUidByUin(msg.user_id.toString())! + peer.peerUid = (await NTQQUserApi.getUidByUin(msg.user_id.toString()))! if (msg.sub_type === 'group') { peer.chatType = ChatType.temp } @@ -81,7 +79,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes peer.peerUid = msg.group_id?.toString()! } if (reply) { - let group: Group | null = null let replyMessage: OB11MessageData[] = [] if (ob11Config.enableQOAutoQuote) { replyMessage.push({ @@ -93,7 +90,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes } if (msg.message_type == 'group') { - group = (await getGroup(msg.group_id?.toString()!))! if ((quickAction as QuickOperationGroupMessage).at_sender) { replyMessage.push({ type: 'at', @@ -104,8 +100,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes } } replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) - const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!) - log(`发送消息给`, peer, sendElements) + const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, peer) sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log) } if (msg.message_type === 'group') { diff --git a/src/onebot11/action/system/GetLoginInfo.ts b/src/onebot11/action/system/GetLoginInfo.ts index a247a76..c05fa33 100644 --- a/src/onebot11/action/system/GetLoginInfo.ts +++ b/src/onebot11/action/system/GetLoginInfo.ts @@ -1,6 +1,6 @@ import { OB11User } from '../../types' import { OB11Constructor } from '../../constructor' -import { selfInfo } from '../../../common/data' +import { getSelfInfo, getSelfNick } from '../../../common/data' import BaseAction from '../BaseAction' import { ActionName } from '../types' @@ -8,7 +8,10 @@ class GetLoginInfo extends BaseAction { actionName = ActionName.GetLoginInfo protected async _handle(payload: null) { - return OB11Constructor.selfInfo(selfInfo) + return OB11Constructor.selfInfo({ + ...getSelfInfo(), + nick: await getSelfNick(true) + }) } } diff --git a/src/onebot11/action/system/GetStatus.ts b/src/onebot11/action/system/GetStatus.ts index cac3008..c3aa3da 100644 --- a/src/onebot11/action/system/GetStatus.ts +++ b/src/onebot11/action/system/GetStatus.ts @@ -1,14 +1,14 @@ import BaseAction from '../BaseAction' import { OB11Status } from '../../types' import { ActionName } from '../types' -import { selfInfo } from '../../../common/data' +import { getSelfInfo } from '../../../common/data' export default class GetStatus extends BaseAction { actionName = ActionName.GetStatus protected async _handle(payload: any): Promise { return { - online: selfInfo.online!, + online: getSelfInfo().online!, good: true, } } diff --git a/src/onebot11/action/user/GetCookie.ts b/src/onebot11/action/user/GetCookie.ts index 55b4115..968759c 100644 --- a/src/onebot11/action/user/GetCookie.ts +++ b/src/onebot11/action/user/GetCookie.ts @@ -1,16 +1,24 @@ import BaseAction from '../BaseAction' -import { NTQQUserApi } from '@/ntqqapi/api' +import { NTQQUserApi, WebApi } from '@/ntqqapi/api' import { ActionName } from '../types' +interface Response { + cookies: string + bkn: string +} + interface Payload { domain: string } -export class GetCookies extends BaseAction { +export class GetCookies extends BaseAction { actionName = ActionName.GetCookies protected async _handle(payload: Payload) { - const domain = payload.domain || 'qun.qq.com' - return NTQQUserApi.getCookies(domain); + const cookiesObject = await NTQQUserApi.getCookies(payload.domain) + //把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起 + const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') + const bkn = WebApi.genBkn(cookiesObject.p_skey) + return { cookies, bkn } } } diff --git a/src/onebot11/action/user/GetFriendList.ts b/src/onebot11/action/user/GetFriendList.ts index bab6ea2..8025153 100644 --- a/src/onebot11/action/user/GetFriendList.ts +++ b/src/onebot11/action/user/GetFriendList.ts @@ -1,11 +1,10 @@ +import BaseAction from '../BaseAction' import { OB11User } from '../../types' import { OB11Constructor } from '../../constructor' -import { friends, rawFriends } from '@/common/data' -import BaseAction from '../BaseAction' +import { friends } from '@/common/data' import { ActionName } from '../types' import { NTQQFriendApi } from '@/ntqqapi/api' -import { CategoryFriend } from '@/ntqqapi/types' -import { qqPkgInfo } from '@/common/utils/QQBasicInfo' +import { getBuildVersion } from '@/common/utils/QQBasicInfo' interface Payload { no_cache: boolean | string @@ -15,7 +14,7 @@ export class GetFriendList extends BaseAction { actionName = ActionName.GetFriendList protected async _handle(payload: Payload) { - if (+qqPkgInfo.buildVersion >= 26702) { + if (getBuildVersion() >= 26702) { return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true')) } if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { @@ -30,11 +29,16 @@ export class GetFriendList extends BaseAction { } } - -export class GetFriendWithCategory extends BaseAction> { +// extend +export class GetFriendWithCategory extends BaseAction { actionName = ActionName.GetFriendsWithCategory; protected async _handle(payload: void) { - return rawFriends; + if (getBuildVersion() >= 26702) { + //全新逻辑 + return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2ExWithCate(true)) + } else { + throw new Error('this ntqq version not support, must be 26702 or later') + } } } \ No newline at end of file diff --git a/src/onebot11/action/user/SetFriendAddRequest.ts b/src/onebot11/action/user/SetFriendAddRequest.ts index 2905fd7..0813288 100644 --- a/src/onebot11/action/user/SetFriendAddRequest.ts +++ b/src/onebot11/action/user/SetFriendAddRequest.ts @@ -4,7 +4,7 @@ import { NTQQFriendApi } from '../../../ntqqapi/api/friend' interface Payload { flag: string - approve: boolean + approve?: boolean | string remark?: string } @@ -12,7 +12,7 @@ export default class SetFriendAddRequest extends BaseAction { actionName = ActionName.SetFriendAddRequest protected async _handle(payload: Payload): Promise { - const approve = payload.approve.toString() === 'true' + const approve = payload.approve?.toString() !== 'false' await NTQQFriendApi.handleFriendRequest(payload.flag, approve) return null } diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index 671d97a..9566fec 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -24,9 +24,10 @@ import { TipGroupElementType, User, VideoElement, - FriendV2 + FriendV2, + ChatType2 } from '../ntqqapi/types' -import { deleteGroup, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data' +import { deleteGroup, getGroupMember, getSelfUin } from '../common/data' import { EventType } from './event/OB11BaseEvent' import { encodeCQCode } from './cqcode' import { dbUtil } from '../common/db' @@ -59,15 +60,15 @@ export class OB11Constructor { debug, ob11: { messagePostFormat }, } = config - const message_type = msg.chatType == ChatType.group ? 'group' : 'private' + const selfUin = getSelfUin() const resMsg: OB11Message = { - self_id: parseInt(selfInfo.uin), + self_id: parseInt(selfUin), user_id: parseInt(msg.senderUin!), time: parseInt(msg.msgTime) || Date.now(), message_id: msg.msgShortId!, real_id: msg.msgShortId!, message_seq: msg.msgShortId!, - message_type: msg.chatType == ChatType.group ? 'group' : 'private', + message_type: msg.chatType === ChatType.group ? 'group' : 'private', sender: { user_id: parseInt(msg.senderUin!), nickname: msg.sendNickName, @@ -78,7 +79,7 @@ export class OB11Constructor { sub_type: 'friend', message: messagePostFormat === 'string' ? '' : [], message_format: messagePostFormat === 'string' ? 'string' : 'array', - post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE, + post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE, } if (debug) { resMsg.raw = msg @@ -96,11 +97,15 @@ export class OB11Constructor { resMsg.sub_type = 'friend' resMsg.sender.nickname = (await NTQQUserApi.getUserDetailInfo(msg.senderUid)).nick } - else if (msg.chatType == ChatType.temp) { + else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) { resMsg.sub_type = 'group' - const tempGroupCode = tempGroupCodeMap[msg.peerUin] - if (tempGroupCode) { - resMsg.group_id = parseInt(tempGroupCode) + const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid) + if (ret.result === 0) { + resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode) + resMsg.sender.nickname = ret.tmpChatInfo!.fromNick + } else { + resMsg.group_id = 284840486 //兜底数据 + resMsg.sender.nickname = '临时会话' } } @@ -175,7 +180,7 @@ export class OB11Constructor { // message_data["data"]["path"] = element.picElement.sourcePath // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" - message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement, msg.chatType) + 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 @@ -328,7 +333,11 @@ export class OB11Constructor { //筛选item带有uid的元素 const poke_uid = pokedetail.filter(item => item.uid) if (poke_uid.length == 2) { - return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) + return new OB11FriendPokeEvent( + parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)), + parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)), + pokedetail + ) } } //下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE @@ -417,6 +426,7 @@ export class OB11Constructor { 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 || @@ -424,13 +434,13 @@ export class OB11Constructor { if (adminUin) { return new OB11GroupDecreaseEvent( parseInt(msg.peerUid), - parseInt(selfInfo.uin), + parseInt(selfUin), parseInt(adminUin), 'kick_me', ) } } catch (e) { - return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave') + return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfUin), 0, 'leave') } } } @@ -533,7 +543,12 @@ export class OB11Constructor { //筛选item带有uid的元素 const poke_uid = pokedetail.filter(item => item.uid) if (poke_uid.length == 2) { - return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) + return new OB11GroupPokeEvent( + parseInt(msg.peerUid), + parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)), + parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)), + pokedetail + ) } } if (grayTipElement.jsonGrayTipElement.busiId == 2401) { @@ -552,7 +567,7 @@ export class OB11Constructor { const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg // 如果 senderUin 为 0,可能是 历史消息 或 自身消息 if (msgList[0].senderUin === '0') { - msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin + msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin() } return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin!)) // 获取MsgSeq+Peer可获取具体消息 diff --git a/src/onebot11/event/OB11BaseEvent.ts b/src/onebot11/event/OB11BaseEvent.ts index bcdd45b..88d3338 100644 --- a/src/onebot11/event/OB11BaseEvent.ts +++ b/src/onebot11/event/OB11BaseEvent.ts @@ -1,4 +1,4 @@ -import { selfInfo } from '../../common/data' +import { getSelfUin } from '../../common/data' export enum EventType { META = 'meta_event', @@ -10,6 +10,6 @@ export enum EventType { export abstract class OB11BaseEvent { time = Math.floor(Date.now() / 1000) - self_id = parseInt(selfInfo.uin) + self_id = parseInt(getSelfUin()) abstract post_type: EventType } diff --git a/src/onebot11/server/http.ts b/src/onebot11/server/http.ts index 79f3a2e..5c1db8d 100644 --- a/src/onebot11/server/http.ts +++ b/src/onebot11/server/http.ts @@ -1,11 +1,11 @@ import { Response } from 'express' import { OB11Response } from '../action/OB11Response' import { HttpServerBase } from '@/common/server/http' -import { actionHandlers, actionMap } from '../action' +import { actionMap } from '../action' import { getConfigUtil } from '@/common/config' import { postOb11Event } from './post-ob11-event' import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' -import { selfInfo } from '@/common/data' +import { getSelfInfo } from '@/common/data' class OB11HTTPServer extends HttpServerBase { name = 'LLOneBot server' @@ -40,7 +40,7 @@ class HTTPHeart { } this.intervalId = setInterval(() => { // ws的心跳是ws自己维护的 - postOb11Event(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!), false, false) + postOb11Event(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!), false, false) }, heartInterval) } diff --git a/src/onebot11/server/post-ob11-event.ts b/src/onebot11/server/post-ob11-event.ts index f6d2cb4..f979dd4 100644 --- a/src/onebot11/server/post-ob11-event.ts +++ b/src/onebot11/server/post-ob11-event.ts @@ -1,5 +1,5 @@ import { OB11Message } from '../types' -import { selfInfo } from '@/common/data' +import { getSelfUin } from '@/common/data' import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent' import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent' import { WebSocket as WebSocketClass } from 'ws' @@ -35,9 +35,10 @@ export function postWsEvent(event: PostEventType) { export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = true) { const config = getConfigUtil().getConfig() + const selfUin = getSelfUin() // 判断msg是否是event if (!config.reportSelfMessage && !reportSelf) { - if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfInfo.uin) { + if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfUin) { return } } @@ -48,7 +49,7 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t const sig = hmac.digest('hex') let headers = { 'Content-Type': 'application/json', - 'x-self-id': selfInfo.uin, + 'x-self-id': selfUin, } if (config.ob11.httpSecret) { headers['x-signature'] = 'sha1=' + sig diff --git a/src/onebot11/server/ws/ReverseWebsocket.ts b/src/onebot11/server/ws/ReverseWebsocket.ts index 47ab083..c7c0462 100644 --- a/src/onebot11/server/ws/ReverseWebsocket.ts +++ b/src/onebot11/server/ws/ReverseWebsocket.ts @@ -1,4 +1,4 @@ -import { selfInfo } from '../../../common/data' +import { getSelfInfo } from '../../../common/data' import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' import { ActionName } from '../../action/types' import { OB11Response } from '../../action/OB11Response' @@ -78,6 +78,7 @@ export class ReverseWebsocket { private connect() { const { token, heartInterval } = getConfigUtil().getConfig() + const selfInfo = getSelfInfo() this.websocket = new WebSocketClass(this.url, { maxPayload: 1024 * 1024 * 1024, handshakeTimeout: 2000, diff --git a/src/onebot11/server/ws/WebsocketServer.ts b/src/onebot11/server/ws/WebsocketServer.ts index eeb8b9f..4cff47e 100644 --- a/src/onebot11/server/ws/WebsocketServer.ts +++ b/src/onebot11/server/ws/WebsocketServer.ts @@ -9,7 +9,7 @@ import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent' import { WebsocketServerBase } from '../../../common/server/websocket' import { IncomingMessage } from 'node:http' import { wsReply } from './reply' -import { selfInfo } from '../../../common/data' +import { getSelfInfo } from '../../../common/data' import { log } from '../../../common/utils/log' import { getConfigUtil } from '../../../common/config' @@ -59,7 +59,7 @@ class OB11WebsocketServer extends WebsocketServerBase { } const { heartInterval } = getConfigUtil().getConfig() const wsClientInterval = setInterval(() => { - postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!)) + postWsEvent(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!)) }, heartInterval) // 心跳包 wsClient.on('close', () => { log('event上报ws客户端已断开') diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 9cf9b97..70d9083 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -411,10 +411,7 @@ async function onSettingWindowCreated(view: Element) { buttonDom.addEventListener('click', async () => { window.llonebot.checkVersion().then(checkVersionFunc) }) - - return - } - if (!ResultVersion.result) { + } else if (!ResultVersion.result) { titleDom.innerHTML = '当前已是最新版本 v' + version buttonDom.innerHTML = '无需更新' } else { diff --git a/src/version.ts b/src/version.ts index 235a755..b13a226 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '3.28.2' +export const version = '3.28.6'