oicq
Version:
QQ protocol!
742 lines (703 loc) • 23.2 kB
JavaScript
/**
* 解析消息节点
*/
"use strict";
const http = require("http");
const https = require("https");
const zlib = require("zlib");
const querystring = require("querystring");
const tea = require("../algo/tea");
const pb = require("../algo/pb");
const face = require("./face");
const { getGroupMsgs } = require("./history");
const { int32ip2str } = require("../service");
const { buildImageFileParam } = require("./image");
const { Gfs, getC2CFileUrl } = require("./file");
const { getVideoUrl } = require("./ptt");
const { genC2CMessageId, genGroupMessageId, timestamp, parseFunString, code2uin, genRandom } = require("../common");
function escapeCQInside(s) {
if (s === "&") return "&";
if (s === ",") return ",";
if (s === "[") return "[";
if (s === "]") return "]";
}
function escapeCQ(s) {
if (s === "&") return "&";
if (s === "[") return "[";
if (s === "]") return "]";
}
/**
* @this {import("../ref").Client}
* @param {Buffer} resid
* @param {Number} bu
* @returns {Promise<Buffer>}
*/
async function _downloadMultiMsg(resid, bu) {
const body = pb.encode({
1: 2,
2: 5,
3: 9,
4: 3,
5: this.apk.version,
7: [{
1: resid,
2: 3,
}],
8: bu,
9: 2,
});
const blob = await this.sendUni("MultiMsg.ApplyDown", body);
const rsp = pb.decode(blob)[3];
const ip = int32ip2str(Array.isArray(rsp[4]) ? rsp[4][0] : rsp[4]),
port = Array.isArray(rsp[5]) ? rsp[5][0] : rsp[5];
let url = port == 443 ? "https://ssl.htdata.qq.com" : `http://${ip}:${port}`;
url += rsp[2];
const headers = {
"User-Agent": `QQ/${this.apk.version} CFNetwork/1126`,
"Net-Type": "Wifi"
};
return new Promise((resolve, reject) => {
const protocol = port == 443 ? https : http;
protocol.get(url, { headers }, (res) => {
const data = [];
res.on("data", (chunk) => data.push(chunk));
res.on("end", () => {
try {
let buf = Buffer.concat(data);
if (res.headers["accept-encoding"] && res.headers["accept-encoding"].includes("gzip"))
buf = zlib.unzipSync(buf);
const head_len = buf.readUInt32BE(1);
const body_len = buf.readUInt32BE(5);
buf = tea.decrypt(buf.slice(head_len + 9, head_len + 9 + body_len), rsp[3].toBuffer());
buf = pb.decode(buf)[3];
// if (Array.isArray(buf)) buf = buf[0];
buf = zlib.unzipSync(buf[3].toBuffer());
resolve(buf);
} catch (e) {
e.message = "wrong resid";
reject(e);
}
});
}).on("error", reject);
});
}
/**
* 解析消息
*/
class Parser {
/**
* @type {import("../ref").MessageElem[]}
*/
message = [];
raw_message = "";
/**
* @type {import("../ref").Anonymous}
*/
anonymous = null;
/**
* @type {import("../ref").Proto}
*/
extra;
/**
* @private
* 排他型消息:语音、视频、闪照、json、xml、poke、文件
*/
exclusive = false;
/**
* @private
* @type {IterableIterator<[number, import("../ref").Proto]>}
*/
it;
/**
* @param {import("../ref").Client} c
* @param {number} uid 发送者
* @param {number} gid 群号
*/
constructor(c, uid, gid) {
this.c = c;
this.uid = uid;
this.gid = gid;
}
/**
* @public
* @param {import("../ref").RichMsg} rich
*/
async parseMsg(rich) {
let elems = rich[2], ptt = rich[4];
if (!Array.isArray(elems))
elems = [elems];
if (ptt)
await this.parseExclusiveElem(0, ptt);
await this.parseElems(elems);
}
/**
* 获取下一个节点的文本
* @private
* @returns {string}
*/
getNextText() {
try {
const elem = this.it.next().value[1][1];
return String(elem[1]);
} catch {
return "[未知]";
}
}
/**
* 解析排他型消息节点
* xml, json, ptt, video, flash, file, shake, poke
* @private
* @param {number} type
* @param {import("../ref").Proto} elem
*/
async parseExclusiveElem(type, elem) {
/**
* @type {import("../ref").MessageElem}
*/
const msg = {
type: "",
data: {}
};
let brief = "";
switch (type) {
case 12: //xml
case 51: //json
msg.type = type === 12 ? "xml" : "json";
if (elem[1].toBuffer()[0] > 0)
msg.data.data = String(zlib.unzipSync(elem[1].toBuffer().slice(1)));
else
msg.data.data = String(elem[1].toBuffer().slice(1));
if (elem[2] > 0)
msg.data.type = elem[2];
brief = `[${msg.type}消息]`;
break;
case 3: //flash
msg.type = "flash";
msg.data = this.parseImgElem(elem);
brief = "[闪照]";
break;
case 0: //ptt
msg.type = "record";
msg.data.file = "protobuf://" + elem.toBase64();
if (elem[20]) {
const url = String(elem[20]);
msg.data.url = url.startsWith("http") ? url : "https://grouptalk.c2c.qq.com" + url;
}
brief = "[语音]";
break;
case 19: //video
msg.type = "video";
msg.data.file = "protobuf://" + elem.toBase64();
try {
msg.data.url = await getVideoUrl.call(this.c, elem);
} catch { }
brief = "[视频]";
break;
case 5: //transElem
msg.type = "file";
msg.data = await this.parseTransElem(elem);
brief = "[群文件]";
break;
case 17: //shake
msg.type = "shake";
brief = "[窗口抖动]";
break;
case 126: //poke
if (!elem[3]) {
msg.type = "shake";
brief = "[窗口抖动]";
break;
}
msg.type = "poke";
msg.data.type = elem[3];
if (elem[3] === 126) {
msg.data.id = elem[2][4];
msg.data.name = face.pokemap[elem[2][4]];
} else {
msg.data.id = -1;
msg.data.name = face.pokemap[elem[3]];
}
brief = "[" + msg.data.name + "]";
break;
default:
return;
}
this.exclusive = true;
this.message = [msg];
if (this.c.config.brief)
this.raw_message = brief;
else
this.raw_message = genCQMsg(msg);
}
/**
* 解析连续型消息节点
* text, at, face, bface, sface, image, mirai
* @private
* @param {number} type
* @param {import("../ref").Proto} elem
*/
parsePartialElem(type, elem) {
/**
* @type {import("../ref").MessageElem}
*/
const msg = {
type: "",
data: {}
};
let brief = "";
switch (type) {
case 1: //text&at
brief = String(elem[1]);
if (elem[3] && elem[3].toBuffer()[1] === 1) {
msg.type = "at";
if (elem[3].toBuffer()[6] === 1)
msg.data.qq = "all";
else
msg.data.qq = elem[3].toBuffer().readUInt32BE(7);
brief = "@" + brief ? brief : msg.data.qq;
} else {
if (!brief)
return;
msg.type = "text";
}
msg.data.text = brief;
break;
case 2: //face
msg.type = "face";
msg.data.id = elem[1];
brief = "[表情]";
break;
case 33: //face(id>255)
msg.type = "face";
msg.data.id = elem[1];
if (face.map[msg.data.id])
msg.data.text = face.map[msg.data.id];
else if (elem[2])
msg.data.text = String(elem[2]);
brief = msg.data.text;
break;
case 6: //bface
brief = this.getNextText();
if (brief.includes("骰子") || brief.includes("猜拳")) {
msg.type = brief.includes("骰子") ? "dice" : "rps";
msg.data.id = elem[12].toBuffer()[16] - 0x30 + 1;
} else {
msg.type = "bface";
msg.data.file = elem[4].toHex() + elem[7].toHex() + elem[5];
msg.data.text = brief.replace(/[[\]]/g, "");
}
break;
case 4:
case 8:
msg.type = "image";
msg.data = this.parseImgElem(elem);
brief = "[图片]";
break;
case 34: //sface
brief = this.getNextText();
msg.type = "sface";
msg.data.id = elem[1];
msg.data.text = brief.replace(/[[\]]/g, "");
break;
case 31: //mirai
if (elem[3] === 103904510) {
brief = String(elem[2]);
msg.type = "mirai";
msg.data.data = brief;
} else {
return;
}
break;
default:
return;
}
if (msg.type === "text") {
if (!this.c.config.brief)
brief = msg.data.text.replace(/[&[\]]/g, escapeCQ);
if (this.message.length > 0 && this.message[this.message.length - 1].type === "text") {
//合并文本节点
this.message[this.message.length - 1].data.text += msg.data.text;
} else {
this.message.push(msg);
}
} else {
if (!this.c.config.brief)
brief = genCQMsg(msg);
this.message.push(msg);
}
this.raw_message += brief;
}
/**
* @private
* @param {import("../ref").Proto[]} elems
*/
async parseElems(elems) {
this.it = elems.entries();
while (true) {
let wrapper = this.it.next().value;
if (!wrapper)
break;
wrapper = wrapper[1];
const type = parseInt(Object.keys(Reflect.getPrototypeOf(wrapper))[0]);
const elem = wrapper[type];
if (type === 16) { //extraInfo 额外情报
this.extra = elem;
} else if (type === 21) { //anonGroupMsg 匿名情况
try {
const name = String(elem[3]);
this.anonymous = {
id: elem[6], name,
flag: name + "@" + elem[2].toBase64(),
};
} catch {
this.c.logger.warn("解析匿名失败");
}
} else if (type === 37) { //generalFlags 超长消息,气泡等
if (elem[6] === 1 && elem[7]) {
const buf = await _downloadMultiMsg.call(this.c, elem[7].toBuffer(), 1);
let msg = pb.decode(buf)[1];
if (Array.isArray(msg)) msg = msg[0];
const parser = new Parser(this.c, this.uid, this.gid);
await parser.parseMsg(msg[3][1]);
this.message = parser.message;
this.raw_message = parser.raw_message;
this.anonymous = parser.anonymous;
this.extra = parser.extra;
return;
}
} else if (!this.exclusive) {
switch (type) {
case 1: //text
case 2: //face
case 4: //notOnlineImage
case 6: //bface
case 8: //customFace
case 31: //mirai
case 34: //sface
this.parsePartialElem(type, elem);
break;
case 5: //transElem
case 12: //xml
case 17: //shake
case 19: //video
case 51: //json
await this.parseExclusiveElem(type, elem);
break;
case 53: //commonElem
if (elem[1] === 3) { //flash
await this.parseExclusiveElem(3, elem[2][1] ? elem[2][1] : elem[2][2]);
} else if (elem[1] === 33) { //face(id>255)
this.parsePartialElem(33, elem[2]);
} else if (elem[1] === 2) { //poke
await this.parseExclusiveElem(126, elem);
}
break;
case 45: //reply
await this.parseReplyElem(elem);
break;
default:
break;
}
}
}
}
/**
* 解析图片
* @private
* @param {import("../ref").Proto} elem
*/
parseImgElem(elem) {
const data = { };
if (!this.gid) { //私图
data.file = buildImageFileParam(elem[7].toHex(), elem[2], elem[9], elem[8], elem[5]);
if (elem[15])
data.url = "https://c2cpicdw.qpic.cn" + elem[15];
else if (elem[10])
data.url = `https://c2cpicdw.qpic.cn/offpic_new/${this.uid}/${elem[10]}/0?term=2`;
} else { //群图
data.file = buildImageFileParam(elem[13].toHex(), elem[25], elem[22], elem[23], elem[20]);
if (elem[16])
data.url = "https://gchat.qpic.cn" + elem[16];
else
data.url = `https://gchat.qpic.cn/gchatpic_new/${this.uid}/${code2uin(this.gid)}-${elem[7]>>>0}-${elem[13].toHex().toUpperCase()}/0?term=2`;
}
return data;
}
/**
* 解析回复message_id
* @private
* @param {import("../ref").Proto} elem
*/
async parseReplyElem(elem) {
if (Array.isArray(elem[1]))
elem[1] = elem[1][0];
try {
const msg = {
type: "reply",
data: {
id: ""
}
};
let seq = elem[1], user_id = elem[2];
if (this.gid) {
let m = (await getGroupMsgs.call(this.c, this.gid, seq, seq))[0];
let random = m[3][1][1][3];
let time = m[1][6];
msg.data.id = genGroupMessageId(this.gid, user_id, seq, random, time);
} else {
let random = genRandom(elem[8][3]);
let time = elem[3];
let flag = user_id === this.c.uin ? 1 : 0;
msg.data.id = genC2CMessageId(this.uid, seq, random, time, flag);
}
this.message.unshift(msg);
this.raw_message = (this.c.config.brief ? "[回复]" : genCQMsg(msg)) + this.raw_message;
} catch { }
}
/**
* 解析群文件
* @private
* @param {import("../ref").Proto} elem
*/
parseTransElem(elem) {
elem = pb.decode(elem[2].toBuffer().slice(3))[7][2];
const fid = String(elem[2]);
const gfs = new Gfs(this.c, this.gid);
return gfs.download(fid);
}
}
/**
* 生成CQ码字符串消息
* @param {import("../ref").MessageElem} msg
* @returns {string}
*/
function genCQMsg(msg) {
const data = querystring.stringify(msg.data, ",", "=", { encodeURIComponent: (s) => s.replace(/&|,|\[|\]/g, escapeCQInside) });
return "[CQ:" + msg.type + (data ? "," : "") + data + "]";
}
/**
* 解析离线文件
* @this {import("../ref").Client}
* @param {import("../ref").Proto} elem
* @param {number} from
*/
async function _parseC2CFileElem(elem) {
const fileid = elem[3].toBuffer(),
md5 = elem[4].toHex(),
name = String(elem[5]),
size = elem[6],
duration = elem[51] ? timestamp() + elem[51] : 0;
const url = await getC2CFileUrl.call(this, fileid);
const message = {
type: "file",
data: {
name, url, size, md5, duration,
busid: 0,
fileid: String(fileid)
}
};
const raw_message = this.config.brief ? "[离线文件]" : genCQMsg(message);
return {
message, raw_message
};
}
/**
* @this {import("../ref").Client}
* @param {import("../ref").Msg} msg
* @param {boolean} realtime
*/
async function parseC2CMsg(msg, realtime = false) {
const head = msg[1], content = msg[2], body = msg[3];
const type = head[3]; //141|166|167|208|529
let from_uin = head[1], to_uin = head[2], flag = 0,
seq = head[5], random = genRandom(head[7]),
time = body[1] && body[1][1] ? body[1][1][2] : head[6];
let uid = from_uin;
if (from_uin === this.uin) {
uid = to_uin;
flag = 1;
}
let sub_type,
message_id = genC2CMessageId(uid, seq, random, time, flag),
font = body[1] && body[1][1] ? String(body[1][1][9]) : "unknown";
const sender = Object.assign({ user_id: from_uin }, this.fl.get(from_uin));
if (type === 141) {
sub_type = "other";
if (head[8] && head[8][4]) {
sub_type = "group";
sender.group_id = head[8][4];
}
} else if (type === 167) {
sub_type = "single";
} else {
sub_type = this.fl.has(from_uin) ? "friend" : "single";
}
if (sender.nickname === undefined) {
const stranger = (await this.getStrangerInfo(from_uin, seq % 5 == 0 && realtime)).data;
if (stranger) {
stranger.group_id = sender.group_id;
Object.assign(sender, stranger);
if (!this.sl.has(from_uin) || realtime)
this.sl.set(from_uin, stranger);
}
}
if (type === 529) {
if (head[4] !== 4)
return;
var parser = await _parseC2CFileElem.call(this, body[2][1]);
} else if (body[1] && body[1][2]) {
var parser = new Parser(this, uid, 0);
await parser.parseMsg(body[1]);
} else {
return;
}
return {
sub_type, message_id, user_id: from_uin,
message: parser.message,
raw_message: parser.raw_message,
font, sender, time,
auto_reply: !!(content && content[4])
};
}
/**
* @this {import("../ref").Client}
* @param {import("../ref").Msg} msg
* @param {boolean} realtime
*/
async function parseGroupMsg(msg, realtime = false) {
const head = msg[1], content = msg[2], body = msg[3];
const user_id = head[1],
time = head[6],
seq = head[5],
random = body[1][1][3];
let group = head[9],
group_id = group[1],
group_name = group[8] ? String(group[8]) : "";
if (!group_name) {
try {
group_name = this.gl.get(group_id).group_name;
} catch { }
}
if (realtime) {
this.msgExists(group_id, 0, seq, time);
this.getGroupInfo(group_id);
}
const parser = new Parser(this, user_id, group_id);
await parser.parseMsg(body[1]);
let font = String(body[1][1][9]),
card = parseFunString(group[4].toBuffer()),
message_id = genGroupMessageId(group_id, user_id, seq, random, time, content[1]);
let user;
if (!parser.anonymous) {
try {
try {
user = this.gml.get(group_id).get(user_id);
this.getGroupMemberInfo(group_id, user_id);
} catch {
user = (await this.getGroupMemberInfo(group_id, user_id)).data;
}
if (user && realtime) {
const extra = parser.extra;
if (extra[7])
user.title = String(extra[7]);
if (extra[3])
user.level = extra[3];
if (extra[1] && !extra[2]) {
user.card = card = "";
user.nickname = String(extra[1]);
} else {
user.card = card;
}
user.last_sent_time = time;
this.gl.get(group_id).last_sent_time = time;
}
} catch (e) { }
}
if (user) {
var { nickname, sex, age, area, level, role, title } = user;
} else {
var nickname = card, sex = "unknown", age = 0, area = "", level = 0, role = "member", title = "";
}
const sender = {
user_id, nickname, card, sex, age, area, level, role, title
};
return {
sub_type: parser.anonymous ? "anonymous" : "normal",
message_id, group_id, group_name, user_id,
anonymous: parser.anonymous,
message: parser.message,
raw_message: parser.raw_message,
font, sender, time
};
}
/**
* @this {import("../ref").Client}
* @param {import("../ref").Msg} msg
*/
async function parseDiscussMsg(msg) {
const head = msg[1], body = msg[3];
const user_id = head[1],
time = head[6],
seq = head[5];
const discuss = head[13],
discuss_id = discuss[1],
discuss_name = String(discuss[5]);
this.msgExists(discuss_id, 0, seq, time);
const font = String(body[1][1][9]),
card = String(discuss[4]),
nickname = card;
const sender = {
user_id, nickname, card
};
const parser = new Parser(this, user_id, discuss_id);
await parser.parseMsg(body[1]);
return {
discuss_id, discuss_name, user_id,
message: parser.message,
raw_message: parser.raw_message,
font, sender, time
};
}
/**
* 解析转发消息
* @this {import("../ref").Client}
* @param {string} resid
* @returns {import("../ref").ProtocolResponse}
*/
async function parseForwardMsg(resid) {
const data = [];
const blob = await _downloadMultiMsg.call(this, String(resid), 2);
/**
* @type {import("../ref").Msg[]}
*/
let msgs = pb.decode(blob)[2];
if (Array.isArray(msgs))
msgs = msgs[0];
msgs = msgs[2][1];
if (!Array.isArray(msgs))
msgs = [msgs];
for (let msg of msgs) {
const head = msg[1];
let time = head[6];
let user_id = head[1], nickname = "unknown", group_id;
if (head[14]) {
nickname = String(head[14]);
} else {
try {
nickname = String(head[9][4]);
group_id = head[9][1];
} catch { }
}
const parser = new Parser(this, user_id, group_id);
await parser.parseMsg(msg[3][1]);
data.push({
group_id, user_id, nickname, time,
message: parser.message,
raw_message: parser.raw_message
});
}
return { result: 0, data };
}
module.exports = {
parseC2CMsg, parseGroupMsg, parseDiscussMsg, genCQMsg, parseForwardMsg
};