-
-
Save zoubingwu/c4bf1e1f26ad5d3190786bc4a000da62 to your computer and use it in GitHub Desktop.
// ==UserScript== | |
// @name Twitter Insta Block | |
// @namespace your-namespace-here | |
// @version 3 | |
// @description Adds a "Block User" button on Twitter timeline conversations and blocks the user with one click | |
// @author zoubingwu | |
// @match https://x.com/* | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
let authorization = ''; | |
// Save a reference to the original XMLHttpRequest constructor | |
const OriginalXHR = window.XMLHttpRequest; | |
// Define a new constructor for our modified XMLHttpRequest object | |
function ModifiedXHR() { | |
const xhr = new OriginalXHR(); | |
// Override the setRequestHeader() method to intercept the Authorization header | |
const originalSetRequestHeader = xhr.setRequestHeader; | |
xhr.setRequestHeader = function (name, value) { | |
if (name.toLowerCase() === 'authorization' && !authorization) { | |
authorization = value; | |
} | |
originalSetRequestHeader.apply(this, arguments); | |
} | |
return xhr; | |
} | |
// Replace the original XMLHttpRequest constructor with our modified constructor | |
window.XMLHttpRequest = ModifiedXHR; | |
const homePageRegex = /^https:\/\/x\.com\/\w+/; | |
const statusPageRegex = /^https:\/\/x\.com\/\w+\/status\/(\d+)/; | |
const style = document.createElement("style"); | |
style.innerHTML = ` | |
.insta-block-button { | |
margin-right: 10px; | |
height: 36px; | |
width: 36px; | |
border-radius: 50%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
transition-duration: 0.2s; | |
} | |
.insta-block-button:hover { | |
background-color: rgba(29, 155, 240, 0.1) | |
} | |
.insta-block-button > svg { | |
height: 16px; | |
color: ${reverseColor(document.body.style.backgroundColor)}; | |
fill: currentColor; | |
} | |
` | |
document.body.appendChild(style); | |
const tampered = new WeakSet(); | |
const observer = new MutationObserver(function (mutationList, observer) { | |
const conversationItems = document.querySelectorAll('[data-testid="cellInnerDiv"]') | |
// For each conversation item, add a "Block User" button | |
conversationItems.forEach((item, index) => { | |
if (tampered.has(item)) { | |
return; | |
} | |
const tweet = item.querySelector('[data-testid="tweet"]'); | |
// already blocked | |
if (!tweet && ['已屏蔽的账号', 'account you blocked'].some(i => item.innerText.includes(i))) { | |
item.style.display = 'none' | |
console.log('already blocked'); | |
return; | |
} | |
const link = item.querySelector('[data-testid="User-Name"] a[role="link"]') | |
if (!link) { | |
console.log('no link found'); | |
return; | |
} | |
const screenName = link.getAttribute('href').substring(1); | |
if (item.querySelector(`.insta-block-button[data-name="${screenName}"]`)) { | |
console.log('already have block button'); | |
return; | |
} | |
// Create a "Block User" button | |
const blockButton = document.createElement('div'); | |
blockButton.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><g><path d="M12 3.75c-4.55 0-8.25 3.69-8.25 8.25 0 1.92.66 3.68 1.75 5.08L17.09 5.5C15.68 4.4 13.92 3.75 12 3.75zm6.5 3.17L6.92 18.5c1.4 1.1 3.16 1.75 5.08 1.75 4.56 0 8.25-3.69 8.25-8.25 0-1.92-.65-3.68-1.75-5.08zM1.75 12C1.75 6.34 6.34 1.75 12 1.75S22.25 6.34 22.25 12 17.66 22.25 12 22.25 1.75 17.66 1.75 12z"></path></g></svg>` | |
blockButton.dataset.name = screenName; | |
blockButton.classList.add('insta-block-button') | |
// Add a click event listener to the button to block the user | |
blockButton.addEventListener('click', (e) => { | |
e.preventDefault(); | |
e.stopPropagation(); | |
item.remove(); | |
blockUser(screenName) | |
}); | |
// Add the "Block User" button to the conversation item | |
const menu = item.querySelector('button[data-testid="caret"]'); | |
if (menu) { | |
menu.parentNode.prepend(blockButton); | |
tampered.add(item); | |
} | |
}); | |
}) | |
observer.observe(document.documentElement, { childList: true, subtree: true }); | |
function getCookie(name) { | |
const cookieString = document.cookie; | |
const cookies = cookieString.split(';'); | |
const cookieMap = cookies.reduce((map, cookie) => { | |
const [key, value] = cookie.trim().split('='); | |
map[key] = value; | |
return map; | |
}, {}); | |
return cookieMap[name]; | |
} | |
function getHeader() { | |
const authHeaders = JSON.parse(localStorage.getItem('authHeaders')); | |
const headers = { | |
'Authorization': authHeaders ? Object.values(authHeaders).at(0).authorization : authorization, | |
'x-csrf-token': getCookie('ct0'), | |
'x-twitter-auth-type': 'OAuth2Session', | |
'x-twitter-active-user': 'yes', | |
'x-twitter-client-language': 'en', | |
} | |
return headers; | |
} | |
// Define a function to block a user by screen name | |
function blockUser(screenName) { | |
fetch(`https://x.com/i/api/1.1/blocks/create.json?screen_name=${screenName}`, { | |
method: 'POST', | |
credentials: 'include', | |
headers: getHeader(), | |
}) | |
.then(response => { | |
if (response.ok) { | |
console.log(`User @${screenName} blocked successfully`); | |
} else { | |
console.error(`Failed to block user @${screenName}`); | |
} | |
}) | |
.catch(error => { | |
console.error(error); | |
}); | |
} | |
function reverseColor(color) { | |
// Check if the input is a hex color code | |
if (color.startsWith('#')) { | |
// Convert hex color code to RGB color values | |
const r = parseInt(color.substring(1, 3), 16); | |
const g = parseInt(color.substring(3, 5), 16); | |
const b = parseInt(color.substring(5, 7), 16); | |
// Compute the reverse color values by subtracting each component from 255 | |
const reverseR = 255 - r; | |
const reverseG = 255 - g; | |
const reverseB = 255 - b; | |
// Convert reverse RGB color values to hex color code | |
const reverseHex = `#${reverseR.toString(16).padStart(2, '0')}${reverseG.toString(16).padStart(2, '0')}${reverseB.toString(16).padStart(2, '0')}`; | |
return reverseHex; | |
} else if (color.startsWith('rgb')) { | |
// Parse the RGB color values from the input string | |
const match = color.match(/\d+/g); | |
const r = parseInt(match[0]); | |
const g = parseInt(match[1]); | |
const b = parseInt(match[2]); | |
// Compute the reverse color values by subtracting each component from 255 | |
const reverseR = 255 - r; | |
const reverseG = 255 - g; | |
const reverseB = 255 - b; | |
// Construct the reverse RGB color string | |
const reverseRGB = `rgb(${reverseR}, ${reverseG}, ${reverseB})`; | |
return reverseRGB; | |
} else { | |
// Invalid input | |
return null; | |
} | |
} | |
})(); |
Add the following code to L96 to display a white icon when the website is in dark mode
@media (prefers-color-scheme: dark) { .insta-block-button { color: white } }
Looks like twitter not following the system color scheme setting, instead it uses its own background color setting, so I added a function to simply reverse that color for the icon
Firefox's querySelector does not support the pseduo-class :has()
well enough like Chrome does.
BTW, the twitter auth token cannot be found in localStorage
when using Firefox so I have to find it manually.
Here is my forked snippet with Firefox support.
https://gist.github.com/tizee/495e3411d6311c12ea3c06c4bf3c0bcf#file-main-js
This would make the script more robust.
has
selector was removed, it just get all the querySelectorAll('[data-testid="cellInnerDiv"]')
then remove that item in the click event
Firefox's querySelector does not support the pseduo-class
:has()
well enough like Chrome does.BTW, the twitter auth token cannot be found in
localStorage
when using Firefox so I have to find it manually.Here is my forked snippet with Firefox support.
tizee/495e3411d6311c12ea3c06c4bf3c0bcf#file-main-js
![]()
This would make the script more robust.
tried with firefox, seems twitter no longer stores auth token in local storage, so I have to monkey patch the XHR :(
tried with firefox, seems twitter no longer stores auth token in local storage, so I have to monkey patch the XHR :(
Thanks for solving the token issue. This hack is very creative.
Add the following code to L96 to display a white icon when the website is in dark mode