MAX Blinder

Ограничение телеметрии мессенджера MAX

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         MAX Blinder
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Ограничение телеметрии мессенджера MAX
// @author       Echo91
// @match        https://*.max.ru/*
// @license      MIT
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        BLOCK_FETCH_XHR: true,
        BLOCK_WEBSOCKET_OPCODE5: true,
        BLOCK_WEBRTC: true,
        BLOCK_BEACON: true,
        PROTECT_CANVAS: true,
        PROTECT_CANVAS_TO_DATA_URL: true,
        PROTECT_AUDIO: true,
        HIDE_WEBDRIVER: true,
        HIDE_CONNECTION: true,
        FAKE_PLUGINS: true,
        FIXED_SCREEN: true,
        LOG_SERVICE_WORKER: true
    };

    let blockedCount = 0;
    let fullLogHistory = [];
    const maxLogs = 20;
    let pendingLogs = [];
    let uiShadow = null;

    const blackList = [
        'api.ipify.org', 'ifconfig.me', 'ident.me', 'checkip.amazonaws.com',
        'ip.mail.ru', '2ip.ru', 'ipinfo.io', 'ip-api.com', 'myexternalip.com',
        'icanhazip.com', 'jsonip.com', 'httpbin.org/ip', 'wtfismyip.com',
        'apptracer.ru', 'sdk-api', 'crash', 'metrics', 'telemetry', 'analytics',
        'vigo', 'collector', 'log-api', 'error_report', 'notify-stat', 'event/send',
        'tracker-api.vk-analytics.ru', 'my.tracker', 'data.mail.ru', 'fb.do',
        'doubleclick.net', 'google-analytics.com', 'top-fwz1.mail.ru', 'counter.yadro.ru'
    ];

    function shouldBlock(url) {
        if (!url || !CONFIG.BLOCK_FETCH_XHR) return false;
        const sUrl = String(url).toLowerCase();
        return blackList.some(bad => sUrl.includes(bad));
    }

    const makeNative = (obj, prop) => {
        const original = obj[prop];
        if (typeof original === 'function') {
            Object.defineProperty(original, 'toString', {
                value: () => `function ${prop}() { [native code] }`,
                configurable: true,
                writable: true
            });
        }
    };

    // --- ЛОГИРОВАНИЕ (без времени в интерфейсе) ---
    function addEntryToLog(container, data) {
        const entry = document.createElement('div');
        entry.style.cssText = 'border-bottom: 1px solid rgba(255,255,255,0.1); padding: 4px 0; color: #ffcc00; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 9px;';
        // Время не выводим, только тип и URL
        entry.innerHTML = `<span style="color: #ff4d4d;">[${data.type}]</span> ${data.url}`;
        container.prepend(entry);
        if (container.childNodes.length > maxLogs) container.removeChild(container.lastChild);
    }

    function logBlock(type, url) {
        blockedCount++;
        const time = new Date().toLocaleTimeString();
        let displayUrl = String(url).split('?')[0];
        try {
            const urlObj = new URL(url);
            displayUrl = urlObj.hostname + urlObj.pathname;
        } catch(e) {}
        const entryData = {
            type: type,
            url: displayUrl,
            full: `[${time}] [${type}] ${url}`  // время только здесь
        };
        fullLogHistory.push(entryData.full);

        if (uiShadow) {
            const counter = uiShadow.getElementById('pm-counter');
            if (counter) counter.innerText = blockedCount;
            const logContainer = uiShadow.getElementById('pm-log-list');
            if (logContainer) {
                addEntryToLog(logContainer, entryData);
                return;
            }
        }
        pendingLogs.push(entryData);
    }

    // --- СЕТЕВЫЕ ПЕРЕХВАТЫ (как в 15.6) ---
    if (CONFIG.BLOCK_FETCH_XHR) {
        window.fetch = new Proxy(window.fetch, {
            apply(target, thisArg, args) {
                const url = typeof args[0] === 'object' ? args[0].url : args[0];
                if (shouldBlock(url)) {
                    logBlock('FETCH', url);
                    return Promise.resolve(new Response('{"status":"ok"}', { status: 200 }));
                }
                return Reflect.apply(target, thisArg, args);
            }
        });
        makeNative(window, 'fetch');

        const origOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            this._isTracker = shouldBlock(url);
            this._blockUrl = url;
            return origOpen.apply(this, arguments);
        };
        makeNative(XMLHttpRequest.prototype, 'open');

        const origSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = function() {
            if (this._isTracker) {
                logBlock('XHR', this._blockUrl);
                setTimeout(() => {
                    Object.defineProperty(this, 'readyState', { value: 4 });
                    Object.defineProperty(this, 'status', { value: 200 });
                    Object.defineProperty(this, 'responseText', { value: '{"status":"ok"}' });
                    this.dispatchEvent(new Event('load'));
                    this.dispatchEvent(new Event('readystatechange'));
                }, 1);
                return;
            }
            return origSend.apply(this, arguments);
        };
        makeNative(XMLHttpRequest.prototype, 'send');
    }

    if (CONFIG.BLOCK_WEBSOCKET_OPCODE5) {
        const origWSSend = WebSocket.prototype.send;
        WebSocket.prototype.send = function(data) {
            if (typeof data === 'string' && data.includes('"opcode"')) {
                try {
                    const msg = JSON.parse(data);
                    if (msg.opcode === 5) {
                        logBlock('WS-TELE', `opcode 5 blocked`);
                        return;
                    }
                } catch (e) {}
            }
            return origWSSend.apply(this, arguments);
        };
        makeNative(WebSocket.prototype, 'send');
    }

    if (CONFIG.BLOCK_BEACON) {
        const origBeacon = navigator.sendBeacon;
        navigator.sendBeacon = function(url, data) {
            if (shouldBlock(url)) {
                logBlock('BEACON', url);
                return true;
            }
            return origBeacon.call(this, url, data);
        };
        makeNative(navigator, 'sendBeacon');
    }

    // --- ЗАЩИТА ОТ ФИНГЕРПРИНТИНГА ---
    try {
        Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 });
        Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });

        if (CONFIG.PROTECT_CANVAS) {
            const orgGetImageData = CanvasRenderingContext2D.prototype.getImageData;
            CanvasRenderingContext2D.prototype.getImageData = function(x, y, w, h) {
                const imageData = orgGetImageData.call(this, x, y, w, h);
                const data = imageData.data;
                if (data.length > 0) {
                    data[0] = Math.min(255, Math.max(0, data[0] + (Math.random() > 0.5 ? 1 : -1)));
                }
                return imageData;
            };
        }

        if (CONFIG.PROTECT_CANVAS_TO_DATA_URL && CONFIG.PROTECT_CANVAS) {
            const orgToDataURL = HTMLCanvasElement.prototype.toDataURL;
            HTMLCanvasElement.prototype.toDataURL = function(type, quality) {
                const canvas = this;
                const ctx = canvas.getContext('2d');
                if (ctx && canvas.width > 0 && canvas.height > 0) {
                    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                    ctx.putImageData(imageData, 0, 0);
                }
                return orgToDataURL.call(this, type, quality);
            };
            makeNative(HTMLCanvasElement.prototype, 'toDataURL');
        }

        if (CONFIG.PROTECT_AUDIO && window.AudioContext) {
            const originalGetChannelData = AudioBuffer.prototype.getChannelData;
            AudioBuffer.prototype.getChannelData = function(channel) {
                const originalData = originalGetChannelData.call(this, channel);
                const noisyData = new Float32Array(originalData.length);
                for (let i = 0; i < originalData.length; i++) {
                    noisyData[i] = originalData[i] + (Math.random() * 0.00001 - 0.000005);
                }
                return noisyData;
            };
            makeNative(AudioBuffer.prototype, 'getChannelData');
        }

        if (CONFIG.BLOCK_WEBRTC && window.RTCPeerConnection) {
            const originalRTCPeerConnection = window.RTCPeerConnection;
            window.RTCPeerConnection = function(config) {
                config = config || {};
                config.iceServers = [];
                config.iceTransportPolicy = 'relay';
                const pc = new originalRTCPeerConnection(config);
                pc.addIceCandidate = function() {
                    return Promise.resolve();
                };
                return pc;
            };
            window.RTCPeerConnection.prototype = originalRTCPeerConnection.prototype;
        }

        if (CONFIG.HIDE_CONNECTION && 'connection' in navigator) {
            const connection = navigator.connection;
            if (connection) {
                Object.defineProperty(connection, 'effectiveType', { get: () => '4g' });
                Object.defineProperty(connection, 'downlink', { get: () => 10 });
                Object.defineProperty(connection, 'rtt', { get: () => 50 });
            }
        }

        if (CONFIG.HIDE_WEBDRIVER && navigator.webdriver !== undefined) {
            Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
        }

        if (CONFIG.FAKE_PLUGINS && navigator.plugins) {
            const fakePlugins = [
                { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
                { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
                { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }
            ];
            const pluginArray = Object.create(PluginArray.prototype);
            pluginArray.length = fakePlugins.length;
            fakePlugins.forEach((p, i) => {
                pluginArray[i] = p;
                pluginArray[p.name] = p;
            });
            pluginArray.item = (index) => pluginArray[index];
            pluginArray.namedItem = (name) => fakePlugins.find(p => p.name === name) || null;
            Object.defineProperty(pluginArray, Symbol.toStringTag, { value: 'PluginArray', configurable: true, writable: false });
            Object.defineProperty(navigator, 'plugins', { get: () => pluginArray, configurable: true });

            const fakeMimes = [
                { type: 'application/pdf', suffixes: 'pdf', description: '' },
                { type: 'text/pdf', suffixes: 'pdf', description: '' }
            ];
            const mimeArray = Object.create(MimeTypeArray.prototype);
            mimeArray.length = fakeMimes.length;
            fakeMimes.forEach((m, i) => {
                mimeArray[i] = m;
                mimeArray[m.type] = m;
            });
            mimeArray.item = (index) => mimeArray[index];
            mimeArray.namedItem = (type) => fakeMimes.find(m => m.type === type) || null;
            Object.defineProperty(mimeArray, Symbol.toStringTag, { value: 'MimeTypeArray', configurable: true, writable: false });
            Object.defineProperty(navigator, 'mimeTypes', { get: () => mimeArray, configurable: true });
        }

        if (CONFIG.LOG_SERVICE_WORKER && navigator.serviceWorker && navigator.serviceWorker.register) {
            const originalRegister = navigator.serviceWorker.register;
            navigator.serviceWorker.register = function(scriptURL, options) {
                console.log("📦 Service Worker registered:", scriptURL);
                return originalRegister.call(this, scriptURL, options);
            };
            makeNative(navigator.serviceWorker, 'register');
        }

    } catch (e) {}

    // --- ИНТЕРФЕЙС В SHADOW DOM С ТЁМНЫМ СКРОЛЛОМ ---
    function createUI() {
        if (document.getElementById('privacy-monitor-shadow-host')) return;

        const host = document.createElement('div');
        host.id = 'privacy-monitor-shadow-host';
        host.style.cssText = 'all: initial; display: block;';
        (document.body || document.documentElement).appendChild(host);

        const shadow = host.attachShadow({ mode: 'open' });
        uiShadow = shadow;

        // Стили для тёмного скролла
        const style = document.createElement('style');
        style.textContent = `
            #pm-log-list::-webkit-scrollbar {
                width: 6px;
                height: 6px;
            }
            #pm-log-list::-webkit-scrollbar-track {
                background: rgba(255, 255, 255, 0.05);
                border-radius: 3px;
            }
            #pm-log-list::-webkit-scrollbar-thumb {
                background: rgba(255, 255, 255, 0.2);
                border-radius: 3px;
            }
            #pm-log-list::-webkit-scrollbar-thumb:hover {
                background: rgba(255, 255, 255, 0.3);
            }
        `;
        shadow.appendChild(style);

        const main = document.createElement('div');
        main.id = 'privacy-monitor';
        Object.assign(main.style, {
            position: 'fixed', bottom: '15px', right: '20px', zIndex: '2147483647',
            background: 'rgba(15, 15, 15, 0.7)', color: '#00ff00', padding: '10px 14px',
            borderRadius: '10px', fontSize: '11px', fontFamily: 'monospace',
            boxShadow: '0 8px 32px rgba(0,0,0,0.5)', backdropFilter: 'blur(10px)', pointerEvents: 'auto'
        });

        main.innerHTML = `
            <div id="pm-header" style="display: flex; justify-content: space-between; align-items: center; min-width: 130px; cursor: pointer;">
                <span>🪬 BLINDER: <span id="pm-counter">${blockedCount}</span></span>
                <span id="pm-arrow">▲</span>
            </div>
            <div id="pm-content" style="display: none; margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.2); padding-top: 8px; width: 260px;">
                <div id="pm-log-list" style="max-height: 150px; overflow-y: auto; margin-bottom: 8px;"></div>
                <div style="display: flex; gap: 6px;">
                    <button id="pm-copy" style="flex: 1; background: rgba(50,50,50,0.8); color: #0f0; border: none; padding: 2px 4px; cursor: pointer; border-radius: 3px; font-size: 9px; height: 18px;">Copy log</button>
                    <button id="pm-clear" style="flex: 1; background: rgba(50,50,50,0.8); color: #f44; border: none; padding: 2px 4px; cursor: pointer; border-radius: 3px; font-size: 9px; height: 18px;">Clear log</button>
                </div>
            </div>
        `;

        main.querySelector('#pm-header').onclick = () => {
            const content = main.querySelector('#pm-content');
            const arrow = main.querySelector('#pm-arrow');
            const isOpen = content.style.display === 'block';
            content.style.display = isOpen ? 'none' : 'block';
            arrow.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(180deg)';
        };

        // Кнопка копирования с обратной связью
        main.querySelector('#pm-copy').onclick = async (e) => {
            e.stopPropagation();
            const btn = e.target;
            const originalText = btn.innerText;
            try {
                await navigator.clipboard.writeText(fullLogHistory.join('\n'));
                btn.innerText = 'Copied!';
                setTimeout(() => btn.innerText = originalText, 1000);
            } catch (err) {
                console.warn('Clipboard API failed, trying fallback...', err);
                try {
                    const textarea = document.createElement('textarea');
                    textarea.value = fullLogHistory.join('\n');
                    document.body.appendChild(textarea);
                    textarea.select();
                    document.execCommand('copy');
                    document.body.removeChild(textarea);
                    btn.innerText = 'Copied!';
                    setTimeout(() => btn.innerText = originalText, 1000);
                } catch (fallbackErr) {
                    console.error('Fallback copy failed:', fallbackErr);
                    btn.innerText = 'Error!';
                    setTimeout(() => btn.innerText = originalText, 1500);
                }
            }
        };

        main.querySelector('#pm-clear').onclick = (e) => {
            e.stopPropagation();
            blockedCount = 0;
            fullLogHistory = [];
            pendingLogs = [];
            main.querySelector('#pm-counter').innerText = '0';
            main.querySelector('#pm-log-list').innerHTML = '';
        };

        shadow.appendChild(main);

        // Добавляем накопленные записи
        const logContainer = shadow.getElementById('pm-log-list');
        while (pendingLogs.length > 0) {
            addEntryToLog(logContainer, pendingLogs.shift());
        }
        shadow.getElementById('pm-counter').innerText = blockedCount;
    }

    setInterval(createUI, 1500);
})();