block CCP propaganda tweets likers

Block with love.

  1. // ==UserScript==
  2. // @name block CCP propaganda tweets likers
  3. // @namespace https://eolstudy.com
  4. // @version 4.5.7
  5. // @description Block with love.
  6. // @author amormaid
  7. // @run-at document-end
  8. // @grant GM_registerMenuCommand
  9. // @match https://twitter.com/*
  10. // @match https://mobile.twitter.com/*
  11. // @match https://tweetdeck.twitter.com/*
  12. // @exclude https://twitter.com/account/*
  13.  
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. /* Inspired by Twitter-Block-With-Love https://greasyfork.org/en/scripts/398540-twitter-block-with-love */
  18.  
  19. (_ => {
  20. // @require https://cdn.jsdelivr.net/npm/axios@0.25.0/dist/axios.min.js
  21.  
  22. let timer_id
  23. function block_notice (message) {
  24. let btnNode = document.getElementById('block_root')
  25. if (!btnNode) {
  26. btnNode = document.createElement('div')
  27. btnNode.setAttribute('id', 'block_root')
  28. }
  29.  
  30. btnNode.innerText = message
  31. btnNode.style.cssText = `
  32. position: fixed;
  33. left: calc(50% - 100px);
  34. top: calc(50vh - 60px);;
  35. width: 200px;
  36. height: 120px;
  37. background-color: rgba(255, 255, 255, 0.5);
  38. border: 1px solid #eaeaea;
  39. text-align: center;
  40. line-height: 120px;
  41. font-size: 16px;
  42. `
  43. document.body.append(btnNode)
  44. clearTimeout(timer_id)
  45. timer_id = setTimeout( () => {
  46. btnNode.parentNode.removeChild(btnNode)
  47. }, 5000)
  48. }
  49.  
  50.  
  51. class HTTPError extends Error {
  52. constructor(response, ...params) {
  53. // Pass remaining arguments (including vendor specific ones) to parent constructor
  54. super(...params);
  55.  
  56. this.response = response;
  57.  
  58. // Maintains proper stack trace for where our error was thrown (only available on V8)
  59. if (Error.captureStackTrace) {
  60. Error.captureStackTrace(this, HTTPError);
  61. }
  62.  
  63. this.name = 'HTTPError';
  64. }
  65. }
  66.  
  67. // window.axios = {};
  68. const ajax = {};
  69. ['get','post','put','delete'].forEach(requestType => {
  70. ajax[requestType] = function (url, data){
  71. return fetch(`https://api.twitter.com${url}`, {
  72. headers: {
  73. // modify Content-Type for /1.1/blocks/create.json
  74. "Content-Type": requestType.toUpperCase() === 'POST' ? 'application/x-www-form-urlencoded' : "application/json",
  75. "Accept": "application/json",
  76. //"X-Requested-With": "XMLHttpRequest",
  77. // "X-CSRF-Token": token.content
  78. 'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  79. 'X-Twitter-Auth-Type': 'OAuth2Session',
  80. 'X-Twitter-Active-User': 'yes',
  81. 'X-Csrf-Token': get_cookie('ct0')
  82.  
  83. },
  84. method: requestType,
  85. mode: "cors",
  86. credentials: "include",
  87. body: requestType.toUpperCase() === 'POST' ? stringify(data) : JSON.stringify(data),
  88. })
  89. .then( async response=>{
  90. if (!response.ok) {
  91. let data=await response.json();
  92. response.data = data;
  93. console.log(response);
  94. throw new HTTPError(response, response.statusText);
  95. }
  96. return response;
  97. }).then(response=>{
  98. return response.json();
  99. }).then(jsonResponse => {
  100. return {data:jsonResponse};
  101. });
  102. }
  103. });
  104.  
  105.  
  106. function stringify(obj, sep, eq) {
  107. sep = sep || '&';
  108. eq = eq || '=';
  109. let str = "";
  110. for (var k in obj) {
  111. str += k + eq + decodeURIComponent(obj[k]) + sep
  112. }
  113. return str.slice(0, -1)
  114. };
  115.  
  116. function parse(str) {
  117. var obj = new Object();
  118. strs = str.split("&");
  119. for (var i = 0; i < strs.length; i++) {
  120. let index = strs[i].indexOf("=")
  121. obj[strs[i].slice(0, index)] = decodeURIComponent(strs[i].slice(index + 1));
  122. }
  123. return obj;
  124. }
  125.  
  126. //解析url地址
  127. function getRequest() {
  128. var url = location.search;
  129. var theRequest = new Object();
  130. if (url.indexOf("?") != -1) {
  131. var str = url.substr(1);
  132. return parse(str)
  133. }
  134. }
  135.  
  136. function get_theme_color (){
  137. const close_icon = $('div[aria-label] > div[dir="auto"] > svg[viewBox="0 0 24 24"]')[0]
  138. return window.getComputedStyle(close_icon).color
  139. }
  140.  
  141. async function sleep (ms = 50) {
  142. return new Promise(resolve => setTimeout(resolve, ms))
  143. }
  144.  
  145. function component_to_hex (c) {
  146. if (typeof(c) === 'string') c = Number(c)
  147. const hex = c.toString(16);
  148. return hex.length === 1 ? ("0" + hex) : hex;
  149. }
  150.  
  151. function rgb_to_hex (r, g, b) {
  152. return "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b);
  153. }
  154.  
  155. function get_cookie (cname) {
  156. let name = cname + '='
  157. let ca = document.cookie.split(';')
  158. for (let i = 0; i < ca.length; ++i) {
  159. let c = ca[i].trim()
  160. if (c.indexOf(name) === 0) {
  161. return c.substring(name.length, c.length)
  162. }
  163. }
  164. return ''
  165. }
  166.  
  167. function getStorage (name) {
  168. try {
  169. return window.JSON.parse(sessionStorage.getItem(name) || '[]')
  170. } catch (err) {
  171. sessionStorage.setItem(name, '[]')
  172. return []
  173. }
  174. }
  175.  
  176. function setStorage (name, val) {
  177. sessionStorage.setItem(name, window.JSON.stringify(val))
  178. }
  179.  
  180. function get_ancestor (dom, level) {
  181. for (let i = 0; i < level; ++i) {
  182. dom = dom.parent()
  183. }
  184. return dom
  185. }
  186.  
  187. // const ajax = axios.create({
  188. // baseURL: 'https://api.twitter.com',
  189. // withCredentials: true,
  190. // headers: {
  191. // // 'User-Agent': '',
  192. // 'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  193. // 'X-Twitter-Auth-Type': 'OAuth2Session',
  194. // 'X-Twitter-Active-User': 'yes',
  195. // 'X-Csrf-Token': get_cookie('ct0')
  196. // }
  197. // })
  198.  
  199. // https://developer.twitter.com/en/docs/twitter-api/v1
  200. async function get_user_timeline (screen_name) {
  201. // https://api.twitter.com/1.1/statuses/user_timeline.json
  202. const tweets_list = await ajax.get(`/1.1/statuses/user_timeline.json?screen_name=${screen_name}&count=5000&stringify_ids=true`).then(
  203. res => res.data
  204. ) || []
  205. return tweets_list
  206. }
  207. // users this user is following
  208. async function get_friends (userId) {
  209. const cachedFriends = getStorage('friends')
  210. if (cachedFriends && cachedFriends.length) {
  211. return cachedFriends
  212. }
  213. const my_id = get_cookie('twid').replace('u%3D', '')
  214. const users = await ajax.get(`/1.1/friends/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
  215. res => res.data.ids
  216. ) || []
  217. // console.log('get_friends', users)
  218. setStorage('friends', window.JSON.stringify(users))
  219. return users
  220. }
  221.  
  222. // users follow this user
  223. async function get_followers (userId) {
  224. const cachedUsers = getStorage('followers')
  225. if (cachedUsers && cachedUsers.length) {
  226. return cachedUsers
  227. }
  228. const my_id = get_cookie('twid').replace('u%3D', '')
  229. const users = await ajax.get(`/1.1/followers/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
  230. res => res.data.ids
  231. ) || []
  232. // console.log('get_followers', users)
  233. setStorage('followers', window.JSON.stringify(users))
  234. return users
  235. }
  236.  
  237. async function get_list_menber (listId) {
  238. const cachedUsers = getStorage('ccpmember')
  239. if (cachedUsers && cachedUsers.length) {
  240. return cachedUsers
  241. }
  242. const users = await ajax.get(`/1.1/lists/members.json?list_id=${listId}&count=5000`).then(
  243. res => res.data.users
  244. )
  245. // console.log('get_list_menber', users)
  246. const newUsers = (users || []).map(({ id_str }) => id_str)
  247. setStorage('ccpmember', window.JSON.stringify(newUsers))
  248. return newUsers
  249. }
  250.  
  251. // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/overview
  252. async function get_list_tweets(listId, tweets_number = 100, last_id) {
  253. 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}` : ''}`)
  254. // console.log('get_list_tweets res before ', last_id, ' is ', tweets_list_res.data)
  255. return (tweets_list_res || {}).data || []
  256. }
  257.  
  258. function get_tweet_id () {
  259. // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
  260. return location.href.split('status/')[1].split('/')[0]
  261. }
  262.  
  263. // function get_list_id () {
  264. // // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
  265. // return location.href.split('lists/')[1].split('/')[0]
  266. // }
  267.  
  268. // fetch_likers and fetch_no_comment_retweeters need to be merged into one function
  269. async function fetch_likers (tweetId) {
  270. const users = await ajax.get(`/2/timeline/liked_by.json?tweet_id=${tweetId}`).then(
  271. res => res.data.globalObjects.users
  272. )
  273.  
  274. let likers = []
  275. Object.keys(users).forEach(user => likers.push(user)) // keys of users are id strings
  276. return likers
  277. }
  278.  
  279. // async function fetch_no_comment_retweeters (tweetId) {
  280. // const users = (await ajax.get(`/2/timeline/retweeted_by.json?tweet_id=${tweetId}`)).data.globalObjects.users
  281.  
  282. // let targets = []
  283. // Object.keys(users).forEach(user => targets.push(user))
  284. // return targets
  285. // }
  286.  
  287. // async function fetch_list_members (listId) {
  288. // const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users
  289. // let members = []
  290. // members = users.map(u => u.id_str)
  291. // return members
  292. // }
  293.  
  294. function block_user (id) {
  295. // ajax.post('/1.1/blocks/create.json', Qs.stringify({
  296. // ajax.post('/1.1/blocks/create.json', stringify({
  297. // user_id: id
  298. // }), {
  299. // headers: {
  300. // 'Content-Type': 'application/x-www-form-urlencoded'
  301. // }
  302. // })
  303. ajax.post('/1.1/blocks/create.json', {
  304. user_id: id
  305. })
  306. }
  307.  
  308. // function mute_user (id) {
  309. // ajax.post('/1.1/mutes/users/create.json', Qs.stringify({
  310. // user_id: id
  311. // }), {
  312. // headers: {
  313. // 'Content-Type': 'application/x-www-form-urlencoded'
  314. // }
  315. // })
  316. // }
  317.  
  318.  
  319. async function get_list_likers_a (tweets_list = [], pre_id, tweets_number_max = 5,count = 0) {
  320. const max_tweets_per_request = 2
  321. // let tweets_list = []
  322. let last_id = pre_id
  323.  
  324. if (tweets_list.length >= tweets_number_max) {
  325. return tweets_list
  326. }
  327. if (count >= 5) {
  328. return tweets_list
  329. }
  330.  
  331. let new_list
  332. new_list = await get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
  333. // it is so wired, code has TDZ con not access new_list before initialize
  334. // console.log('pre_id is ', pre_id, new_list)
  335. // console.log('new_list', new_list)
  336. (new_list || []).forEach(list_item => {
  337. const {id} = list_item || {}
  338. last_id = id
  339. tweets_list.push(list_item)
  340. })
  341. return await get_list_likers(tweets_list, last_id, tweets_number_max, count + 1)
  342. }
  343.  
  344. let last_id
  345. async function get_list_likers (tweets_number_max = 1000,) {
  346. const max_tweets_per_request = Math.min(85, tweets_number_max)
  347. let tweets_list = []
  348. // let last_id
  349.  
  350. const query_times = Math.ceil(tweets_number_max/max_tweets_per_request)
  351. const promise_sequence = Array.from(new Array(query_times + 1)).reduce(async (acc, cur) => {
  352. // const pre_res = await acc || []
  353. const [, pre_res = []] = await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
  354. const last_tweet = (pre_res.slice(-1) || [])[0] || {}
  355. const {id} = last_tweet
  356. // console.log('pre_res ', pre_res)
  357. last_id = id
  358. tweets_list = [...tweets_list, ...pre_res]
  359.  
  360. if (tweets_list.length >= tweets_number_max) {
  361. return Promise.resolve()
  362. } else {
  363. const new_request = get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
  364. return new_request
  365. }
  366. }, Promise.resolve())
  367. await promise_sequence
  368. console.log('get_list_likers end')
  369. return tweets_list
  370. }
  371.  
  372. let last_run
  373. let block_list_cache = []
  374. const block_like_threshlod = 50 // 99
  375. const block_number_per_batch = 60
  376. const query_tweets_number = 120 // 100
  377.  
  378. // block_all_liker and block_no_comment_retweeters need to be merged
  379. async function block_all_pinker () {
  380. if (last_run && (new Date().getTime() - last_run < 30 * 1000)) {
  381. return block_notice('API limit, please wait')
  382. }
  383. block_notice('blocker running !')
  384. last_run = new Date().getTime()
  385.  
  386. try {
  387. let likers = []
  388.  
  389. // at likes page, block likes user
  390. if (window.location.href.match(/\d+\/likes$/)) {
  391. const tweetId = get_tweet_id()
  392. likers = await fetch_likers(tweetId)
  393. }
  394.  
  395.  
  396.  
  397. if (!window.location.href.match(/\d+\/likes$/)) {
  398. let twetts_asemble = []
  399. // at home page, block list tweets likes user
  400. if (window.location.pathname.includes('lists/')) {
  401. // at list menber page, block member tweets likes user
  402. twetts_asemble = await get_user_timeline(window.location.pathname.replace('/', ''))
  403. console.log('get twetts_asemble ', twetts_asemble.length)
  404. } else {
  405. return
  406. }
  407.  
  408. likers = await get_likers_by_tweet_list(twetts_asemble)
  409.  
  410. }
  411.  
  412. await block_action(likers)
  413.  
  414. // likers.forEach(id => block_user(id))
  415. // last_run = null
  416.  
  417.  
  418. } catch (err) {
  419. console.error(err)
  420. // last_run = null
  421. block_notice('block failed')
  422. }
  423. }
  424.  
  425. async function block_all_pinker_home () {
  426. last_run = last_run || (localStorage.getItem('last_run') - 0)
  427. if (last_run && (new Date().getTime() - last_run < 20 * 60 * 1000)) {
  428. return
  429. // block_notice('API limit, please wait')
  430. }
  431. // block_notice('blocker running !')
  432. last_run = new Date().getTime()
  433. // const tweetId = get_tweet_id()
  434. try {
  435. // at home page, block list tweets likes user
  436.  
  437. const twetts_asemble = await get_list_likers(query_tweets_number)
  438. console.log('twetts_asemble ', twetts_asemble.length)
  439. const likers = await get_likers_by_tweet_list(twetts_asemble)
  440.  
  441. const pure_new_likers = await block_action(Array.from(new Set([...block_list_cache, ...likers])))
  442.  
  443. block_list_cache = pure_new_likers.slice(block_number_per_batch)
  444. console.log('block_list_cache has ', block_list_cache.length, ' users')
  445. console.log(new Date().toLocaleString() + '\r\n\r\n')
  446. setTimeout(block_all_pinker_home, 20 * 60 * 1000)
  447. localStorage.setItem('last_run', new Date().getTime())
  448. } catch (err) {
  449. console.error(err)
  450. // last_run = null
  451. block_notice('block failed')
  452. } finally {
  453.  
  454. }
  455.  
  456.  
  457. }
  458.  
  459. async function get_likers_by_tweet_list(twetts_asemble) {
  460. let likers = []
  461. const promise_sequence = ([...twetts_asemble, {}]).reduce(async (acc, cur, index) => {
  462. const pre_res = await acc || []
  463. likers = [...likers, ...pre_res]
  464. // const last_tweet = (pre_res.slice(-1) || [])[0] || {}
  465. const {id_str, favorite_count} = cur
  466. // only query 100th ~ 200th tweet liker
  467. if (favorite_count > block_like_threshlod && id_str && index > 100) {
  468. const new_request = await fetch_likers(id_str)
  469. // console.log(cur, new_request, '\r\n\r\n\r\n')
  470. return new_request
  471. } else {
  472. return Promise.resolve()
  473. }
  474. }, Promise.resolve())
  475. await promise_sequence
  476. console.log('find ', likers.length, ' pinkers in ', twetts_asemble.length, ' tweets')
  477. return likers
  478. }
  479.  
  480. async function block_action(likers) {
  481.  
  482. const [my_followers, my_friends, listMember] = await Promise.all([
  483. get_followers(),
  484. get_friends(),
  485. get_list_menber('1497432788634841089') // ccp propaganda list
  486. ])
  487.  
  488. const newLikers = likers.filter(id => {
  489. const flag_a = !my_followers.includes(id)
  490. const flag_b = !my_friends.includes(id)
  491. const flag_c = !listMember.includes(id)
  492. return flag_a && flag_b && flag_c
  493. })
  494.  
  495. const pure_new_likers = Array.from(new Set(newLikers || []))
  496.  
  497. console.log('pure_new_likers ', pure_new_likers)
  498. const block_sequence = ([...pure_new_likers, '']).reduce(async (acc, cur, index) => {
  499. await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
  500. // if (index === 80 || index === 350 || index === 700) {
  501. // // limit 300 per minute
  502. // await sleep(15 * 60 * 1000)
  503. // }
  504. let id = cur
  505. if (id && index < block_number_per_batch) {
  506. block_notice(`bocked ${index + 1} / ${pure_new_likers.length}`)
  507. const new_request = block_user(id)
  508. return new_request
  509. } else {
  510. return Promise.resolve()
  511. }
  512. }, Promise.resolve())
  513. await block_sequence
  514. return pure_new_likers
  515. }
  516.  
  517.  
  518.  
  519. function mount_button (parentDom, name, executer) {
  520. const btnNode = document.createElement('button')
  521. btnNode.innerText = name
  522. btnNode.style.cssText = `
  523. position: fixed;
  524. right: 0px;
  525. top: 5px;
  526. `
  527. btnNode.addEventListener('click', executer)
  528. parentDom.append(btnNode)
  529. }
  530.  
  531.  
  532. // function use_MutationObserver () {
  533.  
  534. // const targetNode = document.getElementById('react-root');
  535. // // Options for the observer (which mutations to observe)
  536. // const config = { attributes: true, childList: true, subtree: true };
  537. // // Callback function to execute when mutations are observed
  538. // const callback = (mutationList, observer) => {
  539. // for (const mutation of mutationList) {
  540.  
  541. // if (mutation.type === 'childList' && mutation.target?.innerText?.includes('liked by')) {
  542. // // console.log('mutation', mutation)
  543. // const domList = Array.from(mutation.target.getElementsByTagName('h2'))
  544. // const domTarget = domList.find(i => i.innerText === 'liked by')
  545. // if (domTarget) {
  546. // mount_button(domTarget, 'Block Pinker', block_all_pinker)
  547. // }
  548. // }
  549. // // if (mutation.type === 'childList') {
  550. // // console.log('A child node has been added or removed.');
  551. // // } else if (mutation.type === 'attributes') {
  552. // // console.log(`The ${mutation.attributeName} attribute was modified.`);
  553. // // }
  554. // }
  555. // };
  556. // const observer = new MutationObserver(callback)
  557. // observer.observe(targetNode, config);
  558. // }
  559.  
  560. if (window.top === window) {
  561. const mount_root = document.getElementsByTagName('body')[0]
  562. mount_button(mount_root, 'Block Pinker', block_all_pinker)
  563. setTimeout(block_all_pinker_home, 10 * 1000)
  564. }
  565.  
  566. // setTimeout(block_all_pinker_home, 5 * 1000)
  567. // use_MutationObserver()
  568. // main()
  569. })()