Remove ads and promoted tweets on Twitter

Removes ads and promoted tweets on Twitter, as well as the large bold "Promoted Tweet" headings

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Remove ads and promoted tweets on Twitter
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Removes ads and promoted tweets on Twitter, as well as the large bold "Promoted Tweet" headings
// @author       https://greasyfork.org/en/users/728793-keyboard-shortcuts
// @match        https://twitter.com/*
// @match        https://*.twitter.com/*
// @match        https://x.com/*
// @match        https://*.x.com/*
// @icon         https://www.google.com/s2/favicons?sz=128&domain=twitter.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    /***************** CONFIGURATION STARTS HERE *****************/
    const FIND_AND_REMOVE_FREQUENCY_MS = 500; // how often to search for promoted tweets to remove them, in milliseconds (by default 500, to do it twice a second)
    const LOG_REMOVALS = false; // set to true if you want the removals to be reported in the web developer console (not usually visible unless you open it yourself)
    /****************** CONFIGURATION ENDS HERE ******************/

    const PROMOTED_ARTICLE_PATH = '//span[text()="Promoted"]/ancestor::article[@aria-labelledby]'; // xPath query to find promoted tweets, specifically their <article> DOM node
    const PROMOTED_TWEET_HEADING_PATH = '//span[text()="Promoted Tweet"]/parent::div/parent::h2/parent::div'; // Finds "<div><h2><div><span>Promoted tweet</span>…", selects the outer <div>.
    const PROMOTED_TREND_PATH = '//span[starts-with(text(),"Promoted by")]/ancestor::div[@aria-labelledby]'; // xPath query to find promoted trending topics, specifically their <div> DOM node
    const ADS_ARTICLE_PATH = '//span[text()="Ad"]/ancestor::article[@aria-labelledby]'; // xPath query to find advertising tweets, specifically their <article> DOM node

    /**
     * For an <article> element, take its list of IDs in aria-labelledby and return the DOM nodes with these IDs
     * e.g. for <article aria-labelledby="foo bar">, return the nodes with id="foo" and id="bar".
     */
    function getLabelNodes(article) {
        const labelledBy = article.getAttribute('aria-labelledby'); // space-separated node IDs
        const selector = labelledBy.split(' ').map(id => '#' + id).join(', '); // convert to a selector like '#foo, #bar'
        return Array.from(document.querySelectorAll(selector)); // returns the list of nodes found with these IDs
    }

    /** Returns whether one of the "label nodes" this article is labeled by has the text "Promoted" as its contents **/
    function isPromotedArticle(article) {
        return getLabelNodes(article)
            .some(node => ('' + node.textContent).toLowerCase() === 'promoted');
    }

    /** Returns whether the article contains a span with only the text "Ad" **/
    function isAdvertisement(article) {
        return Array.from(article.querySelectorAll('span'))
            .some(node => ('' + node.textContent).toLowerCase() === 'ad');
    }

    /** Runs an XPath query with a snapshot of the results, returns an array of the resulting elements **/
    function xPathQuerySnapshot(path) {
        const queryResult = document.evaluate(path, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
        return [...Array(queryResult.snapshotLength).keys()] // [0,1,2… N-1]
            .map(i => queryResult.snapshotItem(i));
    }

    /** Remove element and log reason, if needed **/
    function removeAndMaybeLog(element, logMessage) {
        if (LOG_REMOVALS) {
            console.log('Removing ' + logMessage);
        }
        element.style.display = 'none';
    }

    /** Finds and removes promoted tweets and the headings above these tweets **/
    function findAndRemoveUnwantedTweets() {
        xPathQuerySnapshot(PROMOTED_ARTICLE_PATH)
            .filter(article => isPromotedArticle(article))
            .forEach(article => removeAndMaybeLog(article, 'promoted tweet:' + article.textContent));

        xPathQuerySnapshot(ADS_ARTICLE_PATH)
            .filter(article => isAdvertisement(article))
            .forEach(article => removeAndMaybeLog(article, 'advertisement:' + article.textContent));

        xPathQuerySnapshot(PROMOTED_TWEET_HEADING_PATH)
            .forEach(div => removeAndMaybeLog(div, 'promoted tweet heading'));

        xPathQuerySnapshot(PROMOTED_TREND_PATH)
            .forEach(div => removeAndMaybeLog(div, 'promoted trending topic:' + div.textContent));
    }

    findAndRemoveUnwantedTweets(); // do it once when the page loads
    setInterval(findAndRemoveUnwantedTweets, FIND_AND_REMOVE_FREQUENCY_MS); // and then at regular intervals after that
})();