diff --git a/README.md b/README.md index 309364fd..9a54e4ed 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,13 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现 ## 回家旅途 [QQ Group](https://qm.qq.com/q/I6LU87a0Yq) +## 性能设计/协议标准 +NapCat 已实现90%+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。 + +由此设计带来一系列好处,在开发中,获取群员列表通常小于50Ms,单条文本消息发送在320Ms以内,在1k+的群聊流畅运行,同时带来一些副作用,上报数据中大量使用Magic生成字段,消息Id无法持久,无法上报撤回消息原始内容。 + +NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。 + ## 感谢他们 感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 diff --git a/external/LiteLoaderWrapper.zip b/external/LiteLoaderWrapper.zip index bae1ad15..ef57db0f 100644 Binary files a/external/LiteLoaderWrapper.zip and b/external/LiteLoaderWrapper.zip differ diff --git a/launcher/NapCatWinBootMain.exe b/launcher/NapCatWinBootMain.exe index 813f3427..9501691c 100644 Binary files a/launcher/NapCatWinBootMain.exe and b/launcher/NapCatWinBootMain.exe differ diff --git a/manifest.json b/manifest.json index 9b04b238..6453a0aa 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "NapCatQQ", "slug": "NapCat.Framework", "description": "高性能的 OneBot 11 协议实现", - "version": "4.2.9", + "version": "4.2.24", "icon": "./logo.png", "authors": [ { diff --git a/package.json b/package.json index 369262d5..3a2cccdb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "napcat", "private": true, "type": "module", - "version": "4.2.9", + "version": "4.2.24", "scripts": { "build:universal": "npm run build:webui && vite build --mode universal || exit 1", "build:framework": "npm run build:webui && vite build --mode framework || exit 1", diff --git a/src/common/file.ts b/src/common/file.ts index cab8abb0..6dac529b 100644 --- a/src/common/file.ts +++ b/src/common/file.ts @@ -1,9 +1,7 @@ import fs from 'fs'; import { stat } from 'fs/promises'; import crypto, { randomUUID } from 'crypto'; -import util from 'util'; import path from 'node:path'; -import * as fileType from 'file-type'; import { solveProblem } from '@/common/helper'; export interface HttpDownloadOptions { @@ -15,7 +13,6 @@ type Uri2LocalRes = { success: boolean, errMsg: string, fileName: string, - ext: string, path: string } @@ -73,27 +70,6 @@ async function checkFile(path: string): Promise { // 如果文件存在,则无需做任何事情,Promise 解决(resolve)自身 } -export async function file2base64(path: string) { - const readFile = util.promisify(fs.readFile); - const result = { - err: '', - data: '', - }; - try { - try { - await checkFileExist(path, 5000); - } catch (e: any) { - result.err = e.toString(); - return result; - } - const data = await readFile(path); - result.data = data.toString('base64'); - } catch (err: any) { - result.err = err.toString(); - } - return result; -} - export function calculateFileMD5(filePath: string): Promise { return new Promise((resolve, reject) => { // 创建一个流式读取器 @@ -160,20 +136,6 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi return Buffer.from(buffer); } -export async function checkFileV2(filePath: string) { - try { - const ext: string | undefined = (await fileType.fileTypeFromFile(filePath))?.ext; - if (ext) { - fs.renameSync(filePath, filePath + `.${ext}`); - filePath += `.${ext}`; - return { success: true, ext: ext, path: filePath }; - } - } catch (e) { - // log("获取文件类型失败", filePath,e.stack) - } - return { success: false, ext: '', path: filePath }; -} - export enum FileUriType { Unknown = 0, Local = 1, @@ -213,63 +175,35 @@ export async function checkUriType(Uri: string) { return { Uri: Uri, Type: FileUriType.Unknown }; } -export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise { +export async function uriToLocalFile(dir: string, uri: string): Promise { const { Uri: HandledUri, Type: UriType } = await checkUriType(uri); - //解析失败 - const tempName = randomUUID(); - if (!filename) filename = randomUUID(); + const filename = randomUUID(); + const filePath = path.join(dir, filename); - //解析Http和Https协议 - if (UriType == FileUriType.Unknown) { - return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' }; - } - - //解析File协议和本地文件 - if (UriType == FileUriType.Local) { + switch (UriType) { + case FileUriType.Local: { const fileExt = path.extname(HandledUri); - let filename = path.basename(HandledUri, fileExt); - filename += fileExt; - //复制文件到临时文件并保持后缀 - const filenameTemp = tempName + fileExt; - const filePath = path.join(dir, filenameTemp); - fs.copyFileSync(HandledUri, filePath); - return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath }; + const localFileName = path.basename(HandledUri, fileExt) + fileExt; + const tempFilePath = path.join(dir, filename + fileExt); + fs.copyFileSync(HandledUri, tempFilePath); + return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath }; } - //接下来都要有文件名 - if (UriType == FileUriType.Remote) { - const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname)); - if (pathInfo.name) { - const pathlen = 200 - dir.length - pathInfo.name.length; - filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断 - if (pathInfo.ext) { - filename += pathInfo.ext; - } - } - filename = filename.replace(/[/\\:*?"<>|]/g, '_'); - const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10); - const filePath = path.join(dir, tempName + fileExt); + case FileUriType.Remote: { const buffer = await httpDownload(HandledUri); - //没有文件就创建 fs.writeFileSync(filePath, buffer, { flag: 'wx' }); - return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath }; + return { success: true, errMsg: '', fileName: filename, path: filePath }; } - //解析Base64 - if (UriType == FileUriType.Base64) { + case FileUriType.Base64: { const base64 = HandledUri.replace(/^base64:\/\//, ''); - const buffer = Buffer.from(base64, 'base64'); - let filePath = path.join(dir, filename); - let fileExt = ''; - fs.writeFileSync(filePath, buffer); - const { success, ext, path: fileTypePath } = await checkFileV2(filePath); - if (success) { - filePath = fileTypePath; - fileExt = ext; - filename = filename + '.' + ext; - } - return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath }; + const base64Buffer = Buffer.from(base64, 'base64'); + fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' }); + return { success: true, errMsg: '', fileName: filename, path: filePath }; } - return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' }; -} + + default: + return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' }; + } +} \ No newline at end of file diff --git a/src/common/log.ts b/src/common/log.ts index 7b34f46a..ea9beb09 100644 --- a/src/common/log.ts +++ b/src/common/log.ts @@ -1,7 +1,7 @@ import winston, { format, transports } from 'winston'; import { truncateString } from '@/common/helper'; import path from 'node:path'; -import fs from 'node:fs'; +import fs from 'node:fs/promises'; import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core'; import EventEmitter from 'node:events'; export enum LogLevel { @@ -97,26 +97,20 @@ export class LogWrapper { cleanOldLogs(logDir: string) { const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; - fs.readdir(logDir, (err, files) => { - if (err) { - this.logger.error('Failed to read log directory', err); - return; - } + fs.readdir(logDir).then((files) => { files.forEach((file) => { const filePath = path.join(logDir, file); this.deleteOldLogFile(filePath, oneWeekAgo); }); + }).catch((err) => { + this.logger.error('Failed to read log directory', err); }); } private deleteOldLogFile(filePath: string, oneWeekAgo: number) { - fs.stat(filePath, (err, stats) => { - if (err) { - this.logger.error('Failed to get file stats', err); - return; - } + fs.stat(filePath).then((stats) => { if (stats.mtime.getTime() < oneWeekAgo) { - fs.unlink(filePath, (err) => { + fs.unlink(filePath).catch((err) => { if (err) { if (err.code === 'ENOENT') { this.logger.warn(`File already deleted: ${filePath}`); @@ -128,6 +122,8 @@ export class LogWrapper { } }); } + }).catch((err) => { + this.logger.error('Failed to get file stats', err); }); } @@ -187,7 +183,7 @@ export class LogWrapper { // eslint-disable-next-line no-control-regex this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, '')); } - logSubscription.notify(message); + logSubscription.notify(JSON.stringify({ level, message })); } log(...args: any[]) { @@ -316,9 +312,8 @@ function textElementToText(textElement: any): string { function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string { const recordMsgOrNull = msg.records.find((record) => replyElement.sourceMsgIdInRecords === record.msgId); - return `[回复消息 ${ - recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020' - ? rawMessageToText(recordMsgOrNull, recursiveLevel + 1) - : `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})` + return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020' + ? rawMessageToText(recordMsgOrNull, recursiveLevel + 1) + : `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})` }]`; } diff --git a/src/common/request.ts b/src/common/request.ts index aecc5b1b..11523842 100644 --- a/src/common/request.ts +++ b/src/common/request.ts @@ -1,6 +1,5 @@ import https from 'node:https'; import http from 'node:http'; -import { readFileSync } from 'node:fs'; export class RequestUtil { // 适用于获取服务器下发cookies时获取,仅GET @@ -112,24 +111,4 @@ export class RequestUtil { static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) { return this.HttpGetJson(url, method, data, headers, false, false); } - - static async createFormData(boundary: string, filePath: string): Promise { - let type = 'image/png'; - if (filePath.endsWith('.jpg')) { - type = 'image/jpeg'; - } - const formDataParts = [ - `------${boundary}\r\n`, - `Content-Disposition: form-data; name="share_image"; filename="${filePath}"\r\n`, - 'Content-Type: ' + type + '\r\n\r\n', - ]; - - const fileContent = readFileSync(filePath); - const footer = `\r\n------${boundary}--`; - return Buffer.concat([ - Buffer.from(formDataParts.join(''), 'utf8'), - fileContent, - Buffer.from(footer, 'utf8'), - ]); - } } diff --git a/src/common/version.ts b/src/common/version.ts index 944ed73c..4fada46c 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const napCatVersion = '4.2.9'; +export const napCatVersion = '4.2.24'; diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 07860ca8..926ba64a 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -6,7 +6,6 @@ import { Peer, PicElement, PicSubType, - PicType, RawMessage, SendFileElement, SendPicElement, @@ -17,7 +16,7 @@ import path from 'path'; import fs from 'fs'; import fsPromises from 'fs/promises'; import { InstanceContext, NapCatCore, SearchResultItem } from '@/core'; -import * as fileType from 'file-type'; +import { fileTypeFromFile } from 'file-type'; import imageSize from 'image-size'; import { ISizeCalculationResult } from 'image-size/dist/types/interface'; import { RkeyManager } from '@/core/helper/rkey'; @@ -62,7 +61,7 @@ export class NTQQFileApi { async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { const fileMd5 = await calculateFileMD5(filePath); - const extOrEmpty = (await fileType.fileTypeFromFile(filePath))?.ext; + const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => ''); const ext = extOrEmpty ? `.${extOrEmpty}` : ''; let fileName = `${path.basename(filePath)}`; if (fileName.indexOf('.') === -1) { @@ -158,7 +157,7 @@ export class NTQQFileApi { let fileExt = 'mp4'; try { - const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext; + const tempExt = (await fileTypeFromFile(filePath))?.ext; if (tempExt) fileExt = tempExt; } catch (e) { this.context.logger.logError('获取文件类型失败', e); diff --git a/src/core/apis/friend.ts b/src/core/apis/friend.ts index eff2fcd1..7714d683 100644 --- a/src/core/apis/friend.ts +++ b/src/core/apis/friend.ts @@ -1,4 +1,4 @@ -import { FriendV2 } from '@/core/types'; +import { FriendRequest, FriendV2 } from '@/core/types'; import { BuddyListReqType, InstanceContext, NapCatCore } from '@/core'; import { LimitedHashTable } from '@/common/message-unique'; @@ -79,16 +79,10 @@ export class NTQQFriendApi { return ret; } - async handleFriendRequest(flag: string, accept: boolean) { - const data = flag.split('|'); - if (data.length < 2) { - return; - } - const friendUid = data[0]; - const reqTime = data[1]; + async handleFriendRequest(notify: FriendRequest, accept: boolean) { this.context.session.getBuddyService()?.approvalFriendRequest({ - friendUid: friendUid, - reqTime: reqTime, + friendUid: notify.friendUid, + reqTime: notify.reqTime, accept, }); } diff --git a/src/core/apis/group.ts b/src/core/apis/group.ts index 074155e9..60bf065a 100644 --- a/src/core/apis/group.ts +++ b/src/core/apis/group.ts @@ -1,6 +1,5 @@ import { GeneralCallResult, - Group, GroupMember, NTGroupMemberRole, NTGroupRequestOperateTypes, @@ -8,6 +7,7 @@ import { KickMemberV2Req, MemberExtSourceType, NapCatCore, + GroupNotify, } from '@/core'; import { isNumeric, solveAsyncProblem } from '@/common/helper'; import { LimitedHashTable } from '@/common/message-unique'; @@ -16,34 +16,22 @@ import { NTEventWrapper } from '@/common/event'; export class NTQQGroupApi { context: InstanceContext; core: NapCatCore; - groupCache: Map = new Map(); groupMemberCache: Map> = new Map>(); - groups: Group[] = []; essenceLRU = new LimitedHashTable(1000); - session: any; constructor(context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; } + async initApi() { this.initCache().then().catch(e => this.context.logger.logError(e)); } - async initCache() { - this.groups = await this.getGroups(); - for (const group of this.groups) { - this.groupCache.set(group.groupCode, group); - } - this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`); - // process.pid 调试点 - } - async getCoreAndBaseInfo(uids: string[]) { - return await this.core.eventWrapper.callNoListenerEvent( - 'NodeIKernelProfileService/getCoreAndBaseInfo', - 'nodeStore', - uids, - ); + async initCache() { + for (const group of await this.getGroups(true)) { + this.refreshGroupMemberCache(group.groupCode).then().catch(); + } } async fetchGroupEssenceList(groupCode: string) { @@ -54,20 +42,22 @@ export class NTQQGroupApi { pageLimit: 300, }, pskey); } + async getGroupShutUpMemberList(groupCode: string) { const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000); this.context.session.getGroupService().getGroupShutUpMemberList(groupCode); return (await data)[1]; } - async clearGroupNotifiesUnreadCount(uk: boolean) { - return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk); + + async clearGroupNotifiesUnreadCount(doubt: boolean) { + return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(doubt); } - async setGroupAvatar(gc: string, filePath: string) { - return this.context.session.getGroupService().setHeader(gc, filePath); + async setGroupAvatar(groupCode: string, filePath: string) { + return this.context.session.getGroupService().setHeader(groupCode, filePath); } - async getGroups(forced = false) { + async getGroups(forced: boolean = false) { const [, , groupList] = await this.core.eventWrapper.callNormalEventV2( 'NodeIKernelGroupService/getGroupList', 'NodeIKernelGroupListener/onGroupListUpdate', @@ -76,9 +66,9 @@ export class NTQQGroupApi { return groupList; } - async getGroupExtFE0Info(groupCode: string[], forced = true) { + async getGroupExtFE0Info(groupCodes: Array, forced = true) { return this.context.session.getGroupService().getGroupExt0xEF0Info( - groupCode, + groupCodes, [], { bindGuildId: 1, @@ -118,53 +108,42 @@ export class NTQQGroupApi { ); } - async getGroup(groupCode: string, forced = false) { - let group = this.groupCache.get(groupCode.toString()); - if (!group) { - try { - const groupList = await this.getGroups(forced); - if (groupList.length) { - groupList.forEach(g => { - this.groupCache.set(g.groupCode, g); - }); - } - } catch (e) { - return undefined; - } - } - group = this.groupCache.get(groupCode.toString()); - return group; - } - async getGroupMemberAll(groupCode: string, forced = false) { return this.context.session.getGroupService().getAllMemberList(groupCode, forced); } + async refreshGroupMemberCache(groupCode: string) { + try { + const members = await this.getGroupMemberAll(groupCode, true); + this.groupMemberCache.set(groupCode, members.result.infos); + } catch (e) { + this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`); + } + return this.groupMemberCache; + } + async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { const groupCodeStr = groupCode.toString(); const memberUinOrUidStr = memberUinOrUid.toString(); + + // 获取群成员缓存 let members = this.groupMemberCache.get(groupCodeStr); if (!members) { - try { - members = await this.getGroupMembers(groupCodeStr); - this.groupMemberCache.set(groupCodeStr, members); - } catch (e) { - return null; - } - } - function getMember() { - let member: GroupMember | undefined; - if (isNumeric(memberUinOrUidStr)) { - member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr); - } else { - member = members!.get(memberUinOrUidStr); - } - return member; + members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr); } + const getMember = () => { + if (isNumeric(memberUinOrUidStr)) { + return Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr); + } else { + return members!.get(memberUinOrUidStr); + } + }; + let member = getMember(); + // 如果缓存中不存在该成员,尝试刷新缓存 if (!member) { - members = await this.getGroupMembers(groupCodeStr); + members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr); member = getMember(); } return member; @@ -174,26 +153,26 @@ export class NTQQGroupApi { return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode); } - async CreatGroupFileFolder(groupCode: string, folderName: string) { + async creatGroupFileFolder(groupCode: string, folderName: string) { return this.context.session.getRichMediaService().createGroupFolder(groupCode, folderName); } - async DelGroupFile(groupCode: string, files: string[]) { + async delGroupFile(groupCode: string, files: Array) { return this.context.session.getRichMediaService().deleteGroupFile(groupCode, [102], files); } - async DelGroupFileFolder(groupCode: string, folderId: string) { + async delGroupFileFolder(groupCode: string, folderId: string) { return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId); } - async addGroupEssence(GroupCode: string, msgId: string) { + async addGroupEssence(groupCode: string, msgId: string) { const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', - peerUid: GroupCode, + peerUid: groupCode, }, msgId, 1, false); const param = { - groupCode: GroupCode, + groupCode: groupCode, msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgSeq: parseInt(MsgData.msgList[0].msgSeq), }; @@ -204,9 +183,9 @@ export class NTQQGroupApi { return this.context.session.getGroupService().kickMemberV2(param); } - async deleteGroupBulletin(GroupCode: string, noticeId: string) { + async deleteGroupBulletin(groupCode: string, noticeId: string) { const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; - return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, psKey, noticeId); + return this.context.session.getGroupService().deleteGroupBulletin(groupCode, psKey, noticeId); } async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) { @@ -217,65 +196,42 @@ export class NTQQGroupApi { return this.context.session.getGroupService().quitGroupV2(param); } - async removeGroupEssenceBySeq(GroupCode: string, msgRandom: string, msgSeq: string) { + async removeGroupEssenceBySeq(groupCode: string, msgRandom: string, msgSeq: string) { const param = { - groupCode: GroupCode, + groupCode: groupCode, msgRandom: parseInt(msgRandom), msgSeq: parseInt(msgSeq), }; return this.context.session.getGroupService().removeGroupEssence(param); } - async removeGroupEssence(GroupCode: string, msgId: string) { + async removeGroupEssence(groupCode: string, msgId: string) { const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', - peerUid: GroupCode, + peerUid: groupCode, }, msgId, 1, false); const param = { - groupCode: GroupCode, + groupCode: groupCode, msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgSeq: parseInt(MsgData.msgList[0].msgSeq), }; return this.context.session.getGroupService().removeGroupEssence(param); } - async getSingleScreenNotifies(doubt: boolean, num: number) { + async getSingleScreenNotifies(doubt: boolean, count: number) { const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2( 'NodeIKernelGroupService/getSingleScreenNotifies', 'NodeIKernelGroupListener/onGroupSingleScreenNotifies', [ doubt, '', - num, + count, ], ); return notifies; } - async getGroupMemberV2(GroupCode: string, uid: string, forced = false) { - const Listener = this.core.eventWrapper.registerListen( - 'NodeIKernelGroupListener/onMemberInfoChange', - (params, _, members) => params === GroupCode && members.size > 0, - 1, - forced ? 5000 : 250, - ); - const retData = await ( - this.core.eventWrapper - .createEventFunction('NodeIKernelGroupService/getMemberInfo') - )!(GroupCode, [uid], forced); - if (retData.result !== 0) { - throw new Error(`${retData.errMsg}`); - } - const result = await Listener as unknown; - let member: GroupMember | undefined; - if (Array.isArray(result) && result?.[2] instanceof Map) { - const members = result[2] as Map; - member = members.get(uid); - } - return member; - } - async searchGroup(groupCode: string) { const [, ret] = await this.core.eventWrapper.callNormalEventV2( 'NodeIKernelSearchService/searchGroup', @@ -294,178 +250,89 @@ export class NTQQGroupApi { return ret.groupInfos.find(g => g.groupCode === groupCode); } - async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) { + async getGroupMemberEx(groupCode: string, uid: string, forced: boolean = false, retry: number = 2) { const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => { return eventWrapper.callNormalEventV2( 'NodeIKernelGroupService/getMemberInfo', 'NodeIKernelGroupListener/onMemberInfoChange', - [GroupCode, [uid], forced], + [groupCode, [uid], forced], (ret) => ret.result === 0, (params, _, members) => params === GroupCode && members.size > 0 && members.has(uid), 1, forced ? 2500 : 250 ); - }, this.core.eventWrapper, GroupCode, uid, forced); + }, this.core.eventWrapper, groupCode, uid, forced); if (data && data[3] instanceof Map && data[3].has(uid)) { return data[3].get(uid); } if (retry > 0) { - const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined; + const trydata = await this.getGroupMemberEx(groupCode, uid, true, retry - 1) as GroupMember | undefined; if (trydata) return trydata; } return undefined; } - async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{ - infos: Map; - finish: boolean; - hasNext: boolean | undefined; - }> { - const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1'); - const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout) - .catch(() => { }); - const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num); - if (result.errCode !== 0) { - throw new Error('获取群成员列表出错,' + result.errMsg); - } - let resMode2; - if (modeListener) { - const ret = (await once)?.[0]; - if (ret) { - resMode2 = ret; - } - } - this.context.session.getGroupService().destroyMemberListScene(sceneId); - return { - infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]), - finish: result.result.finish, - hasNext: resMode2?.hasNext, - }; + async getGroupFileCount(groupCodes: Array) { + return this.context.session.getRichMediaService().batchGetGroupFileCount(groupCodes); } - async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{ - infos: Map; - finish: boolean; - hasNext: boolean | undefined; - listenerMode: boolean; - }> { - const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1'); - const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout) - .catch(() => { }); - const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num); - if (result.errCode !== 0) { - throw new Error('获取群成员列表出错,' + result.errMsg); - } - let resMode2; - if (result.result.finish && result.result.infos.size === 0) { - const ret = (await once)?.[0]; - if (ret) { - resMode2 = ret; - } - } - this.context.session.getGroupService().destroyMemberListScene(sceneId); - return { - infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]), - finish: result.result.finish, - hasNext: resMode2?.hasNext, - listenerMode: resMode2?.hasNext !== undefined - }; - } - - async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise> { - if (no_cache) { - return (await this.getGroupMemberAll(groupQQ, true)).result.infos; - } - let res = await this.GetGroupMembersV3(groupQQ, num); - let ret = res.infos; - if (res.infos.size === 0 && !res.listenerMode) { - res = await this.GetGroupMembersV3(groupQQ, num); - ret = res.infos; - } - if (res.infos.size === 0) { - ret = (await this.getGroupMemberAll(groupQQ)).result.infos; - } - return ret; - } - - async getGroupMembers(groupQQ: string, num = 3000): Promise> { - const groupService = this.context.session.getGroupService(); - const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); - const result = await groupService.getNextMemberList(sceneId, undefined, num); - if (result.errCode !== 0) { - throw new Error('获取群成员列表出错,' + result.errMsg); - } - this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`); - return result.result.infos; - } - - async getGroupFileCount(group_ids: Array) { - return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids); - } - - async getArkJsonGroupShare(GroupCode: string) { + async getArkJsonGroupShare(groupCode: string) { const ret = await this.core.eventWrapper.callNoListenerEvent( 'NodeIKernelGroupService/getGroupRecommendContactArkJson', - GroupCode, + groupCode, ) as GeneralCallResult & { arkJson: string }; return ret.arkJson; } - //需要异常处理 - async uploadGroupBulletinPic(GroupCode: string, imageurl: string) { + async uploadGroupBulletinPic(groupCode: string, imageurl: string) { const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; - return this.context.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl); + return this.context.session.getGroupService().uploadGroupBulletinPic(groupCode, _Pskey, imageurl); } - async handleGroupRequest(flag: string, operateType: NTGroupRequestOperateTypes, reason?: string) { - const flagitem = flag.split('|'); - const groupCode = flagitem[0]; - const seq = flagitem[1]; - const type = parseInt(flagitem[2]); - + async handleGroupRequest(notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) { return this.context.session.getGroupService().operateSysNotify( false, { operateType: operateType, targetMsg: { - seq: seq, // 通知序列号 - type: type, - groupCode: groupCode, + seq: notify.seq, // 通知序列号 + type: notify.type, + groupCode: notify.group.groupCode, postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格 }, }); } - async quitGroup(groupQQ: string) { - return this.context.session.getGroupService().quitGroup(groupQQ); + async quitGroup(groupCode: string) { + return this.context.session.getGroupService().quitGroup(groupCode); } - async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { - return this.context.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason); + async kickMember(groupCode: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { + return this.context.session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason); } - async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { + async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) { // timeStamp为秒数, 0为解除禁言 - return this.context.session.getGroupService().setMemberShutUp(groupQQ, memList); + return this.context.session.getGroupService().setMemberShutUp(groupCode, memList); } - async banGroup(groupQQ: string, shutUp: boolean) { - return this.context.session.getGroupService().setGroupShutUp(groupQQ, shutUp); + async banGroup(groupCode: string, shutUp: boolean) { + return this.context.session.getGroupService().setGroupShutUp(groupCode, shutUp); } - async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { - return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName); + async setMemberCard(groupCode: string, memberUid: string, cardName: string) { + return this.context.session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName); } - async setMemberRole(groupQQ: string, memberUid: string, role: NTGroupMemberRole) { - return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role); + async setMemberRole(groupCode: string, memberUid: string, role: NTGroupMemberRole) { + return this.context.session.getGroupService().modifyMemberRole(groupCode, memberUid, role); } - async setGroupName(groupQQ: string, groupName: string) { - return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false); + async setGroupName(groupCode: string, groupName: string) { + return this.context.session.getGroupService().modifyGroupName(groupCode, groupName, false); } - async publishGroupBulletin(groupQQ: string, content: string, picInfo: { + async publishGroupBulletin(groupCode: string, content: string, picInfo: { id: string, width: number, height: number @@ -479,11 +346,11 @@ export class NTQQGroupApi { pinned: pinned, confirmRequired: confirmRequired, }; - return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data); + return this.context.session.getGroupService().publishGroupBulletin(groupCode, psKey!, data); } - async getGroupRemainAtTimes(GroupCode: string) { - return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode); + async getGroupRemainAtTimes(groupCode: string) { + return this.context.session.getGroupService().getGroupRemainAtTimes(groupCode); } async getMemberExtInfo(groupCode: string, uin: string) { diff --git a/src/core/apis/sign.ts b/src/core/apis/sign.ts deleted file mode 100644 index c8e2ad6f..00000000 --- a/src/core/apis/sign.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { InstanceContext, NapCatCore } from '..'; - -export class NTQQMusicSignApi { - context: InstanceContext; - core: NapCatCore; - - constructor(context: InstanceContext, core: NapCatCore) { - this.context = context; - this.core = core; - } - //转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o - //https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM - - //外域名不行得走qgroup中转 - //https://proxy.gtimg.cn/tx_tls_gate=y.qq.com/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg - - //可外域名 - //https://pic.ugcimg.cn/500955bdd6657ecc8e82e02d2df06800/jpg1 - - //QQ音乐gtimg接口 - //https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000 - - //还有一处公告上传可以上传高质量图片 持久为qq域名 -} - diff --git a/src/core/apis/user.ts b/src/core/apis/user.ts index 2e4a96cd..010318d1 100644 --- a/src/core/apis/user.ts +++ b/src/core/apis/user.ts @@ -2,6 +2,8 @@ import { ModifyProfileParams, User, UserDetailSource } from '@/core/types'; import { RequestUtil } from '@/common/request'; import { InstanceContext, NapCatCore, ProfileBizType } from '..'; import { solveAsyncProblem } from '@/common/helper'; +import { promisify } from 'node:util'; +import { LRUCache } from '@/common/lru-cache'; export class NTQQUserApi { context: InstanceContext; @@ -11,13 +13,15 @@ export class NTQQUserApi { this.context = context; this.core = core; } - //self_tind格式 - async createUidFromTinyId(tinyId: string) { - return this.context.session.getMsgService().createUidFromTinyId(this.core.selfInfo.uin, tinyId); - } - async getStatusByUid(uid: string) { - return this.context.session.getProfileService().getStatus(uid); + + async getCoreAndBaseInfo(uids: string[]) { + return await this.core.eventWrapper.callNoListenerEvent( + 'NodeIKernelProfileService/getCoreAndBaseInfo', + 'nodeStore', + uids, + ); } + // 默认获取自己的 type = 2 获取别人 type = 1 async getProfileLike(uid: string, start: number, count: number, type: number = 2) { return this.context.session.getProfileLikeService().getBuddyProfileLike({ @@ -161,35 +165,51 @@ export class NTQQUserApi { if (!skey) { throw new Error('SKey is Empty'); } + return skey; } - //后期改成流水线处理 async getUidByUinV2(Uin: string) { - let uid = (await this.context.session.getGroupService().getUidByUins([Uin])).uids.get(Uin); - if (uid) return uid; - uid = (await this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin); - if (uid) return uid; - uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin); - if (uid) return uid; - const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换 - if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid; - //if (uid) return uid; - return uid; + if (!Uin) { + return ''; + } + const services = [ + () => this.context.session.getUixConvertService().getUid([Uin]).then((data) => data.uidInfo.get(Uin)).catch(() => undefined), + () => promisify> + (this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined), + () => this.context.session.getGroupService().getUidByUins([Uin]).then((data) => data.uids.get(Uin)).catch(() => undefined), + () => this.getUserDetailInfoByUin(Uin).then((data) => data.detail.uid).catch(() => undefined), + ]; + let uid: string | undefined = undefined; + for (const service of services) { + uid = await service(); + if (uid && uid.indexOf('*') == -1 && uid !== '') { + break; + } + } + return uid ?? ''; } - //后期改成流水线处理 async getUinByUidV2(Uid: string) { - let uin = (await this.context.session.getGroupService().getUinByUids([Uid])).uins.get(Uid); - if (uin) return uin; - uin = (await this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid); - if (uin) return uin; - uin = (await this.context.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid); - if (uin) return uin; - uin = (await this.core.apis.FriendApi.getBuddyIdMap(true)).getKey(Uid); - if (uin) return uin; - uin = (await this.getUserDetailInfo(Uid)).uin; //从QQ Native 转换 - return uin; + if (!Uid) { + return '0'; + } + const services = [ + () => this.context.session.getUixConvertService().getUin([Uid]).then((data) => data.uinInfo.get(Uid)).catch(() => undefined), + () => this.context.session.getGroupService().getUinByUids([Uid]).then((data) => data.uins.get(Uid)).catch(() => undefined), + () => promisify> + (this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined), + () => this.core.apis.FriendApi.getBuddyIdMap(true).then((data) => data.getKey(Uid)).catch(() => undefined), + () => this.getUserDetailInfo(Uid).then((data) => data.uin).catch(() => undefined), + ]; + let uin: string | undefined = undefined; + for (const service of services) { + uin = await service(); + if (uin && uin !== '0' && uin !== '') { + break; + } + } + return uin ?? '0'; } async getRecentContactListSnapShot(count: number) { diff --git a/src/core/apis/webapi.ts b/src/core/apis/webapi.ts index 8cb147b1..933a0941 100644 --- a/src/core/apis/webapi.ts +++ b/src/core/apis/webapi.ts @@ -366,50 +366,4 @@ export class NTQQWebApi { return post; } - - async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) { - const img_size = statSync(path).size; - const img_name = basename(path); - let seq = 0; - let offset = 0; - const GTK = this.getBknFromSKey(pskey); - const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`; - - const stream = createReadStream(path, { highWaterMark: slice_size }); - - for await (const chunk of stream) { - const end = Math.min(offset + chunk.length, img_size); - const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; - const formData = await RequestUtil.createFormData(boundary, path); - - const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`; - const body = { - uin: uin, - appid: "qun", - session: session, - offset: offset, - data: formData, - checksum: "", - check_type: 0, - retry: 0, - seq: seq, - end: end, - cmd: "FileUpload", - slice_size: slice_size, - "biz_req.iUploadType": 0 - }; - - const post = await RequestUtil.HttpGetJson(api, 'POST', body, { - "Cookie": cookie, - "Content-Type": `multipart/form-data; boundary=${boundary}` - }); - - offset += chunk.length; - seq++; - } - } - async uploadQunAlbum(path: string, albumId: string, group: string, skey: string, pskey: string, uin: string) { - const session = (await this.createQunAlbumSession(group, albumId, group, path, skey, pskey, uin) as { data: { session: string } }).data.session; - return await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 1024 * 1024); - } } diff --git a/src/core/external/appid.json b/src/core/external/appid.json index c1284c54..d728732a 100644 --- a/src/core/external/appid.json +++ b/src/core/external/appid.json @@ -98,5 +98,17 @@ "6.9.61-29927": { "appid": 537255836, "qua": "V1_MAC_NQ_6.9.61_29927_GW_B" + }, + "9.9.17-30366": { + "appid": 537258389, + "qua": "V1_WIN_NQ_9.9.17_30366_GW_B" + }, + "3.2.15-30366": { + "appid": 537258413, + "qua": "V1_LNX_NQ_3.2.15_30366_GW_B" + }, + "6.9.62-30366": { + "appid": 537258401, + "qua": "V1_MAC_NQ_6.9.62_30366_GW_B" } -} +} \ No newline at end of file diff --git a/src/core/external/offset.json b/src/core/external/offset.json index f6dabbb7..47e466ad 100644 --- a/src/core/external/offset.json +++ b/src/core/external/offset.json @@ -102,5 +102,25 @@ "6.9.61-29927-arm64": { "send": "4038740", "recv": "403AF58" + }, + "9.9.17-30366-x64": { + "send": "39AB0B0", + "recv": "39AF4E4" + }, + "3.2.15-30366-x64": { + "send": "A402380", + "recv": "A405C80" + }, + "3.2.15-30366-arm64": { + "send": "70C3FA8", + "recv": "70C77E0" + }, + "6.9.62-30366-x64": { + "send": "4669760", + "recv": "466BFCC" + }, + "6.9.62-30366-arm64": { + "send": "4189770", + "recv": "418BF88" } -} +} \ No newline at end of file diff --git a/src/core/helper/msg.ts b/src/core/helper/msg.ts index e1d3992e..c28858d8 100644 --- a/src/core/helper/msg.ts +++ b/src/core/helper/msg.ts @@ -1,7 +1,7 @@ -import * as fileType from 'file-type'; +import { fileTypeFromFile } from 'file-type'; import { PicType } from '../types'; export async function getFileTypeForSendType(picPath: string): Promise { - const fileTypeResult = (await fileType.fileTypeFromFile(picPath))?.ext ?? 'jpg'; + const fileTypeResult = (await fileTypeFromFile(picPath))?.ext ?? 'jpg'; const picTypeMap: { [key: string]: PicType } = { //'webp': PicType.NEWPIC_WEBP, 'gif': PicType.NEWPIC_GIF, diff --git a/src/core/index.ts b/src/core/index.ts index 3b318054..3cdf6158 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -24,10 +24,10 @@ import path from 'node:path'; import fs from 'node:fs'; import { hostname, systemName, systemVersion } from '@/common/system'; import { NTEventWrapper } from '@/common/event'; -import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types'; +import { GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types'; import { NapCatConfigLoader } from '@/core/helper/config'; import os from 'node:os'; -import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners'; +import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners'; import { proxiedListenerOf } from '@/common/proxy-handler'; import { NTQQPacketApi } from './apis/packet'; export * from './wrapper'; @@ -163,7 +163,6 @@ export class NapCatCore { msgListener.onAddSendMsg = (msg) => { this.context.logger.logMessage(msg, this.selfInfo); }; - //await sleep(2500); this.context.session.getMsgService().addKernelMsgListener( proxiedListenerOf(msgListener, this.context.logger), ); @@ -185,92 +184,6 @@ export class NapCatCore { this.context.session.getProfileService().addKernelProfileListener( proxiedListenerOf(profileListener, this.context.logger), ); - - // 群相关 - const groupListener = new NodeIKernelGroupListener(); - groupListener.onGroupListUpdate = (updateType, groupList) => { - // console.log("onGroupListUpdate", updateType, groupList) - groupList.map(g => { - const existGroup = this.apis.GroupApi.groupCache.get(g.groupCode); - //群成员数量变化 应该刷新缓存 - if (existGroup && g.memberCount === existGroup.memberCount) { - Object.assign(existGroup, g); - } else { - this.apis.GroupApi.groupCache.set(g.groupCode, g); - // 获取群成员 - } - const sceneId = this.context.session.getGroupService().createMemberListScene(g.groupCode, 'groupMemberList_MainWindow'); - this.context.session.getGroupService().getNextMemberList(sceneId, undefined, 3000).then( /* r => { - // console.log(`get group ${g.groupCode} members`, r); - // r.result.infos.forEach(member => { - // }); - // groupMembers.set(g.groupCode, r.result.infos); - } */); - this.context.session.getGroupService().destroyMemberListScene(sceneId); - }); - }; - groupListener.onMemberListChange = (arg) => { - // TODO: 应该加一个内部自己维护的成员变动callback,用于判断成员变化通知 - const groupCode = arg.sceneId.split('_')[0]; - if (this.apis.GroupApi.groupMemberCache.has(groupCode)) { - const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!; - arg.infos.forEach((member, uid) => { - //console.log('onMemberListChange', member); - const existMember = existMembers.get(uid); - if (existMember) { - Object.assign(existMember, member); - } else { - existMembers.set(uid, member); - } - //移除成员 - if (member.isDelete) { - existMembers.delete(uid); - } - }); - } else { - this.apis.GroupApi.groupMemberCache.set(groupCode, arg.infos); - } - }; - groupListener.onMemberInfoChange = (groupCode, dataSource, members) => { - if (dataSource === DataSource.LOCAL && members.get(this.selfInfo.uid)?.isDelete) { - // 自身退群或者被踢退群 5s用于Api操作 之后不再出现 - setTimeout(() => { - this.apis.GroupApi.groupCache.delete(groupCode); - }, 5000); - - } - const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode); - if (existMembers) { - members.forEach((member, uid) => { - const existMember = existMembers.get(uid); - if (existMember) { - // 检查管理变动 - member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember); - // 更新成员信息 - Object.assign(existMember, member); - } else { - existMembers.set(uid, member); - } - //移除成员 - if (member.isDelete) { - existMembers.delete(uid); - } - }); - } else { - this.apis.GroupApi.groupMemberCache.set(groupCode, members); - } - }; - this.context.session.getGroupService().addKernelGroupListener( - proxiedListenerOf(groupListener, this.context.logger), - ); - } - - checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean { - if (memberNew.role !== memberOld?.role) { - this.context.logger.logDebug(`群 ${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`); - return true; - } - return false; } } diff --git a/src/core/packet/transformer/proto/message/groupAdmin.ts b/src/core/packet/transformer/proto/message/groupAdmin.ts new file mode 100644 index 00000000..42030063 --- /dev/null +++ b/src/core/packet/transformer/proto/message/groupAdmin.ts @@ -0,0 +1,18 @@ +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; + +export const GroupAdminExtra = { + adminUid: ProtoField(1, ScalarType.STRING), + isPromote: ProtoField(2, ScalarType.BOOL), +}; + +export const GroupAdminBody = { + extraDisable: ProtoField(1, () => GroupAdminExtra), + extraEnable: ProtoField(2, () => GroupAdminExtra), +}; + +export const GroupAdmin = { + groupUin: ProtoField(1, ScalarType.UINT32), + flag: ProtoField(2, ScalarType.UINT32), + isPromote: ProtoField(3, ScalarType.BOOL), + body: ProtoField(4, () => GroupAdminBody), +}; \ No newline at end of file diff --git a/src/core/services/NodeIKernelGroupService.ts b/src/core/services/NodeIKernelGroupService.ts index 61f3179a..1b748883 100644 --- a/src/core/services/NodeIKernelGroupService.ts +++ b/src/core/services/NodeIKernelGroupService.ts @@ -187,13 +187,13 @@ export interface NodeIKernelGroupService { destroyGroup(groupCode: string): void; - getSingleScreenNotifies(doubted: boolean, start_seq: string, num: number): Promise; + getSingleScreenNotifies(doubt: boolean, startSeq: string, count: number): Promise; clearGroupNotifies(groupCode: string): void; - getGroupNotifiesUnreadCount(unknown: boolean): Promise; + getGroupNotifiesUnreadCount(doubt: boolean): Promise; - clearGroupNotifiesUnreadCount(unknown: boolean): void; + clearGroupNotifiesUnreadCount(doubt: boolean): void; operateSysNotify( doubt: boolean, diff --git a/src/core/services/NodeIKernelProfileService.ts b/src/core/services/NodeIKernelProfileService.ts index cfcb18bf..e4f6e6a6 100644 --- a/src/core/services/NodeIKernelProfileService.ts +++ b/src/core/services/NodeIKernelProfileService.ts @@ -4,14 +4,14 @@ import { GeneralCallResult } from '@/core/services/common'; export interface NodeIKernelProfileService { getOtherFlag(callfrom: string, uids: string[]): Promise>; - + getVasInfo(callfrom: string, uids: string[]): Promise>; getRelationFlag(callfrom: string, uids: string[]): Promise>; - getUidByUin(callfrom: string, uin: Array): Promise>; + getUidByUin(callfrom: string, uin: Array): Map; - getUinByUid(callfrom: string, uid: Array): Promise>; + getUinByUid(callfrom: string, uid: Array): Map; getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise>; diff --git a/src/core/types/element.ts b/src/core/types/element.ts index 579603ba..8a848876 100644 --- a/src/core/types/element.ts +++ b/src/core/types/element.ts @@ -29,6 +29,7 @@ export interface TextElement { } export interface FaceElement { + pokeType?: number; faceIndex: number; faceType: FaceType; faceText?: string; diff --git a/src/onebot/action/extends/GetCollectionList.ts b/src/onebot/action/extends/GetCollectionList.ts index f1a31c16..a80484dd 100644 --- a/src/onebot/action/extends/GetCollectionList.ts +++ b/src/onebot/action/extends/GetCollectionList.ts @@ -14,6 +14,6 @@ export class GetCollectionList extends OneBotAction { payloadSchema = SchemaData; async _handle(payload: Payload) { - return await this.core.apis.CollectionApi.getAllCollection(parseInt(payload.category.toString()), +payload.count); + return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count); } } diff --git a/src/onebot/action/extends/GetGroupAddRequest.ts b/src/onebot/action/extends/GetGroupAddRequest.ts index c886ed2f..1469292a 100644 --- a/src/onebot/action/extends/GetGroupAddRequest.ts +++ b/src/onebot/action/extends/GetGroupAddRequest.ts @@ -1,33 +1,37 @@ import { GroupNotifyMsgStatus } from '@/core'; import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; +import { Notify } from '@/onebot/types'; -interface OB11GroupRequestNotify { - group_id: number, - user_id: number, - flag: string -} - -export default class GetGroupAddRequest extends OneBotAction { +export default class GetGroupAddRequest extends OneBotAction { actionName = ActionName.GetGroupIgnoreAddRequest; - async _handle(payload: null): Promise { - const ignoredNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 10); - const retData: any = { - join_requests: await Promise.all( - ignoredNotifies - .filter(notify => notify.type === 7) - .map(async SSNotify => ({ - request_id: SSNotify.seq, - requester_uin: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1?.uid), - requester_nick: SSNotify.user1?.nickName, - group_id: SSNotify.group?.groupCode, - group_name: SSNotify.group?.groupName, - checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE, - actor: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2?.uid) || 0, - }))), - }; + async _handle(payload: null): Promise { + const NTQQUserApi = this.core.apis.UserApi; + const NTQQGroupApi = this.core.apis.GroupApi; + const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10); + const retData: Notify[] = []; + + const notifyPromises = ignoredNotifies + .filter(notify => notify.type === 7) + .map(async SSNotify => { + const invitorUin = SSNotify.user1?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user1.uid) : 0; + const actorUin = SSNotify.user2?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user2.uid) : 0; + retData.push({ + request_id: +SSNotify.seq, + invitor_uin: invitorUin, + invitor_nick: SSNotify.user1?.nickName, + group_id: +SSNotify.group?.groupCode, + message: SSNotify?.postscript, + group_name: SSNotify.group?.groupName, + checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE, + actor: actorUin, + requester_nick: SSNotify.user1?.nickName, + }); + }); + + await Promise.all(notifyPromises); return retData; } -} +} \ No newline at end of file diff --git a/src/onebot/action/extends/OCRImage.ts b/src/onebot/action/extends/OCRImage.ts index 7f79eab4..dc39b9e0 100644 --- a/src/onebot/action/extends/OCRImage.ts +++ b/src/onebot/action/extends/OCRImage.ts @@ -1,6 +1,6 @@ import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; -import { checkFileExist, uri2local } from '@/common/file'; +import { checkFileExist, uriToLocalFile } from '@/common/file'; import fs from 'fs'; import { Static, Type } from '@sinclair/typebox'; @@ -15,7 +15,7 @@ export class OCRImage extends OneBotAction { payloadSchema = SchemaData; async _handle(payload: Payload) { - const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.image)); + const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image)); if (!success) { throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`); } diff --git a/src/onebot/action/extends/SetQQAvatar.ts b/src/onebot/action/extends/SetQQAvatar.ts index 3f96a91c..d5b656a1 100644 --- a/src/onebot/action/extends/SetQQAvatar.ts +++ b/src/onebot/action/extends/SetQQAvatar.ts @@ -1,7 +1,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; -import * as fs from 'node:fs'; -import { checkFileExist, uri2local } from '@/common/file'; +import fs from 'node:fs/promises'; +import { checkFileExist, uriToLocalFile } from '@/common/file'; import { Static, Type } from '@sinclair/typebox'; const SchemaData = Type.Object({ @@ -14,16 +14,14 @@ export default class SetAvatar extends OneBotAction { actionName = ActionName.SetQQAvatar; payloadSchema = SchemaData; async _handle(payload: Payload): Promise { - const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file)); + const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file)); if (!success) { throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`); } if (path) { await checkFileExist(path, 5000);// 避免崩溃 const ret = await this.core.apis.UserApi.setQQAvatar(path); - fs.unlink(path, () => { - }); - + fs.unlink(path).catch(() => { }); if (!ret) { throw new Error(`头像${payload.file}设置失败,api无返回`); } @@ -34,7 +32,7 @@ export default class SetAvatar extends OneBotAction { throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`); } } else { - fs.unlink(path, () => { }); + fs.unlink(path).catch(() => { }); throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`); } return null; diff --git a/src/onebot/action/go-cqhttp/CreateGroupFileFolder.ts b/src/onebot/action/go-cqhttp/CreateGroupFileFolder.ts index 01af44c6..3764f148 100644 --- a/src/onebot/action/go-cqhttp/CreateGroupFileFolder.ts +++ b/src/onebot/action/go-cqhttp/CreateGroupFileFolder.ts @@ -13,6 +13,6 @@ export class CreateGroupFileFolder extends OneBotAction { actionName = ActionName.GoCQHTTP_CreateGroupFileFolder; payloadSchema = SchemaData; async _handle(payload: Payload) { - return (await this.core.apis.GroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem; + return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem; } } diff --git a/src/onebot/action/go-cqhttp/DeleteGroupFile.ts b/src/onebot/action/go-cqhttp/DeleteGroupFile.ts index 244a5368..85b43cce 100644 --- a/src/onebot/action/go-cqhttp/DeleteGroupFile.ts +++ b/src/onebot/action/go-cqhttp/DeleteGroupFile.ts @@ -17,6 +17,6 @@ export class DeleteGroupFile extends OneBotAction { async _handle(payload: Payload) { const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id); if (!data) throw new Error('Invalid file_id'); - return await this.core.apis.GroupApi.DelGroupFile(payload.group_id.toString(), [data.fileId]); + return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]); } } diff --git a/src/onebot/action/go-cqhttp/DeleteGroupFileFolder.ts b/src/onebot/action/go-cqhttp/DeleteGroupFileFolder.ts index c6e20df9..f8a813a4 100644 --- a/src/onebot/action/go-cqhttp/DeleteGroupFileFolder.ts +++ b/src/onebot/action/go-cqhttp/DeleteGroupFileFolder.ts @@ -14,7 +14,7 @@ export class DeleteGroupFileFolder extends OneBotAction { actionName = ActionName.GoCQHTTP_DeleteGroupFileFolder; payloadSchema = SchemaData; async _handle(payload: Payload) { - return (await this.core.apis.GroupApi.DelGroupFileFolder( + return (await this.core.apis.GroupApi.delGroupFileFolder( payload.group_id.toString(), payload.folder ?? payload.folder_id ?? '')).groupFileCommonResult; } } diff --git a/src/onebot/action/go-cqhttp/SendGroupNotice.ts b/src/onebot/action/go-cqhttp/SendGroupNotice.ts index b36146a8..6dd8a343 100644 --- a/src/onebot/action/go-cqhttp/SendGroupNotice.ts +++ b/src/onebot/action/go-cqhttp/SendGroupNotice.ts @@ -1,7 +1,7 @@ -import { checkFileExist, uri2local } from '@/common/file'; +import { checkFileExist, uriToLocalFile } from '@/common/file'; import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; -import { unlink } from 'node:fs'; +import { unlink } from 'node:fs/promises'; import { Static, Type } from '@sinclair/typebox'; const SchemaData = Type.Object({ @@ -28,7 +28,7 @@ export class SendGroupNotice extends OneBotAction { const { path, success, - } = (await uri2local(this.core.NapCatTempPath, payload.image)); + } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image)); if (!success) { throw new Error(`群公告${payload.image}设置失败,image字段可能格式不正确`); } @@ -41,8 +41,7 @@ export class SendGroupNotice extends OneBotAction { throw new Error(`群公告${payload.image}设置失败,图片上传失败`); } - unlink(path, () => { - }); + unlink(path).catch(() => { }); UploadImage = ImageUploadResult.picInfo; } diff --git a/src/onebot/action/go-cqhttp/SetGroupPortrait.ts b/src/onebot/action/go-cqhttp/SetGroupPortrait.ts index 9845aad7..41532109 100644 --- a/src/onebot/action/go-cqhttp/SetGroupPortrait.ts +++ b/src/onebot/action/go-cqhttp/SetGroupPortrait.ts @@ -1,9 +1,8 @@ import { OneBotAction } from '@/onebot/action/OneBotAction'; -import { ActionName, BaseCheckResult } from '@/onebot/action/router'; -import * as fs from 'node:fs'; -import { checkFileExistV2, uri2local } from '@/common/file'; +import { ActionName } from '@/onebot/action/router'; +import { checkFileExistV2, uriToLocalFile } from '@/common/file'; import { Static, Type } from '@sinclair/typebox'; - +import fs from 'node:fs/promises'; const SchemaData = Type.Object({ file: Type.String(), group_id: Type.Union([Type.Number(), Type.String()]) @@ -14,16 +13,16 @@ type Payload = Static; export default class SetGroupPortrait extends OneBotAction { actionName = ActionName.SetGroupPortrait; payloadSchema = SchemaData; - + async _handle(payload: Payload): Promise { - const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file)); + const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file)); if (!success) { throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`); } if (path) { await checkFileExistV2(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 const ret = await this.core.apis.GroupApi.setGroupAvatar(payload.group_id.toString(), path); - fs.unlink(path, () => { }); + fs.unlink(path).catch(() => { }); if (!ret) { throw new Error(`头像${payload.file}设置失败,api无返回`); } @@ -34,7 +33,7 @@ export default class SetGroupPortrait extends OneBotAction { } return ret; } else { - fs.unlink(path, () => { }); + fs.unlink(path).catch(() => { }); throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`); } } diff --git a/src/onebot/action/go-cqhttp/UploadGroupFile.ts b/src/onebot/action/go-cqhttp/UploadGroupFile.ts index 62e5cf70..3c44458a 100644 --- a/src/onebot/action/go-cqhttp/UploadGroupFile.ts +++ b/src/onebot/action/go-cqhttp/UploadGroupFile.ts @@ -2,7 +2,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; import { ChatType, Peer } from '@/core/types'; import fs from 'fs'; -import { uri2local } from '@/common/file'; +import { uriToLocalFile } from '@/common/file'; import { SendMessageContext } from '@/onebot/api'; import { Static, Type } from '@sinclair/typebox'; @@ -25,7 +25,7 @@ export default class GoCQHTTPUploadGroupFile extends OneBotAction if (fs.existsSync(file)) { file = `file://${file}`; } - const downloadResult = await uri2local(this.core.NapCatTempPath, file); + const downloadResult = await uriToLocalFile(this.core.NapCatTempPath, file); const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString(), diff --git a/src/onebot/action/go-cqhttp/UploadPrivateFile.ts b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts index 4633162e..aa555852 100644 --- a/src/onebot/action/go-cqhttp/UploadPrivateFile.ts +++ b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts @@ -2,7 +2,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; import { ChatType, Peer, SendFileElement } from '@/core/types'; import fs from 'fs'; -import { uri2local } from '@/common/file'; +import { uriToLocalFile } from '@/common/file'; import { SendMessageContext } from '@/onebot/api'; import { ContextMode, createContext } from '@/onebot/action/msg/SendMsg'; import { Static, Type } from '@sinclair/typebox'; @@ -36,7 +36,7 @@ export default class GoCQHTTPUploadPrivateFile extends OneBotAction { +import { Notify } from '@/onebot/types'; + +interface RetData { + InvitedRequest: Notify[]; + join_requests: Notify[]; +} + +export class GetGroupIgnoredNotifies extends OneBotAction { actionName = ActionName.GetGroupIgnoredNotifies; - async _handle(payload: void) { - const ignoredNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 10); - const retData: any = { - join_requests: await Promise.all( - ignoredNotifies - .filter(notify => notify.type === 7) - .map(async SSNotify => ({ - request_id: SSNotify.seq, - requester_uin: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1?.uid), - requester_nick: SSNotify.user1?.nickName, - group_id: SSNotify.group?.groupCode, - group_name: SSNotify.group?.groupName, - checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE, - actor: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2?.uid) || 0, - }))), - }; + async _handle(): Promise { + const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50); + const retData: RetData = { InvitedRequest: [], join_requests: [] }; + + const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => { + const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0; + const actorUin = SSNotify.user2?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2.uid) : 0; + const commonData = { + request_id: +SSNotify.seq, + invitor_uin: invitorUin, + invitor_nick: SSNotify.user1?.nickName, + group_id: +SSNotify.group?.groupCode, + message: SSNotify?.postscript, + group_name: SSNotify.group?.groupName, + checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE, + actor: actorUin, + requester_nick: SSNotify.user1?.nickName, + }; + + if (SSNotify.type === 1) { + retData.InvitedRequest.push(commonData); + } else if (SSNotify.type === 7) { + retData.join_requests.push(commonData); + } + }); + + await Promise.all(notifyPromises); return retData; } -} +} \ No newline at end of file diff --git a/src/onebot/action/group/GetGroupMemberList.ts b/src/onebot/action/group/GetGroupMemberList.ts index 451ae6e1..35500059 100644 --- a/src/onebot/action/group/GetGroupMemberList.ts +++ b/src/onebot/action/group/GetGroupMemberList.ts @@ -19,11 +19,14 @@ export class GetGroupMemberList extends OneBotAction const groupIdStr = payload.group_id.toString(); const noCache = payload.no_cache ? this.stringToBoolean(payload.no_cache) : false; const memberCache = this.core.apis.GroupApi.groupMemberCache; - let groupMembers; - try { - groupMembers = await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr, 3000, noCache); - } catch (error) { - groupMembers = memberCache.get(groupIdStr) ?? await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr); + let groupMembers = memberCache.get(groupIdStr); + if (noCache || !groupMembers) { + this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr).then().catch(); + //下次刷新 + groupMembers = memberCache.get(groupIdStr); + if (!groupMembers) { + throw new Error(`Failed to get group member list for group ${groupIdStr}`); + } } const memberPromises = Array.from(groupMembers.values()).map(item => OB11Construct.groupMember(groupIdStr, item) diff --git a/src/onebot/action/group/SendGroupAiRecord.ts b/src/onebot/action/group/SendGroupAiRecord.ts index e76c70e0..9b051469 100644 --- a/src/onebot/action/group/SendGroupAiRecord.ts +++ b/src/onebot/action/group/SendGroupAiRecord.ts @@ -1,6 +1,6 @@ import { ActionName } from '@/onebot/action/router'; import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; -import { uri2local } from "@/common/file"; +import { uriToLocalFile } from "@/common/file"; import { ChatType, Peer } from "@/core"; import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; import { Static, Type } from '@sinclair/typebox'; @@ -23,7 +23,7 @@ export class SendGroupAiRecord extends GetPacketStatusDepends; @@ -18,10 +18,26 @@ export default class SetGroupAddRequest extends OneBotAction { async _handle(payload: Payload): Promise { const flag = payload.flag.toString(); const approve = payload.approve?.toString() !== 'false'; - await this.core.apis.GroupApi.handleGroupRequest(flag, + const reason = payload.reason ?? ' '; + + const notify = await this.findNotify(flag); + if (!notify) { + throw new Error('No such request'); + } + + await this.core.apis.GroupApi.handleGroupRequest( + notify, approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE, - payload.reason ?? ' ', + reason, ); return null; } -} + + private async findNotify(flag: string) { + let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag); + if (!notify) { + notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag); + } + return notify; + } +} \ No newline at end of file diff --git a/src/onebot/action/group/SetGroupBan.ts b/src/onebot/action/group/SetGroupBan.ts index 88687582..7b73a007 100644 --- a/src/onebot/action/group/SetGroupBan.ts +++ b/src/onebot/action/group/SetGroupBan.ts @@ -18,7 +18,7 @@ export default class SetGroupBan extends OneBotAction { const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); if (!uid) throw new Error('uid error'); await this.core.apis.GroupApi.banMember(payload.group_id.toString(), - [{ uid: uid, timeStamp: parseInt(payload.duration.toString()) }]); + [{ uid: uid, timeStamp: +payload.duration }]); return null; } } diff --git a/src/onebot/action/msg/ForwardSingleMsg.ts b/src/onebot/action/msg/ForwardSingleMsg.ts index f37bf4c9..61beb99d 100644 --- a/src/onebot/action/msg/ForwardSingleMsg.ts +++ b/src/onebot/action/msg/ForwardSingleMsg.ts @@ -25,7 +25,7 @@ class ForwardSingleMsg extends OneBotAction { } async _handle(payload: Payload): Promise { - const msg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString())); + const msg = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id); if (!msg) { throw new Error(`无法找到消息${payload.message_id}`); } diff --git a/src/onebot/action/msg/SetMsgEmojiLike.ts b/src/onebot/action/msg/SetMsgEmojiLike.ts index 1211a62b..0ac44b1a 100644 --- a/src/onebot/action/msg/SetMsgEmojiLike.ts +++ b/src/onebot/action/msg/SetMsgEmojiLike.ts @@ -16,7 +16,7 @@ export class SetMsgEmojiLike extends OneBotAction { payloadSchema = SchemaData; async _handle(payload: Payload) { - const msg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString())); + const msg = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id); if (!msg) { throw new Error('msg not found'); } diff --git a/src/onebot/action/system/GetSystemMsg.ts b/src/onebot/action/system/GetSystemMsg.ts index 435d17bb..01605869 100644 --- a/src/onebot/action/system/GetSystemMsg.ts +++ b/src/onebot/action/system/GetSystemMsg.ts @@ -1,38 +1,43 @@ import { GroupNotifyMsgStatus } from '@/core'; import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; -export class GetGroupSystemMsg extends OneBotAction { +import { Notify } from '@/onebot/types'; + +interface RetData { + InvitedRequest: Notify[]; + join_requests: Notify[]; +} + +export class GetGroupSystemMsg extends OneBotAction { actionName = ActionName.GetGroupSystemMsg; - async _handle() { - const NTQQUserApi = this.core.apis.UserApi; - const NTQQGroupApi = this.core.apis.GroupApi; - // 默认10条 该api未完整实现 包括响应数据规范化 类型规范化 - const SingleScreenNotifies = await NTQQGroupApi.getSingleScreenNotifies(false,10); - const retData: any = { InvitedRequest: [], join_requests: [] }; - for (const SSNotify of SingleScreenNotifies) { - if (SSNotify.type == 1) { - retData.InvitedRequest.push({ - request_id: SSNotify.seq, - invitor_uin: await NTQQUserApi.getUinByUidV2(SSNotify.user1?.uid), - invitor_nick: SSNotify.user1?.nickName, - group_id: SSNotify.group?.groupCode, - group_name: SSNotify.group?.groupName, - checked: SSNotify.status === GroupNotifyMsgStatus.KUNHANDLE ? false : true, - actor: await NTQQUserApi.getUinByUidV2(SSNotify.user2?.uid) || 0, - }); - } else if (SSNotify.type == 7) { - retData.join_requests.push({ - request_id: SSNotify.seq, - requester_uin: await NTQQUserApi.getUinByUidV2(SSNotify.user1?.uid), - requester_nick: SSNotify.user1?.nickName, - group_id: SSNotify.group?.groupCode, - group_name: SSNotify.group?.groupName, - checked: SSNotify.status === GroupNotifyMsgStatus.KUNHANDLE ? false : true, - actor: await NTQQUserApi.getUinByUidV2(SSNotify.user2?.uid) || 0, - }); + async _handle(): Promise { + const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50); + const retData: RetData = { InvitedRequest: [], join_requests: [] }; + + const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => { + const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0; + const actorUin = SSNotify.user2?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2.uid) : 0; + const commonData = { + request_id: +SSNotify.seq, + invitor_uin: invitorUin, + invitor_nick: SSNotify.user1?.nickName, + group_id: +SSNotify.group?.groupCode, + message: SSNotify?.postscript, + group_name: SSNotify.group?.groupName, + checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE, + actor: actorUin, + requester_nick: SSNotify.user1?.nickName, + }; + + if (SSNotify.type === 1) { + retData.InvitedRequest.push(commonData); + } else if (SSNotify.type === 7) { + retData.join_requests.push(commonData); } - } + }); + + await Promise.all(notifyPromises); return retData; } diff --git a/src/onebot/action/user/SetFriendAddRequest.ts b/src/onebot/action/user/SetFriendAddRequest.ts index 1170b552..dfe29e6c 100644 --- a/src/onebot/action/user/SetFriendAddRequest.ts +++ b/src/onebot/action/user/SetFriendAddRequest.ts @@ -3,7 +3,7 @@ import { ActionName } from '@/onebot/action/router'; import { Static, Type } from '@sinclair/typebox'; const SchemaData = Type.Object({ - flag: Type.String(), + flag: Type.Union([Type.String(), Type.Number()]), approve: Type.Optional(Type.Union([Type.String(), Type.Boolean()])), remark: Type.Optional(Type.String()) }); @@ -16,14 +16,13 @@ export default class SetFriendAddRequest extends OneBotAction { async _handle(payload: Payload): Promise { const approve = payload.approve?.toString() !== 'false'; - await this.core.apis.FriendApi.handleFriendRequest(payload.flag, approve); + const notify = (await this.core.apis.FriendApi.getBuddyReq()).buddyReqs.find(e => e.reqTime == payload.flag.toString()); + if (!notify) { + throw new Error('No such request'); + } + await this.core.apis.FriendApi.handleFriendRequest(notify, approve); if (payload.remark) { - const data = payload.flag.split('|'); - if (data.length < 2) { - throw new Error('Invalid flag'); - } - const friendUid = data[0]; - await this.core.apis.FriendApi.setBuddyRemark(friendUid, payload.remark); + await this.core.apis.FriendApi.setBuddyRemark(notify.friendUid, payload.remark); } return null; } diff --git a/src/onebot/api/group.ts b/src/onebot/api/group.ts index c760dfd9..f4d3666b 100644 --- a/src/onebot/api/group.ts +++ b/src/onebot/api/group.ts @@ -7,15 +7,10 @@ import { MessageElement, NapCatCore, NTGrayTipElementSubTypeV2, - NTMsgType, RawMessage, - TipGroupElement, - TipGroupElementType, } from '@/core'; import { NapCatOneBot11Adapter } from '@/onebot'; import { OB11GroupBanEvent } from '@/onebot/event/notice/OB11GroupBanEvent'; -import { OB11GroupIncreaseEvent } from '@/onebot/event/notice/OB11GroupIncreaseEvent'; -import { OB11GroupDecreaseEvent } from '@/onebot/event/notice/OB11GroupDecreaseEvent'; import fastXmlParser from 'fast-xml-parser'; import { OB11GroupMsgEmojiLikeEvent } from '@/onebot/event/notice/OB11MsgEmojiLikeEvent'; import { MessageUnique } from '@/common/message-unique'; @@ -56,9 +51,9 @@ export class OneBotGroupApi { if (memberUin && adminUin) { return new OB11GroupBanEvent( this.core, - parseInt(GroupCode), - parseInt(memberUin), - parseInt(adminUin), + +GroupCode, + +memberUin, + +adminUin, duration, subType, ); @@ -66,67 +61,6 @@ export class OneBotGroupApi { return undefined; } - // async parseGroupIncreaseEvent(GroupCode: string, grayTipElement: GrayTipElement) { - // this.core.context.logger.logDebug('收到新人被邀请进群消息', grayTipElement); - // const xmlElement = grayTipElement.xmlElement; - // if (xmlElement?.content) { - // const regex = /jp="(\d+)"/g; - - // const matches = []; - // let match = null; - - // while ((match = regex.exec(xmlElement.content)) !== null) { - // matches.push(match[1]); - // } - // if (matches.length === 2) { - // const [inviter, invitee] = matches; - // return new OB11GroupIncreaseEvent( - // this.core, - // parseInt(GroupCode), - // parseInt(invitee), - // parseInt(inviter), - // 'invite', - // ); - // } - // } - // return undefined; - // } - - // async parseGroupMemberIncreaseEvent(GroupCode: string, grayTipElement: GrayTipElement) { - // const groupElement = grayTipElement?.groupElement; - // if (!groupElement) return undefined; - // const member = await this.core.apis.UserApi.getUserDetailInfo(groupElement.memberUid); - // const memberUin = member?.uin; - // const adminMember = await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid); - // if (memberUin) { - // const operatorUin = adminMember?.uin ?? memberUin; - // return new OB11GroupIncreaseEvent( - // this.core, - // parseInt(GroupCode), - // parseInt(memberUin), - // parseInt(operatorUin), - // ); - // } else { - // return undefined; - // } - // } - - // async parseGroupKickEvent(GroupCode: string, grayTipElement: GrayTipElement) { - // const groupElement = grayTipElement?.groupElement; - // if (!groupElement) return undefined; - // const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid))?.uin ?? (await this.core.apis.UserApi.getUidByUinV2(groupElement.adminUid)); - // if (adminUin) { - // return new OB11GroupDecreaseEvent( - // this.core, - // parseInt(GroupCode), - // parseInt(this.core.selfInfo.uin), - // parseInt(adminUin), - // 'kick_me', - // ); - // } - // return undefined; - // } - async parseGroupEmojiLikeEventByGrayTip( groupCode: string, grayTipElement: GrayTipElement @@ -164,8 +98,8 @@ export class OneBotGroupApi { } return new OB11GroupMsgEmojiLikeEvent( this.core, - parseInt(groupCode), - parseInt(senderUin), + +groupCode, + +senderUin, MessageUnique.getShortIdByMsgId(replyMsg.msgId)!, [{ emoji_id: emojiId, @@ -177,9 +111,10 @@ export class OneBotGroupApi { async parseCardChangedEvent(msg: RawMessage) { if (msg.senderUin && msg.senderUin !== '0') { const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin); - if (member && member.cardName !== msg.sendMemberName) { + const oldName = member?.cardName || member?.nick || ''; + if (member && oldName !== msg.sendMemberName) { const newCardName = msg.sendMemberName ?? ''; - const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName); + const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, oldName); member.cardName = newCardName; return event; } @@ -187,31 +122,6 @@ export class OneBotGroupApi { return undefined; } - // async parseGroupElement(msg: RawMessage, groupElement: TipGroupElement, elementWrapper: GrayTipElement) { - // if (groupElement.type == TipGroupElementType.KMEMBERADD) { - // const MemberIncreaseEvent = await this.obContext.apis.GroupApi.parseGroupMemberIncreaseEvent(msg.peerUid, elementWrapper); - // if (MemberIncreaseEvent) return MemberIncreaseEvent; - // } else if (groupElement.type === TipGroupElementType.KSHUTUP) { - // const BanEvent = await this.obContext.apis.GroupApi.parseGroupBanEvent(msg.peerUid, elementWrapper); - // if (BanEvent) return BanEvent; - // } else if (groupElement.type == TipGroupElementType.KQUITTE) { - // this.core.apis.GroupApi.quitGroup(msg.peerUid).then(); - // try { - // const KickEvent = await this.obContext.apis.GroupApi.parseGroupKickEvent(msg.peerUid, elementWrapper); - // if (KickEvent) return KickEvent; - // } catch (e) { - // return new OB11GroupDecreaseEvent( - // this.core, - // parseInt(msg.peerUid), - // parseInt(this.core.selfInfo.uin), - // 0, - // 'leave', - // ); - // } - // } - // return undefined; - // } - async parsePaiYiPai(msg: RawMessage, jsonStr: string) { const json = JSON.parse(jsonStr); diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index eba61da8..09564992 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -15,7 +15,6 @@ import { RawMessage, SendMessageElement, SendTextElement, - BaseEmojiType, FaceType, GrayTipElement, } from '@/core'; @@ -24,18 +23,17 @@ import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataTyp import { OB11Construct } from '@/onebot/helper/data'; import { EventType } from '@/onebot/event/OneBotEvent'; import { encodeCQCode } from '@/onebot/helper/cqcode'; -import { uri2local } from '@/common/file'; +import { uriToLocalFile } from '@/common/file'; import { RequestUtil } from '@/common/request'; -import fs from 'node:fs'; -import fsPromise from 'node:fs/promises'; +import fsPromise, { constants } from 'node:fs/promises'; import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; -// import { decodeSysMessage } from '@/core/packet/proto/old/ProfileLike'; import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; -import { decodeSysMessage } from "@/core/helper/adaptDecoder"; import { GroupChange, PushMsgBody } from "@/core/packet/transformer/proto"; import { NapProtoMsg } from '@napneko/nap-proto-core'; import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent'; import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent'; +import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin'; +import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent'; type RawToOb11Converters = { [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( @@ -155,6 +153,17 @@ export class OneBotMsgApi { faceElement: async element => { const faceIndex = element.faceIndex; + if (element.faceType == FaceType.Poke) { + return { + type: OB11MessageDataType.poke, + data: { + type: element?.pokeType?.toString() ?? '0', + id: faceIndex.toString(), + } + }; + + } + if (faceIndex === FaceIndex.DICE) { return { type: OB11MessageDataType.dice, @@ -452,7 +461,7 @@ export class OneBotMsgApi { }, [OB11MessageDataType.face]: async ({ data: { id } }) => { - let parsedFaceId = parseInt(id); + const parsedFaceId = +id; // 从face_config.json中获取表情名称 const sysFaces = faceConfig.sysface; const face: any = sysFaces.find((systemFace) => systemFace.QSid === parsedFaceId.toString()); @@ -460,7 +469,6 @@ export class OneBotMsgApi { this.core.context.logger.logError('不支持的ID', id); return undefined; } - parsedFaceId = parseInt(parsedFaceId.toString()); let faceType = 1; if (parsedFaceId >= 222) { faceType = 2; @@ -517,7 +525,7 @@ export class OneBotMsgApi { let thumb = sendMsg.data.thumb; if (thumb) { - const uri2LocalRes = await uri2local(this.core.NapCatTempPath, thumb); + const uri2LocalRes = await uriToLocalFile(this.core.NapCatTempPath, thumb); if (uri2LocalRes.success) thumb = uri2LocalRes.path; } return await this.core.apis.FileApi.createValidSendVideoElement(context, path, fileName, thumb); @@ -883,51 +891,55 @@ export class OneBotMsgApi { if (!sendElements.length) { throw new Error('消息体无法解析, 请检查是否发送了不支持的消息类型'); } - let totalSize = 0; - let timeout = 10000; - try { - for (const fileElement of sendElements) { - if (fileElement.elementType === ElementType.PTT) { - totalSize += fs.statSync(fileElement.pttElement.filePath).size; + + const calculateTotalSize = async (elements: SendMessageElement[]): Promise => { + const sizePromises = elements.map(async element => { + switch (element.elementType) { + case ElementType.PTT: + return (await fsPromise.stat(element.pttElement.filePath)).size; + case ElementType.FILE: + return (await fsPromise.stat(element.fileElement.filePath)).size; + case ElementType.VIDEO: + return (await fsPromise.stat(element.videoElement.filePath)).size; + case ElementType.PIC: + return (await fsPromise.stat(element.picElement.sourcePath)).size; + default: + return 0; } - if (fileElement.elementType === ElementType.FILE) { - totalSize += fs.statSync(fileElement.fileElement.filePath).size; - } - if (fileElement.elementType === ElementType.VIDEO) { - totalSize += fs.statSync(fileElement.videoElement.filePath).size; - } - if (fileElement.elementType === ElementType.PIC) { - totalSize += fs.statSync(fileElement.picElement.sourcePath).size; - } - } - //且 PredictTime ((totalSize / 1024 / 512) * 1000)不等于Nan - const PredictTime = totalSize / 1024 / 256 * 1000; - if (!Number.isNaN(PredictTime)) { - timeout += PredictTime;// 10S Basic Timeout + PredictTime( For File 512kb/s ) - } - } catch (e) { + }); + const sizes = await Promise.all(sizePromises); + return sizes.reduce((total, size) => total + size, 0); + }; + + const totalSize = await calculateTotalSize(sendElements).catch(e => { this.core.context.logger.logError('发送消息计算预计时间异常', e); - } + return 0; + }); + + const timeout = 10000 + (totalSize / 1024 / 256 * 1000); + const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, waitComplete, timeout); if (!returnMsg) throw new Error('发送消息失败'); + returnMsg.id = MessageUnique.createUniqueMsgId({ chatType: peer.chatType, guildId: '', peerUid: peer.peerUid, }, returnMsg.msgId); - - setTimeout(() => { - deleteAfterSentFiles.forEach(file => { + + setTimeout(async () => { + const deletePromises = deleteAfterSentFiles.map(async file => { try { - if (fs.existsSync(file)) { - fsPromise.unlink(file).then().catch(e => this.core.context.logger.logError('发送消息删除文件失败', e)); + if (await fsPromise.access(file, constants.W_OK).then(() => true).catch(() => false)) { + await fsPromise.unlink(file); } - } catch (error) { - this.core.context.logger.logError('发送消息删除文件失败', (error as Error).message); + } catch (e) { + this.core.context.logger.logError('发送消息删除文件失败', e); } }); + await Promise.all(deletePromises); }, 60000); - + return returnMsg; } @@ -935,7 +947,7 @@ export class OneBotMsgApi { { data: inputdata }: OB11MessageFileBase, { deleteAfterSentFiles }: SendMessageContext, ) { - const realUri = inputdata.url || inputdata.file || inputdata.path || ''; + const realUri = inputdata.url ?? inputdata.file ?? inputdata.path ?? ''; if (realUri.length === 0) { this.core.context.logger.logError('文件消息缺少参数', inputdata); throw Error('文件消息缺少参数'); @@ -945,7 +957,7 @@ export class OneBotMsgApi { fileName, errMsg, success, - } = (await uri2local(this.core.NapCatTempPath, realUri)); + } = (await uriToLocalFile(this.core.NapCatTempPath, realUri)); if (!success) { this.core.context.logger.logError('文件下载失败', errMsg); @@ -970,11 +982,10 @@ export class OneBotMsgApi { } async parseSysMessage(msg: number[]) { - // Todo Refactor const SysMessage = new NapProtoMsg(PushMsgBody).decode(Uint8Array.from(msg)); if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) { const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent); - console.log(JSON.stringify(groupChange)); + this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch(); return new OB11GroupIncreaseEvent( this.core, groupChange.groupUin, @@ -984,6 +995,14 @@ export class OneBotMsgApi { ); } else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) { const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent); + if (groupChange.memberUid === this.core.selfInfo.uid) { + setTimeout(() => { + this.core.apis.GroupApi.groupMemberCache.delete(groupChange.groupUin.toString()); + }, 5000); + // 自己被踢了 5S后回收 + } else { + this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch(); + } return new OB11GroupDecreaseEvent( this.core, groupChange.groupUin, @@ -991,31 +1010,26 @@ export class OneBotMsgApi { groupChange.operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(groupChange.operatorUid) : 0, this.groupChangDecreseType2String(groupChange.decreaseType), ); + } else if (SysMessage.contentHead.type == 44 && SysMessage.body?.msgContent) { + const groupAmin = new NapProtoMsg(GroupAdmin).decode(SysMessage.body.msgContent); + this.core.apis.GroupApi.refreshGroupMemberCache(groupAmin.groupUin.toString()).then().catch(); + let enabled = false; + let uid = ''; + if (groupAmin.body.extraEnable != null) { + uid = groupAmin.body.extraEnable.adminUid; + enabled = true; + } else if (groupAmin.body.extraDisable != null) { + uid = groupAmin.body.extraDisable.adminUid; + enabled = false; + } + return new OB11GroupAdminNoticeEvent( + this.core, + groupAmin.groupUin, + +await this.core.apis.UserApi.getUinByUidV2(uid), + enabled ? 'set' : 'unset' + ); } else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) { return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent); } - - /* - if (msgType === 732 && subType === 16 && subSubType === 16) { - const greyTip = GreyTipWrapper.fromBinary(Uint8Array.from(sysMsg.bodyWrapper!.wrappedBody.slice(7))); - if (greyTip.subTypeId === 36) { - const emojiLikeToOthers = EmojiLikeToOthersWrapper1 - .fromBinary(greyTip.rest) - .wrapper! - .body!; - if (emojiLikeToOthers.attributes?.operation !== 1) { // Un-like - return; - } - const eventOrEmpty = await this.apis.GroupApi.createGroupEmojiLikeEvent( - greyTip.groupCode.toString(), - await this.core.apis.UserApi.getUinByUidV2(emojiLikeToOthers.attributes!.senderUid), - emojiLikeToOthers.msgSpec!.msgSeq.toString(), - emojiLikeToOthers.attributes!.emojiId, - ); - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - eventOrEmpty && await this.networkManager.emitEvent(eventOrEmpty); - } - } - */ } } diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 9d858d74..95035243 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -1,8 +1,6 @@ import { BuddyReqType, ChatType, - DataSource, - NTGroupMemberRole, GroupNotifyMsgStatus, GroupNotifyMsgType, InstanceContext, @@ -41,8 +39,6 @@ import { OB11InputStatusEvent } from '@/onebot/event/notice/OB11InputStatusEvent import { MessageUnique } from '@/common/message-unique'; import { proxiedListenerOf } from '@/common/proxy-handler'; import { OB11FriendRequestEvent } from '@/onebot/event/request/OB11FriendRequest'; -import { OB11GroupAdminNoticeEvent } from '@/onebot/event/notice/OB11GroupAdminNoticeEvent'; -// import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '@/onebot/event/notice/OB11GroupDecreaseEvent'; import { OB11GroupRequestEvent } from '@/onebot/event/request/OB11GroupRequest'; import { OB11FriendRecallNoticeEvent } from '@/onebot/event/notice/OB11FriendRecallNoticeEvent'; import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecallNoticeEvent'; @@ -337,7 +333,7 @@ export class NapCatOneBot11Adapter { this.core, +requesterUin, req.extWords, - req.friendUid + '|' + req.reqTime + req.reqTime ) ); } catch (e) { @@ -355,7 +351,6 @@ export class NapCatOneBot11Adapter { const groupListener = new NodeIKernelGroupListener(); groupListener.onGroupNotifiesUpdated = async (_, notifies) => { - //console.log('ob11 onGroupNotifiesUpdated', notifies[0]); await this.core.apis.GroupApi.clearGroupNotifiesUnreadCount(false); if ( ![ @@ -370,177 +365,78 @@ export class NapCatOneBot11Adapter { if (notifyTime < this.bootTime) { continue; } - - const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type; + const flag = notify.seq; this.context.logger.logDebug('收到群通知', notify); - if ( - [ - GroupNotifyMsgType.SET_ADMIN, - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED, - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN, - ].includes(notify.type) + [GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) && + notify.status == GroupNotifyMsgStatus.KUNHANDLE ) { - const member1 = await this.core.apis.GroupApi.getGroupMember( - notify.group.groupCode, - notify.user1.uid - ); - this.context.logger.logDebug('有管理员变动通知'); - // refreshGroupMembers(notify.group.groupCode).then(); - this.context.logger.logDebug('开始获取变动的管理员'); - if (member1) { - this.context.logger.logDebug('变动管理员获取成功'); - const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent( - this.core, - parseInt(notify.group.groupCode), - parseInt(member1.uin), - [ - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED, - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN, - ].includes(notify.type) - ? 'unset' - : 'set' - ); - this.networkManager - .emitEvent(groupAdminNoticeEvent) - .catch((e) => - this.context.logger.logError('处理群管理员变动失败', e) - ); - } else { - this.context.logger.logDebug( - '获取群通知的成员信息失败', - notify, - this.core.apis.GroupApi.getGroup(notify.group.groupCode) - ); - } - } else - // if ( - // notify.type == GroupNotifyMsgType.MEMBER_LEAVE_NOTIFY_ADMIN || - // notify.type == GroupNotifyMsgType.KICK_MEMBER_NOTIFY_ADMIN - // ) { - // this.context.logger.logDebug('有成员退出通知', notify); - // const member1Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid); - // let operatorId = member1Uin; - // let subType: GroupDecreaseSubType = 'leave'; - // if (notify.user2.uid) { - // // 是被踢的 - // const member2Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid); - // if (member2Uin) { - // operatorId = member2Uin; - // } - // subType = 'kick'; - // } - // const groupDecreaseEvent = new OB11GroupDecreaseEvent( - // this.core, - // parseInt(notify.group.groupCode), - // parseInt(member1Uin), - // parseInt(operatorId), - // subType - // ); - // this.networkManager - // .emitEvent(groupDecreaseEvent) - // .catch((e) => - // this.context.logger.logError('处理群成员退出失败', e) - // ); - // // notify.status == 1 表示未处理 2表示处理完成 - // } else - if ( - [GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) && - notify.status == GroupNotifyMsgStatus.KUNHANDLE - ) { - this.context.logger.logDebug('有加群请求'); - try { - let requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid); - if (isNaN(parseInt(requestUin))) { - requestUin = (await this.core.apis.UserApi.getUserDetailInfo(notify.user1.uid)).uin; - } - const groupRequestEvent = new OB11GroupRequestEvent( - this.core, - parseInt(notify.group.groupCode), - parseInt(requestUin), - 'add', - notify.postscript, - flag - ); - this.networkManager - .emitEvent(groupRequestEvent) - .catch((e) => - this.context.logger.logError('处理加群请求失败', e) - ); - } catch (e) { - this.context.logger.logError( - '获取加群人QQ号失败 Uid:', - notify.user1.uid, - e - ); + this.context.logger.logDebug('有加群请求'); + try { + let requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid); + if (isNaN(parseInt(requestUin))) { + requestUin = (await this.core.apis.UserApi.getUserDetailInfo(notify.user1.uid)).uin; } - } else if ( - notify.type == GroupNotifyMsgType.INVITED_BY_MEMBER && - notify.status == GroupNotifyMsgStatus.KUNHANDLE - ) { - this.context.logger.logDebug(`收到邀请我加群通知:${notify}`); - const groupInviteEvent = new OB11GroupRequestEvent( + const groupRequestEvent = new OB11GroupRequestEvent( this.core, parseInt(notify.group.groupCode), - parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid)), - 'invite', - notify.postscript, - flag - ); - this.networkManager - .emitEvent(groupInviteEvent) - .catch((e) => - this.context.logger.logError('处理邀请本人加群失败', e) - ); - } else if ( - notify.type == GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS && - notify.status == GroupNotifyMsgStatus.KUNHANDLE - ) { - this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`); - const groupInviteEvent = new OB11GroupRequestEvent( - this.core, - parseInt(notify.group.groupCode), - parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid)), + parseInt(requestUin), 'add', notify.postscript, flag ); this.networkManager - .emitEvent(groupInviteEvent) + .emitEvent(groupRequestEvent) .catch((e) => - this.context.logger.logError('处理邀请本人加群失败', e) + this.context.logger.logError('处理加群请求失败', e) ); + } catch (e) { + this.context.logger.logError( + '获取加群人QQ号失败 Uid:', + notify.user1.uid, + e + ); } + } else if ( + notify.type == GroupNotifyMsgType.INVITED_BY_MEMBER && + notify.status == GroupNotifyMsgStatus.KUNHANDLE + ) { + this.context.logger.logDebug(`收到邀请我加群通知:${notify}`); + const groupInviteEvent = new OB11GroupRequestEvent( + this.core, + +notify.group.groupCode, + +await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid), + 'invite', + notify.postscript, + flag + ); + this.networkManager + .emitEvent(groupInviteEvent) + .catch((e) => + this.context.logger.logError('处理邀请本人加群失败', e) + ); + } else if ( + notify.type == GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS && + notify.status == GroupNotifyMsgStatus.KUNHANDLE + ) { + this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`); + const groupInviteEvent = new OB11GroupRequestEvent( + this.core, + +notify.group.groupCode, + +await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid), + 'add', + notify.postscript, + flag + ); + this.networkManager + .emitEvent(groupInviteEvent) + .catch((e) => + this.context.logger.logError('处理邀请本人加群失败', e) + ); + } } } }; - - groupListener.onMemberInfoChange = async (groupCode, dataSource, members) => { - //this.context.logger.logDebug('收到群成员信息变动通知', groupCode, changeType); - if (dataSource === DataSource.LOCAL) { - const existMembers = this.core.apis.GroupApi.groupMemberCache.get(groupCode); - if (!existMembers) return; - members.forEach((member) => { - const existMember = existMembers.get(member.uid); - if (!existMember?.isChangeRole) return; - this.context.logger.logDebug('变动管理员获取成功'); - const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent( - this.core, - parseInt(groupCode), - parseInt(member.uin), - member.role === NTGroupMemberRole.KADMIN ? 'set' : 'unset' - ); - this.networkManager - .emitEvent(groupAdminNoticeEvent) - .catch((e) => - this.context.logger.logError('处理群管理员变动失败', e) - ); - existMember.isChangeRole = false; - this.context.logger.logDebug('群管理员变动处理完毕'); - }); - } - }; - this.context.session .getGroupService() .addKernelGroupListener(proxiedListenerOf(groupListener, this.context.logger)); @@ -625,21 +521,27 @@ export class NapCatOneBot11Adapter { // 群名片修改事件解析 任何都该判断 if (message.senderUin && message.senderUin !== '0') { const cardChangedEvent = await this.apis.GroupApi.parseCardChangedEvent(message); - cardChangedEvent && await this.networkManager.emitEvent(cardChangedEvent); + if (cardChangedEvent) { + await this.networkManager.emitEvent(cardChangedEvent); + } } if (message.msgType === NTMsgType.KMSGTYPEFILE) { // 文件为单元素消息 const elementWrapper = message.elements.find(e => !!e.fileElement); if (elementWrapper?.fileElement) { const uploadGroupFileEvent = await this.apis.GroupApi.parseGroupUploadFileEvene(message, elementWrapper.fileElement, elementWrapper); - uploadGroupFileEvent && await this.networkManager.emitEvent(uploadGroupFileEvent); + if (uploadGroupFileEvent) { + await this.networkManager.emitEvent(uploadGroupFileEvent); + } } } else if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) { // 灰条为单元素消息 const grayTipElement = message.elements[0].grayTipElement; if (grayTipElement) { const event = await this.apis.GroupApi.parseGrayTipElement(message, grayTipElement); - event && await this.networkManager.emitEvent(event); + if (event) { + await this.networkManager.emitEvent(event); + } } } } catch (e) { @@ -654,7 +556,10 @@ export class NapCatOneBot11Adapter { const grayTipElement = message.elements[0].grayTipElement; if (grayTipElement) { const event = await this.apis.MsgApi.parsePrivateMsgEvent(message, grayTipElement); - event && await this.networkManager.emitEvent(event); + if (event) { + await this.networkManager.emitEvent(event); + } + } } } catch (e) { @@ -674,6 +579,8 @@ export class NapCatOneBot11Adapter { } private async emitFriendRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) { + const operatorUid = element.grayTipElement?.revokeElement.operatorUid; + if (!operatorUid) return undefined; return new OB11FriendRecallNoticeEvent( this.core, +message.senderUin, @@ -684,7 +591,7 @@ export class NapCatOneBot11Adapter { private async emitGroupRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) { const operatorUid = element.grayTipElement?.revokeElement.operatorUid; if (!operatorUid) return undefined; - const operatorId = message.senderUin ?? await this.core.apis.UserApi.getUinByUidV2(operatorUid); + const operatorId = await this.core.apis.UserApi.getUinByUidV2(operatorUid); return new OB11GroupRecallNoticeEvent( this.core, +message.peerUin, diff --git a/src/onebot/types/data.ts b/src/onebot/types/data.ts index 3039ba0b..8b3acab2 100644 --- a/src/onebot/types/data.ts +++ b/src/onebot/types/data.ts @@ -11,6 +11,17 @@ export interface OB11User { categoryName?: string; // 分组名称 categoryId?: number; // 分组ID 999为特别关心 } +export interface Notify { + request_id: number; + invitor_uin: number; + invitor_nick?: string; + group_id?: number; + group_name?: string; + message?: string; + checked: boolean; + actor: number; + requester_nick?: string; +} export enum OB11UserSex { male = 'male', // 男性 diff --git a/src/onebot/types/message.ts b/src/onebot/types/message.ts index 12a7aa9e..645f0b5f 100644 --- a/src/onebot/types/message.ts +++ b/src/onebot/types/message.ts @@ -71,6 +71,14 @@ export enum OB11MessageDataType { location = 'location' } +export interface OB11MessagePoke { + type: OB11MessageDataType.poke; + data: { + type: string; + id: string; + }; +} + // 商城表情消息接口定义 export interface OB11MessageMFace { type: OB11MessageDataType.mface; @@ -247,7 +255,7 @@ export type OB11MessageData = OB11MessageAt | OB11MessageReply | OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo | OB11MessageNode | OB11MessageIdMusic | OB11MessageCustomMusic | OB11MessageJson | - OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContext; + OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContext | OB11MessagePoke; // 发送消息接口定义 export interface OB11PostSendMsg { diff --git a/src/webui/src/api/Log.ts b/src/webui/src/api/Log.ts index c4037169..28dde732 100644 --- a/src/webui/src/api/Log.ts +++ b/src/webui/src/api/Log.ts @@ -9,13 +9,13 @@ export const LogHandler: RequestHandler = async (req, res) => { if (filename.includes('..')) { return sendError(res, 'ID不合法'); } - const logContent = WebUiConfigWrapper.GetLogContent(filename); + const logContent = await WebUiConfigWrapper.GetLogContent(filename); return sendSuccess(res, logContent); }; // 日志列表 export const LogListHandler: RequestHandler = async (_, res) => { - const logList = WebUiConfigWrapper.GetLogsList(); + const logList = await WebUiConfigWrapper.GetLogsList(); return sendSuccess(res, logList); }; // 实时日志(SSE) diff --git a/src/webui/src/helper/config.ts b/src/webui/src/helper/config.ts index 9c1d7413..6f74e93e 100644 --- a/src/webui/src/helper/config.ts +++ b/src/webui/src/helper/config.ts @@ -1,5 +1,5 @@ import { webUiPathWrapper } from '@/webui'; -import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'; +import fs, { constants } from 'node:fs/promises'; import * as net from 'node:net'; import { resolve } from 'node:path'; @@ -90,18 +90,18 @@ export class WebUiConfigWrapper { try { const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); - if (!existsSync(configPath)) { - writeFileSync(configPath, JSON.stringify(defaultconfig, null, 4)); + if (!await fs.access(configPath, constants.R_OK | constants.W_OK).then(() => true).catch(() => false)) { + await fs.writeFile(configPath, JSON.stringify(defaultconfig, null, 4)); } - const fileContent = readFileSync(configPath, 'utf-8'); + const fileContent = await fs.readFile(configPath, 'utf-8'); // 更新配置字段后新增字段可能会缺失,同步一下 const parsedConfig = this.applyDefaults(JSON.parse(fileContent) as Partial, defaultconfig); if (!parsedConfig.prefix.startsWith('/')) parsedConfig.prefix = '/' + parsedConfig.prefix; if (parsedConfig.prefix.endsWith('/')) parsedConfig.prefix = parsedConfig.prefix.slice(0, -1); // 配置已经被操作过了,还是回写一下吧,不然新配置不会出现在配置文件里 - writeFileSync(configPath, JSON.stringify(parsedConfig, null, 4)); + await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4)); // 不希望回写的配置放后面 // 查询主机地址是否可用 @@ -137,19 +137,19 @@ export class WebUiConfigWrapper { return resolve(webUiPathWrapper.logsPath); } // 获取日志列表 - public static GetLogsList(): string[] { - if (existsSync(webUiPathWrapper.logsPath)) { - return readdirSync(webUiPathWrapper.logsPath) + public static async GetLogsList(): Promise { + if (await fs.access(webUiPathWrapper.logsPath, constants.F_OK).then(() => true).catch(() => false)) { + return (await fs.readdir(webUiPathWrapper.logsPath)) .filter((file) => file.endsWith('.log')) .map((file) => file.replace('.log', '')); } return []; } // 获取指定日志文件内容 - public static GetLogContent(filename: string): string { + public static async GetLogContent(filename: string): Promise { const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`); - if (existsSync(logPath)) { - return readFileSync(logPath, 'utf-8'); + if (await fs.access(logPath, constants.R_OK).then(() => true).catch(() => false)) { + return await fs.readFile(logPath, 'utf-8'); } return ''; }