Twitter/X: Block, Download & Not Interested (Stable Fix 3.1)

Версия 3.1: Исправлено вертикальное смещение кнопок (выравнивание по Flexbox).

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Twitter/X: Block, Download & Not Interested (Stable Fix 3.1)
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Версия 3.1: Исправлено вертикальное смещение кнопок (выравнивание по Flexbox).
// @author       Expert Dev & Gemini
// @match        https://twitter.com/*
// @match        https://x.com/*
// @icon         https://abs.twimg.com/favicons/twitter.ico
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const ICONS = {
        BLOCK: '⛔',
        SOFT_BAN: '👎',
        DOWNLOAD: '🎬',
        LOADING: '⏳',
        DONE: '✅',
        ERROR: '❌'
    };

    const KEYWORDS = {
        notInterested: ['не интересна', 'Not interested', 'не цікавить', 'No me interesa'],
        block: ['Внести', 'Block', 'Заблокувати', 'Bloquear']
    };

    const BTN_STYLE = `
        margin-right: 4px;
        cursor: pointer;
        font-size: 16px;
        opacity: 0.7;
        transition: transform 0.1s, opacity 0.2s;
        border: none;
        background: transparent;
        padding: 4px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        z-index: 10000;
    `;

    const sleep = ms => new Promise(r => setTimeout(r, ms));

    // --- REACT UTILS ---
    function getReactProps(dom) {
        const key = Object.keys(dom).find(key => key.startsWith("__reactFiber"));
        return key ? dom[key] : null;
    }

    function findTweetData(node, depth = 0) {
        if (!node || depth > 25) return null;
        const props = node.memoizedProps;
        if (props) {
            if (props.tweet) return props.tweet;
            if (props.data?.tweetResult?.result) return props.data.tweetResult.result;
            if (props.item?.itemContent?.tweet_results?.result) return props.item.itemContent.tweet_results.result;
        }
        return findTweetData(node.return, depth + 1);
    }

    function getCombinedTweetData(tweetNode) {
        let fiber = getReactProps(tweetNode);
        let data = findTweetData(fiber);
        if (data) return data;

        const timeNode = tweetNode.querySelector('time');
        if (timeNode) {
            fiber = getReactProps(timeNode);
            data = findTweetData(fiber);
        }
        return data;
    }

    function getMediaUrl(tweetRawData) {
        if (!tweetRawData) return null;
        const legacy = tweetRawData.legacy || (tweetRawData.tweet && tweetRawData.tweet.legacy) || tweetRawData;
        if (!legacy?.extended_entities?.media) return null;

        const media = legacy.extended_entities.media.find(m => m.type === 'video' || m.type === 'animated_gif');
        if (!media) return null;

        let best = null;
        let maxBr = -1;
        media.video_info?.variants?.forEach(v => {
            if (v.content_type === 'video/mp4' && v.bitrate > maxBr) {
                maxBr = v.bitrate;
                best = v.url;
            }
        });
        return best;
    }

    // --- ACTIONS ---
    async function handleDownload(url, btn) {
        const originalIcon = btn.innerHTML;
        btn.innerHTML = ICONS.LOADING;
        try {
            const response = await fetch(url);
            const blob = await response.blob();
            const blobUrl = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = blobUrl;
            a.download = `twitter_video_${Date.now()}.mp4`;
            document.body.appendChild(a);
            a.click();
            window.URL.revokeObjectURL(blobUrl);
            document.body.removeChild(a);
            btn.innerHTML = ICONS.DONE;
        } catch (e) {
            window.open(url, '_blank');
            btn.innerHTML = '↗️';
        }
        setTimeout(() => btn.innerHTML = originalIcon, 2000);
    }

    function findMenuOptionByText(actionType) {
        const layers = document.querySelector('#layers') || document;
        const items = layers.querySelectorAll('[role="menuitem"]');
        const words = KEYWORDS[actionType];
        for (let item of items) {
            const text = item.innerText || item.textContent;
            if (words.some(word => text.includes(word))) return item;
        }
        return null;
    }

    async function clickMenuOption(tweetNode, btn, actionType) {
        const originalIcon = btn.innerHTML;
        btn.innerHTML = ICONS.LOADING;
        const testIdMap = { 'notInterested': 'notInterested', 'block': 'block' };

        try {
            const caret = tweetNode.querySelector('[data-testid="caret"]');
            if (!caret) throw new Error("Caret missing");
            caret.click();

            let attempts = 0;
            let option = null;

            while (attempts < 40) {
                await sleep(50);
                const layers = document.querySelector('#layers') || document;
                option = layers.querySelector(`[data-testid="${testIdMap[actionType]}"]`);
                if (!option) option = findMenuOptionByText(actionType);
                if (option) break;
                attempts++;
            }

            if (!option) {
                caret.click();
                throw new Error("Option missing");
            }

            await sleep(50);
            option.click();

            if (actionType === 'block') {
                let confirmAttempts = 0;
                let confirmBtn = null;
                while (confirmAttempts < 20) {
                    await sleep(50);
                    const layers = document.querySelector('#layers') || document;
                    confirmBtn = layers.querySelector('[data-testid="confirmationSheetConfirm"]');
                    if (confirmBtn) break;
                    confirmAttempts++;
                }
                if (confirmBtn) {
                    await sleep(50);
                    confirmBtn.click();
                }
            }

            tweetNode.style.opacity = '0.2';
            tweetNode.style.filter = 'grayscale(100%)';
            tweetNode.style.pointerEvents = 'none';
            btn.innerHTML = ICONS.DONE;

        } catch (e) {
            btn.innerHTML = ICONS.ERROR;
            await sleep(2000);
            btn.innerHTML = originalIcon;
        }
    }

    // --- UI INJECTION ---
    function injectButtons(tweetNode) {
        const caretSvg = tweetNode.querySelector('[data-testid="caret"]');
        if (!caretSvg) return;

        const menuBtn = caretSvg.closest('[role="button"]');
        if (!menuBtn) return;

        const menuBtnWrapper = menuBtn.parentNode;
        if (!menuBtnWrapper || !menuBtnWrapper.parentNode) return;

        if (tweetNode.querySelector('.xtools-container')) return;
        tweetNode.dataset.xtoolsInjected = "true";

        // Ключевое исправление: превращаем родителя в строку (row)
        const parentContainer = menuBtnWrapper.parentNode;
        parentContainer.style.display = 'flex';
        parentContainer.style.flexDirection = 'row';
        parentContainer.style.alignItems = 'center';

        const toolsDiv = document.createElement('div');
        toolsDiv.className = 'xtools-container';
        toolsDiv.style.cssText = 'display: flex; flex-direction: row; align-items: center; justify-content: flex-end;';

        const btnSoft = document.createElement('button');
        btnSoft.innerHTML = ICONS.SOFT_BAN;
        btnSoft.title = "Не интересно";
        btnSoft.style.cssText = BTN_STYLE;
        btnSoft.onclick = (e) => { e.preventDefault(); e.stopPropagation(); clickMenuOption(tweetNode, btnSoft, 'notInterested'); };
        toolsDiv.appendChild(btnSoft);

        const btnBlock = document.createElement('button');
        btnBlock.innerHTML = ICONS.BLOCK;
        btnBlock.title = "Блокировать";
        btnBlock.style.cssText = BTN_STYLE;
        btnBlock.onclick = (e) => { e.preventDefault(); e.stopPropagation(); clickMenuOption(tweetNode, btnBlock, 'block'); };
        toolsDiv.appendChild(btnBlock);

        const tweetData = getCombinedTweetData(tweetNode);
        const videoUrl = getMediaUrl(tweetData);

        if (videoUrl) {
            const btnDl = document.createElement('button');
            btnDl.innerHTML = ICONS.DOWNLOAD;
            btnDl.title = "Скачать видео";
            btnDl.style.cssText = BTN_STYLE + 'color: #1d9bf0;';
            btnDl.onclick = (e) => { e.preventDefault(); e.stopPropagation(); handleDownload(videoUrl, btnDl); };
            toolsDiv.appendChild(btnDl);
        }

        parentContainer.insertBefore(toolsDiv, menuBtnWrapper);
    }

    // --- ENGINE ---
    setInterval(() => {
        const unprocessedTweets = document.querySelectorAll('article[data-testid="tweet"]:not([data-xtools-injected="true"])');
        for (let i = 0; i < unprocessedTweets.length; i++) {
            injectButtons(unprocessedTweets[i]);
        }

        const injectedTweets = document.querySelectorAll('article[data-testid="tweet"][data-xtools-injected="true"]');
        for (let i = 0; i < injectedTweets.length; i++) {
            if (!injectedTweets[i].querySelector('.xtools-container')) {
                injectedTweets[i].removeAttribute('data-xtools-injected');
            }
        }
    }, 1000);

})();