├── .prettierignore ├── lib ├── Cobbler-SemiBold.ttf ├── shell │ ├── logger.sh │ ├── nodejs.sh │ └── system.sh ├── api │ ├── ytmp3.js │ ├── spotifydl.js │ ├── ytmp4.js │ ├── twitter.js │ ├── tiktok.js │ ├── spotify.js │ ├── play.js │ ├── removebg.js │ ├── threads.js │ ├── remini.js │ └── instagram.js ├── cpp │ ├── core │ │ ├── converter_core.h │ │ └── sticker_core.h │ ├── bindings │ │ ├── converter.cpp │ │ └── converter_async.cpp │ └── workers │ │ ├── sticker-worker.js │ │ └── converter-worker.js ├── console.js └── core │ └── smsg.js ├── plugins ├── info │ ├── info-ping.js │ ├── info-totalfitur.js │ ├── info-listblock.js │ ├── info-owner.js │ ├── info-os.js │ └── info-sc.js ├── group │ ├── group-revoke.js │ ├── group-unpin.js │ ├── group-delete.js │ ├── group-settings.js │ ├── group-link.js │ ├── group-polling.js │ ├── group-ephemeral.js │ ├── group-modebot.js │ ├── group-kick.js │ ├── group-pin.js │ ├── group-demote.js │ ├── group-promote.js │ ├── group-hidetag.js │ ├── group-add.js │ └── group-info.js ├── owner │ ├── owner-leavegc.js │ ├── owner-setnamebot.js │ ├── owner-setbiobot.js │ ├── owner-setppbot.js │ ├── owner-df.js │ ├── owner-eval.js │ ├── owner-gf.js │ ├── owner-block.js │ ├── owner-unblock.js │ ├── owner-upch.js │ ├── owner-status.js │ ├── owner-sf.js │ ├── owner-groupstatus.js │ └── owner-exec.js ├── _ │ ├── _antistatus.js │ ├── _antiaudio.js │ ├── _antifoto.js │ ├── _antifile.js │ ├── _antivideo.js │ └── _antisticker.js ├── tool │ ├── tool-toptv.js │ ├── tool-ssweb.js │ ├── tool-cekip.js │ ├── tool-tomp3.js │ ├── tool-cekresolution.js │ ├── tool-removebg.js │ ├── tool-sendviewonce.js │ ├── tool-getlid.js │ ├── tool-readviewonce.js │ ├── tool-toptt.js │ ├── tool-remini.js │ ├── tool-cekid.js │ ├── tool-resize.js │ ├── tool-getq.js │ └── tool-setting.js ├── maker │ ├── maker-toimg.js │ ├── maker-brat.js │ ├── maker-smeme.js │ ├── maker-wm.js │ └── maker-sticker.js ├── ai │ ├── ai-copilot.js │ └── ai-feloai.js ├── downloader │ ├── downloader-spotifydl.js │ ├── downloader-gitclone.js │ ├── downloader-ytvideo.js │ ├── downloader-ytaudio.js │ ├── downloader-twitter.js │ ├── downloader-threads.js │ ├── downloader-tiktok.js │ ├── downloader-ig.js │ ├── downloader-ytplay.js │ └── downloader-spotify.js └── internet │ ├── internet-npm.js │ ├── internet-tiktok.js │ └── internet-yts.js ├── .env.example ├── eslint.config.js ├── CONTRIBUTORS.md ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── .commitlintrc.json ├── dependabot.yml ├── labeler.yml ├── issue-labeler.yml ├── SECURITY.md ├── workflows │ ├── 03-build-test.yml │ ├── 02-security-scan.yml │ ├── 04-release.yml │ └── 01b-cpp-quality.yml ├── ISSUE_TEMPLATE │ └── feature_request.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── package.json ├── .clang-tidy └── .clang-format /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .github/ 3 | *.log 4 | *.tmp -------------------------------------------------------------------------------- /lib/Cobbler-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naruyaizumi/liora/HEAD/lib/Cobbler-SemiBold.ttf -------------------------------------------------------------------------------- /plugins/info/info-ping.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | await conn.sendMessage(m.chat, { text: "PUNG 🏓" }); 3 | }; 4 | 5 | handler.help = ["ping"]; 6 | handler.tags = ["info"]; 7 | handler.command = /^(ping)$/i; 8 | 9 | export default handler; 10 | -------------------------------------------------------------------------------- /plugins/group/group-revoke.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | try { 3 | await conn.groupRevokeInvite(m.chat); 4 | m.reply("Group invite link has been successfully reset."); 5 | } catch (e) { 6 | conn.logger.error(e); 7 | m.reply(`Error: ${e.message}`); 8 | } 9 | }; 10 | 11 | handler.help = ["revoke"]; 12 | handler.tags = ["group"]; 13 | handler.command = /^(revoke)$/i; 14 | handler.group = true; 15 | handler.botAdmin = true; 16 | handler.admin = true; 17 | 18 | export default handler; 19 | -------------------------------------------------------------------------------- /plugins/owner/owner-leavegc.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text }) => { 2 | const group = text || m.chat; 3 | 4 | try { 5 | await conn.sendMessage(group, { 6 | text: "This bot is leaving the group.", 7 | }); 8 | await conn.groupLeave(group); 9 | } catch (e) { 10 | conn.logger.error(e); 11 | m.reply(`Error: ${e.message}`); 12 | } 13 | }; 14 | 15 | handler.help = ["leavegc"]; 16 | handler.tags = ["owner"]; 17 | handler.command = /^(out|leavegc)$/i; 18 | handler.owner = true; 19 | 20 | export default handler; 21 | -------------------------------------------------------------------------------- /lib/shell/logger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | print_error() { 4 | echo "[ERROR] $1" >&2 5 | } 6 | 7 | print_success() { 8 | echo "[SUCCESS] $1" 9 | } 10 | 11 | print_info() { 12 | echo "[INFO] $1" 13 | } 14 | 15 | print_warning() { 16 | echo "[WARNING] $1" 17 | } 18 | 19 | print_header() { 20 | echo "" 21 | echo "================================================================" 22 | echo " $1" 23 | echo "================================================================" 24 | echo "" 25 | } 26 | 27 | print_separator() { 28 | echo "================================================================" 29 | } 30 | 31 | print_subsection() { 32 | echo "" 33 | echo "--- $1 ---" 34 | echo "" 35 | } -------------------------------------------------------------------------------- /plugins/owner/owner-setnamebot.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text, command, usedPrefix }) => { 2 | if (!text) { 3 | return m.reply(`Enter the new bot name.\nExample: ${usedPrefix + command} Liora`); 4 | } 5 | 6 | try { 7 | await conn.updateProfileName(text); 8 | 9 | const response = ` 10 | New Name: ${text} 11 | WhatsApp bot name updated successfully. 12 | `.trim(); 13 | 14 | m.reply(response); 15 | } catch (e) { 16 | conn.logger.error(e); 17 | m.reply(`Error: ${e.message}`); 18 | } 19 | }; 20 | 21 | handler.help = ["setnamebot"]; 22 | handler.tags = ["owner"]; 23 | handler.command = /^set(name(bot)?)$/i; 24 | handler.owner = true; 25 | 26 | export default handler; 27 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ============================================ 2 | # LIORA BOT CONFIG 3 | # ============================================ 4 | # Format: [local_identifier] 5 | # local_identifier: User's native LID, NOT phone number 6 | 7 | # Notes: 8 | # 1. Always use native LID from WhatsApp/WhiskeySocket to ensure consistency. 9 | # 2. Do NOT use phone numbers, as JIDs can vary across environments. 10 | OWNERS=["113748182302861","227551947555018"] 11 | 12 | # WhatsApp pairing code 13 | PAIRING_NUMBER= 14 | 15 | # WhatsApp group invite link for bot operations 16 | GROUP_LINK=https://chat.whatsapp.com 17 | 18 | # Sticker metadata (optional - leave empty for defaults) 19 | WATERMARK=Liora 20 | AUTHOR=Naruya Izumi 21 | STICKPACK=Liora Stickers 22 | STICKAUTH=Naruya Izumi -------------------------------------------------------------------------------- /plugins/info/info-totalfitur.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | const plugins = Object.values(global.plugins); 3 | const totalCommands = plugins.reduce((sum, p) => sum + (p.help ? p.help.length : 0), 0); 4 | const totalTags = [...new Set(plugins.flatMap((v) => v.tags || []))].length; 5 | const totalPlugins = plugins.length; 6 | 7 | const text = ` 8 | Liora Plugin Statistics 9 | 10 | Total Features: ${totalCommands} 11 | Total Categories: ${totalTags} 12 | Total Plugins: ${totalPlugins} 13 | `.trim(); 14 | 15 | await conn.sendMessage(m.chat, { text }, { quoted: m }); 16 | }; 17 | 18 | handler.help = ["totalfitur"]; 19 | handler.tags = ["info"]; 20 | handler.command = /^(totalfitur)$/i; 21 | 22 | export default handler; 23 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | 4 | export default [ 5 | js.configs.recommended, 6 | { 7 | files: ["**/*.js"], 8 | ignores: [ 9 | "node_modules/", 10 | "build/", 11 | "lib/rs/target/", 12 | ".cache/", 13 | ".github/", 14 | "*.log", 15 | "tmp/", 16 | "temp/", 17 | ".DS_Store", 18 | ".oven/", 19 | "coverage/", 20 | "**/cache/**", 21 | "**/dist/**", 22 | "**/Release/**", 23 | ], 24 | languageOptions: { 25 | globals: { 26 | ...globals.node, 27 | }, 28 | }, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /plugins/group/group-unpin.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | if (!m.quoted) return m.reply("Reply a message to unpin."); 3 | 4 | const quotedKey = m.quoted?.vM?.key; 5 | if (!quotedKey) return m.reply("Cannot unpin: quoted message key not found"); 6 | 7 | try { 8 | await conn.sendMessage(m.chat, { 9 | pin: quotedKey, 10 | type: 2, 11 | }); 12 | m.reply(`Message unpinned.`); 13 | } catch (e) { 14 | conn.logger.error(e); 15 | m.reply(`Error: ${e.message}`); 16 | } 17 | }; 18 | 19 | handler.help = ["unpin"]; 20 | handler.tags = ["group"]; 21 | handler.command = /^(unpin)$/i; 22 | handler.group = true; 23 | handler.admin = true; 24 | handler.botAdmin = true; 25 | 26 | export default handler; 27 | -------------------------------------------------------------------------------- /plugins/owner/owner-setbiobot.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text, command, usedPrefix }) => { 2 | if (!text) { 3 | return m.reply( 4 | `Enter the new bio text.\nExample: ${usedPrefix + command} I am the best bot owned by Izumi.` 5 | ); 6 | } 7 | 8 | try { 9 | await conn.setStatus(text); 10 | 11 | const response = ` 12 | New Bio: ${text} 13 | WhatsApp bot bio updated successfully. 14 | `.trim(); 15 | 16 | m.reply(response); 17 | } catch (e) { 18 | conn.logger.error(e); 19 | m.reply(`Error: ${e.message}`); 20 | } 21 | }; 22 | 23 | handler.help = ["setbiobot"]; 24 | handler.tags = ["owner"]; 25 | handler.command = /^set(bio(bot)?)$/i; 26 | handler.owner = true; 27 | 28 | export default handler; 29 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | Thank you to everyone who has contributed to Liora! 🎉 4 | 5 | ## Core Team 6 | 7 | - **Naruya Izumi** ([@naruyaizumi](https://github.com/naruyaizumi)) - Creator & Maintainer 8 | 9 | ## Contributors 10 | 11 | - **Shadow** - 14 commits 12 | - **Terror-Machine** - 1 commits 13 | - **dependabot[bot]** - 0 commits 14 | - **github-actions** - 10 commits 15 | - **github-actions[bot]** - 0 commits 16 | - **liora-bot** - 5 commits 17 | - **mkfs.ext4 /dev/naruyaizumi** - 41 commits 18 | - **naruyaizumi** - 417 commits 19 | - **𝑵𝒂𝒓𝒖𝒚𝒂 𝑰𝒛𝒖𝒎𝒊** - 41 commits 20 | - **🩷 Liora Bot** - 5 commits 21 | 22 | ## How to Contribute 23 | 24 | See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. 25 | 26 | --- 27 | 28 | *Last updated: $(date -u +"%Y-%m-%d")* 29 | -------------------------------------------------------------------------------- /plugins/_/_antistatus.js: -------------------------------------------------------------------------------- 1 | export async function before(m, { conn, isOwner, isAdmin, isBotAdmin }) { 2 | if (!m.isGroup) return true; 3 | if (isOwner) return true; 4 | if (isAdmin) return true; 5 | let chat = global.db.data.chats[m.chat]; 6 | if (!chat) return true; 7 | if (!chat?.antiStatus || !isBotAdmin) return true; 8 | 9 | if (m.mtype === "groupStatusMentionMessage") { 10 | try { 11 | await this.sendMessage(m.chat, { 12 | delete: { 13 | remoteJid: m.chat, 14 | fromMe: false, 15 | id: m.key.id, 16 | participant: m.key.participant || m.sender, 17 | }, 18 | }); 19 | } catch (e) { 20 | conn.logger.error(e); 21 | } 22 | } 23 | 24 | return true; 25 | } 26 | -------------------------------------------------------------------------------- /plugins/_/_antiaudio.js: -------------------------------------------------------------------------------- 1 | export async function before(m, { conn, isOwner, isAdmin, isBotAdmin }) { 2 | if (m.isBaileys || m.fromMe) return true; 3 | if (isOwner) return true; 4 | if (!m.isGroup) return true; 5 | if (isAdmin) return true; 6 | let chat = global.db.data.chats[m.chat]; 7 | if (!chat) return true; 8 | if (!chat?.antiAudio || !isBotAdmin) return true; 9 | if (m.mtype === "audioMessage") { 10 | try { 11 | await this.sendMessage(m.chat, { 12 | delete: { 13 | remoteJid: m.chat, 14 | fromMe: false, 15 | id: m.key.id, 16 | participant: m.key.participant || m.sender, 17 | }, 18 | }); 19 | } catch (e) { 20 | conn.logger.error(e); 21 | } 22 | } 23 | 24 | return true; 25 | } 26 | -------------------------------------------------------------------------------- /plugins/_/_antifoto.js: -------------------------------------------------------------------------------- 1 | export async function before(m, { conn, isOwner, isAdmin, isBotAdmin }) { 2 | if (m.isBaileys || m.fromMe) return true; 3 | if (isOwner) return true; 4 | if (!m.isGroup) return true; 5 | if (isAdmin) return true; 6 | let chat = global.db.data.chats[m.chat]; 7 | if (!chat) return true; 8 | if (!chat?.antiFoto || !isBotAdmin) return true; 9 | if (m.mtype === "imageMessage") { 10 | try { 11 | await this.sendMessage(m.chat, { 12 | delete: { 13 | remoteJid: m.chat, 14 | fromMe: false, 15 | id: m.key.id, 16 | participant: m.key.participant || m.sender, 17 | }, 18 | }); 19 | } catch (e) { 20 | conn.logger.error(e); 21 | } 22 | } 23 | 24 | return true; 25 | } 26 | -------------------------------------------------------------------------------- /plugins/_/_antifile.js: -------------------------------------------------------------------------------- 1 | export async function before(m, { conn, isOwner, isAdmin, isBotAdmin }) { 2 | if (m.isBaileys || m.fromMe) return true; 3 | if (isOwner) return true; 4 | if (!m.isGroup) return true; 5 | if (isAdmin) return true; 6 | let chat = global.db.data.chats[m.chat]; 7 | if (!chat) return true; 8 | if (!chat?.antiFile || !isBotAdmin) return true; 9 | 10 | if (m.mtype === "documentMessage") { 11 | try { 12 | await this.sendMessage(m.chat, { 13 | delete: { 14 | remoteJid: m.chat, 15 | fromMe: false, 16 | id: m.key.id, 17 | participant: m.key.participant || m.sender, 18 | }, 19 | }); 20 | } catch (e) { 21 | conn.logger.error(e); 22 | } 23 | } 24 | 25 | return true; 26 | } 27 | -------------------------------------------------------------------------------- /plugins/_/_antivideo.js: -------------------------------------------------------------------------------- 1 | export async function before(m, { conn, isOwner, isAdmin, isBotAdmin }) { 2 | if (m.isBaileys || m.fromMe) return true; 3 | if (isOwner) return true; 4 | if (!m.isGroup) return true; 5 | if (isAdmin) return true; 6 | let chat = global.db.data.chats[m.chat]; 7 | if (!chat) return true; 8 | if (!chat?.antiVideo || !isBotAdmin) return true; 9 | 10 | if (m.mtype === "videoMessage") { 11 | try { 12 | await this.sendMessage(m.chat, { 13 | delete: { 14 | remoteJid: m.chat, 15 | fromMe: false, 16 | id: m.key.id, 17 | participant: m.key.participant || m.sender, 18 | }, 19 | }); 20 | } catch (e) { 21 | conn.logger.error(e); 22 | } 23 | } 24 | 25 | return true; 26 | } 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: naruyaizumi 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: naruyaizumi 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /plugins/_/_antisticker.js: -------------------------------------------------------------------------------- 1 | export async function before(m, { conn, isOwner, isAdmin, isBotAdmin }) { 2 | if (m.isBaileys || m.fromMe) return true; 3 | if (isOwner) return true; 4 | if (!m.isGroup) return true; 5 | if (isAdmin) return true; 6 | let chat = global.db.data.chats[m.chat]; 7 | if (!chat) return true; 8 | if (!chat?.antiSticker || !isBotAdmin) return true; 9 | 10 | if (m.mtype === "stickerMessage") { 11 | try { 12 | await this.sendMessage(m.chat, { 13 | delete: { 14 | remoteJid: m.chat, 15 | fromMe: false, 16 | id: m.key.id, 17 | participant: m.key.participant || m.sender, 18 | }, 19 | }); 20 | } catch (e) { 21 | conn.logger.error(e); 22 | } 23 | } 24 | 25 | return true; 26 | } 27 | -------------------------------------------------------------------------------- /plugins/owner/owner-setppbot.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, usedPrefix, command }) => { 2 | const bot = conn.decodeJid(conn.user.id); 3 | const q = m.quoted ? m.quoted : m; 4 | const mime = (q.msg || q).mimetype || ""; 5 | 6 | if (!/image/.test(mime)) 7 | return m.reply(`Send or reply an image with caption ${usedPrefix + command}`); 8 | 9 | try { 10 | const img = await q.download(); 11 | if (!img) return m.reply("Failed to download image."); 12 | await conn.updateProfilePicture(bot, img); 13 | m.reply("Bot profile picture updated successfully."); 14 | } catch (e) { 15 | conn.logger.error(e); 16 | m.reply(`Error: ${e.message}`); 17 | } 18 | }; 19 | 20 | handler.help = ["setppbot"]; 21 | handler.tags = ["owner"]; 22 | handler.command = /^setpp(bot)?$/i; 23 | handler.owner = true; 24 | 25 | export default handler; 26 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code Owners for Liora WhatsApp Bot 2 | # These owners will be the default owners for everything in the repo. 3 | # Unless a later match takes precedence, they will be requested for 4 | # review when someone opens a pull request. 5 | 6 | # Default owners 7 | * @naruyaizumi 8 | 9 | # GitHub Actions and CI/CD 10 | /.github/ @naruyaizumi 11 | /.github/workflows/ @naruyaizumi 12 | 13 | # Core application 14 | /src/ @naruyaizumi 15 | /lib/core/ @naruyaizumi 16 | /lib/auth/ @naruyaizumi 17 | 18 | # APIs 19 | /lib/api/ @naruyaizumi 20 | 21 | # C++ code 22 | /lib/cpp/ @naruyaizumi 23 | binding.gyp @naruyaizumi 24 | 25 | # Configuration files 26 | package.json @naruyaizumi 27 | *.config.js @naruyaizumi 28 | *.config.json @naruyaizumi 29 | 30 | # Documentation 31 | /docs/ @naruyaizumi 32 | *.md @naruyaizumi 33 | README.md @naruyaizumi 34 | CHANGELOG.md @naruyaizumi 35 | 36 | # Scripts 37 | *.sh @naruyaizumi 38 | install.sh @naruyaizumi -------------------------------------------------------------------------------- /plugins/tool/tool-toptv.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, usedPrefix, command }) => { 2 | if (!m.quoted || !/video/.test(m.quoted.mimetype || "")) 3 | return m.reply(`Reply a video with command:\n› ${usedPrefix + command}`); 4 | 5 | await global.loading(m, conn); 6 | 7 | try { 8 | const q = await m.quoted.download(); 9 | if (!q || !q.length) return m.reply("Failed to download video buffer."); 10 | 11 | await conn.sendMessage( 12 | m.chat, 13 | { 14 | video: q, 15 | mimetype: "video/mp4", 16 | ptv: true, 17 | }, 18 | { quoted: m } 19 | ); 20 | } catch (e) { 21 | conn.logger.error(e); 22 | m.reply(`Error: ${e.message}`); 23 | } finally { 24 | await global.loading(m, conn, true); 25 | } 26 | }; 27 | 28 | handler.help = ["toptv"]; 29 | handler.tags = ["tools"]; 30 | handler.command = /^(toptv)$/i; 31 | 32 | export default handler; 33 | -------------------------------------------------------------------------------- /plugins/info/info-listblock.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | try { 3 | const data = await conn.fetchBlocklist(); 4 | if (!data || !data.length) return m.reply("No blocked numbers found."); 5 | 6 | const list = data 7 | .map((jid, i) => { 8 | const user = jid.split("@")[0]; 9 | return `${i + 1}. @${user}`; 10 | }) 11 | .join("\n"); 12 | 13 | const output = [ 14 | "=== Blocked Numbers ===", 15 | `Total: ${data.length}`, 16 | "──────────────────────", 17 | list, 18 | ].join("\n"); 19 | 20 | await conn.sendMessage(m.chat, { text: output, mentions: data }, { quoted: m }); 21 | } catch (e) { 22 | conn.logger.error(e); 23 | m.reply(`Error: ${e.message}`); 24 | } 25 | }; 26 | 27 | handler.help = ["listblock"]; 28 | handler.tags = ["info"]; 29 | handler.command = /^(listb(lo(ck|k)?)?)$/i; 30 | handler.owner = true; 31 | 32 | export default handler; 33 | -------------------------------------------------------------------------------- /lib/api/ytmp3.js: -------------------------------------------------------------------------------- 1 | export async function ytmp3(url) { 2 | const encoded = encodeURIComponent(url); 3 | const endpoints = [ 4 | `https://api.nekolabs.web.id/downloader/youtube/v1?url=${encoded}`, 5 | `https://api.ootaizumi.web.id/downloader/youtube?url=${encoded}&format=mp3`, 6 | `https://api.elrayyxml.web.id/api/downloader/ytmp3?url=${encoded}`, 7 | ]; 8 | 9 | for (const endpoint of endpoints) { 10 | const res = await fetch(endpoint).catch(() => null); 11 | if (!res) continue; 12 | 13 | const json = await res.json().catch(() => null); 14 | if (!json || (!json.success && !json.status)) continue; 15 | 16 | const downloadUrl = json.result?.downloadUrl || json.result?.download || json.result?.url; 17 | 18 | if (downloadUrl) { 19 | return { 20 | success: true, 21 | downloadUrl, 22 | }; 23 | } 24 | } 25 | 26 | return { success: false, error: "Failed to retrieve audio from the provided link." }; 27 | } 28 | -------------------------------------------------------------------------------- /.github/.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | "feat", 9 | "fix", 10 | "docs", 11 | "style", 12 | "refactor", 13 | "perf", 14 | "test", 15 | "build", 16 | "ci", 17 | "chore", 18 | "revert" 19 | ] 20 | ], 21 | "type-case": [2, "always", "lower-case"], 22 | "type-empty": [2, "never"], 23 | "scope-case": [2, "always", "lower-case"], 24 | "subject-empty": [2, "never"], 25 | "subject-full-stop": [2, "never", "."], 26 | "header-max-length": [2, "always", 100], 27 | "body-leading-blank": [1, "always"], 28 | "body-max-line-length": [2, "always", 100], 29 | "footer-leading-blank": [1, "always"], 30 | "footer-max-line-length": [2, "always", 100] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | time: "08:00" 9 | timezone: "Asia/Jakarta" 10 | open-pull-requests-limit: 10 11 | reviewers: 12 | - "naruyaizumi" 13 | assignees: 14 | - "naruyaizumi" 15 | labels: 16 | - "dependencies" 17 | - "automated" 18 | commit-message: 19 | prefix: "chore(deps)" 20 | include: "scope" 21 | ignore: 22 | - dependency-name: "*" 23 | update-types: ["version-update:semver-major"] 24 | 25 | - package-ecosystem: "github-actions" 26 | directory: "/" 27 | schedule: 28 | interval: "weekly" 29 | day: "monday" 30 | time: "11:00" 31 | timezone: "Asia/Jakarta" 32 | open-pull-requests-limit: 5 33 | reviewers: 34 | - "naruyaizumi" 35 | labels: 36 | - "dependencies" 37 | - "ci-cd" 38 | - "automated" 39 | commit-message: 40 | prefix: "chore(ci)" 41 | include: "scope" -------------------------------------------------------------------------------- /plugins/group/group-delete.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | if (!m.quoted) return m.reply("No quoted message found to delete."); 3 | const { chat, id, participant, sender, fromMe } = m.quoted; 4 | if (m.isBaileys || m.fromMe) return true; 5 | const quotedSender = participant || sender; 6 | if (!quotedSender) return m.reply("Could not identify quoted sender."); 7 | if (fromMe) return m.reply("Cannot delete messages sent by the bot."); 8 | try { 9 | await conn.sendMessage(chat, { 10 | delete: { 11 | remoteJid: m.chat, 12 | fromMe: false, 13 | id, 14 | participant: quotedSender, 15 | }, 16 | }); 17 | } catch (e) { 18 | conn.logger.error(e); 19 | m.reply(`Error: ${e.message}`); 20 | } 21 | }; 22 | 23 | handler.help = ["delete"]; 24 | handler.tags = ["group"]; 25 | handler.command = /^(d|delete)$/i; 26 | handler.group = true; 27 | handler.admin = true; 28 | handler.botAdmin = true; 29 | 30 | export default handler; 31 | -------------------------------------------------------------------------------- /plugins/owner/owner-df.js: -------------------------------------------------------------------------------- 1 | import { unlink, access } from "fs/promises"; 2 | import path from "path"; 3 | 4 | let handler = async (m, { args, usedPrefix, command, conn }) => { 5 | if (!args.length) 6 | return m.reply( 7 | `Enter the file path to delete.\n› Example: ${usedPrefix + command} plugins/owner/owner-sf` 8 | ); 9 | 10 | let target = path.join(...args); 11 | if (!path.extname(target)) target += ".js"; 12 | const filepath = path.resolve(process.cwd(), target); 13 | 14 | try { 15 | await access(filepath); 16 | await unlink(filepath); 17 | m.reply("File deleted successfully."); 18 | } catch (e) { 19 | conn.logger.error(e); 20 | if (e.code === "ENOENT") { 21 | m.reply(`File not found: ${filepath}`); 22 | } else { 23 | m.reply(`Error: ${e.message}`); 24 | } 25 | } 26 | }; 27 | 28 | handler.help = ["deletefile"]; 29 | handler.tags = ["owner"]; 30 | handler.command = /^(df|deletefile)$/i; 31 | handler.owner = true; 32 | 33 | export default handler; 34 | -------------------------------------------------------------------------------- /plugins/group/group-settings.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | try { 3 | const arg = (args[0] || "").toLowerCase(); 4 | const isClose = { open: "not_announcement", close: "announcement" }[arg]; 5 | 6 | if (isClose === undefined) { 7 | return m.reply( 8 | `Usage: ${usedPrefix + command} open | close\n\nopen → allow members to send messages\nclose → only admins can send messages` 9 | ); 10 | } 11 | 12 | await conn.groupSettingUpdate(m.chat, isClose); 13 | 14 | const status = 15 | arg === "open" ? "Group opened (members can chat)" : "Group closed (admins only)"; 16 | return m.reply(`Status: ${status}`); 17 | } catch (e) { 18 | conn.logger.error(e); 19 | m.reply(`Error: ${e.message}`); 20 | } 21 | }; 22 | 23 | handler.help = ["group"]; 24 | handler.tags = ["group"]; 25 | handler.command = /^(g|group)$/i; 26 | handler.group = true; 27 | handler.admin = true; 28 | handler.botAdmin = true; 29 | 30 | export default handler; 31 | -------------------------------------------------------------------------------- /plugins/owner/owner-eval.js: -------------------------------------------------------------------------------- 1 | import { inspect } from "util"; 2 | 3 | let handler = async (m, { conn, noPrefix, isOwner }) => { 4 | if (!isOwner) return; 5 | let _text = noPrefix; 6 | let _return; 7 | 8 | try { 9 | if (m.text.startsWith("=>")) { 10 | _return = await eval(`(async () => { return ${_text} })()`); 11 | } else { 12 | _return = await eval(`(async () => { ${_text} })()`); 13 | } 14 | } catch (e) { 15 | _return = e; 16 | } 17 | 18 | let output; 19 | if ( 20 | Array.isArray(_return) && 21 | _return.every((item) => item && typeof item === "object" && !Array.isArray(item)) 22 | ) { 23 | output = inspect(_return, { depth: null, maxArrayLength: null }); 24 | } else if (typeof _return === "string") { 25 | output = _return; 26 | } else { 27 | output = inspect(_return, { depth: null, maxArrayLength: null }); 28 | } 29 | 30 | await conn.sendMessage(m.chat, { text: output }); 31 | }; 32 | 33 | handler.help = [">", "=>"]; 34 | handler.tags = ["owner"]; 35 | handler.customPrefix = /^=?> /; 36 | handler.command = /(?:)/i; 37 | 38 | export default handler; 39 | -------------------------------------------------------------------------------- /plugins/group/group-link.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, groupMetadata }) => { 2 | try { 3 | const invite = await conn.groupInviteCode(m.chat); 4 | const link = `https://chat.whatsapp.com/${invite}`; 5 | const info = ` 6 | Group Name: ${groupMetadata.subject} 7 | Group ID: ${m.chat}`; 8 | 9 | await conn.sendButton(m.chat, { 10 | text: info, 11 | title: "Group Link", 12 | footer: "Use the button below to copy the group link", 13 | interactiveButtons: [ 14 | { 15 | name: "cta_copy", 16 | buttonParamsJson: JSON.stringify({ 17 | display_text: "Copy Group Link", 18 | copy_code: link, 19 | }), 20 | }, 21 | ], 22 | hasMediaAttachment: false, 23 | }); 24 | } catch (e) { 25 | conn.logger.error(e); 26 | m.reply(`Error: ${e.message}`); 27 | } 28 | }; 29 | 30 | handler.help = ["grouplink"]; 31 | handler.tags = ["group"]; 32 | handler.command = /^(grouplink|link)$/i; 33 | handler.group = true; 34 | handler.botAdmin = true; 35 | 36 | export default handler; 37 | -------------------------------------------------------------------------------- /plugins/owner/owner-gf.js: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | import { join, extname } from "path"; 3 | 4 | let handler = async (m, { conn, args, usedPrefix, command }) => { 5 | if (!args.length) 6 | return m.reply( 7 | `Enter the target file path.\n› Example: ${usedPrefix + command} plugins/owner/owner-sf` 8 | ); 9 | 10 | let target; 11 | try { 12 | target = join(...args); 13 | if (!extname(target)) target += ".js"; 14 | const filepath = join(process.cwd(), target); 15 | 16 | const fileBuffer = await readFile(filepath); 17 | const fileName = target.split("/").pop(); 18 | 19 | await conn.sendMessage( 20 | m.chat, 21 | { 22 | document: fileBuffer, 23 | fileName, 24 | mimetype: "application/javascript", 25 | }, 26 | { quoted: m } 27 | ); 28 | } catch (e) { 29 | conn.logger.error(e); 30 | m.reply(`Error: ${e.message}`); 31 | } 32 | }; 33 | 34 | handler.help = ["getfile"]; 35 | handler.tags = ["owner"]; 36 | handler.command = /^(getfile|gf)$/i; 37 | handler.owner = true; 38 | 39 | export default handler; 40 | -------------------------------------------------------------------------------- /lib/api/spotifydl.js: -------------------------------------------------------------------------------- 1 | export async function spotifydl(url) { 2 | const encoded = encodeURIComponent(url); 3 | const endpoints = [ 4 | `https://api.nekolabs.web.id/downloader/spotify/v1?url=${encoded}`, 5 | `https://api.ootaizumi.web.id/downloader/spotify?url=${encoded}`, 6 | `https://api.elrayyxml.web.id/api/downloader/spotify?url=${encoded}`, 7 | `https://api.rikishop.my.id/download/spotify?url=${encoded}`, 8 | ]; 9 | 10 | for (const endpoint of endpoints) { 11 | const res = await fetch(endpoint).catch(() => null); 12 | if (!res) continue; 13 | 14 | const json = await res.json().catch(() => null); 15 | if (!json || (!json.success && !json.status)) continue; 16 | 17 | const downloadUrl = 18 | json.result?.downloadUrl || 19 | json.result?.download || 20 | json.result?.url || 21 | json.result?.res_data?.formats?.[0]?.url; 22 | 23 | if (downloadUrl) { 24 | return { 25 | success: true, 26 | downloadUrl, 27 | }; 28 | } 29 | } 30 | 31 | return { success: false, error: "Failed to retrieve audio from the provided link." }; 32 | } 33 | -------------------------------------------------------------------------------- /plugins/group/group-polling.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | const input = args.join(" "); 3 | if (!input.includes("|")) 4 | return m.reply( 5 | `Invalid format.\nExample: ${usedPrefix + command} Who is the most active?|Naruya, Liora, Member` 6 | ); 7 | 8 | const [title, optionsRaw] = input.split("|"); 9 | const options = optionsRaw 10 | .split(",") 11 | .map((v) => v.trim()) 12 | .filter(Boolean); 13 | 14 | if (!title || options.length < 2) 15 | return m.reply("Poll must have a title and at least 2 options."); 16 | 17 | try { 18 | await conn.sendMessage(m.chat, { 19 | poll: { 20 | name: title.trim(), 21 | values: options, 22 | selectableCount: 1, 23 | toAnnouncementGroup: true, 24 | }, 25 | }); 26 | } catch (e) { 27 | conn.logger.error(e); 28 | m.reply(`Error: ${e.message}`); 29 | } 30 | }; 31 | 32 | handler.help = ["poll"]; 33 | handler.tags = ["group"]; 34 | handler.command = /^(poll)$/i; 35 | handler.group = true; 36 | handler.admin = true; 37 | handler.botAdmin = true; 38 | 39 | export default handler; 40 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | whatsapp: 2 | - changed-files: 3 | - any-glob-to-any-file: 4 | - "src/**/*" 5 | - "lib/core/**/*" 6 | - "lib/auth/**/*" 7 | 8 | api: 9 | - changed-files: 10 | - any-glob-to-any-file: "lib/api/**/*" 11 | 12 | documentation: 13 | - changed-files: 14 | - any-glob-to-any-file: 15 | - "**/*.md" 16 | - "README.md" 17 | - "CHANGELOG.md" 18 | 19 | config: 20 | - changed-files: 21 | - any-glob-to-any-file: 22 | - "*.json" 23 | - "*.yml" 24 | - "*.yaml" 25 | - ".github/**/*" 26 | 27 | cpp: 28 | - changed-files: 29 | - any-glob-to-any-file: 30 | - "lib/cpp/**/*" 31 | - "**/*.cpp" 32 | - "**/*.h" 33 | - "**/*.cc" 34 | - "binding.gyp" 35 | 36 | ci-cd: 37 | - changed-files: 38 | - any-glob-to-any-file: 39 | - ".github/workflows/**/*" 40 | - ".github/actions/**/*" 41 | 42 | dependencies: 43 | - changed-files: 44 | - any-glob-to-any-file: 45 | - "package.json" 46 | 47 | security: 48 | - changed-files: 49 | - any-glob-to-any-file: 50 | - ".github/workflows/*security*" 51 | - ".github/workflows/*scan*" -------------------------------------------------------------------------------- /plugins/maker/maker-toimg.js: -------------------------------------------------------------------------------- 1 | import sharp from "sharp"; 2 | 3 | let handler = async (m, { conn, usedPrefix, command }) => { 4 | try { 5 | const q = m.quoted ? m.quoted : m; 6 | const mime = (q.msg || q).mimetype || q.mediaType || ""; 7 | if (!/webp/.test(mime)) 8 | return m.reply(`Reply a sticker with the command: ${usedPrefix + command}`); 9 | 10 | await global.loading(m, conn); 11 | 12 | const buffer = await q.download?.(); 13 | if (!buffer || !Buffer.isBuffer(buffer)) 14 | throw new Error("Failed to download sticker buffer."); 15 | 16 | const output = await sharp(buffer).png().toBuffer(); 17 | if (!output.length) throw new Error("Conversion failed, output is empty."); 18 | 19 | await conn.sendMessage( 20 | m.chat, 21 | { image: output, caption: "Sticker successfully converted to image." }, 22 | { quoted: m } 23 | ); 24 | } catch (e) { 25 | conn.logger.error(e); 26 | m.reply(`Error: ${e.message}`); 27 | } finally { 28 | await global.loading(m, conn, true); 29 | } 30 | }; 31 | 32 | handler.help = ["toimg"]; 33 | handler.tags = ["maker"]; 34 | handler.command = /^(toimg)$/i; 35 | 36 | export default handler; 37 | -------------------------------------------------------------------------------- /plugins/group/group-ephemeral.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | const input = args[0]; 3 | const durations = { 4 | 0: { seconds: 0, label: "disabled" }, 5 | 1: { seconds: 86400, label: "1 day" }, 6 | 2: { seconds: 604800, label: "7 days" }, 7 | 3: { seconds: 7776000, label: "90 days" }, 8 | }; 9 | 10 | if (!input || !durations[input]) { 11 | return m.reply( 12 | `Invalid option.\n\nExamples:\n` + 13 | `› ${usedPrefix + command} 0 = remove\n` + 14 | `› ${usedPrefix + command} 1 = 1 day\n` + 15 | `› ${usedPrefix + command} 2 = 7 days\n` + 16 | `› ${usedPrefix + command} 3 = 90 days` 17 | ); 18 | } 19 | 20 | try { 21 | await conn.groupToggleEphemeral(m.chat, durations[input].seconds); 22 | m.reply(`Ephemeral messages set to ${durations[input].label}.`); 23 | } catch (e) { 24 | conn.logger.error(e); 25 | m.reply(`Error: ${e.message}`); 26 | } 27 | }; 28 | 29 | handler.help = ["ephemeral"]; 30 | handler.tags = ["group"]; 31 | handler.command = /^(ephemeral)$/i; 32 | handler.group = true; 33 | handler.admin = true; 34 | handler.botAdmin = true; 35 | 36 | export default handler; 37 | -------------------------------------------------------------------------------- /plugins/ai/ai-copilot.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text }) => { 2 | if (!text || typeof text !== "string") { 3 | return m.reply("Please provide a valid query for Copilot AI."); 4 | } 5 | 6 | try { 7 | await global.loading(m, conn); 8 | 9 | const apiUrl = `https://api.nekolabs.web.id/text-generation/copilot?text=${encodeURIComponent(text)}`; 10 | const response = await fetch(apiUrl); 11 | if (!response.ok) { 12 | return m.reply("Unable to connect to Copilot AI. Please try again later."); 13 | } 14 | 15 | const json = await response.json(); 16 | const replyText = json?.result?.text; 17 | 18 | if (typeof replyText !== "string") { 19 | return m.reply("Copilot AI did not return a valid response."); 20 | } 21 | 22 | await conn.sendMessage( 23 | m.chat, 24 | { text: `Copilot AI:\n${replyText.trim()}` }, 25 | { quoted: m } 26 | ); 27 | } catch (e) { 28 | conn.logger.error(e); 29 | m.reply(`Error: ${e.message}`); 30 | } finally { 31 | await global.loading(m, conn, true); 32 | } 33 | }; 34 | 35 | handler.help = ["copilot"]; 36 | handler.tags = ["ai"]; 37 | handler.command = /^(copilot)$/i; 38 | 39 | export default handler; 40 | -------------------------------------------------------------------------------- /plugins/tool/tool-ssweb.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | if (args.length === 0) { 3 | return m.reply( 4 | `Please provide a URL.\nExample: ${usedPrefix + command} https://example.com` 5 | ); 6 | } 7 | 8 | const url = args.join(" "); 9 | 10 | await global.loading(m, conn); 11 | 12 | try { 13 | const apiUrl = `https://api.nekolabs.web.id/tools/ssweb?url=${encodeURIComponent(url)}&device=desktop&fullPage=false`; 14 | 15 | const res = await fetch(apiUrl); 16 | if (!res.ok) throw new Error(`HTTP ${res.status}`); 17 | const data = await res.json(); 18 | if (!data.success || !data.result) throw new Error("No screenshot returned."); 19 | 20 | const imageUrl = data.result; 21 | 22 | const caption = ` 23 | Screenshot (DESKTOP) 24 | URL: ${url} 25 | `.trim(); 26 | 27 | await conn.sendMessage(m.chat, { image: { url: imageUrl }, caption }, { quoted: m }); 28 | } catch (e) { 29 | conn.logger.error(e); 30 | m.reply(`Error: ${e.message}`); 31 | } finally { 32 | await global.loading(m, conn, true); 33 | } 34 | }; 35 | 36 | handler.help = ["ssweb"]; 37 | handler.tags = ["tools"]; 38 | handler.command = /^(ssweb)$/i; 39 | 40 | export default handler; 41 | -------------------------------------------------------------------------------- /plugins/tool/tool-cekip.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | if (!args[0]) { 3 | return m.reply(`Enter a domain name or URL.\nExample: ${usedPrefix + command} google.com`); 4 | } 5 | 6 | const domain = args[0] 7 | .replace(/^https?:\/\//i, "") 8 | .replace(/^www\./i, "") 9 | .split("/")[0]; 10 | 11 | try { 12 | const res = await fetch(`http://ip-api.com/json/${domain}`); 13 | const data = await res.json(); 14 | 15 | if (data.status !== "success") { 16 | return m.reply(`Failed to resolve IP for domain: ${domain}`); 17 | } 18 | 19 | const result = ` 20 | Network Lookup 21 | Query: ${data.query} 22 | Country: ${data.country} (${data.countryCode}) 23 | Region: ${data.regionName} (${data.region}) 24 | City: ${data.city} 25 | ZIP: ${data.zip} 26 | Latitude: ${data.lat} 27 | Longitude: ${data.lon} 28 | Timezone: ${data.timezone} 29 | ISP: ${data.isp} 30 | Org: ${data.org} 31 | AS: ${data.as} 32 | `.trim(); 33 | 34 | await m.reply(result); 35 | } catch (e) { 36 | conn.logger.error(e); 37 | m.reply(`Error: ${e.message}`); 38 | } 39 | }; 40 | 41 | handler.help = ["cekip"]; 42 | handler.tags = ["tools"]; 43 | handler.command = /^(cekip|ip)$/i; 44 | 45 | export default handler; 46 | -------------------------------------------------------------------------------- /plugins/maker/maker-brat.js: -------------------------------------------------------------------------------- 1 | import { sticker } from "#add-on"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | try { 5 | if (!args[0]) 6 | return m.reply(`Enter sticker text.\n› Example: ${usedPrefix + command} Konichiwa~`); 7 | 8 | await global.loading(m, conn); 9 | 10 | const res = await fetch( 11 | `https://api.nekolabs.web.id/canvas/brat/v1?text=${encodeURIComponent(args.join(" "))}` 12 | ); 13 | if (!res.ok) throw new Error("Failed to fetch Brat API."); 14 | 15 | const buffer = Buffer.from(await res.arrayBuffer()); 16 | 17 | const stickerImage = await sticker(buffer, { 18 | packName: global.config.stickpack || "", 19 | authorName: global.config.stickauth || "", 20 | }); 21 | 22 | await conn.sendMessage( 23 | m.chat, 24 | { 25 | sticker: stickerImage, 26 | }, 27 | { quoted: m } 28 | ); 29 | } catch (e) { 30 | conn.logger.error(e); 31 | m.reply(`Error: ${e.message}`); 32 | } finally { 33 | await global.loading(m, conn, true); 34 | } 35 | }; 36 | 37 | handler.help = ["brat"]; 38 | handler.tags = ["maker"]; 39 | handler.command = /^(brat)$/i; 40 | 41 | export default handler; 42 | -------------------------------------------------------------------------------- /plugins/downloader/downloader-spotifydl.js: -------------------------------------------------------------------------------- 1 | import { spotifydl } from "#spotifydl"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | if (!args[0]) 5 | return m.reply( 6 | `Please provide a valid Spotify track URL.\n› Example: ${usedPrefix + command} https://open.spotify.com/track/...` 7 | ); 8 | 9 | const url = args[0]; 10 | const spotifyRegex = /^https?:\/\/open\.spotify\.com\/track\/[\w-]+(\?.*)?$/i; 11 | if (!spotifyRegex.test(url)) 12 | return m.reply("Invalid URL! Please provide a valid Spotify track link."); 13 | 14 | await global.loading(m, conn); 15 | 16 | try { 17 | const { success, downloadUrl, error } = await spotifydl(url); 18 | if (!success) throw new Error(error); 19 | 20 | await conn.sendMessage( 21 | m.chat, 22 | { 23 | audio: { url: downloadUrl }, 24 | mimetype: "audio/mpeg", 25 | }, 26 | { quoted: m } 27 | ); 28 | } catch (e) { 29 | conn.logger.error(e); 30 | m.reply(`Error: ${e.message}`); 31 | } finally { 32 | await global.loading(m, conn, true); 33 | } 34 | }; 35 | 36 | handler.help = ["spotifydl"]; 37 | handler.tags = ["downloader"]; 38 | handler.command = /^(spotifydl)$/i; 39 | 40 | export default handler; 41 | -------------------------------------------------------------------------------- /plugins/downloader/downloader-gitclone.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text, usedPrefix, command }) => { 2 | try { 3 | if (!text || !/^https:\/\/github\.com\/[\w-]+\/[\w-]+/i.test(text)) 4 | return m.reply( 5 | `Please provide a valid GitHub repository URL.\n› Example: ${usedPrefix + command} https://github.com/username/repo` 6 | ); 7 | 8 | const parts = text.split("/"); 9 | if (parts.length < 5) return m.reply("Incomplete GitHub repository URL."); 10 | 11 | await global.loading(m, conn); 12 | 13 | const user = parts[3]; 14 | const repo = parts[4]; 15 | const url = `https://api.github.com/repos/${user}/${repo}/zipball`; 16 | const filename = `${repo}.zip`; 17 | 18 | await conn.sendMessage( 19 | m.chat, 20 | { 21 | document: { url }, 22 | fileName: filename, 23 | mimetype: "application/zip", 24 | }, 25 | { quoted: m } 26 | ); 27 | } catch (e) { 28 | conn.logger.error(e); 29 | m.reply(`Error: ${e.message}`); 30 | } finally { 31 | await global.loading(m, conn, true); 32 | } 33 | }; 34 | 35 | handler.help = ["gitclone"]; 36 | handler.tags = ["downloader"]; 37 | handler.command = /^(gitclone)$/i; 38 | 39 | export default handler; 40 | -------------------------------------------------------------------------------- /plugins/group/group-modebot.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { text, usedPrefix, command, conn }) => { 2 | try { 3 | const chat = global.db.data.chats[m.chat]; 4 | 5 | if (!text) { 6 | const status = chat.mute ? "OFFLINE" : "ONLINE"; 7 | return m.reply( 8 | `Bot status: ${status}\nUse '${usedPrefix + command} on' or '${usedPrefix + command} off' to change mode.` 9 | ); 10 | } 11 | 12 | switch (text.toLowerCase()) { 13 | case "off": 14 | case "mute": 15 | if (chat.mute) return m.reply("Bot is already OFFLINE."); 16 | chat.mute = true; 17 | return m.reply("Bot is now OFFLINE."); 18 | 19 | case "on": 20 | case "unmute": 21 | if (!chat.mute) return m.reply("Bot is already ONLINE."); 22 | chat.mute = false; 23 | return m.reply("Bot is now ONLINE."); 24 | 25 | default: 26 | return m.reply(`Invalid parameter.\nUsage: ${usedPrefix + command} on | off`); 27 | } 28 | } catch (e) { 29 | conn.logger.error(e); 30 | m.reply(`Error: ${e.message}`); 31 | } 32 | }; 33 | 34 | handler.help = ["botmode"]; 35 | handler.tags = ["group"]; 36 | handler.command = /^(bot(mode)?)$/i; 37 | handler.owner = true; 38 | 39 | export default handler; 40 | -------------------------------------------------------------------------------- /plugins/downloader/downloader-ytvideo.js: -------------------------------------------------------------------------------- 1 | import { ytmp4 } from "#ytmp4"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | if (!args[0]) 5 | return m.reply( 6 | `Please provide a valid YouTube video link.\n› Example: ${usedPrefix + command} https://youtu.be/N2P6ARXAWMQ` 7 | ); 8 | 9 | const url = args[0]; 10 | const youtubeRegex = 11 | /^(https?:\/\/)?((www|m)\.)?(youtube(-nocookie)?\.com\/(watch\?v=|shorts\/|live\/)|youtu\.be\/)[\w-]+(\S+)?$/i; 12 | if (!youtubeRegex.test(url)) 13 | return m.reply("Invalid URL. Only standard YouTube video links are supported."); 14 | 15 | await global.loading(m, conn); 16 | 17 | try { 18 | const { success, downloadUrl, error } = await ytmp4(url); 19 | if (!success) throw new Error(error); 20 | 21 | await conn.sendMessage( 22 | m.chat, 23 | { 24 | video: { url: downloadUrl }, 25 | mimetype: "video/mp4", 26 | }, 27 | { quoted: m } 28 | ); 29 | } catch (e) { 30 | conn.logger.error(e); 31 | m.reply(`Error: ${e.message}`); 32 | } finally { 33 | await global.loading(m, conn, true); 34 | } 35 | }; 36 | 37 | handler.help = ["ytmp4"]; 38 | handler.tags = ["downloader"]; 39 | handler.command = /^(ytmp4)$/i; 40 | 41 | export default handler; 42 | -------------------------------------------------------------------------------- /plugins/tool/tool-tomp3.js: -------------------------------------------------------------------------------- 1 | import { convert } from "#add-on"; 2 | 3 | let handler = async (m, { conn, usedPrefix, command }) => { 4 | try { 5 | const q = m.quoted ? m.quoted : m; 6 | const mime = (q.msg || q).mimetype || q.mediaType || ""; 7 | 8 | if (!mime || !/^(video|audio)\//.test(mime)) 9 | return m.reply(`Reply a video or audio with command:\n› ${usedPrefix + command}`); 10 | 11 | await global.loading(m, conn); 12 | 13 | const buffer = await q.download?.(); 14 | if (!Buffer.isBuffer(buffer)) return m.reply("Failed to fetch media buffer."); 15 | 16 | const audio = await convert(buffer, { format: "mp3" }); 17 | if (!Buffer.isBuffer(audio) || !audio.length) 18 | return m.reply("Conversion failed: empty result."); 19 | 20 | await conn.sendMessage( 21 | m.chat, 22 | { 23 | audio, 24 | mimetype: "audio/mpeg", 25 | fileName: "output.mp3", 26 | }, 27 | { quoted: m } 28 | ); 29 | } catch (e) { 30 | conn.logger.error(e); 31 | m.reply(`Error: ${e.message}`); 32 | } finally { 33 | await global.loading(m, conn, true); 34 | } 35 | }; 36 | 37 | handler.help = ["tomp3"]; 38 | handler.tags = ["tools"]; 39 | handler.command = /^(tomp3|toaudio)$/i; 40 | 41 | export default handler; 42 | -------------------------------------------------------------------------------- /lib/api/ytmp4.js: -------------------------------------------------------------------------------- 1 | export async function ytmp4(url) { 2 | const encoded = encodeURIComponent(url); 3 | const endpoints = [ 4 | `https://api.nekolabs.web.id/downloader/youtube/v1?url=${encoded}&format=720`, 5 | `https://api-faa.my.id/faa/ytmp4?url=${encoded}`, 6 | `https://api.kyyokatsu.my.id/api/downloader/ytmp4?url=${encoded}`, 7 | `https://api.rikishop.my.id/download/ytmp4?url=${encoded}`, 8 | ]; 9 | 10 | for (const endpoint of endpoints) { 11 | const res = await fetch(endpoint).catch(() => null); 12 | if (!res) continue; 13 | 14 | const json = await res.json().catch(() => null); 15 | if (!json || (!json.success && !json.status)) continue; 16 | 17 | const downloadUrl = 18 | json.result?.downloadUrl || 19 | json.result?.download_url || 20 | json.result?.mp4 || 21 | json.result?.url; 22 | 23 | const isVideo = 24 | json.result?.type === "video" || 25 | json.result?.format === "mp4" || 26 | json.result?.mp4 || 27 | json.result?.url; 28 | 29 | if (downloadUrl && isVideo) { 30 | return { 31 | success: true, 32 | downloadUrl, 33 | }; 34 | } 35 | } 36 | 37 | return { success: false, error: "Failed to retrieve video. Use .ytmp3 for audio-only links." }; 38 | } 39 | -------------------------------------------------------------------------------- /plugins/downloader/downloader-ytaudio.js: -------------------------------------------------------------------------------- 1 | import { ytmp3 } from "#ytmp3"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | if (!args[0]) 5 | return m.reply( 6 | `Please provide a valid YouTube or YouTube Music link.\n› Example: ${usedPrefix + command} https://music.youtube.com` 7 | ); 8 | 9 | const url = args[0]; 10 | const youtubeRegex = 11 | /^(https?:\/\/)?((www|m|music)\.)?(youtube(-nocookie)?\.com\/(watch\?v=|shorts\/|live\/)|youtu\.be\/)[\w-]+(\S+)?$/i; 12 | if (!youtubeRegex.test(url)) 13 | return m.reply("Invalid URL! Please provide a valid YouTube or YouTube Music link."); 14 | 15 | await global.loading(m, conn); 16 | 17 | try { 18 | const { success, downloadUrl, error } = await ytmp3(url); 19 | if (!success) throw new Error(error); 20 | 21 | await conn.sendMessage( 22 | m.chat, 23 | { 24 | audio: { url: downloadUrl }, 25 | mimetype: "audio/mpeg", 26 | }, 27 | { quoted: m } 28 | ); 29 | } catch (e) { 30 | conn.logger.error(e); 31 | m.reply(`Error: ${e.message}`); 32 | } finally { 33 | await global.loading(m, conn, true); 34 | } 35 | }; 36 | 37 | handler.help = ["ytmp3"]; 38 | handler.tags = ["downloader"]; 39 | handler.command = /^(ytmp3)$/i; 40 | 41 | export default handler; 42 | -------------------------------------------------------------------------------- /plugins/owner/owner-block.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | try { 3 | let target = m.mentionedJid?.[0] || m.quoted?.sender || null; 4 | 5 | if (!target && args[0] && /^\d{5,}$/.test(args[0])) { 6 | const pn = args[0].replace(/[^0-9]/g, "") + "@s.whatsapp.net"; 7 | const lid = await conn.signalRepository.lidMapping.getLIDForPN(pn); 8 | target = lid || pn; 9 | } 10 | 11 | if (!target && args[0]) { 12 | const raw = args[0].replace(/[^0-9]/g, "") + "@lid"; 13 | target = raw; 14 | } 15 | 16 | if (!target) { 17 | return m.reply( 18 | `Specify one valid JID to block.\n› Example: ${usedPrefix + command} @628xxxx` 19 | ); 20 | } 21 | 22 | await conn.updateBlockStatus(target, "block"); 23 | 24 | await conn.sendMessage( 25 | m.chat, 26 | { 27 | text: `Successfully blocked @${target.split("@")[0]}.`, 28 | mentions: [target], 29 | }, 30 | { quoted: m } 31 | ); 32 | } catch (e) { 33 | conn.logger.error(e); 34 | m.reply(`Error: ${e.message}`); 35 | } 36 | }; 37 | 38 | handler.help = ["block"]; 39 | handler.tags = ["owner"]; 40 | handler.command = /^block$/i; 41 | handler.owner = true; 42 | 43 | export default handler; 44 | -------------------------------------------------------------------------------- /plugins/owner/owner-unblock.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | try { 3 | let target = m.mentionedJid?.[0] || m.quoted?.sender || null; 4 | 5 | if (!target && args[0] && /^\d{5,}$/.test(args[0])) { 6 | const pn = args[0].replace(/[^0-9]/g, "") + "@s.whatsapp.net"; 7 | const lid = await conn.signalRepository.lidMapping.getLIDForPN(pn); 8 | target = lid || pn; 9 | } 10 | 11 | if (!target && args[0]) { 12 | const raw = args[0].replace(/[^0-9]/g, "") + "@lid"; 13 | target = raw; 14 | } 15 | 16 | if (!target) { 17 | return m.reply( 18 | `Specify one valid JID to unblock.\n› Example: ${usedPrefix + command} @628xxxx` 19 | ); 20 | } 21 | 22 | await conn.updateBlockStatus(target, "unblock"); 23 | 24 | await conn.sendMessage( 25 | m.chat, 26 | { 27 | text: `Successfully unblocked @${target.split("@")[0]}.`, 28 | mentions: [target], 29 | }, 30 | { quoted: m } 31 | ); 32 | } catch (e) { 33 | conn.logger.error(e); 34 | m.reply(`Error: ${e.message}`); 35 | } 36 | }; 37 | 38 | handler.help = ["unblock"]; 39 | handler.tags = ["owner"]; 40 | handler.command = /^unblock$/i; 41 | handler.owner = true; 42 | 43 | export default handler; 44 | -------------------------------------------------------------------------------- /plugins/internet/internet-npm.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text, usedPrefix, command }) => { 2 | try { 3 | if (!text) { 4 | return m.reply( 5 | `Usage: ${usedPrefix + command} \nExample: ${usedPrefix + command} sharp` 6 | ); 7 | } 8 | 9 | await global.loading(m, conn); 10 | 11 | const res = await fetch( 12 | `https://registry.npmjs.com/-/v1/search?text=${encodeURIComponent(text)}` 13 | ); 14 | const { objects } = await res.json(); 15 | 16 | if (!objects.length) { 17 | return m.reply(`No results found for "${text}".`); 18 | } 19 | 20 | const limited = objects.slice(0, 10); 21 | const result = [ 22 | `NPM Search Result for "${text}"`, 23 | "", 24 | ...limited.map( 25 | ({ package: pkg }, i) => 26 | `${i + 1}. ${pkg.name} (v${pkg.version})\n ↳ ${pkg.links.npm}` 27 | ), 28 | ].join("\n"); 29 | 30 | await conn.sendMessage(m.chat, { text: result }, { quoted: m }); 31 | } catch (e) { 32 | conn.logger.error(e); 33 | m.reply(`Error: ${e.message}`); 34 | } finally { 35 | await global.loading(m, conn, true); 36 | } 37 | }; 38 | 39 | handler.help = ["npmsearch"]; 40 | handler.tags = ["internet"]; 41 | handler.command = /^(npm(js|search)?)$/i; 42 | 43 | export default handler; 44 | -------------------------------------------------------------------------------- /plugins/group/group-kick.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, participants, usedPrefix, command }) => { 2 | let target = m.mentionedJid?.[0] || m.quoted?.sender || null; 3 | 4 | if (!target && args[0]) { 5 | const pn = args[0].replace(/[^0-9]/g, "") + "@s.whatsapp.net"; 6 | const lid = await conn.signalRepository.lidMapping.getLIDForPN(pn); 7 | if (lid) target = lid; 8 | } 9 | 10 | if (!target && args[0]) { 11 | const raw = args[0].replace(/[^0-9]/g, "") + "@lid"; 12 | if (participants.some((p) => p.id === raw)) target = raw; 13 | } 14 | 15 | if (!target || !participants.some((p) => p.id === target)) 16 | return m.reply( 17 | `Specify one valid member to remove.\n› Example: ${usedPrefix + command} @628xxxx` 18 | ); 19 | 20 | try { 21 | await conn.groupParticipantsUpdate(m.chat, [target], "remove"); 22 | await conn.sendMessage( 23 | m.chat, 24 | { 25 | text: `Successfully removed @${target.split("@")[0]}.`, 26 | mentions: [target], 27 | }, 28 | { quoted: m } 29 | ); 30 | } catch (e) { 31 | conn.logger.error(e); 32 | m.reply(`Error: ${e.message}`); 33 | } 34 | }; 35 | 36 | handler.help = ["kick"]; 37 | handler.tags = ["group"]; 38 | handler.command = /^(kick|k)$/i; 39 | handler.group = true; 40 | handler.botAdmin = true; 41 | handler.admin = true; 42 | 43 | export default handler; 44 | -------------------------------------------------------------------------------- /plugins/group/group-pin.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | if (!m.quoted) return m.reply("Reply a message to pin."); 3 | 4 | if (!args[0]) { 5 | return m.reply( 6 | `Specify duration.\n\nExamples:\n` + 7 | `› ${usedPrefix + command} 1 = 1 day\n` + 8 | `› ${usedPrefix + command} 2 = 7 days\n` + 9 | `› ${usedPrefix + command} 3 = 30 days` 10 | ); 11 | } 12 | 13 | const durations = { 14 | 1: { seconds: 86400, label: "1 day" }, 15 | 2: { seconds: 604800, label: "7 days" }, 16 | 3: { seconds: 2592000, label: "30 days" }, 17 | }; 18 | 19 | const selected = durations[args[0]]; 20 | if (!selected) return m.reply("Invalid option. Use 1, 2, or 3 only."); 21 | 22 | const quotedKey = m.quoted?.vM?.key; 23 | if (!quotedKey) return m.reply("Cannot pin: quoted message key not found"); 24 | try { 25 | await conn.sendMessage(m.chat, { 26 | pin: quotedKey, 27 | type: 1, 28 | time: selected.seconds, 29 | }); 30 | m.reply(`Message pinned for ${selected.label}.`); 31 | } catch (e) { 32 | conn.logger.error(e); 33 | m.reply(`Error: ${e.message}`); 34 | } 35 | }; 36 | 37 | handler.help = ["pin"]; 38 | handler.tags = ["group"]; 39 | handler.command = /^(pin)$/i; 40 | handler.group = true; 41 | handler.admin = true; 42 | handler.botAdmin = true; 43 | 44 | export default handler; 45 | -------------------------------------------------------------------------------- /plugins/tool/tool-cekresolution.js: -------------------------------------------------------------------------------- 1 | import sharp from "sharp"; 2 | 3 | let handler = async (m, { conn }) => { 4 | let q, mime; 5 | 6 | if (m.message?.imageMessage) { 7 | q = m.message.imageMessage; 8 | mime = q.mimetype; 9 | } else if (m.quoted) { 10 | q = m.quoted.msg || m.quoted; 11 | mime = q.mimetype || ""; 12 | } 13 | 14 | if (!mime || !/image\/(jpe?g|png|webp)/.test(mime)) 15 | return m.reply("Send or reply to an image to check its resolution."); 16 | 17 | try { 18 | const buffer = await q.download?.().catch(() => null); 19 | if (!buffer || !buffer.length) return m.reply("Failed to download the image."); 20 | 21 | const { width, height } = await sharp(buffer).metadata(); 22 | const sizeKB = (buffer.length / 1024).toFixed(2); 23 | 24 | const text = ` 25 | Image Resolution 26 | Width: ${width}px 27 | Height: ${height}px 28 | File Size: ${sizeKB} KB 29 | ────────────────── 30 | Image metadata retrieved successfully. 31 | `.trim(); 32 | 33 | await conn.sendMessage( 34 | m.chat, 35 | { 36 | image: buffer, 37 | caption: text, 38 | }, 39 | { quoted: m } 40 | ); 41 | } catch (e) { 42 | conn.logger.error(e); 43 | m.reply(`Error: ${e.message}`); 44 | } 45 | }; 46 | 47 | handler.help = ["cekresolution"]; 48 | handler.tags = ["tools"]; 49 | handler.command = /^(cekreso(lution)?)$/i; 50 | 51 | export default handler; 52 | -------------------------------------------------------------------------------- /plugins/owner/owner-upch.js: -------------------------------------------------------------------------------- 1 | /* todo: 2 | let handler = async (m, { conn, text }) => { 3 | try { 4 | const q = m.quoted ? m.quoted : m; 5 | const mime = q.mimetype || ""; 6 | 7 | await global.loading(m, conn); 8 | 9 | const jid = "120363417411850319@newsletter"; 10 | const caption = text ? text.trim() : ""; 11 | 12 | if (/image|video|audio/.test(mime)) { 13 | const media = await q.download(); 14 | if (!Buffer.isBuffer(media)) throw new Error("Invalid media buffer"); 15 | 16 | const message = /audio/.test(mime) 17 | ? { audio: media, mimetype: mime, ptt: true, caption } 18 | : /video/.test(mime) 19 | ? { video: media, mimetype: mime, caption } 20 | : { image: media, mimetype: mime, caption }; 21 | 22 | await conn.sendMessage(jid, message, { quoted: m }); 23 | } else if (text) { 24 | await conn.sendMessage(jid, { text }, { quoted: m }); 25 | } else { 26 | return m.reply("Provide media or text to send."); 27 | } 28 | 29 | await m.reply("Message successfully sent to channel."); 30 | } catch (e) { 31 | conn.logger.error(e); 32 | m.reply(`Error: ${e.message}`); 33 | } finally { 34 | await global.loading(m, conn, true); 35 | } 36 | }; 37 | 38 | handler.help = ["upch"]; 39 | handler.tags = ["owner"]; 40 | handler.command = /^(ch|upch)$/i; 41 | handler.owner = true; 42 | 43 | export default handler; 44 | */ 45 | -------------------------------------------------------------------------------- /plugins/tool/tool-removebg.js: -------------------------------------------------------------------------------- 1 | import { removebg } from "#removebg"; 2 | 3 | let handler = async (m, { conn, command, usedPrefix }) => { 4 | const q = m.quoted && m.quoted.mimetype ? m.quoted : m; 5 | const mime = (q.msg || q).mimetype || ""; 6 | 7 | if (!q || typeof q.download !== "function" || !/image\/(jpe?g|png|webp)/i.test(mime)) { 8 | return m.reply( 9 | `Please send or reply to an image before using this command.\nExample: ${usedPrefix}${command} < reply to image or send image with caption` 10 | ); 11 | } 12 | 13 | try { 14 | await global.loading(m, conn); 15 | 16 | const img = await q.download().catch(() => null); 17 | if (!img || !(img instanceof Buffer)) return; 18 | 19 | const { success, resultUrl, resultBuffer, error } = await removebg(img); 20 | if (!success) throw new Error(error || "Background removal failed"); 21 | 22 | await conn.sendMessage( 23 | m.chat, 24 | { 25 | image: resultBuffer ? { buffer: resultBuffer } : { url: resultUrl }, 26 | caption: "Background removed successfully.", 27 | }, 28 | { quoted: m } 29 | ); 30 | } catch (e) { 31 | conn.logger.error(e); 32 | m.reply("Failed to remove background."); 33 | } finally { 34 | await global.loading(m, conn, true); 35 | } 36 | }; 37 | 38 | handler.help = ["removebg"]; 39 | handler.tags = ["tools"]; 40 | handler.command = /^(removebg)$/i; 41 | 42 | export default handler; 43 | -------------------------------------------------------------------------------- /plugins/group/group-demote.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, participants, usedPrefix, command }) => { 2 | try { 3 | let target = m.mentionedJid?.[0] || m.quoted?.sender || null; 4 | 5 | if (!target && args[0] && /^\d{5,}$/.test(args[0])) { 6 | const pn = args[0].replace(/[^0-9]/g, "") + "@s.whatsapp.net"; 7 | const lid = await conn.signalRepository.lidMapping.getLIDForPN(pn); 8 | if (lid) target = lid; 9 | } 10 | 11 | if (!target && args[0]) { 12 | const raw = args[0].replace(/[^0-9]/g, "") + "@lid"; 13 | if (participants.some((p) => p.id === raw)) { 14 | target = raw; 15 | } 16 | } 17 | 18 | if (!target || !participants.some((p) => p.id === target)) 19 | throw `Specify one valid member to demote.\n› Example: ${usedPrefix + command} @628xxxx`; 20 | 21 | await conn.groupParticipantsUpdate(m.chat, [target], "demote"); 22 | 23 | await conn.sendMessage( 24 | m.chat, 25 | { 26 | text: `Successfully demoted @${target.split("@")[0]}.`, 27 | mentions: [target], 28 | }, 29 | { quoted: m } 30 | ); 31 | } catch (e) { 32 | conn.logger.error(e); 33 | m.reply(`Error: ${e.message}`); 34 | } 35 | }; 36 | 37 | handler.help = ["demote"]; 38 | handler.tags = ["group"]; 39 | handler.command = /^(demote)$/i; 40 | handler.group = true; 41 | handler.botAdmin = true; 42 | handler.admin = true; 43 | 44 | export default handler; 45 | -------------------------------------------------------------------------------- /plugins/group/group-promote.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, participants, usedPrefix, command }) => { 2 | try { 3 | let target = m.mentionedJid?.[0] || m.quoted?.sender || null; 4 | 5 | if (!target && args[0] && /^\d{5,}$/.test(args[0])) { 6 | const pn = args[0].replace(/[^0-9]/g, "") + "@s.whatsapp.net"; 7 | const lid = await conn.signalRepository.lidMapping.getLIDForPN(pn); 8 | if (lid) target = lid; 9 | } 10 | 11 | if (!target && args[0]) { 12 | const raw = args[0].replace(/[^0-9]/g, "") + "@lid"; 13 | if (participants.some((p) => p.id === raw)) { 14 | target = raw; 15 | } 16 | } 17 | 18 | if (!target || !participants.some((p) => p.id === target)) 19 | throw `Specify one valid member to promote.\n› Example: ${usedPrefix + command} @628xxxx`; 20 | 21 | await conn.groupParticipantsUpdate(m.chat, [target], "promote"); 22 | 23 | await conn.sendMessage( 24 | m.chat, 25 | { 26 | text: `Successfully promoted @${target.split("@")[0]}.`, 27 | mentions: [target], 28 | }, 29 | { quoted: m } 30 | ); 31 | } catch (e) { 32 | conn.logger.error(e); 33 | m.reply(`Error: ${e.message}`); 34 | } 35 | }; 36 | 37 | handler.help = ["promote"]; 38 | handler.tags = ["group"]; 39 | handler.command = /^(promote)$/i; 40 | handler.group = true; 41 | handler.botAdmin = true; 42 | handler.admin = true; 43 | 44 | export default handler; 45 | -------------------------------------------------------------------------------- /plugins/tool/tool-sendviewonce.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, command, usedPrefix }) => { 2 | const q = m.quoted && m.quoted.mimetype ? m.quoted : m; 3 | const mime = (q.msg || q).mimetype || ""; 4 | 5 | if (!q || typeof q.download !== "function" || !/^(image|video|audio)\//i.test(mime)) { 6 | return m.reply( 7 | `Please send or reply to an image, video, or audio file.\nExample: ${usedPrefix}${command} reply to media or ${usedPrefix}${command} send media with caption` 8 | ); 9 | } 10 | 11 | const buffer = await q.download?.().catch(() => null); 12 | if (!buffer) return m.reply("Failed to retrieve the media."); 13 | 14 | const type = mime.startsWith("image/") 15 | ? "image" 16 | : mime.startsWith("video/") 17 | ? "video" 18 | : mime.startsWith("audio/") 19 | ? "audio" 20 | : null; 21 | 22 | if (!type) return m.reply("Unsupported media type."); 23 | 24 | const rawText = m.text || ""; 25 | const caption = rawText.replace(new RegExp(`^${usedPrefix}${command}\\s*`, "i"), "").trim(); 26 | const mentionMatches = [...caption.matchAll(/@(\d{5,})/g)]; 27 | const mentionedJid = mentionMatches.map((m) => `${m[1]}@lid`); 28 | const contextInfo = mentionedJid.length > 0 ? { mentionedJid } : {}; 29 | await conn.sendMessage(m.chat, { 30 | [type]: buffer, 31 | mimetype: mime, 32 | caption, 33 | contextInfo, 34 | viewOnce: true, 35 | }); 36 | }; 37 | 38 | handler.help = ["svo"]; 39 | handler.tags = ["tools"]; 40 | handler.command = /^(send(view(once)?)?|svo)$/i; 41 | 42 | export default handler; 43 | -------------------------------------------------------------------------------- /plugins/tool/tool-getlid.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text }) => { 2 | try { 3 | await global.loading(m, conn); 4 | 5 | const input = 6 | m.mentionedJid?.[0] || 7 | m.quoted?.sender || 8 | (text && /^\d+$/.test(text) ? text + "@s.whatsapp.net" : null); 9 | 10 | if (!input) return m.reply("Enter a number, mention, or reply to a user."); 11 | 12 | let lid; 13 | 14 | if (/@lid$/.test(input)) { 15 | lid = input.replace(/@lid$/, ""); 16 | } else { 17 | const raw = await conn.signalRepository.lidMapping.getLIDForPN(input); 18 | if (!raw) return m.reply("Cannot resolve LID for this user."); 19 | lid = raw.replace(/@lid$/, ""); 20 | } 21 | 22 | await conn.sendButton(m.chat, { 23 | text: `Target LID: ${lid}`, 24 | title: "Result", 25 | footer: "Use the button below to copy the LID", 26 | interactiveButtons: [ 27 | { 28 | name: "cta_copy", 29 | buttonParamsJson: JSON.stringify({ 30 | display_text: "Copy LID", 31 | copy_code: lid, 32 | }), 33 | }, 34 | ], 35 | hasMediaAttachment: false, 36 | }); 37 | } catch (e) { 38 | conn.logger.error(e); 39 | m.reply(`Error: ${e.message}`); 40 | } finally { 41 | await global.loading(m, conn, true); 42 | } 43 | }; 44 | 45 | handler.help = ["getlid"]; 46 | handler.tags = ["tools"]; 47 | handler.command = /^getlid$/i; 48 | 49 | export default handler; 50 | -------------------------------------------------------------------------------- /plugins/tool/tool-readviewonce.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | const q = m.quoted; 3 | try { 4 | const msg = q?.msg?.message?.viewOnceMessageV2?.message || q?.msg || q?.message || {}; 5 | const mediaMsg = 6 | msg.imageMessage || msg.videoMessage || msg.audioMessage || msg.documentMessage; 7 | 8 | if (!mediaMsg) return m.reply("No media found in view-once message."); 9 | 10 | const buffer = await q.download?.(); 11 | if (!buffer) return m.reply("Failed to retrieve media."); 12 | 13 | const mime = mediaMsg.mimetype || ""; 14 | const type = mime.startsWith("image/") 15 | ? "image" 16 | : mime.startsWith("video/") 17 | ? "video" 18 | : mime.startsWith("audio/") 19 | ? "audio" 20 | : null; 21 | 22 | if (!type) return m.reply("Unsupported media type."); 23 | 24 | const caption = mediaMsg.caption || q.text || ""; 25 | const contextInfo = {}; 26 | if (mediaMsg.contextInfo?.mentionedJid) { 27 | contextInfo.mentionedJid = mediaMsg.contextInfo.mentionedJid; 28 | } 29 | 30 | await conn.sendMessage( 31 | m.chat, 32 | { 33 | [type]: buffer, 34 | mimetype: mime, 35 | caption, 36 | contextInfo, 37 | }, 38 | { quoted: q.fakeObj } 39 | ); 40 | } catch (e) { 41 | conn.logger.error(e); 42 | m.reply(`Error: ${e.message}`); 43 | } 44 | }; 45 | 46 | handler.help = ["readviewonce"]; 47 | handler.tags = ["tools"]; 48 | handler.command = /^(read(view(once)?)?|rvo)$/i; 49 | 50 | export default handler; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ========================= 2 | # Node Package Manager Lockfiles 3 | # ========================= 4 | package-lock.json 5 | yarn.lock 6 | pnpm-lock.yaml 7 | shrinkwrap.yaml 8 | npm-shrinkwrap.json 9 | 10 | # ========================= 11 | # Node 12 | # ========================= 13 | node_modules/ 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # ========================= 20 | # Build artifacts 21 | # ========================= 22 | build/ 23 | target/ 24 | dist/ 25 | *.o 26 | *.so 27 | *.dll 28 | *.dylib 29 | *.obj 30 | *.pdb 31 | *.ilk 32 | *.pch 33 | 34 | # ========================= 35 | # Logs 36 | # ========================= 37 | *.log 38 | logs/ 39 | *.log.* 40 | 41 | # ========================= 42 | # Cache / temp 43 | # ========================= 44 | .cache/ 45 | tmp/ 46 | *.tmp 47 | *.swp 48 | *.swo 49 | *~ 50 | .DS_Store 51 | .vscode/ 52 | .idea/ 53 | *.tsbuildinfo 54 | 55 | # ========================= 56 | # Environment 57 | # ========================= 58 | .env 59 | .env.local 60 | .env.*.local 61 | 62 | # ========================= 63 | # OS / Misc 64 | # ========================= 65 | Thumbs.db 66 | desktop.ini 67 | .pid 68 | *.pid.lock 69 | 70 | # ========================= 71 | # Database (SQLite) 72 | # ========================= 73 | database/*.db 74 | database/*.sqlite 75 | database/*.sqlite3 76 | *.db-wal 77 | *.db-shm 78 | 79 | # ========================= 80 | # Secrets 81 | # ========================= 82 | keys/ 83 | secrets/ 84 | *.key 85 | *.pem 86 | *.crt 87 | *.p12 88 | *.enc 89 | 90 | # ========================= 91 | # Coverage / Testing 92 | # ========================= 93 | coverage/ 94 | coverage-final.json 95 | .nyc_output/ 96 | benchmark-results/ -------------------------------------------------------------------------------- /plugins/downloader/downloader-twitter.js: -------------------------------------------------------------------------------- 1 | import { twitter } from "#twitter"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | if (!args[0]) 5 | return m.reply( 6 | `Please provide a valid Twitter/X URL.\n› Example: ${usedPrefix + command} https://x.com` 7 | ); 8 | 9 | await global.loading(m, conn); 10 | try { 11 | const { success, photos, video, error } = await twitter(args[0]); 12 | if (!success) throw new Error(error); 13 | 14 | if (photos?.length === 1) { 15 | await conn.sendMessage( 16 | m.chat, 17 | { image: { url: photos[0] }, caption: null }, 18 | { quoted: m } 19 | ); 20 | } else if (photos?.length > 1) { 21 | const album = photos.map((img, i) => ({ 22 | image: { url: img }, 23 | caption: `Slide ${i + 1} of ${photos.length}`, 24 | })); 25 | await conn.sendAlbum(m.chat, album, { quoted: m }); 26 | } else if (video) { 27 | await conn.sendMessage( 28 | m.chat, 29 | { 30 | video: { url: video }, 31 | fileName: `twitter.mp4`, 32 | }, 33 | { quoted: m } 34 | ); 35 | } else { 36 | throw new Error("No media found in this tweet."); 37 | } 38 | } catch (e) { 39 | conn.logger.error(e); 40 | m.reply(`Error: ${e.message}`); 41 | } finally { 42 | await global.loading(m, conn, true); 43 | } 44 | }; 45 | 46 | handler.help = ["twitter"]; 47 | handler.tags = ["downloader"]; 48 | handler.command = /^(twitter)$/i; 49 | 50 | export default handler; 51 | -------------------------------------------------------------------------------- /lib/cpp/core/converter_core.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace ConverterCore { 8 | 9 | struct ConvertOptions { 10 | std::string format{"opus"}; 11 | int64_t bitrate{64000}; 12 | int channels{2}; 13 | int sampleRate{48000}; 14 | bool ptt{false}; 15 | bool vbr{true}; 16 | 17 | void validate() { 18 | if (channels < 1) 19 | channels = 1; 20 | if (channels > 2) 21 | channels = 2; 22 | if (sampleRate < 8000) 23 | sampleRate = 8000; 24 | if (sampleRate > 96000) 25 | sampleRate = 96000; 26 | if (bitrate < 8000) 27 | bitrate = 8000; 28 | if (bitrate > 320000) 29 | bitrate = 320000; 30 | 31 | if (format == "ogg" || format == "ogg_opus" || format == "opus_ogg") { 32 | format = "opus"; 33 | } 34 | } 35 | 36 | bool isValidFormat() const { 37 | return format == "opus" || format == "mp3" || format == "aac" || format == "m4a" || 38 | format == "wav"; 39 | } 40 | }; 41 | 42 | struct ConvertResult { 43 | std::vector data; 44 | std::string error; 45 | bool success{false}; 46 | 47 | ConvertResult() = default; 48 | ConvertResult(std::vector&& d) : data(std::move(d)), success(true) {} 49 | ConvertResult(const std::string& err) : error(err), success(false) {} 50 | }; 51 | 52 | ConvertResult ConvertAudio(const uint8_t* input_data, 53 | size_t input_size, 54 | const ConvertOptions& options) noexcept; 55 | 56 | int64_t ParseBitrate(const std::string& bitrate_str, int64_t default_value) noexcept; 57 | 58 | } // namespace ConverterCore -------------------------------------------------------------------------------- /plugins/downloader/downloader-threads.js: -------------------------------------------------------------------------------- 1 | import { threads } from "#threads"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | const url = args[0]; 5 | if (!url) 6 | return m.reply( 7 | `Please provide a valid Threads URL.\n› Example: ${usedPrefix + command} https://www.threads.net` 8 | ); 9 | 10 | await global.loading(m, conn); 11 | 12 | try { 13 | const { success, caption, images, videos, error } = await threads(url); 14 | if (!success) throw new Error(error); 15 | 16 | if (videos.length > 0) { 17 | const videoUrl = videos[videos.length - 1]; 18 | await conn.sendMessage(m.chat, { video: { url: videoUrl }, caption }, { quoted: m }); 19 | } else if (images.length > 0) { 20 | if (images.length === 1) { 21 | await conn.sendMessage( 22 | m.chat, 23 | { image: { url: images[0] }, caption }, 24 | { quoted: m } 25 | ); 26 | } else { 27 | const album = images.map((img, i) => ({ 28 | image: { url: img }, 29 | caption: `Slide ${i + 1} of ${images.length}`, 30 | })); 31 | await conn.sendAlbum(m.chat, album, { quoted: m }); 32 | } 33 | } else { 34 | throw new Error("No media found in this Threads post."); 35 | } 36 | } catch (e) { 37 | conn.logger.error(e); 38 | m.reply(`Error: ${e.message}`); 39 | } finally { 40 | await global.loading(m, conn, true); 41 | } 42 | }; 43 | 44 | handler.help = ["threads"]; 45 | handler.tags = ["downloader"]; 46 | handler.command = /^(threads)$/i; 47 | 48 | export default handler; 49 | -------------------------------------------------------------------------------- /plugins/tool/tool-toptt.js: -------------------------------------------------------------------------------- 1 | import { convert } from "#add-on"; 2 | 3 | let handler = async (m, { conn, usedPrefix, command }) => { 4 | try { 5 | const q = m.quoted ? m.quoted : m; 6 | const mime = (q.msg || q).mimetype || q.mediaType || ""; 7 | 8 | if (!mime || !/^(video|audio)\//.test(mime)) 9 | return m.reply(`Reply a video or audio with command:\n› ${usedPrefix + command}`); 10 | 11 | await global.loading(m, conn); 12 | 13 | const buffer = await q.download?.(); 14 | if (!Buffer.isBuffer(buffer) || buffer.length === 0) 15 | return m.reply("Failed to get media buffer."); 16 | 17 | const audio = await convert(buffer, { 18 | format: "opus", 19 | sampleRate: 48000, 20 | channels: 1, 21 | bitrate: "64k", 22 | ptt: true, 23 | }); 24 | 25 | const finalBuffer = 26 | audio instanceof Buffer 27 | ? audio 28 | : audio?.buffer 29 | ? Buffer.from(audio.buffer) 30 | : audio?.data 31 | ? Buffer.from(audio.data) 32 | : Buffer.from(audio); 33 | 34 | await conn.sendMessage( 35 | m.chat, 36 | { 37 | audio: finalBuffer, 38 | mimetype: "audio/ogg; codecs=opus", 39 | ptt: true, 40 | }, 41 | { quoted: m } 42 | ); 43 | } catch (e) { 44 | conn.logger.error(e); 45 | m.reply(`Error: ${e.message}`); 46 | } finally { 47 | await global.loading(m, conn, true); 48 | } 49 | }; 50 | 51 | handler.help = ["toptt"]; 52 | handler.tags = ["tools"]; 53 | handler.command = /^(toptt|tovn)$/i; 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /plugins/downloader/downloader-tiktok.js: -------------------------------------------------------------------------------- 1 | import { tiktok } from "#tiktok"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | const url = args[0]; 5 | if (!url) 6 | return m.reply( 7 | `Please provide a valid TikTok link.\n› Example: ${usedPrefix + command} https://vt.tiktok.com` 8 | ); 9 | if (!/^https?:\/\/(www\.)?(vm\.|vt\.|m\.)?tiktok\.com\/.+/i.test(url)) 10 | return m.reply("Invalid URL. Please provide a proper TikTok link."); 11 | 12 | await global.loading(m, conn); 13 | 14 | try { 15 | const { success, type, images, videoUrl, error } = await tiktok(url); 16 | if (!success) throw new Error(error || "Failed to fetch media."); 17 | 18 | if (type === "images") { 19 | if (images.length === 1) { 20 | await conn.sendMessage(m.chat, { image: { url: images[0] } }, { quoted: m }); 21 | } else { 22 | const album = images.map((img, i) => ({ 23 | image: { url: img }, 24 | caption: `Slide ${i + 1} of ${images.length}`, 25 | })); 26 | await conn.sendAlbum(m.chat, album, { quoted: m }); 27 | } 28 | } else if (type === "video") { 29 | await conn.sendMessage( 30 | m.chat, 31 | { video: { url: videoUrl }, mimetype: "video/mp4" }, 32 | { quoted: m } 33 | ); 34 | } 35 | } catch (e) { 36 | conn.logger.error(e); 37 | m.reply(`Error: ${e.message}`); 38 | } finally { 39 | await global.loading(m, conn, true); 40 | } 41 | }; 42 | 43 | handler.help = ["tiktok"]; 44 | handler.tags = ["downloader"]; 45 | handler.command = /^(tiktok|tt)$/i; 46 | 47 | export default handler; 48 | -------------------------------------------------------------------------------- /plugins/ai/ai-feloai.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text, usedPrefix, command }) => { 2 | if (!text || typeof text !== "string") { 3 | return m.reply( 4 | `Please enter a query for Felo AI.\n› Example: ${usedPrefix}${command} what date is it today?` 5 | ); 6 | } 7 | 8 | try { 9 | await global.loading(m, conn); 10 | 11 | const apiUrl = `https://api.nekolabs.web.id/text-generation/feloai?text=${encodeURIComponent(text)}`; 12 | const response = await fetch(apiUrl); 13 | 14 | if (!response.ok) { 15 | return m.reply("Failed to connect to Felo AI. Please try again later."); 16 | } 17 | 18 | const json = await response.json(); 19 | const result = json?.result; 20 | const replyText = result?.text; 21 | 22 | if (!replyText) { 23 | return m.reply("No response received from Felo AI."); 24 | } 25 | 26 | let sources = ""; 27 | if (Array.isArray(result?.sources) && result.sources.length > 0) { 28 | sources = 29 | "\n\n*Sources:*\n" + 30 | result.sources 31 | .slice(0, 10) 32 | .map((src) => `${src.index}. ${src.title || "Untitled"}\n${src.url}`) 33 | .join("\n\n"); 34 | } 35 | 36 | await conn.sendMessage( 37 | m.chat, 38 | { text: `Felo AI:\n${replyText.trim()}${sources}` }, 39 | { quoted: m } 40 | ); 41 | } catch (e) { 42 | conn.logger.error(e); 43 | m.reply(`Error: ${e.message}`); 44 | } finally { 45 | await global.loading(m, conn, true); 46 | } 47 | }; 48 | 49 | handler.help = ["feloai"]; 50 | handler.tags = ["ai"]; 51 | handler.command = /^(feloai)$/i; 52 | 53 | export default handler; 54 | -------------------------------------------------------------------------------- /plugins/owner/owner-status.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text }) => { 2 | const quoted = m.quoted; 3 | if (!quoted) return m.reply("Reply to a media message to send status mentions."); 4 | 5 | let content = {}; 6 | 7 | try { 8 | const groupData = await conn.groupFetchAllParticipating(); 9 | const groupJids = Object.values(groupData) 10 | .map((g) => g.id) 11 | .filter((id) => id.endsWith("@g.us")) 12 | .slice(0, 5); 13 | 14 | if (!groupJids.length) return m.reply("No active group found."); 15 | 16 | await global.loading(m, conn); 17 | 18 | const mime = (quoted.msg || quoted).mimetype || ""; 19 | const mediaBuffer = await quoted.download(); 20 | if (!mediaBuffer) return m.reply("Failed to download media."); 21 | 22 | if (/image/.test(mime)) { 23 | content = { image: mediaBuffer, caption: text || "" }; 24 | } else if (/video/.test(mime)) { 25 | content = { video: mediaBuffer, caption: text || "" }; 26 | } else if (/audio/.test(mime)) { 27 | content = { 28 | audio: mediaBuffer, 29 | mimetype: "audio/mpeg", 30 | ptt: true, 31 | }; 32 | } else { 33 | return m.reply("Unsupported media type."); 34 | } 35 | 36 | await conn.sendStatusMentions(content, groupJids); 37 | m.reply(`Status mention sent to ${groupJids.length} group(s).`); 38 | } catch (e) { 39 | conn.logger.error(e); 40 | m.reply(`Error: ${e.message}`); 41 | } finally { 42 | await global.loading(m, conn, true); 43 | } 44 | }; 45 | 46 | handler.help = ["tagsw"]; 47 | handler.tags = ["owner"]; 48 | handler.command = /^(tagsw)$/i; 49 | handler.owner = true; 50 | 51 | export default handler; 52 | -------------------------------------------------------------------------------- /lib/api/twitter.js: -------------------------------------------------------------------------------- 1 | export async function twitter(url) { 2 | const encoded = encodeURIComponent(url); 3 | const endpoints = [ 4 | `https://api.nekolabs.web.id/downloader/twitter?url=${encoded}`, 5 | `https://api.ootaizumi.web.id/downloader/twitter?url=${encoded}`, 6 | `https://anabot.my.id/api/download/twitter?url=${encoded}&apikey=freeApikey`, 7 | ]; 8 | 9 | for (const endpoint of endpoints) { 10 | const res = await fetch(endpoint).catch(() => null); 11 | if (!res) continue; 12 | 13 | const json = await res.json().catch(() => null); 14 | if (!json || (!json.success && !json.status)) continue; 15 | 16 | const raw = 17 | json.result?.media || // Nekolabs 18 | json.result || // Ootaizumi 19 | json.data?.result || 20 | []; // AnaBot 21 | 22 | if (!Array.isArray(raw)) continue; 23 | 24 | const photos = raw 25 | .filter( 26 | (m) => 27 | m.type === "photo" || 28 | m.type === "image" || 29 | m.quality?.toLowerCase().includes("photo") || 30 | m.quality?.toLowerCase().includes("download photo") 31 | ) 32 | .map((m) => m.url || m.link) 33 | .filter(Boolean); 34 | 35 | const video = raw.find( 36 | (m) => 37 | (m.type === "video" || m.quality?.toLowerCase().includes("mp4")) && 38 | m.link && 39 | m.link.startsWith("http") 40 | ); 41 | 42 | return { 43 | success: true, 44 | photos, 45 | video: video?.link || video?.variants?.at(-1)?.url || null, 46 | }; 47 | } 48 | 49 | return { success: false, error: "Unable to process this Twitter/X URL." }; 50 | } 51 | -------------------------------------------------------------------------------- /lib/console.js: -------------------------------------------------------------------------------- 1 | export default async function ( 2 | m, 3 | conn = { user: {}, decodeJid: (id) => id, getName: async () => "Unknown", logger: console } 4 | ) { 5 | try { 6 | if (global.db?.data?.settings?.[conn.user?.lid]?.noprint) return; 7 | if (!m || !m.sender || !m.chat || !m.mtype) return; 8 | const sender = conn.decodeJid(m.sender); 9 | const chat = conn.decodeJid(m.chat); 10 | const user = (await conn.getName(sender)) || "Unknown"; 11 | const getIdFormat = (id) => { 12 | if (id?.endsWith("@lid")) return "LID"; 13 | if (id?.endsWith("@s.whatsapp.net")) return "PN"; 14 | if (id?.startsWith("@")) return "Username"; 15 | return "Unknown"; 16 | }; 17 | const getChatContext = (id) => { 18 | if (id?.endsWith("@g.us")) return "Group"; 19 | if (id?.endsWith("@broadcast")) return "Broadcast"; 20 | if (id?.endsWith("@newsletter")) return "Channel"; 21 | if (id?.endsWith("@lid") || id?.endsWith("@s.whatsapp.net")) return "Private"; 22 | return "Unknown"; 23 | }; 24 | const rawText = m.text?.trim() || ""; 25 | const prefixMatch = rawText.match(/^([/!.])\s*(\S+)/); 26 | const prefix = m.prefix || (prefixMatch ? prefixMatch[1] : null); 27 | const command = m.command || (prefixMatch ? prefixMatch[2] : null); 28 | if (!prefix || !command) return; 29 | 30 | const cmd = `${prefix}${command}`; 31 | const idFormat = getIdFormat(sender); 32 | const chatContext = getChatContext(chat); 33 | 34 | conn.logger.info(`${cmd} — ${user}`); 35 | conn.logger.debug(`↳ ${sender} [${idFormat}]`); 36 | conn.logger.debug(`↳ [${chatContext}]`); 37 | } catch (e) { 38 | conn.logger.error(e); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /plugins/maker/maker-smeme.js: -------------------------------------------------------------------------------- 1 | import { sticker } from "#add-on"; 2 | import { uploader2 } from "../../lib/uploader.js"; 3 | 4 | let handler = async (m, { conn, args, usedPrefix, command }) => { 5 | await global.loading(m, conn); 6 | try { 7 | if (!args[0]) 8 | return m.reply( 9 | `Enter top and bottom text for meme (use | separator).\n› Example: ${usedPrefix + command} Top|Bottom` 10 | ); 11 | 12 | const q = m.quoted ? m.quoted : m; 13 | const mime = (q.msg || q).mimetype || ""; 14 | if (!mime || !/image\/(jpeg|png)/.test(mime)) 15 | return m.reply("Reply to a JPG or PNG image to make a meme sticker."); 16 | 17 | const media = await q.download(); 18 | const up = await uploader2(media).catch(() => null); 19 | if (!up) return m.reply("Failed to upload image to server. Try again later."); 20 | 21 | const [top, bottom] = args.join(" ").split("|"); 22 | const apiUrl = `https://api.memegen.link/images/custom/${encodeURIComponent( 23 | top || "_" 24 | )}/${encodeURIComponent(bottom || "_")}.png?background=${encodeURIComponent(up)}`; 25 | 26 | const buffer = Buffer.from(await (await fetch(apiUrl)).arrayBuffer()); 27 | const stickerImage = await sticker(buffer, { 28 | packName: global.config.stickpack || "", 29 | authorName: global.config.stickauth || "", 30 | }); 31 | 32 | await conn.sendMessage(m.chat, { sticker: stickerImage }, { quoted: m }); 33 | } catch (e) { 34 | console.error(e); 35 | m.reply("Error: " + e.message); 36 | } finally { 37 | await global.loading(m, conn, true); 38 | } 39 | }; 40 | 41 | handler.help = ["smeme"]; 42 | handler.tags = ["maker"]; 43 | handler.command = /^(smeme)$/i; 44 | 45 | export default handler; 46 | -------------------------------------------------------------------------------- /lib/api/tiktok.js: -------------------------------------------------------------------------------- 1 | export async function tiktok(url) { 2 | const encoded = encodeURIComponent(url); 3 | const endpoints = [ 4 | `https://tikwm.com/api/?url=${encoded}`, 5 | `https://api.nekolabs.web.id/downloader/tiktok?url=${encoded}`, 6 | `https://api.elrayyxml.web.id/api/downloader/tiktok?url=${encoded}`, 7 | `https://api.ootaizumi.web.id/downloader/tiktok?url=${encoded}`, 8 | `https://anabot.my.id/api/download/tiktok?url=${encoded}&apikey=freeApikey`, 9 | ]; 10 | 11 | for (const endpoint of endpoints) { 12 | const res = await fetch(endpoint).catch(() => null); 13 | if (!res) continue; 14 | 15 | const json = await res.json().catch(() => null); 16 | if (!json || (!json.success && !json.status)) continue; 17 | 18 | const data = json.data?.result || json.result || json.data; 19 | if (!data) continue; 20 | 21 | const images = Array.isArray(data.images) 22 | ? data.images 23 | : Array.isArray(data.image) 24 | ? data.image 25 | : Array.isArray(data.data) 26 | ? data.data 27 | : null; 28 | if (images?.length) { 29 | return { 30 | success: true, 31 | type: "images", 32 | images, 33 | }; 34 | } 35 | 36 | const videoUrl = 37 | data.play || 38 | data.video || 39 | data.videoUrl || 40 | data.hdplay || 41 | (typeof data.data === "string" ? data.data : null); 42 | 43 | if (videoUrl) { 44 | return { 45 | success: true, 46 | type: "video", 47 | videoUrl, 48 | }; 49 | } 50 | } 51 | 52 | return { success: false, error: "No downloadable media found." }; 53 | } 54 | -------------------------------------------------------------------------------- /plugins/tool/tool-remini.js: -------------------------------------------------------------------------------- 1 | import { remini } from "#remini"; 2 | 3 | let handler = async (m, { conn, command, usedPrefix }) => { 4 | const q = m.quoted && m.quoted.mimetype ? m.quoted : m; 5 | const mime = (q.msg || q).mimetype || ""; 6 | 7 | if (!q || typeof q.download !== "function" || !/image\/(jpe?g|png|webp)/i.test(mime)) { 8 | return m.reply( 9 | `Please send or reply to an image before using this command.\nExample: ${usedPrefix}${command} < reply to image or send image with caption` 10 | ); 11 | } 12 | 13 | try { 14 | await global.loading(m, conn); 15 | const media = await q.download().catch(() => null); 16 | if (!media || !Buffer.isBuffer(media)) return m.reply("Invalid image buffer."); 17 | const { success, resultUrl, resultBuffer, error } = await remini(media); 18 | if (!success) throw new Error(error || "Enhancement failed"); 19 | 20 | if (resultBuffer) { 21 | await conn.sendMessage( 22 | m.chat, 23 | { 24 | image: resultBuffer, 25 | caption: "Image enhancement successful.", 26 | }, 27 | { quoted: m } 28 | ); 29 | } else { 30 | await conn.sendMessage( 31 | m.chat, 32 | { 33 | image: { url: resultUrl }, 34 | caption: "Image enhancement successful.", 35 | }, 36 | { quoted: m } 37 | ); 38 | } 39 | } catch (e) { 40 | conn.logger.error(e); 41 | m.reply("Failed to enhance image."); 42 | } finally { 43 | await global.loading(m, conn, true); 44 | } 45 | }; 46 | 47 | handler.help = ["hd"]; 48 | handler.tags = ["tools"]; 49 | handler.command = /^(remini|hd)$/i; 50 | 51 | export default handler; 52 | -------------------------------------------------------------------------------- /plugins/downloader/downloader-ig.js: -------------------------------------------------------------------------------- 1 | import { instagram } from "#instagram"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | const url = args[0]; 5 | if (!url) 6 | return m.reply( 7 | `Please provide a valid Instagram link.\n› Example: ${usedPrefix + command} https://www.instagram.com/p/...` 8 | ); 9 | 10 | if (!/^https?:\/\/(www\.)?instagram\.com\//i.test(url)) 11 | return m.reply("Invalid URL. Please provide a proper Instagram link."); 12 | if (/\/stories\//i.test(url)) 13 | return m.reply("Instagram stories are not supported. Please provide a post or reel URL."); 14 | 15 | await global.loading(m, conn); 16 | 17 | try { 18 | const { success, type, urls, error } = await instagram(url); 19 | if (!success) throw new Error(error || "Failed to fetch media."); 20 | 21 | if (type === "video") { 22 | await conn.sendMessage( 23 | m.chat, 24 | { video: { url: urls[0] }, mimetype: "video/mp4" }, 25 | { quoted: m } 26 | ); 27 | } else if (type === "images") { 28 | if (urls.length === 1) { 29 | await conn.sendMessage(m.chat, { image: { url: urls[0] } }, { quoted: m }); 30 | } else { 31 | const album = urls.map((img, i) => ({ 32 | image: { url: img }, 33 | caption: `Slide ${i + 1} of ${urls.length}`, 34 | })); 35 | await conn.sendAlbum(m.chat, album, { quoted: m }); 36 | } 37 | } 38 | } catch (e) { 39 | conn.logger.error(e); 40 | m.reply(`Error: ${e.message}`); 41 | } finally { 42 | await global.loading(m, conn, true); 43 | } 44 | }; 45 | 46 | handler.help = ["instagram"]; 47 | handler.tags = ["downloader"]; 48 | handler.command = /^(instagram|ig)$/i; 49 | 50 | export default handler; 51 | -------------------------------------------------------------------------------- /plugins/tool/tool-cekid.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix }) => { 2 | try { 3 | const text = args[0]; 4 | if (!text) return m.reply(`Usage: ${usedPrefix}cekid `); 5 | 6 | let url; 7 | try { 8 | url = new URL(text); 9 | } catch { 10 | return m.reply("Invalid link format."); 11 | } 12 | 13 | let isGroup = 14 | url.hostname === "chat.whatsapp.com" && /^\/[A-Za-z0-9]{20,}$/.test(url.pathname); 15 | let isChannel = url.hostname === "whatsapp.com" && url.pathname.startsWith("/channel/"); 16 | let id; 17 | 18 | if (isGroup) { 19 | const code = url.pathname.replace(/^\/+/, ""); 20 | const res = await conn.groupGetInviteInfo(code); 21 | id = res.id; 22 | } else if (isChannel) { 23 | const code = url.pathname.split("/channel/")[1]?.split("/")[0]; 24 | const res = await conn.newsletterMetadata("invite", code, "GUEST"); 25 | id = res.id; 26 | } else { 27 | return m.reply("Unsupported link. Provide a valid group or channel link."); 28 | } 29 | 30 | await conn.sendButton(m.chat, { 31 | text: `Target ID: ${id}`, 32 | title: "Result", 33 | footer: "Use the button below to copy the ID", 34 | interactiveButtons: [ 35 | { 36 | name: "cta_copy", 37 | buttonParamsJson: JSON.stringify({ 38 | display_text: "Copy ID", 39 | copy_code: id, 40 | }), 41 | }, 42 | ], 43 | hasMediaAttachment: false, 44 | }); 45 | } catch (e) { 46 | conn.logger.error(e); 47 | m.reply(`Error: ${e.message}`); 48 | } 49 | }; 50 | 51 | handler.help = ["cekid"]; 52 | handler.tags = ["tools"]; 53 | handler.command = /^(cekid|id)$/i; 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /lib/api/spotify.js: -------------------------------------------------------------------------------- 1 | export async function spotify(query) { 2 | const encoded = encodeURIComponent(query); 3 | const endpoints = [ 4 | //`https://api.nekolabs.web.id/downloader/spotify/play/v1?q=${encoded}`, 5 | `https://api.ootaizumi.web.id/downloader/spotifyplay?query=${encoded}`, 6 | `https://kyyokatsurestapi.my.id/search/spotify?q=${encoded}`, 7 | ]; 8 | 9 | for (const endpoint of endpoints) { 10 | const res = await fetch(endpoint).catch(() => null); 11 | if (!res) continue; 12 | 13 | const json = await res.json().catch(() => null); 14 | if (!json || (!json.success && !json.status)) continue; 15 | 16 | if (json.result?.downloadUrl && json.result?.metadata) { 17 | const { title, artist, cover, url } = json.result.metadata; 18 | return { 19 | success: true, 20 | title, 21 | channel: artist, 22 | cover, 23 | url, 24 | downloadUrl: json.result.downloadUrl, 25 | }; 26 | } 27 | 28 | const oota = json.result; 29 | if (oota?.download && oota?.title && oota?.artists && oota?.image && oota?.external_url) { 30 | return { 31 | success: true, 32 | title: oota.title, 33 | channel: oota.artists, 34 | cover: oota.image, 35 | url: oota.external_url, 36 | downloadUrl: oota.download, 37 | }; 38 | } 39 | 40 | const kyy = json.result; 41 | if (kyy?.audio && kyy?.title && kyy?.artist && kyy?.thumbnail && kyy?.url) { 42 | return { 43 | success: true, 44 | title: kyy.title, 45 | channel: kyy.artist, 46 | cover: kyy.thumbnail, 47 | url: kyy.url, 48 | downloadUrl: kyy.audio, 49 | }; 50 | } 51 | } 52 | 53 | return { success: false, error: "No downloadable track found from any provider." }; 54 | } 55 | -------------------------------------------------------------------------------- /plugins/info/info-owner.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | const vcard = `BEGIN:VCARD 3 | VERSION:3.0 4 | FN:Naruya Izumi 5 | ORG:Naruya Izumi 6 | TITLE:Epictetus, Enchiridion — Chapter 1 (verse 1) 7 | EMAIL;type=INTERNET:sexystyle088@gmail.com 8 | TEL;type=CELL;waid=6283143663697:+6283143663697 9 | ADR;type=WORK:;;2-chōme-7-5 Fuchūchō;Izumi;Osaka;594-0071;Japan 10 | URL;type=WORK:https://www.instagram.com/naruyaizumi 11 | X-WA-BIZ-NAME:Naruya Izumi 12 | X-WA-BIZ-DESCRIPTION:𝙊𝙬𝙣𝙚𝙧 𝙤𝙛 𝙇𝙞𝙤𝙧𝙖 𝙎𝙘𝙧𝙞𝙥𝙩 13 | X-WA-BIZ-HOURS:Mo-Su 00:00-23:59 14 | END:VCARD`; 15 | 16 | const q = { 17 | key: { 18 | fromMe: false, 19 | participant: "12066409886@s.whatsapp.net", 20 | remoteJid: "status@broadcast", 21 | }, 22 | message: { 23 | contactMessage: { 24 | displayName: "Naruya Izumi", 25 | vcard, 26 | }, 27 | }, 28 | }; 29 | 30 | await conn.sendMessage( 31 | m.chat, 32 | { 33 | contacts: { 34 | displayName: "Naruya Izumi", 35 | contacts: [{ vcard }], 36 | }, 37 | contextInfo: { 38 | forwardingScore: 999, 39 | isForwarded: true, 40 | forwardedNewsletterMessageInfo: { 41 | newsletterJid: "120363144038483540@newsletter", 42 | newsletterName: "mkfs.ext4 /dev/naruyaizumi", 43 | }, 44 | externalAdReply: { 45 | title: "© 2024–2025 Liora Project", 46 | body: "Contact the Owner via WhatsApp", 47 | thumbnailUrl: "https://files.catbox.moe/8tw69l.jpeg", 48 | mediaType: 1, 49 | renderLargerThumbnail: true, 50 | }, 51 | }, 52 | }, 53 | { quoted: q } 54 | ); 55 | }; 56 | 57 | handler.help = ["owner"]; 58 | handler.tags = ["info"]; 59 | handler.command = /^(owner|creator)$/i; 60 | 61 | export default handler; 62 | -------------------------------------------------------------------------------- /lib/shell/nodejs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | install_nodejs() { 4 | print_info "Installing Node.js via NVM..." 5 | 6 | export NVM_DIR="$HOME/.nvm" 7 | 8 | if [ ! -d "$NVM_DIR" ]; then 9 | print_info "NVM not found, installing..." 10 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash || { 11 | print_error "Failed to install NVM" 12 | exit 1 13 | } 14 | else 15 | print_info "NVM already installed" 16 | fi 17 | 18 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 19 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 20 | 21 | if ! command -v nvm &> /dev/null; then 22 | print_error "NVM failed to load" 23 | exit 1 24 | fi 25 | 26 | print_info "Installing Node.js v24..." 27 | nvm install 24 || { 28 | print_error "Failed to install Node.js" 29 | exit 1 30 | } 31 | 32 | nvm use 24 33 | nvm alias default 24 34 | 35 | if ! command -v node &> /dev/null; then 36 | print_error "Node.js installation failed" 37 | exit 1 38 | fi 39 | 40 | NODE_VERSION=$(node -v) 41 | NODE_PATH=$(which node) 42 | NODE_DIR=$(dirname "$NODE_PATH") 43 | 44 | if [ -z "$NODE_DIR" ] || [ ! -d "$NODE_DIR" ]; then 45 | print_error "Failed to determine Node.js directory" 46 | exit 1 47 | fi 48 | 49 | print_success "Node.js installed: $NODE_VERSION at $NODE_DIR" 50 | 51 | print_info "Enabling Corepack for package managers..." 52 | corepack enable || { 53 | print_error "Failed to enable Corepack" 54 | exit 1 55 | } 56 | 57 | print_info "Preparing pnpm..." 58 | corepack prepare pnpm@latest --activate || { 59 | print_warning "Failed to prepare pnpm, but continuing..." 60 | } 61 | 62 | if command -v pnpm &> /dev/null; then 63 | PNPM_VERSION=$(pnpm -v) 64 | print_success "pnpm ready: v$PNPM_VERSION" 65 | fi 66 | 67 | print_success "Node.js setup completed" 68 | } -------------------------------------------------------------------------------- /plugins/tool/tool-resize.js: -------------------------------------------------------------------------------- 1 | import sharp from "sharp"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | try { 5 | const towidth = parseInt(args[0]); 6 | const toheight = parseInt(args[1]); 7 | if (!towidth || !toheight) 8 | return m.reply(`Enter target size.\n› Example: ${usedPrefix + command} 1000 500`); 9 | 10 | const q = m.quoted ? m.quoted : m; 11 | const mime = (q.msg || q).mimetype || q.mediaType || ""; 12 | if (!mime) return m.reply("No media detected. Reply or send an image."); 13 | if (!/image\/(jpe?g|png|webp)/i.test(mime)) return m.reply(`Unsupported format: ${mime}`); 14 | 15 | await global.loading(m, conn); 16 | 17 | const media = await q.download(); 18 | if (!media?.length) return m.reply("Failed to download media."); 19 | 20 | const before = await sharp(media).metadata(); 21 | const beforeRatio = before.width / before.height; 22 | const targetRatio = towidth / toheight; 23 | const fitMode = Math.abs(beforeRatio - targetRatio) < 0.05 ? "inside" : "cover"; 24 | 25 | const resized = await sharp(media) 26 | .resize(towidth, toheight, { fit: fitMode, position: "centre" }) 27 | .toBuffer(); 28 | 29 | const after = await sharp(resized).metadata(); 30 | 31 | const caption = [ 32 | "Image Resize", 33 | `Original : ${before.width}×${before.height}px`, 34 | `Resized : ${after.width}×${after.height}px`, 35 | `Mode : ${fitMode.toUpperCase()}`, 36 | "Resize completed successfully.", 37 | ].join("\n"); 38 | 39 | await conn.sendMessage(m.chat, { image: resized, caption }, { quoted: m }); 40 | } catch (e) { 41 | conn.logger.error(e); 42 | m.reply(`Error: ${e.message}`); 43 | } finally { 44 | await global.loading(m, conn, true); 45 | } 46 | }; 47 | 48 | handler.help = ["resize"]; 49 | handler.tags = ["tools"]; 50 | handler.command = /^(resize|crop)$/i; 51 | 52 | export default handler; 53 | -------------------------------------------------------------------------------- /plugins/info/info-os.js: -------------------------------------------------------------------------------- 1 | import { 2 | getOSPrettyName, 3 | getCPUInfo, 4 | getCPUUsageSinceBoot, 5 | getRAMInfo, 6 | getDiskUsage, 7 | getHeapInfo, 8 | getProcessInfo, 9 | getNetworkStats, 10 | getKernelInfo, 11 | getSystemUptime, 12 | getWarnings, 13 | formatTime, 14 | } from "../../lib/system-info.js"; 15 | 16 | import { canvas } from "../../lib/canvas/canvas-os.js"; 17 | 18 | let handler = async (m, { conn }) => { 19 | try { 20 | await global.loading(m, conn); 21 | 22 | const osInfo = await getOSPrettyName(); 23 | const kernel = await getKernelInfo(); 24 | const cpu = await getCPUInfo(); 25 | const cpuBootUsage = await getCPUUsageSinceBoot(); 26 | const ram = await getRAMInfo(); 27 | const disk = await getDiskUsage(); 28 | const heap = getHeapInfo(); 29 | const proc = getProcessInfo(); 30 | const network = await getNetworkStats(); 31 | 32 | const uptimeBot = formatTime(process.uptime()); 33 | const uptimeSys = await getSystemUptime(); 34 | 35 | const warnings = getWarnings(cpu, ram, disk); 36 | 37 | const systemData = { 38 | osInfo, 39 | kernel, 40 | cpu, 41 | cpuBootUsage, 42 | ram, 43 | disk, 44 | heap, 45 | proc, 46 | network, 47 | uptimeBot, 48 | uptimeSys, 49 | warnings, 50 | }; 51 | 52 | const imageBuffer = await canvas(systemData); 53 | 54 | await conn.sendMessage( 55 | m.chat, 56 | { 57 | image: imageBuffer, 58 | caption: "*SYSTEM MONITOR REPORT*", 59 | }, 60 | { quoted: m } 61 | ); 62 | } catch (e) { 63 | conn.logger.error(e); 64 | m.reply(`Error: ${e.message}`); 65 | } finally { 66 | await global.loading(m, conn, true); 67 | } 68 | }; 69 | 70 | handler.help = ["os"]; 71 | handler.tags = ["info"]; 72 | handler.command = /^(os)$/i; 73 | 74 | export default handler; 75 | -------------------------------------------------------------------------------- /lib/cpp/bindings/converter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../core/converter_core.h" 4 | 5 | static Napi::Value Convert(const Napi::CallbackInfo& info) { 6 | Napi::Env env = info.Env(); 7 | 8 | if (info.Length() < 1 || !info[0].IsBuffer()) { 9 | Napi::TypeError::New(env, "convert(buffer, options?)").ThrowAsJavaScriptException(); 10 | return env.Null(); 11 | } 12 | 13 | auto buffer = info[0].As>(); 14 | Napi::Object opt = (info.Length() >= 2 && info[1].IsObject()) ? info[1].As() 15 | : Napi::Object::New(env); 16 | 17 | ConverterCore::ConvertOptions options; 18 | options.format = opt.Has("format") ? opt.Get("format").ToString().Utf8Value() : "opus"; 19 | options.sampleRate = opt.Has("sampleRate") ? opt.Get("sampleRate").ToNumber().Int32Value() 20 | : 48000; 21 | options.channels = opt.Has("channels") ? opt.Get("channels").ToNumber().Int32Value() : 2; 22 | options.ptt = opt.Has("ptt") ? opt.Get("ptt").ToBoolean() : false; 23 | options.vbr = opt.Has("vbr") ? opt.Get("vbr").ToBoolean() : true; 24 | 25 | if (opt.Has("bitrate")) { 26 | Napi::Value br = opt.Get("bitrate"); 27 | if (br.IsNumber()) { 28 | options.bitrate = static_cast(br.As().DoubleValue()); 29 | } else if (br.IsString()) { 30 | options.bitrate = ConverterCore::ParseBitrate(br.ToString().Utf8Value(), 64000); 31 | } 32 | } 33 | 34 | auto result = ConverterCore::ConvertAudio(buffer.Data(), buffer.Length(), options); 35 | 36 | if (!result.success) { 37 | Napi::Error::New(env, result.error).ThrowAsJavaScriptException(); 38 | return env.Null(); 39 | } 40 | 41 | return Napi::Buffer::Copy(env, result.data.data(), result.data.size()); 42 | } 43 | 44 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 45 | exports.Set("convert", Napi::Function::New(env, Convert)); 46 | return exports; 47 | } 48 | 49 | NODE_API_MODULE(converter, Init) -------------------------------------------------------------------------------- /lib/cpp/bindings/converter_async.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../core/converter_core.h" 4 | 5 | static Napi::Value Convert(const Napi::CallbackInfo& info) { 6 | Napi::Env env = info.Env(); 7 | 8 | if (info.Length() < 1 || !info[0].IsBuffer()) { 9 | Napi::TypeError::New(env, "convert(buffer, options?)").ThrowAsJavaScriptException(); 10 | return env.Null(); 11 | } 12 | 13 | auto buffer = info[0].As>(); 14 | Napi::Object opt = (info.Length() >= 2 && info[1].IsObject()) ? info[1].As() 15 | : Napi::Object::New(env); 16 | 17 | ConverterCore::ConvertOptions options; 18 | options.format = opt.Has("format") ? opt.Get("format").ToString().Utf8Value() : "opus"; 19 | options.sampleRate = opt.Has("sampleRate") ? opt.Get("sampleRate").ToNumber().Int32Value() 20 | : 48000; 21 | options.channels = opt.Has("channels") ? opt.Get("channels").ToNumber().Int32Value() : 2; 22 | options.ptt = opt.Has("ptt") ? opt.Get("ptt").ToBoolean() : false; 23 | options.vbr = opt.Has("vbr") ? opt.Get("vbr").ToBoolean() : true; 24 | 25 | if (opt.Has("bitrate")) { 26 | Napi::Value br = opt.Get("bitrate"); 27 | if (br.IsNumber()) { 28 | options.bitrate = static_cast(br.As().DoubleValue()); 29 | } else if (br.IsString()) { 30 | options.bitrate = ConverterCore::ParseBitrate(br.ToString().Utf8Value(), 64000); 31 | } 32 | } 33 | 34 | auto result = ConverterCore::ConvertAudio(buffer.Data(), buffer.Length(), options); 35 | 36 | if (!result.success) { 37 | Napi::Error::New(env, result.error).ThrowAsJavaScriptException(); 38 | return env.Null(); 39 | } 40 | 41 | return Napi::Buffer::Copy(env, result.data.data(), result.data.size()); 42 | } 43 | 44 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 45 | exports.Set("convert", Napi::Function::New(env, Convert)); 46 | return exports; 47 | } 48 | 49 | NODE_API_MODULE(converter, Init) -------------------------------------------------------------------------------- /plugins/maker/maker-wm.js: -------------------------------------------------------------------------------- 1 | import { addExif, sticker } from "#add-on"; 2 | 3 | let handler = async (m, { conn, text }) => { 4 | const q = m.quoted ? m.quoted : m; 5 | if (!q || !/sticker|image|video/.test(q.mtype)) 6 | return m.reply("Reply to a sticker, image, or video to change its watermark."); 7 | 8 | let [packName, authorName] = (text || "").split("|"); 9 | packName = (packName || global.config.stickpack || "").trim(); 10 | authorName = (authorName || global.config.stickauth || "").trim(); 11 | 12 | await global.loading(m, conn); 13 | 14 | try { 15 | let buffer; 16 | const media = await q.download?.(); 17 | if (!media) throw new Error("Failed to download media."); 18 | 19 | if (typeof media === "string" && /^https?:\/\//.test(media)) { 20 | const res = await fetch(media); 21 | if (!res.ok) throw new Error("Failed to fetch file from URL."); 22 | buffer = Buffer.from(await res.arrayBuffer()); 23 | } else if (Buffer.isBuffer(media)) { 24 | buffer = media; 25 | } else if (media?.data) { 26 | buffer = Buffer.from(media.data); 27 | } 28 | 29 | if (!buffer) throw new Error("Empty buffer, media could not be processed."); 30 | 31 | let result; 32 | const isWebp = 33 | buffer.slice(0, 4).toString() === "RIFF" && buffer.slice(8, 12).toString() === "WEBP"; 34 | 35 | if (isWebp) { 36 | result = await addExif(buffer, { packName, authorName, emojis: [] }); 37 | } else { 38 | const temp = await sticker(buffer, { packName, authorName }); 39 | result = await addExif(temp, { packName, authorName, emojis: [] }); 40 | } 41 | 42 | await conn.sendMessage(m.chat, { sticker: result }, { quoted: m }); 43 | } catch (e) { 44 | conn.logger.error(e); 45 | m.reply(`Error: ${e.message}`); 46 | } finally { 47 | await global.loading(m, conn, true); 48 | } 49 | }; 50 | 51 | handler.help = ["watermark"]; 52 | handler.tags = ["maker"]; 53 | handler.command = /^(wm|watermark)$/i; 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /plugins/group/group-hidetag.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { text, participants, conn }) => { 2 | try { 3 | const q = m.quoted || m; 4 | const mime = (q.msg || q).mimetype || ""; 5 | const teks = text || q.text || ""; 6 | const allJids = participants.map((p) => p.id); 7 | let finalText = teks; 8 | const mentions = allJids.filter((jid) => { 9 | const username = jid.split("@")[0]; 10 | if (teks.includes("@" + username)) { 11 | return true; 12 | } 13 | return false; 14 | }); 15 | 16 | const sendOpts = { quoted: m, mentions: mentions.length ? mentions : allJids }; 17 | 18 | if (mime) { 19 | const media = await q.download(); 20 | const messageContent = {}; 21 | 22 | if (/image/.test(mime)) messageContent.image = media; 23 | else if (/video/.test(mime)) messageContent.video = media; 24 | else if (/audio/.test(mime)) { 25 | messageContent.audio = media; 26 | messageContent.ptt = true; 27 | } else if (/document/.test(mime)) { 28 | messageContent.document = media; 29 | messageContent.mimetype = mime; 30 | messageContent.fileName = "file"; 31 | } else return m.reply("Unsupported media type."); 32 | 33 | if (finalText) messageContent.caption = finalText; 34 | await conn.sendMessage(m.chat, messageContent, sendOpts); 35 | } else if (finalText) { 36 | await conn.sendMessage( 37 | m.chat, 38 | { text: finalText, mentions: sendOpts.mentions }, 39 | sendOpts 40 | ); 41 | } else { 42 | m.reply("Please provide media or text, or reply to a message."); 43 | } 44 | } catch (e) { 45 | conn.logger.error(e); 46 | m.reply(`Error: ${e.message}`); 47 | } 48 | }; 49 | 50 | handler.help = ["hidetag"]; 51 | handler.tags = ["group"]; 52 | handler.command = /^(hidetag|ht|h)$/i; 53 | handler.group = true; 54 | handler.admin = true; 55 | 56 | export default handler; 57 | -------------------------------------------------------------------------------- /plugins/maker/maker-sticker.js: -------------------------------------------------------------------------------- 1 | import { sticker } from "#add-on"; 2 | 3 | let handler = async (m, { conn, args, usedPrefix, command }) => { 4 | try { 5 | const q = m.quoted ? m.quoted : m; 6 | const mime = (q.msg || q).mimetype || q.mediaType || ""; 7 | 8 | if (!mime && !args[0]) 9 | return m.reply( 10 | `Send or reply with an image, GIF, or video using: ${usedPrefix + command}` 11 | ); 12 | 13 | await global.loading(m, conn); 14 | 15 | let buffer; 16 | if (args[0] && isUrl(args[0])) { 17 | const res = await fetch(args[0]); 18 | if (!res.ok) throw new Error("Failed to fetch file from URL."); 19 | buffer = Buffer.from(await res.arrayBuffer()); 20 | } else { 21 | const media = await q.download?.(); 22 | if (!media) return m.reply("Failed to download media."); 23 | buffer = Buffer.isBuffer(media) ? media : media.data ? Buffer.from(media.data) : null; 24 | } 25 | 26 | if (!buffer) throw new Error("Empty buffer. File could not be processed."); 27 | 28 | const opts = { 29 | crop: false, 30 | quality: 90, 31 | fps: 30, 32 | maxDuration: 10, 33 | packName: global.config.stickpack || "", 34 | authorName: global.config.stickauth || "", 35 | emojis: [], 36 | }; 37 | 38 | let result = await sticker(buffer, opts); 39 | const maxSize = 1024 * 1024; 40 | let step = 0; 41 | 42 | while (result.length > maxSize && step < 4) { 43 | step++; 44 | opts.quality -= 10; 45 | if (opts.quality < 50) opts.quality = 50; 46 | if (step >= 2) opts.fps = Math.max(8, opts.fps - 2); 47 | result = await sticker(buffer, opts); 48 | } 49 | 50 | await conn.sendMessage(m.chat, { sticker: result }, { quoted: m }); 51 | } catch (e) { 52 | conn.logger.error(e); 53 | m.reply(`Error: ${e.message}`); 54 | } finally { 55 | await global.loading(m, conn, true); 56 | } 57 | }; 58 | 59 | handler.help = ["sticker"]; 60 | handler.tags = ["maker"]; 61 | handler.command = /^(s(tic?ker)?)$/i; 62 | 63 | export default handler; 64 | 65 | const isUrl = (text) => /^https?:\/\/.+\.(jpe?g|png|gif|mp4|webm|mkv|mov)$/i.test(text); 66 | -------------------------------------------------------------------------------- /.github/issue-labeler.yml: -------------------------------------------------------------------------------- 1 | # Bug-related labels 2 | bug: 3 | - '\b(bug|error|crash|exception|fail|broken|not working)\b' 4 | - '\b(segfault|memory leak|undefined behavior)\b' 5 | 6 | # Priority labels 7 | priority-high: 8 | - '\b(critical|urgent|blocker|severe)\b' 9 | - '\b(security|vulnerability|exploit)\b' 10 | - '\b(data loss|corruption)\b' 11 | 12 | priority-medium: 13 | - '\b(important|significant|should have)\b' 14 | 15 | priority-low: 16 | - '\b(nice to have|enhancement|minor)\b' 17 | 18 | # Feature requests 19 | enhancement: 20 | - '\b(feature|enhance|improvement|suggest)\b' 21 | - '\b(add support for|new feature|feature request)\b' 22 | 23 | # Documentation 24 | documentation: 25 | - '\b(docs|documentation|readme|wiki|guide|tutorial)\b' 26 | - '\b(typo|grammar|spelling)\b' 27 | 28 | # Questions/Help 29 | question: 30 | - '\b(how to|how do I|question|help|support)\b' 31 | - '\b(what is|why|when|where)\b' 32 | 33 | # Component-specific labels 34 | whatsapp: 35 | - '\b(baileys|whatsapp|connection|qr|pairing)\b' 36 | - '\b(message|chat|group|contact)\b' 37 | 38 | sticker: 39 | - '\b(sticker|webp|image conversion)\b' 40 | 41 | media-processing: 42 | - '\b(video|audio|image|ffmpeg|conversion)\b' 43 | - '\b(webp|mp4|mp3|converter)\b' 44 | 45 | cpp: 46 | - '\b(c\+\+|native|addon|binding|gyp)\b' 47 | - '\b(memory leak|segfault|valgrind)\b' 48 | 49 | api: 50 | - '\b(api|endpoint|integration)\b' 51 | - '\b(tiktok|instagram|twitter|spotify|youtube)\b' 52 | 53 | # Performance 54 | performance: 55 | - '\b(slow|performance|speed|optimize|latency)\b' 56 | - '\b(memory usage|cpu usage|resource)\b' 57 | 58 | # Installation/Setup 59 | installation: 60 | - '\b(install|setup|configure|deployment)\b' 61 | - '\b(dependencies|npm|node-gyp)\b' 62 | 63 | # Platform-specific 64 | ubuntu: 65 | - '\bubuntu\b' 66 | 67 | debian: 68 | - '\bdebian\b' 69 | 70 | windows: 71 | - '\bwindows\b' 72 | 73 | # Testing 74 | testing: 75 | - '\b(test|testing|unit test|integration test)\b' 76 | 77 | # CI/CD 78 | ci-cd: 79 | - '\b(github actions|workflow|ci|cd|pipeline)\b' 80 | - '\b(build failed|test failed)\b' 81 | 82 | # Security 83 | security: 84 | - '\b(security|vulnerability|cve|exploit|injection)\b' 85 | - '\b(xss|sql injection|auth)\b' 86 | 87 | # Good first issue (for contributors) 88 | good-first-issue: 89 | - '\b(beginner|easy|good first issue|starter)\b' -------------------------------------------------------------------------------- /plugins/internet/internet-tiktok.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, text, usedPrefix, command }) => { 2 | if (!text) 3 | return m.reply( 4 | `Please enter a TikTok keyword!\nExample: ${usedPrefix + command} luxury girl` 5 | ); 6 | 7 | await global.loading(m, conn); 8 | 9 | try { 10 | let res = await fetch( 11 | `https://api.elrayyxml.web.id/api/search/tiktok?q=${encodeURIComponent(text)}` 12 | ); 13 | let json = await res.json(); 14 | 15 | if (!json.status || !json.result || json.result.length === 0) 16 | throw "*No results found on TikTok!*"; 17 | 18 | let results = json.result; 19 | let cards = []; 20 | 21 | for (let i = 0; i < Math.min(10, results.length); i++) { 22 | let item = results[i]; 23 | 24 | let caption = ` 25 | Statistics: 26 | Views: ${item.stats?.views || 0} 27 | Likes: ${item.stats?.likes || 0} 28 | Comments: ${item.stats?.comment || 0} 29 | Shares: ${item.stats?.share || 0} 30 | Downloads: ${item.stats?.download || 0} 31 | 32 | Date: ${item.taken_at || "Unknown"} 33 | `.trim(); 34 | 35 | cards.push({ 36 | video: { url: item.data }, 37 | title: `${i + 1}. ${item.author?.nickname || "Unknown"}`, 38 | body: caption, 39 | footer: "TikTok Search Engine", 40 | interactiveButtons: [ 41 | { 42 | name: "cta_url", 43 | buttonParamsJson: JSON.stringify({ 44 | display_text: "Watch on TikTok", 45 | url: `https://www.tiktok.com/@${item.author?.nickname}/video/${item.id}`, 46 | }), 47 | }, 48 | ], 49 | }); 50 | } 51 | 52 | await conn.sendCard(m.chat, { 53 | text: `Search results for: ${text}`, 54 | title: "TikTok Search", 55 | footer: "Select a button to view", 56 | cards: cards, 57 | }); 58 | } catch (e) { 59 | conn.logger.error(e); 60 | m.reply(`Error: ${e.message}`); 61 | } finally { 62 | await global.loading(m, conn, true); 63 | } 64 | }; 65 | 66 | handler.help = ["tts"]; 67 | handler.tags = ["internet"]; 68 | handler.command = /^(tts)$/i; 69 | 70 | export default handler; 71 | -------------------------------------------------------------------------------- /plugins/owner/owner-sf.js: -------------------------------------------------------------------------------- 1 | import { readdir, mkdir, writeFile } from "fs/promises"; 2 | import path from "path"; 3 | 4 | const handler = async (m, { args, conn }) => { 5 | try { 6 | let target = args.length ? path.join(process.cwd(), ...args) : process.cwd(); 7 | target = path.resolve(target); 8 | 9 | if (!m.quoted) { 10 | const items = await readdir(target, { withFileTypes: true }).catch(() => null); 11 | if (!items) return m.reply(`Folder not found: ${target}`); 12 | 13 | const list = 14 | items 15 | .sort((a, b) => 16 | a.isDirectory() === b.isDirectory() 17 | ? a.name.localeCompare(b.name) 18 | : a.isDirectory() 19 | ? -1 20 | : 1 21 | ) 22 | .map( 23 | (item) => 24 | `${item.isDirectory() ? "📁" : "📄"} ${item.name}${item.isDirectory() ? "/" : ""}` 25 | ) 26 | .join("\n") || "(empty directory)"; 27 | return m.reply(`Path: ${target}\n\n${list}`); 28 | } 29 | 30 | const q = m.quoted; 31 | const mime = q.mimetype || q.mediaType || ""; 32 | if (!q?.download || !/^(image|video|audio|application)/.test(mime)) 33 | return m.reply("Quoted message must be a media or document file."); 34 | 35 | const buffer = await q.download().catch(() => null); 36 | if (!buffer?.length) return m.reply("Failed to download the quoted media."); 37 | 38 | const ext = mime?.split("/")[1] || path.extname(q.fileName || "")?.slice(1) || "bin"; 39 | const baseName = q.fileName ? path.basename(q.fileName) : `file-${Date.now()}.${ext}`; 40 | const fullpath = path.resolve(target, baseName); 41 | await mkdir(path.dirname(fullpath), { recursive: true }); 42 | 43 | await writeFile(fullpath, buffer); 44 | 45 | return m.reply(`Saved as: ${path.relative(process.cwd(), fullpath)}`); 46 | } catch (e) { 47 | conn.logger.error(e); 48 | return m.reply(`Error: ${e.message}`); 49 | } 50 | }; 51 | 52 | handler.help = ["sf"]; 53 | handler.tags = ["owner"]; 54 | handler.command = /^(sf|savefile)$/i; 55 | handler.owner = true; 56 | 57 | export default handler; 58 | -------------------------------------------------------------------------------- /lib/core/smsg.js: -------------------------------------------------------------------------------- 1 | import { proto } from "baileys"; 2 | 3 | const SYM_PROCESSED = Symbol.for("smsg.processed"); 4 | const SYM_CONN = Symbol.for("smsg.conn"); 5 | 6 | let cachedBotId = null; 7 | let cachedBotIdTime = 0; 8 | const BOT_ID_CACHE_TTL = 300000; 9 | 10 | export function smsg(conn, m) { 11 | if (!m) return m; 12 | if (m[SYM_PROCESSED]) { 13 | if (m[SYM_CONN] !== conn) { 14 | m.conn = conn; 15 | m[SYM_CONN] = conn; 16 | } 17 | return m; 18 | } 19 | const M = proto.WebMessageInfo; 20 | if (M?.create) { 21 | try { 22 | m = M.create(m); 23 | } catch (e) { 24 | conn.logger.error(e.message); 25 | return m; 26 | } 27 | } 28 | m.conn = conn; 29 | m[SYM_CONN] = conn; 30 | const msg = m.message; 31 | if (!msg) { 32 | m[SYM_PROCESSED] = true; 33 | return m; 34 | } 35 | 36 | try { 37 | if (m.mtype === "protocolMessage" && m.msg?.key) { 38 | const key = { ...m.msg.key }; 39 | 40 | if (key.remoteJid === "status@broadcast" && m.chat) { 41 | key.remoteJid = m.chat; 42 | } 43 | 44 | if ((!key.participant || key.participant === "status_me") && m.sender) { 45 | key.participant = m.sender; 46 | } 47 | 48 | const now = Date.now(); 49 | if (!cachedBotId || now - cachedBotIdTime > BOT_ID_CACHE_TTL) { 50 | cachedBotId = conn.decodeJid?.(conn.user?.lid || "") || ""; 51 | cachedBotIdTime = now; 52 | } 53 | 54 | if (cachedBotId) { 55 | const partId = conn.decodeJid?.(key.participant) || ""; 56 | key.fromMe = partId === cachedBotId; 57 | 58 | if (!key.fromMe && key.remoteJid === cachedBotId && m.sender) { 59 | key.remoteJid = m.sender; 60 | } 61 | } 62 | 63 | m.msg.key = key; 64 | conn.ev?.emit("messages.delete", { keys: [key] }); 65 | } 66 | 67 | if (m.quoted && !m.quoted.mediaMessage && m.quoted.download !== undefined) { 68 | delete m.quoted.download; 69 | } 70 | if (!m.mediaMessage && m.download !== undefined) { 71 | delete m.download; 72 | } 73 | } catch (e) { 74 | conn.logger.error(e.message); 75 | } 76 | 77 | m[SYM_PROCESSED] = true; 78 | 79 | return m; 80 | } 81 | -------------------------------------------------------------------------------- /lib/shell/system.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | validate_os() { 4 | print_info "Validating OS compatibility..." 5 | 6 | if [ ! -f /etc/os-release ]; then 7 | print_error "Cannot detect OS. /etc/os-release not found." 8 | exit 1 9 | fi 10 | 11 | OS_ID=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"') 12 | OS_VERSION_ID=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"') 13 | 14 | if [[ "$OS_ID" != "ubuntu" && "$OS_ID" != "debian" ]]; then 15 | print_error "Unsupported OS: $OS_ID. Only Ubuntu 24.04 or Debian 12 are supported." 16 | exit 1 17 | fi 18 | 19 | if [[ "$OS_ID" == "ubuntu" && "$OS_VERSION_ID" != "24.04" ]]; then 20 | print_error "Ubuntu version $OS_VERSION_ID is not supported. Use Ubuntu 24.04." 21 | exit 1 22 | fi 23 | 24 | if [[ "$OS_ID" == "debian" && "$OS_VERSION_ID" != "12" ]]; then 25 | print_error "Debian version $OS_VERSION_ID is not supported. Use Debian 12." 26 | exit 1 27 | fi 28 | 29 | print_success "OS validation passed: $OS_ID $OS_VERSION_ID" 30 | } 31 | 32 | install_system_dependencies() { 33 | print_info "Installing system dependencies..." 34 | 35 | apt-get update -qq || { 36 | print_error "Failed to update package lists" 37 | exit 1 38 | } 39 | 40 | apt-get install -y \ 41 | ffmpeg libwebp-dev libavformat-dev \ 42 | libavcodec-dev libavutil-dev libswresample-dev \ 43 | libswscale-dev libavfilter-dev build-essential \ 44 | python3 g++ pkg-config jq \ 45 | cmake git curl unzip wget ca-certificates gnupg lsb-release || { 46 | print_error "Failed to install system dependencies" 47 | exit 1 48 | } 49 | 50 | print_success "System dependencies installed" 51 | } 52 | 53 | validate_ffmpeg() { 54 | print_info "Validating FFmpeg version..." 55 | 56 | if ! command -v ffmpeg &> /dev/null; then 57 | print_error "FFmpeg not found after installation" 58 | exit 1 59 | fi 60 | 61 | FFMPEG_VERSION=$(ffmpeg -version | head -n1 | awk '{print $3}' | cut -d. -f1) 62 | 63 | if [[ "$FFMPEG_VERSION" != "5" && "$FFMPEG_VERSION" != "6" ]]; then 64 | print_error "FFmpeg version $FFMPEG_VERSION detected. Only version 5 or 6 are supported." 65 | print_error "Please install FFmpeg 5 or 6 manually." 66 | exit 1 67 | fi 68 | 69 | print_success "FFmpeg version $FFMPEG_VERSION detected" 70 | } -------------------------------------------------------------------------------- /plugins/downloader/downloader-ytplay.js: -------------------------------------------------------------------------------- 1 | import { convert } from "#add-on"; 2 | import { play } from "#play"; 3 | import { youtubeCanvas } from "../../lib/canvas/canvas-play.js"; 4 | 5 | let handler = async (m, { conn, args, usedPrefix, command }) => { 6 | if (!args[0]) 7 | return m.reply(`Please provide a song title.\n› Example: ${usedPrefix + command} Bye`); 8 | 9 | await global.loading(m, conn); 10 | try { 11 | const { success, title, channel, cover, url, downloadUrl, error } = await play( 12 | args.join(" ") 13 | ); 14 | if (!success) throw new Error(error); 15 | 16 | const canvasBuffer = await youtubeCanvas(cover, title, channel); 17 | 18 | const audioRes = await fetch(downloadUrl); 19 | if (!audioRes.ok) throw new Error(`Failed to fetch audio. Status: ${audioRes.status}`); 20 | 21 | const audioBuffer = Buffer.from(await audioRes.arrayBuffer()); 22 | 23 | const converted = await convert(audioBuffer, { 24 | format: "opus", 25 | bitrate: "128k", 26 | channels: 1, 27 | sampleRate: 48000, 28 | ptt: true, 29 | }); 30 | 31 | const finalBuffer = 32 | converted instanceof Buffer 33 | ? converted 34 | : converted?.buffer 35 | ? Buffer.from(converted.buffer) 36 | : converted?.data 37 | ? Buffer.from(converted.data) 38 | : Buffer.from(converted); 39 | 40 | await conn.sendMessage( 41 | m.chat, 42 | { 43 | audio: finalBuffer, 44 | mimetype: "audio/ogg; codecs=opus", 45 | ptt: true, 46 | contextInfo: { 47 | externalAdReply: { 48 | title, 49 | body: channel, 50 | thumbnail: canvasBuffer, 51 | mediaUrl: url, 52 | mediaType: 1, 53 | renderLargerThumbnail: true, 54 | }, 55 | }, 56 | }, 57 | { quoted: m } 58 | ); 59 | } catch (e) { 60 | conn.logger.error(e); 61 | m.reply(`Error: ${e.message}`); 62 | } finally { 63 | await global.loading(m, conn, true); 64 | } 65 | }; 66 | 67 | handler.help = ["play"]; 68 | handler.tags = ["downloader"]; 69 | handler.command = /^(play)$/i; 70 | 71 | export default handler; 72 | -------------------------------------------------------------------------------- /plugins/owner/owner-groupstatus.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, usedPrefix, command }) => { 2 | const quoted = m.quoted ? m.quoted : m; 3 | const mime = (quoted.msg || quoted).mimetype || ""; 4 | 5 | const textToParse = m.text || ""; 6 | const caption = textToParse.replace(new RegExp(`^[.!#/](${command})\\s*`, "i"), "").trim(); 7 | 8 | const jid = m.chat; 9 | 10 | try { 11 | if (!mime && !caption) { 12 | return m.reply( 13 | `Reply to media or provide text.\nExamples: ${usedPrefix + command} Hello everyone! or ${usedPrefix + command} reply to image/video/audio` 14 | ); 15 | } 16 | 17 | await global.loading(m, conn); 18 | 19 | let payload = {}; 20 | 21 | if (/image/.test(mime)) { 22 | const buffer = await quoted.download(); 23 | if (!buffer) return m.reply("Failed to download image."); 24 | 25 | payload = { 26 | image: buffer, 27 | caption: caption || "", 28 | }; 29 | } else if (/video/.test(mime)) { 30 | const buffer = await quoted.download(); 31 | if (!buffer) return m.reply("Failed to download video."); 32 | 33 | payload = { 34 | video: buffer, 35 | caption: caption || "", 36 | }; 37 | } else if (/audio/.test(mime)) { 38 | const buffer = await quoted.download(); 39 | if (!buffer) return m.reply("Failed to download audio."); 40 | 41 | payload = { 42 | audio: buffer, 43 | mimetype: "audio/mp4", 44 | }; 45 | } else if (caption) { 46 | payload = { 47 | text: caption, 48 | }; 49 | } else { 50 | return m.reply( 51 | `Reply to media or provide text.\nExamples: ${usedPrefix + command} Hello everyone! or ${usedPrefix + command} reply to image/video/audio` 52 | ); 53 | } 54 | 55 | await conn.sendGroupStatus(jid, payload); 56 | 57 | m.reply("Group status sent successfully."); 58 | } catch (e) { 59 | conn.logger?.error(e); 60 | m.reply(`Error: ${e.message}`); 61 | } finally { 62 | await global.loading?.(m, conn, true); 63 | } 64 | }; 65 | 66 | handler.help = ["groupstatus"]; 67 | handler.tags = ["owner"]; 68 | handler.command = /^(statusgc|swgc)$/i; 69 | handler.owner = true; 70 | handler.group = true; 71 | 72 | export default handler; 73 | -------------------------------------------------------------------------------- /plugins/tool/tool-getq.js: -------------------------------------------------------------------------------- 1 | import { BufferJSON } from "baileys"; 2 | 3 | let handler = async (m, { conn }) => { 4 | if (!m.quoted) return m.reply("Reply to a message to debug its structure."); 5 | try { 6 | const output = inspect(m.quoted); 7 | await m.reply(output); 8 | } catch (e) { 9 | conn.logger.error(e); 10 | m.reply(`Error: ${e.message}`); 11 | } 12 | }; 13 | 14 | handler.help = ["debug"]; 15 | handler.tags = ["tool"]; 16 | handler.command = /^(getq|q|debug)$/i; 17 | handler.owner = true; 18 | 19 | export default handler; 20 | 21 | function isByteArray(obj) { 22 | return ( 23 | typeof obj === "object" && 24 | obj !== null && 25 | Object.keys(obj).every((k) => /^\d+$/.test(k)) && 26 | Object.values(obj).every((v) => typeof v === "number" && v >= 0 && v <= 255) 27 | ); 28 | } 29 | 30 | function inspect(obj, depth = 0, seen = new WeakSet()) { 31 | if (obj === null) return "null"; 32 | if (obj === undefined) return "undefined"; 33 | if (typeof obj !== "object") return JSON.stringify(obj); 34 | if (seen.has(obj)) return "[Circular]"; 35 | seen.add(obj); 36 | if (depth > 15) return "[Depth limit reached]"; 37 | 38 | const result = {}; 39 | for (const key of Reflect.ownKeys(obj)) { 40 | try { 41 | const desc = Object.getOwnPropertyDescriptor(obj, key); 42 | let value = desc?.get ? desc.get.call(obj) : obj[key]; 43 | 44 | if (Buffer.isBuffer(value)) { 45 | const hex = BufferJSON.toJSON(value) 46 | .data.map((v) => v.toString(16).padStart(2, "0")) 47 | .join(""); 48 | result[key] = ``; 49 | } else if (isByteArray(value)) { 50 | const hex = Object.values(value) 51 | .map((v) => v.toString(16).padStart(2, "0")) 52 | .join(""); 53 | result[key] = ``; 54 | } else if (typeof value === "function") { 55 | result[key] = `[Function ${value.name || "anonymous"}]`; 56 | } else if (typeof value === "object" && value !== null) { 57 | result[key] = inspect(value, depth + 1, seen); 58 | } else { 59 | result[key] = value; 60 | } 61 | } catch (e) { 62 | result[key] = `[Error: ${e.message}]`; 63 | } 64 | } 65 | 66 | return depth === 0 ? JSON.stringify(result, null, 2) : result; 67 | } 68 | -------------------------------------------------------------------------------- /plugins/downloader/downloader-spotify.js: -------------------------------------------------------------------------------- 1 | import { convert } from "#add-on"; 2 | import { spotify } from "#spotify"; 3 | import { spotifyCanvas } from "../../lib/canvas/canvas-spotify.js"; 4 | 5 | let handler = async (m, { conn, args, usedPrefix, command }) => { 6 | if (!args[0]) 7 | return m.reply(`Please provide a song title.\n› Example: ${usedPrefix + command} Swim`); 8 | 9 | await global.loading(m, conn); 10 | try { 11 | const { success, title, channel, cover, url, downloadUrl, duration, error } = await spotify( 12 | args.join(" ") 13 | ); 14 | if (!success) throw new Error(error); 15 | 16 | const canvasBuffer = await spotifyCanvas(cover, title, channel, duration); 17 | 18 | const audioRes = await fetch(downloadUrl); 19 | if (!audioRes.ok) throw new Error(`Failed to fetch audio. Status: ${audioRes.status}`); 20 | 21 | const audioBuffer = Buffer.from(await audioRes.arrayBuffer()); 22 | 23 | const converted = await convert(audioBuffer, { 24 | format: "opus", 25 | bitrate: "128k", 26 | channels: 1, 27 | sampleRate: 48000, 28 | ptt: false, 29 | }); 30 | 31 | const finalBuffer = 32 | converted instanceof Buffer 33 | ? converted 34 | : converted?.buffer 35 | ? Buffer.from(converted.buffer) 36 | : converted?.data 37 | ? Buffer.from(converted.data) 38 | : Buffer.from(converted); 39 | 40 | await conn.sendMessage( 41 | m.chat, 42 | { 43 | audio: finalBuffer, 44 | mimetype: "audio/ogg; codecs=opus", 45 | ptt: true, 46 | contextInfo: { 47 | externalAdReply: { 48 | title, 49 | body: channel, 50 | thumbnail: canvasBuffer, 51 | mediaUrl: url, 52 | mediaType: 1, 53 | renderLargerThumbnail: true, 54 | }, 55 | }, 56 | }, 57 | { quoted: m } 58 | ); 59 | } catch (e) { 60 | conn.logger.error(e); 61 | m.reply(`Error: ${e.message}`); 62 | } finally { 63 | await global.loading(m, conn, true); 64 | } 65 | }; 66 | 67 | handler.help = ["spotify"]; 68 | handler.tags = ["downloader"]; 69 | handler.command = /^(spotify|sp)$/i; 70 | 71 | export default handler; 72 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## 🛡️ Supported Versions 4 | 5 | We provide security updates for active versions. Please always use the latest stable release. 6 | 7 | | Version | Supported | Security Updates Until | 8 | | ------- | ------------------ | ---------------------- | 9 | | 1.x.x | ✅ Active | TBD | 10 | | 0.x.x | ❌ Deprecated | Ended | 11 | 12 | ## 🚨 Reporting a Vulnerability 13 | 14 | **DO NOT report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 15 | 16 | ### Preferred Method 17 | **Email**: [liora.bot.official@gmail.com](mailto:liora.bot.official@gmail.com?subject=Security%20Vulnerability%20Report%20-%20Liora) 18 | 19 | ### What to Include 20 | - **Description**: Detailed explanation of the vulnerability 21 | - **Impact**: Potential risks and affected components 22 | - **Steps to Reproduce**: Clear reproduction steps 23 | - **Environment**: OS, versions, configuration details 24 | - **Proof of Concept**: Code or commands demonstrating the issue 25 | - **Suggested Fix**: If you have any ideas for remediation 26 | 27 | ### Response Timeline 28 | - **Initial Response**: Within 24-48 hours 29 | - **Status Update**: Weekly updates during investigation 30 | - **Resolution**: Depends on severity (see below) 31 | 32 | ## 🔒 Security Update Policy 33 | 34 | | Severity | Response Time | Patch Release | 35 | |----------|---------------|---------------| 36 | | Critical | 24-48 hours | Immediate patch | 37 | | High | 3-5 days | Next patch release | 38 | | Medium | 7-14 days | Scheduled release | 39 | | Low | 14-30 days | Next minor release | 40 | 41 | ## 🛠️ Security Features 42 | 43 | ### Code Security 44 | - **Memory Safety**: Rust components with zero-cost abstractions 45 | - **Type Safety**: TypeScript and Rust type systems 46 | - **Input Validation**: Comprehensive sanitization for all inputs 47 | - **SQL Injection Prevention**: Parameterized queries and ORM 48 | - **XSS Protection**: Output encoding and CSP-ready 49 | 50 | ### Infrastructure Security 51 | - **Dependency Scanning**: Automated audits for npm and cargo 52 | - **Code Analysis**: GitHub CodeQL integration 53 | - **SAST**: Static Application Security Testing in CI/CD 54 | - **Secrets Detection**: Pre-commit hooks and CI scanning 55 | 56 | ### Runtime Security 57 | - **Sandboxing**: Isolated execution environments 58 | - **Resource Limits**: Memory and CPU constraints 59 | - **File System Restrictions**: Limited access patterns 60 | - **Network Security**: TLS/SSL enforcement -------------------------------------------------------------------------------- /plugins/internet/internet-yts.js: -------------------------------------------------------------------------------- 1 | import yts from "yt-search"; 2 | import { canvas } from "../../lib/canvas/canvas-yts.js"; 3 | 4 | let handler = async (m, { conn, text, usedPrefix, command }) => { 5 | if (!text) { 6 | return m.reply( 7 | `Usage: ${usedPrefix + command} \n› Example: ${usedPrefix + command} YAD` 8 | ); 9 | } 10 | 11 | try { 12 | await global.loading(m, conn); 13 | 14 | const search = await yts(text); 15 | const videos = search.videos; 16 | 17 | if (!Array.isArray(videos) || videos.length === 0) { 18 | return m.reply(`No results found for "${text}".`); 19 | } 20 | 21 | const imageBuffer = await canvas(videos, text); 22 | const rows = videos.map((video, index) => ({ 23 | header: `Result ${index + 1}`, 24 | title: video.title, 25 | description: `${video.author.name} • ${video.timestamp || "-"} • ${formatNumber(video.views)} views`, 26 | id: `.play ${video.url}`, 27 | })); 28 | 29 | await conn.sendButton(m.chat, { 30 | image: imageBuffer || videos[0].thumbnail, 31 | caption: "*Select a song from the results above*", 32 | title: "YouTube Search Results", 33 | footer: `Found ${videos.length} results for "${text}"`, 34 | interactiveButtons: [ 35 | { 36 | name: "single_select", 37 | buttonParamsJson: JSON.stringify({ 38 | title: "Select Video", 39 | sections: [ 40 | { 41 | title: "Top Results", 42 | rows: rows, 43 | }, 44 | ], 45 | }), 46 | }, 47 | ], 48 | hasMediaAttachment: true, 49 | }); 50 | } catch (e) { 51 | conn.logger.error(e); 52 | m.reply(`Error: ${e.message}`); 53 | } finally { 54 | await global.loading(m, conn, true); 55 | } 56 | }; 57 | 58 | handler.help = ["yts"]; 59 | handler.tags = ["internet"]; 60 | handler.command = /^(yts)$/i; 61 | 62 | export default handler; 63 | 64 | function formatNumber(num) { 65 | if (!num) return "0"; 66 | if (num >= 1e9) return (num / 1e9).toFixed(1).replace(/\.0$/, "") + "B"; 67 | if (num >= 1e6) return (num / 1e6).toFixed(1).replace(/\.0$/, "") + "M"; 68 | if (num >= 1e3) return (num / 1e3).toFixed(1).replace(/\.0$/, "") + "K"; 69 | return num.toString(); 70 | } 71 | -------------------------------------------------------------------------------- /lib/cpp/core/sticker_core.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace StickerCore { 10 | 11 | struct StickerOptions { 12 | bool crop{false}; 13 | int quality{80}; 14 | int fps{15}; 15 | int maxDuration{15}; 16 | std::string packName; 17 | std::string authorName; 18 | std::vector emojis; 19 | 20 | void validate() { 21 | if (quality < 1) 22 | quality = 1; 23 | if (quality > 100) 24 | quality = 100; 25 | if (fps < 1) 26 | fps = 1; 27 | if (fps > 30) 28 | fps = 30; 29 | if (maxDuration < 1) 30 | maxDuration = 1; 31 | if (maxDuration > 60) 32 | maxDuration = 60; 33 | } 34 | }; 35 | 36 | struct StickerResult { 37 | std::vector data; 38 | std::string error; 39 | bool success{false}; 40 | 41 | StickerResult() = default; 42 | StickerResult(std::vector&& d) : data(std::move(d)), success(true) {} 43 | StickerResult(const std::string& err) : error(err), success(false) {} 44 | }; 45 | 46 | struct RGBAFrame { 47 | std::vector data; 48 | int width{0}; 49 | int height{0}; 50 | int64_t pts_ms{0}; 51 | 52 | RGBAFrame() = default; 53 | RGBAFrame(std::vector&& d, int w, int h, int64_t pts) 54 | : data(std::move(d)), width(w), height(h), pts_ms(pts) {} 55 | 56 | size_t size() const { return data.size(); } 57 | bool valid() const { return !data.empty() && width > 0 && height > 0; } 58 | }; 59 | 60 | StickerResult ProcessSticker(const uint8_t* input_data, 61 | size_t input_size, 62 | const StickerOptions& options) noexcept; 63 | 64 | bool IsWebPFormat(const uint8_t* data, size_t size) noexcept; 65 | 66 | std::vector BuildWhatsAppExif(const std::string& packName, 67 | const std::string& authorName, 68 | const std::vector& emojis) noexcept; 69 | 70 | std::vector AttachExifToWebP(const std::vector& webp_data, 71 | const std::vector& exif_data) noexcept; 72 | 73 | std::optional> EncodeRGBA(const uint8_t* rgba_data, 74 | int width, 75 | int height, 76 | int quality) noexcept; 77 | 78 | } // namespace StickerCore -------------------------------------------------------------------------------- /.github/workflows/03-build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | checks: write 12 | attestations: write 13 | id-token: write 14 | 15 | jobs: 16 | build-ubuntu: 17 | name: Build on Ubuntu 24.04 18 | runs-on: self-hosted 19 | outputs: 20 | build-artifact: ${{ steps.upload.outputs.artifact-id }} 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v6 24 | 25 | - name: Setup Node.js 24 26 | uses: actions/setup-node@v6 27 | with: 28 | node-version: '24' 29 | 30 | - name: Setup pnpm 31 | uses: pnpm/action-setup@v4 32 | with: 33 | version: 10 34 | run_install: false 35 | 36 | - name: Check native dependencies 37 | id: check-deps 38 | run: | 39 | for lib in libavformat libavcodec libavutil libswresample libswscale libwebp; do 40 | pkg-config --exists "$lib" || exit 1 41 | done 42 | 43 | for tool in g++ cmake pkg-config python3; do 44 | command -v "$tool" || exit 1 45 | done 46 | 47 | - name: Install dependencies 48 | run: | 49 | pnpm install 50 | 51 | - name: Build C++ Addons 52 | run: | 53 | pnpm exec node-gyp rebuild 54 | 55 | - name: Verify build 56 | run: | 57 | ls build/Release/*.node 1>/dev/null 2>&1 || exit 1 58 | 59 | - name: Upload build artifacts 60 | id: upload 61 | uses: actions/upload-artifact@v6 62 | with: 63 | name: build-ubuntu-24 64 | path: build/Release/*.node 65 | retention-days: 7 66 | 67 | - name: Attest Build Provenance 68 | uses: actions/attest-build-provenance@v3.0.0 69 | with: 70 | subject-path: 'build/Release/*.node' 71 | 72 | validate-installer: 73 | name: Validate Installer Script 74 | runs-on: self-hosted 75 | steps: 76 | - name: Checkout code 77 | uses: actions/checkout@v6 78 | 79 | - name: Check shellcheck availability 80 | id: check-shellcheck 81 | run: | 82 | command -v shellcheck && echo "available=true" >> $GITHUB_OUTPUT || echo "available=false" >> $GITHUB_OUTPUT 83 | 84 | - name: Lint installer script 85 | if: steps.check-shellcheck.outputs.available == 'true' 86 | run: shellcheck -e SC1091 -e SC2034 install.sh || true 87 | 88 | - name: Test installer (dry-run) 89 | run: | 90 | bash -n install.sh || exit 1 -------------------------------------------------------------------------------- /lib/api/play.js: -------------------------------------------------------------------------------- 1 | export async function play(query) { 2 | const encoded = encodeURIComponent(query); 3 | const endpoints = [ 4 | `https://api.nekolabs.web.id/downloader/youtube/play/v1?q=${encoded}`, 5 | `https://api.ootaizumi.web.id/downloader/youtube-play?query=${encoded}`, 6 | `https://anabot.my.id/api/download/playmusic?query=${encoded}&apikey=freeApikey`, 7 | `https://api.elrayyxml.web.id/api/downloader/ytplay?q=${encoded}`, 8 | ]; 9 | 10 | for (const endpoint of endpoints) { 11 | const res = await fetch(endpoint).catch(() => null); 12 | if (!res) continue; 13 | 14 | let json; 15 | try { 16 | json = await res.json(); 17 | } catch { 18 | continue; 19 | } 20 | 21 | if (!json || (!json.success && !json.status)) continue; 22 | 23 | if (json.result?.downloadUrl && json.result?.metadata) { 24 | const { title, channel, cover, url } = json.result.metadata; 25 | return { 26 | success: true, 27 | title, 28 | channel, 29 | cover, 30 | url, 31 | downloadUrl: json.result.downloadUrl, 32 | }; 33 | } 34 | 35 | if (json.result?.download && json.result?.title) { 36 | return { 37 | success: true, 38 | title: json.result.title, 39 | channel: json.result.author?.name || "Unknown Channel", 40 | cover: json.result.thumbnail, 41 | url: json.result.url || null, 42 | downloadUrl: json.result.download, 43 | }; 44 | } 45 | 46 | const ana = json.data?.result; 47 | if (ana?.success && ana?.urls && ana?.metadata) { 48 | return { 49 | success: true, 50 | title: ana.metadata.title, 51 | channel: ana.metadata.channel, 52 | cover: ana.metadata.thumbnail, 53 | url: ana.metadata.webpage_url || null, 54 | downloadUrl: ana.urls, 55 | }; 56 | } 57 | 58 | const elray = json.result; 59 | if ( 60 | elray?.download_url && 61 | elray?.title && 62 | elray?.channel && 63 | elray?.thumbnail && 64 | elray?.url 65 | ) { 66 | return { 67 | success: true, 68 | title: elray.title, 69 | channel: elray.channel, 70 | cover: elray.thumbnail, 71 | url: elray.url, 72 | downloadUrl: elray.download_url, 73 | }; 74 | } 75 | } 76 | 77 | return { success: false, error: "No downloadable track found from any provider." }; 78 | } 79 | -------------------------------------------------------------------------------- /lib/api/removebg.js: -------------------------------------------------------------------------------- 1 | import { uploader } from "../uploader.js"; 2 | 3 | export async function removebg(buffer) { 4 | const up = await uploader(buffer).catch(() => null); 5 | if (!up || !up.url) return { success: false, error: "Upload failed" }; 6 | 7 | const encoded = encodeURIComponent(up.url); 8 | const endpoints = [ 9 | `https://api.nekolabs.web.id/tools/remove-bg/v1?imageUrl=${encoded}`, 10 | `https://api.nekolabs.web.id/tools/remove-bg/v2?imageUrl=${encoded}`, 11 | `https://api.nekolabs.web.id/tools/remove-bg/v3?imageUrl=${encoded}`, 12 | `https://api.nekolabs.web.id/tools/remove-bg/v4?imageUrl=${encoded}`, 13 | `https://api.ootaizumi.web.id/tools/removebg?imageUrl=${encoded}`, 14 | `https://api.elrayyxml.web.id/api/tools/removebg?url=${encoded}`, 15 | ]; 16 | 17 | for (const endpoint of endpoints) { 18 | const res = await fetch(endpoint).catch(() => null); 19 | if (!res) continue; 20 | 21 | const contentType = res.headers.get("content-type") || ""; 22 | 23 | if (/application\/json/.test(contentType)) { 24 | const json = await res.json().catch(() => null); 25 | const result = json?.result || json?.data?.result || json?.output || null; 26 | const success = json?.success === true || json?.status === true; 27 | 28 | if (success && result) { 29 | return { 30 | success: true, 31 | resultUrl: result, 32 | }; 33 | } 34 | } else if (/image\/(png|jpe?g|webp)/.test(contentType)) { 35 | let arrayBuffer = null; 36 | try { 37 | const chunks = []; 38 | const reader = res.body.getReader(); 39 | 40 | while (true) { 41 | const { done, value } = await reader.read(); 42 | if (done) break; 43 | chunks.push(value); 44 | } 45 | 46 | reader.releaseLock(); 47 | 48 | const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); 49 | const combined = new Uint8Array(totalLength); 50 | let offset = 0; 51 | 52 | for (const chunk of chunks) { 53 | combined.set(chunk, offset); 54 | offset += chunk.length; 55 | } 56 | 57 | arrayBuffer = combined.buffer; 58 | } catch { 59 | arrayBuffer = null; 60 | } 61 | 62 | if (arrayBuffer) { 63 | return { 64 | success: true, 65 | resultBuffer: Buffer.from(arrayBuffer), 66 | }; 67 | } 68 | } 69 | } 70 | 71 | return { success: false, error: "All background removal attempts failed" }; 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liora", 3 | "version": "9.0.0", 4 | "description": "Modern WhatsApp bot", 5 | "keywords": [ 6 | "whatsapp", 7 | "baileys", 8 | "nodejs", 9 | "automation" 10 | ], 11 | "type": "module", 12 | "private": false, 13 | "engines": { 14 | "node": ">=24.0.0" 15 | }, 16 | "scripts": { 17 | "start": "node --expose-gc --max-old-space-size=4096 --trace-warnings --trace-deprecation ./src/index.js" 18 | }, 19 | "license": "Apache-2.0", 20 | "author": { 21 | "name": "Naruya Izumi", 22 | "url": "https://linkbio.co/naruyaizumi" 23 | }, 24 | "dependencies": { 25 | "@napi-rs/canvas": "^0.1.84", 26 | "async-mutex": "^0.5.0", 27 | "audio-decode": "^2.2.3", 28 | "baileys": "^7.0.0-rc.9", 29 | "better-sqlite3": "^12.5.0", 30 | "chokidar": "^5.0.0", 31 | "dotenv": "^17.2.3", 32 | "file-type": "^21.1.1", 33 | "lru-cache": "^11.2.4", 34 | "p-queue": "^9.0.1", 35 | "pino": "^10.1.0", 36 | "pino-pretty": "^13.1.3", 37 | "sharp": "^0.34.5", 38 | "yt-search": "^2.13.1" 39 | }, 40 | "devDependencies": { 41 | "@eslint/js": "^9.39.2", 42 | "eslint": "^9.39.2", 43 | "globals": "^16.5.0", 44 | "node-addon-api": "^8.5.0", 45 | "node-gyp": "^12.1.0", 46 | "prettier": "^3.7.4" 47 | }, 48 | "imports": { 49 | "#tiktok": "./lib/api/tiktok.js", 50 | "#instagram": "./lib/api/instagram.js", 51 | "#twitter": "./lib/api/twitter.js", 52 | "#threads": "./lib/api/threads.js", 53 | "#play": "./lib/api/play.js", 54 | "#spotify": "./lib/api/spotify.js", 55 | "#ytmp3": "./lib/api/ytmp3.js", 56 | "#ytmp4": "./lib/api/ytmp4.js", 57 | "#spotifydl": "./lib/api/spotifydl.js", 58 | "#removebg": "./lib/api/removebg.js", 59 | "#remini": "./lib/api/remini.js", 60 | "#config": "./src/config.js", 61 | "#global": "./src/global.js", 62 | "#message": "./lib/core/message.js", 63 | "#socket": "./lib/core/socket.js", 64 | "#connection": "./lib/core/connection.js", 65 | "#add-on": "./lib/cpp/bridge-async.js", 66 | "#auth": "./lib/auth/sqlite-auth.js" 67 | }, 68 | "prettier": { 69 | "printWidth": 100, 70 | "tabWidth": 4, 71 | "useTabs": false, 72 | "semi": true, 73 | "singleQuote": false, 74 | "quoteProps": "as-needed", 75 | "jsxSingleQuote": false, 76 | "trailingComma": "es5", 77 | "bracketSpacing": true, 78 | "bracketSameLine": false, 79 | "arrowParens": "always", 80 | "endOfLine": "lf", 81 | "proseWrap": "preserve", 82 | "htmlWhitespaceSensitivity": "css", 83 | "embeddedLanguageFormatting": "auto" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /plugins/info/info-sc.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | const text = ` 3 | Liora Repository 4 | 5 | Project Script Izumi 6 | Repository: https://github.com/naruyaizumi/liora 7 | Report Bug: https://github.com/naruyaizumi/liora/issues 8 | Pull Req: https://github.com/naruyaizumi/liora/pulls 9 | 10 | © 2024 – 2025 Naruya Izumi • All Rights Reserved 11 | `.trim(); 12 | 13 | const q = { 14 | key: { 15 | fromMe: false, 16 | participant: m.sender, 17 | remoteJid: m.chat, 18 | }, 19 | message: { 20 | requestPaymentMessage: { 21 | amount: { 22 | currencyCode: "USD", 23 | offset: 0, 24 | value: 99999999999, 25 | }, 26 | expiryTimestamp: Date.now() + 24 * 60 * 60 * 1000, 27 | amount1000: 99999999999 * 1000, 28 | currencyCodeIso4217: "USD", 29 | requestFrom: m.sender, 30 | noteMessage: { 31 | extendedTextMessage: { 32 | text: "𝗟 𝗜 𝗢 𝗥 𝗔", 33 | }, 34 | }, 35 | background: { 36 | placeholderArgb: 4278190080, 37 | textArgb: 4294967295, 38 | subtextArgb: 4294967295, 39 | type: 1, 40 | }, 41 | }, 42 | }, 43 | }; 44 | 45 | await conn.sendMessage( 46 | m.chat, 47 | { 48 | product: { 49 | productImage: { 50 | url: "https://files.catbox.moe/wwboj3.jpg", 51 | }, 52 | productId: "32409523241994909", 53 | title: "mkfs.ext4 /dev/naruyaizumi", 54 | description: "", 55 | currencyCode: "IDR", 56 | priceAmount1000: String(23 * 2 ** 32 + 1215752192), 57 | retailerId: "IZUMI", 58 | url: "https://linkbio.co/naruyaizumi", 59 | productImageCount: 5, 60 | signedUrl: 61 | "https://l.wl.co/l/?u=https%3A%2F%2Flinkbio.co%2Fnaruyaizumi&e=AT065QDZzUpFex4H3JaKX1B3jFxLs90G3NEOHbP-LeDGmNM4QfwzF76CAPV6ODSxeErfWu-ZjaaihkWeRUJcUKOdiAfCTnSh3v8uQMqc2-eqKvM8EYzip2AAR-5GsbNJH16tEQ", 62 | }, 63 | businessOwnerJid: "113748182302861@lid", 64 | footer: text, 65 | contextInfo: { 66 | forwardingScore: 999, 67 | isForwarded: true, 68 | forwardedNewsletterMessageInfo: { 69 | newsletterJid: "120363144038483540@newsletter", 70 | newsletterName: "mkfs.ext4 /dev/naruyaizumi", 71 | }, 72 | }, 73 | }, 74 | { quoted: q } 75 | ); 76 | }; 77 | 78 | handler.help = ["script"]; 79 | handler.tags = ["info"]; 80 | handler.command = /^(script|sc)$/i; 81 | 82 | export default handler; 83 | -------------------------------------------------------------------------------- /.github/workflows/02-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Security Scanning 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: "0 0 * * 0" 10 | 11 | permissions: 12 | contents: read 13 | security-events: write 14 | actions: read 15 | id-token: write 16 | 17 | jobs: 18 | codeql: 19 | name: CodeQL Analysis 20 | runs-on: self-hosted 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: ["javascript"] 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v6 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v4 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: security-extended,security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v4 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v4 40 | with: 41 | category: "/language:${{matrix.language}}" 42 | 43 | njsscan: 44 | name: Njsscan Security Analysis 45 | runs-on: self-hosted 46 | steps: 47 | - name: Checkout code 48 | uses: actions/checkout@v6 49 | 50 | - name: Cache Python packages 51 | uses: actions/cache@v4 52 | with: 53 | path: ~/.cache/pip 54 | key: ${{ runner.os }}-pip-njsscan 55 | 56 | - name: Setup Python 57 | uses: actions/setup-python@v6 58 | with: 59 | python-version: "3.12" 60 | 61 | - name: Install Njsscan 62 | run: pip install njsscan 63 | 64 | - name: Run Njsscan 65 | run: njsscan --json > report.json || exit 1 66 | 67 | - name: Upload Njsscan Report 68 | if: always() 69 | uses: actions/upload-artifact@v6 70 | with: 71 | name: njsscan-report 72 | path: report.json 73 | retention-days: 30 74 | 75 | secret-scan: 76 | name: Secret Scanning 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Checkout code 80 | uses: actions/checkout@v6 81 | with: 82 | fetch-depth: 0 83 | 84 | - name: TruffleHog Secret Scan 85 | uses: trufflesecurity/trufflehog@main 86 | with: 87 | path: ./ 88 | extra_args: --only-verified 89 | 90 | trivy-scan: 91 | name: Trivy Container Scan 92 | runs-on: self-hosted 93 | steps: 94 | - name: Checkout code 95 | uses: actions/checkout@v6 96 | 97 | - name: Run Trivy vulnerability scanner 98 | uses: aquasecurity/trivy-action@master 99 | with: 100 | scan-type: "fs" 101 | scan-ref: "." 102 | format: "sarif" 103 | output: "trivy-results.sarif" 104 | 105 | - name: Upload Trivy results to GitHub Security 106 | uses: github/codeql-action/upload-sarif@v4 107 | if: always() 108 | with: 109 | sarif_file: "trivy-results.sarif" -------------------------------------------------------------------------------- /lib/api/threads.js: -------------------------------------------------------------------------------- 1 | export async function threads(url) { 2 | const encoded = encodeURIComponent(url); 3 | const endpoints = [ 4 | `https://api.nekolabs.web.id/downloader/threads?url=${encoded}`, 5 | `https://anabot.my.id/api/download/threads?url=${encoded}&apikey=freeApikey`, 6 | `https://api.deline.web.id/downloader/threads?url=${encoded}`, 7 | ]; 8 | 9 | for (const endpoint of endpoints) { 10 | const res = await fetch(endpoint).catch(() => null); 11 | if (!res) continue; 12 | 13 | const json = await res.json().catch(() => null); 14 | if (!json || (!json.success && !json.status)) continue; 15 | 16 | if (json.result?.images || json.result?.videos) { 17 | const extractMedia = (data) => { 18 | if (!Array.isArray(data)) return []; 19 | return data 20 | .map((group) => { 21 | if (Array.isArray(group) && group.length > 0) { 22 | const best = group[group.length - 1]; 23 | return best?.url_cdn || best?.url; 24 | } 25 | return null; 26 | }) 27 | .filter(Boolean); 28 | }; 29 | 30 | return { 31 | success: true, 32 | caption: json.result.text || json.result.caption || "", 33 | images: extractMedia(json.result.images), 34 | videos: extractMedia(json.result.videos), 35 | }; 36 | } 37 | 38 | const ana = json.data?.result; 39 | if (ana?.image_urls || ana?.video_urls) { 40 | const images = Array.isArray(ana.image_urls) 41 | ? ana.image_urls.filter((x) => typeof x === "string" && x.startsWith("http")) 42 | : []; 43 | 44 | const videos = Array.isArray(ana.video_urls) 45 | ? ana.video_urls 46 | .map((v) => v?.download_url) 47 | .filter((x) => typeof x === "string" && x.startsWith("http")) 48 | : []; 49 | 50 | return { 51 | success: true, 52 | caption: "", 53 | images, 54 | videos, 55 | }; 56 | } 57 | 58 | const agas = json.result; 59 | if (agas?.image || agas?.video) { 60 | const images = Array.isArray(agas.image) 61 | ? agas.image.filter((x) => typeof x === "string" && x.startsWith("http")) 62 | : []; 63 | 64 | const videos = Array.isArray(agas.video) 65 | ? agas.video 66 | .map((v) => v?.download_url) 67 | .filter((x) => typeof x === "string" && x.startsWith("http")) 68 | : []; 69 | 70 | return { 71 | success: true, 72 | caption: "", 73 | images, 74 | videos, 75 | }; 76 | } 77 | } 78 | 79 | return { success: false, error: "No media found from any provider." }; 80 | } 81 | -------------------------------------------------------------------------------- /lib/api/remini.js: -------------------------------------------------------------------------------- 1 | import { uploader } from "../uploader.js"; 2 | 3 | export async function remini(buffer) { 4 | const up = await uploader(buffer).catch(() => null); 5 | if (!up || !up.url) return { success: false, error: "Upload failed" }; 6 | 7 | const encoded = encodeURIComponent(up.url); 8 | const attempts = [ 9 | `https://api.nekolabs.web.id/tools/pxpic/upscale?imageUrl=${encoded}`, 10 | `https://api.nekolabs.web.id/tools/pxpic/enhance?imageUrl=${encoded}`, 11 | `https://api.nekolabs.web.id/tools/ihancer?imageUrl=${encoded}`, 12 | `https://api.zenzxz.my.id/api/tools/upscale?url=${encoded}`, 13 | `https://api.zenzxz.my.id/api/tools/upscalev2?url=${encoded}&scale=2`, 14 | `https://api.zenzxz.my.id/api/tools/upscalev2?url=${encoded}&scale=4`, 15 | `https://api.siputzx.my.id/api/iloveimg/upscale?image=${encoded}&scale=2`, 16 | `https://api.ootaizumi.web.id/tools/upscale?imageUrl=${encoded}`, 17 | `https://api.elrayyxml.web.id/api/tools/remini?url=${encoded}`, 18 | `https://api.elrayyxml.web.id/api/tools/upscale?url=${encoded}&resolusi=5`, 19 | ]; 20 | 21 | for (const url of attempts) { 22 | const res = await fetch(url).catch(() => null); 23 | if (!res) continue; 24 | const type = res.headers.get("content-type") || ""; 25 | 26 | if (type.includes("application/json")) { 27 | const json = await res.json().catch(() => null); 28 | 29 | if (json?.result) { 30 | return { success: true, resultUrl: json.result }; 31 | } 32 | if (json?.data?.url) { 33 | return { success: true, resultUrl: json.data.url }; 34 | } 35 | if (json?.result?.imageUrl) { 36 | return { success: true, resultUrl: json.result.imageUrl }; 37 | } 38 | } 39 | 40 | if (type.includes("image")) { 41 | let arrayBuffer = null; 42 | try { 43 | const chunks = []; 44 | const reader = res.body.getReader(); 45 | 46 | while (true) { 47 | const { done, value } = await reader.read(); 48 | if (done) break; 49 | chunks.push(value); 50 | } 51 | 52 | reader.releaseLock(); 53 | 54 | const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); 55 | const combined = new Uint8Array(totalLength); 56 | let offset = 0; 57 | 58 | for (const chunk of chunks) { 59 | combined.set(chunk, offset); 60 | offset += chunk.length; 61 | } 62 | 63 | arrayBuffer = combined.buffer; 64 | } catch { 65 | arrayBuffer = null; 66 | } 67 | 68 | if (arrayBuffer) { 69 | const buf = Buffer.from(arrayBuffer); 70 | if (buf.length) return { success: true, resultBuffer: buf }; 71 | } 72 | } 73 | } 74 | 75 | return { success: false, error: "All enhancement methods failed" }; 76 | } 77 | -------------------------------------------------------------------------------- /plugins/group/group-add.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn, args, usedPrefix, command }) => { 2 | let target = m.quoted?.sender || null; 3 | 4 | if (!target && args[0]) { 5 | const raw = args[0].replace(/[^0-9]/g, ""); 6 | if (raw.length >= 5) { 7 | target = raw + "@s.whatsapp.net"; 8 | } 9 | } 10 | 11 | if (!target || !target.endsWith("@s.whatsapp.net")) { 12 | return m.reply( 13 | `Specify one valid member to add.\n› Example: ${usedPrefix + command} 6281234567890` 14 | ); 15 | } 16 | 17 | try { 18 | const result = await conn.groupParticipantsUpdate(m.chat, [target], "add"); 19 | const userResult = result?.[0]; 20 | 21 | if (userResult?.status === "200") { 22 | return await conn.sendMessage( 23 | m.chat, 24 | { 25 | text: `Successfully added @${target.split("@")[0]}.`, 26 | mentions: [target], 27 | }, 28 | { quoted: m } 29 | ); 30 | } else if (userResult?.status === "403") { 31 | const groupMetadata = await conn.groupMetadata(m.chat); 32 | const inviteCode = await conn.groupInviteCode(m.chat); 33 | const groupName = groupMetadata.subject || "Unknown Subject"; 34 | const inviteExpiration = Date.now() + 3 * 24 * 60 * 60 * 1000; 35 | 36 | let jpegThumbnail = null; 37 | try { 38 | const profilePic = await conn.profilePictureUrl(m.chat, "image").catch(() => null); 39 | if (profilePic) { 40 | const response = await fetch(profilePic); 41 | const buffer = await response.arrayBuffer(); 42 | jpegThumbnail = Buffer.from(buffer); 43 | } 44 | } catch (e) { 45 | conn.logger.warn({ err: e.message }, "Failed to fetch group thumbnail"); 46 | } 47 | 48 | await conn.sendInviteGroup( 49 | m.chat, 50 | target, 51 | inviteCode, 52 | inviteExpiration, 53 | groupName, 54 | `Cannot add you directly. Here is the invitation to join ${groupName}`, 55 | jpegThumbnail, 56 | { mentions: [target] } 57 | ); 58 | 59 | return await conn.sendMessage( 60 | m.chat, 61 | { 62 | text: `Cannot add @${target.split("@")[0]} directly. Group invitation has been sent to their private chat.`, 63 | mentions: [target], 64 | }, 65 | { quoted: m } 66 | ); 67 | } else { 68 | return m.reply(`Failed to add the member. Status: ${userResult?.status || "unknown"}`); 69 | } 70 | } catch (e) { 71 | conn.logger.error(e); 72 | return m.reply(`Error: ${e.message}`); 73 | } 74 | }; 75 | 76 | handler.help = ["add"]; 77 | handler.tags = ["group"]; 78 | handler.command = /^(add)$/i; 79 | handler.group = true; 80 | handler.botAdmin = true; 81 | handler.admin = true; 82 | 83 | export default handler; 84 | -------------------------------------------------------------------------------- /plugins/tool/tool-setting.js: -------------------------------------------------------------------------------- 1 | const features = [ 2 | { key: "adminOnly", scope: "chat", name: "Admin Only" }, 3 | { key: "antiLinks", scope: "chat", name: "Anti Link" }, 4 | { key: "antiStatus", scope: "chat", name: "Anti Status Mention" }, 5 | { key: "antiSticker", scope: "chat", name: "Anti Sticker" }, 6 | { key: "antiAudio", scope: "chat", name: "Anti Audio" }, 7 | { key: "antiFile", scope: "chat", name: "Anti File" }, 8 | { key: "antiFoto", scope: "chat", name: "Anti Foto" }, 9 | { key: "antiVideo", scope: "chat", name: "Anti Video" }, 10 | 11 | { key: "self", scope: "bot", name: "Self Mode" }, 12 | { key: "gconly", scope: "bot", name: "Group Only" }, 13 | { key: "noprint", scope: "bot", name: "No Print" }, 14 | { key: "autoread", scope: "bot", name: "Auto Read" }, 15 | { key: "restrict", scope: "bot", name: "Restrict" }, 16 | { key: "adReply", scope: "bot", name: "Ad Reply" }, 17 | ]; 18 | 19 | function listFeatures(isOwner, chat, bot) { 20 | const available = isOwner ? features : features.filter((f) => f.scope === "chat"); 21 | return available 22 | .map((f, i) => { 23 | const state = f.scope === "chat" ? chat[f.key] : bot[f.key]; 24 | return `${i + 1}. ${f.name} [${state ? "ON" : "OFF"}]`; 25 | }) 26 | .join("\n"); 27 | } 28 | 29 | let handler = async (m, { conn, isOwner, isAdmin, args, usedPrefix, command }) => { 30 | try { 31 | const chat = global.db.data.chats[m.chat]; 32 | const bot = global.db.data.settings[conn.user.lid] || {}; 33 | 34 | if (!args[0]) { 35 | const daftar = listFeatures(isOwner, chat, bot); 36 | return m.reply( 37 | `=== Feature Toggle === 38 | ${daftar} 39 | 40 | Usage: 41 | › ${usedPrefix + command} 1 2 3 => enable multiple features 42 | › ${usedPrefix + (command === "on" ? "off" : "on")} 4 5 6 => disable features` 43 | ); 44 | } 45 | 46 | const enable = command === "on"; 47 | const indexes = args.map((n) => parseInt(n)).filter((n) => !isNaN(n)); 48 | 49 | if (!indexes.length) return m.reply("Invalid feature number."); 50 | 51 | const results = []; 52 | for (const i of indexes) { 53 | const fitur = (isOwner ? features : features.filter((f) => f.scope === "chat"))[i - 1]; 54 | if (!fitur) continue; 55 | 56 | if (fitur.scope === "chat") { 57 | if (!(isAdmin || isOwner)) continue; 58 | chat[fitur.key] = enable; 59 | } else if (fitur.scope === "bot") { 60 | if (!isOwner) continue; 61 | bot[fitur.key] = enable; 62 | } 63 | results.push(`${fitur.name}: ${enable ? "ON" : "OFF"}`); 64 | } 65 | 66 | if (!results.length) return m.reply("No features were modified."); 67 | return m.reply(`Updated features:\n${results.join("\n")}`); 68 | } catch (e) { 69 | conn.logger.error(e); 70 | m.reply(`Error: ${e.message}`); 71 | } 72 | }; 73 | 74 | handler.help = ["on", "off"]; 75 | handler.tags = ["tools"]; 76 | handler.command = /^(on|off)$/i; 77 | handler.group = true; 78 | 79 | export default handler; 80 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | # Clang-Tidy configuration for Liora C++ code 2 | Checks: > 3 | -*, 4 | bugprone-*, 5 | -bugprone-easily-swappable-parameters, 6 | cert-*, 7 | clang-analyzer-*, 8 | concurrency-*, 9 | cppcoreguidelines-*, 10 | -cppcoreguidelines-avoid-magic-numbers, 11 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 12 | -cppcoreguidelines-pro-type-reinterpret-cast, 13 | misc-*, 14 | -misc-non-private-member-variables-in-classes, 15 | modernize-*, 16 | -modernize-use-trailing-return-type, 17 | performance-*, 18 | readability-*, 19 | -readability-magic-numbers, 20 | -readability-identifier-length, 21 | -readability-function-cognitive-complexity 22 | 23 | WarningsAsErrors: "" 24 | 25 | HeaderFilterRegex: "lib/cpp/.*" 26 | 27 | CheckOptions: 28 | # Naming conventions 29 | - key: readability-identifier-naming.ClassCase 30 | value: CamelCase 31 | - key: readability-identifier-naming.StructCase 32 | value: CamelCase 33 | - key: readability-identifier-naming.FunctionCase 34 | value: camelBack 35 | - key: readability-identifier-naming.VariableCase 36 | value: lower_case 37 | - key: readability-identifier-naming.ConstantCase 38 | value: UPPER_CASE 39 | - key: readability-identifier-naming.ParameterCase 40 | value: lower_case 41 | - key: readability-identifier-naming.MemberCase 42 | value: lower_case 43 | - key: readability-identifier-naming.MemberSuffix 44 | value: "_" 45 | - key: readability-identifier-naming.PrivateMemberSuffix 46 | value: "_" 47 | - key: readability-identifier-naming.NamespaceCase 48 | value: lower_case 49 | 50 | # Performance 51 | - key: performance-move-const-arg.CheckTriviallyCopyableMove 52 | value: "false" 53 | - key: performance-unnecessary-value-param.AllowedTypes 54 | value: "Napi::.*" 55 | 56 | # Modernize 57 | - key: modernize-use-auto.MinTypeNameLength 58 | value: "8" 59 | - key: modernize-loop-convert.MinConfidence 60 | value: "reasonable" 61 | 62 | # Readability 63 | - key: readability-function-size.LineThreshold 64 | value: "100" 65 | - key: readability-function-size.StatementThreshold 66 | value: "50" 67 | - key: readability-function-size.BranchThreshold 68 | value: "10" 69 | - key: readability-function-size.ParameterThreshold 70 | value: "6" 71 | - key: readability-simplify-boolean-expr.ChainedConditionalReturn 72 | value: "true" 73 | - key: readability-simplify-boolean-expr.ChainedConditionalAssignment 74 | value: "true" 75 | 76 | # C++ Core Guidelines 77 | - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor 78 | value: "true" 79 | - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors 80 | value: "true" 81 | 82 | # Bugprone 83 | - key: bugprone-assert-side-effect.AssertMacros 84 | value: "assert,NAPI_ASSERT" 85 | - key: bugprone-exception-escape.FunctionsThatShouldNotThrow 86 | value: "" 87 | 88 | # Specific rules for N-API code 89 | # Allow raw pointers in N-API bindings as they're required by the API 90 | # Allow reinterpret_cast for N-API data handling 91 | # Allow magic numbers in N-API status codes 92 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature Request 2 | description: Suggest a new feature or enhancement 3 | title: "[Feature]: " 4 | labels: ["enhancement", "needs-discussion"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting a feature! Please provide as much detail as possible. 10 | 11 | - type: textarea 12 | id: problem 13 | attributes: 14 | label: Problem Statement 15 | description: Is your feature request related to a problem? Please describe. 16 | placeholder: I'm frustrated when... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: solution 22 | attributes: 23 | label: Proposed Solution 24 | description: Describe the solution you'd like 25 | placeholder: I would like to... 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: alternatives 31 | attributes: 32 | label: Alternative Solutions 33 | description: Describe alternatives you've considered 34 | placeholder: I've also considered... 35 | 36 | - type: dropdown 37 | id: category 38 | attributes: 39 | label: Feature Category 40 | description: What area does this feature belong to? 41 | options: 42 | - Core Bot Functionality 43 | - Media Processing 44 | - API Integration (new platform) 45 | - Performance Optimization 46 | - Developer Experience 47 | - Documentation 48 | - Testing 49 | - Security 50 | - Other 51 | validations: 52 | required: true 53 | 54 | - type: dropdown 55 | id: priority 56 | attributes: 57 | label: Priority 58 | description: How important is this feature to you? 59 | options: 60 | - Low (Nice to have) 61 | - Medium (Would help) 62 | - High (Need it soon) 63 | - Critical (Blocking my use case) 64 | validations: 65 | required: true 66 | 67 | - type: textarea 68 | id: use-case 69 | attributes: 70 | label: Use Case 71 | description: Describe your use case and how this feature would help 72 | placeholder: | 73 | I want to... 74 | So that I can... 75 | validations: 76 | required: true 77 | 78 | - type: textarea 79 | id: examples 80 | attributes: 81 | label: Examples 82 | description: Provide examples, mockups, or code snippets 83 | render: javascript 84 | placeholder: | 85 | // Example usage 86 | await bot.newFeature(options); 87 | 88 | - type: checkboxes 89 | id: implementation 90 | attributes: 91 | label: Implementation Willingness 92 | description: Are you willing to help implement this feature? 93 | options: 94 | - label: I can implement this feature and submit a PR 95 | - label: I can help test this feature 96 | - label: I can provide additional information/feedback 97 | - label: I'm requesting someone else to implement this 98 | 99 | - type: textarea 100 | id: additional 101 | attributes: 102 | label: Additional Context 103 | description: Add any other context, screenshots, or references 104 | placeholder: Any additional information... -------------------------------------------------------------------------------- /plugins/group/group-info.js: -------------------------------------------------------------------------------- 1 | let handler = async (m, { conn }) => { 2 | try { 3 | await global.loading(m, conn); 4 | let groupMeta; 5 | 6 | if (conn.chats[m.chat]?.metadata) { 7 | groupMeta = conn.chats[m.chat].metadata; 8 | } else { 9 | return m.reply("Group metadata is not available. Please run groupUp first."); 10 | } 11 | 12 | const participants = groupMeta.participants || []; 13 | const groupAdmins = participants.filter((p) => p.admin); 14 | const owner = 15 | groupMeta.owner || 16 | groupAdmins.find((p) => p.admin === "superadmin")?.id || 17 | m.chat.split`-`[0] + "@s.whatsapp.net"; 18 | 19 | const listAdmin = 20 | groupAdmins.map((v, i) => `${i + 1}. @${v.id.split("@")[0]}`).join("\n") || "-"; 21 | 22 | const ephemeralTime = (() => { 23 | switch (groupMeta.ephemeralDuration) { 24 | case 86400: 25 | return "24 hours"; 26 | case 604800: 27 | return "7 days"; 28 | case 2592000: 29 | return "30 days"; 30 | case 7776000: 31 | return "90 days"; 32 | default: 33 | return "None"; 34 | } 35 | })(); 36 | 37 | const creationDate = groupMeta.creation 38 | ? new Date(groupMeta.creation * 1000).toLocaleString("en-US", { 39 | timeZone: "UTC", 40 | dateStyle: "medium", 41 | timeStyle: "short", 42 | }) 43 | : "(unknown)"; 44 | 45 | const desc = groupMeta.desc || "(none)"; 46 | let pp = null; 47 | try { 48 | pp = await conn.profilePictureUrl(m.chat, "image"); 49 | } catch (e) { 50 | conn.logger.warn(`No profile picture for group ${m.chat}: ${e.message}`); 51 | } 52 | 53 | const mentions = [...new Set([...groupAdmins.map((v) => v.id), owner])]; 54 | 55 | const text = ` 56 | 『 Group Information 』 57 | 58 | ID: ${m.chat} 59 | Name: ${groupMeta.subject || "(unknown)"} 60 | Members: ${participants.length} 61 | Owner: @${owner.split("@")[0]} 62 | 63 | Administrators: 64 | ${listAdmin} 65 | 66 | Description: 67 | ${desc} 68 | 69 | Creation: ${creationDate} 70 | Ephemeral Timer: ${ephemeralTime} 71 | Announcement Only: ${groupMeta.announce ? "Yes" : "No"} 72 | `.trim(); 73 | 74 | if (pp) { 75 | await conn.sendMessage(m.chat, { 76 | image: { url: pp }, 77 | caption: text, 78 | mentions: mentions, 79 | }); 80 | } else { 81 | await conn.sendMessage(m.chat, { 82 | text: text, 83 | mentions: mentions, 84 | }); 85 | } 86 | } catch (e) { 87 | conn.logger.error(e); 88 | m.reply(`Error: ${e.message}`); 89 | } finally { 90 | await global.loading(m, conn, true); 91 | } 92 | }; 93 | 94 | handler.help = ["groupinfo"]; 95 | handler.tags = ["group"]; 96 | handler.command = /^(groupinfo|info(gro?up|gc))$/i; 97 | handler.group = true; 98 | handler.admin = true; 99 | 100 | export default handler; 101 | -------------------------------------------------------------------------------- /lib/cpp/workers/sticker-worker.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { parentPort } from "worker_threads"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | let stickerAddon = null; 9 | let consecutiveErrors = 0; 10 | const MAX_CONSECUTIVE_ERRORS = 5; 11 | 12 | function loadAddon() { 13 | if (stickerAddon) return stickerAddon; 14 | 15 | const projectRoot = path.resolve(__dirname, "../../.."); 16 | const searchPaths = [ 17 | path.join(projectRoot, "build", "Release", "sticker.node"), 18 | path.join(projectRoot, "build", "Debug", "sticker.node"), 19 | ]; 20 | 21 | for (const addonPath of searchPaths) { 22 | try { 23 | stickerAddon = require(addonPath); 24 | consecutiveErrors = 0; 25 | return stickerAddon; 26 | } catch { 27 | continue; 28 | } 29 | } 30 | 31 | throw new Error("sticker.node not found in Node.js worker thread"); 32 | } 33 | 34 | async function processTask(data) { 35 | const { buffer, options } = data; 36 | 37 | if (!Buffer.isBuffer(buffer)) { 38 | throw new Error("Invalid input: buffer must be a Buffer"); 39 | } 40 | 41 | if (buffer.length === 0) { 42 | throw new Error("Invalid input: empty buffer"); 43 | } 44 | 45 | if (buffer.length > 100 * 1024 * 1024) { 46 | throw new Error("Input too large (max 100MB)"); 47 | } 48 | 49 | const addon = loadAddon(); 50 | 51 | return addon.sticker(buffer, options); 52 | } 53 | 54 | if (parentPort) { 55 | parentPort.on("message", async (data) => { 56 | try { 57 | const result = await processTask(data); 58 | 59 | consecutiveErrors = 0; 60 | 61 | parentPort.postMessage({ 62 | data: result, 63 | error: null, 64 | }); 65 | } catch (error) { 66 | consecutiveErrors++; 67 | 68 | parentPort.postMessage({ 69 | data: null, 70 | error: error.message, 71 | }); 72 | 73 | if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { 74 | console.error( 75 | `[StickerWorker] Too many consecutive errors (${consecutiveErrors}), exiting...` 76 | ); 77 | process.exit(1); 78 | } 79 | } 80 | }); 81 | 82 | parentPort.on("error", (error) => { 83 | console.error("[StickerWorker] Parent port error:", error); 84 | process.exit(1); 85 | }); 86 | 87 | process.on("uncaughtException", (error) => { 88 | console.error("[StickerWorker] Uncaught exception:", error); 89 | if (parentPort) { 90 | parentPort.postMessage({ 91 | data: null, 92 | error: `Uncaught exception: ${error.message}`, 93 | }); 94 | } 95 | process.exit(1); 96 | }); 97 | 98 | process.on("unhandledRejection", (reason) => { 99 | console.error("[StickerWorker] Unhandled rejection:", reason); 100 | if (parentPort) { 101 | parentPort.postMessage({ 102 | data: null, 103 | error: `Unhandled rejection: ${reason}`, 104 | }); 105 | } 106 | process.exit(1); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /lib/cpp/workers/converter-worker.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { parentPort } from "worker_threads"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | let converterAddon = null; 9 | let consecutiveErrors = 0; 10 | const MAX_CONSECUTIVE_ERRORS = 5; 11 | 12 | function loadAddon() { 13 | if (converterAddon) return converterAddon; 14 | 15 | const projectRoot = path.resolve(__dirname, "../../.."); 16 | const searchPaths = [ 17 | path.join(projectRoot, "build", "Release", "converter.node"), 18 | path.join(projectRoot, "build", "Debug", "converter.node"), 19 | ]; 20 | 21 | for (const addonPath of searchPaths) { 22 | try { 23 | converterAddon = require(addonPath); 24 | consecutiveErrors = 0; 25 | return converterAddon; 26 | } catch { 27 | continue; 28 | } 29 | } 30 | 31 | throw new Error("converter.node not found in Node.js worker thread"); 32 | } 33 | 34 | async function processTask(data) { 35 | const { buffer, options } = data; 36 | 37 | if (!Buffer.isBuffer(buffer)) { 38 | throw new Error("Invalid input: buffer must be a Buffer"); 39 | } 40 | 41 | if (buffer.length === 0) { 42 | throw new Error("Invalid input: empty buffer"); 43 | } 44 | 45 | if (buffer.length > 100 * 1024 * 1024) { 46 | throw new Error("Input too large (max 100MB)"); 47 | } 48 | 49 | const addon = loadAddon(); 50 | 51 | return addon.convert(buffer, options); 52 | } 53 | 54 | if (parentPort) { 55 | parentPort.on("message", async (data) => { 56 | try { 57 | const result = await processTask(data); 58 | 59 | consecutiveErrors = 0; 60 | 61 | parentPort.postMessage({ 62 | data: result, 63 | error: null, 64 | }); 65 | } catch (error) { 66 | consecutiveErrors++; 67 | 68 | parentPort.postMessage({ 69 | data: null, 70 | error: error.message, 71 | }); 72 | 73 | if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { 74 | console.error( 75 | `[ConverterWorker] Too many consecutive errors (${consecutiveErrors}), exiting...` 76 | ); 77 | process.exit(1); 78 | } 79 | } 80 | }); 81 | 82 | parentPort.on("error", (error) => { 83 | console.error("[ConverterWorker] Parent port error:", error); 84 | process.exit(1); 85 | }); 86 | 87 | process.on("uncaughtException", (error) => { 88 | console.error("[ConverterWorker] Uncaught exception:", error); 89 | if (parentPort) { 90 | parentPort.postMessage({ 91 | data: null, 92 | error: `Uncaught exception: ${error.message}`, 93 | }); 94 | } 95 | process.exit(1); 96 | }); 97 | 98 | process.on("unhandledRejection", (reason) => { 99 | console.error("[ConverterWorker] Unhandled rejection:", reason); 100 | if (parentPort) { 101 | parentPort.postMessage({ 102 | data: null, 103 | error: `Unhandled rejection: ${reason}`, 104 | }); 105 | } 106 | process.exit(1); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | Language: Cpp 3 | Standard: c++20 4 | 5 | # Indentation 6 | IndentWidth: 4 7 | TabWidth: 4 8 | UseTab: Never 9 | ContinuationIndentWidth: 8 10 | IndentCaseLabels: true 11 | IndentPPDirectives: BeforeHash 12 | IndentWrappedFunctionNames: false 13 | 14 | # Line breaking 15 | ColumnLimit: 100 16 | MaxEmptyLinesToKeep: 2 17 | KeepEmptyLinesAtTheStartOfBlocks: false 18 | 19 | # Braces 20 | BraceWrapping: 21 | AfterClass: false 22 | AfterControlStatement: Never 23 | AfterEnum: false 24 | AfterFunction: false 25 | AfterNamespace: false 26 | AfterStruct: false 27 | AfterUnion: false 28 | AfterExternBlock: false 29 | BeforeCatch: false 30 | BeforeElse: false 31 | BeforeLambdaBody: false 32 | BeforeWhile: false 33 | SplitEmptyFunction: false 34 | SplitEmptyRecord: false 35 | SplitEmptyNamespace: false 36 | 37 | BreakBeforeBraces: Attach 38 | BreakConstructorInitializers: BeforeColon 39 | BreakInheritanceList: BeforeColon 40 | CompactNamespaces: false 41 | 42 | # Spacing 43 | SpaceAfterCStyleCast: false 44 | SpaceAfterLogicalNot: false 45 | SpaceAfterTemplateKeyword: true 46 | SpaceBeforeAssignmentOperators: true 47 | SpaceBeforeCpp11BracedList: false 48 | SpaceBeforeCtorInitializerColon: true 49 | SpaceBeforeInheritanceColon: true 50 | SpaceBeforeParens: ControlStatements 51 | SpaceBeforeRangeBasedForLoopColon: true 52 | SpaceInEmptyParentheses: false 53 | SpacesBeforeTrailingComments: 2 54 | SpacesInAngles: false 55 | SpacesInCStyleCastParentheses: false 56 | SpacesInContainerLiterals: false 57 | SpacesInParentheses: false 58 | SpacesInSquareBrackets: false 59 | 60 | # Alignment 61 | AlignAfterOpenBracket: Align 62 | AlignConsecutiveAssignments: false 63 | AlignConsecutiveDeclarations: false 64 | AlignConsecutiveMacros: true 65 | AlignEscapedNewlines: Left 66 | AlignOperands: true 67 | AlignTrailingComments: true 68 | 69 | # Pointers and references 70 | DerivePointerAlignment: false 71 | PointerAlignment: Left 72 | 73 | # Includes 74 | SortIncludes: true 75 | IncludeBlocks: Regroup 76 | IncludeCategories: 77 | # N-API headers first 78 | - Regex: '^|^' 79 | Priority: 1 80 | # System headers 81 | - Regex: "^<.*>" 82 | Priority: 2 83 | # FFmpeg headers 84 | - Regex: "^" 85 | Priority: 3 86 | # libwebp headers 87 | - Regex: "^" 88 | Priority: 4 89 | # Project headers 90 | - Regex: '^".*"' 91 | Priority: 5 92 | 93 | # Function and method formatting 94 | AllowShortFunctionsOnASingleLine: Inline 95 | AllowShortIfStatementsOnASingleLine: Never 96 | AllowShortLoopsOnASingleLine: false 97 | AllowShortCaseLabelsOnASingleLine: false 98 | AllowShortBlocksOnASingleLine: Never 99 | AllowShortLambdasOnASingleLine: Inline 100 | 101 | BinPackArguments: false 102 | BinPackParameters: false 103 | AlwaysBreakAfterReturnType: None 104 | AlwaysBreakBeforeMultilineStrings: false 105 | AlwaysBreakTemplateDeclarations: Yes 106 | 107 | # Comments 108 | ReflowComments: true 109 | FixNamespaceComments: true 110 | 111 | # Penalties (for line breaking decisions) 112 | PenaltyBreakAssignment: 100 113 | PenaltyBreakBeforeFirstCallParameter: 100 114 | PenaltyBreakComment: 300 115 | PenaltyBreakFirstLessLess: 120 116 | PenaltyBreakString: 1000 117 | PenaltyExcessCharacter: 1000000 118 | PenaltyReturnTypeOnItsOwnLine: 200 119 | 120 | # Other 121 | Cpp11BracedListStyle: true 122 | NamespaceIndentation: None 123 | AccessModifierOffset: -4 124 | ConstructorInitializerIndentWidth: 4 125 | -------------------------------------------------------------------------------- /.github/workflows/04-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Management 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: write 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | check-version: 14 | name: Check Version Change 15 | runs-on: self-hosted 16 | outputs: 17 | should_release: ${{ steps.check.outputs.release }} 18 | new_version: ${{ steps.check.outputs.version }} 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v6 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Check version vs tag 26 | id: check 27 | run: | 28 | PACKAGE_VERSION=$(jq -r '.version' package.json) 29 | echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT 30 | 31 | if git rev-parse "v$PACKAGE_VERSION" >/dev/null 2>&1; then 32 | echo "Tag already exists: v$PACKAGE_VERSION" 33 | echo "release=false" >> $GITHUB_OUTPUT 34 | else 35 | echo "New version detected: $PACKAGE_VERSION" 36 | echo "release=true" >> $GITHUB_OUTPUT 37 | fi 38 | 39 | create-tag: 40 | name: Create Tag if Needed 41 | needs: check-version 42 | if: needs.check-version.outputs.should_release == 'true' 43 | runs-on: self-hosted 44 | steps: 45 | - name: Checkout code 46 | uses: actions/checkout@v6 47 | with: 48 | token: ${{ secrets.PAT_TOKEN }} 49 | fetch-depth: 0 50 | 51 | - name: Create tag 52 | run: | 53 | VERSION="${{ needs.check-version.outputs.new_version }}" 54 | git config user.name "liora-bot" 55 | git config user.email "liora.bot.official@gmail.com" 56 | git tag -a "v$VERSION" -m "Release v$VERSION" 57 | git push origin "v$VERSION" 58 | 59 | create-release: 60 | name: Create GitHub Release 61 | needs: [check-version, create-tag] 62 | if: needs.check-version.outputs.should_release == 'true' 63 | runs-on: self-hosted 64 | steps: 65 | - name: Checkout code 66 | uses: actions/checkout@v6 67 | with: 68 | fetch-depth: 0 69 | 70 | - name: Generate changelog 71 | id: changelog 72 | run: | 73 | VERSION="${{ needs.check-version.outputs.new_version }}" 74 | PREV_TAG=$(git describe --tags --abbrev=0 "v$VERSION^" 2>/dev/null || echo "") 75 | 76 | if [ -z "$PREV_TAG" ]; then 77 | CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) 78 | else 79 | CHANGELOG=$(git log "$PREV_TAG..v$VERSION" --pretty=format:"- %s (%h)" --no-merges) 80 | fi 81 | 82 | echo "changelog<> $GITHUB_OUTPUT 83 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 84 | echo "EOF" >> $GITHUB_OUTPUT 85 | 86 | - name: Create GitHub Release 87 | uses: softprops/action-gh-release@v2 88 | with: 89 | tag_name: v${{ needs.check-version.outputs.new_version }} 90 | name: Release v${{ needs.check-version.outputs.new_version }} 91 | body: | 92 | # 🎉 Release v${{ needs.check-version.outputs.new_version }} 93 | 94 | ## What's Changed 95 | 96 | ${{ steps.changelog.outputs.changelog }} 97 | 98 | ## Full Changelog 99 | 100 | **Full Changelog**: https://github.com/naruyaizumi/liora/compare/v${{ needs.check-version.outputs.new_version }} 101 | draft: false 102 | prerelease: false 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Type of Change 6 | 7 | 8 | 9 | - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) 10 | - [ ] ✨ New feature (non-breaking change which adds functionality) 11 | - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] 📝 Documentation update 13 | - [ ] 🎨 Code style/formatting 14 | - [ ] ♻️ Code refactoring (no functional changes) 15 | - [ ] ⚡ Performance improvement 16 | - [ ] ✅ Test updates 17 | - [ ] 🔧 Build/CI changes 18 | - [ ] 📦 Dependency updates 19 | 20 | ## Changes Made 21 | 22 | 23 | 24 | - 25 | - 26 | - 27 | 28 | ## Related Issues 29 | 30 | 31 | 32 | - Fixes # 33 | - Related to # 34 | 35 | ## Testing 36 | 37 | ### How to Test 38 | 39 | 40 | 41 | 1. 42 | 2. 43 | 3. 44 | 45 | ### Test Results 46 | 47 | 48 | 49 | - [ ] All existing tests pass 50 | - [ ] New tests added (if applicable) 51 | - [ ] Manual testing completed 52 | 53 | ## Performance Impact 54 | 55 | 56 | 57 | **Before:** 58 | ``` 59 | 60 | ``` 61 | 62 | **After:** 63 | ``` 64 | 65 | ``` 66 | 67 | **Impact:** 68 | 69 | 70 | ## Code Quality Checklist 71 | 72 | 73 | 74 | - [ ] Code follows the project's style guidelines (Prettier for JS, clang-format for C++) 75 | - [ ] Self-reviewed my own code 76 | - [ ] Commented hard-to-understand areas 77 | - [ ] Made corresponding changes to documentation 78 | - [ ] No new warnings generated 79 | - [ ] Added tests that prove the fix is effective or the feature works 80 | - [ ] New and existing unit tests pass locally 81 | 82 | ## C++ Specific (if applicable) 83 | 84 | - [ ] No memory leaks (checked with valgrind or AddressSanitizer) 85 | - [ ] Uses RAII for resource management 86 | - [ ] Uses smart pointers instead of raw pointers where appropriate 87 | - [ ] Proper error handling in N-API bindings 88 | - [ ] No blocking operations in async workers 89 | - [ ] Follows C++20 best practices 90 | 91 | ## Breaking Changes 92 | 93 | 94 | 95 | **Breaking:** 96 | - 97 | 98 | **Migration Guide:** 99 | ```javascript 100 | // Before 101 | ... 102 | 103 | // After 104 | ... 105 | ``` 106 | 107 | ## Screenshots/Demos 108 | 109 | 110 | 111 | ## Additional Context 112 | 113 | 114 | 115 | ## Checklist for Reviewers 116 | 117 | 118 | 119 | - [ ] Code quality is acceptable 120 | - [ ] Tests are comprehensive 121 | - [ ] Documentation is updated 122 | - [ ] Performance impact is acceptable 123 | - [ ] Security implications have been considered 124 | - [ ] Breaking changes are properly documented 125 | - [ ] Commit messages follow conventional commits 126 | 127 | --- 128 | 129 | **Note:** This PR will trigger automated checks: 130 | - ✅ Prettier/ESLint for JavaScript 131 | - ✅ clang-format/clang-tidy for C++ 132 | - ✅ Security scans (CodeQL, Trivy) 133 | - ✅ Build tests (Ubuntu 24.04, Debian 12) 134 | - ✅ Memory safety checks (if C++ changes) -------------------------------------------------------------------------------- /lib/api/instagram.js: -------------------------------------------------------------------------------- 1 | export async function instagram(url) { 2 | const encoded = encodeURIComponent(url); 3 | const endpoints = [ 4 | `https://api.nekolabs.web.id/downloader/instagram?url=${encoded}`, 5 | `https://api.elrayyxml.web.id/api/downloader/instagram?url=${encoded}`, 6 | `https://api.zenzxz.my.id/api/downloader/instagram?url=${encoded}`, 7 | `https://anabot.my.id/api/download/instagram?url=${encoded}&apikey=freeApikey`, 8 | `https://api.ootaizumi.web.id/downloader/instagram?url=${encoded}`, 9 | ]; 10 | 11 | for (const endpoint of endpoints) { 12 | const res = await fetch(endpoint).catch(() => null); 13 | if (!res) continue; 14 | 15 | const json = await res.json().catch(() => null); 16 | if (!json || (!json.success && !json.status)) continue; 17 | 18 | const raw = 19 | json.result || 20 | json.data?.result || 21 | json.data || 22 | json.result?.media || 23 | json.result?.media?.media; 24 | 25 | if ( 26 | json.result?.media && 27 | typeof json.result.media === "string" && 28 | json.result.isVideo === true 29 | ) { 30 | return { 31 | success: true, 32 | type: "video", 33 | urls: [json.result.media], 34 | }; 35 | } 36 | 37 | if ( 38 | json.result?.media && 39 | Array.isArray(json.result.media) && 40 | json.result.isVideo === false 41 | ) { 42 | const uniqueImages = [...new Set(json.result.media)]; 43 | return { 44 | success: true, 45 | type: "images", 46 | urls: uniqueImages, 47 | }; 48 | } 49 | 50 | if (Array.isArray(raw)) { 51 | const formatZenz = raw.every( 52 | (item) => typeof item === "object" && ("videoUrl" in item || "imageUrl" in item) 53 | ); 54 | 55 | if (formatZenz) { 56 | const videoItems = raw.filter((item) => item.videoUrl); 57 | const imageItems = raw.filter((item) => item.imageUrl); 58 | 59 | if (videoItems.length === 1 && imageItems.length === 0) { 60 | return { 61 | success: true, 62 | type: "video", 63 | urls: [videoItems[0].videoUrl], 64 | }; 65 | } 66 | 67 | if (imageItems.length > 0) { 68 | const uniqueImages = [...new Set(imageItems.map((item) => item.imageUrl))]; 69 | return { 70 | success: true, 71 | type: "images", 72 | urls: uniqueImages, 73 | }; 74 | } 75 | 76 | continue; 77 | } 78 | 79 | const urls = raw.map((item) => item.url).filter(Boolean); 80 | if (urls.length) { 81 | const uniqueUrls = [...new Set(urls)]; 82 | return { 83 | success: true, 84 | type: uniqueUrls.length === 1 ? "video" : "images", 85 | urls: uniqueUrls, 86 | }; 87 | } 88 | } 89 | 90 | const fallbackUrl = raw?.url || raw?.downloadUrl; 91 | if (fallbackUrl) { 92 | return { 93 | success: true, 94 | type: "video", 95 | urls: [fallbackUrl], 96 | }; 97 | } 98 | } 99 | 100 | return { success: false, error: "No downloadable media found." }; 101 | } 102 | -------------------------------------------------------------------------------- /plugins/owner/owner-exec.js: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { promisify } from "util"; 3 | const execAsync = promisify(exec); 4 | 5 | const blocked = [ 6 | "rm -rf /", 7 | "rm -rf *", 8 | "rm --no-preserve-root -rf /", 9 | "mkfs.ext4", 10 | "dd if=", 11 | "chmod 777 /", 12 | "chown root:root /", 13 | "mv /", 14 | "cp /", 15 | "shutdown", 16 | "reboot", 17 | "poweroff", 18 | "halt", 19 | "kill -9 1", 20 | ">:(){ :|: & };:", 21 | ]; 22 | 23 | const handler = async (m, { conn, isOwner }) => { 24 | if (!isOwner) return; 25 | const fullText = m.text || ""; 26 | if (!fullText.startsWith("$ ")) return; 27 | 28 | let cmdText = fullText.slice(2).trim(); 29 | if (!cmdText) return; 30 | 31 | const flags = { 32 | cwd: null, 33 | env: {}, 34 | timeout: null, 35 | }; 36 | 37 | // $ --cwd=/tmp --env=KEY=VALUE --timeout=5000 command 38 | const flagRegex = /^--(\w+)(?:=(.+?))?(?:\s+|$)/; 39 | while (flagRegex.test(cmdText)) { 40 | const match = cmdText.match(flagRegex); 41 | const [fullMatch, flag, value] = match; 42 | 43 | if (flag === "cwd") { 44 | flags.cwd = value; 45 | } else if (flag === "env") { 46 | const [key, val] = value.split("="); 47 | flags.env[key] = val; 48 | } else if (flag === "timeout") { 49 | flags.timeout = parseInt(value); 50 | } 51 | 52 | cmdText = cmdText.slice(fullMatch.length); 53 | } 54 | 55 | if (blocked.some((cmd) => cmdText.startsWith(cmd))) { 56 | return conn.sendMessage(m.chat, { 57 | text: ["Command blocked for security reasons.", `> ${cmdText}`].join("\n"), 58 | }); 59 | } 60 | 61 | let resultText; 62 | try { 63 | const options = { 64 | cwd: flags.cwd || process.cwd(), 65 | env: { ...process.env, ...flags.env }, 66 | timeout: flags.timeout || 30000, 67 | shell: "/bin/bash", 68 | maxBuffer: 1024 * 1024 * 10, 69 | }; 70 | 71 | const result = await execAsync(cmdText, options); 72 | const stdout = result.stdout || ""; 73 | const stderr = result.stderr || ""; 74 | const exitCode = 0; 75 | const output = stdout || stderr || "(no output)"; 76 | const parts = [`${cmdText}`, "────────────────────────────"]; 77 | 78 | if (output.trim()) { 79 | parts.push(output.trim()); 80 | } 81 | const footer = []; 82 | if (exitCode !== 0) { 83 | footer.push(`Exit code: ${exitCode}`); 84 | } 85 | if (flags.cwd) { 86 | footer.push(`📁 ${flags.cwd}`); 87 | } 88 | 89 | if (footer.length > 0) { 90 | parts.push("", footer.join(" • ")); 91 | } 92 | 93 | resultText = parts.join("\n"); 94 | } catch (err) { 95 | const exitCode = err.code || 1; 96 | const stdout = err.stdout || ""; 97 | const stderr = err.stderr || err.message || ""; 98 | const output = stdout || stderr || "(no output)"; 99 | 100 | const parts = [`${cmdText}`, "────────────────────────────", output.trim()]; 101 | 102 | const footer = [`Exit code: ${exitCode}`]; 103 | if (flags.cwd) { 104 | footer.push(`📁 ${flags.cwd}`); 105 | } 106 | 107 | if (footer.length > 0) { 108 | parts.push("", footer.join(" • ")); 109 | } 110 | 111 | resultText = parts.join("\n"); 112 | } 113 | 114 | await conn.sendMessage(m.chat, { text: resultText }); 115 | }; 116 | 117 | handler.help = ["$"]; 118 | handler.tags = ["owner"]; 119 | handler.customPrefix = /^\$ /; 120 | handler.command = /(?:)/i; 121 | 122 | export default handler; 123 | -------------------------------------------------------------------------------- /.github/workflows/01b-cpp-quality.yml: -------------------------------------------------------------------------------- 1 | name: C++ Code Quality 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | paths: 9 | - 'lib/cpp/**/*.cpp' 10 | - 'lib/cpp/**/*.h' 11 | - 'lib/cpp/**/*.cc' 12 | - 'binding.gyp' 13 | - '.clang-format' 14 | - '.clang-tidy' 15 | 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | 20 | jobs: 21 | clang-format-check: 22 | name: Clang-Format Check 23 | runs-on: self-hosted 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v6 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Check C++ formatting 31 | id: format-check 32 | run: | 33 | command -v clang-format || exit 1 34 | 35 | UNFORMATTED_FILES="" 36 | for file in $(find lib/cpp -name '*.cpp' -o -name '*.h' -o -name '*.cc'); do 37 | clang-format --dry-run --Werror "$file" 2>/dev/null || UNFORMATTED_FILES="$UNFORMATTED_FILES$file\n" 38 | done 39 | 40 | if [ -n "$UNFORMATTED_FILES" ]; then 41 | echo "needs_format=true" >> $GITHUB_OUTPUT 42 | else 43 | echo "needs_format=false" >> $GITHUB_OUTPUT 44 | fi 45 | 46 | - name: Auto-format if needed 47 | if: steps.format-check.outputs.needs_format == 'true' && github.event_name == 'pull_request' 48 | run: | 49 | command -v clang-format || exit 1 50 | find lib/cpp -name '*.cpp' -o -name '*.h' -o -name '*.cc' | xargs clang-format -i 51 | 52 | - name: Commit formatting changes 53 | if: steps.format-check.outputs.needs_format == 'true' && github.event_name == 'pull_request' 54 | run: | 55 | git config user.name "liora-bot" 56 | git config user.email "liora.bot.official@gmail.com" 57 | 58 | if [[ -n "$(git status --porcelain)" ]]; then 59 | git add lib/cpp/ 60 | git commit -m "style(cpp): auto-format with clang-format" 61 | git push 62 | fi 63 | 64 | clang-tidy-analysis: 65 | name: Clang-Tidy Static Analysis 66 | runs-on: self-hosted 67 | steps: 68 | - name: Checkout code 69 | uses: actions/checkout@v6 70 | 71 | - name: Setup Node.js 72 | uses: actions/setup-node@v6 73 | with: 74 | node-version: '24' 75 | 76 | - name: Setup pnpm 77 | uses: pnpm/action-setup@v4 78 | with: 79 | version: 10 80 | run_install: false 81 | 82 | - name: Install node-addon-api 83 | run: | 84 | pnpm add node-addon-api 85 | 86 | - name: Run clang-tidy analysis 87 | run: | 88 | command -v clang-tidy || exit 0 89 | 90 | NAPI_INCLUDE=$(node -p "require('node-addon-api').include") 91 | 92 | for file in $(find lib/cpp -name '*.cpp'); do 93 | clang-tidy "$file" \ 94 | --checks='-*,bugprone-*,cert-*,performance-*,readability-*' \ 95 | -- \ 96 | -std=c++20 \ 97 | -I"$NAPI_INCLUDE" \ 98 | -I/usr/include \ 99 | -Ilib/cpp/core \ 100 | -DNAPI_CPP_EXCEPTIONS \ 101 | || true 102 | done 103 | 104 | memory-safety-check: 105 | name: Memory Safety Check (Headers) 106 | runs-on: self-hosted 107 | steps: 108 | - name: Checkout code 109 | uses: actions/checkout@v6 110 | 111 | - name: Check for unsafe patterns 112 | run: | 113 | grep -rn "new \|delete " lib/cpp/ --include="*.cpp" --include="*.h" && echo "Found raw new/delete" || true 114 | grep -rn "malloc\|free\|calloc\|realloc" lib/cpp/ --include="*.cpp" --include="*.h" && echo "Found malloc/free" || true 115 | grep -rn "void.*\*" lib/cpp/ --include="*.h" | grep -v "Napi::" | grep -v "//" && echo "Found raw pointers" || true 116 | --------------------------------------------------------------------------------