- Add comprehensive session persistence with cookies, localStorage, and sessionStorage - Implement stealth browser features to reduce bot detection - Add smartLogin() method that prioritizes saved sessions over fresh logins - Create session management utilities (refresh, clear, test validity) - Update enhanced screenshot service to use session persistence - Add comprehensive documentation and test script - Support manual login fallback when captcha is encountered - Sessions stored in .tradingview-session/ directory for Docker compatibility This solves the captcha problem by avoiding repeated logins through persistent sessions.
1598 lines
54 KiB
TypeScript
1598 lines
54 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
|
|
|
|
async init(): Promise<void> {
|
|
console.log('🚀 Initializing TradingView automation with session persistence...')
|
|
|
|
// Ensure session directory exists
|
|
await fs.mkdir(SESSION_DATA_DIR, { recursive: true })
|
|
|
|
this.browser = await chromium.launch({
|
|
headless: true, // Must be true for Docker containers
|
|
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=9222',
|
|
// 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'
|
|
]
|
|
})
|
|
|
|
if (!this.browser) {
|
|
throw new Error('Failed to launch browser')
|
|
}
|
|
|
|
// Create browser context with session persistence
|
|
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 },
|
|
// Add additional 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',
|
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
'Cache-Control': 'no-cache',
|
|
'Pragma': 'no-cache',
|
|
'Sec-Fetch-Dest': 'document',
|
|
'Sec-Fetch-Mode': 'navigate',
|
|
'Sec-Fetch-Site': 'none',
|
|
'Upgrade-Insecure-Requests': '1'
|
|
}
|
|
})
|
|
|
|
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 stealth measures to reduce bot detection
|
|
await this.page.addInitScript(() => {
|
|
// Override the navigator.webdriver property
|
|
Object.defineProperty(navigator, 'webdriver', {
|
|
get: () => undefined,
|
|
});
|
|
|
|
// Mock plugins
|
|
Object.defineProperty(navigator, 'plugins', {
|
|
get: () => [1, 2, 3, 4, 5],
|
|
});
|
|
|
|
// Mock languages
|
|
Object.defineProperty(navigator, 'languages', {
|
|
get: () => ['en-US', 'en'],
|
|
});
|
|
|
|
// Override permissions API to avoid detection
|
|
const originalQuery = window.navigator.permissions.query;
|
|
window.navigator.permissions.query = (parameters: any) => {
|
|
if (parameters.name === 'notifications') {
|
|
return Promise.resolve({
|
|
state: Notification.permission,
|
|
name: parameters.name,
|
|
onchange: null,
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
dispatchEvent: () => false
|
|
} as PermissionStatus);
|
|
}
|
|
return originalQuery.call(window.navigator.permissions, parameters);
|
|
};
|
|
})
|
|
|
|
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(3000)
|
|
}
|
|
|
|
// Check for login indicators
|
|
const loginIndicators = [
|
|
'[data-name="watchlist-button"]',
|
|
'.tv-header__watchlist-button',
|
|
'.tv-header__user-menu-button',
|
|
'button:has-text("M")',
|
|
'.js-header-user-menu-button',
|
|
'[data-name="user-menu"]'
|
|
]
|
|
|
|
for (const selector of loginIndicators) {
|
|
try {
|
|
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
|
|
console.log(`✅ Found login indicator: ${selector}`)
|
|
this.isAuthenticated = true
|
|
return true
|
|
}
|
|
} catch (e) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Additional check: look for sign-in buttons (indicates not logged in)
|
|
const signInSelectors = [
|
|
'a[href*="signin"]',
|
|
'button:has-text("Sign in")',
|
|
'.tv-header__user-menu-button--anonymous'
|
|
]
|
|
|
|
for (const selector of signInSelectors) {
|
|
try {
|
|
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
|
|
console.log(`❌ Found sign-in button: ${selector} - not logged in`)
|
|
this.isAuthenticated = false
|
|
return false
|
|
}
|
|
} catch (e) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
console.log('🤔 Login status unclear, will attempt login')
|
|
this.isAuthenticated = false
|
|
return false
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error checking 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
|
|
const loggedIn = await this.checkLoginStatus()
|
|
if (loggedIn) {
|
|
console.log('✅ Already logged in, skipping login steps')
|
|
return true
|
|
}
|
|
|
|
console.log('Navigating to TradingView login page...')
|
|
|
|
// Try different login URLs that TradingView might use
|
|
const loginUrls = [
|
|
'https://www.tradingview.com/accounts/signin/',
|
|
'https://www.tradingview.com/sign-in/',
|
|
'https://www.tradingview.com/'
|
|
]
|
|
|
|
let loginPageLoaded = false
|
|
for (const url of loginUrls) {
|
|
try {
|
|
console.log(`Trying login URL: ${url}`)
|
|
await this.page.goto(url, {
|
|
waitUntil: 'networkidle',
|
|
timeout: 30000
|
|
})
|
|
|
|
// Check if we're on the login page or need to navigate to it
|
|
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/') {
|
|
// If we're on the main page, try to find and click the Sign In button
|
|
console.log('On main page, looking for Sign In button...')
|
|
const signInSelectors = [
|
|
'a[href*="signin"]',
|
|
'a:has-text("Sign in")',
|
|
'button:has-text("Sign in")',
|
|
'.tv-header__user-menu-button--anonymous',
|
|
'[data-name="header-user-menu-sign-in"]',
|
|
'.js-signin-button'
|
|
]
|
|
|
|
for (const selector of signInSelectors) {
|
|
try {
|
|
console.log(`Trying sign in selector: ${selector}`)
|
|
await this.page.waitForSelector(selector, { timeout: 3000 })
|
|
await this.page.click(selector)
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 })
|
|
|
|
const newUrl = await this.page.url()
|
|
if (newUrl.includes('signin') || newUrl.includes('login')) {
|
|
console.log('Successfully navigated to login page via sign in button')
|
|
loginPageLoaded = true
|
|
break
|
|
}
|
|
} catch (e) {
|
|
console.log(`Sign in selector ${selector} not found or failed`)
|
|
}
|
|
}
|
|
|
|
if (loginPageLoaded) break
|
|
}
|
|
|
|
} catch (e) {
|
|
console.log(`Failed to load ${url}:`, e)
|
|
}
|
|
}
|
|
|
|
if (!loginPageLoaded) {
|
|
console.log('Could not reach login page, trying to proceed anyway...')
|
|
}
|
|
|
|
// Take a screenshot to debug the current page
|
|
await this.takeDebugScreenshot('page_loaded')
|
|
|
|
// Wait for page to settle and dynamic content to load
|
|
await this.page.waitForTimeout(5000)
|
|
|
|
// Log current URL and page title for debugging
|
|
const currentUrl = await this.page.url()
|
|
const pageTitle = await this.page.title()
|
|
console.log('Current URL:', currentUrl)
|
|
console.log('Page title:', pageTitle)
|
|
|
|
// Check if we got redirected or are on an unexpected page
|
|
if (!currentUrl.includes('tradingview.com')) {
|
|
console.log('WARNING: Not on TradingView domain!')
|
|
await this.takeDebugScreenshot('wrong_domain')
|
|
}
|
|
|
|
// Log page content length and check for common elements
|
|
const bodyContent = await this.page.textContent('body')
|
|
console.log('Page content length:', bodyContent?.length || 0)
|
|
console.log('Page content preview:', bodyContent?.substring(0, 500) || 'No content')
|
|
|
|
// Check for iframes that might contain the login form
|
|
const iframes = await this.page.$$('iframe')
|
|
console.log('Number of iframes found:', iframes.length)
|
|
if (iframes.length > 0) {
|
|
for (let i = 0; i < iframes.length; i++) {
|
|
const src = await iframes[i].getAttribute('src')
|
|
console.log(`Iframe ${i} src:`, src)
|
|
}
|
|
}
|
|
|
|
// Wait for any dynamic content to load
|
|
try {
|
|
// Wait for form or login-related elements to appear
|
|
await Promise.race([
|
|
this.page.waitForSelector('form', { timeout: 10000 }),
|
|
this.page.waitForSelector('input[type="email"]', { timeout: 10000 }),
|
|
this.page.waitForSelector('input[type="text"]', { timeout: 10000 }),
|
|
this.page.waitForSelector('input[name*="email"]', { timeout: 10000 }),
|
|
this.page.waitForSelector('input[name*="username"]', { timeout: 10000 })
|
|
])
|
|
console.log('Form elements detected, proceeding...')
|
|
} catch (e) {
|
|
console.log('No form elements detected within timeout, continuing anyway...')
|
|
}
|
|
|
|
// Check for common login-related elements
|
|
const loginElements = await this.page.$$eval('*', (elements: Element[]) => {
|
|
const found = []
|
|
for (const el of elements) {
|
|
const text = el.textContent?.toLowerCase() || ''
|
|
if (text.includes('login') || text.includes('sign in') || text.includes('email') || text.includes('username')) {
|
|
found.push({
|
|
tagName: el.tagName,
|
|
text: text.substring(0, 100),
|
|
className: el.className,
|
|
id: el.id
|
|
})
|
|
}
|
|
}
|
|
return found.slice(0, 10) // Limit to first 10 matches
|
|
})
|
|
console.log('Login-related elements found:', JSON.stringify(loginElements, null, 2))
|
|
|
|
// CRITICAL FIX: TradingView requires clicking "Email" button to show login form
|
|
console.log('🔍 Looking for Email login trigger button...')
|
|
|
|
try {
|
|
// Wait for the "Email" button to appear and click it
|
|
const emailButton = this.page.locator('text="Email"').first()
|
|
await emailButton.waitFor({ state: 'visible', timeout: 10000 })
|
|
console.log('✅ Found Email button, clicking...')
|
|
|
|
await emailButton.click()
|
|
console.log('🖱️ Clicked Email button successfully')
|
|
|
|
// Wait for login form to appear after clicking
|
|
await this.page.waitForTimeout(3000)
|
|
console.log('⏳ Waiting for login form to appear...')
|
|
|
|
} catch (error) {
|
|
console.log(`❌ Could not find or click Email button: ${error}`)
|
|
|
|
// Fallback: try other possible email triggers
|
|
const emailTriggers = [
|
|
'button:has-text("Email")',
|
|
'button:has-text("email")',
|
|
'[data-name="email"]',
|
|
'text="Sign in with email"',
|
|
'text="Continue with email"'
|
|
]
|
|
|
|
let triggerFound = false
|
|
for (const trigger of emailTriggers) {
|
|
try {
|
|
const element = this.page.locator(trigger).first()
|
|
if (await element.isVisible({ timeout: 2000 })) {
|
|
console.log(`🔄 Trying fallback trigger: ${trigger}`)
|
|
await element.click()
|
|
await this.page.waitForTimeout(2000)
|
|
triggerFound = true
|
|
break
|
|
}
|
|
} catch (e) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (!triggerFound) {
|
|
await this.takeDebugScreenshot('no_email_trigger')
|
|
throw new Error('Could not find Email button or trigger to show login form')
|
|
}
|
|
}
|
|
|
|
// Try to find email input with various selectors
|
|
console.log('Looking for email input field...')
|
|
const emailSelectors = [
|
|
// TradingView specific selectors (discovered through debugging) - PRIORITY
|
|
'input[name="id_username"]',
|
|
|
|
// Standard selectors
|
|
'input[name="username"]',
|
|
'input[type="email"]',
|
|
'input[data-name="email"]',
|
|
'input[placeholder*="email" i]',
|
|
'input[placeholder*="username" i]',
|
|
'input[id*="email" i]',
|
|
'input[id*="username" i]',
|
|
'input[class*="email" i]',
|
|
'input[class*="username" i]',
|
|
'input[data-testid*="email" i]',
|
|
'input[data-testid*="username" i]',
|
|
'input[name*="email" i]',
|
|
'input[name*="user" i]',
|
|
'form input[type="text"]',
|
|
'form input:not([type="password"]):not([type="hidden"])',
|
|
'.signin-form input[type="text"]',
|
|
'.login-form input[type="text"]',
|
|
'[data-role="email"] input',
|
|
'[data-role="username"] input',
|
|
|
|
// More TradingView specific selectors
|
|
'input[autocomplete="username"]',
|
|
'input[autocomplete="email"]',
|
|
'.tv-signin-dialog input[type="text"]',
|
|
'.tv-signin-dialog input[type="email"]',
|
|
'#id_username',
|
|
'#email',
|
|
'#username',
|
|
'input[data-test="username"]',
|
|
'input[data-test="email"]'
|
|
]
|
|
|
|
let emailInput = null
|
|
// First pass: Try selectors with timeout
|
|
for (const selector of emailSelectors) {
|
|
try {
|
|
console.log(`Trying email selector: ${selector}`)
|
|
await this.page.waitForSelector(selector, { timeout: 2000 })
|
|
const isVisible = await this.page.isVisible(selector)
|
|
if (isVisible) {
|
|
emailInput = selector
|
|
console.log(`Found email input with selector: ${selector}`)
|
|
break
|
|
}
|
|
} catch (e) {
|
|
console.log(`Email selector ${selector} not found or not visible`)
|
|
}
|
|
}
|
|
|
|
// Second pass: If no input found, check all visible inputs
|
|
if (!emailInput) {
|
|
console.log('No email input found with standard selectors. Checking all visible inputs...')
|
|
|
|
try {
|
|
const visibleInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) =>
|
|
inputs
|
|
.filter((input: HTMLInputElement) => {
|
|
const style = window.getComputedStyle(input)
|
|
return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null
|
|
})
|
|
.map((input: HTMLInputElement, index: number) => ({
|
|
index,
|
|
type: input.type,
|
|
name: input.name,
|
|
id: input.id,
|
|
className: input.className,
|
|
placeholder: input.placeholder,
|
|
'data-name': input.getAttribute('data-name'),
|
|
'data-testid': input.getAttribute('data-testid'),
|
|
'autocomplete': input.getAttribute('autocomplete'),
|
|
outerHTML: input.outerHTML.substring(0, 300)
|
|
}))
|
|
)
|
|
|
|
console.log('Visible inputs found:', JSON.stringify(visibleInputs, null, 2))
|
|
|
|
// Try to find the first visible text or email input
|
|
if (visibleInputs.length > 0) {
|
|
const usernameInput = visibleInputs.find((input: any) =>
|
|
input.type === 'email' ||
|
|
input.type === 'text' ||
|
|
input.name?.toLowerCase().includes('user') ||
|
|
input.name?.toLowerCase().includes('email') ||
|
|
input.placeholder?.toLowerCase().includes('email') ||
|
|
input.placeholder?.toLowerCase().includes('user')
|
|
)
|
|
|
|
if (usernameInput) {
|
|
// Create selector for this input
|
|
if (usernameInput.id) {
|
|
emailInput = `#${usernameInput.id}`
|
|
} else if (usernameInput.name) {
|
|
emailInput = `input[name="${usernameInput.name}"]`
|
|
} else {
|
|
emailInput = `input:nth-of-type(${usernameInput.index + 1})`
|
|
}
|
|
console.log(`Using detected email input: ${emailInput}`)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('Error analyzing visible inputs:', e)
|
|
}
|
|
}
|
|
|
|
if (!emailInput) {
|
|
console.log('No email input found. Logging all input elements on page...')
|
|
const allInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) =>
|
|
inputs.map((input: HTMLInputElement) => ({
|
|
type: input.type,
|
|
name: input.name,
|
|
id: input.id,
|
|
className: input.className,
|
|
placeholder: input.placeholder,
|
|
'data-name': input.getAttribute('data-name'),
|
|
'data-testid': input.getAttribute('data-testid'),
|
|
outerHTML: input.outerHTML.substring(0, 200)
|
|
}))
|
|
)
|
|
console.log('All inputs found:', JSON.stringify(allInputs, null, 2))
|
|
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)
|
|
|
|
// Try to find password input with various selectors
|
|
console.log('Looking for password input field...')
|
|
const passwordSelectors = [
|
|
// TradingView specific selectors (discovered through debugging) - PRIORITY
|
|
'input[name="id_password"]',
|
|
|
|
// Standard selectors
|
|
'input[name="password"]',
|
|
'input[type="password"]',
|
|
'input[data-name="password"]',
|
|
'input[placeholder*="password" i]',
|
|
'input[id*="password" i]',
|
|
'input[class*="password" i]',
|
|
'input[data-testid*="password" i]',
|
|
'form input[type="password"]',
|
|
'.signin-form input[type="password"]',
|
|
'.login-form input[type="password"]',
|
|
'[data-role="password"] input',
|
|
|
|
// More TradingView specific selectors
|
|
'input[autocomplete="current-password"]',
|
|
'.tv-signin-dialog input[type="password"]',
|
|
'#id_password',
|
|
'#password',
|
|
'input[data-test="password"]'
|
|
]
|
|
|
|
let passwordInput = null
|
|
// First pass: Try selectors with timeout
|
|
for (const selector of passwordSelectors) {
|
|
try {
|
|
console.log(`Trying password selector: ${selector}`)
|
|
await this.page.waitForSelector(selector, { timeout: 2000 })
|
|
const isVisible = await this.page.isVisible(selector)
|
|
if (isVisible) {
|
|
passwordInput = selector
|
|
console.log(`Found password input with selector: ${selector}`)
|
|
break
|
|
}
|
|
} catch (e) {
|
|
console.log(`Password selector ${selector} not found or not visible`)
|
|
}
|
|
}
|
|
|
|
// Second pass: If no password input found, look for any visible password field
|
|
if (!passwordInput) {
|
|
console.log('No password input found with standard selectors. Checking all password inputs...')
|
|
|
|
try {
|
|
const passwordInputs = await this.page.$$eval('input[type="password"]', (inputs: HTMLInputElement[]) =>
|
|
inputs
|
|
.filter((input: HTMLInputElement) => {
|
|
const style = window.getComputedStyle(input)
|
|
return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null
|
|
})
|
|
.map((input: HTMLInputElement, index: number) => ({
|
|
index,
|
|
name: input.name,
|
|
id: input.id,
|
|
className: input.className,
|
|
placeholder: input.placeholder,
|
|
outerHTML: input.outerHTML.substring(0, 300)
|
|
}))
|
|
)
|
|
|
|
console.log('Password inputs found:', JSON.stringify(passwordInputs, null, 2))
|
|
|
|
if (passwordInputs.length > 0) {
|
|
const firstPassword = passwordInputs[0]
|
|
if (firstPassword.id) {
|
|
passwordInput = `#${firstPassword.id}`
|
|
} else if (firstPassword.name) {
|
|
passwordInput = `input[name="${firstPassword.name}"]`
|
|
} else {
|
|
passwordInput = `input[type="password"]:nth-of-type(${firstPassword.index + 1})`
|
|
}
|
|
console.log(`Using detected password input: ${passwordInput}`)
|
|
}
|
|
} catch (e) {
|
|
console.log('Error analyzing password inputs:', e)
|
|
}
|
|
}
|
|
|
|
if (!passwordInput) {
|
|
console.log('No password input found. Taking debug screenshot...')
|
|
await this.takeDebugScreenshot('no_password_field')
|
|
throw new Error('Could not find password input field')
|
|
}
|
|
|
|
// Fill password
|
|
console.log('Filling password field...')
|
|
await this.page.fill(passwordInput, password)
|
|
|
|
// Handle potential captcha
|
|
console.log('Checking for captcha...')
|
|
try {
|
|
const captchaFrame = this.page.frameLocator('iframe[src*="recaptcha"]').first()
|
|
const captchaCheckbox = captchaFrame.locator('div.recaptcha-checkbox-border')
|
|
|
|
if (await captchaCheckbox.isVisible({ timeout: 3000 })) {
|
|
console.log('Captcha detected, clicking checkbox...')
|
|
await captchaCheckbox.click()
|
|
|
|
// Wait a bit for captcha to process
|
|
await this.page.waitForTimeout(5000)
|
|
|
|
// Check if captcha is solved
|
|
const isSolved = await captchaFrame.locator('.recaptcha-checkbox-checked').isVisible({ timeout: 10000 })
|
|
if (!isSolved) {
|
|
console.log('Captcha may require manual solving. Waiting 15 seconds...')
|
|
await this.page.waitForTimeout(15000)
|
|
}
|
|
}
|
|
} catch (captchaError: any) {
|
|
console.log('No captcha found or captcha handling failed:', captchaError?.message || 'Unknown error')
|
|
}
|
|
|
|
// Find and click sign in button
|
|
console.log('Looking for sign in button...')
|
|
const submitSelectors = [
|
|
'button[type="submit"]',
|
|
'button:has-text("Sign in")',
|
|
'button:has-text("Sign In")',
|
|
'button:has-text("Log in")',
|
|
'button:has-text("Log In")',
|
|
'button:has-text("Login")',
|
|
'.tv-button--primary',
|
|
'input[type="submit"]',
|
|
'[data-testid="signin-button"]',
|
|
'[data-testid="login-button"]',
|
|
'.signin-button',
|
|
'.login-button',
|
|
'form button',
|
|
'button[class*="submit"]',
|
|
'button[class*="signin"]',
|
|
'button[class*="login"]'
|
|
]
|
|
|
|
let submitButton = null
|
|
for (const selector of submitSelectors) {
|
|
try {
|
|
console.log(`Trying submit selector: ${selector}`)
|
|
const element = this.page.locator(selector)
|
|
if (await element.isVisible({ timeout: 2000 })) {
|
|
submitButton = selector
|
|
console.log(`Found submit button with selector: ${selector}`)
|
|
break
|
|
}
|
|
} catch (e) {
|
|
console.log(`Submit selector ${selector} not found`)
|
|
}
|
|
}
|
|
|
|
if (!submitButton) {
|
|
console.log('No submit button found. Taking debug screenshot...')
|
|
await this.takeDebugScreenshot('no_submit_button')
|
|
|
|
// Log all buttons on the page
|
|
const allButtons = await this.page.$$eval('button', (buttons: HTMLButtonElement[]) =>
|
|
buttons.map((button: HTMLButtonElement) => ({
|
|
type: button.type,
|
|
textContent: button.textContent?.trim(),
|
|
className: button.className,
|
|
id: button.id,
|
|
outerHTML: button.outerHTML.substring(0, 200)
|
|
}))
|
|
)
|
|
console.log('All buttons found:', JSON.stringify(allButtons, null, 2))
|
|
|
|
throw new Error('Could not find submit button')
|
|
}
|
|
|
|
console.log('Clicking sign in button...')
|
|
await this.page.click(submitButton)
|
|
|
|
// Wait for successful login - look for the "M" watchlist indicator
|
|
console.log('Waiting for login success indicators...')
|
|
try {
|
|
// Wait for any of these success indicators
|
|
await Promise.race([
|
|
this.page.waitForSelector('[data-name="watchlist-button"], .tv-header__watchlist-button, button:has-text("M")', {
|
|
timeout: 20000
|
|
}),
|
|
this.page.waitForSelector('.tv-header__user-menu-button, .js-header-user-menu-button', {
|
|
timeout: 20000
|
|
}),
|
|
this.page.waitForSelector('.tv-header__logo', {
|
|
timeout: 20000
|
|
})
|
|
])
|
|
|
|
// Additional check - make sure we're not still on login page
|
|
await this.page.waitForFunction(
|
|
() => !window.location.href.includes('/accounts/signin/'),
|
|
{ timeout: 10000 }
|
|
)
|
|
|
|
console.log('Login successful!')
|
|
this.isAuthenticated = true
|
|
|
|
// Save session after successful login
|
|
await this.saveSession()
|
|
|
|
return true
|
|
} catch (error) {
|
|
console.error('Login verification failed:', error)
|
|
|
|
// Take a debug screenshot
|
|
await this.takeDebugScreenshot('login_failed')
|
|
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, proceeding with manual login...')
|
|
console.log('⚠️ IMPORTANT: Manual intervention required due to captcha protection.')
|
|
console.log('📱 Please log in manually in a browser and the session will be saved for future use.')
|
|
|
|
// Navigate to login page for manual login
|
|
await this.page.goto('https://www.tradingview.com/accounts/signin/', {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000
|
|
})
|
|
|
|
// Wait and give user time to manually complete login
|
|
console.log('⏳ Waiting for manual login completion...')
|
|
console.log('💡 You have 2 minutes to complete login manually in the browser.')
|
|
|
|
// Check every 10 seconds for login completion
|
|
let attempts = 0
|
|
const maxAttempts = 12 // 2 minutes
|
|
|
|
while (attempts < maxAttempts) {
|
|
await this.page.waitForTimeout(10000) // Wait 10 seconds
|
|
attempts++
|
|
|
|
console.log(`🔄 Checking login status (attempt ${attempts}/${maxAttempts})...`)
|
|
const loggedIn = await this.checkLoginStatus()
|
|
|
|
if (loggedIn) {
|
|
console.log('✅ Manual login detected! Saving session for future use.')
|
|
this.isAuthenticated = true
|
|
await this.saveSession()
|
|
return true
|
|
}
|
|
}
|
|
|
|
console.log('⏰ Timeout waiting for manual login.')
|
|
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 {
|
|
const { symbol = 'SOLUSD', timeframe = '5', waitForChart = true } = options
|
|
|
|
console.log('Navigating to chart page...')
|
|
|
|
// Wait a bit after login before navigating
|
|
await this.page.waitForTimeout(2000)
|
|
|
|
// 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
|
|
})
|
|
}
|
|
|
|
// 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
|
|
await this.page.waitForTimeout(5000)
|
|
}
|
|
|
|
// 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')
|
|
}
|
|
}
|
|
|
|
async takeScreenshot(filename: string): Promise<string> {
|
|
if (!this.page) throw new Error('Page not initialized')
|
|
|
|
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
|
await fs.mkdir(screenshotsDir, { recursive: true })
|
|
|
|
const fullPath = path.join(screenshotsDir, filename)
|
|
|
|
await this.page.screenshot({
|
|
path: fullPath,
|
|
fullPage: false, // Only visible area
|
|
type: 'png'
|
|
})
|
|
|
|
console.log(`Screenshot saved: ${filename}`)
|
|
return filename
|
|
}
|
|
|
|
private async takeDebugScreenshot(prefix: string): Promise<void> {
|
|
try {
|
|
const timestamp = Date.now()
|
|
const filename = `debug_${prefix}_${timestamp}.png`
|
|
await this.takeScreenshot(filename)
|
|
} catch (error) {
|
|
console.error('Failed to take debug screenshot:', error)
|
|
}
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
// Save session data before closing
|
|
if (this.isAuthenticated) {
|
|
await this.saveSession()
|
|
}
|
|
|
|
if (this.page) {
|
|
await this.page.close()
|
|
this.page = null
|
|
}
|
|
if (this.context) {
|
|
await this.context.close()
|
|
this.context = null
|
|
}
|
|
if (this.browser) {
|
|
await this.browser.close()
|
|
this.browser = 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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test session persistence by checking if saved session data exists and is valid
|
|
*/
|
|
async testSessionPersistence(): Promise<{
|
|
hasSessionData: boolean
|
|
isValid: boolean
|
|
sessionInfo: any
|
|
}> {
|
|
try {
|
|
console.log('🧪 Testing session persistence...')
|
|
|
|
const sessionInfo = await this.getSessionInfo()
|
|
console.log('📊 Current session info:', sessionInfo)
|
|
|
|
if (!sessionInfo.hasSavedCookies && !sessionInfo.hasSavedStorage) {
|
|
console.log('❌ No saved session data found')
|
|
return {
|
|
hasSessionData: false,
|
|
isValid: false,
|
|
sessionInfo
|
|
}
|
|
}
|
|
|
|
console.log('✅ Saved session data found')
|
|
console.log(`🍪 Cookies: ${sessionInfo.cookiesCount}`)
|
|
console.log(`💾 Storage: ${sessionInfo.hasSavedStorage ? 'Yes' : 'No'}`)
|
|
|
|
// Try to use the session
|
|
if (this.page) {
|
|
// Navigate to TradingView to test session validity
|
|
await this.page.goto('https://www.tradingview.com', {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000
|
|
})
|
|
|
|
// Restore session storage
|
|
await this.restoreSessionStorage()
|
|
|
|
// Check if session is still valid
|
|
const isLoggedIn = await this.checkLoginStatus()
|
|
|
|
if (isLoggedIn) {
|
|
console.log('🎉 Session is valid and user is logged in!')
|
|
return {
|
|
hasSessionData: true,
|
|
isValid: true,
|
|
sessionInfo
|
|
}
|
|
} else {
|
|
console.log('⚠️ Session data exists but appears to be expired')
|
|
return {
|
|
hasSessionData: true,
|
|
isValid: false,
|
|
sessionInfo
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
hasSessionData: true,
|
|
isValid: false,
|
|
sessionInfo
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Session persistence test failed:', error)
|
|
return {
|
|
hasSessionData: false,
|
|
isValid: false,
|
|
sessionInfo: null
|
|
}
|
|
}
|
|
}
|
|
|
|
// Utility method to wait for chart data to load
|
|
async waitForChartData(timeout: number = 15000): Promise<boolean> {
|
|
if (!this.page) return false
|
|
|
|
try {
|
|
console.log('Waiting for chart data to load...')
|
|
|
|
// Wait for chart canvas or chart elements to be present
|
|
await Promise.race([
|
|
this.page.waitForSelector('canvas', { timeout }),
|
|
this.page.waitForSelector('.tv-lightweight-charts', { timeout }),
|
|
this.page.waitForSelector('.tv-chart-view', { timeout })
|
|
])
|
|
|
|
// Additional wait for data to load
|
|
await this.page.waitForTimeout(3000)
|
|
|
|
console.log('Chart data loaded successfully')
|
|
return true
|
|
} catch (error) {
|
|
console.error('Chart data loading timeout:', error)
|
|
await this.takeDebugScreenshot('chart_data_timeout')
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Get current page URL for debugging
|
|
async getCurrentUrl(): Promise<string> {
|
|
if (!this.page) return ''
|
|
return await this.page.url()
|
|
}
|
|
|
|
// Check if we're logged in
|
|
async isLoggedIn(): Promise<boolean> {
|
|
if (!this.page) return false
|
|
|
|
try {
|
|
const indicators = [
|
|
'[data-name="watchlist-button"]',
|
|
'.tv-header__watchlist-button',
|
|
'.tv-header__user-menu-button',
|
|
'button:has-text("M")'
|
|
]
|
|
|
|
for (const selector of indicators) {
|
|
try {
|
|
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
|
|
return true
|
|
}
|
|
} catch (e) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return false
|
|
} catch (error) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Check if file exists
|
|
private async fileExists(filePath: string): Promise<boolean> {
|
|
try {
|
|
await fs.access(filePath)
|
|
return true
|
|
} catch (error) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
export const tradingViewAutomation = new TradingViewAutomation()
|