Add request logging middleware and improve avatar/image URL handling
This commit is contained in:
201
api.js
201
api.js
@@ -16,6 +16,42 @@ if (!fs.existsSync(CACHE_DIR)) {
|
|||||||
app.use(express.json({ limit: '1gb' }));
|
app.use(express.json({ limit: '1gb' }));
|
||||||
app.use(express.urlencoded({ limit: '1gb', extended: true }));
|
app.use(express.urlencoded({ limit: '1gb', extended: true }));
|
||||||
|
|
||||||
|
// Request logging middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`\n[${timestamp}] ${req.method} ${req.url}`);
|
||||||
|
|
||||||
|
if (req.method === 'POST' && req.body) {
|
||||||
|
const logBody = { ...req.body };
|
||||||
|
|
||||||
|
// Truncate long base64 strings for readability
|
||||||
|
if (logBody.avatarUrl && logBody.avatarUrl.startsWith('data:')) {
|
||||||
|
logBody.avatarUrl = logBody.avatarUrl.substring(0, 50) + '... (base64 truncated)';
|
||||||
|
}
|
||||||
|
if (logBody.imageUrl && logBody.imageUrl.startsWith('data:')) {
|
||||||
|
logBody.imageUrl = logBody.imageUrl.substring(0, 50) + '... (base64 truncated)';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Body:', JSON.stringify(logBody, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET' && Object.keys(req.query).length > 0) {
|
||||||
|
const logQuery = { ...req.query };
|
||||||
|
|
||||||
|
// Truncate long base64 strings for readability
|
||||||
|
if (logQuery.avatarUrl && logQuery.avatarUrl.startsWith('data:')) {
|
||||||
|
logQuery.avatarUrl = logQuery.avatarUrl.substring(0, 50) + '... (base64 truncated)';
|
||||||
|
}
|
||||||
|
if (logQuery.imageUrl && logQuery.imageUrl.startsWith('data:')) {
|
||||||
|
logQuery.imageUrl = logQuery.imageUrl.substring(0, 50) + '... (base64 truncated)';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Query:', JSON.stringify(logQuery, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
function normalizeConfig(config) {
|
function normalizeConfig(config) {
|
||||||
// Remove null, undefined, and empty string values
|
// Remove null, undefined, and empty string values
|
||||||
const normalized = {};
|
const normalized = {};
|
||||||
@@ -115,18 +151,119 @@ function formatTimestamp(epoch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateQuoteBuffer(config) {
|
async function generateQuoteBuffer(config) {
|
||||||
const htmlTemplate = fs.readFileSync(path.join(__dirname, 'quote.html'), 'utf8');
|
// Build HTML directly with values injected
|
||||||
|
const avatarHtml = config.avatarUrl
|
||||||
|
? `<img src="${config.avatarUrl}" style="width:100%;height:100%;object-fit:cover;" />`
|
||||||
|
: `<div style="width:100%;height:100%;background:rgb(51,54,57);"></div>`;
|
||||||
|
|
||||||
const configScript = `
|
const imageHtml = config.imageUrl
|
||||||
|
? `<div class="tweet-image-container" style="margin-bottom:12px;border-radius:16px;overflow:hidden;border:1px solid rgb(47,51,54);"><img src="${config.imageUrl}" style="width:100%;display:block;" /></div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
.square-container {
|
||||||
|
width: calc(100vmin - 20px);
|
||||||
|
height: calc(100vmin - 20px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.tweet-container {
|
||||||
|
width: 450px;
|
||||||
|
background-color: #000;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.tweet-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 12px;
|
||||||
|
background: rgb(51, 54, 57);
|
||||||
|
}
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.display-name {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgb(231, 233, 234);
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.username {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
color: rgb(113, 118, 123);
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.tweet-text {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
color: rgb(231, 233, 234);
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.tweet-time {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
color: rgb(113, 118, 123);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="square-container">
|
||||||
|
<div class="tweet-container">
|
||||||
|
<div class="tweet-header">
|
||||||
|
<div class="avatar">${avatarHtml}</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="display-name">${config.displayName}</span>
|
||||||
|
<span class="username">${config.username}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tweet-text">${config.text}</div>
|
||||||
|
${imageHtml}
|
||||||
|
<div class="tweet-time">${config.timestamp}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
window.tweetConfig = ${JSON.stringify(config)};
|
function fitToSquare() {
|
||||||
|
const square = document.querySelector('.square-container');
|
||||||
|
const tweet = document.querySelector('.tweet-container');
|
||||||
|
const scaleX = square.offsetWidth / tweet.offsetWidth;
|
||||||
|
const scaleY = square.offsetHeight / tweet.offsetHeight;
|
||||||
|
const scale = Math.min(scaleX, scaleY) * 0.95;
|
||||||
|
tweet.style.transform = 'scale(' + scale + ')';
|
||||||
|
}
|
||||||
|
window.onload = fitToSquare;
|
||||||
</script>
|
</script>
|
||||||
`;
|
</body>
|
||||||
|
</html>`;
|
||||||
const modifiedHtml = htmlTemplate.replace('</head>', `${configScript}</head>`);
|
|
||||||
|
|
||||||
const image = await nodeHtmlToImage({
|
const image = await nodeHtmlToImage({
|
||||||
html: modifiedHtml,
|
html: html,
|
||||||
puppeteerArgs: {
|
puppeteerArgs: {
|
||||||
args: ['--no-sandbox'],
|
args: ['--no-sandbox'],
|
||||||
defaultViewport: {
|
defaultViewport: {
|
||||||
@@ -135,7 +272,7 @@ async function generateQuoteBuffer(config) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeScreenshot: async (page) => {
|
beforeScreenshot: async (page) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,9 +287,9 @@ app.get('/generate', async (req, res) => {
|
|||||||
const config = {
|
const config = {
|
||||||
displayName: req.query.displayName || "Anonymous",
|
displayName: req.query.displayName || "Anonymous",
|
||||||
username: req.query.username || "@anonymous",
|
username: req.query.username || "@anonymous",
|
||||||
avatarUrl: req.query.avatarUrl || null,
|
avatarUrl: fixDataUri(req.query.avatarUrl) || null,
|
||||||
text: req.query.text || "No text provided",
|
text: req.query.text || "No text provided",
|
||||||
imageUrl: req.query.imageUrl || null,
|
imageUrl: fixDataUri(req.query.imageUrl) || null,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
viewsCount: req.query.viewsCount || "0"
|
viewsCount: req.query.viewsCount || "0"
|
||||||
};
|
};
|
||||||
@@ -178,16 +315,54 @@ app.get('/generate', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// POST endpoint - use request body
|
// POST endpoint - use request body
|
||||||
|
function detectImageType(base64String) {
|
||||||
|
// Extract base64 data
|
||||||
|
const base64Data = base64String.includes(',') ? base64String.split(',')[1] : base64String;
|
||||||
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
|
||||||
|
// Check magic numbers
|
||||||
|
if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) {
|
||||||
|
return 'image/jpeg';
|
||||||
|
} else if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) {
|
||||||
|
return 'image/png';
|
||||||
|
} else if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
||||||
|
return 'image/gif';
|
||||||
|
} else if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46) {
|
||||||
|
return 'image/webp';
|
||||||
|
}
|
||||||
|
return 'image/png'; // default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixDataUri(dataUri) {
|
||||||
|
if (!dataUri || !dataUri.startsWith('data:')) return dataUri;
|
||||||
|
|
||||||
|
// Extract the base64 part
|
||||||
|
const parts = dataUri.split(',');
|
||||||
|
if (parts.length !== 2) return dataUri;
|
||||||
|
|
||||||
|
const base64Data = parts[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const correctType = detectImageType(base64Data);
|
||||||
|
return `data:${correctType};base64,${base64Data}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Invalid base64 data:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.post('/generate', async (req, res) => {
|
app.post('/generate', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const timestamp = req.body.timestamp ? formatTimestamp(parseInt(req.body.timestamp)) : formatTimestamp(Date.now() / 1000);
|
const timestamp = req.body.timestamp ? formatTimestamp(parseInt(req.body.timestamp)) : formatTimestamp(Date.now() / 1000);
|
||||||
|
|
||||||
|
const username = req.body.username?.trim();
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
displayName: req.body.displayName || "Anonymous",
|
displayName: req.body.displayName || "Anonymous",
|
||||||
username: req.body.username || "@anonymous",
|
username: (username && username !== "@") ? username : "@anonymous",
|
||||||
avatarUrl: req.body.avatarUrl || null,
|
avatarUrl: fixDataUri(req.body.avatarUrl) || null,
|
||||||
text: req.body.text || "No text provided",
|
text: req.body.text || "No text provided",
|
||||||
imageUrl: req.body.imageUrl || null,
|
imageUrl: fixDataUri(req.body.imageUrl) || null,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
viewsCount: req.body.viewsCount || "0"
|
viewsCount: req.body.viewsCount || "0"
|
||||||
};
|
};
|
||||||
|
|||||||
26
quote.html
26
quote.html
@@ -47,13 +47,17 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-color: rgb(51, 54, 57);
|
background-color: rgb(51, 54, 57);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-placeholder {
|
.avatar-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -180,6 +184,7 @@
|
|||||||
<div class="tweet-header">
|
<div class="tweet-header">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<div class="avatar" id="avatar">
|
<div class="avatar" id="avatar">
|
||||||
|
<img id="avatarImg" style="display: none;" />
|
||||||
<div class="avatar-placeholder" id="avatarPlaceholder" style="display: none;">
|
<div class="avatar-placeholder" id="avatarPlaceholder" style="display: none;">
|
||||||
<div class="css-175oi2r r-sdzlij r-1udh08x r-45ll9u r-u8s1d r-1v2oles r-176fswd" style="width: calc(100% - 4px); height: calc(100% - 4px);">
|
<div class="css-175oi2r r-sdzlij r-1udh08x r-45ll9u r-u8s1d r-1v2oles r-176fswd" style="width: calc(100% - 4px); height: calc(100% - 4px);">
|
||||||
<div class="css-175oi2r r-172uzmj r-1pi2tsx r-13qz1uu r-1ny4l3l"></div>
|
<div class="css-175oi2r r-172uzmj r-1pi2tsx r-13qz1uu r-1ny4l3l"></div>
|
||||||
@@ -260,7 +265,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function loadTweetData() {
|
function loadTweetData() {
|
||||||
|
try {
|
||||||
const config = window.tweetConfig;
|
const config = window.tweetConfig;
|
||||||
|
console.log('Loading tweet data with config:', config);
|
||||||
|
|
||||||
document.getElementById('displayName').textContent = config.displayName;
|
document.getElementById('displayName').textContent = config.displayName;
|
||||||
document.getElementById('username').textContent = config.username;
|
document.getElementById('username').textContent = config.username;
|
||||||
@@ -268,14 +275,18 @@
|
|||||||
document.getElementById('tweetTime').textContent = config.timestamp;
|
document.getElementById('tweetTime').textContent = config.timestamp;
|
||||||
document.getElementById('viewsCount').textContent = config.viewsCount;
|
document.getElementById('viewsCount').textContent = config.viewsCount;
|
||||||
|
|
||||||
const avatar = document.getElementById('avatar');
|
const avatarImg = document.getElementById('avatarImg');
|
||||||
const avatarPlaceholder = document.getElementById('avatarPlaceholder');
|
const avatarPlaceholder = document.getElementById('avatarPlaceholder');
|
||||||
|
|
||||||
if (config.avatarUrl) {
|
if (config.avatarUrl) {
|
||||||
avatar.style.backgroundImage = `url("${config.avatarUrl}")`;
|
console.log('Setting avatar URL, length:', config.avatarUrl.length);
|
||||||
|
avatarImg.src = config.avatarUrl;
|
||||||
|
avatarImg.style.display = 'block';
|
||||||
avatarPlaceholder.style.display = 'none';
|
avatarPlaceholder.style.display = 'none';
|
||||||
|
console.log('Avatar image set successfully');
|
||||||
} else {
|
} else {
|
||||||
avatar.style.backgroundImage = 'none';
|
console.log('No avatarUrl provided, showing placeholder');
|
||||||
|
avatarImg.style.display = 'none';
|
||||||
avatarPlaceholder.style.display = 'flex';
|
avatarPlaceholder.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,11 +294,16 @@
|
|||||||
const tweetImage = document.getElementById('tweetImage');
|
const tweetImage = document.getElementById('tweetImage');
|
||||||
|
|
||||||
if (config.imageUrl) {
|
if (config.imageUrl) {
|
||||||
|
console.log('Setting image URL, length:', config.imageUrl.length);
|
||||||
tweetImage.src = config.imageUrl;
|
tweetImage.src = config.imageUrl;
|
||||||
imageContainer.style.display = 'block';
|
imageContainer.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
|
console.log('No imageUrl provided, hiding image container');
|
||||||
imageContainer.style.display = 'none';
|
imageContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading tweet data:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitTweetToSquare() {
|
function fitTweetToSquare() {
|
||||||
|
|||||||
Reference in New Issue
Block a user