Files
manyplug-repo/figurinha/index.js
synt-xerror eecef0be88 first commit
2026-04-14 21:15:05 -03:00

250 lines
8.3 KiB
JavaScript

/**
* plugins/figurinha/index.js
*
* Usage modes:
* command + attached media → creates 1 sticker directly
* command + replying to media → creates 1 sticker directly
* command + attached media + replying → creates 2 stickers directly
* command (no media) → opens session
* command create (with open session) → processes session media
*/
import fs from "fs";
import path from "path";
import os from "os";
import { execFile } from "child_process";
import { promisify } from "util";
import { createSticker } from "wa-sticker-formatter";
import { emptyFolder } from "../../utils/file.js";
import { CMD_PREFIX } from "../../config.js";
import { createPluginI18n } from "../../utils/pluginI18n.js";
const { t } = createPluginI18n(import.meta.url);
const execFileAsync = promisify(execFile);
// ── Constants ────────────────────────────────────────────────
const DOWNLOADS_DIR = path.resolve("downloads");
const FFMPEG = os.platform() === "win32" ? ".\\bin\\ffmpeg.exe" : "./bin/ffmpeg";
const MAX_STICKER_SIZE = 900 * 1024;
const SESSION_TIMEOUT = 2 * 60 * 1000;
const MAX_MEDIA = 30;
const getHelp = () =>
`${t("help")} \`${CMD_PREFIX}figurinha\` ${t("helpMedia")} \`${CMD_PREFIX}figurinha\` ${t("helpSession")} \`${CMD_PREFIX}figurinha criar\` ${t("helpCreate")}`;
// ── Internal state ───────────────────────────────────────────
// { chatId → { author, medias[], timeout } }
const sessions = new Map();
// ── Conversion ────────────────────────────────────────────────
function ensureDir() {
fs.mkdirSync(DOWNLOADS_DIR, { recursive: true });
}
function cleanup(...files) {
for (const f of files) {
try { if (f && fs.existsSync(f)) fs.unlinkSync(f); } catch { }
}
}
async function convertToGif(input, output, fps = 12) {
const filter = [
`fps=${Math.min(fps, 12)},scale=512:512:flags=lanczos,split[s0][s1]`,
`[s0]palettegen=max_colors=256:reserve_transparent=1[p]`,
`[s1][p]paletteuse=dither=bayer`,
].join(";");
await execFileAsync(FFMPEG, ["-i", input, "-filter_complex", filter, "-loop", "0", "-y", output]);
}
async function resizeImage(input, output) {
await execFileAsync(FFMPEG, ["-i", input, "-vf", "scale=512:512:flags=lanczos", "-y", output]);
}
async function buildSticker(inputPath, isAnimated) {
for (const quality of [80, 60, 40, 20]) {
const buf = await createSticker(fs.readFileSync(inputPath), {
pack: t("pack"),
author: t("author"),
type: isAnimated ? "FULL" : "STATIC",
categories: ["🤖"],
quality,
});
if (buf.length <= MAX_STICKER_SIZE) return buf;
}
throw new Error(t("error.tooLarge"));
}
/**
* Converte um objeto { mimetype, data } em sticker e envia.
* Retorna true se ok, false se falhou.
*/
async function processarUmaMedia(media, isGif, api, msg) {
ensureDir();
const ext = media.mimetype.split("/")[1];
const isVideo = media.mimetype.startsWith("video/");
const isAnimated = isVideo || isGif;
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const inputPath = path.join(DOWNLOADS_DIR, `${id}.${ext}`);
const gifPath = path.join(DOWNLOADS_DIR, `${id}.gif`);
const resizedPath = path.join(DOWNLOADS_DIR, `${id}-scaled.${ext}`);
try {
fs.writeFileSync(inputPath, Buffer.from(media.data, "base64"));
let stickerInput;
if (isAnimated) {
await convertToGif(inputPath, gifPath, isVideo ? 12 : 24);
stickerInput = gifPath;
} else {
await resizeImage(inputPath, resizedPath);
stickerInput = resizedPath;
}
const buf = await buildSticker(stickerInput, isAnimated);
await api.sendSticker(buf);
return true;
} catch (err) {
api.log.error(`Sticker generation error: ${err.message}`);
await msg.reply(t("error.generic"));
return false;
} finally {
cleanup(inputPath, gifPath, resizedPath);
}
}
/**
* Verifica se uma mídia é suportada para sticker.
*/
function isSupported(media, isGif) {
return (
media.mimetype?.startsWith("image/") ||
media.mimetype?.startsWith("video/") ||
isGif
);
}
// ── Plugin ───────────────────────────────────────────────────
export default async function ({ msg, api }) {
const chatId = api.chat.id;
if (!msg.is(CMD_PREFIX + "figurinha")) {
// ── Coleta de mídia durante sessão ──────────────────────
const session = sessions.get(chatId);
if (!session) return;
if (!msg.hasMedia) return;
if (msg.sender !== session.author) return;
const media = await msg.downloadMedia();
if (!media) return;
const gif = media.mimetype === "image/gif" ||
(media.mimetype === "video/mp4" && msg.isGif);
if (isSupported(media, gif) && session.medias.length < MAX_MEDIA) {
session.medias.push({ media, isGif: gif });
}
return;
}
const sub = msg.args[1];
// ── figurinha parar ──────────────────────────────────────
if (sub === "parar") {
const session = sessions.get(chatId);
if (!session) {
await msg.reply(`${t("session.noneActive")}\n\n${getHelp()}`);
return;
}
clearTimeout(session.timeout);
sessions.delete(chatId);
await msg.reply(t("session.stopped"));
return;
}
// ── figurinha criar ──────────────────────────────────────
if (sub === "criar") {
const session = sessions.get(chatId);
if (!session) {
await msg.reply(`${t("session.noneActive")}\n\n${getHelp()}`);
return;
}
if (!session.medias.length) {
await msg.reply(`${t("session.noMedia")}\n\n${getHelp()}`);
return;
}
clearTimeout(session.timeout);
await msg.reply(t("session.generating"));
for (const { media, isGif } of session.medias) {
await processarUmaMedia(media, isGif, api, msg);
}
await msg.reply(t("session.success"));
sessions.delete(chatId);
emptyFolder(DOWNLOADS_DIR);
return;
}
// ── figurinha com mídia direta ───────────────────────────
const mediasParaCriar = [];
// Mídia anexa à própria mensagem
if (msg.hasMedia) {
const media = await msg.downloadMedia();
if (media) {
const gif = media.mimetype === "image/gif" ||
(media.mimetype === "video/mp4" && msg.isGif);
if (isSupported(media, gif)) mediasParaCriar.push({ media, isGif: gif });
}
}
// Mídia da mensagem citada
if (msg.hasReply) {
const quoted = await msg.getReply();
if (quoted?.hasMedia) {
const media = await quoted.downloadMedia();
if (media) {
const gif = media.mimetype === "image/gif" ||
(media.mimetype === "video/mp4" && quoted.isGif);
if (isSupported(media, gif)) mediasParaCriar.push({ media, isGif: gif });
}
}
}
// Tem mídia para criar direto
if (mediasParaCriar.length > 0) {
await msg.reply(t("session.generatingOne"));
for (const { media, isGif } of mediasParaCriar) {
await processarUmaMedia(media, isGif, api, msg);
}
emptyFolder(DOWNLOADS_DIR);
return;
}
// ── figurinha sem mídia → abre sessão ───────────────────
if (sessions.has(chatId)) {
await msg.reply(
`${t("session.alreadyOpen")} \`${CMD_PREFIX}figurinha criar\`.\n` +
t("session.waitExpire")
);
return;
}
const timeout = setTimeout(async () => {
sessions.delete(chatId);
try {
await msg.reply(
`${t("session.expired")} \`${CMD_PREFIX}figurinha\` ${t("session.expiredEnd")}`
);
} catch { }
}, SESSION_TIMEOUT);
sessions.set(chatId, { author: msg.sender, medias: [], timeout });
await msg.reply(`${t("session.started")} *${msg.senderName}*!\n\n${getHelp()}`);
}