generated from tailored/router-template
temp
This commit is contained in:
300
packages/xhs-core/python/xhs/core.ts
Normal file
300
packages/xhs-core/python/xhs/core.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { sign } from './helper.ts';
|
||||
enum FeedType {
|
||||
RECOMMEND = 'homefeed_recommend',
|
||||
FASION = 'homefeed.fashion_v3',
|
||||
FOOD = 'homefeed.food_v3',
|
||||
COSMETICS = 'homefeed.cosmetics_v3',
|
||||
MOVIE = 'homefeed.movie_and_tv_v3',
|
||||
CAREER = 'homefeed.career_v3',
|
||||
EMOTION = 'homefeed.love_v3',
|
||||
HOURSE = 'homefeed.household_product_v3',
|
||||
GAME = 'homefeed.gaming_v3',
|
||||
TRAVEL = 'homefeed.travel_v3',
|
||||
FITNESS = 'homefeed.fitness_v3',
|
||||
}
|
||||
|
||||
enum NoteType {
|
||||
NORMAL = 'normal',
|
||||
VIDEO = 'video',
|
||||
}
|
||||
|
||||
enum SearchSortType {
|
||||
GENERAL = 'general',
|
||||
MOST_POPULAR = 'popularity_descending',
|
||||
LATEST = 'time_descending',
|
||||
}
|
||||
|
||||
enum SearchNoteType {
|
||||
ALL = 0,
|
||||
VIDEO = 1,
|
||||
IMAGE = 2,
|
||||
}
|
||||
|
||||
interface Note {
|
||||
note_id: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
type: string;
|
||||
user: Record<string, any>;
|
||||
img_urls: string[];
|
||||
video_url: string;
|
||||
tag_list: any[];
|
||||
at_user_list: any[];
|
||||
collected_count: string;
|
||||
comment_count: string;
|
||||
liked_count: string;
|
||||
share_count: string;
|
||||
time: number;
|
||||
last_update_time: number;
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
success?: boolean;
|
||||
code?: number;
|
||||
msg?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
class XhsError extends Error {
|
||||
response?: Response;
|
||||
verify_type?: string;
|
||||
verify_uuid?: string;
|
||||
|
||||
constructor(message: string, options?: { response?: Response; verify_type?: string | null; verify_uuid?: string | null }) {
|
||||
super(message);
|
||||
this.response = options?.response;
|
||||
this.verify_type = options?.verify_type ?? '';
|
||||
this.verify_uuid = options?.verify_uuid ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
class DataFetchError extends XhsError {}
|
||||
class IPBlockError extends XhsError {}
|
||||
class NeedVerifyError extends XhsError {}
|
||||
class SignError extends XhsError {}
|
||||
|
||||
export class XhsClientBase {
|
||||
private timeout: number;
|
||||
private externalSign?: (url: string, data: any, options: { a1?: string; web_session?: string }) => Record<string, string>;
|
||||
private host: string;
|
||||
private creatorHost: string;
|
||||
private home: string;
|
||||
private userAgent: string;
|
||||
private cookies: Record<string, string> = {};
|
||||
private headers: Record<string, string>;
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
cookie?: string;
|
||||
user_agent?: string;
|
||||
timeout?: number;
|
||||
sign?: (url: string, data: any, options: { a1?: string; web_session?: string }) => Record<string, string>;
|
||||
} = {},
|
||||
) {
|
||||
this.timeout = options.timeout || 10 * 60 * 1000; // 10 minutes
|
||||
this.externalSign = options.sign;
|
||||
this.host = 'https://edith.xiaohongshu.com';
|
||||
this.creatorHost = 'https://creator.xiaohongshu.com';
|
||||
this.home = 'https://www.xiaohongshu.com';
|
||||
this.userAgent = options.user_agent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36';
|
||||
|
||||
this.headers = {
|
||||
'user-agent': this.userAgent,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (options.cookie) {
|
||||
this.cookie = options.cookie;
|
||||
}
|
||||
}
|
||||
|
||||
get cookie(): string {
|
||||
return Object.entries(this.cookies)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
set cookie(cookie: string) {
|
||||
this.cookies = cookie.split(';').reduce((acc, pair) => {
|
||||
const [key, value] = pair.trim().split('=');
|
||||
if (key && value) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
get cookieDict(): Record<string, string> {
|
||||
return { ...this.cookies };
|
||||
}
|
||||
|
||||
private async request(method: string, url: string, options: RequestInit = {}): Promise<any> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
...this.headers,
|
||||
...options.headers,
|
||||
cookie: this.cookie,
|
||||
},
|
||||
body: options.body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
// No content
|
||||
return response;
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => response.text());
|
||||
|
||||
if (response.status === 471 || response.status === 461) {
|
||||
const verify_type = response.headers.get('Verifytype');
|
||||
const verify_uuid = response.headers.get('Verifyuuid');
|
||||
throw new NeedVerifyError(`出现验证码,请求失败,Verifytype: ${verify_type},Verifyuuid: ${verify_uuid}`, { response, verify_type, verify_uuid });
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
return data.data || data.success;
|
||||
} else if (data.code === 300007) {
|
||||
// IP_BLOCK
|
||||
throw new IPBlockError('IP blocked', { response });
|
||||
} else if (data.code === 400002) {
|
||||
// SIGN_FAULT
|
||||
throw new SignError('Sign fault', { response });
|
||||
} else {
|
||||
throw new DataFetchError(data, { response });
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error(`Request timeout after ${this.timeout} seconds`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw new Error('Unknown error occurred');
|
||||
}
|
||||
}
|
||||
|
||||
preHeaders(url: string, data?: any, isCreator: boolean = false): void {
|
||||
if (isCreator) {
|
||||
// Implement sign function in TypeScript or import it
|
||||
const signs = sign(url, data, { a1: this.cookies.a1 });
|
||||
this.headers['x-s'] = signs['x-s'];
|
||||
this.headers['x-t'] = signs['x-t'];
|
||||
this.headers['x-s-common'] = signs['x-s-common'];
|
||||
} else if (this.externalSign) {
|
||||
const signs = this.externalSign(url, data, {
|
||||
a1: this.cookies.a1,
|
||||
web_session: this.cookies.web_session || '',
|
||||
});
|
||||
Object.assign(this.headers, signs);
|
||||
}
|
||||
}
|
||||
|
||||
async get(uri: string, params?: Record<string, any>, isCreator: boolean = false, options: RequestInit = {}): Promise<any> {
|
||||
let finalUri = uri;
|
||||
if (params) {
|
||||
const query = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined) {
|
||||
query.append(key, String(value));
|
||||
}
|
||||
}
|
||||
finalUri = `${uri}?${query.toString()}`;
|
||||
}
|
||||
this.preHeaders(finalUri, undefined, isCreator);
|
||||
const url = `${isCreator ? this.creatorHost : this.host}${finalUri}`;
|
||||
return this.request('GET', url, options);
|
||||
}
|
||||
|
||||
async post(uri: string, data: any, isCreator: boolean = false, options: RequestInit = {}): Promise<any> {
|
||||
this.preHeaders(uri, data, isCreator);
|
||||
const url = `${isCreator ? this.creatorHost : this.host}${uri}`;
|
||||
return this.request('POST', url, {
|
||||
...options,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async getNoteById(noteId: string): Promise<any> {
|
||||
const data = { source_note_id: noteId, image_scenes: ['CRD_WM_WEBP'] };
|
||||
const uri = '/api/sns/web/v1/feed';
|
||||
const res = await this.post(uri, data);
|
||||
return res.items[0].note_card;
|
||||
}
|
||||
|
||||
async getNoteByIdFromHtml(noteId: string): Promise<any> {
|
||||
const url = `${this.home}/explore/${noteId}`;
|
||||
const response = await this.request('GET', url, {
|
||||
headers: {
|
||||
'user-agent': this.userAgent,
|
||||
referer: `${this.home}/`,
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof response === 'string') {
|
||||
const html = response;
|
||||
const stateMatch = html.match(/window\.__INITIAL_STATE__=({.*?})<\/script>/);
|
||||
if (!stateMatch) {
|
||||
throw new Error('Could not find initial state in HTML');
|
||||
}
|
||||
|
||||
let state = stateMatch[1].replace(/undefined/g, '""');
|
||||
if (state === '{}') {
|
||||
throw new DataFetchError('Empty state');
|
||||
}
|
||||
|
||||
// Implement transformJsonKeys in TypeScript if needed
|
||||
const noteDict = this.transformJsonKeys(JSON.parse(state));
|
||||
return noteDict.note.note_detail_map[noteId].note;
|
||||
}
|
||||
|
||||
throw new DataFetchError('Invalid response');
|
||||
}
|
||||
|
||||
private transformJsonKeys(data: any): any {
|
||||
// Implement camelToUnderscore and transform logic here
|
||||
// Similar to the Python version
|
||||
return data; // Placeholder
|
||||
}
|
||||
|
||||
// Implement all other methods similarly, converting Python to TypeScript
|
||||
// For example:
|
||||
|
||||
async reportNoteMetrics(
|
||||
noteId: string,
|
||||
noteType: number,
|
||||
noteUserId: string,
|
||||
viewerUserId: string,
|
||||
followedAuthor: number = 0,
|
||||
reportType: number = 1,
|
||||
staySeconds: number = 0,
|
||||
): Promise<any> {
|
||||
const uri = '/api/sns/web/v1/note/metrics_report';
|
||||
const data = {
|
||||
note_id: noteId,
|
||||
note_type: noteType,
|
||||
report_type: reportType,
|
||||
stress_test: false,
|
||||
viewer: { user_id: viewerUserId, followed_author: followedAuthor },
|
||||
author: { user_id: noteUserId },
|
||||
interaction: { like: 0, collect: 0, comment: 0, comment_read: 0 },
|
||||
note: { stay_seconds: staySeconds },
|
||||
other: { platform: 'web' },
|
||||
};
|
||||
return this.post(uri, data);
|
||||
}
|
||||
|
||||
// Continue with all other methods...
|
||||
}
|
||||
48
packages/xhs-core/python/xhs/exception.ts
Normal file
48
packages/xhs-core/python/xhs/exception.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export enum ErrorCode {
|
||||
/** 网络连接异常,请检查网络设置或重启试试 */
|
||||
IP_BLOCK = 300012,
|
||||
/** 笔记状态异常,请稍后查看 */
|
||||
NOTE_ABNORMAL = -510001,
|
||||
/** 当前内容无法展示 */
|
||||
NOTE_SECRETE_FAULT = -510001,
|
||||
/** 浏览器异常,请尝试关闭/卸载风险插件或重启试试! */
|
||||
SIGN_FAULT = 300015,
|
||||
/** 登录已过期 */
|
||||
SESSION_EXPIRED = -100
|
||||
}
|
||||
|
||||
|
||||
export class XhsError extends Error {
|
||||
response?: Response;
|
||||
verifyType?: string;
|
||||
verifyUuid?: string;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
options?: {
|
||||
response?: Response;
|
||||
verifyType?: string;
|
||||
verifyUuid?: string;
|
||||
}
|
||||
) {
|
||||
super(message);
|
||||
this.response = options?.response;
|
||||
this.verifyType = options?.verifyType;
|
||||
this.verifyUuid = options?.verifyUuid;
|
||||
}
|
||||
}
|
||||
|
||||
export class DataFetchError extends XhsError {}
|
||||
export class IPBlockError extends XhsError {}
|
||||
export class SignError extends XhsError {}
|
||||
export class NeedVerifyError extends XhsError {
|
||||
constructor(
|
||||
message: string,
|
||||
options?: {
|
||||
verifyType?: string;
|
||||
verifyUuid?: string;
|
||||
}
|
||||
) {
|
||||
super(message, options);
|
||||
}
|
||||
}
|
||||
401
packages/xhs-core/python/xhs/helper.ts
Normal file
401
packages/xhs-core/python/xhs/helper.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
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<void> {
|
||||
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<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
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<string, any> {
|
||||
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<string, string> {
|
||||
return cookieStr.split(';').reduce((acc, cookieBlock) => {
|
||||
const [key, value] = cookieBlock.trim().split('=');
|
||||
if (key && value) acc[key] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
}
|
||||
|
||||
function cookieJarToCookieStr(cookieJar: Record<string, string>): string {
|
||||
return Object.entries(cookieJar).map(([key, value]) => `${key}=${value}`).join(';');
|
||||
}
|
||||
|
||||
function updateSessionCookiesFromCookie(session: { cookies: Record<string, string> }, 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,
|
||||
};
|
||||
Reference in New Issue
Block a user