Files
FocusGram-Android/v2/ghost_mode_script.dart
T
2026-05-23 11:56:23 +05:45

284 lines
9.8 KiB
Dart

// 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');
})();
""";