Skip to content

Instantly share code, notes, and snippets.

@adalinesimonian
Last active February 21, 2024 22:56
  • Star 61 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save adalinesimonian/b52a753c9fd6c176598745df01ba12dc to your computer and use it in GitHub Desktop.
Block all verified Twitter accounts on screen

#BlockTheBlue

Table of Contents

What is this?

The script at the bottom will block verified Twitter accounts whose tweets are visible on the current page.

Screen capture

To run this script, paste it into your browser's developer tools console, usually accessible by pressing F12 or Cmd+Shift+I.

To stop the execution of this script for any reason, just refresh the page.

Bookmarklet

You can also pin this script to your browser's bookmarks bar, and click it to run it on any Twitter page. To do this, create a new bookmark, and paste the following code into the URL field:

javascript:(async()=>{const e=e=>new Promise(t=>{let o=document.querySelector(e);if(o)return void t(o);const l=new MutationObserver(()=>{(o=document.querySelector(e))&&(l.disconnect(),t(o))});l.observe(document.body,{childList:!0,subtree:!0})}),t=(()=>{let e=document.getElementById("visual-log");return e||((e=document.createElement("div")).id="visual-log",e.style.position="fixed",e.style.bottom="0",e.style.left="0",e.style.zIndex="9999",e.style.backgroundColor="white",e.style.padding="1em",e.style.border="1px solid black",e.style.borderRadius="1em",e.style.margin="1em",e.style.maxHeight="50vh",e.style.maxWidth="50vw",e.style.overflowY="scroll",e.style.fontFamily="monospace",document.body.appendChild(e),e)})(),o=e=>{const o=document.createElement("div");o.innerText=e,o.style.opacity="1",o.style.transition="opacity 1s",o.style.marginBottom="0.5em",t.appendChild(o),setTimeout(()=>{o.style.opacity="0",setTimeout(()=>{t.removeChild(o)},1e3)},4e3)};let l;for(;l=document.querySelector('[role="article"]:has(> * > * > * > * > * > * > * > * > * > * > * > * > * > * > * > [aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]');){const t=[...l.closest('[role="article"]').querySelector('[data-testid="User-Name"]').querySelectorAll('[role="link"]:not(:has(time))')].map(e=>e.innerText).join(" ");l.click(),(await e('[data-testid="block"]')).click(),(await e('[data-testid="confirmationSheetConfirm"]')).click(),o(`Blocked ${t}`),await new Promise(e=>setTimeout(e,800))}o("No more verified accounts to block")})();

You can then click the bookmark to run the script on any Twitter page. Keep in mind that the copy of the script in the bookmark will not be updated alongside this file, so you may want to check back here for updates.

Screenshot

If you don't trust the above bookmarklet, you can create your own by copying the code below into a JavaScript minifier, such as the one at https://skalman.github.io/UglifyJS-online/, and then pasting the minified code into the URL field of a new bookmark with javascript: at the beginning.

Finding Verified Accounts

You can locate tweets by verified accounts by searching for -filter:verified filter:blue_verified in the Twitter search bar.

Firefox Users

You need to first enable the :has() CSS pseudo-class in about:config. Browse to about:config, accept the warning, and search for layout.css.has-selector.enabled. If it is set to false, double-click it to change it to true.

Screenshot
This is what you should see when your settings are correct.

Disclaimer

This script is provided as-is, and is not guaranteed to work. It may stop working at any time if Twitter changes their website. It is your responsibility to ensure that you are using this script in accordance with Twitter's terms of service. Otherwise, you may experience rate limiting, account lockouts, or other issues I cannot foresee.

The Script

;(async () => {
  const waitSeconds = 0.8 // How long to wait in between blocking accounts, in
  // seconds. If you're getting rate limited or logged out, try increasing this
  // time span so that you're not making too many requests too quickly.
  const waitForElement = selector =>
    new Promise(resolve => {
      let element = document.querySelector(selector)
      if (element) {
        resolve(element)
        return
      }
      const observer = new MutationObserver(() => {
        element = document.querySelector(selector)
        if (element) {
          observer.disconnect()
          resolve(element)
        }
      })
      observer.observe(document.body, { childList: true, subtree: true })
    })
  const createVisualLog = () => {
    // Create a visual log to show what's happening
    let log = document.getElementById('visual-log')
    if (log) {
      return log
    }
    log = document.createElement('div')
    log.id = 'visual-log'
    log.style.position = 'fixed'
    log.style.bottom = '0'
    log.style.left = '0'
    log.style.zIndex = '9999'
    log.style.backgroundColor = 'white'
    log.style.padding = '1em'
    log.style.border = '1px solid black'
    log.style.borderRadius = '1em'
    log.style.margin = '1em'
    log.style.maxHeight = '50vh'
    log.style.maxWidth = '50vw'
    log.style.overflowY = 'scroll'
    log.style.fontFamily = 'monospace'
    document.body.appendChild(log)
    return log
  }
  const log = createVisualLog()
  const logMessage = message => {
    // add a message element that fades out and is removed after 5 seconds
    const messageElement = document.createElement('div')
    messageElement.innerText = message
    messageElement.style.opacity = '1'
    messageElement.style.transition = 'opacity 1s'
    messageElement.style.marginBottom = '0.5em'
    log.appendChild(messageElement)
    setTimeout(() => {
      messageElement.style.opacity = '0'
      setTimeout(() => {
        log.removeChild(messageElement)
      }, 1000)
    }, 4000)
  }
  let more
  while (
    (more = document.querySelector(
      '[role="article"]:has(> * > * > * > * > * > * > * > * > * > * > * > * > * > * > * > [aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]'
    ))
  ) {
    const username = [
      ...more
        .closest('[role="article"]')
        .querySelector('[data-testid="User-Name"]')
        .querySelectorAll('[role="link"]:not(:has(time))')
    ]
      .map(el => el.innerText)
      .join(' ')
    more.click()
    ;(await waitForElement('[data-testid="block"]')).click()
    ;(await waitForElement('[data-testid="confirmationSheetConfirm"]')).click()
    logMessage(`Blocked ${username}`)
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000))
  }
  logMessage('No more verified accounts to block')
})()
@Buntix
Copy link

Buntix commented Apr 25, 2023

Thank you for this! I've added some hacky workarounds to check if you follow the twitter user and if they have more than a million followers - both cases one might not want to block accounts and it does the job.

I also have re-forked yours to make the minimum followers to escape auto blocking configurable. Also some other stuff like not rechecking previously non-blocked accounts.

@adbenitez
Copy link

someone should create a greasemonkey script with this

@python-nerd-3
Copy link

he spent his time and effort to make a script to block everyone on twitter who is verified
these are the only people that make me retain my hope in society
respect

@sekoku
Copy link

sekoku commented Apr 26, 2023

Doesn't work for me on Chrome. Probably because I have Ublock enabled? The Bookmark option gives the notification but no blocks happen.

@ofasgard
Copy link

someone should create a greasemonkey script with this

haven't tested much, but this works for me

// ==UserScript==
// @name     Block Blue
// @version  2
// @grant    none
// ==/UserScript==

async function blockBlue() {
  const waitSeconds = 1.0 // How long to wait in between blocking accounts, in
  // seconds. If you're getting rate limited or logged out, try increasing this
  // time span so that you're not making too many requests too quickly.
  const waitForElement = selector =>
    new Promise(resolve => {
      let element = document.querySelector(selector)
      if (element) {
        resolve(element)
        return
      }
      const observer = new MutationObserver(() => {
        element = document.querySelector(selector)
        if (element) {
          observer.disconnect()
          resolve(element)
        }
      })
      observer.observe(document.body, { childList: true, subtree: true })
    })
  const createVisualLog = () => {
    // Create a visual log to show what's happening
    let log = document.getElementById('visual-log')
    if (log) {
      return log
    }
    log = document.createElement('div')
    log.id = 'visual-log'
    log.style.position = 'fixed'
    log.style.bottom = '0'
    log.style.left = '0'
    log.style.zIndex = '9999'
    log.style.backgroundColor = 'white'
    log.style.padding = '1em'
    log.style.border = '1px solid black'
    log.style.borderRadius = '1em'
    log.style.margin = '1em'
    log.style.maxHeight = '50vh'
    log.style.maxWidth = '50vw'
    log.style.overflowY = 'scroll'
    log.style.fontFamily = 'monospace'
    document.body.appendChild(log)
    return log
  }
  const log = createVisualLog()
  const logMessage = message => {
    // add a message element that fades out and is removed after 5 seconds
    const messageElement = document.createElement('div')
    messageElement.innerText = message
    messageElement.style.opacity = '1'
    messageElement.style.transition = 'opacity 1s'
    messageElement.style.marginBottom = '0.5em'
    log.appendChild(messageElement)
    setTimeout(() => {
      messageElement.style.opacity = '0'
      setTimeout(() => {
        log.removeChild(messageElement)
      }, 1000)
    }, 4000)
  }
  let more
  while (
    (more = document.querySelector(
      '[role="article"]:has(* > * > * > * > * > * > * > * > * > * > * > * > * > * > * > * > [aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]'
    ))
  ) {
    const username = [
      ...more
        .closest('[role="article"]')
        .querySelector('[data-testid="User-Name"]')
        .querySelectorAll('[role="link"]:not(:has(time))')
    ]
      .map(el => el.innerText)
      .join(' ')
    more.click()
    ;(await waitForElement('[data-testid="block"]')).click()
    ;(await waitForElement('[data-testid="confirmationSheetConfirm"]')).click()
    logMessage(`Blocked ${username}`)
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000))
  }
  logMessage('No more verified accounts to block')
}

async function startBlocking() {
  const delay = 10.0;
  await blockBlue();
  setTimeout(startBlocking, delay * 1000);
}

startBlocking();

@Danrarbc
Copy link

Danrarbc commented Apr 26, 2023

someone should create a greasemonkey script with this

haven't tested much, but this works for me

I tried to add the following check and user count filters to this and failed.

Activating on press of a button would probably be better than auto running on page load too.

@cbenard
Copy link

cbenard commented Apr 26, 2023

Is there a way to modify it so that it only blocks if their follower count is under a certain amount? Now that Elon has forcefully applied Blue to accounts that didn't want it, I can't block them all.

@seangilleran
Copy link

seangilleran commented May 1, 2023

@cbenard here from @Buntix https://gist.github.com/Buntix/99fea81924bcadf6405cec9872845398#gistcomment-4548437

I’ve had mixed success with this. The only way to get this info seems to be triggering the user card before each block, which takes a moment and can get you stuck in a loop. So far the most reliable approach I’ve found has been to do it by (sigh) manually blocking the accounts you want to save, running the script, and then going back and unblocking them.

@alanpaone
Copy link

One strategy I've found to avoid getting rate limited or logged out is to start the script but scroll up, so that the blocked accounts are disappearing offscreen, that way they'll stop loading eventually, then you can wait a minute or two, scroll down and run the bookmarklet again. If you're looking at a big tweet, you can hit the 500 (is it actually 500?) limit pretty quick!

@Ackhuman
Copy link

Ackhuman commented May 4, 2023

The latest change to verification badges breaks this. Here is an improved version that is less brittle and only targets twitter blue subs rather than all verified accounts:

;(async () => {
  const waitSeconds = 0.8 // How long to wait in between blocking accounts, in
  // seconds. If you're getting rate limited or logged out, try increasing this
  // time span so that you're not making too many requests too quickly.
  const waitForElement = selector =>
    new Promise(resolve => {
      let element = document.querySelector(selector)
      if (element) {
        resolve(element)
        return
      }
      const observer = new MutationObserver(() => {
        element = document.querySelector(selector)
        if (element) {
          observer.disconnect()
          resolve(element)
        }
      })
      observer.observe(document.body, { childList: true, subtree: true })
    })
  const createVisualLog = () => {
    // Create a visual log to show what's happening
    let log = document.getElementById('visual-log')
    if (log) {
      return log
    }
    log = document.createElement('div')
    log.id = 'visual-log'
    log.style.position = 'fixed'
    log.style.bottom = '0'
    log.style.left = '0'
    log.style.zIndex = '9999'
    log.style.backgroundColor = 'white'
    log.style.padding = '1em'
    log.style.border = '1px solid black'
    log.style.borderRadius = '1em'
    log.style.margin = '1em'
    log.style.maxHeight = '50vh'
    log.style.maxWidth = '50vw'
    log.style.overflowY = 'scroll'
    log.style.fontFamily = 'monospace'
    document.body.appendChild(log)
    return log
  }
  const log = createVisualLog()
  const logMessage = message => {
    // add a message element that fades out and is removed after 5 seconds
    const messageElement = document.createElement('div')
    messageElement.innerText = message
    messageElement.style.opacity = '1'
    messageElement.style.transition = 'opacity 1s'
    messageElement.style.marginBottom = '0.5em'
    log.appendChild(messageElement)
    setTimeout(() => {
      messageElement.style.opacity = '0'
      setTimeout(() => {
        log.removeChild(messageElement)
      }, 1000)
    }, 4000)
  }
  let more
  while (
    (more = document.querySelector(
      '[role="article"]:has(> * [data-eight-dollars-status="blueVerified"] path:not([clip-rule="evenodd"])) [aria-label="More"]'
    ))
  ) {
    const username = [
      ...more
        .closest('[role="article"]')
        .querySelector('[data-testid="User-Name"]')
        .querySelectorAll('[role="link"]:not(:has(time))')
    ]
      .map(el => el.innerText)
      .join(' ')
    more.click()
    ;(await waitForElement('[data-testid="block"]')).click()
    ;(await waitForElement('[data-testid="confirmationSheetConfirm"]')).click()
    logMessage(`Blocked ${username}`)
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000))
  }
  logMessage('No more verified accounts to block')
})()

Bookmarklet version:

javascript:(async()=>{let e=e=>new Promise(t=>{let l=document.querySelector(e);if(l){t(l);return}let i=new MutationObserver(()=>{(l=document.querySelector(e))&&(i.disconnect(),t(l))});i.observe(document.body,{childList:!0,subtree:!0})}),t=()=>{let e=document.getElementById("visual-log");return e||((e=document.createElement("div")).id="visual-log",e.style.position="fixed",e.style.bottom="0",e.style.left="0",e.style.zIndex="9999",e.style.backgroundColor="white",e.style.padding="1em",e.style.border="1px solid black",e.style.borderRadius="1em",e.style.margin="1em",e.style.maxHeight="50vh",e.style.maxWidth="50vw",e.style.overflowY="scroll",e.style.fontFamily="monospace",document.body.appendChild(e)),e},l=t(),i=e=>{let t=document.createElement("div");t.innerText=e,t.style.opacity="1",t.style.transition="opacity 1s",t.style.marginBottom="0.5em",l.appendChild(t),setTimeout(()=>{t.style.opacity="0",setTimeout(()=>{l.removeChild(t)},1e3)},4e3)},o;for(;o=document.querySelector('[role="article"]:has(> * [data-eight-dollars-status="blueVerified"] path:not([clip-rule="evenodd"])) [aria-label="More"]');){let a=[...o.closest('[role="article"]').querySelector('[data-testid="User-Name"]').querySelectorAll('[role="link"]:not(:has(time))')].map(e=>e.innerText).join(" ");o.click(),(await e('[data-testid="block"]')).click(),(await e('[data-testid="confirmationSheetConfirm"]')).click(),i(`Blocked ${a}`),await new Promise(e=>setTimeout(e,800))}i("No more verified accounts to block")})();

@cbenard
Copy link

cbenard commented May 4, 2023

@Ackhuman this doesn't work for me. It just logs "No more verified accounts to block". Also the query selector document.querySelector('[role="article"]:has(> * [data-eight-dollars-status="blueVerified"] path:not([clip-rule="evenodd"])) [aria-label="More"]') returns null.

This is on a page with a ton of blue-checks. When inspecting, I don't see any data-eight-dollars-status attributes anywhere.

@Ackhuman
Copy link

Ackhuman commented May 5, 2023

@cbenard You haven't had the new UI change rolled out to you yet.

@felixding
Copy link

How can I update the selector so that it blocks ALL accounts on the page? For example, I'd like to search a specific keyword, and block all the accounts who have tweeted the keyword.

@MacsInSpace
Copy link

MacsInSpace commented Sep 24, 2023

Disclaimer: I dont know js.
Now that the ads have the span class thing at the top with the "Ad" text,
<span class="css-901oao css-16my406 r-poiln3 r-bcqeeo r-qvutc0">Ad</span>
Can this be forked and modified to look for ^it^ and used to block advertisers?

Something like:
document.querySelector( '[role="article"]:has(> * > * > * > * > * > * > * > * > [div=ltr]" > * > ) [aria-label="More"]' )
or
document.querySelector( '[role="article"]:has(> * > * > * > * > * > * > * > * > [span class=<*>Ad</span]" > * > ) [aria-label="More"]' )

@noahdominic
Copy link

I've been having problems with this script, which is, I suspect, due to some UI changes rolling out. But I've solved this problem by modifying the query selector from
document.querySelector(article"]:has(* > * > * > * > * > * > * > * > * > * > * > * > * > * > * > * > [aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]')
to
document.querySelector(article"]:has([aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]'). It's less specific than the original selector so it may be more robust against UI changes, but might also introduce unintended effects. But so far, I haven't seen any 🤷.

Here is the modified bookmarklet:

javascript:(async()=>{const e=e=>new Promise(t=>{let o=document.querySelector(e);if(o)return void t(o);const l=new MutationObserver(()=>{(o=document.querySelector(e))&&(l.disconnect(),t(o))});l.observe(document.body,{childList:!0,subtree:!0})}),t=(()=>{let e=document.getElementById("visual-log");return e||((e=document.createElement("div")).id="visual-log",e.style.position="fixed",e.style.bottom="0",e.style.left="0",e.style.zIndex="9999",e.style.backgroundColor="white",e.style.padding="1em",e.style.border="1px solid black",e.style.borderRadius="1em",e.style.margin="1em",e.style.maxHeight="50vh",e.style.maxWidth="50vw",e.style.overflowY="scroll",e.style.fontFamily="monospace",document.body.appendChild(e),e)})(),o=e=>{const o=document.createElement("div");o.innerText=e,o.style.opacity="1",o.style.transition="opacity 1s",o.style.marginBottom="0.5em",t.appendChild(o),setTimeout(()=>{o.style.opacity="0",setTimeout(()=>{t.removeChild(o)},1e3)},4e3)};let l;for(;l=document.querySelector('[role="article"]:has([aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]');){const t=[...l.closest('[role="article"]').querySelector('[data-testid="User-Name"]').querySelectorAll('[role="link"]:not(:has(time))')].map(e=>e.innerText).join(" ");l.click(),(await e('[data-testid="block"]')).click(),(await e('[data-testid="confirmationSheetConfirm"]')).click(),o(`Blocked ${t}`),await new Promise(e=>setTimeout(e,800))}o("No more verified accounts to block")})();

@noahdominic
Copy link

@cbenard @Ackhuman I'm a few months late, but I believe data-eight-dollars-status is dependent on the browser extension Eight Dollars. It is not a native Twitter attribute; I've just checked this, and the attribute is only present when I have the extension installed.

@MacsInSpace
Copy link

MacsInSpace commented Sep 25, 2023

I (GPT) managed to get Ad blocking working after some back and forth with this code and Chat GPT.
I added a scroller at the bottom but have it commented out.

(async () => {
  const waitSeconds = 0.8; // How long to wait in between blocking accounts, in seconds.

  const waitForElement = selector => new Promise(resolve => {
    let element = document.querySelector(selector);
    if (element) {
      resolve(element);
      return;
    }
    const observer = new MutationObserver(() => {
      element = document.querySelector(selector);
      if (element) {
        observer.disconnect();
        resolve(element);
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  });

  const createVisualLog = () => {
    // Create a visual log to show what's happening
    let log = document.getElementById('visual-log');
    if (log) {
      return log;
    }
    log = document.createElement('div');
    log.id = 'visual-log';
    log.style.position = 'fixed';
    log.style.bottom = '0';
    log.style.left = '0';
    log.style.zIndex = '9999';
    log.style.backgroundColor = 'white';
    log.style.padding = '1em';
    log.style.border = '1px solid black';
    log.style.borderRadius = '1em';
    log.style.margin = '1em';
    log.style.maxHeight = '50vh';
    log.style.maxWidth = '50vw';
    log.style.overflowY = 'scroll';
    log.style.fontFamily = 'monospace';
    document.body.appendChild(log);
    return log;
  };

  const log = createVisualLog();

  const logMessage = message => {
    // add a message element that fades out and is removed after 5 seconds
    const messageElement = document.createElement('div');
    messageElement.innerText = message;
    messageElement.style.opacity = '1';
    messageElement.style.transition = 'opacity 1s';
    messageElement.style.marginBottom = '0.5em';
    log.appendChild(messageElement);
    setTimeout(() => {
      messageElement.style.opacity = '0';
      setTimeout(() => {
        log.removeChild(messageElement);
      }, 1000);
    }, 4000);
  };

  let more;
  while ((more = document.querySelector('[aria-label="More"]'))) {
    console.log('Checking for "More" button.');

    // Find the account name associated with the "More" button
    const parentArticle = more.closest('[role="article"]');
    const accountNameElement = parentArticle.querySelector('[data-testid="User-Name"] a');
    const accountName = accountNameElement ? accountNameElement.textContent : 'Unknown User';

    // Check if the parent element contains the text "Ad"
    const parentElement = more.closest('div.css-1dbjc4n.r-1jkjb');
    if (parentElement) {
      const adElements = parentElement.querySelectorAll('span.css-901oao.css-16my406.r-poiln3.r-bcqeeo.r-qvutc0');
      for (const adElement of adElements) {
        if (adElement.textContent.includes('Ad')) {
          console.log(`Found an advertisement for ${accountName}. Blocking...`);
          more.click();
          (await waitForElement('[data-testid="block"]')).click();
          (await waitForElement('[data-testid="confirmationSheetConfirm"]')).click();
          logMessage(`Blocked advertisement for ${accountName}`);
          console.log(`Blocked an advertisement for ${accountName}.`);
          break; // Exit the loop once an ad is blocked
        }
      }
    }

    //window.scrollTo(0, document.body.scrollHeight);
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
  }

  logMessage('No more advertisements to block');
})();
`
```

@Arkatruc
Copy link

Is it broken forever now? It just doesn't work

@Trekeln
Copy link

Trekeln commented Jan 12, 2024

It just doesn't work anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment