Use full Chrome for interactive Reddit login

This commit is contained in:
jjbrunton committed 2026-08-08 15:30:24 +01:00
1 parent 70d62b2da3
commit 674cd89cd7
5 files changed
+241 -3

No files matched your search

+69 -3
View File
@@ -4,6 +4,25 @@ import type { Logger } from '../utils/logger.js';
import path from 'path';
import fs from 'fs';
export interface ConnectedBrowser {
contexts(): BrowserContext[];
close(): Promise<void>;
}
export interface BrowserRuntime {
connectOverCDP(endpoint: string): Promise<ConnectedBrowser>;
launchPersistentContext(
userDataDir: string,
options: Parameters<typeof chromium.launchPersistentContext>[1]
): Promise<BrowserContext>;
}
const defaultBrowserRuntime: BrowserRuntime = {
connectOverCDP: (endpoint) => chromium.connectOverCDP(endpoint),
launchPersistentContext: (userDataDir, options) =>
chromium.launchPersistentContext(userDataDir, options),
};
export type RedditSessionState =
| 'unknown'
| 'authenticated'
@@ -19,13 +38,16 @@ export function classifyRedditSessionError(error: unknown): Exclude<RedditSessio
export class BrowserClient {
private context: BrowserContext | null = null;
private connectedBrowser: ConnectedBrowser | null = null;
private usingExternalChrome = false;
private sessionDir: string | null = null;
private sessionValidityInFlight: Promise<boolean> | null = null;
private sessionState: RedditSessionState = 'unknown';
constructor(
private config: Config,
private logger: Logger
private logger: Logger,
private browserRuntime: BrowserRuntime = defaultBrowserRuntime
) { }
async initialize(): Promise<BrowserContext> {
@@ -37,10 +59,31 @@ export class BrowserClient {
fs.mkdirSync(sessionDir, { recursive: true });
}
this.logger.info(`Launching browser with session dir: ${sessionDir}`);
const cdpEndpoint = process.env.CHROME_CDP_URL?.trim();
if (cdpEndpoint) {
try {
this.logger.info(`Attaching to independently launched Google Chrome at ${cdpEndpoint}`);
const browser = await this.browserRuntime.connectOverCDP(cdpEndpoint);
const context = browser.contexts()[0];
if (!context) {
await browser.close().catch(() => undefined);
throw new Error('External Chrome did not expose a default browser context');
}
this.connectedBrowser = browser;
this.usingExternalChrome = true;
this.context = context;
this.logger.info('Attached to Google Chrome; interactive login is available through VNC');
return context;
} catch (error) {
this.logger.warn('Could not attach to Google Chrome; falling back to Playwright browser launch:', error);
}
}
this.logger.info(`Launching fallback Playwright browser with session dir: ${sessionDir}`);
// Use launchPersistentContext to maintain cookies/localStorage
this.context = await chromium.launchPersistentContext(sessionDir, {
this.context = await this.browserRuntime.launchPersistentContext(sessionDir, {
headless: this.config.browser.headless,
viewport: { width: 1280, height: 720 },
userAgent: this.config.reddit.userAgent,
@@ -70,6 +113,15 @@ export class BrowserClient {
}
async close(): Promise<void> {
if (this.connectedBrowser) {
this.logger.info('Closing Google Chrome connection');
await this.connectedBrowser.close();
this.connectedBrowser = null;
this.context = null;
this.usingExternalChrome = false;
return;
}
if (this.context) {
this.logger.info('Closing browser');
await this.context.close();
@@ -150,6 +202,20 @@ export class BrowserClient {
}
async login(): Promise<boolean> {
if (this.usingExternalChrome) {
try {
const page = await this.getPage();
await page.bringToFront();
await page.goto('https://www.reddit.com/login', { waitUntil: 'domcontentloaded' });
this.sessionState = 'login-required';
this.logger.warn('Interactive Reddit login required. Open the VNC page and sign in using Google Chrome.');
} catch (error) {
this.sessionState = classifyRedditSessionError(error);
this.logger.error('Failed to open Google Chrome for interactive login:', error);
}
return false;
}
if (!this.config.reddit.username || !this.config.reddit.password) {
this.sessionState = 'login-required';
this.logger.error('Login failed: Missing credentials in config/env');