UNPKG

@shaivpidadi/trends-js

Version:
166 lines (165 loc) 7.27 kB
import https from 'https'; import { URL } from 'url'; export class SessionManager { constructor(config = {}) { this.cookies = {}; this.lastRefresh = 0; this.userAgents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15' ]; this.config = { autoRefresh: config.autoRefresh ?? true, maxRetries: config.maxRetries ?? 3, baseDelayMs: config.baseDelayMs ?? 750, cookieRefreshInterval: config.cookieRefreshInterval ?? 30 * 60 * 1000 // 30 min }; if (config.initialCookies) { if (typeof config.initialCookies === 'string') { this.cookies = this.parseCookieString(config.initialCookies); } else { this.cookies = config.initialCookies; } } } async initialize() { // If we already have essential cookies (like NID) provided manually, // we can skip the initial fetch if desired, or verify them. // For now, we'll only fetch if we don't have them or if autoRefresh implies we should. if (Object.keys(this.cookies).length === 0) { await this.refreshSession(); } } parseCookieString(cookieStr) { const cookies = {}; if (!cookieStr) return cookies; cookieStr.split(';').forEach(pair => { const [key, value] = pair.split('='); if (key && value) { cookies[key.trim()] = value.trim(); } }); return cookies; } async refreshSession() { try { // Step 1: Hit main trends page to get initial cookies const mainCookies = await this.fetchCookies('https://trends.google.com/trends/'); // Step 2: Hit explore page to get additional auth cookies const exploreCookies = await this.fetchCookies('https://trends.google.com/trends/explore?geo=US&q=test', mainCookies); this.cookies = { ...this.cookies, ...mainCookies, ...exploreCookies }; this.lastRefresh = Date.now(); // console.log('[SessionManager] Session refreshed. Cookies:', Object.keys(this.cookies)); } catch (error) { console.error('[SessionManager] Failed to refresh session:', error); // Don't throw if we have fallback cookies, otherwise propagate if (Object.keys(this.cookies).length === 0) { throw error; } } } fetchCookies(url, existingCookies) { return new Promise((resolve, reject) => { const parsedUrl = new URL(url); const options = { hostname: parsedUrl.hostname, path: parsedUrl.pathname + parsedUrl.search, method: 'GET', headers: { 'User-Agent': this.getRandomUserAgent(), 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', ...(existingCookies ? { Cookie: this.serializeCookies(existingCookies) } : {}) } }; const req = https.request(options, (res) => { const cookies = this.parseCookies(res.headers['set-cookie'] || []); // Consume response body to free up memory/socket res.on('data', () => { }); res.on('end', () => resolve(cookies)); }); req.on('error', reject); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timeout')); }); req.end(); }); } parseCookies(setCookieHeaders) { const cookies = {}; for (const header of setCookieHeaders) { const [cookiePart] = header.split(';'); const [key, value] = cookiePart.split('='); const normalizedKey = key.replace(/__Secure-/g, '__Secure_'); // We accept more than just specific keys to be robust, but we can filter if needed. // Keeping the filter for now to avoid junk. if (['NID', 'AEC', '__Secure_BUCKET', 'OTZ', '_ga', '_gid', '__utma', '__utmz'].includes(normalizedKey)) { cookies[normalizedKey] = value; } } return cookies; } serializeCookies(cookies) { return Object.entries(cookies) .filter(([_, v]) => v !== undefined) .map(([k, v]) => { // Convert back to __Secure- format for sending const key = k.replace(/__Secure_/g, '__Secure-'); return `${key}=${v}`; }) .join('; '); } getRandomUserAgent() { return this.userAgents[Math.floor(Math.random() * this.userAgents.length)]; } async getCookieHeader() { // Auto-refresh if needed if (this.config.autoRefresh) { const timeSinceRefresh = Date.now() - this.lastRefresh; if (this.lastRefresh > 0 && timeSinceRefresh > this.config.cookieRefreshInterval) { // console.log('[SessionManager] Auto-refreshing session...'); await this.refreshSession(); // Background refresh? No, await to ensure valid session. } } return this.serializeCookies(this.cookies); } getRequestHeaders() { return { 'User-Agent': this.getRandomUserAgent(), 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', 'Referer': 'https://trends.google.com/', 'Origin': 'https://trends.google.com', 'Connection': 'keep-alive', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }; } // Manual cookie update from 429 responses updateFromSetCookie(setCookieHeaders) { const newCookies = this.parseCookies(setCookieHeaders); this.cookies = { ...this.cookies, ...newCookies }; // console.log('[SessionManager] Updated cookies from response'); } // For testing/debugging getCookies() { return { ...this.cookies }; } }