This commit is contained in:
jjbrunton committed 2026-01-05 00:17:06 +00:00
1 parent b780bff850
commit 7b3ea37127
1752 files changed
+1341 -166

No files matched your search

+72 -1
View File
@@ -10,7 +10,7 @@ export class BrowserClient {
constructor(
private config: Config,
private logger: Logger
) {}
) { }
async initialize(): Promise<BrowserContext> {
const sessionDir = path.resolve(process.cwd(), this.config.browser.sessionDir);
@@ -82,4 +82,75 @@ export class BrowserClient {
return false;
}
}
async login(): Promise<boolean> {
if (!this.config.reddit.username || !this.config.reddit.password) {
this.logger.error('Login failed: Missing credentials in config/env');
return false;
}
try {
this.logger.info(`Attempting to log in as ${this.config.reddit.username}...`);
const page = await this.getPage();
// Navigate to login page
await page.goto('https://www.reddit.com/login', { waitUntil: 'domcontentloaded' });
// Wait for inputs
// Reddit uses various IDs but name="username" and name="password" are reliable
this.logger.debug('Waiting for login form...');
// Handle the new Reddit login UI (sometimes it's split into two steps or inside a shadow DOM/iframe,
// but usually the specific login page is standard)
await page.waitForSelector('input[name="username"]', { timeout: 10000 });
await page.waitForSelector('input[name="password"]', { timeout: 10000 });
// Fill credentials
await page.fill('input[name="username"]', this.config.reddit.username);
await page.fill('input[name="password"]', this.config.reddit.password);
// Click login button
// Look for a button with type="submit" or text containing "Log In"
await page.click('button[type="submit"]'); // Standard form submit
this.logger.info('Credentials submitted. Waiting for navigation...');
// Wait for successful login indicator
// We can reuse the checks from isSessionValid logic, but with a longer timeout
try {
await page.waitForSelector('[data-testid="user-drawer-button"]', { timeout: 20000 });
this.logger.info('Login successful!');
return true;
} catch (e) {
// Double check if we are redirected to home
const url = page.url();
if (url === 'https://www.reddit.com/' || url.match(/reddit\.com\/?$/)) {
// Verify one more time
const isValid = await this.isSessionValid();
if (isValid) {
this.logger.info('Login successful (verified by session check)');
return true;
}
}
// Check for error messages
const errorContent = await page.evaluate(() => {
const err = document.querySelector('[role="alert"], .AnimatedForm__errorMessage');
return err ? err.textContent : null;
});
if (errorContent) {
this.logger.error(`Login failed with error: ${errorContent}`);
} else {
this.logger.error('Login flow finished but session is invalid.');
}
return false;
}
} catch (error) {
this.logger.error('Error during automated login:', error);
return false;
}
}
}
+22 -19
View File
@@ -7,7 +7,7 @@ export class RedditMessagePage {
constructor(
private page: Page,
private logger: Logger
) {}
) { }
setDryRun(enabled: boolean): void {
this.dryRun = enabled;
@@ -60,7 +60,7 @@ export class RedditMessagePage {
}
}
if (!found) {
throw new Error('Could not find recipient input field');
throw new Error('Could not find recipient input field');
}
}
@@ -69,7 +69,7 @@ export class RedditMessagePage {
['faceplate-text-input[name="message-title"]', 'input[name="subject"]', '#subject'],
subject
);
if (!subjectFilled) {
throw new Error('Could not find subject input field');
}
@@ -98,8 +98,11 @@ export class RedditMessagePage {
return true;
}
// Find and click submit button
// Find and click submit button - prioritized by specificity to user provided snippet
const submitButton =
(await this.page.$('button[type="submit"]:has-text("Send")')) ||
(await this.page.$('button.button-primary:has-text("Send")')) ||
(await this.page.$('button:has(span:has-text("Send"))')) ||
(await this.page.$('button[type="submit"]')) ||
(await this.page.$('button:has-text("Send")')) ||
(await this.page.$('input[type="submit"]'));
@@ -114,7 +117,7 @@ export class RedditMessagePage {
await submitButton.waitForElementState('enabled', { timeout: 5000 });
} catch (e) {
this.logger.warn('Submit button still disabled after filling fields. Attempting to trigger validation...');
// Try to trigger validation by blurring/focusing the last field
await this.page.keyboard.press('Tab');
await this.page.waitForTimeout(500);
@@ -124,15 +127,15 @@ export class RedditMessagePage {
// One last check
const isEnabled = await submitButton.isEnabled();
if (!isEnabled) {
this.logger.warn('Button still disabled. Attempting force click...');
await submitButton.click({ force: true });
// If force click fails/doesn't work, we proceed to success check anyway
// (sometimes it works even if reported disabled in shadow dom scenarios)
this.logger.warn('Button still disabled. Attempting force click...');
await submitButton.click({ force: true });
// If force click fails/doesn't work, we proceed to success check anyway
// (sometimes it works even if reported disabled in shadow dom scenarios)
} else {
await submitButton.click();
await submitButton.click();
}
}
}
// If we didn't force click above (normal path)
// Actually, the try-catch block above handles all clicking scenarios.
// We don't need a second click here.
@@ -144,8 +147,8 @@ export class RedditMessagePage {
this.page.waitForSelector('.success, [class*="success"], [class*="Success"]', { timeout: 15000 }),
// Check for the specific success message in the new UI
this.page.waitForFunction(() => {
const confirmation = document.querySelector('.message-sent-confirmation-text');
return confirmation && !confirmation.hasAttribute('hidden') && confirmation.textContent?.trim().length > 0;
const confirmation = document.querySelector('.message-sent-confirmation-text');
return confirmation && !confirmation.hasAttribute('hidden') && confirmation.textContent?.trim().length > 0;
}, null, { timeout: 15000 })
]);
@@ -162,8 +165,8 @@ export class RedditMessagePage {
// Also check if the success message is visible now (in case race failed slightly early)
const successMsg = await this.page.$('.message-sent-confirmation-text:not([hidden])');
if (successMsg && (await successMsg.isVisible())) {
this.logger.info(`Message sent successfully to u/${recipient}`);
return true;
this.logger.info(`Message sent successfully to u/${recipient}`);
return true;
}
// Check for error messages
@@ -172,7 +175,7 @@ export class RedditMessagePage {
if (errorElement && await errorElement.isVisible()) {
const errorText = await errorElement.textContent();
if (errorText && errorText.trim().length > 0) {
throw new Error(`Reddit error: ${errorText}`);
throw new Error(`Reddit error: ${errorText}`);
}
}
@@ -181,7 +184,7 @@ export class RedditMessagePage {
// Or just log warning.
// For now, let's trust the user report that it works and be less aggressive with throwing.
this.logger.warn('Message send status unclear (no success indicator found, but no error either). Assuming success if no error alert appeared.');
return true;
return true;
}
} catch (error) {
this.logger.error(`Failed to send message to u/${recipient}:`, error);
@@ -198,7 +201,7 @@ export class RedditMessagePage {
// Clear potentially existing content if it's an input/textarea
// Note: .fill() usually clears, but for custom elements or div-inputs we might need to select all + delete
// For now, we assume click focuses it.
if (useDelay) {
await this.typeWithDelay(value);
} else {