import { getApiInfo } from './xhs-api/api.ts'; import { XhsClient as XhsClientBase } from '@kevisual/xhs-core'; import { Mention, CommonentInfo, ResponseMession } from './xhs-type/mention.ts'; import { pick } from 'lodash-es'; import { getNote } from './modules/get-note.ts'; export type Result = { code: number; // 0: success msg?: string; data?: T; success?: boolean; }; type SignInfo = { uri: string; data: any; a1: string; web_session?: string; }; type SignResponse = { a1: string; sign: { b1: string; b1b1: string; ['x-s']: string; ['x-t']: string; }; [key: string]: any; }; type SignOptions = { signUrl?: string; }; export const getSign = async (signInfo: SignInfo, options?: SignOptions): Promise => { const { uri, data, a1, web_session } = signInfo; // console.log('getSign', uri, data, a1, web_session); // let signUri = new URL(uri, 'http://light.xiongxiao.me:5006').pathname; // signUri = '/api/sns/web/v2/user/me'; try { let signUrl = options?.signUrl || 'http://localhost:5005/sign'; // signUrl = 'http://localhost:5005/sign'; // const urlA1 = ''http://light.xiongxiao.me:5006/a1'; // const urlA1 = 'http://localhost:5005/a1'; const signs = await fetch(signUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ uri: uri, data, a1, web_session: web_session, }), }).then((res) => res.json()); return signs as SignResponse; } catch (error) { return null; } }; type XhsOptions = { cookie?: string; }; type XhsSign = { signUrl?: string; }; export class XhsClient extends XhsClientBase { signConfig?: XhsSign; constructor(opts: XhsOptions) { super(opts as any); } setCookie(cookie: string) { this.cookie = cookie; } getApiInfo = getApiInfo; printResult(msg: string, data: any) { if (msg === 'response') { console.log('url', data.url); console.log('status', data?.response?.status); if (data.response) { // console.log('data', data.response.data); } } else if (msg === 'request') { const { method, url } = data || {}; const headers = pick(data?.headers || {}, ['Cookie', 'x-s', 'x-t', 'x-s-common']); // console.log('request', { headers, method, url }); } else if (msg === 'html') { // console.log('html', response); } switch (msg) { case 'get': // console.log('get', data); break; case 'sign': // console.log('sign', data); break; } } /** * 获取未读消息 * @returns */ async getUnread(): Promise> { const url = '/api/sns/web/unread_count'; const response = await this.get(url, null, { needSign: false }); return response; } async postRead(type: 'unread_count' | 'likes' | 'mentions' | 'unread_count' = 'mentions') { const uri = '/api/sns/web/v1/message/read'; const data = { type, }; type RetrunData = { code: number; msg: string; success: boolean; }; const response = await this.post(uri, data, { sign: this.sign.bind(this), }); return response as Result; } async getUserInfoFromHtml(userId: string) { type ReturnData = { ip_location: string; desc: string; imageb: string; nickname: string; images: string; red_id: string; gender: number; }; const response = await super.getUserInfoFromHtml(userId); return response as ReturnData; } /** * 这个接口不能多用,否则会出现封控406错误 * @param num * @param cursor * @returns */ async getFollowNotifications(num = 10, cursor = '') { const url = '/api/sns/web/v1/you/connections'; type Connection = { type: 'fllow/you'; title: string; id: string; track_type: string; user: { fstatus: 'both' | 'follows' | 'fans'; red_official_verify_type: number; xsec_token: string; userid: string; nickname: string; images: string }; time: number; score: number; }; type ReturnData = { message_list: Connection[]; has_more: boolean; cursor: number; strCursor: string; }; const response = await this.get(url, { num, cursor }, { sign: this.sign.bind(this) }); return response as Result; } async getComment(noteId: string, xsecToken?: string) {} /** * * @uri /api/sns/web/v1/you/mentions * @returns */ async getMention(num = 20): Promise> { const url = '/api/sns/web/v1/you/mentions'; const response = await this.get( url, { num: num, cursor: '' }, { sign: this.sign.bind(this), needSign: true, }, ); return response; } async sign(uri: string, data: any, config: any) { let headers = config?.headers || {}; const cookieDist = this.getCookieMap(); const apiInfo = this.getApiInfo(uri); if (apiInfo && !apiInfo?.needSign) { return config || {}; } const web_session = cookieDist['web_session']; const a1 = cookieDist['a1']; const res = await getSign({ uri, data, a1, web_session }, this.signConfig); const _sign = res.sign; this.printResult('sign', { uri, apiInfo, res }); const xs = _sign?.['x-s']; const xt = _sign?.['x-t']; const b1 = _sign?.['b1']; const newA1 = _sign?.['a1']; if (a1 !== newA1) { this.setCookieMap({ a1: newA1 }); this.printResult('cookie change', a1); } if (res && xs) { headers['x-s'] = xs; headers['x-t'] = xt; // headers['x-s-common'] = this.getXCommon(a1, b1, xs, xt); config.headers = headers; } else { console.log('get sign error', res); throw new Error('get sign error'); } return config; } async getNoteById(noteId: string, xsecToken?: string) { const data = { source_note_id: noteId, image_scenes: ['CRD_WM_WEBP'], }; const uri = '/api/sns/web/v1/feed'; try { const response = await this.post(uri, data, { sign: this.sign.bind(this) }); // return response['items'][0]['node_card']; return response; } catch (error) { console.log(error); } } /** * 发送评论,content最多为300个字符,如果多余300个字符,发送两次 * @param comment * @returns */ async postComment(comment: { note_id: string; comment_id?: string; content: string; images_info?: any; images?: string[] }) { const uri = '/api/sns/web/v1/comment/post'; try { const data = { note_id: comment.note_id, content: comment.content, at_users: [], // }; if (comment.comment_id) { data['target_comment_id'] = comment.comment_id; } if (comment.images_info) { data['images_info'] = comment.images_info; } if (comment.images) { data['images'] = comment.images; } type PostCommentResponse = { comment: CommonentInfo; time: number; toast: string; }; const response = await this.post(uri, data, { sign: this.sign.bind(this) }); return response as Result; } catch (error) { console.log(error); } } getNote = getNote; } type UnreadCount = { unread_count: number; likes: number; connections: number; mentions: number; };