// ==UserScript== // @name AutoFUT Trader // @namespace https://www.autofuttrader.fr // @description AutoFUT Trader bot for the EA FC Ultimate Team web app (iPhone / Safari, Userscripts app). AutoFUT Trader account required. // @version 1.1.4 // @updateURL https://www.autofuttrader.fr/downloads/autofut-trader-iphone.user.js // @downloadURL https://www.autofuttrader.fr/downloads/autofut-trader-iphone.user.js // @match https://www.ea.com/* // @match https://ea.com/* // @match https://api.autofuttrader.fr/web/auth/discord/done* // @match https://api.captain-trading.fr/web/auth/discord/done* // @run-at document-end // @inject-into content // @grant GM.xmlHttpRequest // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @connect api.autofuttrader.fr // @connect api.captain-trading.fr // @connect www.ea.com // @noframes // ==/UserScript== // © 2026 AutoFUT Trader — logiciel propriétaire, tous droits réservés. Voir le fichier LICENSE. (() => { // langue : français si l'iPhone est en français, anglais sinon const EN = !/^fr/i.test(navigator.language || ''); const EN_TXT = { "Connexion avec Discord annulée ou refusée. Retourne sur la web app EA pour réessayer.": "Discord sign-in cancelled or refused. Go back to the EA web app to try again.", "Connexion avec Discord échouée.": "Discord sign-in failed.", " Retourne sur la web app EA pour réessayer.": " Go back to the EA web app to try again.", "Erreur : ": "Error: ", "Serveur AutoFUT Trader injoignable. Vérifie ta connexion internet.": "Can't reach the AutoFUT Trader server. Check your internet connection.", "Vérification du bot trop longue.": "Bot verification took too long.", "Connecte-toi à ton compte AutoFUT Trader.": "Sign in to your AutoFUT Trader account.", "Mets ton iPhone à jour (iOS 17 minimum) pour utiliser AutoFUT Trader. (": "Update your iPhone (iOS 17 minimum) to use AutoFUT Trader. (", "Le code reçu n'est pas signé par AutoFUT Trader : chargement refusé.": "The code received isn't signed by AutoFUT Trader: loading refused.", "Code du bot invalide.": "Invalid bot code.", "Connecte-toi avec ton code (Mon compte → Code de connexion iPhone), ton e-mail ou Discord.": "Sign in with your code (My account → iPhone sign-in code), your email or Discord.", "Changer de compte / code": "Change account / code", "Réessayer": "Try again", "Connecte-toi à ton compte AutoFUT Trader pour charger le bot.": "Sign in to your AutoFUT Trader account to load the bot.", "Mot de passe": "Password", "Se connecter": "Sign in", "Créer un compte": "Create an account", "Se connecter avec Discord": "Continue with Discord", "ou avec ton e-mail": "or with your email", "Code de connexion (ex. AB12-CD34)": "Sign-in code (e.g. AB12-CD34)", "Valider le code": "Use this code", "Le plus simple : génère un code sur le site, page Mon compte → « Code de connexion iPhone ».": "Easiest: create a code on the website, My account page → \"iPhone sign-in code\".", "ou": "or", "Ton compte n'a pas de licence active : le bot n'est pas chargé.": "Your account has no active license: the bot isn't loaded.", "Voir les formules": "See the plans", "Essai gratuit 24 h": "Free 24h trial", "J'ai acheté : vérifier": "I bought it: check", "Nouvelle version du script : ouvre l’app Userscripts et mets AutoFUT Trader à jour (ou recopie-le depuis autofuttrader.fr/iphone).": "New script version: open the Userscripts app and update AutoFUT Trader (or install it again from autofuttrader.fr/iphone).", "Connecte-toi à la web app EA : le bot se chargera automatiquement.": "Sign in to the EA web app: the bot will load automatically.", "Chargement du bot…": "Loading the bot…", "Route non autorisée.": "Route not allowed." }; const T = (fr) => (EN && EN_TXT[fr]) || fr; const API_EN = { TOO_MANY: 'Too many attempts. Wait 15 minutes or reset your password.', NEED_PASSWORD: 'Your account was created when you paid but has no password yet. We just sent you an email so you can choose one.', BAD_LOGIN: 'Wrong email or password. (If your account uses Discord sign-in, use "Continue with Discord".)', NEED_PSEUDO: 'First choose your username on the website (My account), then sign in again.', NO_SESSION: 'Session expired, please sign in again.', TRIAL_USED: 'You have already used your free trial with this account.', NEED_VERIFIED_EMAIL: 'First confirm your email address (link sent by email, or resend it from My account) to start the free trial.', TRIAL_REFUSED: 'Your free trial request was refused by the team. If you have any questions, open a ticket on our Discord.', TRIAL_PENDING: 'Your free trial is being checked by the team. Try again in a few minutes.', INVALID_KEY: 'Unknown license key.', REVOKED: 'This license has been disabled. Contact support.', EA_MISMATCH: 'This license is linked to another EA account. Contact support to transfer it.', }; const EA_URL = 'https://www.ea.com/ea-sports-fc/ultimate-team/web-app/'; if (/^api.(autofuttrader|captain-trading).fr$/.test(location.hostname)) { const code = new URLSearchParams(location.hash.slice(1)).get('dcode'); history.replaceState(null, '', location.pathname); const msg = (t) => { const m = document.getElementById('m'); if (m) m.textContent = t; }; (async () => { const nonce = await GM.getValue('discordNonce'); await GM.deleteValue('discordNonce'); if (!code || !nonce) return msg(T('Connexion avec Discord annulée ou refusée. Retourne sur la web app EA pour réessayer.')); try { const r = await fetch('/web/api/discord/exchange', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code, nonce }) }); const d = await r.json(); if (!r.ok || !d.token) throw new Error((d && d.message) || T('Connexion avec Discord échouée.')); await GM.setValue('accountToken', d.token); location.replace(EA_URL); } catch (e) { msg(e.message + T(' Retourne sur la web app EA pour réessayer.')); } })(); return; } if (window.__captainUserscript || !/ultimate-team\/web-app/.test(location.pathname)) return; window.__captainUserscript = true; const fail = (e) => { try { notice(T('Erreur : ') + (e && e.message ? e.message : e), true); } catch (_) {} }; window.addEventListener('unhandledrejection', (ev) => fail(ev.reason)); const API = 'https://api.autofuttrader.fr'; const SITE = 'https://www.autofuttrader.fr'; const APP_VERSION = 'ios-1.1.2'; const SCRIPT_VERSION = '1.1.3'; // « ios-x.y.z » : les versions trop vieilles peuvent être bloquées (MIN_CLIENT_VERSIONS) const PUBLIC_KEY_B64 = 'MCowBQYDK2VwAyEAyJ5RjW0tFQ6xsxC4tokJS1SaDuoJ++McltJ5dgExXZs='; const FATAL = new Set(['INVALID_KEY', 'REVOKED', 'EXPIRED', 'DEVICE_MISMATCH', 'EA_MISMATCH', 'BAD_SIGNATURE']); const API_PATH = /^\/v1\/[a-z0-9/_-]+(\?[\w=&%.-]*)?$/i; const API_FORBIDDEN = /^\/v1\/(activate|heartbeat|bundle|bind|trial)(\/|$|\?)/; const S = { token: null, tokenExp: 0, tier: 'standard', ea: null, loaded: false, activating: null }; class ApiError extends Error { constructor(code, message, status = 0, data = null) { super((EN && API_EN[code]) || message); this.code = code; this.status = status; this.data = data; } } async function xhr(opts) { const { method = 'GET', path, token, body, binary = false } = opts; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 25000); try { const res = await fetch(API + path, { method, signal: ctrl.signal, credentials: 'omit', cache: 'no-store', headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body ? { 'content-type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined, }); const responseHeaders = Array.from(res.headers.entries()).map(([k, v]) => `${k}: ${v}`).join('\r\n'); const response = binary ? await res.arrayBuffer() : await res.text(); return { status: res.status, response, responseText: binary ? '' : response, responseHeaders }; } catch (_) { return gmXhr(opts); } finally { clearTimeout(timer); } } function gmXhr({ method = 'GET', path, token, body, binary = false }) { return new Promise((resolve) => { if (typeof GM === 'undefined' || !GM.xmlHttpRequest) return resolve(null); GM.xmlHttpRequest({ method, url: API + path, timeout: 25000, headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body ? { 'content-type': 'application/json' } : {}) }, data: body ? JSON.stringify(body) : undefined, responseType: binary ? 'arraybuffer' : 'text', onload: (r) => resolve(r), onerror: () => resolve(null), ontimeout: () => resolve(null), }); }); } async function json(path, opts = {}) { const r = await xhr({ ...opts, path }); if (!r) throw new ApiError('NETWORK', T('Serveur AutoFUT Trader injoignable. Vérifie ta connexion internet.')); let data = null; try { data = JSON.parse(typeof r.response === 'string' ? r.response : r.responseText); } catch (_) {} if (r.status < 200 || r.status > 299 || !data || data.ok === false) { throw new ApiError((data && data.error) || `HTTP_${r.status}`, (data && data.message) || `Erreur serveur (${r.status}).`, r.status, data); } return data; } // identifiant d'appareil : tiré au hasard une fois, gardé dans l'extension (pas de crypto.subtle : refusé par Safari ici) async function deviceId() { let raw = await GM.getValue('deviceRaw'); if (!raw) { raw = typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join(''); await GM.setValue('deviceRaw', raw); } return 'ios-' + String(raw).replace(/[^a-f0-9]/gi, '').slice(0, 40); } // copie dans un ArrayBuffer neuf (Safari refuse parfois les Uint8Array venant d'un autre contexte) function toAB(u8) { const ab = new ArrayBuffer(u8.byteLength); new Uint8Array(ab).set(u8); return ab; } // repli : même opération faite dans la page (son crypto.subtle accepte les données), réponse par postMessage let cryptoSeq = 0; const cryptoWait = new Map(); function pageCrypto(op, args) { return new Promise((resolve, reject) => { const id = 'c' + (++cryptoSeq); const timer = setTimeout(() => { cryptoWait.delete(id); reject(new Error(T('Vérification du bot trop longue.'))); }, 20000); cryptoWait.set(id, { resolve, reject, timer }); window.postMessage({ direction: 'CAPTAIN_CRYPTO_REQ', id, op, args }, location.origin); }); } function applySession(data) { S.token = data.token; S.tokenExp = Date.now() + (data.tokenTtlMs || 30 * 60000); S.tier = data.license && data.license.tier === 'premium' ? 'premium' : 'standard'; } async function activate() { const acc = await GM.getValue('accountToken'); if (!acc) throw new ApiError('NO_SESSION', T('Connecte-toi à ton compte AutoFUT Trader.')); try { applySession(await json('/web/api/app/activate', { method: 'POST', token: acc, body: { deviceId: await deviceId(), appVersion: APP_VERSION } })); } catch (e) { if (e.code === 'NO_SESSION' || e.code === 'BAD_SESSION' || e.status === 401) await GM.deleteValue('accountToken'); throw e; } } async function freshToken() { if (S.token && Date.now() < S.tokenExp - 60000) return S.token; if (!S.activating) S.activating = activate().finally(() => { S.activating = null; }); await S.activating; return S.token; } const b64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); async function verify(buf, sigB64) { if (!sigB64) return false; let pub, sig, data; try { pub = toAB(b64(PUBLIC_KEY_B64)); sig = toAB(b64(sigB64)); data = toAB(buf); } catch (_) { return false; } // signature illisible = refus try { const key = await crypto.subtle.importKey('spki', pub, { name: 'Ed25519' }, false, ['verify']); return await crypto.subtle.verify({ name: 'Ed25519' }, key, sig, data); } catch (_) { try { return (await pageCrypto('verify', [pub, sig, data])) === true; } catch (e) { throw new ApiError('OLD_IOS', T('Mets ton iPhone à jour (iOS 17 minimum) pour utiliser AutoFUT Trader. (') + (e && e.message ? e.message : e) + ')'); } } } async function gunzip(buf) { const ab = toAB(buf); try { const stream = new Blob([ab]).stream().pipeThrough(new DecompressionStream('gzip')); return await new Response(stream).text(); } catch (_) { return String(await pageCrypto('gunzip', [ab])); } } function header(r, name) { const m = String(r.responseHeaders || '').match(new RegExp(`^${name}:\\s*(.+)$`, 'im')); return m ? m[1].trim() : null; } async function loadBundle(ea) { const token = await freshToken(); const r = await xhr({ method: 'POST', path: '/v1/bundle', token, body: { eaAccount: ea }, binary: true }); if (!r) throw new ApiError('NETWORK', T('Serveur AutoFUT Trader injoignable. Vérifie ta connexion internet.')); const type = header(r, 'content-type') || ''; if (r.status < 200 || r.status > 299 || type.includes('json')) { let data = null; try { data = JSON.parse(new TextDecoder().decode(r.response)); } catch (_) {} throw new ApiError((data && data.error) || `HTTP_${r.status}`, (data && data.message) || `Erreur serveur (${r.status}).`, r.status, data); } const buf = new Uint8Array(r.response); if (!(await verify(buf, header(r, 'x-bundle-signature')))) { throw new ApiError('BAD_SIGNATURE', T("Le code reçu n'est pas signé par AutoFUT Trader : chargement refusé.")); } const parsed = JSON.parse(await gunzip(buf)); const files = parsed.files || {}; if (!files['content.js']) throw new ApiError('BAD_BUNDLE', T('Code du bot invalide.')); return { content: files['content.js'], style: files['style.css'] || '' }; } function pageRun(code) { const s = document.createElement('script'); s.textContent = code; (document.head || document.documentElement).appendChild(s); s.remove(); } function startBot(bundle) { if (!document.getElementById('captain-style')) { const st = document.createElement('style'); st.id = 'captain-style'; st.textContent = bundle.style; document.documentElement.appendChild(st); } const license = JSON.stringify({ ok: true, tier: S.tier, platform: 'ios' }); pageRun(`if (!window.__ezzouDesktopInjected) { window.__ezzouDesktopInjected = true; window.__captainLicense = ${license}; window.__captainPlatform = 'ios';\n${bundle.content}\n}`); } pageRun(`(() => { if (window.__captainProbe) return; window.__captainProbe = true; let last = null; const read = () => { try { const u = window.services && window.services.User && window.services.User.getUser && window.services.User.getUser(); if (!u || u.id == null || u.id === '' || u.id === 0) return null; const ids = [String(u.id).slice(0, 40)]; const p = typeof u.getSelectedPersona === 'function' ? u.getSelectedPersona() : null; if (p && p.id != null && p.id !== 0 && String(p.id) !== ids[0]) ids.push(String(p.id).slice(0, 40)); return 'ea:' + ids.join('|'); } catch (_) { return null; } }; const tick = () => { const id = read(); if (id && id !== last) { last = id; window.postMessage({ direction: 'CAPTAIN_EA', eaAccount: id }, location.origin); } }; tick(); setInterval(tick, 2000); window.addEventListener('message', async (ev) => { const m = ev.data; if (ev.source !== window || !m || m.direction !== 'CAPTAIN_CRYPTO_REQ') return; const reply = (ok, out) => window.postMessage({ direction: 'CAPTAIN_CRYPTO_RES', id: m.id, ok, out }, location.origin); try { const a = m.args || []; if (m.op === 'verify') { const key = await crypto.subtle.importKey('spki', a[0], { name: 'Ed25519' }, false, ['verify']); reply(true, await crypto.subtle.verify({ name: 'Ed25519' }, key, a[1], a[2])); } else if (m.op === 'gunzip') { reply(true, await new Response(new Blob([a[0]]).stream().pipeThrough(new DecompressionStream('gzip'))).text()); } else reply(false, 'op'); } catch (e) { reply(false, String(e && e.message ? e.message : e)); } }); })();`); const BOX = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:2147483647;width:min(420px,calc(100vw - 24px));box-sizing:border-box;padding:14px 16px;border-radius:14px;background:#13111c;border:1px solid rgba(139,92,246,.55);box-shadow:0 12px 40px rgba(0,0,0,.6);font:600 14px -apple-system,Segoe UI,Arial,sans-serif;color:#f3f3f3;line-height:1.4'; const BTN = 'border:0;border-radius:10px;padding:10px 14px;font:800 14px -apple-system,Arial,sans-serif;cursor:pointer;'; const INPUT = 'width:100%;box-sizing:border-box;margin-top:8px;padding:11px 12px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:#1e1e1e;color:#fff;font:500 16px -apple-system,Arial,sans-serif'; function hide() { document.getElementById('captain-us-box')?.remove(); } function box() { hide(); const el = document.createElement('div'); el.id = 'captain-us-box'; el.style.cssText = BOX; document.documentElement.appendChild(el); return el; } function head(el, text, color = '#a78bfa') { const t = document.createElement('div'); const b = document.createElement('b'); b.style.color = color; b.textContent = 'AutoFUT Trader'; const v = document.createElement('span'); v.style.cssText = 'margin-left:6px;font-weight:500;font-size:11px;color:#8a8a8a'; v.textContent = 'v' + SCRIPT_VERSION; b.appendChild(v); const s = document.createElement('div'); s.style.cssText = 'margin-top:4px;font-weight:500;color:#cfcfcf'; s.textContent = text; t.append(b, s); el.appendChild(t); } function button(label, primary, onClick) { const b = document.createElement('button'); b.type = 'button'; b.textContent = label; b.style.cssText = BTN + (primary ? 'background:#8b5cf6;color:#fff' : 'background:#262433;color:#f3f3f3'); b.addEventListener('click', onClick); return b; } // déconnexion du compte AutoFUT dans le script : on peut se reconnecter avec un code, un e-mail ou Discord async function switchAccount() { await GM.deleteValue('accountToken'); S.token = null; S.tokenExp = 0; S.loaded = false; loginForm(T('Connecte-toi avec ton code (Mon compte → Code de connexion iPhone), ton e-mail ou Discord.')); } function notice(text, error = false) { const el = box(); if (error) el.style.borderColor = 'rgba(248,113,113,.6)'; head(el, text, error ? '#fca5a5' : '#a78bfa'); const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;margin-top:10px'; if (error) { row.append(button(T('Changer de compte / code'), false, switchAccount)); row.append(button(T('Réessayer'), true, () => { hide(); S.token = null; S.tokenExp = 0; boot(); })); } else { row.append(button('OK', false, hide)); } el.appendChild(row); } function loginForm(msg) { const el = box(); head(el, msg || T('Connecte-toi à ton compte AutoFUT Trader pour charger le bot.')); const form = document.createElement('form'); const email = document.createElement('input'); email.type = 'email'; email.placeholder = 'E-mail'; email.autocomplete = 'username'; email.style.cssText = INPUT; const pass = document.createElement('input'); pass.type = 'password'; pass.placeholder = T('Mot de passe'); pass.autocomplete = 'current-password'; pass.style.cssText = INPUT; const err = document.createElement('div'); err.style.cssText = 'margin-top:8px;color:#fca5a5;font-weight:600;display:none'; const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;margin-top:12px;flex-wrap:wrap'; const submit = button(T('Se connecter'), true, () => {}); submit.type = 'submit'; row.append(submit, button(T('Créer un compte'), false, () => window.open(`${SITE}/compte`, '_blank'))); const discord = button(T('Se connecter avec Discord'), false, async () => { const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(24)))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); await GM.setValue('discordNonce', nonce); location.href = `${API}/web/auth/discord?n=${encodeURIComponent(nonce)}&ret=us`; }); discord.style.cssText = BTN + 'background:#5865f2;color:#fff;width:100%;margin-top:12px'; const sep = document.createElement('div'); sep.style.cssText = 'margin-top:12px;text-align:center;color:#8a8a8a;font-weight:500;font-size:13px'; sep.textContent = T('ou avec ton e-mail'); const codeIn = document.createElement('input'); codeIn.placeholder = T('Code de connexion (ex. AB12-CD34)'); codeIn.autocomplete = 'one-time-code'; codeIn.autocapitalize = 'characters'; codeIn.spellcheck = false; codeIn.style.cssText = INPUT; const codeBtn = button(T('Valider le code'), true, async () => { codeBtn.disabled = true; err.style.display = 'none'; try { const data = await json('/web/api/app/code/use', { method: 'POST', body: { code: codeIn.value } }); await GM.setValue('accountToken', data.token); S.token = null; hide(); boot(); } catch (x) { err.textContent = x.message; err.style.display = 'block'; codeBtn.disabled = false; } }); codeBtn.style.cssText += 'width:100%;margin-top:8px'; const codeHelp = document.createElement('div'); codeHelp.style.cssText = 'margin-top:6px;color:#8a8a8a;font-weight:500;font-size:13px'; codeHelp.textContent = T('Le plus simple : génère un code sur le site, page Mon compte → « Code de connexion iPhone ».'); const sep2 = document.createElement('div'); sep2.style.cssText = sep.style.cssText; sep2.textContent = T('ou'); form.append(codeIn, codeBtn, codeHelp, sep2, discord, sep, email, pass, err, row); el.appendChild(form); form.addEventListener('submit', async (e) => { e.preventDefault(); submit.disabled = true; submit.textContent = '…'; err.style.display = 'none'; try { const data = await json('/web/api/login', { method: 'POST', body: { email: email.value.trim(), password: pass.value } }); await GM.setValue('accountToken', data.token); S.token = null; hide(); boot(); } catch (x) { err.textContent = x.message; err.style.display = 'block'; submit.disabled = false; submit.textContent = T('Se connecter'); } }); } function limited(trialAvailable, note) { const el = box(); head(el, note || T("Ton compte n'a pas de licence active : le bot n'est pas chargé.")); const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;margin-top:12px;flex-wrap:wrap'; row.append(button(T('Voir les formules'), true, () => window.open(`${SITE}/tarifs`, '_blank'))); if (trialAvailable) { row.append(button(T('Essai gratuit 24 h'), false, async () => { try { applySession(await json('/web/api/app/trial', { method: 'POST', token: await GM.getValue('accountToken'), body: { deviceId: await deviceId(), appVersion: APP_VERSION } })); hide(); boot(); } catch (e) { limited(false, e.message); } })); } row.append(button(T("J'ai acheté : vérifier"), false, () => { hide(); S.token = null; boot(); })); row.append(button(T('Changer de compte / code'), false, switchAccount)); el.appendChild(row); } async function boot() { if (S.loaded) return; if (!(await GM.getValue('accountToken'))) return loginForm(); try { await freshToken(); } catch (e) { if (e.code === 'NO_SESSION' || e.status === 401) return loginForm(); if (e.code === 'NO_LICENSE') return limited(!!(e.data && e.data.trialAvailable)); if (e.code === 'REVOKED' && e.data && e.data.update) return notice(T('Nouvelle version du script : ouvre l’app Userscripts et mets AutoFUT Trader à jour (ou recopie-le depuis autofuttrader.fr/iphone).'), true); return notice(e.message + (e.code ? ' [' + e.code + ']' : ''), true); } if (!S.ea) { setTimeout(() => { if (!S.ea && !S.loaded && !document.getElementById('captain-us-box')) notice(T('Connecte-toi à la web app EA : le bot se chargera automatiquement.')); }, 6000); return; } try { notice(T('Chargement du bot…')); const bundle = await loadBundle(S.ea); hide(); S.loaded = true; startBot(bundle); heartbeat(); keepAwake(); } catch (e) { if (e.code === 'NO_LICENSE') return limited(!!(e.data && e.data.trialAvailable)); notice(e.message + (e.code ? ' [' + e.code + ']' : ''), true); if (FATAL.has(e.code)) S.token = null; } } function keepAwake() { if (!navigator.wakeLock) return; let lock = null; const ask = async () => { if (lock || document.visibilityState !== 'visible') return; try { lock = await navigator.wakeLock.request('screen'); lock.addEventListener('release', () => { lock = null; }); } catch (_) {} }; document.addEventListener('visibilitychange', ask); document.addEventListener('touchend', ask, { passive: true }); // Safari demande souvent un geste de l'utilisateur ask(); } function heartbeat() { setInterval(async () => { try { applySession(await json('/v1/heartbeat', { method: 'POST', token: await freshToken() })); } catch (e) { if (FATAL.has(e.code)) notice(e.message, true); } }, 10 * 60000); } async function botMessage(type, data) { if (type === 'CAPTAIN_POSTS' || type === 'CAPTAIN_API') { try { let method = 'GET', path, body; if (type === 'CAPTAIN_POSTS') path = `/v1/posts?kind=${data.kind === 'invest' ? 'invest' : 'filter'}`; else { method = String(data.method || 'GET').toUpperCase(); path = String(data.path || ''); if (!API_PATH.test(path) || API_FORBIDDEN.test(path) || !['GET', 'POST'].includes(method)) return { ok: false, error: 'FORBIDDEN_ROUTE', message: T('Route non autorisée.') }; body = method === 'POST' ? (data.body && typeof data.body === 'object' && JSON.stringify(data.body).length < 20000 ? data.body : {}) : undefined; } const res = await json(path, { method, token: await freshToken(), body }); return type === 'CAPTAIN_POSTS' ? { ok: true, posts: res.posts || [] } : res; } catch (e) { return { ok: false, error: e.code || 'ERROR', message: e.message }; } } if (type === 'CAPTAIN_LOG' || type === 'CAPTAIN_NOTIFY') return { ok: true }; if (type === 'CAPTAIN_SIGNALS_INFO') return { ok: true, handled: true }; if (type === 'CAPTAIN_RELOAD_BOT') { setTimeout(() => location.reload(), 300); return { ok: true }; } if (type === 'CAPTAIN_INSTALL_UPDATE') return { ok: false, error: 'NO_UPDATE' }; return { ok: false, error: 'DISABLED', disabled: true, hasTab: false }; } window.addEventListener('message', async (ev) => { if (ev.origin !== location.origin || !ev.data) return; const m = ev.data; if (m.direction === 'CAPTAIN_CRYPTO_RES' && cryptoWait.has(m.id)) { const w = cryptoWait.get(m.id); cryptoWait.delete(m.id); clearTimeout(w.timer); if (m.ok) w.resolve(m.out); else w.reject(new Error(String(m.out || 'erreur'))); return; } if (m.direction === 'CAPTAIN_EA' && typeof m.eaAccount === 'string') { const first = !S.ea; S.ea = m.eaAccount.slice(0, 128); if (first && !S.loaded) { if (document.getElementById('captain-us-box')?.textContent.includes('web app EA')) hide(); boot(); } return; } if (m.direction === 'FROM_CONTENT_SCRIPT' && typeof m.type === 'string') { const response = await botMessage(m.type, m.data || {}); window.postMessage({ direction: 'FROM_BRIDGE', requestId: m.requestId, response }, location.origin); } }); try { boot().catch(fail); } catch (e) { fail(e); } })();