wx2tg-puppet 3.0.9 → 6.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -25
- package/VERSION +1 -0
- package/dist/api/FriendApi.js +1 -1
- package/dist/api/ToolsApi.d.ts +6 -4
- package/dist/api/ToolsApi.js +10 -8
- package/dist/core/Contact.d.ts +5 -0
- package/dist/core/Contact.js +71 -30
- package/dist/core/Message.d.ts +2 -0
- package/dist/core/Message.js +50 -16
- package/dist/entities/ContactEntity.d.ts +1 -0
- package/dist/entities/ContactEntity.js +4 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +231 -164
- package/dist/services/contact.service.js +9 -13
- package/dist/types/MessageType.d.ts +1 -0
- package/dist/types/MessageType.js +1 -0
- package/dist/utils/CallbackPayload.d.ts +4 -0
- package/dist/utils/CallbackPayload.js +52 -0
- package/dist/utils/ContactClassification.d.ts +4 -0
- package/dist/utils/ContactClassification.js +21 -0
- package/dist/utils/FileDownloadError.d.ts +7 -0
- package/dist/utils/FileDownloadError.js +14 -0
- package/dist/utils/SolitairePayload.d.ts +5 -0
- package/dist/utils/SolitairePayload.js +78 -0
- package/dist/utils/downloadFileSection.d.ts +11 -0
- package/dist/utils/downloadFileSection.js +153 -0
- package/dist/utils/fileDownload.d.ts +5 -16
- package/dist/utils/fileDownload.js +15 -134
- package/package.json +50 -43
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Only static diagnostics and numeric status codes belong in download errors. */
|
|
2
|
+
export declare class FileDownloadError extends Error {
|
|
3
|
+
httpStatus?: number | undefined;
|
|
4
|
+
businessCode?: number | undefined;
|
|
5
|
+
retryable: boolean;
|
|
6
|
+
constructor(message: string, httpStatus?: number | undefined, businessCode?: number | undefined, retryable?: boolean);
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileDownloadError = void 0;
|
|
4
|
+
/** Only static diagnostics and numeric status codes belong in download errors. */
|
|
5
|
+
class FileDownloadError extends Error {
|
|
6
|
+
constructor(message, httpStatus, businessCode, retryable = false) {
|
|
7
|
+
super(`DownloadFile: ${message}`);
|
|
8
|
+
this.httpStatus = httpStatus;
|
|
9
|
+
this.businessCode = businessCode;
|
|
10
|
+
this.retryable = retryable;
|
|
11
|
+
this.name = 'FileDownloadError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.FileDownloadError = FileDownloadError;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SOLITAIRE_FALLBACK = "[\u63A5\u9F99] \u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u89E3\u6790\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u67E5\u770B\u3002";
|
|
2
|
+
/** Bound solitaire display parsing; do not impose this size limit on other app types. */
|
|
3
|
+
export declare function isBoundedAppMessageXml(value: string): boolean;
|
|
4
|
+
/** Only appmsg.title is display text; solitaire_info is deliberately never interpreted. */
|
|
5
|
+
export declare function getSolitaireDisplayText(payload: string): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SOLITAIRE_FALLBACK = void 0;
|
|
4
|
+
exports.isBoundedAppMessageXml = isBoundedAppMessageXml;
|
|
5
|
+
exports.getSolitaireDisplayText = getSolitaireDisplayText;
|
|
6
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
7
|
+
exports.SOLITAIRE_FALLBACK = '[接龙] 内容暂时无法解析,请在微信查看。';
|
|
8
|
+
const MAX_XML_BYTES = 1024 * 1024;
|
|
9
|
+
/** Bound solitaire display parsing; do not impose this size limit on other app types. */
|
|
10
|
+
function isBoundedAppMessageXml(value) {
|
|
11
|
+
return typeof value === 'string' && value.length <= MAX_XML_BYTES &&
|
|
12
|
+
Buffer.byteLength(value, 'utf8') <= MAX_XML_BYTES &&
|
|
13
|
+
!/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(value);
|
|
14
|
+
}
|
|
15
|
+
function decodeXmlText(value) {
|
|
16
|
+
const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
17
|
+
// A single pass is important: &lt; is visible text "<", not "<".
|
|
18
|
+
return value.replace(/&([^;]*);|&/g, (match, entity) => {
|
|
19
|
+
if (entity && Object.prototype.hasOwnProperty.call(named, entity))
|
|
20
|
+
return named[entity];
|
|
21
|
+
if (!entity || !/^#(?:[0-9]+|x[0-9a-fA-F]+)$/.test(entity)) {
|
|
22
|
+
throw new Error('Invalid XML entity');
|
|
23
|
+
}
|
|
24
|
+
const code = entity[1] === 'x' ? parseInt(entity.slice(2), 16) : Number(entity.slice(1));
|
|
25
|
+
if (!(code === 9 || code === 10 || code === 13 ||
|
|
26
|
+
(code >= 0x20 && code <= 0xd7ff) || (code >= 0xe000 && code <= 0xfffd) ||
|
|
27
|
+
(code >= 0x10000 && code <= 0x10ffff))) {
|
|
28
|
+
throw new Error('Invalid XML character');
|
|
29
|
+
}
|
|
30
|
+
return String.fromCodePoint(code);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Only appmsg.title is display text; solitaire_info is deliberately never interpreted. */
|
|
34
|
+
function getSolitaireDisplayText(payload) {
|
|
35
|
+
var _a;
|
|
36
|
+
try {
|
|
37
|
+
if (!isBoundedAppMessageXml(payload))
|
|
38
|
+
return exports.SOLITAIRE_FALLBACK;
|
|
39
|
+
// Only a single, identifier-shaped WeChat sender line may precede the XML.
|
|
40
|
+
const xml = payload.replace(/^[A-Za-z0-9_.@-]{1,256}:\r?\n/, '').trimStart();
|
|
41
|
+
if (fast_xml_parser_1.XMLValidator.validate(xml) !== true)
|
|
42
|
+
return exports.SOLITAIRE_FALLBACK;
|
|
43
|
+
// Convert CDATA before locating scalar title text, so a literal closing
|
|
44
|
+
// tag inside CDATA cannot end it. Protect CR only inside title text:
|
|
45
|
+
// replacing XML-declaration/root whitespace would create invalid entities.
|
|
46
|
+
const escaped = xml.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'))
|
|
47
|
+
.replace(/(<title\s*>)([\s\S]*?)(<\/title\s*>)/g, (_, open, text, close) => open + text.replace(/\r/g, ' ') + close);
|
|
48
|
+
const parsed = new fast_xml_parser_1.XMLParser({
|
|
49
|
+
parseTagValue: false,
|
|
50
|
+
trimValues: false,
|
|
51
|
+
processEntities: false,
|
|
52
|
+
ignoreAttributes: false,
|
|
53
|
+
ignoreDeclaration: true,
|
|
54
|
+
commentPropName: '#comment',
|
|
55
|
+
}).parse(escaped);
|
|
56
|
+
// Require an unambiguous envelope and scalar title (no nested markup/attributes).
|
|
57
|
+
if (Object.keys(parsed).some(key => key !== 'msg'))
|
|
58
|
+
return exports.SOLITAIRE_FALLBACK;
|
|
59
|
+
const app = (_a = parsed.msg) === null || _a === void 0 ? void 0 : _a.appmsg;
|
|
60
|
+
if (!app || Array.isArray(parsed.msg) || Array.isArray(app) ||
|
|
61
|
+
typeof app.type !== 'string' || app.type.trim() !== '53' ||
|
|
62
|
+
typeof app.title !== 'string')
|
|
63
|
+
return exports.SOLITAIRE_FALLBACK;
|
|
64
|
+
const title = decodeXmlText(app.title);
|
|
65
|
+
if (!title.trim() || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff]/u.test(title)) {
|
|
66
|
+
return exports.SOLITAIRE_FALLBACK;
|
|
67
|
+
}
|
|
68
|
+
for (const point of title) {
|
|
69
|
+
const code = point.codePointAt(0);
|
|
70
|
+
if (code >= 0xd800 && code <= 0xdfff)
|
|
71
|
+
return exports.SOLITAIRE_FALLBACK;
|
|
72
|
+
}
|
|
73
|
+
return title;
|
|
74
|
+
}
|
|
75
|
+
catch (_b) {
|
|
76
|
+
return exports.SOLITAIRE_FALLBACK;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AxiosResponse } from 'axios';
|
|
2
|
+
import { ApiResponse } from '../types/ApiResponse';
|
|
3
|
+
export interface FileSection {
|
|
4
|
+
response: AxiosResponse;
|
|
5
|
+
buffer: Buffer;
|
|
6
|
+
legacy?: ApiResponse;
|
|
7
|
+
}
|
|
8
|
+
/** The single section engine; DownloadFile alone bypasses the shared JSON transformer. */
|
|
9
|
+
export declare function fetchFileSection(body: any): Promise<FileSection>;
|
|
10
|
+
/** v6 contract: exactly one section, retaining the Axios response and raw bytes. */
|
|
11
|
+
export declare function downloadFileSection(body: any): Promise<AxiosResponse<Buffer>>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.fetchFileSection = fetchFileSection;
|
|
16
|
+
exports.downloadFileSection = downloadFileSection;
|
|
17
|
+
const json_bigint_1 = __importDefault(require("json-bigint"));
|
|
18
|
+
const axiosInstance_1 = require("./axiosInstance");
|
|
19
|
+
const FileDownloadError_1 = require("./FileDownloadError");
|
|
20
|
+
const json = (0, json_bigint_1.default)();
|
|
21
|
+
const responseJson = (0, json_bigint_1.default)({ storeAsString: true });
|
|
22
|
+
const TRANSIENT_CODES = ['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ECONNREFUSED'];
|
|
23
|
+
function businessCode(value) {
|
|
24
|
+
var _a, _b, _c, _d;
|
|
25
|
+
const native = [(_b = (_a = value === null || value === void 0 ? void 0 : value.Data) === null || _a === void 0 ? void 0 : _a.BaseResponse) === null || _b === void 0 ? void 0 : _b.ret, (_d = (_c = value === null || value === void 0 ? void 0 : value.Data) === null || _c === void 0 ? void 0 : _c.baseResponse) === null || _d === void 0 ? void 0 : _d.ret]
|
|
26
|
+
.find(ret => Number.isSafeInteger(ret) && ret !== 0);
|
|
27
|
+
const code = native !== null && native !== void 0 ? native : value === null || value === void 0 ? void 0 : value.Code;
|
|
28
|
+
return Number.isSafeInteger(code) ? code : undefined;
|
|
29
|
+
}
|
|
30
|
+
function header(response, name) {
|
|
31
|
+
const key = Object.keys(response.headers || {}).find(key => key.toLowerCase() === name);
|
|
32
|
+
return key === undefined || response.headers[key] === undefined ? undefined : String(response.headers[key]);
|
|
33
|
+
}
|
|
34
|
+
function decodeLegacy(value, total, start, status) {
|
|
35
|
+
const payload = value === null || value === void 0 ? void 0 : value.Data;
|
|
36
|
+
if ((value === null || value === void 0 ? void 0 : value.Success) !== true || (value === null || value === void 0 ? void 0 : value.Code) !== 0 ||
|
|
37
|
+
[payload === null || payload === void 0 ? void 0 : payload.BaseResponse, payload === null || payload === void 0 ? void 0 : payload.baseResponse].some(native => (native === null || native === void 0 ? void 0 : native.ret) !== undefined && native.ret !== 0)) {
|
|
38
|
+
const code = businessCode(value);
|
|
39
|
+
throw new FileDownloadError_1.FileDownloadError(`legacy response failed or malformed${code === undefined ? '' : ` (code ${code})`}`, status, code);
|
|
40
|
+
}
|
|
41
|
+
const data = payload === null || payload === void 0 ? void 0 : payload.data;
|
|
42
|
+
const encoded = data === null || data === void 0 ? void 0 : data.buffer;
|
|
43
|
+
if (typeof encoded !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) ||
|
|
44
|
+
(encoded.includes('=') && encoded.length % 4 !== 0)) {
|
|
45
|
+
throw new FileDownloadError_1.FileDownloadError('malformed legacy Base64', status);
|
|
46
|
+
}
|
|
47
|
+
const bytes = Buffer.from(encoded, 'base64');
|
|
48
|
+
if (bytes.toString('base64').replace(/=+$/, '') !== encoded.replace(/=+$/, '')) {
|
|
49
|
+
throw new FileDownloadError_1.FileDownloadError('malformed legacy Base64', status);
|
|
50
|
+
}
|
|
51
|
+
for (const [actual, expected] of [[data.iLen, bytes.length], [payload.dataLen, bytes.length],
|
|
52
|
+
[payload.totalLen, total], [payload.startPos, start]]) {
|
|
53
|
+
if (actual !== undefined && actual !== expected) {
|
|
54
|
+
throw new FileDownloadError_1.FileDownloadError('truncated or inconsistent legacy section metadata', status);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
/** The single section engine; DownloadFile alone bypasses the shared JSON transformer. */
|
|
60
|
+
function fetchFileSection(body) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
var _a, _b, _c, _d;
|
|
63
|
+
const total = body === null || body === void 0 ? void 0 : body.DataLen;
|
|
64
|
+
const start = (_a = body === null || body === void 0 ? void 0 : body.Section) === null || _a === void 0 ? void 0 : _a.StartPos;
|
|
65
|
+
const length = (_b = body === null || body === void 0 ? void 0 : body.Section) === null || _b === void 0 ? void 0 : _b.DataLen;
|
|
66
|
+
if (!Number.isSafeInteger(total) || total <= 0 || total > 0xffffffff ||
|
|
67
|
+
!Number.isSafeInteger(start) || start < 0 || start >= total ||
|
|
68
|
+
!Number.isSafeInteger(length) || length <= 0 || length > 0xffffffff) {
|
|
69
|
+
throw new FileDownloadError_1.FileDownloadError('invalid total or section length');
|
|
70
|
+
}
|
|
71
|
+
const expected = Math.min(length, total - start);
|
|
72
|
+
// JSONbig keeps uint64 message IDs numeric on the wire without rounding them.
|
|
73
|
+
if (typeof body.NewMsgId === 'number' && !Number.isSafeInteger(body.NewMsgId)) {
|
|
74
|
+
throw new FileDownloadError_1.FileDownloadError('invalid NewMsgId');
|
|
75
|
+
}
|
|
76
|
+
const newMsgId = String((_c = body.NewMsgId) !== null && _c !== void 0 ? _c : 0);
|
|
77
|
+
if (!/^(0|[1-9]\d*)$/.test(newMsgId) || BigInt(newMsgId) > BigInt('18446744073709551615')) {
|
|
78
|
+
throw new FileDownloadError_1.FileDownloadError('invalid NewMsgId');
|
|
79
|
+
}
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = yield axiosInstance_1.axiosInstance.post('/Tools/DownloadFile', json.stringify(Object.assign(Object.assign({}, body), { NewMsgId: json.parse(newMsgId) })), {
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
responseType: 'arraybuffer',
|
|
85
|
+
transformResponse: [data => data],
|
|
86
|
+
validateStatus: () => true,
|
|
87
|
+
// First CDN fallback can consume the server's entire 120-second I/O budget.
|
|
88
|
+
timeout: 180000,
|
|
89
|
+
maxContentLength: Math.ceil(expected / 3) * 4 + 64 * 1024,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const code = error === null || error === void 0 ? void 0 : error.code;
|
|
94
|
+
const timedOut = code === 'ECONNABORTED' || code === 'ETIMEDOUT';
|
|
95
|
+
throw new FileDownloadError_1.FileDownloadError(timedOut ? 'request timed out' :
|
|
96
|
+
'transport failed (interrupted, truncated or oversized response)', undefined, undefined, typeof code === 'string' && TRANSIENT_CODES.includes(code));
|
|
97
|
+
}
|
|
98
|
+
const contentType = String((_d = header(response, 'content-type')) !== null && _d !== void 0 ? _d : '').split(';')[0].trim().toLowerCase();
|
|
99
|
+
const isJson = /^application\/(?:[\w.+-]+\+)?json$/.test(contentType);
|
|
100
|
+
const raw = response.data;
|
|
101
|
+
const bytes = Buffer.isBuffer(raw) ? raw : raw instanceof ArrayBuffer ? Buffer.from(raw) :
|
|
102
|
+
ArrayBuffer.isView(raw) ? Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength) :
|
|
103
|
+
typeof raw === 'string' && isJson ? Buffer.from(raw, 'utf8') : undefined;
|
|
104
|
+
let value;
|
|
105
|
+
if (bytes && (isJson || !contentType)) {
|
|
106
|
+
try {
|
|
107
|
+
value = responseJson.parse(bytes.toString('utf8'));
|
|
108
|
+
}
|
|
109
|
+
catch ( /* Parser diagnostics can expose response contents. */_e) { /* Parser diagnostics can expose response contents. */ }
|
|
110
|
+
}
|
|
111
|
+
// HTTP status wins over malformed error bodies, including HTML/invalid JSON 503s.
|
|
112
|
+
if (response.status !== 200 && response.status !== 206) {
|
|
113
|
+
const status = Number.isSafeInteger(response.status) ? response.status : undefined;
|
|
114
|
+
const code = businessCode(value);
|
|
115
|
+
throw new FileDownloadError_1.FileDownloadError(`HTTP ${status !== null && status !== void 0 ? status : 0} unexpected status${code === undefined ? '' : ` (code ${code})`}`, status, code, [429, 502, 503, 504].includes(status !== null && status !== void 0 ? status : 0));
|
|
116
|
+
}
|
|
117
|
+
if (!bytes)
|
|
118
|
+
throw new FileDownloadError_1.FileDownloadError('expected binary response body', response.status);
|
|
119
|
+
const declared = header(response, 'content-length');
|
|
120
|
+
if (declared !== undefined && (!/^\d+$/.test(String(declared)) || Number(declared) !== bytes.length)) {
|
|
121
|
+
throw new FileDownloadError_1.FileDownloadError('truncated or inconsistent Content-Length', response.status);
|
|
122
|
+
}
|
|
123
|
+
const legacy = isJson || !contentType;
|
|
124
|
+
if (!legacy && contentType !== 'application/octet-stream') {
|
|
125
|
+
throw new FileDownloadError_1.FileDownloadError('unexpected response Content-Type', response.status);
|
|
126
|
+
}
|
|
127
|
+
const buffer = legacy ? decodeLegacy(value, total, start, response.status) : bytes;
|
|
128
|
+
if (!buffer.length)
|
|
129
|
+
throw new FileDownloadError_1.FileDownloadError('empty section', response.status);
|
|
130
|
+
if (buffer.length > expected)
|
|
131
|
+
throw new FileDownloadError_1.FileDownloadError('oversized section', response.status);
|
|
132
|
+
const range = header(response, 'content-range');
|
|
133
|
+
// Binary 206 requires a range; JSON servers may return HTTP 200 per section.
|
|
134
|
+
if (range !== undefined || (!legacy && response.status === 206)) {
|
|
135
|
+
const match = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(String(range));
|
|
136
|
+
if (!match || Number(match[1]) !== start || Number(match[2]) !== start + buffer.length - 1 ||
|
|
137
|
+
Number(match[3]) !== total) {
|
|
138
|
+
throw new FileDownloadError_1.FileDownloadError('truncated or inconsistent Content-Range', response.status);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!legacy && response.status === 200 && (start !== 0 || buffer.length !== total)) {
|
|
142
|
+
throw new FileDownloadError_1.FileDownloadError('HTTP 200 returned an incomplete file', response.status);
|
|
143
|
+
}
|
|
144
|
+
return Object.assign({ response, buffer }, (legacy ? { legacy: value } : {}));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** v6 contract: exactly one section, retaining the Axios response and raw bytes. */
|
|
148
|
+
function downloadFileSection(body) {
|
|
149
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
150
|
+
const section = yield fetchFileSection(body);
|
|
151
|
+
return Object.assign(Object.assign({}, section.response), { data: section.buffer });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import { AxiosResponse } from 'axios';
|
|
2
2
|
import { ApiResponse } from '../types/ApiResponse';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
constructor(message: string, httpStatus?: number | undefined, businessCode?: number | undefined, retryable?: boolean);
|
|
8
|
-
}
|
|
9
|
-
interface FileSection {
|
|
10
|
-
response: AxiosResponse;
|
|
11
|
-
buffer: Buffer;
|
|
12
|
-
legacy?: ApiResponse;
|
|
13
|
-
}
|
|
14
|
-
/** Read bytes without invoking the shared JSON-only Axios transformer. */
|
|
15
|
-
export declare function fetchFileSection(body: any): Promise<FileSection>;
|
|
16
|
-
/** Preserve the existing SDK envelope for consumers using Data.data.buffer. */
|
|
3
|
+
import { FileSection } from './downloadFileSection';
|
|
4
|
+
export { fetchFileSection, FileSection } from './downloadFileSection';
|
|
5
|
+
export { FileDownloadError } from './FileDownloadError';
|
|
6
|
+
/** Explicit compatibility API for consumers using Data.data.buffer. */
|
|
17
7
|
export declare function legacyFileSection(section: FileSection, body: any): AxiosResponse<ApiResponse>;
|
|
18
|
-
/** Advance by actual
|
|
8
|
+
/** Advance by actual bytes, retrying only transient failures at the same offset. */
|
|
19
9
|
export declare function downloadFileBuffer(body: any): Promise<Buffer>;
|
|
20
|
-
export {};
|
|
@@ -8,150 +8,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.FileDownloadError = void 0;
|
|
16
|
-
exports.fetchFileSection = fetchFileSection;
|
|
12
|
+
exports.FileDownloadError = exports.fetchFileSection = void 0;
|
|
17
13
|
exports.legacyFileSection = legacyFileSection;
|
|
18
14
|
exports.downloadFileBuffer = downloadFileBuffer;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
15
|
+
const buffer_1 = require("buffer");
|
|
16
|
+
const downloadFileSection_1 = require("./downloadFileSection");
|
|
17
|
+
const FileDownloadError_1 = require("./FileDownloadError");
|
|
18
|
+
var downloadFileSection_2 = require("./downloadFileSection");
|
|
19
|
+
Object.defineProperty(exports, "fetchFileSection", { enumerable: true, get: function () { return downloadFileSection_2.fetchFileSection; } });
|
|
20
|
+
var FileDownloadError_2 = require("./FileDownloadError");
|
|
21
|
+
Object.defineProperty(exports, "FileDownloadError", { enumerable: true, get: function () { return FileDownloadError_2.FileDownloadError; } });
|
|
23
22
|
const CHUNK_SIZE = 512 * 1024;
|
|
24
|
-
|
|
25
|
-
constructor(message, httpStatus, businessCode, retryable = false) {
|
|
26
|
-
super(message);
|
|
27
|
-
this.httpStatus = httpStatus;
|
|
28
|
-
this.businessCode = businessCode;
|
|
29
|
-
this.retryable = retryable;
|
|
30
|
-
this.name = 'FileDownloadError';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
exports.FileDownloadError = FileDownloadError;
|
|
34
|
-
function requestRange(body) {
|
|
35
|
-
var _a, _b;
|
|
36
|
-
const total = body === null || body === void 0 ? void 0 : body.DataLen, start = (_a = body === null || body === void 0 ? void 0 : body.Section) === null || _a === void 0 ? void 0 : _a.StartPos, length = (_b = body === null || body === void 0 ? void 0 : body.Section) === null || _b === void 0 ? void 0 : _b.DataLen;
|
|
37
|
-
if (![total, start, length].every(Number.isSafeInteger) || total <= 0 || total > 0xffffffff || start < 0 || start >= total || length <= 0 || length > 0xffffffff) {
|
|
38
|
-
throw new FileDownloadError('Invalid file size or section range');
|
|
39
|
-
}
|
|
40
|
-
return { total, start, length: Math.min(length, total - start) };
|
|
41
|
-
}
|
|
42
|
-
function header(response, name) {
|
|
43
|
-
var _a, _b;
|
|
44
|
-
return String((_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a[name]) !== null && _b !== void 0 ? _b : '').trim();
|
|
45
|
-
}
|
|
46
|
-
function safeMessage(value, body) {
|
|
47
|
-
if (typeof value !== 'string')
|
|
48
|
-
return '';
|
|
49
|
-
let message = value;
|
|
50
|
-
for (const key of ['AttachId', 'Wxid', 'UserName', 'AppID']) {
|
|
51
|
-
const secret = body === null || body === void 0 ? void 0 : body[key];
|
|
52
|
-
if (typeof secret === 'string' && secret.length > 3)
|
|
53
|
-
message = message.split(secret).join('[redacted]');
|
|
54
|
-
}
|
|
55
|
-
return message.replace(/@cdn_\S+/g, '[redacted]')
|
|
56
|
-
.replace(/https?:\/\/\S+/gi, '[url redacted]')
|
|
57
|
-
.replace(/\b[a-f\d]{24,}\b/gi, '[redacted]')
|
|
58
|
-
.replace(/[A-Za-z\d+/=]{100,}/g, '[data redacted]')
|
|
59
|
-
.replace(/[\r\n]+/g, ' ').slice(0, 400);
|
|
60
|
-
}
|
|
61
|
-
function invalid(message) {
|
|
62
|
-
throw new FileDownloadError(message);
|
|
63
|
-
}
|
|
64
|
-
function decodeLegacy(data, body) {
|
|
65
|
-
var _a, _b;
|
|
66
|
-
const { total, start, length } = requestRange(body);
|
|
67
|
-
const payload = data === null || data === void 0 ? void 0 : data.Data, native = (_a = payload === null || payload === void 0 ? void 0 : payload.BaseResponse) !== null && _a !== void 0 ? _a : payload === null || payload === void 0 ? void 0 : payload.baseResponse;
|
|
68
|
-
if ((data === null || data === void 0 ? void 0 : data.Success) !== true || (data === null || data === void 0 ? void 0 : data.Code) !== 0 || ((native === null || native === void 0 ? void 0 : native.ret) !== undefined && native.ret !== 0)) {
|
|
69
|
-
const code = typeof (data === null || data === void 0 ? void 0 : data.Code) === 'number' ? data.Code : undefined;
|
|
70
|
-
throw new FileDownloadError(`File download rejected${code !== undefined ? ` (Code=${code})` : ''}${(native === null || native === void 0 ? void 0 : native.ret) !== undefined ? ` (ret=${native.ret})` : ''}: ${safeMessage(data === null || data === void 0 ? void 0 : data.Message, body)}`, 200, code);
|
|
71
|
-
}
|
|
72
|
-
const encoded = (_b = payload === null || payload === void 0 ? void 0 : payload.data) === null || _b === void 0 ? void 0 : _b.buffer;
|
|
73
|
-
if (typeof encoded !== 'string' || !encoded.length || !/^[A-Za-z\d+/]*={0,2}$/.test(encoded) || encoded.length % 4 === 1) {
|
|
74
|
-
return invalid('File download returned an empty or invalid Base64 section');
|
|
75
|
-
}
|
|
76
|
-
const bytes = Buffer.from(encoded, 'base64');
|
|
77
|
-
if (!bytes.length || bytes.length > length || bytes.toString('base64').replace(/=+$/, '') !== encoded.replace(/=+$/, '')) {
|
|
78
|
-
return invalid('File download returned an invalid section length');
|
|
79
|
-
}
|
|
80
|
-
for (const [value, expected] of [[payload.data.iLen, bytes.length], [payload.dataLen, bytes.length], [payload.totalLen, total], [payload.startPos, start]]) {
|
|
81
|
-
if (value !== undefined && value !== expected)
|
|
82
|
-
return invalid('File download section metadata does not match the requested range');
|
|
83
|
-
}
|
|
84
|
-
return bytes;
|
|
85
|
-
}
|
|
86
|
-
/** Read bytes without invoking the shared JSON-only Axios transformer. */
|
|
87
|
-
function fetchFileSection(body) {
|
|
88
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
-
const { total, start, length } = requestRange(body);
|
|
90
|
-
let response;
|
|
91
|
-
try {
|
|
92
|
-
response = yield axiosInstance_1.axiosInstance.post('/Tools/DownloadFile', body, {
|
|
93
|
-
responseType: 'arraybuffer',
|
|
94
|
-
transformResponse: [data => data],
|
|
95
|
-
validateStatus: () => true,
|
|
96
|
-
timeout: FILE_TIMEOUT,
|
|
97
|
-
maxContentLength: Math.ceil(length * 4 / 3) + 64 * 1024,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
const errorCode = error === null || error === void 0 ? void 0 : error.code;
|
|
102
|
-
const code = typeof errorCode === 'string' && /^[A-Z_\d]+$/.test(errorCode) ? errorCode : 'NETWORK_ERROR';
|
|
103
|
-
throw new FileDownloadError(`File section request failed (${code})`, undefined, undefined, ['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ECONNREFUSED', 'NETWORK_ERROR'].includes(code));
|
|
104
|
-
}
|
|
105
|
-
const contentType = header(response, 'content-type').split(';')[0].toLowerCase();
|
|
106
|
-
const raw = response.data;
|
|
107
|
-
const bytes = Buffer.isBuffer(raw) ? raw : raw instanceof ArrayBuffer ? Buffer.from(raw) :
|
|
108
|
-
ArrayBuffer.isView(raw) ? Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength) :
|
|
109
|
-
typeof raw === 'string' && contentType === 'application/json' ? Buffer.from(raw) : invalid('File download returned an unsupported response');
|
|
110
|
-
let json;
|
|
111
|
-
if (contentType === 'application/json' || (!contentType && bytes[0] === 123)) {
|
|
112
|
-
try {
|
|
113
|
-
json = parseJSON.parse(bytes.toString('utf8'));
|
|
114
|
-
}
|
|
115
|
-
catch (_a) {
|
|
116
|
-
return invalid(`File download returned invalid JSON (HTTP ${response.status})`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (response.status !== 200 && response.status !== 206) {
|
|
120
|
-
const code = typeof (json === null || json === void 0 ? void 0 : json.Code) === 'number' ? json.Code : undefined;
|
|
121
|
-
throw new FileDownloadError(`File download failed (HTTP ${response.status}${code !== undefined ? `, Code=${code}` : ''}): ${safeMessage(json === null || json === void 0 ? void 0 : json.Message, body)}`, response.status, code, [429, 502, 503, 504].includes(response.status));
|
|
122
|
-
}
|
|
123
|
-
if (json)
|
|
124
|
-
return { response, buffer: decodeLegacy(json, body), legacy: json };
|
|
125
|
-
if (contentType !== 'application/octet-stream' || !bytes.length || bytes.length > length) {
|
|
126
|
-
return invalid('File download returned an empty or invalid binary section');
|
|
127
|
-
}
|
|
128
|
-
const declared = header(response, 'content-length');
|
|
129
|
-
if (declared && (!/^\d+$/.test(declared) || Number(declared) !== bytes.length)) {
|
|
130
|
-
return invalid('File download Content-Length does not match received bytes');
|
|
131
|
-
}
|
|
132
|
-
if (response.status === 206) {
|
|
133
|
-
const range = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(header(response, 'content-range'));
|
|
134
|
-
if (!range || Number(range[1]) !== start || Number(range[2]) !== start + bytes.length - 1 || Number(range[3]) !== total) {
|
|
135
|
-
return invalid('File download Content-Range does not match the requested section');
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
else if (start !== 0 || bytes.length !== total) {
|
|
139
|
-
return invalid('File download returned HTTP 200 for an incomplete file');
|
|
140
|
-
}
|
|
141
|
-
return { response, buffer: bytes };
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
/** Preserve the existing SDK envelope for consumers using Data.data.buffer. */
|
|
23
|
+
/** Explicit compatibility API for consumers using Data.data.buffer. */
|
|
145
24
|
function legacyFileSection(section, body) {
|
|
146
25
|
var _a;
|
|
147
26
|
const { response, buffer, legacy } = section;
|
|
148
27
|
return Object.assign(Object.assign({}, response), { data: Object.assign(Object.assign({}, legacy), { Code: 0, Success: true, Message: (legacy === null || legacy === void 0 ? void 0 : legacy.Message) || '成功', Data: Object.assign(Object.assign({}, legacy === null || legacy === void 0 ? void 0 : legacy.Data), { BaseResponse: { ret: 0 }, totalLen: body.DataLen, startPos: body.Section.StartPos, dataLen: buffer.length, data: Object.assign(Object.assign({}, (_a = legacy === null || legacy === void 0 ? void 0 : legacy.Data) === null || _a === void 0 ? void 0 : _a.data), { iLen: buffer.length, buffer: buffer.toString('base64') }) }) }) });
|
|
149
28
|
}
|
|
150
|
-
/** Advance by actual
|
|
29
|
+
/** Advance by actual bytes, retrying only transient failures at the same offset. */
|
|
151
30
|
function downloadFileBuffer(body) {
|
|
152
31
|
return __awaiter(this, void 0, void 0, function* () {
|
|
153
32
|
const total = body === null || body === void 0 ? void 0 : body.DataLen;
|
|
154
|
-
|
|
33
|
+
if (!Number.isSafeInteger(total) || total <= 0 || total > 0xffffffff || total > buffer_1.constants.MAX_LENGTH) {
|
|
34
|
+
throw new FileDownloadError_1.FileDownloadError('invalid total length');
|
|
35
|
+
}
|
|
155
36
|
const chunks = [];
|
|
156
37
|
let offset = 0;
|
|
157
38
|
while (offset < total) {
|
|
@@ -159,11 +40,11 @@ function downloadFileBuffer(body) {
|
|
|
159
40
|
let section;
|
|
160
41
|
for (let attempt = 0;; attempt++) {
|
|
161
42
|
try {
|
|
162
|
-
section = yield fetchFileSection(request);
|
|
43
|
+
section = yield (0, downloadFileSection_1.fetchFileSection)(request);
|
|
163
44
|
break;
|
|
164
45
|
}
|
|
165
46
|
catch (error) {
|
|
166
|
-
if (!(error instanceof FileDownloadError) || !error.retryable || attempt >= 2)
|
|
47
|
+
if (!(error instanceof FileDownloadError_1.FileDownloadError) || !error.retryable || attempt >= 2)
|
|
167
48
|
throw error;
|
|
168
49
|
yield new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
|
|
169
50
|
}
|
package/package.json
CHANGED
|
@@ -1,43 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "wx2tg-puppet",
|
|
3
|
-
"version": "
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"types": "dist/index.d.ts",
|
|
6
|
-
"files": [
|
|
7
|
-
"dist",
|
|
8
|
-
"README.md"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"start": "node dist/index.js",
|
|
14
|
-
"dev": "ts-node src/index.ts",
|
|
15
|
-
"test": "
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "wx2tg-puppet",
|
|
3
|
+
"version": "6.0.17",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"VERSION"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"dev": "ts-node src/index.ts",
|
|
15
|
+
"test": "npm run build && node --test test/*.test.cjs tests/*.test.cjs",
|
|
16
|
+
"test:download": "npm run build && node --test test/download.test.cjs tests/file-download.test.cjs",
|
|
17
|
+
"check:version": "node scripts/check-version.cjs",
|
|
18
|
+
"prepack": "npm run check:version && npm run build",
|
|
19
|
+
"prepublishOnly": "npm run check:version && npm test"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"wechat",
|
|
23
|
+
"bot",
|
|
24
|
+
"puppet"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"description": "微信机器人核心库,可作为依赖被其他项目引入。",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/finalpi/wx2tg-puppet.git"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/json-bigint": "^1.0.4",
|
|
35
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"typescript": "^5.8.3"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"axios": "^1.9.0",
|
|
41
|
+
"fast-xml-parser": "^5.2.3",
|
|
42
|
+
"file-box": "^1.4.15",
|
|
43
|
+
"json-bigint": "^1.0.0",
|
|
44
|
+
"qrcode-terminal": "^0.12.0",
|
|
45
|
+
"reflect-metadata": "^0.2.2",
|
|
46
|
+
"sqlite3": "^5.1.7",
|
|
47
|
+
"typeorm": "^0.3.23",
|
|
48
|
+
"wx-voice": "^0.2.1"
|
|
49
|
+
}
|
|
50
|
+
}
|