xseaqbka/Nawaab_RoBotPublic · Bot Template
AIThis Telegram bot acts as a TOTP authenticator: users can add accounts via Base32 secret, QR code, or otpauth:// URI, list saved keys, and generate current OTP codes on demand with refresh and delete options. It includes an admin panel with user ban/unban, paginated user management, required-channel management, and a broadcast system that sends text, image, or copied replied messages to all private chats. A web dashboard endpoint provides OTP generation and account management over HTTP. The bot also supports an on/off maintenance switch and per-user access gating.
Utilitytotpauthenticatorotp2fabroadcastadmin
59 commands1 envUpdated 10d agoCreated Aug 28, 2026
commands/_utils.js
javascript · 847 lines
1/**#command2name: /utils3answer: 4keyboard: 5parse_mode: markdown6aliases: 7allow_only_group: false8need_reply: false9is_web: 010#command**/11 12/*13 Shared AuthKey utilities.14 Imported only by the @ initialization command.15*/16 17async function getAdminId() {18 return String(process.env.ADMIN_ID || await db.bot.get("admin_id", ""));19}20 21async function isAdmin() {22 const id = await getAdminId();23 return !!id && String(user?.id) === id;24}25 26async function userCount() {27 const ids = await Bot.getUsers({28 chatType: "private"29 });30 return Array.isArray(ids) ? ids.length : 0;31}32 33async function isEnabled() {34 return await db.bot.get("enabled", true);35}36 37function isCallback() {38 return update_type === "callback_query" && !!update.callback_query;39}40 41async function ack(text) {42 if (!isCallback()) return;43 await Api.answerCallbackQuery({44 callback_query_id: update.callback_query.id,45 text: text || ""46 });47}48 49function btn(text, callback_data, style) {50 const b = {51 text,52 callback_data53 };54 if (style) b.style = style;55 return b;56}57 58function urlBtn(text, url, style) {59 const b = {60 text,61 url62 };63 if (style) b.style = style;64 return b;65}66 67async function sendMedia(text, keyboard, imageKey, targetChatId) {68 const image = await db.bot.get(imageKey, "");69 const chatId = targetChatId || chat.id;70 const markup = keyboard ? {71 inline_keyboard: keyboard72 } : undefined;73 74 if (image) {75 return await Api.sendPhoto({76 chat_id: chatId,77 photo: image,78 caption: text,79 parse_mode: "HTML",80 reply_markup: markup81 });82 }83 84 return await Api.sendMessage({85 chat_id: chatId,86 text,87 parse_mode: "HTML",88 reply_markup: markup89 });90}91 92/*93 Render a screen from either a normal message or callback.94 If imageKey is configured, callback navigation also changes the current95 message into a photo using Telegram editMessageMedia.96*/97async function renderMedia(text, keyboard, imageKey) {98 const image = imageKey ? await db.bot.get(imageKey, "") : "";99 100 if (!isCallback()) {101 if (image) {102 return await Api.sendPhoto({103 chat_id: chat.id,104 photo: image,105 caption: text,106 parse_mode: "HTML",107 reply_markup: {108 inline_keyboard: keyboard109 }110 });111 }112 113 return await Api.sendMessage({114 chat_id: chat.id,115 text,116 parse_mode: "HTML",117 reply_markup: {118 inline_keyboard: keyboard119 }120 });121 }122 123 const m = update.callback_query.message;124 await ack();125 126 if (image) {127 const result = await Api.call("editMessageMedia", {128 chat_id: m.chat.id,129 message_id: m.message_id,130 media: {131 type: "photo",132 media: image,133 caption: text,134 parse_mode: "HTML"135 },136 reply_markup: {137 inline_keyboard: keyboard138 }139 });140 141 if (result?.ok) return result;142 143 // The callback message may have been deleted or replaced.144 return await Api.sendPhoto({145 chat_id: chat.id,146 photo: image,147 caption: text,148 parse_mode: "HTML",149 reply_markup: {150 inline_keyboard: keyboard151 }152 });153 }154 155 if (m.photo) {156 return await Api.editMessageCaption({157 chat_id: m.chat.id,158 message_id: m.message_id,159 caption: text,160 parse_mode: "HTML",161 reply_markup: {162 inline_keyboard: keyboard163 }164 });165 }166 167 return await Api.editMessageText({168 chat_id: m.chat.id,169 message_id: m.message_id,170 text,171 parse_mode: "HTML",172 reply_markup: {173 inline_keyboard: keyboard174 }175 });176}177 178async function render(text, keyboard, extra) {179 const opts = Object.assign({180 parse_mode: "HTML"181 }, extra || {});182 183 if (!isCallback()) {184 185 // Remove the previous menu186 await clearMenu();187 188 const sent = await Api.sendMessage(189 Object.assign({190 chat_id: chat.id,191 text192 }, opts, {193 reply_markup: keyboard ?194 {195 inline_keyboard: keyboard196 } :197 undefined198 })199 );200 201 await setCurrentMenu(sent);202 203 return sent;204 }205 206 const m = update.callback_query.message;207 208 await ack();209 210 const result = await Api.editMessageText({211 chat_id: m.chat.id,212 message_id: m.message_id,213 text,214 parse_mode: "HTML",215 reply_markup: keyboard ?216 {217 inline_keyboard: keyboard218 } :219 undefined220 });221 222 // Keep track of the callback menu223 await db.user.set(224 "current_menu_id",225 m.message_id226 );227 228 return result;229}230 231async function closeCurrent() {232 if (isCallback()) {233 await ack();234 return await Api.deleteMessage({235 chat_id: update.callback_query.message.chat.id,236 message_id: update.callback_query.message.message_id237 });238 }239 if (msg?.message_id) {240 return await Api.deleteMessage({241 chat_id: chat.id,242 message_id: msg.message_id243 });244 }245}246 247async function adminOnly() {248 if (await isAdmin()) return true;249 await render(250 "🚫 <b>Access Denied</b>\n\nAdministrator access is required.",251 [252 [btn("✕ Close", "close", "danger")]253 ]254 );255 return false;256}257 258async function userGuard() {259 if (await isAdmin()) return true;260 261 const banned = await db.user.get("banned", false);262 if (banned) {263 const text =264 "🚫 <b>Access Restricted</b>\n\n" +265 "Your access to this bot has been disabled.\n\n" +266 "If you believe this is a mistake, contact support.";267 268 const image = await db.bot.get("ban_image", "");269 const owner = await db.bot.get("owner_url", "");270 const keyboard = [271 [{272 text: "🆘 Support",273 url: owner274 }]275 ];276 277 if (image) {278 await Api.sendPhoto({279 chat_id: chat.id,280 photo: image,281 caption: text,282 parse_mode: "HTML",283 reply_markup: {284 inline_keyboard: keyboard285 }286 });287 } else {288 await Api.sendMessage({289 chat_id: chat.id,290 text,291 parse_mode: "HTML",292 reply_markup: {293 inline_keyboard: keyboard294 }295 });296 }297 return false;298 }299 300 if (!(await isEnabled())) {301 await render(302 "🛠️ <b>Temporarily Unavailable</b>\n\nPlease try again later.",303 [304 [btn("✕ Close", "close", "danger")]305 ]306 );307 return false;308 }309 return true;310}311 312async function getChannels() {313 return await db.bot.get("channels", []);314}315 316async function membership() {317 const channels = await getChannels();318 if (!channels.length) return {319 all_joined: true,320 valid: [],321 left: [],322 invalid: [],323 details: []324 };325 return await Libs.mcl.check(user.id, channels.map(c => c.id));326}327 328function joinKeyboard(channels) {329 const rows = [];330 for (let i = 0; i < channels.length; i += 2) {331 const row = [];332 if (channels[i]?.url) row.push(urlBtn("📢 " + channels[i].name, channels[i].url));333 if (channels[i + 1]?.url) row.push(urlBtn("📢 " + channels[i + 1].name, channels[i + 1].url));334 if (row.length) rows.push(row);335 }336 rows.push([btn("✅ Verify Membership", "check_join", "success")]);337 return rows;338}339 340async function joinGate() {341 const channels = await getChannels();342 if (!channels.length) return true;343 344 const result = await membership();345 if (result.all_joined) return true;346 347 const text =348 "🔐 <b>Membership Required</b>\n\n" +349 "Join all required channels below, then tap <b>Verify Membership</b>.";350 351 const image = await db.bot.get("force_join_image", "");352 const keyboard = joinKeyboard(channels);353 354 if (isCallback()) {355 await renderMedia(text, keyboard, "force_join_image");356 } else if (image) {357 await Api.sendPhoto({358 chat_id: chat.id,359 photo: image,360 caption: text,361 parse_mode: "HTML",362 reply_markup: {363 inline_keyboard: keyboard364 }365 });366 } else {367 await Api.sendMessage({368 chat_id: chat.id,369 text,370 parse_mode: "HTML",371 reply_markup: {372 inline_keyboard: keyboard373 }374 });375 }376 return false;377}378 379async function userGate() {380 if (!(await userGuard())) return false;381 return await joinGate();382}383 384function mainKeyboard() {385 return [];386}387 388function adminKeyboard() { return []; }389 390function base32Decode(input) {391 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";392 const clean = String(input || "").toUpperCase().replace(/[\s=-]/g, "");393 if (!clean || !/^[A-Z2-7]+$/.test(clean)) return null;394 395 let bits = "";396 for (const c of clean) {397 const n = alphabet.indexOf(c);398 if (n < 0) return null;399 bits += n.toString(2).padStart(5, "0");400 }401 402 const bytes = [];403 for (let i = 0; i + 8 <= bits.length; i += 8) {404 bytes.push(parseInt(bits.slice(i, i + 8), 2));405 }406 return Buffer.from(bytes);407}408 409function parseOtpAuth(value) {410 let raw = String(value || "").trim();411 if (!raw) return null;412 413 if (/^otpauth:\/\//i.test(raw)) {414 try {415 const u = new URL(raw);416 const type = String(u.hostname || "").toLowerCase();417 if (type !== "totp") return null;418 419 const secret = (u.searchParams.get("secret") || "").replace(/\s/g, "").toUpperCase();420 if (!secret || !base32Decode(secret)) return null;421 422 const label = decodeURIComponent((u.pathname || "").replace(/^\/+/, ""));423 const issuer = u.searchParams.get("issuer") || "";424 const algorithm = (u.searchParams.get("algorithm") || "SHA1").toUpperCase();425 const digits = parseInt(u.searchParams.get("digits") || "6", 10);426 const period = parseInt(u.searchParams.get("period") || "30", 10);427 428 if (!/^(SHA1|SHA256|SHA512)$/.test(algorithm)) return null;429 if (![6, 7, 8].includes(digits)) return null;430 if (!Number.isFinite(period) || period < 1 || period > 3600) return null;431 432 let name = label || issuer || "Authenticator Account";433 return {434 secret,435 name,436 issuer,437 algorithm,438 digits,439 period440 };441 } catch (_) {442 return null;443 }444 }445 446 const secret = raw.replace(/\s/g, "").toUpperCase();447 if (!/^[A-Z2-7]+$/.test(secret) || !base32Decode(secret)) return null;448 449 return {450 secret,451 name: "Authenticator Account",452 issuer: "",453 algorithm: "SHA1",454 digits: 6,455 period: 30456 };457}458 459function generateTotp(account, nowMs) {460 const key = base32Decode(account.secret);461 if (!key) throw new Error("Invalid Base32 secret");462 463 const period = Number(account.period || 30);464 const digits = Number(account.digits || 6);465 const counter = Math.floor((nowMs || Date.now()) / 1000 / period);466 467 const buf = Buffer.alloc(8);468 let n = counter;469 for (let i = 7; i >= 0; i--) {470 buf[i] = n & 255;471 n = Math.floor(n / 256);472 }473 474 const algo = String(account.algorithm || "SHA1").toLowerCase().replace("-", "");475 const digest = crypto.createHmac(algo, key).update(buf).digest();476 const offset = digest[digest.length - 1] & 15;477 478 const binary =479 ((digest[offset] & 127) << 24) |480 ((digest[offset + 1] & 255) << 16) |481 ((digest[offset + 2] & 255) << 8) |482 (digest[offset + 3] & 255);483 484 return String(binary % Math.pow(10, digits)).padStart(digits, "0");485}486 487function secondsRemaining(period, nowMs) {488 return period - (Math.floor((nowMs || Date.now()) / 1000) % period);489}490 491async function getKeys() {492 return await db.user.get("keys", []);493}494 495async function saveKeys(keys) {496 return await db.user.set("keys", keys);497}498 499function escapeHtml(s) {500 return String(s ?? "")501 .replace(/&/g, "&").replace(/</g, "<")502 .replace(/>/g, ">").replace(/"/g, """);503}504 505async function addAccount(raw, fallbackName) {506 const account = parseOtpAuth(raw);507 if (!account) return {508 ok: false,509 reason: "invalid"510 };511 512 const keys = await getKeys();513 const max = await db.bot.get("max_keys_per_user", 0);514 if (max > 0 && keys.length >= max) return {515 ok: false,516 reason: "limit"517 };518 519 if (account.name === "Authenticator Account" && fallbackName) account.name = fallbackName;520 521 account.id = modules.UUID.uuidv4();522 account.created_at = Date.now();523 keys.push(account);524 await saveKeys(keys);525 return {526 ok: true,527 account,528 index: keys.length529 };530}531 532async function savePendingAccount(account, source) {533 await db.user.set("pending_account", account);534 await db.user.set("pending_source", source || "secret");535}536 537async function getPendingAccount() {538 return await db.user.get("pending_account", null);539}540 541async function clearPendingAccount() {542 await db.user.set("pending_account", null);543 await db.user.set("pending_source", null);544}545 546async function finalizePendingAccount(name) {547 const pending = await getPendingAccount();548 if (!pending) return {549 ok: false,550 reason: "missing"551 };552 553 const keys = await getKeys();554 const max = await db.bot.get("max_keys_per_user", 0);555 if (max > 0 && keys.length >= max) {556 await clearPendingAccount();557 return {558 ok: false,559 reason: "limit"560 };561 }562 563 const cleanName = String(name || "").trim().replace(/\s+/g, " ");564 if (cleanName) pending.name = cleanName;565 566 if (!pending.name || pending.name === "Authenticator Account") {567 pending.name = pending.issuer || "Authenticator Account";568 }569 570 pending.id = modules.UUID.uuidv4();571 pending.created_at = Date.now();572 keys.push(pending);573 await saveKeys(keys);574 await clearPendingAccount();575 576 return {577 ok: true,578 account: pending,579 index: keys.length580 };581}582 583function normalizeDownloadUrl(value) {584 if (typeof value === "string") return value;585 if (!value || typeof value !== "object") return "";586 return String(587 value.url ||588 value.file_url ||589 value.download_url ||590 value.result?.url ||591 value.result?.file_url ||592 value.data?.url ||593 value.data?.file_url ||594 ""595 );596}597 598 599async function downloadIncomingMedia() {600 if (!msg) return null;601 602 let fileId = "";603 let mimeType = "image/jpeg";604 let fileName = "authenticator-qr.jpg";605 606 if (msg.photo && Array.isArray(msg.photo) && msg.photo.length) {607 const photo = msg.photo[msg.photo.length - 1];608 fileId = photo.file_id;609 mimeType = "image/jpeg";610 } else if (msg.document && msg.document.file_id) {611 const mime = String(msg.document.mime_type || "").toLowerCase();612 if (mime.startsWith("image/")) {613 fileId = msg.document.file_id;614 mimeType = mime || "image/jpeg";615 fileName = msg.document.file_name || fileName;616 }617 }618 619 if (!fileId) return null;620 621 // TBL-native download first.622 try {623 const downloaded = await msg.downloadFile();624 const url = normalizeDownloadUrl(downloaded);625 if (url) {626 const fetched = await HTTP.get({627 url,628 responseType: "buffer",629 timeout: 10000630 });631 if (fetched?.ok && fetched?.data) {632 return {633 url,634 buffer: fetched.data,635 mimeType,636 fileName637 };638 }639 }640 } catch (_) {}641 642 // Reliable Telegram API fallback.643 try {644 const fileResult = await Api.call("getFile", {645 file_id: fileId646 });647 648 if (649 !fileResult?.ok ||650 !fileResult.result?.file_path ||651 !bot?.token652 ) return null;653 654 const url =655 "https://api.telegram.org/file/bot" +656 bot.token +657 "/" +658 fileResult.result.file_path;659 660 const fetched = await HTTP.get({661 url,662 responseType: "buffer",663 timeout: 10000664 });665 666 if (fetched?.ok && fetched?.data) {667 return {668 url,669 buffer: fetched.data,670 mimeType,671 fileName672 };673 }674 } catch (_) {}675 676 return null;677}678 679async function decodeQrFromIncomingMedia() {680 const media = await downloadIncomingMedia();681 if (!media?.buffer) return {682 ok: false,683 reason: "download"684 };685 686 // Decode the actual bytes. This avoids requiring an external service687 // to fetch a Telegram URL.688 try {689 const response = await HTTP.post({690 url: "https://useqr.app/api/v1/decode",691 body: media.buffer,692 headers: {693 "Content-Type": media.mimeType || "image/jpeg"694 },695 responseType: "json",696 timeout: 15000697 });698 699 if (response?.ok) {700 const data = response.data;701 702 let decoded = "";703 704 if (typeof data === "string") {705 decoded = data.trim();706 } else if (data && typeof data === "object") {707 decoded =708 String(709 data.text ||710 data.data ||711 data.result ||712 data.code ||713 data.results?.[0]?.text ||714 data.results?.[0]?.data ||715 data.codes?.[0]?.text ||716 data.codes?.[0]?.data ||717 ""718 ).trim();719 }720 721 if (decoded) return {722 ok: true,723 data: decoded724 };725 }726 } catch (_) {}727 728 // Fallback to QRServer multipart if supported by the runtime.729 try {730 const form = new FormData();731 form.append("file", media.buffer, {732 filename: media.fileName || "authenticator-qr.jpg",733 contentType: media.mimeType || "image/jpeg"734 });735 736 const headers =737 typeof form.getHeaders === "function" ?738 form.getHeaders() :739 {};740 741 const response = await HTTP.post({742 url: "https://api.qrserver.com/v1/read-qr-code/",743 body: form,744 headers,745 timeout: 15000746 });747 748 if (response?.ok) {749 let data = response.data;750 if (!data && response.content) {751 try {752 data = JSON.parse(response.content);753 } catch (_) {}754 }755 756 const decoded = data?.[0]?.symbol?.[0]?.data;757 if (decoded) return {758 ok: true,759 data: String(decoded).trim()760 };761 }762 } catch (_) {}763 764 return {765 ok: false,766 reason: "decode"767 };768}769 770 771function startKeyboard() { return []; }772 773/*774 Track and manage the current bot menu message.775*/776 777async function clearMenu() {778 const menuId = await db.user.get("current_menu_id", null);779 780 if (!menuId) return;781 782 try {783 await Api.deleteMessage({784 chat_id: chat.id,785 message_id: menuId786 });787 } catch (_) {}788 789 await db.user.set("current_menu_id", null);790}791 792async function setCurrentMenu(messageResult) {793 if (!messageResult) return;794 795 const messageId =796 messageResult.message_id ||797 messageResult?.result?.message_id;798 799 if (messageId) {800 await db.user.set(801 "current_menu_id",802 messageId803 );804 }805}806 807module.exports = {808 getAdminId,809 isAdmin,810 userCount,811 isEnabled,812 isCallback,813 ack,814 btn,815 urlBtn,816 sendMedia,817 renderMedia,818 render,819 closeCurrent,820 adminOnly,821 userGuard,822 getChannels,823 membership,824 joinKeyboard,825 joinGate,826 userGate,827 mainKeyboard,828 startKeyboard,829 adminKeyboard,830 base32Decode,831 parseOtpAuth,832 generateTotp,833 secondsRemaining,834 getKeys,835 saveKeys,836 escapeHtml,837 addAccount,838 savePendingAccount,839 getPendingAccount,840 clearPendingAccount,841 finalizePendingAccount,842 normalizeDownloadUrl,843 downloadIncomingMedia,844 decodeQrFromIncomingMedia,845 clearMenu,846 setCurrentMenu847};