icqq
Version:
QQ protocol for NodeJS!
488 lines (487 loc) • 19 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getGroupImageUrl = exports.Parser = exports.parse = void 0;
const zlib_1 = require("zlib");
const core_1 = require("../core");
const face_1 = require("./face");
const image_1 = require("./image");
/** 解析消息 */
function parse(rich, uin) {
return new Parser(rich, uin);
}
exports.parse = parse;
/** 消息解析器 */
class Parser {
constructor(rich, uin) {
this.uin = uin;
this.message = [];
this.brief = "";
this.content = "";
this.atme = false;
this.atall = false;
this.newImg = false;
this.imgprefix = {};
this.exclusive = false;
if (Array.isArray(rich)) {
this.parseElems(rich);
}
else {
if (rich[4] && rich[4].length)
this.parseExclusiveElem(0, rich[4]);
this.parseElems(Array.isArray(rich[2]) ? rich[2] : [rich[2]]);
}
}
/** 获取下一个节点的文本 */
getNextText() {
try {
const elem = this.it?.next().value[1][1];
return String(elem[1]);
}
catch {
return "[未知]";
}
}
/** 解析: xml, json, ptt, video, flash, file, shake, poke */
parseExclusiveElem(type, proto) {
let elem;
let brief;
switch (type) {
case 12: //xml
case 51: //json
const buf = proto[1].toBuffer();
elem = {
type: type === 12 ? "xml" : "json",
data: String(buf[0] > 0 ? (0, zlib_1.unzipSync)(buf.slice(1)) : buf.slice(1)),
id: proto[2]
};
brief = elem.type + "消息";
this.content = elem.data;
break;
case 3: //flash
elem = this.parseNewImgElem(proto, "flash");
brief = "闪照";
this.content = `{flash:${elem.file.slice(0, 32).toUpperCase()}}`;
break;
case 0: //ptt
elem = {
type: "record",
file: "protobuf://" + proto.toBase64(),
url: "",
md5: proto[4].toHex(),
size: proto[6] || 0,
seconds: proto[19] || 0,
};
if (proto[20]) {
const url = String(proto[20]);
elem.url = url.startsWith("http") ? url : "https://grouptalk.c2c.qq.com" + url;
}
brief = "语音";
this.content = `{ptt:${elem.url}}`;
break;
case 19: //video
elem = {
type: "video",
file: "protobuf://" + proto.toBase64(),
name: proto[3]?.toString() || "",
fid: String(proto[1]),
md5: proto[2].toBase64(),
size: proto[6] || 0,
seconds: proto[5] || 0,
};
brief = "视频";
this.content = `{video:${elem.fid}}`;
break;
case 5: //transElem
const trans = core_1.pb.decode(proto[2].toBuffer().slice(3))[7][2];
elem = {
type: "file",
name: String(trans[4]),
fid: String(trans[2]).replace("/", ""),
md5: String(trans[8]),
size: trans[3],
duration: trans[5],
};
brief = "群文件";
this.content = `{file:${elem.fid}}`;
break;
case 37: //qlottie
elem = {
type: "face",
id: proto[2][3],
text: face_1.facemap[proto[2][3]]
};
if (!elem.text)
elem.text = proto[2][7] ? String(proto[2][7]) : '超级表情';
if (proto[2][2])
elem.qlottie = String(proto[2][2]);
brief = elem.text;
this.content = `{face:${elem.id},text:${elem.text},qlottie:${elem.qlottie}}`;
break;
case 126: //poke
if (!proto[3])
return;
const pokeid = proto[3] === 126 ? proto[2][4] : proto[3];
elem = {
type: "poke",
id: pokeid,
text: face_1.pokemap[pokeid]
};
brief = face_1.pokemap[pokeid];
this.content = `{poke:${elem.id}}`;
break;
default:
return;
}
this.message = [elem];
this.brief = "[" + brief + "]";
this.exclusive = true;
}
/** 解析: text, at, face, bface, sface, image, mirai */
parsePartialElem(type, proto) {
let elem;
let brief = "";
let content = "";
switch (type) {
case 1: //text&at
brief = String(proto[1]);
const buf = proto[3]?.toBuffer();
if (buf && buf[1] === 1) {
elem = {
type: "at",
qq: 0,
text: brief
};
if (buf[6] === 1) {
elem.qq = "all";
this.atall = true;
}
else {
elem.qq = buf.readUInt32BE(7);
if (elem.qq === this.uin)
this.atme = true;
}
brief = brief || ("@" + elem.qq);
content = `{at:${elem.qq}}`;
}
else if (proto[12] && !proto[12][1]) {
// 频道中的AT
elem = {
type: "at",
qq: 0,
text: brief
};
elem.id = proto[12][5] ? String(proto[12][5]) : "all";
brief = brief || ("@" + elem.qq);
content = `{at:${elem.qq}}`;
}
else {
if (!brief)
return;
content = brief;
elem = {
type: "text",
text: brief
};
}
break;
case 2: //face
elem = {
type: "face",
id: proto[1],
text: face_1.facemap[proto[1]] || "表情",
};
brief = `[${elem.text}]`;
content = `{face:${elem.id}}`;
break;
case 33: //face(id>255)
elem = {
type: "face",
id: proto[1],
text: face_1.facemap[proto[1]],
};
if (!elem.text)
elem.text = proto[2] ? String(proto[2]) : ("/" + elem.id);
brief = `[${elem.text}]`;
content = `{face:${elem.id}}`;
break;
case 6: //bface
brief = this.getNextText();
if (brief.includes("骰子") || brief.includes("猜拳")) {
elem = {
type: brief.includes("骰子") ? "dice" : "rps",
id: proto[12].toBuffer()[16] - 0x30 + 1
};
content = `{${elem.type}:${elem.id}}`;
}
else {
elem = {
type: "bface",
file: proto[4].toHex() + proto[7].toHex() + proto[5],
text: brief.replace(/[[\]]/g, "")
};
content = `{bface:${elem.text}}`;
}
break;
case 4:
case 8:
if (this.newImg)
return;
elem = this.parseImgElem(type, proto, "image");
brief = (elem.asface ? "[动画表情]" : "[图片]") + (elem.summary || "");
content = `{image:${elem.md5.toUpperCase()}}`;
break;
case 34: //sface
brief = this.getNextText();
elem = {
type: "sface",
id: proto[1],
text: brief.replace(/[[\]]/g, ""),
};
content = `{sface:${elem.id}}`;
break;
case 31: //mirai
if (proto[3] === 103904510) {
elem = {
type: "mirai",
data: String(proto[2]),
};
}
else {
return;
}
break;
case 45:
elem = {
type: "markdown",
content: proto[1]?.toString()
};
break;
case 46:
try {
const rows = Array.isArray(proto[1][1]) ? proto[1][1] : [proto[1][1]];
elem = {
type: "button",
content: {
appid: Number(proto[1][2]) || 0,
rows: rows.map(row => {
row = Array.isArray(row[1]) ? row[1] : [row[1]];
const buttons = [];
for (let val of row) {
const button = {
id: "",
render_data: {},
action: {
permission: {}
}
};
if (val[1])
button.id = val[1]?.toString();
if (val[2]) {
button.render_data.label = val[2][1]?.toString();
button.render_data.visited_label = val[2][2]?.toString();
button.render_data.style = Number(val[2][3]) || 0;
}
if (val[3]) {
button.action.type = Number(val[3][1]) || 0;
button.action.unsupport_tips = val[3][4]?.toString();
button.action.data = val[3][5]?.toString();
button.action.reply = val[3][7] === 1;
button.action.enter = val[3][8] === 1;
if (val[3][2]) {
button.action.permission.type = Number(val[3][2][1]) || 0;
button.action.permission.specify_role_ids = val[3][2][2] || [];
button.action.permission.specify_user_ids = val[3][2][3] || [];
}
}
buttons.push(button);
}
return { buttons };
})
}
};
}
catch {
return;
}
break;
case 48:
elem = this.parseNewImgElem(proto, "image");
if (!elem)
return;
brief = (elem.asface ? "[动画表情]" : "[图片]") + (elem.summary || "");
content = `{image:${elem.md5.toUpperCase()}}`;
break;
default:
return;
}
// 删除回复中多余的AT元素
if (this.message.length === 2 && elem.type === "at" && this.message[0]?.type === "at" && this.message[1]?.type === "text") {
if (this.message[0].qq === elem.qq && this.message[1].text === " ") {
this.message.splice(0, 2);
this.brief = "";
}
}
this.brief += brief;
this.content += content;
if (!Array.isArray(this.message))
this.message = [];
const prev = this.message[this.message.length - 1];
if (elem.type === "text" && prev?.type === "text")
prev.text += elem.text;
else
this.message.push(elem);
}
parseElems(arr) {
this.it = arr.entries();
while (true) {
let wrapper = this.it.next().value?.[1];
if (!wrapper)
break;
const type = Number(Object.keys(Reflect.getPrototypeOf(wrapper))[0]);
const proto = wrapper[type];
if (type === 16) { //extraInfo
this.extra = proto;
}
else if (type === 21) { //anonGroupMsg
this.anon = proto;
}
else if (type === 45) { //sourceMsg
this.quotation = proto;
}
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, proto);
break;
case 5: //transElem
case 12: //xml
case 19: //video
case 51: //json
this.parseExclusiveElem(type, proto);
break;
case 53: //commonElem
if (proto[1] === 3) { //flash
this.parseExclusiveElem(3, proto[2][1] ? proto[2][1] : proto[2][2]);
}
else if (proto[1] === 33) { //face(id>255)
this.parsePartialElem(33, proto[2]);
}
else if (proto[1] === 2) { //poke
this.parseExclusiveElem(126, proto);
}
else if (proto[1] === 37) { //qlottie
this.parseExclusiveElem(37, proto);
}
else if (proto[1] === 20) { //json
this.parseExclusiveElem(51, proto[2]);
}
else if (proto[1] === 45) {
this.parsePartialElem(proto[1], proto[2]);
}
else if (proto[1] === 46) {
this.parsePartialElem(proto[1], proto[2]);
}
else if (proto[1] === 48) {
this.parsePartialElem(proto[1], proto[2]);
}
break;
default:
break;
}
}
}
}
parseNewImgElem(proto, type) {
let elem;
const path = (proto[2][1]?.[11] || proto[2][1]?.[12])?.[30];
if (path) {
this.newImg = true;
elem = {
type,
file: proto[1][1][1][4]?.toString(),
url: `https://${proto[1][2][3]}${path}${proto[1][2][2][1] || "&spec=0"}`,
fid: proto[1][1][2]?.toString(),
md5: proto[1][1][1][2]?.toString(),
height: proto[1][1][1][7],
width: proto[1][1][1][6],
size: proto[1][1][1][1],
summary: proto[2][1]?.[2]?.toString()
};
if (type === "image")
elem.asface = proto[2][1]?.[1] === 1;
elem.file = (0, image_1.buildImageFileParam)(elem.md5, elem.size, elem.width, elem.height, proto[1][1][1][5][2]);
return elem;
}
else {
elem = {
type,
file: proto[1][1][1][4]?.toString(),
url: `https://${proto[1][2][3]}${proto[1][2][1]}`,
fid: proto[1][1][2]?.toString(),
md5: proto[1][1][1][2]?.toString(),
height: proto[1][1][1][7],
width: proto[1][1][1][6],
size: proto[1][1][1][1],
summary: proto[2][1]?.[2]?.toString()
};
if (type === "image")
elem.asface = proto[2][1]?.[1] === 1;
elem.file = (0, image_1.buildImageFileParam)(elem.md5, elem.size, elem.width, elem.height, proto[1][1][1][5][2]);
this.imgprefix[elem.md5] = elem;
}
}
parseImgElem(source_type, proto, type) {
let elem;
let dm = type === 'flash' ? (proto[1] ? true : false) : (source_type === 8 ? false : true);
let md5 = proto[dm ? 7 : 13].toHex();
let path = proto[dm ? 29 : 34]?.[30];
if (this.imgprefix[md5] && path) {
elem = {
...this.imgprefix[md5],
type,
url: `${new URL(this.imgprefix[md5].url).origin}${path}&spec=0`
};
}
else {
elem = {
type,
file: '',
url: '',
md5: md5,
height: proto[dm ? 8 : 23],
width: proto[dm ? 9 : 22],
size: proto[dm ? 2 : 25],
summary: proto[dm ? 29 : 34]?.[dm ? 8 : 9]?.toString()
};
elem.file = (0, image_1.buildImageFileParam)(elem.md5, elem.size, elem.width, elem.height, proto[dm ? 5 : 20]);
}
if (type === "image")
elem.asface = proto[dm ? 29 : 34]?.[1] === 1;
if (!elem.url) {
if (path) {
elem.url = `https://c2cpicdw.qpic.cn${path}&spec=0`;
}
else if (proto[16]) {
elem.url = `https://gchat.qpic.cn${proto[16]}`;
}
else if (proto[15]) {
elem.url = `https://c2cpicdw.qpic.cn${proto[15]}`;
}
else {
elem.url = getGroupImageUrl(md5);
}
}
return elem;
}
}
exports.Parser = Parser;
function getGroupImageUrl(md5) {
return `https://gchat.qpic.cn/gchatpic_new/0/0-0-${md5.toUpperCase()}/0`;
}
exports.getGroupImageUrl = getGroupImageUrl;