The job_run_logs table tracks scheduled job orchestration, not individual worker jobs. Worker info (worker_id, worker_hostname) belongs on dispensary_crawl_jobs, not job_run_logs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
176 lines
7.4 KiB
JavaScript
176 lines
7.4 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.hasAgeGatePlaywright = hasAgeGatePlaywright;
|
|
exports.bypassAgeGatePlaywright = bypassAgeGatePlaywright;
|
|
exports.detectStateFromUrlPlaywright = detectStateFromUrlPlaywright;
|
|
const logger_1 = require("../services/logger");
|
|
/**
|
|
* Detects if a Playwright page has an age verification gate
|
|
*/
|
|
async function hasAgeGatePlaywright(page) {
|
|
try {
|
|
const url = page.url();
|
|
const bodyText = await page.textContent('body') || '';
|
|
const hasAgeVerification = url.includes('/age-gate') ||
|
|
bodyText.includes('age verification') ||
|
|
bodyText.includes('Please select your state') ||
|
|
bodyText.includes('are you 21') ||
|
|
bodyText.includes('are you 18') ||
|
|
bodyText.includes('Enter your date of birth') ||
|
|
bodyText.toLowerCase().includes('verify your age');
|
|
return hasAgeVerification;
|
|
}
|
|
catch (err) {
|
|
logger_1.logger.warn('age-gate', `Error detecting age gate: ${err}`);
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Attempts to bypass an age gate using Playwright
|
|
* Handles multiple age gate patterns including Curaleaf's complex React-based gate
|
|
*
|
|
* @param page - Playwright page object
|
|
* @param state - State to select (e.g., 'Arizona', 'California')
|
|
* @returns Promise<boolean> - true if bypass succeeded, false otherwise
|
|
*/
|
|
async function bypassAgeGatePlaywright(page, state = 'Arizona') {
|
|
try {
|
|
const hasGate = await hasAgeGatePlaywright(page);
|
|
if (!hasGate) {
|
|
logger_1.logger.info('age-gate', 'No age gate detected');
|
|
return true;
|
|
}
|
|
logger_1.logger.info('age-gate', `Age gate detected - attempting to bypass with state: ${state}...`);
|
|
// Wait for age gate to fully render
|
|
await page.waitForTimeout(2000);
|
|
// Method 1: Curaleaf-style (state dropdown + "I'm over 21" button)
|
|
try {
|
|
const stateButton = page.locator('button#state, button[id="state"]').first();
|
|
const stateButtonExists = await stateButton.count() > 0;
|
|
if (stateButtonExists) {
|
|
logger_1.logger.info('age-gate', 'Found Curaleaf-style state dropdown...');
|
|
await stateButton.click();
|
|
await page.waitForTimeout(1000);
|
|
// Select state
|
|
const stateOption = page.locator('[role="option"]').filter({ hasText: new RegExp(`^${state}$`, 'i') });
|
|
const stateExists = await stateOption.count() > 0;
|
|
if (stateExists) {
|
|
logger_1.logger.info('age-gate', `Clicking ${state} option...`);
|
|
await stateOption.first().click();
|
|
await page.waitForTimeout(2000);
|
|
// Look for "I'm over 21" button
|
|
const ageButton = page.locator('button').filter({ hasText: /I'm over 21|I am 21|I'm 21|over 21/i });
|
|
const ageButtonExists = await ageButton.count() > 0;
|
|
if (ageButtonExists) {
|
|
logger_1.logger.info('age-gate', 'Clicking age verification button...');
|
|
await ageButton.first().click();
|
|
await page.waitForLoadState('domcontentloaded', { timeout: 15000 });
|
|
await page.waitForTimeout(3000);
|
|
// Check if we successfully bypassed
|
|
const finalUrl = page.url();
|
|
if (!finalUrl.includes('/age-gate')) {
|
|
logger_1.logger.info('age-gate', `✅ Age gate bypass successful`);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
logger_1.logger.warn('age-gate', `Curaleaf method failed: ${e}`);
|
|
}
|
|
// Method 2: Simple "Yes" or "I'm 21" button (for simpler age gates)
|
|
try {
|
|
const simpleButton = page.locator('button, a, [role="button"]').filter({
|
|
hasText: /yes|i am 21|i'm 21|enter the site|continue|confirm/i
|
|
});
|
|
const simpleExists = await simpleButton.count() > 0;
|
|
if (simpleExists) {
|
|
logger_1.logger.info('age-gate', 'Found simple age gate button...');
|
|
await simpleButton.first().click();
|
|
await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
|
|
await page.waitForTimeout(2000);
|
|
const finalUrl = page.url();
|
|
if (!finalUrl.includes('/age-gate')) {
|
|
logger_1.logger.info('age-gate', `✅ Age gate bypass successful`);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
logger_1.logger.warn('age-gate', `Simple button method failed: ${e}`);
|
|
}
|
|
// Method 3: Standard select dropdown
|
|
try {
|
|
const selectExists = await page.locator('select').count() > 0;
|
|
if (selectExists) {
|
|
logger_1.logger.info('age-gate', 'Found select dropdown...');
|
|
const select = page.locator('select').first();
|
|
await select.selectOption({ label: state });
|
|
await page.waitForTimeout(1000);
|
|
// Look for submit button
|
|
const submitButton = page.locator('button[type="submit"], input[type="submit"]');
|
|
const submitExists = await submitButton.count() > 0;
|
|
if (submitExists) {
|
|
await submitButton.first().click();
|
|
await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
|
|
await page.waitForTimeout(2000);
|
|
const finalUrl = page.url();
|
|
if (!finalUrl.includes('/age-gate')) {
|
|
logger_1.logger.info('age-gate', `✅ Age gate bypass successful`);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
logger_1.logger.warn('age-gate', `Select dropdown method failed: ${e}`);
|
|
}
|
|
// Verify final state
|
|
const finalUrl = page.url();
|
|
if (finalUrl.includes('/age-gate')) {
|
|
logger_1.logger.error('age-gate', `❌ Age gate bypass failed - still at: ${finalUrl}`);
|
|
return false;
|
|
}
|
|
logger_1.logger.info('age-gate', `✅ Age gate bypass successful`);
|
|
return true;
|
|
}
|
|
catch (err) {
|
|
logger_1.logger.error('age-gate', `Error bypassing age gate: ${err}`);
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Helper to detect the state from a store URL
|
|
*/
|
|
function detectStateFromUrlPlaywright(url) {
|
|
const stateMap = {
|
|
'-az-': 'Arizona',
|
|
'arizona': 'Arizona',
|
|
'-ca-': 'California',
|
|
'california': 'California',
|
|
'-co-': 'Colorado',
|
|
'colorado': 'Colorado',
|
|
'-fl-': 'Florida',
|
|
'florida': 'Florida',
|
|
'-il-': 'Illinois',
|
|
'illinois': 'Illinois',
|
|
'-ma-': 'Massachusetts',
|
|
'-mi-': 'Michigan',
|
|
'-nv-': 'Nevada',
|
|
'-nj-': 'New Jersey',
|
|
'-ny-': 'New York',
|
|
'-or-': 'Oregon',
|
|
'-pa-': 'Pennsylvania',
|
|
'-wa-': 'Washington',
|
|
};
|
|
const lowerUrl = url.toLowerCase();
|
|
for (const [pattern, stateName] of Object.entries(stateMap)) {
|
|
if (lowerUrl.includes(pattern)) {
|
|
return stateName;
|
|
}
|
|
}
|
|
// Default to Arizona
|
|
return 'Arizona';
|
|
}
|