rss-feedmonitor/scripts/setup-alerts-automated.js

440 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 <markdown-file>');
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);
});