generated from tailored/router-template
301 lines
8.7 KiB
TypeScript
301 lines
8.7 KiB
TypeScript
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...
|
||
}
|