socialdatax-skills 0.2.21 → 0.2.22
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 +9 -3
- package/cli.mjs +596 -6
- package/package.json +1 -1
- package/skills/media-comments/SKILL.md +93 -20
- package/skills/media-detail/SKILL.md +45 -12
- package/skills/media-search/SKILL.md +39 -10
- package/skills/media-transcript/SKILL.md +63 -15
- package/skills/media-user-info/SKILL.md +43 -10
- package/skills/media-user-posts/SKILL.md +76 -18
- package/skills/sensitive-check/SKILL.md +3 -1
- package/skills/socialdatax-content-research-assistant/SKILL.md +337 -78
package/cli.mjs
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
cp,
|
|
7
|
+
mkdir,
|
|
8
|
+
open as openFile,
|
|
9
|
+
readFile,
|
|
10
|
+
rename,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
unlink,
|
|
14
|
+
} from "node:fs/promises";
|
|
5
15
|
import { homedir } from "node:os";
|
|
6
16
|
import { dirname, join, resolve } from "node:path";
|
|
7
17
|
import { fileURLToPath } from "node:url";
|
|
8
18
|
|
|
9
19
|
const PACKAGE_NAME = "socialdatax-skills";
|
|
10
|
-
const PACKAGE_VERSION = "0.2.
|
|
20
|
+
const PACKAGE_VERSION = "0.2.22";
|
|
11
21
|
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
12
22
|
const LOG_PREFIX = `[${PACKAGE_NAME}]`;
|
|
13
23
|
const MIN_NODE_VERSION = "20.18.1";
|
|
14
24
|
const HOMEPAGE_URL = "https://socialdatax.com";
|
|
25
|
+
const MEDIA_DOWNLOAD_USER_AGENT =
|
|
26
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
|
|
27
|
+
const WECHAT_MEDIA_DOWNLOAD_REFERER = "https://weixin.qq.com/";
|
|
28
|
+
const MEDIA_DOWNLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
29
|
+
const MEDIA_DOWNLOAD_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
30
|
+
const MEDIA_DECRYPT_CHUNK_SIZE = 64 * 1024;
|
|
31
|
+
const MEDIA_DOWNLOAD_MAX_REDIRECTS = 5;
|
|
32
|
+
// Keep this local helper scoped to WeChat media host trees; require k for the decrypt key.
|
|
33
|
+
const WECHAT_MEDIA_DOWNLOAD_HOST_SUFFIXES = [
|
|
34
|
+
".wxapp.tc.qq.com",
|
|
35
|
+
".video.qq.com",
|
|
36
|
+
];
|
|
37
|
+
const WECHAT_MEDIA_DOWNLOAD_EXACT_HOSTS = new Set(["wxapp.tc.qq.com"]);
|
|
38
|
+
const WECHAT_MEDIA_DECODE_KEY_MAX = (1n << 64n) - 1n;
|
|
39
|
+
const WECHAT_DETAIL_MEDIA_URL_ERROR =
|
|
40
|
+
"Media URL cannot be decrypted. Use the video.video_url returned by SocialDataX WeChat detail.";
|
|
41
|
+
const WECHAT_MEDIA_RESPONSE_DECRYPT_ERROR =
|
|
42
|
+
"Downloaded media cannot be decrypted. Use the video.video_url returned by SocialDataX WeChat detail.";
|
|
15
43
|
const PRIMARY_API_KEY_ENV = "SOCIALDATAX_API_KEY";
|
|
16
44
|
const LEGACY_API_KEY_ENV = "SOCIAL_MEDIA_MCP_API_KEY";
|
|
17
45
|
const API_KEY_ENV_NAMES = [PRIMARY_API_KEY_ENV, LEGACY_API_KEY_ENV];
|
|
@@ -254,6 +282,7 @@ const DOUYIN_OPTION_DISPLAY_NAMES = {
|
|
|
254
282
|
jobId: "--job-id",
|
|
255
283
|
};
|
|
256
284
|
const KUAISHOU_DIRECT_ACTION_OPTIONS = {
|
|
285
|
+
"hot-search": ["pretty"],
|
|
257
286
|
search: ["keyword", "pageToken", "pages", "all", "maxItems", "sinceDays", "pretty"],
|
|
258
287
|
"user-search": ["keyword", "pageToken", "pages", "maxItems", "pretty"],
|
|
259
288
|
detail: ["photoId", "url", "pretty"],
|
|
@@ -410,7 +439,9 @@ const WECHAT_DIRECT_ACTION_OPTIONS = {
|
|
|
410
439
|
"pretty",
|
|
411
440
|
],
|
|
412
441
|
transcript: ["url", "encryptedObjectId", "jobId", "pretty"],
|
|
442
|
+
"decrypt-media": ["mediaUrl", "output", "pretty"],
|
|
413
443
|
};
|
|
444
|
+
const WECHAT_LOCAL_DIRECT_ACTIONS = new Set(["decrypt-media"]);
|
|
414
445
|
const WECHAT_DIRECT_ACTION_NAMES = Object.keys(WECHAT_DIRECT_ACTION_OPTIONS).join(", ");
|
|
415
446
|
const WECHAT_SEARCH_SORT_TYPES = [
|
|
416
447
|
"all",
|
|
@@ -440,6 +471,8 @@ const WECHAT_OPTION_DISPLAY_NAMES = {
|
|
|
440
471
|
commentId: "--comment-id",
|
|
441
472
|
userId: "--user-id",
|
|
442
473
|
jobId: "--job-id",
|
|
474
|
+
mediaUrl: "--media-url",
|
|
475
|
+
output: "--output",
|
|
443
476
|
};
|
|
444
477
|
const SENSITIVE_CHECK_DIRECT_ACTION_OPTIONS = {
|
|
445
478
|
text: ["text", "platform", "pretty"],
|
|
@@ -633,6 +666,10 @@ const PLATFORMS = {
|
|
|
633
666
|
"KUAISHOU_MCP_UPSTREAM_URL",
|
|
634
667
|
],
|
|
635
668
|
tools: [
|
|
669
|
+
{
|
|
670
|
+
name: "kuaishou_get_hot_search_list",
|
|
671
|
+
description: "Fetch the current Kuaishou / 快手 hot-search list.",
|
|
672
|
+
},
|
|
636
673
|
{
|
|
637
674
|
name: "kuaishou_search_videos",
|
|
638
675
|
description:
|
|
@@ -904,7 +941,7 @@ function isMainModule() {
|
|
|
904
941
|
}
|
|
905
942
|
}
|
|
906
943
|
|
|
907
|
-
|
|
944
|
+
async function main() {
|
|
908
945
|
try {
|
|
909
946
|
if (command === "install") {
|
|
910
947
|
await installSkills(cliArgs.slice(1));
|
|
@@ -1092,9 +1129,14 @@ function validateWechatDirectActionOptions(action, options) {
|
|
|
1092
1129
|
return;
|
|
1093
1130
|
}
|
|
1094
1131
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1132
|
+
if (WECHAT_LOCAL_DIRECT_ACTIONS.has(action)) {
|
|
1133
|
+
validateKnownOptions(options, allowedOptions);
|
|
1134
|
+
validateFlagOption(options, "pretty", "--pretty");
|
|
1135
|
+
} else {
|
|
1136
|
+
validateKnownOptions(options, allowedDirectOptions(allowedOptions));
|
|
1137
|
+
validateDirectMetaOptions(options);
|
|
1138
|
+
validateDirectPaginationOptions("wechat", action, options);
|
|
1139
|
+
}
|
|
1098
1140
|
for (const key of allowedOptions) {
|
|
1099
1141
|
if (!DIRECT_BOOLEAN_OPTIONS.has(key)) {
|
|
1100
1142
|
requireOptionValue(options, key, WECHAT_OPTION_DISPLAY_NAMES[key]);
|
|
@@ -1786,6 +1828,9 @@ function printHelp() {
|
|
|
1786
1828
|
console.log(` npx -y ${PACKAGE_SPEC} douyin transcript --aweme-id "<aweme_id>" --pretty`);
|
|
1787
1829
|
console.log(" Submit or check a Douyin video speech-to-text transcript job.");
|
|
1788
1830
|
console.log("");
|
|
1831
|
+
console.log(` npx -y ${PACKAGE_SPEC} kuaishou hot-search --pretty`);
|
|
1832
|
+
console.log(" Call the Kuaishou hot-search list tool directly and print JSON.");
|
|
1833
|
+
console.log("");
|
|
1789
1834
|
console.log(` npx -y ${PACKAGE_SPEC} kuaishou search --keyword "露营" --pretty`);
|
|
1790
1835
|
console.log(" Call the Kuaishou work search tool directly and print JSON.");
|
|
1791
1836
|
console.log("");
|
|
@@ -1883,6 +1928,9 @@ function printHelp() {
|
|
|
1883
1928
|
console.log(` npx -y ${PACKAGE_SPEC} wechat detail --url "<wechat_video_url_or_share_text>" --pretty`);
|
|
1884
1929
|
console.log(" Call the WeChat Channels / 视频号 video detail tool from a video link or share text.");
|
|
1885
1930
|
console.log("");
|
|
1931
|
+
console.log(` npx -y ${PACKAGE_SPEC} wechat decrypt-media --media-url "<video.video_url>" --output video.mp4`);
|
|
1932
|
+
console.log(" Save the WeChat Channels / 视频号 media URL returned by detail and locally decrypt when needed.");
|
|
1933
|
+
console.log("");
|
|
1886
1934
|
console.log(` npx -y ${PACKAGE_SPEC} wechat article --url "<mp_article_url_or_share_text>" --pretty`);
|
|
1887
1935
|
console.log(" Call the WeChat Official Account / 微信公众号 article detail tool from an article link or share text.");
|
|
1888
1936
|
console.log("");
|
|
@@ -1980,6 +2028,10 @@ function printHelp() {
|
|
|
1980
2028
|
console.log(" --url <url-or-share-text>");
|
|
1981
2029
|
console.log(" Content link, short link, or share text for URL-based detail/comment/article commands.");
|
|
1982
2030
|
console.log(" For Douyin detail/comments, pass a content page link, not video.play_url.");
|
|
2031
|
+
console.log(" --media-url <video.video_url>");
|
|
2032
|
+
console.log(" WeChat detail result media URL for local decrypt-media download.");
|
|
2033
|
+
console.log(" --output <file>");
|
|
2034
|
+
console.log(" Output file path for local decrypt-media download.");
|
|
1983
2035
|
console.log(" --note-id <note_id>");
|
|
1984
2036
|
console.log(" --aweme-id <aweme_id>");
|
|
1985
2037
|
console.log(" --photo-id <photo_id>");
|
|
@@ -2215,6 +2267,13 @@ async function runWechatDirectCommand(args) {
|
|
|
2215
2267
|
}
|
|
2216
2268
|
validateWechatDirectActionOptions(action, options);
|
|
2217
2269
|
|
|
2270
|
+
if (action === "decrypt-media") {
|
|
2271
|
+
const data = await decryptWechatMediaCommand(options);
|
|
2272
|
+
process.stdout.write(JSON.stringify(data, null, options.pretty ? 2 : 0));
|
|
2273
|
+
process.stdout.write("\n");
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2218
2277
|
const operation = attachDirectMetadata(buildWechatOperation(action, options), options);
|
|
2219
2278
|
const data = shouldUsePaginatedDirectOutput(options)
|
|
2220
2279
|
? await callPaginatedDirectOperation(operation, options)
|
|
@@ -2486,6 +2545,15 @@ function buildDouyinOperation(action, options) {
|
|
|
2486
2545
|
|
|
2487
2546
|
function buildKuaishouOperation(action, options) {
|
|
2488
2547
|
switch (action) {
|
|
2548
|
+
case "hot-search":
|
|
2549
|
+
return buildDirectOperation(
|
|
2550
|
+
"hot-search",
|
|
2551
|
+
{
|
|
2552
|
+
tool: "kuaishou_get_hot_search_list",
|
|
2553
|
+
toolArguments: {},
|
|
2554
|
+
},
|
|
2555
|
+
PLATFORMS.kuaishou
|
|
2556
|
+
);
|
|
2489
2557
|
case "search":
|
|
2490
2558
|
return buildDirectOperation(
|
|
2491
2559
|
"search",
|
|
@@ -2819,6 +2887,524 @@ function buildWechatOperation(action, options) {
|
|
|
2819
2887
|
}
|
|
2820
2888
|
}
|
|
2821
2889
|
|
|
2890
|
+
export async function decryptWechatMediaCommand(
|
|
2891
|
+
options,
|
|
2892
|
+
{
|
|
2893
|
+
fetchMedia = fetch,
|
|
2894
|
+
downloadTimeoutMs = MEDIA_DOWNLOAD_TIMEOUT_MS,
|
|
2895
|
+
maxDownloadBytes = MEDIA_DOWNLOAD_MAX_BYTES,
|
|
2896
|
+
} = {}
|
|
2897
|
+
) {
|
|
2898
|
+
if (!options.mediaUrl) {
|
|
2899
|
+
throw new Error("Missing --media-url for wechat decrypt-media.");
|
|
2900
|
+
}
|
|
2901
|
+
if (!options.output) {
|
|
2902
|
+
throw new Error("Missing --output for wechat decrypt-media.");
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
const { mediaUrl, decodeKey } = parseWechatDecryptMediaUrl(options.mediaUrl);
|
|
2906
|
+
const outputPath = resolve(options.output);
|
|
2907
|
+
const tempPath = `${outputPath}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
2908
|
+
await assertWechatMediaOutputPath(outputPath);
|
|
2909
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
2910
|
+
|
|
2911
|
+
let encryptedLength;
|
|
2912
|
+
let decrypted = false;
|
|
2913
|
+
let bytesWritten = 0;
|
|
2914
|
+
try {
|
|
2915
|
+
const signal = createMediaDownloadAbortSignal(downloadTimeoutMs);
|
|
2916
|
+
const response = await fetchWechatMedia(mediaUrl, {
|
|
2917
|
+
fetchMedia,
|
|
2918
|
+
signal,
|
|
2919
|
+
});
|
|
2920
|
+
if (!response.ok) {
|
|
2921
|
+
throw new Error(`Media download failed with HTTP ${response.status}.`);
|
|
2922
|
+
}
|
|
2923
|
+
assertMediaDownloadWithinLimit(
|
|
2924
|
+
parseOptionalIntegerHeader(response.headers.get("content-length")),
|
|
2925
|
+
maxDownloadBytes
|
|
2926
|
+
);
|
|
2927
|
+
encryptedLength = parseOptionalWechatEncryptedLengthHeader(
|
|
2928
|
+
response.headers.get("x-enclen")
|
|
2929
|
+
);
|
|
2930
|
+
assertMediaDownloadWithinLimit(encryptedLength, maxDownloadBytes);
|
|
2931
|
+
bytesWritten = await writeResponseBodyToFile(response, tempPath, {
|
|
2932
|
+
maxBytes: maxDownloadBytes,
|
|
2933
|
+
});
|
|
2934
|
+
if (encryptedLength !== undefined) {
|
|
2935
|
+
await decryptWechatMediaFilePrefix(tempPath, {
|
|
2936
|
+
encryptedLength,
|
|
2937
|
+
decodeKey,
|
|
2938
|
+
});
|
|
2939
|
+
decrypted = true;
|
|
2940
|
+
}
|
|
2941
|
+
await rename(tempPath, outputPath);
|
|
2942
|
+
} catch (error) {
|
|
2943
|
+
await unlink(tempPath).catch(() => {});
|
|
2944
|
+
if (isAbortLikeError(error)) {
|
|
2945
|
+
throw new Error("Media download timed out.");
|
|
2946
|
+
}
|
|
2947
|
+
throw error;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
return {
|
|
2951
|
+
platform: "wechat",
|
|
2952
|
+
action: "decrypt-media",
|
|
2953
|
+
output_path: outputPath,
|
|
2954
|
+
decrypted,
|
|
2955
|
+
bytes_written: bytesWritten,
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
async function assertWechatMediaOutputPath(outputPath) {
|
|
2960
|
+
const existingOutput = await stat(outputPath).catch((error) => {
|
|
2961
|
+
if (error?.code === "ENOENT") {
|
|
2962
|
+
return undefined;
|
|
2963
|
+
}
|
|
2964
|
+
throw error;
|
|
2965
|
+
});
|
|
2966
|
+
if (existingOutput?.isDirectory()) {
|
|
2967
|
+
throw new Error("--output must be a file path for wechat decrypt-media.");
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
async function fetchWechatMedia(
|
|
2972
|
+
mediaUrl,
|
|
2973
|
+
{
|
|
2974
|
+
fetchMedia,
|
|
2975
|
+
signal,
|
|
2976
|
+
maxRedirects = MEDIA_DOWNLOAD_MAX_REDIRECTS,
|
|
2977
|
+
}
|
|
2978
|
+
) {
|
|
2979
|
+
let currentUrl = mediaUrl;
|
|
2980
|
+
for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
|
|
2981
|
+
const response = await fetchMedia(currentUrl, {
|
|
2982
|
+
headers: {
|
|
2983
|
+
Referer: WECHAT_MEDIA_DOWNLOAD_REFERER,
|
|
2984
|
+
"User-Agent": MEDIA_DOWNLOAD_USER_AGENT,
|
|
2985
|
+
},
|
|
2986
|
+
redirect: "manual",
|
|
2987
|
+
...(signal ? { signal } : {}),
|
|
2988
|
+
});
|
|
2989
|
+
if (!isHttpRedirectStatus(response.status)) {
|
|
2990
|
+
return response;
|
|
2991
|
+
}
|
|
2992
|
+
currentUrl = resolveWechatMediaRedirectUrl(
|
|
2993
|
+
currentUrl,
|
|
2994
|
+
response.headers?.get("location")
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
throw new Error("Media download redirected too many times.");
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
function isHttpRedirectStatus(status) {
|
|
3001
|
+
return [301, 302, 303, 307, 308].includes(status);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
function resolveWechatMediaRedirectUrl(currentUrl, location) {
|
|
3005
|
+
if (
|
|
3006
|
+
location === undefined ||
|
|
3007
|
+
location === null ||
|
|
3008
|
+
String(location).trim() === ""
|
|
3009
|
+
) {
|
|
3010
|
+
throw new Error("Media download redirect is invalid.");
|
|
3011
|
+
}
|
|
3012
|
+
let parsed;
|
|
3013
|
+
try {
|
|
3014
|
+
parsed = new URL(String(location), currentUrl);
|
|
3015
|
+
} catch {
|
|
3016
|
+
throw new Error("Media download redirect is invalid.");
|
|
3017
|
+
}
|
|
3018
|
+
if (!isWechatDetailMediaDownloadUrl(parsed)) {
|
|
3019
|
+
throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
|
|
3020
|
+
}
|
|
3021
|
+
return parsed.href;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function createMediaDownloadAbortSignal(timeoutMs) {
|
|
3025
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
3026
|
+
return undefined;
|
|
3027
|
+
}
|
|
3028
|
+
if (
|
|
3029
|
+
typeof AbortSignal !== "undefined" &&
|
|
3030
|
+
typeof AbortSignal.timeout === "function"
|
|
3031
|
+
) {
|
|
3032
|
+
return AbortSignal.timeout(timeoutMs);
|
|
3033
|
+
}
|
|
3034
|
+
const controller = new AbortController();
|
|
3035
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3036
|
+
timer.unref?.();
|
|
3037
|
+
return controller.signal;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function parseWechatDecryptMediaUrl(mediaUrl) {
|
|
3041
|
+
let parsed;
|
|
3042
|
+
try {
|
|
3043
|
+
parsed = new URL(mediaUrl);
|
|
3044
|
+
} catch {
|
|
3045
|
+
throw new Error("Invalid --media-url.");
|
|
3046
|
+
}
|
|
3047
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
3048
|
+
throw new Error("Invalid --media-url.");
|
|
3049
|
+
}
|
|
3050
|
+
if (!isWechatDetailMediaDownloadUrl(parsed)) {
|
|
3051
|
+
throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
const decodeKeyText = parsed.searchParams.get("k");
|
|
3055
|
+
if (!decodeKeyText) {
|
|
3056
|
+
throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
|
|
3057
|
+
}
|
|
3058
|
+
if (!/^\d+$/.test(decodeKeyText)) {
|
|
3059
|
+
throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
|
|
3060
|
+
}
|
|
3061
|
+
if (decodeKeyText.length > 20) {
|
|
3062
|
+
throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
|
|
3063
|
+
}
|
|
3064
|
+
const decodeKey = BigInt(decodeKeyText);
|
|
3065
|
+
if (decodeKey > WECHAT_MEDIA_DECODE_KEY_MAX) {
|
|
3066
|
+
throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
|
|
3067
|
+
}
|
|
3068
|
+
return {
|
|
3069
|
+
mediaUrl,
|
|
3070
|
+
decodeKey,
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
function isWechatDetailMediaDownloadUrl(parsed) {
|
|
3075
|
+
return (
|
|
3076
|
+
parsed.username === "" &&
|
|
3077
|
+
parsed.password === "" &&
|
|
3078
|
+
parsed.port === "" &&
|
|
3079
|
+
isAllowedWechatMediaDownloadHost(parsed.hostname)
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
function isAllowedWechatMediaDownloadHost(hostname) {
|
|
3084
|
+
return (
|
|
3085
|
+
WECHAT_MEDIA_DOWNLOAD_EXACT_HOSTS.has(hostname) ||
|
|
3086
|
+
WECHAT_MEDIA_DOWNLOAD_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix))
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
function parseOptionalWechatEncryptedLengthHeader(value) {
|
|
3091
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
3092
|
+
return undefined;
|
|
3093
|
+
}
|
|
3094
|
+
if (!/^\d+$/.test(String(value).trim())) {
|
|
3095
|
+
throw new Error(WECHAT_MEDIA_RESPONSE_DECRYPT_ERROR);
|
|
3096
|
+
}
|
|
3097
|
+
const encryptedLength = Number.parseInt(String(value).trim(), 10);
|
|
3098
|
+
if (!Number.isSafeInteger(encryptedLength)) {
|
|
3099
|
+
throw new Error(WECHAT_MEDIA_RESPONSE_DECRYPT_ERROR);
|
|
3100
|
+
}
|
|
3101
|
+
return encryptedLength > 0 ? encryptedLength : undefined;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
function parseOptionalIntegerHeader(value) {
|
|
3105
|
+
if (value === null || String(value).trim() === "") {
|
|
3106
|
+
return undefined;
|
|
3107
|
+
}
|
|
3108
|
+
if (!/^\d+$/.test(String(value).trim())) {
|
|
3109
|
+
return undefined;
|
|
3110
|
+
}
|
|
3111
|
+
const parsed = Number.parseInt(String(value).trim(), 10);
|
|
3112
|
+
return Number.isSafeInteger(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function assertMediaDownloadWithinLimit(bytes, maxBytes) {
|
|
3116
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || bytes === undefined) {
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
if (bytes > maxBytes) {
|
|
3120
|
+
throw new Error("Media download is too large for local decrypt-media.");
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
async function writeResponseBodyToFile(response, filePath, { maxBytes } = {}) {
|
|
3125
|
+
if (!response.body) {
|
|
3126
|
+
throw new Error("Media download response has no body.");
|
|
3127
|
+
}
|
|
3128
|
+
const file = await openFile(filePath, "w");
|
|
3129
|
+
let bytesWritten = 0;
|
|
3130
|
+
try {
|
|
3131
|
+
for await (const chunk of response.body) {
|
|
3132
|
+
const buffer = Buffer.from(chunk);
|
|
3133
|
+
if (buffer.length === 0) {
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
assertMediaDownloadWithinLimit(
|
|
3137
|
+
bytesWritten + buffer.length,
|
|
3138
|
+
maxBytes
|
|
3139
|
+
);
|
|
3140
|
+
await writeAllToFile(file, buffer);
|
|
3141
|
+
bytesWritten += buffer.length;
|
|
3142
|
+
}
|
|
3143
|
+
} finally {
|
|
3144
|
+
await file.close();
|
|
3145
|
+
}
|
|
3146
|
+
return bytesWritten;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
async function decryptWechatMediaFilePrefix(
|
|
3150
|
+
filePath,
|
|
3151
|
+
{
|
|
3152
|
+
encryptedLength,
|
|
3153
|
+
decodeKey,
|
|
3154
|
+
}
|
|
3155
|
+
) {
|
|
3156
|
+
const file = await openFile(filePath, "r+");
|
|
3157
|
+
try {
|
|
3158
|
+
const stats = await file.stat();
|
|
3159
|
+
if (encryptedLength > stats.size) {
|
|
3160
|
+
throw new Error("Downloaded media is incomplete and cannot be decrypted.");
|
|
3161
|
+
}
|
|
3162
|
+
const decryptStream = createWechatMediaDecryptStream(decodeKey);
|
|
3163
|
+
let offset = 0;
|
|
3164
|
+
while (offset < encryptedLength) {
|
|
3165
|
+
const chunkLength = Math.min(
|
|
3166
|
+
MEDIA_DECRYPT_CHUNK_SIZE,
|
|
3167
|
+
encryptedLength - offset
|
|
3168
|
+
);
|
|
3169
|
+
const encryptedChunk = Buffer.allocUnsafe(chunkLength);
|
|
3170
|
+
const { bytesRead } = await file.read(
|
|
3171
|
+
encryptedChunk,
|
|
3172
|
+
0,
|
|
3173
|
+
chunkLength,
|
|
3174
|
+
offset
|
|
3175
|
+
);
|
|
3176
|
+
if (bytesRead !== chunkLength) {
|
|
3177
|
+
throw new Error("Downloaded media is incomplete and cannot be decrypted.");
|
|
3178
|
+
}
|
|
3179
|
+
decryptWechatMediaBufferChunk(encryptedChunk, decryptStream);
|
|
3180
|
+
await writeAllToFile(file, encryptedChunk, offset);
|
|
3181
|
+
offset += chunkLength;
|
|
3182
|
+
}
|
|
3183
|
+
} finally {
|
|
3184
|
+
await file.close();
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
function isAbortLikeError(error) {
|
|
3189
|
+
return error?.name === "AbortError" || error?.name === "TimeoutError";
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
async function writeAllToFile(file, buffer, position) {
|
|
3193
|
+
let offset = 0;
|
|
3194
|
+
while (offset < buffer.length) {
|
|
3195
|
+
const writePosition = position === undefined ? undefined : position + offset;
|
|
3196
|
+
const { bytesWritten } = await file.write(
|
|
3197
|
+
buffer,
|
|
3198
|
+
offset,
|
|
3199
|
+
buffer.length - offset,
|
|
3200
|
+
writePosition
|
|
3201
|
+
);
|
|
3202
|
+
if (bytesWritten <= 0) {
|
|
3203
|
+
throw new Error("Downloaded media could not be written.");
|
|
3204
|
+
}
|
|
3205
|
+
offset += bytesWritten;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function createWechatMediaDecryptStream(decodeKey) {
|
|
3210
|
+
return {
|
|
3211
|
+
context: new WechatFinderIsaac64Context(decodeKey),
|
|
3212
|
+
block: Buffer.allocUnsafe(8),
|
|
3213
|
+
index: 8,
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function decryptWechatMediaBufferChunk(buffer, stream) {
|
|
3218
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
3219
|
+
if (stream.index === 8) {
|
|
3220
|
+
stream.block.writeBigUInt64BE(stream.context.next());
|
|
3221
|
+
stream.index = 0;
|
|
3222
|
+
}
|
|
3223
|
+
buffer[index] ^= stream.block[stream.index];
|
|
3224
|
+
stream.index += 1;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
class WechatFinderIsaac64Context {
|
|
3229
|
+
static MASK = (1n << 64n) - 1n;
|
|
3230
|
+
static RANDSIZ = 1 << 8;
|
|
3231
|
+
|
|
3232
|
+
constructor(decodeKey) {
|
|
3233
|
+
this.seed = Array(WechatFinderIsaac64Context.RANDSIZ).fill(0n);
|
|
3234
|
+
this.mm = Array(WechatFinderIsaac64Context.RANDSIZ).fill(0n);
|
|
3235
|
+
this.aa = 0n;
|
|
3236
|
+
this.bb = 0n;
|
|
3237
|
+
this.cc = 0n;
|
|
3238
|
+
this.randcnt = 0;
|
|
3239
|
+
this.seed[0] = this.u64(decodeKey);
|
|
3240
|
+
this.init(true);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
next() {
|
|
3244
|
+
if (this.randcnt === 0) {
|
|
3245
|
+
this.isaac64();
|
|
3246
|
+
this.randcnt = WechatFinderIsaac64Context.RANDSIZ;
|
|
3247
|
+
}
|
|
3248
|
+
this.randcnt -= 1;
|
|
3249
|
+
return this.seed[this.randcnt];
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
init(seeded) {
|
|
3253
|
+
let a = 0x647c4677a2884b7cn;
|
|
3254
|
+
let b = 0xb9f8b322c73ac862n;
|
|
3255
|
+
let c = 0x8c0ea5053d4712a0n;
|
|
3256
|
+
let d = 0xb29b2e824a595524n;
|
|
3257
|
+
let e = 0x82f053db8355e0cen;
|
|
3258
|
+
let f = 0x48fe4a0fa5a09315n;
|
|
3259
|
+
let g = 0xae985bf2cbfc89edn;
|
|
3260
|
+
let h = 0x98f5704f6c44c0abn;
|
|
3261
|
+
|
|
3262
|
+
for (let index = 0; index < WechatFinderIsaac64Context.RANDSIZ; index += 8) {
|
|
3263
|
+
if (seeded) {
|
|
3264
|
+
a = this.u64(a + this.seed[index]);
|
|
3265
|
+
b = this.u64(b + this.seed[index + 1]);
|
|
3266
|
+
c = this.u64(c + this.seed[index + 2]);
|
|
3267
|
+
d = this.u64(d + this.seed[index + 3]);
|
|
3268
|
+
e = this.u64(e + this.seed[index + 4]);
|
|
3269
|
+
f = this.u64(f + this.seed[index + 5]);
|
|
3270
|
+
g = this.u64(g + this.seed[index + 6]);
|
|
3271
|
+
h = this.u64(h + this.seed[index + 7]);
|
|
3272
|
+
}
|
|
3273
|
+
[a, b, c, d, e, f, g, h] = this.mix64(a, b, c, d, e, f, g, h);
|
|
3274
|
+
this.mm.splice(index, 8, a, b, c, d, e, f, g, h);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
if (seeded) {
|
|
3278
|
+
for (let index = 0; index < WechatFinderIsaac64Context.RANDSIZ; index += 8) {
|
|
3279
|
+
a = this.u64(a + this.mm[index]);
|
|
3280
|
+
b = this.u64(b + this.mm[index + 1]);
|
|
3281
|
+
c = this.u64(c + this.mm[index + 2]);
|
|
3282
|
+
d = this.u64(d + this.mm[index + 3]);
|
|
3283
|
+
e = this.u64(e + this.mm[index + 4]);
|
|
3284
|
+
f = this.u64(f + this.mm[index + 5]);
|
|
3285
|
+
g = this.u64(g + this.mm[index + 6]);
|
|
3286
|
+
h = this.u64(h + this.mm[index + 7]);
|
|
3287
|
+
[a, b, c, d, e, f, g, h] = this.mix64(a, b, c, d, e, f, g, h);
|
|
3288
|
+
this.mm.splice(index, 8, a, b, c, d, e, f, g, h);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
this.isaac64();
|
|
3293
|
+
this.randcnt = WechatFinderIsaac64Context.RANDSIZ;
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
isaac64() {
|
|
3297
|
+
let a = this.aa;
|
|
3298
|
+
let b = this.u64(this.bb + this.cc + 1n);
|
|
3299
|
+
this.cc = this.u64(this.cc + 1n);
|
|
3300
|
+
|
|
3301
|
+
let resultIndex = 0;
|
|
3302
|
+
for (let index = 0; index < WechatFinderIsaac64Context.RANDSIZ / 2; index += 4) {
|
|
3303
|
+
[a, b] = this.rngstep64(
|
|
3304
|
+
this.u64(~(a ^ this.u64(a << 21n))),
|
|
3305
|
+
a,
|
|
3306
|
+
b,
|
|
3307
|
+
index,
|
|
3308
|
+
index + 128,
|
|
3309
|
+
resultIndex
|
|
3310
|
+
);
|
|
3311
|
+
resultIndex += 1;
|
|
3312
|
+
[a, b] = this.rngstep64(a ^ (a >> 5n), a, b, index + 1, index + 129, resultIndex);
|
|
3313
|
+
resultIndex += 1;
|
|
3314
|
+
[a, b] = this.rngstep64(
|
|
3315
|
+
a ^ this.u64(a << 12n),
|
|
3316
|
+
a,
|
|
3317
|
+
b,
|
|
3318
|
+
index + 2,
|
|
3319
|
+
index + 130,
|
|
3320
|
+
resultIndex
|
|
3321
|
+
);
|
|
3322
|
+
resultIndex += 1;
|
|
3323
|
+
[a, b] = this.rngstep64(a ^ (a >> 33n), a, b, index + 3, index + 131, resultIndex);
|
|
3324
|
+
resultIndex += 1;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
for (
|
|
3328
|
+
let index = WechatFinderIsaac64Context.RANDSIZ / 2;
|
|
3329
|
+
index < WechatFinderIsaac64Context.RANDSIZ;
|
|
3330
|
+
index += 4
|
|
3331
|
+
) {
|
|
3332
|
+
const m2Index = index - WechatFinderIsaac64Context.RANDSIZ / 2;
|
|
3333
|
+
[a, b] = this.rngstep64(
|
|
3334
|
+
this.u64(~(a ^ this.u64(a << 21n))),
|
|
3335
|
+
a,
|
|
3336
|
+
b,
|
|
3337
|
+
index,
|
|
3338
|
+
m2Index,
|
|
3339
|
+
resultIndex
|
|
3340
|
+
);
|
|
3341
|
+
resultIndex += 1;
|
|
3342
|
+
[a, b] = this.rngstep64(a ^ (a >> 5n), a, b, index + 1, m2Index + 1, resultIndex);
|
|
3343
|
+
resultIndex += 1;
|
|
3344
|
+
[a, b] = this.rngstep64(
|
|
3345
|
+
a ^ this.u64(a << 12n),
|
|
3346
|
+
a,
|
|
3347
|
+
b,
|
|
3348
|
+
index + 2,
|
|
3349
|
+
m2Index + 2,
|
|
3350
|
+
resultIndex
|
|
3351
|
+
);
|
|
3352
|
+
resultIndex += 1;
|
|
3353
|
+
[a, b] = this.rngstep64(a ^ (a >> 33n), a, b, index + 3, m2Index + 3, resultIndex);
|
|
3354
|
+
resultIndex += 1;
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
this.bb = b;
|
|
3358
|
+
this.aa = a;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
rngstep64(mix, a, b, mIndex, m2Index, resultIndex) {
|
|
3362
|
+
const x = this.mm[mIndex];
|
|
3363
|
+
const nextA = this.u64(mix + this.mm[m2Index]);
|
|
3364
|
+
const y = this.u64(this.ind64(x) + nextA + b);
|
|
3365
|
+
this.mm[mIndex] = y;
|
|
3366
|
+
const nextB = this.u64(this.ind64(y >> 8n) + x);
|
|
3367
|
+
this.seed[resultIndex] = nextB;
|
|
3368
|
+
return [nextA, nextB];
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
ind64(value) {
|
|
3372
|
+
return this.mm[Number((value >> 3n) & BigInt(WechatFinderIsaac64Context.RANDSIZ - 1))];
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
mix64(a, b, c, d, e, f, g, h) {
|
|
3376
|
+
a = this.u64(a - e);
|
|
3377
|
+
f ^= h >> 9n;
|
|
3378
|
+
h = this.u64(h + a);
|
|
3379
|
+
b = this.u64(b - f);
|
|
3380
|
+
g ^= this.u64(a << 9n);
|
|
3381
|
+
a = this.u64(a + b);
|
|
3382
|
+
c = this.u64(c - g);
|
|
3383
|
+
h ^= b >> 23n;
|
|
3384
|
+
b = this.u64(b + c);
|
|
3385
|
+
d = this.u64(d - h);
|
|
3386
|
+
a ^= this.u64(c << 15n);
|
|
3387
|
+
c = this.u64(c + d);
|
|
3388
|
+
e = this.u64(e - a);
|
|
3389
|
+
b ^= d >> 14n;
|
|
3390
|
+
d = this.u64(d + e);
|
|
3391
|
+
f = this.u64(f - b);
|
|
3392
|
+
c ^= this.u64(e << 20n);
|
|
3393
|
+
e = this.u64(e + f);
|
|
3394
|
+
g = this.u64(g - c);
|
|
3395
|
+
d ^= f >> 17n;
|
|
3396
|
+
f = this.u64(f + g);
|
|
3397
|
+
h = this.u64(h - d);
|
|
3398
|
+
e ^= this.u64(g << 14n);
|
|
3399
|
+
g = this.u64(g + h);
|
|
3400
|
+
return [a, b, c, d, e, f, g, h].map((value) => this.u64(value));
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
u64(value) {
|
|
3404
|
+
return value & WechatFinderIsaac64Context.MASK;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
2822
3408
|
function buildSensitiveCheckOperation(action, options) {
|
|
2823
3409
|
switch (action) {
|
|
2824
3410
|
case "text":
|
|
@@ -4014,3 +4600,7 @@ function extractTextContent(content) {
|
|
|
4014
4600
|
.join("\n")
|
|
4015
4601
|
.trim();
|
|
4016
4602
|
}
|
|
4603
|
+
|
|
4604
|
+
if (isMainModule()) {
|
|
4605
|
+
await main();
|
|
4606
|
+
}
|
package/package.json
CHANGED