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 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' });
});
// Run cleanup every hour
setInterval(() => {
cleanupOldCache();
}, 60 * 60 * 1000);
// Run cleanup on startup
cleanupOldCache();
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 cleanup runs every hour, images older than 24h are removed`);
});