// ==UserScript==
// @name block CCP propaganda tweets likers
// @namespace https://eolstudy.com
// @version 4.5.7
// @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/*
// @license MIT
// ==/UserScript==
/* Inspired by Twitter-Block-With-Love https://greasyfork.org/en/scripts/398540-twitter-block-with-love */
(_ => {
// @require https://cdn.jsdelivr.net/npm/axios@0.25.0/dist/axios.min.js
let timer_id
function block_notice (message) {
let btnNode = document.getElementById('block_root')
if (!btnNode) {
btnNode = document.createElement('div')
btnNode.setAttribute('id', 'block_root')
}
btnNode.innerText = message
btnNode.style.cssText = `
position: fixed;
left: calc(50% - 100px);
top: calc(50vh - 60px);;
width: 200px;
height: 120px;
background-color: rgba(255, 255, 255, 0.5);
border: 1px solid #eaeaea;
text-align: center;
line-height: 120px;
font-size: 16px;
`
document.body.append(btnNode)
clearTimeout(timer_id)
timer_id = setTimeout( () => {
btnNode.parentNode.removeChild(btnNode)
}, 5000)
}
class HTTPError extends Error {
constructor(response, ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
this.response = response;
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HTTPError);
}
this.name = 'HTTPError';
}
}
// window.axios = {};
const ajax = {};
['get','post','put','delete'].forEach(requestType => {
ajax[requestType] = function (url, data){
return fetch(`https://api.twitter.com${url}`, {
headers: {
// modify Content-Type for /1.1/blocks/create.json
"Content-Type": requestType.toUpperCase() === 'POST' ? 'application/x-www-form-urlencoded' : "application/json",
"Accept": "application/json",
//"X-Requested-With": "XMLHttpRequest",
// "X-CSRF-Token": token.content
'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
'X-Twitter-Auth-Type': 'OAuth2Session',
'X-Twitter-Active-User': 'yes',
'X-Csrf-Token': get_cookie('ct0')
},
method: requestType,
mode: "cors",
credentials: "include",
body: requestType.toUpperCase() === 'POST' ? stringify(data) : JSON.stringify(data),
})
.then( async response=>{
if (!response.ok) {
let data=await response.json();
response.data = data;
console.log(response);
throw new HTTPError(response, response.statusText);
}
return response;
}).then(response=>{
return response.json();
}).then(jsonResponse => {
return {data:jsonResponse};
});
}
});
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;
var theRequest = new Object();
if (url.indexOf("?") != -1) {
var str = url.substr(1);
return parse(str)
}
}
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
}
async function sleep (ms = 50) {
return new Promise(resolve => setTimeout(resolve, ms))
}
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: {
// // 'User-Agent': '',
// 'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
// 'X-Twitter-Auth-Type': 'OAuth2Session',
// 'X-Twitter-Active-User': 'yes',
// 'X-Csrf-Token': get_cookie('ct0')
// }
// })
// https://developer.twitter.com/en/docs/twitter-api/v1
async function get_user_timeline (screen_name) {
// https://api.twitter.com/1.1/statuses/user_timeline.json
const tweets_list = await ajax.get(`/1.1/statuses/user_timeline.json?screen_name=${screen_name}&count=5000&stringify_ids=true`).then(
res => res.data
) || []
return tweets_list
}
// 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
}
// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/overview
async function get_list_tweets(listId, tweets_number = 100, last_id) {
const tweets_list_res = await ajax.get(`/1.1/lists/statuses.json?list_id=${listId}&include_rts=false&count=${tweets_number}${last_id ? `&max_id=${last_id}` : ''}`)
// console.log('get_list_tweets res before ', last_id, ' is ', tweets_list_res.data)
return (tweets_list_res || {}).data || []
}
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'
// }
// })
ajax.post('/1.1/blocks/create.json', {
user_id: id
})
}
// 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'
// }
// })
// }
async function get_list_likers_a (tweets_list = [], pre_id, tweets_number_max = 5,count = 0) {
const max_tweets_per_request = 2
// let tweets_list = []
let last_id = pre_id
if (tweets_list.length >= tweets_number_max) {
return tweets_list
}
if (count >= 5) {
return tweets_list
}
let new_list
new_list = await get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
// it is so wired, code has TDZ con not access new_list before initialize
// console.log('pre_id is ', pre_id, new_list)
// console.log('new_list', new_list)
(new_list || []).forEach(list_item => {
const {id} = list_item || {}
last_id = id
tweets_list.push(list_item)
})
return await get_list_likers(tweets_list, last_id, tweets_number_max, count + 1)
}
let last_id
async function get_list_likers (tweets_number_max = 1000,) {
const max_tweets_per_request = Math.min(85, tweets_number_max)
let tweets_list = []
// let last_id
const query_times = Math.ceil(tweets_number_max/max_tweets_per_request)
const promise_sequence = Array.from(new Array(query_times + 1)).reduce(async (acc, cur) => {
// const pre_res = await acc || []
const [, pre_res = []] = await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
const last_tweet = (pre_res.slice(-1) || [])[0] || {}
const {id} = last_tweet
// console.log('pre_res ', pre_res)
last_id = id
tweets_list = [...tweets_list, ...pre_res]
if (tweets_list.length >= tweets_number_max) {
return Promise.resolve()
} else {
const new_request = get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
return new_request
}
}, Promise.resolve())
await promise_sequence
console.log('get_list_likers end')
return tweets_list
}
let last_run
let block_list_cache = []
const block_like_threshlod = 50 // 99
const block_number_per_batch = 60
const query_tweets_number = 120 // 100
// block_all_liker and block_no_comment_retweeters need to be merged
async function block_all_pinker () {
if (last_run && (new Date().getTime() - last_run < 30 * 1000)) {
return block_notice('API limit, please wait')
}
block_notice('blocker running !')
last_run = new Date().getTime()
try {
let likers = []
// at likes page, block likes user
if (window.location.href.match(/\d+\/likes$/)) {
const tweetId = get_tweet_id()
likers = await fetch_likers(tweetId)
}
if (!window.location.href.match(/\d+\/likes$/)) {
let twetts_asemble = []
// at home page, block list tweets likes user
if (window.location.pathname.includes('lists/')) {
// at list menber page, block member tweets likes user
twetts_asemble = await get_user_timeline(window.location.pathname.replace('/', ''))
console.log('get twetts_asemble ', twetts_asemble.length)
} else {
return
}
likers = await get_likers_by_tweet_list(twetts_asemble)
}
await block_action(likers)
// likers.forEach(id => block_user(id))
// last_run = null
} catch (err) {
console.error(err)
// last_run = null
block_notice('block failed')
}
}
async function block_all_pinker_home () {
last_run = last_run || (localStorage.getItem('last_run') - 0)
if (last_run && (new Date().getTime() - last_run < 20 * 60 * 1000)) {
return
// block_notice('API limit, please wait')
}
// block_notice('blocker running !')
last_run = new Date().getTime()
// const tweetId = get_tweet_id()
try {
// at home page, block list tweets likes user
const twetts_asemble = await get_list_likers(query_tweets_number)
console.log('twetts_asemble ', twetts_asemble.length)
const likers = await get_likers_by_tweet_list(twetts_asemble)
const pure_new_likers = await block_action(Array.from(new Set([...block_list_cache, ...likers])))
block_list_cache = pure_new_likers.slice(block_number_per_batch)
console.log('block_list_cache has ', block_list_cache.length, ' users')
console.log(new Date().toLocaleString() + '\r\n\r\n')
setTimeout(block_all_pinker_home, 20 * 60 * 1000)
localStorage.setItem('last_run', new Date().getTime())
} catch (err) {
console.error(err)
// last_run = null
block_notice('block failed')
} finally {
}
}
async function get_likers_by_tweet_list(twetts_asemble) {
let likers = []
const promise_sequence = ([...twetts_asemble, {}]).reduce(async (acc, cur, index) => {
const pre_res = await acc || []
likers = [...likers, ...pre_res]
// const last_tweet = (pre_res.slice(-1) || [])[0] || {}
const {id_str, favorite_count} = cur
// only query 100th ~ 200th tweet liker
if (favorite_count > block_like_threshlod && id_str && index > 100) {
const new_request = await fetch_likers(id_str)
// console.log(cur, new_request, '\r\n\r\n\r\n')
return new_request
} else {
return Promise.resolve()
}
}, Promise.resolve())
await promise_sequence
console.log('find ', likers.length, ' pinkers in ', twetts_asemble.length, ' tweets')
return likers
}
async function block_action(likers) {
const [my_followers, my_friends, listMember] = await Promise.all([
get_followers(),
get_friends(),
get_list_menber('1497432788634841089') // ccp propaganda list
])
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
})
const pure_new_likers = Array.from(new Set(newLikers || []))
console.log('pure_new_likers ', pure_new_likers)
const block_sequence = ([...pure_new_likers, '']).reduce(async (acc, cur, index) => {
await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
// if (index === 80 || index === 350 || index === 700) {
// // limit 300 per minute
// await sleep(15 * 60 * 1000)
// }
let id = cur
if (id && index < block_number_per_batch) {
block_notice(`bocked ${index + 1} / ${pure_new_likers.length}`)
const new_request = block_user(id)
return new_request
} else {
return Promise.resolve()
}
}, Promise.resolve())
await block_sequence
return pure_new_likers
}
function mount_button (parentDom, name, executer) {
const btnNode = document.createElement('button')
btnNode.innerText = name
btnNode.style.cssText = `
position: fixed;
right: 0px;
top: 5px;
`
btnNode.addEventListener('click', executer)
parentDom.append(btnNode)
}
// function use_MutationObserver () {
// 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('liked by')) {
// // console.log('mutation', mutation)
// const domList = Array.from(mutation.target.getElementsByTagName('h2'))
// const domTarget = domList.find(i => i.innerText === 'liked by')
// if (domTarget) {
// mount_button(domTarget, 'Block Pinker', block_all_pinker)
// }
// }
// // 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);
// }
if (window.top === window) {
const mount_root = document.getElementsByTagName('body')[0]
mount_button(mount_root, 'Block Pinker', block_all_pinker)
setTimeout(block_all_pinker_home, 10 * 1000)
}
// setTimeout(block_all_pinker_home, 5 * 1000)
// use_MutationObserver()
// main()
})()