mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b9ec8ac9b1 | ||
![]() |
28d973b9cb | ||
![]() |
a6be54937c | ||
![]() |
0664b9af84 | ||
![]() |
407d8d1fd2 | ||
![]() |
108897f6ad | ||
![]() |
3d2decb0ec | ||
![]() |
386b884f1b | ||
![]() |
ace4da2297 | ||
![]() |
a8fb48fb50 | ||
![]() |
61f065c0c6 | ||
![]() |
d6cf6d120a | ||
![]() |
c20c19d8e0 | ||
![]() |
bd8bbf76ab | ||
![]() |
faccff1834 | ||
![]() |
99d3c5a117 | ||
![]() |
31eb09edef | ||
![]() |
4180c2d754 | ||
![]() |
68f5deedff | ||
![]() |
9f72196414 | ||
![]() |
b32efa9131 | ||
![]() |
3cf502fea3 | ||
![]() |
863a953ae1 | ||
![]() |
a44104d8f7 | ||
![]() |
f602bbb0cf | ||
![]() |
2807ff5927 | ||
![]() |
4fb8e6a4da | ||
![]() |
7ec61f089d | ||
![]() |
f7a500a8cf | ||
![]() |
2b319bd694 |
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
Binary file not shown.
1
launcher/ReadMe.txt
Normal file
1
launcher/ReadMe.txt
Normal file
@@ -0,0 +1 @@
|
||||
带有34231数字的是指QQ 9.9.19-34231 适配的启动脚本
|
32
launcher/launcher-user-34231.bat
Normal file
32
launcher/launcher-user-34231.bat
Normal file
@@ -0,0 +1,32 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook_34231.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
pause
|
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.7.12",
|
||||
"version": "4.7.16",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.7.12",
|
||||
"version": "4.7.16",
|
||||
"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",
|
||||
@@ -57,9 +57,9 @@
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-plugin-cp": "^6.0.0",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"napcat.protobuf": "^1.1.3",
|
||||
"napcat.protobuf": "^1.1.4",
|
||||
"winston": "^3.17.0",
|
||||
"compressing": "^1.10.1"
|
||||
},
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.7.12';
|
||||
export const napCatVersion = '4.7.16';
|
||||
|
@@ -41,8 +41,8 @@ export class NTQQFileApi {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.rkeyManager = new RkeyManager([
|
||||
'https://ss.xingzhige.com/music_card/rkey', // 国内
|
||||
'https://secret-service.bietiaop.com/rkeys',//国内
|
||||
'https://secret-service.bietiaop.com/rkeys',
|
||||
'http://ss.xingzhige.com/music_card/rkey',
|
||||
],
|
||||
this.context.logger
|
||||
);
|
||||
@@ -231,7 +231,7 @@ export class NTQQFileApi {
|
||||
},
|
||||
};
|
||||
}
|
||||
async createValidSendPttElement(context: SendMessageContext, pttPath: string): Promise<SendPttElement> {
|
||||
async createValidSendPttElement(_context: SendMessageContext, pttPath: string): Promise<SendPttElement> {
|
||||
|
||||
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
|
||||
if (!silkPath) {
|
||||
|
@@ -218,6 +218,10 @@ export class NTQQGroupApi {
|
||||
return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
|
||||
}
|
||||
|
||||
async transGroupFile(groupCode: string, fileId: string) {
|
||||
return this.context.session.getRichMediaService().transGroupFile(groupCode, fileId);
|
||||
}
|
||||
|
||||
async addGroupEssence(groupCode: string, msgId: string) {
|
||||
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||
chatType: 2,
|
||||
|
@@ -12,7 +12,7 @@ export class NTQQMsgApi {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
|
||||
async clickInlineKeyboardButton(...params: Parameters<NodeIKernelMsgService['clickInlineKeyboardButton']>) {
|
||||
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
|
||||
}
|
||||
|
8
src/core/external/appid.json
vendored
8
src/core/external/appid.json
vendored
@@ -234,5 +234,13 @@
|
||||
"3.2.16-33800": {
|
||||
"appid": 537274009,
|
||||
"qua": "V1_LNX_NQ_3.2.16_33800_GW_B"
|
||||
},
|
||||
"9.9.19-34231": {
|
||||
"appid": 537279209,
|
||||
"qua": "V1_WIN_NQ_9.9.19_34231_GW_B"
|
||||
},
|
||||
"3.2.17-34231": {
|
||||
"appid": 537279245,
|
||||
"qua": "V1_LNX_NQ_3.2.17_34231_GW_B"
|
||||
}
|
||||
}
|
12
src/core/external/offset.json
vendored
12
src/core/external/offset.json
vendored
@@ -314,5 +314,17 @@
|
||||
"3.2.16-33800-arm64": {
|
||||
"send": "7262BB0",
|
||||
"recv": "72664E0"
|
||||
},
|
||||
"9.9.19-34231-x64": {
|
||||
"send": "3BD73D0",
|
||||
"recv": "3BDBBD0"
|
||||
},
|
||||
"3.2.17-34231-x64": {
|
||||
"send": "AD787E0",
|
||||
"recv": "AD7C200"
|
||||
},
|
||||
"3.2.17-34231-arm64": {
|
||||
"send": "770CDC0",
|
||||
"recv": "77106F0"
|
||||
}
|
||||
}
|
@@ -6,6 +6,17 @@ interface ServerRkeyData {
|
||||
private_rkey: string;
|
||||
expired_time: number;
|
||||
}
|
||||
interface OneBotApiRet {
|
||||
status: string,
|
||||
retcode: number,
|
||||
data: ServerRkeyData,
|
||||
message: string,
|
||||
wording: string,
|
||||
}
|
||||
interface UrlFailureInfo {
|
||||
count: number;
|
||||
lastTimestamp: number;
|
||||
}
|
||||
|
||||
export class RkeyManager {
|
||||
serverUrl: string[] = [];
|
||||
@@ -15,9 +26,8 @@ export class RkeyManager {
|
||||
private_rkey: '',
|
||||
expired_time: 0,
|
||||
};
|
||||
private failureCount: number = 0;
|
||||
private lastFailureTimestamp: number = 0;
|
||||
private readonly FAILURE_LIMIT: number = 8;
|
||||
private urlFailures: Map<string, UrlFailureInfo> = new Map();
|
||||
private readonly FAILURE_LIMIT: number = 4;
|
||||
private readonly ONE_DAY: number = 24 * 60 * 60 * 1000;
|
||||
|
||||
constructor(serverUrl: string[], logger: LogWrapper) {
|
||||
@@ -26,50 +36,92 @@ export class RkeyManager {
|
||||
}
|
||||
|
||||
async getRkey() {
|
||||
const now = new Date().getTime();
|
||||
if (now - this.lastFailureTimestamp > this.ONE_DAY) {
|
||||
this.failureCount = 0; // 重置失败计数器
|
||||
}
|
||||
|
||||
if (this.failureCount >= this.FAILURE_LIMIT) {
|
||||
this.logger.logError('[Rkey] 服务存在异常, 图片使用FallBack机制');
|
||||
throw new Error('获取rkey失败次数过多,请稍后再试');
|
||||
const availableUrls = this.getAvailableUrls();
|
||||
if (availableUrls.length === 0) {
|
||||
this.logger.logError('[Rkey] 所有服务均已禁用, 图片使用FallBack机制');
|
||||
throw new Error('获取rkey失败:所有服务URL均已被禁用');
|
||||
}
|
||||
|
||||
if (this.isExpired()) {
|
||||
try {
|
||||
await this.refreshRkey();
|
||||
} catch (e) {
|
||||
throw new Error(`${e}`);//外抛
|
||||
throw new Error(`${e}`);
|
||||
}
|
||||
}
|
||||
return this.rkeyData;
|
||||
}
|
||||
|
||||
private getAvailableUrls(): string[] {
|
||||
return this.serverUrl.filter(url => !this.isUrlDisabled(url));
|
||||
}
|
||||
|
||||
private isUrlDisabled(url: string): boolean {
|
||||
const failureInfo = this.urlFailures.get(url);
|
||||
if (!failureInfo) return false;
|
||||
|
||||
const now = new Date().getTime();
|
||||
// 如果已经过了一天,重置失败计数
|
||||
if (now - failureInfo.lastTimestamp > this.ONE_DAY) {
|
||||
failureInfo.count = 0;
|
||||
this.urlFailures.set(url, failureInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
return failureInfo.count >= this.FAILURE_LIMIT;
|
||||
}
|
||||
|
||||
private updateUrlFailure(url: string) {
|
||||
const now = new Date().getTime();
|
||||
const failureInfo = this.urlFailures.get(url) || { count: 0, lastTimestamp: 0 };
|
||||
|
||||
// 如果已经过了一天,重置失败计数
|
||||
if (now - failureInfo.lastTimestamp > this.ONE_DAY) {
|
||||
failureInfo.count = 1;
|
||||
} else {
|
||||
failureInfo.count++;
|
||||
}
|
||||
|
||||
failureInfo.lastTimestamp = now;
|
||||
this.urlFailures.set(url, failureInfo);
|
||||
|
||||
if (failureInfo.count >= this.FAILURE_LIMIT) {
|
||||
this.logger.logError(`[Rkey] URL ${url} 已被禁用,失败次数达到 ${this.FAILURE_LIMIT} 次`);
|
||||
}
|
||||
}
|
||||
|
||||
isExpired(): boolean {
|
||||
const now = new Date().getTime() / 1000;
|
||||
return now > this.rkeyData.expired_time;
|
||||
}
|
||||
|
||||
async refreshRkey() {
|
||||
//刷新rkey
|
||||
for (const url of this.serverUrl) {
|
||||
const availableUrls = this.getAvailableUrls();
|
||||
|
||||
if (availableUrls.length === 0) {
|
||||
this.logger.logError('[Rkey] 所有服务均已禁用');
|
||||
throw new Error('获取rkey失败:所有服务URL均已被禁用');
|
||||
}
|
||||
|
||||
for (const url of availableUrls) {
|
||||
try {
|
||||
const temp = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
|
||||
let temp = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
|
||||
if ('retcode' in temp) {
|
||||
// 支持Onebot Ret风格
|
||||
temp = (temp as unknown as OneBotApiRet).data;
|
||||
}
|
||||
this.rkeyData = {
|
||||
group_rkey: temp.group_rkey.slice(6),
|
||||
private_rkey: temp.private_rkey.slice(6),
|
||||
expired_time: temp.expired_time
|
||||
};
|
||||
this.failureCount = 0;
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logger.logError(`[Rkey] 异常服务 ${url} 异常 / `, e);
|
||||
this.failureCount++;
|
||||
this.lastFailureTimestamp = new Date().getTime();
|
||||
//是否为最后一个url
|
||||
if (url === this.serverUrl[this.serverUrl.length - 1]) {
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
this.updateUrlFailure(url);
|
||||
|
||||
if (url === availableUrls[availableUrls.length - 1]) {
|
||||
throw new Error(`获取rkey失败: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -68,8 +68,8 @@ export class PacketOperationContext {
|
||||
}
|
||||
}
|
||||
|
||||
async SetGroupSpecialTitle(groupUin: number, uid: string, tittle: string) {
|
||||
const req = trans.SetSpecialTitle.build(groupUin, uid, tittle);
|
||||
async SetGroupSpecialTitle(groupUin: number, uid: string, title: string) {
|
||||
const req = trans.SetSpecialTitle.build(groupUin, uid, title);
|
||||
await this.context.client.sendOidbPacket(req);
|
||||
}
|
||||
|
||||
@@ -154,6 +154,20 @@ export class PacketOperationContext {
|
||||
return res.result.resId;
|
||||
}
|
||||
|
||||
async MoveGroupFile(groupUin: number, fileUUID: string, currentParentDirectory: string, targetParentDirectory: string) {
|
||||
const req = trans.MoveGroupFile.build(groupUin, fileUUID, currentParentDirectory, targetParentDirectory);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = trans.MoveGroupFile.parse(resp);
|
||||
return res.move.retCode;
|
||||
}
|
||||
|
||||
async RenameGroupFile(groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string) {
|
||||
const req = trans.RenameGroupFile.build(groupUin, fileUUID, currentParentDirectory, newName);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = trans.RenameGroupFile.parse(resp);
|
||||
return res.rename.retCode;
|
||||
}
|
||||
|
||||
async GetGroupFileUrl(groupUin: number, fileUUID: string) {
|
||||
const req = trans.DownloadGroupFile.build(groupUin, fileUUID);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
|
35
src/core/packet/transformer/action/MoveGroupFile.ts
Normal file
35
src/core/packet/transformer/action/MoveGroupFile.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
class MoveGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6Response> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
build(groupUin: number, fileUUID: string, currentParentDirectory: string, targetParentDirectory: string): OidbPacket {
|
||||
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({
|
||||
move: {
|
||||
groupUin: groupUin,
|
||||
appId: 5,
|
||||
busId: 102,
|
||||
fileId: fileUUID,
|
||||
parentDirectory: currentParentDirectory,
|
||||
targetDirectory: targetParentDirectory,
|
||||
}
|
||||
});
|
||||
return OidbBase.build(0x6D6, 5, body, true, false);
|
||||
}
|
||||
|
||||
parse(data: Buffer) {
|
||||
const oidbBody = OidbBase.parse(data).body;
|
||||
const res = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
|
||||
if (res.move.retCode !== 0) {
|
||||
throw new Error(`sendGroupFileMoveReq error: ${res.move.clientWording} (code=${res.move.retCode})`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export default new MoveGroupFile();
|
34
src/core/packet/transformer/action/RenameGroupFile.ts
Normal file
34
src/core/packet/transformer/action/RenameGroupFile.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as proto from '@/core/packet/transformer/proto';
|
||||
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||
|
||||
class RenameGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6Response> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
build(groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string): OidbPacket {
|
||||
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({
|
||||
rename: {
|
||||
groupUin: groupUin,
|
||||
busId: 102,
|
||||
fileId: fileUUID,
|
||||
parentFolder: currentParentDirectory,
|
||||
newFileName: newName,
|
||||
}
|
||||
});
|
||||
return OidbBase.build(0x6D6, 4, body, true, false);
|
||||
}
|
||||
|
||||
parse(data: Buffer) {
|
||||
const oidbBody = OidbBase.parse(data).body;
|
||||
const res = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
|
||||
if (res.rename.retCode !== 0) {
|
||||
throw new Error(`sendGroupFileRenameReq error: ${res.rename.clientWording} (code=${res.rename.retCode})`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export default new RenameGroupFile();
|
@@ -8,14 +8,14 @@ class SetSpecialTitle extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase>
|
||||
super();
|
||||
}
|
||||
|
||||
build(groupCode: number, uid: string, tittle: string): OidbPacket {
|
||||
build(groupCode: number, uid: string, title: string): OidbPacket {
|
||||
const oidb_0x8FC_2 = new NapProtoMsg(proto.OidbSvcTrpcTcp0X8FC_2).encode({
|
||||
groupUin: +groupCode,
|
||||
body: {
|
||||
targetUid: uid,
|
||||
specialTitle: tittle,
|
||||
specialTitle: title,
|
||||
expiredTime: -1,
|
||||
uinName: tittle
|
||||
uinName: title
|
||||
}
|
||||
});
|
||||
return OidbBase.build(0x8FC, 2, oidb_0x8FC_2, false, false);
|
||||
|
@@ -6,3 +6,5 @@ export { default as GetStrangerInfo } from './GetStrangerInfo';
|
||||
export { default as SendPoke } from './SendPoke';
|
||||
export { default as SetSpecialTitle } from './SetSpecialTitle';
|
||||
export { default as ImageOCR } from './ImageOCR';
|
||||
export { default as MoveGroupFile } from './MoveGroupFile';
|
||||
export { default as RenameGroupFile } from './RenameGroupFile';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { AnyCnameRecord } from 'node:dns';
|
||||
import { BizKey, ModifyProfileParams, NodeIKernelProfileListener, ProfileBizType, SimpleInfo, UserDetailInfoByUin, UserDetailSource } from '@/core';
|
||||
import { BizKey, ModifyProfileParams, NodeIKernelProfileListener, ProfileBizType, SimpleInfo, UserDetailInfoByUin, UserDetailInfoListenerArg, UserDetailSource } from '@/core';
|
||||
import { GeneralCallResult } from '@/core/services/common';
|
||||
|
||||
export interface NodeIKernelProfileService {
|
||||
@@ -15,7 +15,13 @@ export interface NodeIKernelProfileService {
|
||||
|
||||
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>;
|
||||
|
||||
fetchUserDetailInfo(trace: string, uids: string[], source: UserDetailSource, bizType: ProfileBizType[]): Promise<GeneralCallResult>;
|
||||
fetchUserDetailInfo(trace: string, uids: string[], source: UserDetailSource, bizType: ProfileBizType[]): Promise<GeneralCallResult &
|
||||
{
|
||||
source: UserDetailSource,
|
||||
// uid -> detail
|
||||
detail: Map<string, UserDetailInfoListenerArg>,
|
||||
}
|
||||
>;
|
||||
|
||||
addKernelProfileListener(listener: NodeIKernelProfileListener): number;
|
||||
|
||||
|
@@ -198,9 +198,29 @@ export interface NodeIKernelRichMediaService {
|
||||
|
||||
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
|
||||
|
||||
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
|
||||
moveGroupFile(groupCode: string, busId: Array<number>, fileList: Array<string>, currentParentDirectory: string, targetParentDirectory: string): Promise<GeneralCallResult & {
|
||||
moveGroupFileResult: {
|
||||
result: {
|
||||
retCode: number,
|
||||
retMsg: symbol,
|
||||
clientWording: string
|
||||
},
|
||||
successFileIdList: Array<string>,
|
||||
failFileIdList: Array<string>
|
||||
}
|
||||
}>;
|
||||
|
||||
transGroupFile(arg1: unknown, arg2: unknown): unknown;
|
||||
transGroupFile(groupCode: string, fileId: string): Promise<GeneralCallResult & {
|
||||
transGroupFileResult: {
|
||||
result: {
|
||||
retCode: number
|
||||
retMsg: string
|
||||
clientWording: string
|
||||
}
|
||||
saveBusId: number
|
||||
saveFilePath: string
|
||||
}
|
||||
}>;
|
||||
|
||||
searchGroupFile(
|
||||
keywords: Array<string>,
|
||||
|
@@ -3,6 +3,7 @@ import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
|
||||
import { NapCatCore } from '@/core';
|
||||
import { NapCatOneBot11Adapter, OB11Return } from '@/onebot';
|
||||
import { NetworkAdapterConfig } from '../config/config';
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
|
||||
export class OB11Response {
|
||||
private static createResponse<T>(data: T, status: string, retcode: number, message: string = '', echo: unknown = null): OB11Return<T> {
|
||||
@@ -33,7 +34,7 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
|
||||
core: NapCatCore;
|
||||
private validate?: ValidateFunction<unknown> = undefined;
|
||||
payloadSchema?: unknown = undefined;
|
||||
payloadSchema?: TSchema = undefined;
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
|
||||
constructor(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||
@@ -43,7 +44,7 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||
|
||||
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
|
||||
if (this.payloadSchema) {
|
||||
this.validate = new Ajv({ allowUnionTypes: true, useDefaults: true }).compile(this.payloadSchema);
|
||||
this.validate = new Ajv({ allowUnionTypes: true, useDefaults: true, coerceTypes: true }).compile(this.payloadSchema);
|
||||
}
|
||||
if (this.validate && !this.validate(payload)) {
|
||||
const errors = this.validate.errors as ErrorObject[];
|
||||
|
33
src/onebot/action/extends/MoveGroupFile.ts
Normal file
33
src/onebot/action/extends/MoveGroupFile.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||
file_id: Type.String(),
|
||||
current_parent_directory: Type.String(),
|
||||
target_parent_directory: Type.String(),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
interface MoveGroupFileResponse {
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export class MoveGroupFile extends GetPacketStatusDepends<Payload, MoveGroupFileResponse> {
|
||||
override actionName = ActionName.MoveGroupFile;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||
if (contextMsgFile?.fileUUID) {
|
||||
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
throw new Error('real fileUUID not found!');
|
||||
}
|
||||
}
|
33
src/onebot/action/extends/RenameGroupFile.ts
Normal file
33
src/onebot/action/extends/RenameGroupFile.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||
file_id: Type.String(),
|
||||
current_parent_directory: Type.String(),
|
||||
new_name: Type.String(),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
interface RenameGroupFileResponse {
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export class RenameGroupFile extends GetPacketStatusDepends<Payload, RenameGroupFileResponse> {
|
||||
override actionName = ActionName.RenameGroupFile;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||
if (contextMsgFile?.fileUUID) {
|
||||
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
throw new Error('real fileUUID not found!');
|
||||
}
|
||||
}
|
@@ -10,8 +10,8 @@ const SchemaData = Type.Object({
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
export class SetSpecialTittle extends GetPacketStatusDepends<Payload, void> {
|
||||
override actionName = ActionName.SetSpecialTittle;
|
||||
export class SetSpecialTitle extends GetPacketStatusDepends<Payload, void> {
|
||||
override actionName = ActionName.SetSpecialTitle;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
34
src/onebot/action/extends/TransGroupFile.ts
Normal file
34
src/onebot/action/extends/TransGroupFile.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||
file_id: Type.String(),
|
||||
});
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
interface TransGroupFileResponse {
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export class TransGroupFile extends GetPacketStatusDepends<Payload, TransGroupFileResponse> {
|
||||
override actionName = ActionName.TransGroupFile;
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||
if (contextMsgFile?.fileUUID) {
|
||||
const result = await this.core.apis.GroupApi.transGroupFile(payload.group_id.toString(), contextMsgFile.fileUUID);
|
||||
if (result.transGroupFileResult.result.retCode === 0) {
|
||||
return {
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
throw new Error(result.transGroupFileResult.result.retMsg);
|
||||
}
|
||||
throw new Error('real fileUUID not found!');
|
||||
}
|
||||
}
|
@@ -20,6 +20,7 @@ class GetGroupInfo extends OneBotAction<Payload, OB11Group> {
|
||||
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
|
||||
return {
|
||||
...data,
|
||||
group_all_shut: data.shutUpAllTimestamp > 0 ? -1 : 0,
|
||||
group_remark: '',
|
||||
group_id: +payload.group_id,
|
||||
group_name: data.groupName,
|
||||
|
@@ -81,7 +81,7 @@ import { GetGroupSystemMsg } from './system/GetSystemMsg';
|
||||
import { GroupPoke } from './group/GroupPoke';
|
||||
import { GetUserStatus } from './extends/GetUserStatus';
|
||||
import { GetRkey } from './extends/GetRkey';
|
||||
import { SetSpecialTittle } from './extends/SetSpecialTittle';
|
||||
import { SetSpecialTitle } from './extends/SetSpecialTitle';
|
||||
import { GetGroupShutList } from './group/GetGroupShutList';
|
||||
import { GetGroupMemberList } from './group/GetGroupMemberList';
|
||||
import { GetGroupFileUrl } from '@/onebot/action/file/GetGroupFileUrl';
|
||||
@@ -109,10 +109,17 @@ import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
|
||||
import { GetPrivateFileUrl } from './file/GetPrivateFileUrl';
|
||||
import { GetUnidirectionalFriendList } from './extends/GetUnidirectionalFriendList';
|
||||
import SetGroupRemark from './extends/SetGroupRemark';
|
||||
import { MoveGroupFile } from './extends/MoveGroupFile';
|
||||
import { TransGroupFile } from './extends/TransGroupFile';
|
||||
import { RenameGroupFile } from './extends/RenameGroupFile';
|
||||
import { GetRkeyServer } from './packet/GetRkeyServer';
|
||||
import { GetRkeyEx } from './packet/GetRkeyEx';
|
||||
|
||||
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||
|
||||
const actionHandlers = [
|
||||
new GetRkeyEx(obContext, core),
|
||||
new GetRkeyServer(obContext, core),
|
||||
new SetGroupRemark(obContext, core),
|
||||
new GetGroupInfoEx(obContext, core),
|
||||
new FetchEmojiLike(obContext, core),
|
||||
@@ -132,6 +139,9 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
new SetGroupSign(obContext, core),
|
||||
new SendGroupSign(obContext, core),
|
||||
new GetClientkey(obContext, core),
|
||||
new MoveGroupFile(obContext, core),
|
||||
new RenameGroupFile(obContext, core),
|
||||
new TransGroupFile(obContext, core),
|
||||
// onebot11
|
||||
new SendLike(obContext, core),
|
||||
new GetMsg(obContext, core),
|
||||
@@ -215,7 +225,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
new FriendPoke(obContext, core),
|
||||
new GetUserStatus(obContext, core),
|
||||
new GetRkey(obContext, core),
|
||||
new SetSpecialTittle(obContext, core),
|
||||
new SetSpecialTitle(obContext, core),
|
||||
new SetDiyOnlineStatus(obContext, core),
|
||||
// new UploadForwardMsg(obContext, core),
|
||||
new GetGroupShutList(obContext, core),
|
||||
|
18
src/onebot/action/packet/GetRkeyEx.ts
Normal file
18
src/onebot/action/packet/GetRkeyEx.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
||||
|
||||
export class GetRkeyEx extends GetPacketStatusDepends<void, unknown> {
|
||||
override actionName = ActionName.GetRkeyEx;
|
||||
|
||||
async _handle() {
|
||||
let rkeys = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
||||
return rkeys.map(rkey => {
|
||||
return {
|
||||
type: rkey.type === 10 ? "private" : "group",
|
||||
rkey: rkey.rkey,
|
||||
created_at: rkey.time,
|
||||
ttl: rkey.ttl,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
38
src/onebot/action/packet/GetRkeyServer.ts
Normal file
38
src/onebot/action/packet/GetRkeyServer.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ActionName } from '@/onebot/action/router';
|
||||
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
|
||||
|
||||
export class GetRkeyServer extends GetPacketStatusDepends<void, { private_rkey?: string; group_rkey?: string; expired_time?: number; name: string }> {
|
||||
override actionName = ActionName.GetRkeyServer;
|
||||
|
||||
private rkeyCache: {
|
||||
private_rkey?: string;
|
||||
group_rkey?: string;
|
||||
expired_time?: number;
|
||||
name: string;
|
||||
} | null = null;
|
||||
private expiryTime: number | null = null;
|
||||
|
||||
async _handle() {
|
||||
// 检查缓存是否有效
|
||||
if (this.expiryTime && this.expiryTime > Math.floor(Date.now() / 1000) && this.rkeyCache) {
|
||||
return this.rkeyCache;
|
||||
}
|
||||
|
||||
// 获取新的 Rkey
|
||||
let rkeys = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
||||
let privateRkeyItem = rkeys.filter(rkey => rkey.type === 10)[0];
|
||||
let groupRkeyItem = rkeys.filter(rkey => rkey.type === 20)[0];
|
||||
|
||||
this.expiryTime = Math.floor(Date.now() / 1000) + Math.min(+groupRkeyItem!.ttl.toString(),+privateRkeyItem!.ttl.toString());
|
||||
|
||||
// 更新缓存
|
||||
this.rkeyCache = {
|
||||
private_rkey: privateRkeyItem ? privateRkeyItem.rkey : undefined,
|
||||
group_rkey: groupRkeyItem ? groupRkeyItem.rkey : undefined,
|
||||
expired_time: this.expiryTime,
|
||||
name: "NapCat 4"
|
||||
};
|
||||
|
||||
return this.rkeyCache;
|
||||
}
|
||||
}
|
@@ -10,6 +10,8 @@ export interface InvalidCheckResult {
|
||||
}
|
||||
|
||||
export const ActionName = {
|
||||
GetRkeyEx: 'get_rkey',
|
||||
GetRkeyServer: 'get_rkey_server',
|
||||
SetGroupRemark: 'set_group_remark',
|
||||
NapCat_GetPrivateFileUrl: 'get_private_file_url',
|
||||
ClickInlineKeyboardButton: 'click_inline_keyboard_button',
|
||||
@@ -31,7 +33,7 @@ export const ActionName = {
|
||||
SetGroupCard: 'set_group_card',
|
||||
SetGroupName: 'set_group_name',
|
||||
SetGroupLeave: 'set_group_leave',
|
||||
SetSpecialTittle: 'set_group_special_title',
|
||||
SetSpecialTitle: 'set_group_special_title',
|
||||
SetFriendAddRequest: 'set_friend_add_request',
|
||||
SetGroupAddRequest: 'set_group_add_request',
|
||||
GetLoginInfo: 'get_login_info',
|
||||
@@ -130,6 +132,10 @@ export const ActionName = {
|
||||
GetRkey: 'nc_get_rkey',
|
||||
GetGroupShutList: 'get_group_shut_list',
|
||||
|
||||
MoveGroupFile: 'move_group_file',
|
||||
TransGroupFile: 'trans_group_file',
|
||||
RenameGroupFile: 'rename_group_file',
|
||||
|
||||
GetGuildList: 'get_guild_list',
|
||||
GetGuildProfile: 'get_guild_service_profile',
|
||||
|
||||
|
@@ -151,14 +151,15 @@ export class OneBotGroupApi {
|
||||
async parseOtherJsonEvent(msg: RawMessage, jsonStr: string, context: InstanceContext) {
|
||||
const json = JSON.parse(jsonStr);
|
||||
const type = json.items[json.items.length - 1]?.txt;
|
||||
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
|
||||
if (type === '头衔') {
|
||||
const memberUin = json.items[1].param[0];
|
||||
const title = json.items[3].txt;
|
||||
context.logger.logDebug('收到群成员新头衔消息', json);
|
||||
return new OB11GroupTitleEvent(
|
||||
this.core,
|
||||
parseInt(msg.peerUid),
|
||||
parseInt(memberUin),
|
||||
+msg.peerUid,
|
||||
+memberUin,
|
||||
title,
|
||||
);
|
||||
} else if (type === '移出') {
|
||||
|
@@ -373,7 +373,8 @@ export class OneBotMsgApi {
|
||||
try {
|
||||
multiMsgs = await this.core.apis.PacketApi.pkt.operation.FetchForwardMsg(element.resId);
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('Protocol FetchForwardMsg fallback failed!', e);
|
||||
this.core.context.logger.logError(`Protocol FetchForwardMsg fallback failed!
|
||||
element = ${JSON.stringify(element)} , error=${e})`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -713,6 +714,56 @@ export class OneBotMsgApi {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
}
|
||||
/**
|
||||
* 解析带有JSON标记的文本
|
||||
* @param text 要解析的文本
|
||||
* @returns 解析后的结果数组,每个元素包含类型(text或json)和内容
|
||||
*/
|
||||
parseTextWithJson(text: string) {
|
||||
// 匹配<{...}>格式的JSON
|
||||
const regex = /<(\{.*?\})>/g;
|
||||
const parts: Array<{ type: 'text' | 'json', content: string | object }> = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
// 查找所有匹配项
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
// 添加匹配前的文本
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
content: text.substring(lastIndex, match.index)
|
||||
});
|
||||
}
|
||||
|
||||
// 添加JSON部分
|
||||
try {
|
||||
const jsonContent = JSON.parse(match[1] ?? '');
|
||||
parts.push({
|
||||
type: 'json',
|
||||
content: jsonContent
|
||||
});
|
||||
} catch (e) {
|
||||
// 如果JSON解析失败,作为普通文本处理
|
||||
parts.push({
|
||||
type: 'text',
|
||||
content: match[0]
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
// 添加最后一部分文本
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
content: text.substring(lastIndex)
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
async parsePrivateMsgEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
|
||||
@@ -1214,6 +1265,41 @@ export class OneBotMsgApi {
|
||||
} else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
|
||||
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
|
||||
}
|
||||
// else if (SysMessage.contentHead.type == 732 && SysMessage.contentHead.subType == 16 && SysMessage.body?.msgContent) {
|
||||
// let data_wrap = PBString(2);
|
||||
// let user_wrap = PBUint64(5);
|
||||
// let group_wrap = PBUint64(4);
|
||||
|
||||
// ProtoBuf(class extends ProtoBufBase {
|
||||
// group = group_wrap;
|
||||
// content = ProtoBufIn(5, { data: data_wrap, user: user_wrap });
|
||||
// }).decode(SysMessage.body?.msgContent.slice(7));
|
||||
// let xml_data = UnWrap(data_wrap);
|
||||
// let group = UnWrap(group_wrap).toString();
|
||||
// //let user = UnWrap(user_wrap).toString();
|
||||
// const parsedParts = this.parseTextWithJson(xml_data);
|
||||
// //解析JSON
|
||||
// if (parsedParts[1] && parsedParts[3]) {
|
||||
// let set_user_id: string = (parsedParts[1].content as { data: string }).data;
|
||||
// let uid = await this.core.apis.UserApi.getUidByUinV2(set_user_id);
|
||||
// let new_title: string = (parsedParts[3].content as { text: string }).text;
|
||||
// console.log(this.core.apis.GroupApi.groupMemberCache.get(group)?.get(uid)?.memberSpecialTitle, new_title)
|
||||
// if (this.core.apis.GroupApi.groupMemberCache.get(group)?.get(uid)?.memberSpecialTitle == new_title) {
|
||||
// return;
|
||||
// }
|
||||
// await this.core.apis.GroupApi.refreshGroupMemberCachePartial(group, uid);
|
||||
// //let json_data_1_url_search = new URL((parsedParts[3].content as { url: string }).url).searchParams;
|
||||
// //let is_new: boolean = json_data_1_url_search.get('isnew') === '1';
|
||||
|
||||
// //console.log(group, set_user_id, is_new, new_title);
|
||||
// return new GroupMemberTitle(
|
||||
// this.core,
|
||||
// +group,
|
||||
// +set_user_id,
|
||||
// new_title
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -93,6 +93,7 @@ export class OB11Construct {
|
||||
|
||||
static group(group: Group): OB11Group {
|
||||
return {
|
||||
group_all_shut: (+group.groupShutupExpireTime > 0 )? -1 : 0,
|
||||
group_remark: group.remarkName,
|
||||
group_id: +group.groupCode,
|
||||
group_name: group.groupName,
|
||||
|
@@ -63,6 +63,7 @@ export interface OB11GroupMember {
|
||||
}
|
||||
|
||||
export interface OB11Group {
|
||||
group_all_shut: number; // 群全员禁言
|
||||
group_remark: string; // 群备注
|
||||
group_id: number; // 群ID
|
||||
group_name: string; // 群名称
|
||||
|
Reference in New Issue
Block a user