diff --git a/services/twitter-services/package.json b/services/twitter-services/package.json index 874a200..61e4377 100644 --- a/services/twitter-services/package.json +++ b/services/twitter-services/package.json @@ -7,7 +7,6 @@ "license": "MIT", "scripts": { "dev": "tsx src/main.ts", - "dev:mcp": "tsx src/mcp-server.ts", "mcp:ui": "pnpx @modelcontextprotocol/inspector", "postinstall": "playwright install chromium" }, diff --git a/services/twitter-services/src/adapters/browser-adapter.ts b/services/twitter-services/src/adapters/browser-adapter.ts deleted file mode 100644 index 0c2f260..0000000 --- a/services/twitter-services/src/adapters/browser-adapter.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { Buffer } from 'node:buffer' -import type { BrowserConfig, ElementHandle, WaitOptions } from '../types/browser' - -/** - * Generic browser adapter interface - * Defines the basic operations required for interacting with different browser backends - */ -export interface BrowserAdapter { - /** - * Initialize browser session - */ - initialize: (config: BrowserConfig) => Promise - - /** - * Navigate to specified URL - */ - navigate: (url: string) => Promise - - /** - * Execute JavaScript script - */ - executeScript: (script: string) => Promise - - /** - * Wait for element to appear - */ - waitForSelector: (selector: string, options?: WaitOptions) => Promise - - /** - * Click element - */ - click: (selector: string) => Promise - - /** - * Type text into input - */ - type: (selector: string, text: string) => Promise - - /** - * Get element text content - */ - getText: (selector: string) => Promise - - /** - * Get multiple element handles - */ - getElements: (selector: string) => Promise - - /** - * Get all cookies from the browser context - * This includes HTTP_ONLY cookies that can't be accessed via document.cookie - */ - getAllCookies: () => Promise> - - /** - * Set cookies in the browser context - * This can set HTTP_ONLY cookies that can't be set via document.cookie - */ - setCookies: (cookies: Array<{ - name: string - value: string - domain?: string - path?: string - expires?: number - httpOnly?: boolean - secure?: boolean - sameSite?: 'Strict' | 'Lax' | 'None' - }>) => Promise - - /** - * Get screenshot - */ - getScreenshot: () => Promise - - /** - * Close browser session - */ - close: () => Promise -} diff --git a/services/twitter-services/src/adapters/browserbase-adapter.ts b/services/twitter-services/src/adapters/browserbase-adapter.ts deleted file mode 100644 index d19693c..0000000 --- a/services/twitter-services/src/adapters/browserbase-adapter.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { Buffer } from 'node:buffer' -import type { StagehandClientOptions } from '../browser/browserbase' -import type { BrowserConfig, ElementHandle, WaitOptions } from '../types/browser' -import type { BrowserAdapter } from './browser-adapter' - -import { StagehandClient } from '../browser/browserbase' -import { errorToMessage } from '../utils/error' -import { logger } from '../utils/logger' - -/** - * Stagehand element handle implementation - */ -class StagehandElementHandle implements ElementHandle { - private client: StagehandClient - private selector: string - - constructor(client: StagehandClient, selector: string) { - this.client = client - this.selector = selector - } - - async getText(): Promise { - return this.client.executeScript(` - document.querySelector('${this.selector}').textContent.trim() - `) - } - - async getAttribute(name: string): Promise { - return this.client.executeScript(` - document.querySelector('${this.selector}').getAttribute('${name}') - `) - } - - async click(): Promise { - await this.client.click(this.selector) - } - - async type(text: string): Promise { - await this.client.type(this.selector, text) - } -} - -/** - * Stagehand browser adapter implementation - * Adapts the Stagehand API to a common browser interface - */ -export class StagehandBrowserAdapter implements BrowserAdapter { - private client: StagehandClient - - constructor(apiKey: string, baseUrl?: string, options: Partial = {}) { - this.client = new StagehandClient({ - apiKey, - baseUrl, - ...options, - }) - } - - async initialize(config: BrowserConfig): Promise { - try { - await this.client.createSession({ - headless: config.headless, - userAgent: config.userAgent, - viewport: config.viewport, - }) - logger.browser.withFields({ - headless: config.headless, - }).log('Browser session created') - } - catch (error) { - logger.browser.withError(error).error('Failed to initialize browser') - throw new Error(`Unable to initialize browser: ${errorToMessage(error)}`) - } - } - - async navigate(url: string): Promise { - await this.client.navigate(url) - } - - async executeScript(script: string): Promise { - return this.client.executeScript(script) - } - - async waitForSelector(selector: string, options?: WaitOptions): Promise { - await this.client.waitForSelector(selector, { - timeout: options?.timeout, - }) - } - - async click(selector: string): Promise { - await this.client.click(selector) - } - - async type(selector: string, text: string): Promise { - await this.client.type(selector, text) - } - - async getText(selector: string): Promise { - return this.client.getText(selector) - } - - async getElements(selector: string): Promise { - // Get all matching element selectors - const selectors = await this.executeScript(` - Array.from(document.querySelectorAll('${selector}')).map((el, i) => { - const uniqueId = 'stagehand-' + Date.now() + '-' + i; - el.setAttribute('data-stagehand-id', uniqueId); - return '[data-stagehand-id="' + uniqueId + '"]'; - }) - `) - - // Create an ElementHandle for each match - return selectors.map(selector => new StagehandElementHandle(this.client, selector)) - } - - // Add Stagehand specific methods - async act(instruction: string): Promise { - await this.client.act(instruction) - } - - async getScreenshot(): Promise { - return this.client.getScreenshot() - } - - async getAllCookies(): Promise> { - return this.client.getAllCookies() - } - - async setCookies(cookies: Array<{ - name: string - value: string - domain?: string - path?: string - expires?: number - httpOnly?: boolean - secure?: boolean - sameSite?: 'Strict' | 'Lax' | 'None' - }>): Promise { - return this.client.setCookies(cookies) - } - - async close(): Promise { - await this.client.closeSession() - } -} diff --git a/services/twitter-services/src/browser/browserbase.ts b/services/twitter-services/src/browser/browserbase.ts deleted file mode 100644 index 4d5ca27..0000000 --- a/services/twitter-services/src/browser/browserbase.ts +++ /dev/null @@ -1,314 +0,0 @@ -import type { Buffer } from 'node:buffer' -import type { Browser, Page } from 'playwright' -import type { z } from 'zod' - -import { chromium } from 'playwright' - -import { logger } from '../utils/logger' - -/** - * Stagehand client configuration options - */ -export interface StagehandClientOptions { - apiKey: string - baseUrl?: string - timeout?: number - headless?: boolean - userAgent?: string - viewport?: { width: number, height: number } -} - -/** - * Stagehand client - * Implements browser automation using @browserbasehq/stagehand - */ -export class StagehandClient { - private browser: Browser | null = null - private page: Page | null = null - private apiKey: string - private options: Omit - - constructor(options: StagehandClientOptions) { - const { - apiKey, - baseUrl, - timeout = 30000, - headless = true, - userAgent, - viewport = { width: 1280, height: 800 }, - } = options - - this.apiKey = apiKey - this.options = { - baseUrl, - timeout, - headless, - userAgent, - viewport, - } - } - - /** - * Create browser session - */ - async createSession(options?: { - headless?: boolean - userAgent?: string - viewport?: { width: number, height: number } - }): Promise { - try { - // Launch Playwright browser - this.browser = await chromium.launch({ - headless: options?.headless ?? this.options.headless, - }) - - // Create context - const context = await this.browser.newContext({ - userAgent: options?.userAgent ?? this.options.userAgent, - viewport: options?.viewport ?? this.options.viewport, - // Set any other required browser context options - }) - - // Create page - this.page = await context.newPage() - - // Add Stagehand extension to page - await this.setupStagehand() - - const sessionId = `session-${Date.now()}` - logger.browser.withField('sessionId', sessionId).log('Browser session created successfully') - return sessionId - } - catch (error) { - logger.browser.errorWithError('Failed to create browser session', error) - throw error - } - } - - /** - * Set up Stagehand extension - * This adds act, extract, observe methods to the page object - */ - private async setupStagehand(): Promise { - if (!this.page) { - throw new Error('No active page. Call createSession first.') - } - - // In actual implementation, this would use Stagehand's API to set up the page object - // This might involve page extension or injecting Stagehand functionality - // Example code (actual usage would need to be adjusted based on Stagehand's documentation): - // - // import { extendPage } from '@browserbasehq/stagehand' - // await extendPage(this.page, { - // apiKey: this.apiKey, - // // Other Stagehand options - // }) - } - - /** - * Navigate to specified URL - */ - async navigate(url: string): Promise { - this.ensurePageExists() - await this.page!.goto(url, { timeout: this.options.timeout }) - } - - /** - * Execute JavaScript script - */ - async executeScript(script: string): Promise { - this.ensurePageExists() - return await this.page!.evaluate(script) as T - } - - /** - * Use Stagehand's act API to perform operations - */ - async act(instruction: string): Promise { - this.ensurePageExists() - - // In actual implementation, this would use Stagehand's act API - // Example: await this.page!.act(instruction) - - // Temporary implementation, simulating act behavior with Playwright basics - logger.browser.withField('instruction', instruction).log('Executing act instruction') - - // Simulate act behavior through simple methods - // Actual implementation would use Stagehand's act API - if (instruction.includes('click')) { - const match = instruction.match(/click on the ['"](.+?)['"]/) - if (match && match[1]) { - await this.page!.getByText(match[1]).first().click() - } - } - } - - /** - * Use Stagehand's extract API to extract data - */ - async extract({ - instruction, - _schema, - }: { - instruction: string - _schema: T - }): Promise> { - this.ensurePageExists() - - // In actual implementation, this would use Stagehand's extract API - // Example: return await this.page!.extract({ instruction, schema }) - - // Temporary implementation, log instruction and return empty object - logger.browser.withField('instruction', instruction).log('Executing extract instruction') - - // Simply return an empty object - // Actual implementation would use Stagehand's extract API - return {} as z.infer - } - - /** - * Use Stagehand's observe API to observe page state - */ - async observe(instruction: string): Promise { - this.ensurePageExists() - - // In actual implementation, this would use Stagehand's observe API - // Example: return await this.page!.observe(instruction) - - // Temporary implementation, log instruction and return empty string - logger.browser.withField('instruction', instruction).log('Executing observe instruction') - - // Simply return an empty string - // Actual implementation would use Stagehand's observe API - return '' - } - - /** - * Get page content - */ - async getContent(): Promise { - this.ensurePageExists() - return await this.page!.content() - } - - /** - * Wait for element to appear - */ - async waitForSelector(selector: string, options: { timeout?: number } = {}): Promise { - this.ensurePageExists() - await this.page!.waitForSelector(selector, { - timeout: options.timeout || this.options.timeout, - }) - } - - /** - * Click element - */ - async click(selector: string): Promise { - this.ensurePageExists() - await this.page!.click(selector) - } - - /** - * Type text into input field - */ - async type(selector: string, text: string): Promise { - this.ensurePageExists() - // Clear input field first - await this.page!.fill(selector, '') - // Then type text - await this.page!.fill(selector, text) - } - - /** - * Get element text content - */ - async getText(selector: string): Promise { - this.ensurePageExists() - const element = await this.page!.$(selector) - if (!element) { - throw new Error(`Element not found: ${selector}`) - } - return (await element.textContent() || '').trim() - } - - /** - * Get screenshot - */ - async getScreenshot(): Promise { - this.ensurePageExists() - return await this.page!.screenshot() as Buffer - } - - /** - * Get all cookies from browser context - * This includes HTTP_ONLY cookies that can't be accessed via document.cookie - */ - async getAllCookies(): Promise> { - this.ensurePageExists() - const context = this.page!.context() - return await context.cookies() - } - - /** - * Set cookies in browser context - * This can set HTTP_ONLY cookies that can't be set via document.cookie - */ - async setCookies(cookies: Array<{ - name: string - value: string - domain?: string - path?: string - expires?: number - httpOnly?: boolean - secure?: boolean - sameSite?: 'Strict' | 'Lax' | 'None' - }>): Promise { - this.ensurePageExists() - const context = this.page!.context() - // Format cookies correctly for Playwright - const formattedCookies = cookies.map((cookie) => { - // Ensure domain is set for Twitter - if (!cookie.domain) { - cookie.domain = '.twitter.com' - } - // Ensure path is set - if (!cookie.path) { - cookie.path = '/' - } - return cookie - }) - - await context.addCookies(formattedCookies) - } - - /** - * Close session - */ - async closeSession(): Promise { - if (this.browser) { - await this.browser.close() - this.browser = null - this.page = null - logger.browser.log('Browser session closed') - } - } - - /** - * Ensure page exists - */ - private ensurePageExists(): void { - if (!this.page) { - throw new Error('No active page. Call createSession first.') - } - } -} diff --git a/services/twitter-services/src/core/auth-service.ts b/services/twitter-services/src/core/auth-service.ts index d21a8a3..58d2462 100644 --- a/services/twitter-services/src/core/auth-service.ts +++ b/services/twitter-services/src/core/auth-service.ts @@ -1,21 +1,99 @@ -import type { BrowserAdapter } from '../adapters/browser-adapter' +import type { BrowserContext, Cookie, Page } from 'playwright' import type { TwitterCredentials } from '../types/twitter' -import type { SessionData } from '../utils/session-manager' + +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' import { logger } from '../utils/logger' import { SELECTORS } from '../utils/selectors' -import { getSessionManager } from '../utils/session-manager' + +/** + * Playwright storage state type definition + */ +interface StorageState { + cookies: Cookie[] + origins: { + origin: string + localStorage: { + name: string + value: string + }[] + }[] + path?: string +} + +/** + * Simple session manager for storing and retrieving browser session data + */ +class SessionManager { + private sessionPath: string + + constructor() { + this.sessionPath = path.join(process.cwd(), 'data', 'twitter-session.json') + } + + /** + * Load storage state from disk + */ + async loadStorageState(): Promise { + try { + // Ensure directory exists + const dir = path.dirname(this.sessionPath) + await fs.mkdir(dir, { recursive: true }) + + // Check if file exists + try { + await fs.access(this.sessionPath) + } + catch { + // File doesn't exist + return null + } + + // Read file + const data = await fs.readFile(this.sessionPath, 'utf-8') + return JSON.parse(data) as StorageState + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to load session data') + return null + } + } + + /** + * Save storage state to disk + */ + async saveStorageState(state: StorageState): Promise { + try { + // Ensure directory exists + const dir = path.dirname(this.sessionPath) + await fs.mkdir(dir, { recursive: true }) + + // Write to file + await fs.writeFile(this.sessionPath, JSON.stringify(state, null, 2)) + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to save session data') + } + } +} + +// Singleton instance +const sessionManager = new SessionManager() /** * Twitter Authentication Service * Handles login and session management */ export class TwitterAuthService { - private browser: BrowserAdapter + private page: Page + private context: BrowserContext private isLoggedIn: boolean = false - constructor(browser: BrowserAdapter) { - this.browser = browser + constructor(page: Page, context: BrowserContext) { + this.page = page + this.context = context } /** @@ -26,52 +104,30 @@ export class TwitterAuthService { logger.auth.log('Starting Twitter login process') try { - // First try to load session from file if no cookies provided - if (!credentials.cookies || Object.keys(credentials.cookies).length === 0) { - logger.auth.log('No cookies provided, attempting to load session from file') - - // Get the session manager and load the session - const sessionManager = getSessionManager() - const sessionData = await sessionManager.loadSession() - - if (sessionData && sessionData.cookies && Object.keys(sessionData.cookies).length > 0) { - logger.auth.log(`Found session file with ${Object.keys(sessionData.cookies).length} cookies, attempting login`) - - // Use the dedicated method for session-based login - const sessionLoginSuccess = await this.loginWithSessionData(sessionData) - if (sessionLoginSuccess) { - logger.auth.log('✅ Successfully logged in using saved session') - return true - } - logger.auth.log('Session login failed, continuing with other login methods') - } - else { - logger.auth.log('No valid session file found') - } - } - - // If cookies are provided directly, try to use them for login + // Check if cookies are provided if (credentials.cookies && Object.keys(credentials.cookies).length > 0) { - logger.auth.log('Cookies provided, attempting cookie-based login') - const cookieLoginSuccess = await this.loginWithCookies(credentials.cookies) - if (cookieLoginSuccess) { - return true - } - // If cookie login fails, log the issue but continue to manual login - logger.auth.log('Cookie login failed, falling back to manual login') + logger.auth.log(`Attempting to login with ${Object.keys(credentials.cookies).length} provided cookies`) + return await this.loginWithCookies(credentials.cookies) } - // Check for existing session first - logger.auth.log('Checking for existing session before initiating manual login') - const existingSession = await this.checkExistingSession() - if (existingSession) { - logger.auth.log('✅ Successfully logged in using existing browser session') + // Try to login with existing session first + logger.auth.log('No cookies provided, attempting to load session from file') + const sessionSuccess = await this.checkExistingSession() + + if (sessionSuccess) { + logger.auth.log('Successfully logged in with session file') return true } - // Fallback to manual login flow - logger.auth.log('No existing session found, initiating manual login process') - return this.initiateManualLogin() + // If credentials are provided, try username/password login + if (credentials.username && credentials.password) { + logger.auth.log('Session login failed, attempting username/password login') + return await this.initiateManualLogin(credentials.username, credentials.password) + } + + // No credentials and no session, fail + logger.auth.warn('No cookies, no valid session, and no credentials provided') + return false } catch (error: unknown) { logger.auth.withError(error as Error).error('Login process failed') @@ -88,7 +144,7 @@ export class TwitterAuthService { // Try multiple selectors to determine login status // First check for timeline which is definitive proof of being logged in try { - await this.browser.waitForSelector(SELECTORS.HOME.TIMELINE, { timeout: 15000 }) + await this.page.waitForSelector(SELECTORS.HOME.TIMELINE, { timeout: 15000 }) return true } catch { @@ -98,7 +154,7 @@ export class TwitterAuthService { // Check for profile button which appears when logged in try { const profileSelector = '[data-testid="AppTabBar_Profile_Link"]' - await this.browser.waitForSelector(profileSelector, { timeout: 5000 }) + await this.page.waitForSelector(profileSelector, { timeout: 5000 }) return true } catch { @@ -108,7 +164,7 @@ export class TwitterAuthService { // Check for login form to confirm NOT logged in try { const loginFormSelector = '[data-testid="loginForm"]' - await this.browser.waitForSelector(loginFormSelector, { timeout: 3000 }) + await this.page.waitForSelector(loginFormSelector, { timeout: 3000 }) // If login form is visible, we're definitely not logged in return false } @@ -118,7 +174,7 @@ export class TwitterAuthService { // If we got here, we couldn't definitively confirm login status // Check current URL for additional clues - const currentUrl = await this.browser.executeScript(` + const currentUrl = await this.page.evaluate(` (() => { return window.location.href; })() @@ -142,7 +198,7 @@ export class TwitterAuthService { */ async checkLoginStatus(): Promise { try { - await this.browser.navigate('https://twitter.com/home') + await this.page.goto('https://twitter.com/home') return await this.verifyLogin() } catch { @@ -158,15 +214,13 @@ export class TwitterAuthService { } /** - * Export current session cookies - * Can be used to save and reuse session later - * @param format - The format of the returned cookies ('object' or 'string') + * Export current cookies + * @param format Export format, either 'object' or 'string' */ async exportCookies(format: 'object' | 'string' = 'object'): Promise | string> { try { - // Use the new getAllCookies method to get all cookies, including HTTP_ONLY ones - const allCookies = await this.browser.getAllCookies() - logger.auth.log(`Retrieved ${allCookies.length} cookies from browser context`) + // Get all cookies from browser + const allCookies = await this.context.cookies() if (format === 'string') { // Convert cookie objects to string format @@ -188,9 +242,12 @@ export class TwitterAuthService { return cookiesObj } } - catch (error: unknown) { - logger.auth.withError(error as Error).error('Error exporting cookies') - return format === 'string' ? '' : {} + catch (error) { + logger.auth.withError(error as Error).error('Failed to export cookies') + if (format === 'string') { + return '' + } + return {} } } @@ -202,7 +259,7 @@ export class TwitterAuthService { try { // Navigate to a Twitter page - await this.browser.navigate('https://twitter.com') + await this.page.goto('https://twitter.com') // Convert cookies object to array format required by setCookies const cookieArray = Object.entries(cookies).map(([name, value]) => ({ @@ -213,12 +270,12 @@ export class TwitterAuthService { })) // Set cookies using the browser adapter's API that can set HTTP_ONLY cookies - await this.browser.setCookies(cookieArray) + await this.context.addCookies(cookieArray) logger.auth.log(`Set ${cookieArray.length} cookies via browser API`) // Refresh page to apply cookies - await this.browser.navigate('https://twitter.com/home') + await this.page.goto('https://twitter.com/home') // Verify if login was successful - try multiple times with longer timeout logger.auth.log('Cookies set, verifying login status...') @@ -239,7 +296,7 @@ export class TwitterAuthService { else if (attempt < verificationAttempts) { // If not successful but not last attempt, refresh page and wait logger.auth.log('Refreshing page and trying again...') - await this.browser.navigate('https://twitter.com/home') + await this.page.goto('https://twitter.com/home') await new Promise(resolve => setTimeout(resolve, 3000)) } } @@ -275,85 +332,94 @@ export class TwitterAuthService { } /** - * Checks if there's an existing login session and retrieves it - * This should be called before initiateManualLogin + * Attempt to login with an existing session if available */ async checkExistingSession(): Promise { - logger.auth.log('Checking for existing Twitter session') - try { - // Navigate to home page to check session - await this.browser.navigate('https://twitter.com/home') + // Get the session data + const sessionData = await sessionManager.loadStorageState() - // Verify if login is active - const loginSuccess = await this.verifyLogin() - - if (loginSuccess) { - logger.auth.log('Existing session found and valid') - this.isLoggedIn = true - - // Export and save cookies - try { - const cookies = await this.exportCookies('object') - logger.auth.log(`✅ Exported ${typeof cookies === 'string' ? cookies.length : Object.keys(cookies).length} cookies from existing session`) - - // Save the current session to file - await this.saveCurrentSession() - logger.auth.log('✅ Session saved to file') - } - catch (error) { - logger.auth.withError(error as Error).error('Error exporting cookies from existing session') - } - } - else { - logger.auth.log('No valid session found') + if (!sessionData || !sessionData.cookies || sessionData.cookies.length === 0) { + logger.auth.log('No valid session data found') + return false } - return loginSuccess + logger.auth.log(`Found session file with ${sessionData.cookies.length} cookies, attempting login`) + + // Login with the session data + return await this.loginWithSessionData(sessionData) } catch (error) { - logger.auth.withError(error as Error).error('Error checking session status') - this.isLoggedIn = false + logger.auth.withError(error as Error).warn('Error checking existing session') return false } } /** - * Initiates the manual login process by navigating to Twitter login page - * and waits for user to complete the login process + * Initiate manual login process with username and password + * @param username Twitter username or email + * @param password Twitter password */ - async initiateManualLogin(): Promise { - logger.auth.log('Opening Twitter login page for manual login') + async initiateManualLogin(username?: string, password?: string): Promise { + logger.auth.log('Initiating manual login process') try { - // Store the current URL to detect navigation - const initialUrl = await this.browser.executeScript(` - (() => { - return window.location.href; - })() - `) - // Navigate to login page - await this.browser.navigate('https://twitter.com/i/flow/login') + await this.page.goto('https://twitter.com/login') + + // Wait for login form to appear and enter credentials + try { + // Wait for username input + await this.page.waitForSelector(SELECTORS.LOGIN.USERNAME_INPUT, { timeout: 10000 }) + + // Use provided credentials if available, otherwise fall back to env vars + const loginUsername = username || process.env.TWITTER_USERNAME + const loginPassword = password || process.env.TWITTER_PASSWORD - // Wait for user to manually log in (detected by timeline presence) - logger.auth.log('==============================================') - logger.auth.log('Please log in to Twitter in the opened browser window') - logger.auth.log('The system will wait for you to complete the login process') - logger.auth.log('Cookies will be automatically saved after login') - logger.auth.log('==============================================') + if (!loginUsername || !loginPassword) { + logger.auth.warn('Missing Twitter credentials, manual login cannot proceed') + return false + } + + // Enter username + await this.page.fill(SELECTORS.LOGIN.USERNAME_INPUT, loginUsername) + logger.auth.debug('Username entered') + + // Click next button + await this.page.click(SELECTORS.LOGIN.NEXT_BUTTON) + logger.auth.debug('Next button clicked') + + // Wait for password input + await this.page.waitForSelector(SELECTORS.LOGIN.PASSWORD_INPUT, { timeout: 10000 }) + + // Enter password + await this.page.fill(SELECTORS.LOGIN.PASSWORD_INPUT, loginPassword) + logger.auth.debug('Password entered') + + // Click login button + await this.page.click(SELECTORS.LOGIN.LOGIN_BUTTON) + logger.auth.debug('Login button clicked') + } + catch (error) { + logger.auth.withError(error as Error).error('Error during manual login process') + return false + } - // Poll for login success at intervals + // Wait for login success at intervals let attempts = 0 const maxAttempts = 60 // 10 minutes (10 seconds * 60) - let lastUrl = initialUrl + let lastUrl = await this.page.evaluate(` + (() => { + return window.location.href; + })() + `) while (attempts < maxAttempts) { attempts++ try { // Get current URL to detect page changes - const currentUrl = await this.browser.executeScript(` + const currentUrl = await this.page.evaluate(` (() => { return window.location.href; })() @@ -365,7 +431,7 @@ export class TwitterAuthService { logger.auth.log('Attempting to navigate to home page and verify login status') // URL changed - try navigating to home to verify - await this.browser.navigate('https://twitter.com/home') + await this.page.goto('https://twitter.com/home') // Check if login was successful const isLoggedIn = await this.verifyLogin() @@ -439,114 +505,92 @@ export class TwitterAuthService { } /** - * Save the current session to a file for future use. - * This includes cookies and authentication details. + * Save the current session to a file */ async saveCurrentSession(): Promise { try { - // Export cookies in object format + // Get the cookies from the browser const cookies = await this.exportCookies('object') - if (!cookies || (typeof cookies === 'object' && Object.keys(cookies).length === 0)) { - logger.auth.warn('No cookies available to save') - return - } + // Format the cookies for the session manager + // We need to convert Record to the Cookie[] format + const cookieArray = Object.entries(cookies).map(([name, value]) => ({ + name, + value, // This will be a string now + domain: '.twitter.com', + path: '/', + expires: -1, // Session cookie + })) - // Create a session data object - const sessionData: SessionData = { - cookies: typeof cookies === 'string' ? {} : cookies, - timestamp: new Date().toISOString(), - userAgent: await this.browser.executeScript(` - (() => { - return navigator.userAgent; - })() - `), + // Get the storage state from the browser + const storageState = await this.context.storageState() + + // Create a new session data object with proper type cast + const sessionData: StorageState = { + cookies: cookieArray.map(cookie => ({ + ...cookie, + sameSite: 'Lax' as const, + secure: true, + httpOnly: true, + })), + origins: storageState.origins, + // Don't include path property here as it's not needed for saving } - // Get the session manager and save the session - const sessionManager = getSessionManager() - await sessionManager.saveSession(sessionData) + // Save the session + await sessionManager.saveStorageState(sessionData) logger.auth.log(`Session saved with ${typeof cookies === 'object' ? Object.keys(cookies).length : 0} cookies`) } - catch (error: unknown) { - logger.auth.withError(error as Error).error('Failed to save session') + catch (error) { + logger.auth.withError(error as Error).warn('Failed to save session') } } /** - * Login with a saved session data object - * @param sessionData The session data with cookies to use for login + * Login with stored session data */ - private async loginWithSessionData(sessionData: SessionData): Promise { - logger.auth.log(`Attempting to login using session with ${Object.keys(sessionData.cookies).length} cookies`) - + private async loginWithSessionData(sessionData: StorageState): Promise { try { - // Navigate to a Twitter page - await this.browser.navigate('https://twitter.com') - - // Convert cookies object to array format required by setCookies - const cookieArray = Object.entries(sessionData.cookies).map(([name, value]) => ({ - name, - value, - domain: '.twitter.com', - path: '/', + // Extract cookies from session data + const { cookies } = sessionData + + // Create array of cookie objects for the browser + const cookieArray = cookies.map(cookie => ({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path, })) // Set cookies using the browser adapter's API - await this.browser.setCookies(cookieArray) + await this.context.addCookies(cookieArray) logger.auth.log(`Set ${cookieArray.length} cookies from session file`) - // Continue with similar verification logic as loginWithCookies - await this.browser.navigate('https://twitter.com/home') - - // Verify login status - try multiple times - logger.auth.log('Cookies set from session file, verifying login status...') - - let loginSuccess = false - const verificationAttempts = 3 + // Set localStorage if available + if (sessionData.origins && sessionData.origins.length > 0) { + await this.context.storageState(sessionData) + logger.auth.log(`Set localStorage for ${sessionData.origins.length} origins`) + } - for (let attempt = 1; attempt <= verificationAttempts; attempt++) { - try { - logger.auth.log(`Verification attempt ${attempt}/${verificationAttempts}`) - loginSuccess = await this.verifyLogin() + // Navigate to home to verify login + await this.page.goto('https://twitter.com/home') - if (loginSuccess) { - break - } - else if (attempt < verificationAttempts) { - logger.auth.log('Refreshing page and trying again...') - await this.browser.navigate('https://twitter.com/home') - await new Promise(resolve => setTimeout(resolve, 3000)) - } - } - catch (error: unknown) { - logger.auth.withError(error as Error).debug(`Verification attempt ${attempt} failed`) - } - } + // Verify if login was successful + const loginSuccess = await this.verifyLogin() if (loginSuccess) { - logger.auth.log('Login with session data successful') this.isLoggedIn = true - - // Try to refresh cookies to ensure they're up to date - try { - await this.saveCurrentSession() - logger.auth.log('✅ Session refreshed and saved to file') - } - catch (error: unknown) { - logger.auth.withError(error as Error).debug('Failed to update cookies, but login was successful') - } + logger.auth.log('✅ Successfully logged in with session data') } else { - logger.auth.warn('Login with session data verification failed, cookies may be expired') + logger.auth.warn('⚠️ Session data login failed verification') } return loginSuccess } - catch (error: unknown) { - logger.auth.withError(error as Error).error('Error during session data login process') - this.isLoggedIn = false + catch (error) { + logger.auth.withError(error as Error).error('Failed to login with session data') return false } } diff --git a/services/twitter-services/src/core/timeline-service.ts b/services/twitter-services/src/core/timeline-service.ts index aeb76c3..0e0fb17 100644 --- a/services/twitter-services/src/core/timeline-service.ts +++ b/services/twitter-services/src/core/timeline-service.ts @@ -1,8 +1,8 @@ -import type { BrowserAdapter } from '../adapters/browser-adapter' +import type { Page } from 'playwright' import type { TimelineOptions, Tweet } from '../types/twitter' import { TweetParser } from '../parsers/tweet-parser' -import { RateLimiter } from '../utils/rate-limiter' +import { logger } from '../utils/logger' import { SELECTORS } from '../utils/selectors' /** @@ -10,48 +10,41 @@ import { SELECTORS } from '../utils/selectors' * Handles fetching and parsing timeline content */ export class TwitterTimelineService { - private browser: BrowserAdapter - private rateLimiter: RateLimiter + private page: Page - constructor(browser: BrowserAdapter) { - this.browser = browser - this.rateLimiter = new RateLimiter(10, 60000) // 10 requests per minute + constructor(page: Page) { + this.page = page } /** * Get timeline */ async getTimeline(options: TimelineOptions = {}): Promise { - // Wait for rate limit - await this.rateLimiter.waitUntilReady() - try { // Navigate to home page - await this.browser.navigate('https://twitter.com/home') + await this.page.goto('https://twitter.com/home') // Wait for timeline to load - await this.browser.waitForSelector(SELECTORS.TIMELINE.TWEET) - - // Delay a bit to ensure content is fully loaded - await new Promise(resolve => setTimeout(resolve, 2000)) + await this.page.waitForSelector(SELECTORS.TIMELINE.TWEET, { timeout: 10000 }) - // Get page HTML content - const html = await this.browser.executeScript('document.documentElement.outerHTML') - - // Parse tweets + // Get page HTML and parse all tweets + const html = await this.page.content() const tweets = TweetParser.parseTimelineTweets(html) - // Apply filtering and limits + logger.main.log(`Found ${tweets.length} tweets in timeline`) + + // Apply filters let filteredTweets = tweets if (options.includeReplies === false) { - filteredTweets = filteredTweets.filter(tweet => !tweet.id.includes('reply')) + filteredTweets = filteredTweets.filter(tweet => !tweet.text.startsWith('@')) } if (options.includeRetweets === false) { - filteredTweets = filteredTweets.filter(tweet => !tweet.id.includes('retweet')) + filteredTweets = filteredTweets.filter(tweet => !tweet.text.startsWith('RT @')) } + // Apply count limit if specified if (options.count) { filteredTweets = filteredTweets.slice(0, options.count) } @@ -59,7 +52,7 @@ export class TwitterTimelineService { return filteredTweets } catch (error) { - console.error('Failed to get timeline:', error) + logger.main.withError(error as Error).error('Failed to get timeline') return [] } } diff --git a/services/twitter-services/src/core/twitter-service.ts b/services/twitter-services/src/core/twitter-service.ts index c5f60bd..1d2b485 100644 --- a/services/twitter-services/src/core/twitter-service.ts +++ b/services/twitter-services/src/core/twitter-service.ts @@ -1,4 +1,3 @@ -import type { BrowserAdapter } from '../adapters/browser-adapter' import type { TwitterService as ITwitterService, PostOptions, @@ -9,23 +8,20 @@ import type { TwitterCredentials, UserProfile, } from '../types/twitter' - -import { TwitterAuthService } from './auth-service' -import { TwitterTimelineService } from './timeline-service' +import type { TwitterAuthService } from './auth-service' +import type { TwitterTimelineService } from './timeline-service' /** * Twitter service implementation * Integrates various service components, providing a unified interface */ export class TwitterService implements ITwitterService { - private browser: BrowserAdapter private authService: TwitterAuthService private timelineService: TwitterTimelineService - constructor(browser: BrowserAdapter) { - this.browser = browser - this.authService = new TwitterAuthService(browser) - this.timelineService = new TwitterTimelineService(browser) + constructor(authService: TwitterAuthService, timelineService: TwitterTimelineService) { + this.authService = authService + this.timelineService = timelineService } /** diff --git a/services/twitter-services/src/launcher.ts b/services/twitter-services/src/launcher.ts deleted file mode 100644 index ae9102f..0000000 --- a/services/twitter-services/src/launcher.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { AiriAdapter } from './adapters/airi-adapter' -import type { StagehandBrowserAdapter } from './adapters/browserbase-adapter' -import type { MCPAdapter } from './adapters/mcp-adapter' - -import process from 'node:process' - -import { createDefaultConfig } from './config' -import { TwitterService } from './core/twitter-service' -import { logger } from './utils/logger' - -/** - * Twitter service launcher class - * Responsible for initializing and starting services - */ -export class TwitterServiceLauncher { - private browser?: StagehandBrowserAdapter - private twitterService?: TwitterService - private airiAdapter?: AiriAdapter - private mcpAdapter?: MCPAdapter - - /** - * Start Twitter service - */ - async start() { - try { - // Load configuration - const configManager = createDefaultConfig() - const config = configManager.getConfig() - - logger.main.log('Starting Twitter service...') - - // Initialize browser - // Import handling - const { StagehandBrowserAdapter } = await import('./adapters/browserbase-adapter') - this.browser = new StagehandBrowserAdapter( - config.browser.apiKey, - config.browser.endpoint, - { - timeout: config.browser.requestTimeout, - // retries: config.browser.requestRetries, - }, - ) - - await this.browser.initialize(config.browser) - logger.main.log('Browser initialized') - - // Create Twitter service - this.twitterService = new TwitterService(this.browser) - - // Try to log in - if (config.twitter.credentials) { - const success = await this.twitterService.login(config.twitter.credentials) - if (success) { - logger.main.log('Successfully logged into Twitter') - } - else { - logger.main.error('Twitter login failed!') - } - } - - // Start enabled adapters - if (config.adapters.airi?.enabled) { - logger.main.log('Starting Airi adapter...') - const { AiriAdapter } = await import('./adapters/airi-adapter') - - this.airiAdapter = new AiriAdapter(this.twitterService, { - url: config.adapters.airi.url, - token: config.adapters.airi.token, - credentials: config.twitter.credentials!, - }) - - await this.airiAdapter.start() - logger.main.log('Airi adapter started') - } - - if (config.adapters.mcp?.enabled) { - logger.main.log('Starting MCP adapter...') - const { MCPAdapter } = await import('./adapters/mcp-adapter') - - this.mcpAdapter = new MCPAdapter( - this.twitterService, - config.adapters.mcp.port, - ) - - await this.mcpAdapter.start() - logger.main.log('MCP adapter started') - } - - logger.main.log('Twitter service successfully started!') - - // Set up shutdown hooks - this.setupShutdownHooks() - } - catch (error) { - logger.main.withError(error).error('Failed to start Twitter service') - } - } - - /** - * Stop service - */ - async stop() { - logger.main.log('Stopping Twitter service...') - - // Stop MCP adapter - if (this.mcpAdapter) { - await this.mcpAdapter.stop() - logger.main.log('MCP adapter stopped') - } - - // Close browser - if (this.browser) { - await this.browser.close() - logger.main.log('Browser closed') - } - - logger.main.log('Twitter service stopped') - } - - /** - * Set up shutdown hooks - */ - private setupShutdownHooks() { - // Handle process exit - process.on('SIGINT', async () => { - logger.main.log('Received exit signal...') - await this.stop() - process.exit(0) - }) - - process.on('SIGTERM', async () => { - logger.main.log('Received termination signal...') - await this.stop() - process.exit(0) - }) - - // Handle uncaught exceptions - process.on('uncaughtException', async (error) => { - logger.main.withError(error).error('Uncaught exception') - await this.stop() - process.exit(1) - }) - } -} diff --git a/services/twitter-services/src/main.ts b/services/twitter-services/src/main.ts index b0f048a..c3c033f 100644 --- a/services/twitter-services/src/main.ts +++ b/services/twitter-services/src/main.ts @@ -1,8 +1,165 @@ +import type { Browser, BrowserContext } from 'playwright' +import type { AiriAdapter } from './adapters/airi-adapter' +import type { MCPAdapter } from './adapters/mcp-adapter' + import process from 'node:process' +import { chromium } from 'playwright' -import { TwitterServiceLauncher } from './launcher' +import { createDefaultConfig } from './config' +import { TwitterAuthService } from './core/auth-service' +import { TwitterTimelineService } from './core/timeline-service' +import { TwitterService } from './core/twitter-service' import { initializeLogger, logger } from './utils/logger' +/** + * Twitter service launcher class + * Responsible for initializing and starting services + */ +export class TwitterServiceLauncher { + private browser?: Browser + private context?: BrowserContext + private twitterService?: TwitterService + private airiAdapter?: AiriAdapter + private mcpAdapter?: MCPAdapter + + /** + * Start Twitter service + */ + async start() { + try { + // Load configuration + const configManager = createDefaultConfig() + const config = configManager.getConfig() + + logger.main.log('Starting Twitter service...') + + // Initialize Playwright browser + this.browser = await chromium.launch({ + headless: config.browser.headless, + }) + + // Create a browser context + this.context = await this.browser.newContext({ + userAgent: config.browser.userAgent, + viewport: config.browser.viewport, + bypassCSP: true, + }) + + // Set default timeout for navigation and actions + this.context.setDefaultTimeout(config.browser.timeout || 30000) + + // Create a page + const page = await this.context.newPage() + + logger.main.log('Browser initialized') + + // Create service instances + const authService = new TwitterAuthService(page, this.context) + const timelineService = new TwitterTimelineService(page) + + // Create Twitter service with direct service dependencies + this.twitterService = new TwitterService(authService, timelineService) + + // Try to log in + if (config.twitter.credentials) { + const success = await this.twitterService.login(config.twitter.credentials) + if (success) { + logger.main.log('Successfully logged into Twitter') + } + else { + logger.main.error('Twitter login failed!') + } + } + + // Start enabled adapters + if (config.adapters.airi?.enabled) { + logger.main.log('Starting Airi adapter...') + const { AiriAdapter } = await import('./adapters/airi-adapter') + + this.airiAdapter = new AiriAdapter(this.twitterService, { + url: config.adapters.airi.url, + token: config.adapters.airi.token, + credentials: config.twitter.credentials!, + }) + + await this.airiAdapter.start() + logger.main.log('Airi adapter started') + } + + if (config.adapters.mcp?.enabled) { + logger.main.log('Starting MCP adapter...') + const { MCPAdapter } = await import('./adapters/mcp-adapter') + + this.mcpAdapter = new MCPAdapter( + this.twitterService, + config.adapters.mcp.port, + ) + + await this.mcpAdapter.start() + logger.main.log('MCP adapter started') + } + + logger.main.log('Twitter service successfully started!') + + // Set up shutdown hooks + this.setupShutdownHooks() + } + catch (error) { + logger.main.withError(error).error('Failed to start Twitter service') + } + } + + /** + * Stop service + */ + async stop() { + logger.main.log('Stopping Twitter service...') + + // Stop MCP adapter + if (this.mcpAdapter) { + await this.mcpAdapter.stop() + logger.main.log('MCP adapter stopped') + } + + // Close browser + if (this.context) { + await this.context.close() + } + + if (this.browser) { + await this.browser.close() + logger.main.log('Browser closed') + } + + logger.main.log('Twitter service stopped') + } + + /** + * Set up shutdown hooks + */ + private setupShutdownHooks() { + // Handle process exit + process.on('SIGINT', async () => { + logger.main.log('Received exit signal...') + await this.stop() + process.exit(0) + }) + + process.on('SIGTERM', async () => { + logger.main.log('Received termination signal...') + await this.stop() + process.exit(0) + }) + + // Handle uncaught exceptions + process.on('uncaughtException', async (error) => { + logger.main.withError(error).error('Uncaught exception') + await this.stop() + process.exit(1) + }) + } +} + // Ensure initialization only happens once async function bootstrap() { // 1. First initialize logging system diff --git a/services/twitter-services/src/mcp-server.ts b/services/twitter-services/src/mcp-server.ts deleted file mode 100644 index d6b06cf..0000000 --- a/services/twitter-services/src/mcp-server.ts +++ /dev/null @@ -1,60 +0,0 @@ -import process from 'node:process' -import * as dotenv from 'dotenv' - -import { StagehandBrowserAdapter } from './adapters/browserbase-adapter' -import { MCPAdapter } from './adapters/mcp-adapter' -import { TwitterService } from './core/twitter-service' -import { logger } from './utils/logger' - -// Load environment variables -dotenv.config() - -/** - * Development server entry point - * Provides convenience features for development - */ -async function startDevServer() { - // Create browser and Twitter service - const browser = new StagehandBrowserAdapter(process.env.BROWSERBASE_API_KEY || '') - await browser.initialize({ - headless: true, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - }) - - const twitter = new TwitterService(browser) - - // Optional: If credentials are available, login - if (process.env.TWITTER_USERNAME && process.env.TWITTER_PASSWORD) { - const success = await twitter.login({ - username: process.env.TWITTER_USERNAME, - password: process.env.TWITTER_PASSWORD, - }) - - if (success) { - logger.main.log('✅ Successfully logged in Twitter') - } - else { - logger.main.warn('⚠️ Twitter login failed') - } - } - - // Create and start MCP adapter - const mcpAdapter = new MCPAdapter(twitter, 8080) - await mcpAdapter.start() - - logger.main.log('🚀 Twitter MCP Dev Server started') - - // Handle exit - process.on('SIGINT', async () => { - logger.main.log('Shutting down server...') - await mcpAdapter.stop() - await browser.close() - process.exit(0) - }) -} - -// Execute -startDevServer().catch((error) => { - logger.main.error('Failed to start dev server:', error) - process.exit(1) -}) diff --git a/services/twitter-services/src/types/browser.ts b/services/twitter-services/src/types/browser.ts deleted file mode 100644 index e6b6def..0000000 --- a/services/twitter-services/src/types/browser.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Browser Config Interface - */ -export interface BrowserConfig { - headless?: boolean - userAgent?: string - viewport?: { - width: number - height: number - } - timeout?: number - requestTimeout?: number // API request timeout - requestRetries?: number // Request retries - proxy?: string -} - -/** - * Element Handle Interface - */ -export interface ElementHandle { - getText: () => Promise - getAttribute: (name: string) => Promise - click: () => Promise - type: (text: string) => Promise -} - -/** - * Wait Options Interface - */ -export interface WaitOptions { - timeout?: number - visible?: boolean - hidden?: boolean -} diff --git a/services/twitter-services/src/utils/api.ts b/services/twitter-services/src/utils/api.ts deleted file mode 100644 index a561fc7..0000000 --- a/services/twitter-services/src/utils/api.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ofetch } from 'ofetch' - -import { logger } from './logger' - -/** - * Create a pre-configured ofetch instance - * - * @param baseURL - Base URL for the API - * @param options - Additional options - * @returns - Customized ofetch instance - */ -export function createApiClient(baseURL: string, options: Record = {}) { - const client = ofetch.create({ - baseURL, - retry: 1, - timeout: 30000, // Default 30 second timeout - ...options, - - // Request interceptor - onRequest({ request, options }) { - const method = options.method || 'GET' - const url = request.toString() - logger.browser.withFields({ method, url }).debug('API request') - }, - - // Request error interceptor - onRequestError({ request, error, options }) { - const method = options.method || 'GET' - const url = request.toString() - logger.browser.withFields({ method, url }).errorWithError('API request failed', error) - }, - - // Response interceptor - onResponse({ request, response, options }) { - const method = options.method || 'GET' - const url = request.toString() - const status = response.status - - logger.browser - .withField('method', method) - .withField('url', url) - .withField('status', status) - .debug('API response') - }, - - // Response error interceptor - onResponseError({ request, response, options }) { - const method = options.method || 'GET' - const url = request.toString() - const status = response.status - - logger.browser - .withField('method', method) - .withField('url', url) - .withField('status', status) - .withField('body', response._data) - .error('API response error') - }, - }) - - return client -} diff --git a/services/twitter-services/src/utils/session-manager.ts b/services/twitter-services/src/utils/session-manager.ts deleted file mode 100644 index 2bf549e..0000000 --- a/services/twitter-services/src/utils/session-manager.ts +++ /dev/null @@ -1,129 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import process from 'node:process' - -import { logger } from './logger' - -/** - * Session cookie data structure - */ -export interface SessionData { - cookies: Record - timestamp: string - userAgent?: string - username?: string -} - -/** - * Session manager utility - * Responsible for loading and saving session data to/from files - */ -export class SessionManager { - private sessionFilePath: string - - /** - * Create a new session manager instance - * @param sessionFilePath Optional custom path for the session file - */ - constructor(sessionFilePath?: string) { - this.sessionFilePath = sessionFilePath - || path.join(process.cwd(), '.twitter.session.json') - } - - /** - * Save session data to file - * @param data The session data to save - */ - async saveSession(data: SessionData): Promise { - try { - // Create session directory if it doesn't exist - const dir = path.dirname(this.sessionFilePath) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) - } - - // Write session data to file - fs.writeFileSync( - this.sessionFilePath, - JSON.stringify(data, null, 2), - 'utf8', - ) - - logger.auth.log(`Session saved to ${this.sessionFilePath}`) - return true - } - catch (error: unknown) { - logger.auth.withError(error as Error).error(`Failed to save session to ${this.sessionFilePath}`) - return false - } - } - - /** - * Load session data from file - * @returns The loaded session data or null if file doesn't exist or is invalid - */ - async loadSession(): Promise { - try { - // Check if session file exists - if (!fs.existsSync(this.sessionFilePath)) { - logger.auth.debug(`No session file found at ${this.sessionFilePath}`) - return null - } - - // Read and parse session data - const data = JSON.parse(fs.readFileSync(this.sessionFilePath, 'utf8')) - - // Validate session data - if (!data.cookies || typeof data.cookies !== 'object' || !data.timestamp) { - logger.auth.warn(`Invalid session data in ${this.sessionFilePath}`) - return null - } - - // Check if session is too old (30 days) - const sessionDate = new Date(data.timestamp) - const ageInDays = (Date.now() - sessionDate.getTime()) / (1000 * 60 * 60 * 24) - if (ageInDays > 30) { - logger.auth.warn(`Session is ${Math.floor(ageInDays)} days old and may be expired`) - } - - logger.auth.log(`Loaded session from ${this.sessionFilePath} with ${Object.keys(data.cookies).length} cookies`) - return data - } - catch (error: unknown) { - logger.auth.withError(error as Error).error(`Failed to load session from ${this.sessionFilePath}`) - return null - } - } - - /** - * Delete the session file - * @returns true if deletion was successful, false otherwise - */ - deleteSession(): boolean { - try { - if (fs.existsSync(this.sessionFilePath)) { - fs.unlinkSync(this.sessionFilePath) - logger.auth.log(`Session file deleted: ${this.sessionFilePath}`) - return true - } - return false - } - catch (error: unknown) { - logger.auth.withError(error as Error).error(`Failed to delete session file: ${this.sessionFilePath}`) - return false - } - } -} - -// Create singleton instance -let sessionManagerInstance: SessionManager | null = null - -/** - * Get the session manager instance - */ -export function getSessionManager(sessionFilePath?: string): SessionManager { - if (!sessionManagerInstance) { - sessionManagerInstance = new SessionManager(sessionFilePath) - } - return sessionManagerInstance -}