Merge pull request #329 from LLOneBot/dev

3.28.4
This commit is contained in:
idranme 2024-08-11 12:21:37 +08:00 committed by GitHub
commit f540f324a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 277 additions and 435 deletions

View File

@ -4,7 +4,7 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用以 QQ 机器人开发", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发",
"version": "3.28.3", "version": "3.28.4",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
@ -13,7 +13,7 @@
} }
], ],
"repository": { "repository": {
"repo": "linyuchen/LiteLoaderQQNT-OneBotApi", "repo": "LLOneBot/LLOneBot",
"branch": "main", "branch": "main",
"release": { "release": {
"tag": "latest", "tag": "latest",

View File

@ -16,7 +16,7 @@ const manifest = {
} }
], ],
repository: { repository: {
repo: 'linyuchen/LiteLoaderQQNT-OneBotApi', repo: 'LLOneBot/LLOneBot',
branch: 'main', branch: 'main',
release: { release: {
tag: 'latest', tag: 'latest',

View File

@ -1,5 +1,4 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs'
import os from 'node:os' import os from 'node:os'
import { systemPlatform } from './system' import { systemPlatform } from './system'
@ -38,32 +37,6 @@ type QQPkgInfo = {
platform: string platform: string
eleArch: string eleArch: string
} }
type QQVersionConfigInfo = {
baseVersion: string
curVersion: string
prevVersion: string
onErrorVersions: Array<any>
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) export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3, // platform_type: 3,
@ -74,14 +47,6 @@ export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platVer: '10.0.26100', // platVer: '10.0.26100',
// clientVer: '9.9.9-23159', // 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 { export function getBuildVersion(): number {
return +qqPkgInfo.buildVersion return +qqPkgInfo.buildVersion
} }

View File

@ -16,4 +16,3 @@ if (!fs.existsSync(TEMP_DIR)) {
export { getVideoInfo } from './video' export { getVideoInfo } from './video'
export { checkFfmpeg } from './video' export { checkFfmpeg } from './video'
export { encodeSilk } from './audio' export { encodeSilk } from './audio'
export { isQQ998 } from './QQBasicInfo'

View File

@ -36,7 +36,7 @@ import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequ
import path from 'node:path' import path from 'node:path'
import { dbUtil } from '../common/db' import { dbUtil } from '../common/db'
import { setConfig } from './setConfig' import { setConfig } from './setConfig'
import { NTQQUserApi, NTQQGroupApi, sentMessages } from '../ntqqapi/api' import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
@ -208,10 +208,6 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报 const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
const sentMessage = sentMessages[message.msgId]
if (sentMessage) {
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message) log('message update', message.msgId, message)
if (message.recallTime != '0') { if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) { if (recallMsgIds.includes(message.msgId)) {

View File

@ -9,15 +9,21 @@ import {
ChatType, ChatType,
ElementType, ElementType,
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement, IMAGE_HTTP_HOST_NT,
PicElement,
} from '../types' } from '../types'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils' import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey' import { rkeyManager } from '@/ntqqapi/api/rkey'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg' 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 { export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> { static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
@ -30,19 +36,7 @@ export class NTQQFileApi {
} }
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return fileTypeFromFile(filePath)
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_TYPE,
args: [filePath],
})
}
static async getFileMd5(filePath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath],
})
} }
static async copyFile(filePath: string, destPath: string) { static async copyFile(filePath: string, destPath: string) {
@ -67,44 +61,35 @@ export class NTQQFileApi {
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath) const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext let ext = (await NTQQFileApi.getFileType(filePath))?.ext || ''
if (ext) { if (ext) {
ext = '.' + ext ext = '.' + ext
} else {
ext = ''
} }
let fileName = `${path.basename(filePath)}` let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) { if (fileName.indexOf('.') === -1) {
fileName += ext fileName += ext
} }
const mediaPath = await callNTQQApi<string>({ const session = getSession()
methodName: NTQQApiMethod.MEDIA_FILE_PATH, const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
args: [ md5HexStr: fileMd5,
{
path_info: {
md5HexStr: md5,
fileName: fileName, fileName: fileName,
elementType: elementType, elementType: elementType,
elementSubType, elementSubType,
thumbSize: 0, thumbSize: 0,
needCreate: true, needCreate: true,
downloadType: 1, downloadType: 1,
file_uuid: '', file_uuid: ''
},
},
],
}) })
log('media path', mediaPath) await fsPromise.copyFile(filePath, mediaPath!)
await NTQQFileApi.copyFile(filePath, mediaPath) const fileSize = (await fsPromise.stat(filePath)).size
const fileSize = await NTQQFileApi.getFileSize(filePath)
return { return {
md5, md5: fileMd5,
fileName, fileName,
path: mediaPath, path: mediaPath!,
fileSize, fileSize,
ext, ext
} }
} }
@ -115,19 +100,48 @@ export class NTQQFileApi {
elementId: string, elementId: string,
thumbPath: string, thumbPath: string,
sourcePath: string, sourcePath: string,
force: boolean = false, timeout = 1000 * 60 * 2,
force = false
) { ) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
if (force) { if (force) {
fs.unlinkSync(sourcePath) try {
await fsPromise.unlink(sourcePath)
} catch (e) {
//
}
} else { } else {
return sourcePath 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<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{ {
getReq: {
fileModelId: '0', fileModelId: '0',
downloadSourceType: 0, downloadSourceType: 0,
triggerType: 1, triggerType: 1,
@ -137,22 +151,16 @@ export class NTQQFileApi {
elementId: elementId, elementId: elementId,
thumbSize: 0, thumbSize: 0,
downloadType: 1, downloadType: 1,
filePath: thumbPath, filePath: thumbPath
}, }
}, )
null, let filePath = data[1].filePath
] if (filePath.startsWith('\\')) {
// log("需要下载media", sourcePath); const downloadPath = TEMP_DIR
await callNTQQApi({ filePath = path.join(downloadPath, filePath)
methodName: NTQQApiMethod.DOWNLOAD_MEDIA, // 下载路径是下载文件夹的相对路径
args: apiParams, }
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, return filePath
cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => {
log('media 下载完成判断', payload.notifyInfo.msgId, msgId)
return payload.notifyInfo.msgId == msgId
},
})
return sourcePath
} }
static async getImageSize(filePath: string) { static async getImageSize(filePath: string) {
@ -163,22 +171,27 @@ export class NTQQFileApi {
}) })
} }
static async getImageUrl(picElement: PicElement, chatType: ChatType) { static async getImageUrl(element: PicElement) {
const isPrivateImage = chatType !== ChatType.group if (!element) {
const url = picElement.originImageUrl // 没有域名 return ''
const md5HexStr = picElement.md5HexStr }
const fileMd5 = picElement.md5HexStr const url: string = element.originImageUrl! // 没有域名
const fileUuid = picElement.fileUuid const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr
const fileUuid = element.fileUuid
if (url) { if (url) {
if (url.startsWith('/download')) { const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
// console.log('rkey', rkey); const imageAppid = UrlParse.searchParams.get('appid')
if (url.includes('&rkey=')) { const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey')
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
} }
const rkeyData = await rkeyManager.getRkey()
const rkeyData = await rkeyManager.getRkey(); UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey; return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
} else { } else {
// 老的图片url不需要rkey // 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url return IMAGE_HTTP_HOST + url
@ -187,7 +200,7 @@ export class NTQQFileApi {
// 没有url需要自己拼接 // 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
} }
log('图片url获取失败', picElement) log('图片url获取失败', element)
return '' return ''
} }
} }

View File

@ -1,93 +1,35 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { ChatType, RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { dbUtil } from '../../common/db'
import { selfInfo } from '../../common/data' import { selfInfo } from '../../common/data'
import { ReceiveCmdS, registerReceiveHook } from '../hook' import { getBuildVersion } from '../../common/utils'
import { log } from '../../common/utils/log'
import { sleep } from '../../common/utils/helper'
import { isQQ998, getBuildVersion } from '../../common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc
export let sentMessages: Record<string, RawMessage> = {} // 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<RawMessage> => {
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 { export class NTQQMsgApi {
static async getTempChatInfo(chatType: ChatType2, peerUid: string) { static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession() const session = getSession()
return session?.getMsgService().getTempChatInfo(chatType, peerUid)! return session?.getMsgService().getTempChatInfo(chatType, peerUid)!
} }
static enterOrExitAIO(peer: Peer, enter: boolean) { static async prepareTempChat(toUserUid: string, GroupCode: string, nickname: string) {
return callNTQQApi<GeneralCallResult>({ //By Jadx/Ida Mlikiowa
methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO, let TempGameSession = {
args: [ nickname: '',
{ gameAppId: '',
"info_list": [ selfTinyId: '',
{ peerRoleId: '',
peer, peerOpenId: '',
"option": enter ? 1 : 2
} }
] const session = getSession()
}, return session?.getMsgService().prepareTempChat({
{ chatType: ChatType2.KCHATTYPETEMPC2CFROMGROUP,
"send": true peerUid: toUserUid,
}, peerNickname: nickname,
], fromGroupCode: GroupCode,
sig: '',
selfPhone: '',
selfUid: selfInfo.uid,
gameSession: TempGameSession
}) })
} }
@ -96,47 +38,13 @@ export class NTQQMsgApi {
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // 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 // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString() emojiId = emojiId.toString()
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.EMOJI_LIKE, return session?.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
args: [
{
peer,
msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
} }
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({ const session = getSession()
methodName: NTQQApiMethod.GET_MULTI_MSG, return session?.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)!
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
}
static async getMsgBoxInfo(peer: Peer) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GET_MSG_BOX_INFO,
args: [
{
contacts: [
peer
],
},
null,
],
})
} }
static async activateChat(peer: Peer) { static async activateChat(peer: Peer) {
@ -158,80 +66,80 @@ export class NTQQMsgApi {
}) })
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number) { static async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
// 消息时间从旧到新 if (!peer) throw new Error('peer is not allowed')
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({ if (!msgIds) throw new Error('msgIds is not allowed')
methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG, const session = getSession()
args: [ //Mlikiowa 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得
{ return await session?.getMsgService().getMsgsByMsgId(peer, msgIds)!
peer,
msgId,
cnt: count,
queryOrder: true,
},
null,
],
})
} }
static async fetchRecentContact() { static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
await callNTQQApi({ const session = getSession()
methodName: NTQQApiMethod.RECENT_CONTACT, // 消息时间从旧到新
args: [ return session?.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)!
{
fetchParam: {
anchorPointContact: {
contactId: '',
sortField: '',
pos: 0,
},
relativeMoveCount: 0,
listType: 2, // 1普通消息2群助手内的消息
count: 200,
fetchOld: true,
},
},
],
})
} }
static async recallMsg(peer: Peer, msgIds: string[]) { static async recallMsg(peer: Peer, msgIds: string[]) {
return await callNTQQApi({ const session = getSession()
methodName: NTQQApiMethod.RECALL_MSG, return await session?.getMsgService().recallMsg({
args: [ chatType: peer.chatType,
{ peerUid: peer.peerUid
peer, }, msgIds)
msgIds,
},
null,
],
})
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
if (getBuildVersion() >= 26702) { function generateMsgId() {
return NTQQMsgApi.sendMsgV2(peer, msgElements, waitComplete, timeout) 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) // 此处有采用Hack方法 利用数据返回正确得到对应消息
callNTQQApi({ // 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同
methodName: NTQQApiMethod.SEND_MSG, // 谨慎采用 目前测试暂无问题 Developer.Mlikiowa
args: [ let msgId: string
{ try {
msgId: '0', 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<any, any>) => Promise<unknown>,
(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, peer,
msgElements, msgElements,
msgAttributeInfos: new Map(), new Map()
}, )
null, const retMsg = data[1].find(msgRecord => {
], if (msgRecord.guildId === msgId) {
}).then() return true
return await waiter }
})
return retMsg!
} }
static async sendMsgV2(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { static async sendMsgV2(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
if (peer.chatType === ChatType.temp) {
//await NTQQMsgApi.PrepareTempChat().then().catch()
}
function generateMsgId() { function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000) const timestamp = Math.floor(Date.now() / 1000)
const random = Math.floor(Math.random() * Math.pow(2, 32)) const random = Math.floor(Math.random() * Math.pow(2, 32))
@ -295,74 +203,65 @@ export class NTQQMsgApi {
} }
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const waiter = sendWaiter(destPeer, true, 10000) const session = getSession()
callNTQQApi<GeneralCallResult>({ return session?.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])!
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
}).then().catch(log)
return await waiter
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const msgInfos = msgIds.map((id) => { const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName: selfInfo.nick } return { msgId: id, senderShowName: selfInfo.nick }
}) })
const apiArgs = [ let data = await NTEventDispatch.CallNormalEvent<
{ (msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
msgInfos, (msgList: RawMessage[]) => void
srcContact: srcPeer, >(
dstContact: destPeer, 'NodeIKernelMsgService/multiForwardMsgWithComment',
commentElements: [], 'NodeIKernelMsgListener/onMsgInfoListUpdate',
msgAttributeInfos: new Map(), 1,
}, 5000,
null, (msgRecords: RawMessage[]) => {
] for (let msgRecord of msgRecords) {
return await new Promise<RawMessage>((resolve, reject) => { if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfInfo.uid) {
let complete = false return true
setTimeout(() => {
if (!complete) {
reject('转发消息超时')
} }
}, 5000) }
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { return false
const msg = payload.msgRecord },
// 需要判断它是转发的消息,并且识别到是当前转发的这一条 msgInfos,
const arkElement = msg.elements.find((ele) => ele.arkElement) srcPeer,
destPeer,
[],
new Map()
)
for (let msg of data[1]) {
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) { if (!arkElement) {
// log("收到的不是转发消息") continue
return
} }
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') { if (forwardData.app != 'com.tencent.multimsg') {
return continue
} }
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true return msg
await dbUtil.addMsg(msg)
resolve(msg)
log('转发消息成功:', payload)
} }
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs,
}).then((result) => {
log('转发消息结果:', result, apiArgs)
if (result.result !== 0) {
complete = true
reject('转发消息失败,' + JSON.stringify(result))
} }
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) { static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {

View File

@ -2,7 +2,7 @@ import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../
import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types' import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { selfInfo, friends, groupMembers } from '@/common/data' import { selfInfo, friends, groupMembers } from '@/common/data'
import { CacheClassFuncAsync, isQQ998, log, sleep, getBuildVersion } from '@/common/utils' import { CacheClassFuncAsync, log, getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
@ -85,39 +85,25 @@ export class NTQQUserApi {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid) return this.fetchUserDetailInfo(uid)
} }
// this.getUserInfo(uid) type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']
if (!withBizInfo) { const [_retData, profile] = await NTEventDispatch.CallNormalEvent
methodName = NTQQApiMethod.USER_DETAIL_INFO <EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile: User) => {
if (profile.uid === uid) {
return true
} }
const fetchInfo = async () => { return false
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
}, },
args: [
{
uid, uid,
}, [0]
null, )
], return profile
})
const info = result.info
return info
}
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000)
}
const userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
} }
// return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey=' // return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey='

View File

@ -1,6 +1,6 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTQQMsgApi, sendMessagePool } from './api/msg' import { NTQQMsgApi } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types' import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types'
import { import {
deleteGroup, deleteGroup,
@ -454,18 +454,7 @@ export async function startHook() {
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord const message = msgRecord
const peerUid = message.peerUid
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then() 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) => { registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {

View File

@ -462,6 +462,7 @@ export interface RawMessage {
senderUin?: string // 发送者QQ号 senderUin?: string // 发送者QQ号
peerUid: string // 群号 或者 QQ uid peerUid: string // 群号 或者 QQ uid
peerUin: string // 群号 或者 发送者QQ号 peerUin: string // 群号 或者 发送者QQ号
guildId: string
sendNickName: string sendNickName: string
sendMemberName?: string // 发送者群名片 sendMemberName?: string // 发送者群名片
chatType: ChatType chatType: ChatType

View File

@ -37,7 +37,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
let element = this.getElement(msg, cache.elementId) let element = this.getElement(msg, cache.elementId)
log('找到了文件 element', element) log('找到了文件 element', element)
// 构建下载函数 // 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true) await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '')
// 等待文件下载完成 // 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId) msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg) log('下载完成后的msg', msg)

View File

@ -11,11 +11,7 @@ interface Payload {
user_id?: number | string user_id?: number | string
} }
interface Response { abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
message_id: number
}
abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
protected async getTargetPeer(payload: Payload): Promise<Peer> { protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) { if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
@ -27,13 +23,13 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
return { chatType: ChatType.group, peerUid: payload.group_id!.toString() } return { chatType: ChatType.group, peerUid: payload.group_id!.toString() }
} }
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<null> {
const msg = await dbUtil.getMsgByShortId(payload.message_id) const msg = await dbUtil.getMsgByShortId(payload.message_id)
if (!msg) { if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`) throw new Error(`无法找到消息${payload.message_id}`)
} }
const peer = await this.getTargetPeer(payload) const peer = await this.getTargetPeer(payload)
const sentMsg = await NTQQMsgApi.forwardMsg( const ret = await NTQQMsgApi.forwardMsg(
{ {
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,
@ -41,8 +37,10 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
peer, peer,
[msg.msgId], [msg.msgId],
) )
const ob11MsgId = await dbUtil.addMsg(sentMsg) if (ret.result !== 0) {
return { message_id: ob11MsgId! } throw new Error(`转发消息失败 ${ret.errMsg}`)
}
return null
} }
} }

View File

@ -583,13 +583,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeMsgIds.length === 0) { if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空') throw Error('转发消息失败,节点为空')
} }
try { const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
log('开发转发', nodeMsgIds) returnMsg.msgShortId = await dbUtil.addMsg(returnMsg)
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) return returnMsg
} catch (e) {
log('forward failed', e)
return null
}
} }
} }

View File

@ -179,7 +179,7 @@ export class OB11Constructor {
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" // 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_id"] = element.picElement.fileUuid
message_data['data']['file_size'] = element.picElement.fileSize message_data['data']['file_size'] = element.picElement.fileSize
dbUtil dbUtil

View File

@ -1 +1 @@
export const version = '3.28.3' export const version = '3.28.4'