const express = require('express'); const { createCanvas, loadImage } = require('canvas'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit') const bodyParser = require('body-parser'); const path = require('path'); const loadImg = require('./loader'); const app = express(); const { chromium } = require('playwright'); const port = 7860; app.set('trust proxy', 1); let totalReq = 0; //Brat const config = { maxTextLength: 100, viewport: { width: 1920, height: 1080 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }; let browser, page; const utils = { async initialize() { if (!browser) { browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: config.viewport, userAgent: config.userAgent }); await context.route('**/*', (route) => { const url = route.request().url(); if (url.endsWith('.png') || url.endsWith('.jpg') || url.includes('google-analytics')) { return route.abort(); } route.continue(); }); page = await context.newPage(); await page.goto('https://www.bratgenerator.com/', { waitUntil: 'domcontentloaded', timeout: 10000 }); try { await page.click('#onetrust-accept-btn-handler', { timeout: 2000 }); } catch { } await page.evaluate(() => setupTheme('white')); } }, async generateBrat(text) { await page.fill('#textInput', text); const overlay = page.locator('#textOverlay'); return overlay.screenshot({ timeout: 3000 }); }, async close() { if (browser) await browser.close(); } }; //Security app.use(helmet()); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 menit max: 100, // Membatasi 100 permintaan per IP dalam 15 menit message: 'Terlalu banyak permintaan dari IP ini, coba lagi nanti.', }); app.use(limiter); app.use(express.json()); app.use(express.urlencoded({ extended: true })); /** * Fungsi untuk menghasilkan gambar status kustom * @param {string} profileImage - URL gambar profil * @param {string} mainImage - URL gambar utama * @param {string} caption - Teks caption * @param {number} views - Jumlah tayangan * @returns {Promise} - Buffer gambar dalam format PNG */ async function createCustomSWGenerator({ profileImage, mainImage, caption = "Custom Caption", views = 4 }) { const canvasWidth = 1080; const canvasHeight = 1920; const canvas = createCanvas(canvasWidth, canvasHeight); const ctx = canvas.getContext("2d"); // Background hitam ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); // Tambahkan gambar utama if (mainImage) { try { const mainImg = await loadImg(mainImage); ctx.drawImage(mainImg, 0, 200, canvasWidth, 1080); } catch (error) { console.error("Gagal memuat gambar utama:", error.message); } } // Header background ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; ctx.fillRect(0, 0, canvasWidth, 180); // Gambar profil dengan border putih if (profileImage) { try { const profileImg = await loadImg(profileImage); ctx.save(); ctx.beginPath(); ctx.arc(90, 90, 60, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); // Gambar profil ctx.drawImage(profileImg, 30, 30, 120, 120); // Garis putih di luar profil ctx.beginPath(); ctx.arc(90, 90, 62, 0, Math.PI * 2); // Radius sedikit lebih besar ctx.strokeStyle = "white"; ctx.lineWidth = 10; // Stroke putih tipis ctx.stroke(); ctx.closePath(); ctx.restore(); } catch (error) { console.error("Gagal memuat gambar profil:", error.message); } } // Teks header ctx.fillStyle = "#FFFFFF"; ctx.font = "50px 'Roboto Sans', Arial"; // Gunakan Roboto Sans jika tersedia ctx.fillText("My status", 180, 80); ctx.fillStyle = "#CCCCCC"; ctx.font = "30px 'Roboto Sans', Arial"; // Gunakan Roboto Sans ctx.fillText("Now", 180, 130); // Caption teks ctx.fillStyle = "#FFFFFF"; ctx.font = "40px 'Roboto Sans', Arial"; ctx.textAlign = "center"; ctx.fillText(caption, canvasWidth / 2, 1400); // Footer background ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; ctx.fillRect(0, 1620, canvasWidth, 300); // Ikon dan teks footer try { const viewIcon = await loadImage('./eye.png'); // Ganti dengan URL ikon tayangan const shareIcon = await loadImage('./share.png'); // Ganti dengan URL ikon bagikan const promoteIcon = await loadImage('./promotion.png'); // Ganti dengan URL ikon promosikan // "Tayangan" ctx.drawImage(viewIcon, 80, 1660, 70, 70); // Ikon tayangan ctx.fillStyle = "#FFFFFF"; ctx.font = "30px 'Roboto Sans', Arial"; ctx.textAlign = "center"; ctx.fillText(`${views} Views`, 170, 1780); // "Promosikan" ctx.drawImage(promoteIcon, 460, 1660, 70, 70); // Ikon promosikan ctx.fillText("Promote", 550, 1780); // "Bagikan" ctx.drawImage(shareIcon, 840, 1660, 70, 70); // Ikon bagikan ctx.fillText("Share", 920, 1780); } catch (error) { console.error("Gagal memuat ikon:", error.message); } // Kembalikan gambar sebagai Buffer PNG return canvas.toBuffer('image/png'); } //Threads async function generateThread(username, avatarPath, textContent, countLike = "100") { const canvas = createCanvas(1080, 300); const ctx = canvas.getContext('2d'); // Background putih ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Teks "Utas pertama" dengan ikon bintang di bagian atas kiri const starIcon = await loadImage('./star.svg'); ctx.drawImage(starIcon, 115, 20, 25, 25); ctx.font = 'normal 25px Arial'; ctx.fillStyle = '#888888'; ctx.fillText('First thread', 145, 40); // Titik tiga horizontal const dotSize = 8; const dotsXStart = canvas.width - 100; const dotsY = 110; for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.arc(dotsXStart + i * 20, dotsY, dotSize / 2, 0, Math.PI * 2); ctx.fillStyle = '#888888'; ctx.fill(); } // Load avatar const avatar = await loadImg(avatarPath); const avatarSize = 80; const avatarX = 40; const avatarY = 80; ctx.save(); ctx.beginPath(); ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); ctx.drawImage(avatar, avatarX, avatarY, avatarSize, avatarSize); ctx.restore(); // Nama pengguna const dUser = username.length > 15 ? username.slice(0, 15) + '...' : username; ctx.font = 'bold 30px Arial'; ctx.fillStyle = '#000000'; ctx.fillText(dUser, 130, 110); const usernameWidth = ctx.measureText(dUser).width; // Tulisan 'sekarang' ctx.font = 'normal 25px Arial'; ctx.fillStyle = '#888888'; ctx.fillText('Now', 130 + usernameWidth + 10, 110); // Konten teks ctx.font = 'normal 35px Arial'; ctx.fillStyle = '#000000'; const maxWidth = canvas.width - 40; const lineHeight = 40; wrapText(ctx, textContent, 40, 210, maxWidth, lineHeight); // Reaksi const heart = await loadImage('./heart.svg'); ctx.drawImage(heart, 45, 250, 35, 35); ctx.font = 'normal 25px Arial'; ctx.fillStyle = '#000000'; ctx.fillText(countLike, 90, 275); return canvas.toBuffer('image/png'); } // Fungsi untuk membungkus teks panjang function wrapText(ctx, text, x, y, maxWidth, lineHeight) { const words = text.split(' '); let line = ''; for (let n = 0; n < words.length; n++) { const testLine = line + words[n] + ' '; const metrics = ctx.measureText(testLine); const testWidth = metrics.width; if (testWidth > maxWidth && n > 0) { ctx.fillText(line, x, y); line = words[n] + ' '; y += lineHeight; } else { line = testLine; } } ctx.fillText(line, x, y); } /** * Endpoint untuk membuat gambar status * @route POST /generate-status * @param {string} profileImage - URL gambar profil * @param {string} mainImage - URL gambar utama * @param {string} caption - Teks caption * @param {number} views - Jumlah tayangan * @returns {Buffer} - Gambar dalam format PNG */ app.get('/', (req, res) => { const documentation = { description: 'API documentation for generating image using canvas', totalRequest: totalReq, endpoints: [ { method: 'POST', path: '/generate', description: 'Generate a new WhatsApp status with profile image, main image, caption, and views.', requestBody: { profileImage: 'URL to the profile image', mainImage: 'URL to the main image', caption: 'Caption for the status', views: 'Number of views (integer)', }, exampleRequest: { profileImage: 'https://example.com/image.jpg', mainImage: 'https://example.com/image2.jpg', caption: 'this is the caption!', views: 10, }, response: { status: 'success', ContentType: 'image/png' }, }, { method: 'POST', path: '/generate2', description: 'Generate treads comments with avatar, username, text, and countLike.', requestBody: { avatar: 'URL to the profile image', username: 'username for profile', text: 'main text', countLike: 'total like count', }, exampleRequest: { avatar: 'https://example.com/image.jpg', username: '4rlzyy', text: 'this is the main text!', countLike: 10, }, response: { status: 'success', ContentType: 'image/png' }, }, { method: 'GET', path: '/brat', description: 'Generate brat image using text query.', request: { text: 'input query' }, exampleRequest: { text: 'hii iam usinv brat.' }, response: { status: 'success', ContentType: 'image/png' }, }, ], }; res.json(documentation); }); app.get('/brat', async (req, res) => { try { const { text } = req.query; totalReq++; // Validasi jika parameter `text` tidak ada if (!text || typeof text !== 'string' || text.trim().length === 0) { return res.status(400).json({ message: 'Invalid input. Please provide a valid text query parameter.' }); } // Proses pembuatan gambar const imageBuffer = await utils.generateBrat(text); // Mengirimkan hasil gambar res.set('Content-Type', 'image/png'); res.send(imageBuffer); } catch (error) { console.error('Error generating brat:', error.message); res.status(500).json({ message: 'An error occurred while generating the brat image.', error: error.message }); } }); app.post('/generate', async (req, res) => { const { profileImage, mainImage, caption, views } = req.body; totalReq++; if (!profileImage || !mainImage) { return res.status(400).json({ error: "Gambar profil dan gambar utama harus disediakan." }); } try { const imageBuffer = await createCustomSWGenerator({ profileImage, mainImage, caption, views }); res.setHeader('Content-Type', 'image/png'); res.send(imageBuffer); } catch (error) { console.error(error); res.status(500).json({ error: "Gagal membuat gambar." }); } }); app.post('/generate2', async (req, res) => { try { const { username, avatar, text, countLike } = req.body; totalReq++; // Validasi input if (!username || !text || !avatar) { return res.status(400).json({ error: 'Username, avatar and text are required.' }); } const imageBuffer = await generateThread(username, avatar, text, countLike); // Mengirimkan hasil sebagai gambar res.setHeader('Content-Type', 'image/png'); res.send(imageBuffer); } catch (error) { console.error('Error generating thread:', error); res.status(500).json({ error: 'An error occurred while generating the thread.' }); } }); // Mulai server Express app.listen(port, async () => { console.log(`Server berjalan di http://localhost:${port}`); await utils.initialize(); }); process.on('SIGINT', async () => { process.exit(0); });