socialdatax-skills 0.2.21 → 0.2.23

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/cli.mjs CHANGED
@@ -1,17 +1,76 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { randomUUID } from "node:crypto";
3
4
  import { existsSync, realpathSync } from "node:fs";
4
- import { cp, mkdir, readFile, rm, stat } from "node:fs/promises";
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.21";
20
+ const PACKAGE_VERSION = "0.2.23";
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
+ const MP4_BOX_HEADER_SIZE = 8;
33
+ const MP4_EXTENDED_BOX_HEADER_SIZE = 16;
34
+ const MP4_MAX_BOX_SCAN_DEPTH = 16;
35
+ const MP4_FULL_BOX_HEADER_SIZE = 4;
36
+ const MP4_STSD_ENTRY_COUNT_SIZE = 4;
37
+ const MP4_VISUAL_SAMPLE_ENTRY_HEADER_SIZE = 78;
38
+ const MP4_VVC_NAL_LENGTH_SIZE_BYTE_OFFSET = 4;
39
+ const MP4_CONTAINER_BOX_TYPES = new Set([
40
+ "dinf",
41
+ "edts",
42
+ "mdia",
43
+ "meta",
44
+ "minf",
45
+ "moof",
46
+ "moov",
47
+ "mfra",
48
+ "stbl",
49
+ "traf",
50
+ "trak",
51
+ "udta",
52
+ ]);
53
+ const MP4_VISUAL_SAMPLE_ENTRY_TYPES = new Set([
54
+ "av01",
55
+ "avc1",
56
+ "avc3",
57
+ "encv",
58
+ "hev1",
59
+ "hvc1",
60
+ "vvc1",
61
+ "vvi1",
62
+ ]);
63
+ // Keep this local helper scoped to WeChat media host trees; require k for the decrypt key.
64
+ const WECHAT_MEDIA_DOWNLOAD_HOST_SUFFIXES = [
65
+ ".wxapp.tc.qq.com",
66
+ ".video.qq.com",
67
+ ];
68
+ const WECHAT_MEDIA_DOWNLOAD_EXACT_HOSTS = new Set(["wxapp.tc.qq.com"]);
69
+ const WECHAT_MEDIA_DECODE_KEY_MAX = (1n << 64n) - 1n;
70
+ const WECHAT_DETAIL_MEDIA_URL_ERROR =
71
+ "Media URL cannot be decrypted. Use the video.video_url returned by SocialDataX WeChat detail.";
72
+ const WECHAT_MEDIA_RESPONSE_DECRYPT_ERROR =
73
+ "Downloaded media cannot be decrypted. Use the video.video_url returned by SocialDataX WeChat detail.";
15
74
  const PRIMARY_API_KEY_ENV = "SOCIALDATAX_API_KEY";
16
75
  const LEGACY_API_KEY_ENV = "SOCIAL_MEDIA_MCP_API_KEY";
17
76
  const API_KEY_ENV_NAMES = [PRIMARY_API_KEY_ENV, LEGACY_API_KEY_ENV];
@@ -254,6 +313,7 @@ const DOUYIN_OPTION_DISPLAY_NAMES = {
254
313
  jobId: "--job-id",
255
314
  };
256
315
  const KUAISHOU_DIRECT_ACTION_OPTIONS = {
316
+ "hot-search": ["pretty"],
257
317
  search: ["keyword", "pageToken", "pages", "all", "maxItems", "sinceDays", "pretty"],
258
318
  "user-search": ["keyword", "pageToken", "pages", "maxItems", "pretty"],
259
319
  detail: ["photoId", "url", "pretty"],
@@ -410,7 +470,9 @@ const WECHAT_DIRECT_ACTION_OPTIONS = {
410
470
  "pretty",
411
471
  ],
412
472
  transcript: ["url", "encryptedObjectId", "jobId", "pretty"],
473
+ "decrypt-media": ["mediaUrl", "output", "pretty"],
413
474
  };
475
+ const WECHAT_LOCAL_DIRECT_ACTIONS = new Set(["decrypt-media"]);
414
476
  const WECHAT_DIRECT_ACTION_NAMES = Object.keys(WECHAT_DIRECT_ACTION_OPTIONS).join(", ");
415
477
  const WECHAT_SEARCH_SORT_TYPES = [
416
478
  "all",
@@ -440,6 +502,8 @@ const WECHAT_OPTION_DISPLAY_NAMES = {
440
502
  commentId: "--comment-id",
441
503
  userId: "--user-id",
442
504
  jobId: "--job-id",
505
+ mediaUrl: "--media-url",
506
+ output: "--output",
443
507
  };
444
508
  const SENSITIVE_CHECK_DIRECT_ACTION_OPTIONS = {
445
509
  text: ["text", "platform", "pretty"],
@@ -633,6 +697,10 @@ const PLATFORMS = {
633
697
  "KUAISHOU_MCP_UPSTREAM_URL",
634
698
  ],
635
699
  tools: [
700
+ {
701
+ name: "kuaishou_get_hot_search_list",
702
+ description: "Fetch the current Kuaishou / 快手 hot-search list.",
703
+ },
636
704
  {
637
705
  name: "kuaishou_search_videos",
638
706
  description:
@@ -904,7 +972,7 @@ function isMainModule() {
904
972
  }
905
973
  }
906
974
 
907
- if (isMainModule()) {
975
+ async function main() {
908
976
  try {
909
977
  if (command === "install") {
910
978
  await installSkills(cliArgs.slice(1));
@@ -1092,9 +1160,14 @@ function validateWechatDirectActionOptions(action, options) {
1092
1160
  return;
1093
1161
  }
1094
1162
 
1095
- validateKnownOptions(options, allowedDirectOptions(allowedOptions));
1096
- validateDirectMetaOptions(options);
1097
- validateDirectPaginationOptions("wechat", action, options);
1163
+ if (WECHAT_LOCAL_DIRECT_ACTIONS.has(action)) {
1164
+ validateKnownOptions(options, allowedOptions);
1165
+ validateFlagOption(options, "pretty", "--pretty");
1166
+ } else {
1167
+ validateKnownOptions(options, allowedDirectOptions(allowedOptions));
1168
+ validateDirectMetaOptions(options);
1169
+ validateDirectPaginationOptions("wechat", action, options);
1170
+ }
1098
1171
  for (const key of allowedOptions) {
1099
1172
  if (!DIRECT_BOOLEAN_OPTIONS.has(key)) {
1100
1173
  requireOptionValue(options, key, WECHAT_OPTION_DISPLAY_NAMES[key]);
@@ -1786,6 +1859,9 @@ function printHelp() {
1786
1859
  console.log(` npx -y ${PACKAGE_SPEC} douyin transcript --aweme-id "<aweme_id>" --pretty`);
1787
1860
  console.log(" Submit or check a Douyin video speech-to-text transcript job.");
1788
1861
  console.log("");
1862
+ console.log(` npx -y ${PACKAGE_SPEC} kuaishou hot-search --pretty`);
1863
+ console.log(" Call the Kuaishou hot-search list tool directly and print JSON.");
1864
+ console.log("");
1789
1865
  console.log(` npx -y ${PACKAGE_SPEC} kuaishou search --keyword "露营" --pretty`);
1790
1866
  console.log(" Call the Kuaishou work search tool directly and print JSON.");
1791
1867
  console.log("");
@@ -1883,6 +1959,9 @@ function printHelp() {
1883
1959
  console.log(` npx -y ${PACKAGE_SPEC} wechat detail --url "<wechat_video_url_or_share_text>" --pretty`);
1884
1960
  console.log(" Call the WeChat Channels / 视频号 video detail tool from a video link or share text.");
1885
1961
  console.log("");
1962
+ console.log(` npx -y ${PACKAGE_SPEC} wechat decrypt-media --media-url "<video.video_url>" --output video.mp4`);
1963
+ console.log(" Save the WeChat Channels / 视频号 media URL returned by detail and locally decrypt when needed.");
1964
+ console.log("");
1886
1965
  console.log(` npx -y ${PACKAGE_SPEC} wechat article --url "<mp_article_url_or_share_text>" --pretty`);
1887
1966
  console.log(" Call the WeChat Official Account / 微信公众号 article detail tool from an article link or share text.");
1888
1967
  console.log("");
@@ -1980,6 +2059,10 @@ function printHelp() {
1980
2059
  console.log(" --url <url-or-share-text>");
1981
2060
  console.log(" Content link, short link, or share text for URL-based detail/comment/article commands.");
1982
2061
  console.log(" For Douyin detail/comments, pass a content page link, not video.play_url.");
2062
+ console.log(" --media-url <video.video_url>");
2063
+ console.log(" WeChat detail result media URL for local decrypt-media download.");
2064
+ console.log(" --output <file>");
2065
+ console.log(" Output file path for local decrypt-media download.");
1983
2066
  console.log(" --note-id <note_id>");
1984
2067
  console.log(" --aweme-id <aweme_id>");
1985
2068
  console.log(" --photo-id <photo_id>");
@@ -2215,6 +2298,13 @@ async function runWechatDirectCommand(args) {
2215
2298
  }
2216
2299
  validateWechatDirectActionOptions(action, options);
2217
2300
 
2301
+ if (action === "decrypt-media") {
2302
+ const data = await decryptWechatMediaCommand(options);
2303
+ process.stdout.write(JSON.stringify(data, null, options.pretty ? 2 : 0));
2304
+ process.stdout.write("\n");
2305
+ return;
2306
+ }
2307
+
2218
2308
  const operation = attachDirectMetadata(buildWechatOperation(action, options), options);
2219
2309
  const data = shouldUsePaginatedDirectOutput(options)
2220
2310
  ? await callPaginatedDirectOperation(operation, options)
@@ -2486,6 +2576,15 @@ function buildDouyinOperation(action, options) {
2486
2576
 
2487
2577
  function buildKuaishouOperation(action, options) {
2488
2578
  switch (action) {
2579
+ case "hot-search":
2580
+ return buildDirectOperation(
2581
+ "hot-search",
2582
+ {
2583
+ tool: "kuaishou_get_hot_search_list",
2584
+ toolArguments: {},
2585
+ },
2586
+ PLATFORMS.kuaishou
2587
+ );
2489
2588
  case "search":
2490
2589
  return buildDirectOperation(
2491
2590
  "search",
@@ -2819,6 +2918,677 @@ function buildWechatOperation(action, options) {
2819
2918
  }
2820
2919
  }
2821
2920
 
2921
+ export async function decryptWechatMediaCommand(
2922
+ options,
2923
+ {
2924
+ fetchMedia = fetch,
2925
+ downloadTimeoutMs = MEDIA_DOWNLOAD_TIMEOUT_MS,
2926
+ maxDownloadBytes = MEDIA_DOWNLOAD_MAX_BYTES,
2927
+ } = {}
2928
+ ) {
2929
+ if (!options.mediaUrl) {
2930
+ throw new Error("Missing --media-url for wechat decrypt-media.");
2931
+ }
2932
+ if (!options.output) {
2933
+ throw new Error("Missing --output for wechat decrypt-media.");
2934
+ }
2935
+
2936
+ const { mediaUrl, decodeKey } = parseWechatDecryptMediaUrl(options.mediaUrl);
2937
+ const outputPath = resolve(options.output);
2938
+ const tempPath = `${outputPath}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
2939
+ await assertWechatMediaOutputPath(outputPath);
2940
+ await mkdir(dirname(outputPath), { recursive: true });
2941
+
2942
+ let encryptedLength;
2943
+ let decrypted = false;
2944
+ let bytesWritten = 0;
2945
+ try {
2946
+ const signal = createMediaDownloadAbortSignal(downloadTimeoutMs);
2947
+ const response = await fetchWechatMedia(mediaUrl, {
2948
+ fetchMedia,
2949
+ signal,
2950
+ });
2951
+ if (!response.ok) {
2952
+ throw new Error(`Media download failed with HTTP ${response.status}.`);
2953
+ }
2954
+ assertMediaDownloadWithinLimit(
2955
+ parseOptionalIntegerHeader(response.headers.get("content-length")),
2956
+ maxDownloadBytes
2957
+ );
2958
+ encryptedLength = parseOptionalWechatEncryptedLengthHeader(
2959
+ response.headers.get("x-enclen")
2960
+ );
2961
+ assertMediaDownloadWithinLimit(encryptedLength, maxDownloadBytes);
2962
+ bytesWritten = await writeResponseBodyToFile(response, tempPath, {
2963
+ maxBytes: maxDownloadBytes,
2964
+ });
2965
+ if (encryptedLength !== undefined) {
2966
+ await decryptWechatMediaFilePrefix(tempPath, {
2967
+ encryptedLength,
2968
+ decodeKey,
2969
+ });
2970
+ await patchWechatVvcMp4NalLengthMetadata(tempPath);
2971
+ decrypted = true;
2972
+ }
2973
+ await rename(tempPath, outputPath);
2974
+ } catch (error) {
2975
+ await unlink(tempPath).catch(() => {});
2976
+ if (isAbortLikeError(error)) {
2977
+ throw new Error("Media download timed out.");
2978
+ }
2979
+ throw error;
2980
+ }
2981
+
2982
+ return {
2983
+ platform: "wechat",
2984
+ action: "decrypt-media",
2985
+ output_path: outputPath,
2986
+ decrypted,
2987
+ bytes_written: bytesWritten,
2988
+ };
2989
+ }
2990
+
2991
+ async function assertWechatMediaOutputPath(outputPath) {
2992
+ const existingOutput = await stat(outputPath).catch((error) => {
2993
+ if (error?.code === "ENOENT") {
2994
+ return undefined;
2995
+ }
2996
+ throw error;
2997
+ });
2998
+ if (existingOutput?.isDirectory()) {
2999
+ throw new Error("--output must be a file path for wechat decrypt-media.");
3000
+ }
3001
+ }
3002
+
3003
+ async function fetchWechatMedia(
3004
+ mediaUrl,
3005
+ {
3006
+ fetchMedia,
3007
+ signal,
3008
+ maxRedirects = MEDIA_DOWNLOAD_MAX_REDIRECTS,
3009
+ }
3010
+ ) {
3011
+ let currentUrl = mediaUrl;
3012
+ for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
3013
+ const response = await fetchMedia(currentUrl, {
3014
+ headers: {
3015
+ Referer: WECHAT_MEDIA_DOWNLOAD_REFERER,
3016
+ "User-Agent": MEDIA_DOWNLOAD_USER_AGENT,
3017
+ },
3018
+ redirect: "manual",
3019
+ ...(signal ? { signal } : {}),
3020
+ });
3021
+ if (!isHttpRedirectStatus(response.status)) {
3022
+ return response;
3023
+ }
3024
+ currentUrl = resolveWechatMediaRedirectUrl(
3025
+ currentUrl,
3026
+ response.headers?.get("location")
3027
+ );
3028
+ }
3029
+ throw new Error("Media download redirected too many times.");
3030
+ }
3031
+
3032
+ function isHttpRedirectStatus(status) {
3033
+ return [301, 302, 303, 307, 308].includes(status);
3034
+ }
3035
+
3036
+ function resolveWechatMediaRedirectUrl(currentUrl, location) {
3037
+ if (
3038
+ location === undefined ||
3039
+ location === null ||
3040
+ String(location).trim() === ""
3041
+ ) {
3042
+ throw new Error("Media download redirect is invalid.");
3043
+ }
3044
+ let parsed;
3045
+ try {
3046
+ parsed = new URL(String(location), currentUrl);
3047
+ } catch {
3048
+ throw new Error("Media download redirect is invalid.");
3049
+ }
3050
+ if (!isWechatDetailMediaDownloadUrl(parsed)) {
3051
+ throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
3052
+ }
3053
+ return parsed.href;
3054
+ }
3055
+
3056
+ function createMediaDownloadAbortSignal(timeoutMs) {
3057
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
3058
+ return undefined;
3059
+ }
3060
+ if (
3061
+ typeof AbortSignal !== "undefined" &&
3062
+ typeof AbortSignal.timeout === "function"
3063
+ ) {
3064
+ return AbortSignal.timeout(timeoutMs);
3065
+ }
3066
+ const controller = new AbortController();
3067
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3068
+ timer.unref?.();
3069
+ return controller.signal;
3070
+ }
3071
+
3072
+ function parseWechatDecryptMediaUrl(mediaUrl) {
3073
+ let parsed;
3074
+ try {
3075
+ parsed = new URL(mediaUrl);
3076
+ } catch {
3077
+ throw new Error("Invalid --media-url.");
3078
+ }
3079
+ if (!["http:", "https:"].includes(parsed.protocol)) {
3080
+ throw new Error("Invalid --media-url.");
3081
+ }
3082
+ if (!isWechatDetailMediaDownloadUrl(parsed)) {
3083
+ throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
3084
+ }
3085
+
3086
+ const decodeKeyText = parsed.searchParams.get("k");
3087
+ if (!decodeKeyText) {
3088
+ throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
3089
+ }
3090
+ if (!/^\d+$/.test(decodeKeyText)) {
3091
+ throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
3092
+ }
3093
+ if (decodeKeyText.length > 20) {
3094
+ throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
3095
+ }
3096
+ const decodeKey = BigInt(decodeKeyText);
3097
+ if (decodeKey > WECHAT_MEDIA_DECODE_KEY_MAX) {
3098
+ throw new Error(WECHAT_DETAIL_MEDIA_URL_ERROR);
3099
+ }
3100
+ return {
3101
+ mediaUrl,
3102
+ decodeKey,
3103
+ };
3104
+ }
3105
+
3106
+ function isWechatDetailMediaDownloadUrl(parsed) {
3107
+ return (
3108
+ parsed.username === "" &&
3109
+ parsed.password === "" &&
3110
+ parsed.port === "" &&
3111
+ isAllowedWechatMediaDownloadHost(parsed.hostname)
3112
+ );
3113
+ }
3114
+
3115
+ function isAllowedWechatMediaDownloadHost(hostname) {
3116
+ return (
3117
+ WECHAT_MEDIA_DOWNLOAD_EXACT_HOSTS.has(hostname) ||
3118
+ WECHAT_MEDIA_DOWNLOAD_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix))
3119
+ );
3120
+ }
3121
+
3122
+ function parseOptionalWechatEncryptedLengthHeader(value) {
3123
+ if (value === undefined || value === null || String(value).trim() === "") {
3124
+ return undefined;
3125
+ }
3126
+ if (!/^\d+$/.test(String(value).trim())) {
3127
+ throw new Error(WECHAT_MEDIA_RESPONSE_DECRYPT_ERROR);
3128
+ }
3129
+ const encryptedLength = Number.parseInt(String(value).trim(), 10);
3130
+ if (!Number.isSafeInteger(encryptedLength)) {
3131
+ throw new Error(WECHAT_MEDIA_RESPONSE_DECRYPT_ERROR);
3132
+ }
3133
+ return encryptedLength > 0 ? encryptedLength : undefined;
3134
+ }
3135
+
3136
+ function parseOptionalIntegerHeader(value) {
3137
+ if (value === null || String(value).trim() === "") {
3138
+ return undefined;
3139
+ }
3140
+ if (!/^\d+$/.test(String(value).trim())) {
3141
+ return undefined;
3142
+ }
3143
+ const parsed = Number.parseInt(String(value).trim(), 10);
3144
+ return Number.isSafeInteger(parsed) ? parsed : Number.POSITIVE_INFINITY;
3145
+ }
3146
+
3147
+ function assertMediaDownloadWithinLimit(bytes, maxBytes) {
3148
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0 || bytes === undefined) {
3149
+ return;
3150
+ }
3151
+ if (bytes > maxBytes) {
3152
+ throw new Error("Media download is too large for local decrypt-media.");
3153
+ }
3154
+ }
3155
+
3156
+ async function writeResponseBodyToFile(response, filePath, { maxBytes } = {}) {
3157
+ if (!response.body) {
3158
+ throw new Error("Media download response has no body.");
3159
+ }
3160
+ const file = await openFile(filePath, "w");
3161
+ let bytesWritten = 0;
3162
+ try {
3163
+ for await (const chunk of response.body) {
3164
+ const buffer = Buffer.from(chunk);
3165
+ if (buffer.length === 0) {
3166
+ continue;
3167
+ }
3168
+ assertMediaDownloadWithinLimit(
3169
+ bytesWritten + buffer.length,
3170
+ maxBytes
3171
+ );
3172
+ await writeAllToFile(file, buffer);
3173
+ bytesWritten += buffer.length;
3174
+ }
3175
+ } finally {
3176
+ await file.close();
3177
+ }
3178
+ return bytesWritten;
3179
+ }
3180
+
3181
+ async function decryptWechatMediaFilePrefix(
3182
+ filePath,
3183
+ {
3184
+ encryptedLength,
3185
+ decodeKey,
3186
+ }
3187
+ ) {
3188
+ const file = await openFile(filePath, "r+");
3189
+ try {
3190
+ const stats = await file.stat();
3191
+ if (encryptedLength > stats.size) {
3192
+ throw new Error("Downloaded media is incomplete and cannot be decrypted.");
3193
+ }
3194
+ const decryptStream = createWechatMediaDecryptStream(decodeKey);
3195
+ let offset = 0;
3196
+ while (offset < encryptedLength) {
3197
+ const chunkLength = Math.min(
3198
+ MEDIA_DECRYPT_CHUNK_SIZE,
3199
+ encryptedLength - offset
3200
+ );
3201
+ const encryptedChunk = Buffer.allocUnsafe(chunkLength);
3202
+ const { bytesRead } = await file.read(
3203
+ encryptedChunk,
3204
+ 0,
3205
+ chunkLength,
3206
+ offset
3207
+ );
3208
+ if (bytesRead !== chunkLength) {
3209
+ throw new Error("Downloaded media is incomplete and cannot be decrypted.");
3210
+ }
3211
+ decryptWechatMediaBufferChunk(encryptedChunk, decryptStream);
3212
+ await writeAllToFile(file, encryptedChunk, offset);
3213
+ offset += chunkLength;
3214
+ }
3215
+ } finally {
3216
+ await file.close();
3217
+ }
3218
+ }
3219
+
3220
+ async function patchWechatVvcMp4NalLengthMetadata(filePath) {
3221
+ const file = await openFile(filePath, "r+");
3222
+ try {
3223
+ const stats = await file.stat();
3224
+ await patchMp4ChildBoxes(file, 0, stats.size, 0);
3225
+ } finally {
3226
+ await file.close();
3227
+ }
3228
+ }
3229
+
3230
+ async function patchMp4ChildBoxes(file, startOffset, endOffset, depth) {
3231
+ if (
3232
+ depth > MP4_MAX_BOX_SCAN_DEPTH ||
3233
+ startOffset < 0 ||
3234
+ endOffset - startOffset < MP4_BOX_HEADER_SIZE
3235
+ ) {
3236
+ return;
3237
+ }
3238
+
3239
+ let offset = startOffset;
3240
+ while (offset + MP4_BOX_HEADER_SIZE <= endOffset) {
3241
+ const box = await readMp4BoxHeader(file, offset, endOffset);
3242
+ if (!box) {
3243
+ return;
3244
+ }
3245
+ if (box.type === "vvcC") {
3246
+ await patchVvcConfigurationNalLengthSize(file, box);
3247
+ } else {
3248
+ const childRange = mp4ChildBoxRange(box);
3249
+ if (childRange) {
3250
+ await patchMp4ChildBoxes(
3251
+ file,
3252
+ childRange.startOffset,
3253
+ childRange.endOffset,
3254
+ depth + 1
3255
+ );
3256
+ }
3257
+ }
3258
+ offset = box.endOffset;
3259
+ }
3260
+ }
3261
+
3262
+ async function readMp4BoxHeader(file, offset, parentEndOffset) {
3263
+ const baseHeader = Buffer.allocUnsafe(MP4_BOX_HEADER_SIZE);
3264
+ const baseRead = await file.read(
3265
+ baseHeader,
3266
+ 0,
3267
+ MP4_BOX_HEADER_SIZE,
3268
+ offset
3269
+ );
3270
+ if (baseRead.bytesRead !== MP4_BOX_HEADER_SIZE) {
3271
+ return undefined;
3272
+ }
3273
+
3274
+ const size32 = baseHeader.readUInt32BE(0);
3275
+ const type = baseHeader.toString("ascii", 4, 8);
3276
+ let headerSize = MP4_BOX_HEADER_SIZE;
3277
+ let size;
3278
+ if (size32 === 0) {
3279
+ size = parentEndOffset - offset;
3280
+ } else if (size32 === 1) {
3281
+ if (offset + MP4_EXTENDED_BOX_HEADER_SIZE > parentEndOffset) {
3282
+ return undefined;
3283
+ }
3284
+ const extendedSizeBuffer = Buffer.allocUnsafe(8);
3285
+ const extendedRead = await file.read(
3286
+ extendedSizeBuffer,
3287
+ 0,
3288
+ 8,
3289
+ offset + MP4_BOX_HEADER_SIZE
3290
+ );
3291
+ if (extendedRead.bytesRead !== 8) {
3292
+ return undefined;
3293
+ }
3294
+ const extendedSize = extendedSizeBuffer.readBigUInt64BE(0);
3295
+ if (extendedSize > BigInt(Number.MAX_SAFE_INTEGER)) {
3296
+ return undefined;
3297
+ }
3298
+ size = Number(extendedSize);
3299
+ headerSize = MP4_EXTENDED_BOX_HEADER_SIZE;
3300
+ } else {
3301
+ size = size32;
3302
+ }
3303
+
3304
+ if (
3305
+ size < headerSize ||
3306
+ offset + size > parentEndOffset ||
3307
+ !isMp4BoxType(type)
3308
+ ) {
3309
+ return undefined;
3310
+ }
3311
+
3312
+ return {
3313
+ type,
3314
+ startOffset: offset,
3315
+ headerSize,
3316
+ payloadOffset: offset + headerSize,
3317
+ endOffset: offset + size,
3318
+ };
3319
+ }
3320
+
3321
+ function isMp4BoxType(type) {
3322
+ return /^[\x20-\x7e]{4}$/.test(type);
3323
+ }
3324
+
3325
+ function mp4ChildBoxRange(box) {
3326
+ if (box.type === "stsd") {
3327
+ const childStart =
3328
+ box.payloadOffset + MP4_FULL_BOX_HEADER_SIZE + MP4_STSD_ENTRY_COUNT_SIZE;
3329
+ return childStart < box.endOffset
3330
+ ? { startOffset: childStart, endOffset: box.endOffset }
3331
+ : undefined;
3332
+ }
3333
+ if (box.type === "meta") {
3334
+ const childStart = box.payloadOffset + MP4_FULL_BOX_HEADER_SIZE;
3335
+ return childStart < box.endOffset
3336
+ ? { startOffset: childStart, endOffset: box.endOffset }
3337
+ : undefined;
3338
+ }
3339
+ if (MP4_VISUAL_SAMPLE_ENTRY_TYPES.has(box.type)) {
3340
+ const childStart = box.payloadOffset + MP4_VISUAL_SAMPLE_ENTRY_HEADER_SIZE;
3341
+ return childStart < box.endOffset
3342
+ ? { startOffset: childStart, endOffset: box.endOffset }
3343
+ : undefined;
3344
+ }
3345
+ if (MP4_CONTAINER_BOX_TYPES.has(box.type)) {
3346
+ return box.payloadOffset < box.endOffset
3347
+ ? { startOffset: box.payloadOffset, endOffset: box.endOffset }
3348
+ : undefined;
3349
+ }
3350
+ return undefined;
3351
+ }
3352
+
3353
+ async function patchVvcConfigurationNalLengthSize(file, box) {
3354
+ const targetOffset =
3355
+ box.payloadOffset + MP4_VVC_NAL_LENGTH_SIZE_BYTE_OFFSET;
3356
+ if (targetOffset >= box.endOffset) {
3357
+ return;
3358
+ }
3359
+ const buffer = Buffer.allocUnsafe(1);
3360
+ const { bytesRead } = await file.read(buffer, 0, 1, targetOffset);
3361
+ if (bytesRead !== 1) {
3362
+ return;
3363
+ }
3364
+ const patched = (buffer[0] & 0xf8) | 0x07;
3365
+ if (patched === buffer[0]) {
3366
+ return;
3367
+ }
3368
+ buffer[0] = patched;
3369
+ await writeAllToFile(file, buffer, targetOffset);
3370
+ }
3371
+
3372
+ function isAbortLikeError(error) {
3373
+ return error?.name === "AbortError" || error?.name === "TimeoutError";
3374
+ }
3375
+
3376
+ async function writeAllToFile(file, buffer, position) {
3377
+ let offset = 0;
3378
+ while (offset < buffer.length) {
3379
+ const writePosition = position === undefined ? undefined : position + offset;
3380
+ const { bytesWritten } = await file.write(
3381
+ buffer,
3382
+ offset,
3383
+ buffer.length - offset,
3384
+ writePosition
3385
+ );
3386
+ if (bytesWritten <= 0) {
3387
+ throw new Error("Downloaded media could not be written.");
3388
+ }
3389
+ offset += bytesWritten;
3390
+ }
3391
+ }
3392
+
3393
+ function createWechatMediaDecryptStream(decodeKey) {
3394
+ return {
3395
+ context: new WechatFinderIsaac64Context(decodeKey),
3396
+ block: Buffer.allocUnsafe(8),
3397
+ index: 8,
3398
+ };
3399
+ }
3400
+
3401
+ function decryptWechatMediaBufferChunk(buffer, stream) {
3402
+ for (let index = 0; index < buffer.length; index += 1) {
3403
+ if (stream.index === 8) {
3404
+ stream.block.writeBigUInt64BE(stream.context.next());
3405
+ stream.index = 0;
3406
+ }
3407
+ buffer[index] ^= stream.block[stream.index];
3408
+ stream.index += 1;
3409
+ }
3410
+ }
3411
+
3412
+ class WechatFinderIsaac64Context {
3413
+ static MASK = (1n << 64n) - 1n;
3414
+ static RANDSIZ = 1 << 8;
3415
+
3416
+ constructor(decodeKey) {
3417
+ this.seed = Array(WechatFinderIsaac64Context.RANDSIZ).fill(0n);
3418
+ this.mm = Array(WechatFinderIsaac64Context.RANDSIZ).fill(0n);
3419
+ this.aa = 0n;
3420
+ this.bb = 0n;
3421
+ this.cc = 0n;
3422
+ this.randcnt = 0;
3423
+ this.seed[0] = this.u64(decodeKey);
3424
+ this.init(true);
3425
+ }
3426
+
3427
+ next() {
3428
+ if (this.randcnt === 0) {
3429
+ this.isaac64();
3430
+ this.randcnt = WechatFinderIsaac64Context.RANDSIZ;
3431
+ }
3432
+ this.randcnt -= 1;
3433
+ return this.seed[this.randcnt];
3434
+ }
3435
+
3436
+ init(seeded) {
3437
+ let a = 0x647c4677a2884b7cn;
3438
+ let b = 0xb9f8b322c73ac862n;
3439
+ let c = 0x8c0ea5053d4712a0n;
3440
+ let d = 0xb29b2e824a595524n;
3441
+ let e = 0x82f053db8355e0cen;
3442
+ let f = 0x48fe4a0fa5a09315n;
3443
+ let g = 0xae985bf2cbfc89edn;
3444
+ let h = 0x98f5704f6c44c0abn;
3445
+
3446
+ for (let index = 0; index < WechatFinderIsaac64Context.RANDSIZ; index += 8) {
3447
+ if (seeded) {
3448
+ a = this.u64(a + this.seed[index]);
3449
+ b = this.u64(b + this.seed[index + 1]);
3450
+ c = this.u64(c + this.seed[index + 2]);
3451
+ d = this.u64(d + this.seed[index + 3]);
3452
+ e = this.u64(e + this.seed[index + 4]);
3453
+ f = this.u64(f + this.seed[index + 5]);
3454
+ g = this.u64(g + this.seed[index + 6]);
3455
+ h = this.u64(h + this.seed[index + 7]);
3456
+ }
3457
+ [a, b, c, d, e, f, g, h] = this.mix64(a, b, c, d, e, f, g, h);
3458
+ this.mm.splice(index, 8, a, b, c, d, e, f, g, h);
3459
+ }
3460
+
3461
+ if (seeded) {
3462
+ for (let index = 0; index < WechatFinderIsaac64Context.RANDSIZ; index += 8) {
3463
+ a = this.u64(a + this.mm[index]);
3464
+ b = this.u64(b + this.mm[index + 1]);
3465
+ c = this.u64(c + this.mm[index + 2]);
3466
+ d = this.u64(d + this.mm[index + 3]);
3467
+ e = this.u64(e + this.mm[index + 4]);
3468
+ f = this.u64(f + this.mm[index + 5]);
3469
+ g = this.u64(g + this.mm[index + 6]);
3470
+ h = this.u64(h + this.mm[index + 7]);
3471
+ [a, b, c, d, e, f, g, h] = this.mix64(a, b, c, d, e, f, g, h);
3472
+ this.mm.splice(index, 8, a, b, c, d, e, f, g, h);
3473
+ }
3474
+ }
3475
+
3476
+ this.isaac64();
3477
+ this.randcnt = WechatFinderIsaac64Context.RANDSIZ;
3478
+ }
3479
+
3480
+ isaac64() {
3481
+ let a = this.aa;
3482
+ let b = this.u64(this.bb + this.cc + 1n);
3483
+ this.cc = this.u64(this.cc + 1n);
3484
+
3485
+ let resultIndex = 0;
3486
+ for (let index = 0; index < WechatFinderIsaac64Context.RANDSIZ / 2; index += 4) {
3487
+ [a, b] = this.rngstep64(
3488
+ this.u64(~(a ^ this.u64(a << 21n))),
3489
+ a,
3490
+ b,
3491
+ index,
3492
+ index + 128,
3493
+ resultIndex
3494
+ );
3495
+ resultIndex += 1;
3496
+ [a, b] = this.rngstep64(a ^ (a >> 5n), a, b, index + 1, index + 129, resultIndex);
3497
+ resultIndex += 1;
3498
+ [a, b] = this.rngstep64(
3499
+ a ^ this.u64(a << 12n),
3500
+ a,
3501
+ b,
3502
+ index + 2,
3503
+ index + 130,
3504
+ resultIndex
3505
+ );
3506
+ resultIndex += 1;
3507
+ [a, b] = this.rngstep64(a ^ (a >> 33n), a, b, index + 3, index + 131, resultIndex);
3508
+ resultIndex += 1;
3509
+ }
3510
+
3511
+ for (
3512
+ let index = WechatFinderIsaac64Context.RANDSIZ / 2;
3513
+ index < WechatFinderIsaac64Context.RANDSIZ;
3514
+ index += 4
3515
+ ) {
3516
+ const m2Index = index - WechatFinderIsaac64Context.RANDSIZ / 2;
3517
+ [a, b] = this.rngstep64(
3518
+ this.u64(~(a ^ this.u64(a << 21n))),
3519
+ a,
3520
+ b,
3521
+ index,
3522
+ m2Index,
3523
+ resultIndex
3524
+ );
3525
+ resultIndex += 1;
3526
+ [a, b] = this.rngstep64(a ^ (a >> 5n), a, b, index + 1, m2Index + 1, resultIndex);
3527
+ resultIndex += 1;
3528
+ [a, b] = this.rngstep64(
3529
+ a ^ this.u64(a << 12n),
3530
+ a,
3531
+ b,
3532
+ index + 2,
3533
+ m2Index + 2,
3534
+ resultIndex
3535
+ );
3536
+ resultIndex += 1;
3537
+ [a, b] = this.rngstep64(a ^ (a >> 33n), a, b, index + 3, m2Index + 3, resultIndex);
3538
+ resultIndex += 1;
3539
+ }
3540
+
3541
+ this.bb = b;
3542
+ this.aa = a;
3543
+ }
3544
+
3545
+ rngstep64(mix, a, b, mIndex, m2Index, resultIndex) {
3546
+ const x = this.mm[mIndex];
3547
+ const nextA = this.u64(mix + this.mm[m2Index]);
3548
+ const y = this.u64(this.ind64(x) + nextA + b);
3549
+ this.mm[mIndex] = y;
3550
+ const nextB = this.u64(this.ind64(y >> 8n) + x);
3551
+ this.seed[resultIndex] = nextB;
3552
+ return [nextA, nextB];
3553
+ }
3554
+
3555
+ ind64(value) {
3556
+ return this.mm[Number((value >> 3n) & BigInt(WechatFinderIsaac64Context.RANDSIZ - 1))];
3557
+ }
3558
+
3559
+ mix64(a, b, c, d, e, f, g, h) {
3560
+ a = this.u64(a - e);
3561
+ f ^= h >> 9n;
3562
+ h = this.u64(h + a);
3563
+ b = this.u64(b - f);
3564
+ g ^= this.u64(a << 9n);
3565
+ a = this.u64(a + b);
3566
+ c = this.u64(c - g);
3567
+ h ^= b >> 23n;
3568
+ b = this.u64(b + c);
3569
+ d = this.u64(d - h);
3570
+ a ^= this.u64(c << 15n);
3571
+ c = this.u64(c + d);
3572
+ e = this.u64(e - a);
3573
+ b ^= d >> 14n;
3574
+ d = this.u64(d + e);
3575
+ f = this.u64(f - b);
3576
+ c ^= this.u64(e << 20n);
3577
+ e = this.u64(e + f);
3578
+ g = this.u64(g - c);
3579
+ d ^= f >> 17n;
3580
+ f = this.u64(f + g);
3581
+ h = this.u64(h - d);
3582
+ e ^= this.u64(g << 14n);
3583
+ g = this.u64(g + h);
3584
+ return [a, b, c, d, e, f, g, h].map((value) => this.u64(value));
3585
+ }
3586
+
3587
+ u64(value) {
3588
+ return value & WechatFinderIsaac64Context.MASK;
3589
+ }
3590
+ }
3591
+
2822
3592
  function buildSensitiveCheckOperation(action, options) {
2823
3593
  switch (action) {
2824
3594
  case "text":
@@ -4014,3 +4784,7 @@ function extractTextContent(content) {
4014
4784
  .join("\n")
4015
4785
  .trim();
4016
4786
  }
4787
+
4788
+ if (isMainModule()) {
4789
+ await main();
4790
+ }