import { createHash } from 'crypto'; import fs from 'node:fs'; import { DOMParser } from 'xmldom'; // Helper functions function randomStr(length: number): string { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; return Array.from({ length }, () => alphabet.charAt(Math.floor(Math.random() * alphabet.length))).join(''); } function getA1AndWebId(): [string, string] { const d = Math.floor(Date.now()).toString(16) + randomStr(30) + '5' + '0' + '000'; const crc32 = (str: string): number => { let crc = 0 ^ (-1); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); crc = (crc >>> 8) ^ crc32Table[(crc ^ char) & 0xFF]; } return (crc ^ (-1)) >>> 0; }; const g = (d + crc32(d).toString()).slice(0, 52); const webId = Array.from(new Uint8Array(createHash('md5').update(g).digest())) .map(b => b.toString(16).padStart(2, '0')).join(''); return [g, webId]; } // CRC32 table const crc32Table = Array.from({ length: 256 }, (_, i) => { let crc = i; for (let j = 0; j < 8; j++) { crc = crc & 1 ? 0xEDB88320 ^ (crc >>> 1) : crc >>> 1; } return crc; }); // Image and video CDNs const imgCdns = [ "https://sns-img-qc.xhscdn.com", "https://sns-img-hw.xhscdn.com", "https://sns-img-bd.xhscdn.com", "https://sns-img-qn.xhscdn.com", ]; const videoCdns = [ "https://sns-video-qc.xhscdn.com", "https://sns-video-hw.xhscdn.com", "https://sns-video-bd.xhscdn.com", "https://sns-video-qn.xhscdn.com", ]; // URL handling functions function getImgUrlByTraceId(traceId: string, format: string = "png"): string { return `${imgCdns[Math.floor(Math.random() * imgCdns.length)]}/${traceId}?imageView2/format/${format}`; } function getImgUrlsByTraceId(traceId: string, format: string = "png"): string[] { return imgCdns.map(cdn => `${cdn}/${traceId}?imageView2/format/${format}`); } function getTraceId(imgUrl: string): string { const parts = imgUrl.split('/'); const traceId = parts[parts.length - 1].split('!')[0]; return imgUrl.includes('spectrum') ? `spectrum/${traceId}` : traceId; } function getImgsUrlFromNote(note: any): string[] { const imgs = note.image_list || []; return imgs.map((img: any) => getImgUrlByTraceId(getTraceId(img.info_list[0].url))); } function getImgsUrlsFromNote(note: any): string[][] { const imgs = note.image_list || []; return imgs.map((img: any) => getImgUrlsByTraceId(img.trace_id)); } function getVideoUrlFromNote(note: any): string { if (!note.video) return ''; const originVideoKey = note.video.consumer.origin_video_key; return `${videoCdns[Math.floor(Math.random() * videoCdns.length)]}/${originVideoKey}`; } function getVideoUrlsFromNote(note: any): string[] { if (!note.video) return []; const originVideoKey = note.video.consumer.origin_video_key; return videoCdns.map(cdn => `${cdn}/${originVideoKey}`); } // File download async function downloadFile(url: string, filename: string): Promise { const response = await fetch(url); if (!response.ok) throw new Error(`Failed to download file: ${response.statusText}`); const buffer = await response.arrayBuffer(); await fs.promises.writeFile(filename, Buffer.from(buffer)); } // Path handling function getValidPathName(text: string): string { const invalidChars = '<>:"/\\|?*'; return text.replace(new RegExp(`[${invalidChars.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')}]`, 'g'), '_'); } // Signing functions function mrc(e: string): number { const ie = [ 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117, ]; let o = -1; const rightWithoutSign = (num: number, bit = 0): number => { const val = num >>> bit; const MAX32INT = 4294967295; return (val + (MAX32INT + 1)) % (2 * (MAX32INT + 1)) - MAX32INT - 1; }; for (let n = 0; n < 57; n++) { o = ie[(o & 255) ^ e.charCodeAt(n)] ^ rightWithoutSign(o, 8); } return o ^ -1 ^ 3988292384; } // Base64 encoding const lookup = [ "Z", "m", "s", "e", "r", "b", "B", "o", "H", "Q", "t", "N", "P", "+", "w", "O", "c", "z", "a", "/", "L", "p", "n", "g", "G", "8", "y", "J", "q", "4", "2", "K", "W", "Y", "j", "0", "D", "S", "f", "d", "i", "k", "x", "3", "V", "T", "1", "6", "I", "l", "U", "A", "F", "M", "9", "7", "h", "E", "C", "v", "u", "R", "X", "5", ]; function tripletToBase64(e: number): string { return ( lookup[63 & (e >> 18)] + lookup[63 & (e >> 12)] + lookup[(e >> 6) & 63] + lookup[e & 63] ); } function encodeChunk(e: Uint8Array, t: number, r: number): string { const m: string[] = []; for (let b = t; b < r; b += 3) { const n = (16711680 & (e[b] << 16)) + ((e[b + 1] << 8) & 65280) + (e[b + 2] & 255); m.push(tripletToBase64(n)); } return m.join(''); } function b64Encode(e: Uint8Array): string { const P = e.length; const W = P % 3; const U: string[] = []; const z = 16383; let H = 0; const Z = P - W; while (H < Z) { U.push(encodeChunk(e, H, Math.min(H + z, Z))); H += z; } if (W === 1) { const F = e[P - 1]; U.push(lookup[F >> 2] + lookup[(F << 4) & 63] + "=="); } else if (W === 2) { const F = (e[P - 2] << 8) + e[P - 1]; U.push(lookup[F >> 10] + lookup[63 & (F >> 4)] + lookup[(F << 2) & 63] + "="); } return U.join(''); } function encodeUtf8(e: string): Uint8Array { const b: number[] = []; const m = encodeURIComponent(e).replace(/%([0-9A-F]{2})/g, (match, p1) => { b.push(parseInt(p1, 16)); return ''; }); for (let w = 0; w < m.length; w++) { b.push(m.charCodeAt(w)); } return new Uint8Array(b); } // Base36 encoding function base36encode(number: number, alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'): string { if (!Number.isInteger(number)) { throw new TypeError('number must be an integer'); } let base36 = ''; let sign = ''; if (number < 0) { sign = '-'; number = -number; } if (0 <= number && number < alphabet.length) { return sign + alphabet[number]; } while (number !== 0) { const remainder = number % alphabet.length; number = Math.floor(number / alphabet.length); base36 = alphabet[remainder] + base36; } return sign + base36; } function base36decode(number: string): number { return parseInt(number, 36); } // XML parsing function xmlToDict(element: Element): Record { const result: Record = {}; for (let i = 0; i < element.children.length; i++) { const child = element.children[i]; if (child.children.length > 0) { const childDict = xmlToDict(child); if (child.tagName in result) { if (Array.isArray(result[child.tagName])) { result[child.tagName].push(childDict); } else { result[child.tagName] = [result[child.tagName], childDict]; } } else { result[child.tagName] = childDict; } } else { result[child.tagName] = child.textContent; } } return result; } function parseXml(xmlString: string): Record { const parser = new DOMParser(); const doc = parser.parseFromString(xmlString, "text/xml"); return xmlToDict(doc.documentElement); } // Search ID generation function getSearchId(): string { const e = Math.floor(Date.now()) << 64; const t = Math.floor(Math.random() * 2147483646); return base36encode(e + t); } // Cookie handling function cookieStrToCookieDict(cookieStr: string): Record { return cookieStr.split(';').reduce((acc, cookieBlock) => { const [key, value] = cookieBlock.trim().split('='); if (key && value) acc[key] = value; return acc; }, {} as Record); } function cookieJarToCookieStr(cookieJar: Record): string { return Object.entries(cookieJar).map(([key, value]) => `${key}=${value}`).join(';'); } function updateSessionCookiesFromCookie(session: { cookies: Record }, cookie: string): void { const cookieDict = cookie ? cookieStrToCookieDict(cookie) : {}; if (!cookieDict.a1 || !cookieDict.webId) { const [a1, webId] = getA1AndWebId(); cookieDict.a1 = a1; cookieDict.webId = webId; } if (!cookieDict.gid) { cookieDict['gid.sign'] = 'PSF1M3U6EBC/Jv6eGddPbmsWzLI='; cookieDict.gid = 'yYWfJfi820jSyYWfJfdidiKK0YfuyikEvfISMAM348TEJC28K23TxI888WJK84q8S4WfY2Sy'; } session.cookies = cookieDict; } // Main sign function function sign(uri: string, data: any = null, options: { ctime?: number; a1?: string; b1?: string } = {}): { 'x-s': string; 'x-t': string; 'x-s-common': string; } { function h(n: string): string { let m = ''; const d = 'A4NjFqYu5wPHsO0XTdDgMa2r1ZQocVte9UJBvk6/7=yRnhISGKblCWi+LpfE8xzm3'; for (let i = 0; i < 32; i += 3) { const o = n.charCodeAt(i); const g = i + 1 < 32 ? n.charCodeAt(i + 1) : 0; const h = i + 2 < 32 ? n.charCodeAt(i + 2) : 0; const x = ((o & 3) << 4) | (g >> 4); let p = ((15 & g) << 2) | (h >> 6); const v = o >> 2; let b = h ? h & 63 : 64; if (!g) { p = 64; b = 64; } m += d[v] + d[x] + d[p] + d[b]; } return m; } const v = options.ctime || Math.floor(Date.now()); const rawStr = `${v}test${uri}${ data && typeof data === 'object' ? JSON.stringify(data, null, '') : data || '' }`; const md5Str = createHash('md5').update(rawStr).digest('hex'); const x_s = h(md5Str); const x_t = v.toString(); const common = { s0: 5, s1: '', x0: '1', x1: '3.2.0', x2: 'Windows', x3: 'xhs-pc-web', x4: '2.3.1', x5: options.a1 || '', x6: x_t, x7: x_s, x8: options.b1 || '', x9: mrc(x_t + x_s), x10: 1, }; const encodeStr = encodeUtf8(JSON.stringify(common, null, '')); const x_s_common = b64Encode(encodeStr); return { 'x-s': x_s, 'x-t': x_t, 'x-s-common': x_s_common, }; } // Export all functions export { sign, getA1AndWebId, getImgUrlByTraceId, getImgUrlsByTraceId, getTraceId, getImgsUrlFromNote, getImgsUrlsFromNote, getVideoUrlFromNote, getVideoUrlsFromNote, downloadFile, getValidPathName, mrc, b64Encode, encodeUtf8, base36encode, base36decode, parseXml, getSearchId, cookieStrToCookieDict, cookieJarToCookieStr, updateSessionCookiesFromCookie, };