// backend/services/ScreenshotService.js // Screenshot Service for GOD CRM // Auto-generates screenshots from CRM pages for Telegram posts // Uses Playwright with Chromium for headless browser rendering import { chromium } from 'playwright'; import { apiLogger } from '../utils/logger.js'; import path from 'path'; import fs from 'fs'; const logger = apiLogger; // Screenshot cache directory const SCREENSHOT_DIR = path.join(process.cwd(), 'uploads', 'screenshots'); // Ensure screenshot directory exists if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); } /** * Take a screenshot of any URL * @param {string} url - URL to screenshot * @param {object} opts - Options * @param {number} opts.width - Viewport width (default: 1200) * @param {number} opts.height - Viewport height (default: 630) * @param {boolean} opts.fullPage - Capture full page (default: false) * @param {string} opts.selector - CSS selector to capture specific element * @param {number} opts.delay - Wait ms after load (default: 1000) * @returns {Buffer} PNG buffer */ export async function takeScreenshot(url, opts = {}) { const { width = 1200, height = 630, fullPage = false, selector = null, delay = 1000 } = opts; let browser; try { logger.info(`[ScreenshotService] Taking screenshot of: ${url}`); browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] }); const page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 2 // Retina quality }); await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }); if (delay > 0) { await page.waitForTimeout(delay); } let buffer; if (selector) { const element = await page.$(selector); if (element) { buffer = await element.screenshot({ type: 'png' }); } else { logger.warn(`[ScreenshotService] Selector "${selector}" not found, taking full page`); buffer = await page.screenshot({ type: 'png', fullPage }); } } else { buffer = await page.screenshot({ type: 'png', fullPage }); } logger.info(`[ScreenshotService] Screenshot taken: ${buffer.length} bytes`); return buffer; } catch (error) { logger.error(`[ScreenshotService] Error: ${error.message}`); throw error; } finally { if (browser) await browser.close(); } } /** * Take a screenshot of a CRM widget/dashboard * @param {number} widgetId - Widget ID * @param {object} opts - Screenshot options * @returns {Buffer} PNG buffer */ export async function screenshotWidget(widgetId, opts = {}) { const baseUrl = process.env.CRM_BASE_URL || 'https://devcrm.hltrn.cc'; const url = `${baseUrl}/widgets/${widgetId}`; return takeScreenshot(url, { ...opts, delay: 2000 }); } /** * Take a screenshot of a CRM table view * @param {number} tableId - Table ID * @param {object} opts - Screenshot options * @returns {Buffer} PNG buffer */ export async function screenshotTable(tableId, opts = {}) { const baseUrl = process.env.CRM_BASE_URL || 'https://devcrm.hltrn.cc'; const url = `${baseUrl}/tables/${tableId}`; return takeScreenshot(url, { ...opts, delay: 2000 }); } /** * Generate a styled post card image from HTML template * @param {object} data - Post data * @param {string} data.title - Post title * @param {string} data.summary - Post summary * @param {string} data.source - Source name (Reddit, HN, etc.) * @param {string} data.sourceUrl - Link to original * @param {string} data.category - Category tag * @returns {Buffer} PNG buffer */ export async function generatePostCard(data) { const { title, summary, source = '', sourceUrl = '', category = 'AI News' } = data; const html = `
${escapeHtml(category)}
${escapeHtml(title)}
${escapeHtml(summary)}
`; let browser; try { browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage({ viewport: { width: 1200, height: 630 } }); await page.setContent(html, { waitUntil: 'networkidle' }); const buffer = await page.screenshot({ type: 'png' }); logger.info(`[ScreenshotService] Post card generated: ${buffer.length} bytes`); return buffer; } finally { if (browser) await browser.close(); } } /** * Save screenshot to file system * @param {Buffer} buffer - PNG buffer * @param {string} filename - File name * @returns {string} File path */ export function saveScreenshot(buffer, filename) { const filePath = path.join(SCREENSHOT_DIR, filename); fs.writeFileSync(filePath, buffer); logger.info(`[ScreenshotService] Saved: ${filePath}`); return filePath; } function escapeHtml(text) { if (!text) return ''; return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } export default { takeScreenshot, screenshotWidget, screenshotTable, generatePostCard, saveScreenshot };