const { createCanvas, loadImage } = require("canvas"); const fs = require("fs"); const path = require("path"); const axios = require("axios"); module.exports = { config: { name: "pair", author: "EfxAsif", version: "2.0.0", role: 0, category: "love", shortDescription: { en: "💘 Find your perfect match" }, longDescription: { en: "Pair with a random group member or mention/reply to someone" }, guide: { en: "{p}pair | {p}pair @mention | Reply to a message with {p}pair" } }, onStart: async function ({ api, event }) { let imagePath = null; try { // ========================================================= // GROUP INFO // ========================================================= const threadInfo = await api.getThreadInfo(event.threadID); const participants = threadInfo?.participantIDs || []; const botID = api.getCurrentUserID(); if (!participants.length) { return api.sendMessage( "❌ Could not find group members.", event.threadID ); } // ========================================================= // TARGET DETECTION // // Priority: // 1. Mention // 2. Reply // 3. Random // ========================================================= let matchID = null; // --------------------------------------------------------- // 1. MENTION / TAG // --------------------------------------------------------- if ( event.mentions && typeof event.mentions === "object" && Object.keys(event.mentions).length > 0 ) { const mentionedIDs = Object.keys(event.mentions); const validMentions = mentionedIDs.filter( id => id !== botID && id !== event.senderID && participants.includes(id) ); if (validMentions.length > 0) { matchID = validMentions[0]; } } // --------------------------------------------------------- // 2. REPLY // --------------------------------------------------------- if (!matchID && event.messageReply) { const repliedUserID = event.messageReply.senderID; if ( repliedUserID && repliedUserID !== event.senderID && repliedUserID !== botID && participants.includes(repliedUserID) ) { matchID = repliedUserID; } } // --------------------------------------------------------- // 3. RANDOM // --------------------------------------------------------- if (!matchID) { const availableMembers = participants.filter( id => id !== event.senderID && id !== botID ); if (availableMembers.length === 0) { return api.sendMessage( "❌ No other members found to pair with!", event.threadID ); } matchID = availableMembers[ Math.floor( Math.random() * availableMembers.length ) ]; } // ========================================================= // VALIDATE TARGET // ========================================================= if (!participants.includes(matchID)) { return api.sendMessage( "❌ The selected person is not in this group.", event.threadID ); } // ========================================================= // USER INFORMATION // ========================================================= const userInfo = await api.getUserInfo([ event.senderID, matchID ]); const senderData = userInfo?.[event.senderID] || {}; const matchData = userInfo?.[matchID] || {}; const senderName = senderData.name || "Unknown"; const matchName = matchData.name || "Unknown"; // ========================================================= // LOVE PERCENTAGE // ========================================================= const lovePercent = Math.floor(Math.random() * 31) + 70; // ========================================================= // LOAD PROFILE PHOTOS // ========================================================= const senderAvatar = await loadAvatar( api, event.senderID, senderData ); const matchAvatar = await loadAvatar( api, matchID, matchData ); console.log( `[PAIR] Avatar 1: ${ senderAvatar ? "Loaded" : "Failed" }` ); console.log( `[PAIR] Avatar 2: ${ matchAvatar ? "Loaded" : "Failed" }` ); // ========================================================= // CANVAS // ========================================================= const width = 1200; const height = 675; const canvas = createCanvas(width, height); const ctx = canvas.getContext("2d"); // ========================================================= // BACKGROUND // ========================================================= const mainGradient = ctx.createLinearGradient( 0, 0, width, height ); mainGradient.addColorStop( 0, "#1a0b2e" ); mainGradient.addColorStop( 0.3, "#451a6f" ); mainGradient.addColorStop( 0.6, "#9c27b0" ); mainGradient.addColorStop( 1, "#e91e63" ); ctx.fillStyle = mainGradient; ctx.fillRect( 0, 0, width, height ); // ========================================================= // STARS // ========================================================= ctx.fillStyle = "#ffffff"; for (let i = 0; i < 200; i++) { const x = Math.random() * width; const y = Math.random() * height; const size = Math.random() * 2 + 0.5; const opacity = Math.random() * 0.8 + 0.2; ctx.globalAlpha = opacity; ctx.beginPath(); ctx.arc( x, y, size, 0, Math.PI * 2 ); ctx.fill(); } ctx.globalAlpha = 1; // ========================================================= // NEBULA // ========================================================= const nebulaColors = [ "rgba(156,39,176,0.3)", "rgba(233,30,99,0.25)", "rgba(103,58,183,0.2)", "rgba(255,64,129,0.15)" ]; for (let i = 0; i < 15; i++) { const x = Math.random() * width; const y = Math.random() * height; const radius = Math.random() * 200 + 100; const color = nebulaColors[ Math.floor( Math.random() * nebulaColors.length ) ]; const gradient = ctx.createRadialGradient( x, y, 0, x, y, radius ); gradient.addColorStop( 0, color ); gradient.addColorStop( 1, "transparent" ); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc( x, y, radius, 0, Math.PI * 2 ); ctx.fill(); } // ========================================================= // GRID // ========================================================= ctx.strokeStyle = "rgba(255,255,255,0.05)"; ctx.lineWidth = 1; for ( let x = 0; x <= width; x += 50 ) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } for ( let y = 0; y <= height; y += 50 ) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } // ========================================================= // DECORATIVE SHAPES // ========================================================= for (let i = 0; i < 8; i++) { const x = Math.random() * width; const y = Math.random() * height; const size = Math.random() * 60 + 20; const rotation = Math.random() * Math.PI * 2; ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); ctx.fillStyle = "rgba(255,255,255,0.05)"; ctx.strokeStyle = "rgba(255,255,255,0.1)"; ctx.lineWidth = 2; const type = Math.floor( Math.random() * 3 ); ctx.beginPath(); if (type === 0) { ctx.moveTo( 0, -size ); ctx.lineTo( size * 0.866, size * 0.5 ); ctx.lineTo( -size * 0.866, size * 0.5 ); ctx.closePath(); } else if (type === 1) { for ( let j = 0; j < 6; j++ ) { const angle = (Math.PI / 3) * j; const px = Math.cos(angle) * size; const py = Math.sin(angle) * size; if (j === 0) { ctx.moveTo( px, py ); } else { ctx.lineTo( px, py ); } } ctx.closePath(); } else { ctx.moveTo( 0, -size ); ctx.lineTo( size, 0 ); ctx.lineTo( 0, size ); ctx.lineTo( -size, 0 ); ctx.closePath(); } ctx.fill(); ctx.stroke(); ctx.restore(); } // ========================================================= // CARD // ========================================================= const cardWidth = 1000; const cardHeight = 450; const cardX = (width - cardWidth) / 2; const cardY = (height - cardHeight) / 2; ctx.fillStyle = "rgba(255,255,255,0.08)"; ctx.strokeStyle = "rgba(255,255,255,0.15)"; ctx.lineWidth = 2; roundRect( ctx, cardX, cardY, cardWidth, cardHeight, 30 ); ctx.fill(); ctx.stroke(); ctx.strokeStyle = "rgba(233,30,99,0.3)"; ctx.lineWidth = 4; roundRect( ctx, cardX + 2, cardY + 2, cardWidth - 4, cardHeight - 4, 28 ); ctx.stroke(); // ========================================================= // PROFILE POSITIONS // ========================================================= const profileSize = 180; const leftX = cardX + 100; const leftY = cardY + 100; const rightX = cardX + cardWidth - 100 - profileSize; const rightY = cardY + 100; // ========================================================= // DRAW PROFILES // ========================================================= drawProfile( ctx, senderAvatar, senderName, leftX, leftY, profileSize, [ "#e91e63", "#9c27b0" ] ); drawProfile( ctx, matchAvatar, matchName, rightX, rightY, profileSize, [ "#9c27b0", "#673ab7" ] ); // ========================================================= // NAMES // ========================================================= ctx.fillStyle = "#ffffff"; ctx.font = "bold 28px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "alphabetic"; ctx.fillText( truncateText( senderName, 20 ), leftX + profileSize / 2, leftY + profileSize + 50 ); ctx.fillText( truncateText( matchName, 20 ), rightX + profileSize / 2, rightY + profileSize + 50 ); // ========================================================= // CONNECTION // ========================================================= const centerX = width / 2; const centerY = cardY + 190; ctx.strokeStyle = "#e91e63"; ctx.lineWidth = 4; ctx.shadowColor = "#e91e63"; ctx.shadowBlur = 15; const p0 = leftX + profileSize + 30; const p1 = leftX + profileSize + 100; const p2 = rightX - 100; const p3 = rightX - 30; ctx.beginPath(); ctx.moveTo( p0, centerY ); ctx.bezierCurveTo( p1, centerY - 50, p2, centerY - 50, p3, centerY ); ctx.stroke(); ctx.shadowBlur = 0; // ========================================================= // CONNECTION DOTS // ========================================================= for (let i = 0; i < 5; i++) { const t = i / 4; const x = bezierPoint( p0, p1, p2, p3, t ); const y = bezierPoint( centerY, centerY - 50, centerY - 50, centerY, t ); ctx.fillStyle = i % 2 === 0 ? "#e91e63" : "#9c27b0"; ctx.beginPath(); ctx.arc( x, y, 12, 0, Math.PI * 2 ); ctx.fill(); } // ========================================================= // PERCENTAGE // ========================================================= const percentX = centerX; const percentY = cardY + cardHeight - 120; const percentSize = 120; const percentGradient = ctx.createLinearGradient( percentX - percentSize / 2, percentY - percentSize / 2, percentX + percentSize / 2, percentY + percentSize / 2 ); percentGradient.addColorStop( 0, "#e91e63" ); percentGradient.addColorStop( 1, "#673ab7" ); ctx.strokeStyle = percentGradient; ctx.lineWidth = 10; ctx.beginPath(); ctx.arc( percentX, percentY, percentSize / 2 + 5, 0, Math.PI * 2 ); ctx.stroke(); ctx.fillStyle = "rgba(255,255,255,0.1)"; ctx.beginPath(); ctx.arc( percentX, percentY, percentSize / 2, 0, Math.PI * 2 ); ctx.fill(); ctx.fillStyle = "#ffffff"; ctx.font = "bold 40px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText( `${lovePercent}%`, percentX, percentY ); ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.font = "bold 22px Arial"; ctx.fillText( "MATCH", percentX, percentY + 100 ); // ========================================================= // TITLE // ========================================================= ctx.fillStyle = "#ffffff"; ctx.font = "bold 42px Arial"; ctx.fillText( "PERFECT MATCH", centerX, cardY + 50 ); ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.font = "22px Arial"; ctx.fillText( "Two hearts, one connection", centerX, cardY + 85 ); // ========================================================= // SAVE IMAGE // ========================================================= imagePath = path.join( __dirname, `pair_${event.threadID}_${Date.now()}.png` ); const buffer = canvas.toBuffer( "image/png" ); fs.writeFileSync( imagePath, buffer ); // ========================================================= // MESSAGE // ========================================================= const message = `🌸 𝗠𝗮𝘁𝗰𝗵𝗺𝗮𝗸𝗶𝗻𝗴 𝗖𝗼𝗺𝗽𝗹𝗲𝘁𝗲 🌸\n\n` + `💝 @${senderName}\n` + `💙 @${matchName}\n\n` + `😘 𝗠𝗮𝘁𝗰𝗵: ${lovePercent}%`; const mentions = [ { tag: `@${senderName}`, id: event.senderID }, { tag: `@${matchName}`, id: matchID } ]; // ========================================================= // SEND // ========================================================= await api.sendMessage( { body: message, mentions, attachment: fs.createReadStream( imagePath ) }, event.threadID ); // ========================================================= // DELETE TEMP FILE // ========================================================= setTimeout(() => { try { if ( imagePath && fs.existsSync(imagePath) ) { fs.unlinkSync( imagePath ); } } catch (e) {} }, 10000); } catch (error) { console.error( "❌ Pair command error:", error ); if ( imagePath && fs.existsSync(imagePath) ) { try { fs.unlinkSync( imagePath ); } catch (e) {} } return api.sendMessage( "❌ Pair command failed. Please try again.", event.threadID ); } } }; // ============================================================= // LOAD AVATAR // ============================================================= async function loadAvatar( api, uid, userData = {} ) { /* * Try several possible avatar sources. * * 1. User data image URL * 2. Facebook graph picture * 3. Facebook profile picture */ const avatarURLs = []; // ----------------------------------------------------------- // USER DATA IMAGE URL // ----------------------------------------------------------- const possibleUserURLs = [ userData?.thumbSrc, userData?.profileUrl, userData?.avatar, userData?.photoURL, userData?.picture?.data?.url ]; for (const url of possibleUserURLs) { if ( typeof url === "string" && url.startsWith("http") ) { avatarURLs.push(url); } } // ----------------------------------------------------------- // FACEBOOK GRAPH URLS // ----------------------------------------------------------- avatarURLs.push( `https://graph.facebook.com/${uid}/picture?width=720&height=720&type=large`, `https://graph.facebook.com/${uid}/picture?type=large`, `https://graph.facebook.com/${uid}/picture?width=500&height=500`, `https://graph.facebook.com/${uid}/picture` ); // ----------------------------------------------------------- // TRY EVERY URL // ----------------------------------------------------------- for (const url of avatarURLs) { try { console.log( `[PAIR] Loading avatar: ${url}` ); const response = await axios.get( url, { responseType: "arraybuffer", timeout: 15000, maxRedirects: 10, validateStatus: status => status >= 200 && status < 400, headers: { "User-Agent": "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36", "Accept": "image/avif,image/webp,image/apng,image/jpeg,image/png,image/*,*/*;q=0.8" } } ); if ( !response.data || response.data.length < 100 ) { continue; } const imageBuffer = Buffer.from( response.data ); // ------------------------------------------------------- // Verify image before returning // ------------------------------------------------------- try { const image = await loadImage( imageBuffer ); if ( image && image.width > 0 && image.height > 0 ) { console.log( `[PAIR] Avatar loaded successfully: ${uid}` ); return image; } } catch (imageError) { console.log( `[PAIR] Invalid image from URL` ); } } catch (error) { console.log( `[PAIR] Avatar URL failed: ${error.message}` ); } } console.log( `[PAIR] Could not load avatar for ${uid}` ); return null; } // ============================================================= // DRAW PROFILE // ============================================================= function drawProfile( ctx, avatar, name, x, y, size, gradientColors ) { // ----------------------------------------------------------- // PROFILE BORDER // ----------------------------------------------------------- const gradient = ctx.createLinearGradient( x, y, x + size, y + size ); gradient.addColorStop( 0, gradientColors[0] ); gradient.addColorStop( 1, gradientColors[1] ); ctx.strokeStyle = gradient; ctx.lineWidth = 7; ctx.beginPath(); ctx.arc( x + size / 2, y + size / 2, size / 2 + 4, 0, Math.PI * 2 ); ctx.stroke(); // ----------------------------------------------------------- // AVATAR CLIP // ----------------------------------------------------------- ctx.save(); ctx.beginPath(); ctx.arc( x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2 ); ctx.clip(); // ----------------------------------------------------------- // DRAW AVATAR // ----------------------------------------------------------- if ( avatar && avatar.width > 0 && avatar.height > 0 ) { try { /* * Center crop avatar into square. */ const sourceSize = Math.min( avatar.width, avatar.height ); const sourceX = (avatar.width - sourceSize) / 2; const sourceY = (avatar.height - sourceSize) / 2; ctx.drawImage( avatar, sourceX, sourceY, sourceSize, sourceSize, x, y, size, size ); } catch (error) { drawDefaultAvatar( ctx, name, x, y, size, gradientColors ); } } else { drawDefaultAvatar( ctx, name, x, y, size, gradientColors ); } ctx.restore(); } // ============================================================= // DEFAULT AVATAR // ============================================================= function drawDefaultAvatar( ctx, name, x, y, size, gradientColors ) { const gradient = ctx.createLinearGradient( x, y, x + size, y + size ); gradient.addColorStop( 0, gradientColors[0] ); gradient.addColorStop( 1, gradientColors[1] ); ctx.fillStyle = gradient; ctx.fillRect( x, y, size, size ); ctx.fillStyle = "rgba(255,255,255,0.15)"; ctx.beginPath(); ctx.arc( x + size / 2, y + size / 2, size * 0.38, 0, Math.PI * 2 ); ctx.fill(); ctx.fillStyle = "#ffffff"; ctx.font = "bold 72px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; const firstLetter = String(name || "U") .trim() .charAt(0) .toUpperCase(); ctx.fillText( firstLetter || "U", x + size / 2, y + size / 2 ); } // ============================================================= // ROUND RECTANGLE // ============================================================= function roundRect( ctx, x, y, width, height, radius ) { if ( width < 2 * radius ) { radius = width / 2; } if ( height < 2 * radius ) { radius = height / 2; } 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(); } // ============================================================= // TEXT TRUNCATE // ============================================================= function truncateText( text, maxLength ) { if (!text) { return "Unknown"; } text = String(text); if ( text.length <= maxLength ) { return text; } return ( text.substring( 0, maxLength - 3 ) + "..." ); } // ============================================================= // BEZIER POINT // ============================================================= function bezierPoint( p0, p1, p2, p3, t ) { const c = 1 - t; return ( c * c * c * p0 + 3 * c * c * t * p1 + 3 * c * t * t * p2 + t * t * t * p3 ); }