Explorar el Código

Add booking-2fa script to trigger messaging auth-assurance separately from refresh-creds

ZiB hace 4 meses
padre
commit
f72541aca7

+ 105 - 0
room_cal/service_api/typescript/booking/trigger-2fa.ts

@@ -0,0 +1,105 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+import type { BrowserContext, Page } from 'playwright';
+import { filterCookies, parseOutputMode } from '../shared/cookie-utils';
+import { handleOutput, fetchSmsCode } from '../shared/api-client';
+import { openPersistentContext, profileDir } from '../shared/browser';
+import type { CredentialsPayload } from '../shared/types';
+
+const DATA_HOME = process.env.ROOM_CAL_DATA_HOME ?? path.join(os.homedir(), '.local', 'room_cal');
+const ENV_PATH = path.join(DATA_HOME, 'booking.env');
+const ADMIN_BASE = 'admin.booking.com';
+const STRIP = new Set(['OptanonConsent', 'pcm_consent', 'uz']);
+
+async function handleAuthAssurance(page: Page, masked: string): Promise<void> {
+  console.error(`[booking-2fa] Challenge required (${masked || 'unknown number'}) — attempting SMS automation`);
+
+  try {
+    await page.getByRole('button', { name: /sms|text message/i }).first().click({ timeout: 5_000 });
+    console.error('[booking-2fa] Clicked SMS option');
+    await page.waitForTimeout(1_000);
+  } catch {
+    // Already on PIN entry, or SMS is the only option — continue.
+  }
+
+  const PIN_SEL = 'input[autocomplete="one-time-code"], input[inputmode="numeric"][maxlength], input[name*="pin"], input[name*="code"]';
+  let pinInput;
+  try {
+    pinInput = await page.waitForSelector(PIN_SEL, { timeout: 15_000 });
+  } catch {
+    throw new Error('[booking-2fa] No PIN input found — 2FA could not be completed automatically');
+  }
+
+  console.error('[booking-2fa] Polling /sms/code for up to 2 minutes...');
+  const code = await fetchSmsCode(120);
+  if (!code) {
+    throw new Error('[booking-2fa] No SMS code received within timeout');
+  }
+
+  console.error(`[booking-2fa] Filling code ${code}`);
+  await pinInput.fill(code);
+  await page.keyboard.press('Enter');
+}
+
+export async function triggerChatAuth(mode: string): Promise<CredentialsPayload> {
+  const envText = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf8') : '';
+  const hotelId = envText.match(/^BOOKING_HOTEL_ID=(\S+)/m)?.[1] ?? '';
+  const ses = envText.match(/^BOOKING_SESSION=(\S+)/m)?.[1] ?? '';
+
+  if (!hotelId || !ses) {
+    throw new Error('BOOKING_HOTEL_ID and BOOKING_SESSION must be set in booking.env — run refresh-creds first');
+  }
+
+  const profile = profileDir('booking', mode);
+  let smsMasked = '';
+  const context = await openPersistentContext(profile, true);
+  context.on('response', async res => {
+    if (res.url().includes('challenge-methods')) {
+      try {
+        const body = await res.json() as { challengeMethods?: Array<{ type: string; phones?: Array<{ masked: string }> }> };
+        const sms = body.challengeMethods?.find(m => m.type === 'CHALLENGE_METHOD_SMS');
+        if (sms?.phones?.[0]?.masked) smsMasked = sms.phones[0].masked;
+      } catch {}
+    }
+  });
+
+  const page = context.pages()[0] ?? await context.newPage();
+  const messagingUrl = `https://${ADMIN_BASE}/hotel/hoteladmin/extranet_ng/manage/messaging/inbox.html?hotel_id=${hotelId}&lang=en&ses=${ses}&source=nav&tlc=1`;
+
+  console.error('[booking-2fa] Navigating to messaging inbox (headless)...');
+  await page.goto(messagingUrl);
+  await page.waitForLoadState('domcontentloaded');
+
+  if (!page.url().includes(ADMIN_BASE)) {
+    await context.close();
+    throw new Error(`Session invalid — redirected to ${page.url()} — run refresh-creds first`);
+  }
+
+  if (page.url().includes('auth-assurance')) {
+    await handleAuthAssurance(page, smsMasked);
+    await page.waitForURL(`**${ADMIN_BASE}/**`, { timeout: 120_000 });
+    console.error('[booking-2fa] Auth-assurance completed.');
+  } else {
+    console.error('[booking-2fa] No auth-assurance challenge — messaging already primed for this session.');
+  }
+
+  const all = await context.cookies();
+  await context.close();
+
+  const cookieString = filterCookies(all, STRIP, 'booking.com');
+  if (!cookieString) throw new Error('No cookies found after 2FA — something went wrong');
+
+  console.error('[booking-2fa] Done.');
+  return { BOOKING_COOKIES: cookieString };
+}
+
+async function main(): Promise<void> {
+  const mode = parseOutputMode(process.argv);
+  const credentials = await triggerChatAuth(mode);
+  await handleOutput('booking', credentials, mode, ENV_PATH);
+}
+
+if (require.main === module) {
+  main().catch(err => { console.error(err.message); process.exit(1); });
+}

+ 1 - 0
room_cal/service_api/typescript/package.json

@@ -5,6 +5,7 @@
   "scripts": {
     "refresh-airbnb": "tsx airbnb/refresh-cookies.ts",
     "refresh-booking": "tsx booking/refresh-cookies.ts",
+    "booking-2fa": "tsx booking/trigger-2fa.ts",
     "refresh-all": "tsx scripts/refresh-all.ts",
     "install-browsers": "tsx scripts/install-browsers.ts",
     "check-credentials": "tsx scripts/check-credentials.ts",