Greasy Fork 支持简体中文。

block CCP propaganda tweets likers

Block with love.

目前為 2023-08-08 提交的版本,檢視 最新版本

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