1// Running inside Telegram. 2// 3// A Mini App is an ordinary web page in Telegram's WebView with one extra 4// thing: a signed blob naming the user. That blob replaces the entire wallet 5// handshake for anything that does not move money â which, for a duel, is all 6// of it. Somebody taps a link in a group chat and is playing, 7// with no wallet, no extension, no signature prompt, and no app install. 8// 9// The page is otherwise the same page. Everything here is a no-op in a normal 10// browser, so /duel keeps working exactly as it does today. 11(() => { 12 const tg = window.Telegram && window.Telegram.WebApp; 13 // initData is the test, not the presence of the SDK: the script can load in 14 // a normal browser, where it exists but has nothing signed to offer. 15 const inTelegram = !!(tg && typeof tg.initData === 'string' && tg.initData.length > 0); 16 // On the element rather than the body: this file runs in <head>, before 17 // there is a body to mark. Lets the stylesheet drop the explanatory copy 18 // that reads as clutter on a phone-sized Mini App. 19 if (inTelegram) document.documentElement.classList.add('in-tg'); 20 21 if (tg) { 22 // ready() tells Telegram to drop the loading shroud; expand() takes the 23 // full sheet height, because a cabinet in a half-height sheet is unplayable. 24 try { tg.ready(); } catch (_) {} 25 try { tg.expand(); } catch (_) {} 26 // A DRAG IS NOT A DISMISSAL. 27 // 28 // Telegram reads a vertical swipe inside a Mini App as "close this", and it 29 // reads it BEFORE the page does. Every cabinet here is played with a thumb 30 // on a stick in the lower half of the screen, which is the same gesture â 31 // so the fighter would not move, or the app would slide shut mid-match, and 32 // the page could do nothing about it. Its own handlers are already correct: 33 // touch-action is none and every pointer event is preventDefault'ed. The 34 // gesture never reached them. 35 // 36 // Telegram added this in Bot API 7.7 for exactly this reason. Guarded twice 37 // â the method is absent on older clients, and calling an unsupported one 38 // throws â because a player on an old build should get the old behaviour 39 // rather than a page that stops at the first line of its own setup. 40 // 41 // Not paired with lockOrientation, deliberately: that pins the CURRENT 42 // orientation, so calling it for a landscape game would lock whoever opened 43 // it in portrait into portrait. The rotate gate in orient.js asks properly 44 // and clears when they turn the phone. 45 try { 46 if (typeof tg.disableVerticalSwipes === 'function') tg.disableVerticalSwipes(); 47 } catch (_) { /* older client: the swipe stays Telegram's */ } 48 // Telegram's own back button, so leaving a duel does not mean closing the 49 // app â a person who taps the system back arrow and lands outside is gone. 50 try { 51 if (tg.BackButton && location.pathname.startsWith('/duel/')) { 52 tg.BackButton.show(); 53 tg.BackButton.onClick(() => { location.href = '/duel'; }); 54 } 55 } catch (_) {} 56 } 57 58 const unsafe = (tg && tg.initDataUnsafe) || {}; 59 const user = unsafe.user || null; 60 61 window.ARCADE_TG = { 62 inTelegram, 63 // The signed payload. Sent to the server, which re-derives the HMAC â this 64 // value is never trusted here, and the id below is display only. 65 initData: inTelegram ? tg.initData : null, 66 id: inTelegram && user ? 'tg:' + user.id : null, 67 name: user 68 ? (user.username || [user.first_name, user.last_name].filter(Boolean).join(' ').trim() || null) 69 : null, 70 // t.me/<bot>/<app>?startapp=7 arrives here as "7" â which is a duel id, 71 // and is the whole sharing model. A NAME rather than a number is a 72 // destination instead; see DESTINATIONS below. 73 startParam: unsafe.start_param || null, 74 // A small thing that makes a web page feel like an app. 75 buzz(kind = 'light') { 76 try { tg.HapticFeedback.impactOccurred(kind); } catch (_) {} 77 }, 78 }; 79 80 // Who is making this request â answered in one place, so no page has to know 81 // which kind of player it is holding. Returns the auth fields to merge into 82 // a request body, or null when nobody is signed in at all. 83 // 84 // In Telegram this never prompts and never fails; in a browser it is the 85 // wallet session token exactly as before. 86 window.ARCADE_AUTH = async (opts) => { 87 if (inTelegram) { 88 // A Telegram player who has proved a wallet IS that wallet â same 89 // allowance, same line on the board as when they play on the website.
90 // Without this they would be two players who happen to be one person. 91 const P = window.ARCADE_PHANTOM; 92 if (P) { 93 try { 94 const w = await P.walletAuth(); 95 // The wallet says WHO; initData still says WHERE. Merging the 96 // identity is the point â one allowance, one line on the board â 97 // but it also means the server cannot tell from the identity alone 98 // that a request came from inside the Mini App. tgProof carries that 99 // separately. Nothing on the server asks it today: the one thing 100 // that ever cared was the wagering gate, and wagering is gone. It is 101 // kept because the answer to "where is this from" is cheap to send 102 // and expensive to reconstruct once a page has stopped sending it. 103 if (w) return { ...w, tgProof: tg.initData }; 104 } catch (_) { /* fall back */ } 105 } 106 return { initData: tg.initData }; 107 } 108 const w = window.ARCADE_WALLET; 109 if (!w || !w.publicKey) return null; 110 return { player: w.publicKey, token: await w.token(opts) }; 111 }; 112 113 // The current player's id, for comparing against ids the server sends back. 114 // Who the arcade thinks you are. Set by whichever identity is in force, so 115 // a page can show it without knowing which kind it got. 116 let proved = null; 117 window.ARCADE_SET_PROVED = pk => { proved = pk || null; }; 118 window.ARCADE_ME = () => (inTelegram 119 ? (proved || window.ARCADE_TG.id) 120 : (window.ARCADE_WALLET && window.ARCADE_WALLET.publicKey) || null); 121 122 // ------------------------------------------------------- deep destinations 123 // 124 // A Mini App has exactly ONE address. Whatever BotFather was given is where 125 // every launch lands, however the person got there, so `startapp` is the only 126 // thing a link can say about where it meant to go â and until now the only 127 // thing anything said with it was a duel id. 128 // 129 // That is fine while every launch wants the same page. It stopped being fine 130 // the moment a card in a group chat said "someone is looking for a fight": 131 // tapping it dropped the player on the duel screen, where there is no fight 132 // and no way to find the person who was waiting. A button that does not open 133 // the thing it advertises is worse than no button. 134 // 135 // So a start param that is a NAME from this table is a destination, and the 136 // page it names is opened before anything else on the landing page has run. 137 // Deliberately a table and not a path: `startapp` is attacker-supplied text 138 // from a link anyone can forge and paste into a chat, and treating it as a 139 // URL would make every one of those an open redirect wearing the arcade's 140 // name. A name that is not in this table is not a destination and is left 141 // alone â duel ids, referral codes and Phantom's connect ids all still 142 // arrive through it and are read by whoever owns them. 143 const DESTINATIONS = { 144 brawl: '/brawl', // CRYPTO SMASH â the versus cabinet, and the queue 145 smash: '/brawl', // what the marquee calls it 146 }; 147 148 // The hash travels WITH the redirect, and that is not decoration. Telegram 149 // hands initData over in the launch URL's fragment; the SDK caches it in 150 // sessionStorage and restores it on later same-origin pages, which is what 151 // /duel's back button already leans on â but a cache that is one private-mode 152 // browser away from being empty is a thin thing to hang an identity on when 153 // carrying the fragment forward costs nothing. It never leaves the browser: 154 // a fragment is not sent to the server. 155 if (inTelegram) { 156 const dest = DESTINATIONS[String(unsafe.start_param || '').toLowerCase()]; 157 // Already there â which is what the SECOND run of this file looks like, 158 // on the page we just opened. Without this it would replace itself with 159 // itself forever. 160 if (dest && location.pathname !== dest) { 161 // replace(), not assign(): the launch page is not somewhere the player 162 // chose to be, so Telegram's back button should leave the app rather 163 // than walk back to a screen they never asked for. 164 location.replace(dest + location.search + location.hash); 165 } 166 } 167})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.