/** * Automated Google Alert Setup Script * * This script: * 1. Logs into Google (with manual intervention for first-time auth) * 2. Reads alerts from markdown files * 3. Creates each alert one at a time * 4. Collects RSS feed URLs * 5. Saves RSS feeds to a JSON file * * Usage: * node scripts/setup-alerts-automated.js docs/google-alerts-reddit-tuned.md * * For first-time use, you'll need to manually log in once. * The authentication state will be saved for future runs. */ import { chromium } from 'playwright'; import { readFile, writeFile, mkdir } from 'fs/promises'; import { existsSync } from 'fs'; import { join } from 'path'; const AUTH_STATE_PATH = join(process.cwd(), '.auth', 'google-auth.json'); const RSS_FEEDS_PATH = join(process.cwd(), 'rss-feeds.json'); /** * Parse alerts from markdown file */ async function parseAlertsFromMarkdown(filePath) { const content = await readFile(filePath, 'utf-8'); const lines = content.split('\n'); const alerts = []; let currentAlert = null; let inCodeBlock = false; let queryLines = []; let currentHeading = ''; for (const line of lines) { // Track headings if (line.startsWith('### ')) { currentHeading = line.replace(/^### /, '').trim(); } // Detect alert name if (line.includes('**Alert Name:**')) { if (currentAlert && queryLines.length > 0) { currentAlert.query = queryLines.join('\n').trim(); if (currentAlert.query) { alerts.push(currentAlert); } } const match = line.match(/\*\*Alert Name:\*\*\s*`([^`]+)`/); const name = match ? match[1] : line.split('**Alert Name:**')[1].trim(); currentAlert = { name, query: '', heading: currentHeading }; queryLines = []; continue; } // Detect code blocks containing queries if (line.trim() === '```') { if (!inCodeBlock && currentAlert) { inCodeBlock = true; queryLines = []; } else if (inCodeBlock) { inCodeBlock = false; } continue; } // Collect query lines if (inCodeBlock && currentAlert) { queryLines.push(line); } } // Add last alert if (currentAlert && queryLines.length > 0) { currentAlert.query = queryLines.join('\n').trim(); if (currentAlert.query) { alerts.push(currentAlert); } } return alerts.filter(alert => alert.query); } /** * Load saved authentication state */ async function loadAuthState() { if (existsSync(AUTH_STATE_PATH)) { const authData = await readFile(AUTH_STATE_PATH, 'utf-8'); return JSON.parse(authData); } return null; } /** * Save authentication state */ async function saveAuthState(context) { const authDir = join(process.cwd(), '.auth'); if (!existsSync(authDir)) { await mkdir(authDir, { recursive: true }); } const authState = await context.storageState(); await writeFile(AUTH_STATE_PATH, JSON.stringify(authState, null, 2)); console.log('āœ… Authentication state saved'); } /** * Setup browser with authentication */ async function setupBrowser() { const browser = await chromium.launch({ headless: false, // Show browser for login slowMo: 500 // Slow down actions for visibility }); const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, locale: 'en-CA', timezoneId: 'America/Toronto', }); // Try to load saved auth state const savedAuth = await loadAuthState(); if (savedAuth) { console.log('šŸ“¦ Loading saved authentication state...'); await context.addCookies(savedAuth.cookies); await context.addInitScript(() => { // Restore localStorage if needed if (window.localStorage) { Object.keys(savedAuth.origins?.[0]?.localStorage || {}).forEach(key => { window.localStorage.setItem(key, savedAuth.origins[0].localStorage[key]); }); } }); } return { browser, context }; } /** * Ensure user is logged into Google */ async function ensureLoggedIn(page) { await page.goto('https://www.google.com/alerts'); await page.waitForLoadState('networkidle'); // Check if we need to log in const signInButton = page.getByText('Sign in', { exact: false }).first(); const isVisible = await signInButton.isVisible().catch(() => false); if (isVisible) { console.log('šŸ” Please log in to Google in the browser window...'); console.log(' Waiting for you to complete login...'); // Wait for user to navigate away and back (login process) await page.waitForURL('**/alerts**', { timeout: 300000 }); // 5 min timeout // Wait a bit more to ensure we're fully logged in await page.waitForTimeout(2000); console.log('āœ… Login detected'); } else { console.log('āœ… Already logged in'); } } /** * Create a single Google Alert */ async function createAlert(page, alert) { console.log(`\nšŸ“ Creating alert: ${alert.name}`); console.log(` Query: ${alert.query.substring(0, 60)}...`); try { // Navigate to alerts page await page.goto('https://www.google.com/alerts'); await page.waitForLoadState('networkidle'); // Wait for the search input const searchInput = page.locator('input[type="text"]').first(); await searchInput.waitFor({ state: 'visible', timeout: 10000 }); // Clear and fill the query await searchInput.clear(); await searchInput.fill(alert.query); await page.waitForTimeout(500); // Click "Show options" to expand settings const showOptions = page.getByText('Show options', { exact: false }).first(); if (await showOptions.isVisible()) { await showOptions.click(); await page.waitForTimeout(500); } // Configure settings - these selectors may need adjustment // How often: As-it-happens const frequencySelect = page.locator('select').first(); if (await frequencySelect.isVisible()) { await frequencySelect.selectOption('0'); // As-it-happens } // Sources: Automatic const sourcesSelect = page.locator('select').nth(1); if (await sourcesSelect.isVisible()) { await sourcesSelect.selectOption('automatic'); } // Language: English const languageSelect = page.locator('select').nth(2); if (await languageSelect.isVisible()) { await languageSelect.selectOption('en'); } // Region: Canada const regionSelect = page.locator('select').nth(3); if (await regionSelect.isVisible()) { await regionSelect.selectOption('ca'); } // How many: All results const howManySelect = page.locator('select').nth(4); if (await howManySelect.isVisible()) { await howManySelect.selectOption('all'); } // Deliver to: RSS feed const rssOption = page.getByText('RSS feed', { exact: false }).first(); if (await rssOption.isVisible()) { await rssOption.click(); } await page.waitForTimeout(500); // Click "Create Alert" const createButton = page.getByRole('button', { name: /Create Alert/i }).first(); await createButton.click(); // Wait for alert to be created await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Try to find and click RSS icon/link // The RSS feed URL might be in different places depending on Google's UI let rssUrl = null; // Method 1: Look for RSS icon/link in the alerts list const rssLink = page.locator('a[href*="feed"]').first(); const rssLinkVisible = await rssLink.isVisible().catch(() => false); if (rssLinkVisible) { const href = await rssLink.getAttribute('href'); if (href && href.includes('feed')) { rssUrl = href.startsWith('http') ? href : `https://www.google.com${href}`; } } // Method 2: Check if we're on a feed page if (!rssUrl && page.url().includes('feed')) { rssUrl = page.url(); } // Method 3: Look for feed URL in page content if (!rssUrl) { const feedMatch = await page.content().then(content => { const match = content.match(/https?:\/\/[^"'\s]*feed[^"'\s]*/i); return match ? match[0] : null; }); if (feedMatch) { rssUrl = feedMatch; } } if (rssUrl) { console.log(` āœ… RSS Feed: ${rssUrl}`); return { success: true, rssUrl, alertName: alert.name }; } else { console.log(` āš ļø Alert created but RSS URL not found automatically`); console.log(` šŸ’” You may need to manually get the RSS URL from the alerts page`); return { success: true, rssUrl: null, alertName: alert.name, needsManualCheck: true }; } } catch (error) { console.error(` āŒ Error creating alert: ${error.message}`); return { success: false, error: error.message, alertName: alert.name }; } } /** * Load existing RSS feeds */ async function loadRssFeeds() { if (existsSync(RSS_FEEDS_PATH)) { const content = await readFile(RSS_FEEDS_PATH, 'utf-8'); return JSON.parse(content); } return { alerts: [] }; } /** * Save RSS feeds */ async function saveRssFeeds(feeds) { await writeFile(RSS_FEEDS_PATH, JSON.stringify(feeds, null, 2)); console.log(`\nšŸ’¾ RSS feeds saved to ${RSS_FEEDS_PATH}`); } /** * Main execution */ async function main() { const markdownFile = process.argv[2]; if (!markdownFile) { console.error('Usage: node scripts/setup-alerts-automated.js '); console.error('Example: node scripts/setup-alerts-automated.js docs/google-alerts-reddit-tuned.md'); process.exit(1); } if (!existsSync(markdownFile)) { console.error(`Error: File not found: ${markdownFile}`); process.exit(1); } console.log('šŸ“– Parsing alerts from markdown file...'); const alerts = await parseAlertsFromMarkdown(markdownFile); console.log(`āœ… Found ${alerts.length} alerts to create\n`); if (alerts.length === 0) { console.error('No alerts found in file'); process.exit(1); } // Load existing RSS feeds const rssFeeds = await loadRssFeeds(); const existingAlerts = new Set(rssFeeds.alerts.map(a => a.name)); // Filter out already created alerts const newAlerts = alerts.filter(alert => !existingAlerts.has(alert.name)); if (newAlerts.length === 0) { console.log('āœ… All alerts already created!'); return; } console.log(`šŸ“‹ Will create ${newAlerts.length} new alerts\n`); // Setup browser const { browser, context } = await setupBrowser(); const page = await context.newPage(); try { // Ensure logged in await ensureLoggedIn(page); // Save auth state after login await saveAuthState(context); // Create alerts one at a time const results = []; for (let i = 0; i < newAlerts.length; i++) { const alert = newAlerts[i]; console.log(`\n[${i + 1}/${newAlerts.length}]`); const result = await createAlert(page, alert); results.push(result); // Add delay between alerts to avoid rate limiting if (i < newAlerts.length - 1) { console.log(' ā³ Waiting 3 seconds before next alert...'); await page.waitForTimeout(3000); } } // Collect RSS feeds const successful = results.filter(r => r.success && r.rssUrl); const needsManual = results.filter(r => r.success && !r.rssUrl); const failed = results.filter(r => !r.success); // Update RSS feeds file successful.forEach(result => { rssFeeds.alerts.push({ name: result.alertName, rssUrl: result.rssUrl, createdAt: new Date().toISOString() }); }); // Add placeholders for manual checks needsManual.forEach(result => { rssFeeds.alerts.push({ name: result.alertName, rssUrl: 'MANUAL_CHECK_NEEDED', createdAt: new Date().toISOString(), note: 'RSS URL needs to be retrieved manually' }); }); await saveRssFeeds(rssFeeds); // Summary console.log('\n' + '='.repeat(60)); console.log('šŸ“Š Summary:'); console.log(` āœ… Successfully created: ${successful.length}`); console.log(` āš ļø Needs manual RSS URL: ${needsManual.length}`); console.log(` āŒ Failed: ${failed.length}`); console.log('='.repeat(60)); if (needsManual.length > 0) { console.log('\nāš ļø Alerts that need manual RSS URL retrieval:'); needsManual.forEach(r => console.log(` - ${r.alertName}`)); } if (failed.length > 0) { console.log('\nāŒ Failed alerts:'); failed.forEach(r => console.log(` - ${r.alertName}: ${r.error}`)); } } finally { await browser.close(); } } main().catch(error => { console.error('Fatal error:', error); process.exit(1); });