Spaces:
Paused
Paused
| 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>} - 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); | |
| }); | |