"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Contactable = void 0;
exports.getPttBuffer = getPttBuffer;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const querystring_1 = __importDefault(require("querystring"));
const axios_1 = __importDefault(require("axios"));
const stream_1 = require("stream");
const crypto_1 = require("crypto");
const child_process_1 = require("child_process");
const core_1 = require("../core");
const errors_1 = require("../errors");
const common_1 = require("../common");
const message_1 = require("../message");
const highway_1 = require("./highway");
const share_1 = require("../message/share");
const silk_1 = require("../core/silk");
/** 所有用户和群的基类 */
class Contactable {
    // 对方账号，可能是群号也可能是QQ号
    get target() {
        return this.uid || this.gid || this.c.uin;
    }
    // 是否是 Direct Message (私聊)
    get dm() {
        return !!this.uid;
    }
    /** 返回所属的客户端对象 */
    get client() {
        return this.c;
    }
    constructor(c) {
        this.c = c;
        (0, common_1.lock)(this, "c");
    }
    get [Symbol.unscopables]() {
        return {
            c: true,
        };
    }
    // 取私聊图片fid
    async _offPicUp(imgs) {
        const req = [];
        for (const img of imgs) {
            req.push({
                1: this.c.uin,
                2: this.uid,
                3: 0,
                4: img.md5,
                5: img.size,
                6: img.md5.toString("hex"),
                7: 5,
                8: 9,
                9: 0,
                10: 0,
                11: 0, //retry
                12: 1, //bu
                13: img.origin ? 1 : 0,
                14: img.width,
                15: img.height,
                16: img.type,
                17: this.c.apk.version,
                22: 0,
            });
        }
        const body = core_1.pb.encode({
            1: 1,
            2: req,
            // 10: 3
        });
        const payload = await this.c.sendUni("LongConn.OffPicUp", body);
        return core_1.pb.decode(payload)[2];
    }
    // 取群聊图片fid
    async _groupPicUp(imgs) {
        const req = [];
        for (const img of imgs) {
            req.push({
                1: this.gid,
                2: this.c.uin,
                3: 0,
                4: img.md5,
                5: img.size,
                6: img.md5.toString("hex"),
                7: 5,
                8: 9,
                9: 1, //bu
                10: img.width,
                11: img.height,
                12: img.type,
                13: this.c.apk.version,
                14: 0,
                15: 1052,
                16: img.origin ? 1 : 0,
                18: 0,
                19: 0,
            });
        }
        const body = core_1.pb.encode({
            1: 3,
            2: 1,
            3: req,
        });
        const payload = await this.c.sendUni("ImgStore.GroupPicUp", body);
        return core_1.pb.decode(payload)[3];
    }
    /** 上传一批图片以备发送(无数量限制)，理论上传一次所有群和好友都能发 */
    async uploadImages(_imgs) {
        const imgs = [];
        for (let img of _imgs) {
            if (!img.fid) {
                imgs.push(img);
            }
            else if (img instanceof message_1.Image) {
                if (img.deleteTmpFile)
                    img?.deleteTmpFile();
            }
        }
        this.c.logger.debug(`开始图片任务，共有${imgs.length}张图片`);
        const tasks = [];
        for (let i = 0; i < imgs.length; i++) {
            if (!(imgs[i] instanceof message_1.Image))
                imgs[i] = new message_1.Image(imgs[i], this.dm, path_1.default.join(this.c.dir, "../image"));
            tasks.push(imgs[i].task);
        }
        const res1 = (await Promise.allSettled(tasks));
        for (let i = 0; i < res1.length; i++) {
            if (res1[i].status === "rejected")
                this.c.logger.warn(`图片${i + 1}失败, reason: ` + res1[i].reason?.message);
        }
        let n = 0;
        while (imgs.length > n) {
            let rsp = await (this.dm ? this._offPicUp : this._groupPicUp).call(this, imgs.slice(n, n + 20));
            !Array.isArray(rsp) && (rsp = [rsp]);
            const tasks = [];
            for (let i = n; i < imgs.length; ++i) {
                if (i >= n + 20)
                    break;
                tasks.push(this._uploadImage(imgs[i], rsp[i % 20]));
            }
            const res2 = (await Promise.allSettled(tasks));
            for (let i = 0; i < res2.length; i++) {
                if (res2[i].status === "rejected") {
                    res1[n + i] = res2[i];
                    this.c.logger.warn(`图片${n + i + 1}上传失败, reason: ` + res2[i].reason?.message);
                }
            }
            n += 20;
        }
        this.c.logger.debug(`图片任务结束`);
        return res1;
    }
    async _uploadImage(img, rsp) {
        const j = this.dm ? 1 : 0;
        if (rsp[2 + j] !== 0)
            throw new Error(String(rsp[3 + j]));
        img.fid = rsp[9 + j].toBuffer?.() || rsp[9 + j];
        if (rsp[4 + j]) {
            img.deleteTmpFile();
            return;
        }
        if (!img.readable) {
            img.deleteCacheFile();
            return;
        }
        const ip = rsp[6 + j]?.[0] || rsp[6 + j];
        const port = rsp[7 + j]?.[0] || rsp[7 + j];
        return highway_1.highwayUpload
            .call(this.c, img.readable, {
            cmdid: j ? highway_1.CmdID.DmImage : highway_1.CmdID.GroupImage,
            md5: img.md5,
            size: img.size,
            ticket: rsp[8 + j].toBuffer(),
        }, ip, port)
            .finally(img.deleteTmpFile.bind(img));
    }
    /** 发送网址分享 */
    async shareUrl(content, config) {
        const body = (0, share_1.buildShare)((this.gid || this.uid), this.dm ? 0 : 1, content, config);
        await this.c.sendOidb("OidbSvc.0xb77_9", core_1.pb.encode(body));
    }
    /** 发送音乐分享 */
    async shareMusic(platform, id) {
        const body = await (0, message_1.buildMusic)((this.gid || this.uid), this.dm ? 0 : 1, platform, id);
        await this.c.sendOidb("OidbSvc.0xb77_9", core_1.pb.encode(body));
    }
    /** 发消息预处理 */
    async _preprocess(content, source) {
        try {
            if (!Array.isArray(content))
                content = [content];
            const forwardNode = content.filter(e => typeof e !== "string" && e.type === "node");
            const task = content
                .filter(e => !forwardNode.includes(e))
                .map(item => typeof item === "string" ? { type: "text", text: item } : item)
                .flat()
                .map(async (elem) => {
                if (elem.type === "video")
                    return await this.uploadVideo(elem);
                if (elem.type === "share")
                    return await this.shareUrl(elem);
                if (elem.type === "music")
                    return {
                        ...(await message_1.musicFactory[elem.platform].getMusicInfo(elem.id)),
                        ...elem,
                    };
                if (elem.type === "record")
                    return await this.uploadPtt(elem);
                return Promise.resolve(elem);
            });
            if (forwardNode.length)
                task.push(this.makeForwardMsg(forwardNode));
            content = (await Promise.all(task)).filter(Boolean);
            const converter = new message_1.Converter(content, {
                dm: this.dm,
                cachedir: path_1.default.join(this.c.dir, "image"),
                mlist: this.c.gml.get(this.gid),
            });
            if (source)
                await converter.quote(source);
            if (converter.imgs.length)
                await this.uploadImages(converter.imgs);
            return converter;
        }
        catch (e) {
            (0, errors_1.drop)(errors_1.ErrorCode.MessageBuilderError, e.message);
        }
    }
    async _downloadFileToTmpDir(url, headers) {
        const savePath = path_1.default.join(common_1.TMP_DIR, (0, common_1.uuid)());
        let readable = (await axios_1.default.get(url, {
            headers,
            responseType: "stream",
        })).data;
        readable = readable.pipe(new common_1.DownloadTransform());
        await (0, common_1.pipeline)(readable, fs_1.default.createWriteStream(savePath));
        return savePath;
    }
    async _saveFileToTmpDir(file) {
        const buf = file instanceof Buffer ? file : Buffer.from(file.slice(9), "base64");
        const savePath = path_1.default.join(common_1.TMP_DIR, (0, common_1.uuid)());
        await fs_1.default.promises.writeFile(savePath, buf);
        return savePath;
    }
    /** 上传一个视频以备发送(理论上传一次所有群和好友都能发) */
    async uploadVideo(elem) {
        let { file, temp = false } = elem;
        if (file instanceof Buffer || file.startsWith("base64://")) {
            file = await this._saveFileToTmpDir(file);
            temp = true;
        }
        else if (file.startsWith("protobuf://")) {
            return elem;
        }
        else if (file.startsWith("https://") || file.startsWith("http://")) {
            file = await this._downloadFileToTmpDir(file);
            temp = true;
        }
        file = file.replace(/^file:\/{2}/, "");
        common_1.IS_WIN && file.startsWith("/") && (file = file.slice(1));
        const thumb = path_1.default.join(common_1.TMP_DIR, (0, common_1.uuid)());
        await new Promise((resolve, reject) => {
            (0, child_process_1.exec)(`${this.c.config.ffmpeg_path || "ffmpeg"} -y -i "${file}" -f image2 -frames:v 1 "${thumb}"`, (error, stdout, stderr) => {
                this.c.logger.debug("ffmpeg output: " + stdout + stderr);
                fs_1.default.stat(thumb, err => {
                    if (err)
                        reject(new core_1.ApiRejection(errors_1.ErrorCode.FFmpegVideoThumbError, "ffmpeg获取视频图像帧失败"));
                    else
                        resolve(undefined);
                });
            });
        });
        const [width, height, seconds] = await new Promise(resolve => {
            (0, child_process_1.exec)(`${this.c.config.ffprobe_path || "ffprobe"} -i "${file}" -show_streams`, (error, stdout, stderr) => {
                const lines = (stdout || stderr || "").split("\n");
                let width = 1280, height = 720, seconds = 120;
                for (const line of lines) {
                    if (line.startsWith("width=")) {
                        width = parseInt(line.slice(6));
                    }
                    else if (line.startsWith("height=")) {
                        height = parseInt(line.slice(7));
                    }
                    else if (line.startsWith("duration=")) {
                        seconds = parseInt(line.slice(9));
                        break;
                    }
                }
                resolve([width, height, seconds]);
            });
        });
        const md5video = await (0, common_1.md5Stream)(fs_1.default.createReadStream(file));
        const md5thumb = await (0, common_1.md5Stream)(fs_1.default.createReadStream(thumb));
        const name = md5video.toString("hex") + ".mp4";
        const videosize = (await fs_1.default.promises.stat(file)).size;
        const thumbsize = (await fs_1.default.promises.stat(thumb)).size;
        const ext = core_1.pb.encode({
            1: this.c.uin,
            2: this.target,
            3: 1,
            4: 2,
            5: {
                1: name,
                2: md5video,
                3: md5thumb,
                4: videosize,
                5: height,
                6: width,
                7: 3,
                8: seconds,
                9: thumbsize,
            },
            6: this.target,
            20: 1,
        });
        const body = core_1.pb.encode({
            1: 300,
            3: ext,
            100: {
                1: 0,
                2: 1,
            },
        });
        const payload = await this.c.sendUni("PttCenterSvr.GroupShortVideoUpReq", body);
        const rsp = core_1.pb.decode(payload)[3];
        if (rsp[1])
            throw new Error(String(rsp[2]));
        if (!rsp[7]) {
            const md5 = await (0, common_1.md5Stream)(createReadable(thumb, file));
            await highway_1.highwayUpload.call(this.c, createReadable(thumb, file), {
                cmdid: highway_1.CmdID.ShortVideo,
                md5,
                size: thumbsize + videosize,
                ext,
                encrypt: true,
            });
        }
        fs_1.default.unlink(thumb, common_1.NOOP);
        if (temp)
            fs_1.default.unlink(file, common_1.NOOP);
        const buf = core_1.pb.encode({
            1: rsp[5].toBuffer(),
            2: md5video,
            3: name,
            4: 3,
            5: seconds,
            6: videosize,
            7: width,
            8: height,
            9: md5thumb,
            10: "camera",
            11: thumbsize,
            12: 0,
            15: 1,
            16: width,
            17: height,
            18: 0,
            19: 0,
        });
        return {
            type: "video",
            file: "protobuf://" + Buffer.from(buf).toString("base64"),
        };
    }
    /** 上传一个语音以备发送(理论上传一次所有群和好友都能发) */
    async uploadPtt(elem, transcoding = true, brief = "") {
        this.c.logger.debug("开始语音任务");
        if (typeof elem.file === "string" && elem.file.startsWith("protobuf://"))
            return elem;
        const buf = await getPttBuffer(elem.file, transcoding, this.c.config.ffmpeg_path || "ffmpeg");
        if (!elem.seconds && String(buf.slice(0, 7)).includes("SILK")) {
            elem.seconds = Math.ceil(((await (0, silk_1.getDuration)(buf)) || 0) / 1000);
        }
        const hash = (0, common_1.md5)(buf);
        const codec = String(buf.slice(0, 7)).includes("SILK") || !transcoding ? 1 : 0;
        const body = {
            1: 3,
            2: 3,
            5: {
                1: this.target,
                2: this.c.uin,
                3: 0,
                4: hash,
                5: buf.length,
                6: hash.toString("hex") + (codec ? ".slk" : ".amr"),
                7: 2,
                8: 9,
                9: 3,
                10: this.c.apk.version,
                12: elem.seconds || 1,
                13: 1,
                14: codec,
                15: 2,
            },
        };
        const payload = await this.c.sendUni("PttStore.GroupPttUp", core_1.pb.encode(body));
        const rsp = core_1.pb.decode(payload)[5];
        rsp[2] && (0, errors_1.drop)(rsp[2], rsp[3]);
        const ip = rsp[5]?.[0] || rsp[5], port = rsp[6]?.[0] || rsp[6];
        const ukey = rsp[7].toHex(), filekey = rsp[11].toHex();
        if (this.c.sig.bigdata.port) {
            await highway_1.highwayUpload.call(this.c, stream_1.Readable.from(Buffer.from(buf), { objectMode: false }), {
                cmdid: highway_1.CmdID.GroupPtt,
                md5: hash,
                size: buf.length,
                ext: core_1.pb.encode(body),
            });
        }
        else {
            const params = {
                ver: 4679,
                ukey,
                filekey,
                filesize: buf.length,
                bmd5: hash.toString("hex"),
                mType: "pttDu",
                voice_encodec: codec,
            };
            const url = `http://${(0, common_1.int32ip2str)(ip)}:${port}/?` + querystring_1.default.stringify(params);
            const headers = {
                "User-Agent": `QQ/${this.c.apk.version} CFNetwork/1126`,
                "Net-Type": "Wifi",
            };
            await axios_1.default.post(url, buf, { headers });
        }
        this.c.logger.debug("语音任务结束");
        const fid = rsp[11].toBuffer();
        const b = {
            1: 4,
            2: this.c.uin,
            3: fid,
            4: hash,
            5: hash.toString("hex") + ".amr",
            6: buf.length,
            8: 0,
            11: 1,
            18: fid,
            29: codec,
            30: {
                1: 0,
                5: 0,
                6: "",
                7: 0,
                8: brief,
            },
        };
        if (elem.seconds)
            b[19] = elem.seconds;
        return {
            type: "record",
            file: "protobuf://" + Buffer.from(core_1.pb.encode(b)).toString("base64"),
        };
    }
    async _newUploadMultiMsg(compressed) {
        const body = core_1.pb.encode({
            2: {
                1: this.dm ? 1 : 3,
                2: {
                    2: this.target,
                },
                4: compressed,
            },
            15: {
                1: 4,
                2: 2,
                3: 9,
                4: 0,
            },
        });
        const payload = await this.c.sendUni("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", body);
        const rsp = core_1.pb.decode(payload)?.[2];
        if (!rsp?.[3])
            (0, errors_1.drop)(rsp?.[1], rsp?.[2]?.toString() ||
                "unknown trpc.group.long_msg_interface.MsgService.SsoSendLongMsg error");
        return rsp[3].toString();
    }
    async _uploadMultiMsg(compressed) {
        const body = core_1.pb.encode({
            1: 1,
            2: 5,
            3: 9,
            4: 3,
            5: this.c.apk.version,
            6: [
                {
                    1: this.target,
                    2: compressed.length,
                    3: (0, common_1.md5)(compressed),
                    4: 3,
                    5: 0,
                },
            ],
            8: 1,
        });
        const payload = await this.c.sendUni("MultiMsg.ApplyUp", body);
        let rsp = core_1.pb.decode(payload)[2];
        if (rsp[1] !== 0)
            (0, errors_1.drop)(rsp[1], rsp[2]?.toString() || "unknown MultiMsg.ApplyUp error");
        const buf = core_1.pb.encode({
            1: 1,
            2: 5,
            3: 9,
            4: [
                {
                    1: this.dm ? 1 : 3,
                    2: this.target,
                    4: compressed,
                    5: 2,
                    6: rsp[3].toBuffer(),
                },
            ],
        });
        const ip = rsp[4]?.[0] || rsp[4], port = rsp[5]?.[0] || rsp[5];
        await highway_1.highwayUpload.call(this.c, stream_1.Readable.from(Buffer.from(buf), { objectMode: false }), {
            cmdid: highway_1.CmdID.MultiMsg,
            md5: (0, common_1.md5)(buf),
            size: buf.length,
            ticket: rsp[10].toBuffer(),
        }, ip, port);
        return rsp[2].toString();
    }
    async uploadLongMsg(content, source, isNT = this.c.apk.nt) {
        const converter = content instanceof message_1.Converter ? content : await this._preprocess(content, source);
        const seq = (0, crypto_1.randomBytes)(2).readInt16BE();
        const rand = (0, crypto_1.randomBytes)(4).readInt32BE();
        const nickname = String(this.c.nickname || this.c.uin);
        const nodes = [];
        if (isNT) {
            nodes.push({
                1: {
                    1: this.c.uin,
                    2: this.c.uid || null,
                    3: 1001,
                    5: this.dm ? this.c.uin : null,
                    6: this.dm ? this.c.uid || null : null,
                    7: this.dm
                        ? {
                            6: nickname,
                        }
                        : null,
                    8: this.dm
                        ? null
                        : {
                            1: this.target,
                            4: nickname,
                            5: 2,
                        },
                },
                2: {
                    1: this.dm ? 9 : 82,
                    2: this.dm ? 175 : null,
                    3: this.dm ? 175 : null,
                    4: rand,
                    5: seq,
                    6: (0, common_1.timestamp)(),
                    7: 1,
                    8: 0,
                    9: 0,
                    15: {
                        1: 0,
                        2: 0,
                        3: 0,
                        4: "",
                        5: "",
                    },
                },
                3: {
                    1: converter.rich,
                },
            });
        }
        else {
            nodes.push({
                1: {
                    1: this.c.uin,
                    2: this.target,
                    3: this.dm ? 166 : 82,
                    4: this.dm ? 11 : null,
                    9: this.dm
                        ? null
                        : {
                            1: this.target,
                        },
                    10: 1001,
                },
                3: {
                    1: converter.rich,
                },
            });
        }
        const compressed = await (0, common_1.gzip)(core_1.pb.encode(isNT ? { 2: { 1: "MultiMsg", 2: { 1: nodes } } } : { 1: nodes }));
        let resid;
        try {
            resid = isNT
                ? await this._newUploadMultiMsg(compressed)
                : await this._uploadMultiMsg(compressed);
        }
        catch {
            resid = isNT
                ? await this._newUploadMultiMsg(compressed)
                : await this._uploadMultiMsg(compressed);
        }
        return { type: "long_msg", resid };
    }
    /**
     * 制作一条合并转发消息以备发送（制作一次可以到处发）
     * 需要注意的是，好友图片和群图片的内部格式不一样，对着群制作的转发消息中的图片，发给好友可能会裂图，反过来也一样
     * 支持4层套娃转发（PC仅显示3层）
     */
    async makeForwardMsg(msglist) {
        const isNT = this.c.apk.nt;
        if (!Array.isArray(msglist))
            msglist = [msglist];
        const nodes = [];
        const makers = [];
        let preview = [];
        let cnt = 0;
        let MultiMsg = [];
        let brief;
        for (const fake of msglist) {
            brief = null;
            if (!Array.isArray(fake.message))
                fake.message = [fake.message];
            if (fake.message.length === 1 &&
                typeof fake.message[0] !== "string" &&
                ["xml", "json"].includes(fake.message[0].type)) {
                const elem = fake.message[0];
                let resid;
                let fileName;
                if (elem.type === "xml") {
                    let brief_reg = /brief\=\"(.*?)\"/gm.exec(elem.data);
                    if (brief_reg && brief_reg.length > 0) {
                        brief = brief_reg[1];
                    }
                    else {
                        brief = "[XML]";
                    }
                    let resid_reg = /m_resid\=\"(.*?)\"/gm.exec(elem.data);
                    let fileName_reg = /m_fileName\=\"(.*?)\"/gm.exec(elem.data);
                    if (resid_reg &&
                        resid_reg.length > 1 &&
                        fileName_reg &&
                        fileName_reg.length > 1) {
                        resid = resid_reg[1];
                        fileName = fileName_reg[1];
                    }
                }
                else if (elem.type === "json") {
                    brief = "[JSON]";
                    let json;
                    try {
                        json = typeof elem.data === "object" ? elem.data : JSON.parse(elem.data);
                    }
                    catch { }
                    if (json) {
                        brief = json.prompt;
                        if (json.app === "com.tencent.multimsg" && json.meta?.detail) {
                            let detail = json.meta.detail;
                            resid = detail.resid;
                            fileName = detail.uniseq;
                        }
                    }
                }
                if (!isNT && resid && fileName) {
                    const buff = isNT
                        ? await this._newDownloadMultiMsg(String(resid), this.dm ? 1 : 2)
                        : await this._downloadMultiMsg(String(resid), this.dm ? 1 : 2);
                    let arr = core_1.pb.decode(buff)[2];
                    if (!Array.isArray(arr))
                        arr = [arr];
                    for (let val of arr) {
                        let m_fileName = val[1].toString();
                        if (m_fileName === "MultiMsg") {
                            MultiMsg.push({
                                1: fileName,
                                2: val[2],
                            });
                        }
                        else {
                            MultiMsg.push(val);
                        }
                    }
                }
            }
            const maker = await this._preprocess(fake.message);
            if (maker?.brief && brief) {
                maker.brief = brief;
            }
            makers.push(maker);
            const seq = (0, crypto_1.randomBytes)(2).readInt16BE();
            const rand = (0, crypto_1.randomBytes)(4).readInt32BE();
            let nickname = String(fake.nickname || fake.user_id);
            if (!nickname && fake instanceof message_1.PrivateMessage)
                nickname =
                    this.c.fl.get(fake.user_id)?.nickname ||
                        this.c.sl.get(fake.user_id)?.nickname ||
                        nickname;
            if (cnt < 4) {
                preview.push({
                    text: `${nickname}: ${maker.brief.slice(0, 50)}`,
                });
                cnt++;
            }
            if (isNT) {
                nodes.push({
                    1: {
                        1: fake.user_id,
                        //2: 'uid',
                        5: this.dm ? this.c.uin : null,
                        6: this.dm ? this.c.uid || null : null,
                        7: this.dm
                            ? {
                                6: nickname,
                            }
                            : null,
                        8: this.dm
                            ? null
                            : {
                                1: this.target,
                                4: nickname,
                                5: 2,
                            },
                    },
                    2: {
                        1: this.dm ? 9 : 82,
                        2: this.dm ? 175 : null,
                        3: this.dm ? 175 : null,
                        4: rand,
                        5: seq,
                        6: fake.time || (0, common_1.timestamp)(),
                        7: 1,
                        8: 0,
                        9: 0,
                        15: {
                            1: 0,
                            2: 0,
                            3: 0,
                            4: "",
                            5: "",
                        },
                    },
                    3: {
                        1: maker.rich,
                    },
                });
            }
            else {
                nodes.push({
                    1: {
                        1: fake.user_id,
                        2: this.target,
                        3: this.dm ? 166 : 82,
                        4: this.dm ? 11 : null,
                        5: seq,
                        6: fake.time || (0, common_1.timestamp)(),
                        7: (0, message_1.rand2uuid)(rand),
                        9: this.dm
                            ? null
                            : {
                                1: this.target,
                                4: nickname,
                            },
                        14: this.dm ? nickname : null,
                        20: {
                            1: 0,
                            2: rand,
                        },
                    },
                    3: {
                        1: maker.rich,
                    },
                });
            }
        }
        MultiMsg.push({
            1: "MultiMsg",
            2: {
                1: nodes,
            },
        });
        const compressed = await (0, common_1.gzip)(core_1.pb.encode({
            2: MultiMsg,
        }));
        let resid;
        try {
            resid = isNT
                ? await this._newUploadMultiMsg(compressed)
                : await this._uploadMultiMsg(compressed);
        }
        catch {
            resid = isNT
                ? await this._newUploadMultiMsg(compressed)
                : await this._uploadMultiMsg(compressed);
        }
        const json = {
            "app": "com.tencent.multimsg",
            "config": { "autosize": 1, "forward": 1, "round": 1, "type": "normal", "width": 300 },
            "desc": "[聊天记录]",
            "extra": "",
            "meta": {
                "detail": {
                    "news": preview,
                    "resid": resid,
                    "source": "群聊的聊天记录",
                    "summary": `查看${nodes.length}条转发消息`,
                    "uniseq": (0, common_1.uuid)().toUpperCase(),
                },
            },
            "prompt": "[聊天记录]",
            "ver": "0.0.0.5",
            "view": "contact",
        };
        return {
            type: "json",
            data: json,
        };
    }
    /** 下载并解析合并转发 */
    async getForwardMsg(resid, fileName = "MultiMsg") {
        const isNT = this.c.apk.nt;
        const ret = [];
        const buf = isNT
            ? await this._newDownloadMultiMsg(String(resid), this.dm ? 1 : 2)
            : await this._downloadMultiMsg(String(resid), this.dm ? 1 : 2);
        let a = core_1.pb.decode(buf)[2];
        if (!Array.isArray(a))
            a = [a];
        for (let b of a) {
            const m_fileName = b[1].toString();
            if (m_fileName === fileName) {
                a = b;
                break;
            }
        }
        if (Array.isArray(a))
            a = a[0];
        a = a[2][1];
        if (!Array.isArray(a))
            a = [a];
        for (let proto of a) {
            try {
                ret.push(new message_1.ForwardMessage(proto));
            }
            catch { }
        }
        return ret;
    }
    async _newDownloadMultiMsg(resid, bu) {
        const body = core_1.pb.encode({
            1: {
                1: {
                    2: this.target,
                },
                2: resid,
                3: bu === 2 ? 3 : 1,
            },
            15: {
                1: 2,
                2: 2,
                3: 9,
                4: 0,
            },
        });
        const payload = await this.c.sendUni("trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg", body);
        const rsp = core_1.pb.decode(payload)?.[1];
        if (!rsp?.[4])
            return common_1.BUF0;
        return (0, common_1.unzip)(rsp[4].toBuffer());
    }
    async _downloadMultiMsg(resid, bu) {
        const body = core_1.pb.encode({
            1: 2,
            2: 5,
            3: 9,
            4: 3,
            5: this.c.apk.version,
            7: [
                {
                    1: resid,
                    2: 3,
                },
            ],
            8: bu,
            9: 2,
        });
        const payload = await this.c.sendUni("MultiMsg.ApplyDown", body);
        const rsp = core_1.pb.decode(payload)[3];
        const ip = (0, common_1.int32ip2str)(rsp[4]?.[0] || rsp[4]);
        const port = rsp[5]?.[0] || rsp[5];
        let url = port == 443 ? `https://${ip}` : `http://${ip}:${port}`;
        url += rsp[2];
        let { data, headers } = await axios_1.default.get(url, {
            headers: {
                "Host": `${port == 443 ? "ssl." : ""}htdata.qq.com`,
                "User-Agent": `QQ/${this.c.apk.version} CFNetwork/1126`,
                "Net-Type": "Wifi",
            },
            responseType: "arraybuffer",
        });
        data = Buffer.from(data);
        let buf = headers["accept-encoding"]?.includes("gzip")
            ? await (0, common_1.unzip)(data)
            : data;
        const head_len = buf.readUInt32BE(1);
        const body_len = buf.readUInt32BE(5);
        buf = core_1.tea.decrypt(buf.slice(head_len + 9, head_len + 9 + body_len), rsp[3].toBuffer());
        return (0, common_1.unzip)(core_1.pb.decode(buf)[3][3].toBuffer());
    }
    /** 获取视频下载地址 */
    async getVideoUrl(fid, md5) {
        if (md5 === "ntvideo") {
            const body = core_1.pb.encode({
                1: {
                    1: {
                        1: 1,
                        2: 200,
                    },
                    2: this.dm
                        ? {
                            101: 2,
                            102: 2,
                            200: 1,
                            201: {
                                1: 2,
                                2: this.uid?.toString(),
                            },
                        }
                        : {
                            101: 2,
                            102: 2,
                            200: 2,
                            202: {
                                1: this.gid,
                            },
                        },
                    3: {
                        1: 2,
                    },
                },
                3: {
                    1: fid,
                    2: {
                        2: {
                            1: 0,
                            3: 0,
                        },
                    },
                },
            });
            try {
                const rsp = await this.c.sendOidbSvcTrpcTcp(this.dm ? "OidbSvcTrpcTcp.0x11e9_200" : "OidbSvcTrpcTcp.0x11ea_200", body);
                const rkey = rsp[3][1];
                if (!rkey)
                    throw new Error("rkey不存在");
                return `https://${rsp[3][3][1]}${rsp[3][3][2]}${rkey}`;
            }
            catch (e) {
                this.c.logger.warn("获取QQNT视频URL失败", e.message);
                return null;
            }
        }
        const body = core_1.pb.encode({
            1: 400,
            4: {
                1: this.c.uin,
                2: this.c.uin,
                3: 1,
                4: 7,
                5: fid,
                6: 1,
                8: md5 instanceof Buffer ? md5 : Buffer.from(md5, "hex"),
                9: 1,
                10: 2,
                11: 2,
                12: 2,
            },
        });
        const payload = await this.c.sendUni("PttCenterSvr.ShortVideoDownReq", body);
        const rsp = core_1.pb.decode(payload)[4];
        if (rsp[1] !== 0)
            (0, errors_1.drop)(rsp[1], "获取视频下载地址失败");
        const obj = rsp[9];
        return String(Array.isArray(obj[10]) ? obj[10][0] : obj[10]) + String(obj[11]);
    }
    async getNtVoiceUrl(file) {
        const body = core_1.pb.encode({
            1: {
                1: {
                    1: 1,
                    2: 200,
                },
                2: this.dm
                    ? {
                        101: 1,
                        102: 3,
                        200: 1,
                        201: {
                            1: 2,
                            2: this.uid?.toString(),
                        },
                    }
                    : {
                        101: 1,
                        102: 3,
                        200: 2,
                        202: {
                            1: this.gid,
                        },
                    },
                3: {
                    1: 2,
                },
            },
            3: {
                1: file,
                2: {
                    2: {
                        1: 0,
                        3: 0,
                    },
                },
            },
        });
        try {
            const rsp = await this.c.sendOidbSvcTrpcTcp(this.dm ? "OidbSvcTrpcTcp.0x126d_200" : "OidbSvcTrpcTcp.0x126e_200", body);
            const rkey = rsp[3][1];
            if (!rkey)
                throw new Error("rkey不存在");
            return `https://${rsp[3][3][1]}${rsp[3][3][2]}${rkey}`;
        }
        catch (e) {
            this.c.logger.warn("获取QQNT语音URL失败", e.message);
            return null;
        }
    }
    /**
     * 获取QQNT图片rkey
     * @this {import("../client").Client}
     */
    async getNTPicRkey() {
        const body = core_1.pb.encode({
            1: {
                1: {
                    1: 1,
                    2: 202,
                },
                2: {
                    101: 2,
                    102: 1,
                    200: 0,
                },
                3: {
                    1: 2,
                },
            },
            4: {
                1: [10, 20],
                2: 2,
            },
        });
        const ret = {
            offNTPicRkey: "&rkey=CAQSKDOc_jvbthUjAatuFPQIo-x9wwcDhDGd8SOEu5FyJWNxNMabJTTRpO8",
            groupNTPicRkey: "&rkey=CAQSKDOc_jvbthUjZrMxJG2jNZ-rIWue47Q3PGKb_GWljzyvOUZOv0-EVao",
        };
        try {
            const rsp = await this.c.sendOidbSvcTrpcTcp("OidbSvcTrpcTcp.0x9067_202", body);
            const rkeys = rsp[4][1];
            if (!rkeys)
                throw new Error("rkey不存在");
            const rkeyType = {
                10: "offNTPicRkey",
                20: "groupNTPicRkey",
            };
            for (const v of rkeys) {
                ret[rkeyType[v[5]]] = v[1].toString();
            }
        }
        catch (e) {
            this.c.logger.warn("获取QQNT Rkey失败", e.message);
        }
        return ret;
    }
    /**
     * 获取QQNT群图rkey
     * @deprecated
     * @this {import("../client").Client}
     */
    async getGroupNTPicRkey() {
        const rsp = await this.getGroupNTPicURL({
            2: "CNL-BhjClnU",
            3: 1,
        }, this.gid);
        if (!rsp)
            return "&rkey=CAQSKDOc_jvbthUjZrMxJG2jNZ-rIWue47Q3PGKb_GWljzyvOUZOv0-EVao";
        return "&rkey=" + rsp.match(/&rkey=([^&]+)/)[1];
    }
    /**
     * 获取QQNT私聊图rkey
     * @deprecated
     * @this {import("../client").Client}
     */
    async getOffNTPicRkey() {
        const rsp = await this.getOffNTPicURL({
            2: "CNL-BhjClnU",
            3: 1,
        }, this.c.uid);
        if (!rsp)
            return "&rkey=CAQSKDOc_jvbthUjAatuFPQIo-x9wwcDhDGd8SOEu5FyJWNxNMabJTTRpO8";
        return "&rkey=" + rsp.match(/&rkey=([^&]+)/)[1];
    }
    /**
     * 获取QQNT群图URL
     * @this {import("../client").Client}
     * @param {Object} imgInfo
     * @param {number} gid
     */
    async getGroupNTPicURL(imgInfo, gid) {
        const body = core_1.pb.encode({
            1: {
                1: {
                    1: 1,
                    2: 200,
                },
                2: {
                    101: 2,
                    102: 1,
                    200: 2,
                    202: {
                        1: gid,
                    },
                },
                3: {
                    1: 2,
                },
            },
            3: {
                1: imgInfo,
                2: {
                    2: {
                        1: 0,
                        3: 0,
                    },
                },
            },
        });
        try {
            const rsp = await this.c.sendOidbSvcTrpcTcp("OidbSvcTrpcTcp.0x11c4_200", body);
            const rkey = rsp[3][1];
            if (!rkey)
                throw new Error("rkey不存在");
            return `https://${rsp[3][3][1]}${rsp[3][3][2]}${rkey}&spec=0`;
        }
        catch (e) {
            this.c.logger.warn("获取QQNT图片URL失败", e.message);
            return null;
        }
    }
    /**
     * 获取QQNT私聊图URL
     * @this {import("../client").Client}
     * @param {Object} imgInfo
     * @param {String} uid
     */
    async getOffNTPicURL(imgInfo, uid) {
        if (!uid)
            uid = String(this.c.uid || this.c.uin);
        const body = core_1.pb.encode({
            1: {
                1: {
                    1: 1,
                    2: 200,
                },
                2: {
                    101: 2,
                    102: 1,
                    200: 1,
                    201: {
                        1: 2,
                        2: uid,
                    },
                },
                3: {
                    1: 2,
                },
            },
            3: {
                1: imgInfo,
                2: {
                    2: {
                        1: 0,
                        3: 0,
                    },
                },
            },
        });
        try {
            const rsp = await this.c.sendOidbSvcTrpcTcp("OidbSvcTrpcTcp.0x11c5_200", body);
            const rkey = rsp[3][1];
            if (!rkey)
                throw new Error("rkey不存在");
            return `https://${rsp[3][3][1]}${rsp[3][3][2]}${rkey}&spec=0`;
        }
        catch (e) {
            this.c.logger.warn("获取QQNT图片URL失败", e.message);
            return null;
        }
    }
    /**
     * 通过fileid获取QQNT图片URL
     * @this {import("../client").Client}
     * @param {string} fileid
     */
    async getNTPicURLbyFileid(fileid) {
        const appidInFileId = core_1.pb.decode(Buffer.from(fileid, "base64"))[4];
        const { offNTPicRkey, groupNTPicRkey } = await this.getNTPicRkey();
        let url = "";
        if ([1406, 1407].includes(appidInFileId)) {
            const newRkey = appidInFileId === 1406 ? offNTPicRkey : groupNTPicRkey;
            url = `https://gchat.qpic.cn/download?appid=${appidInFileId}&fileid=${fileid}${newRkey}&spec=0`;
        }
        else {
            // 异常 fileid，只能挨个尝试
            url = `https://gchat.qpic.cn/download?appid=1407&fileid=${fileid}${groupNTPicRkey}&spec=0`;
            if (!(await checkImgUrl(url))) {
                url = `https://gchat.qpic.cn/download?appid=1406&fileid=${fileid}${offNTPicRkey}&spec=0`;
            }
        }
        return url;
    }
    /**
     * 通过fileid获取图片url
     * @this {import("../client").Client}
     * @param {string} fileid
     */
    async getNTPicURLbyFileidApi(fileid) {
        const url = await this.getNTPicURLbyFileid(fileid);
        return { result: 0, data: url };
    }
    /**
     * 通过fileid获取图片信息
     * @this {import("../client").Client}
     * @param {string} fileid
     */
    async getNTPicInfobyFileid(fileid) {
        const proto = core_1.pb.decode(Buffer.from(fileid, "base64"));
        const info = {
            uin: proto[1].toString(),
            md5: proto[2].toHex(),
            size: proto[3],
            appid: proto[4],
            time: proto[5],
            expires: proto[10],
        };
        return info;
    }
}
exports.Contactable = Contactable;
// 两个文件合并到一个流
function createReadable(file1, file2) {
    return stream_1.Readable.from(concatStreams(fs_1.default.createReadStream(file1, { highWaterMark: 256 * 1024 }), fs_1.default.createReadStream(file2, { highWaterMark: 256 * 1024 })));
}
// 合并两个流
async function* concatStreams(readable1, readable2) {
    for await (const chunk of readable1)
        yield chunk;
    for await (const chunk of readable2)
        yield chunk;
}
async function getPttBuffer(file, transcoding = true, ffmpeg = "ffmpeg") {
    if (file instanceof Buffer || file.startsWith("base64://")) {
        // Buffer或base64
        const buf = file instanceof Buffer ? file : Buffer.from(file.slice(9), "base64");
        const head = buf.slice(0, 7).toString();
        if (head.includes("SILK") || head.includes("AMR") || !transcoding) {
            return buf;
        }
        else {
            const tmpfile = path_1.default.join(common_1.TMP_DIR, (0, common_1.uuid)());
            await fs_1.default.promises.writeFile(tmpfile, buf);
            return audioTrans(tmpfile, ffmpeg, true);
        }
    }
    else if (file.startsWith("http://") || file.startsWith("https://")) {
        // 网络文件
        const readable = (await axios_1.default.get(file, { responseType: "stream" })).data;
        const tmpfile = path_1.default.join(common_1.TMP_DIR, (0, common_1.uuid)());
        await (0, common_1.pipeline)(readable.pipe(new common_1.DownloadTransform()), fs_1.default.createWriteStream(tmpfile));
        const head = await read7Bytes(tmpfile);
        if (head.includes("SILK") || head.includes("AMR") || !transcoding) {
            const buf = await fs_1.default.promises.readFile(tmpfile);
            fs_1.default.unlink(tmpfile, common_1.NOOP);
            return buf;
        }
        else {
            return audioTrans(tmpfile, ffmpeg, true);
        }
    }
    else {
        // 本地文件
        file = String(file).replace(/^file:\/{2}/, "");
        common_1.IS_WIN && file.startsWith("/") && (file = file.slice(1));
        const head = await read7Bytes(file);
        if (head.includes("SILK") || head.includes("AMR") || !transcoding) {
            return fs_1.default.promises.readFile(file);
        }
        else {
            return audioTrans(file, ffmpeg);
        }
    }
}
function audioTransSlik(file, ffmpeg = "ffmpeg", temp = false) {
    return new Promise((resolve, reject) => {
        const tmpfile = path_1.default.join(common_1.TMP_DIR, (0, common_1.uuid)());
        (0, child_process_1.exec)(`${ffmpeg} -y -i "${file}" -f s16le -ar 24000 -ac 1 -fs 31457280 "${tmpfile}"`, async (error, stdout, stderr) => {
            try {
                const pcm = await fs_1.default.promises.readFile(tmpfile);
                try {
                    const slik = (await (0, silk_1.encode)(pcm, 24000)).data;
                    resolve(Buffer.from(slik));
                }
                catch {
                    reject(new core_1.ApiRejection(errors_1.ErrorCode.FFmpegPttTransError, "音频转码到silk失败"));
                }
            }
            catch {
                reject(new core_1.ApiRejection(errors_1.ErrorCode.FFmpegPttTransError, "音频转码到pcm失败，请确认你的ffmpeg可以处理此转换"));
            }
            finally {
                fs_1.default.unlink(tmpfile, common_1.NOOP);
                if (temp)
                    fs_1.default.unlink(file, common_1.NOOP);
            }
        });
    });
}
function audioTrans(file, ffmpeg = "ffmpeg", temp = false) {
    return new Promise(async (resolve, reject) => {
        try {
            const slik = await audioTransSlik(file, ffmpeg, temp);
            resolve(slik);
            return;
        }
        catch { }
        const tmpfile = path_1.default.join(common_1.TMP_DIR, (0, common_1.uuid)());
        (0, child_process_1.exec)(`${ffmpeg} -y -i "${file}" -ac 1 -ar 8000 -f amr "${tmpfile}"`, async (error, stdout, stderr) => {
            try {
                const amr = await fs_1.default.promises.readFile(tmpfile);
                resolve(amr);
            }
            catch {
                reject(new core_1.ApiRejection(errors_1.ErrorCode.FFmpegPttTransError, "音频转码到amr失败，请确认你的ffmpeg可以处理此转换"));
            }
            finally {
                fs_1.default.unlink(tmpfile, common_1.NOOP);
                if (temp)
                    fs_1.default.unlink(file, common_1.NOOP);
            }
        });
    });
}
async function read7Bytes(file) {
    const fd = await fs_1.default.promises.open(file, "r");
    const buf = (await fd.read(Buffer.alloc(7), 0, 7, 0)).buffer;
    fd.close();
    return buf;
}
/**
 * 判断图片URL是否有效
 * @this {import("../client").Client}
 * @param {string} url
 */
async function checkImgUrl(url) {
    const data = await axios_1.default
        .get(url, {
        timeout: 5000,
        headers: { Range: "bytes=0-1" },
    })
        .then(res => res.data)
        .catch(() => null);
    return !!data;
}
