Фильтрация ленты в ИТД
// ==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 });
})();