const express = require('express'); const { Canvas, loadImage } = require('skia-canvas'); const axios = require('axios'); // For Translation const app = express(); const multer = require('multer'); // ෆයිල් ගන්න const { Sticker, StickerTypes } = require('wa-sticker-formatter'); const upload = multer({ storage: multer.memoryStorage() }); app.use(express.json({ limit: '50mb' })); app.get('/', (req, res) => res.send("🚀 Cipher-MD ULTRA-SHARP ENGINE")); // ============================================================ // 🎨 1. WELCOME IMAGE GENERATOR (Exact Custom Design) // ============================================================ app.post('/welcome', upload.single('image'), async (req, res) => { try { // ෆොටෝ එකක් නැත්නම් එරර් එකක් if (!req.file) return res.status(400).send("No Profile Pic Uploaded"); // බොට්ගෙන් එන ඩේටා (gName = title විදිහට ගන්නවා) const { title, count, userName } = req.body; const gName = title || "Group Name"; // නම හිස් නම් default එකක් const size = 1024; const canvas = new Canvas(size, size); const ctx = canvas.getContext('2d'); const watermark = "SYNTIOX VISUAL ENGINE"; // Style 1 හෝ 3 විතරයි පාවිච්චි කරන්නේ const styleId = Math.random() > 0.5 ? 1 : 3; // 1. Image Load (Buffer එකෙන් කෙලින්ම ගන්නවා) const img = await loadImage(req.file.buffer); // 2. Background Blur ctx.filter = 'blur(50px) brightness(0.5)'; ctx.drawImage(img, -50, -50, size + 100, size + 100); ctx.filter = 'none'; // 3. Styles Logic (1 & 3) if (styleId === 1) { ctx.strokeStyle = 'rgba(0, 242, 255, 0.1)'; for (let i = 0; i < size; i += 60) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, size); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(size, i); ctx.stroke(); } } else { ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; ctx.lineWidth = 1; for (let i = 0; i < 20; i++) { ctx.beginPath(); ctx.moveTo(Math.random() * size, Math.random() * size); ctx.lineTo(Math.random() * size, Math.random() * size); ctx.stroke(); } } // 4. Highlight Beam (උඹේ කෝඩ් එකේ තිබ්බ අලුත් කෑල්ල) ctx.save(); const beamGrad = ctx.createLinearGradient(0, 0, size, size); beamGrad.addColorStop(0, 'rgba(0, 242, 255, 0)'); beamGrad.addColorStop(0.5, 'rgba(0, 242, 255, 0.15)'); beamGrad.addColorStop(1, 'rgba(0, 242, 255, 0)'); ctx.fillStyle = beamGrad; ctx.rotate(-15 * Math.PI / 180); ctx.fillRect(-size, size / 3, size * 2, 150); ctx.restore(); // 5. ✅ AVATAR CENTERED (පොටෝ එක හරි මැදට) const centerX = size / 2; const centerY = size / 2; const radius = 250; ctx.save(); ctx.strokeStyle = '#00f2ff'; ctx.lineWidth = 12; ctx.shadowBlur = 40; ctx.shadowColor = '#00f2ff'; ctx.beginPath(); ctx.arc(centerX, centerY, radius + 15, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); ctx.save(); ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(img, centerX - radius, centerY - radius, radius * 2, radius * 2); ctx.restore(); // 6. ✅ TEXT LAYOUT ctx.textAlign = 'center'; // "W E L C O M E" Label ctx.font = 'bold 60px sans-serif'; ctx.strokeStyle = 'rgba(0, 242, 255, 0.8)'; ctx.lineWidth = 3; ctx.strokeText("W E L C O M E", centerX, centerY + radius + 120); // Group Name & Count ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; ctx.font = '35px sans-serif'; // Emoji Safe Regex එකත් දාලා තියෙන්නේ ආරක්ෂාවට let safeTitle = gName.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, ''); safeTitle = await translateSinhalaToEnglish(safeTitle); ctx.fillText(`${safeTitle} • MEMBER #${count}`, centerX, centerY + radius + 190); // User Name (if provided) if (userName) { let safeUserName = userName.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, ''); safeUserName = await translateSinhalaToEnglish(safeUserName); ctx.fillStyle = '#00f2ff'; ctx.font = 'bold 45px sans-serif'; ctx.fillText(safeUserName, centerX, centerY + radius + 250); } // Watermark ctx.globalAlpha = 0.3; ctx.font = 'bold 25px sans-serif'; ctx.fillStyle = '#ffffff'; ctx.fillText(watermark, centerX, size - 30); // යැවීම const buffer = await canvas.toBuffer('png'); res.set('Content-Type', 'image/png'); res.send(buffer); } catch (e) { console.error(e); res.status(500).send("Welcome Gen Error: " + e.message); } }); // ============================================================ // 🖼️ 2. WATERMARK (Only for .img gen) // ============================================================ app.post('/watermark', upload.single('image'), async (req, res) => { try { if (!req.file) return res.status(400).send("No image"); const { watermarkText } = req.body; const finalWatermark = watermarkText || 'CIPHER-MD GEN ENGINE'; const img = await loadImage(req.file.buffer); const canvas = new Canvas(img.width, img.height); const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); // Add Watermark const fontSize = Math.floor(img.width * 0.02); ctx.font = `bold ${fontSize}px sans-serif`; ctx.textAlign = 'right'; ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; ctx.shadowBlur = 5; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3; ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; ctx.fillText(finalWatermark, img.width - 20, img.height - 20); res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png')); } catch (e) { res.status(500).send("WM Error"); } }); // ============================================================ // 🛠️ HELPER: SINHALA TO ENGLISH TRANSLATION & TRANSLITERATION // ============================================================ async function translateSinhalaToEnglish(text) { if (!text) return text; // Check if it has Sinhala characters if (!/[\u0D80-\u0DFF]/.test(text)) return text; try { const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=si&tl=en&dt=t&q=${encodeURIComponent(text)}`; const res = await axios.get(url); let translated = res.data[0][0][0]; // Capitalize words return translated.replace(/\b\w/g, l => l.toUpperCase()); } catch (e) { console.error("Translation API error:", e.message); return transliterateSinhala(text); } } const transliterateSinhala = (text) => { if (!text) return text; text = text.replace(/\u200D/g, ''); // Remove ZWJ const vowels = { 'අ': 'a', 'ආ': 'aa', 'ඇ': 'ae', 'ඈ': 'aae', 'ඉ': 'i', 'ඊ': 'ii', 'උ': 'u', 'ඌ': 'uu', 'එ': 'e', 'ඒ': 'ee', 'ඓ': 'ai', 'ඔ': 'o', 'ඕ': 'oo', 'ඖ': 'au' }; const consonants = { 'ක': 'k', 'ඛ': 'kh', 'ග': 'g', 'ඝ': 'gh', 'ඞ': 'ng', 'ඟ': 'ng', 'ච': 'ch', 'ඡ': 'chh', 'ජ': 'j', 'ඣ': 'jh', 'ඤ': 'ny', 'ඥ': 'gn', 'ඦ': 'ndj', 'ට': 't', 'ඨ': 'th', 'ඩ': 'd', 'ඪ': 'dh', 'ණ': 'n', 'ඬ': 'nd', 'ත': 't', 'ථ': 'th', 'ද': 'd', 'ධ': 'dh', 'න': 'n', 'ඳ': 'nd', 'ප': 'p', 'ඵ': 'ph', 'බ': 'b', 'භ': 'bh', 'ම': 'm', 'ඹ': 'mb', 'ය': 'y', 'ර': 'r', 'ල': 'l', 'ව': 'v', 'ශ': 'sh', 'ෂ': 'sh', 'ස': 's', 'හ': 'h', 'ළ': 'l', 'ෆ': 'f' }; const modifiers = { 'ා': 'a', 'ැ': 'ae', 'ෑ': 'aae', 'ි': 'i', 'ී': 'ii', 'ු': 'u', 'ූ': 'uu', 'ෘ': 'ru', 'ෙ': 'e', 'ේ': 'ee', 'ෛ': 'ai', 'ො': 'o', 'ෝ': 'oo', 'ෞ': 'au', '්': '' }; const specials = { 'ං': 'n', 'ඃ': 'h', 'ෲ': 'ruu' }; let res = ""; for (let i = 0; i < text.length; i++) { let c = text[i]; if (vowels[c]) { res += vowels[c]; } else if (specials[c]) { res += specials[c]; } else if (consonants[c]) { res += consonants[c]; if (i + 1 < text.length && modifiers[text[i + 1]] !== undefined) { res += modifiers[text[i + 1]]; i++; } else { res += "a"; } } else { res += c; } } // Capitalize first letter of words to make it look nicer return res.replace(/\b\w/g, l => l.toUpperCase()); }; // ============================================================ // 🛠️ HELPER: ROUNDED RECTANGLE // ============================================================ function roundedRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); } // ============================================================ // 📈 ENGINE 1: ULTRA-SHARP LINE CHART (Gold, P2P, CSE) // ============================================================ async function drawLineChart(title, subTitle, history, colorTheme) { const width = 2400; const height = 1500; const canvas = new Canvas(width, height); const ctx = canvas.getContext('2d'); ctx.scale(2, 2); // 1. Background (Solid Dark) ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, width, height); // 📐 Precise Layout Calculation // යට ඉඩ (Bottom Padding) වැඩි කළා අකුරු ගෑවෙන්නෙ නැති වෙන්න const chartX = 20; const chartY = 120; // Title area height const chartW = 1050; const chartH = 530; // Chart height reduced slightly to keep bottom clear // Data Min/Max const values = history.map(h => (h.price || h.rate || h.value)); const minVal = Math.min(...values) * 0.9995; const maxVal = Math.max(...values) * 1.0005; const range = maxVal - minVal; const getY = (val) => chartY + chartH - ((val - minVal) / range) * chartH; const getX = (i) => chartX + (i / (values.length - 1)) * chartW; // 2. GRID LINES (Very Subtle) ctx.lineWidth = 1; ctx.strokeStyle = '#2a2e39'; // Horizontal Grids const rows = 6; for (let i = 0; i <= rows; i++) { const y = chartY + (chartH / rows) * i; ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX + chartW, y); ctx.stroke(); // Price Label (Sharp Font) const price = maxVal - (range / rows) * i; ctx.fillStyle = '#787b86'; ctx.font = '13px Consolas, monospace'; // Monospace for alignment ctx.textAlign = 'left'; ctx.fillText(price.toLocaleString(undefined, { minimumFractionDigits: 2 }), chartX + chartW + 12, y + 8); } // Vertical Grids const cols = 8; for (let i = 0; i <= cols; i++) { const x = chartX + (chartW / cols) * i; ctx.beginPath(); ctx.moveTo(x, chartY); ctx.lineTo(x, chartY + chartH + 60); ctx.stroke(); } // 3. THE CHART AREA (Clean Gradient) ctx.save(); ctx.beginPath(); ctx.moveTo(getX(0), getY(values[0])); for (let i = 0; i < values.length; i++) ctx.lineTo(getX(i), getY(values[i])); // Close path to bottom of chart area ctx.lineTo(getX(values.length - 1), chartY + chartH + 60); ctx.lineTo(getX(0), chartY + chartH + 60); ctx.closePath(); // Sharp Gradient (No blurring at bottom) const gradient = ctx.createLinearGradient(0, chartY, 0, chartY + chartH + 60); gradient.addColorStop(0, `${colorTheme}55`); // Top: Semi-transparent gradient.addColorStop(1, `${colorTheme}00`); // Bottom: Completely transparent ctx.fillStyle = gradient; ctx.fill(); ctx.restore(); // 4. THE MAIN LINE (Thin & Sharp) ctx.beginPath(); ctx.lineWidth = 3; // Thinner line for "Pro" look (was 5) ctx.strokeStyle = colorTheme; ctx.lineJoin = 'round'; // Removed shadowBlur here to avoid "smudge" look for (let i = 0; i < values.length; i++) { if (i === 0) ctx.moveTo(getX(i), getY(values[i])); else ctx.lineTo(getX(i), getY(values[i])); } ctx.stroke(); // 5. CURRENT PRICE BADGE (Right Axis) const lastVal = values[values.length - 1]; const lastY = getY(lastVal); // Dotted line to axis ctx.setLineDash([2, 2]); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(getX(values.length - 1), lastY); ctx.lineTo(chartX + chartW, lastY); ctx.stroke(); ctx.setLineDash([]); // Badge ctx.fillStyle = colorTheme; roundedRect(ctx, chartX + chartW + 5, lastY - 11, 115, 22, 3); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 13px Consolas'; ctx.textAlign = 'left'; ctx.fillText(lastVal.toLocaleString(undefined, { minimumFractionDigits: 2 }), chartX + chartW + 10, lastY + 5); // Pulsing Dot (Sharp) const lastX = getX(values.length - 1); ctx.beginPath(); ctx.arc(lastX, lastY, 5, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.beginPath(); ctx.arc(lastX, lastY, 12, 0, Math.PI * 2); ctx.strokeStyle = `${colorTheme}66`; ctx.lineWidth = 2; ctx.stroke(); // 6. HEADER (Aligned) ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(title, 30, 60); ctx.fillStyle = colorTheme; ctx.font = 'bold 32px sans-serif'; ctx.fillText(subTitle, 30, 100); // 7. WATERMARK (Safe Distance) ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right'; ctx.fillText('⚡ Cipher-MD Analytics (4K)', (width / 2) - 20, (height / 2) - 20); return canvas.toBuffer('png'); } // ============================================================ // 🕯️ ENGINE 2: CANDLESTICK CHART (Fixed Sharpness) // ============================================================ app.post('/chart/candle', async (req, res) => { try { const { candles, symbol, change, currentPrice } = req.body; const width = 2400; const height = 1500; const canvas = new Canvas(width, height); const ctx = canvas.getContext('2d'); ctx.scale(2, 2); ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, width, height); const chartX = 20; const chartY = 60; const chartW = 1050; const chartH = 530; const minPrice = Math.min(...candles.map(c => c.low)); const maxPrice = Math.max(...candles.map(c => c.high)); const range = maxPrice - minPrice; const maxVol = Math.max(...candles.map(c => c.volume)); const getY = (p) => chartY + chartH - ((p - minPrice) / range) * chartH; // Grid ctx.lineWidth = 1; ctx.strokeStyle = '#2a2e39'; const rows = 8; for (let i = 0; i <= rows; i++) { const y = chartY + (chartH / rows) * i; ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX + chartW, y); ctx.stroke(); const p = maxPrice - (range / rows) * i; ctx.fillStyle = '#787b86'; ctx.textAlign = 'left'; ctx.font = '13px Consolas'; ctx.fillText(p.toFixed(2), chartX + chartW + 10, y + 4); } const candleW = (chartW / candles.length) * 0.7; const spacing = chartW / candles.length; candles.forEach((c, i) => { const x = chartX + (i * spacing) + (spacing / 2); const isUp = c.close >= c.open; const color = isUp ? '#089981' : '#f23645'; // Volume (Sharp, low opacity) const volH = (c.volume / maxVol) * 120; ctx.fillStyle = isUp ? 'rgba(8, 153, 129, 0.2)' : 'rgba(242, 54, 69, 0.2)'; ctx.fillRect(x - candleW / 2, chartY + chartH - volH + 150, candleW, volH); // Wick (Thin line) ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(x, getY(c.high)); ctx.lineTo(x, getY(c.low)); ctx.stroke(); // Body ctx.fillStyle = color; const yOpen = getY(c.open); const yClose = getY(c.close); ctx.fillRect(x - candleW / 2, Math.min(yOpen, yClose), candleW, Math.max(Math.abs(yOpen - yClose), 1)); }); // Price Badge const lastC = candles[candles.length - 1]; const lastY = getY(lastC.close); const lastColor = lastC.close >= lastC.open ? '#089981' : '#f23645'; ctx.strokeStyle = '#ffffff'; ctx.setLineDash([2, 2]); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, lastY); ctx.lineTo(chartX + chartW, lastY); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = lastColor; roundedRect(ctx, chartX + chartW + 5, lastY - 12, 115, 24, 3); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 13px Consolas'; ctx.fillText(parseFloat(currentPrice).toFixed(2), chartX + chartW + 10, lastY + 5); // Header ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`${symbol} / USDT`, 30, 60); ctx.fillStyle = parseFloat(change) >= 0 ? '#089981' : '#f23645'; ctx.font = 'bold 32px sans-serif'; ctx.fillText(`${change}%`, 30, 100); ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right'; ctx.fillText('⚡ Cipher-MD Analytics', (width / 2) - 20, (height / 2) - 20); res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png')); } catch (e) { res.status(500).send(e.message); } }); // ============================================================ // 💎 ENGINE 3: NFT & ARB (Retained Fixed Version) // ============================================================ app.post('/chart/nft', async (req, res) => { try { const { data } = req.body; const canvas = new Canvas(800, 400); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#0a0b0d'; roundedRect(ctx, 0, 0, 800, 400, 25); ctx.fill(); if (data.image_url) { try { const img = await loadImage(data.image_url); ctx.save(); ctx.beginPath(); roundedRect(ctx, 40, 40, 200, 200, 20); ctx.clip(); ctx.drawImage(img, 40, 40, 200, 200); ctx.restore(); } catch (e) { } } ctx.fillStyle = '#ffffff'; ctx.font = 'bold 36px sans-serif'; ctx.fillText(data.name.substring(0, 18), 270, 70); ctx.fillStyle = '#8a939b'; ctx.font = '24px sans-serif'; ctx.fillText(`Chain: ${data.chain.toUpperCase()}`, 270, 110); ctx.fillStyle = '#2081e2'; ctx.font = 'bold 55px sans-serif'; ctx.fillText(`${data.floor_price} ${data.floor_price_symbol}`, 270, 190); ctx.fillStyle = '#ffffff'; ctx.font = '18px sans-serif'; ctx.fillText('FLOOR PRICE', 270, 220); ctx.fillStyle = '#161b22'; roundedRect(ctx, 270, 260, 480, 80, 15); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 20px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`Vol: ${Math.round(data.total_volume)}`, 290, 308); ctx.textAlign = 'right'; ctx.fillText(`Owners: ${data.owner_count}`, 730, 308); ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.fillText('Cipher-MD NFT', 750, 380); res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png')); } catch (e) { res.status(500).send(e.message); } }); app.post('/chart/arb', async (req, res) => { try { const { coin, exData } = req.body; const canvas = new Canvas(900, 600); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, 900, 600); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 40px sans-serif'; ctx.fillText(`${coin} Arbitrage Monitor`, 40, 60); let y = 120; const avg = exData.reduce((a, b) => a + b.price, 0) / exData.length; exData.forEach(ex => { const diff = (((ex.price - avg) / avg) * 100); const color = diff >= 0 ? '#089981' : '#f23645'; ctx.fillStyle = '#1e222d'; roundedRect(ctx, 40, y, 820, 70, 10); ctx.fill(); ctx.fillStyle = '#b2b5be'; ctx.textAlign = 'left'; ctx.font = 'bold 24px sans-serif'; ctx.fillText(ex.name, 70, y + 45); ctx.fillStyle = '#ffffff'; ctx.textAlign = 'right'; ctx.fillText(`$${ex.price.toLocaleString()}`, 600, y + 45); ctx.fillStyle = color; ctx.fillText(`${diff > 0 ? '+' : ''}${diff.toFixed(2)}%`, 830, y + 45); y += 85; }); res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png')); } catch (e) { res.status(500).send(e.message); } }); // ============================================================ // ⚡ ROUTERS // ============================================================ app.post('/chart/p2p', async (req, res) => { try { const { history, currentRate } = req.body; const buf = await drawLineChart("USDT / LKR (P2P)", `Rs. ${currentRate}`, history, '#2962ff'); res.set('Content-Type', 'image/png'); res.send(buf); } catch (e) { res.status(500).send(e.message); } }); app.post('/chart/forex', async (req, res) => { try { const { history, currentRate } = req.body; const buf = await drawLineChart("USD / LKR (Official)", `Rs. ${currentRate}`, history, '#089981'); res.set('Content-Type', 'image/png'); res.send(buf); } catch (e) { res.status(500).send(e.message); } }); app.post('/chart/gold', async (req, res) => { try { const { history, currentRate } = req.body; const buf = await drawLineChart("Gold (24K)", `Rs. ${currentRate}`, history, '#f2a900'); res.set('Content-Type', 'image/png'); res.send(buf); } catch (e) { res.status(500).send(e.message); } }); app.post('/chart/cse', async (req, res) => { try { const { history, currentRate } = req.body; const buf = await drawLineChart("CSE ASPI", `${currentRate}`, history, '#a300cc'); res.set('Content-Type', 'image/png'); res.send(buf); } catch (e) { res.status(500).send(e.message); } }); app.post('/chart/fear', async (req, res) => { try { const { history } = req.body; const last = history[history.length - 1].value; const color = last > 50 ? '#089981' : '#f23645'; const buf = await drawLineChart("Fear & Greed", `${last}/100`, history, color); res.set('Content-Type', 'image/png'); res.send(buf); } catch (e) { res.status(500).send(e.message); } }); const port = 7861; app.listen(port, () => console.log(`🚀 ULTRA-SHARP ENGINE running on port ${port}`));