Compare commits

...

10 Commits

Author SHA1 Message Date
idranme
41c04faa05 Merge pull request #330 from LLOneBot/dev
3.28.5
2024-08-11 20:01:29 +08:00
idranme
6ad4492f01 chore: v3.28.5 2024-08-11 20:00:47 +08:00
idranme
d52f16bc88 opt 2024-08-11 19:42:44 +08:00
idranme
2b0179acd1 opt 2024-08-11 18:10:27 +08:00
idranme
f540f324a1 Merge pull request #329 from LLOneBot/dev
3.28.4
2024-08-11 12:21:37 +08:00
idranme
128f40a51d chore: v3.28.4 2024-08-11 12:17:47 +08:00
idranme
c815e0ca6b sync 2024-08-11 12:16:53 +08:00
idranme
1da720e0a7 sync 2024-08-11 02:43:14 +08:00
idranme
1472c9c949 opt 2024-08-11 00:23:17 +08:00
idranme
4678253815 sync 2024-08-11 00:18:54 +08:00
29 changed files with 420 additions and 627 deletions

View File

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

View File

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

View File

@@ -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)
}

View File

@@ -8,14 +8,8 @@ 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 { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export let groups: Group[] = []
export let friends: Friend[] = []
export const llonebotError: LLOneBotError = {
@@ -98,4 +92,39 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s
member = getMember()
}
return member
}
const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export async function getSelfNick(force = false): Promise<string> {
if (!selfInfo.nick || force) {
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
selfInfo.nick = userInfo.nick
return userInfo.nick
}
}
return selfInfo.nick
}
export function getSelfInfo() {
return selfInfo
}
export function setSelfInfo(data: Partial<SelfInfo>) {
Object.assign(selfInfo, data)
}
export function getSelfUid() {
return selfInfo['uid']
}
export function getSelfUin() {
return selfInfo['uin']
}

View File

@@ -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 = {}
// 清理时间较久的缓存

View File

@@ -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<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)
// 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
}

View File

@@ -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)
}
}

View File

@@ -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'
export { encodeSilk } from './audio'

View File

@@ -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, () => {})
}

View File

@@ -17,7 +17,10 @@ import { DATA_DIR } from '../common/utils'
import {
getGroupMember,
llonebotError,
selfInfo,
setSelfInfo,
getSelfInfo,
getSelfUid,
getSelfUin
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
@@ -36,7 +39,7 @@ import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequ
import path from 'node:path'
import { dbUtil } from '../common/db'
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 { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config'
@@ -50,7 +53,6 @@ let mainWindow: BrowserWindow | null = null
// 加载插件时触发
function onLoad() {
log('llonebot main onLoad')
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkNewVersion()
})
@@ -160,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
}
@@ -208,10 +210,6 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([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)) {
@@ -282,39 +280,6 @@ function onLoad() {
log('收到群通知', notify)
await dbUtil.addGroupNotify(notify)
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
// 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
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify)
try {
@@ -423,7 +388,7 @@ function onLoad() {
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) {
@@ -433,6 +398,8 @@ function onLoad() {
llonebotError.otherError = ''
startTime = Date.now()
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)
@@ -454,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

View File

@@ -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<string> {
@@ -30,19 +36,7 @@ export class NTQQFileApi {
}
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<string>({
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<string>({
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<unknown>,
(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 ''
}
}

View File

@@ -1,93 +1,35 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { ChatType, RawMessage, SendMessageElement, Peer, ChatType2 } 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<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 {
static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession()
return session?.getMsgService().getTempChatInfo(chatType, peerUid)!
}
static enterOrExitAIO(peer: Peer, enter: boolean) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO,
args: [
{
"info_list": [
{
peer,
"option": enter ? 1 : 2
}
]
},
{
"send": true
},
],
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
})
}
@@ -96,47 +38,13 @@ export class NTQQMsgApi {
// 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<GeneralCallResult>({
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<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTQQApiMethod.GET_MULTI_MSG,
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
}
static async getMsgBoxInfo(peer: Peer) {
return await callNTQQApi<GeneralCallResult>({
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) {
@@ -158,80 +66,80 @@ export class NTQQMsgApi {
})
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
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<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,
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))
@@ -295,74 +203,66 @@ export class NTQQMsgApi {
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const waiter = sendWaiter(destPeer, true, 10000)
callNTQQApi<GeneralCallResult>({
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<RawMessage> {
const msgInfos = msgIds.map(async id => {
return { msgId: id, senderShowName: await getSelfNick() }
})
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<any>, attr: Map<any, any>,) => Promise<unknown>,
(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<RawMessage>((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<GeneralCallResult>({
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) {

View File

@@ -1,8 +1,8 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types'
import { ReceiveCmdS } from '../hook'
import { selfInfo, friends, groupMembers } from '@/common/data'
import { CacheClassFuncAsync, 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,8 +10,6 @@ import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NTQQFriendApi } from './friend'
const userInfoCache: Record<string, User> = {} // uid: User
export class NTQQUserApi {
static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({
@@ -85,39 +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
<EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile: User) => {
if (profile.uid === uid) {
return true
}
return false
},
args: [
{
uid,
},
null,
],
})
const info = result.info
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='
@@ -134,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)
@@ -149,7 +134,7 @@ 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
@@ -158,7 +143,8 @@ export class NTQQUserApi {
@CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) {
const ClientKeyData = await NTQQUserApi.forceFetchClientKey()
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
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
}

View File

@@ -1,4 +1,4 @@
import { 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'
@@ -192,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<any>(url, 'GET', '', { 'Cookie': CookieValue });
return ret;
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue })
return ret
} catch (e) {
return undefined;
return undefined
}
return undefined;
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
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<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue });
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(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) {

View File

@@ -175,7 +175,6 @@ export class SendMsgElementConstructor {
setTimeout(useDefaultThumb, 5000)
ffmpeg(filePath)
.on('end', () => { })
.on('error', (err) => {
if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath)

View File

@@ -1,6 +1,6 @@
import type { BrowserWindow } from 'electron'
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 {
deleteGroup,
@@ -8,7 +8,8 @@ import {
getFriend,
getGroupMember,
groups,
selfInfo,
getSelfUin,
setSelfInfo
} from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
@@ -242,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)
@@ -293,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),
@@ -454,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[] = []

View File

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

View File

@@ -37,7 +37,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
let element = this.getElement(msg, cache.elementId)
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)
log('下载完成后的msg', msg)

View File

@@ -11,11 +11,7 @@ interface Payload {
user_id?: number | string
}
interface Response {
message_id: number
}
abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
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() }
}
protected async _handle(payload: Payload): Promise<Response> {
protected async _handle(payload: Payload): Promise<null> {
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,
@@ -41,8 +37,10 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
peer,
[msg.msgId],
)
const ob11MsgId = await dbUtil.addMsg(sentMsg)
return { message_id: ob11MsgId! }
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`)
}
return null
}
}

View File

@@ -7,7 +7,7 @@ import {
RawMessage,
SendMessageElement,
} from '../../../ntqqapi/types'
import { getGroup, getGroupMember, selfInfo } from '../../../common/data'
import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '../../../common/data'
import {
OB11MessageCustomMusic,
OB11MessageData,
@@ -101,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) {
}
@@ -457,7 +457,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const nodeMsg = await NTQQMsgApi.sendMsg(
{
chatType: ChatType.friend,
peerUid: selfInfo.uid,
peerUid: getSelfUid(),
},
sendElements,
true,
@@ -473,7 +473,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid,
peerUid: getSelfUid(),
}
let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用
@@ -489,7 +489,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
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)
@@ -562,7 +562,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
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
@@ -583,13 +583,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
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
}
}

View File

@@ -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<null, OB11User> {
actionName = ActionName.GetLoginInfo
protected async _handle(payload: null) {
return OB11Constructor.selfInfo(selfInfo)
return OB11Constructor.selfInfo({
...getSelfInfo(),
nick: await getSelfNick(true)
})
}
}

View File

@@ -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<any, OB11Status> {
actionName = ActionName.GetStatus
protected async _handle(payload: any): Promise<OB11Status> {
return {
online: selfInfo.online!,
online: getSelfInfo().online!,
good: true,
}
}

View File

@@ -27,7 +27,7 @@ import {
FriendV2,
ChatType2
} from '../ntqqapi/types'
import { deleteGroup, getGroupMember, selfInfo } from '../common/data'
import { deleteGroup, getGroupMember, getSelfUin } from '../common/data'
import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db'
@@ -60,8 +60,9 @@ export class OB11Constructor {
debug,
ob11: { messagePostFormat },
} = config
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!,
@@ -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
@@ -179,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
@@ -425,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 ||
@@ -432,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')
}
}
}
@@ -565,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可获取具体消息

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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客户端已断开')

View File

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