ITD Feed Filter

Фильтрация ленты в ИТД

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         ITD Feed Filter
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Фильтрация ленты в ИТД
// @author       0wn3df1x
// @license      MIT
// @icon         https://xn--d1ah4a.com/favicon.ico
// @match        *://*.xn--d1ah4a.com/*
// @match        *://*.итд.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const customStyles = document.createElement('style');
    customStyles.innerHTML = `
        #itd-filter-list::-webkit-scrollbar { width: 6px; }
        #itd-filter-list::-webkit-scrollbar-track { background: transparent; }
        #itd-filter-list::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.3); border-radius: 3px; }
        #itd-filter-list::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
    `;
    document.head.appendChild(customStyles);

    // Функция для извлечения всего полезного текста из объекта поста
    function extractTextPayload(p, elements) {
        if (!p) return;
        if (p.content) elements.push(p.content);
        if (p.author) {
            if (p.author.username) elements.push(p.author.username);
            if (p.author.displayName) elements.push(p.author.displayName);
        }
        if (p.dominantEmoji) elements.push(p.dominantEmoji);

        if (p.spans && Array.isArray(p.spans)) {
            p.spans.forEach(span => {
                if (span.tag) {
                    elements.push(span.tag);
                    if (span.type === 'hashtag') elements.push('#' + span.tag);
                }
            });
        }
    }

    function checkSpam(post, words) {
        if (!post) return false;

        let elements = [];

        // Проверяем сам пост
        extractTextPayload(post, elements);

        // Если это репост, обязательно проверяем и оригинальный пост
        if (post.originalPost) {
            extractTextPayload(post.originalPost, elements);
        }

        const textPayload = elements.join(' ').toLowerCase();
        return words.some(w => textPayload.includes(w));
    }

    function getActiveWords() {
        return GM_getValue('itd_filter_words', []).map(w => w.toLowerCase());
    }

    function isProfilePage() {
        return window.location.pathname.startsWith('/@');
    }

    const _window = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

    // --- 1. ПЕРЕХВАТ FETCH ---
    const origFetch = _window.fetch;
    _window.fetch = async function(...args) {
        const urlStr = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url ? args[0].url : '');

        if (urlStr.includes('/api/posts') && (!args[1] || args[1].method === 'GET' || !args[1].method)) {
            const filterActive = GM_getValue('itd_filter_active', true);
            const words = getActiveWords();

            if (!filterActive || words.length === 0 || isProfilePage()) {
                return origFetch.apply(this, args);
            }

            try {
                let response = await origFetch.apply(this, args);
                if (!response.ok) return response;

                let json = await response.clone().json();

                if (json && json.data && json.data.posts) {
                    json.data.posts = json.data.posts.filter(p => !checkSpam(p, words));

                    let maxRecursion = 5;
                    while (json.data.posts.length < 5 && json.data.pagination && json.data.pagination.hasMore && maxRecursion > 0) {
                        maxRecursion--;

                        const nextCursor = json.data.pagination.nextCursor;
                        if (!nextCursor) break;

                        const nextUrl = new URL(urlStr, _window.location.origin);
                        nextUrl.searchParams.set('cursor', nextCursor);

                        const nextArgs = [...args];
                        nextArgs[0] = nextUrl.toString();

                        const nextRes = await origFetch.apply(this, nextArgs);
                        if (!nextRes.ok) break;

                        const nextJson = await nextRes.json();
                        if (nextJson && nextJson.data && nextJson.data.posts) {
                            const cleanNextPosts = nextJson.data.posts.filter(p => !checkSpam(p, words));
                            json.data.posts = json.data.posts.concat(cleanNextPosts);
                            json.data.pagination = nextJson.data.pagination;
                        } else {
                            break;
                        }
                    }

                    const newHeaders = new Headers(response.headers);
                    newHeaders.delete('content-length');

                    return new Response(JSON.stringify(json), {
                        status: response.status,
                        statusText: response.statusText,
                        headers: newHeaders
                    });
                }
            } catch (e) {
                console.error("[ITD Filter] Error in Ghost Fetch:", e);
            }
        }
        return origFetch.apply(this, args);
    };

    // --- 2. ПЕРЕХВАТ JSON.PARSE ---
    const origParse = _window.JSON.parse;
    _window.JSON.parse = function(text, reviver) {
        const data = origParse(text, reviver);
        try {
            const filterActive = GM_getValue('itd_filter_active', true);
            const words = getActiveWords();
            if (filterActive && words.length > 0 && data && data.data && Array.isArray(data.data.posts) && !isProfilePage()) {
                data.data.posts = data.data.posts.filter(p => !checkSpam(p, words));
            }
        } catch(e) {}
        return data;
    };

    // --- 3. ВСТРАИВАНИЕ КНОПКИ-ШЕСТЕРЕНКИ ---
    function injectTools() {
        const panel = document.querySelector('.yhENW-7a');
        if (!panel) return;

        const tabs = Array.from(panel.querySelectorAll('button.DPhGAlZS'));
        const forYouBtn = tabs.find(btn => btn.textContent.trim().includes('Для вас'));

        if (forYouBtn && !forYouBtn.querySelector('.itd-filter-btn')) {
            const toolsContainer = document.createElement('span');
            toolsContainer.className = 'itd-filter-btn';
            toolsContainer.style.cssText = `
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 28px;
                height: 28px;
                margin-right: 8px;
                z-index: 10;
                transform: translateY(3px);
                background: var(--bg-tertiary);
                color: var(--text-secondary);
                border-radius: 50%;
                cursor: pointer;
                transition: background 0.2s ease, color 0.2s ease;
            `;

            toolsContainer.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <circle cx="12" cy="12" r="3"></circle>
                    <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
                </svg>
            `;

            toolsContainer.onmouseover = () => {
                toolsContainer.style.background = 'var(--bg-hover)';
                toolsContainer.style.color = 'var(--text-primary)';
            };
            toolsContainer.onmouseout = () => {
                toolsContainer.style.background = 'var(--bg-tertiary)';
                toolsContainer.style.color = 'var(--text-secondary)';
            };

            forYouBtn.insertBefore(toolsContainer, forYouBtn.firstChild);

            toolsContainer.addEventListener('click', (e) => {
                e.stopPropagation();
                e.preventDefault();
                openModal();
            });
        }
    }

    // --- 4. МОДАЛЬНОЕ ОКНО ---
    function openModal() {
        if (document.getElementById('itd-native-filter-modal')) return;

        let initialWords = JSON.stringify(GM_getValue('itd_filter_words', []));
        let initialActive = GM_getValue('itd_filter_active', true);

        const overlay = document.createElement('div');
        overlay.id = 'itd-native-filter-modal';
        overlay.style.cssText = `
            position: fixed; inset: 0; z-index: 10000;
            display: flex; align-items: center; justify-content: center;
            background: rgba(0, 0, 0, 0.6);
            backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
        `;

        const modal = document.createElement('div');
        modal.style.cssText = `
            background: var(--block-bg);
            color: var(--text-primary);
            padding: 24px;
            border-radius: 24px;
            width: 420px;
            max-width: 90vw;
            box-shadow: var(--shadow-elevated);
            display: flex; flex-direction: column; gap: 20px;
            max-height: 85vh;
            border: 1px solid var(--border-color);
        `;

        modal.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center;">
                <h2 style="font-size: 20px; font-weight: 600; margin: 0; color: var(--text-primary);">Настройки фильтра</h2>

                <label style="display: flex; align-items: center; gap: 12px; cursor: pointer; font-size: 14px; color: var(--text-secondary); font-weight: 500;">
                    Вкл
                    <div id="itd-toggle-ui" style="position: relative; width: 44px; height: 24px; background: ${initialActive ? 'var(--toggle-active-bg)' : 'var(--bg-tertiary)'}; border-radius: 12px; transition: background 0.2s;">
                        <div id="itd-toggle-knob" style="position: absolute; top: 2px; left: ${initialActive ? '22px' : '2px'}; width: 20px; height: 20px; background: var(--bg-primary); border-radius: 50%; transition: left 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"></div>
                    </div>
                    <input type="checkbox" id="itd-filter-toggle" ${initialActive ? 'checked' : ''} style="display: none;">
                </label>
            </div>

            <div style="display: flex; gap: 8px;">
                <input type="text" id="itd-filter-input" placeholder="Хештег или слово..."
                    style="flex-grow: 1; padding: 12px 16px; border-radius: 12px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); outline: none; font-size: 14px; transition: border 0.2s;">
                <button id="itd-filter-add"
                    style="padding: 0 20px; background: var(--accent-primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-weight: 600; font-size: 18px; transition: opacity 0.2s;">+</button>
            </div>

            <div id="itd-filter-list" style="overflow-y: auto; border: 1px solid var(--border-color); border-radius: 12px; padding: 8px; background: var(--bg-secondary); flex-grow: 1; min-height: 150px; max-height: 250px; display: flex; flex-direction: column; gap: 4px;"></div>

            <div style="display: flex; gap: 8px;">
                <button id="itd-filter-export" style="flex: 1; padding: 10px; background: var(--bg-tertiary); color: var(--text-primary); border: none; border-radius: 12px; cursor: pointer; font-weight: 500; font-size: 13px; transition: background 0.2s;">Экспорт</button>
                <button id="itd-filter-import" style="flex: 1; padding: 10px; background: var(--bg-tertiary); color: var(--text-primary); border: none; border-radius: 12px; cursor: pointer; font-weight: 500; font-size: 13px; transition: background 0.2s;">Импорт</button>
                <button id="itd-filter-clear"  style="flex: 1; padding: 10px; background: rgba(239, 68, 68, 0.1); color: #ef4444; border: none; border-radius: 12px; cursor: pointer; font-weight: 500; font-size: 13px; transition: background 0.2s;">Очистить</button>
            </div>

            <textarea id="itd-filter-io" placeholder="Вставьте список через точку с запятой (;)"
                style="width: 100%; height: 80px; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 12px; padding: 12px; display: none; resize: vertical; font-size: 13px; outline: none;"></textarea>

            <button id="itd-filter-close"
                style="width: 100%; padding: 12px; background: var(--text-primary); color: var(--bg-primary); border: none; border-radius: 16px; cursor: pointer; font-weight: 600; font-size: 15px; margin-top: 8px; transition: opacity 0.2s;">Готово</button>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        const addHover = (id) => {
            const el = document.getElementById(id);
            if(el) {
                el.onmouseover = () => el.style.opacity = '0.8';
                el.onmouseout = () => el.style.opacity = '1';
            }
        };
        addHover('itd-filter-add'); addHover('itd-filter-export'); addHover('itd-filter-import'); addHover('itd-filter-clear'); addHover('itd-filter-close');

        const inputEl = document.getElementById('itd-filter-input');
        inputEl.onfocus = () => inputEl.style.borderColor = 'var(--accent-primary)';
        inputEl.onblur = () => inputEl.style.borderColor = 'var(--border-color)';

        let filterWords = GM_getValue('itd_filter_words', []);
        const listContainer = document.getElementById('itd-filter-list');
        const ioArea = document.getElementById('itd-filter-io');
        const toggleCheckbox = document.getElementById('itd-filter-toggle');
        const toggleUi = document.getElementById('itd-toggle-ui');
        const toggleKnob = document.getElementById('itd-toggle-knob');

        function saveState() {
            GM_setValue('itd_filter_words', filterWords);
            GM_setValue('itd_filter_active', toggleCheckbox.checked);
        }

        function renderList() {
            listContainer.innerHTML = '';
            if (filterWords.length === 0) {
                listContainer.innerHTML = '<div style="color: var(--text-tertiary); text-align: center; padding: 30px 10px; font-size: 13px;">Список пуст</div>';
                return;
            }
            filterWords.forEach((word, index) => {
                const item = document.createElement('div');
                item.style.cssText = `
                    display: flex; justify-content: space-between; align-items: center;
                    padding: 8px 12px; background: var(--block-bg);
                    border: 1px solid var(--border-color); border-radius: 8px;
                `;

                const textSpan = document.createElement('span');
                textSpan.textContent = word;
                textSpan.style.cssText = 'word-break: break-word; font-size: 14px; font-weight: 500; color: var(--text-primary);';

                const removeBtn = document.createElement('button');
                removeBtn.innerHTML = `
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"></path></svg>
                `;
                removeBtn.style.cssText = `
                    background: transparent; color: var(--text-tertiary); border: none;
                    border-radius: 50%; width: 24px; height: 24px; cursor: pointer;
                    display: flex; justify-content: center; align-items: center; transition: color 0.2s;
                `;
                removeBtn.onmouseover = () => removeBtn.style.color = '#ef4444';
                removeBtn.onmouseout = () => removeBtn.style.color = 'var(--text-tertiary)';

                removeBtn.onclick = () => {
                    filterWords.splice(index, 1);
                    saveState();
                    renderList();
                };

                item.appendChild(textSpan);
                item.appendChild(removeBtn);
                listContainer.appendChild(item);
            });
        }

        toggleCheckbox.onchange = (e) => {
            const isChecked = e.target.checked;
            toggleUi.style.background = isChecked ? 'var(--toggle-active-bg)' : 'var(--bg-tertiary)';
            toggleKnob.style.left = isChecked ? '22px' : '2px';
            saveState();
        };

        document.getElementById('itd-filter-add').onclick = () => {
            const val = inputEl.value.trim().toLowerCase();
            if (val && !filterWords.includes(val)) {
                filterWords.push(val);
                inputEl.value = '';
                saveState();
                renderList();
            }
        };

        inputEl.onkeypress = (e) => { if (e.key === 'Enter') document.getElementById('itd-filter-add').click(); };

        document.getElementById('itd-filter-clear').onclick = () => {
            if (confirm('Очистить фильтр?')) {
                filterWords = [];
                saveState();
                renderList();
            }
        };

        document.getElementById('itd-filter-export').onclick = () => {
            ioArea.style.display = 'block';
            ioArea.value = filterWords.join(';');
            ioArea.select();
        };

        document.getElementById('itd-filter-import').onclick = () => {
            if (ioArea.style.display === 'none' || ioArea.value.trim() === '') {
                ioArea.style.display = 'block';
                ioArea.value = '';
            } else {
                const newWords = ioArea.value.split(';').map(w => w.trim().toLowerCase()).filter(w => w !== '');
                newWords.forEach(w => {
                    if (!filterWords.includes(w)) filterWords.push(w);
                });
                ioArea.value = '';
                ioArea.style.display = 'none';
                saveState();
                renderList();
            }
        };

        function closeModal() {
            saveState();
            const currentWords = JSON.stringify(filterWords);
            const currentActive = toggleCheckbox.checked;

            if (currentWords !== initialWords || currentActive !== initialActive) {
                location.reload();
            } else {
                overlay.remove();
            }
        }

        document.getElementById('itd-filter-close').onclick = closeModal;
        overlay.onclick = (e) => { if (e.target === overlay) closeModal(); };

        renderList();
    }

    const observer = new MutationObserver(() => {
        if (document.body) {
            injectTools();
        }
    });
    observer.observe(document.documentElement, { childList: true, subtree: true });

})();