2025-05-01 03:59:37 +08:00

301 lines
8.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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...
}