icqq
Version:
QQ protocol for NodeJS!
904 lines (903 loc) • 34.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPttBuffer = exports.Contactable = void 0;
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) {
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();
}
/**
* 制作一条合并转发消息以备发送(制作一次可以到处发)
* 需要注意的是,好友图片和群图片的内部格式不一样,对着群制作的转发消息中的图片,发给好友可能会裂图,反过来也一样
* 支持4层套娃转发(PC仅显示3层)
*/
async makeForwardMsg(msglist, nt = false) {
if (!Array.isArray(msglist))
msglist = [msglist];
const nodes = [];
const makers = [];
let imgs = [];
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 (err) {
}
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 (resid && fileName) {
const buff = nt ? 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: `${(0, common_1.escapeXml)(nickname)}: ${(0, common_1.escapeXml)(maker.brief.slice(0, 50))}`
});
cnt++;
}
if (nt) {
nodes.push({
1: {
1: fake.user_id,
//2: 'uid',
6: this.dm ? this.c.uin : null,
8: this.dm ? null : {
1: this.target,
2: nickname,
5: 2
}
},
2: {
1: this.dm ? 166 : 82,
4: rand,
5: seq,
6: fake.time || (0, common_1.timestamp)(),
7: 1,
8: 0,
9: 0
},
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({
//1: nodes,
2: MultiMsg
}));
let resid;
try {
resid = nt ? await this._newUploadMultiMsg(compressed) : await this._uploadMultiMsg(compressed);
}
catch {
resid = nt ? 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", nt = false) {
const ret = [];
const buf = nt ? 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) {
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]);
}
}
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);
}
}
}
exports.getPttBuffer = getPttBuffer;
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失败,请确认你的ffmpeg可以处理此转换"));
}
}
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;
}