commit eecef0be8856cd38c5f77b6832dd9898e52f0540 Author: synt-xerror <169557594+synt-xerror@users.noreply.github.com> Date: Tue Apr 14 21:15:05 2026 -0300 first commit 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