generated from tailored/router-db-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...
|
||||
}
|
||||
Reference in New Issue
Block a user