ITD Feed Filter

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 });

})();