- Updated Dashboard.tsx to remove demo data fallbacks - Updated TradingHistory.tsx to use new Drift trading history endpoint - Added getTradingHistory method to DriftTradingService - Created new /api/drift/trading-history endpoint - Removed fallback demo positions from getPositions method - All UI components now show only real Drift account data or empty states - No more hardcoded mock trades or positions
2354 lines
78 KiB
TypeScript
2354 lines
78 KiB
TypeScript
import { chromium, Browser, Page, BrowserContext } from 'playwright'
|
||
import fs from 'fs/promises'
|
||
import path from 'path'
|
||
|
||
export interface TradingViewCredentials {
|
||
email: string
|
||
password: string
|
||
}
|
||
|
||
// Environment variables fallback
|
||
const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL
|
||
const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
|
||
|
||
export interface NavigationOptions {
|
||
symbol?: string // e.g., 'SOLUSD', 'BTCUSD'
|
||
timeframe?: string // e.g., '5', '15', '1H'
|
||
waitForChart?: boolean
|
||
}
|
||
|
||
// Session persistence configuration
|
||
const SESSION_DATA_DIR = path.join(process.cwd(), '.tradingview-session')
|
||
const COOKIES_FILE = path.join(SESSION_DATA_DIR, 'cookies.json')
|
||
const SESSION_STORAGE_FILE = path.join(SESSION_DATA_DIR, 'session-storage.json')
|
||
|
||
export class TradingViewAutomation {
|
||
private browser: Browser | null = null
|
||
private context: BrowserContext | null = null
|
||
private page: Page | null = null
|
||
private isAuthenticated: boolean = false
|
||
private static instance: TradingViewAutomation | null = null
|
||
private initPromise: Promise<void> | null = null
|
||
private operationLock: boolean = false
|
||
private lastRequestTime = 0
|
||
private requestCount = 0
|
||
private sessionFingerprint: string | null = null
|
||
private humanBehaviorEnabled = true
|
||
|
||
// Singleton pattern to prevent multiple browser instances
|
||
static getInstance(): TradingViewAutomation {
|
||
if (!TradingViewAutomation.instance) {
|
||
TradingViewAutomation.instance = new TradingViewAutomation()
|
||
}
|
||
return TradingViewAutomation.instance
|
||
}
|
||
|
||
/**
|
||
* Acquire operation lock to prevent concurrent operations
|
||
*/
|
||
private async acquireOperationLock(timeout = 30000): Promise<void> {
|
||
const startTime = Date.now()
|
||
while (this.operationLock) {
|
||
if (Date.now() - startTime > timeout) {
|
||
throw new Error('Operation lock timeout - another operation is in progress')
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, 100))
|
||
}
|
||
this.operationLock = true
|
||
}
|
||
|
||
/**
|
||
* Release operation lock
|
||
*/
|
||
private releaseOperationLock(): void {
|
||
this.operationLock = false
|
||
}
|
||
|
||
async init(): Promise<void> {
|
||
// Acquire operation lock
|
||
await this.acquireOperationLock()
|
||
|
||
try {
|
||
// Prevent multiple initialization calls
|
||
if (this.initPromise) {
|
||
console.log('🔄 Browser initialization already in progress, waiting...')
|
||
return this.initPromise
|
||
}
|
||
|
||
if (this.browser && !this.browser.isConnected()) {
|
||
console.log('🔄 Browser disconnected, cleaning up...')
|
||
await this.forceCleanup()
|
||
}
|
||
|
||
if (this.browser) {
|
||
console.log('✅ Browser already initialized and connected')
|
||
return
|
||
}
|
||
|
||
this.initPromise = this._doInit()
|
||
try {
|
||
await this.initPromise
|
||
} finally {
|
||
this.initPromise = null
|
||
}
|
||
} finally {
|
||
this.releaseOperationLock()
|
||
}
|
||
}
|
||
|
||
private async _doInit(): Promise<void> {
|
||
console.log('🚀 Initializing TradingView automation with session persistence...')
|
||
|
||
// Ensure session directory exists
|
||
await fs.mkdir(SESSION_DATA_DIR, { recursive: true })
|
||
|
||
// Use a random port to avoid conflicts
|
||
const debugPort = 9222 + Math.floor(Math.random() * 1000)
|
||
|
||
try {
|
||
this.browser = await chromium.launch({
|
||
headless: true, // Must be true for Docker containers
|
||
timeout: 60000, // Reduce timeout to 60 seconds
|
||
args: [
|
||
'--no-sandbox',
|
||
'--disable-setuid-sandbox',
|
||
'--disable-dev-shm-usage',
|
||
'--disable-accelerated-2d-canvas',
|
||
'--no-first-run',
|
||
'--no-zygote',
|
||
'--disable-gpu',
|
||
'--disable-web-security',
|
||
'--disable-features=VizDisplayCompositor',
|
||
'--disable-background-timer-throttling',
|
||
'--disable-backgrounding-occluded-windows',
|
||
'--disable-renderer-backgrounding',
|
||
'--disable-features=TranslateUI',
|
||
'--disable-ipc-flooding-protection',
|
||
'--disable-extensions',
|
||
'--disable-default-apps',
|
||
'--disable-sync',
|
||
'--metrics-recording-only',
|
||
'--safebrowsing-disable-auto-update',
|
||
'--disable-component-extensions-with-background-pages',
|
||
'--disable-background-networking',
|
||
'--disable-software-rasterizer',
|
||
`--remote-debugging-port=${debugPort}`,
|
||
// Additional args to reduce captcha detection
|
||
'--disable-blink-features=AutomationControlled',
|
||
'--disable-features=VizDisplayCompositor,VizHitTestSurfaceLayer',
|
||
'--disable-features=ScriptStreaming',
|
||
'--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||
]
|
||
})
|
||
} catch (error) {
|
||
console.error('❌ Failed to launch browser:', error)
|
||
// Cleanup any partial state
|
||
await this.forceCleanup()
|
||
throw new Error(`Failed to launch browser: ${error}`)
|
||
}
|
||
|
||
if (!this.browser) {
|
||
throw new Error('Failed to launch browser')
|
||
}
|
||
|
||
// Create browser context with enhanced stealth features
|
||
this.context = await this.browser.newContext({
|
||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||
viewport: { width: 1920, height: 1080 },
|
||
// Enhanced HTTP headers to appear more human-like
|
||
extraHTTPHeaders: {
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||
'Accept-Language': 'en-US,en;q=0.9,de;q=0.8',
|
||
'Accept-Encoding': 'gzip, deflate, br',
|
||
'Cache-Control': 'max-age=0',
|
||
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
||
'Sec-Ch-Ua-Mobile': '?0',
|
||
'Sec-Ch-Ua-Platform': '"Linux"',
|
||
'Sec-Fetch-Dest': 'document',
|
||
'Sec-Fetch-Mode': 'navigate',
|
||
'Sec-Fetch-Site': 'none',
|
||
'Sec-Fetch-User': '?1',
|
||
'Upgrade-Insecure-Requests': '1',
|
||
'Dnt': '1'
|
||
},
|
||
// Additional context options for stealth
|
||
javaScriptEnabled: true,
|
||
acceptDownloads: false,
|
||
bypassCSP: false,
|
||
colorScheme: 'light',
|
||
deviceScaleFactor: 1,
|
||
hasTouch: false,
|
||
isMobile: false,
|
||
locale: 'en-US',
|
||
permissions: ['geolocation'],
|
||
timezoneId: 'America/New_York'
|
||
})
|
||
|
||
if (!this.context) {
|
||
throw new Error('Failed to create browser context')
|
||
}
|
||
|
||
// Load saved session if available
|
||
await this.loadSession()
|
||
|
||
this.page = await this.context.newPage()
|
||
|
||
if (!this.page) {
|
||
throw new Error('Failed to create new page')
|
||
}
|
||
|
||
// Add enhanced stealth measures to reduce bot detection
|
||
await this.page.addInitScript(() => {
|
||
// Remove webdriver property completely
|
||
Object.defineProperty(navigator, 'webdriver', {
|
||
get: () => undefined,
|
||
});
|
||
|
||
// Enhanced plugins simulation
|
||
Object.defineProperty(navigator, 'plugins', {
|
||
get: () => {
|
||
const plugins = [
|
||
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
|
||
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
|
||
{ name: 'Native Client', filename: 'internal-nacl-plugin' }
|
||
];
|
||
plugins.length = 3;
|
||
return plugins;
|
||
},
|
||
});
|
||
|
||
// Enhanced language simulation
|
||
Object.defineProperty(navigator, 'languages', {
|
||
get: () => ['en-US', 'en', 'de'],
|
||
});
|
||
|
||
// Mock hardware concurrency
|
||
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
||
get: () => 8,
|
||
});
|
||
|
||
// Mock device memory
|
||
Object.defineProperty(navigator, 'deviceMemory', {
|
||
get: () => 8,
|
||
});
|
||
|
||
// Mock connection
|
||
Object.defineProperty(navigator, 'connection', {
|
||
get: () => ({
|
||
effectiveType: '4g',
|
||
rtt: 50,
|
||
downlink: 10
|
||
}),
|
||
});
|
||
|
||
// Override permissions API with realistic responses
|
||
const originalQuery = window.navigator.permissions.query;
|
||
window.navigator.permissions.query = (parameters: any) => {
|
||
const permission = parameters.name;
|
||
let state = 'prompt';
|
||
|
||
if (permission === 'notifications') {
|
||
state = 'default';
|
||
} else if (permission === 'geolocation') {
|
||
state = 'prompt';
|
||
}
|
||
|
||
return Promise.resolve({
|
||
state: state,
|
||
name: permission,
|
||
onchange: null,
|
||
addEventListener: () => {},
|
||
removeEventListener: () => {},
|
||
dispatchEvent: () => false
|
||
} as PermissionStatus);
|
||
};
|
||
|
||
// Mock WebGL fingerprinting resistance
|
||
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
||
WebGLRenderingContext.prototype.getParameter = function(parameter: any) {
|
||
if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL
|
||
return 'Intel Inc.';
|
||
}
|
||
if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL
|
||
return 'Intel Iris OpenGL Engine';
|
||
}
|
||
return getParameter.call(this, parameter);
|
||
};
|
||
|
||
// Mock screen properties to be consistent
|
||
Object.defineProperties(screen, {
|
||
width: { get: () => 1920 },
|
||
height: { get: () => 1080 },
|
||
availWidth: { get: () => 1920 },
|
||
availHeight: { get: () => 1040 },
|
||
colorDepth: { get: () => 24 },
|
||
pixelDepth: { get: () => 24 }
|
||
});
|
||
|
||
// Remove automation detection markers
|
||
delete (window as any).chrome.runtime.onConnect;
|
||
delete (window as any).chrome.runtime.onMessage;
|
||
|
||
// Mock battery API
|
||
Object.defineProperty(navigator, 'getBattery', {
|
||
get: () => () => Promise.resolve({
|
||
charging: true,
|
||
chargingTime: 0,
|
||
dischargingTime: Infinity,
|
||
level: 1
|
||
}),
|
||
});
|
||
})
|
||
|
||
console.log('✅ Browser and session initialized successfully')
|
||
}
|
||
|
||
/**
|
||
* Check if user is already logged in to TradingView
|
||
*/
|
||
async checkLoginStatus(): Promise<boolean> {
|
||
if (!this.page) return false
|
||
|
||
try {
|
||
console.log('🔍 Checking login status...')
|
||
|
||
// Navigate to TradingView if not already there
|
||
const currentUrl = await this.page.url()
|
||
if (!currentUrl.includes('tradingview.com')) {
|
||
console.log('📄 Navigating to TradingView...')
|
||
await this.page.goto('https://www.tradingview.com', {
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: 30000
|
||
})
|
||
|
||
// Restore session storage after navigation
|
||
await this.restoreSessionStorage()
|
||
|
||
// Wait for page to settle
|
||
await this.page.waitForTimeout(5000)
|
||
}
|
||
|
||
// Take a debug screenshot to see the current state
|
||
await this.takeDebugScreenshot('login_status_check')
|
||
|
||
// Enhanced login detection with multiple strategies
|
||
console.log('🔍 Strategy 1: Checking for user account indicators...')
|
||
|
||
// Strategy 1: Look for user account elements (more comprehensive)
|
||
const userAccountSelectors = [
|
||
// User menu and profile elements
|
||
'[data-name="header-user-menu"]',
|
||
'[data-name="user-menu"]',
|
||
'.tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous)',
|
||
'.tv-header__user-menu',
|
||
'.js-header-user-menu-button',
|
||
|
||
// Account/profile indicators
|
||
'[data-name="account-menu"]',
|
||
'[data-testid="header-user-menu"]',
|
||
'.tv-header__dropdown-toggle',
|
||
|
||
// Watchlist indicators (user-specific)
|
||
'[data-name="watchlist-button"]',
|
||
'.tv-header__watchlist-button',
|
||
'.tv-watchlist-container',
|
||
|
||
// Personal layout elements
|
||
'button:has-text("M")', // Watchlist "M" button
|
||
'[data-name="watchlist-dropdown"]',
|
||
|
||
// Pro/subscription indicators
|
||
'.tv-header__pro-button',
|
||
'[data-name="go-pro-button"]'
|
||
]
|
||
|
||
let foundUserElement = false
|
||
for (const selector of userAccountSelectors) {
|
||
try {
|
||
if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
|
||
console.log(`✅ Found user account element: ${selector}`)
|
||
foundUserElement = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
// Strategy 2: Check for sign-in/anonymous indicators (should NOT be present if logged in)
|
||
console.log('🔍 Strategy 2: Checking for anonymous/sign-in indicators...')
|
||
|
||
const anonymousSelectors = [
|
||
// Sign in buttons/links
|
||
'a[href*="signin"]',
|
||
'a[href*="/accounts/signin"]',
|
||
'button:has-text("Sign in")',
|
||
'button:has-text("Log in")',
|
||
'a:has-text("Sign in")',
|
||
'a:has-text("Log in")',
|
||
|
||
// Anonymous user indicators
|
||
'.tv-header__user-menu-button--anonymous',
|
||
'[data-name="header-user-menu-sign-in"]',
|
||
'.tv-header__sign-in',
|
||
|
||
// Guest mode indicators
|
||
'text="Continue as guest"',
|
||
'text="Sign up"',
|
||
'button:has-text("Sign up")'
|
||
]
|
||
|
||
let foundAnonymousElement = false
|
||
for (const selector of anonymousSelectors) {
|
||
try {
|
||
if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
|
||
console.log(`❌ Found anonymous indicator: ${selector} - not logged in`)
|
||
foundAnonymousElement = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
// Strategy 3: Check page URL patterns for authentication
|
||
console.log('🔍 Strategy 3: Checking URL patterns...')
|
||
|
||
const url = await this.page.url()
|
||
const isOnLoginPage = url.includes('/accounts/signin') ||
|
||
url.includes('/signin') ||
|
||
url.includes('/login')
|
||
|
||
if (isOnLoginPage) {
|
||
console.log(`❌ Currently on login page: ${url}`)
|
||
this.isAuthenticated = false
|
||
return false
|
||
}
|
||
|
||
// Strategy 4: Check for authentication-specific cookies
|
||
console.log('🔍 Strategy 4: Checking authentication cookies...')
|
||
|
||
let hasAuthCookies = false
|
||
if (this.context) {
|
||
const cookies = await this.context.cookies()
|
||
const authCookieNames = [
|
||
'sessionid',
|
||
'auth_token',
|
||
'user_token',
|
||
'session_token',
|
||
'authentication',
|
||
'logged_in',
|
||
'tv_auth',
|
||
'tradingview_auth'
|
||
]
|
||
|
||
for (const cookie of cookies) {
|
||
if (authCookieNames.some(name => cookie.name.toLowerCase().includes(name.toLowerCase()))) {
|
||
console.log(`🍪 Found potential auth cookie: ${cookie.name}`)
|
||
hasAuthCookies = true
|
||
break
|
||
}
|
||
}
|
||
|
||
console.log(`📊 Total cookies: ${cookies.length}, Auth cookies found: ${hasAuthCookies}`)
|
||
}
|
||
|
||
// Strategy 5: Try to detect personal elements by checking page content
|
||
console.log('🔍 Strategy 5: Checking for personal content...')
|
||
|
||
let hasPersonalContent = false
|
||
try {
|
||
// Look for elements that indicate a logged-in user
|
||
const personalContentSelectors = [
|
||
// Watchlist with custom symbols
|
||
'.tv-screener-table',
|
||
'.tv-widget-watch-list',
|
||
|
||
// Personal layout elements
|
||
'.layout-with-border-radius',
|
||
'.tv-chart-view',
|
||
|
||
// Settings/customization elements available only to logged-in users
|
||
'[data-name="chart-settings"]',
|
||
'[data-name="chart-properties"]'
|
||
]
|
||
|
||
for (const selector of personalContentSelectors) {
|
||
try {
|
||
if (await this.page.locator(selector).isVisible({ timeout: 1000 })) {
|
||
console.log(`✅ Found personal content: ${selector}`)
|
||
hasPersonalContent = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
// Additional check: see if we can access account-specific features
|
||
const pageText = await this.page.textContent('body') || ''
|
||
if (pageText.includes('My Watchlist') ||
|
||
pageText.includes('Portfolio') ||
|
||
pageText.includes('Alerts') ||
|
||
pageText.includes('Account')) {
|
||
console.log('✅ Found account-specific text content')
|
||
hasPersonalContent = true
|
||
}
|
||
|
||
} catch (e) {
|
||
console.log('⚠️ Error checking personal content:', e)
|
||
}
|
||
|
||
// Final decision logic
|
||
console.log('📊 Login detection summary:')
|
||
console.log(` User elements found: ${foundUserElement}`)
|
||
console.log(` Anonymous elements found: ${foundAnonymousElement}`)
|
||
console.log(` On login page: ${isOnLoginPage}`)
|
||
console.log(` Has auth cookies: ${hasAuthCookies}`)
|
||
console.log(` Has personal content: ${hasPersonalContent}`)
|
||
|
||
// Determine login status based on multiple indicators
|
||
const isLoggedIn = (foundUserElement || hasPersonalContent || hasAuthCookies) &&
|
||
!foundAnonymousElement &&
|
||
!isOnLoginPage
|
||
|
||
if (isLoggedIn) {
|
||
console.log('✅ User appears to be logged in')
|
||
this.isAuthenticated = true
|
||
return true
|
||
} else {
|
||
console.log('❌ User appears to be NOT logged in')
|
||
this.isAuthenticated = false
|
||
return false
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error checking login status:', error)
|
||
await this.takeDebugScreenshot('login_status_error')
|
||
this.isAuthenticated = false
|
||
return false
|
||
}
|
||
}
|
||
|
||
async login(credentials?: TradingViewCredentials): Promise<boolean> {
|
||
if (!this.page) throw new Error('Page not initialized')
|
||
|
||
// Use provided credentials or fall back to environment variables
|
||
const email = credentials?.email || TRADINGVIEW_EMAIL
|
||
const password = credentials?.password || TRADINGVIEW_PASSWORD
|
||
|
||
if (!email || !password) {
|
||
throw new Error('TradingView credentials not provided. Either pass credentials parameter or set TRADINGVIEW_EMAIL and TRADINGVIEW_PASSWORD in .env file')
|
||
}
|
||
|
||
try {
|
||
// Check if already logged in with enhanced detection
|
||
const loggedIn = await this.checkLoginStatus()
|
||
if (loggedIn) {
|
||
console.log('✅ Already logged in, skipping login steps')
|
||
return true
|
||
}
|
||
|
||
console.log('🔐 Starting login process...')
|
||
|
||
// Clear any existing session first to ensure clean login
|
||
if (this.context) {
|
||
try {
|
||
await this.context.clearCookies()
|
||
console.log('🧹 Cleared existing cookies for clean login')
|
||
} catch (e) {
|
||
console.log('⚠️ Could not clear cookies:', e)
|
||
}
|
||
}
|
||
|
||
// Navigate to login page with multiple attempts
|
||
console.log('📄 Navigating to TradingView login page...')
|
||
|
||
const loginUrls = [
|
||
'https://www.tradingview.com/accounts/signin/',
|
||
'https://www.tradingview.com/'
|
||
]
|
||
|
||
let loginPageLoaded = false
|
||
for (const url of loginUrls) {
|
||
try {
|
||
console.log(`🔄 Trying URL: ${url}`)
|
||
await this.page.goto(url, {
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: 30000
|
||
})
|
||
|
||
// Wait for page to settle
|
||
await this.page.waitForTimeout(3000)
|
||
|
||
const currentUrl = await this.page.url()
|
||
console.log(`📍 Current URL after navigation: ${currentUrl}`)
|
||
|
||
if (currentUrl.includes('signin') || currentUrl.includes('login')) {
|
||
loginPageLoaded = true
|
||
break
|
||
} else if (url === 'https://www.tradingview.com/') {
|
||
// Try to find and click sign in button
|
||
console.log('🔍 Looking for Sign In button on main page...')
|
||
|
||
const signInSelectors = [
|
||
'a[href*="signin"]:visible',
|
||
'a[href*="/accounts/signin/"]:visible',
|
||
'button:has-text("Sign in"):visible',
|
||
'a:has-text("Sign in"):visible',
|
||
'.tv-header__user-menu-button--anonymous:visible',
|
||
'[data-name="header-user-menu-sign-in"]:visible'
|
||
]
|
||
|
||
for (const selector of signInSelectors) {
|
||
try {
|
||
console.log(`🎯 Trying sign in selector: ${selector}`)
|
||
const element = this.page.locator(selector).first()
|
||
if (await element.isVisible({ timeout: 3000 })) {
|
||
await element.click()
|
||
console.log(`✅ Clicked sign in button: ${selector}`)
|
||
|
||
// Wait for navigation to login page
|
||
await this.page.waitForTimeout(3000)
|
||
|
||
const newUrl = await this.page.url()
|
||
if (newUrl.includes('signin') || newUrl.includes('login')) {
|
||
console.log('✅ Successfully navigated to login page')
|
||
loginPageLoaded = true
|
||
break
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log(`❌ Sign in selector failed: ${selector}`)
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (loginPageLoaded) break
|
||
}
|
||
|
||
} catch (e) {
|
||
console.log(`❌ Failed to load ${url}:`, e)
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (!loginPageLoaded) {
|
||
throw new Error('Could not reach TradingView login page')
|
||
}
|
||
|
||
// Take screenshot of login page
|
||
await this.takeDebugScreenshot('login_page_loaded')
|
||
|
||
// Wait for login form to be ready
|
||
console.log('⏳ Waiting for login form to be ready...')
|
||
await this.page.waitForTimeout(5000)
|
||
|
||
// CRITICAL: Look for and click "Email" button if present (TradingView uses this pattern)
|
||
console.log('🔍 Looking for Email login option...')
|
||
|
||
// First try Playwright locator approach
|
||
const emailTriggers = [
|
||
'button:has-text("Email")',
|
||
'button:has-text("email")',
|
||
'text="Email"',
|
||
'text="Continue with email"',
|
||
'text="Sign in with email"',
|
||
'[data-name="email"]',
|
||
'[data-testid="email-button"]'
|
||
]
|
||
|
||
let emailFormVisible = false
|
||
for (const trigger of emailTriggers) {
|
||
try {
|
||
const element = this.page.locator(trigger).first()
|
||
if (await element.isVisible({ timeout: 2000 })) {
|
||
console.log(`🎯 Found email trigger: ${trigger}`)
|
||
await element.click()
|
||
console.log('✅ Clicked email trigger')
|
||
|
||
// Wait for email form to appear
|
||
await this.page.waitForTimeout(3000)
|
||
emailFormVisible = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
// If locator approach failed, use manual button enumeration (like old working code)
|
||
if (!emailFormVisible) {
|
||
console.log('🔄 Locator approach failed, trying manual button search...')
|
||
try {
|
||
// Wait for buttons to be available
|
||
await this.page.waitForSelector('button', { timeout: 10000 })
|
||
|
||
// Get all buttons and check their text content
|
||
const buttons = await this.page.locator('button').all()
|
||
console.log(`🔍 Found ${buttons.length} buttons to check`)
|
||
|
||
for (let i = 0; i < buttons.length; i++) {
|
||
try {
|
||
const button = buttons[i]
|
||
if (await button.isVisible({ timeout: 1000 })) {
|
||
const text = await button.textContent() || ''
|
||
const trimmedText = text.trim().toLowerCase()
|
||
|
||
console.log(`📝
|
||
|
||
if (trimmedText.includes('email') ||
|
||
trimmedText.includes('continue with email') ||
|
||
trimmedText.includes('sign in with email')) {
|
||
console.log(`🎯 Found email button: "${trimmedText}"`)
|
||
await button.click()
|
||
console.log('✅ Clicked email button')
|
||
|
||
// Wait for email form to appear
|
||
await this.page.waitForTimeout(3000)
|
||
emailFormVisible = true
|
||
break
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log(`⚠️ Error checking button ${i + 1}:`, e)
|
||
continue
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('❌ Manual button search failed:', e)
|
||
}
|
||
}
|
||
|
||
// Check if email input is now visible
|
||
if (!emailFormVisible) {
|
||
// Look for email input directly (might already be visible)
|
||
const emailInputSelectors = [
|
||
'input[type="email"]',
|
||
'input[name*="email"]',
|
||
'input[name*="username"]',
|
||
'input[name="username"]', // TradingView often uses this
|
||
'input[placeholder*="email" i]',
|
||
'input[placeholder*="username" i]'
|
||
]
|
||
|
||
for (const selector of emailInputSelectors) {
|
||
try {
|
||
if (await this.page.locator(selector).isVisible({ timeout: 1000 })) {
|
||
console.log(`✅ Email input already visible: ${selector}`)
|
||
emailFormVisible = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!emailFormVisible) {
|
||
await this.takeDebugScreenshot('no_email_form')
|
||
|
||
// Additional debugging: show what elements are available
|
||
const availableElements = await this.page.evaluate(() => {
|
||
const buttons = Array.from(document.querySelectorAll('button')).map(btn => btn.textContent?.trim()).filter(Boolean)
|
||
const inputs = Array.from(document.querySelectorAll('input')).map(input => ({
|
||
type: input.type,
|
||
name: input.name,
|
||
placeholder: input.placeholder
|
||
}))
|
||
const forms = Array.from(document.querySelectorAll('form')).length
|
||
return { buttons, inputs, forms }
|
||
})
|
||
|
||
console.log('🔍 Available elements:', JSON.stringify(availableElements, null, 2))
|
||
throw new Error('Could not find or activate email login form')
|
||
}
|
||
|
||
// Find and fill email field
|
||
console.log('📧 Looking for email input field...')
|
||
|
||
const emailSelectors = [
|
||
'input[name="username"]', // TradingView commonly uses this
|
||
'input[type="email"]',
|
||
'input[name="email"]',
|
||
'input[name="id_username"]',
|
||
'input[placeholder*="email" i]',
|
||
'input[placeholder*="username" i]',
|
||
'input[autocomplete="username"]',
|
||
'input[autocomplete="email"]',
|
||
'form input[type="text"]:not([type="password"])'
|
||
]
|
||
|
||
let emailInput = null
|
||
for (const selector of emailSelectors) {
|
||
try {
|
||
console.log(`🔍 Trying email selector: ${selector}`)
|
||
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
|
||
emailInput = selector
|
||
console.log(`✅ Found email input: ${selector}`)
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (!emailInput) {
|
||
// Try manual search like the old code
|
||
console.log('🔄 Selector approach failed, trying manual input search...')
|
||
try {
|
||
const inputs = await this.page.locator('input').all()
|
||
console.log(`🔍 Found ${inputs.length} inputs to check`)
|
||
|
||
for (let i = 0; i < inputs.length; i++) {
|
||
try {
|
||
const input = inputs[i]
|
||
if (await input.isVisible({ timeout: 1000 })) {
|
||
const type = await input.getAttribute('type') || ''
|
||
const name = await input.getAttribute('name') || ''
|
||
const placeholder = await input.getAttribute('placeholder') || ''
|
||
|
||
console.log(`📝 Input ${i + 1}: type="${type}" name="${name}" placeholder="${placeholder}"`)
|
||
|
||
if (type === 'email' ||
|
||
name.toLowerCase().includes('email') ||
|
||
name.toLowerCase().includes('username') ||
|
||
placeholder.toLowerCase().includes('email') ||
|
||
placeholder.toLowerCase().includes('username')) {
|
||
console.log(`🎯 Found email input manually: ${name || type || placeholder}`)
|
||
emailInput = `input:nth-of-type(${i + 1})`
|
||
break
|
||
}
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('❌ Manual input search failed:', e)
|
||
}
|
||
}
|
||
|
||
if (!emailInput) {
|
||
await this.takeDebugScreenshot('no_email_input')
|
||
throw new Error('Could not find email input field')
|
||
}
|
||
|
||
// Fill email
|
||
console.log('📧 Filling email field...')
|
||
await this.page.fill(emailInput, email)
|
||
console.log('✅ Email filled')
|
||
|
||
// Find and fill password field
|
||
console.log('🔑 Looking for password input field...')
|
||
|
||
const passwordSelectors = [
|
||
'input[type="password"]',
|
||
'input[name="password"]',
|
||
'input[name="id_password"]',
|
||
'input[placeholder*="password" i]',
|
||
'input[autocomplete="current-password"]'
|
||
]
|
||
|
||
let passwordInput = null
|
||
for (const selector of passwordSelectors) {
|
||
try {
|
||
console.log(`🔍 Trying password selector: ${selector}`)
|
||
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
|
||
passwordInput = selector
|
||
console.log(`✅ Found password input: ${selector}`)
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (!passwordInput) {
|
||
await this.takeDebugScreenshot('no_password_input')
|
||
throw new Error('Could not find password input field')
|
||
}
|
||
|
||
// Fill password
|
||
console.log('🔑 Filling password field...')
|
||
await this.page.fill(passwordInput, password)
|
||
console.log('✅ Password filled')
|
||
|
||
// Handle potential captcha
|
||
console.log('🤖 Checking for captcha...')
|
||
try {
|
||
// Look for different types of captcha and robot confirmation
|
||
const captchaSelectors = [
|
||
'iframe[src*="recaptcha"]',
|
||
'iframe[src*="captcha"]',
|
||
'.recaptcha-checkbox',
|
||
'[data-testid="captcha"]',
|
||
'.captcha-container',
|
||
'text="Please confirm that you are not a robot"',
|
||
'text="Are you human?"',
|
||
'text="Please verify you are human"',
|
||
'text="Security check"',
|
||
'.tv-dialog__error:has-text("robot")',
|
||
'.alert:has-text("robot")',
|
||
'.error:has-text("robot")'
|
||
]
|
||
|
||
let captchaFound = false
|
||
let captchaType = ''
|
||
for (const selector of captchaSelectors) {
|
||
try {
|
||
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
|
||
console.log(`🤖 Captcha/Robot check detected: ${selector}`)
|
||
captchaFound = true
|
||
captchaType = selector
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (captchaFound) {
|
||
console.log('⚠️ CAPTCHA/Robot verification detected!')
|
||
console.log('🚫 This indicates TradingView has flagged this as automated behavior.')
|
||
console.log('<27> In a Docker environment, automated captcha solving is not feasible.')
|
||
|
||
// Take a screenshot for debugging
|
||
await this.takeDebugScreenshot('captcha_detected')
|
||
|
||
// Instead of waiting, we should fail fast and suggest alternatives
|
||
console.log('❌ Cannot proceed with automated login due to captcha protection.')
|
||
console.log('🔧 Possible solutions:')
|
||
console.log(' 1. Use a different IP address or VPN')
|
||
console.log(' 2. Wait some time before retrying (rate limiting)')
|
||
console.log(' 3. Use session persistence from a manually authenticated browser')
|
||
console.log(' 4. Contact TradingView support if this persists')
|
||
|
||
// Mark captcha detection for future reference
|
||
await this.markCaptchaDetected()
|
||
|
||
// Return false immediately instead of waiting
|
||
throw new Error(`Captcha detected (${captchaType}) - automated login blocked`)
|
||
}
|
||
|
||
} catch (captchaError: any) {
|
||
if (captchaError.message.includes('Captcha detected')) {
|
||
throw captchaError
|
||
}
|
||
console.log('⚠️ Captcha check failed:', captchaError?.message)
|
||
}
|
||
|
||
// Find and click submit button
|
||
console.log('🔘 Looking for submit button...')
|
||
|
||
const submitSelectors = [
|
||
'button[type="submit"]',
|
||
'input[type="submit"]',
|
||
'button:has-text("Sign in")',
|
||
'button:has-text("Log in")',
|
||
'button:has-text("Login")',
|
||
'button:has-text("Sign In")',
|
||
'.tv-button--primary',
|
||
'form button:not([type="button"])',
|
||
'[data-testid="signin-button"]',
|
||
'[data-testid="login-button"]'
|
||
]
|
||
|
||
let submitButton = null
|
||
for (const selector of submitSelectors) {
|
||
try {
|
||
console.log(`🔍 Trying submit selector: ${selector}`)
|
||
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
|
||
submitButton = selector
|
||
console.log(`✅ Found submit button: ${selector}`)
|
||
break
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (!submitButton) {
|
||
// Try manual search like the old code
|
||
console.log('🔄 Selector approach failed, trying manual button search for submit...')
|
||
try {
|
||
const buttons = await this.page.locator('button').all()
|
||
console.log(`🔍 Found ${buttons.length} buttons to check for submit`)
|
||
|
||
for (let i = 0; i < buttons.length; i++) {
|
||
try {
|
||
const button = buttons[i]
|
||
if (await button.isVisible({ timeout: 1000 })) {
|
||
const text = (await button.textContent() || '').toLowerCase()
|
||
const type = await button.getAttribute('type') || ''
|
||
|
||
console.log(`📝 Submit Button ${i + 1}: "${text}" type="${type}"`)
|
||
|
||
if (type === 'submit' ||
|
||
text.includes('sign in') ||
|
||
text.includes('login') ||
|
||
text.includes('submit')) {
|
||
console.log(`🎯 Found submit button manually: "${text}"`)
|
||
submitButton = `button:nth-of-type(${i + 1})`
|
||
break
|
||
}
|
||
}
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('❌ Manual submit button search failed:', e)
|
||
}
|
||
}
|
||
|
||
if (!submitButton) {
|
||
await this.takeDebugScreenshot('no_submit_button')
|
||
throw new Error('Could not find submit button')
|
||
}
|
||
|
||
// Click submit button
|
||
console.log('🖱️ Clicking submit button...')
|
||
await this.page.click(submitButton)
|
||
console.log('✅ Submit button clicked')
|
||
|
||
// Wait for login to complete
|
||
console.log('⏳ Waiting for login to complete...')
|
||
|
||
try {
|
||
// Wait for login completion without using waitForFunction (CSP violation)
|
||
// Instead, check URL and elements periodically
|
||
let attempts = 0
|
||
let maxAttempts = 15 // Reduced to 15 seconds with 1 second intervals
|
||
let loginDetected = false
|
||
|
||
while (attempts < maxAttempts && !loginDetected) {
|
||
await this.page.waitForTimeout(1000) // Wait 1 second
|
||
attempts++
|
||
|
||
console.log(`🔄 Login check attempt ${attempts}/${maxAttempts}`)
|
||
|
||
// Check if we navigated away from login page
|
||
const currentUrl = await this.page.url()
|
||
console.log(`📍 Current URL: ${currentUrl}`)
|
||
const notOnLoginPage = !currentUrl.includes('/accounts/signin') && !currentUrl.includes('/signin')
|
||
|
||
// Check for user-specific elements
|
||
let hasUserElements = false
|
||
try {
|
||
const userElement = await this.page.locator(
|
||
'[data-name="watchlist-button"], .tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous), [data-name="user-menu"]'
|
||
).first()
|
||
hasUserElements = await userElement.isVisible({ timeout: 500 })
|
||
if (hasUserElements) {
|
||
console.log('✅ Found user-specific elements')
|
||
}
|
||
} catch (e) {
|
||
// Element not found, continue checking
|
||
}
|
||
|
||
// Check for error messages
|
||
try {
|
||
const errorSelectors = [
|
||
'.tv-dialog__error',
|
||
'.error-message',
|
||
'[data-testid="error"]',
|
||
'.alert-danger'
|
||
]
|
||
|
||
for (const selector of errorSelectors) {
|
||
if (await this.page.locator(selector).isVisible({ timeout: 500 })) {
|
||
const errorText = await this.page.locator(selector).textContent()
|
||
console.log(`❌ Login error detected: ${errorText}`)
|
||
throw new Error(`Login failed: ${errorText}`)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (e instanceof Error && e.message.includes('Login failed:')) {
|
||
throw e
|
||
}
|
||
// Continue if just element not found
|
||
}
|
||
|
||
if (notOnLoginPage || hasUserElements) {
|
||
loginDetected = true
|
||
console.log('🎉 Navigation/elements suggest login success!')
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!loginDetected) {
|
||
throw new Error('Login verification timeout - no success indicators found')
|
||
}
|
||
|
||
// Additional wait for page to fully load
|
||
await this.page.waitForTimeout(5000)
|
||
|
||
// Verify login status with enhanced detection
|
||
const loginSuccessful = await this.checkLoginStatus()
|
||
|
||
if (loginSuccessful) {
|
||
console.log('✅ Login verified successful!')
|
||
this.isAuthenticated = true
|
||
|
||
// Save session for future use
|
||
await this.saveSession()
|
||
console.log('💾 Session saved for future use')
|
||
|
||
return true
|
||
} else {
|
||
console.log('❌ Login verification failed')
|
||
await this.takeDebugScreenshot('login_verification_failed')
|
||
return false
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Login completion timeout or error:', error)
|
||
await this.takeDebugScreenshot('login_timeout')
|
||
return false
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Login failed:', error)
|
||
await this.takeDebugScreenshot('login_error')
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Smart login that prioritizes session persistence to avoid captchas
|
||
*/
|
||
async smartLogin(credentials?: TradingViewCredentials): Promise<boolean> {
|
||
if (!this.page) throw new Error('Page not initialized')
|
||
|
||
try {
|
||
console.log('🔍 Attempting smart login with session persistence...')
|
||
|
||
// First check if already logged in
|
||
const alreadyLoggedIn = await this.checkLoginStatus()
|
||
if (alreadyLoggedIn) {
|
||
console.log('🎉 Already logged in to TradingView! Skipping login process.')
|
||
await this.saveSession() // Save current session
|
||
return true
|
||
}
|
||
|
||
console.log('🔐 Not logged in, checking session persistence options...')
|
||
|
||
// Before attempting login, check if we have any saved session data
|
||
const sessionInfo = await this.testSessionPersistence()
|
||
console.log('📊 Session persistence check:', sessionInfo)
|
||
|
||
if (sessionInfo.cookiesCount > 0) {
|
||
console.log('🍪 Found saved session data, attempting to restore...')
|
||
|
||
// Try navigating to TradingView with saved session first
|
||
try {
|
||
await this.page.goto('https://www.tradingview.com/', {
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: 30000
|
||
})
|
||
|
||
// Restore session storage after navigation
|
||
await this.restoreSessionStorage()
|
||
|
||
// Wait for page to settle and check login status
|
||
await this.page.waitForTimeout(5000)
|
||
|
||
const nowLoggedIn = await this.checkLoginStatus()
|
||
if (nowLoggedIn) {
|
||
console.log('✅ Session restoration successful! Login confirmed.')
|
||
this.isAuthenticated = true
|
||
await this.saveSession()
|
||
return true
|
||
} else {
|
||
console.log('⚠️ Session restoration failed, saved session may be expired')
|
||
}
|
||
} catch (e) {
|
||
console.log('❌ Session restoration attempt failed:', e)
|
||
}
|
||
}
|
||
|
||
// Only attempt automated login if we don't have valid session data
|
||
console.log('🤖 Attempting automated login...')
|
||
try {
|
||
const autoLoginSuccess = await this.login(credentials)
|
||
|
||
if (autoLoginSuccess) {
|
||
console.log('✅ Automated login successful! Saving session for future use.')
|
||
this.isAuthenticated = true
|
||
await this.saveSession()
|
||
return true
|
||
}
|
||
} catch (loginError: any) {
|
||
if (loginError.message.includes('Captcha detected')) {
|
||
console.log('🚫 Captcha protection encountered during automated login.')
|
||
console.log('💡 To resolve this issue:')
|
||
console.log(' 1. Clear any existing session data: docker-compose exec app rm -f /app/session_*.json')
|
||
console.log(' 2. Wait 1-2 hours before retrying (to clear rate limiting)')
|
||
console.log(' 3. Consider using a different IP address or VPN')
|
||
console.log(' 4. Manually log in to TradingView in a browser to establish a valid session')
|
||
return false
|
||
}
|
||
throw loginError
|
||
}
|
||
|
||
console.log('❌ Automated login failed, this is likely due to captcha protection.')
|
||
console.log('⚠️ In Docker environment, manual login is not practical.')
|
||
console.log('<27> Checking if we can proceed with session persistence...')
|
||
|
||
// Try to check if there are any existing valid session cookies
|
||
const sessionInfo = await this.testSessionPersistence()
|
||
if (sessionInfo.isValid) {
|
||
console.log('✅ Found valid session data, attempting to use it...')
|
||
try {
|
||
// Navigate to main TradingView page to test session
|
||
await this.page.goto('https://www.tradingview.com/', {
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: 30000
|
||
})
|
||
|
||
await this.page.waitForTimeout(5000)
|
||
|
||
const nowLoggedIn = await this.checkLoginStatus()
|
||
if (nowLoggedIn) {
|
||
console.log('✅ Session persistence worked! Login successful.')
|
||
this.isAuthenticated = true
|
||
await this.saveSession()
|
||
return true
|
||
}
|
||
} catch (e) {
|
||
console.log('❌ Session persistence test failed:', e)
|
||
}
|
||
}
|
||
|
||
console.log('❌ All login methods failed. This may require manual intervention.')
|
||
console.log('💡 To fix: Log in manually in a browser with the same credentials and restart the application.')
|
||
return false
|
||
|
||
} catch (error) {
|
||
console.error('❌ Smart login failed:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async navigateToChart(options: NavigationOptions = {}): Promise<boolean> {
|
||
if (!this.page) throw new Error('Page not initialized')
|
||
|
||
try {
|
||
// Throttle requests to avoid suspicious patterns
|
||
await this.throttleRequests()
|
||
|
||
// Validate session integrity before proceeding
|
||
const sessionValid = await this.validateSessionIntegrity()
|
||
if (!sessionValid) {
|
||
console.log('⚠️ Session integrity compromised, may require re-authentication')
|
||
}
|
||
|
||
const { symbol = 'SOLUSD', timeframe = '5', waitForChart = true } = options
|
||
|
||
console.log('Navigating to chart page...')
|
||
|
||
// Perform human-like interactions before navigation
|
||
await this.performHumanLikeInteractions()
|
||
|
||
// Generate session fingerprint for tracking
|
||
await this.generateSessionFingerprint()
|
||
|
||
// Navigate to chart page with more flexible waiting strategy
|
||
try {
|
||
await this.page.goto('https://www.tradingview.com/chart/', {
|
||
waitUntil: 'domcontentloaded', // More lenient than 'networkidle'
|
||
timeout: 45000 // Increased timeout
|
||
})
|
||
} catch (error) {
|
||
console.log('Standard navigation failed, trying fallback...')
|
||
// Fallback: navigate without waiting for full network idle
|
||
await this.page.goto('https://www.tradingview.com/chart/', {
|
||
waitUntil: 'load',
|
||
timeout: 30000
|
||
})
|
||
}
|
||
|
||
// Human-like delay after navigation
|
||
await this.humanDelay(2000, 4000)
|
||
|
||
// Wait for chart to load
|
||
if (waitForChart) {
|
||
console.log('Waiting for chart container...')
|
||
try {
|
||
await this.page.waitForSelector('.chart-container, #tv_chart_container, .tv-layout', { timeout: 30000 })
|
||
console.log('Chart container found')
|
||
} catch (error) {
|
||
console.log('Chart container not found with standard selectors, trying alternatives...')
|
||
// Try alternative selectors for chart elements
|
||
const chartSelectors = [
|
||
'.chart-widget',
|
||
'.tradingview-widget-container',
|
||
'[data-name="chart"]',
|
||
'canvas',
|
||
'.tv-chart'
|
||
]
|
||
|
||
let chartFound = false
|
||
for (const selector of chartSelectors) {
|
||
try {
|
||
await this.page.waitForSelector(selector, { timeout: 5000 })
|
||
console.log(`Chart found with selector: ${selector}`)
|
||
chartFound = true
|
||
break
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (!chartFound) {
|
||
console.log('No chart container found, proceeding anyway...')
|
||
}
|
||
}
|
||
|
||
// Additional wait for chart initialization with human-like delay
|
||
await this.humanDelay(3000, 6000)
|
||
}
|
||
|
||
// Change symbol if not BTC
|
||
if (symbol !== 'BTCUSD') {
|
||
console.log(`Changing symbol to ${symbol}...`)
|
||
await this.changeSymbol(symbol)
|
||
}
|
||
|
||
// Change timeframe if specified
|
||
if (timeframe) {
|
||
console.log(`Setting timeframe to ${timeframe}...`)
|
||
await this.changeTimeframe(timeframe)
|
||
}
|
||
|
||
console.log(`Successfully navigated to ${symbol} chart with ${timeframe} timeframe`)
|
||
return true
|
||
|
||
} catch (error) {
|
||
console.error('Navigation to chart failed:', error)
|
||
await this.takeDebugScreenshot('navigation_failed')
|
||
return false
|
||
}
|
||
}
|
||
|
||
private async changeSymbol(symbol: string): Promise<void> {
|
||
if (!this.page) return
|
||
|
||
try {
|
||
// Try multiple selectors for the symbol searcher
|
||
const symbolSelectors = [
|
||
'.tv-symbol-header__short-title',
|
||
'.js-symbol-title',
|
||
'[data-name="legend-source-title"]',
|
||
'.tv-symbol-header',
|
||
'.tv-chart-header__symbol'
|
||
]
|
||
|
||
let symbolElement = null
|
||
for (const selector of symbolSelectors) {
|
||
try {
|
||
await this.page.waitForSelector(selector, { timeout: 3000 })
|
||
symbolElement = selector
|
||
break
|
||
} catch (e) {
|
||
console.log(`Symbol selector ${selector} not found, trying next...`)
|
||
}
|
||
}
|
||
|
||
if (!symbolElement) {
|
||
throw new Error('Could not find symbol selector')
|
||
}
|
||
|
||
await this.page.click(symbolElement)
|
||
|
||
// Wait for search input
|
||
const searchSelectors = [
|
||
'input[data-role="search"]',
|
||
'.tv-dialog__body input',
|
||
'.tv-symbol-search-dialog__input input',
|
||
'input[placeholder*="Search"]'
|
||
]
|
||
|
||
let searchInput = null
|
||
for (const selector of searchSelectors) {
|
||
try {
|
||
await this.page.waitForSelector(selector, { timeout: 3000 })
|
||
searchInput = selector
|
||
break
|
||
} catch (e) {
|
||
console.log(`Search input selector ${selector} not found, trying next...`)
|
||
}
|
||
}
|
||
|
||
if (!searchInput) {
|
||
throw new Error('Could not find search input')
|
||
}
|
||
|
||
// Clear and type new symbol
|
||
await this.page.fill(searchInput, symbol)
|
||
|
||
// Wait a bit for search results
|
||
await this.page.waitForTimeout(2000)
|
||
|
||
// Try to click first result or press Enter
|
||
const resultSelectors = [
|
||
'.tv-screener-table__row',
|
||
'.js-searchbar-suggestion',
|
||
'.tv-symbol-search-dialog__item',
|
||
'.tv-symbol-search-dialog__symbol'
|
||
]
|
||
|
||
let clicked = false
|
||
for (const selector of resultSelectors) {
|
||
try {
|
||
const firstResult = this.page.locator(selector).first()
|
||
if (await firstResult.isVisible({ timeout: 2000 })) {
|
||
await firstResult.click()
|
||
clicked = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
console.log(`Result selector ${selector} not found, trying next...`)
|
||
}
|
||
}
|
||
|
||
if (!clicked) {
|
||
console.log('No result found, pressing Enter...')
|
||
await this.page.press(searchInput, 'Enter')
|
||
}
|
||
|
||
// Wait for symbol to change
|
||
await this.page.waitForTimeout(3000)
|
||
|
||
} catch (error) {
|
||
console.error('Failed to change symbol:', error)
|
||
await this.takeDebugScreenshot('symbol_change_failed')
|
||
}
|
||
}
|
||
|
||
private async changeTimeframe(timeframe: string): Promise<void> {
|
||
if (!this.page) return
|
||
|
||
try {
|
||
console.log(`Attempting to change timeframe to: ${timeframe}`)
|
||
|
||
// Wait for chart to be ready
|
||
await this.page.waitForTimeout(3000)
|
||
|
||
// Map common timeframe values to TradingView format
|
||
const timeframeMap: { [key: string]: string[] } = {
|
||
'1': ['1', '1m', '1min'],
|
||
'5': ['5', '5m', '5min'],
|
||
'15': ['15', '15m', '15min'],
|
||
'30': ['30', '30m', '30min'],
|
||
'60': ['1h', '1H', '60', '60m', '60min'], // Prioritize 1h format
|
||
'240': ['4h', '4H', '240', '240m'],
|
||
'1D': ['1D', 'D', 'daily'],
|
||
'1W': ['1W', 'W', 'weekly']
|
||
}
|
||
|
||
// Get possible timeframe values to try
|
||
const timeframesToTry = timeframeMap[timeframe] || [timeframe]
|
||
console.log(`Will try these timeframe values: ${timeframesToTry.join(', ')}`)
|
||
|
||
let found = false
|
||
|
||
// Take a screenshot to see current timeframe bar
|
||
await this.takeDebugScreenshot('before_timeframe_change')
|
||
|
||
// CRITICAL: Click the interval legend to open timeframe selector
|
||
console.log('🎯 Looking for interval legend to open timeframe selector...')
|
||
const intervalLegendSelectors = [
|
||
'[data-name="legend-source-interval"]',
|
||
'.intervalTitle-l31H9iuA',
|
||
'[title="Change interval"]',
|
||
'.intervalTitle-l31H9iuA button',
|
||
'[data-name="legend-source-interval"] button'
|
||
]
|
||
|
||
let intervalLegendClicked = false
|
||
for (const selector of intervalLegendSelectors) {
|
||
try {
|
||
console.log(`Trying interval legend selector: ${selector}`)
|
||
const element = this.page.locator(selector).first()
|
||
if (await element.isVisible({ timeout: 3000 })) {
|
||
console.log(`✅ Found interval legend: ${selector}`)
|
||
await element.click()
|
||
await this.page.waitForTimeout(2000)
|
||
console.log('🖱️ Clicked interval legend - timeframe selector should be open')
|
||
intervalLegendClicked = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
console.log(`Interval legend selector ${selector} not found`)
|
||
}
|
||
}
|
||
|
||
if (!intervalLegendClicked) {
|
||
console.log('❌ Could not find interval legend to click')
|
||
await this.takeDebugScreenshot('no_interval_legend')
|
||
return
|
||
}
|
||
|
||
// Now look for timeframe options in the opened selector
|
||
console.log('🔍 Looking for timeframe options in selector...')
|
||
|
||
for (const tf of timeframesToTry) {
|
||
const timeframeSelectors = [
|
||
// After clicking interval legend, look for options
|
||
`[data-value="${tf}"]`,
|
||
`button:has-text("${tf}")`,
|
||
`.tv-dropdown__item:has-text("${tf}")`,
|
||
`.tv-interval-item:has-text("${tf}")`,
|
||
`[title="${tf}"]`,
|
||
`[aria-label*="${tf}"]`,
|
||
|
||
// Look in the opened dropdown/menu
|
||
`.tv-dropdown-behavior__body [data-value="${tf}"]`,
|
||
`.tv-dropdown-behavior__body button:has-text("${tf}")`,
|
||
|
||
// Look for list items or menu items
|
||
`li:has-text("${tf}")`,
|
||
`div[role="option"]:has-text("${tf}")`,
|
||
`[role="menuitem"]:has-text("${tf}")`,
|
||
|
||
// TradingView specific interval selectors
|
||
`.tv-screener-table__row:has-text("${tf}")`,
|
||
`.tv-interval-tabs button:has-text("${tf}")`,
|
||
`.intervals-GwQQdU8S [data-value="${tf}"]`,
|
||
|
||
// Generic selectors in visible containers
|
||
`.tv-dialog [data-value="${tf}"]`,
|
||
`.tv-dialog button:has-text("${tf}")`
|
||
]
|
||
|
||
for (const selector of timeframeSelectors) {
|
||
try {
|
||
console.log(`Trying timeframe option selector: ${selector}`)
|
||
const element = this.page.locator(selector).first()
|
||
|
||
// Check if element exists and is visible
|
||
const isVisible = await element.isVisible({ timeout: 2000 })
|
||
if (isVisible) {
|
||
console.log(`✅ Found timeframe option: ${selector}`)
|
||
await element.click()
|
||
await this.page.waitForTimeout(2000)
|
||
console.log(`🎉 Successfully clicked timeframe option for ${tf}`)
|
||
found = true
|
||
break
|
||
}
|
||
} catch (e) {
|
||
console.log(`Timeframe option selector ${selector} not found or not clickable`)
|
||
}
|
||
}
|
||
if (found) break
|
||
}
|
||
|
||
// Fallback: Try keyboard navigation
|
||
if (!found) {
|
||
console.log('🔄 Timeframe options not found, trying keyboard navigation...')
|
||
|
||
// Try pressing specific keys for common timeframes
|
||
const keyMap: { [key: string]: string } = {
|
||
'60': '1', // Often 1h is mapped to '1' key
|
||
'1': '1',
|
||
'5': '5',
|
||
'15': '1',
|
||
'30': '3',
|
||
'240': '4',
|
||
'1D': 'D'
|
||
}
|
||
|
||
if (keyMap[timeframe]) {
|
||
console.log(`🎹 Trying keyboard shortcut: ${keyMap[timeframe]}`)
|
||
await this.page.keyboard.press(keyMap[timeframe])
|
||
await this.page.waitForTimeout(1000)
|
||
found = true
|
||
}
|
||
}
|
||
|
||
if (found) {
|
||
console.log(`✅ Successfully changed timeframe to ${timeframe}`)
|
||
await this.takeDebugScreenshot('after_timeframe_change')
|
||
} else {
|
||
console.log(`❌ Could not change timeframe to ${timeframe} - timeframe options not found`)
|
||
// Take a debug screenshot to see current state
|
||
await this.takeDebugScreenshot('timeframe_change_failed')
|
||
|
||
// Log all visible elements that might be timeframe related
|
||
try {
|
||
const visibleElements = await this.page.$$eval('[data-value], button, [role="option"], [role="menuitem"], li', (elements: Element[]) =>
|
||
elements
|
||
.filter((el: Element) => {
|
||
const style = window.getComputedStyle(el)
|
||
return style.display !== 'none' && style.visibility !== 'hidden'
|
||
})
|
||
.slice(0, 20)
|
||
.map((el: Element) => ({
|
||
tagName: el.tagName,
|
||
text: el.textContent?.trim().substring(0, 20),
|
||
className: el.className.substring(0, 50),
|
||
dataValue: el.getAttribute('data-value'),
|
||
role: el.getAttribute('role'),
|
||
outerHTML: el.outerHTML.substring(0, 150)
|
||
}))
|
||
)
|
||
console.log('Visible interactive elements:', JSON.stringify(visibleElements, null, 2))
|
||
} catch (e) {
|
||
console.log('Could not analyze visible elements')
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Failed to change timeframe:', error)
|
||
await this.takeDebugScreenshot('timeframe_change_error')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Test if session persistence is working and valid
|
||
*/
|
||
async testSessionPersistence(): Promise<{ isValid: boolean; cookiesCount: number; hasStorage: boolean; currentUrl: string }> {
|
||
if (!this.page) {
|
||
return { isValid: false, cookiesCount: 0, hasStorage: false, currentUrl: 'about:blank' }
|
||
}
|
||
|
||
try {
|
||
console.log('🧪 Testing session persistence...')
|
||
|
||
// Count cookies and check storage
|
||
const cookies = await this.context?.cookies() || []
|
||
const hasLocalStorage = await this.page.evaluate(() => {
|
||
try {
|
||
return localStorage.length > 0
|
||
} catch {
|
||
return false
|
||
}
|
||
})
|
||
|
||
const currentUrl = await this.page.url()
|
||
|
||
const result = {
|
||
isValid: cookies.length > 0 && hasLocalStorage,
|
||
cookiesCount: cookies.length,
|
||
hasStorage: hasLocalStorage,
|
||
currentUrl
|
||
}
|
||
|
||
console.log('📊 Current session info:', result)
|
||
|
||
return result
|
||
} catch (error) {
|
||
console.error('❌ Error testing session persistence:', error)
|
||
return { isValid: false, cookiesCount: 0, hasStorage: false, currentUrl: 'about:blank' }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if user is logged in (alias for checkLoginStatus)
|
||
*/
|
||
async isLoggedIn(): Promise<boolean> {
|
||
return this.checkLoginStatus()
|
||
}
|
||
|
||
/**
|
||
* Wait for chart data to load with enhanced detection
|
||
*/
|
||
async waitForChartData(): Promise<boolean> {
|
||
if (!this.page) return false
|
||
|
||
try {
|
||
console.log('Waiting for chart data to load...')
|
||
|
||
// Wait for various chart loading indicators
|
||
const chartLoadingSelectors = [
|
||
'canvas',
|
||
'.tv-lightweight-charts',
|
||
'[data-name="chart"]',
|
||
'.chart-container canvas',
|
||
'.tv-chart canvas'
|
||
]
|
||
|
||
// Wait for at least one chart element
|
||
let chartFound = false
|
||
for (const selector of chartLoadingSelectors) {
|
||
try {
|
||
await this.page.waitForSelector(selector, { timeout: 10000 })
|
||
chartFound = true
|
||
break
|
||
} catch (e) {
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (!chartFound) {
|
||
console.log('⚠️ No chart elements found')
|
||
return false
|
||
}
|
||
|
||
// Additional wait for chart data to load
|
||
await this.humanDelay(3000, 6000)
|
||
|
||
// Check if chart appears to have data (not just loading screen)
|
||
const hasData = await this.page.evaluate(() => {
|
||
const canvases = document.querySelectorAll('canvas')
|
||
for (const canvas of canvases) {
|
||
const rect = canvas.getBoundingClientRect()
|
||
if (rect.width > 100 && rect.height > 100) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
})
|
||
|
||
console.log('Chart data loaded successfully')
|
||
return hasData
|
||
} catch (error) {
|
||
console.error('❌ Error waiting for chart data:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Take screenshot with anti-detection measures
|
||
*/
|
||
async takeScreenshot(filename: string): Promise<string> {
|
||
if (!this.page) throw new Error('Page not initialized')
|
||
|
||
try {
|
||
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||
await fs.mkdir(screenshotsDir, { recursive: true })
|
||
|
||
const filePath = path.join(screenshotsDir, filename)
|
||
|
||
// Perform human-like interaction before screenshot
|
||
await this.simulateHumanScrolling()
|
||
await this.humanDelay(1000, 2000)
|
||
|
||
// Take screenshot
|
||
console.log(`Taking screenshot: ${filename}`)
|
||
await this.page.screenshot({
|
||
path: filePath,
|
||
fullPage: false,
|
||
type: 'png'
|
||
})
|
||
|
||
console.log(`Screenshot saved: ${filename}`)
|
||
return filePath
|
||
} catch (error) {
|
||
console.error('❌ Error taking screenshot:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Take a debug screenshot for troubleshooting
|
||
*/
|
||
private async takeDebugScreenshot(prefix: string): Promise<void> {
|
||
if (!this.page) return
|
||
|
||
try {
|
||
const timestamp = Date.now()
|
||
const filename = `debug_${prefix}_${timestamp}.png`
|
||
const filePath = path.join(process.cwd(), 'screenshots', filename)
|
||
|
||
// Ensure directory exists
|
||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||
|
||
await this.page.screenshot({
|
||
path: filePath,
|
||
fullPage: true,
|
||
type: 'png'
|
||
})
|
||
|
||
console.log(`Screenshot saved: ${filename}`)
|
||
} catch (error) {
|
||
console.log('⚠️ Error taking debug screenshot:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get current URL
|
||
*/
|
||
async getCurrentUrl(): Promise<string> {
|
||
if (!this.page) return 'about:blank'
|
||
return this.page.url()
|
||
}
|
||
|
||
/**
|
||
* Enhanced cleanup method
|
||
*/
|
||
async close(): Promise<void> {
|
||
return this.forceCleanup()
|
||
}
|
||
|
||
/**
|
||
* Force cleanup of browser resources
|
||
*/
|
||
async forceCleanup(): Promise<void> {
|
||
// Don't use operation lock here to avoid deadlocks during cleanup
|
||
try {
|
||
if (this.page) {
|
||
try {
|
||
await this.page.close()
|
||
} catch (e) {
|
||
console.log('⚠️ Error closing page:', e)
|
||
}
|
||
this.page = null
|
||
}
|
||
|
||
if (this.context) {
|
||
try {
|
||
await this.context.close()
|
||
} catch (e) {
|
||
console.log('⚠️ Error closing context:', e)
|
||
}
|
||
this.context = null
|
||
}
|
||
|
||
if (this.browser) {
|
||
try {
|
||
await this.browser.close()
|
||
} catch (e) {
|
||
console.log('⚠️ Error closing browser:', e)
|
||
}
|
||
this.browser = null
|
||
}
|
||
|
||
// Reset flags
|
||
this.isAuthenticated = false
|
||
this.operationLock = false
|
||
this.initPromise = null
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error during force cleanup:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Reset the singleton instance (useful for testing or forcing recreation)
|
||
*/
|
||
static resetInstance(): void {
|
||
if (TradingViewAutomation.instance) {
|
||
TradingViewAutomation.instance.forceCleanup().catch(console.error)
|
||
TradingViewAutomation.instance = null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load saved session data (cookies, localStorage, etc.)
|
||
*/
|
||
private async loadSession(): Promise<void> {
|
||
try {
|
||
console.log('🔄 Loading saved session data...')
|
||
|
||
// Load cookies
|
||
if (await this.fileExists(COOKIES_FILE)) {
|
||
const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8')
|
||
const cookies = JSON.parse(cookiesData)
|
||
await this.context!.addCookies(cookies)
|
||
console.log(`✅ Loaded ${cookies.length} cookies from saved session`)
|
||
}
|
||
|
||
// Note: Session storage will be loaded after page navigation
|
||
|
||
} catch (error) {
|
||
console.log('⚠️ Could not load session data (starting fresh):', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Save current session data for future use
|
||
*/
|
||
private async saveSession(): Promise<void> {
|
||
try {
|
||
console.log('💾 Saving session data...')
|
||
|
||
if (!this.context || !this.page) return
|
||
|
||
// Save cookies
|
||
const cookies = await this.context.cookies()
|
||
await fs.writeFile(COOKIES_FILE, JSON.stringify(cookies, null, 2))
|
||
console.log(`✅ Saved ${cookies.length} cookies`)
|
||
|
||
// Save session storage and localStorage
|
||
const sessionData = await this.page.evaluate(() => {
|
||
const localStorage: { [key: string]: string | null } = {}
|
||
const sessionStorage: { [key: string]: string | null } = {}
|
||
|
||
// Extract localStorage
|
||
for (let i = 0; i < window.localStorage.length; i++) {
|
||
const key = window.localStorage.key(i)
|
||
if (key) {
|
||
localStorage[key] = window.localStorage.getItem(key)
|
||
}
|
||
}
|
||
|
||
// Extract sessionStorage
|
||
for (let i = 0; i < window.sessionStorage.length; i++) {
|
||
const key = window.sessionStorage.key(i)
|
||
if (key) {
|
||
sessionStorage[key] = window.sessionStorage.getItem(key)
|
||
}
|
||
}
|
||
|
||
return { localStorage, sessionStorage }
|
||
})
|
||
|
||
await fs.writeFile(SESSION_STORAGE_FILE, JSON.stringify(sessionData, null, 2))
|
||
console.log('✅ Saved session storage and localStorage')
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to save session data:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Restore session storage and localStorage
|
||
*/
|
||
private async restoreSessionStorage(): Promise<void> {
|
||
try {
|
||
if (!this.page || !await this.fileExists(SESSION_STORAGE_FILE)) return
|
||
|
||
const sessionData = JSON.parse(await fs.readFile(SESSION_STORAGE_FILE, 'utf8'))
|
||
|
||
await this.page.evaluate((data) => {
|
||
// Restore localStorage
|
||
if (data.localStorage) {
|
||
for (const [key, value] of Object.entries(data.localStorage)) {
|
||
try {
|
||
window.localStorage.setItem(key, value as string)
|
||
} catch (e) {
|
||
console.log('Could not restore localStorage item:', key)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Restore sessionStorage
|
||
if (data.sessionStorage) {
|
||
for (const [key, value] of Object.entries(data.sessionStorage)) {
|
||
try {
|
||
window.sessionStorage.setItem(key, value as string)
|
||
} catch (e) {
|
||
console.log('Could not restore sessionStorage item:', key)
|
||
}
|
||
}
|
||
}
|
||
}, sessionData)
|
||
|
||
console.log('✅ Restored session storage and localStorage')
|
||
|
||
} catch (error) {
|
||
console.log('⚠️ Could not restore session storage:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Refresh session to keep it alive
|
||
*/
|
||
async refreshSession(): Promise<boolean> {
|
||
if (!this.page || !this.isAuthenticated) return false
|
||
|
||
try {
|
||
console.log('🔄 Refreshing TradingView session...')
|
||
|
||
// Just reload the current page to refresh session
|
||
await this.page.reload({
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: 30000
|
||
})
|
||
|
||
// Wait for page to settle
|
||
await this.page.waitForTimeout(2000)
|
||
|
||
// Verify still logged in
|
||
const stillLoggedIn = await this.checkLoginStatus()
|
||
if (stillLoggedIn) {
|
||
console.log('✅ Session refreshed successfully')
|
||
await this.saveSession() // Save refreshed session
|
||
return true
|
||
} else {
|
||
console.log('❌ Session expired during refresh')
|
||
this.isAuthenticated = false
|
||
return false
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to refresh session:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clear all saved session data
|
||
*/
|
||
async clearSession(): Promise<void> {
|
||
try {
|
||
console.log('🗑️ Clearing saved session data...')
|
||
|
||
if (await this.fileExists(COOKIES_FILE)) {
|
||
await fs.unlink(COOKIES_FILE)
|
||
console.log('✅ Cleared cookies file')
|
||
}
|
||
|
||
if (await this.fileExists(SESSION_STORAGE_FILE)) {
|
||
await fs.unlink(SESSION_STORAGE_FILE)
|
||
console.log('✅ Cleared session storage file')
|
||
}
|
||
|
||
// Clear browser context storage if available
|
||
if (this.context) {
|
||
await this.context.clearCookies()
|
||
console.log('✅ Cleared browser context cookies')
|
||
}
|
||
|
||
this.isAuthenticated = false
|
||
console.log('✅ Session data cleared successfully')
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to clear session data:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get session status information
|
||
*/
|
||
async getSessionInfo(): Promise<{
|
||
isAuthenticated: boolean
|
||
hasSavedCookies: boolean
|
||
hasSavedStorage: boolean
|
||
cookiesCount: number
|
||
currentUrl: string
|
||
}> {
|
||
const hasSavedCookies = await this.fileExists(COOKIES_FILE)
|
||
const hasSavedStorage = await this.fileExists(SESSION_STORAGE_FILE)
|
||
|
||
let cookiesCount = 0
|
||
if (hasSavedCookies) {
|
||
try {
|
||
const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8')
|
||
const cookies = JSON.parse(cookiesData)
|
||
cookiesCount = cookies.length
|
||
} catch (e) {
|
||
// Ignore error
|
||
}
|
||
}
|
||
|
||
const currentUrl = this.page ? await this.page.url() : ''
|
||
|
||
return {
|
||
isAuthenticated: this.isAuthenticated,
|
||
hasSavedCookies,
|
||
hasSavedStorage,
|
||
cookiesCount,
|
||
currentUrl
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get lightweight session status without triggering navigation
|
||
*/
|
||
async getQuickSessionStatus(): Promise<{
|
||
isAuthenticated: boolean
|
||
hasSavedCookies: boolean
|
||
hasSavedStorage: boolean
|
||
cookiesCount: number
|
||
currentUrl: string
|
||
browserActive: boolean
|
||
}> {
|
||
const sessionInfo = await this.getSessionInfo()
|
||
|
||
return {
|
||
...sessionInfo,
|
||
browserActive: !!(this.browser && this.page)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Add random delay to mimic human behavior
|
||
*/
|
||
private async humanDelay(minMs = 500, maxMs = 2000): Promise<void> {
|
||
if (!this.humanBehaviorEnabled) return
|
||
|
||
const delay = Math.random() * (maxMs - minMs) + minMs
|
||
console.log(`⏱️ Human-like delay: ${Math.round(delay)}ms`)
|
||
await new Promise(resolve => setTimeout(resolve, delay))
|
||
}
|
||
|
||
/**
|
||
* Simulate human-like mouse movements
|
||
*/
|
||
private async simulateHumanMouseMovement(): Promise<void> {
|
||
if (!this.page || !this.humanBehaviorEnabled) return
|
||
|
||
try {
|
||
// Random mouse movements
|
||
const movements = Math.floor(Math.random() * 3) + 2 // 2-4 movements
|
||
|
||
for (let i = 0; i < movements; i++) {
|
||
const x = Math.random() * 1920
|
||
const y = Math.random() * 1080
|
||
|
||
await this.page.mouse.move(x, y, { steps: Math.floor(Math.random() * 10) + 5 })
|
||
await new Promise(resolve => setTimeout(resolve, Math.random() * 300 + 100))
|
||
}
|
||
} catch (error) {
|
||
console.log('⚠️ Error simulating mouse movement:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Simulate human-like scrolling
|
||
*/
|
||
private async simulateHumanScrolling(): Promise<void> {
|
||
if (!this.page || !this.humanBehaviorEnabled) return
|
||
|
||
try {
|
||
const scrollCount = Math.floor(Math.random() * 3) + 1 // 1-3 scrolls
|
||
|
||
for (let i = 0; i < scrollCount; i++) {
|
||
const direction = Math.random() > 0.5 ? 1 : -1
|
||
const distance = (Math.random() * 500 + 200) * direction
|
||
|
||
await this.page.mouse.wheel(0, distance)
|
||
await new Promise(resolve => setTimeout(resolve, Math.random() * 800 + 300))
|
||
}
|
||
} catch (error) {
|
||
console.log('⚠️ Error simulating scrolling:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Throttle requests to avoid suspicious patterns
|
||
*/
|
||
private async throttleRequests(): Promise<void> {
|
||
const now = Date.now()
|
||
const timeSinceLastRequest = now - this.lastRequestTime
|
||
const minInterval = 10000 + (this.requestCount * 2000) // Increase delay with request count
|
||
|
||
if (timeSinceLastRequest < minInterval) {
|
||
const waitTime = minInterval - timeSinceLastRequest
|
||
console.log(`🚦 Throttling request: waiting ${Math.round(waitTime / 1000)}s before next request (request #${this.requestCount + 1})`)
|
||
await new Promise(resolve => setTimeout(resolve, waitTime))
|
||
}
|
||
|
||
this.lastRequestTime = now
|
||
this.requestCount++
|
||
|
||
// Reset request count periodically to avoid indefinite delays
|
||
if (this.requestCount > 10) {
|
||
console.log('🔄 Resetting request count for throttling')
|
||
this.requestCount = 0
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Generate and store session fingerprint for validation
|
||
*/
|
||
private async generateSessionFingerprint(): Promise<string> {
|
||
if (!this.page) throw new Error('Page not initialized')
|
||
|
||
try {
|
||
const fingerprint = await this.page.evaluate(() => {
|
||
const canvas = document.createElement('canvas')
|
||
const ctx = canvas.getContext('2d')
|
||
if (ctx) {
|
||
ctx.textBaseline = 'top'
|
||
ctx.font = '14px Arial'
|
||
ctx.fillText('Session fingerprint', 2, 2)
|
||
}
|
||
|
||
return JSON.stringify({
|
||
userAgent: navigator.userAgent,
|
||
language: navigator.language,
|
||
platform: navigator.platform,
|
||
cookieEnabled: navigator.cookieEnabled,
|
||
onLine: navigator.onLine,
|
||
screen: {
|
||
width: screen.width,
|
||
height: screen.height,
|
||
colorDepth: screen.colorDepth
|
||
},
|
||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||
canvas: canvas.toDataURL(),
|
||
timestamp: Date.now()
|
||
})
|
||
})
|
||
|
||
this.sessionFingerprint = fingerprint
|
||
return fingerprint
|
||
} catch (error) {
|
||
console.error('❌ Error generating session fingerprint:', error)
|
||
return `fallback-${Date.now()}`
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Enhanced session validation using fingerprinting
|
||
*/
|
||
private async validateSessionIntegrity(): Promise<boolean> {
|
||
if (!this.page) return false
|
||
|
||
try {
|
||
// Check if TradingView shows any session invalidation indicators
|
||
const invalidationIndicators = [
|
||
'text="Are you human?"',
|
||
'text="Please verify you are human"',
|
||
'text="Security check"',
|
||
'[data-name="captcha"]',
|
||
'.captcha-container',
|
||
'iframe[src*="captcha"]',
|
||
'text="Session expired"',
|
||
'text="Please log in again"'
|
||
]
|
||
|
||
for (const indicator of invalidationIndicators) {
|
||
try {
|
||
if (await this.page.locator(indicator).isVisible({ timeout: 1000 })) {
|
||
console.log(`⚠️ Session invalidation detected: ${indicator}`)
|
||
return false
|
||
}
|
||
} catch (e) {
|
||
// Ignore timeout errors, continue checking
|
||
}
|
||
}
|
||
|
||
// Check if current fingerprint matches stored one
|
||
if (this.sessionFingerprint) {
|
||
const currentFingerprint = await this.generateSessionFingerprint()
|
||
const stored = JSON.parse(this.sessionFingerprint)
|
||
const current = JSON.parse(currentFingerprint)
|
||
|
||
// Allow some variation in timestamp but check core properties
|
||
if (stored.userAgent !== current.userAgent ||
|
||
stored.platform !== current.platform ||
|
||
stored.language !== current.language) {
|
||
console.log('⚠️ Session fingerprint mismatch detected')
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('❌ Error validating session integrity:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Perform human-like interactions before automation
|
||
*/
|
||
private async performHumanLikeInteractions(): Promise<void> {
|
||
if (!this.page || !this.humanBehaviorEnabled) return
|
||
|
||
console.log('🤖 Performing human-like interactions...')
|
||
|
||
try {
|
||
// Random combination of human-like behaviors
|
||
const behaviors = [
|
||
() => this.simulateHumanMouseMovement(),
|
||
() => this.simulateHumanScrolling(),
|
||
() => this.humanDelay(1000, 3000)
|
||
]
|
||
|
||
// Perform 1-2 random behaviors
|
||
const behaviorCount = Math.floor(Math.random() * 2) + 1
|
||
for (let i = 0; i < behaviorCount; i++) {
|
||
const behavior = behaviors[Math.floor(Math.random() * behaviors.length)]
|
||
await behavior()
|
||
}
|
||
|
||
// Wait a bit longer to let the page settle
|
||
await this.humanDelay(2000, 4000)
|
||
} catch (error) {
|
||
console.log('⚠️ Error performing human-like interactions:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Mark that a captcha was detected (to implement cooldown)
|
||
*/
|
||
private async markCaptchaDetected(): Promise<void> {
|
||
try {
|
||
const captchaMarkerFile = path.join(process.cwd(), 'captcha_detected.json')
|
||
const markerData = {
|
||
timestamp: new Date().toISOString(),
|
||
count: 1
|
||
}
|
||
|
||
// If marker already exists, increment count
|
||
if (await this.fileExists(captchaMarkerFile)) {
|
||
try {
|
||
const existing = JSON.parse(await fs.readFile(captchaMarkerFile, 'utf8'))
|
||
markerData.count = (existing.count || 0) + 1
|
||
} catch (e) {
|
||
// Use defaults if can't read existing file
|
||
}
|
||
}
|
||
|
||
await fs.writeFile(captchaMarkerFile, JSON.stringify(markerData, null, 2))
|
||
console.log(`📝 Marked captcha detection #${markerData.count} at ${markerData.timestamp}`)
|
||
} catch (error) {
|
||
console.log('⚠️ Error marking captcha detection:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if file exists
|
||
*/
|
||
private async fileExists(filePath: string): Promise<boolean> {
|
||
try {
|
||
await fs.access(filePath)
|
||
return true
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if browser is healthy and connected
|
||
*/
|
||
isBrowserHealthy(): boolean {
|
||
return !!(this.browser && this.browser.isConnected())
|
||
}
|
||
|
||
/**
|
||
* Ensure browser is ready for operations
|
||
*/
|
||
async ensureBrowserReady(): Promise<void> {
|
||
if (!this.isBrowserHealthy()) {
|
||
console.log('🔄 Browser not healthy, reinitializing...')
|
||
await this.forceCleanup()
|
||
await this.init()
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add process cleanup handlers to ensure browser instances are properly cleaned up
|
||
process.on('SIGTERM', async () => {
|
||
console.log('🔄 SIGTERM received, cleaning up browser...')
|
||
await TradingViewAutomation.getInstance().forceCleanup()
|
||
process.exit(0)
|
||
})
|
||
|
||
process.on('SIGINT', async () => {
|
||
console.log('🔄 SIGINT received, cleaning up browser...')
|
||
await TradingViewAutomation.getInstance().forceCleanup()
|
||
process.exit(0)
|
||
})
|
||
|
||
process.on('uncaughtException', async (error) => {
|
||
console.error('💥 Uncaught exception, cleaning up browser:', error)
|
||
await TradingViewAutomation.getInstance().forceCleanup()
|
||
process.exit(1)
|
||
})
|
||
|
||
process.on('unhandledRejection', async (reason, promise) => {
|
||
console.error('💥 Unhandled rejection, cleaning up browser:', reason)
|
||
await TradingViewAutomation.getInstance().forceCleanup()
|
||
process.exit(1)
|
||
})
|
||
|
||
export const tradingViewAutomation = TradingViewAutomation.getInstance()
|