mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ec25e09d73 | ||
![]() |
cba9c78ab1 | ||
![]() |
c32db4a881 | ||
![]() |
871add3071 | ||
![]() |
e661c617a3 | ||
![]() |
d4bf721540 | ||
![]() |
d91b55faed | ||
![]() |
9687832d4d | ||
![]() |
fc3e436744 | ||
![]() |
da90245f7b | ||
![]() |
410d6a85d7 | ||
![]() |
b693342e4f | ||
![]() |
acca361f2e | ||
![]() |
b663f47713 | ||
![]() |
d332b199b5 | ||
![]() |
78bac1dbd1 | ||
![]() |
724ff215f9 | ||
![]() |
68ea146469 | ||
![]() |
82583e616f | ||
![]() |
bfc339c58d | ||
![]() |
fe4427c076 | ||
![]() |
5745f388a9 | ||
![]() |
377e3c253f | ||
![]() |
3007a0c00e | ||
![]() |
f51ffc091d |
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "qq-chat",
|
||||
"version": "9.9.15-28418",
|
||||
"verHash": "512caf78",
|
||||
"linuxVersion": "3.2.12-28327",
|
||||
"linuxVerHash": "f60e8252",
|
||||
"verHash": "206bfa62",
|
||||
"linuxVersion": "3.2.12-28418",
|
||||
"linuxVerHash": "0256c948",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "QQ",
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "2.6.16",
|
||||
"version": "2.6.22",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
105
package.json
105
package.json
@@ -1,53 +1,52 @@
|
||||
{
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "2.6.16",
|
||||
"scripts": {
|
||||
"build:framework": "vite build --mode framework",
|
||||
"build:shell": "vite build --mode shell",
|
||||
"build:webui": "cd ./src/webui && vite build",
|
||||
"lint": "eslint --fix src/**/*.{js,ts}",
|
||||
"depend": "cd dist && npm install --omit=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@protobuf-ts/plugin": "^2.9.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/node": "^22.0.1",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.6",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.0.0-beta.2",
|
||||
"fast-xml-parser": "^4.3.6",
|
||||
"file-type": "^19.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"image-size": "^1.1.1",
|
||||
"json-schema-to-ts": "^3.1.0",
|
||||
"log4js": "^6.9.1",
|
||||
"protobufjs": "~7.4.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "2.6.22",
|
||||
"scripts": {
|
||||
"build:framework": "vite build --mode framework",
|
||||
"build:shell": "vite build --mode shell",
|
||||
"build:webui": "cd ./src/webui && vite build",
|
||||
"lint": "eslint --fix src/**/*.{js,ts}",
|
||||
"depend": "cd dist && npm install --omit=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@protobuf-ts/runtime": "^2.9.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/node": "^22.0.1",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.6",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.0.0-beta.2",
|
||||
"fast-xml-parser": "^4.3.6",
|
||||
"file-type": "^19.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"image-size": "^1.1.1",
|
||||
"json-schema-to-ts": "^3.1.0",
|
||||
"log4js": "^6.9.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
@@ -139,8 +139,13 @@ export class LogWrapper {
|
||||
|
||||
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
|
||||
const isSelfSent = msg.senderUin === selfInfo.uin;
|
||||
this.log(`${isSelfSent ? '发送 ->' : '接收 <-'
|
||||
} ${rawMessageToText(msg)}`);
|
||||
|
||||
// Intercept grey tip
|
||||
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +159,12 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
if (msg.chatType == ChatType.KCHATTYPEC2C) {
|
||||
tokens.push(`私聊 (${msg.peerUin})`);
|
||||
} else if (msg.chatType == ChatType.KCHATTYPEGROUP) {
|
||||
tokens.push(`群聊 (群 ${msg.peerUin} 的 ${msg.senderUin})`);
|
||||
if (recursiveLevel < 1) {
|
||||
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
|
||||
}
|
||||
if (msg.senderUin !== '0') {
|
||||
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
|
||||
}
|
||||
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
|
||||
tokens.push('移动设备');
|
||||
} else /* temp */ {
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '2.6.16';
|
||||
export const napCatVersion = '2.6.22';
|
||||
|
@@ -365,22 +365,41 @@ export class NTQQFileApi {
|
||||
|
||||
if (url) {
|
||||
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
|
||||
const urlRkey = parsedUrl.searchParams.get('rkey');
|
||||
const imageAppid = parsedUrl.searchParams.get('appid');
|
||||
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||
if (isNTV2) {
|
||||
let rkey = parsedUrl.searchParams.get('rkey');
|
||||
if (rkey) {
|
||||
return IMAGE_HTTP_HOST_NT + url;
|
||||
}
|
||||
const rkeyData = await this.rkeyManager.getRkey();
|
||||
rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST_NT + url + `${rkey}`;
|
||||
} else {
|
||||
return IMAGE_HTTP_HOST + url;
|
||||
const imageFileId = parsedUrl.searchParams.get('fileid');
|
||||
|
||||
let rkeyData = {
|
||||
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
||||
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
||||
online_rkey: false
|
||||
};
|
||||
|
||||
try {
|
||||
let tempRkeyData = await this.rkeyManager.getRkey();
|
||||
rkeyData.group_rkey = tempRkeyData.group_rkey;
|
||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
||||
}
|
||||
|
||||
|
||||
if (isNTV2 && urlRkey) {
|
||||
return IMAGE_HTTP_HOST_NT + urlRkey;
|
||||
} else if (isNTV2 && rkeyData.online_rkey) {
|
||||
let rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST_NT + url + `&rkey=${rkey}`;
|
||||
} else if (isNTV2 && imageFileId) {
|
||||
let rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
}
|
||||
|
||||
} else if (fileMd5 || md5HexStr) {
|
||||
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
|
||||
}
|
||||
|
||||
this.context.logger.logDebug('图片url获取失败', element);
|
||||
return '';
|
||||
}
|
||||
|
@@ -909,11 +909,26 @@ export interface RawMessage {
|
||||
*/
|
||||
peerUin: string;
|
||||
|
||||
/**
|
||||
* 好友备注(如果是好友消息)
|
||||
*/
|
||||
remark?: string;
|
||||
|
||||
/**
|
||||
* 群名(如果是群消息)
|
||||
*/
|
||||
peerName: string;
|
||||
|
||||
/**
|
||||
* 发送者昵称(如果是好友消息)
|
||||
*/
|
||||
sendNickName: string;
|
||||
|
||||
/**
|
||||
* 发送者好友备注(如果是群消息并且有发送者好友)
|
||||
*/
|
||||
sendRemarkName: string;
|
||||
|
||||
/**
|
||||
* 发送者群名片(如果是群消息)
|
||||
*/
|
||||
@@ -974,4 +989,4 @@ export interface MsgReqType {
|
||||
extraCnt: number
|
||||
}
|
||||
//getMsgsIncludeSelf Peer必须 byType 1
|
||||
//getMsgsWithMsgTimeAndClientSeqForC2C Peer必须 byType 3
|
||||
//getMsgsWithMsgTimeAndClientSeqForC2C Peer必须 byType 3
|
||||
|
10
src/core/external/appid.json
vendored
10
src/core/external/appid.json
vendored
@@ -15,6 +15,10 @@
|
||||
"appid": 537246140,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28131_GW_B"
|
||||
},
|
||||
"6.9.55-28131": {
|
||||
"appid": 537246115,
|
||||
"qua": "V1_MAC_NQ_6.9.55_28131_GW_B"
|
||||
},
|
||||
"9.9.15-28327":{
|
||||
"appid": 537249321,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28327_GW_B"
|
||||
@@ -30,5 +34,9 @@
|
||||
"3.2.12-28418":{
|
||||
"appid": 537249393,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28418_GW_B"
|
||||
},
|
||||
"6.9.56-28418": {
|
||||
"appid": 537249367,
|
||||
"qua": "V1_MAC_NQ_6.9.56_28418_GW_B"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,8 @@ export class RkeyManager {
|
||||
try {
|
||||
await this.refreshRkey();
|
||||
} catch (e) {
|
||||
this.logger.logError.bind(this.logger)('获取rkey失败', e);
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
//this.logger.logError.bind(this.logger)('获取rkey失败', e);
|
||||
}
|
||||
}
|
||||
return this.rkeyData;
|
||||
@@ -42,9 +43,18 @@ export class RkeyManager {
|
||||
//刷新rkey
|
||||
for (const url of this.serverUrl) {
|
||||
try {
|
||||
this.rkeyData = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
|
||||
let temp = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
|
||||
this.rkeyData = {
|
||||
group_rkey: temp.group_rkey.slice(6),
|
||||
private_rkey: temp.private_rkey.slice(6),
|
||||
expired_time: temp.expired_time
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.logError.bind(this.logger)(`[Rkey] Get Rkey ${url} Error `, e);
|
||||
//是否为最后一个url
|
||||
if (url === this.serverUrl[this.serverUrl.length - 1]) {
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
21
src/core/proto/ImageFileId.ts
Normal file
21
src/core/proto/ImageFileId.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { MessageType, BinaryReader, ScalarType, BinaryWriter } from '@protobuf-ts/runtime';
|
||||
|
||||
export const FileId = new MessageType("FileId", [
|
||||
{ no: 2, name: "sha1", kind: "scalar", T: ScalarType.BYTES },
|
||||
{ no: 4, name: "appid", kind: "scalar", T: ScalarType.UINT32 },
|
||||
]);
|
||||
|
||||
export function encodePBFileId(message: any) {
|
||||
return FileId.internalBinaryWrite(message, new BinaryWriter(), {
|
||||
writerFactory: () => new BinaryWriter(),
|
||||
writeUnknownFields: false
|
||||
}).finish();
|
||||
}
|
||||
|
||||
export function decodePBFileId(buffer: Uint8Array): any {
|
||||
const reader = new BinaryReader(buffer);
|
||||
return FileId.internalBinaryRead(reader, reader.len, {
|
||||
readUnknownField: true,
|
||||
readerFactory: () => new BinaryReader(buffer)
|
||||
});
|
||||
}
|
@@ -1,37 +1,48 @@
|
||||
import * as pb from 'protobufjs';
|
||||
import { MessageType, BinaryReader, ScalarType } from '@protobuf-ts/runtime';
|
||||
|
||||
export const BodyInner = new MessageType("BodyInner", [
|
||||
{ no: 1, name: "msgType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */, opt: true },
|
||||
{ no: 2, name: "subType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */, opt: true }
|
||||
]);
|
||||
|
||||
export const BodyInner = new pb.Type("BodyInner")
|
||||
.add(new pb.Field("msgType", 1, "uint32", "optional"))
|
||||
.add(new pb.Field("subType", 2, "uint32", "optional"))
|
||||
export const NoifyData = new MessageType("NoifyData", [
|
||||
{ no: 1, name: "skip", kind: "scalar", T: ScalarType.BYTES /* bytes */, opt: true },
|
||||
{ no: 2, name: "innerData", kind: "scalar", T: ScalarType.BYTES /* bytes */, opt: true }
|
||||
]);
|
||||
|
||||
export const NoifyData = new pb.Type("NoifyData")
|
||||
.add(new pb.Field("skip", 1, "bytes", "optional"))
|
||||
.add(new pb.Field("innerData", 2, "bytes", "optional"))
|
||||
export const MsgHead = new MessageType("MsgHead", [
|
||||
{ no: 2, name: "bodyInner", kind: "message", T: () => BodyInner, opt: true },
|
||||
{ no: 3, name: "noifyData", kind: "message", T: () => NoifyData, opt: true }
|
||||
]);
|
||||
|
||||
export const MsgHead = new pb.Type("MsgHead")
|
||||
.add(BodyInner)
|
||||
.add(NoifyData)
|
||||
.add(new pb.Field("bodyInner", 2, "BodyInner", "optional"))
|
||||
.add(new pb.Field("noifyData", 3, "NoifyData", "optional"));
|
||||
export const Message = new MessageType("Message", [
|
||||
{ no: 1, name: "msgHead", kind: "message", T: () => MsgHead }
|
||||
]);
|
||||
|
||||
export const Message = new pb.Type("Message")
|
||||
.add(MsgHead)
|
||||
.add(new pb.Field("msgHead", 1, "MsgHead"))
|
||||
export const SubDetail = new MessageType("SubDetail", [
|
||||
{ no: 1, name: "msgSeq", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 2, name: "msgTime", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 6, name: "senderUid", kind: "scalar", T: ScalarType.STRING /* string */ }
|
||||
]);
|
||||
|
||||
export const SubDetail = new pb.Type("SubDetail")
|
||||
.add(new pb.Field("msgSeq", 1, "uint32"))
|
||||
.add(new pb.Field("msgTime", 2, "uint32"))
|
||||
.add(new pb.Field("senderUid", 6, "string"))
|
||||
export const RecallDetails = new MessageType("RecallDetails", [
|
||||
{ no: 1, name: "operatorUid", kind: "scalar", T: ScalarType.STRING /* string */ },
|
||||
{ no: 3, name: "subDetail", kind: "message", T: () => SubDetail }
|
||||
]);
|
||||
|
||||
export const RecallDetails = new pb.Type("RecallDetails")
|
||||
.add(SubDetail)
|
||||
.add(new pb.Field("operatorUid", 1, "string"))
|
||||
.add(new pb.Field("subDetail", 3, "SubDetail"))
|
||||
export const RecallGroup = new MessageType("RecallGroup", [
|
||||
{ no: 1, name: "type", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
|
||||
{ no: 4, name: "peerUid", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 11, name: "recallDetails", kind: "message", T: () => RecallDetails },
|
||||
{ no: 37, name: "grayTipsSeq", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ }
|
||||
]);
|
||||
|
||||
export const RecallGroup = new pb.Type("RecallGroup")
|
||||
.add(RecallDetails)
|
||||
.add(new pb.Field("type", 1, "int32"))
|
||||
.add(new pb.Field("peerUid", 4, "uint32"))
|
||||
.add(new pb.Field("recallDetails", 11, "RecallDetails"))
|
||||
.add(new pb.Field("grayTipsSeq", 37, "uint32"))
|
||||
export function decodeMessage(buffer: Uint8Array): any {
|
||||
const reader = new BinaryReader(buffer);
|
||||
return Message.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
|
||||
}
|
||||
|
||||
export function decodeRecallGroup(buffer: Uint8Array): any {
|
||||
const reader = new BinaryReader(buffer);
|
||||
return RecallGroup.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
|
||||
}
|
@@ -1,95 +1,58 @@
|
||||
import * as pb from 'protobufjs';
|
||||
import { MessageType, BinaryReader, ScalarType, RepeatType } from '@protobuf-ts/runtime';
|
||||
|
||||
// Proto: from src/core/proto/ProfileLike.proto
|
||||
// Author: Mlikiowa
|
||||
export const LikeDetail = new MessageType("likeDetail", [
|
||||
{ no: 1, name: "txt", kind: "scalar", T: ScalarType.STRING /* string */ },
|
||||
{ no: 3, name: "uin", kind: "scalar", T: ScalarType.INT64 /* int64 */ },
|
||||
{ no: 5, name: "nickname", kind: "scalar", T: ScalarType.STRING /* string */ }
|
||||
]);
|
||||
|
||||
export interface LikeDetailType {
|
||||
txt: string;
|
||||
uin: pb.Long;
|
||||
nickname: string;
|
||||
}
|
||||
export interface LikeMsgType {
|
||||
times: number;
|
||||
time: number;
|
||||
detail: LikeDetailType;
|
||||
export const LikeMsg = new MessageType("likeMsg", [
|
||||
{ no: 1, name: "times", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
|
||||
{ no: 2, name: "time", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
|
||||
{ no: 3, name: "detail", kind: "message", T: () => LikeDetail }
|
||||
]);
|
||||
|
||||
export const ProfileLikeSubTip = new MessageType("profileLikeSubTip", [
|
||||
{ no: 14, name: "msg", kind: "message", T: () => LikeMsg }
|
||||
]);
|
||||
export const ProfileLikeTip = new MessageType("profileLikeTip", [
|
||||
{ no: 1, name: "msgType", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
|
||||
{ no: 2, name: "subType", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
|
||||
{ no: 203, name: "content", kind: "message", T: () => ProfileLikeSubTip }
|
||||
]);
|
||||
export const SysMessageHeader = new MessageType("SysMessageHeader", [
|
||||
{ no: 1, name: "PeerNumber", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 2, name: "PeerString", kind: "scalar", T: ScalarType.STRING /* string */ },
|
||||
{ no: 5, name: "Uin", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 6, name: "Uid", kind: "scalar", T: ScalarType.STRING /* string */, opt: true }
|
||||
]);
|
||||
|
||||
export const SysMessageMsgSpec = new MessageType("SysMessageMsgSpec", [
|
||||
{ no: 1, name: "msgType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 2, name: "subType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 3, name: "subSubType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 5, name: "msgSeq", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 6, name: "time", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
|
||||
{ no: 12, name: "msgId", kind: "scalar", T: ScalarType.UINT64 /* uint64 */ },
|
||||
{ no: 13, name: "other", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ }
|
||||
]);
|
||||
|
||||
export const SysMessageBodyWrapper = new MessageType("SysMessageBodyWrapper", [
|
||||
{ no: 2, name: "wrappedBody", kind: "scalar", T: ScalarType.BYTES /* bytes */ }
|
||||
]);
|
||||
|
||||
export const SysMessage = new MessageType("SysMessage", [
|
||||
{ no: 1, name: "header", kind: "message", T: () => SysMessageHeader, repeat: RepeatType.UNPACKED },
|
||||
{ no: 2, name: "msgSpec", kind: "message", T: () => SysMessageMsgSpec, repeat: RepeatType.UNPACKED },
|
||||
{ no: 3, name: "bodyWrapper", kind: "message", T: () => SysMessageBodyWrapper }
|
||||
]);
|
||||
|
||||
export function decodeProfileLikeTip(buffer: Uint8Array): any {
|
||||
const reader = new BinaryReader(buffer);
|
||||
return ProfileLikeTip.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
|
||||
}
|
||||
|
||||
export interface profileLikeSubTipType {
|
||||
msg: LikeMsgType;
|
||||
}
|
||||
|
||||
export interface ProfileLikeTipType {
|
||||
msgType: number;
|
||||
subType: number;
|
||||
content: profileLikeSubTipType;
|
||||
}
|
||||
export interface SysMessageHeaderType {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
sender: string;
|
||||
}
|
||||
|
||||
export interface SysMessageMsgSpecType {
|
||||
msgType: number;
|
||||
subType: number;
|
||||
subSubType: number;
|
||||
msgSeq: number;
|
||||
time: number;
|
||||
msgId: pb.Long;
|
||||
other: number;
|
||||
}
|
||||
export interface SysMessageBodyWrapperType {
|
||||
wrappedBody: Uint8Array;
|
||||
}
|
||||
export interface SysMessageType {
|
||||
header: SysMessageHeaderType[];
|
||||
msgSpec: SysMessageMsgSpecType[];
|
||||
bodyWrapper: SysMessageBodyWrapperType;
|
||||
}
|
||||
|
||||
export const SysMessageHeader = new pb.Type("SysMessageHeader")
|
||||
.add(new pb.Field("PeerNumber", 1, "uint32"))
|
||||
.add(new pb.Field("PeerString", 2, "string"))
|
||||
.add(new pb.Field("Uin", 5, "uint32"))
|
||||
.add(new pb.Field("Uid", 6, "string", "optional"));
|
||||
|
||||
export const SysMessageMsgSpec = new pb.Type("SysMessageMsgSpec")
|
||||
.add(new pb.Field("msgType", 1, "uint32"))
|
||||
.add(new pb.Field("subType", 2, "uint32"))
|
||||
.add(new pb.Field("subSubType", 3, "uint32"))
|
||||
.add(new pb.Field("msgSeq", 5, "uint32"))
|
||||
.add(new pb.Field("time", 6, "uint32"))
|
||||
.add(new pb.Field("msgId", 12, "uint64"))
|
||||
.add(new pb.Field("other", 13, "uint32"));
|
||||
|
||||
export const SysMessageBodyWrapper = new pb.Type("SysMessageBodyWrapper")
|
||||
.add(new pb.Field("wrappedBody", 2, "bytes"));
|
||||
|
||||
export const SysMessage = new pb.Type("SysMessage")
|
||||
.add(SysMessageHeader)
|
||||
.add(SysMessageMsgSpec)
|
||||
.add(SysMessageBodyWrapper)
|
||||
.add(new pb.Field("header", 1, "SysMessageHeader", "repeated"))
|
||||
.add(new pb.Field("msgSpec", 2, "SysMessageMsgSpec", "repeated"))
|
||||
.add(new pb.Field("bodyWrapper", 3, "SysMessageBodyWrapper"));
|
||||
|
||||
export const likeDetail = new pb.Type("likeDetail")
|
||||
.add(new pb.Field("txt", 1, "string"))
|
||||
.add(new pb.Field("uin", 3, "int64"))
|
||||
.add(new pb.Field("nickname", 5, "string"));
|
||||
|
||||
export const likeMsg = new pb.Type("likeMsg")
|
||||
.add(likeDetail)
|
||||
.add(new pb.Field("times", 1, "int32"))
|
||||
.add(new pb.Field("time", 2, "int32"))
|
||||
.add(new pb.Field("detail", 3, "likeDetail"));
|
||||
|
||||
export const profileLikeSubTip = new pb.Type("profileLikeSubTip")
|
||||
.add(likeMsg)
|
||||
.add(new pb.Field("msg", 14, "likeMsg"))
|
||||
|
||||
export const profileLikeTip = new pb.Type("profileLikeTip")
|
||||
.add(profileLikeSubTip)
|
||||
.add(new pb.Field("msgType", 1, "int32"))
|
||||
.add(new pb.Field("subType", 2, "int32"))
|
||||
.add(new pb.Field("content", 203, "profileLikeSubTip"));
|
||||
export function decodeSysMessage(buffer: Uint8Array): any {
|
||||
const reader = new BinaryReader(buffer);
|
||||
return SysMessage.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
|
||||
}
|
@@ -1,23 +1,40 @@
|
||||
import { constants } from "node:os";
|
||||
import path from "path";
|
||||
import { dlopen } from "process";
|
||||
import fs from "fs";
|
||||
export class Native {
|
||||
platform: string;
|
||||
supportedPlatforms = ['win32'];
|
||||
MoeHooExport: any = { exports: {} };
|
||||
recallHookEnabled: boolean = false;
|
||||
inited = true;
|
||||
constructor(nodePath: string, platform: string = process.platform) {
|
||||
this.platform = platform;
|
||||
if (!this.supportedPlatforms.includes(this.platform)) {
|
||||
throw new Error(`Platform ${this.platform} is not supported`);
|
||||
try {
|
||||
if (!this.supportedPlatforms.includes(this.platform)) {
|
||||
throw new Error(`Platform ${this.platform} is not supported`);
|
||||
}
|
||||
let nativeNode = path.join(nodePath, './native/MoeHoo.win32.node');
|
||||
if (fs.existsSync(nativeNode)) {
|
||||
dlopen(this.MoeHooExport, nativeNode, constants.dlopen.RTLD_LAZY);
|
||||
}
|
||||
} catch (error) {
|
||||
this.inited = false;
|
||||
}
|
||||
dlopen(this.MoeHooExport, path.join(nodePath, './native/MoeHoo.win32.node'), constants.dlopen.RTLD_LAZY);
|
||||
|
||||
}
|
||||
isSetReCallEnabled(): boolean {
|
||||
return this.recallHookEnabled;
|
||||
return this.recallHookEnabled && this.inited;
|
||||
}
|
||||
registerRecallCallback(callback: (hex: string) => any): void {
|
||||
this.recallHookEnabled = true;
|
||||
return this.MoeHooExport.exports.registMsgPush(callback);
|
||||
try {
|
||||
if (!this.inited) throw new Error('Native Not Init');
|
||||
if (this.MoeHooExport.exports?.registMsgPush) {
|
||||
this.MoeHooExport.exports.registMsgPush(callback);
|
||||
this.recallHookEnabled = true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.recallHookEnabled = false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,9 +1,16 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { OB11ForwardMessage } from '@/onebot';
|
||||
import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNode as OriginalOB11MessageNode } from '@/onebot';
|
||||
import { ActionName } from '../types';
|
||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
||||
import { MessageUnique } from '@/common/message-unique';
|
||||
|
||||
type OB11MessageNode = OriginalOB11MessageNode & {
|
||||
data: {
|
||||
content?: Array<OB11MessageData>;
|
||||
message: Array<OB11MessageData>;
|
||||
};
|
||||
};
|
||||
|
||||
const SchemaData = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -18,36 +25,69 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
|
||||
actionName = ActionName.GoCQHTTP_GetForwardMsg;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
private createTemplateNode(message: OB11Message): OB11MessageNode {
|
||||
return {
|
||||
type: OB11MessageDataType.node,
|
||||
data: {
|
||||
user_id: message.user_id,
|
||||
nickname: message.sender.nickname,
|
||||
message: [],
|
||||
content: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async parseForward(messages: OB11Message[]): Promise<OB11MessageNode[]> {
|
||||
const retMsg: OB11MessageNode[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
const templateNode = this.createTemplateNode(message);
|
||||
|
||||
for (const msgdata of message.message) {
|
||||
if ((msgdata as OB11MessageData).type === OB11MessageDataType.forward) {
|
||||
const newNode = this.createTemplateNode(message);
|
||||
newNode.data.message = await this.parseForward((msgdata as OB11MessageForward).data.content);
|
||||
|
||||
templateNode.data.message.push(newNode);
|
||||
} else {
|
||||
templateNode.data.message.push(msgdata as OB11MessageData);
|
||||
}
|
||||
}
|
||||
retMsg.push(templateNode);
|
||||
}
|
||||
|
||||
return retMsg;
|
||||
}
|
||||
|
||||
async _handle(payload: Payload): Promise<any> {
|
||||
const msgId = payload.message_id || payload.id;
|
||||
if (!msgId) {
|
||||
throw Error('message_id is required');
|
||||
throw new Error('message_id is required');
|
||||
}
|
||||
|
||||
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId);
|
||||
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || parseInt(msgId));
|
||||
if (!rootMsg) {
|
||||
throw Error('msg not found');
|
||||
throw new Error('msg not found');
|
||||
}
|
||||
const data = await this.core.apis.MsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId);
|
||||
const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]);
|
||||
|
||||
if (!data || data.result !== 0) {
|
||||
throw Error('找不到相关的聊天记录' + data?.errMsg);
|
||||
throw new Error('找不到相关的聊天记录' + data?.errMsg);
|
||||
}
|
||||
const msgList = data.msgList;
|
||||
const messages = (await Promise.all(msgList.map(async msg => {
|
||||
const resMsg = await this.obContext.apis.MsgApi
|
||||
.parseMessage(msg);
|
||||
if (!resMsg) return;
|
||||
resMsg.message_id = MessageUnique.createUniqueMsgId({
|
||||
guildId: '',
|
||||
chatType: msg.chatType,
|
||||
peerUid: msg.peerUid,
|
||||
}, msg.msgId)!;
|
||||
return resMsg;
|
||||
}))).filter(msg => !!msg);
|
||||
messages.map(msg => {
|
||||
(<OB11ForwardMessage>msg).content = msg.message;
|
||||
delete (<any>msg).message;
|
||||
});
|
||||
return { messages };
|
||||
|
||||
const singleMsg = data.msgList[0];
|
||||
const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理
|
||||
if (!resMsg) {
|
||||
throw new Error('找不到相关的聊天记录');
|
||||
}
|
||||
//if (this.obContext.configLoader.configData.messagePostFormat === 'array') {
|
||||
//提取
|
||||
let realmsg = ((await this.parseForward([resMsg]))[0].data.message as OB11MessageNode[])[0].data.message;
|
||||
//里面都是offline消息 id都是0 没得说话
|
||||
return { message: realmsg };
|
||||
//}
|
||||
|
||||
// return { message: resMsg };
|
||||
}
|
||||
}
|
||||
}
|
@@ -34,7 +34,7 @@ import { RequestUtil } from '@/common/request';
|
||||
import fs from 'node:fs';
|
||||
import fsPromise from 'node:fs/promises';
|
||||
import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
|
||||
import { SysMessage, SysMessageType } from '@/core/proto/ProfileLike';
|
||||
import { decodeSysMessage } from '@/core/proto/ProfileLike';
|
||||
|
||||
type RawToOb11Converters = {
|
||||
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
|
||||
@@ -841,7 +841,7 @@ export class OneBotMsgApi {
|
||||
return { path, fileName: inputdata.name ?? fileName };
|
||||
}
|
||||
async parseSysMessage(msg: number[]) {
|
||||
const sysMsg = SysMessage.decode(Uint8Array.from(msg)) as unknown as SysMessageType;
|
||||
const sysMsg = decodeSysMessage(Uint8Array.from(msg));
|
||||
if (sysMsg.msgSpec.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { NapCatCore } from '@/core';
|
||||
import { profileLikeTip, ProfileLikeTipType } from '@/core/proto/ProfileLike';
|
||||
import { decodeProfileLikeTip } from '@/core/proto/ProfileLike';
|
||||
|
||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||
import { OB11ProfileLikeEvent } from '../event/notice/OB11ProfileLikeEvent';
|
||||
@@ -13,7 +13,7 @@ export class OneBotUserApi {
|
||||
this.core = core;
|
||||
}
|
||||
async parseLikeEvent(wrappedBody: Uint8Array): Promise<OB11ProfileLikeEvent | undefined> {
|
||||
const likeTip = profileLikeTip.decode(Uint8Array.from(wrappedBody)) as unknown as ProfileLikeTipType;
|
||||
const likeTip = decodeProfileLikeTip(Uint8Array.from(wrappedBody));
|
||||
if (likeTip?.msgType !== 0 || likeTip?.subType !== 203) return;
|
||||
this.core.context.logger.logDebug("收到点赞通知消息");
|
||||
const likeMsg = likeTip.content.msg;
|
||||
|
@@ -45,7 +45,7 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal
|
||||
import { LRUCache } from '@/common/lru-cache';
|
||||
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
|
||||
import { Native } from '@/native';
|
||||
import { Message, RecallGroup } from '@/core/proto/Message';
|
||||
import { decodeMessage, decodeRecallGroup, Message, RecallGroup } from '@/core/proto/Message';
|
||||
|
||||
//OneBot实现类
|
||||
export class NapCatOneBot11Adapter {
|
||||
@@ -73,53 +73,40 @@ export class NapCatOneBot11Adapter {
|
||||
};
|
||||
this.actions = createActionMap(this, core);
|
||||
this.networkManager = new OB11NetworkManager();
|
||||
this.registerNative(core, context).then().catch();
|
||||
this.registerNative(core, context).catch(e => this.context.logger.logWarn.bind(this.context.logger)('初始化Native失败', e)).then();
|
||||
this.InitOneBot()
|
||||
.catch(e => this.context.logger.logError.bind(this.context.logger)('初始化OneBot失败', e));
|
||||
|
||||
}
|
||||
async registerNative(core: NapCatCore, context: InstanceContext) {
|
||||
this.nativeCore = new Native(context.pathWrapper.binaryPath);
|
||||
this.nativeCore.registerRecallCallback(async (hex: string) => {
|
||||
try {
|
||||
let data = Message.decode(Buffer.from(hex, 'hex')) as any;
|
||||
//data.MsgHead.BodyInner.MsgType SubType
|
||||
let bodyInner = data.msgHead?.bodyInner;
|
||||
//context.logger.log("[appNative] Parse MsgType:" + bodyInner.msgType + " / SubType:" + bodyInner.subType);
|
||||
if (bodyInner && bodyInner.msgType == 732 && bodyInner.subType == 17) {
|
||||
let RecallData = Buffer.from(data.msgHead.noifyData.innerData);
|
||||
//跳过 4字节 群号 + 不知道的1字节 +2字节 长度
|
||||
let uid = RecallData.readUint32BE();
|
||||
const buffer = Buffer.from(RecallData.toString('hex').slice(14), 'hex');
|
||||
let seq: number = (RecallGroup.decode(buffer) as any).recallDetails.subDetail.msgSeq;
|
||||
let peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: uid.toString() };
|
||||
context.logger.log("[Native] 群消息撤回 Peer: " + uid.toString() + " / MsgSeq:" + seq);
|
||||
let msgs = await core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, seq.toString());
|
||||
this.recallMsgCache.put(msgs.msgList[0].msgId, msgs.msgList[0]);
|
||||
// let ob11 = await this.apis.MsgApi.parseMessage(msgs.msgList[0], 'array')
|
||||
// .catch(e => this.context.logger.logError.bind(this.context.logger)('处理消息失败', e));
|
||||
// if (ob11) {
|
||||
// const { sendElements, deleteAfterSentFiles } = await this.apis.MsgApi.createSendElements(ob11.message as OB11MessageData[], peer);
|
||||
// this.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles);
|
||||
// }
|
||||
|
||||
|
||||
// this.apis.MsgApi.sendMsg(peer, [{
|
||||
// elementType: 1,
|
||||
// elementId: '',
|
||||
// textElement: {
|
||||
// content: "[Native] 群消息撤回 Peer: " + uid.toString() + " / MsgSeq:" + seq,
|
||||
// atType: 0,
|
||||
// atUid: '',
|
||||
// atTinyId: '',
|
||||
// atNtUid: '',
|
||||
// },
|
||||
// }]);
|
||||
try {
|
||||
this.nativeCore = new Native(context.pathWrapper.binaryPath);
|
||||
if (!this.nativeCore.inited) throw new Error('Native Not Init');
|
||||
this.nativeCore.registerRecallCallback(async (hex: string) => {
|
||||
try {
|
||||
let data = decodeMessage(Buffer.from(hex, 'hex')) as any;
|
||||
//data.MsgHead.BodyInner.MsgType SubType
|
||||
let bodyInner = data.msgHead?.bodyInner;
|
||||
//context.logger.log("[appNative] Parse MsgType:" + bodyInner.msgType + " / SubType:" + bodyInner.subType);
|
||||
if (bodyInner && bodyInner.msgType == 732 && bodyInner.subType == 17) {
|
||||
let RecallData = Buffer.from(data.msgHead.noifyData.innerData);
|
||||
//跳过 4字节 群号 + 不知道的1字节 +2字节 长度
|
||||
let uid = RecallData.readUint32BE();
|
||||
const buffer = Buffer.from(RecallData.toString('hex').slice(14), 'hex');
|
||||
let seq: number = decodeRecallGroup(buffer).recallDetails.subDetail.msgSeq;
|
||||
let peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: uid.toString() };
|
||||
context.logger.log("[Native] 群消息撤回 Peer: " + uid.toString() + " / MsgSeq:" + seq);
|
||||
let msgs = await core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, seq.toString());
|
||||
this.recallMsgCache.put(msgs.msgList[0].msgId, msgs.msgList[0]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
context.logger.logWarn("[Native] Error:", (error as Error).message, ' HEX:', hex);
|
||||
}
|
||||
} catch (error: any) {
|
||||
context.logger.logWarn("[Native] Error:", (error as Error).message, ' HEX:', hex);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
context.logger.logWarn("[Native] Error:", (error as Error).message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
async InitOneBot() {
|
||||
const selfInfo = this.core.selfInfo;
|
||||
|
@@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
|
||||
SettingItem(
|
||||
'<span id="napcat-update-title">Napcat</span>',
|
||||
undefined,
|
||||
SettingButton('V2.6.16', 'napcat-update-button', 'secondary'),
|
||||
SettingButton('V2.6.22', 'napcat-update-button', 'secondary'),
|
||||
),
|
||||
]),
|
||||
SettingList([
|
||||
|
@@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
|
||||
SettingItem(
|
||||
'<span id="napcat-update-title">Napcat</span>',
|
||||
void 0,
|
||||
SettingButton("V2.6.16", "napcat-update-button", "secondary")
|
||||
SettingButton("V2.6.22", "napcat-update-button", "secondary")
|
||||
)
|
||||
]),
|
||||
SettingList([
|
||||
|
Reference in New Issue
Block a user