From 2cf81617f334c2439426fe2d047859e7ad20570c Mon Sep 17 00:00:00 2001 From: takayama Date: Wed, 3 Mar 2021 01:00:28 +0900 Subject: [PATCH 01/20] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.js | 6 +- lib/message/recv.js | 252 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 254 insertions(+), 4 deletions(-) diff --git a/client.js b/client.js index 2feec2b1..aaa09409 100644 --- a/client.js +++ b/client.js @@ -183,7 +183,7 @@ class AndroidClient extends Client { if (this.status !== Client.OFFLINE) this.terminate(); if (this.config.reconn_interval >= 1) { - this.logger.warn(this.config.reconn_interval + "秒后重新连接。"); + this.logger.mark(this.config.reconn_interval + "秒后重新连接。"); setTimeout(this.login.bind(this), this.config.reconn_interval * 1000); } this.em("system.offline.network", { message }); @@ -739,8 +739,8 @@ function setGlobalConfig() { } //---------------------------------------------------------------------------------------------------- /** - * @param {Number} uin - * @param {JSON} config + * @param {number} uin + * @param {import("./client").ConfBot} config * @returns {AndroidClient} */ function createClient(uin, config = {}) { diff --git a/lib/message/recv.js b/lib/message/recv.js index 5e50b78a..3bf10e42 100644 --- a/lib/message/recv.js +++ b/lib/message/recv.js @@ -10,7 +10,7 @@ const { int32ip2str } = require("../service"); const { buildImageFileParam } = require("./image"); const { getGroupFileUrl, getC2CFileUrl } = require("./file"); const pb = require("../pb"); -const { genC2CMessageId, genGroupMessageId, timestamp, parseFunString, log } = require("../common"); +const { genC2CMessageId, genGroupMessageId, timestamp, parseFunString, code2uin, log } = require("../common"); function escapeCQInside(s) { if (s === "&") return "&"; @@ -105,6 +105,250 @@ async function getVideoUrl(elem) { return (Array.isArray(o[10]) ? o[10][1].raw : o[10].raw) + String(o[11].raw); } +/** + * 解析消息 + */ +class Parser { + + /** + * @type {import("../../client").MessageElem[]} + */ + message = []; + raw_message = ""; + brief = ""; + + /** + * @type {import("../ref").Proto} + */ + anonymous; + + /** + * @type {import("../ref").Proto} + */ + extra; + + /** + * 排他型消息:语音、视频、闪照、json、xml、poke、文件 + */ + exclusive = false; + + /** + * @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; + } + + makeExclusive() { + this.exclusive = true; + this.message = []; + } + + /** + * @public + * @param {import("../ref").Proto[]} elems + * @param {import("../ref").Proto} ptt + */ + async parseMsg(elems, ptt) { + if (!Array.isArray(elems)) + elems = [elems]; + if (ptt) + await this.parseExclusiveElem(0, ptt); + await this.parseElems(elems); + } + + getNextText() { + try { + const elem = this.it.next().value[1][1]; + return String(elem[1].raw); + } catch { + return "unknown"; + } + } + + /** + * @param {number} type + * @param {import("../ref").Proto} elem + */ + async parseExclusiveElem(type, elem) { + if (this.exclusive) + return; + this.exclusive = true; + this.message = []; + const msg = { + type: "", + data: {} + }; + switch (type) { + case 12: //xml + case 51: //json + msg.type = type === 12 ? "xml" : "json"; + if (elem[1].raw[0] > 0) + msg.data.data = String(zlib.unzipSync(elem[1].raw.slice(1))); + else + msg.data.data = String(elem[1].raw.slice(1)); + if (elem[2] > 0) + msg.data.type = elem[2]; + break; + case 0: //ptt + msg.type = "record"; + msg.data.file = "protobuf://" + elem.raw.toString("base64"); + if (elem[20]) { + const url = String(elem[20].raw); + msg.data.url = url.startsWith("http") ? url : "https://grouptalk.c2c.qq.com" + url; + } + this.brief = "[语音]"; + break; + case 19: //video + msg.type = "video"; + msg.data.file = "protobuf://" + elem.raw.toString("base64"); + try { + msg.data.url = await getVideoUrl.call(this.c, elem); + } catch { } + this.brief = "[视频]"; + break; + case 5: //transElem + msg.type = "file"; + let v = pb.decode(elem[2].raw.slice(3))[7]; + v = v[2]; + let rsp = await getGroupFileUrl.call(this, this.gid, v[1], v[2].raw); + msg.data = { + name: String(v[4].raw), + url: `http://${rsp[4].raw}/ftn_handler/${rsp[6].raw.toString("hex")}/?fname=${v[4].raw}`, + size: v[3], + md5: rsp[9].raw.toString("hex"), + duration: v[5], + busid: this.gid.toString(36) + "-" + v[1], + fileid: String(v[2].raw) + }; + this.brief = "[群文件]"; + break; + case 17: //shake + msg.type = "shake"; + this.brief = "[窗口抖动]"; + break; + } + this.raw_message = genCQMsg(msg); + } + + async parsePartialElem(type, elem) { + if (this.exclusive) + return; + + } + + parseCommonElem(elem) { + + } + + /** + * @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 + this.anonymous = elem; + } else if (type === 37) { //generalFlags + if (elem[6] === 1 && elem[7]) { + const buf = await downloadMultiMsg.call(this, elem[7].raw, 1); + let msg = pb.decode(buf)[1]; + if (Array.isArray(msg)) msg = msg[0]; + // return await parseMessage.call(this, msg[3][1], from); + } + } else if (!this.exclusive) { + switch (type) { + case 1: //text + case 2: //face + case 4: //notOnlineImage + case 6: //bface + case 8: //customFace + case 34: //sface + case 45: //reply + await this.parsePartialElem(elem); + break; + case 5: //transElem + case 12: //xml + case 17: //shake + case 19: //video + case 51: //json + await this.parseExclusiveElem(elem); + break; + case 53: //commonElem + this.parseCommonElem(elem); + break; + default: + break; + } + } + + } + } + + /** + * @param {import("../ref").Proto} elem + */ + parseImgElem(elem) { + const data = { }; + if (!this.gid) { + data.file = buildImageFileParam(elem[7].raw, elem[2], elem[9], elem[8], elem[5]); + if (elem[15]) + data.url = "http://c2cpicdw.qpic.cn" + elem[15].raw; + else if (elem[10]) + data.url = `http://c2cpicdw.qpic.cn/offpic_new/${this.uid}/${elem[10].raw}/0?term=2`; + } else { + data.file = buildImageFileParam(elem[13].raw, elem[25], elem[22], elem[23], elem[20]); + if (elem[16]) + data.url = "http://gchat.qpic.cn" + elem[16].raw; + else + data.url = `http://gchat.qpic.cn/gchatpic_new/${this.uid}/${code2uin(this.gid)}-${elem[7]}-${elem[13].raw.toString("hex").toUpperCase()}/0?term=2`; + } + return data; + } + + /** + * @param {import("../ref").Proto} elem + */ + async parseReplyElem(elem) { + if (Array.isArray(elem[1])) + elem[1] = elem[1][0]; + try { + const msg = genSegment(); + if (this.gid) { + let m = await getGroupMsgs.call(this, this.gid, elem[1], elem[1]); + m = m[0]; + msg.data.id = genGroupMessageId(this.gid, elem[2], elem[1], m[3][1][1][3], m[1][6]); + } else { + let random = elem[8][3]; + if (typeof random === "bigint") + random = parseInt(random & 0xffffffffn); + msg.data.id = genC2CMessageId(this.uid, elem[1], random, elem[3]); + } + msg.type = "reply"; + this.message.unshift(msg); + this.raw_message = genCQMsg(msg) + this.raw_message; + this.brief = "[回复]" + this.brief; + } catch { } + } +} + /** * @this {import("../ref").Client} */ @@ -281,6 +525,12 @@ async function parseMessage(rich, from = 0, gflag = false) { return { chain, raw_message, extra, anon }; } +function genSegment() { + return { + data: { } + }; +} + /** * 生成CQ码字符串消息 * @param {import("../../client").MessageElem} msg From 9864450916dd4148b0ce33f6f3078ba9d97afeda Mon Sep 17 00:00:00 2001 From: takayama Date: Wed, 3 Mar 2021 18:24:00 +0900 Subject: [PATCH 02/20] =?UTF-8?q?add=20jsdoc=20=E9=87=8D=E6=9E=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core.js | 1 + lib/message/recv.js | 173 +++++++++++++++++++++++++++++++++++--------- lib/online-push.js | 27 +++++-- lib/ref.d.ts | 5 ++ lib/wtlogin/tlv.js | 15 +++- 5 files changed, 177 insertions(+), 44 deletions(-) diff --git a/lib/core.js b/lib/core.js index 2c2a81f1..9e9cdd65 100644 --- a/lib/core.js +++ b/lib/core.js @@ -124,6 +124,7 @@ async function getMsg(sync_flag = 0) { if (this.gml.get(group_id).size) ginfo.member_count = this.gml.get(group_id).size; } catch { } + this.logger.info(`${user_id}(${this.nickname}) 加入了群 ${group_id}`); } this.em("notice.group.increase", { group_id, user_id, diff --git a/lib/message/recv.js b/lib/message/recv.js index 3bf10e42..18635470 100644 --- a/lib/message/recv.js +++ b/lib/message/recv.js @@ -148,11 +148,6 @@ class Parser { this.gid = gid; } - makeExclusive() { - this.exclusive = true; - this.message = []; - } - /** * @public * @param {import("../ref").Proto[]} elems @@ -166,24 +161,31 @@ class Parser { await this.parseElems(elems); } + /** + * 获取下一个节点的文本 + * @private + * @returns {string} + */ getNextText() { try { const elem = this.it.next().value[1][1]; return String(elem[1].raw); } catch { - return "unknown"; + return "[未知]"; } } /** + * 解析排他性消息节点 + * xml, json, ptt, video, flash, file, shake, poke + * @private * @param {number} type * @param {import("../ref").Proto} elem */ async parseExclusiveElem(type, elem) { - if (this.exclusive) - return; - this.exclusive = true; - this.message = []; + /** + * @type {import("../../client").MessageElem} + */ const msg = { type: "", data: {} @@ -198,6 +200,12 @@ class Parser { msg.data.data = String(elem[1].raw.slice(1)); if (elem[2] > 0) msg.data.type = elem[2]; + this.brief = `[${msg.type}消息]`; + break; + case 3: //flash + msg.type = "flash"; + msg.data = this.parseImgElem(elem); + this.brief = "[闪照]"; break; case 0: //ptt msg.type = "record"; @@ -236,21 +244,110 @@ class Parser { msg.type = "shake"; this.brief = "[窗口抖动]"; break; + case 126: //poke + 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]]; + } + this.brief = "[" + msg.data.name + "]"; + break; + default: + return; } + this.exclusive = true; + this.message = [msg]; this.raw_message = genCQMsg(msg); } - async parsePartialElem(type, elem) { - if (this.exclusive) + /** + * 解析连续性消息节点 + * text, at, face, bface, sface, image + * @private + * @param {number} type + * @param {import("../ref").Proto} elem + */ + parsePartialElem(type, elem) { + /** + * @type {import("../../client").MessageElem} + */ + const msg = { + type: "", + data: {} + }; + let brief = ""; + switch (type) { + case 1: //text&at + msg.data.text = String(elem[1].raw); + if (elem[3] && elem[3].raw[1] === 1) { + msg.type = "at"; + if (elem[3].raw[6] === 1) + msg.data.qq = "all"; + else + msg.data.qq = elem[3].raw.readUInt32BE(7); + brief = "@" + msg.data.text ? msg.data.text : msg.data.qq; + } else { + if (!msg.data.text) + return; + msg.type = "text"; + brief += msg.data.text; + } + 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].raw); + 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].raw[16] - 0x30 + 1; + } else { + msg.type = "bface"; + msg.data.file = elem[4].raw.toString("hex") + elem[7].raw.toString("hex") + elem[5]; + msg.data.text = brief.replace(/\[\]/, ""); + } + break; + case 4: + case 8: + msg.type = "image"; + msg.data = this.parseImgElem(elem); + brief = "[图片]"; + break; + case 34: //sface + msg.type = "sface"; + msg.data.id = elem[1]; + msg.data.text = this.getNextText(); + brief = msg.data.text; + break; + default: return; - - } - - parseCommonElem(elem) { - + } + if (msg.type === "text" && 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); + } + this.raw_message += genCQMsg(msg); + this.brief += brief; } /** + * @private * @param {import("../ref").Proto[]} elems */ async parseElems(elems) { @@ -262,11 +359,11 @@ class Parser { wrapper = wrapper[1]; const type = parseInt(Object.keys(Reflect.getPrototypeOf(wrapper))[0]); const elem = wrapper[type]; - if (type === 16) { //extraInfo + if (type === 16) { //extraInfo 额外情报 this.extra = elem; - } else if (type === 21) { //anonGroupMsg + } else if (type === 21) { //anonGroupMsg 匿名情况 this.anonymous = elem; - } else if (type === 37) { //generalFlags + } else if (type === 37) { //generalFlags 超长消息,气泡等 if (elem[6] === 1 && elem[7]) { const buf = await downloadMultiMsg.call(this, elem[7].raw, 1); let msg = pb.decode(buf)[1]; @@ -281,8 +378,7 @@ class Parser { case 6: //bface case 8: //customFace case 34: //sface - case 45: //reply - await this.parsePartialElem(elem); + this.parsePartialElem(elem); break; case 5: //transElem case 12: //xml @@ -292,7 +388,16 @@ class Parser { await this.parseExclusiveElem(elem); break; case 53: //commonElem - this.parseCommonElem(elem); + 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; @@ -303,17 +408,19 @@ class Parser { } /** + * 解析图片 + * @private * @param {import("../ref").Proto} elem */ parseImgElem(elem) { const data = { }; - if (!this.gid) { + if (!this.gid) { //私图 data.file = buildImageFileParam(elem[7].raw, elem[2], elem[9], elem[8], elem[5]); if (elem[15]) data.url = "http://c2cpicdw.qpic.cn" + elem[15].raw; else if (elem[10]) data.url = `http://c2cpicdw.qpic.cn/offpic_new/${this.uid}/${elem[10].raw}/0?term=2`; - } else { + } else { //群图 data.file = buildImageFileParam(elem[13].raw, elem[25], elem[22], elem[23], elem[20]); if (elem[16]) data.url = "http://gchat.qpic.cn" + elem[16].raw; @@ -324,13 +431,20 @@ class Parser { } /** + * 解析回复message_id + * @private * @param {import("../ref").Proto} elem */ async parseReplyElem(elem) { if (Array.isArray(elem[1])) elem[1] = elem[1][0]; try { - const msg = genSegment(); + const msg = { + type: "reply", + data: { + id: "" + } + }; if (this.gid) { let m = await getGroupMsgs.call(this, this.gid, elem[1], elem[1]); m = m[0]; @@ -341,7 +455,6 @@ class Parser { random = parseInt(random & 0xffffffffn); msg.data.id = genC2CMessageId(this.uid, elem[1], random, elem[3]); } - msg.type = "reply"; this.message.unshift(msg); this.raw_message = genCQMsg(msg) + this.raw_message; this.brief = "[回复]" + this.brief; @@ -525,12 +638,6 @@ async function parseMessage(rich, from = 0, gflag = false) { return { chain, raw_message, extra, anon }; } -function genSegment() { - return { - data: { } - }; -} - /** * 生成CQ码字符串消息 * @param {import("../../client").MessageElem} msg diff --git a/lib/online-push.js b/lib/online-push.js index 74b2a1cb..8cfd833e 100644 --- a/lib/online-push.js +++ b/lib/online-push.js @@ -7,8 +7,8 @@ const { genC2CMessageId, genGroupMessageId, log } = require("./common"); /** * OnlinePush回执 * @this {import("./ref").Client} - * @param {Number} svrip - * @param {Number} seq + * @param {number} svrip + * @param {number} seq * @param {Buffer[]} rubbish */ function handleOnlinePush(svrip, seq, rubbish = []) { @@ -24,6 +24,9 @@ function handleOnlinePush(svrip, seq, rubbish = []) { this.writeUni("OnlinePush.RespPush", body); } +/** + * @type {({[k: number]: (this: import("./ref").Client, data: import("./ref").Proto, time: number) => void})} + */ const sub0x27 = { 80: function (data, time) { const o = data[12]; @@ -108,6 +111,9 @@ const sub0x27 = { } }; +/** + * @type {({[k: number]: (this: import("./ref").Client, buf: Buffer, time: number) => void})} + */ const push528 = { 0x8A: function (buf, time) { let data = pb.decode(buf)[1]; @@ -191,12 +197,12 @@ function parsePoke(data) { return { user_id, operator_id, action, suffix }; } - /** - * @param {Number} group_id - * @param {String} field - * @param {Boolean} enable - * @param {Number} time + * @this {import("./ref").Client} + * @param {number} group_id + * @param {string} field + * @param {boolean} enable + * @param {number} time */ function onGroupSetting(group_id, field, enable, time) { if (!field) return; @@ -207,6 +213,9 @@ function onGroupSetting(group_id, field, enable, time) { this.em("notice.group.setting", e); } +/** + * @type {({[k: number]: (this: import("./ref").Client, group_id: number, buf: Buffer, time: number) => void})} + */ const push732 = { 0x0C: function (group_id, buf, time) { const operator_id = buf.readUInt32BE(6); @@ -413,6 +422,7 @@ function onOnlinePushTrans(blob, seq) { try { this.gml.get(group_id).delete(user_id); } catch { } + this.logger.info(`${user_id}离开了群${group_id}`); } } try { @@ -424,6 +434,9 @@ function onOnlinePushTrans(blob, seq) { } } +/** + * @this {import("./ref").Client} + */ function onC2CMsgSync(blob, seq) { handleOnlinePush.call(this, pb.decode(blob)[2], seq); } diff --git a/lib/ref.d.ts b/lib/ref.d.ts index 89a9225c..aab7b026 100644 --- a/lib/ref.d.ts +++ b/lib/ref.d.ts @@ -133,6 +133,11 @@ export class Client extends oicq.Client { logining: boolean; status: Symbol; + fl: Map; + sl: Map; + gl: Map; + gml: Map>; + apk: ApkInfo; ksid: string | Buffer; device: Device; diff --git a/lib/wtlogin/tlv.js b/lib/wtlogin/tlv.js index 2a5898ad..43a1ccca 100644 --- a/lib/wtlogin/tlv.js +++ b/lib/wtlogin/tlv.js @@ -7,19 +7,26 @@ const pb = require("../pb"); /** * @this {import("../ref").Client} + * @param {number} tag * @returns {Buffer} */ function packTlv(tag, ...args) { + /** + * @type {Writer} + */ const stream = tlv_map[tag].apply(this, args); const len = Buffer.allocUnsafe(2); len.writeUInt16BE(stream.readableLength); stream.unshift(len); - const series = Buffer.allocUnsafe(2); - series.writeUInt16BE(tag); - stream.unshift(series); + const tag_buf = Buffer.allocUnsafe(2); + tag_buf.writeUInt16BE(tag); + stream.unshift(tag_buf); return stream.read(); } +/** + * @type {({[k: number]: (this: import("../ref").Client, ...args: any[]) => Writer})} + */ const tlv_map = { 0x01: function () { return new Writer() @@ -252,7 +259,7 @@ const tlv_map = { /** * @param {import("../ref").Client} client - * @returns {Function} + * @returns {(this: import("../ref").Client, tag: number, ...args: any[]) => Buffer} */ module.exports.getPacker = function (client) { return packTlv.bind(client); From ca7f07c56e0acfd4534cb9e74669b8c15dc3b4f2 Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 6 Mar 2021 04:58:04 +0900 Subject: [PATCH 03/20] =?UTF-8?q?add=20brief=20=E9=87=8D=E6=9E=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.js | 1 + lib/core.js | 2 +- lib/message/file.js | 28 ++- lib/message/recv.js | 457 ++++++++++++-------------------------------- lib/online-push.js | 6 +- lib/ref.d.ts | 18 +- 6 files changed, 172 insertions(+), 340 deletions(-) diff --git a/client.js b/client.js index aaa09409..54bf1aea 100644 --- a/client.js +++ b/client.js @@ -98,6 +98,7 @@ class AndroidClient extends Client { platform: 1, log_level: "info", kickoff: false, + brief: false, ignore_self: true, resend: true, reconn_interval: 5, diff --git a/lib/core.js b/lib/core.js index 9e9cdd65..387b3fbe 100644 --- a/lib/core.js +++ b/lib/core.js @@ -138,7 +138,7 @@ async function getMsg(sync_flag = 0) { ++this.stat.recv_msg_cnt; (async () => { try { - const data = await parseC2CMsg.call(this, msg); + const data = await parseC2CMsg.call(this, msg, true); if (data && data.raw_message) { this.logger.info(`recv from: [Private: ${data.user_id}(${data.sub_type})] ` + data.raw_message); this.em("message.private." + data.sub_type, data); diff --git a/lib/message/file.js b/lib/message/file.js index 444240dd..d57ff54b 100644 --- a/lib/message/file.js +++ b/lib/message/file.js @@ -46,6 +46,32 @@ async function getC2CFileUrl(fileid) { return url; } +/** + * @this {import("../ref").Client} + * @param {import("../ref").Proto} elem + */ +async function getVideoUrl(elem) { + const body = pb.encode({ + 1: 400, + 4: { + 1: this.uin, + 2: this.uin, + 3: 1, + 4: 7, + 5: elem[1], + 6: 1, + 8: elem[2], + 9: 1, + 10: 2, + 11: 2, + 12: 2, + } + }); + const blob = await this.sendUni("PttCenterSvr.ShortVideoDownReq", body); + const o = pb.decode(blob)[4][9]; + return (Array.isArray(o[10]) ? o[10][1].raw : o[10].raw) + String(o[11].raw); +} + module.exports = { - getGroupFileUrl, getC2CFileUrl, + getGroupFileUrl, getC2CFileUrl, getVideoUrl }; diff --git a/lib/message/recv.js b/lib/message/recv.js index 18635470..c06b968c 100644 --- a/lib/message/recv.js +++ b/lib/message/recv.js @@ -8,7 +8,7 @@ const face = require("./face"); const { getGroupMsgs } = require("./history"); const { int32ip2str } = require("../service"); const { buildImageFileParam } = require("./image"); -const { getGroupFileUrl, getC2CFileUrl } = require("./file"); +const { getGroupFileUrl, getC2CFileUrl, getVideoUrl } = require("./file"); const pb = require("../pb"); const { genC2CMessageId, genGroupMessageId, timestamp, parseFunString, code2uin, log } = require("../common"); @@ -79,32 +79,6 @@ async function downloadMultiMsg(resid, bu) { }); } -/** - * @this {import("../ref").Client} - * @param {import("../ref").Proto} elem - */ -async function getVideoUrl(elem) { - const body = pb.encode({ - 1: 400, - 4: { - 1: this.uin, - 2: this.uin, - 3: 1, - 4: 7, - 5: elem[1], - 6: 1, - 8: elem[2], - 9: 1, - 10: 2, - 11: 2, - 12: 2, - } - }); - const blob = await this.sendUni("PttCenterSvr.ShortVideoDownReq", body); - const o = pb.decode(blob)[4][9]; - return (Array.isArray(o[10]) ? o[10][1].raw : o[10].raw) + String(o[11].raw); -} - /** * 解析消息 */ @@ -115,12 +89,11 @@ class Parser { */ message = []; raw_message = ""; - brief = ""; /** - * @type {import("../ref").Proto} + * @type {import("../../client").Anonymous} */ - anonymous; + anonymous = null; /** * @type {import("../ref").Proto} @@ -128,19 +101,21 @@ class Parser { 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 群号 + * @param {number} uid 发送者 + * @param {number} gid 群号 */ constructor(c, uid, gid) { this.c = c; @@ -150,10 +125,10 @@ class Parser { /** * @public - * @param {import("../ref").Proto[]} elems - * @param {import("../ref").Proto} ptt + * @param {import("../ref").RichMsg} rich */ - async parseMsg(elems, ptt) { + async parseMsg(rich) { + let elems = rich[2], ptt = rich[4]; if (!Array.isArray(elems)) elems = [elems]; if (ptt) @@ -176,7 +151,7 @@ class Parser { } /** - * 解析排他性消息节点 + * 解析排他型消息节点 * xml, json, ptt, video, flash, file, shake, poke * @private * @param {number} type @@ -190,6 +165,7 @@ class Parser { type: "", data: {} }; + let brief = ""; switch (type) { case 12: //xml case 51: //json @@ -200,12 +176,12 @@ class Parser { msg.data.data = String(elem[1].raw.slice(1)); if (elem[2] > 0) msg.data.type = elem[2]; - this.brief = `[${msg.type}消息]`; + brief = `[${msg.type}消息]`; break; case 3: //flash msg.type = "flash"; msg.data = this.parseImgElem(elem); - this.brief = "[闪照]"; + brief = "[闪照]"; break; case 0: //ptt msg.type = "record"; @@ -214,7 +190,7 @@ class Parser { const url = String(elem[20].raw); msg.data.url = url.startsWith("http") ? url : "https://grouptalk.c2c.qq.com" + url; } - this.brief = "[语音]"; + brief = "[语音]"; break; case 19: //video msg.type = "video"; @@ -222,29 +198,23 @@ class Parser { try { msg.data.url = await getVideoUrl.call(this.c, elem); } catch { } - this.brief = "[视频]"; + brief = "[视频]"; break; case 5: //transElem msg.type = "file"; - let v = pb.decode(elem[2].raw.slice(3))[7]; - v = v[2]; - let rsp = await getGroupFileUrl.call(this, this.gid, v[1], v[2].raw); - msg.data = { - name: String(v[4].raw), - url: `http://${rsp[4].raw}/ftn_handler/${rsp[6].raw.toString("hex")}/?fname=${v[4].raw}`, - size: v[3], - md5: rsp[9].raw.toString("hex"), - duration: v[5], - busid: this.gid.toString(36) + "-" + v[1], - fileid: String(v[2].raw) - }; - this.brief = "[群文件]"; + msg.data = await this.parseTransElem(elem); + brief = "[群文件]"; break; case 17: //shake msg.type = "shake"; - this.brief = "[窗口抖动]"; + 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) { @@ -254,18 +224,21 @@ class Parser { msg.data.id = -1; msg.data.name = face.pokemap[elem[3]]; } - this.brief = "[" + msg.data.name + "]"; + brief = "[" + msg.data.name + "]"; break; default: return; } this.exclusive = true; this.message = [msg]; - this.raw_message = genCQMsg(msg); + if (this.c.config.brief) + this.raw_message = brief; + else + this.raw_message = genCQMsg(msg); } /** - * 解析连续性消息节点 + * 解析连续型消息节点 * text, at, face, bface, sface, image * @private * @param {number} type @@ -282,20 +255,20 @@ class Parser { let brief = ""; switch (type) { case 1: //text&at - msg.data.text = String(elem[1].raw); + brief = String(elem[1].raw); if (elem[3] && elem[3].raw[1] === 1) { msg.type = "at"; if (elem[3].raw[6] === 1) msg.data.qq = "all"; else msg.data.qq = elem[3].raw.readUInt32BE(7); - brief = "@" + msg.data.text ? msg.data.text : msg.data.qq; + brief = "@" + brief ? brief : msg.data.qq; } else { - if (!msg.data.text) + if (!brief) return; msg.type = "text"; - brief += msg.data.text; } + msg.data.text = brief; break; case 2: //face msg.type = "face"; @@ -319,7 +292,7 @@ class Parser { } else { msg.type = "bface"; msg.data.file = elem[4].raw.toString("hex") + elem[7].raw.toString("hex") + elem[5]; - msg.data.text = brief.replace(/\[\]/, ""); + msg.data.text = brief.replace(/[\[\]]/g, ""); } break; case 4: @@ -329,21 +302,27 @@ class Parser { brief = "[图片]"; break; case 34: //sface + brief = this.getNextText(); msg.type = "sface"; msg.data.id = elem[1]; - msg.data.text = this.getNextText(); - brief = msg.data.text; + msg.data.text = brief.replace(/[\[\]]/g, ""); break; default: return; } - if (msg.type === "text" && this.message.length > 0 && this.message[this.message.length - 1].type === "text") { //合并文本节点 - this.message[this.message.length - 1].data.text += msg.data.text; + 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 { + if (!this.c.config.brief) + brief = genCQMsg(msg); this.message.push(msg); } - this.raw_message += genCQMsg(msg); - this.brief += brief; + this.raw_message += brief; } /** @@ -362,13 +341,28 @@ class Parser { if (type === 16) { //extraInfo 额外情报 this.extra = elem; } else if (type === 21) { //anonGroupMsg 匿名情况 - this.anonymous = elem; + try { + const name = String(elem[3].raw); + this.anonymous = { + id: elem[6], name, + flag: name + "@" + elem[2].raw.toString("base64"), + }; + } catch { + this.c.logger.warn("解析匿名失败"); + this.c.logger.debug(elem.raw); + } } else if (type === 37) { //generalFlags 超长消息,气泡等 if (elem[6] === 1 && elem[7]) { - const buf = await downloadMultiMsg.call(this, elem[7].raw, 1); + const buf = await downloadMultiMsg.call(this.c, elem[7].raw, 1); let msg = pb.decode(buf)[1]; if (Array.isArray(msg)) msg = msg[0]; - // return await parseMessage.call(this, msg[3][1], from); + 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) { @@ -378,14 +372,14 @@ class Parser { case 6: //bface case 8: //customFace case 34: //sface - this.parsePartialElem(elem); + this.parsePartialElem(type, elem); break; case 5: //transElem case 12: //xml case 17: //shake case 19: //video case 51: //json - await this.parseExclusiveElem(elem); + await this.parseExclusiveElem(type, elem); break; case 53: //commonElem if (elem[1] === 3) { //flash @@ -403,7 +397,6 @@ class Parser { break; } } - } } @@ -446,7 +439,7 @@ class Parser { } }; if (this.gid) { - let m = await getGroupMsgs.call(this, this.gid, elem[1], elem[1]); + let m = await getGroupMsgs.call(this.c, this.gid, elem[1], elem[1]); m = m[0]; msg.data.id = genGroupMessageId(this.gid, elem[2], elem[1], m[3][1][1][3], m[1][6]); } else { @@ -456,186 +449,28 @@ class Parser { msg.data.id = genC2CMessageId(this.uid, elem[1], random, elem[3]); } this.message.unshift(msg); - this.raw_message = genCQMsg(msg) + this.raw_message; - this.brief = "[回复]" + this.brief; + this.raw_message = (this.c.config.brief ? "[回复]" : genCQMsg(msg)) + this.raw_message; } catch { } } -} -/** - * @this {import("../ref").Client} - */ -async function parseMessage(rich, from = 0, gflag = false) { - const elems = Array.isArray(rich[2]) ? rich[2] : [rich[2]]; - if (rich[4]) - elems.unshift(Object.setPrototypeOf({}, { 9999: rich[4] })); - let extra = {}, anon = {}; - const chain = []; - let raw_message = ""; - let bface_tmp = null, bface_magic = null, ignore_text = false; - for (let v of elems) { - const type = parseInt(Object.keys(Reflect.getPrototypeOf(v))[0]); - const msg = { type: "", data: {} }; - let o = v[type]; - switch (type) { - case 45: //reply - if (Array.isArray(o[1])) - o[1] = o[1][0]; - try { - if (gflag) { - let m = await getGroupMsgs.call(this, from, o[1], o[1]); - m = m[0]; - msg.data.id = genGroupMessageId(from, o[2], o[1], m[3][1][1][3], m[1][6]); - } else { - let random = o[8][3]; - if (typeof random === "bigint") - random = parseInt(random & 0xffffffffn); - msg.data.id = genC2CMessageId(from, o[1], random, o[3]); - } - msg.type = "reply"; - } catch { } - break; - case 21: //anonGroupMsg - anon = o; - break; - case 16: //extraInfo - extra = o; - break; - case 37: //generalFlags - if (o[6] === 1 && o[7]) { - const buf = await downloadMultiMsg.call(this, o[7].raw, 1); - let msg = pb.decode(buf)[1]; - // if (Array.isArray(msg)) msg = msg[0]; - return await parseMessage.call(this, msg[3][1], from); - } - break; - case 34: //sface - msg.type = "sface"; - msg.data.id = o[1]; - break; - case 17: - msg.type = "shake"; - ignore_text = true; - break; - case 12: //xml - case 51: //json - msg.type = type === 12 ? "xml" : "json"; - if (o[1].raw[0] > 0) - msg.data.data = String(zlib.unzipSync(o[1].raw.slice(1))); - else - msg.data.data = String(o[1].raw.slice(1)); - if (o[2] > 0) - msg.data.type = o[2]; - ignore_text = true; - break; - case 5: //file - [msg.type, msg.data] = await parseTransElem.call(this, o, from); - ignore_text = true; - break; - case 1: //text - if (ignore_text) break; - if (bface_tmp && o[1]) { - const text = String(o[1].raw).replace("[", "").replace("]", "").trim(); - if (text.includes("猜拳") && bface_magic) { - msg.type = "rps"; - msg.data.id = bface_magic.raw[16] - 0x30 + 1; - } else if (text.includes("骰子") && bface_magic) { - msg.type = "dice"; - msg.data.id = bface_magic.raw[16] - 0x30 + 1; - } else { - msg.data.file = bface_tmp, msg.type = "bface"; - msg.data.text = text; - } - bface_tmp = null; - bface_magic = null; - break; - } - if (o[3] && o[3].raw[1] === 1) { - msg.type = "at"; - if (o[3].raw[6] === 1) - msg.data.qq = "all"; - else - msg.data.qq = o[3].raw.readUInt32BE(7); - } else { - msg.type = "text"; - } - msg.data.text = String(o[1].raw); - break; - case 2: //face - msg.type = "face", msg.data.id = o[1]; - break; - case 6: //bface - bface_tmp = o[4].raw.toString("hex") + o[7].raw.toString("hex") + o[5]; - bface_magic = o[12]; - break; - case 4: //notOnlineImage - msg.type = "image"; - msg.data = parseImageElem(o, from, 1); - break; - case 8: //customFace - msg.type = "image"; - msg.data = parseImageElem(o, from, 0); - break; - case 53: //commonElem - if (o[1] === 3) { - msg.type = "flash"; - if (o[2][1]) { //customFace - msg.data = parseImageElem(o[2][1], from, 0); - } - else if (o[2][2]) { //notOnlineImage - msg.data = parseImageElem(o[2][2], from, 1); - } - ignore_text = true; - } else if (o[1] === 33) { - msg.type = "face"; - msg.data.id = o[2][1]; - if (face.map[msg.data.id]) - msg.data.text = face.map[msg.data.id]; - else if (o[2][2]) - msg.data.text = String(o[2][2].raw); - } else if (o[1] === 2) { - msg.type = "poke"; - msg.data.type = o[3]; - if (o[3] === 126) { - msg.data.id = o[2][4]; - msg.data.name = face.pokemap[o[2][4]]; - } else { - msg.data.id = -1; - msg.data.name = face.pokemap[o[3]]; - } - ignore_text = true; - } - break; - case 19: //video - msg.type = "video"; - msg.data.file = "protobuf://" + o.raw.toString("base64"); - ignore_text = true; - try { - msg.data.url = await getVideoUrl.call(this, o); - } catch { } - break; - case 9999: //ptt - msg.type = "record"; - msg.data.file = "protobuf://" + o.raw.toString("base64"); - ignore_text = true; - if (o[20]) { - const url = String(o[20].raw); - msg.data.url = url.startsWith("http") ? url : "https://grouptalk.c2c.qq.com" + url; - } - break; - } - if (msg.type) { - if (msg.type === "text" && chain[chain.length - 1] && chain[chain.length - 1].type === "text") - chain[chain.length - 1].data.text += msg.data.text; - else - chain.push(msg); - if (msg.type === "text") - raw_message += msg.data.text.replace(/[&\[\]]/g, escapeCQ); - else - raw_message += genCQMsg(msg); - } + /** + * 解析群文件 + * @private + * @param {import("../ref").Proto} elem + */ + async parseTransElem(elem) { + elem = pb.decode(elem[2].raw.slice(3))[7][2]; + let rsp = await getGroupFileUrl.call(this.c, this.gid, elem[1], elem[2].raw); + return { + name: String(elem[4].raw), + url: `http://${rsp[4].raw}/ftn_handler/${rsp[6].raw.toString("hex")}/?fname=${elem[4].raw}`, + size: elem[3], + md5: rsp[9].raw.toString("hex"), + duration: elem[5], + busid: this.gid.toString(36) + "-" + elem[1], + fileid: String(elem[2].raw) + }; } - return { chain, raw_message, extra, anon }; } /** @@ -649,52 +484,8 @@ function genCQMsg(msg) { } /** - * 解析图片protobuf - * @param {import("../ref").Proto} o - * @param {number} from - * @param {boolean} c2c - */ -function parseImageElem(o, from, c2c = false) { - const data = { }; - if (c2c) { - data.file = buildImageFileParam(o[7].raw, o[2], o[9], o[8], o[5]); - if (o[15]) - data.url = "http://c2cpicdw.qpic.cn" + o[15].raw; - else if (o[10]) - data.url = `http://c2cpicdw.qpic.cn/offpic_new/${from}/${o[10].raw}/0?term=2`; - } else { - data.file = buildImageFileParam(o[13].raw, o[25], o[22], o[23], o[20]); - if (o[16]) - data.url = "http://gchat.qpic.cn" + o[16].raw; - else - data.url = `http://gchat.qpic.cn/gchatpic_new/0/${from}-0-${o[13].raw.toString("hex").toUpperCase()}/0?term=2`; - } - return data; -} - -/** - * 群文件 - * @param {import("../ref").Proto} o - * @param {number} from - */ -async function parseTransElem(o, from) { - let v = pb.decode(o[2].raw.slice(3))[7]; - v = v[2]; - let rsp = await getGroupFileUrl.call(this, from, v[1], v[2].raw); - const data = { - name: String(v[4].raw), - url: `http://${rsp[4].raw}/ftn_handler/${rsp[6].raw.toString("hex")}/?fname=${v[4].raw}`, - size: v[3], - md5: rsp[9].raw.toString("hex"), - duration: v[5], - busid: from.toString(36) + "-" + v[1], - fileid: String(v[2].raw) - }; - return ["file", data]; -} - -/** - * 离线文件 + * 解析离线文件 + * @this {import("../ref").Client} * @param {import("../ref").Proto} elem * @param {number} from */ @@ -705,7 +496,7 @@ async function parseC2CFileElem(elem) { size = elem[6], duration = elem[51] ? timestamp() + elem[51] : 0; const url = await getC2CFileUrl.call(this, fileid); - const msg = { + const message = { type: "file", data: { name, url, size, md5, duration, @@ -713,17 +504,18 @@ async function parseC2CFileElem(elem) { fileid: String(fileid) } }; - const raw_message = genCQMsg(msg); + const raw_message = this.config.brief ? "[离线文件]" : genCQMsg(message); return { - raw_message, chain: [msg] + message, raw_message }; } /** * @this {import("../ref").Client} * @param {import("../ref").Msg} msg + * @param {boolean} realtime */ -async function parseC2CMsg(msg) { +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 @@ -745,11 +537,11 @@ async function parseC2CMsg(msg) { sub_type = this.fl.has(user_id) ? "friend" : "single"; } if (sender.nickname === undefined) { - const stranger = (await this.getStrangerInfo(user_id, seq % 5 == 0)).data; + const stranger = (await this.getStrangerInfo(user_id, seq % 5 == 0 && realtime)).data; if (stranger) { stranger.group_id = sender.group_id; Object.assign(sender, stranger); - if (!this.sl.has(user_id) || timestamp() - time < 5) + if (!this.sl.has(user_id) || realtime) this.sl.set(user_id, stranger); } } @@ -760,14 +552,16 @@ async function parseC2CMsg(msg) { if (type === 529) { if (head[4] !== 4) return; - var { chain, raw_message } = await parseC2CFileElem.call(this, body[2][1]); + var parser = await parseC2CFileElem.call(this, body[2][1]); } else if (body[1] && body[1][2]) { - var { chain, raw_message } = await parseMessage.call(this, body[1], user_id); + var parser = new Parser(this, user_id, 0); + await parser.parseMsg(body[1]); } return { sub_type, message_id, user_id, - message: chain, - raw_message, font, sender, time, + message: parser.message, + raw_message: parser.raw_message, + font, sender, time, auto_reply: !!(content && content[4]) }; } @@ -775,8 +569,9 @@ async function parseC2CMsg(msg) { /** * @this {import("../ref").Client} * @param {import("../ref").Msg} msg + * @param {boolean} realtime */ -async function parseGroupMsg(msg) { +async function parseGroupMsg(msg, realtime = false) { const head = msg[1], body = msg[3]; const user_id = head[1], @@ -791,31 +586,23 @@ async function parseGroupMsg(msg) { } catch { } } - this.msgExists(group_id, 0, seq, time); - - this.getGroupInfo(group_id); + if (realtime) { + this.msgExists(group_id, 0, seq, time); + this.getGroupInfo(group_id); + } - var { chain, raw_message, extra, anon } = await parseMessage.call(this, body[1], group_id, 1); + const parser = new Parser(this, user_id, group_id); + await parser.parseMsg(body[1]); let font = String(body[1][1][9].raw), card = parseFunString(group[4].raw); - let anonymous = null, user = null; - if (user_id === 80000000 && anon) { - try { - anonymous = { - id: anon[6], - name: String(anon[3].raw), - }; - anonymous.flag = anonymous.name + "@" + anon[2].raw.toString("base64"); - } catch { - this.logger.debug("解析匿名失败"); - this.logger.debug(anon.raw); - } - } else { + let user; + if (!parser.anonymous) { try { user = (await this.getGroupMemberInfo(group_id, user_id)).data; - if (time >= user.last_sent_time) { + if (user && realtime) { + const extra = parser.extra; if (extra[7]) user.title = String(extra[7].raw); if (extra[3]) @@ -841,10 +628,12 @@ async function parseGroupMsg(msg) { user_id, nickname, card, sex, age, area, level, role, title }; return { - sub_type: anonymous ? "anonymous" : "normal", - group_id, group_name, user_id, anonymous, //message_id - message: chain, - raw_message, font, sender, time + sub_type: parser.anonymous ? "anonymous" : "normal", + group_id, group_name, user_id, + anonymous: parser.anonymous, + message: parser.message, + raw_message: parser.raw_message, + font, sender, time }; } @@ -872,12 +661,14 @@ async function parseDiscussMsg(msg) { user_id, nickname, card }; - const { chain, raw_message } = await parseMessage.call(this, body[1], discuss_id); + const parser = new Parser(this, user_id, discuss_id); + await parser.parseMsg(body[1]); return { discuss_id, discuss_name, user_id, - message: chain, - raw_message, font, sender, time + message: parser.message, + raw_message: parser.raw_message, + font, sender, time }; } diff --git a/lib/online-push.js b/lib/online-push.js index 8cfd833e..c064c470 100644 --- a/lib/online-push.js +++ b/lib/online-push.js @@ -448,6 +448,9 @@ async function onGroupMsg(blob, seq) { if (!this.sync_finished) return; try { + /** + * @type {import("./ref").Msg} + */ let msg = pb.decode(blob)[1]; //生成消息id @@ -471,7 +474,7 @@ async function onGroupMsg(blob, seq) { ++this.stat.recv_msg_cnt; //解析消息 - const data = await parseGroupMsg.call(this, msg); + const data = await parseGroupMsg.call(this, msg, true); if (data && data.raw_message) { if (data.user_id === this.uin && this.config.ignore_self) return; @@ -515,6 +518,7 @@ const FRAG = new Map; * 1.是最后一个分片,返回组装好的消息 * 2.不是最后一个分片,返回空 * @param {import("./ref").Msg} msg + * @returns {Promise} */ async function rebuildFragments(msg) { const head = msg[1], content = msg[2], body = msg[3]; diff --git a/lib/ref.d.ts b/lib/ref.d.ts index aab7b026..b6344376 100644 --- a/lib/ref.d.ts +++ b/lib/ref.d.ts @@ -98,10 +98,20 @@ export interface MsgHead extends Proto { 4: bigint, //uuid 5: number, //seqid 6: number, //time - // 8: Routing, - // 9: Group, + 8: { //routing + 4: number //group_id + }, + 9: { + 1: number, //group_id + 4: Proto, //card + 8: Proto, //group_name + }, 10: number, //appid - // 13: Discuss, + 13: { + 1: number, //disscus_id + 4: Proto, //card + 5: Proto, //disscus_name + }, } export interface MsgContent extends Proto { @@ -113,7 +123,7 @@ export interface MsgContent extends Proto { export interface MsgBody extends Proto { 1: RichMsg, - // 2: FileMsg, + 2: Proto, //离线文件 } export interface RichMsg extends Proto { From a30eff8ef890abea9c390a204786c128ccab919a Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 6 Mar 2021 10:44:39 +0900 Subject: [PATCH 04/20] =?UTF-8?q?add=20util(oicq.cq,=20oicq.cqStr)=20?= =?UTF-8?q?=E7=94=A8=E4=BA=8E=E5=BF=AB=E9=80=9F=E7=94=9F=E6=88=90=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.d.ts | 59 ++++++++++++++++++++++++ client.js | 4 +- lib/message/recv.js | 2 +- package.json | 1 + util.js | 107 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 util.js diff --git a/client.d.ts b/client.d.ts index 67c67965..76d0fb19 100644 --- a/client.d.ts +++ b/client.d.ts @@ -27,6 +27,9 @@ export interface ConfBot { //被风控时是否尝试用分片发送,默认true resend?: boolean, + //raw_message里不使用CQ码字符串,而是使用简短易读的形式(如:"[图片][表情]"),可以加快解析速度,默认false + brief?: boolean, + //数据存储文件夹,需要可写权限,默认主目录下的data文件夹 data_dir?: string, @@ -615,3 +618,59 @@ export class Client extends events.EventEmitter { } export function createClient(uin: number, config?: ConfBot): Client; + +/** + * 生成消息元素的快捷函数 + */ +export namespace cq { + function text(text: string): TextElem; + function at(qq: number, text?: string, dummy?: boolean): AtElem; + function face(id: number, text?: string): FaceElem; //经典表情 + function sface(id: number, text?: string): FaceElem; + function bface(file: string): BfaceElem; //原创表情 + function rps(id?: number): MfaceElem; //猜拳 + function dice(id?: number): MfaceElem; //骰子 + function image(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): ImgPttElem; //图片 + function flash(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): ImgPttElem; //闪照 + function ptt(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): ImgPttElem; //语音 + function location(lat: number, lng: number, address: string, id?: string): LocationElem; //位置分享 + function music(type: "qq" | "163", id: number): MusicElem; + function json(data: any): JsonElem; + function xml(data: string, type?: number): XmlElem; + function share(url: string, title: string, image?: string, content?: string): ShareElem; //内容分享 + function shake(): ShakeElem; //抖动 + function poke(type: number, id?: number): PokeElem; //戳一戳 + function reply(id: string): ReplyElem; //引用回复 + function node(id: string): NodeElem; //转发节点 + function anonymous(ignore?: boolean): AnonymousElem; //匿名 + + //转换到CQ码字符串 + function toString(elem: MessageElem): string; + function toString(elems: MessageElem[]): string; +} + +/** + * 生成CQ码字符串的快捷函数 + */ +export namespace cqStr { + function text(text: string): string; + function at(qq: number, text?: string, dummy?: boolean): string; + function face(id: number, text?: string): string; + function sface(id: number, text?: string): string; + function bface(file: string): string; + function rps(id?: number): string; + function dice(id?: number): string; + function image(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): string; + function flash(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): string; + function ptt(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): string; + function location(lat: number, lng: number, address: string, id?: string): string; + function music(type: "qq" | "163", id: number): string; + function json(data: any): string; + function xml(data: string, type?: number): string; + function share(url: string, title: string, image?: string, content?: string): string; + function shake(): string; + function poke(type: number, id?: number): string; + function reply(id: string): string; + function node(id: string): string; + function anonymous(ignore?: boolean): string; +} diff --git a/client.js b/client.js index 54bf1aea..589a6deb 100644 --- a/client.js +++ b/client.js @@ -20,6 +20,7 @@ const wt = require("./lib/wtlogin/wt"); const chat = require("./lib/message/chat"); const troop = require("./lib/troop"); const { getErrorMessage, TimeoutError } = require("./exception"); +const { cq, cqStr } = require("./util"); const BUF0 = Buffer.alloc(0); function buildApiRet(retcode, data = null, error = null) { @@ -752,5 +753,6 @@ function createClient(uin, config = {}) { } module.exports = { - createClient, setGlobalConfig + createClient, setGlobalConfig, + cq, cqStr, }; diff --git a/lib/message/recv.js b/lib/message/recv.js index c06b968c..ea969b8c 100644 --- a/lib/message/recv.js +++ b/lib/message/recv.js @@ -673,5 +673,5 @@ async function parseDiscussMsg(msg) { } module.exports = { - parseC2CMsg, parseGroupMsg, parseDiscussMsg + parseC2CMsg, parseGroupMsg, parseDiscussMsg, genCQMsg }; diff --git a/package.json b/package.json index 7b4006e2..38e27ce8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "url": "https://github.com/takayama-lily/oicq/issues" }, "homepage": "https://github.com/takayama-lily/oicq#readme", + "changelogs": "https://github.com/takayama-lily/oicq/releases", "dependencies": { "crypto-tea": "^0.2.0", "https-proxy-agent": "^5.0.0", diff --git a/util.js b/util.js new file mode 100644 index 00000000..6125d8ba --- /dev/null +++ b/util.js @@ -0,0 +1,107 @@ +"use strict"; +const { genCQMsg } = require("./lib/message/recv"); +const cq = {}; +const cqStr = {}; + +const elem_map = { + text: ["text"], + at: ["qq", "text", "dummy"], + face: ["id", "text"], + sface: ["id", "text"], + bface: ["file", "text"], + rps: ["id"], + dice: ["id"], + image: ["file", "cache", "timeout", "headers", "proxy"], + flash: ["file", "cache", "timeout", "headers", "proxy"], + ptt: ["file", "cache", "timeout", "headers", "proxy"], + location: ["lat", "lng", "address", "id"], + music: ["type", "id"], + json: ["data"], + xml: ["data", "type"], + share: ["url", "title", "image", "content"], + shake: [], + poke: ["type", "id"], + reply: ["id"], + node: ["id"], + anonymous: ["ignore"], +}; + +for (const [type, params] of Object.entries(elem_map)) { + cq[type] = (...args) => { + const data = {}; + for (let i = 0; i < params.length; ++i) { + if (Reflect.has(args, i)) { + data[params[i]] = args[i]; + } + } + return { + type, data, + }; + }; + cqStr[type] = (...args) => { + return genCQMsg(cq[type](...args)); + }; +} + +/** + * @param {import("./client").MessageElem | import("./client").MessageElem[]} arg + */ +cq.toString = (arg) => { + if (typeof arg === "string") + return arg; + if (typeof arg[Symbol.iterator] === "function") { + let str = ""; + for (let elem of arg) { + str += genCQMsg(elem); + } + return str; + } else { + return genCQMsg(arg); + } +}; + +// function test() { +// console.log(cq.text("aaa")); +// console.log(cqStr.text("aaa")); + +// console.log(cq.image("/aaa/bbb")); +// console.log(cqStr.image("/aaa/bbb")); + +// console.log(cq.image("/aaa/bbb",1)); +// console.log(cqStr.image("/aaa/bbb",true)); + +// console.log(cq.music("qq",123)); +// console.log(cqStr.music("163")); + +// console.log(cq.json({"a": 1})); +// console.log(cqStr.json("{\"a\": 1}")); + +// console.log(cq.toString({ +// type: "at", +// data: { +// qq: "all", +// text: "@全体成员" +// } +// })); + +// console.log(cq.toString([ +// { +// type: "at", +// data: { +// qq: "all", +// text: "@全体成员" +// } +// }, +// { +// type: "image", +// data: { +// file: "[123456&&&,,,]" +// } +// }, +// ])); +// } +// test(); + +module.exports = { + cq, cqStr, +}; From f1387d607f18b3d47b89752939c8973639b4c59a Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 7 Mar 2021 02:23:20 +0900 Subject: [PATCH 05/20] rename --- client.d.ts | 2 +- lib/common.js | 13 ++++++++----- lib/core.js | 2 +- lib/message/chat.js | 2 +- lib/message/{recv.js => parser.js} | 0 lib/online-push.js | 4 ++-- package.json | 3 ++- util.js | 2 +- 8 files changed, 16 insertions(+), 12 deletions(-) rename lib/message/{recv.js => parser.js} (100%) diff --git a/client.d.ts b/client.d.ts index 76d0fb19..bd22f2a8 100644 --- a/client.d.ts +++ b/client.d.ts @@ -665,7 +665,7 @@ export namespace cqStr { function ptt(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): string; function location(lat: number, lng: number, address: string, id?: string): string; function music(type: "qq" | "163", id: number): string; - function json(data: any): string; + function json(data: string): string; function xml(data: string, type?: number): string; function share(url: string, title: string, image?: string, content?: string): string; function shake(): string; diff --git a/lib/common.js b/lib/common.js index b68b0d1d..8d36b0c4 100644 --- a/lib/common.js +++ b/lib/common.js @@ -17,12 +17,15 @@ function checkUin(uin) { } function uinAutoCheck(group_id, user_id) { group_id = parseInt(group_id); - if (arguments.length == 2) - user_id = parseInt(user_id); - else - user_id = 12345; - if (!checkUin(group_id) || !checkUin(user_id)) + if (!checkUin(group_id)) { throw new Error("bad group_id or user_id"); + } + if (user_id) { + user_id = parseInt(user_id); + if (!checkUin(user_id)) { + throw new Error("bad group_id or user_id"); + } + } return [group_id, user_id]; } diff --git a/lib/core.js b/lib/core.js index 387b3fbe..2dd6edb6 100644 --- a/lib/core.js +++ b/lib/core.js @@ -7,7 +7,7 @@ const Readable = require("stream").Readable; const common = require("./common"); const pb = require("./pb"); const jce = require("./jce"); -const { parseC2CMsg } = require("./message/recv"); +const { parseC2CMsg } = require("./message/parser"); const push = require("./online-push"); const sysmsg = require("./sysmsg"); const BUF0 = Buffer.alloc(0); diff --git a/lib/message/chat.js b/lib/message/chat.js index 52d7eab0..dfb85cc9 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -3,7 +3,7 @@ const zlib = require("zlib"); const crypto = require("crypto"); const { Builder } = require("./builder"); const { getOneC2CMsg, getOneGroupMsg } = require("./history"); -const { parseC2CMsg, parseGroupMsg } = require("./recv"); +const { parseC2CMsg, parseGroupMsg } = require("./parser"); const { highwayUpload } = require("../service"); const common = require("../common"); const pb = require("../pb"); diff --git a/lib/message/recv.js b/lib/message/parser.js similarity index 100% rename from lib/message/recv.js rename to lib/message/parser.js diff --git a/lib/online-push.js b/lib/online-push.js index c064c470..86e85d17 100644 --- a/lib/online-push.js +++ b/lib/online-push.js @@ -1,8 +1,8 @@ "use strict"; const pb = require("./pb"); const jce = require("./jce"); -const { parseGroupMsg, parseDiscussMsg } = require("./message/recv"); -const { genC2CMessageId, genGroupMessageId, log } = require("./common"); +const { parseGroupMsg, parseDiscussMsg } = require("./message/parser"); +const { genC2CMessageId, genGroupMessageId } = require("./common"); /** * OnlinePush回执 diff --git a/package.json b/package.json index 38e27ce8..3e56868c 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "/device.js", "/exception.js", "/client.d.ts", - "/test.js" + "/test.js", + "/util.js" ] } diff --git a/util.js b/util.js index 6125d8ba..3c162214 100644 --- a/util.js +++ b/util.js @@ -1,5 +1,5 @@ "use strict"; -const { genCQMsg } = require("./lib/message/recv"); +const { genCQMsg } = require("./lib/message/parser"); const cq = {}; const cqStr = {}; From 15236667f96799b7e8e90e8ee4d857819e0bdd45 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 7 Mar 2021 11:16:33 +0900 Subject: [PATCH 06/20] add reply function --- README.md | 10 +++------- client.d.ts | 1 + lib/core.js | 1 + lib/online-push.js | 2 ++ 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f290e8de..bdf6e59f 100644 --- a/README.md +++ b/README.md @@ -26,18 +26,14 @@ const uin = 123456789; // your account const bot = createClient(uin); //监听并输入滑动验证码ticket -bot.on("system.login.slider", (data) => { +bot.on("system.login.slider", () => { process.stdin.once("data", (input) => { bot.sliderLogin(input); }); }); -bot.on("message", (data) => { - if (data.group_id > 0) - bot.sendGroupMsg(data.group_id, "hello"); - else - bot.sendPrivateMsg(data.user_id, "hello"); -}); +//回复消息 +bot.on("message", (data) => data.reply("hello world")); bot.login("password"); // your password or password_md5 ``` diff --git a/client.d.ts b/client.d.ts index bd22f2a8..d92fdadb 100644 --- a/client.d.ts +++ b/client.d.ts @@ -374,6 +374,7 @@ interface MessageEventData extends CommonEventData { message_id: string, user_id: number, font: string, + reply: (message: string | MessageElem[], auto_escape?: boolean) => Promise>, } export interface PrivateMessageEventData extends MessageEventData { sender: FriendInfo, diff --git a/lib/core.js b/lib/core.js index 2dd6edb6..e04bcc85 100644 --- a/lib/core.js +++ b/lib/core.js @@ -140,6 +140,7 @@ async function getMsg(sync_flag = 0) { try { const data = await parseC2CMsg.call(this, msg, true); if (data && data.raw_message) { + data.reply = (message, auto_escape = false) => this.sendPrivateMsg(data.user_id, message, auto_escape); this.logger.info(`recv from: [Private: ${data.user_id}(${data.sub_type})] ` + data.raw_message); this.em("message.private." + data.sub_type, data); } diff --git a/lib/online-push.js b/lib/online-push.js index 86e85d17..edb40ba9 100644 --- a/lib/online-push.js +++ b/lib/online-push.js @@ -478,6 +478,7 @@ async function onGroupMsg(blob, seq) { if (data && data.raw_message) { if (data.user_id === this.uin && this.config.ignore_self) return; + data.reply = (message, auto_escape = false) => this.sendGroupMsg(data.group_id, message, auto_escape); data.message_id = message_id; const sender = data.sender; this.logger.info(`recv from: [Group: ${data.group_name}(${data.group_id}), Member: ${sender.card ? sender.card : sender.nickname}(${data.user_id})] ` + data.raw_message); @@ -502,6 +503,7 @@ async function onDiscussMsg(blob, seq) { if (data && data.raw_message) { if (data.user_id === this.uin && this.config.ignore_self) return; + data.reply = (message, auto_escape = false) => this.sendDiscussMsg(data.discuss_id, message, auto_escape); const sender = data.sender; this.logger.info(`recv from: [Discuss: ${data.discuss_name}(${data.discuss_id}), Member: ${sender.card}(${data.user_id})] ` + data.raw_message); this.em("message.discuss", data); From 4ecbb1680f1c85da6e12f989ffb75a238466be60 Mon Sep 17 00:00:00 2001 From: takayama Date: Thu, 11 Mar 2021 20:14:37 +0900 Subject: [PATCH 07/20] =?UTF-8?q?add=20sendTempMsg=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.d.ts | 17 +- client.js | 3 + docs/api.md | 170 +------------ docs/event.md | 269 +------------------- docs/project.md | 103 -------- lib/message/builder.js | 565 +++++++++++++++++++++++++++++++++++++---- lib/message/chat.js | 361 ++------------------------ lib/ref.d.ts | 2 + 8 files changed, 553 insertions(+), 937 deletions(-) delete mode 100644 docs/project.md diff --git a/client.d.ts b/client.d.ts index d92fdadb..9b1cc715 100644 --- a/client.d.ts +++ b/client.d.ts @@ -374,7 +374,7 @@ interface MessageEventData extends CommonEventData { message_id: string, user_id: number, font: string, - reply: (message: string | MessageElem[], auto_escape?: boolean) => Promise>, + reply: (message: MessageElem | Iterable | string, auto_escape?: boolean) => Promise>, } export interface PrivateMessageEventData extends MessageEventData { sender: FriendInfo, @@ -521,9 +521,10 @@ export class Client extends events.EventEmitter { getGroupInfo(group_id: number, no_cache?: boolean): Promise>; getGroupMemberInfo(group_id: number, user_id: number, no_cache?: boolean): Promise>; - sendPrivateMsg(user_id: number, message: MessageElem[] | string, auto_escape?: boolean): Promise>; - sendGroupMsg(group_id: number, message: MessageElem[] | string, auto_escape?: boolean): Promise>; - sendDiscussMsg(discuss_id: number, message: MessageElem[] | string, auto_escape?: boolean): Promise; + sendPrivateMsg(user_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; + sendGroupMsg(group_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; + sendTempMsg(group_id: number, user_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; + sendDiscussMsg(discuss_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise; deleteMsg(message_id: string): Promise; getMsg(message_id: string): Promise>; @@ -647,7 +648,7 @@ export namespace cq { //转换到CQ码字符串 function toString(elem: MessageElem): string; - function toString(elems: MessageElem[]): string; + function toString(elems: Iterable): string; } /** @@ -661,9 +662,9 @@ export namespace cqStr { function bface(file: string): string; function rps(id?: number): string; function dice(id?: number): string; - function image(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): string; - function flash(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): string; - function ptt(file: Buffer | Uint8Array | string, cache?: boolean, timeout?: number, headers: OutgoingHttpHeaders, proxy?: boolean): string; + function image(file: string, cache?: boolean, timeout?: number, headers: string, proxy?: boolean): string; + function flash(file: string, cache?: boolean, timeout?: number, headers: string, proxy?: boolean): string; + function ptt(file: string, cache?: boolean, timeout?: number, headers: string, proxy?: boolean): string; function location(lat: number, lng: number, address: string, id?: string): string; function music(type: "qq" | "163", id: number): string; function json(data: string): string; diff --git a/client.js b/client.js index 589a6deb..e26ba88c 100644 --- a/client.js +++ b/client.js @@ -524,6 +524,9 @@ class AndroidClient extends Client { async sendDiscussMsg(discuss_id, message = "", auto_escape = false) { return await this.useProtocol(chat.sendMsg, [discuss_id, message, auto_escape, 2]); } + async sendTempMsg(group_id, user_id, message = "", auto_escape = false) { + return await this.useProtocol(chat.sendTempMsg, arguments); + } async deleteMsg(message_id) { return await this.useProtocol(chat.recallMsg, arguments); } diff --git a/docs/api.md b/docs/api.md index aed5075a..226ea89a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,169 +1 @@ -# API - -+ [启动-创建实例](#createclientuinconfig) -+ [系统类API](#系统类API) -+ [应用类API](#应用类API) - + [获取列表和资料](#获取好友群群员列表和资料) - + [发消息和撤回](#发消息和撤回) - + [群操作](#群操作踢人禁言退群设置等) - + [好友操作](#加群加好友删好友邀请好友点赞) - + [设置状态和资料](#设置状态和资料) - + [其他](#其他) - -**可以直接照头文件 [client.d.ts](../client.d.ts)** - -> 使用 VScode 编辑器可以获得完整的智能提示 - ----- - -## `createClient(uin[,config])` - -+ *`uin`* \ -+ *`config`* \ - -创建一个client实例: - -```js -const oicq = require("oicq"); -const uin = 123456789, config = {}; -const client = oicq.createClient(uin, config); -``` - - > 关于config请参考头文件中的 [ConfBot](../client.d.ts#ConfBot) - ----- - -## 系统类API - -### `client.login(password)` 密码登陆 - -+ *`password`* \ 支持传递明文或md5后的密码 - -### `client.captchaLogin(captcha)` 提交图片验证码 - -+ *`captcha`* \ 4个字母 - -### `client.sliderLogin(ticket)` 提交滑动验证码 - -+ *`ticket`* \ 在浏览器滑动后,响应结果中的ticket - -### `client.logout()` 先下线再关闭连接 - -### `client.terminate()` 直接关闭连接 - ----- - -## 应用类API - -所有API都会返回以下格式的JSON对象 - -```js -{ - retcode: 0, //0成功 1状态未知 100参数错误 102失败 103超时 104断线中 - status: "ok", //ok或async或failed - data: null, //数据,只有获取列表以及发消息会返回message_id,其他时候为null - error: {code: -1, message: ""}, //TX返回的错误代码和错误消息 -} -``` - ----- - -### 获取好友、群、群员列表和资料 - -+ `client.getFriendList()` -+ `client.getStrangerList()` -+ `client.getGroupList()` -+ async `client.getGroupMemberList(group_id[, no_cache])` -+ async `client.getGroupInfo(group_id[, no_cache])` - + 返回值参照 [GroupInfo](../client.d.ts#GroupInfo) -+ async `client.getGroupMemberInfo(group_id, user_id[, no_cache])` - + 返回值参照 [MemberInfo](../client.d.ts#MemberInfo) -+ async `client.getStrangerInfo(user_id[, no_cache])` - + 返回值参照 [StrangerInfo](../client.d.ts#StrangerInfo) - ----- - -### 发消息和撤回 - -message可以使用 `Array` 格式或 `String` 格式,支持CQ码 -参考 [消息段类型](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md) - -+ async `client.sendPrivateMsg(user_id, message[, auto_escape])` 返回message_id -+ async `client.sendGroupMsg(group_id, message[, auto_escape])` 返回message_id -+ async `client.sendDiscussMsg(discuss_id, message[, auto_escape])` -+ async `client.deleteMsg(message_id)` -+ async `client.getMsg(message_id)` - - > auto_escape参数:是否原样输出CQ码(既不解析),默认false - ----- - -### 处理申请和邀请 - -+ async `client.setFriendAddRequest(flag[, approve, remark, block])` block默认false -+ async `client.setGroupAddRequest(flag[, approve, reason, block])` block默认false - ----- - -### 群操作(踢人、禁言、退群、设置等) - -+ async `client.setGroupKick(group_id, user_id[, reject_add_request])` -+ async `client.setGroupBan(group_id, user_id[, duration])` -+ async `client.setGroupLeave(group_id[, is_dismiss])` -+ async `client.setGroupCard(group_id, user_id[, card])` -+ async `client.setGroupName(group_id, group_name)` -+ async `client.setGroupAdmin(group_id, user_id[, enable])` -+ async `client.setGroupSpecialTitle(group_id, user_id[, special_title, duration])` -+ async `client.sendGroupNotice(group_id, content)` -+ async `client.sendGroupPoke(group_id, user_id)` -+ async `client.setGroupAnonymous(group_id[, enable])` -+ async `client.setGroupWholeBan(group_id[, enable])` -+ async `client.setGroupAnonymousBan(group_id, flag[, duration])` - ----- - -### 加群加好友、删好友、邀请好友、点赞 - - > 注意:加群加好友本身是风险接口,频繁调用会被风控(表现为几天内别人无法看到你的请求) - -+ async `client.addGroup(group_id[, comment])` -+ async `client.addFriend(group_id, user_id[, comment])` -+ async `client.deleteFriend(user_id[, block])` block默认true -+ async `client.inviteFriend(group_id, user_id)` -+ async `client.sendLike(user_id[, times])` times默认1,不能大于20 - ----- - -### 设置状态和资料 - -+ async `client.setOnlineStatus(status)` 设置在线状态,仅支持手机协议 - + `status` 允许的值:11我在线上 31离开 41隐身 50忙碌 60Q我吧 70请勿打扰 -+ async `client.setNickname(nickname)` 设置昵称 -+ async `client.setGender(gender)` 0未知 1男 2女 -+ async `client.setBirthday(birthday)` 20110202的形式 -+ async `client.setDescription([description])` 设置个人说明 -+ async `client.setSignature([signature])` 设置个性签名 -+ async `client.setPortrait(file)` 设置个人头像(file为Buffer或图片CQ码中相同格式的字符串) -+ async `client.setGroupPortrait(group_id, file)` 设置群头像 - ----- - -### 其他 - -+ async `client.getCookies([domain])` -+ async `client.getCsrfToken()` -+ async `client.cleanCache([type])` -+ `client.canSendImage()` -+ `client.canSendRecord()` -+ `client.getStatus()` 该函数返回一些有用的统计信息 -+ `client.getVersionInfo()` -+ `client.getLoginInfo()` - ----- - -### 重载好友列表、群列表 - - > 注意:一旦调用,重载完成之前bot不接受其他任何请求,也不会上报任何事件 - -+ async `client.reloadFriendList()` -+ async `client.reloadGroupList()` +请访问 [https://github.com/takayama-lily/oicq/wiki/91.API文档](https://github.com/takayama-lily/oicq/wiki/91.API%E6%96%87%E6%A1%A3) \ No newline at end of file diff --git a/docs/event.md b/docs/event.md index 8938b20d..8b14a46c 100644 --- a/docs/event.md +++ b/docs/event.md @@ -1,268 +1 @@ -# 事件 - -一级事件共有以下四类 - -+ [Event: system](#Event-system) 系统类(如上下线、验证码、设备锁等) -+ [Event: message](#Event-message) 聊天消息类 -+ [Event: request](#Event-request) 请求类 -+ [Event: notice](#Event-notice) 通知类 - ----- - -实例化client后,使用 `client.on()` 来监听一个事件: - -```js -//examples: -client.on("system.login.slider", (data)=>console.log(data)); //监听登陆时的验证码事件 -client.on("message", (data)=>console.log(data)); //监听所有的消息事件 -client.on("message.group", (data)=>console.log(data)); //监听群消息事件 -client.on("request", (data)=>console.log(data)); //监听所有的请求事件 -client.on("request.group.invite", (data)=>console.log(data)); //监听群邀请事件 -client.on("notice", (data)=>console.log(data)); //监听所有的通知事件 -client.on("notice.group.increase", (data)=>console.log(data)); //监听成员入群事件 -``` - -> 使用 VScode 编辑器可以获得完整的智能提示 - -事件为连续传递,例如触发 `request.group.add` 事件后,会按顺序触发 `request.group` 事件和 `request` 事件 -任意监听都可以捕获该事件,事件可以重复监听 (继承 `EventEmitter`) - -事件使用cqhttp风格命名和参数,所有事件数据都为json对象,并包含以下共通字段: - -+ `post_type` 一级分类 system, message, request, notice -+ `{post_type}_type` 二级分类 如:login, online, offline, group, friend, private 等 -+ `sub_type` 三级分类 如:captcha, add, invite 等,有时会没有 -+ `self_id` bot自己的号码 -+ `time` unix时间戳 - -之后只列出其他非共通的字段。 - ----- - -## Event: `system` - -系统类事件 - -+ `system.login` - + `system.login.captcha` 收到图片验证码事件 - + *`image`* Buffer 图片字节集 - + `system.login.slider` 收到滑动验证码事件 - + *`url`* string 滑动地址 - + `system.login.device` 需要解设备锁 - + *`url`* string 设备锁验证地址 - + `system.login.error` 其他原因导致登陆失败 - + *`message`* string "密码错误"等 - + *`code`* number 错误码 - -+ `system.online` 上线事件,可以开始处理消息 - -+ `system.offline` 下线事件 - + `system.offline.network` 网络断线事件 (见相关配置 `reconn_interval`) - + `system.offline.kickoff` 被踢下线 (将配置中的`kickoff`设置为true会在3秒后重新登陆) - + `system.offline.frozen` 被冻结事件 - + `system.offline.device` 由于开启设备锁,需要重新验证 - + `system.offline.unknown` 未知事件(目前尚未遇到过,如果你遇到了请告诉我) - + *`message`* string 下线原因 - -> 一般情况下,掉线后内部会自动完成重连或重新登录,开发者无需自己处理,但仍然会上报`offline`事件 -> `offline`和`online`事件是成对出现的,触发过`offline`后重新登录成功后必然会再次触发`online`事件 -> 除非遇到了无法处理的事件,比如被冻结等 - ----- - -## Event: `message` - -消息类事件 - -+ **message.private** - - + `message.private.friend` 好友消息 - + `message.private.single` 单向好友消息 - + `message.private.group` 群临时会话 - + `message.private.other` 其他临时会话 - + *`message_id`* - + *`user_id`* - + *`font`* - + *`message`* 数组格式的消息 - + *`raw_message`* 字符串格式的消息(CQ码已转义) - + *`sender`* - + *`user_id`* - + *`nickname`* - + *`remark`* - + *`sex`* - + *`age`* - + *`area`* - + *`auto_reply`* 是否是自动回复(boolean) - -+ **message.group** - - + `message.group.normal` 群普通消息 - + `message.group.anonymous` 群匿名消息 - + *`message_id`* - + *`group_id`* - + *`group_name`* - + *`user_id`* - + *`anonymous`* 非匿名消息时为null - + *`id`* - + *`name`* - + *`flag`* - + *`font`* - + *`message`* - + *`raw_message`* - + *`sender`* - + *`user_id`* - + *`nickname`* - + *`card`* - + *`sex`* - + *`age`* - + *`area`* - + *`level`* - + *`role`* - + *`title`* - + `message.discuss` 讨论组消息 - + *`discuss_id`* - + *`discuss_name`* - + *`user_id`* - + *`font`* - + *`message`* - + *`raw_message`* - + *`sender`* - + *`user_id`* - + *`nickname`* - + *`card`* - ----- - -## Event: `request` - -请求类事件 - -+ **request.friend** - - + `request.friend.add` 好友请求 - + *`user_id`* - + *`nickname`* - + *`source`* 来源("QQ群-xxx"或"QQ查找"等) - + *`comment`* 附加信息 - + *`sex`* - + *`age`* - + *`flag`* 用于处理请求时传入 - -+ **request.group** - - + `request.group.add` 加群申请 - + *`group_id`* - + *`group_name`* - + *`user_id`* - + *`nickname`* - + *`comment`* - + *`inviter_id`* - + *`flag`* - - + `request.group.invite` 加群邀请 - + *`group_id`* - + *`group_name`* - + *`user_id`* - + *`nickname`* - + *`role`* 邀请者的权限("admin"或"member") - + *`flag`* - ----- - -## Event: `notice` - -通知类事件 - -> 为了统一风格,notice事件的命名和原版cqhttp有一定出入 - -+ **notice.friend** - - + `notice.friend.increase` 好友增加 - + *`user_id`* - + *`nickname`* - - + `notice.friend.decrease` 好友减少(被拉黑或自己删除都会触发) - + *`user_id`* - + *`nickname`* - - + `notice.friend.recall` 消息撤回事件 - + *`user_id`* - + *`message_id`* - - + `notice.friend.profile` 好友资料变更 - + *`user_id`* - + *`nickname`* - + *`signature`* - - + `notice.friend.poke` 好友戳一戳事件 - + *`operator_id`* 操作者 - + *`user_id`* 目标 - + *`action`* 动作名 - + *`suffix`* 动作后缀 - -+ **notice.group** - - + `notice.group.increase` 群员增加 - + *`group_id`* - + *`user_id`* - + *`nickname`* - - + `notice.group.decrease` 群员减少 - + *`group_id`* - + *`operator_id`* - + *`user_id`* - + *`dismiss`* 是否是解散(boolean型) - + *`member`* 该群员资料 - - + `notice.group.recall` 群消息撤回事件 - + *`group_id`* - + *`operator_id`* - + *`user_id`* - + *`message_id`* - - + `notice.group.admin` 管理变更事件 - + *`group_id`* - + *`user_id`* - + *`set`* boolean型 - - + `notice.group.ban` 群禁言事件 - + *`group_id`* - + *`operator_id`* - + *`user_id`* 匿名用户为80000000 - + *`nickname`* 匿名用户才有这个字段 - + *`duration`* 时间(0为解禁) - - + `notice.group.transfer` 群转让事件 - + *`group_id`* - + *`operator_id`* 旧群主 - + *`user_id`* 新群主 - - + `notice.group.title` 群头衔变更事件 - + *`group_id`* - + *`user_id`* - + *`nickname`* - + *`title`* - - + `notice.group.poke` 群戳一戳事件 - + *`group_id`* - + *`operator_id`* 操作者 - + *`user_id`* 目标 - + *`action`* 动作名 - + *`suffix`* 动作后缀 - - + `notice.group.setting` 群设置变更事件,以下带有enable的字段都为 `boolean` - + *`enable_guest`* 允许游客进入 - + *`enable_anonymous`* 允许匿名 - + *`enable_upload_album`* 允许群员上传相册 - + *`enable_upload_file`* 允许群员上传文件 - + *`enable_temp_chat`* 允许临时会话 - + *`enable_new_group`* 允许发起新群聊 - + *`enable_show_honor`* 展示群互动标识(龙王等) - + *`enable_show_level`* 展示群等级 - + *`enable_show_title`* 展示群头衔 - + *`enable_confess`* 开启坦白说 - + *`group_name`* 群名也是变更对象 - + *`group_id`* - + *`user_id`* 操作者不明的时候为 -1 - ----- +请访问 [https://github.com/takayama-lily/oicq/wiki/92.事件文档](https://github.com/takayama-lily/oicq/wiki/92.%E4%BA%8B%E4%BB%B6%E6%96%87%E6%A1%A3) \ No newline at end of file diff --git a/docs/project.md b/docs/project.md deleted file mode 100644 index c8573a76..00000000 --- a/docs/project.md +++ /dev/null @@ -1,103 +0,0 @@ -# 已支持和尚未支持的功能 - -* ◯已支持 -* ✕尚未支持 - ----- - -|[消息]|文字和表情|长消息|图片|语音|合并转发|匿名| -|-|-|-|-|-|-|-| -|好友|◯|◯|◯|◯|◯|| -|群聊|◯|◯|◯|◯|◯|◯| -|讨论组|◯|◯|◯|◯|◯|| -|临时会话|◯|◯|◯||||| - ----- - -|[好友功能]|API|事件| -|-|-|-| -|好友列表|◯|◯(好友增减)| -|陌生人列表|◯|| -|处理申请|◯|◯| -|撤回消息|◯|◯| -|点赞|◯|| -|加群员好友|◯|| -|删除好友|◯|◯| - - ----- - -|[群功能]|API|事件| -|-|-|-| -|群列表|◯|◯(群增减)| -|成员列表|◯|◯(成员增减)| -|踢人|◯|◯| -|禁言|◯|◯| -|撤回|◯|◯| -|修改名片|◯|| -|修改群名|◯|◯| -|修改群头像|◯|| -|发群公告|◯|◯| -|其他设置|✕|◯| -|群文件|✕|◯| -|设置头衔|◯|◯| -|设置管理|◯|◯| -|创建|✕|✕| -|转让|✕|◯| -|解散|◯|◯| -|退群|◯|◯| -|群邀请|◯|◯| -|群申请|◯|◯| -|邀请好友|◯|| -|添加群|◯|| -|戳一戳|◯|◯| - ----- - -|[个人]|| -|-|-| -|设置QQ状态|◯| -|修改昵称|◯| -|修改性别|◯| -|修改生日|◯| -|修改个人说明|◯| -|修改签名|◯| -|修改头像|◯| -|获取cookies|◯| - ----- - -## 什么是CQ码? - -CQ码是指字符串格式下用于表示多媒体内容的方式,形如: -`[CQ:image,file=123.jpg]` -`[CQ:at,qq=123456]` -常用的有at、表情、图片、语音、音乐、分享等。 -因此混在字符串中的CQ码,以下字符会被转义:`[]&,` -推荐使用数组格式的消息链,可以避免转义操作。 - -|[CQ码]|收|发|说明| -|-|-|-|-| -|at|◯|◯|[CQ:at,qq=123456]| -|face|◯|◯|[CQ:face,id=104]| -|bface|◯|◯|原创表情,[CQ:bface,file=xxxxxxxx,text=摸头]| -|dice&rps|◯|◯|骰子和猜拳:
[CQ:dice,id=1]
[CQ:rps,id=1]| -|image|◯|◯|参考 [图片](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%9B%BE%E7%89%87)| -|record|◯|◯|语音,写法同image
支持任何格式的音频自动转amr(必须将 [ffmpeg](http://ffmpeg.org/download.html) 加入环境变量path)
linux下的ffmpeg不自带amr解码器,可能需要自行编译ffmpeg| -|flash|◯|◯|闪照,写法同image| -|anonymous||◯|发匿名,[CQ:anonymous,ignore=1]
ignore为0时匿名失败不发送| -|file|◯|✕|群文件| -|music|json|◯|[CQ:music,type=qq,id=xxxxxx]
[CQ:music,type=163,id=xxxxxx]| -|location|json|◯|[CQ:location,address=江西省九江市修水县,lat=29.063940,lng=114.339610]| -|reply|◯|◯|[CQ:reply,id=xxxxxx] -|shake|◯|◯|[CQ:shake] -|poke|◯|◯|[CQ:poke,type=6] 暂时支持0~6,可以在群里发 -|xml&json|◯|◯|可用于接收群公告等消息。封杀比较严重,不推荐发原生。 -|share|xml|◯|链接分享 -|video|✕|✕| -|node|✕|◯|合并转发[CQ:node,id=xxxxxx][CQ:node,id=xxxxxx]
关于自定义转发内容,由于可以达到伪造消息的效果,可能有一定争议,暂不实现| - -完整内容可参考[此文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md) - -> 另可参考头文件 [client.d.ts](../client.d.ts) -> 使用 VScode 编辑器可以获得完整的智能提示 diff --git a/lib/message/builder.js b/lib/message/builder.js index 8e117666..854bdcf4 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -1,5 +1,6 @@ "use strict"; const zlib = require("zlib"); +const { randomBytes } = require("crypto"); const music = require("./music"); const face = require("./face"); const { getOneC2CMsg, getOneGroupMsg } = require("./history"); @@ -7,9 +8,24 @@ const { genPttElem } = require("./ptt"); const { ImageBuilder, uploadImages } = require("./image"); const pb = require("../pb"); const common = require("../common"); -const { parseC2CMessageId, parseGroupMessageId, genMessageUuid } = common; +const { highwayUpload } = require("../service"); +const { parseC2CMessageId, parseGroupMessageId, genMessageUuid, genC2CMessageId } = common; const EMOJI_NOT_ENDING = ["\uD83C", "\uD83D", "\uD83E", "\u200D"]; const EMOJI_NOT_STARTING = ["\uFE0F", "\u200D", "\u20E3"]; +const PB_CONTENT = pb.encode({ 1: 1, 2: 0, 3: 0 }); +const PB_RESERVER = pb.encode({ + 37: { + 17: 0, + 19: { + 15: 0, + 31: 0, + 41: 0 + }, + } +}); +const TYPES = { + 0: "Private", 1: "Group", 2: "Discuss" +}; function unescapeCQ(s) { if (s === "[") return "["; @@ -52,36 +68,153 @@ async function getAnonInfo(group_id) { return anon; } +/** + * @this {import("../ref").Client} + * @param {number} target + * @param {Buffer} compressed + * @returns {Promise} resid + */ +async function uploadMultiMsg(target, compressed) { + const body = pb.encode({ + 1: 1, + 2: 5, + 3: 9, + 4: 3, + 5: this.apk.version, + 6: [{ + 1: target, + 2: compressed.length, + 3: common.md5(compressed), + 4: 3, + 5: 0, + }], + 8: 1, + }); + const blob = await this.sendUni("MultiMsg.ApplyUp", body); + const rsp = pb.decode(blob)[2]; + if (rsp[1] > 0) + throw new Error(); + const buf = pb.encode({ + 1: 1, + 2: 5, + 3: 9, + 4: [{ + //1: 3, + 2: target, + 4: compressed, + 5: 2, + 6: rsp[3].raw, + }], + }); + const o = { + buf: buf, + md5: common.md5(buf), + key: rsp[10].raw + }; + const ip = Array.isArray(rsp[4]) ? rsp[4][0] : rsp[4], + port = Array.isArray(rsp[5]) ? rsp[5][0] : rsp[5]; + await highwayUpload.call(this, ip, port, o, 27); + return rsp[2].raw; +} + const AT_BUF = Buffer.from([0, 1, 0, 0, 0]); const BUF1 = Buffer.from([1]); const BUF2 = Buffer.alloc(2); const FACE_OLD_BUF = Buffer.from([0x00, 0x01, 0x00, 0x04, 0x52, 0xCC, 0xF5, 0xD0]); +/** + * @type {import("../ref")} + */ class Builder { + + /** + * 连续节点 + * @private + * @type {import("../ref").RichMsg[2]} + */ elems = []; + + /** + * 排他节点 + * @private + * @type {import("../ref").RichMsg[2][]} + */ + elems2 = []; + + /** + * 语音节点 + * @private + * @type {import("../ref").RichMsg[4][]} + */ ptts = []; - flashs = []; - jsons = []; - xmls = []; - b77 = []; - anon; - stat = { - length: 0, - at_cnt: 0, - img_cnt: 0, - face_cnt: 0, - sface_cnt: 0, - bface_cnt: 0, - }; + + /** + * b77节点 + * @private + * @type {Buffer[]} + */ + b77s = []; + + /** + * 匿名节点 + * @private + * @type {Buffer} + */ + anon = undefined; + + /** + * 回复节点 + * @private + * @type {Buffer} + */ + reply = undefined; + + /** + * 连续节点数量 + * @private + */ length = 0; + + /** + * 文本长度 + * @private + */ + size = 0; + + /** + * 异步任务 + * @private + * @type {Promise[]} + */ tasks = []; + + /** + * 未完成的图片 + * @private + * @type {ImageBuilder[]} + */ imgs = []; + + /** + * 转发节点 + * @private + * @type {Buffer[]} + */ nodes = []; - reply = false; /** + * 发送路径 + * @private + * @type {Buffer} + */ + routing; + seq = 0; + random = 0; + + /** + * @public * @param {import("../ref").Client} c - * @param {Number} target + * @param {number} target * @param {0|1|2} type //0私聊 1群聊 2讨论组 */ constructor(c, target, type) { @@ -90,6 +223,11 @@ class Builder { this.type = type; } + /** + * @private + * @param {string} text + * @param {Buffer} attr6 + */ buildTextElem(text, attr6 = null) { if (text || attr6) { text = String(text); @@ -120,9 +258,14 @@ class Builder { } }); } - this.stat.length += text.length; + this.length += text.length; } } + + /** + * @private + * @param {import("../ref").AtElem["data"]} cq + */ buildAtElem(cq) { let { qq, text, dummy } = cq; if (qq === "all") { @@ -146,8 +289,12 @@ class Builder { buf.writeUInt8(display.length), buf.writeUInt8(flag, 1), buf.writeUInt32BE(q, 2); const attr6 = Buffer.concat([AT_BUF, buf, BUF2]); this.buildTextElem(display, attr6); - ++this.stat.at_cnt; } + + /** + * @private + * @param {import("../ref").FaceElem["data"]} cq + */ buildFaceElem(cq) { let { id, text } = cq; id = parseInt(id); @@ -163,7 +310,6 @@ class Builder { 11: FACE_OLD_BUF } }); - ++this.stat.face_cnt; } else { if (face.map[id]) text = face.map[id]; @@ -180,9 +326,13 @@ class Builder { 3: 1 } }); - ++this.stat.sface_cnt; } } + + /** + * @private + * @param {import("../ref").FaceElem["data"]} cq + */ buildSFaceElem(cq) { let { id, text } = cq; if (!text) @@ -195,9 +345,13 @@ class Builder { 2: 1, } }); - ++this.stat.sface_cnt; this.buildTextElem(text); } + + /** + * @private + * @param {import("../ref").BfaceElem["data"]} cq + */ buildBFaceElem(cq) { try { var { file, text } = cq; @@ -218,12 +372,17 @@ class Builder { if (cq.magic && cq.magic instanceof Buffer) o[12] = cq.magic; this.elems.push({ 6: o }); - ++this.stat.bface_cnt; this.buildTextElem(text); } catch { this.c.logger.warn("不正确的原创表情(bface)file: " + file); } } + + /** + * @private + * @param {import("../ref").MfaceElem["type"]} type + * @param {import("../ref").MfaceElem["data"]} cq + */ buildMagicFaceElem(type, cq) { const rand = (a, b) => Math.floor(Math.random() * (b - a) + a); if (type === "dice") { @@ -242,6 +401,10 @@ class Builder { } } + /** + * @private + * @param {import("../ref").ImgPttElem["data"]} cq + */ async buildImageElem(cq) { const img = new ImageBuilder(this.c, !this.type); await img.buildNested(cq); @@ -256,7 +419,7 @@ class Builder { 3: 0, } }; - this.flashs.push([ + this.elems2.push([ elem, { 1: { @@ -266,7 +429,6 @@ class Builder { ]); } else { const elem = this.type ? { 8: img.nested } : { 4: img.nested }; - ++this.stat.img_cnt; this.elems.push(elem); } if (img.task) @@ -275,6 +437,10 @@ class Builder { this.imgs.push(img); } + /** + * @private + * @param {import("../ref").ImgPttElem["data"]} cq + */ async buildPttElem(cq) { try { const elem = await genPttElem.call(this.c, this.type == 1 ? this.target : 1, cq); @@ -284,17 +450,24 @@ class Builder { } } + /** + * @private + * @param {import("../ref").VideoElem["data"]} cq + */ buildVideoElem(cq) { let file = String(cq.file); if (!file.startsWith("protobuf://")) { return this.c.logger.warn("尚未支持的file类型:" + file); } - this.elems.push({ + this.elems2.push([{ 19: Buffer.from(file.replace("protobuf://", ""), "base64") - }); - this.stat.length++; + }]); } + /** + * @private + * @param {import("../ref").LocationElem["data"]} cq + */ buildLocationElem(cq) { let { address, lat, lng, name, id, lon, title, content } = cq; if (!lng) lng = lon; @@ -323,17 +496,25 @@ class Builder { this.buildJsonElem(obj, "收到[[应用]地图]消息,请升级QQ版本查看"); } + /** + * @private + * @param {import("../ref").MusicElem["data"]} cq + */ async buildMusicElem(cq) { const { type, id } = cq; try { const buf = await music.build(this.target, type, id, this.type); - this.b77.push(buf); + this.b77s.push(buf); } catch (e) { this.c.logger.debug(e); this.c.logger.warn(`音乐获取失败:type=${type},id=${id}`); } } + /** + * @private + * @param {import("../ref").ShareElem["data"]} cq + */ buildShareElem(cq) { let { url, title, content, image } = cq; if (!url || !title) { @@ -347,7 +528,12 @@ class Builder { this.buildXmlElem(xml, 1, url); } - buildJsonElem(obj, text) { + /** + * @private + * @param {any} obj + * @param {string} text + */ + buildJsonElem(obj, text = "") { if (typeof obj !== "string") obj = JSON.stringify(obj); const elems = [{ @@ -362,9 +548,16 @@ class Builder { } }); } - this.jsons.push(elems); + this.elems2.push(elems); } - buildXmlElem(xml, svcid, text) { + + /** + * @private + * @param {string} xml + * @param {number} svcid + * @param {string} text + */ + buildXmlElem(xml, svcid = 60, text = "") { svcid = parseInt(svcid); const elems = [{ 12: { @@ -379,9 +572,13 @@ class Builder { } }); } - this.xmls.push(elems); + this.elems2.push(elems); } + /** + * @private + * @param {import("../ref").AnonymousElem["data"]} cq + */ async buildAnonElem(cq) { if (this.anon !== undefined) return; @@ -411,6 +608,10 @@ class Builder { }); } + /** + * @private + * @param {import("../ref").ReplyElem["data"]} cq + */ async buildReplyElem(cq) { if (this.reply) return; @@ -442,7 +643,7 @@ class Builder { } catch { return this.c.logger.warn("incorrect reply id: " + id); } - this.elems.unshift({ + this.reply = pb.encode({ 45: { 1: [seq], 2: user_id, @@ -456,19 +657,25 @@ class Builder { 10: this.type ? common.code2uin(this.target) : this.c.uin } }); - this.reply = true; } + /** + * @private + */ buildShakeElem() { - this.elems.push({ + this.elems2.push([{ 17: { 1: 0, 2: 0, 3: this.target, } - }); - ++this.stat.length; + }]); } + + /** + * @private + * @param {import("../ref").PokeElem["data"]} cq + */ buildPokeElem(cq) { let { type } = cq; type = parseInt(type); @@ -479,16 +686,19 @@ class Builder { 7: 0, 10: 0, }; - this.elems.push({ + this.elems.push([{ 53: { 1: 2, 2: nested, 3: type, } - }); - ++this.stat.length; + }]); } + /** + * @private + * @param {import("../ref").NodeElem["data"]} cq + */ buildNodeElem(cq) { const { id } = cq; const task = (async () => { @@ -505,7 +715,9 @@ class Builder { } /** - * @param {import("../../client").MessageElem} + * @private + * @param {import("../ref").MessageElem["type"]} type + * @param {import("../ref").MessageElem["data"]} data */ async buildElem(type, data) { if (!data) @@ -582,8 +794,9 @@ class Builder { } /** - * @param {import("../../client").MessageElem[]|String} message - * @param {Boolean} escape + * @private + * @param {string} message + * @param {boolean} escape */ async buildFromString(message, escape) { if (escape) @@ -607,27 +820,275 @@ class Builder { } /** - * @param {import("../../client").MessageElem[]|String} message - * @param {Boolean} escape + * @public + * @param {import("../ref").MessageElem[]|string} message + * @param {boolean} escape */ - async exec(message, escape) { + async buildAndSend(message, escape) { + if (typeof message[Symbol.iterator] === "function" && typeof message !== "string") { for (let v of message) { if (!v || !v.type) continue; await this.buildElem(v.type, v.data); } + } else if (typeof message === "object" && message !== null && message.type) { + await this.buildElem(message.type, message.data); } else if (message) { await this.buildFromString(String(message), escape); } await Promise.all(this.tasks); this.nodes = this.nodes.filter(v => v); // 去除空值 await uploadImages.call(this.c, this.target, this.imgs, !this.type); - this.length = this.stat.length + - this.stat.at_cnt * 22 + - this.stat.face_cnt * 23 + - this.stat.sface_cnt * 42 + - this.stat.bface_cnt * 135 + - this.stat.img_cnt * (this.type ? 90 : 295); + + this.setRouting(); + + const tasks = []; + for (let buf of this.b77s) { + tasks.push(this.sendB77(buf)); + } + + for (let ptt of this.ptts) { + tasks.push(this.send({ 2: [], 4: ptt })); + } + + for (let elems of this.elems2) { + tasks.push(this.send({ 2: elems })); + } + + if (this.nodes.length > 0) { + const elems = await this.toForwardMsgElems(); + tasks.push(this.send({ 2: elems })); + } + + if (tasks.length > 0) + var rsp = await Promise.race(tasks); + + if (!this.elems.length) { + if (rsp) return rsp; + throw new Error("empty message"); + } + if (this.reply) + this.elems.unshift(this.reply); + return await this.send({ 2: this.elems }, true); + } + + /** + * @private + */ + async setRouting() { + if (this.routing) { + return; + } + let routing; + if (this.type > 0) { + routing = this.type === 1 ? { 2: { 1: this.target } } : { 4: { 1: this.target } }; + } else { + let user_id = this.target; + routing = { 1: { 1: user_id } }; + if (this.c.sl.has(user_id)) { + try { + const group_id = this.c.sl.get(user_id).group_id; + if (group_id && (await this.c.getGroupMemberInfo(group_id, user_id)).data) + routing = { + 3: { + 1: common.code2uin(group_id), + 2: user_id, + } + }; + } catch (e) { } + } else if (!this.c.fl.has(user_id)) { + for (const [k, v] of this.c.gml) { + if (v instanceof Map && v.has(user_id)) { + routing = { + 3: { + 1: common.code2uin(k), + 2: user_id, + } + }; + break; + } + } + } + } + this.routing = pb.encode(routing); + } + + /** + * @private + * @param {import("../ref").RichMsg} rich + * @param {Buffer} content + */ + buildPbSendMsgPkt(rich, content = PB_CONTENT) { + this.seq = this.c.seq_id + 1; + this.random = randomBytes(4).readUInt32BE(); + if (this.anon) + rich[2].push(this.anon); + rich[2].push(PB_RESERVER); + return pb.encode({ + 1: this.routing, + 2: content, + 3: { 1: rich }, + 4: this.seq, + 5: this.random, + 6: this.type > 0 ? null : this.c.buildSyncCookie(), + 8: 0 + }); + } + + /** + * @private + * @param {import("../ref").RichMsg} rich + * @param {boolean} flag + */ + async send(rich, flag = false) { + ++this.c.stat.sent_msg_cnt; + const body = this.buildPbSendMsgPkt(rich); + const event_id = `interval.${this.target}.${this.random}`; + let message_id = ""; + this.c.once(event_id, (id) => message_id = id); + try { + var blob = await this.c.sendUni("MessageSvc.PbSendMsg", body); + } finally { + this.c.removeAllListeners(event_id); + } + const rsp = pb.decode(blob); + const retcode = rsp[1]; + if (retcode !== 0) { + let emsg = rsp[2] ? String(rsp[2].raw) : ""; + this.c.logger.error(`send failed: [${TYPES[this.type]}: ${this.target}] ${emsg}(${retcode})`); + return { result: retcode, emsg }; + } + if (retcode === 0) { + if (this.type === 0) { //私聊 + message_id = genC2CMessageId(this.target, this.seq, this.random, rsp[3]); + } + if (this.type === 1 && !message_id) { //群聊 + message_id = await this.waitForMessageId(this.c.config.resend ? 500 : 5000); + if (!message_id) { + if (this.length <= 80) { + const emsg = "群消息可能发送失败,请检查消息内容。"; + this.c.logger.error(`send failed: [Group: ${this.target}] ` + emsg); + return { result: -1, emsg }; + } + if (flag && this.c.config.resend) { + this.c.logger.warn("群消息被风控,将尝试使用分片发送。"); + return await this.sendByFrag(); + } else { + const emsg = "群消息被风控,发送失败。"; + this.c.logger.error(`send failed: [Group: ${this.target}] ` + emsg); + return { result: -1, emsg }; + } + } + } + this.c.logger.info(`send to: [${TYPES[this.type]}: ${this.target} / message_id: ${message_id}]`); + return { result: 0, data: { message_id } }; + } + } + + /** + * @private + */ + async sendByFrag() { + this.elems.pop(); + + const fragments = []; + let fragment = []; + for (let elem of this.elems) { + fragment.push(elem); + if (elem[1] && !elem[1][3]) { //1:text 1[3]:at + fragment.push({ + 37: PB_RESERVER + }); + fragments.push(fragment); + fragment = []; + } + } + if (fragment.length > 0) { + fragment.push(PB_RESERVER); + fragments.push(fragment); + } + + let n = 0; + const div = randomBytes(2).readUInt16BE(); + for (let fragment of fragments) { + const content = pb.encode({ + 1: fragments.length, + 2: n++, + 3: div + }); + const body = this.buildPbSendMsgPkt({ 2: fragment }, content); + this.c.writeUni("MessageSvc.PbSendMsg", body); + } + let message_id = await this.waitForMessageId(5000); + if (!message_id) { + const emsg = "群分片消息可能发送失败,请检查消息内容。"; + this.c.logger.error(`send failed: [Group: ${this.target}] ` + emsg); + return { result: -1, emsg }; + } else { + this.c.logger.info(`send to: [Group: ${this.target} / message_id: ${message_id}]`); + return { result: 0, data: { message_id } }; + } + } + + /** + * @private + * @param {number} time + * @returns {Promise} message_id + */ + waitForMessageId(time) { + const event_id = `interval.${this.target}.${this.random}`; + return new Promise((resolve) => { + const timeout = setTimeout(() => { + this.c.removeAllListeners(event_id); + resolve(""); + }, time); + this.c.once(event_id, (id) => { + clearTimeout(timeout); + resolve(id); + }); + }); + } + + /** + * @private + * @param {Buffer} buf + */ + async sendB77(buf) { + ++this.c.stat.sent_msg_cnt; + await this.c.sendOidb("OidbSvc.0xb77_9", buf); + return { result: 0, data: { message_id: "" } }; + } + + /** + * @private + * @returns {Promise} + */ + async toForwardMsgElems() { + const compressed = zlib.gzipSync(pb.encode({ + 1: this.nodes, + 2: { + 1: "MultiMsg", + 2: { + 1: this.nodes + } + } + })); + try { + var resid = await uploadMultiMsg.call(this.c, this.target, compressed); + } catch (e) { + throw new Error("failed to upload forward msg"); + } + const preview = " 转发的聊天记录 "; + const template = ` + 转发的聊天记录 ${preview}
查看${this.nodes.length}条转发消息
`; + return [ + { + 12: { + 1: Buffer.concat([BUF1, zlib.deflateSync(template)]), + 2: 35, + }, + }, + ]; } } diff --git a/lib/message/chat.js b/lib/message/chat.js index dfb85cc9..00202c83 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -1,366 +1,52 @@ "use strict"; -const zlib = require("zlib"); -const crypto = require("crypto"); const { Builder } = require("./builder"); const { getOneC2CMsg, getOneGroupMsg } = require("./history"); const { parseC2CMsg, parseGroupMsg } = require("./parser"); -const { highwayUpload } = require("../service"); const common = require("../common"); const pb = require("../pb"); -const { genC2CMessageId, parseC2CMessageId, parseGroupMessageId, genMessageUuid } = common; -const BUF1 = Buffer.from([1]); -const PB_CONTENT = pb.encode({ 1: 1, 2: 0, 3: 0 }); -const PB_RESERVER = pb.encode({ - 17: 0, - 19: { - 15: 0, - 31: 0, - 41: 0 - }, -}); +const { parseC2CMessageId, parseGroupMessageId, genMessageUuid } = common; //send msg---------------------------------------------------------------------------------------------------- /** * @this {import("../ref").Client} - * @param {Number} target - * @param {import("../../client").MessageElem[]|String} message - * @param {Boolean} escape - * @param {0|1|2} type //0私聊 1群聊 2讨论组 - * @returns {import("../ref").ProtocolResponse} - */ -async function sendMsg(target, message, escape, type) { - [target] = common.uinAutoCheck(target); - const builder = new Builder(this, target, type); - await builder.exec(message, escape); - - const _sendMsg = async (rich, frag = false) => { - if (builder.anon) { - if (!rich[2]) - rich[2] = []; - rich[2].push(builder.anon); - } - ++this.stat.sent_msg_cnt; - if (frag && rich[2] && type === 1) { - rich[2].pop(); - return await sendGroupMsgByFrag.call(this, target, toFragments(rich[2])); - } else { - if (rich[2]) { - rich[2].push({ - 37: PB_RESERVER - }); - } - return await (type ? sendGroupMsg : sendPrivateMsg).call(this, target, rich, type); - } - }; - - let rsp; - for (const buf of builder.b77) { - rsp = await sendB77RichMsg.call(this, buf); - } - for (const elem of builder.ptts) { - rsp = await _sendMsg({ 4: elem }); - } - for (const elems of builder.flashs.concat(builder.jsons, builder.xmls)) { - rsp = await _sendMsg({ 2: elems }); - } - - if (builder.nodes.length > 0) { - const elems = await toForwardMsgElems.call(this, target, builder.nodes); - rsp = await _sendMsg({ 2: elems }); - } - - if (!builder.length) { - if (rsp) return rsp; - throw new Error("empty message"); - } - rsp = await _sendMsg({ 2: builder.elems }); - if (this.config.resend && rsp.data && rsp.data.message_id === "") { - if (builder.stat.length <= 80) { - const emsg = "群消息可能发送失败,请检查消息内容。"; - this.logger.error(`send failed: [Group: ${target}] ` + emsg); - return { result: -1, emsg }; - } - this.logger.warn("此消息将尝试使用分片发送。"); - return await _sendMsg({ 2: builder.elems }, true); - } - return rsp; -} - -/** - * @this {import("../ref").Client} + * @param {number} group_id + * @param {number} user_id + * @param {import("../ref").MessageElem[]|String} message + * @param {boolean} escape * @returns {import("../ref").ProtocolResponse} */ -async function sendPrivateMsg(user_id, rich) { - let routing = { 1: { 1: user_id } }; - if (this.sl.has(user_id)) { - try { - const group_id = this.sl.get(user_id).group_id; - if (group_id && (await this.getGroupMemberInfo(group_id, user_id)).data) - routing = { - 3: { - 1: common.code2uin(group_id), - 2: user_id, - } - }; - } catch (e) { } - } else if (!this.fl.has(user_id)) { - for (const [k, v] of this.gml) { - if (v instanceof Map && v.has(user_id)) { - routing = { - 3: { - 1: common.code2uin(k), - 2: user_id, - } - }; - break; - } +async function sendTempMsg(group_id, user_id, message, escape) { + [group_id, user_id] = common.uinAutoCheck(group_id, user_id); + const builder = new Builder(this, user_id, 0); + builder.routing = pb.encode({ + 3: { + 1: common.code2uin(group_id), + 2: user_id, } - } - const seq = this.seq_id; - const random = crypto.randomBytes(4).readUInt32BE(); - const body = pb.encode({ - 1: routing, - 2: PB_CONTENT, - 3: { 1: rich }, - 4: seq, - 5: random, - 6: this.buildSyncCookie(), - 8: 1, }); - const blob = await this.sendUni("MessageSvc.PbSendMsg", body); - const rsp = pb.decode(blob); - if (rsp[1] === 0) { - const message_id = genC2CMessageId(user_id, seq, random, rsp[3]); - this.logger.info(`send to: [Private: ${user_id} / message_id: ${message_id}]`); - return { result: 0, data: { message_id } }; - } - let emsg = rsp[2] ? String(rsp[2].raw) : ""; - this.logger.error(`send failed: [Private: ${user_id}] ${emsg}(${rsp[1]})`); - return { result: rsp[1], emsg }; -} - -/** - * @this {import("../ref").Client} - * @returns {import("../ref").ProtocolResponse} - */ -async function sendGroupMsg(target, rich, type) { - const routing = type === 1 ? { 2: { 1: target } } : { 4: { 1: target } }; - const random = crypto.randomBytes(4).readUInt32BE(); - const body = pb.encode({ - 1: routing, - 2: PB_CONTENT, - 3: { 1: rich }, - 4: this.seq_id + 1, - 5: random, - 8: 0 - }); - const event_id = `interval.${target}.${random}`; - let message_id = ""; - this.once(event_id, (id) => message_id = id); - try { - var blob = await this.sendUni("MessageSvc.PbSendMsg", body); - } catch (e) { - this.removeAllListeners(event_id); - throw e; - } - this.removeAllListeners(event_id); - const rsp = pb.decode(blob); - if (rsp[1] !== 0) { - let emsg = rsp[2] ? String(rsp[2].raw) : ""; - this.logger.error(`send failed: [Group: ${target}] ${emsg}(${rsp[1]})`); - return { result: rsp[1], emsg }; - } - if (type === 2) { - return { result: rsp[1] }; - } - if (!message_id) { - await new Promise((resolve) => { - const timeout = setTimeout(() => { - this.removeAllListeners(event_id); - resolve(); - }, this.config.resend ? 500 : 5000); - this.once(event_id, (id) => { - message_id = id; - clearTimeout(timeout); - resolve(); - }); - }); - } - this.logger.info(`send to: [Group: ${target} / message_id: ${message_id}]`); - if (!message_id) - this.logger.warn("生成message_id失败,此消息大概率被风控了。"); - return { result: 0, data: { message_id } }; + return await builder.buildAndSend(message, escape); } /** * @this {import("../ref").Client} + * @param {number} target + * @param {import("../ref").MessageElem[]|String} message + * @param {boolean} escape + * @param {0|1|2} type //0私聊 1群聊 2讨论组 * @returns {import("../ref").ProtocolResponse} */ -async function sendGroupMsgByFrag(group_id, fragments) { - const routing = { 2: { 1: group_id } }; - let n = 0; - const random = crypto.randomBytes(4).readUInt32BE(); - const div = crypto.randomBytes(2).readUInt16BE(); - for (let fragment of fragments) { - const body = pb.encode({ - 1: routing, - 2: { - 1: fragments.length, - 2: n, - 3: div - }, - 3: { 1: { 2: fragment } }, - 4: this.seq_id + 1, - 5: random, - 8: 0, - }); - ++n; - this.writeUni("MessageSvc.PbSendMsg", body); - } - const event_id = `interval.${group_id}.${random}`; - let message_id = ""; - await new Promise((resolve) => { - const timeout = setTimeout(() => { - this.removeAllListeners(event_id); - resolve(); - }, 3000); - this.once(event_id, (id) => { - message_id = id; - clearTimeout(timeout); - resolve(); - }); - }); - if (!message_id) { - const emsg = "群分片消息可能发送失败,请检查消息内容。"; - this.logger.error(`send failed: [Group: ${group_id}] ` + emsg); - return { result: -1, emsg }; - } - this.logger.info(`send to: [Group: ${group_id} / message_id: ${message_id}]`); - return { result: 0, data: { message_id } }; -} - -function toFragments(elems) { - const fragments = []; - let fragment = []; - for (let elem of elems) { - fragment.push(elem); - if (elem[1] && !elem[1][3]) { //1:text 1[3]:at - fragment.push({ - 37: PB_RESERVER - }); - fragments.push(fragment); - fragment = []; - } - } - if (fragment.length > 0) { - fragment.push({ - 37: PB_RESERVER - }); - fragments.push(fragment); - } - return fragments; -} - -/** - * @this {import("../ref").Client} - * @param {Buffer[]} nodes - */ -async function toForwardMsgElems(target, nodes) { - const compressed = zlib.gzipSync(pb.encode({ - 1: nodes, - 2: { - 1: "MultiMsg", - 2: { - 1: nodes - } - } - })); - try { - var resid = await uploadMultiMsg.call(this, target, compressed); - } catch (e) { - throw new Error("failed to upload forward msg"); - } - const preview = " 转发的聊天记录 "; - const template = ` - 转发的聊天记录 ${preview}
查看${nodes.length}条转发消息
`; - return [ - { - 12: { - 1: Buffer.concat([BUF1, zlib.deflateSync(template)]), - 2: 35, - }, - }, - { - 37: PB_RESERVER - }, - ]; -} - -/** - * @this {import("../ref").Client} - * @param {Number} target - * @param {Buffer} compressed - * @returns {Promise} resid - */ -async function uploadMultiMsg(target, compressed) { - const body = pb.encode({ - 1: 1, - 2: 5, - 3: 9, - 4: 3, - 5: this.apk.version, - 6: [{ - 1: target, - 2: compressed.length, - 3: common.md5(compressed), - 4: 3, - 5: 0, - }], - 8: 1, - }); - const blob = await this.sendUni("MultiMsg.ApplyUp", body); - const rsp = pb.decode(blob)[2]; - if (rsp[1] > 0) - throw new Error(); - const buf = pb.encode({ - 1: 1, - 2: 5, - 3: 9, - 4: [{ - //1: 3, - 2: target, - 4: compressed, - 5: 2, - 6: rsp[3].raw, - }], - }); - const o = { - buf: buf, - md5: common.md5(buf), - key: rsp[10].raw - }; - const ip = Array.isArray(rsp[4]) ? rsp[4][0] : rsp[4], - port = Array.isArray(rsp[5]) ? rsp[5][0] : rsp[5]; - await highwayUpload.call(this, ip, port, o, 27); - return rsp[2].raw; -} - -/** - * @this {import("../ref").Client} - */ -async function sendB77RichMsg(buf) { - try { - ++this.stat.sent_msg_cnt; - await this.sendOidb("OidbSvc.0xb77_9", buf); - } catch { } - return { result: 0, data: { message_id: "" } }; +async function sendMsg(target, message, escape, type) { + [target] = common.uinAutoCheck(target); + const builder = new Builder(this, target, type); + return await builder.buildAndSend(message, escape); } //recall---------------------------------------------------------------------------------------------------- /** * @this {import("../ref").Client} + * @param {string} message_id */ async function recallMsg(message_id) { let body; @@ -440,6 +126,7 @@ function buildRecallGroupMsgBody(message_id) { /** * @this {import("../ref").Client} + * @param {string} message_id */ async function getHistoryMsg(message_id) { if (message_id.length > 24) { @@ -472,5 +159,5 @@ async function getHistoryMsg(message_id) { } module.exports = { - sendMsg, recallMsg, getHistoryMsg + sendMsg, sendTempMsg, recallMsg, getHistoryMsg }; diff --git a/lib/ref.d.ts b/lib/ref.d.ts index b6344376..6ec718cd 100644 --- a/lib/ref.d.ts +++ b/lib/ref.d.ts @@ -184,3 +184,5 @@ export class Client extends oicq.Client { msgExists(from: number, type: number, seq: number, time: number): boolean; buildSyncCookie(): Buffer; } + +export * from '../client'; From 2aa312a36eff1414f88ded11d8f176a4c694ff59 Mon Sep 17 00:00:00 2001 From: takayama Date: Fri, 12 Mar 2021 14:45:15 +0900 Subject: [PATCH 08/20] add getSystemMsg api add actor_id --- client.d.ts | 7 ++- client.js | 27 ++++++--- lib/ref.d.ts | 1 + lib/sysmsg.js | 160 +++++++++++++++++++++++++++++++++----------------- 4 files changed, 130 insertions(+), 65 deletions(-) diff --git a/client.d.ts b/client.d.ts index 9b1cc715..1f77f3b1 100644 --- a/client.d.ts +++ b/client.d.ts @@ -360,12 +360,13 @@ export interface GroupAddEventData extends RequestEventData { group_id: number, group_name: string, comment: string, - inviter_id?: number, + inviter_id?: number, //邀请人 + actor_id?: number, //处理人 } export interface GroupInviteEventData extends RequestEventData { group_id: number, group_name: string, - role: GroupRole, + role: GroupRole, //邀请者权限 } interface MessageEventData extends CommonEventData { @@ -563,7 +564,7 @@ export class Client extends events.EventEmitter { // uploadGroupImages(group_id: number, images: ImgPttElem["data"][]): Promise>; //上传群图以备发送 // getSummaryCard(user_id: number): Promise>; //查看用户资料 // getForwardMsg(resid: string): Promise>; - // getSystemMsg(): Promise>>; + getSystemMsg(): Promise>>; getCookies(domain?: string): Promise>; getCsrfToken(): Promise>; diff --git a/client.js b/client.js index e26ba88c..7fa16a01 100644 --- a/client.js +++ b/client.js @@ -341,23 +341,29 @@ class AndroidClient extends Client { } } - em(name = "", data = {}) { + parseEventType(name = "") { const slice = name.split("."); const post_type = slice[0], sub_type = slice[2]; - const param = { + const data = { self_id: this.uin, time: timestamp(), - post_type: post_type + post_type: post_type, }; const type_name = slice[0] + "_type"; - param[type_name] = slice[1]; + data[type_name] = slice[1]; if (sub_type) - param.sub_type = sub_type; - Object.assign(param, data); - while (slice.length > 0) { - this.emit(slice.join("."), param); - slice.pop(); + data.sub_type = sub_type; + return data; + } + + em(name = "", data = {}) { + data = Object.assign(this.parseEventType(name), data); + let i = name.lastIndexOf("."); + while (i > -1) { + this.emit(name, data); + name = name.slice(0, i); } + this.emit(name, data); } msgExists(from, type, seq, time) { @@ -584,6 +590,9 @@ class AndroidClient extends Client { async setGroupAddRequest(flag, approve = true, reason = "", block = false) { return await this.useProtocol(sysmsg.groupAction, arguments); } + async getSystemMsg() { + return await this.useProtocol(sysmsg.getSysMsg, arguments); + } async addGroup(group_id, comment = "") { return await this.useProtocol(troop.addGroup, arguments); diff --git a/lib/ref.d.ts b/lib/ref.d.ts index 6ec718cd..d15e7f35 100644 --- a/lib/ref.d.ts +++ b/lib/ref.d.ts @@ -183,6 +183,7 @@ export class Client extends oicq.Client { em(name: string, data: object): void; msgExists(from: number, type: number, seq: number, time: number): boolean; buildSyncCookie(): Buffer; + parseEventType(name: string): oicq.CommonEventData; } export * from '../client'; diff --git a/lib/sysmsg.js b/lib/sysmsg.js index e1e5aab6..0d293acb 100644 --- a/lib/sysmsg.js +++ b/lib/sysmsg.js @@ -2,15 +2,15 @@ const pb = require("./pb"); /** - * @param {Number} user_id - * @param {BigInt|Number} seq + * @param {number} user_id + * @param {BigInt|number} seq */ function genFriendRequestFlag(user_id, seq) { return user_id.toString(16).padStart(8, "0") + seq.toString(16); } /** - * @param {String} flag + * @param {string} flag */ function parseFriendRequestFlag(flag) { const user_id = parseInt(flag.slice(0, 8), 16); @@ -19,9 +19,9 @@ function parseFriendRequestFlag(flag) { } /** - * @param {Number} user_id - * @param {Number} group_id - * @param {BigInt|Number} seq + * @param {number} user_id + * @param {number} group_id + * @param {BigInt|number} seq * @param {0|1} invite */ function genGroupRequestFlag(user_id, group_id, seq, invite) { @@ -31,7 +31,7 @@ function genGroupRequestFlag(user_id, group_id, seq, invite) { } /** - * @param {String} flag + * @param {string} flag */ function parseGroupRequestFlag(flag) { const user_id = parseInt(flag.slice(0, 8), 16); @@ -41,8 +41,51 @@ function parseGroupRequestFlag(flag) { return { user_id, group_id, seq, invite }; } +/** + * @param {import("./ref").Proto} proto + * @returns {import("./ref").FriendAddEventData} + */ +function parseFrdSysMsg(proto) { + const time = proto[4]; + const user_id = proto[5]; + const nickname = String(proto[50][51].raw); + const flag = genFriendRequestFlag(user_id, proto[3]); + const source = String(proto[50][5].raw); + const comment = String(proto[50][4].raw); + const sex = proto[50][67] === 0 ? "male" : (proto[50][67] === 1 ? "famale" : "unknown"); + const age = proto[50][68]; + return { user_id, nickname, source, comment, sex, age, flag, time }; +} + +/** + * @param {import("./ref").Proto} proto + * @returns {import("./ref").GroupAddEventData | import("./ref").GroupInviteEventData} + */ +function parseGrpSysMsg(proto) { + const type = proto[50][12]; + const time = proto[4]; + const group_id = proto[50][10]; + const group_name = String(proto[50][52].raw); + const data = { time, group_id, group_name }; + let invite = 0; + if (type === 2) { //invite + data.user_id = proto[50][11]; + data.nickname = String(proto[50][53].raw); + data.role = proto[50][13] === 1 ? "member" : "admin"; + invite = 1; + } else { //add + data.user_id = proto[5]; + data.nickname = String(proto[50][51].raw); + data.comment = String(proto[50][4].raw); + data.inviter_id = proto[50][11]; + data.actor_id = proto[50][16]; + } + data.flag = genGroupRequestFlag(data.user_id, group_id, proto[3], invite); + return data; +} + const frd_buf = pb.encode({ - 1: 10, + 1: 20, 4: 1000, 5: 2, 6: { @@ -66,24 +109,12 @@ async function getNewFriend() { try { const blob = await this.sendUni("ProfileService.Pb.ReqSystemMsgNew.Friend", frd_buf); const rsp = pb.decode(blob)[9]; - const v = Array.isArray(rsp) ? rsp[0] : rsp; - const time = v[4]; - const user_id = v[5]; - - if (this.msgExists(user_id, 187, v[3], time)) + const proto = Array.isArray(rsp) ? rsp[0] : rsp; + const data = parseFrdSysMsg(proto); + if (this.msgExists(data.user_id, 187, proto[3], data.time)) return; - - const nickname = String(v[50][51].raw); - const flag = genFriendRequestFlag(user_id, v[3]); - this.logger.info(`收到 ${user_id}(${nickname}) 的加好友请求 (flag: ${flag})`); - this.em("request.friend.add", { - user_id, nickname, - source: String(v[50][5].raw), - comment: String(v[50][4].raw), - sex: v[50][67] === 0 ? "male" : (v[50][67] === 1 ? "famale" : "unknown"), - age: v[50][68], - flag, time - }); + this.logger.info(`收到 ${data.user_id}(${data.nickname}) 的加好友请求 (flag: ${data.flag})`); + this.em("request.friend.add", data); } catch (e) { this.logger.debug("获取好友请求失败。"); this.logger.debug(e); @@ -92,7 +123,7 @@ async function getNewFriend() { const notify_types = { 84: 1, 87: 2, 525: 22 }; const grp_buf = pb.encode({ - 1: 10, + 1: 20, 4: 1000, 5: 3, 6: { @@ -140,35 +171,15 @@ async function getNewGroup(type) { } } if (!v) return; - const time = v[4]; - const group_id = v[50][10]; - - if (this.msgExists(group_id, type, v[3], time)) + const data = parseGrpSysMsg(v); + if (this.msgExists(data.group_id, type, v[3], data.time)) return; - if (type === 84 || type === 525) { - const user_id = v[5]; - const nickname = String(v[50][51].raw); - const group_name = String(v[50][52].raw); - const flag = genGroupRequestFlag(user_id, group_id, v[3], 0); - this.logger.info(`用户 ${user_id}(${nickname}) 请求加入群 ${group_id}(${group_name}) (flag: ${flag})`); - this.em("request.group.add", { - group_id, user_id, group_name, nickname, - comment: String(v[50][4].raw), - inviter_id: type === 525 ? v[50][11] : undefined, - flag, time - }); + this.logger.info(`用户 ${data.user_id}(${data.nickname}) 请求加入群 ${data.group_id}(${data.group_name}) (flag: ${data.flag})`); + this.em("request.group.add", data); } else if (type === 87) { - const user_id = v[50][11]; - const nickname = String(v[50][53].raw); - const group_name = String(v[50][52].raw); - const flag = genGroupRequestFlag(user_id, group_id, v[3], 1); - this.logger.info(`用户 ${user_id}(${nickname}) 邀请你加入群 ${group_id}(${group_name}) (flag: ${flag})`); - this.em("request.group.invite", { - group_id, user_id, group_name, nickname, - role: v[50][13] === 1 ? "member" : "admin", - flag, time - }); + this.logger.info(`用户 ${data.user_id}(${data.nickname}) 邀请你加入群 ${data.group_id}(${data.group_name}) (flag: ${data.flag})`); + this.em("request.group.invite", data); } } catch (e) { this.logger.debug("获取群请求失败。"); @@ -176,6 +187,49 @@ async function getNewGroup(type) { } } +/** + * 获取系统消息 + * @this {import("./ref").Client} + * @returns {import("./ref").ProtocolResponse} + */ +async function getSysMsg() { + const data = []; + + const frd_tsk = (async () => { + const blob = await this.sendUni("ProfileService.Pb.ReqSystemMsgNew.Friend", frd_buf); + let rsp = pb.decode(blob)[9]; + if (!Array.isArray(rsp)) + rsp = [rsp]; + for (let proto of rsp) { + data.push(Object.assign(this.parseEventType("request.friend.add"), parseFrdSysMsg(proto))); + } + })(); + + const grp_tsk = (async () => { + const blob = await this.sendUni("ProfileService.Pb.ReqSystemMsgNew.Group", grp_buf); + let rsp = pb.decode(blob)[10]; + if (!Array.isArray(rsp)) + rsp = [rsp]; + for (let proto of rsp) { + if (proto[50][1] !== 1) + continue; + const type = proto[50][12]; + let sub_type; + if (type === 1 || type === 22) { + sub_type = "add"; + } else if (type === 2) { + sub_type = "invite"; + } else { + continue; + } + data.push(Object.assign(this.parseEventType("request.group." + sub_type), parseGrpSysMsg(proto))); + } + })(); + + await Promise.all([frd_tsk, grp_tsk]); + return { result: 0, data }; +} + /** * 处理好友请求 * @this {import("./ref").Client} @@ -229,5 +283,5 @@ async function groupAction(flag, approve = true, reason = "", block = false) { } module.exports = { - getNewFriend, getNewGroup, friendAction, groupAction + getNewFriend, getNewGroup, friendAction, groupAction, getSysMsg }; From 0289204e63c3784d4838879ccd732fab146158c1 Mon Sep 17 00:00:00 2001 From: takayama Date: Fri, 12 Mar 2021 15:45:35 +0900 Subject: [PATCH 09/20] add getChatHistory, getForwardMsg api --- client.d.ts | 6 ++-- client.js | 8 ++++- lib/message/chat.js | 75 ++++++++++++++++++++++++++++-------------- lib/message/history.js | 30 +---------------- 4 files changed, 61 insertions(+), 58 deletions(-) diff --git a/client.d.ts b/client.d.ts index 1f77f3b1..5c26f712 100644 --- a/client.d.ts +++ b/client.d.ts @@ -528,6 +528,8 @@ export class Client extends events.EventEmitter { sendDiscussMsg(discuss_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise; deleteMsg(message_id: string): Promise; getMsg(message_id: string): Promise>; + getChatHistory(message_id: string, count?: number): Promise>; //获取msgid(包括自身)往前的count条消息 + getForwardMsg(resid: string): Promise>; sendGroupNotice(group_id: number, content: string): Promise; setGroupName(group_id: number, group_name: string): Promise; @@ -544,6 +546,7 @@ export class Client extends events.EventEmitter { setFriendAddRequest(flag: string, approve?: boolean, remark?: string, block?: boolean): Promise; setGroupAddRequest(flag: string, approve?: boolean, reason?: string, block?: boolean): Promise; + getSystemMsg(): Promise>>; addGroup(group_id: number, comment?: string): Promise; addFriend(group_id: number, user_id: number, comment?: string): Promise; @@ -559,12 +562,9 @@ export class Client extends events.EventEmitter { setGroupPortrait(group_id: number, file: Buffer | string): Promise; // getFile(fileid: string, busid?: string): Promise>; //用于下载链接失效后重新获取 - // getChatHistory(message_id: string, num?: number): Promise>; //获取msgid往前的num条消息 // uploadC2CImages(user_id: number, images: ImgPttElem["data"][]): Promise>; //上传好友图以备发送 // uploadGroupImages(group_id: number, images: ImgPttElem["data"][]): Promise>; //上传群图以备发送 // getSummaryCard(user_id: number): Promise>; //查看用户资料 - // getForwardMsg(resid: string): Promise>; - getSystemMsg(): Promise>>; getCookies(domain?: string): Promise>; getCsrfToken(): Promise>; diff --git a/client.js b/client.js index 7fa16a01..6f64e65d 100644 --- a/client.js +++ b/client.js @@ -537,7 +537,13 @@ class AndroidClient extends Client { return await this.useProtocol(chat.recallMsg, arguments); } async getMsg(message_id) { - return await this.useProtocol(chat.getHistoryMsg, arguments); + return await this.useProtocol(chat.getOneMsg, arguments); + } + async getChatHistory(message_id, count = 10) { + return await this.useProtocol(chat.getMsgs, arguments); + } + async getForwardMsg(resid) { + return await this.useProtocol(chat.getForwardMsg, arguments); } /////////////////////////////////////////////////// diff --git a/lib/message/chat.js b/lib/message/chat.js index 00202c83..26bb6b8f 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -1,6 +1,6 @@ "use strict"; const { Builder } = require("./builder"); -const { getOneC2CMsg, getOneGroupMsg } = require("./history"); +const { getC2CMsgs, getGroupMsgs } = require("./history"); const { parseC2CMsg, parseGroupMsg } = require("./parser"); const common = require("../common"); const pb = require("../pb"); @@ -127,37 +127,62 @@ function buildRecallGroupMsgBody(message_id) { /** * @this {import("../ref").Client} * @param {string} message_id + * @returns {import("../ref").ProtocolResponse} + */ +async function getOneMsg(message_id) { + const ret = await getMsgs.call(this, message_id, 1); + if (ret.data && ret.data.length) + return { result: 0, data: ret.data[0] }; + else + return { result: -1, emsg: "msg not exists" }; +} + +/** + * 获取从message_id(包括自身)往前的count条消息 + * @this {import("../ref").Client} + * @param {string} message_id + * @param {number} count + * @returns {import("../ref").ProtocolResponse} */ -async function getHistoryMsg(message_id) { +async function getMsgs(message_id, count = 10) { + + /** + * @type {import("../ref").Msg[]} + */ + let msgs, data = []; if (message_id.length > 24) { - const msg = await getOneGroupMsg.call(this, message_id); - try { - const data = await parseGroupMsg.call(this, msg); - data.post_type = "message"; - data.message_type = "group"; - data.message_id = message_id; - data.real_id = message_id; - return { result: 0, data }; - } catch (e) { - this.logger.debug(e); - return { result: -1, emsg: "failed to get group msg" }; + const { group_id, seq } = parseGroupMessageId(message_id); + let from_seq = seq - count; + if (from_seq <= 0) + from_seq = 1; + msgs = await getGroupMsgs.call(this, group_id, from_seq, seq); + // todo 分片处理 + for (let msg of msgs) { + data.push(Object.assign(this.parseEventType("message.group"), await parseGroupMsg.call(this, msg))); } } else { - const msg = await getOneC2CMsg.call(this, message_id); - try { - const data = await parseC2CMsg.call(this, msg); - data.post_type = "message"; - data.message_type = "private"; - data.message_id = message_id; - data.real_id = message_id; - return { result: 0, data }; - } catch (e) { - this.logger.debug(e); - return { result: -1, emsg: "failed to get c2c msg" }; + const { user_id, time, random } = parseC2CMessageId(message_id); + msgs = await getC2CMsgs.call(this, user_id, time, count); + for (let i = msgs.length - 1; i >= 0; --i) { + const msg = msgs[i]; + if (msg[3][1][1][3] !== random && !data.length) + continue; + data.unshift(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); } } + return { result: 0, data }; +} + +/** + * 获取转发消息 + * @this {import("../ref").Client} + * @param {string} resid + * @returns {import("../ref").ProtocolResponse} + */ +async function getForwardMsg(resid) { + return { result: -1, emsg: "not support yet" }; } module.exports = { - sendMsg, sendTempMsg, recallMsg, getHistoryMsg + sendMsg, sendTempMsg, recallMsg, getOneMsg, getMsgs, getForwardMsg }; diff --git a/lib/message/history.js b/lib/message/history.js index 330798e0..a8d24ae8 100644 --- a/lib/message/history.js +++ b/lib/message/history.js @@ -1,6 +1,5 @@ "use strict"; const pb = require("../pb"); -const { parseC2CMessageId, parseGroupMessageId } = require("../common"); /** * @this {import("../ref").Client} @@ -23,22 +22,6 @@ async function getC2CMsgs(user_id, time, num) { return Array.isArray(o[6]) ? o[6] : [o[6]]; } -/** - * @this {import("../ref").Client} - * @param {string} message_id - * @returns {Promise} - */ -async function getOneC2CMsg(message_id) { - const { user_id, time, random } = parseC2CMessageId(message_id); - const msgs = await getC2CMsgs.call(this, user_id, time, 10); - for (let i = msgs.length - 1; i >= 0; --i) { - const v = msgs[i]; - if (v[3][1] && v[3][1][1] && v[3][1][1][3] === random) - return v; - } - throw new Error("msg not found"); -} - /** * @this {import("../ref").Client} * @param {number} group_id @@ -60,17 +43,6 @@ async function getGroupMsgs(group_id, from_seq, to_seq) { return Array.isArray(o[6]) ? o[6] : [o[6]]; } -/** - * @this {import("../ref").Client} - * @param {string} message_id - * @returns {Promise} - */ -async function getOneGroupMsg(message_id) { - const { group_id, seq } = parseGroupMessageId(message_id); - const msgs = await getGroupMsgs.call(this, group_id, seq, seq); - return msgs[0]; -} - module.exports = { - getOneC2CMsg, getOneGroupMsg, getC2CMsgs, getGroupMsgs + getC2CMsgs, getGroupMsgs }; From 1ec252e5db06ad27749ed812a127eda26a0a4976 Mon Sep 17 00:00:00 2001 From: takayama Date: Fri, 12 Mar 2021 17:13:25 +0900 Subject: [PATCH 10/20] rename namespace --- client.d.ts | 10 +++++----- client.js | 6 +++--- util.js | 38 +++++++++++++++++++------------------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/client.d.ts b/client.d.ts index 5c26f712..298b31e0 100644 --- a/client.d.ts +++ b/client.d.ts @@ -625,7 +625,7 @@ export function createClient(uin: number, config?: ConfBot): Client; /** * 生成消息元素的快捷函数 */ -export namespace cq { +export namespace segment { function text(text: string): TextElem; function at(qq: number, text?: string, dummy?: boolean): AtElem; function face(id: number, text?: string): FaceElem; //经典表情 @@ -647,15 +647,15 @@ export namespace cq { function node(id: string): NodeElem; //转发节点 function anonymous(ignore?: boolean): AnonymousElem; //匿名 - //转换到CQ码字符串 - function toString(elem: MessageElem): string; - function toString(elems: Iterable): string; + //将元素转换到CQ码字符串 (CQ码字符串无法逆转换到元素,因为类型会丢失) + function toCqcode(elem: MessageElem): string; + function toCqcode(elems: Iterable): string; } /** * 生成CQ码字符串的快捷函数 */ -export namespace cqStr { +export namespace cqcode { function text(text: string): string; function at(qq: number, text?: string, dummy?: boolean): string; function face(id: number, text?: string): string; diff --git a/client.js b/client.js index 6f64e65d..307acf1b 100644 --- a/client.js +++ b/client.js @@ -20,7 +20,7 @@ const wt = require("./lib/wtlogin/wt"); const chat = require("./lib/message/chat"); const troop = require("./lib/troop"); const { getErrorMessage, TimeoutError } = require("./exception"); -const { cq, cqStr } = require("./util"); +const { segment, cqcode } = require("./util"); const BUF0 = Buffer.alloc(0); function buildApiRet(retcode, data = null, error = null) { @@ -771,6 +771,6 @@ function createClient(uin, config = {}) { } module.exports = { - createClient, setGlobalConfig, - cq, cqStr, + createClient, setGlobalConfig, Client: AndroidClient, + segment, cqcode, }; diff --git a/util.js b/util.js index 3c162214..41d17ac1 100644 --- a/util.js +++ b/util.js @@ -1,7 +1,7 @@ "use strict"; const { genCQMsg } = require("./lib/message/parser"); -const cq = {}; -const cqStr = {}; +const segment = {}; +const cqcode = {}; const elem_map = { text: ["text"], @@ -27,7 +27,7 @@ const elem_map = { }; for (const [type, params] of Object.entries(elem_map)) { - cq[type] = (...args) => { + segment[type] = (...args) => { const data = {}; for (let i = 0; i < params.length; ++i) { if (Reflect.has(args, i)) { @@ -38,15 +38,15 @@ for (const [type, params] of Object.entries(elem_map)) { type, data, }; }; - cqStr[type] = (...args) => { - return genCQMsg(cq[type](...args)); + cqcode[type] = (...args) => { + return genCQMsg(segment[type](...args)); }; } /** * @param {import("./client").MessageElem | import("./client").MessageElem[]} arg */ -cq.toString = (arg) => { +segment.toCqcode = (arg) => { if (typeof arg === "string") return arg; if (typeof arg[Symbol.iterator] === "function") { @@ -61,22 +61,22 @@ cq.toString = (arg) => { }; // function test() { -// console.log(cq.text("aaa")); -// console.log(cqStr.text("aaa")); +// console.log(segment.text("aaa")); +// console.log(cqcode.text("aaa")); -// console.log(cq.image("/aaa/bbb")); -// console.log(cqStr.image("/aaa/bbb")); +// console.log(segment.image("/aaa/bbb")); +// console.log(cqcode.image("/aaa/bbb")); -// console.log(cq.image("/aaa/bbb",1)); -// console.log(cqStr.image("/aaa/bbb",true)); +// console.log(segment.image("/aaa/bbb",1)); +// console.log(cqcode.image("/aaa/bbb",true)); -// console.log(cq.music("qq",123)); -// console.log(cqStr.music("163")); +// console.log(segment.music("qq",123)); +// console.log(cqcode.music("163")); -// console.log(cq.json({"a": 1})); -// console.log(cqStr.json("{\"a\": 1}")); +// console.log(segment.json({"a": 1})); +// console.log(cqcode.json("{\"a\": 1}")); -// console.log(cq.toString({ +// console.log(segment.toCqcode({ // type: "at", // data: { // qq: "all", @@ -84,7 +84,7 @@ cq.toString = (arg) => { // } // })); -// console.log(cq.toString([ +// console.log(segment.toCqcode([ // { // type: "at", // data: { @@ -103,5 +103,5 @@ cq.toString = (arg) => { // test(); module.exports = { - cq, cqStr, + segment, cqcode, }; From d98c577d8f3e4a0a1cfebf67f865a4d3db23e3ef Mon Sep 17 00:00:00 2001 From: takayama Date: Fri, 12 Mar 2021 19:24:24 +0900 Subject: [PATCH 11/20] add comments --- client.js | 4 ++++ device.js | 14 ++++++++++--- exception.js | 9 +++++++-- lib/common.js | 45 ++++++++++++++++++++++++++++++++++++++++++ lib/core.js | 12 +++++++++++ lib/friendlist.js | 13 ++++++++---- lib/jce.js | 11 +++++++---- lib/message/builder.js | 4 ++++ lib/message/chat.js | 4 ++++ lib/message/file.js | 11 +++++++---- lib/message/history.js | 3 +++ lib/message/image.js | 4 ++++ lib/message/music.js | 3 +++ lib/message/parser.js | 3 +++ lib/message/ptt.js | 5 +++++ lib/online-push.js | 4 ++++ lib/pb.js | 18 ++++++++++++++--- lib/service.js | 26 +++++++++++++----------- lib/sysmsg.js | 3 +++ lib/troop.js | 3 +++ lib/wtlogin/wt.js | 19 ++++++++++-------- util.js | 6 ++++++ 22 files changed, 185 insertions(+), 39 deletions(-) diff --git a/client.js b/client.js index 307acf1b..ca647acb 100644 --- a/client.js +++ b/client.js @@ -1,3 +1,7 @@ +/** + * 网络层(断线重连、心跳) + * api入口 + */ "use strict"; const version = require("./package.json"); version.app_name = version.name; diff --git a/device.js b/device.js index 55869534..a8d16e6e 100644 --- a/device.js +++ b/device.js @@ -1,3 +1,6 @@ +/** + * 设备文件和协议 + */ "use strict"; const fs = require("fs"); const path = require("path"); @@ -40,6 +43,9 @@ function genIMEI() { return imei + calcSP(imei); } +/** + * @param {string} filepath + */ function genDevice(filepath) { const device = `{ "--begin--": "修改后可能需要重新验证设备。", @@ -67,7 +73,7 @@ function genDevice(filepath) { } /** - * @param {String} filepath + * @param {string} filepath * @returns {import("./lib/ref").Device} */ function getDeviceInfo(filepath) { @@ -111,6 +117,9 @@ function getDeviceInfo(filepath) { return device; } +/** + * @type {{[k: number]: import("./lib/ref").ApkInfo}} + */ const apk = { //android phone 1: { @@ -156,8 +165,7 @@ apk[5] = { ...apk[2] }; apk[5].subid = 537065739; /** - * @param {Number} platform - * @returns {import("./lib/ref").ApkInfo} + * @param {number} platform */ function getApkInfo(platform) { return apk[platform] ? apk[platform] : apk[2]; diff --git a/exception.js b/exception.js index 6577c3b8..5663ad74 100644 --- a/exception.js +++ b/exception.js @@ -1,7 +1,13 @@ +/** + * 错误码和错误消息 + */ "use strict"; const troop = require("./lib/troop"); const chat = require("./lib/message/chat"); +/** + * @type {Map} + */ const exceptions = new Map([ [troop.kickMember, { 2: "权限不足" @@ -32,8 +38,7 @@ class TimeoutError extends Error { } /** * @param {Function} fn - * @param {Number} code - * @returns {String} + * @param {number} code */ function getErrorMessage(fn, code) { if (!exceptions.has(fn)) diff --git a/lib/common.js b/lib/common.js index 8d36b0c4..1ae20144 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,3 +1,6 @@ +/** + * 通用函数 + */ "use strict"; const crypto = require("crypto"); const util = require("util"); @@ -15,6 +18,11 @@ const md5 = (data) => crypto.createHash("md5").update(data).digest(); function checkUin(uin) { return uin >= 10000 && uin <= 0xffffffff; } + +/** + * @param {number} group_id + * @param {number} user_id + */ function uinAutoCheck(group_id, user_id) { group_id = parseInt(group_id); if (!checkUin(group_id)) { @@ -29,6 +37,9 @@ function uinAutoCheck(group_id, user_id) { return [group_id, user_id]; } +/** + * @param {number} groupCode + */ function code2uin(groupCode) { let left = parseInt(groupCode / 1000000); if (left >= 0 && left <= 10) @@ -47,6 +58,10 @@ function code2uin(groupCode) { left += 3890; return left * 1000000 + groupCode % 1000000; } + +/** + * @param {number} groupUin + */ function uin2code(groupUin) { let left = parseInt(groupUin / 1000000); if (left >= 202 && left <= 212) @@ -70,6 +85,12 @@ function log(any) { console.log(util.inspect(any, { depth: 20, showHidden: false, maxArrayLength: 1000, maxStringLength: 5000 })); } +/** + * @param {number} user_id + * @param {number} seq + * @param {number} random + * @param {number} time + */ function genC2CMessageId(user_id, seq, random, time) { const buf = Buffer.allocUnsafe(16); buf.writeUInt32BE(user_id); @@ -78,6 +99,10 @@ function genC2CMessageId(user_id, seq, random, time) { buf.writeUInt32BE(time, 12); return buf.toString("base64"); } + +/** + * @param {string} message_id + */ function parseC2CMessageId(message_id) { const buf = Buffer.from(message_id, "base64"); const user_id = buf.readUInt32BE(), @@ -86,6 +111,15 @@ function parseC2CMessageId(message_id) { time = buf.readUInt32BE(12); return { user_id, seq, random, time }; } + +/** + * @param {number} group_id + * @param {number} user_id + * @param {number} seq + * @param {number} random + * @param {number} time + * @param {number} pktnum + */ function genGroupMessageId(group_id, user_id, seq, random, time, pktnum = 1) { const buf = Buffer.allocUnsafe(21); buf.writeUInt32BE(group_id); @@ -96,6 +130,10 @@ function genGroupMessageId(group_id, user_id, seq, random, time, pktnum = 1) { buf.writeUInt8(pktnum > 1 ? pktnum : 1, 20); return buf.toString("base64"); } + +/** + * @param {string} message_id + */ function parseGroupMessageId(message_id) { const buf = Buffer.from(message_id, "base64"); const group_id = buf.readUInt32BE(), @@ -106,10 +144,17 @@ function parseGroupMessageId(message_id) { pktnum = buf.length === 21 ? buf.readUInt8(20) : 1; return { group_id, user_id, seq, random, time, pktnum }; } + +/** + * @param {number} random + */ function genMessageUuid(random) { return 16777216n << 32n | BigInt(random); } +/** + * @param {Buffer} buf + */ function parseFunString(buf) { if (buf[0] === 0xa) { let res = ""; diff --git a/lib/core.js b/lib/core.js index e04bcc85..382307ba 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,3 +1,8 @@ +/** + * 数据包解析 + * 系统事件处理 + * 好友消息入口 + */ "use strict"; const fs = require("fs"); const path = require("path"); @@ -205,6 +210,10 @@ function onPushDomain(blob, seq) { // common.log(blob.toString("hex").replace(/(.)(.)/g, '$1$2 ')); } +/** + * @param {Buffer} buf + * @returns {{seq: number, cmd: string, payload: Buffer}} + */ function parseSSO(buf) { const stream = Readable.from(buf, { objectMode: false }); stream.read(0); @@ -242,6 +251,9 @@ function parseSSO(buf) { }; } +/** + * @type {{[k: string]: (this: import("./ref").Client, payload: Buffer, seq?: number) => void}} + */ const events = { "OnlinePush.PbPushGroupMsg": push.onGroupMsg, "OnlinePush.PbPushDisMsg": push.onDiscussMsg, diff --git a/lib/friendlist.js b/lib/friendlist.js index b6a40012..6ad83a78 100644 --- a/lib/friendlist.js +++ b/lib/friendlist.js @@ -1,3 +1,8 @@ +/** + * 好友列表 + * 群列表,群资料 + * 群员列表,群员资料 + */ "use strict"; const common = require("./common"); const pb = require("./pb"); @@ -15,10 +20,10 @@ const group_role_map = { /** * @this {import("./ref").Client} - * @param {Number} start - * @param {Number} limit - * @param {Map} tmp - * @returns {Number} 好友总数 + * @param {number} start + * @param {number} limit + * @param {Map} tmp + * @returns {number} 好友总数 */ async function _initFL(start, limit, tmp) { const d50 = pb.encode({ diff --git a/lib/jce.js b/lib/jce.js index c72c279e..3f117d79 100644 --- a/lib/jce.js +++ b/lib/jce.js @@ -1,3 +1,6 @@ +/** + * jce组包解包 + */ "use strict"; const jce = require("jce"); const WRAPPER = { @@ -26,8 +29,8 @@ function decodeWrapper(blob) { } /** - * @param {Object} map - * @param {Object} extra + * @param {object} map + * @param {typeof WRAPPER} extra */ function encodeWrapper(map, extra) { const body = { @@ -47,8 +50,8 @@ function encodeWrapper(map, extra) { } /** - * @param {Object|Array} nested - * @param {Object|undefined} struct + * @param {object|Array} nested + * @param {object|undefined} struct */ function encodeStruct(nested, struct) { return jce.encode([jce.encodeNested(nested, struct)]); diff --git a/lib/message/builder.js b/lib/message/builder.js index 854bdcf4..9f0e4056 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -1,3 +1,7 @@ +/** + * 构建消息节点 + * 消息发送 + */ "use strict"; const zlib = require("zlib"); const { randomBytes } = require("crypto"); diff --git a/lib/message/chat.js b/lib/message/chat.js index 26bb6b8f..46a36000 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -1,3 +1,7 @@ +/** + * api入口 + * 发送,撤回,获取聊天记录,获取转发消息 + */ "use strict"; const { Builder } = require("./builder"); const { getC2CMsgs, getGroupMsgs } = require("./history"); diff --git a/lib/message/file.js b/lib/message/file.js index d57ff54b..d0daf899 100644 --- a/lib/message/file.js +++ b/lib/message/file.js @@ -1,11 +1,14 @@ +/** + * 群文件、离线文件相关 + */ "use strict"; const pb = require("../pb"); /** * @this {import("../ref").Client} - * @param {Number} group_id - * @param {Number} busid - * @param {Buffer|String} fileid + * @param {number}} group_id + * @param {number} busid + * @param {Buffer|string} fileid */ async function getGroupFileUrl(group_id, busid, fileid) { const body = pb.encode({ @@ -22,7 +25,7 @@ async function getGroupFileUrl(group_id, busid, fileid) { /** * @this {import("../ref").Client} - * @param {Buffer|String} fileid + * @param {Buffer|string} fileid */ async function getC2CFileUrl(fileid) { const body = pb.encode({ diff --git a/lib/message/history.js b/lib/message/history.js index a8d24ae8..5bb9724d 100644 --- a/lib/message/history.js +++ b/lib/message/history.js @@ -1,3 +1,6 @@ +/** + * 聊天记录获取协议 + */ "use strict"; const pb = require("../pb"); diff --git a/lib/message/image.js b/lib/message/image.js index e2c8c045..be1e2a30 100644 --- a/lib/message/image.js +++ b/lib/message/image.js @@ -1,3 +1,7 @@ +/** + * 构造图片节点 + * 上传图片 + */ "use strict"; const fs = require("fs"); const path = require("path"); diff --git a/lib/message/music.js b/lib/message/music.js index 852ce6fc..e12a2962 100644 --- a/lib/message/music.js +++ b/lib/message/music.js @@ -1,3 +1,6 @@ +/** + * 构造音乐分享 + */ "use strict"; const { URL } = require("url"); const http = require("http"); diff --git a/lib/message/parser.js b/lib/message/parser.js index ea969b8c..4f6b838d 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -1,3 +1,6 @@ +/** + * 解析消息节点 + */ "use strict"; const http = require("http"); const https = require("https"); diff --git a/lib/message/ptt.js b/lib/message/ptt.js index 14eed909..0930fe41 100644 --- a/lib/message/ptt.js +++ b/lib/message/ptt.js @@ -1,3 +1,8 @@ +/** + * 构造语音节点 + * 上传语音 + * 音频转换 + */ "use strict"; const fs = require("fs"); const path = require("path"); diff --git a/lib/online-push.js b/lib/online-push.js index edb40ba9..1b0cf335 100644 --- a/lib/online-push.js +++ b/lib/online-push.js @@ -1,3 +1,7 @@ +/** + * 群消息事件入口 + * 好友事件和群事件(禁言、踢人等) + */ "use strict"; const pb = require("./pb"); const jce = require("./jce"); diff --git a/lib/pb.js b/lib/pb.js index b7b61210..580c9136 100644 --- a/lib/pb.js +++ b/lib/pb.js @@ -1,3 +1,6 @@ +/** + * protobuf组包解包 + */ "use strict"; const pb = require("protobufjs/light"); @@ -6,6 +9,10 @@ function isBuffer(buf) { } class Nested { + /** + * @param {Buffer} bytes + * @param {import("./ref").Proto} decoded + */ constructor(bytes, decoded) { if (decoded) Reflect.setPrototypeOf(this, decoded); @@ -14,7 +21,9 @@ class Nested { } /** - * @param {import("protobufjs").Writer} writer + * @param {pb.Writer} writer + * @param {number} tag + * @param {any} value */ function _encode(writer, tag, value) { if (value === null || value === undefined) @@ -69,6 +78,9 @@ function encode(o) { return writer.finish(); } +/** + * @param {pb.Long} long + */ function long2int(long) { const bigint = (BigInt(long.high) << 32n) | (BigInt(long.low) & 0xffffffffn); const int = parseInt(long); @@ -118,8 +130,8 @@ function decode(buf) { } /** - * @param {String} cmd example: OidbSvc.0x568_22 - * @param {Buffer|object} body + * @param {string} cmd example: OidbSvc.0x568_22 + * @param {Buffer|import("./ref").Proto} body */ function encodeOIDB(cmd, body) { cmd = cmd.replace("OidbSvc.", "").replace("oidb_", "").split("_"); diff --git a/lib/service.js b/lib/service.js index 0ddf0cb8..31d00a66 100644 --- a/lib/service.js +++ b/lib/service.js @@ -1,3 +1,7 @@ +/** + * tcp上传数据 + * 网络下载 + */ "use strict"; const net = require("net"); const http = require("http"); @@ -26,7 +30,7 @@ function int32ip2str(ip) { /** * @this {import("./ref").Client} * @param {import("./ref").HighwayUploadObject} o - * @param {Number} cmd + * @param {number} cmd * @returns {Buffer[]} */ function buildHighwayUploadRequestPackets(o, cmd, seq = randomBytes(2).readUInt16BE()) { @@ -68,10 +72,10 @@ function buildHighwayUploadRequestPackets(o, cmd, seq = randomBytes(2).readUInt1 /** * @this {import("./ref").Client} - * @param {Number|String} ip Int32ip - * @param {Number} port + * @param {number|string} ip Int32ip + * @param {number} port * @param {import("./ref").HighwayUploadObject} o - * @param {Number} cmd + * @param {number} cmd * @returns {Promise} */ async function highwayUpload(ip, port, o, cmd) { @@ -98,11 +102,11 @@ async function highwayUpload(ip, port, o, cmd) { } /** - * @param {String} url + * @param {string} url * @param {object|null} headers - * @param {Number} timeout - * @param {Boolean} proxy - * @param {String} mime_type + * @param {number} timeout + * @param {boolean} proxy + * @param {string} mime_type * @returns {Promise} */ async function downloadFromWeb(url, headers, timeout, proxy, mime_type, maxsize = MAX_UPLOAD_SIZE, redirect = false) { @@ -169,9 +173,9 @@ async function downloadFromWeb(url, headers, timeout, proxy, mime_type, maxsize } /** - * @param {String} url - * @param {Boolean} proxy - * @param {Number} timeout + * @param {string} url + * @param {boolean} proxy + * @param {number} timeout */ async function downloadWebImage(url, proxy, timeout, headers = null) { return await downloadFromWeb(url, headers, timeout, proxy, "image"); diff --git a/lib/sysmsg.js b/lib/sysmsg.js index 0d293acb..e67c6c48 100644 --- a/lib/sysmsg.js +++ b/lib/sysmsg.js @@ -1,3 +1,6 @@ +/** + * 系统消息相关处理(好友请求、群申请、群邀请) + */ "use strict"; const pb = require("./pb"); diff --git a/lib/troop.js b/lib/troop.js index 5b903f20..5046c55b 100644 --- a/lib/troop.js +++ b/lib/troop.js @@ -1,3 +1,6 @@ +/** + * 常用群功能和好友功能 + */ "use strict"; const querystring = require("querystring"); const http = require("http"); diff --git a/lib/wtlogin/wt.js b/lib/wtlogin/wt.js index 99f7ae15..faf31204 100644 --- a/lib/wtlogin/wt.js +++ b/lib/wtlogin/wt.js @@ -1,3 +1,6 @@ +/** + * login相关处理 + */ "use strict"; const fs = require("fs"); const path = require("path"); @@ -32,7 +35,7 @@ function encryptEMPBody(body) { /** * @this {import("../ref").Client} * @param {Buffer} body - * @param {Boolean} emp + * @param {boolean} emp * @returns {Buffer} */ function buildOICQPacket(body, emp = false) { @@ -57,7 +60,7 @@ function buildOICQPacket(body, emp = false) { /** * @this {import("../ref").Client} - * @param {String} cmd + * @param {string} cmd * @param {Buffer} body * @param {0|1|2} type * @returns {Buffer} @@ -95,9 +98,9 @@ function build0x0APacket(cmd, body, type) { /** * @this {import("../ref").Client} - * @param {String} cmd + * @param {string} cmd * @param {Buffer} body - * @param {Number} seq + * @param {number} seq * @returns {Buffer} */ function build0x0BPacket(cmd, body, seq = 0) { @@ -189,7 +192,7 @@ async function passwordLogin() { /** * @this {import("../ref").Client} - * @param {String} captcha Buffer length must be 4 + * @param {string} captcha Buffer length must be 4 */ async function captchaLogin(captcha) { this.logining = true; @@ -219,7 +222,7 @@ async function captchaLogin(captcha) { /** * @this {import("../ref").Client} - * @param {String} ticket + * @param {string} ticket */ async function sliderLogin(ticket) { this.logining = true; @@ -394,8 +397,8 @@ async function register(logout = false) { /** * @param {Readable} stream - * @param {Number} size - * @returns {Object} + * @param {number} size + * @returns {{[k: number]: Buffer}} */ function readTlv(stream, size) { const t = {}; diff --git a/util.js b/util.js index 41d17ac1..be96c461 100644 --- a/util.js +++ b/util.js @@ -1,8 +1,14 @@ +/** + * 常用工具包 + */ "use strict"; const { genCQMsg } = require("./lib/message/parser"); const segment = {}; const cqcode = {}; +/** + * @type {{[k: string]: string[]}} + */ const elem_map = { text: ["text"], at: ["qq", "text", "dummy"], From a2c4924bcce8e4df6ae2305884031d597e3a0ae5 Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 13 Mar 2021 14:30:59 +0900 Subject: [PATCH 12/20] history msg --- client.d.ts | 13 ++++++++++++- client.js | 7 ++++--- lib/message/chat.js | 22 +++++++++++++++------- lib/message/parser.js | 12 ++++++++---- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/client.d.ts b/client.d.ts index 298b31e0..a338e9a9 100644 --- a/client.d.ts +++ b/client.d.ts @@ -527,8 +527,19 @@ export class Client extends events.EventEmitter { sendTempMsg(group_id: number, user_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; sendDiscussMsg(discuss_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise; deleteMsg(message_id: string): Promise; + + /** + * 获取一条消息 + * 无法获取被撤回的消息 + */ getMsg(message_id: string): Promise>; - getChatHistory(message_id: string, count?: number): Promise>; //获取msgid(包括自身)往前的count条消息 + + /** + * 获取message_id往前的count条消息(包括自身) + * 无法获取被撤回的消息,因此返回的数量并不一定为count + * count默认为20,不能超过20 + */ + getChatHistory(message_id: string, count?: number): Promise>; // getForwardMsg(resid: string): Promise>; sendGroupNotice(group_id: number, content: string): Promise; diff --git a/client.js b/client.js index ca647acb..4ed1e8b0 100644 --- a/client.js +++ b/client.js @@ -362,12 +362,13 @@ class AndroidClient extends Client { em(name = "", data = {}) { data = Object.assign(this.parseEventType(name), data); - let i = name.lastIndexOf("."); - while (i > -1) { + while (true) { this.emit(name, data); + let i = name.lastIndexOf("."); + if (i === -1) + break; name = name.slice(0, i); } - this.emit(name, data); } msgExists(from, type, seq, time) { diff --git a/lib/message/chat.js b/lib/message/chat.js index 46a36000..6841035d 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -148,7 +148,10 @@ async function getOneMsg(message_id) { * @param {number} count * @returns {import("../ref").ProtocolResponse} */ -async function getMsgs(message_id, count = 10) { +async function getMsgs(message_id, count = 20) { + + if (count > 20) + count = 20; /** * @type {import("../ref").Msg[]} @@ -156,22 +159,27 @@ async function getMsgs(message_id, count = 10) { let msgs, data = []; if (message_id.length > 24) { const { group_id, seq } = parseGroupMessageId(message_id); - let from_seq = seq - count; + let from_seq = seq - count + 1; if (from_seq <= 0) from_seq = 1; msgs = await getGroupMsgs.call(this, group_id, from_seq, seq); // todo 分片处理 for (let msg of msgs) { - data.push(Object.assign(this.parseEventType("message.group"), await parseGroupMsg.call(this, msg))); + try { + const obj = await parseGroupMsg.call(this, msg); + data.push(Object.assign(this.parseEventType("message.group"), obj)); + } catch { } } } else { const { user_id, time, random } = parseC2CMessageId(message_id); msgs = await getC2CMsgs.call(this, user_id, time, count); for (let i = msgs.length - 1; i >= 0; --i) { - const msg = msgs[i]; - if (msg[3][1][1][3] !== random && !data.length) - continue; - data.unshift(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); + try { + const msg = msgs[i]; + if (msg[3][1][1][3] !== random && !data.length) + continue; + data.unshift(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); + } catch { } } } return { result: 0, data }; diff --git a/lib/message/parser.js b/lib/message/parser.js index 4f6b838d..1c71960f 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -319,6 +319,8 @@ class Parser { 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) @@ -576,10 +578,11 @@ async function parseC2CMsg(msg, realtime = false) { */ async function parseGroupMsg(msg, realtime = false) { - const head = msg[1], body = msg[3]; + const head = msg[1], content = msg[2], body = msg[3]; const user_id = head[1], time = head[6], - seq = head[5]; + seq = head[5], + random = body[1][1][3]; let group = head[9], group_id = group[1], group_name = group[8] ? String(group[8].raw) : ""; @@ -598,7 +601,8 @@ async function parseGroupMsg(msg, realtime = false) { await parser.parseMsg(body[1]); let font = String(body[1][1][9].raw), - card = parseFunString(group[4].raw); + card = parseFunString(group[4].raw), + message_id = genGroupMessageId(group_id, user_id, seq, random, time, content[1]); let user; if (!parser.anonymous) { @@ -632,7 +636,7 @@ async function parseGroupMsg(msg, realtime = false) { }; return { sub_type: parser.anonymous ? "anonymous" : "normal", - group_id, group_name, user_id, + message_id, group_id, group_name, user_id, anonymous: parser.anonymous, message: parser.message, raw_message: parser.raw_message, From 1126a8dd4c86259e67a12c6b888c3ab5433755bc Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 13 Mar 2021 15:45:56 +0900 Subject: [PATCH 13/20] parse forward msg --- client.d.ts | 16 +++++++++++++-- client.js | 2 +- lib/message/chat.js | 6 +++--- lib/message/parser.js | 46 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/client.d.ts b/client.d.ts index a338e9a9..c665e01e 100644 --- a/client.d.ts +++ b/client.d.ts @@ -539,8 +539,20 @@ export class Client extends events.EventEmitter { * 无法获取被撤回的消息,因此返回的数量并不一定为count * count默认为20,不能超过20 */ - getChatHistory(message_id: string, count?: number): Promise>; // - getForwardMsg(resid: string): Promise>; + getChatHistory(message_id: string, count?: number): Promise>; + + /** + * 获取转发消息 + * resid在xml消息中,需要自行解析xml获得 + */ + getForwardMsg(resid: string): Promise>>; sendGroupNotice(group_id: number, content: string): Promise; setGroupName(group_id: number, group_name: string): Promise; diff --git a/client.js b/client.js index 4ed1e8b0..76ac71d6 100644 --- a/client.js +++ b/client.js @@ -547,7 +547,7 @@ class AndroidClient extends Client { async getChatHistory(message_id, count = 10) { return await this.useProtocol(chat.getMsgs, arguments); } - async getForwardMsg(resid) { + async getForwardMsg(id) { return await this.useProtocol(chat.getForwardMsg, arguments); } diff --git a/lib/message/chat.js b/lib/message/chat.js index 6841035d..0d9a472e 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -5,7 +5,7 @@ "use strict"; const { Builder } = require("./builder"); const { getC2CMsgs, getGroupMsgs } = require("./history"); -const { parseC2CMsg, parseGroupMsg } = require("./parser"); +const { parseC2CMsg, parseGroupMsg, parseForwardMsg } = require("./parser"); const common = require("../common"); const pb = require("../pb"); const { parseC2CMessageId, parseGroupMessageId, genMessageUuid } = common; @@ -191,8 +191,8 @@ async function getMsgs(message_id, count = 20) { * @param {string} resid * @returns {import("../ref").ProtocolResponse} */ -async function getForwardMsg(resid) { - return { result: -1, emsg: "not support yet" }; +function getForwardMsg(resid) { + return parseForwardMsg.call(this, resid); } module.exports = { diff --git a/lib/message/parser.js b/lib/message/parser.js index 1c71960f..e225df39 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -75,7 +75,8 @@ async function downloadMultiMsg(resid, bu) { buf = zlib.unzipSync(buf[3].raw); resolve(buf); } catch (e) { - reject(); + e.message = "wrong resid"; + reject(e); } }); }).on("error", reject); @@ -679,6 +680,47 @@ async function parseDiscussMsg(msg) { }; } +/** + * 解析转发消息 + * @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].raw); + } else { + try { + nickname = String(head[9][4].raw); + 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 + parseC2CMsg, parseGroupMsg, parseDiscussMsg, genCQMsg, parseForwardMsg }; From 17b08dfafcc722789f321b15a93a6c2526c1d946 Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 13 Mar 2021 19:51:56 +0900 Subject: [PATCH 14/20] fix img cache bug fix send fragments --- client.d.ts | 2 +- lib/message/builder.js | 14 +++++++------- lib/message/image.js | 10 ---------- lib/sysmsg.js | 6 ++++-- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/client.d.ts b/client.d.ts index c665e01e..410cca8a 100644 --- a/client.d.ts +++ b/client.d.ts @@ -361,7 +361,7 @@ export interface GroupAddEventData extends RequestEventData { group_name: string, comment: string, inviter_id?: number, //邀请人 - actor_id?: number, //处理人 + // actor_id?: number, //处理人 } export interface GroupInviteEventData extends RequestEventData { group_id: number, diff --git a/lib/message/builder.js b/lib/message/builder.js index 9f0e4056..c7e043c5 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -690,7 +690,7 @@ class Builder { 7: 0, 10: 0, }; - this.elems.push([{ + this.elems2.push([{ 53: { 1: 2, 2: nested, @@ -921,10 +921,11 @@ class Builder { * @private * @param {import("../ref").RichMsg} rich * @param {Buffer} content + * @param {number} random */ - buildPbSendMsgPkt(rich, content = PB_CONTENT) { + buildPbSendMsgPkt(rich, content = PB_CONTENT, random = undefined) { this.seq = this.c.seq_id + 1; - this.random = randomBytes(4).readUInt32BE(); + this.random = random === undefined ? randomBytes(4).readUInt32BE() : random; if (this.anon) rich[2].push(this.anon); rich[2].push(PB_RESERVER); @@ -1000,9 +1001,7 @@ class Builder { for (let elem of this.elems) { fragment.push(elem); if (elem[1] && !elem[1][3]) { //1:text 1[3]:at - fragment.push({ - 37: PB_RESERVER - }); + fragment.push(PB_RESERVER); fragments.push(fragment); fragment = []; } @@ -1013,6 +1012,7 @@ class Builder { } let n = 0; + const random = randomBytes(4).readUInt32BE(); const div = randomBytes(2).readUInt16BE(); for (let fragment of fragments) { const content = pb.encode({ @@ -1020,7 +1020,7 @@ class Builder { 2: n++, 3: div }); - const body = this.buildPbSendMsgPkt({ 2: fragment }, content); + const body = this.buildPbSendMsgPkt({ 2: fragment }, content, random); this.c.writeUni("MessageSvc.PbSendMsg", body); } let message_id = await this.waitForMessageId(5000); diff --git a/lib/message/image.js b/lib/message/image.js index be1e2a30..bac6f655 100644 --- a/lib/message/image.js +++ b/lib/message/image.js @@ -215,15 +215,6 @@ class ImageBuilder { } } - /** - * 删除缓存文件 - * @public - */ - deleteCache() { - if (this.filepath) - fs.unlink(this.filepath, () => { }); - } - /** * 服务端返回的fid(fileid)写入图片节点 * @public @@ -409,7 +400,6 @@ async function uploadImages(target, imgs, c2c) { const v = rsp[i % 20]; imgs[i].setFid(c2c ? v[10].raw : v[9]); if (v[4 + j] || !imgs[i].buf) { - imgs[i].deleteCache(); continue; } let ip = v[6 + j], port = v[7 + j]; diff --git a/lib/sysmsg.js b/lib/sysmsg.js index e67c6c48..e81b1b94 100644 --- a/lib/sysmsg.js +++ b/lib/sysmsg.js @@ -81,7 +81,7 @@ function parseGrpSysMsg(proto) { data.nickname = String(proto[50][51].raw); data.comment = String(proto[50][4].raw); data.inviter_id = proto[50][11]; - data.actor_id = proto[50][16]; + // data.actor_id = proto[50][16]; } data.flag = genGroupRequestFlag(data.user_id, group_id, proto[3], invite); return data; @@ -204,7 +204,9 @@ async function getSysMsg() { if (!Array.isArray(rsp)) rsp = [rsp]; for (let proto of rsp) { - data.push(Object.assign(this.parseEventType("request.friend.add"), parseFrdSysMsg(proto))); + try { + data.push(Object.assign(this.parseEventType("request.friend.add"), parseFrdSysMsg(proto))); + } catch { } } })(); From 1bca7fb307304dc4996a83108014bbd1927e2743 Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 13 Mar 2021 22:42:24 +0900 Subject: [PATCH 15/20] rename some methods --- client.d.ts | 131 +++++++++++++++++++++-------------------- client.js | 8 +-- device.js | 12 ++-- lib/friendlist.js | 1 + lib/message/builder.js | 2 +- lib/message/chat.js | 21 +++---- lib/message/image.js | 66 +++++++++++++++++---- lib/message/parser.js | 20 +++---- lib/message/ptt.js | 24 ++++---- lib/service.js | 12 ++-- lib/sysmsg.js | 32 +++++----- lib/troop.js | 111 +++++++++++++++++----------------- lib/wtlogin/wt.js | 2 +- 13 files changed, 245 insertions(+), 197 deletions(-) diff --git a/client.d.ts b/client.d.ts index 410cca8a..b926d34a 100644 --- a/client.d.ts +++ b/client.d.ts @@ -2,7 +2,7 @@ /// -import * as events from 'events'; +import { EventEmitter } from 'events'; import { OutgoingHttpHeaders } from 'http'; import * as log4js from 'log4js'; @@ -76,12 +76,13 @@ export interface RetError { code?: number, message?: string, } -export interface RetCommon { +export interface Ret { retcode: 0 | 1 | 100 | 102 | 103 | 104, //0ok 1async 100error 102failed 103timeout 104offline status: "ok" | "async" | "failed", data: T | null, error: RetError | null, } +export type RetCommon = Ret; ////////// @@ -375,7 +376,7 @@ interface MessageEventData extends CommonEventData { message_id: string, user_id: number, font: string, - reply: (message: MessageElem | Iterable | string, auto_escape?: boolean) => Promise>, + reply: (message: MessageElem | Iterable | string, auto_escape?: boolean) => Promise>, } export interface PrivateMessageEventData extends MessageEventData { sender: FriendInfo, @@ -486,7 +487,7 @@ export type EventData = CaptchaEventData | DeviceEventData | LoginErrorEventData ////////// -export class Client extends events.EventEmitter { +export class Client extends EventEmitter { private constructor(); @@ -512,40 +513,40 @@ export class Client extends events.EventEmitter { logout(): Promise; //先下线再关闭连接 isOnline(): boolean; - setOnlineStatus(status: 11 | 31 | 41 | 50 | 60 | 70): Promise; //11我在线上 31离开 41隐身 50忙碌 60Q我吧 70请勿打扰 + setOnlineStatus(status: 11 | 31 | 41 | 50 | 60 | 70): Promise; //11我在线上 31离开 41隐身 50忙碌 60Q我吧 70请勿打扰 - getFriendList(): RetCommon>; - getStrangerList(): RetCommon>; - getGroupList(): RetCommon>; - getGroupMemberList(group_id: number, no_cache?: boolean): Promise>>; - getStrangerInfo(user_id: number, no_cache?: boolean): Promise>; - getGroupInfo(group_id: number, no_cache?: boolean): Promise>; - getGroupMemberInfo(group_id: number, user_id: number, no_cache?: boolean): Promise>; + getFriendList(): Ret; + getStrangerList(): Ret; + getGroupList(): Ret; + getGroupMemberList(group_id: number, no_cache?: boolean): Promise>; + getStrangerInfo(user_id: number, no_cache?: boolean): Promise>; + getGroupInfo(group_id: number, no_cache?: boolean): Promise>; + getGroupMemberInfo(group_id: number, user_id: number, no_cache?: boolean): Promise>; - sendPrivateMsg(user_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; - sendGroupMsg(group_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; - sendTempMsg(group_id: number, user_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; - sendDiscussMsg(discuss_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise; - deleteMsg(message_id: string): Promise; + sendPrivateMsg(user_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; + sendGroupMsg(group_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; + sendTempMsg(group_id: number, user_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise>; + sendDiscussMsg(discuss_id: number, message: MessageElem | Iterable | string, auto_escape?: boolean): Promise; + deleteMsg(message_id: string): Promise; /** * 获取一条消息 * 无法获取被撤回的消息 */ - getMsg(message_id: string): Promise>; + getMsg(message_id: string): Promise>; /** * 获取message_id往前的count条消息(包括自身) * 无法获取被撤回的消息,因此返回的数量并不一定为count * count默认为20,不能超过20 */ - getChatHistory(message_id: string, count?: number): Promise>; + getChatHistory(message_id: string, count?: number): Promise>; /** * 获取转发消息 * resid在xml消息中,需要自行解析xml获得 */ - getForwardMsg(resid: string): Promise>>; - sendGroupNotice(group_id: number, content: string): Promise; - setGroupName(group_id: number, group_name: string): Promise; - setGroupAnonymous(group_id: number, enable?: boolean): Promise; - setGroupWholeBan(group_id: number, enable?: boolean): Promise; - setGroupAdmin(group_id: number, user_id: number, enable?: boolean): Promise; - setGroupSpecialTitle(group_id: number, user_id: number, special_title?: string, duration?: number): Promise; - setGroupCard(group_id: number, user_id: number, card?: string): Promise; - setGroupKick(group_id: number, user_id: number, reject_add_request?: boolean): Promise; - setGroupBan(group_id: number, user_id: number, duration?: number): Promise; - setGroupAnonymousBan(group_id: number, flag: string, duration?: number): Promise; - setGroupLeave(group_id: number, is_dismiss?: boolean): Promise; - sendGroupPoke(group_id: number, user_id: number): Promise; //group_id是好友时可以私聊戳一戳(命名可能会在之后改进) - - setFriendAddRequest(flag: string, approve?: boolean, remark?: string, block?: boolean): Promise; - setGroupAddRequest(flag: string, approve?: boolean, reason?: string, block?: boolean): Promise; - getSystemMsg(): Promise>>; - - addGroup(group_id: number, comment?: string): Promise; - addFriend(group_id: number, user_id: number, comment?: string): Promise; - deleteFriend(user_id: number, block?: boolean): Promise; - inviteFriend(group_id: number, user_id: number): Promise; - sendLike(user_id: number, times?: number): Promise; - setNickname(nickname: string): Promise; - setGender(gender: 0 | 1 | 2): Promise; //0未知 1男 2女 - setBirthday(birthday: string | number): Promise; //20110202的形式 - setDescription(description?: string): Promise; - setSignature(signature?: string): Promise; - setPortrait(file: Buffer | string): Promise; //图片CQ码中file相同格式 - setGroupPortrait(group_id: number, file: Buffer | string): Promise; - - // getFile(fileid: string, busid?: string): Promise>; //用于下载链接失效后重新获取 - // uploadC2CImages(user_id: number, images: ImgPttElem["data"][]): Promise>; //上传好友图以备发送 - // uploadGroupImages(group_id: number, images: ImgPttElem["data"][]): Promise>; //上传群图以备发送 - // getSummaryCard(user_id: number): Promise>; //查看用户资料 - - getCookies(domain?: string): Promise>; - getCsrfToken(): Promise>; - cleanCache(type?: "image" | "record"): Promise; - canSendImage(): RetCommon; - canSendRecord(): RetCommon; - getVersionInfo(): RetCommon; //暂时为返回package.json中的信息 - getStatus(): RetCommon; - getLoginInfo(): RetCommon; + sendGroupNotice(group_id: number, content: string): Promise; + setGroupName(group_id: number, group_name: string): Promise; + setGroupAnonymous(group_id: number, enable?: boolean): Promise; + setGroupWholeBan(group_id: number, enable?: boolean): Promise; + setGroupAdmin(group_id: number, user_id: number, enable?: boolean): Promise; + setGroupSpecialTitle(group_id: number, user_id: number, special_title?: string, duration?: number): Promise; + setGroupCard(group_id: number, user_id: number, card?: string): Promise; + setGroupKick(group_id: number, user_id: number, reject_add_request?: boolean): Promise; + setGroupBan(group_id: number, user_id: number, duration?: number): Promise; + setGroupAnonymousBan(group_id: number, flag: string, duration?: number): Promise; + setGroupLeave(group_id: number, is_dismiss?: boolean): Promise; + sendGroupPoke(group_id: number, user_id: number): Promise; //group_id是好友时可以私聊戳一戳(命名可能会在之后改进) + + setFriendAddRequest(flag: string, approve?: boolean, remark?: string, block?: boolean): Promise; + setGroupAddRequest(flag: string, approve?: boolean, reason?: string, block?: boolean): Promise; + getSystemMsg(): Promise>>; + + addGroup(group_id: number, comment?: string): Promise; + addFriend(group_id: number, user_id: number, comment?: string): Promise; + deleteFriend(user_id: number, block?: boolean): Promise; + inviteFriend(group_id: number, user_id: number): Promise; + sendLike(user_id: number, times?: number): Promise; + setNickname(nickname: string): Promise; + setGender(gender: 0 | 1 | 2): Promise; //0未知 1男 2女 + setBirthday(birthday: string | number): Promise; //20110202的形式 + setDescription(description?: string): Promise; + setSignature(signature?: string): Promise; + setPortrait(file: Buffer | string): Promise; //图片CQ码中file相同格式 + setGroupPortrait(group_id: number, file: Buffer | string): Promise; + + // getFile(fileid: string, busid?: string): Promise>; //用于下载链接失效后重新获取 + // uploadC2CImages(user_id: number, images: ImgPttElem["data"][]): Promise>; //上传好友图以备发送 + // uploadGroupImages(group_id: number, images: ImgPttElem["data"][]): Promise>; //上传群图以备发送 + // getSummaryCard(user_id: number): Promise>; //查看用户资料 + + getCookies(domain?: string): Promise>; + getCsrfToken(): Promise>; + cleanCache(type?: "image" | "record"): Promise; + canSendImage(): Ret; + canSendRecord(): Ret; + getVersionInfo(): Ret; //暂时为返回package.json中的信息 + getStatus(): Ret; + getLoginInfo(): Ret; on(event: "system.login.captcha", listener: (this: Client, data: CaptchaEventData) => void): this; on(event: "system.login.device" | "system.login.slider", listener: (this: Client, data: DeviceEventData) => void): this; @@ -639,8 +640,8 @@ export class Client extends events.EventEmitter { on(event: string | symbol, listener: (this: Client, ...args: any[]) => void): this; //重载完成之前bot不接受其他任何请求,也不会上报任何事件 - reloadFriendList(): Promise; - reloadGroupList(): Promise; + reloadFriendList(): Promise; + reloadGroupList(): Promise; } export function createClient(uin: number, config?: ConfBot): Client; diff --git a/client.js b/client.js index 76ac71d6..a8e43df1 100644 --- a/client.js +++ b/client.js @@ -40,8 +40,6 @@ class Client extends net.Socket { static OFFLINE = Symbol("OFFLINE"); static INIT = Symbol("INIT"); static ONLINE = Symbol("ONLINE"); -} -class AndroidClient extends Client { logining = false; status = Client.OFFLINE; nickname = ""; @@ -766,16 +764,16 @@ function setGlobalConfig() { } /** * @param {number} uin * @param {import("./client").ConfBot} config - * @returns {AndroidClient} + * @returns {Client} */ function createClient(uin, config = {}) { uin = parseInt(uin); if (!checkUin(uin)) throw new Error("Argument uin is not an OICQ account."); - return new AndroidClient(uin, config); + return new Client(uin, config); } module.exports = { - createClient, setGlobalConfig, Client: AndroidClient, + createClient, setGlobalConfig, Client, segment, cqcode, }; diff --git a/device.js b/device.js index a8d16e6e..f61d85ea 100644 --- a/device.js +++ b/device.js @@ -14,7 +14,7 @@ function rand(n = 9) { return parseInt(Math.random() * (max - min) + min); } -function getMac() { +function _getMac() { const o = os.networkInterfaces(); for (let k in o) { for (let v of o[k]) { @@ -25,7 +25,7 @@ function getMac() { return `00:50:A${rand(1)}:${rand(1)}D:${rand(1)}B:C${rand(1)}`; } -function genIMEI() { +function _genIMEI() { let imei = Math.random() > 0.5 ? "86" : "35"; imei += rand(4) + "0" + rand(7); function calcSP(imei) { @@ -46,7 +46,7 @@ function genIMEI() { /** * @param {string} filepath */ -function genDevice(filepath) { +function _genDevice(filepath) { const device = `{ "--begin--": "修改后可能需要重新验证设备。", "product": "iarim", @@ -60,9 +60,9 @@ function genDevice(filepath) { "android_id": "BRAND.${rand(6)}.${rand(3)}", "boot_id": "${uuid()}", "proc_version": "Linux version 4.19.71-${rand(5)} (oicq@takayama.github.com)", - "mac_address": "${getMac()}", + "mac_address": "${_getMac()}", "ip_address": "10.0.${rand(2)}.${rand(2)}", - "imei": "${genIMEI()}", + "imei": "${_genIMEI()}", "incremental": "${rand(7)}" }`; const dir = path.dirname(filepath); @@ -81,7 +81,7 @@ function getDeviceInfo(filepath) { try { d = JSON.parse(fs.readFileSync(filepath, { encoding: "utf-8" })); } catch { - d = genDevice(filepath); + d = _genDevice(filepath); } const device = { display: d.android_id, diff --git a/lib/friendlist.js b/lib/friendlist.js index 6ad83a78..6b17ab6e 100644 --- a/lib/friendlist.js +++ b/lib/friendlist.js @@ -2,6 +2,7 @@ * 好友列表 * 群列表,群资料 * 群员列表,群员资料 + * 相关api */ "use strict"; const common = require("./common"); diff --git a/lib/message/builder.js b/lib/message/builder.js index c7e043c5..23823bd6 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -844,7 +844,7 @@ class Builder { this.nodes = this.nodes.filter(v => v); // 去除空值 await uploadImages.call(this.c, this.target, this.imgs, !this.type); - this.setRouting(); + await this.setRouting(); const tasks = []; for (let buf of this.b77s) { diff --git a/lib/message/chat.js b/lib/message/chat.js index 0d9a472e..072f4d0f 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -1,5 +1,5 @@ /** - * api入口 + * 消息相关api入口 * 发送,撤回,获取聊天记录,获取转发消息 */ "use strict"; @@ -20,7 +20,7 @@ const { parseC2CMessageId, parseGroupMessageId, genMessageUuid } = common; * @param {boolean} escape * @returns {import("../ref").ProtocolResponse} */ -async function sendTempMsg(group_id, user_id, message, escape) { +function sendTempMsg(group_id, user_id, message, escape) { [group_id, user_id] = common.uinAutoCheck(group_id, user_id); const builder = new Builder(this, user_id, 0); builder.routing = pb.encode({ @@ -29,7 +29,7 @@ async function sendTempMsg(group_id, user_id, message, escape) { 2: user_id, } }); - return await builder.buildAndSend(message, escape); + return builder.buildAndSend(message, escape); } /** @@ -40,10 +40,10 @@ async function sendTempMsg(group_id, user_id, message, escape) { * @param {0|1|2} type //0私聊 1群聊 2讨论组 * @returns {import("../ref").ProtocolResponse} */ -async function sendMsg(target, message, escape, type) { +function sendMsg(target, message, escape, type) { [target] = common.uinAutoCheck(target); const builder = new Builder(this, target, type); - return await builder.buildAndSend(message, escape); + return builder.buildAndSend(message, escape); } //recall---------------------------------------------------------------------------------------------------- @@ -56,15 +56,15 @@ async function recallMsg(message_id) { let body; try { if (message_id.length > 24) - body = buildRecallGroupMsgBody.call(this, message_id); + body = _buildRecallGroupMsgBody.call(this, message_id); else - body = buildRecallPrivateMsgBody.call(this, message_id); + body = _buildRecallPrivateMsgBody.call(this, message_id); } catch { throw new Error("incorrect message_id"); } await this.sendUni("PbMessageSvc.PbMsgWithDraw", body); } -function buildRecallPrivateMsgBody(message_id) { +function _buildRecallPrivateMsgBody(message_id) { const { user_id, seq, random, time } = parseC2CMessageId(message_id); let type = 0; try { @@ -87,7 +87,7 @@ function buildRecallPrivateMsgBody(message_id) { }] }); } -function buildRecallGroupMsgBody(message_id) { +function _buildRecallGroupMsgBody(message_id) { var { group_id, seq, random, pktnum } = parseGroupMessageId(message_id); if (pktnum > 1) { //分片消息 @@ -196,5 +196,6 @@ function getForwardMsg(resid) { } module.exports = { - sendMsg, sendTempMsg, recallMsg, getOneMsg, getMsgs, getForwardMsg + sendMsg, sendTempMsg, recallMsg, + getOneMsg, getMsgs, getForwardMsg }; diff --git a/lib/message/image.js b/lib/message/image.js index bac6f655..35eb9e49 100644 --- a/lib/message/image.js +++ b/lib/message/image.js @@ -49,43 +49,86 @@ class ImageBuilder { /** * 图片protobuf节点 + * @public * @type {import("../ref").Proto} */ nested; /** * 图片bytes + * @public * @type {Buffer} */ buf; /** * 服务端返回的fileid + * @public * @type {Buffer} */ fid; + /** + * 网络图片下载任务 + * @public + * @type {Promise} + */ + task; + + /** + * 上传key + * @public + * @type {Buffer} + */ + key; + + /** + * @public + */ md5 = randomBytes(16); + /** + * @public + */ size = 0xff; + /** + * @private + */ width = 960; + /** + * @private + */ height = 640; - type = 1000; - /** - * 网络图片下载任务 - * @type {Promise} + * @public */ - task; + type = 1000; /** * 缓存文件路径 + * @private * @type {string} */ filepath; + /** + * @private + * @type {boolean} + */ proxy; + /** + * @private + * @type {number} + */ timeout; + /** + * @private + * @type {import("http").OutgoingHttpHeaders} + */ headers; + /** + * @private + * @type {string} + */ address; /** @@ -142,8 +185,9 @@ class ImageBuilder { * @private */ setNested() { + let nested; if (this.c2c) { - var nested = { + nested = { 1: this.md5.toString("hex"), 2: this.size, 3: this.fid, @@ -158,7 +202,7 @@ class ImageBuilder { 25: 0, }; } else { - var nested = { + nested = { 2: this.md5.toString("hex") + ".gif", 7: this.fid, 8: 0, @@ -234,7 +278,7 @@ class ImageBuilder { /** * @public - * @param {import("../../client").ImgPttElem["data"]} cq + * @param {import("../ref").ImgPttElem["data"]} cq */ async buildNested(cq) { let { file, url, cache, timeout, proxy, headers } = cq; @@ -317,7 +361,7 @@ class ImageBuilder { * @param {number} group_id * @param {ImageBuilder[]} imgs */ -async function groupPicUp(group_id, imgs) { +async function _groupPicUp(group_id, imgs) { const req = []; for (const v of imgs) { req.push({ @@ -352,7 +396,7 @@ async function groupPicUp(group_id, imgs) { * @param {number} user_id * @param {ImageBuilder[]} imgs */ -async function offPicUp(user_id, imgs) { +async function _offPicUp(user_id, imgs) { const req = []; for (const v of imgs) { req.push({ @@ -393,7 +437,7 @@ async function uploadImages(target, imgs, c2c) { while (imgs.length > n) { try { this.logger.debug("开始请求上传图片到tx服务器"); - let rsp = await (c2c ? offPicUp : groupPicUp).call(this, target, imgs.slice(n, n + 20)); + let rsp = await (c2c ? _offPicUp : _groupPicUp).call(this, target, imgs.slice(n, n + 20)); rsp = Array.isArray(rsp) ? rsp : [rsp]; const tasks = []; for (let i = n; i < imgs.length; ++i) { diff --git a/lib/message/parser.js b/lib/message/parser.js index e225df39..5d2c4b0d 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -33,7 +33,7 @@ function escapeCQ(s) { * @param {Number} bu * @returns {Promise} */ -async function downloadMultiMsg(resid, bu) { +async function _downloadMultiMsg(resid, bu) { const body = pb.encode({ 1: 2, 2: 5, @@ -89,13 +89,13 @@ async function downloadMultiMsg(resid, bu) { class Parser { /** - * @type {import("../../client").MessageElem[]} + * @type {import("../ref").MessageElem[]} */ message = []; raw_message = ""; /** - * @type {import("../../client").Anonymous} + * @type {import("../ref").Anonymous} */ anonymous = null; @@ -163,7 +163,7 @@ class Parser { */ async parseExclusiveElem(type, elem) { /** - * @type {import("../../client").MessageElem} + * @type {import("../ref").MessageElem} */ const msg = { type: "", @@ -250,7 +250,7 @@ class Parser { */ parsePartialElem(type, elem) { /** - * @type {import("../../client").MessageElem} + * @type {import("../ref").MessageElem} */ const msg = { type: "", @@ -359,7 +359,7 @@ class Parser { } } else if (type === 37) { //generalFlags 超长消息,气泡等 if (elem[6] === 1 && elem[7]) { - const buf = await downloadMultiMsg.call(this.c, elem[7].raw, 1); + const buf = await _downloadMultiMsg.call(this.c, elem[7].raw, 1); let msg = pb.decode(buf)[1]; if (Array.isArray(msg)) msg = msg[0]; const parser = new Parser(this.c, this.uid, this.gid); @@ -481,7 +481,7 @@ class Parser { /** * 生成CQ码字符串消息 - * @param {import("../../client").MessageElem} msg + * @param {import("../ref").MessageElem} msg * @returns {string} */ function genCQMsg(msg) { @@ -495,7 +495,7 @@ function genCQMsg(msg) { * @param {import("../ref").Proto} elem * @param {number} from */ -async function parseC2CFileElem(elem) { +async function _parseC2CFileElem(elem) { const fileid = elem[3].raw, md5 = elem[4].raw.toString("hex"), name = String(elem[5].raw), @@ -558,7 +558,7 @@ async function parseC2CMsg(msg, realtime = false) { if (type === 529) { if (head[4] !== 4) return; - var parser = await parseC2CFileElem.call(this, body[2][1]); + var parser = await _parseC2CFileElem.call(this, body[2][1]); } else if (body[1] && body[1][2]) { var parser = new Parser(this, user_id, 0); await parser.parseMsg(body[1]); @@ -688,7 +688,7 @@ async function parseDiscussMsg(msg) { */ async function parseForwardMsg(resid) { const data = []; - const blob = await downloadMultiMsg.call(this, String(resid), 2); + const blob = await _downloadMultiMsg.call(this, String(resid), 2); /** * @type {import("../ref").Msg[]} */ diff --git a/lib/message/ptt.js b/lib/message/ptt.js index 0930fe41..c08a9868 100644 --- a/lib/message/ptt.js +++ b/lib/message/ptt.js @@ -16,7 +16,7 @@ const common = require("../common"); /** * @this {import("../ref").Client} * @param {number} target - * @param {import("../../client").ImgPttElem["data"]} cq + * @param {import("../ref").ImgPttElem["data"]} cq * @returns {Promise} */ async function genPttElem(target, cq) { @@ -34,7 +34,7 @@ async function genPttElem(target, cq) { try { buf = await fs.promises.readFile(cache_file); this.logger.debug("使用缓存的amr音频文件"); - return await uploadPtt.call(this, target, buf); + return await _uploadPtt.call(this, target, buf); } catch { } } @@ -55,15 +55,15 @@ async function genPttElem(target, cq) { tmp_file = String(file).trim().replace(/^file:\/{2,3}/, ""); // 读取前7个字节,若为为silk或amr格式直接发送 - const head = await read7Bytes(tmp_file); + const head = await _read7Bytes(tmp_file); if (head.includes("SILK") || head.includes("AMR")) { buf = await fs.promises.readFile(tmp_file); - return await uploadPtt.call(this, target, buf); + return await _uploadPtt.call(this, target, buf); } // 音频转换(生成缓存文件)->发送 - buf = await audioTrans.call(this, cache_file, tmp_file); - return await uploadPtt.call(this, target, buf); + buf = await _audioTrans.call(this, cache_file, tmp_file); + return await _uploadPtt.call(this, target, buf); } // 非本地文件 @@ -72,18 +72,18 @@ async function genPttElem(target, cq) { const head = buf.slice(0, 7).toString(); if (head.includes("SILK") || head.includes("AMR")) { fs.writeFile(cache_file, buf, () => { }); - return await uploadPtt.call(this, target, buf); + return await _uploadPtt.call(this, target, buf); } // 写入临时文件->音频转换(生成缓存文件)->删除临时文件->发送 tmp_file = path.join(path.dirname(cache_file), Math.random() + "" + Date.now()); await fs.promises.writeFile(tmp_file, buf); try { - buf = await audioTrans.call(this, cache_file, tmp_file); + buf = await _audioTrans.call(this, cache_file, tmp_file); } finally { fs.unlink(tmp_file, () => { }); } - return await uploadPtt.call(this, target, buf); + return await _uploadPtt.call(this, target, buf); } } @@ -93,7 +93,7 @@ async function genPttElem(target, cq) { * @param {string} tmp_file * @returns {Promise} */ -async function audioTrans(cache_file, tmp_file) { +async function _audioTrans(cache_file, tmp_file) { return new Promise((resolve, reject) => { exec(`ffmpeg -y -i ${tmp_file} -ac 1 -ar 8000 -f amr ${cache_file}`, async (error, stdout, stderr) => { this.logger.debug("ffmpeg output: " + stdout + stderr); @@ -111,7 +111,7 @@ async function audioTrans(cache_file, tmp_file) { /** * @param {string} filepath */ -async function read7Bytes(filepath) { +async function _read7Bytes(filepath) { const fd = await fs.promises.open(filepath, "r"); const buf = (await fd.read(Buffer.alloc(7), 0, 7, 0)).buffer; fd.close(); @@ -124,7 +124,7 @@ async function read7Bytes(filepath) { * @param {Buffer} buf * @returns {Promise} */ -async function uploadPtt(target, buf) { +async function _uploadPtt(target, buf) { const md5 = common.md5(buf); const codec = String(buf.slice(0, 7)).includes("SILK") ? 1 : 0; const body = pb.encode({ diff --git a/lib/service.js b/lib/service.js index 31d00a66..a73c0579 100644 --- a/lib/service.js +++ b/lib/service.js @@ -33,7 +33,7 @@ function int32ip2str(ip) { * @param {number} cmd * @returns {Buffer[]} */ -function buildHighwayUploadRequestPackets(o, cmd, seq = randomBytes(2).readUInt16BE()) { +function _buildHighwayUploadRequestPackets(o, cmd, seq = randomBytes(2).readUInt16BE()) { const packets = [], limit = 3000000, size = o.buf.length; let chunk, offset = 0; while (1) { @@ -97,7 +97,7 @@ async function highwayUpload(ip, port, o, cmd) { }); client.on("close", resolve); client.on("error", resolve); - var packets = buildHighwayUploadRequestPackets.call(this, o, cmd); + var packets = _buildHighwayUploadRequestPackets.call(this, o, cmd); }); } @@ -109,7 +109,7 @@ async function highwayUpload(ip, port, o, cmd) { * @param {string} mime_type * @returns {Promise} */ -async function downloadFromWeb(url, headers, timeout, proxy, mime_type, maxsize = MAX_UPLOAD_SIZE, redirect = false) { +async function _downloadFromWeb(url, headers, timeout, proxy, mime_type, maxsize = MAX_UPLOAD_SIZE, redirect = false) { if (timeout > 0 === false) timeout = 60; const protocol = url.startsWith("https") ? https : http; @@ -134,7 +134,7 @@ async function downloadFromWeb(url, headers, timeout, proxy, mime_type, maxsize const req = protocol.get(url, options, (res) => { // 重定向一次(暂时手动实现) if (String(res.statusCode).startsWith("3") && !redirect && res.headers["location"]) { - return downloadFromWeb(res.headers["location"], headers, timeout, proxy, mime_type, maxsize, true) + return _downloadFromWeb(res.headers["location"], headers, timeout, proxy, mime_type, maxsize, true) .then(resolve) .catch(reject); } @@ -178,10 +178,10 @@ async function downloadFromWeb(url, headers, timeout, proxy, mime_type, maxsize * @param {number} timeout */ async function downloadWebImage(url, proxy, timeout, headers = null) { - return await downloadFromWeb(url, headers, timeout, proxy, "image"); + return await _downloadFromWeb(url, headers, timeout, proxy, "image"); } async function downloadWebRecord(url, proxy, timeout, headers = null) { - return await downloadFromWeb(url, headers, timeout, proxy, "", 0xfffffff); + return await _downloadFromWeb(url, headers, timeout, proxy, "", 0xfffffff); } async function readFile(path, maxsize = MAX_UPLOAD_SIZE) { diff --git a/lib/sysmsg.js b/lib/sysmsg.js index e81b1b94..4e39a119 100644 --- a/lib/sysmsg.js +++ b/lib/sysmsg.js @@ -1,5 +1,6 @@ /** * 系统消息相关处理(好友请求、群申请、群邀请) + * 相关api */ "use strict"; const pb = require("./pb"); @@ -8,14 +9,14 @@ const pb = require("./pb"); * @param {number} user_id * @param {BigInt|number} seq */ -function genFriendRequestFlag(user_id, seq) { +function _genFriendRequestFlag(user_id, seq) { return user_id.toString(16).padStart(8, "0") + seq.toString(16); } /** * @param {string} flag */ -function parseFriendRequestFlag(flag) { +function _parseFriendRequestFlag(flag) { const user_id = parseInt(flag.slice(0, 8), 16); const seq = BigInt("0x" + flag.slice(8)); return { user_id, seq }; @@ -27,7 +28,7 @@ function parseFriendRequestFlag(flag) { * @param {BigInt|number} seq * @param {0|1} invite */ -function genGroupRequestFlag(user_id, group_id, seq, invite) { +function _genGroupRequestFlag(user_id, group_id, seq, invite) { const buf = Buffer.allocUnsafe(8); buf.writeUInt32BE(user_id), buf.writeUInt32BE(group_id, 4); return buf.toString("hex") + invite + seq.toString(16); @@ -36,7 +37,7 @@ function genGroupRequestFlag(user_id, group_id, seq, invite) { /** * @param {string} flag */ -function parseGroupRequestFlag(flag) { +function _parseGroupRequestFlag(flag) { const user_id = parseInt(flag.slice(0, 8), 16); const group_id = parseInt(flag.slice(8, 16), 16); const invite = parseInt(flag.slice(16, 17)); @@ -48,11 +49,11 @@ function parseGroupRequestFlag(flag) { * @param {import("./ref").Proto} proto * @returns {import("./ref").FriendAddEventData} */ -function parseFrdSysMsg(proto) { +function _parseFrdSysMsg(proto) { const time = proto[4]; const user_id = proto[5]; const nickname = String(proto[50][51].raw); - const flag = genFriendRequestFlag(user_id, proto[3]); + const flag = _genFriendRequestFlag(user_id, proto[3]); const source = String(proto[50][5].raw); const comment = String(proto[50][4].raw); const sex = proto[50][67] === 0 ? "male" : (proto[50][67] === 1 ? "famale" : "unknown"); @@ -64,7 +65,7 @@ function parseFrdSysMsg(proto) { * @param {import("./ref").Proto} proto * @returns {import("./ref").GroupAddEventData | import("./ref").GroupInviteEventData} */ -function parseGrpSysMsg(proto) { +function _parseGrpSysMsg(proto) { const type = proto[50][12]; const time = proto[4]; const group_id = proto[50][10]; @@ -83,7 +84,7 @@ function parseGrpSysMsg(proto) { data.inviter_id = proto[50][11]; // data.actor_id = proto[50][16]; } - data.flag = genGroupRequestFlag(data.user_id, group_id, proto[3], invite); + data.flag = _genGroupRequestFlag(data.user_id, group_id, proto[3], invite); return data; } @@ -113,7 +114,7 @@ async function getNewFriend() { const blob = await this.sendUni("ProfileService.Pb.ReqSystemMsgNew.Friend", frd_buf); const rsp = pb.decode(blob)[9]; const proto = Array.isArray(rsp) ? rsp[0] : rsp; - const data = parseFrdSysMsg(proto); + const data = _parseFrdSysMsg(proto); if (this.msgExists(data.user_id, 187, proto[3], data.time)) return; this.logger.info(`收到 ${data.user_id}(${data.nickname}) 的加好友请求 (flag: ${data.flag})`); @@ -174,7 +175,7 @@ async function getNewGroup(type) { } } if (!v) return; - const data = parseGrpSysMsg(v); + const data = _parseGrpSysMsg(v); if (this.msgExists(data.group_id, type, v[3], data.time)) return; if (type === 84 || type === 525) { @@ -205,7 +206,7 @@ async function getSysMsg() { rsp = [rsp]; for (let proto of rsp) { try { - data.push(Object.assign(this.parseEventType("request.friend.add"), parseFrdSysMsg(proto))); + data.push(Object.assign(this.parseEventType("request.friend.add"), _parseFrdSysMsg(proto))); } catch { } } })(); @@ -227,7 +228,7 @@ async function getSysMsg() { } else { continue; } - data.push(Object.assign(this.parseEventType("request.group." + sub_type), parseGrpSysMsg(proto))); + data.push(Object.assign(this.parseEventType("request.group." + sub_type), _parseGrpSysMsg(proto))); } })(); @@ -241,7 +242,7 @@ async function getSysMsg() { * @returns {import("./ref").ProtocolResponse} */ async function friendAction(flag, approve = true, remark = "", block = false) { - const { user_id, seq } = parseFriendRequestFlag(flag); + const { user_id, seq } = _parseFriendRequestFlag(flag); const body = pb.encode({ 1: 1, 2: seq, @@ -266,7 +267,7 @@ async function friendAction(flag, approve = true, remark = "", block = false) { * @returns {import("./ref").ProtocolResponse} */ async function groupAction(flag, approve = true, reason = "", block = false) { - const { user_id, group_id, seq, invite } = parseGroupRequestFlag(flag); + const { user_id, group_id, seq, invite } = _parseGroupRequestFlag(flag); const body = pb.encode({ 1: 1, 2: seq, @@ -288,5 +289,6 @@ async function groupAction(flag, approve = true, reason = "", block = false) { } module.exports = { - getNewFriend, getNewGroup, friendAction, groupAction, getSysMsg + getNewFriend, getNewGroup, + getSysMsg, friendAction, groupAction, }; diff --git a/lib/troop.js b/lib/troop.js index 5046c55b..aa7615a3 100644 --- a/lib/troop.js +++ b/lib/troop.js @@ -1,5 +1,6 @@ /** * 常用群功能和好友功能 + * 相关api */ "use strict"; const querystring = require("querystring"); @@ -12,9 +13,9 @@ const { downloadWebImage, highwayUpload, readFile } = require("./service"); /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Number} user_id - * @param {Boolean} enable + * @param {number} group_id + * @param {number} user_id + * @param {boolean} enable * @returns {import("./ref").ProtocolResponse} */ async function setAdmin(group_id, user_id, enable = true) { @@ -43,10 +44,10 @@ async function setAdmin(group_id, user_id, enable = true) { /** * 设置头衔 * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Number} user_id - * @param {String} title - * @param {Number} duration + * @param {number} group_id + * @param {number} user_id + * @param {string} title + * @param {number} duration * @returns {import("./ref").ProtocolResponse} */ async function setTitle(group_id, user_id, title = "", duration = -1) { @@ -69,8 +70,8 @@ async function setTitle(group_id, user_id, title = "", duration = -1) { /** * 群设置 * @this {import("./ref").Client} - * @param {Number} group_id - * @param {String} k + * @param {number} group_id + * @param {string} k * @param {any} v * @returns {import("./ref").ProtocolResponse} */ @@ -94,9 +95,9 @@ async function doSetting(group_id, k, v) { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Number} user_id - * @param {String} card + * @param {number} group_id + * @param {number} user_id + * @param {string} card * @returns {import("./ref").ProtocolResponse} */ async function setCard(group_id, user_id, card = "") { @@ -123,9 +124,9 @@ async function setCard(group_id, user_id, card = "") { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Number} user_id - * @param {Boolean} block + * @param {number} group_id + * @param {number} user_id + * @param {boolean} block * @returns {import("./ref").ProtocolResponse} */ async function kickMember(group_id, user_id, block = false) { @@ -158,9 +159,9 @@ async function kickMember(group_id, user_id, block = false) { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Number} user_id - * @param {Number} duration + * @param {number} group_id + * @param {number} user_id + * @param {number} duration * @returns {import("./ref").ProtocolResponse} */ async function muteMember(group_id, user_id, duration = 1800) { @@ -176,8 +177,8 @@ async function muteMember(group_id, user_id, duration = 1800) { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Boolean} dismiss + * @param {number} group_id + * @param {boolean} dismiss * @returns {import("./ref").ProtocolResponse} */ async function quitGroup(group_id, dismiss = false) { @@ -207,8 +208,8 @@ async function quitGroup(group_id, dismiss = false) { /** * @this {import("./ref").Client} - * @param {Number} group_id 发送的对象,可以是好友uin - * @param {Number} user_id 戳一戳的对象 + * @param {number} group_id 发送的对象,可以是好友uin + * @param {number} user_id 戳一戳的对象 * @returns {import("./ref").ProtocolResponse} */ async function pokeMember(group_id, user_id) { @@ -224,8 +225,8 @@ async function pokeMember(group_id, user_id) { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {String} comment + * @param {number} group_id + * @param {string} comment * @returns {import("./ref").ProtocolResponse} */ async function addGroup(group_id, comment = "") { @@ -253,8 +254,8 @@ async function addGroup(group_id, comment = "") { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Number} user_id + * @param {number} group_id + * @param {number} user_id * @returns {import("./ref").ProtocolResponse} */ async function inviteFriend(group_id, user_id) { @@ -271,8 +272,8 @@ async function inviteFriend(group_id, user_id) { /** * 启用/禁用 匿名 * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Boolean} enable + * @param {number} group_id + * @param {boolean} enable * @returns {import("./ref").ProtocolResponse} */ async function setAnonymous(group_id, enable = true) { @@ -283,9 +284,9 @@ async function setAnonymous(group_id, enable = true) { } /** - * @param {String} flag + * @param {string} flag */ -function parseAnonFlag(flag) { +function _parseAnonFlag(flag) { const split = flag.split("@"); return { id: split[1], @@ -295,9 +296,9 @@ function parseAnonFlag(flag) { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {String} flag - * @param {Number} duration + * @param {number} group_id + * @param {string} flag + * @param {number} duration * @returns {import("./ref").ProtocolResponse} */ async function muteAnonymous(group_id, flag, duration = 1800) { @@ -305,7 +306,7 @@ async function muteAnonymous(group_id, flag, duration = 1800) { duration = parseInt(duration); if (duration > 2592000 || duration < 0) duration = 2592000; - const { id, nick } = parseAnonFlag(flag); + const { id, nick } = _parseAnonFlag(flag); const body = querystring.stringify({ anony_id: id, group_code: group_id, @@ -342,10 +343,10 @@ async function muteAnonymous(group_id, flag, duration = 1800) { } /** - * @param {String|Buffer} file + * @param {string|Buffer} file * @returns {Buffer} */ -async function getImgBuf(file) { +async function _getImgBuf(file) { let buf; if (file instanceof Buffer) { buf = file; @@ -366,10 +367,10 @@ async function getImgBuf(file) { /** * 设置头像 * @this {import("./ref").Client} - * @param {String|Buffer} file + * @param {string|Buffer} file */ async function setPortrait(file) { - const buf = await getImgBuf(file); + const buf = await _getImgBuf(file); const body = pb.encode({ 1281: { 1: this.uin, @@ -391,11 +392,11 @@ async function setPortrait(file) { /** * 群头像 * @this {import("./ref").Client} - * @param {String|Buffer} file + * @param {string|Buffer} file */ async function setGroupPortrait(group_id, file) { [group_id] = uinAutoCheck(group_id); - const buf = await getImgBuf(file); + const buf = await _getImgBuf(file); await this.getCookies(); const url = `http://htdata3.qq.com/cgi-bin/httpconn?htcmd=0x6ff0072&ver=5520&ukey=${this.sig.skey}&range=0&uin=${this.uin}&seq=${this.seq_id}&groupuin=${group_id}&filetype=3&imagetype=5&userdata=0&subcmd=1&subver=101&clip=0_0_0_0&filesize=${buf.length}`; try { @@ -415,8 +416,8 @@ async function setGroupPortrait(group_id, file) { /** * @this {import("./ref").Client} - * @param {Number} user_id - * @param {Number} times 1~20 + * @param {number} user_id + * @param {number} times 1~20 * @returns {import("./ref").ProtocolResponse} */ async function sendLike(user_id, times = 1) { @@ -445,10 +446,10 @@ async function sendLike(user_id, times = 1) { /** * 获取对方加好友设置(暂不对外开放) * @this {import("./ref").Client} - * @param {Number} user_id - * @returns {Number} + * @param {number} user_id + * @returns {number} */ -async function getAddSetting(user_id) { +async function _getAddSetting(user_id) { const FS = jce.encodeStruct([ this.uin, user_id, 3004, 0, null, 1 @@ -468,9 +469,9 @@ async function getAddSetting(user_id) { /** * @this {import("./ref").Client} - * @param {Number} group_id - * @param {Number} user_id - * @param {String} comment + * @param {number} group_id + * @param {number} user_id + * @param {string} comment * @returns {import("./ref").ProtocolResponse} */ async function addFriend(group_id, user_id, comment = "") { @@ -480,7 +481,7 @@ async function addFriend(group_id, user_id, comment = "") { } else { [group_id, user_id] = uinAutoCheck(group_id, user_id); } - const type = await getAddSetting.call(this, user_id); + const type = await _getAddSetting.call(this, user_id); if (![0, 1, 4].includes(type)) return { result: type }; comment = String(comment); @@ -503,8 +504,8 @@ async function addFriend(group_id, user_id, comment = "") { /** * @this {import("./ref").Client} - * @param {Number} user_id - * @param {Boolean} block + * @param {number} user_id + * @param {boolean} block * @returns {import("./ref").ProtocolResponse} */ async function delFriend(user_id, block = true) { @@ -528,8 +529,8 @@ async function delFriend(user_id, block = true) { /** * 设置个人资料 * @this {import("./ref").Client} - * @param {Number} k - * @param {Buffer|String} v + * @param {number} k + * @param {Buffer|string} v * @returns {import("./ref").ProtocolResponse} */ async function setProfile(k, v) { @@ -548,7 +549,7 @@ async function setProfile(k, v) { /** * 设置签名 * @this {import("./ref").Client} - * @param {String} sign + * @param {string} sign * @returns {import("./ref").ProtocolResponse} */ async function setSign(sign = "") { @@ -581,7 +582,7 @@ async function setSign(sign = "") { /** * 设置在线状态 * @this {import("./ref").Client} - * @param {Number} status + * @param {number} status * @returns {import("./ref").ProtocolResponse} */ async function setStatus(status) { diff --git a/lib/wtlogin/wt.js b/lib/wtlogin/wt.js index faf31204..0da4bb1e 100644 --- a/lib/wtlogin/wt.js +++ b/lib/wtlogin/wt.js @@ -1,5 +1,5 @@ /** - * login相关处理 + * login相关处理和api */ "use strict"; const fs = require("fs"); From 5af8dc1438bfc02a37115b6d8c6d859eeaf22a06 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 14 Mar 2021 19:17:53 +0900 Subject: [PATCH 16/20] up to 1.14 --- package-lock.json | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41cdeaff..39d6d3e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "oicq", - "version": "1.13.3", + "version": "1.14.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3e56868c..553b005d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "oicq", - "version": "1.13.3", - "upday": "2021/3/1", + "version": "1.14.0", + "upday": "2021/3/14", "description": "QQ protocol!", "main": "client.js", "types": "client.d.ts", From bde605930ad4c78017252f3f2d18be31f01264c0 Mon Sep 17 00:00:00 2001 From: takayama Date: Tue, 16 Mar 2021 12:38:14 +0900 Subject: [PATCH 17/20] =?UTF-8?q?c2c=20msg=20id=20=E6=9C=AB=E5=B0=BE?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=80=E4=B8=AA=E5=8F=91=E9=80=81flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common.js | 56 ++++++++++++++++++-------------- lib/message/builder.js | 73 +++++++++++++++++++++--------------------- lib/message/chat.js | 18 +++++++---- lib/message/parser.js | 16 +++++++-- lib/online-push.js | 10 +++--- 5 files changed, 99 insertions(+), 74 deletions(-) diff --git a/lib/common.js b/lib/common.js index 1ae20144..9b377f44 100644 --- a/lib/common.js +++ b/lib/common.js @@ -20,21 +20,21 @@ function checkUin(uin) { } /** - * @param {number} group_id - * @param {number} user_id + * @param {number} gid + * @param {number} uid */ -function uinAutoCheck(group_id, user_id) { - group_id = parseInt(group_id); - if (!checkUin(group_id)) { +function uinAutoCheck(gid, uid) { + gid = parseInt(gid); + if (!checkUin(gid)) { throw new Error("bad group_id or user_id"); } - if (user_id) { - user_id = parseInt(user_id); - if (!checkUin(user_id)) { + if (uid) { + uid = parseInt(uid); + if (!checkUin(uid)) { throw new Error("bad group_id or user_id"); } } - return [group_id, user_id]; + return [gid, uid]; } /** @@ -86,21 +86,25 @@ function log(any) { } /** - * @param {number} user_id - * @param {number} seq - * @param {number} random - * @param {number} time + * 生成私聊消息id + * @param {number} user_id 对方QQ号 + * @param {number} seq 序号 + * @param {number} random 随机数 + * @param {number} time unix时间戳 + * @param {number} flag 接收为0 发送为1 */ -function genC2CMessageId(user_id, seq, random, time) { - const buf = Buffer.allocUnsafe(16); +function genC2CMessageId(user_id, seq, random, time, flag = 0) { + const buf = Buffer.allocUnsafe(17); buf.writeUInt32BE(user_id); buf.writeInt32BE(seq & 0xffffffff, 4); buf.writeInt32BE(random & 0xffffffff, 8); buf.writeUInt32BE(time, 12); + buf.writeUInt8(flag, 16); return buf.toString("base64"); } /** + * 解析私聊消息id * @param {string} message_id */ function parseC2CMessageId(message_id) { @@ -108,17 +112,19 @@ function parseC2CMessageId(message_id) { const user_id = buf.readUInt32BE(), seq = buf.readUInt32BE(4), random = buf.readUInt32BE(8), - time = buf.readUInt32BE(12); - return { user_id, seq, random, time }; + time = buf.readUInt32BE(12), + flag = buf.length >= 17 ? buf.readUInt8(16) : 0; + return { user_id, seq, random, time, flag }; } /** - * @param {number} group_id - * @param {number} user_id - * @param {number} seq - * @param {number} random - * @param {number} time - * @param {number} pktnum + * 生成群消息id + * @param {number} group_id 群号 + * @param {number} user_id 发送者QQ号 + * @param {number} seq 序号 + * @param {number} random 随机数 + * @param {number} time unix时间戳 + * @param {number} pktnum 分片数 */ function genGroupMessageId(group_id, user_id, seq, random, time, pktnum = 1) { const buf = Buffer.allocUnsafe(21); @@ -132,6 +138,7 @@ function genGroupMessageId(group_id, user_id, seq, random, time, pktnum = 1) { } /** + * 解析群消息id * @param {string} message_id */ function parseGroupMessageId(message_id) { @@ -141,7 +148,7 @@ function parseGroupMessageId(message_id) { seq = buf.readUInt32BE(8), random = buf.readUInt32BE(12), time = buf.readUInt32BE(16), - pktnum = buf.length === 21 ? buf.readUInt8(20) : 1; + pktnum = buf.length >= 21 ? buf.readUInt8(20) : 1; return { group_id, user_id, seq, random, time, pktnum }; } @@ -153,6 +160,7 @@ function genMessageUuid(random) { } /** + * 解析彩色群名片 * @param {Buffer} buf */ function parseFunString(buf) { diff --git a/lib/message/builder.js b/lib/message/builder.js index 23823bd6..01d70f07 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -7,7 +7,7 @@ const zlib = require("zlib"); const { randomBytes } = require("crypto"); const music = require("./music"); const face = require("./face"); -const { getOneC2CMsg, getOneGroupMsg } = require("./history"); +const { getC2CMsgs, getGroupMsgs } = require("./history"); const { genPttElem } = require("./ptt"); const { ImageBuilder, uploadImages } = require("./image"); const pb = require("../pb"); @@ -619,23 +619,9 @@ class Builder { async buildReplyElem(cq) { if (this.reply) return; - var { id } = cq; - var source = [{ - 1: { - 1: "[消息]" - } - }]; - try { - if (this.type) - var { user_id, seq, random, time } = parseGroupMessageId(id); - else - var { user_id, seq, random, time } = parseC2CMessageId(id); - } catch { - return this.c.logger.warn("incorrect reply id: " + id); - } try { - var msg = await (this.type ? getOneGroupMsg : getOneC2CMsg).call(this.c, id); - source = msg[3][1][2]; + const { user_id, seq, random, time, msg, flag } = await this.getMsg(cq.id); + let source = msg[3][1][2]; if (Array.isArray(source)) { const bufs = []; for (let v of source) @@ -644,23 +630,23 @@ class Builder { } else { source = source.raw; } + this.reply = pb.encode({ + 45: { + 1: [seq], + 2: flag ? this.c.uin : user_id, + 3: time, + 4: 1, + 5: source, + 6: 0, + 8: { + 3: genMessageUuid(random) + }, + 10: this.type ? common.code2uin(this.target) : this.c.uin + } + }); } catch { - return this.c.logger.warn("incorrect reply id: " + id); + return this.c.logger.warn("incorrect reply id: " + cq.id); } - this.reply = pb.encode({ - 45: { - 1: [seq], - 2: user_id, - 3: time, - 4: 1, - 5: source, - 6: 0, - 8: { - 3: genMessageUuid(random) - }, - 10: this.type ? common.code2uin(this.target) : this.c.uin - } - }); } /** @@ -704,20 +690,35 @@ class Builder { * @param {import("../ref").NodeElem["data"]} cq */ buildNodeElem(cq) { - const { id } = cq; const task = (async () => { try { this.nodes.push(null); const index = this.nodes.length - 1; - const msg = await (id.length > 24 ? getOneGroupMsg : getOneC2CMsg).call(this.c, id); + const { msg } = await this.getMsg(cq.id); this.nodes[index] = msg.raw; } catch { - this.c.logger.warn("获取消息节点失败,message_id: " + id); + this.c.logger.warn("获取消息节点失败,message_id: " + cq.id); } })(); this.tasks.push(task); } + /** + * @private + * @param {string} id + */ + async getMsg(id) { + if (id.length > 24) { + const { group_id ,user_id, seq, random, time } = parseGroupMessageId(id); + const msgs = await getGroupMsgs.call(this.c, group_id, seq, seq); + return { user_id, seq, random, time, msg: msgs[0] }; + } else { + const { user_id, seq, random, time, flag } = parseC2CMessageId(id); + const msgs = await getC2CMsgs.call(this.c, user_id, time, 1); + return { user_id, seq, random, time, msg: msgs[0], flag }; + } + } + /** * @private * @param {import("../ref").MessageElem["type"]} type @@ -965,7 +966,7 @@ class Builder { } if (retcode === 0) { if (this.type === 0) { //私聊 - message_id = genC2CMessageId(this.target, this.seq, this.random, rsp[3]); + message_id = genC2CMessageId(this.target, this.seq, this.random, rsp[3], 1); } if (this.type === 1 && !message_id) { //群聊 message_id = await this.waitForMessageId(this.c.config.resend ? 500 : 5000); diff --git a/lib/message/chat.js b/lib/message/chat.js index 072f4d0f..0884f742 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -166,21 +166,25 @@ async function getMsgs(message_id, count = 20) { // todo 分片处理 for (let msg of msgs) { try { - const obj = await parseGroupMsg.call(this, msg); - data.push(Object.assign(this.parseEventType("message.group"), obj)); + data.push(Object.assign(this.parseEventType("message.group"), await parseGroupMsg.call(this, msg))); } catch { } } } else { const { user_id, time, random } = parseC2CMessageId(message_id); msgs = await getC2CMsgs.call(this, user_id, time, count); - for (let i = msgs.length - 1; i >= 0; --i) { + for (let msg of msgs) { try { - const msg = msgs[i]; - if (msg[3][1][1][3] !== random && !data.length) - continue; - data.unshift(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); + data.push(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); } catch { } } + // for (let i = msgs.length - 1; i >= 0; --i) { + // try { + // const msg = msgs[i]; + // if (msg[3][1][1][3] !== random && !data.length) + // continue; + // data.unshift(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); + // } catch { } + // } } return { result: 0, data }; } diff --git a/lib/message/parser.js b/lib/message/parser.js index 5d2c4b0d..dd3612f3 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -452,7 +452,12 @@ class Parser { let random = elem[8][3]; if (typeof random === "bigint") random = parseInt(random & 0xffffffffn); - msg.data.id = genC2CMessageId(this.uid, elem[1], random, elem[3]); + let user_id = this.uid, flag = 0; + if (user_id === this.c.uin) { + user_id = elem[2]; + flag = 1; + } + msg.data.id = genC2CMessageId(user_id, elem[1], random, elem[3], flag); } this.message.unshift(msg); this.raw_message = (this.c.config.brief ? "[回复]" : genCQMsg(msg)) + this.raw_message; @@ -525,7 +530,7 @@ 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 - const user_id = head[1], + let user_id = head[1], flag = 0, time = head[6], seq = head[5]; let sub_type, message_id = "", font = "unknown"; @@ -552,7 +557,12 @@ async function parseC2CMsg(msg, realtime = false) { } } try { - message_id = genC2CMessageId(user_id, seq, body[1][1][3], time); + let uin = user_id; + if (user_id === this.uin) { + uin = head[2]; + flag = 1; + } + message_id = genC2CMessageId(uin, seq, body[1][1][3], time, flag); font = String(body[1][1][9].raw); } catch { } if (type === 529) { diff --git a/lib/online-push.js b/lib/online-push.js index 1b0cf335..1e9ca769 100644 --- a/lib/online-push.js +++ b/lib/online-push.js @@ -123,11 +123,13 @@ const push528 = { let data = pb.decode(buf)[1]; if (Array.isArray(data)) data = data[0]; - const user_id = data[1]; - if (user_id === this.uin) - return; + let user_id = data[1], flag = 0; + if (user_id === this.uin) { + user_id = data[2]; + flag = 1; + } this.em("notice.friend.recall", { - user_id, message_id: genC2CMessageId(user_id, data[3], data[6], data[5]), time + user_id, message_id: genC2CMessageId(user_id, data[3], data[6], data[5], flag), time }); }, 0x8B: function (buf, time) { From 6f15cdc2604a14c746b564505bd8200946d0fe05 Mon Sep 17 00:00:00 2001 From: takayama Date: Wed, 17 Mar 2021 10:01:28 +0900 Subject: [PATCH 18/20] fix #142 --- lib/common.js | 9 ++++++- lib/message/builder.js | 6 +++-- lib/message/chat.js | 19 +++++-------- lib/message/parser.js | 61 +++++++++++++++++++----------------------- lib/ref.d.ts | 2 ++ 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/lib/common.js b/lib/common.js index 9b377f44..d2dd433e 100644 --- a/lib/common.js +++ b/lib/common.js @@ -159,6 +159,13 @@ function genMessageUuid(random) { return 16777216n << 32n | BigInt(random); } +/** + * @param {bigint} msg_uuid + */ +function genRandom(msg_uuid) { + return Number(BigInt(msg_uuid) & 0xffffffffn); +} + /** * 解析彩色群名片 * @param {Buffer} buf @@ -184,5 +191,5 @@ function parseFunString(buf) { module.exports = { uuid, md5, timestamp, checkUin, uinAutoCheck, log, code2uin, uin2code, parseFunString, - genC2CMessageId, parseC2CMessageId, genGroupMessageId, parseGroupMessageId, genMessageUuid + genC2CMessageId, parseC2CMessageId, genGroupMessageId, parseGroupMessageId, genMessageUuid, genRandom }; diff --git a/lib/message/builder.js b/lib/message/builder.js index 01d70f07..e2c494c6 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -13,7 +13,7 @@ const { ImageBuilder, uploadImages } = require("./image"); const pb = require("../pb"); const common = require("../common"); const { highwayUpload } = require("../service"); -const { parseC2CMessageId, parseGroupMessageId, genMessageUuid, genC2CMessageId } = common; +const { parseC2CMessageId, parseGroupMessageId, genMessageUuid, genC2CMessageId, genRandom } = common; const EMOJI_NOT_ENDING = ["\uD83C", "\uD83D", "\uD83E", "\u200D"]; const EMOJI_NOT_STARTING = ["\uFE0F", "\u200D", "\u20E3"]; const PB_CONTENT = pb.encode({ 1: 1, 2: 0, 3: 0 }); @@ -714,7 +714,9 @@ class Builder { return { user_id, seq, random, time, msg: msgs[0] }; } else { const { user_id, seq, random, time, flag } = parseC2CMessageId(id); - const msgs = await getC2CMsgs.call(this.c, user_id, time, 1); + const msgs = await getC2CMsgs.call(this.c, user_id, time + 1, 1); + if (genRandom(msgs[0][1][7]) !== random) + throw new Error(); return { user_id, seq, random, time, msg: msgs[0], flag }; } } diff --git a/lib/message/chat.js b/lib/message/chat.js index 0884f742..4b5a38e8 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -8,7 +8,7 @@ const { getC2CMsgs, getGroupMsgs } = require("./history"); const { parseC2CMsg, parseGroupMsg, parseForwardMsg } = require("./parser"); const common = require("../common"); const pb = require("../pb"); -const { parseC2CMessageId, parseGroupMessageId, genMessageUuid } = common; +const { parseC2CMessageId, parseGroupMessageId, genMessageUuid, genRandom } = common; //send msg---------------------------------------------------------------------------------------------------- @@ -171,20 +171,15 @@ async function getMsgs(message_id, count = 20) { } } else { const { user_id, time, random } = parseC2CMessageId(message_id); - msgs = await getC2CMsgs.call(this, user_id, time, count); - for (let msg of msgs) { + msgs = await getC2CMsgs.call(this, user_id, time + 1, count); + for (let i = msgs.length - 1; i >= 0; --i) { try { - data.push(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); + const msg = msgs[i]; + if (genRandom(msg[1][7]) !== random && !data.length) + continue; + data.unshift(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); } catch { } } - // for (let i = msgs.length - 1; i >= 0; --i) { - // try { - // const msg = msgs[i]; - // if (msg[3][1][1][3] !== random && !data.length) - // continue; - // data.unshift(Object.assign(this.parseEventType("message.private"), await parseC2CMsg.call(this, msg))); - // } catch { } - // } } return { result: 0, data }; } diff --git a/lib/message/parser.js b/lib/message/parser.js index dd3612f3..3745ba62 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -13,7 +13,7 @@ const { int32ip2str } = require("../service"); const { buildImageFileParam } = require("./image"); const { getGroupFileUrl, getC2CFileUrl, getVideoUrl } = require("./file"); const pb = require("../pb"); -const { genC2CMessageId, genGroupMessageId, timestamp, parseFunString, code2uin, log } = require("../common"); +const { genC2CMessageId, genGroupMessageId, timestamp, parseFunString, code2uin, genRandom } = require("../common"); function escapeCQInside(s) { if (s === "&") return "&"; @@ -444,20 +444,17 @@ class Parser { id: "" } }; + let seq = elem[1], user_id = elem[2]; if (this.gid) { - let m = await getGroupMsgs.call(this.c, this.gid, elem[1], elem[1]); - m = m[0]; - msg.data.id = genGroupMessageId(this.gid, elem[2], elem[1], m[3][1][1][3], m[1][6]); + 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 = elem[8][3]; - if (typeof random === "bigint") - random = parseInt(random & 0xffffffffn); - let user_id = this.uid, flag = 0; - if (user_id === this.c.uin) { - user_id = elem[2]; - flag = 1; - } - msg.data.id = genC2CMessageId(user_id, elem[1], random, elem[3], flag); + 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; @@ -530,12 +527,19 @@ 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 user_id = head[1], flag = 0, - time = head[6], - seq = head[5]; - let sub_type, message_id = "", font = "unknown"; + 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].raw) : "unknown"; - const sender = Object.assign({ user_id }, this.fl.get(user_id)); + 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]) { @@ -545,36 +549,27 @@ async function parseC2CMsg(msg, realtime = false) { } else if (type === 167) { sub_type = "single"; } else { - sub_type = this.fl.has(user_id) ? "friend" : "single"; + sub_type = this.fl.has(from_uin) ? "friend" : "single"; } if (sender.nickname === undefined) { - const stranger = (await this.getStrangerInfo(user_id, seq % 5 == 0 && realtime)).data; + 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(user_id) || realtime) - this.sl.set(user_id, stranger); + if (!this.sl.has(from_uin) || realtime) + this.sl.set(from_uin, stranger); } } - try { - let uin = user_id; - if (user_id === this.uin) { - uin = head[2]; - flag = 1; - } - message_id = genC2CMessageId(uin, seq, body[1][1][3], time, flag); - font = String(body[1][1][9].raw); - } catch { } 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, user_id, 0); + var parser = new Parser(this, uid, 0); await parser.parseMsg(body[1]); } return { - sub_type, message_id, user_id, + sub_type, message_id, user_id: from_uin, message: parser.message, raw_message: parser.raw_message, font, sender, time, diff --git a/lib/ref.d.ts b/lib/ref.d.ts index d15e7f35..97ad9d4d 100644 --- a/lib/ref.d.ts +++ b/lib/ref.d.ts @@ -98,6 +98,7 @@ export interface MsgHead extends Proto { 4: bigint, //uuid 5: number, //seqid 6: number, //time + 7: bigint, //uuid 8: { //routing 4: number //group_id }, @@ -133,6 +134,7 @@ export interface RichMsg extends Proto { } export interface MsgAttr extends Proto { + 2: number, //time 3: number, //random integer 9: Proto, //font } From c414685845af4ce106907e1b7712b84c7d261c3a Mon Sep 17 00:00:00 2001 From: takayama Date: Thu, 18 Mar 2021 00:22:22 +0900 Subject: [PATCH 19/20] fix log --- lib/core.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core.js b/lib/core.js index 382307ba..72012cf0 100644 --- a/lib/core.js +++ b/lib/core.js @@ -116,6 +116,7 @@ async function getMsg(sync_flag = 0) { (async () => { const group_id = common.uin2code(uin); const user_id = head[15]; + const nickname = String(head[16].raw); const ginfo = (await this.getGroupInfo(group_id)).data; if (!ginfo) return; if (user_id === this.uin) { @@ -129,11 +130,10 @@ async function getMsg(sync_flag = 0) { if (this.gml.get(group_id).size) ginfo.member_count = this.gml.get(group_id).size; } catch { } - this.logger.info(`${user_id}(${this.nickname}) 加入了群 ${group_id}`); + this.logger.info(`${user_id}(${nickname}) 加入了群 ${group_id}`); } this.em("notice.group.increase", { - group_id, user_id, - nickname: String(head[16].raw) + group_id, user_id, nickname }); })(); } From 0baa41d52891448b97f30a5bd425fc66374a78da Mon Sep 17 00:00:00 2001 From: takayama-lily Date: Thu, 18 Mar 2021 00:23:19 +0900 Subject: [PATCH 20/20] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 553b005d..d7660abb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "oicq", "version": "1.14.0", - "upday": "2021/3/14", + "upday": "2021/3/18", "description": "QQ protocol!", "main": "client.js", "types": "client.d.ts",