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
ProfileTelegram
59 commands1 envUpdated 10d agoCreated Aug 28, 2026
Back to folder

commands/_utils.js

javascript · 847 lines

Raw
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, "&amp;").replace(/</g, "&lt;")502    .replace(/>/g, "&gt;").replace(/"/g, "&quot;");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};