Files
yenai-plugin/model/uploadRecord.js
2022-10-26 11:41:51 +08:00

427 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { core } from "oicq"
import common from "oicq"
import Contactable from "oicq"
import querystring from "querystring"
import fetch from "node-fetch"
import fs from "fs"
import path from "path"
import os from "os"
import util from "util"
import stream from "stream"
import crypto from "crypto"
import child_process from "child_process"
var errors = {};
async function uploadRecord(record_url, seconds = 0,transcoding = true) {
const result = await getPttBuffer(record_url, Bot.config.ffmpeg_path, transcoding);
if(!result.buffer){
return false;
}
let buf = result.buffer;
if(seconds == 0 && result.time) seconds = result.time.seconds;
const hash = (0, md5)(buf);
const codec = String(buf.slice(0, 7)).includes("SILK") ? (transcoding ? 1 : 0) : 0;
const body = core.pb.encode({
1: 3,
2: 3,
5: {
1: Contactable.target,
2: Bot.uin,
3: 0,
4: hash,
5: buf.length,
6: hash,
7: 5,
8: 9,
9: 4,
11: 0,
10: Bot.apk.version,
12: 1,
13: 1,
14: codec,
15: 1,
},
});
const payload = await Bot.sendUni("PttStore.GroupPttUp", body);
const rsp = core.pb.decode(payload)[5];
rsp[2] && (0, errors.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();
const params = {
ver: 4679,
ukey, filekey,
filesize: buf.length,
bmd5: hash.toString("hex"),
mType: "pttDu",
voice_encodec: codec
};
const url = `http://${(0, int32ip2str)(ip)}:${port}/?` + querystring.stringify(params);
const headers = {
"User-Agent": `QQ/${Bot.apk.version} CFNetwork/1126`,
"Net-Type": "Wifi"
};
await fetch(url,{
method: 'POST',//post请求
headers: headers,
body: buf
});
//await axios.post(url, buf, { headers });
const fid = rsp[11].toBuffer();
const b = core.pb.encode({
1: 4,
2: Bot.uin,
3: fid,
4: hash,
5: hash.toString("hex") + ".amr",
6: seconds,
11: 1,
18: fid,
19: seconds,
30: Buffer.from([8, 0, 40, 0, 56, 0]),
});
return {
type: "record", file: "protobuf://" + Buffer.from(b).toString("base64")
};
}
export default uploadRecord
async function getPttBuffer(file, ffmpeg = "ffmpeg", transcoding = true) {
let buffer;
let time;
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) {
const tmpfile = path.join(TMP_DIR, (0, uuid)());
await fs.promises.writeFile(tmpfile, buf);
let result = await getAudioTime(tmpfile,ffmpeg);
if(result.code == 1) time = result.data;
fs.unlink(tmpfile,NOOP);
buffer = buf;
}else {
const tmpfile = path.join(TMP_DIR, (0, uuid)());
let result = await getAudioTime(tmpfile,ffmpeg);
if(result.code == 1) time = result.data;
await fs.promises.writeFile(tmpfile, buf);
buffer = await audioTrans(tmpfile, ffmpeg);
}
}
else if (file.startsWith("http://") || file.startsWith("https://")) {
// 网络文件
//const readable = (await axios.get(file, { responseType: "stream" })).data;
try{
const headers = {
"User-Agent": `Dalvik/2.1.0 (Linux; U; Android 12; MI 9 Build/SKQ1.211230.001)`,
};
let response = await fetch(file,{
method: 'GET',//post请求
headers: headers
});
const buf = Buffer.from(await response.arrayBuffer());
const tmpfile = path.join(TMP_DIR, (0, uuid)());
await fs.promises.writeFile(tmpfile, buf);
//await (0, pipeline)(readable.pipe(new DownloadTransform), fs.createWriteStream(tmpfile));
const head = await read7Bytes(tmpfile);
let result = await getAudioTime(tmpfile,ffmpeg);
if(result.code == 1) time = result.data;
if (head.includes("SILK") || head.includes("AMR") || !transcoding) {
//const buf = await fs.promises.readFile(tmpfile);
fs.unlink(tmpfile,NOOP);
buffer = buf;
} else {
buffer = await audioTrans(tmpfile, ffmpeg);
}
}catch(err){}
}
else {
// 本地文件
file = String(file).replace(/^file:\/{2}/, "");
IS_WIN && file.startsWith("/") && (file = file.slice(1));
const head = await read7Bytes(file);
let result = await getAudioTime(file,ffmpeg);
if(result.code == 1) time = result.data;
if (head.includes("SILK") || head.includes("AMR") || !transcoding) {
buffer = await fs.promises.readFile(file);
} else {
buffer = await audioTrans(file, ffmpeg);
}
}
return {buffer: buffer, time: time}
}
async function getAudioTime(file, ffmpeg = "ffmpeg") {
return new Promise((resolve, reject) => {
(0, child_process.exec)(`${ffmpeg} -i "${file}"`, async (error, stdout, stderr) => {
try {
let time = stderr.split('Duration:')[1]?.split(',')[0].trim();
let arr = time?.split(':');
arr.reverse();
let n = 1;
let s = 0;
for(let val of arr){
if(parseInt(val) > 0) s += parseInt(val) * n;
n *= 60;
}
resolve({code: 1,data: {
time: time,
seconds: s,
exec_text: stderr
}});
}
catch {
resolve({code: -1});
}
});
});
}
async function audioTrans(file, ffmpeg = "ffmpeg") {
return new Promise((resolve, reject) => {
const tmpfile = path.join(TMP_DIR, (0, uuid)());
(0, child_process.exec)(`${ffmpeg} -y -i "${file}" -ac 1 -ar 8000 -f amr "${tmpfile}"`, async (error, stdout, stderr) => {
try {
const amr = await fs.promises.readFile(tmpfile);
resolve(amr);
}
catch {
reject(new core.ApiRejection(errors.ErrorCode.FFmpegPttTransError, "音频转码到amr失败请确认你的ffmpeg可以处理此转换"));
}
finally {
fs.unlink(tmpfile, NOOP);
}
});
});
}
async function read7Bytes(file) {
const fd = await fs.promises.open(file, "r");
const buf = (await fd.read(Buffer.alloc(7), 0, 7, 0)).buffer;
fd.close();
return buf;
}
function uuid() {
let hex = crypto.randomBytes(16).toString("hex");
return hex.substr(0, 8) + "-" + hex.substr(8, 4) + "-" + hex.substr(12, 4) + "-" + hex.substr(16, 4) + "-" + hex.substr(20);
}
/** 计算流的md5 */
function md5Stream(readable) {
return new Promise((resolve, reject) => {
readable.on("error", reject);
readable.pipe(crypto.createHash("md5")
.on("error", reject)
.on("data", resolve));
});
}
/** 计算文件的md5和sha */
function fileHash(filepath) {
const readable = fs.createReadStream(filepath);
const sha = new Promise((resolve, reject) => {
readable.on("error", reject);
readable.pipe(crypto.createHash("sha1")
.on("error", reject)
.on("data", resolve));
});
return Promise.all([md5Stream(readable), sha]);
}
/** 群号转uin */
function code2uin(code) {
let left = Math.floor(code / 1000000);
if (left >= 0 && left <= 10)
left += 202;
else if (left >= 11 && left <= 19)
left += 469;
else if (left >= 20 && left <= 66)
left += 2080;
else if (left >= 67 && left <= 156)
left += 1943;
else if (left >= 157 && left <= 209)
left += 1990;
else if (left >= 210 && left <= 309)
left += 3890;
else if (left >= 310 && left <= 335)
left += 3490;
else if (left >= 336 && left <= 386)
left += 2265;
else if (left >= 387 && left <= 499)
left += 3490;
return left * 1000000 + code % 1000000;
}
/** uin转群号 */
function uin2code(uin) {
let left = Math.floor(uin / 1000000);
if (left >= 202 && left <= 212)
left -= 202;
else if (left >= 480 && left <= 488)
left -= 469;
else if (left >= 2100 && left <= 2146)
left -= 2080;
else if (left >= 2010 && left <= 2099)
left -= 1943;
else if (left >= 2147 && left <= 2199)
left -= 1990;
else if (left >= 2600 && left <= 2651)
left -= 2265;
else if (left >= 3800 && left <= 3989)
left -= 3490;
else if (left >= 4100 && left <= 4199)
left -= 3890;
return left * 1000000 + uin % 1000000;
}
function int32ip2str(ip) {
if (typeof ip === "string")
return ip;
ip = ip & 0xffffffff;
return [
ip & 0xff,
(ip & 0xff00) >> 8,
(ip & 0xff0000) >> 16,
(ip & 0xff000000) >> 24 & 0xff,
].join(".");
}
/** 解析彩色群名片 */
function parseFunString(buf) {
if (buf[0] === 0xA) {
let res = "";
try {
let arr = core.pb.decode(buf)[1];
if (!Array.isArray(arr))
arr = [arr];
for (let v of arr) {
if (v[2])
res += String(v[2]);
}
}
catch { }
return res;
}
else {
return String(buf);
}
}
/** xml转义 */
function escapeXml(str) {
return str.replace(/[&"><]/g, function (s) {
if (s === "&")
return "&amp;";
if (s === "<")
return "&lt;";
if (s === ">")
return "&gt;";
if (s === "\"")
return "&quot;";
return "";
});
}
/** 用于下载限量 */
class DownloadTransform extends stream.Transform {
constructor() {
super(...arguments);
this._size = 0;
}
_transform(data, encoding, callback) {
this._size += data.length;
let error = null;
if (this._size <= MAX_UPLOAD_SIZE)
this.push(data);
else
error = new Error("downloading over 30MB is refused");
callback(error);
}
}
const IS_WIN = os.platform() === "win32";
/** 系统临时目录,用于临时存放下载的图片等内容 */
const TMP_DIR = os.tmpdir();
/** 最大上传和下载大小以图片上传限制为准30MB */
const MAX_UPLOAD_SIZE = 31457280;
/** no operation */
const NOOP = () => { };
/** promisified pipeline */
const pipeline = (0, util.promisify)(stream.pipeline);
/** md5 hash */
const md5 = (data) => (0, crypto.createHash)("md5").update(data).digest();
errors.LoginErrorCode = errors.drop = errors.ErrorCode = void 0;
var ErrorCode;
(function (ErrorCode) {
/** 客户端离线 */
ErrorCode[ErrorCode["ClientNotOnline"] = -1] = "ClientNotOnline";
/** 发包超时未收到服务器回应 */
ErrorCode[ErrorCode["PacketTimeout"] = -2] = "PacketTimeout";
/** 用户不存在 */
ErrorCode[ErrorCode["UserNotExists"] = -10] = "UserNotExists";
/** 群不存在(未加入) */
ErrorCode[ErrorCode["GroupNotJoined"] = -20] = "GroupNotJoined";
/** 群员不存在 */
ErrorCode[ErrorCode["MemberNotExists"] = -30] = "MemberNotExists";
/** 发消息时传入的参数不正确 */
ErrorCode[ErrorCode["MessageBuilderError"] = -60] = "MessageBuilderError";
/** 群消息被风控发送失败 */
ErrorCode[ErrorCode["RiskMessageError"] = -70] = "RiskMessageError";
/** 群消息有敏感词发送失败 */
ErrorCode[ErrorCode["SensitiveWordsError"] = -80] = "SensitiveWordsError";
/** 上传图片/文件/视频等数据超时 */
ErrorCode[ErrorCode["HighwayTimeout"] = -110] = "HighwayTimeout";
/** 上传图片/文件/视频等数据遇到网络错误 */
ErrorCode[ErrorCode["HighwayNetworkError"] = -120] = "HighwayNetworkError";
/** 没有上传通道 */
ErrorCode[ErrorCode["NoUploadChannel"] = -130] = "NoUploadChannel";
/** 不支持的file类型(没有流) */
ErrorCode[ErrorCode["HighwayFileTypeError"] = -140] = "HighwayFileTypeError";
/** 文件安全校验未通过不存在 */
ErrorCode[ErrorCode["UnsafeFile"] = -150] = "UnsafeFile";
/** 离线(私聊)文件不存在 */
ErrorCode[ErrorCode["OfflineFileNotExists"] = -160] = "OfflineFileNotExists";
/** 群文件不存在(无法转发) */
ErrorCode[ErrorCode["GroupFileNotExists"] = -170] = "GroupFileNotExists";
/** 获取视频中的图片失败 */
ErrorCode[ErrorCode["FFmpegVideoThumbError"] = -210] = "FFmpegVideoThumbError";
/** 音频转换失败 */
ErrorCode[ErrorCode["FFmpegPttTransError"] = -220] = "FFmpegPttTransError";
})(ErrorCode = errors.ErrorCode || (errors.ErrorCode = {}));
const ErrorMessage = {
[ErrorCode.UserNotExists]: "查无此人",
[ErrorCode.GroupNotJoined]: "未加入的群",
[ErrorCode.MemberNotExists]: "幽灵群员",
[ErrorCode.RiskMessageError]: "群消息发送失败,可能被风控",
[ErrorCode.SensitiveWordsError]: "群消息发送失败,请检查消息内容",
10: "消息过长",
34: "消息过长",
120: "在该群被禁言",
121: "AT全体剩余次数不足"
};
function drop(code, message) {
if (!message || !message.length)
message = ErrorMessage[code];
throw new core.ApiRejection(code, message);
}
errors.drop = drop;
/** 登录时可能出现的错误,不在列的都属于未知错误,暂时无法解决 */
var LoginErrorCode;
(function (LoginErrorCode) {
/** 密码错误 */
LoginErrorCode[LoginErrorCode["WrongPassword"] = 1] = "WrongPassword";
/** 账号被冻结 */
LoginErrorCode[LoginErrorCode["AccountFrozen"] = 40] = "AccountFrozen";
/** 发短信太频繁 */
LoginErrorCode[LoginErrorCode["TooManySms"] = 162] = "TooManySms";
/** 短信验证码错误 */
LoginErrorCode[LoginErrorCode["WrongSmsCode"] = 163] = "WrongSmsCode";
/** 滑块ticket错误 */
LoginErrorCode[LoginErrorCode["WrongTicket"] = 237] = "WrongTicket";
})(LoginErrorCode = errors.LoginErrorCode || (errors.LoginErrorCode = {}));