const express = require('express'); const nodeHtmlToImage = require('node-html-to-image'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const app = express(); const PORT = 3000; const CACHE_DIR = path.join(__dirname, 'cache'); // Create cache directory if it doesn't exist if (!fs.existsSync(CACHE_DIR)) { fs.mkdirSync(CACHE_DIR); } app.use(express.json()); function hashConfig(config) { const configString = JSON.stringify(config); return crypto.createHash('sha256').update(configString).digest('hex'); } function getCachePath(hash) { return path.join(CACHE_DIR, `${hash}.png`); } function getCachedImage(config) { const hash = hashConfig(config); const cachePath = getCachePath(hash); if (fs.existsSync(cachePath)) { // Update file modification time to mark as recently accessed const now = new Date(); fs.utimesSync(cachePath, now, now); return fs.readFileSync(cachePath); } return null; } function cacheImage(config, imageBuffer) { const hash = hashConfig(config); const cachePath = getCachePath(hash); fs.writeFileSync(cachePath, imageBuffer); } function clearCache() { if (!fs.existsSync(CACHE_DIR)) { return; } const files = fs.readdirSync(CACHE_DIR); files.forEach(file => { const filePath = path.join(CACHE_DIR, file); fs.unlinkSync(filePath); }); console.log(`Cleared ${files.length} cached image(s)`); } function cleanupOldCache() { const ONE_DAY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds const now = Date.now(); if (!fs.existsSync(CACHE_DIR)) { return; } const files = fs.readdirSync(CACHE_DIR); let deletedCount = 0; files.forEach(file => { const filePath = path.join(CACHE_DIR, file); const stats = fs.statSync(filePath); // Check if file was last modified more than 24 hours ago if (now - stats.mtime.getTime() > ONE_DAY) { fs.unlinkSync(filePath); deletedCount++; } }); if (deletedCount > 0) { console.log(`Cleaned up ${deletedCount} cached image(s)`); } } function formatTimestamp(epoch) { const date = new Date(epoch * 1000); const hours = date.getHours(); const minutes = date.getMinutes().toString().padStart(2, '0'); const ampm = hours >= 12 ? 'PM' : 'AM'; const hour12 = hours % 12 || 12; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const month = months[date.getMonth()]; const day = date.getDate(); const year = date.getFullYear(); return `${hour12}:${minutes} ${ampm} ยท ${month} ${day}, ${year}`; } async function generateQuoteBuffer(config) { const htmlTemplate = fs.readFileSync(path.join(__dirname, 'quote.html'), 'utf8'); const configScript = ` `; const modifiedHtml = htmlTemplate.replace('', `${configScript}`); const image = await nodeHtmlToImage({ html: modifiedHtml, puppeteerArgs: { args: ['--no-sandbox'], defaultViewport: { width: 3240, height: 3240 } }, beforeScreenshot: async (page) => { await new Promise(resolve => setTimeout(resolve, 500)); } }); return image; } // GET endpoint - use query parameters app.get('/generate', async (req, res) => { try { const timestamp = req.query.timestamp ? formatTimestamp(parseInt(req.query.timestamp)) : formatTimestamp(Date.now() / 1000); const config = { displayName: req.query.displayName || "Anonymous", username: req.query.username || "@anonymous", avatarUrl: req.query.avatarUrl || "", text: req.query.text || "No text provided", imageUrl: req.query.imageUrl || null, timestamp: timestamp, viewsCount: req.query.viewsCount || "0" }; // Check cache first let image = getCachedImage(config); let fromCache = true; if (!image) { // Generate new image image = await generateQuoteBuffer(config); cacheImage(config, image); fromCache = false; } res.setHeader('Content-Type', 'image/png'); res.setHeader('X-Cache', fromCache ? 'HIT' : 'MISS'); res.send(image); } catch (error) { console.error(error); res.status(500).json({ error: 'Failed to generate image' }); } }); // POST endpoint - use request body app.post('/generate', async (req, res) => { try { const timestamp = req.body.timestamp ? formatTimestamp(parseInt(req.body.timestamp)) : formatTimestamp(Date.now() / 1000); const config = { displayName: req.body.displayName || "Anonymous", username: req.body.username || "@anonymous", avatarUrl: req.body.avatarUrl || "", text: req.body.text || "No text provided", imageUrl: req.body.imageUrl || null, timestamp: timestamp, viewsCount: req.body.viewsCount || "0" }; // Check cache first let image = getCachedImage(config); let fromCache = true; if (!image) { // Generate new image image = await generateQuoteBuffer(config); cacheImage(config, image); fromCache = false; } res.setHeader('Content-Type', 'image/png'); res.setHeader('X-Cache', fromCache ? 'HIT' : 'MISS'); res.send(image); } catch (error) { console.error(error); res.status(500).json({ error: 'Failed to generate image' }); } }); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'ok' }); }); // Clear all cache on startup clearCache(); // Run cleanup every hour setInterval(() => { cleanupOldCache(); }, 60 * 60 * 1000); app.listen(PORT, () => { console.log(`Quote generator API running on http://localhost:${PORT}`); console.log(`GET: http://localhost:${PORT}/generate?text=Hello&displayName=Test&username=@test×tamp=1735574400`); console.log(`POST: http://localhost:${PORT}/generate`); console.log(`Cache cleared on startup, cleanup runs every hour`); });