Skip to content

Instantly share code, notes, and snippets.

@Buntix
Forked from kunalt4/block-the-blue.md
Last active August 29, 2023 18:48
Show Gist options
  • Save Buntix/99fea81924bcadf6405cec9872845398 to your computer and use it in GitHub Desktop.
Save Buntix/99fea81924bcadf6405cec9872845398 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. This is an even hackier fork of kunalt4's hacky fork of adalinesimonian's gist, which includes blocking only accounts you do not follow and can be configured to filter out those with over a certain follower count (minFollowers, set to 2000 by default).

Also updated to avoid rechecking accounts already checked but not blocked and it skips the waitSeconds pause on accounts that haven't been blocked.

Added a 'removeChecked' option (true by default), this clears already checked and blocked entries to keep the infinite scroll loading new comments to prevent it stalling out at lower minFollowers settings. This does mean it's necessary to reload the page to view it properly after the script has run.

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:

(async()=>{const e=[],t=[],o=e=>new Promise(t=>{let o=document.querySelector(e);if(o)return void t(o);const r=new MutationObserver(()=>{(o=document.querySelector(e))&&(r.disconnect(),t(o))});r.observe(document.body,{childList:!0,subtree:!0})}),r=e=>l(Array.from(e).filter(e=>e.innerText.includes("Followers"))[0].innerText),l=e=>(e=String(e),parsed_num=parseFloat(e.replace(/[^0-9.]/g,"")),e.includes("K")&&(parsed_num*=1e3),e.includes("M")&&(parsed_num*=1e6),parsed_num),i=(()=>{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)})(),a=e=>{const t=document.createElement("div");t.innerText=e,t.style.opacity="1",t.style.transition="opacity 1s",t.style.marginBottom="0.5em",i.appendChild(t),setTimeout(()=>{t.style.opacity="0",setTimeout(()=>{i.removeChild(t)},1e3)},4e3)};let s;for(;s=document.querySelector('[role="article"]:has(> * > * > * > * > * > * > * > * > * > * > * > * > * > * > * > [aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]');)userNameList=[...s.closest('[role="article"]').querySelector('[data-testid="User-Name"]').querySelectorAll('[role="link"]:not(:has(time))')],username=userNameList.map(e=>e.innerText).join(" "),twtUser=userNameList[0],twtAt=userNameList[1].innerText,e.includes(twtAt)?wasBlocked=0:(e.push(twtAt),twtUser.dispatchEvent(new MouseEvent("mouseover",{view:window,bubbles:!0,cancelable:!0})),await o('[data-testid="HoverCard"]'),await new Promise(e=>setTimeout(e,1e3)),hoverCard=document.querySelector('[data-testid="HoverCard"]'),hoverCardLinks=hoverCard.querySelectorAll('[role="link"]'),followerCount=r(hoverCardLinks),checkFollowing=hoverCard.innerText.startsWith("Following"),followerCount<=l(2e3)&&!checkFollowing?(s.click(),(await o('[data-testid="block"]')).click(),(await o('[data-testid="confirmationSheetConfirm"]')).click(),a(`Blocked ${username} with ${followerCount} followers`),wasBlocked=1,t.push(twtAt)):(a(`Did not block ${username} with ${followerCount} followers`),twtUser.querySelector('[aria-label="Verified account"]').ariaLabel="ItsOk",wasBlocked=0),twtUser.dispatchEvent(new MouseEvent("mouseout",{view:window,bubbles:!0,cancelable:!0}))),(wrapper=s.closest('[data-testid="cellInnerDiv"]'))&&wrapper.remove(),Array.from(document.querySelectorAll('[data-testid="cellInnerDiv"]')).filter(e=>e.innerText.match(/^(Show replies|This Tweet is from an account you blocked)/)).forEach(e=>{e.remove()}),await new Promise(e=>setTimeout(e,1e3*wasBlocked));let n=`Blocked ${t.length} users out of ${e.length} checked.`;a("No more verified accounts to conquer."),a(n),console.log(n)})();

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 = 1 // 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 popWaitSeconds = 1 // How long to wait for the hover window to load - can be adjusted if loading is slow and causes errors.

  const minFollowers = 2000 // Block accounts with fewer followers than this.
  // Numeric or a string in the same format as the follower count text, e.g. '1.4M' or '100K'.
  // This tests the rounded number for K/M values, not the full precision shown on hover.

  const removeChecked = true // Remove accounts that have been checked.
  // Removes accounts that have been checked whether blocked or not.  Also removes blocked account and 'Show replies' rows.
  // This causes new tweets to be loaded via infinite-scroll so it doesn't run out of accounts to block with lower minFollowers settings and stall.

  const checkedUsers = []
  const blockedUsers = []

  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 parseFollowerCount = (links) => {
    return parseCount(Array.from(links).filter((html) => { return html.innerText.includes('Followers') })[0].innerText)
  }

  const parseCount = (num_str) => {
    num_str    = String(num_str)
    parsed_num = parseFloat(num_str.replace(/[^0-9.]/g, ''))

    if (num_str.includes('K')) parsed_num *= 1000
    if (num_str.includes('M')) parsed_num *= 1000000

    return parsed_num
  }

  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)
  }

  const clearAlreadyBlockedAndReplyDivs = () => {
    Array.from(document.querySelectorAll('[data-testid="cellInnerDiv"]'))
      .filter((el) => { return el.innerText.match(/^(Show replies|This Tweet is from an account you blocked)/) })
      .forEach((el) => { el.remove() })
  }

  let more

  while (
    (more = document.querySelector(
      '[role="article"]:has(> * > * > * > * > * > * > * > * > * > * > * > * > * > * > * > [aria-label="Verified account"] > g > path:not([clip-rule="evenodd"])) [aria-label="More"]'
    ))
  ) {

    userNameList = [
        ...more
          .closest('[role="article"]')
          .querySelector('[data-testid="User-Name"]')
          .querySelectorAll('[role="link"]:not(:has(time))')
      ]

    username = userNameList.map((el) => el.innerText).join(' ')
    twtUser  = userNameList[0]
    twtAt    = userNameList[1].innerText

    if (! checkedUsers.includes(twtAt)) {
      checkedUsers.push(twtAt)

      twtUser.dispatchEvent(
        new MouseEvent(
            'mouseover', {
                'view':window,
                'bubbles':true,
                'cancelable':true
            }
        )
      )
      ;(await waitForElement('[data-testid="HoverCard"]'))

      await new Promise((resolve) => setTimeout(resolve, popWaitSeconds * 1000))
      hoverCard      = document.querySelector('[data-testid="HoverCard"]')
      hoverCardLinks = hoverCard.querySelectorAll('[role="link"]')

      followerCount  = parseFollowerCount(hoverCardLinks)
      checkFollowing = hoverCard.innerText.startsWith('Following')

      if ((followerCount <= parseCount(minFollowers)) && !checkFollowing) {
        more.click()
        ;(await waitForElement('[data-testid="block"]')).click()
        ;(await waitForElement('[data-testid="confirmationSheetConfirm"]')).click()
        logMessage(`Blocked ${username} with ${followerCount} followers`)
        wasBlocked = waitSeconds
        blockedUsers.push(twtAt)

      } else {
        logMessage(`Did not block ${username} with ${followerCount} followers`)

        //temporarily change label so as to not go into a loop
        twtUser.querySelector('[aria-label="Verified account"]').ariaLabel = "ItsOk"

        wasBlocked = 0
      }

      twtUser.dispatchEvent(
          new MouseEvent(
              'mouseout', {
                  'view':window,
                  'bubbles':true,
                  'cancelable':true
              }
          )
      )

    } else {
      // logMessage(`Already checked ${username}`) // Disabled as it can cause a flood of log messages when a given user has posted a lot of replies.
      wasBlocked = 0
    }

    if (removeChecked) {
      // remove the checked comment to trigger more comments to load.
      if (wrapper = more.closest('[data-testid="cellInnerDiv"]')) wrapper.remove()
      clearAlreadyBlockedAndReplyDivs()
    }

    await new Promise((resolve) => setTimeout(resolve, wasBlocked * 1000))
  }

  let blockMsg = `Blocked ${blockedUsers.length} users out of ${checkedUsers.length} checked.`
  logMessage('No more verified accounts to conquer.')
  logMessage(blockMsg)
  console.log(blockMsg)
})()
@kunalt4
Copy link

kunalt4 commented Apr 25, 2023

Much cleaner, thanks for this!

@Buntix
Copy link
Author

Buntix commented Apr 25, 2023

Got a Uni assignment due in a couple of days, so needed something to procrastinate on 😁

Added a couple more updates to keep the infinite scroll going as it was needing manual scrolling and restarting with lower minFollower settings before.

@Danrarbc
Copy link

Note on this line.

"if (num_str.includes('K')) parsed_num *= 1000"

Twitter displays the K at the 10,000 user mark. Up until then it just lists the actual number (9,999).

@Buntix
Copy link
Author

Buntix commented Apr 26, 2023

Twitter displays the K at the 10,000 user mark. Up until then it just lists the actual number (9,999).

Any number without either a K or M should be parsed (after stripping the comma) as is, so it should be working like that for everything < 10K, did notice one account in the logs with something like 12345.000000000001 followers, but guessing that's just the joy of doing FP math with JS. I've been running with it set at 5000 and it seemed to be working OK.

Though, have found that the 1s delay will definitely get you auto-logged out if you run it on target rich threads like the replies to Musk's alt. Will possibly add a timestamp to the console log to make tracking the blocks/hour easier.

@skitij
Copy link

skitij commented Apr 28, 2023

The script seems to get stuck anytime it encounters a user with just 1 follower. My guess is the search is looking for 'Followers' and not 'follower'.

@lauracs
Copy link

lauracs commented Apr 30, 2023

Is there any way to see a log of what it executed? I hit the script on the wrong tweet and I think I got some people I didn't want to block.

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