Files
FocusGram-Android/assets/scripts/ghost_mode.js
T
2026-06-09 23:39:43 +05:45

200 lines
7.1 KiB
JavaScript

/**
* FocusGram Ghost Mode (V2 Overlay)
* Injected at DOCUMENT_START — before Instagram's JS loads.
* Blocks story-seen, message-seen, and online-presence signals.
*
* Uses _prev chain pattern: each section saves the PREVIOUS fetch/XHR
* before overriding, so they compose rather than conflict.
*/
(function () {
'use strict';
// ─── First-interaction DM gate ──────────────────────────────────────────
// On /direct/*, first click blocks all api/graphql (inbox loads first).
window.__fgDirectApiBlocked = false;
document.addEventListener('click', function() {
if (window.location.pathname.indexOf('/direct/') === 0) window.__fgDirectApiBlocked = true;
}, true);
document.addEventListener('touchstart', function() {
if (window.location.pathname.indexOf('/direct/') === 0) window.__fgDirectApiBlocked = true;
}, true);
var _prevD = window.location.pathname.indexOf('/direct/') === 0;
setInterval(function() {
var now = window.location.pathname.indexOf('/direct/') === 0;
if (now !== _prevD) { _prevD = now; window.__fgDirectApiBlocked = false; }
}, 300);
function _blockIfNeeded(url) {
return window.__fgDirectApiBlocked &&
window.location.pathname.indexOf('/direct/') === 0 &&
url.indexOf('/api/graphql') !== -1;
}
// ─── SEEN + ACTIVITY patterns ───────────────────────────────────────────
const SEEN_PATTERNS = [
/\/api\/v1\/media\/[\w-]+\/seen\//,
/\/api\/v1\/stories\/reel\/seen\//,
/\/api\/v1\/direct_v2\/threads\/[\w-]+\/seen\//,
/\/api\/v1\/direct_v2\/visual_message\/[\w-]+\/seen\//,
/\/api\/v1\/live\/[\w-]+\/comment\/seen\//,
];
const ACTIVITY_PATTERNS = [
/\/api\/v1\/web\/likes\/[\w-]+\/like\//,
/\/api\/v1\/web\/comments\/add\//,
/\/api\/v1\/friendships\/[\w-]+\/follow\//,
];
const isSeen = (url) => SEEN_PATTERNS.some((p) => p.test(url));
const isActivity = (url) => ACTIVITY_PATTERNS.some((p) => p.test(url));
// ─── Fetch override — chains with whatever was there ──────────────────────
const _prevFetch = window.fetch;
window.fetch = async function (input, init) {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.href
: input?.url ?? '';
// DM first-interaction gate
if (_blockIfNeeded(url)) {
return new Response(JSON.stringify({ status: 'ok' }), {
status: 200, headers: { 'Content-Type': 'application/json' }
});
}
// Seen pattern block
if (isSeen(url)) {
if (window.GhostChannel) {
window.GhostChannel.postMessage(JSON.stringify({ type: 'seen_blocked', url }));
}
return new Response(JSON.stringify({ status: 'ok' }), {
status: 200, headers: { 'Content-Type': 'application/json' }
});
}
// Activity interceptor for local history
if (isActivity(url) && window.ActivityChannel) {
const body = init?.body;
const bodyText =
body instanceof URLSearchParams
? body.toString()
: typeof body === 'string'
? body
: '';
window.ActivityChannel.postMessage(
JSON.stringify({ url, body: bodyText, timestamp: Date.now() })
);
}
return _prevFetch(input, init);
};
window.fetch.toString = () => 'function fetch() { [native code] }';
// ─── XHR override — chains ──────────────────────────────────────────────
const _prevOpen = XMLHttpRequest.prototype.open;
const _prevSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...args) {
this._fg_url = url ?? '';
this._fg_method = (method ?? '').toUpperCase();
return _prevOpen.call(this, method, url, ...args);
};
XMLHttpRequest.prototype.send = function (body) {
const url = this._fg_url || '';
// DM first-interaction gate
if (_blockIfNeeded(url)) {
const self = this;
setTimeout(() => {
Object.defineProperty(self, 'readyState', { get: () => 4 });
Object.defineProperty(self, 'status', { get: () => 200 });
Object.defineProperty(self, 'responseText', { get: () => '{"status":"ok"}' });
Object.defineProperty(self, 'response', { get: () => '{"status":"ok"}' });
['readystatechange', 'load'].forEach(function(t) {
try { self.dispatchEvent(new Event(t)); } catch(e) {}
});
}, 5);
return;
}
// Seen pattern block
if (url && isSeen(url)) {
const self = this;
setTimeout(() => {
Object.defineProperty(self, 'readyState', { get: () => 4 });
Object.defineProperty(self, 'status', { get: () => 200 });
Object.defineProperty(self, 'responseText', { get: () => '{"status":"ok"}' });
Object.defineProperty(self, 'response', { get: () => '{"status":"ok"}' });
['readystatechange', 'load'].forEach(function(t) {
try { self.dispatchEvent(new Event(t)); } catch(e) {}
});
}, 5);
return;
}
return _prevSend.call(this, body);
};
// ─── WebSocket intercept (message-seen via WS) ──────────────────────────
const _WS = window.WebSocket;
function PatchedWebSocket(url, protocols) {
const ws = protocols ? new _WS(url, protocols) : new _WS(url);
const _send = ws.send.bind(ws);
ws.send = function (data) {
if (typeof data === 'string') {
try {
const parsed = JSON.parse(data);
if (
parsed?.op === '4' ||
parsed?.op === 'seen' ||
(parsed?.payload && JSON.parse(parsed.payload)?.op === 'seen')
) {
return; // drop
}
} catch (_) {}
if (data.includes('"seen"') && data.includes('"thread_id"')) {
return;
}
}
return _send(data);
};
return ws;
}
PatchedWebSocket.prototype = _WS.prototype;
PatchedWebSocket.CONNECTING = _WS.CONNECTING;
PatchedWebSocket.OPEN = _WS.OPEN;
PatchedWebSocket.CLOSING = _WS.CLOSING;
PatchedWebSocket.CLOSED = _WS.CLOSED;
window.WebSocket = PatchedWebSocket;
// ─── Visibility trick — hide "Active Now" ──────────────────────────────
window.__fgEnableOnlineHide = function () {
Object.defineProperty(document, 'visibilityState', {
get: () => 'hidden', configurable: true,
});
Object.defineProperty(document, 'hidden', {
get: () => true, configurable: true,
});
document.dispatchEvent(new Event('visibilitychange'));
};
window.__fgDisableOnlineHide = function () {
delete document.visibilityState;
delete document.hidden;
document.dispatchEvent(new Event('visibilitychange'));
};
// Signal to Flutter that ghost mode JS is active
if (window.GhostChannel) {
window.GhostChannel.postMessage(JSON.stringify({ type: 'ready' }));
}
})();