← back to browse
mirai command

welcome

No description

0
views
0
likes
0
installs
raw source
const { drive } = global.utils;
const { nickNameBot } = global.GoatBot.config;
const { createCanvas, loadImage } = require("canvas");
const fs = require("fs-extra");
const path = require("path");
const axios = require("axios");

module.exports = {
  config: {
    name: "welcome",
    version: "9.0",
    author: "Hridoy",
    category: "events"
  },

  langs: {
    en: {
      defaultWelcomeMessage: "╭───〔 🌸 𝗪𝗘𝗟𝗖𝗢𝗠𝗘 🌸 〕───╮\n│ ✨ Hello, {userName}!\n│ 🎉 We're happy to have you here!\n│ 🌷 Enjoy the vibes & have fun!\n╰──────────────────────╯",
      botAddedMessage:
"━━━━━━━━━━━━━━━━━━━\n🤖 ᴛʜᴀɴᴋ ʏᴏᴜ ꜰᴏʀ ᴀᴅᴅɪɴɢ ᴍᴇ! 💖\n━━━━━━━━━━━━━━━━━━━ \n\nɪ'ᴍ ʜᴇʀᴇ ᴛᴏ ᴍᴀᴋᴇ ᴛʜɪs ɢʀᴏᴜᴘ ᴍᴏʀᴇ ᴀᴄᴛɪᴠᴇ ᴀɴᴅ ꜰᴜɴ! ✨\n\n⚙️ ᴘʀᴇꜰɪx      : .\n📜 ᴄᴏᴍᴍᴀɴᴅs   : .help\n🚀 sᴛᴀᴛᴜs      : ᴏɴʟɪɴᴇ ✅\n\n💡 ᴛɪᴘ: ᴛʏᴘᴇ .help ᴀɴʏᴛɪᴍᴇ ᴛᴏ sᴇᴇ ᴀʟʟ ᴀᴠᴀɪʟᴀʙʟᴇ ᴄᴏᴍᴍᴀɴᴅs!\n\n😄 ʟᴇᴛ's ʜᴀᴠᴇ ꜰᴜɴ ᴛᴏɢᴇᴛʜᴇʀ!\n━━━━━━━━━━━━━━━━━━━"}
  },

  onStart: async ({ threadsData, message, event, api, usersData, getLang }) => {
    if (event.logMessageType !== "log:subscribe") return;

    const { threadID } = event;
    const threadData = await threadsData.get(threadID);
    if (!threadData.settings.sendWelcomeMessage) return;

    const addedMembers = event.logMessageData.addedParticipants;
    const threadName   = threadData.threadName || "our group";
    const prefix       = global.utils.getPrefix(threadID);
    const inviterID    = event.author;

    for (const user of addedMembers) {
      const userID = user.userFbId;
      const botID  = api.getCurrentUserID();

      if (userID == botID) {
        if (nickNameBot) await api.changeNickname(nickNameBot, threadID, botID);
        return message.send(getLang("botAddedMessage", prefix));
      }

      const userName    = user.fullName;
      const inviterName = await usersData.getName(inviterID);
      const memberCount = event.participantIDs.length;

      let { welcomeMessage = getLang("defaultWelcomeMessage") } = threadData.data;
      welcomeMessage = welcomeMessage
        .replace(/\{userName\}/g,    userName)
        .replace(/\{userTag\}/g,     userName)
        .replace(/\{threadName\}/g,  threadName)
        .replace(/\{memberCount\}/g, memberCount)
        .replace(/\{inviterName\}/g, inviterName);

      let welcomeImagePath = null;
      try {
        welcomeImagePath = await createWelcomeCard({
          userName, threadName, memberCount,
          inviterName, newUserID: userID,
          inviterID, threadID, api
        });
      } catch (err) {
        console.error("Welcome image creation failed:", err);
      }

      const form = {
        body:     welcomeMessage,
        mentions: [{ tag: userName, id: userID }]
      };

      if (welcomeImagePath && fs.existsSync(welcomeImagePath)) {
        form.attachment = fs.createReadStream(welcomeImagePath);
      } else if (threadData.data.welcomeAttachment) {
        const attachments = threadData.data.welcomeAttachment
          .map(f => drive.getFile(f, "stream"));
        form.attachment = (await Promise.allSettled(attachments))
          .filter(({ status }) => status === "fulfilled")
          .map(({ value }) => value);
      }

      message.send(form);

      if (welcomeImagePath && fs.existsSync(welcomeImagePath)) {
        setTimeout(() => { try { fs.unlinkSync(welcomeImagePath); } catch (_) {} }, 5000);
      }
    }
  }
};

const ACCESS_TOKEN = "6628568379%7Cc1e620fa708a1d5696fb991c1bde5662";

async function downloadHighQualityProfile(userID) {
  try {
    const url = `https://graph.facebook.com/${userID}/picture?width=500&height=500&access_token=${ACCESS_TOKEN}`;
    const res = await axios({ method: 'GET', url, responseType: 'arraybuffer', timeout: 10000 });
    return Buffer.from(res.data, 'binary');
  } catch { return null; }
}

async function downloadImage(url) {
  try {
    const res = await axios({ method: 'GET', url, responseType: 'arraybuffer', timeout: 10000 });
    return Buffer.from(res.data, 'binary');
  } catch { return null; }
}

async function getGroupImage(threadID, api) {
  try {
    const info = await api.getThreadInfo(threadID);
    if (info.imageSrc) {
      const res = await axios({ method: 'GET', url: info.imageSrc, responseType: 'arraybuffer', timeout: 10000 });
      return Buffer.from(res.data, 'binary');
    }
  } catch {}
  return null;
}

function unicodeToPlain(str) {
  if (!str) return '';
  const ranges = [
    [0x1D400, 0x1D419, 'A'], [0x1D41A, 0x1D433, 'a'],
    [0x1D434, 0x1D44D, 'A'], [0x1D44E, 0x1D467, 'a'],
    [0x1D468, 0x1D481, 'A'], [0x1D482, 0x1D49B, 'a'],
    [0x1D5D4, 0x1D5ED, 'A'], [0x1D5EE, 0x1D607, 'a'],
    [0x1D63C, 0x1D655, 'A'], [0x1D656, 0x1D66F, 'a'],
    [0x1D7CE, 0x1D7D7, '0'],
    [0xFF21, 0xFF3A, 'A'], [0xFF41, 0xFF5A, 'a'],
    [0xFF10, 0xFF19, '0'],
    [0x24B6, 0x24CF, 'A'], [0x24D0, 0x24E9, 'a'],
  ];
  const singles = {
    0x1D49C: 'A', 0x212C: 'B', 0x2102: 'C', 0x2145: 'D',
    0x2130: 'E', 0x2131: 'F', 0x210A: 'g', 0x210B: 'H',
    0x2110: 'I', 0x2111: 'I', 0x2112: 'L', 0x2113: 'l',
    0x2115: 'N', 0x2118: 'P', 0x211A: 'Q', 0x211B: 'R',
    0x211C: 'R', 0x2124: 'Z', 0x2128: 'Z',
    0x2070: '0', 0x00B9: '1', 0x00B2: '2', 0x00B3: '3',
    0x2074: '4', 0x2075: '5', 0x2076: '6', 0x2077: '7',
    0x2078: '8', 0x2079: '9',
  };
  let result = '';
  for (const char of str) {
    const cp = char.codePointAt(0);
    if (singles[cp] !== undefined) { result += singles[cp]; continue; }
    let mapped = false;
    for (const [start, end, base] of ranges) {
      if (cp >= start && cp <= end) {
        result += String.fromCodePoint(base.codePointAt(0) + (cp - start));
        mapped = true; break;
      }
    }
    if (!mapped) result += char;
  }
  return result;
}

function safeStr(str) {
  if (!str) return '';
  try { return Buffer.from(str, 'latin1').toString('utf8'); } catch { return str; }
}

function readableText(str) { return unicodeToPlain(safeStr(str)); }

function ordinal(n) {
  const s = ['th', 'st', 'nd', 'rd'], v = n % 100;
  return n + (s[(v - 20) % 10] || s[v] || s[0]);
}

function roundRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

function drawCircleAvatar(ctx, img, cx, cy, r) {
  ctx.save();
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.clip();
  ctx.drawImage(img, cx - r, cy - r, r * 2, r * 2);
  ctx.restore();
}

function fitText(ctx, text, maxPx, maxSize = 36, minSize = 13, bold = true) {
  let t = text, size = maxSize;
  ctx.font = `${bold ? 'bold' : '400'} ${size}px "Segoe UI", Arial`;
  while (ctx.measureText(t).width > maxPx && size > minSize) {
    size--;
    ctx.font = `${bold ? 'bold' : '400'} ${size}px "Segoe UI", Arial`;
  }
  if (ctx.measureText(t).width > maxPx) {
    while (ctx.measureText(t + '…').width > maxPx && t.length > 1) t = t.slice(0, -1);
    t += '…';
  }
  return { text: t, size };
}

// ─── Helper: pseudo-random from seed ────────────────────────────────────────
function rng(s) { let x = Math.sin(s + 1) * 10000; return x - Math.floor(x); }

// ─── Draw a hexagon path ─────────────────────────────────────────────────────
function hexPath(ctx, cx, cy, r) {
  ctx.beginPath();
  for (let i = 0; i < 6; i++) {
    const a = (Math.PI / 180) * (60 * i - 30);
    const px = cx + r * Math.cos(a), py = cy + r * Math.sin(a);
    i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
  }
  ctx.closePath();
}

async function createWelcomeCard({
  userName, threadName, memberCount,
  inviterName, newUserID, inviterID, threadID, api
}) {
  const W = 1200, H = 600;
  const canvas = createCanvas(W, H);
  const ctx = canvas.getContext('2d');

  // ── Load images ──────────────────────────────────────────────────────────
  async function loadProfile(uid) {
    const buf = await downloadHighQualityProfile(uid);
    if (buf) return loadImage(buf).catch(() => null);
    try {
      const info = await api.getUserInfo([uid]);
      const src  = info[uid]?.thumbSrc;
      if (src) { const b2 = await downloadImage(src); if (b2) return loadImage(b2).catch(() => null); }
    } catch {}
    return null;
  }

  const [newUserImg, inviterImg, groupImg] = await Promise.all([
    loadProfile(newUserID),
    loadProfile(inviterID),
    getGroupImage(threadID, api).then(b => b ? loadImage(b).catch(() => null) : null)
  ]);

  const safeUser    = readableText(userName);
  const safeInviter = readableText(inviterName);
  const safeGroup   = readableText(threadName);

  // ══════════════════════════════════════════════════════════════════════════
  // 1. BACKGROUND — deep space gradient with aurora streaks
  // ══════════════════════════════════════════════════════════════════════════
  const bgGrad = ctx.createLinearGradient(0, 0, W, H);
  bgGrad.addColorStop(0,    '#050510');
  bgGrad.addColorStop(0.45, '#07071a');
  bgGrad.addColorStop(1,    '#04040e');
  ctx.fillStyle = bgGrad;
  ctx.fillRect(0, 0, W, H);

  // Aurora streaks (top-left corner glow)
  const aurora1 = ctx.createRadialGradient(180, 0, 0, 180, 0, 420);
  aurora1.addColorStop(0,   'rgba(120, 60, 255, 0.22)');
  aurora1.addColorStop(0.5, 'rgba(60, 20, 180, 0.10)');
  aurora1.addColorStop(1,   'rgba(0, 0, 0, 0)');
  ctx.fillStyle = aurora1;
  ctx.fillRect(0, 0, W, H);

  // Aurora bottom-right pink
  const aurora2 = ctx.createRadialGradient(W - 200, H, 0, W - 200, H, 480);
  aurora2.addColorStop(0,   'rgba(255, 60, 180, 0.18)');
  aurora2.addColorStop(0.5, 'rgba(200, 30, 120, 0.08)');
  aurora2.addColorStop(1,   'rgba(0, 0, 0, 0)');
  ctx.fillStyle = aurora2;
  ctx.fillRect(0, 0, W, H);

  // Aurora center-right cyan
  const aurora3 = ctx.createRadialGradient(W * 0.72, H * 0.4, 0, W * 0.72, H * 0.4, 300);
  aurora3.addColorStop(0,   'rgba(0, 220, 255, 0.10)');
  aurora3.addColorStop(1,   'rgba(0, 0, 0, 0)');
  ctx.fillStyle = aurora3;
  ctx.fillRect(0, 0, W, H);

  // ── Subtle star field ────────────────────────────────────────────────────
  for (let i = 0; i < 220; i++) {
    const sx = rng(i * 3.7) * W;
    const sy = rng(i * 5.3) * H;
    const sr = rng(i * 8.1) * 1.2 + 0.2;
    const sa = rng(i * 11.9) * 0.6 + 0.05;
    ctx.fillStyle = `rgba(255,255,255,${sa.toFixed(2)})`;
    ctx.beginPath(); ctx.arc(sx, sy, sr, 0, Math.PI * 2); ctx.fill();
  }

  // ── Diagonal grid lines (subtle cyberpunk vibe) ──────────────────────────
  ctx.save();
  ctx.globalAlpha = 0.025;
  ctx.strokeStyle = '#a0b0ff';
  ctx.lineWidth   = 1;
  for (let x = -H; x < W + H; x += 60) {
    ctx.beginPath();
    ctx.moveTo(x, 0); ctx.lineTo(x + H * 0.6, H);
    ctx.stroke();
  }
  ctx.globalAlpha = 1;
  ctx.restore();

  // ══════════════════════════════════════════════════════════════════════════
  // 2. GLASS CARD — full-bleed frosted panel with border
  // ══════════════════════════════════════════════════════════════════════════
  const PAD  = 28;
  const cardR = 26;
  roundRect(ctx, PAD, PAD, W - PAD * 2, H - PAD * 2, cardR);
  ctx.fillStyle = 'rgba(255,255,255,0.032)';
  ctx.fill();

  // card outer border — gradient stroke
  const borderGrad = ctx.createLinearGradient(PAD, PAD, W - PAD, H - PAD);
  borderGrad.addColorStop(0,    'rgba(160,100,255,0.55)');
  borderGrad.addColorStop(0.35, 'rgba(80,200,255,0.45)');
  borderGrad.addColorStop(0.70, 'rgba(255,80,200,0.40)');
  borderGrad.addColorStop(1,    'rgba(160,100,255,0.35)');
  roundRect(ctx, PAD, PAD, W - PAD * 2, H - PAD * 2, cardR);
  ctx.strokeStyle = borderGrad;
  ctx.lineWidth   = 1.8;
  ctx.stroke();

  // ══════════════════════════════════════════════════════════════════════════
  // 3. LEFT PANEL — avatar zone with hex frame
  // ══════════════════════════════════════════════════════════════════════════
  const splitX  = 370;
  const leftCX  = splitX / 2 + PAD / 2;

  // Vertical divider — glowing line
  {
    const dvg = ctx.createLinearGradient(splitX, PAD + 20, splitX, H - PAD - 20);
    dvg.addColorStop(0,   'rgba(120,80,255,0)');
    dvg.addColorStop(0.3, 'rgba(120,80,255,0.7)');
    dvg.addColorStop(0.7, 'rgba(80,200,255,0.7)');
    dvg.addColorStop(1,   'rgba(80,200,255,0)');
    ctx.save();
    ctx.shadowColor = 'rgba(120,80,255,0.6)';
    ctx.shadowBlur  = 14;
    ctx.strokeStyle = dvg;
    ctx.lineWidth   = 1.5;
    ctx.beginPath(); ctx.moveTo(splitX, PAD + 20); ctx.lineTo(splitX, H - PAD - 20); ctx.stroke();
    ctx.restore();
  }

  // "NEW MEMBER" tag — pill with gradient fill
  {
    const tag   = 'NEW  MEMBER';
    ctx.font    = 'bold 13px "Segoe UI", Arial';
    const tw    = ctx.measureText(tag).width + 30;
    const th    = 30, tx = leftCX - tw / 2, ty = PAD + 30;
    const tg    = ctx.createLinearGradient(tx, ty, tx + tw, ty);
    tg.addColorStop(0, 'rgba(140,80,255,0.85)');
    tg.addColorStop(1, 'rgba(60,180,255,0.85)');
    roundRect(ctx, tx, ty, tw, th, 15);
    ctx.fillStyle = tg; ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.shadowColor = 'rgba(160,120,255,0.8)'; ctx.shadowBlur = 10;
    ctx.fillText(tag, leftCX, ty + th / 2);
    ctx.shadowBlur = 0;
  }

  // Avatar — hexagon frame
  const avatarY = H / 2 - 10;
  const avatarR = 108;
  const hexR    = avatarR + 16;

  // Outer hex glow ring
  ctx.save();
  ctx.shadowColor = 'rgba(100,200,255,0.55)'; ctx.shadowBlur = 28;
  const hexGrad = ctx.createLinearGradient(leftCX - hexR, avatarY - hexR, leftCX + hexR, avatarY + hexR);
  hexGrad.addColorStop(0, 'rgba(140,80,255,0.9)');
  hexGrad.addColorStop(0.5, 'rgba(60,200,255,0.9)');
  hexGrad.addColorStop(1, 'rgba(255,80,180,0.8)');
  hexPath(ctx, leftCX, avatarY, hexR);
  ctx.strokeStyle = hexGrad; ctx.lineWidth = 3; ctx.stroke();
  ctx.restore();

  // Second thinner hex
  ctx.save();
  hexPath(ctx, leftCX, avatarY, hexR + 11);
  ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1.2; ctx.stroke();
  ctx.restore();

  // Avatar image or placeholder
  if (newUserImg) {
    ctx.save();
    hexPath(ctx, leftCX, avatarY, avatarR);
    ctx.clip();
    ctx.drawImage(newUserImg, leftCX - avatarR, avatarY - avatarR, avatarR * 2, avatarR * 2);
    ctx.restore();
  } else {
    ctx.save();
    hexPath(ctx, leftCX, avatarY, avatarR);
    const phg = ctx.createRadialGradient(leftCX, avatarY - 20, 0, leftCX, avatarY, avatarR);
    phg.addColorStop(0, '#1a1a35'); phg.addColorStop(1, '#0d0d20');
    ctx.fillStyle = phg; ctx.fill();
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.font = `${Math.round(avatarR * 0.7)}px Arial`;
    ctx.fillStyle = 'rgba(255,255,255,0.12)';
    ctx.fillText('👤', leftCX, avatarY);
    ctx.restore();
  }

  // Username below avatar
  {
    ctx.save();
    ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic';
    const nameY = avatarY + avatarR + 34;
    const { text: uName, size: uSize } = fitText(ctx, safeUser, splitX - 30, 30, 14);
    ctx.font      = `bold ${uSize}px "Segoe UI", Arial`;
    const nameGrad = ctx.createLinearGradient(leftCX - 100, 0, leftCX + 100, 0);
    nameGrad.addColorStop(0, '#e0d4ff');
    nameGrad.addColorStop(0.5, '#ffffff');
    nameGrad.addColorStop(1, '#c8f0ff');
    ctx.fillStyle   = nameGrad;
    ctx.shadowColor = 'rgba(180,140,255,0.5)'; ctx.shadowBlur = 12;
    ctx.fillText(uName, leftCX, nameY);
    ctx.restore();
  }

  // Member badge
  {
    const bText = `✦  ${ordinal(memberCount)} Member  ✦`;
    ctx.save();
    ctx.font = 'bold 14px "Segoe UI", Arial';
    ctx.textAlign = 'center';
    const bw = ctx.measureText(bText).width + 28;
    const bh = 30, bx = leftCX - bw / 2, by = avatarY + avatarR + 46;
    roundRect(ctx, bx, by, bw, bh, 15);
    ctx.fillStyle = 'rgba(60,200,255,0.10)'; ctx.fill();
    ctx.strokeStyle = 'rgba(60,200,255,0.45)'; ctx.lineWidth = 1.2;
    roundRect(ctx, bx, by, bw, bh, 15); ctx.stroke();
    ctx.fillStyle = 'rgba(60,220,255,0.95)';
    ctx.shadowColor = 'rgba(0,200,255,0.5)'; ctx.shadowBlur = 8;
    ctx.fillText(bText, leftCX, by + 20);
    ctx.restore();
  }

  // ══════════════════════════════════════════════════════════════════════════
  // 4. RIGHT PANEL
  // ══════════════════════════════════════════════════════════════════════════
  const rX    = splitX + 44;
  const rRight = W - PAD - 20;
  const rW    = rRight - rX;

  // ── Welcome headline ─────────────────────────────────────────────────────
  ctx.save();
  ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
  ctx.font = 'bold 42px "Segoe UI", Arial';
  const wg = ctx.createLinearGradient(rX, 0, rX + 520, 0);
  wg.addColorStop(0,   '#ffffff');
  wg.addColorStop(0.55, '#d8c8ff');
  wg.addColorStop(1,   '#80d8ff');
  ctx.fillStyle   = wg;
  ctx.shadowColor = 'rgba(160,120,255,0.3)'; ctx.shadowBlur = 20;
  ctx.fillText('Welcome To Our Group', rX, PAD + 78);
  ctx.restore();

  // Underline accent — two-layer glow line
  ctx.save();
  ctx.shadowColor = 'rgba(100,180,255,0.9)'; ctx.shadowBlur = 14;
  const ulg = ctx.createLinearGradient(rX, 0, rX + 160, 0);
  ulg.addColorStop(0,   'rgba(160,100,255,1)');
  ulg.addColorStop(0.5, 'rgba(60,200,255,1)');
  ulg.addColorStop(1,   'rgba(255,80,200,1)');
  ctx.strokeStyle = ulg; ctx.lineWidth = 3;
  ctx.beginPath(); ctx.moveTo(rX, PAD + 90); ctx.lineTo(rX + 150, PAD + 90); ctx.stroke();
  ctx.restore();

  // ── Group section ─────────────────────────────────────────────────────────
  const secLabelStyle = (label, x, y) => {
    ctx.save();
    ctx.font = '600 11px "Segoe UI", Arial';
    ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
    ctx.fillStyle = 'rgba(160,140,220,0.7)';
    ctx.fillText(label, x, y);
    ctx.restore();
  };

  const groupSecY = PAD + 128;
  secLabelStyle('GROUP', rX, groupSecY);

  const gAy    = groupSecY + 10;
  const gAvSz  = 78;

  if (groupImg) {
    ctx.save();
    roundRect(ctx, rX, gAy, gAvSz, gAvSz, 14);
    ctx.clip();
    ctx.drawImage(groupImg, rX, gAy, gAvSz, gAvSz);
    ctx.restore();
    ctx.save();
    ctx.strokeStyle = 'rgba(100,180,255,0.5)'; ctx.lineWidth = 2;
    roundRect(ctx, rX, gAy, gAvSz, gAvSz, 14); ctx.stroke();
    ctx.restore();
  } else {
    const gg = ctx.createLinearGradient(rX, gAy, rX + gAvSz, gAy + gAvSz);
    gg.addColorStop(0, '#12122a'); gg.addColorStop(1, '#0a0a1e');
    roundRect(ctx, rX, gAy, gAvSz, gAvSz, 14);
    ctx.fillStyle = gg; ctx.fill();
    ctx.save();
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.font = '34px Arial'; ctx.fillStyle = 'rgba(255,255,255,0.18)';
    ctx.fillText('🏠', rX + gAvSz / 2, gAy + gAvSz / 2);
    ctx.restore();
  }

  {
    const gTx = rX + gAvSz + 18;
    const gTw = rRight - gTx;
    ctx.save();
    ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
    const { text: gn, size: gs } = fitText(ctx, safeGroup, gTw, 28, 13);
    ctx.font      = `bold ${gs}px "Segoe UI", Arial`;
    ctx.fillStyle = '#dde8ff';
    ctx.shadowColor = 'rgba(0,0,0,0.6)'; ctx.shadowBlur = 6;
    ctx.fillText(gn, gTx, gAy + gAvSz / 2);
    ctx.restore();
  }

  // Separator
  {
    const sy = gAy + gAvSz + 20;
    const sg = ctx.createLinearGradient(rX, 0, rRight, 0);
    sg.addColorStop(0,   'rgba(160,100,255,0.4)');
    sg.addColorStop(0.5, 'rgba(60,200,255,0.2)');
    sg.addColorStop(1,   'rgba(255,80,200,0.0)');
    ctx.strokeStyle = sg; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(rX, sy); ctx.lineTo(rRight, sy); ctx.stroke();
  }

  // ── Inviter section ───────────────────────────────────────────────────────
  const invSecY = gAy + gAvSz + 36;
  secLabelStyle('ADDED BY', rX, invSecY);

  const invAy  = invSecY + 10;
  const invAvR = 46;
  const invCX  = rX + invAvR;
  const invCY  = invAy + invAvR;

  // Inviter avatar ring
  ctx.save();
  ctx.shadowColor = 'rgba(255,180,80,0.45)'; ctx.shadowBlur = 18;
  const invRingG = ctx.createLinearGradient(
    invCX - invAvR - 4, invCY - invAvR - 4,
    invCX + invAvR + 4, invCY + invAvR + 4
  );
  invRingG.addColorStop(0, 'rgba(255,180,60,0.85)');
  invRingG.addColorStop(1, 'rgba(255,100,200,0.70)');
  ctx.strokeStyle = invRingG; ctx.lineWidth = 2.5;
  ctx.beginPath(); ctx.arc(invCX, invCY, invAvR + 4, 0, Math.PI * 2); ctx.stroke();
  ctx.restore();

  if (inviterImg) {
    drawCircleAvatar(ctx, inviterImg, invCX, invCY, invAvR);
  } else {
    ctx.fillStyle = '#12122a';
    ctx.beginPath(); ctx.arc(invCX, invCY, invAvR, 0, Math.PI * 2); ctx.fill();
    ctx.save();
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.font = '28px Arial'; ctx.fillStyle = 'rgba(255,255,255,0.18)';
    ctx.fillText('👤', invCX, invCY);
    ctx.restore();
  }

  {
    const iTx = invCX + invAvR + 18;
    const iTw = rRight - iTx;
    ctx.save();
    ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
    const { text: iname, size: is } = fitText(ctx, safeInviter, iTw, 28, 13);
    ctx.font      = `bold ${is}px "Segoe UI", Arial`;
    ctx.fillStyle = '#e0d8ff';
    ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 5;
    ctx.fillText(iname, iTx, invCY);
    ctx.restore();
  }

  // ── Bottom "Powered By" pill ──────────────────────────────────────────────
  {
    const bY  = invAy + invAvR * 2 + 28;
    const bH  = H - PAD - 18 - bY;
    const bX  = rX;
    const bW  = rW;

    // pill glass bg
    roundRect(ctx, bX, bY, bW, bH, 16);
    const pillBg = ctx.createLinearGradient(bX, bY, bX + bW, bY + bH);
    pillBg.addColorStop(0, 'rgba(140,80,255,0.06)');
    pillBg.addColorStop(1, 'rgba(60,200,255,0.04)');
    ctx.fillStyle = pillBg; ctx.fill();

    roundRect(ctx, bX, bY, bW, bH, 16);
    ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1;
    ctx.stroke();

    // Powered By text
    const cx2 = bX + bW / 2;
    const cy2 = bY + bH / 2;
    ctx.save();
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.font = 'bold 20px "Segoe UI", Arial';
    const pwg = ctx.createLinearGradient(cx2 - 130, 0, cx2 + 130, 0);
    pwg.addColorStop(0,    'rgba(200,180,255,0.9)');
    pwg.addColorStop(0.35, 'rgba(255,255,255,0.95)');
    pwg.addColorStop(0.65, 'rgba(80,210,255,1)');
    pwg.addColorStop(1,    'rgba(255,90,200,1)');
    ctx.fillStyle   = pwg;
    ctx.shadowColor = 'rgba(100,180,255,0.6)'; ctx.shadowBlur = 18;
    ctx.fillText('Powered By HR ID OY', cx2, cy2);
    ctx.restore();

    // small decorative dots left & right of text
    for (let di = 0; di < 3; di++) {
      const alpha = 0.25 - di * 0.07;
      ctx.fillStyle = `rgba(120,200,255,${alpha})`;
      ctx.beginPath();
      ctx.arc(bX + 20 + di * 10, cy2, 3 - di * 0.5, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(bX + bW - 20 - di * 10, cy2, 3 - di * 0.5, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  // ── "Enjoy your stay" watermark ───────────────────────────────────────────
  ctx.save();
  ctx.textAlign = 'right'; ctx.font = '400 13px "Segoe UI", Arial';
  ctx.fillStyle = 'rgba(255,255,255,0.10)';
  ctx.fillText('Enjoy your stay ✨', W - 44, H - 36);
  ctx.restore();

  // ══════════════════════════════════════════════════════════════════════════
  // 5. CORNER ORNAMENTS — four tiny hex corners for extra uniqueness
  // ══════════════════════════════════════════════════════════════════════════
  const corners = [
    [PAD + 24, PAD + 24],
    [W - PAD - 24, PAD + 24],
    [PAD + 24, H - PAD - 24],
    [W - PAD - 24, H - PAD - 24],
  ];
  const ornG = ctx.createLinearGradient(0, 0, W, H);
  ornG.addColorStop(0, 'rgba(160,80,255,0.5)');
  ornG.addColorStop(1, 'rgba(60,200,255,0.4)');
  for (const [cx3, cy3] of corners) {
    hexPath(ctx, cx3, cy3, 11);
    ctx.strokeStyle = ornG; ctx.lineWidth = 1.4;
    ctx.stroke();
  }

  // Save
  const tempPath = path.join(__dirname, `temp_welcome_${Date.now()}.png`);
  await fs.writeFile(tempPath, canvas.toBuffer('image/png'));
  return tempPath;
}

View raw file