Skip to content

Commit

Permalink
feat: load session to auto login
Browse files Browse the repository at this point in the history
  • Loading branch information
luoling8192 committed Mar 4, 2025
1 parent 8457164 commit 1438d09
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 106 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ coverage/

**/temp/

*.session.json
twitter-session.json
41 changes: 33 additions & 8 deletions services/twitter-services/src/adapters/mcp-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,23 @@ export class MCPAdapter {
// Add login tool
this.mcpServer.tool(
'login',
{
username: z.string(),
password: z.string(),
},
async ({ username, password }) => {
{},
async () => {
try {
const success = await this.twitterService.login({ username, password })
const success = await this.twitterService.login()

return {
content: [{
type: 'text',
text: success ? 'Successfully logged into Twitter' : 'Login failed, please check credentials',
text: success
? '成功从会话文件加载登录状态!如果您是手动登录,系统已设置自动监控来保存您的会话。'
: '没有找到有效的会话文件。请在浏览器中手动登录,系统会自动保存您的会话。',
}],
}
}
catch (error) {
return {
content: [{ type: 'text', text: `Login failed: ${errorToMessage(error)}` }],
content: [{ type: 'text', text: `检查登录状态失败: ${errorToMessage(error)}` }],
isError: true,
}
}
Expand Down Expand Up @@ -227,6 +226,32 @@ export class MCPAdapter {
},
)

// Add save session tool
this.mcpServer.tool(
'save-session',
{},
async () => {
try {
const success = await this.twitterService.saveSession()

return {
content: [{
type: 'text',
text: success
? 'Successfully saved browser session to file. This session will be loaded automatically next time.'
: 'Failed to save browser session',
}],
}
}
catch (error) {
return {
content: [{ type: 'text', text: `Failed to save session: ${errorToMessage(error)}` }],
isError: true,
}
}
},
)

// Add search tool
this.mcpServer.tool(
'search',
Expand Down
18 changes: 0 additions & 18 deletions services/twitter-services/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ export class ConfigManager {
if (configPath) {
this.loadFromFile(configPath)
}

// Validate configuration
this.validateConfig()
}

/**
Expand All @@ -83,20 +80,6 @@ export class ConfigManager {
}
}

/**
* Validate configuration validity
*/
private validateConfig(): void {
// Validate Twitter credentials
if (!this.config.twitter.credentials?.username || !this.config.twitter.credentials?.password) {
logger.config.warn('Twitter credentials not set!')
}

logger.config.withFields({
config: this.config,
}).log('Configuration validation complete')
}

/**
* Get complete configuration
*/
Expand All @@ -110,7 +93,6 @@ export class ConfigManager {
updateConfig(newConfig: Partial<Config>): void {
// Use defu to merge new configuration
this.config = defu(newConfig, this.config)
this.validateConfig()
}
}

Expand Down
8 changes: 1 addition & 7 deletions services/twitter-services/src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BrowserConfig } from '../types/browser'
import type { SearchOptions, TimelineOptions, TwitterCredentials } from '../types/twitter'
import type { SearchOptions, TimelineOptions } from '../types/twitter'

import process from 'node:process'

Expand All @@ -15,7 +15,6 @@ export interface Config {

// Twitter configuration
twitter: {
credentials?: TwitterCredentials
defaultOptions?: {
timeline?: TimelineOptions
search?: SearchOptions
Expand Down Expand Up @@ -64,11 +63,6 @@ export function getDefaultConfig(): Config {
requestRetries: Number.parseInt(process.env.BROWSER_REQUEST_RETRIES || '2'),
},
twitter: {
credentials: {
username: process.env.TWITTER_USERNAME || '',
password: process.env.TWITTER_PASSWORD || '',
// Don't include cookies here, they will be loaded from session file
},
defaultOptions: {
timeline: {
count: 20,
Expand Down
96 changes: 47 additions & 49 deletions services/twitter-services/src/core/auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { BrowserContext, Cookie, Page } from 'playwright'
import type { TwitterCredentials } from '../types/twitter'

import fs from 'node:fs/promises'
import path from 'node:path'
Expand Down Expand Up @@ -97,36 +96,24 @@ export class TwitterAuthService {
}

/**
* Login to Twitter - compatibility method for existing code
* Prefers cookie-based login if cookies provided, otherwise redirects to manual login
* Login to Twitter - simplified method that only tries to use session file
* Users are expected to manually login and save the session
*/
async login(credentials: TwitterCredentials = {}): Promise<boolean> {
async login(): Promise<boolean> {
logger.auth.log('Starting Twitter login process')

try {
// Check if cookies are provided
if (credentials.cookies && Object.keys(credentials.cookies).length > 0) {
logger.auth.log(`Attempting to login with ${Object.keys(credentials.cookies).length} provided cookies`)
return await this.loginWithCookies(credentials.cookies)
}

// Try to login with existing session first
logger.auth.log('No cookies provided, attempting to load session from file')
logger.auth.log('Attempting to load session from file')
const sessionSuccess = await this.checkExistingSession()

if (sessionSuccess) {
logger.auth.log('Successfully logged in with session file')
return true
}

// 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')
// Log session failure but don't attempt automatic login
logger.auth.log('No valid session found, manual login is required')
return false
}
catch (error: unknown) {
Expand All @@ -145,6 +132,17 @@ export class TwitterAuthService {
// First check for timeline which is definitive proof of being logged in
try {
await this.page.waitForSelector(SELECTORS.HOME.TIMELINE, { timeout: 15000 })

// Login verification successful - automatically save session
this.isLoggedIn = true
try {
await this.saveCurrentSession()
logger.auth.log('✅ Auto-saved session after successful login verification')
}
catch (error) {
logger.auth.withError(error as Error).warn('Failed to auto-save session')
}

return true
}
catch {
Expand All @@ -155,6 +153,17 @@ export class TwitterAuthService {
try {
const profileSelector = '[data-testid="AppTabBar_Profile_Link"]'
await this.page.waitForSelector(profileSelector, { timeout: 5000 })

// Profile link found - automatically save session
this.isLoggedIn = true
try {
await this.saveCurrentSession()
logger.auth.log('✅ Auto-saved session after finding profile link')
}
catch (error) {
logger.auth.withError(error as Error).warn('Failed to auto-save session')
}

return true
}
catch {
Expand Down Expand Up @@ -199,7 +208,21 @@ export class TwitterAuthService {
async checkLoginStatus(): Promise<boolean> {
try {
await this.page.goto('https://x.com/home')
return await this.verifyLogin()
const isLoggedIn = await this.verifyLogin()

// If already logged in, update state and automatically save session
if (isLoggedIn && !this.isLoggedIn) {
this.isLoggedIn = true
try {
await this.saveCurrentSession()
logger.auth.log('✅ Auto-saved session during status check')
}
catch (error) {
logger.auth.withError(error as Error).warn('Failed to auto-save session during status check')
}
}

return isLoggedIn
}
catch {
return false
Expand Down Expand Up @@ -509,38 +532,13 @@ export class TwitterAuthService {
*/
async saveCurrentSession(): Promise<void> {
try {
// Get the cookies from the browser
const cookies = await this.exportCookies('object')

// Format the cookies for the session manager
// We need to convert Record<string, string> to the Cookie[] format
const cookieArray = Object.entries(cookies).map(([name, value]) => ({
name,
value, // This will be a string now
domain: '.x.com',
path: '/',
expires: -1, // Session cookie
}))

// Get the storage state from the browser
// Get the storage state directly from context
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
}

// Save the session
await sessionManager.saveStorageState(sessionData)
// Save the session using the session manager
await sessionManager.saveStorageState(storageState)

logger.auth.log(`Session saved with ${typeof cookies === 'object' ? Object.keys(cookies).length : 0} cookies`)
logger.auth.log('✅ Session saved to file using browserContext.storageState()')
}
catch (error) {
logger.auth.withError(error as Error).warn('Failed to save session')
Expand Down
65 changes: 62 additions & 3 deletions services/twitter-services/src/core/twitter-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import type {
TimelineOptions,
Tweet,
TweetDetail,
TwitterCredentials,
UserProfile,
} from '../types/twitter'
import type { TwitterAuthService } from './auth-service'
import type { TwitterTimelineService } from './timeline-service'

import process from 'node:process'

/**
* Twitter service implementation
* Integrates various service components, providing a unified interface
Expand All @@ -27,8 +28,8 @@ export class TwitterService implements ITwitterService {
/**
* Log in to Twitter
*/
async login(credentials: TwitterCredentials): Promise<boolean> {
return await this.authService.login(credentials)
async login(): Promise<boolean> {
return await this.authService.login()
}

/**
Expand Down Expand Up @@ -99,6 +100,20 @@ export class TwitterService implements ITwitterService {
throw new Error('Post tweet feature not yet implemented')
}

/**
* Save current browser session to file
* This allows users to manually save their session after logging in
*/
async saveSession(): Promise<boolean> {
try {
await this.authService.saveCurrentSession()
return true
}
catch {
return false
}
}

/**
* Ensure authenticated
*/
Expand All @@ -115,4 +130,48 @@ export class TwitterService implements ITwitterService {
async exportCookies(format: 'object' | 'string' = 'object'): Promise<Record<string, string> | string> {
return await this.authService.exportCookies(format)
}

/**
* Start automatic session monitoring
* Checks login status at regular intervals and saves the session if login is detected
* @param interval Interval in milliseconds, defaults to 30 seconds
*/
startSessionMonitor(interval: number = 30000): void {
// Check immediately in case we're already logged in
this.checkAndSaveSession()

// Set interval for regular checks
const timer = setInterval(() => {
this.checkAndSaveSession()
}, interval)

// Clean up timer on process exit
process.on('exit', () => {
clearInterval(timer)
})

process.on('SIGINT', () => {
clearInterval(timer)
})

process.on('SIGTERM', () => {
clearInterval(timer)
})
}

/**
* Check login status and save session if logged in
* @private
*/
private async checkAndSaveSession(): Promise<void> {
try {
const isLoggedIn = this.authService.isAuthenticated() || await this.authService.checkLoginStatus()
if (isLoggedIn) {
await this.saveSession()
}
}
catch {
// Silently handle errors - don't disrupt the application flow
}
}
}
Loading

0 comments on commit 1438d09

Please sign in to comment.