// ==UserScript==
// @name block CCP propaganda tweets likers
// @namespace https://eolstudy.com
// @version 4.2.2
// @description Block with love.
// @author amormaid
// @run-at document-end
// @grant GM_registerMenuCommand
// @match https://twitter.com/*
// @match https://mobile.twitter.com/*
// @match https://tweetdeck.twitter.com/*
// @exclude https://twitter.com/account/*
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @license MIT
// ==/UserScript==
/* Inspired by Twitter-Block-With-Love https://greasyfork.org/en/scripts/398540-twitter-block-with-love */
(_ => {
function stringify(obj, sep, eq) {
sep = sep || '&';
eq = eq || '=';
let str = "";
for (var k in obj) {
str += k + eq + decodeURIComponent(obj[k]) + sep
}
return str.slice(0, -1)
};
function parse(str) {
var obj = new Object();
strs = str.split("&");
for (var i = 0; i < strs.length; i++) {
let index = strs[i].indexOf("=")
obj[strs[i].slice(0, index)] = decodeURIComponent(strs[i].slice(index + 1));
}
return obj;
}
//解析url地址
function getRequest() {
var url = location.search; //获取url中"?"符后的字串
var theRequest = new Object();
if (url.indexOf("?") != -1) {
var str = url.substr(1);
return parse(str)
}
}
const translations = {
// Please submit a feedback on Greasyfork.com if your language is not in the list bellow
'en': {
lang_name: 'English',
like_title: 'Liked by',
like_list_identifier: 'Timeline: Liked by',
retweet_title: 'Retweeted by',
retweet_list_identifier: 'Timeline: Retweeted by',
block_btn: 'Block all',
block_success: 'All users blocked!',
mute_btn: 'Mute all',
mute_success: 'All users muted!',
include_original_tweeter: 'Include the original Tweeter',
logs: 'Logs',
list_members: 'List members',
list_members_identifier: 'Timeline: List members',
block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
}
}
let i18n = translations.en
function get_theme_color (){
const close_icon = $('div[aria-label] > div[dir="auto"] > svg[viewBox="0 0 24 24"]')[0]
return window.getComputedStyle(close_icon).color
}
function component_to_hex (c) {
if (typeof(c) === 'string') c = Number(c)
const hex = c.toString(16);
return hex.length === 1 ? ("0" + hex) : hex;
}
function rgb_to_hex (r, g, b) {
return "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b);
}
function get_cookie (cname) {
let name = cname + '='
let ca = document.cookie.split(';')
for (let i = 0; i < ca.length; ++i) {
let c = ca[i].trim()
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length)
}
}
return ''
}
function getStorage (name) {
try {
return window.JSON.parse(sessionStorage.getItem(name) || '[]')
} catch (err) {
sessionStorage.setItem(name, '[]')
return []
}
}
function setStorage (name, val) {
sessionStorage.setItem(name, window.JSON.stringify(val))
}
function get_ancestor (dom, level) {
for (let i = 0; i < level; ++i) {
dom = dom.parent()
}
return dom
}
const ajax = axios.create({
baseURL: 'https://api.twitter.com',
withCredentials: true,
headers: {
'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
'X-Twitter-Auth-Type': 'OAuth2Session',
'X-Twitter-Active-User': 'yes',
'X-Csrf-Token': get_cookie('ct0')
}
})
// users this user is following
async function get_friends (userId) {
const cachedFriends = getStorage('friends')
if (cachedFriends && cachedFriends.length) {
return cachedFriends
}
const my_id = get_cookie('twid').replace('u%3D', '')
const users = await ajax.get(`/1.1/friends/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
res => res.data.ids
) || []
// console.log('get_friends', users)
setStorage('friends', window.JSON.stringify(users))
return users
}
// users follow this user
async function get_followers (userId) {
const cachedUsers = getStorage('followers')
if (cachedUsers && cachedUsers.length) {
return cachedUsers
}
const my_id = get_cookie('twid').replace('u%3D', '')
const users = await ajax.get(`/1.1/followers/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
res => res.data.ids
) || []
// console.log('get_followers', users)
setStorage('followers', window.JSON.stringify(users))
return users
}
async function get_list_menber (listId) {
const cachedUsers = getStorage('ccpmember')
if (cachedUsers && cachedUsers.length) {
return cachedUsers
}
const users = await ajax.get(`/1.1/lists/members.json?list_id=${listId}&count=5000`).then(
res => res.data.users
)
// console.log('get_list_menber', users)
const newUsers = (users || []).map(({ id_str }) => id_str)
setStorage('ccpmember', window.JSON.stringify(newUsers))
return newUsers
}
function get_tweet_id () {
// https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
return location.href.split('status/')[1].split('/')[0]
}
// function get_list_id () {
// // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
// return location.href.split('lists/')[1].split('/')[0]
// }
// fetch_likers and fetch_no_comment_retweeters need to be merged into one function
async function fetch_likers (tweetId) {
const users = await ajax.get(`/2/timeline/liked_by.json?tweet_id=${tweetId}`).then(
res => res.data.globalObjects.users
)
let likers = []
Object.keys(users).forEach(user => likers.push(user)) // keys of users are id strings
return likers
}
// async function fetch_no_comment_retweeters (tweetId) {
// const users = (await ajax.get(`/2/timeline/retweeted_by.json?tweet_id=${tweetId}`)).data.globalObjects.users
// let targets = []
// Object.keys(users).forEach(user => targets.push(user))
// return targets
// }
// async function fetch_list_members (listId) {
// const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users
// let members = []
// members = users.map(u => u.id_str)
// return members
// }
function block_user (id) {
// ajax.post('/1.1/blocks/create.json', Qs.stringify({
ajax.post('/1.1/blocks/create.json', stringify({
user_id: id
}), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
// function mute_user (id) {
// ajax.post('/1.1/mutes/users/create.json', Qs.stringify({
// user_id: id
// }), {
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// }
// })
// }
// block_all_liker and block_no_comment_retweeters need to be merged
async function block_all_likers () {
const tweetId = get_tweet_id()
const [my_followers, my_friends, listMember] = await Promise.all([
get_followers(),
get_friends(),
get_list_menber('1497432788634841089') // ccp propaganda list
])
// console.log('my_followers', my_followers)
// console.log('my_friends', my_friends)
// console.log('listMember', listMember)
const likers = await fetch_likers(tweetId)
console.log('likers', likers)
const newLikers = likers.filter(id => {
const flag_a = !my_followers.includes(id)
const flag_b = !my_friends.includes(id)
const flag_c = !listMember.includes(id)
return flag_a && flag_b && flag_c
})
console.log('newLikers', newLikers)
console.log('will not block ', likers.filter(id => !newLikers.includes(id)))
likers.forEach(id => block_user(id))
success_notice(i18n.like_list_identifier, i18n.block_success)
}
function success_notice (identifier, success_msg) {
const btnNode = document.createElement('div')
btnNode.innerText = success_msg
btnNode.style.cssText = `
position: absolute;
left: calc(50% - 25vw);
top: 40vw;
width: 50vw;
height: 10vw;
background-color: rgba(255, 255, 255, 0.5);
border: 1px solid #eaeaea;
text-align: center;
line-height: 10vw;
`
document.body.append(btnNode)
setTimeout( () => {
btnNode.parentNode.removeChild(btnNode)
}, 3000)
}
function mount_button (parentDom, name, executer) {
const btnNode = document.createElement('button')
btnNode.innerText = name
btnNode.style.cssText = `
position: absolute;
right: 0px;
top: 5px;
`
btnNode.addEventListener('click', executer)
parentDom.append(btnNode)
}
function main () {
const targetNode = document.getElementById('react-root');
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList' && mutation.target?.innerText?.includes(i18n.like_title)) {
// console.log('mutation', mutation)
const domList = Array.from(mutation.target.getElementsByTagName('h2'))
const domTarget = domList.find(i => i.innerText === i18n.like_title)
if (domTarget) {
mount_button(domTarget, i18n.block_btn, block_all_likers)
}
}
// if (mutation.type === 'childList') {
// console.log('A child node has been added or removed.');
// } else if (mutation.type === 'attributes') {
// console.log(`The ${mutation.attributeName} attribute was modified.`);
// }
}
};
const observer = new MutationObserver(callback)
observer.observe(targetNode, config);
}
main()
})()