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/web_dashboard.js
javascript · 3234 lines
1/**#command2name: web_dashboard3answer: 4keyboard: 5parse_mode: markdown6aliases: 7allow_only_group: false8need_reply: false9is_web: 010#command**/11 12/* Command: web_dashboard */13 14const method = String((request && request.method) || "GET").toUpperCase();15 16/* =========================17 API / POST REQUESTS18========================= */19 20if (method === "POST") {21 let body = request ? request.body : {};22 23 if (typeof body === "string") {24 try {25 body = JSON.parse(body);26 } catch (e) {27 body = {};28 }29 }30 31 if (!body || typeof body !== "object") {32 body = {};33 }34 35 const action = String(body.action || "");36 37 const keysRaw = await db.user.get("keys", []);38 const keys = Array.isArray(keysRaw) ? keysRaw : [];39 40 /* Generate OTP */41 42 if (action === "otp") {43 const i = parseInt(body.index, 10);44 45 if (!Number.isFinite(i) || i < 0 || !keys[i]) {46 res.status(404);47 res.json({48 ok: false,49 error: "Account not found"50 });51 return;52 }53 54 try {55 const a = keys[i];56 57 const period =58 Number(a.period || 30) > 059 ? Number(a.period || 30)60 : 30;61 62 res.json({63 ok: true,64 otp: String(authUtils.generateTotp(a)),65 period: period,66 remaining: authUtils.secondsRemaining(period)67 });68 } catch (e) {69 res.status(400);70 res.json({71 ok: false,72 error: "Unable to generate this OTP"73 });74 }75 76 return;77 }78 79 /* Add account */80 81 if (action === "add") {82 const raw = String(83 body.secret || body.uri || ""84 ).trim();85 86 const name = String(87 body.name || ""88 ).trim();89 90 const parsed = authUtils.parseOtpAuth(raw);91 92 if (!parsed) {93 res.status(400);94 res.json({95 ok: false,96 error: "Invalid TOTP secret or QR payload"97 });98 return;99 }100 101 if (!name) {102 res.status(400);103 res.json({104 ok: false,105 error: "Please enter an account name"106 });107 return;108 }109 110 const limit = Number(111 await db.bot.get("max_keys_per_user", 0) || 0112 );113 114 if (limit > 0 && keys.length >= limit) {115 res.status(400);116 res.json({117 ok: false,118 error: "Maximum key limit reached"119 });120 return;121 }122 123 parsed.name = name;124 parsed.id = modules.UUID.uuidv4();125 parsed.created_at = Date.now();126 127 keys.push(parsed);128 129 await db.user.set("keys", keys);130 131 res.json({132 ok: true,133 index: keys.length - 1,134 account: {135 index: keys.length - 1,136 name: parsed.name,137 issuer: String(parsed.issuer || ""),138 period: Number(parsed.period || 30),139 digits: Number(parsed.digits || 6)140 }141 });142 143 return;144 }145 146 /* Add multiple migrated accounts */147 148 if (action === "add_batch") {149 const incoming = Array.isArray(body.accounts)150 ? body.accounts151 : [];152 153 if (!incoming.length) {154 res.status(400);155 res.json({156 ok: false,157 error: "No migration accounts found"158 });159 return;160 }161 162 const limit = Number(163 await db.bot.get("max_keys_per_user", 0) || 0164 );165 166 if (167 limit > 0 &&168 keys.length + incoming.length > limit169 ) {170 res.status(400);171 res.json({172 ok: false,173 error: "Maximum key limit reached"174 });175 return;176 }177 178 const added = [];179 180 for (let n = 0; n < incoming.length; n++) {181 const item = incoming[n] || {};182 183 const parsed = authUtils.parseOtpAuth(184 String(item.uri || "")185 );186 187 const name = String(188 item.name ||189 (parsed && parsed.name) ||190 ""191 ).trim();192 193 if (!parsed || !name) {194 res.status(400);195 res.json({196 ok: false,197 error: "Invalid account in migration QR"198 });199 return;200 }201 202 parsed.name = name;203 parsed.id = modules.UUID.uuidv4();204 parsed.created_at = Date.now();205 206 keys.push(parsed);207 208 added.push({209 index: keys.length - 1,210 name: parsed.name,211 issuer: String(parsed.issuer || ""),212 period: Number(parsed.period || 30),213 digits: Number(parsed.digits || 6)214 });215 }216 217 await db.user.set("keys", keys);218 219 res.json({220 ok: true,221 accounts: added222 });223 224 return;225 }226 227 /* Delete account */228 229 if (action === "delete") {230 const i = parseInt(body.index, 10);231 232 if (!Number.isFinite(i) || i < 0 || !keys[i]) {233 res.status(404);234 res.json({235 ok: false,236 error: "Account not found"237 });238 return;239 }240 241 keys.splice(i, 1);242 243 await db.user.set("keys", keys);244 245 res.json({246 ok: true247 });248 249 return;250 }251 252 res.status(400);253 254 res.json({255 ok: false,256 error: "Unknown action"257 });258 259 return;260}261 262 263/* =========================264 GET / WEBAPP265========================= */266 267const rawKeys = await db.user.get("keys", []);268 269const keys = Array.isArray(rawKeys)270 ? rawKeys271 : [];272 273const data = {274 keys: keys.map(function (k, i) {275 return {276 index: i,277 name: String(278 k.name ||279 k.account ||280 ("Account " + (i + 1))281 ),282 issuer: String(k.issuer || ""),283 period: Number(k.period || 30),284 digits: Number(k.digits || 6)285 };286 }),287 288 profile: {289 name: String(290 (user && user.first_name) || "Telegram User"291 ),292 lastName: String(293 (user && user.last_name) || ""294 ),295 username: String(296 (user && user.username) || ""297 ),298 id: String(299 (user && user.id) || ""300 )301 },302 303 support: {304 owner: String(305 await db.bot.get("owner_url", "") || ""306 ),307 308 channel: String(309 await db.bot.get("support_channel_url", "") || ""310 ),311 312 community: String(313 await db.bot.get("support_community_url", "") || ""314 )315 }316};317 318 319/* Safe JSON for inline script */320 321const boot = JSON.stringify(data)322 .replace(/</g, "\\u003c")323 .replace(/>/g, "\\u003e")324 .replace(/&/g, "\\u0026");325 326 327res.html(`<!doctype html>328<html lang="en">329 330<head>331 332<meta charset="utf-8">333 334<meta335 name="viewport"336 content="width=device-width,initial-scale=1,viewport-fit=cover"337>338 339<title>AuthKey</title>340 341<meta342 name="theme-color"343 content="#090b13"344>345 346<script src="https://cdn.tailwindcss.com"></script>347 348<link349 rel="preconnect"350 href="https://fonts.googleapis.com"351>352 353<link354 rel="preconnect"355 href="https://fonts.gstatic.com"356 crossorigin357>358 359<link360 href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap"361 rel="stylesheet"362>363 364<script src="https://unpkg.com/lucide@latest"></script>365 366<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>367 368<script src="https://telegram.org/js/telegram-web-app.js"></script>369 370<style>371 372*{373 box-sizing:border-box374}375 376body{377 margin:0;378 background:#090b13;379 color:#f8fafc;380 font-family:'DM Sans',sans-serif;381 min-height:100vh382}383 384body:before{385 content:"";386 position:fixed;387 inset:0;388 pointer-events:none;389 background:390 radial-gradient(391 circle at 10% 0,392 rgba(139,92,246,.16),393 transparent 32%394 ),395 radial-gradient(396 circle at 95% 18%,397 rgba(34,211,238,.10),398 transparent 28%399 )400}401 402.space{403 font-family:'Space Grotesk',sans-serif404}405 406.app{407 max-width:700px;408 margin:auto;409 padding:410 env(safe-area-inset-top)411 20px412 calc(104px + env(safe-area-inset-bottom));413 min-height:100vh414}415 416.view{417 display:none;418 animation:in .38s cubic-bezier(.2,.8,.2,1)419}420 421.view.active{422 display:block423}424 425@keyframes in{426 from{427 opacity:0;428 transform:translateY(14px)429 }430 431 to{432 opacity:1;433 transform:none434 }435}436 437.card{438 background:439 linear-gradient(440 145deg,441 rgba(28,31,45,.94),442 rgba(16,18,28,.96)443 );444 445 border:1px solid rgba(148,163,184,.13);446 447 box-shadow:448 0 18px 55px rgba(0,0,0,.2);449 450 border-radius:26px451}452 453.btn{454 border:0;455 border-radius:18px;456 padding:15px 17px;457 font-weight:700;458 transition:.2s;459 display:flex;460 align-items:center;461 justify-content:center;462 gap:8px463}464 465.btn:active{466 transform:scale(.98)467}468 469.primary{470 background:471 linear-gradient(472 135deg,473 #7c3aed,474 #a855f7 55%,475 #22c7e5476 );477 478 box-shadow:479 0 12px 30px rgba(124,58,237,.22)480}481 482.soft{483 background:#24283a;484 border:1px solid #30364b485}486 487.input{488 width:100%;489 background:#191d2b;490 border:1px solid #30374c;491 border-radius:18px;492 padding:17px;493 color:#fff;494 outline:none;495 font-size:16px496}497 498.input:focus{499 border-color:#8b5cf6;500 501 box-shadow:502 0 0 0 4px rgba(139,92,246,.12)503}504 505.nav{506 position:fixed;507 bottom:0;508 left:50%;509 transform:translateX(-50%);510 width:min(700px,100%);511 z-index:30;512 513 padding:514 10px515 20px516 calc(12px + env(safe-area-inset-bottom));517 518 background:519 linear-gradient(520 to top,521 #0b0d15 78%,522 transparent523 );524 525 display:flex;526 gap:8px527}528 529.nav button{530 flex:1;531 border:0;532 background:transparent;533 color:#7d8498;534 border-radius:20px;535 padding:12px 5px;536 display:grid;537 place-items:center;538 gap:4px;539 font-size:11px;540 font-weight:700541}542 543.nav button.active{544 color:#fff;545 background:#202536546}547 548.icon{549 width:52px;550 height:52px;551 border-radius:17px;552 background:#202536;553 display:grid;554 place-items:center;555 overflow:hidden;556 flex:none557}558 559.icon img{560 width:100%;561 height:100%;562 object-fit:cover563}564 565.account{566 transition:.22s;567 cursor:pointer568}569 570.account:active{571 transform:scale(.985)572}573 574.toast{575 position:fixed;576 left:50%;577 bottom:105px;578 transform:translate(-50%,18px);579 opacity:0;580 pointer-events:none;581 background:#24283a;582 border:1px solid #3a425a;583 border-radius:15px;584 padding:12px 17px;585 z-index:90;586 transition:.25s587}588 589.toast.show{590 opacity:1;591 transform:translate(-50%,0)592}593 594#splash{595 position:fixed;596 z-index:100;597 inset:0;598 background:#090b13;599 display:grid;600 place-items:center;601 transition:.6s602}603 604#splash.gone{605 opacity:0;606 pointer-events:none607}608 609.modal{610 position:fixed;611 inset:0;612 background:rgba(0,0,0,.62);613 backdrop-filter:blur(10px);614 z-index:70;615 display:none;616 align-items:end;617 padding:18px618}619 620.modal.show{621 display:flex622}623 624.sheet{625 width:min(660px,100%);626 margin:auto;627 background:#171b28;628 border:1px solid #30384e;629 border-radius:28px;630 padding:24px;631 animation:up .3s ease632}633 634@keyframes up{635 from{636 opacity:0;637 transform:translateY(28px)638 }639}640 641.qr-stage{642 min-height:360px;643 display:flex;644 flex-direction:column;645 justify-content:center646}647 648.hidden{649 display:none!important650}651 652</style>653 654</head>655 656<body>657 658 659<div id="splash">660 661 <div class="text-center">662 663 <div class="w-20 h-20 mx-auto rounded-[28px] primary grid place-items-center text-3xl">664 🔐665 </div>666 667 <h1 class="space text-2xl font-bold mt-5">668 AuthKey669 </h1>670 671 <p class="text-slate-500 text-sm mt-2">672 Secure authenticator673 </p>674 675 </div>676 677</div>678 679 680<main class="app">681 682 683<section684 id="home"685 class="view active"686>687 688<div class="pt-6 pb-6 flex items-center justify-between">689 690<div>691 692<p class="text-violet-300 text-xs font-bold tracking-widest">693AUTHKEY694</p>695 696<h1 class="space text-3xl font-bold mt-2">697Your accounts698</h1>699 700</div>701 702<button703 class="icon"704 onclick="showView('profile')"705>706<i data-lucide="user-round"></i>707</button>708 709</div>710 711<div712 id="accountList"713 class="space-y-3"714></div>715 716<button717 onclick="showView('add')"718 class="btn primary w-full mt-5"719>720<i data-lucide="plus"></i>721Add authenticator722</button>723 724</section>725 726 727<section728 id="detail"729 class="view"730>731 732<button733 onclick="showView('home')"734 class="btn soft px-4 mt-5"735>736<i data-lucide="arrow-left"></i>737Back738</button>739 740<div class="text-center pt-8">741 742<div743 id="detailIcon"744 class="icon mx-auto mb-5"745></div>746 747<p748 id="detailIssuer"749 class="text-slate-500 text-sm"750></p>751 752<h1753 id="detailName"754 class="space text-3xl font-bold mt-1"755></h1>756 757</div>758 759<div class="card p-7 mt-8 text-center">760 761<p class="text-slate-500 text-xs tracking-widest">762CURRENT CODE763</p>764 765<div766 id="otp"767 class="space text-5xl tracking-[.14em] font-bold my-7"768>769••••••770</div>771 772<div class="h-2 bg-[#2a3042] rounded-full overflow-hidden">773 774<div775 id="progress"776 class="h-full primary"777></div>778 779</div>780 781<p782 id="timer"783 class="text-slate-400 text-sm mt-4"784></p>785 786<button787 onclick="copyOtp()"788 class="btn soft w-full mt-5"789>790<i data-lucide="copy"></i>791Copy code792</button>793 794</div>795 796<button797 onclick="removeCurrent()"798 class="btn w-full mt-4 border border-red-400/20 text-red-300"799>800Delete account801</button>802 803</section>804 805 806<section807 id="add"808 class="view"809>810 811<div class="pt-6 pb-7">812 813<p class="text-violet-300 text-xs font-bold tracking-widest">814NEW AUTHENTICATOR815</p>816 817<h1 class="space text-4xl font-bold mt-2">818Add account819</h1>820 821<p class="text-slate-400 mt-3">822Add with a secret key or import an authenticator QR.823</p>824 825</div>826 827 828<div class="grid grid-cols-2 gap-3 mb-6">829 830<button831 id="secretTab"832 onclick="selectAdd('secret')"833 class="btn primary"834>835<i data-lucide="key-round"></i>836Secret key837</button>838 839<button840 id="qrTab"841 onclick="selectAdd('qr')"842 class="btn soft"843>844<i data-lucide="scan-line"></i>845QR code846</button>847 848</div>849 850 851<div852 id="secretPane"853 class="space-y-4"854>855 856<input857 id="secretInput"858 class="input"859 placeholder="Secret key or otpauth:// URI"860>861 862<input863 id="nameInput"864 class="input"865 placeholder="Account name"866>867 868<button869 onclick="addSecret()"870 class="btn primary w-full"871>872Add account873</button>874 875</div>876 877 878<div879 id="qrPane"880 class="hidden"881>882 883<div884 id="qrCapture"885 class="card qr-stage p-6"886>887 888<video889 id="camera"890 class="hidden w-full rounded-2xl bg-black"891 playsinline892></video>893 894<div895 id="cameraEmpty"896 class="text-center"897>898 899<div class="icon mx-auto w-16 h-16">900<i data-lucide="scan"></i>901</div>902 903<h2 class="space text-xl font-bold mt-5">904Import QR code905</h2>906 907<p class="text-slate-500 mt-2">908Scan with your camera or choose an image.909</p>910 911</div>912 913 914<div class="grid grid-cols-2 gap-3 mt-7">915 916<button917 onclick="startCamera()"918 class="btn soft" style="font-size: 0.8rem !important"919>920<i data-lucide="camera"></i>921Scan camera922</button>923 924<label class="btn soft cursor-pointer" style="font-size: 0.8rem !important">925 926<i data-lucide="upload"></i>927 928Upload QR929 930<input931 id="qrFile"932 type="file"933 accept="image/*"934 class="hidden"935 onchange="readQrFile(this.files[0])"936>937 938</label>939 940</div>941 942</div>943 944 945<div946 id="qrReview"947 class="hidden"948>949 950<button951 onclick="discardQr()"952 class="btn soft mb-5"953>954<i data-lucide="arrow-left"></i>955Back to scan956</button>957 958<div class="card p-6">959 960<div class="flex items-center gap-3">961 962<div class="icon">963<i data-lucide="badge-check"></i>964</div>965 966<div>967 968<h2 class="space font-bold text-xl">969QR detected970</h2>971 972<p class="text-slate-500 text-sm">973Review the name before saving.974</p>975 976</div>977 978</div>979 980<input981 id="qrName"982 class="input mt-6"983 placeholder="Account name"984>985 986<button987 onclick="addQr()"988 class="btn primary w-full mt-4"989>990Add account991</button>992 993</div>994 995</div>996 997</div>998 999</section>1000 1001 1002<section1003 id="profile"1004 class="view"1005>1006 1007<div class="pt-6 pb-7">1008 1009<p class="text-violet-300 text-xs font-bold tracking-widest">1010YOUR ACCOUNT1011</p>1012 1013<h1 class="space text-4xl font-bold mt-2">1014Profile1015</h1>1016 1017</div>1018 1019 1020<div class="card p-6">1021 1022<div class="flex items-center gap-4">1023 1024<div1025 id="profilePhoto"1026 class="icon !w-20 !h-20"1027></div>1028 1029<div class="min-w-0">1030 1031<h21032 id="profileName"1033 class="space text-xl font-bold truncate"1034></h2>1035 1036<p1037 id="profileHandle"1038 class="text-slate-500 mt-1"1039></p>1040 1041</div>1042 1043</div>1044 1045</div>1046 1047 1048<div class="grid grid-cols-2 gap-3 mt-4">1049 1050<div class="card p-5">1051 1052<p class="text-slate-500 text-xs">1053TOTAL ACCOUNTS1054</p>1055 1056<p1057 id="keyCount"1058 class="space text-4xl font-bold mt-2"1059></p>1060 1061</div>1062 1063 1064<div class="card p-5">1065 1066<p class="text-slate-500 text-xs">1067USER ID1068</p>1069 1070<p1071 id="profileId"1072 class="text-xs font-semibold mt-4 break-all"1073></p>1074 1075</div>1076 1077</div>1078 1079 1080<h2 class="space text-xl font-bold mt-8 mb-4">1081Support1082</h2>1083 1084<div1085 id="supportLinks"1086 class="space-y-3"1087></div>1088 1089</section>1090 1091 1092</main>1093 1094 1095<nav class="nav">1096 1097<button1098 data-nav="home"1099 class="active"1100 onclick="showView('home')"1101>1102<i data-lucide="house"></i>1103Home1104</button>1105 1106<button1107 data-nav="add"1108 onclick="showView('add')"1109>1110<i data-lucide="plus-circle"></i>1111Add1112</button>1113 1114<button1115 data-nav="profile"1116 onclick="showView('profile')"1117>1118<i data-lucide="user-round"></i>1119Profile1120</button>1121 1122</nav>1123 1124 1125<div1126 id="toast"1127 class="toast"1128></div>1129 1130 1131<div1132 id="welcomeModal"1133 class="modal"1134>1135 1136<div class="sheet text-center">1137 1138<div class="text-4xl">1139👋1140</div>1141 1142<h2 class="space text-2xl font-bold mt-4">1143Welcome to AuthKey1144</h2>1145 1146<p class="text-slate-400 mt-2">1147Your private authenticator workspace is ready.1148</p>1149 1150<button1151 onclick="closeWelcome()"1152 class="btn primary w-full mt-6"1153>1154Get started1155</button>1156 1157</div>1158 1159</div>1160 1161 1162<script>1163 1164/* =========================1165 CRITICAL:1166 REMOVE SPLASH IMMEDIATELY1167========================= */1168 1169(function () {1170 1171 function removeSplash() {1172 const splash =1173 document.getElementById("splash");1174 1175 if (!splash) return;1176 1177 splash.classList.add("gone");1178 1179 setTimeout(function () {1180 if (splash && splash.parentNode) {1181 splash.style.display = "none";1182 }1183 }, 800);1184 }1185 1186 /* Backup removal even if app code fails */1187 1188 setTimeout(removeSplash, 500);1189 1190 window.addEventListener(1191 "error",1192 function () {1193 setTimeout(removeSplash, 50);1194 }1195 );1196 1197 window.addEventListener(1198 "unhandledrejection",1199 function () {1200 setTimeout(removeSplash, 50);1201 }1202 );1203 1204})();1205 1206 1207/* =========================1208 BOOT DATA1209========================= */1210 1211const BOOT = ${boot};1212 1213let current = -1;1214let qrPayload = "";1215let timerHandle = null;1216let stream = null;1217let migrationAccounts = [];1218let otpState = null;1219 1220 1221/* =========================1222 TELEGRAM1223========================= */1224 1225const tg =1226 window.Telegram &&1227 window.Telegram.WebApp1228 ? window.Telegram.WebApp1229 : null;1230 1231try {1232 if (tg) {1233 tg.ready();1234 tg.expand();1235 }1236} catch (e) {1237 console.warn("Telegram WebApp init failed", e);1238}1239 1240 1241/* =========================1242 SAFE ICON INITIALIZATION1243========================= */1244 1245function refreshIcons() {1246 1247 try {1248 1249 if (1250 window.lucide &&1251 typeof window.lucide.createIcons === "function"1252 ) {1253 window.lucide.createIcons();1254 }1255 1256 } catch (e) {1257 console.warn("Lucide unavailable", e);1258 }1259 1260}1261 1262 1263/* =========================1264 UTILITIES1265========================= */1266 1267const esc = function (s) {1268 1269 return String(s || "").replace(1270 /[&<>"]/g,1271 function (c) {1272 return {1273 "&": "&",1274 "<": "<",1275 ">": ">",1276 '"': """1277 }[c];1278 }1279 );1280 1281};1282 1283 1284function toast(t) {1285 1286 const x =1287 document.getElementById("toast");1288 1289 if (!x) return;1290 1291 x.textContent = t;1292 1293 x.classList.add("show");1294 1295 setTimeout(function () {1296 x.classList.remove("show");1297 }, 2500);1298 1299}1300 1301 1302function clearPendingQr() {1303 1304 qrPayload = "";1305 migrationAccounts = [];1306 1307 const name =1308 document.getElementById("qrName");1309 1310 const file =1311 document.getElementById("qrFile");1312 1313 const review =1314 document.getElementById("qrReview");1315 1316 const capture =1317 document.getElementById("qrCapture");1318 1319 if (name) name.value = "";1320 if (file) file.value = "";1321 1322 if (review) {1323 review.classList.add("hidden");1324 }1325 1326 if (capture) {1327 capture.classList.remove("hidden");1328 }1329 1330 stopCamera();1331 1332}1333 1334 1335/* =========================1336 NAVIGATION1337========================= */1338 1339function showView(id) {1340 1341 try {1342 1343 if (id !== "add") {1344 clearPendingQr();1345 }1346 1347 document1348 .querySelectorAll(".view")1349 .forEach(function (x) {1350 x.classList.remove("active");1351 });1352 1353 const view =1354 document.getElementById(id);1355 1356 if (view) {1357 view.classList.add("active");1358 }1359 1360 document1361 .querySelectorAll("[data-nav]")1362 .forEach(function (x) {1363 1364 x.classList.toggle(1365 "active",1366 x.dataset.nav === id1367 );1368 1369 });1370 1371 if (id !== "detail") {1372 stopOtp();1373 }1374 1375 window.scrollTo({1376 top: 0,1377 behavior: "smooth"1378 });1379 1380 refreshIcons();1381 1382 } catch (e) {1383 1384 console.error(1385 "showView error",1386 e1387 );1388 1389 }1390 1391}1392 1393 1394function selectAdd(type) {1395 1396 try {1397 1398 if (type !== "qr") {1399 clearPendingQr();1400 }1401 1402 const secretPane =1403 document.getElementById("secretPane");1404 1405 const qrPane =1406 document.getElementById("qrPane");1407 1408 const secretTab =1409 document.getElementById("secretTab");1410 1411 const qrTab =1412 document.getElementById("qrTab");1413 1414 if (secretPane) {1415 secretPane.classList.toggle(1416 "hidden",1417 type !== "secret"1418 );1419 }1420 1421 if (qrPane) {1422 qrPane.classList.toggle(1423 "hidden",1424 type !== "qr"1425 );1426 }1427 1428 if (secretTab) {1429 secretTab.className =1430 "btn " +1431 (1432 type === "secret"1433 ? "primary"1434 : "soft"1435 );1436 }1437 1438 if (qrTab) {1439 qrTab.className =1440 "btn " +1441 (1442 type === "qr"1443 ? "primary"1444 : "soft"1445 );1446 }1447 1448 refreshIcons();1449 1450 } catch (e) {1451 1452 console.error(1453 "selectAdd error",1454 e1455 );1456 1457 }1458 1459}1460 1461 1462/* =========================1463 ACCOUNT ICON1464========================= */1465 1466function logo(a) {1467 1468 const name = String(1469 (a && (a.issuer || a.name)) || ""1470 )1471 .toLowerCase()1472 .replace(/[^a-z0-9.]/g, "");1473 1474 return (1475 "https://www.google.com/s2/favicons?domain=" +1476 encodeURIComponent(1477 name + ".com"1478 ) +1479 "&sz=128"1480 );1481 1482}1483 1484 1485function accountIcon(a) {1486 1487 const fallback =1488 "https://api.dicebear.com/9.x/initials/svg?seed=" +1489 encodeURIComponent(1490 (a && a.name) || "Account"1491 );1492 1493 return (1494 '<img src="' +1495 esc(logo(a)) +1496 '" onerror="this.onerror=null;this.src=' +1497 JSON.stringify(fallback) +1498 '">'1499 );1500 1501}1502 1503 1504/* =========================1505 RENDER ACCOUNTS1506========================= */1507 1508function renderAccounts() {1509 1510 try {1511 1512 const e =1513 document.getElementById("accountList");1514 1515 if (!e) return;1516 1517 if (!BOOT.keys || !BOOT.keys.length) {1518 1519 e.innerHTML =1520 '<div class="card p-10 text-center">' +1521 '<div class="icon mx-auto w-16 h-16">' +1522 '<i data-lucide="shield-plus"></i>' +1523 '</div>' +1524 '<h2 class="space text-xl font-bold mt-5">' +1525 'No accounts yet' +1526 '</h2>' +1527 '<p class="text-slate-500 mt-2">' +1528 'Add your first authenticator to get started.' +1529 '</p>' +1530 '</div>';1531 1532 } else {1533 1534 e.innerHTML =1535 BOOT.keys.map(function (a) {1536 1537 return (1538 '<button onclick="openAccount(' +1539 Number(a.index) +1540 ')" class="account card w-full p-4 flex items-center gap-4 text-left">' +1541 1542 '<div class="icon">' +1543 accountIcon(a) +1544 '</div>' +1545 1546 '<div class="min-w-0 flex-1">' +1547 1548 '<b class="block truncate">' +1549 esc(a.name) +1550 '</b>' +1551 1552 '<span class="text-slate-500 text-sm">' +1553 esc(1554 a.issuer ||1555 "TOTP authenticator"1556 ) +1557 '</span>' +1558 1559 '</div>' +1560 1561 '<i data-lucide="chevron-right" class="text-slate-500"></i>' +1562 1563 '</button>'1564 );1565 1566 }).join("");1567 1568 }1569 1570 const count =1571 document.getElementById("keyCount");1572 1573 if (count) {1574 count.textContent =1575 BOOT.keys.length;1576 }1577 1578 refreshIcons();1579 1580 } catch (e) {1581 1582 console.error(1583 "renderAccounts error",1584 e1585 );1586 1587 }1588 1589}1590 1591 1592/* =========================1593 API1594========================= */1595 1596async function api(body) {1597 1598 const r =1599 await fetch(location.href, {1600 method: "POST",1601 1602 headers: {1603 "Content-Type":1604 "application/json"1605 },1606 1607 body:1608 JSON.stringify(body)1609 });1610 1611 let j = {};1612 1613 try {1614 j = await r.json();1615 } catch (e) {}1616 1617 if (!r.ok || !j.ok) {1618 throw new Error(1619 j.error ||1620 "Request failed"1621 );1622 }1623 1624 return j;1625 1626}1627 1628 1629/* =========================1630 OTP1631========================= */1632 1633async function openAccount(i) {1634 1635 current = i;1636 1637 const a =1638 BOOT.keys[i];1639 1640 if (!a) return;1641 1642 const name =1643 document.getElementById("detailName");1644 1645 const issuer =1646 document.getElementById("detailIssuer");1647 1648 const icon =1649 document.getElementById("detailIcon");1650 1651 if (name) {1652 name.textContent = a.name;1653 }1654 1655 if (issuer) {1656 issuer.textContent =1657 a.issuer ||1658 "TOTP authenticator";1659 }1660 1661 if (icon) {1662 icon.innerHTML =1663 accountIcon(a);1664 }1665 1666 showView("detail");1667 1668 await refreshOtp();1669 1670 stopOtp();1671 1672 timerHandle =1673 setInterval(1674 tickOtp,1675 10001676 );1677 1678}1679 1680 1681async function refreshOtp() {1682 1683 try {1684 1685 otpState =1686 await api({1687 action: "otp",1688 index: current1689 });1690 1691 tickOtp();1692 1693 } catch (e) {1694 1695 toast(e.message);1696 1697 }1698 1699}1700 1701 1702function tickOtp() {1703 1704 if (!otpState) return;1705 1706 const p =1707 otpState.period || 30;1708 1709 const left =1710 p -1711 (1712 Math.floor(1713 Date.now() / 10001714 ) % p1715 ) ||1716 p;1717 1718 const otp =1719 document.getElementById("otp");1720 1721 const timer =1722 document.getElementById("timer");1723 1724 const progress =1725 document.getElementById("progress");1726 1727 if (otp) {1728 otp.textContent =1729 otpState.otp;1730 }1731 1732 if (timer) {1733 timer.textContent =1734 left +1735 " seconds remaining";1736 }1737 1738 if (progress) {1739 progress.style.width =1740 (1741 left / p * 1001742 ) +1743 "%";1744 }1745 1746 if (left === p) {1747 refreshOtp();1748 }1749 1750}1751 1752 1753function stopOtp() {1754 1755 if (timerHandle) {1756 1757 clearInterval(1758 timerHandle1759 );1760 1761 timerHandle = null;1762 1763 }1764 1765 otpState = null;1766 1767}1768 1769 1770function copyOtp() {1771 1772 if (!otpState) return;1773 1774 try {1775 1776 if (1777 navigator.clipboard &&1778 navigator.clipboard.writeText1779 ) {1780 1781 navigator.clipboard.writeText(1782 otpState.otp1783 );1784 1785 toast("Code copied");1786 1787 }1788 1789 } catch (e) {1790 1791 toast("Unable to copy code");1792 1793 }1794 1795}1796 1797 1798/* =========================1799 ADD SECRET1800========================= */1801 1802async function addSecret() {1803 1804 const secret =1805 document1806 .getElementById("secretInput")1807 .value1808 .trim();1809 1810 const name =1811 document1812 .getElementById("nameInput")1813 .value1814 .trim();1815 1816 if (!secret || !name) {1817 1818 toast(1819 "Enter both secret key and account name"1820 );1821 1822 return;1823 }1824 1825 try {1826 1827 const r =1828 await api({1829 action: "add",1830 secret: secret,1831 name: name1832 });1833 1834 BOOT.keys.push(1835 r.account1836 );1837 1838 renderAccounts();1839 1840 document1841 .getElementById("secretInput")1842 .value = "";1843 1844 document1845 .getElementById("nameInput")1846 .value = "";1847 1848 showView("home");1849 1850 toast(1851 "Account added"1852 );1853 1854 } catch (e) {1855 1856 toast(e.message);1857 1858 }1859 1860}1861 1862 1863/* =========================1864 QR FUNCTIONS1865========================= */1866 1867function parseQrName(s) {1868 1869 try {1870 1871 const u =1872 new URL(s);1873 1874 const label =1875 decodeURIComponent(1876 (u.pathname || "")1877 .replace(/^\\//, "")1878 );1879 1880 const issuer =1881 u.searchParams.get(1882 "issuer"1883 ) || "";1884 1885 return (1886 label ||1887 issuer ||1888 "Authenticator Account"1889 );1890 1891 } catch (e) {1892 1893 return "Authenticator Account";1894 1895 }1896 1897}1898 1899 1900function b64urlBytes(s) {1901 1902 s = String(s || "")1903 .replace(/-/g, "+")1904 .replace(/_/g, "/");1905 1906 while (1907 s.length % 41908 ) {1909 s += "=";1910 }1911 1912 const b =1913 atob(s);1914 1915 const a =1916 new Uint8Array(1917 b.length1918 );1919 1920 for (1921 let i = 0;1922 i < b.length;1923 i++1924 ) {1925 1926 a[i] =1927 b.charCodeAt(i);1928 1929 }1930 1931 return a;1932 1933}1934 1935 1936function readVarint(a, st) {1937 1938 let v = 0;1939 let sh = 0;1940 1941 while (1942 st.i < a.length1943 ) {1944 1945 const b =1946 a[st.i++];1947 1948 v +=1949 (b & 127) *1950 Math.pow(2, sh);1951 1952 if (!(b & 128)) {1953 return v;1954 }1955 1956 sh += 7;1957 1958 if (sh > 49) {1959 throw Error(1960 "Invalid migration data"1961 );1962 }1963 1964 }1965 1966 throw Error(1967 "Unexpected migration end"1968 );1969 1970}1971 1972 1973function readBytes(a, st) {1974 1975 const n =1976 readVarint(a, st);1977 1978 const e =1979 st.i + n;1980 1981 if (e > a.length) {1982 throw Error(1983 "Invalid migration length"1984 );1985 }1986 1987 const r =1988 a.slice(1989 st.i,1990 e1991 );1992 1993 st.i = e;1994 1995 return r;1996 1997}1998 1999 2000function utf8(a) {2001 2002 return new TextDecoder()2003 .decode(a);2004 2005}2006 2007 2008function skipField(2009 a,2010 st,2011 w2012) {2013 2014 if (w === 0) {2015 2016 readVarint(2017 a,2018 st2019 );2020 2021 } else if (w === 1) {2022 2023 st.i += 8;2024 2025 } else if (w === 2) {2026 2027 st.i +=2028 readVarint(2029 a,2030 st2031 );2032 2033 } else if (w === 5) {2034 2035 st.i += 4;2036 2037 } else {2038 2039 throw Error(2040 "Unsupported migration field"2041 );2042 2043 }2044 2045}2046 2047 2048function parseMigrationParam(a) {2049 2050 const st = {2051 i: 02052 };2053 2054 const o = {2055 secret: "",2056 name: "",2057 issuer: "",2058 algorithm: 1,2059 digits: 1,2060 type: 22061 };2062 2063 while (2064 st.i < a.length2065 ) {2066 2067 const t =2068 readVarint(2069 a,2070 st2071 );2072 2073 const f =2074 Math.floor(t / 8);2075 2076 const w =2077 t % 8;2078 2079 if (2080 f === 1 &&2081 w === 22082 ) {2083 2084 o.secret =2085 readBytes(2086 a,2087 st2088 );2089 2090 } else if (2091 f === 2 &&2092 w === 22093 ) {2094 2095 o.name =2096 utf8(2097 readBytes(2098 a,2099 st2100 )2101 );2102 2103 } else if (2104 f === 3 &&2105 w === 22106 ) {2107 2108 o.issuer =2109 utf8(2110 readBytes(2111 a,2112 st2113 )2114 );2115 2116 } else if (2117 (2118 f === 4 ||2119 f === 5 ||2120 f === 62121 ) &&2122 w === 02123 ) {2124 2125 o[2126 f === 42127 ? "algorithm"2128 : f === 52129 ? "digits"2130 : "type"2131 ] =2132 readVarint(2133 a,2134 st2135 );2136 2137 } else {2138 2139 skipField(2140 a,2141 st,2142 w2143 );2144 2145 }2146 2147 }2148 2149 if (2150 !o.secret.length ||2151 o.type !== 22152 ) {2153 return null;2154 }2155 2156 let sec = "";2157 2158 let bits = 0;2159 let val = 0;2160 2161 for (2162 const x of o.secret2163 ) {2164 2165 val =2166 (val << 8) |2167 x;2168 2169 bits += 8;2170 2171 while (2172 bits >= 52173 ) {2174 2175 sec +=2176 "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[2177 (val >> (bits - 5)) & 312178 ];2179 2180 bits -= 5;2181 2182 }2183 2184 }2185 2186 if (bits) {2187 2188 sec +=2189 "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[2190 (val << (5 - bits)) & 312191 ];2192 2193 }2194 2195 const alg = {2196 1: "SHA1",2197 2: "SHA256",2198 3: "SHA512",2199 4: "MD5"2200 }[2201 o.algorithm2202 ] || "SHA1";2203 2204 const dig = {2205 1: 6,2206 2: 82207 }[2208 o.digits2209 ] || 6;2210 2211 const label =2212 o.name ||2213 o.issuer ||2214 "Authenticator Account";2215 2216 const issuer =2217 o.issuer ||2218 "";2219 2220 return {2221 2222 name: label,2223 2224 uri:2225 "otpauth://totp/" +2226 encodeURIComponent(label) +2227 "?secret=" +2228 encodeURIComponent(sec) +2229 (2230 issuer2231 ? "&issuer=" +2232 encodeURIComponent(2233 issuer2234 )2235 : ""2236 ) +2237 "&algorithm=" +2238 alg +2239 "&digits=" +2240 dig +2241 "&period=30"2242 2243 };2244 2245}2246 2247 2248function parseMigrationQr(p) {2249 2250 const u =2251 new URL(p);2252 2253 const data =2254 u.searchParams.get(2255 "data"2256 );2257 2258 if (!data) {2259 throw Error(2260 "Missing migration payload"2261 );2262 }2263 2264 const a =2265 b64urlBytes(data);2266 2267 const st = {2268 i: 02269 };2270 2271 const out = [];2272 2273 while (2274 st.i < a.length2275 ) {2276 2277 const t =2278 readVarint(2279 a,2280 st2281 );2282 2283 const f =2284 Math.floor(t / 8);2285 2286 const w =2287 t % 8;2288 2289 if (2290 f === 1 &&2291 w === 22292 ) {2293 2294 const x =2295 parseMigrationParam(2296 readBytes(2297 a,2298 st2299 )2300 );2301 2302 if (x) {2303 out.push(x);2304 }2305 2306 } else {2307 2308 skipField(2309 a,2310 st,2311 w2312 );2313 2314 }2315 2316 }2317 2318 return out;2319 2320}2321 2322 2323function useQr(p) {2324 2325 p =2326 String(p || "");2327 2328 migrationAccounts = [];2329 2330 if (2331 /^otpauth-migration:\\/\\//i2332 .test(p)2333 ) {2334 2335 try {2336 2337 migrationAccounts =2338 parseMigrationQr(p);2339 2340 if (2341 !migrationAccounts.length2342 ) {2343 2344 return toast(2345 "No TOTP accounts found in this migration QR"2346 );2347 2348 }2349 2350 qrPayload =2351 migrationAccounts[0].uri;2352 2353 document2354 .getElementById("qrName")2355 .value =2356 migrationAccounts.length === 12357 ? migrationAccounts[0].name2358 : "Google Authenticator migration (" +2359 migrationAccounts.length +2360 " accounts)";2361 2362 document2363 .getElementById("qrCapture")2364 .classList2365 .add("hidden");2366 2367 document2368 .getElementById("qrReview")2369 .classList2370 .remove("hidden");2371 2372 toast(2373 migrationAccounts.length +2374 " accounts detected"2375 );2376 2377 refreshIcons();2378 2379 return;2380 2381 } catch (e) {2382 2383 return toast(2384 "Migration QR could not be decoded"2385 );2386 2387 }2388 2389 }2390 2391 if (2392 !/^otpauth:\\/\\/totp\\//i2393 .test(p)2394 ) {2395 2396 return toast(2397 "This is not a supported TOTP QR code"2398 );2399 2400 }2401 2402 qrPayload = p;2403 2404 document2405 .getElementById("qrName")2406 .value =2407 parseQrName(p);2408 2409 document2410 .getElementById("qrCapture")2411 .classList2412 .add("hidden");2413 2414 document2415 .getElementById("qrReview")2416 .classList2417 .remove("hidden");2418 2419 refreshIcons();2420 2421}2422 2423 2424function discardQr() {2425 2426 migrationAccounts = [];2427 2428 clearPendingQr();2429 2430 refreshIcons();2431 2432}2433 2434 2435function readQrFile(f) {2436 2437 if (!f) return;2438 2439 if (2440 typeof window.jsQR !== "function"2441 ) {2442 2443 toast(2444 "QR scanner is still loading. Please try again."2445 );2446 2447 return;2448 2449 }2450 2451 const img =2452 new Image();2453 2454 img.onload =2455 function () {2456 2457 try {2458 2459 const c =2460 document.createElement(2461 "canvas"2462 );2463 2464 const x =2465 c.getContext("2d");2466 2467 c.width =2468 img.naturalWidth;2469 2470 c.height =2471 img.naturalHeight;2472 2473 x.drawImage(2474 img,2475 0,2476 02477 );2478 2479 const q =2480 window.jsQR(2481 x.getImageData(2482 0,2483 0,2484 c.width,2485 c.height2486 ).data,2487 2488 c.width,2489 c.height,2490 2491 {2492 inversionAttempts:2493 "attemptBoth"2494 }2495 );2496 2497 if (q) {2498 useQr(q.data);2499 } else {2500 toast(2501 "QR code could not be read"2502 );2503 }2504 2505 } catch (e) {2506 2507 toast(2508 "QR code could not be read"2509 );2510 2511 }2512 2513 };2514 2515 img.onerror =2516 function () {2517 2518 toast(2519 "Unable to read this image"2520 );2521 2522 };2523 2524 img.src =2525 URL.createObjectURL(f);2526 2527}2528 2529 2530/* =========================2531 CAMERA2532========================= */2533 2534async function startCamera() {2535 2536 try {2537 2538 if (2539 !navigator.mediaDevices ||2540 !navigator.mediaDevices.getUserMedia2541 ) {2542 2543 toast(2544 "Camera is not available in this WebView"2545 );2546 2547 return;2548 2549 }2550 2551 if (2552 typeof window.jsQR !== "function"2553 ) {2554 2555 toast(2556 "QR scanner is still loading"2557 );2558 2559 return;2560 2561 }2562 2563 stopCamera();2564 2565 stream =2566 await navigator.mediaDevices.getUserMedia({2567 video: {2568 facingMode: {2569 ideal: "environment"2570 }2571 },2572 audio: false2573 });2574 2575 const v =2576 document.getElementById(2577 "camera"2578 );2579 2580 if (!v) return;2581 2582 v.srcObject =2583 stream;2584 2585 v.classList.remove(2586 "hidden"2587 );2588 2589 document2590 .getElementById("cameraEmpty")2591 .classList2592 .add("hidden");2593 2594 await v.play();2595 2596 scanFrame();2597 2598 } catch (e) {2599 2600 toast(2601 "Camera access was denied or unavailable"2602 );2603 2604 }2605 2606}2607 2608 2609function scanFrame() {2610 2611 const v =2612 document.getElementById(2613 "camera"2614 );2615 2616 if (2617 !stream ||2618 !v ||2619 typeof window.jsQR !== "function"2620 ) {2621 return;2622 }2623 2624 const c =2625 document.createElement(2626 "canvas"2627 );2628 2629 const x =2630 c.getContext("2d");2631 2632 c.width =2633 v.videoWidth;2634 2635 c.height =2636 v.videoHeight;2637 2638 if (2639 c.width &&2640 c.height2641 ) {2642 2643 x.drawImage(2644 v,2645 0,2646 02647 );2648 2649 const q =2650 window.jsQR(2651 x.getImageData(2652 0,2653 0,2654 c.width,2655 c.height2656 ).data,2657 2658 c.width,2659 c.height,2660 2661 {2662 inversionAttempts:2663 "attemptBoth"2664 }2665 );2666 2667 if (q) {2668 2669 useQr(q.data);2670 2671 stopCamera();2672 2673 return;2674 2675 }2676 2677 }2678 2679 requestAnimationFrame(2680 scanFrame2681 );2682 2683}2684 2685 2686function stopCamera() {2687 2688 if (stream) {2689 2690 try {2691 2692 stream2693 .getTracks()2694 .forEach(function (t) {2695 t.stop();2696 });2697 2698 } catch (e) {}2699 2700 stream = null;2701 2702 }2703 2704 const v =2705 document.getElementById(2706 "camera"2707 );2708 2709 if (v) {2710 2711 v.srcObject = null;2712 2713 v.classList.add(2714 "hidden"2715 );2716 2717 }2718 2719 const ce =2720 document.getElementById(2721 "cameraEmpty"2722 );2723 2724 if (ce) {2725 2726 ce.classList.remove(2727 "hidden"2728 );2729 2730 }2731 2732}2733 2734 2735/* =========================2736 ADD QR2737========================= */2738 2739async function addQr() {2740 2741 const name =2742 document2743 .getElementById("qrName")2744 .value2745 .trim();2746 2747 2748 if (2749 migrationAccounts.length2750 ) {2751 2752 try {2753 2754 const list =2755 migrationAccounts.map(2756 function (a) {2757 2758 return {2759 uri: a.uri,2760 2761 name:2762 migrationAccounts.length === 12763 ? (2764 name ||2765 a.name2766 )2767 : a.name2768 };2769 2770 }2771 );2772 2773 const r =2774 await api({2775 action: "add_batch",2776 accounts: list2777 });2778 2779 Array.prototype.push.apply(2780 BOOT.keys,2781 r.accounts2782 );2783 2784 migrationAccounts = [];2785 2786 clearPendingQr();2787 2788 renderAccounts();2789 2790 showView("home");2791 2792 toast(2793 r.accounts.length +2794 " accounts added"2795 );2796 2797 } catch (e) {2798 2799 toast(e.message);2800 2801 }2802 2803 return;2804 2805 }2806 2807 2808 if (2809 !qrPayload ||2810 !name2811 ) {2812 2813 return toast(2814 "Enter an account name"2815 );2816 2817 }2818 2819 2820 try {2821 2822 const r =2823 await api({2824 action: "add",2825 uri: qrPayload,2826 name: name2827 });2828 2829 BOOT.keys.push(2830 r.account2831 );2832 2833 clearPendingQr();2834 2835 renderAccounts();2836 2837 showView("home");2838 2839 toast(2840 "Account added"2841 );2842 2843 } catch (e) {2844 2845 toast(e.message);2846 2847 }2848 2849}2850 2851 2852/* =========================2853 DELETE2854========================= */2855 2856async function removeCurrent() {2857 2858 if (2859 current < 0 ||2860 !confirm(2861 "Delete this account?"2862 )2863 ) {2864 return;2865 }2866 2867 try {2868 2869 await api({2870 action: "delete",2871 index: current2872 });2873 2874 BOOT.keys.splice(2875 current,2876 12877 );2878 2879 BOOT.keys.forEach(2880 function (a, i) {2881 a.index = i;2882 }2883 );2884 2885 current = -1;2886 2887 renderAccounts();2888 2889 showView("home");2890 2891 toast(2892 "Account deleted"2893 );2894 2895 } catch (e) {2896 2897 toast(e.message);2898 2899 }2900 2901}2902 2903 2904/* =========================2905 PROFILE2906========================= */2907 2908function initProfile() {2909 2910 try {2911 2912 const p =2913 BOOT.profile || {};2914 2915 const name =2916 document.getElementById(2917 "profileName"2918 );2919 2920 const handle =2921 document.getElementById(2922 "profileHandle"2923 );2924 2925 const id =2926 document.getElementById(2927 "profileId"2928 );2929 2930 if (name) {2931 2932 name.textContent =2933 (2934 String(p.name || "") +2935 " " +2936 String(p.lastName || "")2937 ).trim();2938 2939 }2940 2941 if (handle) {2942 2943 handle.textContent =2944 p.username2945 ? "@" + p.username2946 : "Telegram user";2947 2948 }2949 2950 if (id) {2951 2952 id.textContent =2953 p.id || "";2954 2955 }2956 2957 2958 const photo =2959 tg &&2960 tg.initDataUnsafe &&2961 tg.initDataUnsafe.user &&2962 tg.initDataUnsafe.user.photo_url;2963 2964 2965 const profilePhoto =2966 document.getElementById(2967 "profilePhoto"2968 );2969 2970 if (profilePhoto) {2971 2972 if (photo) {2973 2974 profilePhoto.innerHTML =2975 '<img src="' +2976 esc(photo) +2977 '">';2978 2979 } else {2980 2981 profilePhoto.innerHTML =2982 '<img src="https://api.dicebear.com/9.x/initials/svg?seed=' +2983 encodeURIComponent(2984 p.name || "User"2985 ) +2986 '">';2987 2988 }2989 2990 }2991 2992 2993 const links = [2994 2995 [2996 "user-round",2997 "Owner",2998 BOOT.support &&2999 BOOT.support.owner3000 ],3001 3002 [3003 "megaphone",3004 "Channel",3005 BOOT.support &&3006 BOOT.support.channel3007 ],3008 3009 [3010 "users",3011 "Community",3012 BOOT.support &&3013 BOOT.support.community3014 ]3015 3016 ].filter(function (x) {3017 return x[2];3018 });3019 3020 3021 const supportLinks =3022 document.getElementById(3023 "supportLinks"3024 );3025 3026 3027 if (supportLinks) {3028 3029 supportLinks.innerHTML =3030 links.length3031 3032 ? links.map(3033 function (x) {3034 3035 return (3036 '<a href="' +3037 esc(x[2]) +3038 '" target="_blank" class="card p-5 flex items-center justify-between">' +3039 3040 '<span class="flex gap-3 items-center">' +3041 3042 '<i data-lucide="' +3043 x[0] +3044 '"></i>' +3045 3046 esc(x[1]) +3047 3048 '</span>' +3049 3050 '<i data-lucide="arrow-up-right"></i>' +3051 3052 '</a>'3053 );3054 3055 }3056 ).join("")3057 3058 : '<div class="card p-5 text-slate-500">' +3059 'Support links are not configured yet.' +3060 '</div>';3061 3062 }3063 3064 refreshIcons();3065 3066 } catch (e) {3067 3068 console.error(3069 "initProfile error",3070 e3071 );3072 3073 }3074 3075}3076 3077 3078/* =========================3079 WELCOME3080========================= */3081 3082function closeWelcome() {3083 3084 const modal =3085 document.getElementById(3086 "welcomeModal"3087 );3088 3089 if (modal) {3090 3091 modal.classList.remove(3092 "show"3093 );3094 3095 }3096 3097 try {3098 3099 localStorage.setItem(3100 "authkeyWelcome",3101 "1"3102 );3103 3104 } catch (e) {}3105 3106}3107 3108 3109/* =========================3110 APP INITIALIZATION3111========================= */3112 3113/*3114 IMPORTANT:3115 3116 Splash removal is deliberately executed3117 before the rest of initialization.3118 3119 Therefore a failed CDN, Lucide error,3120 Telegram error, or rendering error3121 cannot permanently trap the user3122 on the splash screen.3123*/3124 3125(function initApp() {3126 3127 const splash =3128 document.getElementById(3129 "splash"3130 );3131 3132 /* Remove splash immediately */3133 3134 if (splash) {3135 3136 setTimeout(3137 function () {3138 3139 splash.classList.add(3140 "gone"3141 );3142 3143 },3144 1503145 );3146 3147 setTimeout(3148 function () {3149 3150 splash.style.display =3151 "none";3152 3153 },3154 9003155 );3156 3157 }3158 3159 3160 /* Render rest of application safely */3161 3162 try {3163 3164 renderAccounts();3165 3166 } catch (e) {3167 3168 console.error(3169 "Initial account render failed",3170 e3171 );3172 3173 }3174 3175 3176 try {3177 3178 initProfile();3179 3180 } catch (e) {3181 3182 console.error(3183 "Initial profile render failed",3184 e3185 );3186 3187 }3188 3189 3190 try {3191 3192 refreshIcons();3193 3194 } catch (e) {}3195 3196 3197 try {3198 3199 if (3200 !localStorage.getItem(3201 "authkeyWelcome"3202 )3203 ) {3204 3205 setTimeout(3206 function () {3207 3208 const modal =3209 document.getElementById(3210 "welcomeModal"3211 );3212 3213 if (modal) {3214 3215 modal.classList.add(3216 "show"3217 );3218 3219 }3220 3221 },3222 8503223 );3224 3225 }3226 3227 } catch (e) {}3228 3229})();3230 3231</script>3232 3233</body>3234</html>`);