first commit

This commit is contained in:
synt-xerror
2026-04-14 21:15:05 -03:00
commit eecef0be88
44 changed files with 1372 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

5
README.md Normal file
View File

@@ -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`.

9
a/index.js Normal file
View File

@@ -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!");
}

8
a/manyplug.json Normal file
View File

@@ -0,0 +1,8 @@
{
"name": "a",
"author": "freakk.dev",
"version": "1.0.0",
"category": "humor",
"service": false,
"dependencies": {}
}

77
adivinhação/index.js Normal file
View File

@@ -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"));
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "adivinhacao",
"version": "1.0.0",
"category": "games",
"service": false,
"dependencies": {}
}

116
audio/index.js Normal file
View File

@@ -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"));
}
);
}

12
audio/locale/en.json Normal file
View File

@@ -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."
}
}

12
audio/locale/es.json Normal file
View File

@@ -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."
}
}

12
audio/locale/pt.json Normal file
View File

@@ -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."
}
}

7
audio/manyplug.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "audio",
"version": "1.0.0",
"category": "media",
"service": false,
"dependencies": {}
}

250
figurinha/index.js Normal file
View File

@@ -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()}`);
}

25
figurinha/locale/en.json Normal file
View File

@@ -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!"
}
}

25
figurinha/locale/es.json Normal file
View File

@@ -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!"
}
}

25
figurinha/locale/pt.json Normal file
View File

@@ -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!"
}
}

9
figurinha/manyplug.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "figurinha",
"version": "1.0.0",
"category": "media",
"service": false,
"dependencies": {
"wa-sticker-formatter": "*"
}
}

160
forca/index.js Normal file
View File

@@ -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"))
);
}

14
forca/locale/en.json Normal file
View File

@@ -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!"
}

14
forca/locale/es.json Normal file
View File

@@ -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!"
}

14
forca/locale/pt.json Normal file
View File

@@ -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!"
}

7
forca/manyplug.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "forca",
"version": "1.0.0",
"category": "games",
"service": false,
"dependencies": {}
}

74
manager.js Normal file
View File

@@ -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')));

17
many/index.js Normal file
View File

@@ -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 <link>\`${t("video")}\n` +
`🎵 \`${CMD_PREFIX}audio <link>\`${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`
);
}

8
many/locale/en.json Normal file
View File

@@ -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"
}

8
many/locale/es.json Normal file
View File

@@ -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"
}

8
many/locale/pt.json Normal file
View File

@@ -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"
}

7
many/manyplug.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "many",
"version": "1.0.0",
"category": "utility",
"service": false,
"dependencies": {}
}

12
obrigado/index.js Normal file
View File

@@ -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"));
}

3
obrigado/locale/en.json Normal file
View File

@@ -0,0 +1,3 @@
{
"reply": "You're welcome! 🤗"
}

3
obrigado/locale/es.json Normal file
View File

@@ -0,0 +1,3 @@
{
"reply": "¡De nada! 🤗"
}

3
obrigado/locale/pt.json Normal file
View File

@@ -0,0 +1,3 @@
{
"reply": "Por nada! Disponha! 🤗"
}

105
package-lock.json generated Normal file
View File

@@ -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"
}
}
}
}

17
package.json Normal file
View File

@@ -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"
}
}

64
registry.json Normal file
View File

@@ -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": {}
}
}
}

109
video/index.js Normal file
View File

@@ -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"));
}
);
}

12
video/locale/en.json Normal file
View File

@@ -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."
}
}

12
video/locale/es.json Normal file
View File

@@ -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."
}
}

12
video/locale/pt.json Normal file
View File

@@ -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."
}
}

7
video/manyplug.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "video",
"version": "1.0.0",
"category": "media",
"service": false,
"dependencies": {}
}

21
xp/index.js Normal file
View File

@@ -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.
*/

7
xp/manyplug.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "xp",
"version": "0.1.0",
"category": "social",
"service": true,
"dependencies": {}
}