// lib/services/ghost_mode_script.dart // Injected at AT_DOCUMENT_START — before Instagram's JS caches fetch/XHR refs const String kGhostModeJS = r""" (function () { 'use strict'; // ─── BLOCKED REST ENDPOINTS ─────────────────────────────────────────────── // Patterns matched against full request URL const URL_BLOCKLIST = [ // Story viewed receipts /\/api\/v1\/media\/seen\//, /\/api\/v1\/feed\/viewed_story\//, /\/api\/v1\/feed\/reels_tray\/seen\//, // DM read receipts (REST fallback path) /\/api\/v1\/direct_v2\/threads\/[^/]+\/mark_item_seen\//, /\/api\/v1\/direct_v2\/mark_item_seen\//, // Ephemeral photo/video reply viewed (Anti-Reply Image) /\/api\/v1\/direct_v2\/threads\/[^/]+\/items\/[^/]+\/mark_visual_item_seen\//, /\/api\/v1\/direct_v2\/visual_thread\/[^/]+\/seen\//, // Voice message listened receipt /\/api\/v1\/direct_v2\/threads\/[^/]+\/items\/[^/]+\/mark_audio_seen\//, // Live join broadcast notification /\/api\/v1\/live\/[^/]+\/join\//, /\/api\/v1\/live\/[^/]+\/get_join_requests\//, /\/api\/v1\/live\/[^/]+\/start_broadcast\//, // Analytics / tracking /\/api\/v1\/qe\//, /\/api\/v1\/launcher\/sync\//, /\/api\/v1\/logging\//, /\/api\/v1\/fb_onetap_logging\//, /\/ajax\/bz/, /\/ajax\/logging\//, /\/api\/v1\/stats\//, /\/api\/v1\/fbanalytics\//, /\/api\/v1\/growth\/account_linked_now\//, ]; // ─── BLOCKED GRAPHQL OPERATIONS ─────────────────────────────────────────── // Instagram web uses GraphQL for many actions — match by operation name in body const GRAPHQL_OP_BLOCKLIST = [ // Story seen 'MarkStorySeen', 'markStorySeen', 'ReelSeenMutation', 'reel_seen', 'IgFeedSeen', // DM read receipts 'MarkDirectThreadItemSeen', 'markDirectThreadItemSeen', 'DirectMarkItemSeen', 'DirectThreadMarkSeen', // Ephemeral media seen 'MarkVisualMessageSeen', 'DirectMarkVisualItemSeen', // Voice message listened 'MarkAudioMessageSeen', 'AudioSeenMutation', // Live join 'LiveJoinBroadcast', 'JoinLiveBroadcast', 'MarkLiveViewer', // Analytics mutations 'LogImpression', 'LogClick', 'FeedbackSeenMutation', ]; // ─── HELPERS ────────────────────────────────────────────────────────────── function shouldBlockUrl(url) { if (!url) return false; try { const path = new URL(url, location.origin).pathname + new URL(url, location.origin).search; return URL_BLOCKLIST.some(p => p.test(path)); } catch { return URL_BLOCKLIST.some(p => p.test(url)); } } function shouldBlockGraphQL(body) { if (!body) return false; let str = ''; if (typeof body === 'string') { str = body; } else if (body instanceof URLSearchParams) { str = body.toString(); } return GRAPHQL_OP_BLOCKLIST.some(op => str.includes(op)); } function isGraphQLEndpoint(url) { return url.includes('/graphql') || url.includes('/api/graphql'); } function fakeOk(body) { return new Response( JSON.stringify(body || { status: 'ok', result: 'success' }), { status: 200, headers: { 'Content-Type': 'application/json' }, } ); } // ─── FETCH INTERCEPT ────────────────────────────────────────────────────── const _fetch = window.fetch; window.fetch = async function (input, init) { const url = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input); if (shouldBlockUrl(url)) { return fakeOk(); } // Clone body for GraphQL inspection without consuming it if (isGraphQLEndpoint(url) && init) { let bodyStr = ''; if (typeof init.body === 'string') { bodyStr = init.body; } else if (init.body instanceof URLSearchParams) { bodyStr = init.body.toString(); } else if (init.body instanceof FormData) { // FormData: iterate entries to build string try { init.body.forEach((v, k) => { bodyStr += k + '=' + v + '&'; }); } catch {} } if (shouldBlockGraphQL(bodyStr)) { return fakeOk(); } } return _fetch.apply(this, arguments); }; // ─── XHR INTERCEPT ─────────────────────────────────────────────────────── const _xhrOpen = XMLHttpRequest.prototype.open; const _xhrSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__ghostUrl = url; return _xhrOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { const url = this.__ghostUrl || ''; const blockByUrl = shouldBlockUrl(url); const blockByOp = isGraphQLEndpoint(url) && shouldBlockGraphQL( typeof body === 'string' ? body : '' ); if (blockByUrl || blockByOp) { const self = this; // Must use defineProperty because readyState etc are read-only Object.defineProperty(self, 'readyState', { get: () => 4, configurable: true }); Object.defineProperty(self, 'status', { get: () => 200, configurable: true }); Object.defineProperty(self, 'responseText', { get: () => '{"status":"ok"}', configurable: true, }); Object.defineProperty(self, 'response', { get: () => '{"status":"ok"}', configurable: true, }); setTimeout(() => { try { self.onreadystatechange && self.onreadystatechange(); } catch {} try { self.onload && self.onload(); } catch {} // Fire events ['readystatechange', 'load'].forEach(t => { try { self.dispatchEvent(new Event(t)); } catch {} }); }, 10); return; } return _xhrSend.apply(this, arguments); }; // ─── WEBSOCKET INTERCEPT (typing + live join) ───────────────────────────── // Instagram uses MQTT over WebSocket for real-time events. // Typing indicator = MQTT PUBLISH to topic containing typing/activity tokens. // Live join viewer notification = MQTT PUBLISH with live topic. const _OrigWS = window.WebSocket; function GhostWebSocket(url, protocols) { const ws = protocols ? new _OrigWS(url, protocols) : new _OrigWS(url); const _wsSend = ws.send.bind(ws); ws.send = function (data) { if (data instanceof ArrayBuffer || data instanceof Uint8Array) { const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data; // MQTT packet type in top 4 bits of byte 0 // PUBLISH = 0x3x (0x30 QoS0, 0x32 QoS1, 0x34 QoS2) const packetType = bytes[0] & 0xF0; if (packetType === 0x30) { // Read remaining length (byte 1, simplified for short packets) // MQTT topic starts at byte 4 (2 byte remaining-len + 2 byte topic-len) try { const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); // Block typing / activity indicator publishes if ( decoded.includes('/t_fs') || // foreground state (typing) decoded.includes('activity_indicator') || decoded.includes('is_typing') || decoded.includes('direct_typing') || decoded.includes('/live/viewer') || // live join notification decoded.includes('live_viewer_list') ) { return; // Drop packet silently } } catch {} } } else if (typeof data === 'string') { // Some WS implementations send JSON if ( data.includes('typing') || data.includes('live_viewer') || data.includes('is_typing') ) { return; } } return _wsSend(data); }; return ws; } // Preserve static properties GhostWebSocket.prototype = _OrigWS.prototype; Object.assign(GhostWebSocket, { CONNECTING: _OrigWS.CONNECTING, OPEN: _OrigWS.OPEN, CLOSING: _OrigWS.CLOSING, CLOSED: _OrigWS.CLOSED, }); window.WebSocket = GhostWebSocket; // ─── KILL SERVICE WORKER ────────────────────────────────────────────────── // SW runs in separate context — bypasses all JS intercepts above. // Kill registration so our fetch/XHR overrides are the only intercept layer. if ('serviceWorker' in navigator) { // Block new registrations navigator.serviceWorker.register = function () { return Promise.reject(new Error('[GhostMode] SW blocked')); }; // Unregister any already registered navigator.serviceWorker.getRegistrations().then(regs => { regs.forEach(r => r.unregister()); }).catch(() => {}); } // ─── BEACON API BLOCK ──────────────────────────────────────────────────── // Instagram uses sendBeacon for analytics on page unload if (navigator.sendBeacon) { navigator.sendBeacon = function (url) { if (shouldBlockUrl(url)) return true; // Lie — say it succeeded // Block all beacon calls to ig domains — analytics only if (url.includes('instagram.com') || url.includes('facebook.com')) return true; return false; }; } console.log('[FocusGram] GhostMode active'); })(); """;