goat
command
pair
๐ Find your perfect match
0
views
0
likes
0
installs
raw source
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
);
}