facebook - ad block v3

Hides sponsored posts in FB's news-feed (Sept 2021)

目前為 2021-09-22 提交的版本,檢視 最新版本

// ==UserScript==
// @name         facebook - ad block v3
// @version      3.08
// @description  Hides sponsored posts in FB's news-feed (Sept 2021)
// @author       zbluebugz
// @match        https://*.facebook.com/*
// @run-at       document-idle
// @namespace https://greasyfork.org/users/812551
// ==/UserScript==
/*
Comments:
    original: https://pastebin.com/raw/vmaiA8jJ
    05/09/2021: FB has changed the way sponsored posts are created - made harder to detect.
    06/09/2021: Detected a couple of slight variations in how sponsored posts are created.
    14/09/2021: Detected another slight variation in how sponsored posts are created.
    14/09/2021: Fixed bug with getting textNode values.
    18/09/2021: Detected another variation on how sponsored posts are created.
    18/09/2021: Added code to detect 'Sponsored · Paid for by XYZ' (very rare - untested)
    21/09/2021: Added Portuguese (requested by a user)
    21/09/2021: Detected another variation on how sponsored posts are created.
    22/09/2021: Detected another variation on how sponsored posts are created.
*/

(function () {
    'use strict';

    const title = 'facebook - ad block';

    // -- START KEYWORDS

    // Keyword: Sponsored posts'
    // - multivalue object;
    // - 'language code' : [ uses 'span' or 'b' elements, sponsored word ]
    // - (language code located in the HTML element)
    // - various non-sponsored keywords ...
    const SPONSORED_LIST = {
        // english
        'en': ['Sponsored', 'People you may know', 'Suggested for you', 'Suggested Pages', 'Suggested Events', 'Paid partnership', 'Suggested groups'],
        // portuguese
        'pt': ['Patrocinado', 'Pessoas que talvez conheças', 'Sugerido para você', 'Páginas sugeridas', 'Eventos Sugeridos', 'Parceria paga', 'Grupos sugeridos']
    }

    // Hide certain promotional posts ...
    const TOGGLE_PEOPLE_YOU_MAY_KNOW = true;
    const TOGGLE_SUGGESTED_CONTENT = true;
    const TOGGLE_SUGGESTED_PAGES = true ;
    const TOGGLE_SUGGESTED_EVENTS = false;
    const TOGGLE_PAID_PARTNERSHIP = true;
    const TOGGLE_SUGGESTED_GROUPS = false;

    // Create Room - uses the data-pagelet attribute for detection. no language text required.
    const TOGGLE_CREATE_ROOM = true;

    // Sponsored paid for ...
    // *** untested as it is very rare ... ***
    const TOGGLE_SPONSORED_PAID_FOR = false;
    // - just match the first few words ...
    const SPONSORED_PAID_FOR_WORDS = 'Sponsored · Paid for by';


    // -- END KEYWORDS
    // -- rest of code - no more keywords to adjust.

    // which language entry to use?
    const SPONSORED_OBJ = SPONSORED_LIST[document.querySelector('html').getAttribute('lang')] || SPONSORED_LIST.en;
    const SPONSORED_WORD = SPONSORED_OBJ[0];

    // Keywords: text to find for certain promotional elements
    let suggestions = [];
    if(TOGGLE_PEOPLE_YOU_MAY_KNOW) 	suggestions.push(SPONSORED_OBJ[1]);
    if(TOGGLE_SUGGESTED_CONTENT) 	suggestions.push(SPONSORED_OBJ[2]);
    if(TOGGLE_SUGGESTED_PAGES) 	 	suggestions.push(SPONSORED_OBJ[3]);
    if(TOGGLE_SUGGESTED_EVENTS) 	suggestions.push(SPONSORED_OBJ[4]);
    if(TOGGLE_PAID_PARTNERSHIP) 	suggestions.push(SPONSORED_OBJ[5]);
    if(TOGGLE_SUGGESTED_GROUPS)	 	suggestions.push(SPONSORED_OBJ[6]);


    // hide or highlight the selected posts
    let HIDE_STYLE = (true) ? 'display:none !important' : 'border:3px solid yellow !important';

    // how often to run this script (milliseconds)
    const CHECK_RATE_MS = 100;

    // sponsored for posts - length of text
    let SPONSORED_PAID_FOR_WORDS_LEN = SPONSORED_PAID_FOR_WORDS.length;

    function hide(el) {
        return el.setAttribute('style',HIDE_STYLE);
    }

    function doChanges() {

        if(TOGGLE_CREATE_ROOM){
            let create_room = document.querySelector('div[data-pagelet="VideoChatHomeUnitNoDDD"]');
            if(create_room) hide(create_room);
        }

        function findSponsoredPosts() {
            // get collection of posts, ignore those already read by this script.
            let posts = Array.from(
                document.querySelectorAll('div[data-pagelet*=FeedUnit]:not([adbpr])')
                // next line for use in debugging - ignores the posts that have been already processed.
                //document.querySelectorAll('div[data-pagelet*=FeedUnit]')
            );
            // loop through each post to see if it is a sponsored one or not
            let hidePosts = [];
            let csr ; // getComputedStyle results
            let ss = 1; // sponsored structure (1 = uses aria-label, 2 = uses a tag.
            let isFlagged = false; // has post been flagged as sponsored?
            let sob = 'S'; // (S)pan or (B)old tags used for holding Sponsored letters/word
            posts.forEach(
                post => {
                    // flag this post as not to be read/processed again
                    post.setAttribute('adbpr', true);
                    // reset isHidden flag
                    isFlagged = false;
                    // within this unread post, find the SPAN element(s) having aria-label = Sponsored
                    // (usually only one is found)
                    let alSpans = Array.from(post.querySelectorAll('span[aria-label="' + SPONSORED_WORD + '"]'));
                    ss = 1;
                    // console.info('----:', SPONSORED_WORD, alSpans.length );
                    if (alSpans.length === 0) {
                        // sigh!  uses the A tag structure; not language dependent.
                        alSpans = Array.from(post.querySelectorAll('a[href="#"][aria-label="label"]'));
                        ss = 2;
                    }
                    if(alSpans.length === 0) {
                        // sigh!  uses the A tag structure; language dependent
                        alSpans = Array.from(post.querySelectorAll('a[aria-label="' + SPONSORED_WORD +'"]'));
                        ss = 3;
                    }
                    // is the word "Sponsored" visible?
                    // - there are several spans having single letters - all randomised, but will make up "sponsored" when certain span tags are "visible".
                    alSpans.forEach(sp => {
                        let daText = '';
                        // get the next sibling from the <span aria-label="Sponsored"></span>
                        let nsp ;
                        if (ss === 1) {
                            // uses the span[arial-label="sponsored] structure
                            nsp = sp.nextSibling;
                            // which set of TAGs used?
                            sob = 'S'; // SPAN
                        }
                        else {
                            // ss = 2 or 3
                            // ss 2 = uses the a[href=# aria-label=label] structure
                            // ss 3 = uses the a[aria-label=SPONSORED_WORD structure
                            //  - A tag is nested with 2 SPANs then either B or SPAN tag wrapper with lots of B/SPAN tags.
                            //  - grab the B/SPAN tag (wrapper)
                            nsp = sp.firstChild.firstChild.firstChild;
                            // which set of TAGS used?
                            sob = (nsp.tagName === 'B') ? 'B' : 'S' ;
                        }
                        // console.info("--- nsp:", nsp)
                        // note that this sibling is a "parent" ...
                        // .. sometimes it has a textNode (as firstChild) ...
                        if (sob === 'B') {
                            // uses the <b> tags structure
                            daText += nsp.innerText;
                            //console.info("--->", daText);
                        }
                        else {
                            // uses the <span> tags structure
                            if (nsp.tagName === "SPAN") {
                                if (nsp.firstChild.tagName === 'SPAN') {
                                    // no immediate textNode
                                    // - nothing to do.
                                }
                                else {
                                    csr = window.getComputedStyle(nsp);
                                    if (csr.position === 'relative' && csr.display === 'inline') {
                                        // visible ... (need both styles)
                                        // - ok, grab the textNode's value.
                                        daText += nsp.firstChild.textContent ;
                                    }
                                }
                            }
                            //console.info("--::", daText);
                            // the "parent" has childNodes (spans) ...
                            nsp = nsp.firstChild;
                            do {
                                if (nsp.tagName === 'SPAN') {
                                    csr = window.getComputedStyle(nsp);
                                    if (csr.position === 'relative' && csr.display === 'inline') {
                                        // visible ... (need both styles)
                                        if (nsp.innerText.length ===1) {
                                            daText += nsp.innerText;
                                        }
                                    }
                                }
                                nsp = nsp.nextSibling;
                            } while (nsp);
                        }
                        // console.info("--is Sponsored post:", daText, (SPONSORED_WORD.indexOf(daText) > -1));
                        //console.info("-----sp: ", sp);
                        // do we hide this post?
                        if (SPONSORED_WORD.indexOf(daText) > -1 ) {
                            hidePosts.push(sp);
                            isFlagged = true;
                        }
                    });

                    if (!isFlagged) {
                        // post not yet marked to be hidden ...
                        // .. so check for suggestions.
                        if (suggestions) {
                            // scan the a tags
                            let els = Array.from(post.querySelectorAll('a'));
                            let skip = false;
                            for (let x = 0; x < els.length; x++) {
                                if (suggestions.includes(els[x].textContent)) {
                                    hidePosts.push(els[x]);
                                    skip = true;
                                    break;
                                }
                            }
                            // scan the span tags
                            if (!skip) {
                                els = Array.from(post.querySelectorAll('span'));
                                for (let x = 0; x < els.length; x++) {
                                    if (suggestions.includes(els[x].textContent)) {
                                        hidePosts.push(els[x]);
                                        break;
                                    }
                                }
                            }
                        }
                        // sponsored + paid for ... posts (untested)
                        if (TOGGLE_SPONSORED_PAID_FOR) {
                            let tdivs = Array.from(post.querySelectorAll('div[role="button"'));
                            for (let x= 0; x < tdivs.length; x++) {
                                if (tdivs[x].textContent.substring(0,SPONSORED_PAID_FOR_WORDS_LEN) === SPONSORED_PAID_FOR_WORDS) {
                                    hidePosts.push(tdivs[x]);
                                    break;
                                }
                            }
                        }
                    }
                }
            );
            return hidePosts
        }

        function kill(element) {
            try {
                if(element) {
                    let limit = 0;
                    while(limit++ < 100) {
                        if(typeof element.getAttribute('data-pagelet') == 'string')
                        {
                            if(element.getAttribute('data-pagelet').contains('FeedUnit'))
                            {
                                hide(element);
                                return;
                            }
                        }
                        element = element.parentNode;
                    }
                }
            } catch (e) {
            }
        }

        findSponsoredPosts().forEach( e => kill(e) );

    }

    const callback = function () {
        try {
            doChanges();
        } catch (e) {
            console.warn(title, e);
        }
    };

    setInterval(callback, CHECK_RATE_MS);
})();