← back to browse
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
  );
}

View raw file