99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
import type { Page } from 'rebrowser-playwright';
|
|
|
|
import { XHH_SELECTORS } from './selectors.js';
|
|
import { detectCaptchaText } from './extractors.js';
|
|
|
|
function buildDetailUrl(linkId: string): string {
|
|
return `https://www.xiaoheihe.cn/app/bbs/link/${encodeURIComponent(linkId)}`;
|
|
}
|
|
|
|
export async function setLikeState(
|
|
page: Page,
|
|
linkId: string,
|
|
targetState: boolean,
|
|
): Promise<{ success: boolean; state: boolean; changed: boolean }> {
|
|
await page.goto(buildDetailUrl(linkId), { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const text = await page.textContent('body').catch(() => '');
|
|
if (text && detectCaptchaText(text)) {
|
|
throw new Error('CAPTCHA_REQUIRED: captcha detected on interaction page');
|
|
}
|
|
|
|
const current = await readButtonState(page, XHH_SELECTORS.detail.likeButton);
|
|
if (current === targetState) {
|
|
return { success: true, state: current, changed: false };
|
|
}
|
|
|
|
const clicked = await clickAny(page, XHH_SELECTORS.detail.likeButton);
|
|
if (!clicked) {
|
|
return { success: false, state: current, changed: false };
|
|
}
|
|
await page.waitForTimeout(700);
|
|
const state = await readButtonState(page, XHH_SELECTORS.detail.likeButton);
|
|
return {
|
|
success: state === targetState,
|
|
state,
|
|
changed: state !== current,
|
|
};
|
|
}
|
|
|
|
export async function setFavoriteState(
|
|
page: Page,
|
|
linkId: string,
|
|
targetState: boolean,
|
|
): Promise<{ success: boolean; state: boolean; changed: boolean }> {
|
|
await page.goto(buildDetailUrl(linkId), { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const text = await page.textContent('body').catch(() => '');
|
|
if (text && detectCaptchaText(text)) {
|
|
throw new Error('CAPTCHA_REQUIRED: captcha detected on interaction page');
|
|
}
|
|
|
|
const current = await readButtonState(page, XHH_SELECTORS.detail.favoriteButton);
|
|
if (current === targetState) {
|
|
return { success: true, state: current, changed: false };
|
|
}
|
|
|
|
const clicked = await clickAny(page, XHH_SELECTORS.detail.favoriteButton);
|
|
if (!clicked) {
|
|
return { success: false, state: current, changed: false };
|
|
}
|
|
await page.waitForTimeout(700);
|
|
const state = await readButtonState(page, XHH_SELECTORS.detail.favoriteButton);
|
|
return {
|
|
success: state === targetState,
|
|
state,
|
|
changed: state !== current,
|
|
};
|
|
}
|
|
|
|
async function clickAny(page: Page, selectors: readonly string[]): Promise<boolean> {
|
|
for (const selector of selectors) {
|
|
const ok = await page.locator(selector).first().click({ timeout: 2_000 }).then(() => true).catch(() => false);
|
|
if (ok) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function readButtonState(page: Page, selectors: readonly string[]): Promise<boolean> {
|
|
for (const selector of selectors) {
|
|
const state = await page
|
|
.evaluate((sel) => {
|
|
const node = document.querySelector(sel) as HTMLElement | null;
|
|
if (!node) return null;
|
|
if (node.getAttribute('aria-pressed') === 'true') return true;
|
|
const cls = node.className.toString().toLowerCase();
|
|
if (cls.includes('active') || cls.includes('selected')) return true;
|
|
const html = node.innerHTML.toLowerCase();
|
|
if (html.includes('filled') || html.includes('checked')) return true;
|
|
return false;
|
|
}, selector)
|
|
.catch(() => null);
|
|
if (typeof state === 'boolean') return state;
|
|
}
|
|
return false;
|
|
}
|
|
|