234 lines
6.8 KiB
JavaScript
234 lines
6.8 KiB
JavaScript
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({ limit: '1gb' }));
|
|
app.use(express.urlencoded({ limit: '1gb', extended: true }));
|
|
|
|
function normalizeConfig(config) {
|
|
// Remove null, undefined, and empty string values
|
|
const normalized = {};
|
|
for (const [key, value] of Object.entries(config)) {
|
|
if (value !== null && value !== undefined && value !== '') {
|
|
normalized[key] = value;
|
|
}
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function hashConfig(config) {
|
|
const normalized = normalizeConfig(config);
|
|
const configString = JSON.stringify(normalized);
|
|
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 = `
|
|
<script>
|
|
window.tweetConfig = ${JSON.stringify(config)};
|
|
</script>
|
|
`;
|
|
|
|
const modifiedHtml = htmlTemplate.replace('</head>', `${configScript}</head>`);
|
|
|
|
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 || null,
|
|
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 || null,
|
|
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`);
|
|
});
|