block CCP propaganda tweets likers

Block with love.

目前为 2023-08-06 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name block CCP propaganda tweets likers
  3. // @namespace https://eolstudy.com
  4. // @version 4.3.6
  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. // @require https://cdn.jsdelivr.net/npm/axios@0.25.0/dist/axios.min.js
  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.  
  21. function stringify(obj, sep, eq) {
  22. sep = sep || '&';
  23. eq = eq || '=';
  24. let str = "";
  25. for (var k in obj) {
  26. str += k + eq + decodeURIComponent(obj[k]) + sep
  27. }
  28. return str.slice(0, -1)
  29. };
  30.  
  31. function parse(str) {
  32. var obj = new Object();
  33. strs = str.split("&");
  34. for (var i = 0; i < strs.length; i++) {
  35. let index = strs[i].indexOf("=")
  36. obj[strs[i].slice(0, index)] = decodeURIComponent(strs[i].slice(index + 1));
  37. }
  38. return obj;
  39. }
  40.  
  41. //解析url地址
  42. function getRequest() {
  43. var url = location.search; //获取url中"?"符后的字串
  44. var theRequest = new Object();
  45. if (url.indexOf("?") != -1) {
  46. var str = url.substr(1);
  47. return parse(str)
  48. }
  49. }
  50.  
  51.  
  52. const translations = {
  53. // Please submit a feedback on Greasyfork.com if your language is not in the list bellow
  54. 'en': {
  55. lang_name: 'English',
  56. like_title: 'Liked by',
  57. like_list_identifier: 'Timeline: Liked by',
  58. retweet_title: 'Retweeted by',
  59. retweet_list_identifier: 'Timeline: Retweeted by',
  60. block_btn: 'Block Pinker',
  61. block_success: 'All users blocked!',
  62. mute_btn: 'Mute all',
  63. mute_success: 'All users muted!',
  64. include_original_tweeter: 'Include the original Tweeter',
  65. logs: 'Logs',
  66. list_members: 'List members',
  67. list_members_identifier: 'Timeline: List members',
  68. block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
  69. }
  70. }
  71. let i18n = translations.en
  72.  
  73. function get_theme_color (){
  74. const close_icon = $('div[aria-label] > div[dir="auto"] > svg[viewBox="0 0 24 24"]')[0]
  75. return window.getComputedStyle(close_icon).color
  76. }
  77.  
  78. async function sleep (ms = 50) {
  79. return new Promise(resolve => setTimeout(resolve, ms))
  80. }
  81.  
  82. function component_to_hex (c) {
  83. if (typeof(c) === 'string') c = Number(c)
  84. const hex = c.toString(16);
  85. return hex.length === 1 ? ("0" + hex) : hex;
  86. }
  87.  
  88. function rgb_to_hex (r, g, b) {
  89. return "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b);
  90. }
  91.  
  92. function get_cookie (cname) {
  93. let name = cname + '='
  94. let ca = document.cookie.split(';')
  95. for (let i = 0; i < ca.length; ++i) {
  96. let c = ca[i].trim()
  97. if (c.indexOf(name) === 0) {
  98. return c.substring(name.length, c.length)
  99. }
  100. }
  101. return ''
  102. }
  103.  
  104. function getStorage (name) {
  105. try {
  106. return window.JSON.parse(sessionStorage.getItem(name) || '[]')
  107. } catch (err) {
  108. sessionStorage.setItem(name, '[]')
  109. return []
  110. }
  111. }
  112.  
  113. function setStorage (name, val) {
  114. sessionStorage.setItem(name, window.JSON.stringify(val))
  115. }
  116.  
  117. function get_ancestor (dom, level) {
  118. for (let i = 0; i < level; ++i) {
  119. dom = dom.parent()
  120. }
  121. return dom
  122. }
  123.  
  124. const ajax = axios.create({
  125. baseURL: 'https://api.twitter.com',
  126. withCredentials: true,
  127. headers: {
  128. // 'User-Agent': '',
  129. 'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  130. 'X-Twitter-Auth-Type': 'OAuth2Session',
  131. 'X-Twitter-Active-User': 'yes',
  132. 'X-Csrf-Token': get_cookie('ct0')
  133. }
  134. })
  135.  
  136. // https://developer.twitter.com/en/docs/twitter-api/v1
  137. // users this user is following
  138. async function get_friends (userId) {
  139. const cachedFriends = getStorage('friends')
  140. if (cachedFriends && cachedFriends.length) {
  141. return cachedFriends
  142. }
  143. const my_id = get_cookie('twid').replace('u%3D', '')
  144. const users = await ajax.get(`/1.1/friends/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
  145. res => res.data.ids
  146. ) || []
  147. // console.log('get_friends', users)
  148. setStorage('friends', window.JSON.stringify(users))
  149. return users
  150. }
  151.  
  152. // users follow this user
  153. async function get_followers (userId) {
  154. const cachedUsers = getStorage('followers')
  155. if (cachedUsers && cachedUsers.length) {
  156. return cachedUsers
  157. }
  158. const my_id = get_cookie('twid').replace('u%3D', '')
  159. const users = await ajax.get(`/1.1/followers/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
  160. res => res.data.ids
  161. ) || []
  162. // console.log('get_followers', users)
  163. setStorage('followers', window.JSON.stringify(users))
  164. return users
  165. }
  166.  
  167. async function get_list_menber (listId) {
  168. const cachedUsers = getStorage('ccpmember')
  169. if (cachedUsers && cachedUsers.length) {
  170. return cachedUsers
  171. }
  172. const users = await ajax.get(`/1.1/lists/members.json?list_id=${listId}&count=5000`).then(
  173. res => res.data.users
  174. )
  175. // console.log('get_list_menber', users)
  176. const newUsers = (users || []).map(({ id_str }) => id_str)
  177. setStorage('ccpmember', window.JSON.stringify(newUsers))
  178. return newUsers
  179. }
  180.  
  181. // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/overview
  182. async function get_list_tweets(listId, tweets_number = 100, last_id) {
  183. 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}` : ''}`)
  184. // console.log('get_list_tweets res before ', last_id, ' is ', tweets_list_res.data)
  185. return (tweets_list_res || {}).data || []
  186. }
  187.  
  188. function get_tweet_id () {
  189. // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
  190. return location.href.split('status/')[1].split('/')[0]
  191. }
  192.  
  193. // function get_list_id () {
  194. // // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
  195. // return location.href.split('lists/')[1].split('/')[0]
  196. // }
  197.  
  198. // fetch_likers and fetch_no_comment_retweeters need to be merged into one function
  199. async function fetch_likers (tweetId) {
  200. const users = await ajax.get(`/2/timeline/liked_by.json?tweet_id=${tweetId}`).then(
  201. res => res.data.globalObjects.users
  202. )
  203.  
  204. let likers = []
  205. Object.keys(users).forEach(user => likers.push(user)) // keys of users are id strings
  206. return likers
  207. }
  208.  
  209. // async function fetch_no_comment_retweeters (tweetId) {
  210. // const users = (await ajax.get(`/2/timeline/retweeted_by.json?tweet_id=${tweetId}`)).data.globalObjects.users
  211.  
  212. // let targets = []
  213. // Object.keys(users).forEach(user => targets.push(user))
  214. // return targets
  215. // }
  216.  
  217. // async function fetch_list_members (listId) {
  218. // const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users
  219. // let members = []
  220. // members = users.map(u => u.id_str)
  221. // return members
  222. // }
  223.  
  224. function block_user (id) {
  225. // ajax.post('/1.1/blocks/create.json', Qs.stringify({
  226. ajax.post('/1.1/blocks/create.json', stringify({
  227. user_id: id
  228. }), {
  229. headers: {
  230. 'Content-Type': 'application/x-www-form-urlencoded'
  231. }
  232. })
  233. }
  234.  
  235. // function mute_user (id) {
  236. // ajax.post('/1.1/mutes/users/create.json', Qs.stringify({
  237. // user_id: id
  238. // }), {
  239. // headers: {
  240. // 'Content-Type': 'application/x-www-form-urlencoded'
  241. // }
  242. // })
  243. // }
  244.  
  245.  
  246. async function get_list_likers_a (tweets_list = [], pre_id, tweets_number_max = 5,count = 0) {
  247. const max_tweets_per_request = 2
  248. // let tweets_list = []
  249. let last_id = pre_id
  250.  
  251. if (tweets_list.length >= tweets_number_max) {
  252. return tweets_list
  253. }
  254. if (count >= 5) {
  255. return tweets_list
  256. }
  257.  
  258. let new_list
  259. new_list = await get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
  260. // it is so wired, code has TDZ con not access new_list before initialize
  261. // console.log('pre_id is ', pre_id, new_list)
  262. // console.log('new_list', new_list)
  263. (new_list || []).forEach(list_item => {
  264. const {id} = list_item || {}
  265. last_id = id
  266. tweets_list.push(list_item)
  267. })
  268. return await get_list_likers(tweets_list, last_id, tweets_number_max, count + 1)
  269. }
  270.  
  271.  
  272. async function get_list_likers (tweets_number_max = 1000,) {
  273. const max_tweets_per_request = Math.min(85, tweets_number_max)
  274. let tweets_list = []
  275. let last_id
  276.  
  277. const query_times = Math.ceil(tweets_number_max/max_tweets_per_request)
  278. const promise_sequence = Array.from(new Array(query_times + 1)).reduce(async (acc, cur) => {
  279. // const pre_res = await acc || []
  280. const [, pre_res = []] = await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
  281. const last_tweet = (pre_res.slice(-1) || [])[0] || {}
  282. const {id} = last_tweet
  283. // console.log('pre_res ', pre_res)
  284. last_id = id
  285. tweets_list = [...tweets_list, ...pre_res]
  286.  
  287. if (tweets_list.length >= tweets_number_max) {
  288. return Promise.resolve()
  289. } else {
  290. const new_request = get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
  291. return new_request
  292. }
  293. }, Promise.resolve())
  294. await promise_sequence
  295. console.log('get_list_likers end')
  296. return tweets_list
  297. }
  298.  
  299. let block_status
  300. // block_all_liker and block_no_comment_retweeters need to be merged
  301. async function block_all_pinker () {
  302. if (block_status) {
  303. return success_notice('', 'API limit, please wait')
  304. }
  305. block_status = 'running'
  306. // const tweetId = get_tweet_id()
  307. const [my_followers, my_friends, listMember] = await Promise.all([
  308. get_followers(),
  309. get_friends(),
  310. get_list_menber('1497432788634841089') // ccp propaganda list
  311. ])
  312.  
  313. // console.log('my_followers', my_followers)
  314. // console.log('my_friends', my_friends)
  315. // console.log('listMember', listMember)
  316.  
  317. // https://twitter.com/harry_shosta/status/1669140306481278977/likes
  318.  
  319. // const likers = await fetch_likers(tweetId)
  320. // console.log('likers', likers)
  321.  
  322. // console.log('newLikers', newLikers)
  323. // console.log('will not block ', likers.filter(id => !newLikers.includes(id)))
  324. // likers.forEach(id => block_user(id))
  325. // success_notice(i18n.like_list_identifier, i18n.block_success)
  326. try {
  327. let likers = []
  328.  
  329. if (window.location.href.match(/\d+\/likes$/)) {
  330. const tweetId = get_tweet_id()
  331. likers = await fetch_likers(tweetId)
  332.  
  333. // likers.forEach(id => block_user(id))
  334. // success_notice(i18n.like_list_identifier, i18n.block_success)
  335. }
  336.  
  337.  
  338. if (!window.location.href.match(/\d+\/likes$/)) {
  339. const block_like_threshlod = 99
  340. const query_tweets_number = 100
  341. const twetts_aa = await get_list_likers(query_tweets_number)
  342. console.log('twetts_aa ', twetts_aa)
  343. // twetts_aa.forEach(async (item, index) => {
  344. // index < 1 && console.log('twetts_aa item ', item)
  345. // const {id_str, favorite_count} = item
  346. // if (favorite_count > 99) {
  347. // const _likers = await fetch_likers(id_str)
  348. // _likers.forEach(likerId => likers.push(likerId))
  349. // }
  350. // })
  351. const promise_sequence = ([...twetts_aa, {}]).reduce(async (acc, cur) => {
  352. const pre_res = await acc || []
  353. likers = [...likers, ...pre_res]
  354. // const last_tweet = (pre_res.slice(-1) || [])[0] || {}
  355. const {id_str, favorite_count} = cur
  356. if (favorite_count > block_like_threshlod && id_str) {
  357. const new_request = await fetch_likers(id_str)
  358. // console.log(cur, new_request, '\r\n\r\n\r\n')
  359. return new_request
  360. } else {
  361. return Promise.resolve()
  362. }
  363. }, Promise.resolve())
  364. await promise_sequence
  365. }
  366.  
  367. const newLikers = likers.filter(id => {
  368. const flag_a = !my_followers.includes(id)
  369. const flag_b = !my_friends.includes(id)
  370. const flag_c = !listMember.includes(id)
  371. return flag_a && flag_b && flag_c
  372. })
  373. const pure_new_likers = Array.from(new Set(newLikers || []))
  374. console.log('pure_new_likers ', pure_new_likers)
  375. const block_sequence = ([...pure_new_likers, '']).reduce(async (acc, cur, index) => {
  376. await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
  377. if (index > 0 && index % 250 === 0) {
  378. // limit 300 per minute
  379. await sleep(15 * 60 * 1000)
  380. }
  381. let id = cur
  382. if (id) {
  383. block_notice(i18n.like_list_identifier, index, pure_new_likers.length)
  384. const new_request = block_user(id)
  385. return new_request
  386. } else {
  387. return Promise.resolve()
  388. }
  389. }, Promise.resolve())
  390. await block_sequence
  391. // likers.forEach(id => block_user(id))
  392. // success_notice(i18n.like_list_identifier, i18n.block_success)
  393. block_status = null
  394. } catch (err) {
  395. console.error(err)
  396. block_status = null
  397. success_notice('', 'block failed')
  398. }
  399.  
  400.  
  401. }
  402.  
  403.  
  404. function success_notice (identifier, success_msg) {
  405.  
  406. const btnNode = document.createElement('div')
  407. btnNode.innerText = success_msg
  408. btnNode.style.cssText = `
  409. position: absolute;
  410. left: calc(50% - 25vw);
  411. top: 40vw;
  412. width: 50vw;
  413. height: 10vw;
  414. background-color: rgba(255, 255, 255, 0.5);
  415. border: 1px solid #eaeaea;
  416. text-align: center;
  417. line-height: 10vw;
  418. `
  419. document.body.append(btnNode)
  420. setTimeout( () => {
  421. btnNode.parentNode.removeChild(btnNode)
  422. }, 3000)
  423. }
  424.  
  425. let timer_id
  426. function block_notice (identifier, current, total) {
  427. let btnNode = document.getElementById('block_root')
  428. if (!btnNode) {
  429. btnNode = document.createElement('div')
  430. btnNode.setAttribute('id', 'block_root')
  431. }
  432.  
  433. btnNode.innerText = `bocked ${current + 1} / ${total}`
  434. btnNode.style.cssText = `
  435. position: absolute;
  436. left: calc(50% - 25vw);
  437. top: 40vw;
  438. width: 50vw;
  439. height: 10vw;
  440. background-color: rgba(255, 255, 255, 0.5);
  441. border: 1px solid #eaeaea;
  442. text-align: center;
  443. line-height: 10vw;
  444. `
  445. document.body.append(btnNode)
  446. clearTimeout(timer_id)
  447. timer_id = setTimeout( () => {
  448. btnNode.parentNode.removeChild(btnNode)
  449. }, 5000)
  450. }
  451.  
  452. function mount_button (parentDom, name, executer) {
  453. const btnNode = document.createElement('button')
  454. btnNode.innerText = name
  455. btnNode.style.cssText = `
  456. position: absolute;
  457. right: 0px;
  458. top: 5px;
  459. `
  460. btnNode.addEventListener('click', executer)
  461. parentDom.append(btnNode)
  462. }
  463.  
  464.  
  465. function use_MutationObserver () {
  466.  
  467. const targetNode = document.getElementById('react-root');
  468. // Options for the observer (which mutations to observe)
  469. const config = { attributes: true, childList: true, subtree: true };
  470. // Callback function to execute when mutations are observed
  471. const callback = (mutationList, observer) => {
  472. for (const mutation of mutationList) {
  473.  
  474. if (mutation.type === 'childList' && mutation.target?.innerText?.includes(i18n.like_title)) {
  475. // console.log('mutation', mutation)
  476. const domList = Array.from(mutation.target.getElementsByTagName('h2'))
  477. const domTarget = domList.find(i => i.innerText === i18n.like_title)
  478. if (domTarget) {
  479. mount_button(domTarget, i18n.block_btn, block_all_pinker)
  480. }
  481. }
  482. // if (mutation.type === 'childList') {
  483. // console.log('A child node has been added or removed.');
  484. // } else if (mutation.type === 'attributes') {
  485. // console.log(`The ${mutation.attributeName} attribute was modified.`);
  486. // }
  487. }
  488. };
  489. const observer = new MutationObserver(callback)
  490. observer.observe(targetNode, config);
  491. }
  492.  
  493. const mount_root = document.getElementsByTagName('body')[0]
  494. mount_button(mount_root, i18n.block_btn, block_all_pinker)
  495. // main()
  496. })()