From eecef0be8856cd38c5f77b6832dd9898e52f0540 Mon Sep 17 00:00:00 2001
From: synt-xerror <169557594+synt-xerror@users.noreply.github.com>
Date: Tue, 14 Apr 2026 21:15:05 -0300
Subject: [PATCH] first commit
---
.gitignore | 1 +
README.md | 5 +
a/index.js | 9 ++
a/manyplug.json | 8 ++
adivinhação/index.js | 77 ++++++++++++
adivinhação/locale/en.json | 18 +++
adivinhação/locale/es.json | 18 +++
adivinhação/locale/pt.json | 18 +++
adivinhação/manyplug.json | 7 ++
audio/index.js | 116 +++++++++++++++++
audio/locale/en.json | 12 ++
audio/locale/es.json | 12 ++
audio/locale/pt.json | 12 ++
audio/manyplug.json | 7 ++
figurinha/index.js | 250 +++++++++++++++++++++++++++++++++++++
figurinha/locale/en.json | 25 ++++
figurinha/locale/es.json | 25 ++++
figurinha/locale/pt.json | 25 ++++
figurinha/manyplug.json | 9 ++
forca/index.js | 160 ++++++++++++++++++++++++
forca/locale/en.json | 14 +++
forca/locale/es.json | 14 +++
forca/locale/pt.json | 14 +++
forca/manyplug.json | 7 ++
manager.js | 74 +++++++++++
many/index.js | 17 +++
many/locale/en.json | 8 ++
many/locale/es.json | 8 ++
many/locale/pt.json | 8 ++
many/manyplug.json | 7 ++
obrigado/index.js | 12 ++
obrigado/locale/en.json | 3 +
obrigado/locale/es.json | 3 +
obrigado/locale/pt.json | 3 +
package-lock.json | 105 ++++++++++++++++
package.json | 17 +++
registry.json | 64 ++++++++++
video/index.js | 109 ++++++++++++++++
video/locale/en.json | 12 ++
video/locale/es.json | 12 ++
video/locale/pt.json | 12 ++
video/manyplug.json | 7 ++
xp/index.js | 21 ++++
xp/manyplug.json | 7 ++
44 files changed, 1372 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 a/index.js
create mode 100644 a/manyplug.json
create mode 100644 adivinhação/index.js
create mode 100644 adivinhação/locale/en.json
create mode 100644 adivinhação/locale/es.json
create mode 100644 adivinhação/locale/pt.json
create mode 100644 adivinhação/manyplug.json
create mode 100644 audio/index.js
create mode 100644 audio/locale/en.json
create mode 100644 audio/locale/es.json
create mode 100644 audio/locale/pt.json
create mode 100644 audio/manyplug.json
create mode 100644 figurinha/index.js
create mode 100644 figurinha/locale/en.json
create mode 100644 figurinha/locale/es.json
create mode 100644 figurinha/locale/pt.json
create mode 100644 figurinha/manyplug.json
create mode 100644 forca/index.js
create mode 100644 forca/locale/en.json
create mode 100644 forca/locale/es.json
create mode 100644 forca/locale/pt.json
create mode 100644 forca/manyplug.json
create mode 100644 manager.js
create mode 100644 many/index.js
create mode 100644 many/locale/en.json
create mode 100644 many/locale/es.json
create mode 100644 many/locale/pt.json
create mode 100644 many/manyplug.json
create mode 100644 obrigado/index.js
create mode 100644 obrigado/locale/en.json
create mode 100644 obrigado/locale/es.json
create mode 100644 obrigado/locale/pt.json
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 registry.json
create mode 100644 video/index.js
create mode 100644 video/locale/en.json
create mode 100644 video/locale/es.json
create mode 100644 video/locale/pt.json
create mode 100644 video/manyplug.json
create mode 100644 xp/index.js
create mode 100644 xp/manyplug.json
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..28c4c89
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# manyplug-repo
+
+This is a mirror of Freakkdev ManyBot's plugin repository (ManyPlug).
+
+If you want to publish a plugin, you can make a pull request or send a Git patch to the email: `manybot@pm.me`.
diff --git a/a/index.js b/a/index.js
new file mode 100644
index 0000000..67f0f96
--- /dev/null
+++ b/a/index.js
@@ -0,0 +1,9 @@
+import { hangmanActive } from "../forca/index.js";
+
+export default async function ({ msg }) {
+ if (msg.body.trim().toLowerCase() !== "a") return;
+ if (msg.args.length > 1) return;
+ if (hangmanActive) return;
+
+ await msg.reply("B!");
+}
\ No newline at end of file
diff --git a/a/manyplug.json b/a/manyplug.json
new file mode 100644
index 0000000..d04c8b4
--- /dev/null
+++ b/a/manyplug.json
@@ -0,0 +1,8 @@
+{
+ "name": "a",
+ "author": "freakk.dev",
+ "version": "1.0.0",
+ "category": "humor",
+ "service": false,
+ "dependencies": {}
+}
diff --git a/adivinhação/index.js b/adivinhação/index.js
new file mode 100644
index 0000000..2bdc622
--- /dev/null
+++ b/adivinhação/index.js
@@ -0,0 +1,77 @@
+/**
+ * plugins/adivinhacao/index.js
+ *
+ * Game state lives here — isolated in the plugin.
+ * Multiple groups can play simultaneously without conflict.
+ */
+
+import { CMD_PREFIX } from "../../config.js";
+import { createPluginI18n } from "../../utils/pluginI18n.js";
+
+const { t } = createPluginI18n(import.meta.url);
+
+const RANGE = { min: 1, max: 100 };
+const jogosAtivos = new Map();
+
+const sorteio = () =>
+ Math.floor(Math.random() * (RANGE.max - RANGE.min + 1)) + RANGE.min;
+
+export default async function ({ msg, api }) {
+ const chatId = api.chat.id;
+
+ // ── !adivinhação ─────────────────────────────────────────
+ if (msg.is(CMD_PREFIX + "adivinhação")) {
+ const sub = msg.args[1];
+
+ if (!sub) {
+ await api.send(
+ `${t("title")}\n\n` +
+ `\`${CMD_PREFIX}adivinhação começar\` — ${t("startCommand")}\n` +
+ `\`${CMD_PREFIX}adivinhação parar\` — ${t("stopCommand")}`
+ );
+ return;
+ }
+
+ if (sub === "começar") {
+ jogosAtivos.set(chatId, sorteio());
+ await api.send(t("started"));
+ api.log.info(t("gameLog.started"));
+ return;
+ }
+
+ if (sub === "parar") {
+ jogosAtivos.delete(chatId);
+ await api.send(t("stopped"));
+ api.log.info(t("gameLog.stopped"));
+ return;
+ }
+
+ await api.send(
+ `${t("invalidCommand", { sub })} \`${CMD_PREFIX}adivinhação começar\` ${t("or")} \`${CMD_PREFIX}adivinhação parar\`.`
+ );
+ return;
+ }
+
+ // ── Guesses during active game ────────────────────────────
+ const numero = jogosAtivos.get(chatId);
+ if (numero === undefined) return;
+
+ const tentativa = msg.body.trim();
+ if (!/^\d+$/.test(tentativa)) return;
+
+ const num = parseInt(tentativa, 10);
+
+ if (num < RANGE.min || num > RANGE.max) {
+ await msg.reply(t("range", { min: RANGE.min, max: RANGE.max }));
+ return;
+ }
+
+ if (num === numero) {
+ await msg.reply(
+ `${t("correct", { number: numero })} \`${CMD_PREFIX}adivinhação começar\` ${t("playAgain")}`
+ );
+ jogosAtivos.delete(chatId);
+ } else {
+ await api.send(num > numero ? t("lower") : t("higher"));
+ }
+}
\ No newline at end of file
diff --git a/adivinhação/locale/en.json b/adivinhação/locale/en.json
new file mode 100644
index 0000000..d8bd2a4
--- /dev/null
+++ b/adivinhação/locale/en.json
@@ -0,0 +1,18 @@
+{
+ "title": "🎲 Guessing Game",
+ "startCommand": "Start a new game",
+ "stopCommand": "Stop the current game",
+ "started": "Game started! Guess a number between 1 and 100.",
+ "stopped": "Game stopped.",
+ "invalidCommand": "Unknown subcommand: {{sub}}. Use",
+ "or": "or",
+ "range": "The number must be between {{min}} and {{max}}.",
+ "correct": "🎉 Correct! The number was {{number}}. Type",
+ "playAgain": "to play again.",
+ "higher": "📈 Higher!",
+ "lower": "📉 Lower!",
+ "gameLog": {
+ "started": "Guessing game started.",
+ "stopped": "Guessing game stopped."
+ }
+}
\ No newline at end of file
diff --git a/adivinhação/locale/es.json b/adivinhação/locale/es.json
new file mode 100644
index 0000000..c92bd24
--- /dev/null
+++ b/adivinhação/locale/es.json
@@ -0,0 +1,18 @@
+{
+ "title": "🎲 Juego de Adivinanza",
+ "startCommand": "Iniciar un nuevo juego",
+ "stopCommand": "Detener el juego actual",
+ "started": "¡Juego iniciado! Adivina un número entre 1 y 100.",
+ "stopped": "Juego detenido.",
+ "invalidCommand": "Subcomando desconocido: {{sub}}. Usa",
+ "or": "o",
+ "range": "El número debe estar entre {{min}} y {{max}}.",
+ "correct": "🎉 ¡Correcto! El número era {{number}}. Escribe",
+ "playAgain": "para jugar de nuevo.",
+ "higher": "📈 ¡Mayor!",
+ "lower": "📉 ¡Menor!",
+ "gameLog": {
+ "started": "Juego de adivinanza iniciado.",
+ "stopped": "Juego de adivinanza detenido."
+ }
+}
\ No newline at end of file
diff --git a/adivinhação/locale/pt.json b/adivinhação/locale/pt.json
new file mode 100644
index 0000000..38c460c
--- /dev/null
+++ b/adivinhação/locale/pt.json
@@ -0,0 +1,18 @@
+{
+ "title": "🎲 Jogo de Adivinhação",
+ "startCommand": "Iniciar um novo jogo",
+ "stopCommand": "Parar o jogo atual",
+ "started": "Jogo iniciado! Adivinhe um número entre 1 e 100.",
+ "stopped": "Jogo parado.",
+ "invalidCommand": "Subcomando desconhecido: {{sub}}. Use",
+ "or": "ou",
+ "range": "O número deve estar entre {{min}} e {{max}}.",
+ "correct": "🎉 Correto! O número era {{number}}. Digite",
+ "playAgain": "para jogar novamente.",
+ "higher": "📈 Maior!",
+ "lower": "📉 Menor!",
+ "gameLog": {
+ "started": "Jogo de adivinhação iniciado.",
+ "stopped": "Jogo de adivinhação parado."
+ }
+}
\ No newline at end of file
diff --git a/adivinhação/manyplug.json b/adivinhação/manyplug.json
new file mode 100644
index 0000000..963f6fa
--- /dev/null
+++ b/adivinhação/manyplug.json
@@ -0,0 +1,7 @@
+{
+ "name": "adivinhacao",
+ "version": "1.0.0",
+ "category": "games",
+ "service": false,
+ "dependencies": {}
+}
\ No newline at end of file
diff --git a/audio/index.js b/audio/index.js
new file mode 100644
index 0000000..ee060cd
--- /dev/null
+++ b/audio/index.js
@@ -0,0 +1,116 @@
+/**
+ * plugins/audio/index.js
+ *
+ * Downloads video via yt-dlp, converts to mp3 via ffmpeg and sends to chat.
+ * All processing (download + conversion + send + cleanup) is here.
+ */
+
+import { spawn } from "child_process";
+import { execFile } from "child_process";
+import { promisify } from "util";
+import fs from "fs";
+import path from "path";
+import os from "os";
+import { enqueue } from "../../download/queue.js";
+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 logStream = fs.createWriteStream("logs/audio-error.log", { flags: "a" });
+const execFileAsync = promisify(execFile);
+
+const DOWNLOADS_DIR = path.resolve("downloads");
+const YT_DLP = os.platform() === "win32" ? ".\\bin\\yt-dlp.exe" : "./bin/yt-dlp";
+const FFMPEG = os.platform() === "win32" ? ".\\bin\\ffmpeg.exe" : "./bin/ffmpeg";
+
+const ARGS_BASE = [
+ "--extractor-args", "youtube:player_client=android",
+ "--print", "after_move:filepath",
+ "--cookies", "cookies.txt",
+ "--add-header", "User-Agent:Mozilla/5.0",
+ "--add-header", "Referer:https://www.youtube.com/",
+ "--retries", "4",
+ "--fragment-retries", "5",
+ "--socket-timeout", "15",
+ "--sleep-interval", "1",
+ "--max-sleep-interval", "4",
+ "--no-playlist",
+ "-f", "bv+ba/best",
+];
+
+function downloadRaw(url, id) {
+ return new Promise((resolve, reject) => {
+ fs.mkdirSync(DOWNLOADS_DIR, { recursive: true });
+
+ const output = path.join(DOWNLOADS_DIR, `${id}.%(ext)s`);
+ const proc = spawn(YT_DLP, [...ARGS_BASE, "--output", output, url]);
+ let stdout = "";
+
+ proc.on("error", err => reject(new Error(
+ err.code === "EACCES" ? t("error.noPermission")
+ : err.code === "ENOENT" ? t("error.notFound")
+ : `${t("error.startError")} ${err.message}`
+ )));
+
+ proc.stdout.on("data", d => { stdout += d.toString(); });
+ proc.stderr.on("data", d => logStream.write(d));
+
+ proc.on("close", code => {
+ if (code !== 0) return reject(new Error(t("error.downloadFailed")));
+
+ const filePath = stdout.trim().split("\n").filter(Boolean).at(-1);
+ if (!filePath || !fs.existsSync(filePath))
+ return reject(new Error(t("error.fileNotFound")));
+
+ resolve(filePath);
+ });
+ });
+}
+
+async function convertToMp3(videoPath, id) {
+ const mp3Path = path.join(DOWNLOADS_DIR, `${id}.mp3`);
+
+ await execFileAsync(FFMPEG, [
+ "-i", videoPath,
+ "-vn", // no video
+ "-ar", "44100", // sample rate
+ "-ac", "2", // stereo
+ "-b:a", "192k", // bitrate
+ "-y", // overwrite if exists
+ mp3Path,
+ ]);
+
+ fs.unlinkSync(videoPath); // remove intermediate video
+ return mp3Path;
+}
+
+export default async function ({ msg, api }) {
+ if (!msg.is(CMD_PREFIX + "audio")) return;
+
+ const url = msg.args[1];
+
+ if (!url) {
+ await msg.reply(`${t("noUrl")} \`${CMD_PREFIX}audio https://youtube.com/...\``);
+ return;
+ }
+
+ await msg.reply(t("downloading"));
+
+ const id = `audio-${Date.now()}`;
+
+ enqueue(
+ async () => {
+ const videoPath = await downloadRaw(url, id);
+ const mp3Path = await convertToMp3(videoPath, id);
+ await api.sendAudio(mp3Path);
+ fs.unlinkSync(mp3Path);
+ emptyFolder(DOWNLOADS_DIR);
+ api.log.info(`${CMD_PREFIX}audio completed → ${url}`);
+ },
+ async () => {
+ await msg.reply(t("error.generic"));
+ }
+ );
+}
\ No newline at end of file
diff --git a/audio/locale/en.json b/audio/locale/en.json
new file mode 100644
index 0000000..c389a3c
--- /dev/null
+++ b/audio/locale/en.json
@@ -0,0 +1,12 @@
+{
+ "noUrl": "Please provide a URL.",
+ "downloading": "Downloading audio...",
+ "error": {
+ "noPermission": "yt-dlp: permission denied",
+ "notFound": "yt-dlp: not found",
+ "startError": "Failed to start download:",
+ "downloadFailed": "Download failed. Check the URL.",
+ "fileNotFound": "Downloaded file not found.",
+ "generic": "Failed to download audio. Try again later."
+ }
+}
\ No newline at end of file
diff --git a/audio/locale/es.json b/audio/locale/es.json
new file mode 100644
index 0000000..cc27c16
--- /dev/null
+++ b/audio/locale/es.json
@@ -0,0 +1,12 @@
+{
+ "noUrl": "Por favor, proporciona una URL.",
+ "downloading": "Descargando audio...",
+ "error": {
+ "noPermission": "yt-dlp: permiso denegado",
+ "notFound": "yt-dlp: no encontrado",
+ "startError": "Error al iniciar descarga:",
+ "downloadFailed": "Descarga fallida. Verifica la URL.",
+ "fileNotFound": "Archivo descargado no encontrado.",
+ "generic": "Error al descargar audio. Intenta de nuevo más tarde."
+ }
+}
\ No newline at end of file
diff --git a/audio/locale/pt.json b/audio/locale/pt.json
new file mode 100644
index 0000000..26998d7
--- /dev/null
+++ b/audio/locale/pt.json
@@ -0,0 +1,12 @@
+{
+ "noUrl": "Por favor, forneça uma URL.",
+ "downloading": "Baixando áudio...",
+ "error": {
+ "noPermission": "yt-dlp: permissão negada",
+ "notFound": "yt-dlp: não encontrado",
+ "startError": "Falha ao iniciar download:",
+ "downloadFailed": "Download falhou. Verifique a URL.",
+ "fileNotFound": "Arquivo baixado não encontrado.",
+ "generic": "Falha ao baixar áudio. Tente novamente mais tarde."
+ }
+}
\ No newline at end of file
diff --git a/audio/manyplug.json b/audio/manyplug.json
new file mode 100644
index 0000000..2abc89a
--- /dev/null
+++ b/audio/manyplug.json
@@ -0,0 +1,7 @@
+{
+ "name": "audio",
+ "version": "1.0.0",
+ "category": "media",
+ "service": false,
+ "dependencies": {}
+}
\ No newline at end of file
diff --git a/figurinha/index.js b/figurinha/index.js
new file mode 100644
index 0000000..31ec887
--- /dev/null
+++ b/figurinha/index.js
@@ -0,0 +1,250 @@
+/**
+ * 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()}`);
+}
\ No newline at end of file
diff --git a/figurinha/locale/en.json b/figurinha/locale/en.json
new file mode 100644
index 0000000..75243f2
--- /dev/null
+++ b/figurinha/locale/en.json
@@ -0,0 +1,25 @@
+{
+ "pack": "ManyBot Stickers",
+ "author": "ManyBot",
+ "help": "Convert images/videos to stickers.",
+ "helpMedia": "Reply to or attach an image/video with",
+ "helpSession": "Or start a multi-media session with",
+ "helpCreate": "to process all at once.",
+ "error": {
+ "tooLarge": "Could not reduce sticker below 900KB.",
+ "generic": "Failed to create sticker. Try another file."
+ },
+ "session": {
+ "noneActive": "No active sticker session.",
+ "stopped": "Sticker session ended.",
+ "noMedia": "No media collected yet.",
+ "generating": "Creating stickers...",
+ "generatingOne": "Creating sticker...",
+ "alreadyOpen": "Session already open.",
+ "waitExpire": "Wait for it to expire or use",
+ "expired": "Session expired.",
+ "expiredEnd": "to start a new one.",
+ "started": "Sticker session started by",
+ "success": "All stickers created!"
+ }
+}
diff --git a/figurinha/locale/es.json b/figurinha/locale/es.json
new file mode 100644
index 0000000..a2d93fa
--- /dev/null
+++ b/figurinha/locale/es.json
@@ -0,0 +1,25 @@
+{
+ "pack": "ManyBot Stickers",
+ "author": "ManyBot",
+ "help": "Convierte imágenes/videos en stickers.",
+ "helpMedia": "Responde o adjunta una imagen/video con",
+ "helpSession": "O inicia una sesión multi-media con",
+ "helpCreate": "para procesar todo de una vez.",
+ "error": {
+ "tooLarge": "No se pudo reducir el sticker debajo de 900KB.",
+ "generic": "Error al crear sticker. Intenta con otro archivo."
+ },
+ "session": {
+ "noneActive": "No hay sesión de sticker activa.",
+ "stopped": "Sesión de sticker finalizada.",
+ "noMedia": "No se ha recolectado ningún medio aún.",
+ "generating": "Creando stickers...",
+ "generatingOne": "Creando sticker...",
+ "alreadyOpen": "Sesión ya abierta.",
+ "waitExpire": "Espera a que expire o usa",
+ "expired": "Sesión expirada.",
+ "expiredEnd": "para iniciar una nueva.",
+ "started": "Sesión de sticker iniciada por",
+ "success": "¡Todos los stickers creados!"
+ }
+}
diff --git a/figurinha/locale/pt.json b/figurinha/locale/pt.json
new file mode 100644
index 0000000..04387b0
--- /dev/null
+++ b/figurinha/locale/pt.json
@@ -0,0 +1,25 @@
+{
+ "pack": "ManyBot Stickers",
+ "author": "ManyBot",
+ "help": "Converte imagens/vídeos em figurinhas.",
+ "helpMedia": "Responda ou anexe uma imagem/vídeo com",
+ "helpSession": "Ou inicie uma sessão multi-mídia com",
+ "helpCreate": "para processar tudo de uma vez.",
+ "error": {
+ "tooLarge": "Não foi possível reduzir figurinha abaixo de 900KB.",
+ "generic": "Falha ao criar figurinha. Tente outro arquivo."
+ },
+ "session": {
+ "noneActive": "Nenhuma sessão de figurinha ativa.",
+ "stopped": "Sessão de figurinha encerrada.",
+ "noMedia": "Nenhuma mídia coletada ainda.",
+ "generating": "Criando figurinhas...",
+ "generatingOne": "Criando figurinha...",
+ "alreadyOpen": "Sessão já aberta.",
+ "waitExpire": "Aguarde expirar ou use",
+ "expired": "Sessão expirada.",
+ "expiredEnd": "para iniciar uma nova.",
+ "started": "Sessão de figurinha iniciada por",
+ "success": "Todas as figurinhas criadas!"
+ }
+}
diff --git a/figurinha/manyplug.json b/figurinha/manyplug.json
new file mode 100644
index 0000000..b04b46c
--- /dev/null
+++ b/figurinha/manyplug.json
@@ -0,0 +1,9 @@
+{
+ "name": "figurinha",
+ "version": "1.0.0",
+ "category": "media",
+ "service": false,
+ "dependencies": {
+ "wa-sticker-formatter": "*"
+ }
+}
\ No newline at end of file
diff --git a/forca/index.js b/forca/index.js
new file mode 100644
index 0000000..b1dee24
--- /dev/null
+++ b/forca/index.js
@@ -0,0 +1,160 @@
+/**
+ * plugins/forca/index.js
+ *
+ * Hangman game plugin with isolated i18n.
+ * Game state is stored internally per chat.
+ */
+
+import { CMD_PREFIX } from "../../config.js";
+import { createPluginI18n } from "../../utils/pluginI18n.js";
+
+const { t } = createPluginI18n(import.meta.url);
+
+// Game states
+const activeGames = new Map(); // chatId -> { word, theme, lives, progress }
+const activeParticipants = new Map(); // chatId -> Set of users who reacted
+export let hangmanActive = false;
+
+
+// Sample words
+const WORDS = [
+ { word: "python", theme: "Programming Language" },
+ { word: "javascript", theme: "Programming Language" },
+ { word: "java", theme: "Programming Language" },
+ { word: "dog", theme: "Animal" },
+ { word: "cat", theme: "Animal" },
+ { word: "elephant", theme: "Animal" },
+ { word: "giraffe", theme: "Animal" },
+ { word: "guitar", theme: "Musical Instrument" },
+ { word: "piano", theme: "Musical Instrument" },
+ { word: "drums", theme: "Musical Instrument" },
+ { word: "violin", theme: "Musical Instrument" },
+ { word: "soccer", theme: "Sport" },
+ { word: "basketball", theme: "Sport" },
+ { word: "swimming", theme: "Sport" },
+ { word: "tennis", theme: "Sport" },
+ { word: "brazil", theme: "Country" },
+ { word: "japan", theme: "Country" },
+ { word: "canada", theme: "Country" },
+ { word: "france", theme: "Country" },
+ { word: "mars", theme: "Planet" },
+ { word: "venus", theme: "Planet" },
+ { word: "jupiter", theme: "Planet" },
+ { word: "saturn", theme: "Planet" },
+ { word: "minecraft", theme: "Game" },
+ { word: "fortnite", theme: "Game" },
+ { word: "roblox", theme: "Game" },
+ { word: "amongus", theme: "Game" },
+ { word: "rose", theme: "Flower" },
+ { word: "sunflower", theme: "Flower" },
+ { word: "tulip", theme: "Flower" },
+ { word: "orchid", theme: "Flower" },
+ { word: "scissors", theme: "Object" },
+ { word: "notebook", theme: "Object" },
+ { word: "computer", theme: "Object" },
+ { word: "phone", theme: "Object" },
+ { word: "moon", theme: "Celestial Body" },
+ { word: "sun", theme: "Celestial Body" },
+ { word: "star", theme: "Celestial Body" },
+ { word: "comet", theme: "Celestial Body" },
+ { word: "ocean", theme: "Nature" },
+ { word: "mountain", theme: "Nature" },
+];
+
+// Generate word with underscores
+const generateProgress = word =>
+ word.replace(/[a-zA-Z]/g, "_");
+
+export default async function ({ msg, api }) {
+ const chatId = api.chat.id;
+ const sub = msg.args[1];
+
+ // ── Main game command
+ if (msg.is(CMD_PREFIX + "forca")) {
+ if (!sub) {
+ await api.send(
+ `${t("title")}\n\n` +
+ `\`${CMD_PREFIX}forca start\` — ${t("startCommand")}\n` +
+ `\`${CMD_PREFIX}forca stop\` — ${t("stopCommand")}`
+ );
+ return;
+ }
+
+ if (sub === "start") {
+ hangmanActive = true;
+ // Get random word
+ const random = WORDS[Math.floor(Math.random() * WORDS.length)];
+
+ // Initialize game
+ activeGames.set(chatId, {
+ word: random.word.toLowerCase(),
+ theme: random.theme,
+ lives: 6,
+ progress: generateProgress(random.word)
+ });
+
+ activeParticipants.set(chatId, new Set()); // reset participants
+
+ await api.send(
+ t("started", {
+ theme: random.theme,
+ word: generateProgress(random.word),
+ lives: 6
+ })
+ );
+ return;
+ }
+
+ if (sub === "stop") {
+ activeGames.delete(chatId);
+ activeParticipants.delete(chatId);
+ await api.send(t("stopped"));
+ return;
+ }
+
+ await api.send(
+ `${t("invalidCommand", { sub })} \`${CMD_PREFIX}forca start\` ${t("or")} \`${CMD_PREFIX}forca stop\`.`
+ );
+ return;
+ }
+
+ // ── Game attempts
+ const game = activeGames.get(chatId);
+ if (!game) return; // No active game
+
+ const attempt = msg.body.trim().toLowerCase();
+ if (!/^[a-z]$/.test(attempt)) return; // single letters only
+
+ // Check if letter is in word
+ let hit = false;
+ let newProgress = game.progress.split("");
+ for (let i = 0; i < game.word.length; i++) {
+ if (game.word[i] === attempt) {
+ newProgress[i] = attempt;
+ hit = true;
+ }
+ }
+ game.progress = newProgress.join("");
+
+ if (!hit) game.lives--;
+
+ // Feedback for group
+ if (game.progress === game.word) {
+ await msg.reply(t("won", { word: game.word }));
+ activeGames.delete(chatId);
+ activeParticipants.delete(chatId);
+ return;
+ }
+
+ if (game.lives <= 0) {
+ await msg.reply(t("lost", { word: game.word }));
+ activeGames.delete(chatId);
+ activeParticipants.delete(chatId);
+ return;
+ }
+
+ await msg.reply(
+ `${t("status", { word: game.progress, lives: game.lives })}\n` +
+ (hit ? t("correct") : t("wrong"))
+ );
+}
diff --git a/forca/locale/en.json b/forca/locale/en.json
new file mode 100644
index 0000000..e968d50
--- /dev/null
+++ b/forca/locale/en.json
@@ -0,0 +1,14 @@
+{
+ "title": "Hangman Game",
+ "startCommand": "starts the game",
+ "stopCommand": "stops the game",
+ "started": "Hangman game started!\n\nTheme: *{{theme}}*\nWord: `{{word}}`\nLives: {{lives}}\n\nType a letter to guess!",
+ "stopped": "Hangman game ended.",
+ "invalidCommand": "Subcommand *{{sub}}* does not exist.\nUse",
+ "or": "or",
+ "won": "Congratulations! Complete word: `{{word}}`",
+ "lost": "Game over! Word was: `{{word}}`",
+ "status": "Word: `{{word}}`\nLives: {{lives}}",
+ "correct": "Got the letter!",
+ "wrong": "Wrong letter!"
+}
diff --git a/forca/locale/es.json b/forca/locale/es.json
new file mode 100644
index 0000000..0a484f6
--- /dev/null
+++ b/forca/locale/es.json
@@ -0,0 +1,14 @@
+{
+ "title": "Juego del Ahorcado",
+ "startCommand": "inicia el juego",
+ "stopCommand": "detiene el juego",
+ "started": "¡Juego del Ahorcado iniciado!\n\nTema: *{{theme}}*\nPalabra: `{{word}}`\nVidas: {{lives}}\n\nEscribe una letra para adivinar!",
+ "stopped": "Juego del Ahorcado terminado.",
+ "invalidCommand": "El subcomando *{{sub}}* no existe.\nUsa",
+ "or": "o",
+ "won": "¡Felicitaciones! Palabra completa: `{{word}}`",
+ "lost": "¡Fin del juego! La palabra era: `{{word}}`",
+ "status": "Palabra: `{{word}}`\nVidas: {{lives}}",
+ "correct": "¡Letra correcta!",
+ "wrong": "¡Letra incorrecta!"
+}
diff --git a/forca/locale/pt.json b/forca/locale/pt.json
new file mode 100644
index 0000000..b11b0fb
--- /dev/null
+++ b/forca/locale/pt.json
@@ -0,0 +1,14 @@
+{
+ "title": "Jogo da Forca",
+ "startCommand": "inicia o jogo",
+ "stopCommand": "encerra o jogo",
+ "started": "Jogo da Forca iniciado!\n\nTema: *{{theme}}*\nPalavra: `{{word}}`\nVidas: {{lives}}\n\nDigite uma letra para adivinhar!",
+ "stopped": "Jogo da Forca encerrado.",
+ "invalidCommand": "Subcomando *{{sub}}* não existe.\nUse",
+ "or": "ou",
+ "won": "Parabéns! Palavra completa: `{{word}}`",
+ "lost": "Fim de jogo! Palavra era: `{{word}}`",
+ "status": "Palavra: `{{word}}`\nVidas: {{lives}}",
+ "correct": "Acertou a letra!",
+ "wrong": "Errou a letra!"
+}
diff --git a/forca/manyplug.json b/forca/manyplug.json
new file mode 100644
index 0000000..5d56beb
--- /dev/null
+++ b/forca/manyplug.json
@@ -0,0 +1,7 @@
+{
+ "name": "forca",
+ "version": "1.0.0",
+ "category": "games",
+ "service": false,
+ "dependencies": {}
+}
\ No newline at end of file
diff --git a/manager.js b/manager.js
new file mode 100644
index 0000000..540de1b
--- /dev/null
+++ b/manager.js
@@ -0,0 +1,74 @@
+import fs from 'fs-extra';
+import path from 'path';
+import chalk from 'chalk';
+
+let regPath = path.resolve('registry.json');
+
+const entries = await fs.readdir(".", { withFileTypes: true }); // all files and directories of plugins dir
+
+let update = [];
+let added = [];
+
+// Load existing registry or create new one
+let registry;
+if (fs.existsSync(regPath)) {
+ try {
+ registry = JSON.parse(fs.readFileSync(regPath, "utf-8"));
+ } catch (err) {
+ registry = {
+ lastUpdated: new Date().toISOString(),
+ plugins: {}
+ };
+ }
+} else {
+ registry = {
+ lastUpdated: new Date().toISOString(),
+ plugins: {}
+ };
+}
+
+
+for (const entry of entries) {
+ if (!entry.isDirectory()) continue; // if it is not a directory, skip and continue the next loop
+
+ const manifestPath = path.join(".", entry.name, 'manyplug.json');
+ if (!await fs.pathExists(manifestPath)) continue;
+
+ try {
+ const manifest = await fs.readJson(manifestPath); // manifest = data from manyplug.json
+ const pluginName = manifest.name || entry.name;
+
+ // Check if plugin exists in registry and version changed
+ const existing = registry.plugins[pluginName]; // existing = plugin in registry.json
+ if (!existing) {
+ added.push({
+ name: pluginName,
+ version: manifest.version
+ });
+ registry.plugins[pluginName] = manifest;
+ } else if (existing.version !== manifest.version) {
+ const oldVersion = existing.version;
+ existing.version = manifest.version;
+
+ update.push({
+ name: pluginName,
+ oldVersion: oldVersion,
+ newVersion: existing.version
+ });
+ }
+ } catch (err) {
+ console.warn(chalk.yellow(`⚠️ Failed to read ${entry.name}: ${err.message}`));
+ }
+}
+
+// Update timestamp
+registry.lastUpdated = new Date().toISOString();
+
+await fs.writeJson(regPath, registry, { spaces: 2 });
+
+console.log(chalk.green(`Registry synced\n`));
+console.log(chalk.blue(` New plugins registred (${added.length}):`));
+console.log(chalk.blue(added.map(a => ` + ${a.name} (${a.version})`).join('\n')));
+
+console.log(chalk.yellow(` Plugins updated (${update.length}):`));
+console.log(chalk.yellow(update.map(u => ` * ${u.name} (${u.oldVersion}) -> (${u.newVersion})`).join('\n')));
diff --git a/many/index.js b/many/index.js
new file mode 100644
index 0000000..c02bc65
--- /dev/null
+++ b/many/index.js
@@ -0,0 +1,17 @@
+import { CMD_PREFIX } from "../../config.js";
+import { createPluginI18n } from "../../utils/pluginI18n.js";
+
+const { t } = createPluginI18n(import.meta.url);
+
+export default async function ({ msg, api }) {
+ if (!msg.is(CMD_PREFIX + "many")) return;
+
+ await api.send(
+ `${t("title")}\n\n` +
+ `🎬 \`${CMD_PREFIX}video \` — ${t("video")}\n` +
+ `🎵 \`${CMD_PREFIX}audio \` — ${t("audio")}\n` +
+ `🖼️ \`${CMD_PREFIX}figurinha\` — ${t("sticker")}\n` +
+ `🎮 \`${CMD_PREFIX}adivinhação começar|parar\` — ${t("guess")}\n` +
+ `🎮 \`${CMD_PREFIX}forca começar|parar\` — ${t("hangman")}\n`
+ );
+}
\ No newline at end of file
diff --git a/many/locale/en.json b/many/locale/en.json
new file mode 100644
index 0000000..ecc8dc6
--- /dev/null
+++ b/many/locale/en.json
@@ -0,0 +1,8 @@
+{
+ "title": "🤖 ManyBot — Available Commands",
+ "video": "Download video from URL",
+ "audio": "Download audio (mp3) from URL",
+ "sticker": "Convert image/video to sticker",
+ "guess": "Guessing game (number 1-100)",
+ "hangman": "Hangman game"
+}
\ No newline at end of file
diff --git a/many/locale/es.json b/many/locale/es.json
new file mode 100644
index 0000000..f4ba52e
--- /dev/null
+++ b/many/locale/es.json
@@ -0,0 +1,8 @@
+{
+ "title": "🤖 ManyBot — Comandos disponibles",
+ "video": "Descarga video de una URL",
+ "audio": "Descarga audio (mp3) de una URL",
+ "sticker": "Convierte imagen/video en sticker",
+ "guess": "Juego de adivinanza (número 1-100)",
+ "hangman": "Juego del ahorcado"
+}
\ No newline at end of file
diff --git a/many/locale/pt.json b/many/locale/pt.json
new file mode 100644
index 0000000..2826195
--- /dev/null
+++ b/many/locale/pt.json
@@ -0,0 +1,8 @@
+{
+ "title": "🤖 ManyBot — Comandos disponíveis",
+ "video": "Baixa vídeo de uma URL",
+ "audio": "Baixa áudio (mp3) de uma URL",
+ "sticker": "Converte imagem/vídeo em figurinha",
+ "guess": "Jogo de adivinhação (número 1-100)",
+ "hangman": "Jogo da forca"
+}
\ No newline at end of file
diff --git a/many/manyplug.json b/many/manyplug.json
new file mode 100644
index 0000000..4e2f2bd
--- /dev/null
+++ b/many/manyplug.json
@@ -0,0 +1,7 @@
+{
+ "name": "many",
+ "version": "1.0.0",
+ "category": "utility",
+ "service": false,
+ "dependencies": {}
+}
\ No newline at end of file
diff --git a/obrigado/index.js b/obrigado/index.js
new file mode 100644
index 0000000..6b3d30c
--- /dev/null
+++ b/obrigado/index.js
@@ -0,0 +1,12 @@
+import { CMD_PREFIX } from "../../config.js";
+import { createPluginI18n } from "../../utils/pluginI18n.js";
+
+const { t } = createPluginI18n(import.meta.url);
+
+const triggers = ["obrigado", "valeu", "brigado", "obrigada", "thx", "thanks"];
+
+export default async function ({ msg }) {
+ if (!triggers.some(g => msg.is(CMD_PREFIX + g))) return;
+
+ await msg.reply(t("reply"));
+}
\ No newline at end of file
diff --git a/obrigado/locale/en.json b/obrigado/locale/en.json
new file mode 100644
index 0000000..e6ff036
--- /dev/null
+++ b/obrigado/locale/en.json
@@ -0,0 +1,3 @@
+{
+ "reply": "You're welcome! 🤗"
+}
diff --git a/obrigado/locale/es.json b/obrigado/locale/es.json
new file mode 100644
index 0000000..5ecd4c0
--- /dev/null
+++ b/obrigado/locale/es.json
@@ -0,0 +1,3 @@
+{
+ "reply": "¡De nada! 🤗"
+}
diff --git a/obrigado/locale/pt.json b/obrigado/locale/pt.json
new file mode 100644
index 0000000..cee6221
--- /dev/null
+++ b/obrigado/locale/pt.json
@@ -0,0 +1,3 @@
+{
+ "reply": "Por nada! Disponha! 🤗"
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5c854c3
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,105 @@
+{
+ "name": "manyplug-repo",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "manyplug-repo",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "chalk": "^5.6.2",
+ "fs-extra": "^11.3.4",
+ "path": "^0.12.7"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
+ "license": "ISC"
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/path": {
+ "version": "0.12.7",
+ "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+ "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "process": "^0.11.1",
+ "util": "^0.10.3"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "2.0.3"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..72542c8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "manyplug-repo",
+ "version": "1.0.0",
+ "description": "This is a mirror of Freakkdev ManyBot's plugin repository (ManyPlug).",
+ "license": "ISC",
+ "author": "",
+ "type": "module",
+ "main": "manager.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "dependencies": {
+ "chalk": "^5.6.2",
+ "fs-extra": "^11.3.4",
+ "path": "^0.12.7"
+ }
+}
diff --git a/registry.json b/registry.json
new file mode 100644
index 0000000..b576110
--- /dev/null
+++ b/registry.json
@@ -0,0 +1,64 @@
+{
+ "lastUpdated": "2026-04-15T00:07:30.616Z",
+ "plugins": {
+ "a": {
+ "name": "a",
+ "author": "freakk.dev",
+ "version": "1.0.0",
+ "category": "humor",
+ "service": false,
+ "dependencies": {}
+ },
+ "adivinhacao": {
+ "name": "adivinhacao",
+ "version": "1.0.0",
+ "category": "games",
+ "service": false,
+ "dependencies": {}
+ },
+ "audio": {
+ "name": "audio",
+ "version": "1.0.0",
+ "category": "media",
+ "service": false,
+ "dependencies": {}
+ },
+ "figurinha": {
+ "name": "figurinha",
+ "version": "1.0.0",
+ "category": "media",
+ "service": false,
+ "dependencies": {
+ "wa-sticker-formatter": "*"
+ }
+ },
+ "forca": {
+ "name": "forca",
+ "version": "1.0.0",
+ "category": "games",
+ "service": false,
+ "dependencies": {}
+ },
+ "many": {
+ "name": "many",
+ "version": "1.0.0",
+ "category": "utility",
+ "service": false,
+ "dependencies": {}
+ },
+ "video": {
+ "name": "video",
+ "version": "1.0.0",
+ "category": "media",
+ "service": false,
+ "dependencies": {}
+ },
+ "xp": {
+ "name": "xp",
+ "version": "0.1.0",
+ "category": "social",
+ "service": true,
+ "dependencies": {}
+ }
+ }
+}
diff --git a/video/index.js b/video/index.js
new file mode 100644
index 0000000..b06777b
--- /dev/null
+++ b/video/index.js
@@ -0,0 +1,109 @@
+/**
+ * plugins/video/index.js
+ *
+ * Downloads video via yt-dlp and sends to chat.
+ * All processing (download + send + cleanup) is here.
+ */
+
+import { spawn } from "child_process";
+import fs from "fs";
+import path from "path";
+import os from "os";
+import { enqueue } from "../../download/queue.js";
+import { CMD_PREFIX } from "../../config.js";
+import { createPluginI18n } from "../../utils/pluginI18n.js";
+
+const { t } = createPluginI18n(import.meta.url);
+
+fs.mkdirSync("logs", { recursive: true });
+const logStream = fs.createWriteStream("logs/video-error.log", { flags: "a" });
+logStream.on("error", err => console.error("[logStream]", err));
+
+const DOWNLOADS_DIR = path.resolve("downloads");
+const YT_DLP = os.platform() === "win32" ? ".\\bin\\yt-dlp.exe" : "./bin/yt-dlp";
+
+const ARGS_BASE = [
+ "--extractor-args", "youtube:player_client=android",
+ "--print", "after_move:filepath",
+ "--cookies", "cookies.txt",
+ "--add-header", "User-Agent:Mozilla/5.0",
+ "--add-header", "Referer:https://www.youtube.com/",
+ "--retries", "4",
+ "--fragment-retries", "5",
+ "--socket-timeout", "15",
+ "--sleep-interval", "1",
+ "--max-sleep-interval", "4",
+ "--no-playlist",
+ "-f", "bv+ba/best",
+];
+
+function downloadVideo(url, id) {
+ return new Promise((resolve, reject) => {
+ // Isolated folder just for this download
+ const tmpDir = path.join(DOWNLOADS_DIR, id);
+ fs.mkdirSync(tmpDir, { recursive: true });
+
+ const output = path.join(tmpDir, "%(title).80s.%(ext)s");
+ const proc = spawn(YT_DLP, [...ARGS_BASE, "--output", output, url]);
+ let stdout = "";
+
+ proc.on("error", err => reject(new Error(
+ err.code === "EACCES" ? t("error.noPermission")
+ : err.code === "ENOENT" ? t("error.notFound")
+ : `${t("error.startError")} ${err.message}`
+ )));
+
+ proc.stdout.on("data", d => { stdout += d.toString(); });
+ proc.stderr.on("data", d => logStream.write(d));
+
+ proc.on("close", code => {
+ if (code !== 0) {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ return reject(new Error(t("error.downloadFailed")));
+ }
+
+ // Try stdout path first
+ let filePath = stdout.trim().split("\n").filter(Boolean).at(-1);
+
+ // Fallback: get the single file inside the isolated folder
+ if (!filePath || !fs.existsSync(filePath)) {
+ const files = fs.readdirSync(tmpDir).filter(f => !f.endsWith(".part"));
+ filePath = files.length === 1 ? path.join(tmpDir, files[0]) : null;
+ }
+
+ if (!filePath) {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ return reject(new Error(t("error.fileNotFound")));
+ }
+
+ resolve({ filePath, tmpDir });
+ });
+ });
+}
+
+export default async function ({ msg, api }) {
+ if (!msg.is(CMD_PREFIX + "video")) return;
+
+ const url = msg.args[1];
+
+ if (!url) {
+ await msg.reply(`${t("noUrl")} \`${CMD_PREFIX}video https://youtube.com/...\``);
+ return;
+ }
+
+ await msg.reply(t("downloading"));
+
+ const id = `video-${Date.now()}`;
+
+ enqueue(
+ async () => {
+ const { filePath, tmpDir } = await downloadVideo(url, id);
+ await api.sendVideo(filePath);
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ api.log.info(`${CMD_PREFIX}video completed → ${url}`);
+ },
+ async () => {
+ await msg.reply(t("error.generic"));
+ }
+ );
+}
\ No newline at end of file
diff --git a/video/locale/en.json b/video/locale/en.json
new file mode 100644
index 0000000..07aa7ed
--- /dev/null
+++ b/video/locale/en.json
@@ -0,0 +1,12 @@
+{
+ "noUrl": "Please provide a URL.",
+ "downloading": "Downloading video...",
+ "error": {
+ "noPermission": "yt-dlp: permission denied",
+ "notFound": "yt-dlp: not found",
+ "startError": "Failed to start download:",
+ "downloadFailed": "Download failed. Check the URL.",
+ "fileNotFound": "Downloaded file not found.",
+ "generic": "Failed to download video. Try again later."
+ }
+}
diff --git a/video/locale/es.json b/video/locale/es.json
new file mode 100644
index 0000000..0fe3140
--- /dev/null
+++ b/video/locale/es.json
@@ -0,0 +1,12 @@
+{
+ "noUrl": "Por favor, proporciona una URL.",
+ "downloading": "Descargando video...",
+ "error": {
+ "noPermission": "yt-dlp: permiso denegado",
+ "notFound": "yt-dlp: no encontrado",
+ "startError": "Error al iniciar descarga:",
+ "downloadFailed": "Descarga fallida. Verifica la URL.",
+ "fileNotFound": "Archivo descargado no encontrado.",
+ "generic": "Error al descargar video. Intenta de nuevo más tarde."
+ }
+}
diff --git a/video/locale/pt.json b/video/locale/pt.json
new file mode 100644
index 0000000..66b4ce2
--- /dev/null
+++ b/video/locale/pt.json
@@ -0,0 +1,12 @@
+{
+ "noUrl": "Por favor, forneça uma URL.",
+ "downloading": "Baixando vídeo...",
+ "error": {
+ "noPermission": "yt-dlp: permissão negada",
+ "notFound": "yt-dlp: não encontrado",
+ "startError": "Falha ao iniciar download:",
+ "downloadFailed": "Download falhou. Verifique a URL.",
+ "fileNotFound": "Arquivo baixado não encontrado.",
+ "generic": "Falha ao baixar vídeo. Tente novamente mais tarde."
+ }
+}
diff --git a/video/manyplug.json b/video/manyplug.json
new file mode 100644
index 0000000..7bfd775
--- /dev/null
+++ b/video/manyplug.json
@@ -0,0 +1,7 @@
+{
+ "name": "video",
+ "version": "1.0.0",
+ "category": "media",
+ "service": false,
+ "dependencies": {}
+}
\ No newline at end of file
diff --git a/xp/index.js b/xp/index.js
new file mode 100644
index 0000000..4d398cd
--- /dev/null
+++ b/xp/index.js
@@ -0,0 +1,21 @@
+/**
+ * Ideia:
+ *
+ * Quando esse plugin for chamado, vai salvar o id de quem mandou no banco de dados.
+ * Quando esse id mandar mensagem de novo, o plugin vai "se lembrar" dessa pessoa e contar xp com:
+ *
+ * - Número de mensagens a cada 30s > conta 1 ponto cada mensagem sendo de texto ou de audio. Durante o intervalo de 30s ele não conta nada.
+ * - Tipo da mensagem:
+ * - Texto/Audio: multiplicar por 1
+ * - Vídeo/Foto: multiplcar por 2
+ *
+ * - Aculma karma dependendo da mensagem. Se suas mensagens conterem palavrões frequentes ou muito spam (ex. 5 mensagens/s), seu karma diminui:
+ * - Karma negativo (abaixo de 0): divide pontos de xp por 2
+ * - Karma baixo (10-20): multplica pontos por 1
+ * - Karma médio (30-40): multiplica pontos por 2
+ * - Karma alto (50-80): multiplca pontos por 3
+ *
+ * No final de cada mês, esse plugin organiza uma lista com o ranking dos top 10 com maiores XP do mês e manda em ID (chat).
+ *
+ * Esse plugin é a base para fazer um sistema de economia daqui um tempo.
+*/
\ No newline at end of file
diff --git a/xp/manyplug.json b/xp/manyplug.json
new file mode 100644
index 0000000..0eca3d7
--- /dev/null
+++ b/xp/manyplug.json
@@ -0,0 +1,7 @@
+{
+ "name": "xp",
+ "version": "0.1.0",
+ "category": "social",
+ "service": true,
+ "dependencies": {}
+}
\ No newline at end of file