javascript
445 lines ยท 17,907 chars
// Author IQC : Ditzzx & Rin
// Sumber : https://whatsapp.com/channel/0029VbCHRSDAzNboLatr0W0o
import axios from 'axios'
import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas'
import { writeFile, mkdir, readFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import fs from 'node:fs'
const __dirname = dirname(fileURLToPath(import.meta.url));
const APPLE_EMOJI_JSON_URL = 'https://media.githubusercontent.com/media/Ditzzx-vibecoder/entahlah/main/emoji-apple.json';
const pluginConfig = {
name: "iqc",
alias: [],
category: "canvas",
description: "Membuat Fake Quote iOS style.",
usage: ".iqc [text/reply]",
isOwner: false,
isPremium: false,
isGroup: false,
isPrivate: false,
cooldown: 5,
energi: 2,
isEnabled: true,
};
let appleEmojiMap = null;
let fontsLoaded = false;
const emojiImageCache = new Map();
let bgImgBuffer = null;
async function downloadFile(url) {
const res = await axios.get(url, { responseType: 'arraybuffer', headers: { 'User-Agent': 'Mozilla/5.0' }, maxRedirects: 5 });
return Buffer.from(res.data);
}
function emojiToUnicode(emoji) {
return [...emoji].map(c => c.codePointAt(0).toString(16).padStart(4, '0')).join('-');
}
async function loadAppleEmojiMap() {
if (appleEmojiMap) return appleEmojiMap;
const buf = await downloadFile(APPLE_EMOJI_JSON_URL);
appleEmojiMap = JSON.parse(buf.toString('utf-8'));
return appleEmojiMap;
}
async function getEmojiImage(emoji) {
if (emojiImageCache.has(emoji)) return emojiImageCache.get(emoji);
const map = await loadAppleEmojiMap();
const base = emojiToUnicode(emoji);
const variants = [
base,
base.replace(/-fe0f/gi, ''),
`${base.replace(/-fe0f/gi, '')}-fe0f`,
base.toUpperCase(),
base.replace(/-fe0f/gi, '').toUpperCase(),
base.replace(/-fe0f/gi, '').toUpperCase() + '-FE0F',
];
let b64 = null;
for (const v of variants) {
if (map[v]) { b64 = map[v]; break; }
}
if (!b64) return null;
const buf = Buffer.from(b64, 'base64');
const img = await loadImage(buf);
emojiImageCache.set(emoji, img);
return img;
}
async function drawAppleEmoji(ctx, emoji, x, y, size) {
const img = await getEmojiImage(emoji);
if (!img) {
ctx.fillText(emoji, x, y);
return;
}
ctx.drawImage(img, x - size / 2, y - size / 2, size, size);
}
const EMOJI_REGEX = /(\p{Emoji_Modifier_Base}\p{Emoji_Modifier}|\p{Emoji_Presentation}\uFE0F?|\p{Emoji}\uFE0F|[\u{1F1E0}-\u{1F1FF}]{2}|\p{Extended_Pictographic}\uFE0F?)/gu;
function measureTextCustom(ctx, text, fontSize) {
const parts = text.split(EMOJI_REGEX);
let totalWidth = 0;
for (const part of parts) {
if (!part) continue;
EMOJI_REGEX.lastIndex = 0;
if (EMOJI_REGEX.test(part)) {
totalWidth += fontSize * 1.05;
} else {
totalWidth += ctx.measureText(part).width;
}
EMOJI_REGEX.lastIndex = 0;
}
return totalWidth;
}
async function drawTextWithEmojis(ctx, text, x, y, fontSize) {
const parts = text.split(EMOJI_REGEX);
let currentX = x;
for (const part of parts) {
if (!part) continue;
EMOJI_REGEX.lastIndex = 0;
if (EMOJI_REGEX.test(part)) {
const emojiSize = fontSize * 1.05;
const emojiCX = currentX + emojiSize / 2;
const emojiCY = y;
await drawAppleEmoji(ctx, part, emojiCX, emojiCY, emojiSize);
currentX += emojiSize;
} else {
ctx.fillText(part, currentX, y);
currentX += ctx.measureText(part).width;
}
EMOJI_REGEX.lastIndex = 0;
}
}
function wrapText(ctx, text, maxWidth, fontSize) {
ctx.font = `${fontSize}px InterRegular`;
const words = text.split(" ");
const lines = [];
let cur = "";
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (word.includes('\n')) {
const parts = word.split('\n');
for (let j = 0; j < parts.length; j++) {
const test = cur + (cur ? " " : "") + parts[j];
if (measureTextCustom(ctx, test, fontSize) > maxWidth && cur) {
lines.push(cur); cur = parts[j];
} else { cur = test; }
if (j < parts.length - 1) { lines.push(cur); cur = ""; }
}
continue;
}
const test = cur + (cur ? " " : "") + word;
if (measureTextCustom(ctx, test, fontSize) > maxWidth && i > 0) {
lines.push(cur); cur = word;
} else { cur = test; }
}
if (cur) lines.push(cur);
return lines;
}
async function handler(m, { sock, text }) {
try {
const targetText = text || (m.quoted && m.quoted.text ? m.quoted.text : "");
let targetImgBuffer = null;
try {
if (m.quoted && typeof m.quoted.download === 'function') {
targetImgBuffer = await m.quoted.download();
} else if (typeof m.download === 'function' && (m.isMedia || m.mtype === 'imageMessage' || m.type === 'imageMessage')) {
targetImgBuffer = await m.download();
}
} catch (e) {}
if (!targetText && !targetImgBuffer) {
return m.reply(
`๐ฌ *FITUR FAKE QUOTE iOS*\n\n` +
`Sistem ini akan mengubah teks yang kamu berikan menjadi gambar *quote* bergaya iOS yang sangat elegan dan modern.\n\n` +
`*CARA PENGGUNAAN:*\n` +
`- Ketik \`${m.prefix}iqc <teks kamu>\`\n` +
`- Atau balas (*reply*) pesan orang lain dengan perintah \`${m.prefix}iqc\`\n\n` +
`_Pesan yang dibalas akan otomatis diubah menjadi quote menggunakan nama dan foto profil pembuat pesan tersebut!_`
);
}
let ts = m.messageTimestamp;
if (ts && typeof ts === 'object' && ts.low !== undefined) ts = ts.low;
ts = Number(ts) * 1000;
const date = isNaN(ts) ? new Date() : new Date(ts);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const timeStr = `${hours}.${minutes}`;
let imgUrl = "";
let caption = "";
const tmpImgPath = join(process.cwd(), 'temp', `iqc_${Date.now()}.png`);
if (targetImgBuffer) {
await writeFile(tmpImgPath, targetImgBuffer);
imgUrl = tmpImgPath;
caption = targetText || "";
}
const txt = targetText || "";
let emojis = ["๐", "โค๏ธ", "๐", "๐ฎ", "๐ข", "๐"];
await m.react('๐');
const RIN_BG_URL = 'https://files.catbox.moe/29ljw9.png'
const RIN_TMP = join(process.cwd(), 'temp')
const RIN_FONTS = [
{ url: 'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2', file: 'Inter-Regular.ttf' }
]
const BG_W = 941
const BG_H = 1671
await mkdir(RIN_TMP, { recursive: true })
async function rinDownload(url, isJson = false) {
const res = await axios.get(url, { responseType: isJson ? 'json' : 'arraybuffer', headers: { 'User-Agent': 'Mozilla/5.0' }, maxRedirects: 5 })
return isJson ? res.data : Buffer.from(res.data)
}
if (!fontsLoaded) {
for (const f of RIN_FONTS) {
GlobalFonts.register(await rinDownload(f.url), 'InterRegular')
}
fontsLoaded = true;
}
if (!bgImgBuffer) {
bgImgBuffer = await rinDownload(RIN_BG_URL)
}
await loadAppleEmojiMap();
const canvas = createCanvas(BG_W, BG_H)
const ctx = canvas.getContext('2d')
const bgImg = await loadImage(bgImgBuffer)
ctx.drawImage(bgImg, 0, 0, BG_W, BG_H)
const PERMANENT_TIME_X = 463;
const PERMANENT_TIME_Y = 8;
const PERMANENT_TIME_SIZE = 27;
ctx.fillStyle = "#ffffff";
ctx.font = `${PERMANENT_TIME_SIZE}px InterRegular`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(timeStr, PERMANENT_TIME_X, PERMANENT_TIME_Y);
const chatFontSize = 30;
const maxWidthLimit = 530;
const minBubbleWidth = 280;
const lineHeight = chatFontSize + 14;
const paddingX = 30;
const paddingY = 20;
const rad = 28;
const fixedX = 35;
const fixedBaseY = 946;
ctx.font = `22px InterRegular`;
const timeWidth = ctx.measureText(timeStr).width;
let finalY, finalBubbleHeight, bubbleW;
if (!imgUrl) {
ctx.font = `${chatFontSize}px InterRegular`;
const chatLines = wrapText(ctx, txt, maxWidthLimit, chatFontSize);
let longestW = 0;
chatLines.forEach(l => {
const w = measureTextCustom(ctx, l.trim(), chatFontSize);
if (w > longestW) longestW = w;
});
bubbleW = longestW + (paddingX * 2);
bubbleW = Math.max(bubbleW, timeWidth + 75);
bubbleW = Math.max(bubbleW, 180);
const spaceTimeY = 12;
finalBubbleHeight = (chatLines.length * lineHeight) + paddingY + spaceTimeY + 22;
finalY = fixedBaseY - finalBubbleHeight;
ctx.fillStyle = "#1c1c1e";
ctx.beginPath();
ctx.moveTo(fixedX + rad, finalY);
ctx.lineTo(fixedX + bubbleW - rad, finalY);
ctx.quadraticCurveTo(fixedX + bubbleW, finalY, fixedX + bubbleW, finalY + rad);
ctx.lineTo(fixedX + bubbleW, finalY + finalBubbleHeight - rad);
ctx.quadraticCurveTo(fixedX + bubbleW, finalY + finalBubbleHeight, fixedX + bubbleW - rad, finalY + finalBubbleHeight);
ctx.lineTo(fixedX + rad, finalY + finalBubbleHeight);
ctx.quadraticCurveTo(fixedX + 8, finalY + finalBubbleHeight, fixedX + 8, finalY + finalBubbleHeight - 8);
ctx.lineTo(fixedX + 8, finalY + rad);
ctx.quadraticCurveTo(fixedX + 8, finalY, fixedX + rad, finalY);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(fixedX + 12, finalY + finalBubbleHeight - 20);
ctx.quadraticCurveTo(fixedX - 2, finalY + finalBubbleHeight - 4, fixedX - 8, finalY + finalBubbleHeight);
ctx.quadraticCurveTo(fixedX + 6, finalY + finalBubbleHeight, fixedX + 22, finalY + finalBubbleHeight - 2);
ctx.closePath();
ctx.fill();
ctx.save();
ctx.fillStyle = "#ffffff";
ctx.font = `${chatFontSize}px InterRegular`;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
for (let i = 0; i < chatLines.length; i++) {
const lineY = finalY + paddingY + (i * lineHeight) + (chatFontSize / 2);
await drawTextWithEmojis(ctx, chatLines[i].trim(), fixedX + paddingX, lineY, chatFontSize);
}
ctx.restore();
ctx.fillStyle = "#727278";
ctx.font = `22px InterRegular`;
ctx.textAlign = "right";
ctx.textBaseline = "top";
ctx.fillText(timeStr, fixedX + bubbleW - 22, finalY + finalBubbleHeight - 38);
} else {
const imgBuf = imgUrl.startsWith('http')
? await rinDownload(imgUrl)
: await readFile(imgUrl)
const imgObj = await loadImage(imgBuf)
const imgAspect = imgObj.width / imgObj.height
bubbleW = Math.min(Math.max(imgObj.width, minBubbleWidth), maxWidthLimit)
let imgDrawH = Math.round(bubbleW / imgAspect)
bubbleW = Math.max(bubbleW, timeWidth + 75);
let captionLines = [];
if (caption) {
ctx.font = `${chatFontSize}px InterRegular`;
captionLines = wrapText(ctx, caption, bubbleW - paddingX * 2, chatFontSize);
}
const captionH = captionLines.length > 0
? paddingY + (captionLines.length * lineHeight)
: 0;
const timeRowH = 28;
finalBubbleHeight = imgDrawH + captionH + timeRowH + (captionLines.length > 0 ? 4 : 0);
finalY = fixedBaseY - finalBubbleHeight;
ctx.fillStyle = "#1c1c1e";
ctx.beginPath();
ctx.moveTo(fixedX + rad, finalY);
ctx.lineTo(fixedX + bubbleW - rad, finalY);
ctx.quadraticCurveTo(fixedX + bubbleW, finalY, fixedX + bubbleW, finalY + rad);
ctx.lineTo(fixedX + bubbleW, finalY + finalBubbleHeight - rad);
ctx.quadraticCurveTo(fixedX + bubbleW, finalY + finalBubbleHeight, fixedX + bubbleW - rad, finalY + finalBubbleHeight);
ctx.lineTo(fixedX + rad, finalY + finalBubbleHeight);
ctx.quadraticCurveTo(fixedX + 8, finalY + finalBubbleHeight, fixedX + 8, finalY + finalBubbleHeight - 8);
ctx.lineTo(fixedX + 8, finalY + rad);
ctx.quadraticCurveTo(fixedX + 8, finalY, fixedX + rad, finalY);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(fixedX + 12, finalY + finalBubbleHeight - 20);
ctx.quadraticCurveTo(fixedX - 2, finalY + finalBubbleHeight - 4, fixedX - 8, finalY + finalBubbleHeight);
ctx.quadraticCurveTo(fixedX + 6, finalY + finalBubbleHeight, fixedX + 22, finalY + finalBubbleHeight - 2);
ctx.closePath();
ctx.fill();
ctx.save();
ctx.beginPath();
ctx.moveTo(fixedX + rad, finalY);
ctx.lineTo(fixedX + bubbleW - rad, finalY);
ctx.quadraticCurveTo(fixedX + bubbleW, finalY, fixedX + bubbleW, finalY + rad);
ctx.lineTo(fixedX + bubbleW, finalY + imgDrawH);
ctx.lineTo(fixedX + 8, finalY + imgDrawH);
ctx.lineTo(fixedX + 8, finalY + rad);
ctx.quadraticCurveTo(fixedX + 8, finalY, fixedX + rad, finalY);
ctx.closePath();
ctx.clip();
ctx.drawImage(imgObj, fixedX, finalY, bubbleW, imgDrawH);
ctx.beginPath();
ctx.moveTo(fixedX + 8, finalY + imgDrawH);
ctx.lineTo(fixedX + 8, finalY + rad);
ctx.quadraticCurveTo(fixedX + 8, finalY, fixedX + rad, finalY);
ctx.lineTo(fixedX + bubbleW - rad, finalY);
ctx.quadraticCurveTo(fixedX + bubbleW, finalY, fixedX + bubbleW, finalY + rad);
ctx.lineTo(fixedX + bubbleW, finalY + imgDrawH);
ctx.strokeStyle = "#1c1c1e";
ctx.lineWidth = 18;
ctx.stroke();
ctx.restore();
if (captionLines.length > 0) {
ctx.save();
ctx.fillStyle = "#ffffff";
ctx.font = `${chatFontSize}px InterRegular`;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
for (let i = 0; i < captionLines.length; i++) {
const lineY = finalY + imgDrawH + paddingY + (i * lineHeight) + (chatFontSize / 2);
await drawTextWithEmojis(ctx, captionLines[i].trim(), fixedX + paddingX, lineY, chatFontSize);
}
ctx.restore();
}
ctx.fillStyle = "#727278";
ctx.font = `22px InterRegular`;
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(timeStr, fixedX + bubbleW - 22, finalY + finalBubbleHeight - timeRowH);
}
const emojiSize = Math.round(54 * 1.03);
const emCardH = emojiSize + Math.round(44 * 1.03);
const emCardW = Math.round(530 * 1.03);
const emCardX = fixedX + 8;
const emCardY = finalY - emCardH - 18;
ctx.fillStyle = "#1c1c1e";
ctx.beginPath();
ctx.roundRect(emCardX, emCardY, emCardW, emCardH, [emCardH / 2]);
ctx.fill();
const startX = emCardX + 55;
const spacingX = 76;
const emojiCY = emCardY + (emCardH / 2) + 2;
for (let i = 0; i < Math.min(emojis.length, 6); i++) {
await drawAppleEmoji(ctx, emojis[i], startX + (i * spacingX), emojiCY, emojiSize);
}
ctx.fillStyle = "#8e8e93";
ctx.font = `${Math.round(36 * 1.03)}px InterRegular`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("+", startX + (6 * spacingX) - 8, emCardY + (emCardH / 2) - 2);
const rinOut = join(process.cwd(), 'temp', `rinchat-${Date.now()}.png`);
await writeFile(rinOut, await canvas.encode('png'));
await sock.sendMessage(m.chat, { image: { url: rinOut } }, { quoted: m });
if (existsSync(rinOut)) fs.unlinkSync(rinOut);
if (imgUrl && existsSync(imgUrl)) fs.unlinkSync(imgUrl);
await m.react('โ
');
} catch (error) {
bgImgBuffer = null;
fontsLoaded = false;
appleEmojiMap = null;
console.error(error)
await m.react('โ')
m.reply(`โ *GAGAL MEMPROSES QUOTE*\n\nMaaf, terjadi kesalahan saat mencoba membuat gambar quote. Silakan coba lagi beberapa saat.`)
}
}
export { pluginConfig as config, handler };