mirror of
https://github.com/yincongcyincong/wechat_chatter.git
synced 2026-07-15 10:26:52 +08:00
optimize receive
This commit is contained in:
+9
-404
@@ -82,268 +82,6 @@ function generateAESKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
function getProtobufRawBytes(pBuffer, scanSize) {
|
||||
const tags = [0x12, 0x1A, 0x2A, 0x42, 0x52, 0x5A];
|
||||
let uint8Array;
|
||||
|
||||
try {
|
||||
const mem = pBuffer.readByteArray(scanSize);
|
||||
if (!mem) return [];
|
||||
uint8Array = new Uint8Array(mem);
|
||||
} catch (e) {
|
||||
console.error("读取内存失败: " + e);
|
||||
return [];
|
||||
}
|
||||
|
||||
let finalResults = [];
|
||||
|
||||
let i = 0x1a;
|
||||
tags.forEach(targetTag => {
|
||||
let found = false;
|
||||
for (; i < uint8Array.length; i++) {
|
||||
if (uint8Array[i] === targetTag) {
|
||||
// 1. 解析 Varint 长度 (支持 1-5 字节长度标识)
|
||||
let length = 0;
|
||||
let shift = 0;
|
||||
let bytesReadForLen = 0;
|
||||
i = i + 1;
|
||||
|
||||
let lenNum = 0;
|
||||
while (i < uint8Array.length) {
|
||||
let b = uint8Array[i];
|
||||
length |= (b & 0x7F) << shift;
|
||||
bytesReadForLen++;
|
||||
i++;
|
||||
lenNum++;
|
||||
if (!(b & 0x80)) break;
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
// 2. 截取原始 Byte 数据
|
||||
if (i + length <= uint8Array.length) {
|
||||
let addNum = 0
|
||||
if (targetTag === 0x12 || targetTag === 0x1A || targetTag === 0x2A) {
|
||||
addNum = lenNum + 1;
|
||||
}
|
||||
let rawData = uint8Array.slice(i, i + length);
|
||||
if (targetTag === 0x42) {
|
||||
finalResults.push(rawData);
|
||||
} else {
|
||||
finalResults.push(decodeProtobufString(rawData));
|
||||
}
|
||||
i += length;
|
||||
} else {
|
||||
finalResults.push(null); // 长度越界
|
||||
}
|
||||
|
||||
found = true;
|
||||
break; // 找到第一个匹配的 Tag 就跳出
|
||||
}
|
||||
}
|
||||
if (!found) finalResults.push(null); // 未找到该 Tag
|
||||
});
|
||||
|
||||
|
||||
for (; i < uint8Array.length; i++) {
|
||||
if (uint8Array[i] === 0x60 && i + 10 <= uint8Array.length) {
|
||||
finalResults.push(uint8Array.slice(i + 1, i + 10))
|
||||
}
|
||||
}
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
|
||||
function getCleanString(uint8Array) {
|
||||
var out = "";
|
||||
var i = 0;
|
||||
var len = uint8Array.length;
|
||||
|
||||
while (i < len) {
|
||||
var c = uint8Array[i++];
|
||||
|
||||
// 1. 处理单字节 (ASCII: 0xxxxxxx)
|
||||
if (c < 0x80) {
|
||||
// 只保留可见字符 (Space 32 到 ~ 126)
|
||||
if (c >= 32 && c <= 126) {
|
||||
out += String.fromCharCode(c);
|
||||
}
|
||||
}
|
||||
// 2. 处理双字节 (110xxxxx 10xxxxxx)
|
||||
else if ((c & 0xE0) === 0xC0 && i < len) {
|
||||
var c2 = uint8Array[i++];
|
||||
if ((c2 & 0xC0) === 0x80) {
|
||||
// 这种通常是特殊拉丁字母等,按需保留
|
||||
var charCode = ((c & 0x1F) << 6) | (c2 & 0x3F);
|
||||
out += String.fromCharCode(charCode);
|
||||
} else {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
// 3. 处理三字节 (1110xxxx 10xxxxxx 10xxxxxx) -> 绝大多数汉字在此
|
||||
else if ((c & 0xF0) === 0xE0 && i + 1 < len) {
|
||||
var c2 = uint8Array[i++];
|
||||
var c3 = uint8Array[i++];
|
||||
if ((c2 & 0xC0) === 0x80 && (c3 & 0xC0) === 0x80) {
|
||||
var charCode = ((c & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
|
||||
if (
|
||||
(charCode >= 0x4E00 && charCode <= 0x9FA5) || // 基本汉字
|
||||
(charCode >= 0x3000 && charCode <= 0x303F) || // 常用中文标点 (。,、)
|
||||
(charCode >= 0xFF00 && charCode <= 0xFFEF) || // 全角符号/标点 (!:?)
|
||||
(charCode >= 0x2000 && charCode <= 0x206F) || // 常用标点扩展 (含 \u2005)
|
||||
(charCode >= 0x3400 && charCode <= 0x4DBF) // 扩展 A 区汉字
|
||||
) {
|
||||
out += String.fromCharCode(charCode);
|
||||
}
|
||||
} else {
|
||||
i -= 2;
|
||||
}
|
||||
} else if ((c & 0xF8) === 0xF0 && i + 2 < len) {
|
||||
var c2 = uint8Array[i++];
|
||||
var c3 = uint8Array[i++];
|
||||
var c4 = uint8Array[i++];
|
||||
if ((c2 & 0xC0) === 0x80 && (c3 & 0xC0) === 0x80 && (c4 & 0xC0) === 0x80) {
|
||||
// 计算 Unicode 码点
|
||||
var codePoint = ((c & 0x07) << 18) | ((c2 & 0x3F) << 12) | ((c3 & 0x3F) << 6) | (c4 & 0x3F);
|
||||
|
||||
// Emoji 范围通常在 U+1F000 到 U+1F9FF 之间
|
||||
if (codePoint >= 0x1F000 && codePoint <= 0x1FADF) {
|
||||
// 使用 fromCodePoint 处理 4 字节字符
|
||||
out += String.fromCodePoint(codePoint);
|
||||
}
|
||||
} else {
|
||||
i -= 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function readProtobufVarint(uint8Array, offset) {
|
||||
let value = 0;
|
||||
let shift = 0;
|
||||
|
||||
for (let i = offset; i < uint8Array.length && i - offset < 10; i++) {
|
||||
const b = uint8Array[i];
|
||||
value += (b & 0x7F) * Math.pow(2, shift);
|
||||
if ((b & 0x80) === 0) {
|
||||
return {value: value, nextOffset: i + 1};
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function decodeProtobufString(uint8Array, depth) {
|
||||
if (depth === undefined) {
|
||||
depth = 0;
|
||||
}
|
||||
|
||||
if (depth < 2) {
|
||||
const nested = tryDecodeNestedProtobufString(uint8Array, depth);
|
||||
if (nested !== null) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
|
||||
return getCleanString(uint8Array);
|
||||
}
|
||||
|
||||
function tryDecodeNestedProtobufString(uint8Array, depth) {
|
||||
if (!uint8Array || uint8Array.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
const candidates = [];
|
||||
|
||||
while (offset < uint8Array.length) {
|
||||
const tagInfo = readProtobufVarint(uint8Array, offset);
|
||||
if (!tagInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldNumber = Math.floor(tagInfo.value / 8);
|
||||
const wireType = tagInfo.value & 0x07;
|
||||
if (fieldNumber <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
offset = tagInfo.nextOffset;
|
||||
if (wireType === 2) {
|
||||
const lenInfo = readProtobufVarint(uint8Array, offset);
|
||||
if (!lenInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valueStart = lenInfo.nextOffset;
|
||||
const valueEnd = valueStart + lenInfo.value;
|
||||
if (valueEnd > uint8Array.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = uint8Array.slice(valueStart, valueEnd);
|
||||
const text = decodeProtobufString(value, depth + 1);
|
||||
if (text) {
|
||||
candidates.push({fieldNumber: fieldNumber, text: text});
|
||||
}
|
||||
offset = valueEnd;
|
||||
} else if (wireType === 0) {
|
||||
const valueInfo = readProtobufVarint(uint8Array, offset);
|
||||
if (!valueInfo) {
|
||||
return null;
|
||||
}
|
||||
offset = valueInfo.nextOffset;
|
||||
} else if (wireType === 1) {
|
||||
offset += 8;
|
||||
if (offset > uint8Array.length) {
|
||||
return null;
|
||||
}
|
||||
} else if (wireType === 5) {
|
||||
offset += 4;
|
||||
if (offset > uint8Array.length) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.fieldNumber === 2) {
|
||||
return candidate.text;
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.map(candidate => candidate.text).join("");
|
||||
}
|
||||
|
||||
function protobufVarintToNumberString(uint8Array) {
|
||||
let result = BigInt(0);
|
||||
let shift = BigInt(0);
|
||||
|
||||
for (let i = 0; i < uint8Array?.length; i++) {
|
||||
const byte = uint8Array[i];
|
||||
|
||||
// 1. 取出低 7 位并累加到结果中
|
||||
// (BigInt(byte & 0x7F) << shift)
|
||||
result += BigInt(byte & 0x7F) << shift;
|
||||
|
||||
// 2. 检查最高位 (MSB)。如果为 0,说明这个数字结束了
|
||||
if ((byte & 0x80) === 0) {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
// 3. 准备处理下一个 7 位
|
||||
shift += BigInt(7);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function generateBytes(n) {
|
||||
// 生成随机字符串
|
||||
@@ -1858,83 +1596,21 @@ function setReceiver() {
|
||||
// header: true,
|
||||
// ansi: true
|
||||
// }));
|
||||
const fields = getProtobufRawBytes(currentPtr, x2)
|
||||
|
||||
const sender = fields[0]
|
||||
const receiver = fields[1]
|
||||
const content = fields[2]
|
||||
const mediaContent = fields[3]
|
||||
const xml = fields[4]
|
||||
const userContent = fields[5]
|
||||
const msgId = protobufVarintToNumberString(fields[6])
|
||||
|
||||
if (typeof sender !== "string" || sender === "" || typeof receiver !== "string" || receiver === "" ||
|
||||
typeof content !== "string" || content === "" || typeof msgId !== "string" || msgId === "") {
|
||||
// 过滤非聊天消息:
|
||||
// 1. field 2 是 varint (tag=0x10) 而非嵌套 message (tag=0x12) → 同步/通知消息
|
||||
// 2. field 2 wrapper长度 < 128 (单字节varint) → wrapper内容过小,非聊天消息
|
||||
if (currentPtr.add(2).readU8() === 0x10 || currentPtr.add(2).readU8() === 0x16 || (currentPtr.add(3).readU8() & 0x80) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selfId = receiver
|
||||
var msgType = "private"
|
||||
var groupId = ""
|
||||
var senderUser = sender
|
||||
var senderNickname = ""
|
||||
var messages = getMessages(content, sender, mediaContent);
|
||||
|
||||
if (sender.includes("@chatroom")) {
|
||||
msgType = "group"
|
||||
groupId = sender
|
||||
|
||||
let splitIndex = content.indexOf(':')
|
||||
const sendUserStart = content.indexOf('wxid_')
|
||||
senderUser = content.substring(sendUserStart, splitIndex).trim();
|
||||
|
||||
const atUserMatch = xml.match(/<atuserlist>([\s\S]*?)<\/atuserlist>/);
|
||||
const atUser = atUserMatch ? atUserMatch[1] : null;
|
||||
if (atUser) {
|
||||
atUser.split(',').forEach(atUser => {
|
||||
atUser = atUser.trim();
|
||||
if (atUser) {
|
||||
messages.push({type: "at", data: {qq: atUser}});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理用户的名称
|
||||
splitIndex = userContent?.indexOf(':')
|
||||
if (splitIndex === -1) {
|
||||
splitIndex = userContent?.indexOf('在群聊中@了你') !== -1 ? userContent?.indexOf('在群聊中@了你') : userContent?.indexOf('在群聊中发了一段语')
|
||||
senderNickname = userContent?.substring(0, splitIndex).trim();
|
||||
} else {
|
||||
senderNickname = userContent?.substring(0, splitIndex).trim();
|
||||
}
|
||||
if (!senderNickname) {
|
||||
senderNickname = senderUser
|
||||
}
|
||||
|
||||
} else {
|
||||
// 处理用户的名称
|
||||
const splitIndex = userContent?.indexOf(':')
|
||||
senderNickname = userContent?.substring(0, splitIndex).trim();
|
||||
if (!senderNickname) {
|
||||
senderNickname = senderUser
|
||||
}
|
||||
}
|
||||
const mem = currentPtr.readByteArray(x2);
|
||||
if (!mem) return;
|
||||
const uint8Array = new Uint8Array(mem);
|
||||
|
||||
send({
|
||||
time: Date.now(),
|
||||
post_type: "message",
|
||||
message_type: msgType,
|
||||
user_id: senderUser, // 发送人的 ID
|
||||
self_id: selfId, // 接收人的 ID
|
||||
group_id: groupId, // 群 ID
|
||||
message_id: msgId,
|
||||
type: "send",
|
||||
raw: {peerUid: msgId},
|
||||
message: messages,
|
||||
sender: {user_id: senderUser, nickname: senderNickname},
|
||||
msgsource: xml,
|
||||
raw_message: content,
|
||||
show_content: userContent
|
||||
type: "protobuf_msg",
|
||||
data: Array.from(uint8Array),
|
||||
})
|
||||
},
|
||||
});
|
||||
@@ -2179,75 +1855,4 @@ function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
|
||||
return startDwMedia(downloadGlobalX0, downloadFileX1);
|
||||
}
|
||||
|
||||
function getMessages(content, sender, mediaContent) {
|
||||
var messages = [];
|
||||
if (sender.includes("@chatroom")) {
|
||||
let splitIndex = content.indexOf(':')
|
||||
let pureContent = content.substring(splitIndex + 1).trim();
|
||||
const parts = pureContent.split('\u2005');
|
||||
for (let part of parts) {
|
||||
part = part.trim();
|
||||
if (part.startsWith("<?xml version=\"1.0\"?><msg><img")) {
|
||||
messages.push({type: "image", data: {text: part}});
|
||||
} else if (part.startsWith("<msg><voicemsg")) {
|
||||
messages.push({type: "record", data: {text: part}});
|
||||
} else if (part.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
|
||||
const regex = /<type>(.*?)<\/type>/s;
|
||||
const match = part.match(regex);
|
||||
if (match.length > 1) {
|
||||
switch (match[1]) {
|
||||
case "5":
|
||||
messages.push({type: "share", data: {text: part}});
|
||||
break
|
||||
case "6":
|
||||
messages.push({type: "file", data: {text: part}});
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (part.startsWith("<msg><emoji")) {
|
||||
messages.push({type: "face", data: {text: part}});
|
||||
} else if (part.startsWith("<?xml version=\"1.0\"?><msg><videomsg")) {
|
||||
messages.push({type: "video", data: {text: part}});
|
||||
} else if (part.startsWith("<sysmsg") || part.startsWith("<?xml version=\"1.0\"?><sysmsg")) {
|
||||
messages.push({type: "sys", data: {text: part}});
|
||||
} else {
|
||||
messages.push({type: "text", data: {text: part}});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (content.startsWith("<?xml version=\"1.0\"?><msg><img")) {
|
||||
messages.push({type: "image", data: {text: content}});
|
||||
} else if (content.startsWith("<msg><voicemsg")) {
|
||||
const audioStart = mediaContent.indexOf(2);
|
||||
if (audioStart !== -1) {
|
||||
mediaContent = mediaContent.subarray(audioStart);
|
||||
}
|
||||
messages.push({type: "record", data: {text: content, media: Array.from(mediaContent)}});
|
||||
} else if (content.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
|
||||
const regex = /<type>(.*?)<\/type>/s;
|
||||
const match = content.match(regex);
|
||||
if (match.length > 1) {
|
||||
switch (match[1]) {
|
||||
case "5":
|
||||
messages.push({type: "share", data: {text: content}});
|
||||
break
|
||||
case "6":
|
||||
messages.push({type: "file", data: {text: content}});
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (content.startsWith("<msg><emoji")) {
|
||||
messages.push({type: "face", data: {text: content}});
|
||||
} else if (content.startsWith("<?xml version=\"1.0\"?><msg><videomsg")) {
|
||||
messages.push({type: "video", data: {text: content}});
|
||||
} else if (content.startsWith("<sysmsg") || content.startsWith("<?xml version=\"1.0\"?><sysmsg")) {
|
||||
messages.push({type: "sys", data: {text: content}});
|
||||
} else {
|
||||
messages.push({type: "text", data: {text: content}});
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
// -------------------------接收消息分区-------------------------
|
||||
|
||||
Reference in New Issue
Block a user