Greasy Fork 支持简体中文。

allen's block

Block with love.

目前為 2022-06-17 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name allen's block
  3. // @namespace https://eolstudy.com
  4. // @version 3.0.1
  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. // @require https://cdn.jsdelivr.net/npm/qs@6.10.3/dist/qs.min.js
  15. // @require https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. /* global axios $ Qs */
  20.  
  21. (_ => {
  22. /* Begin of Dependencies */
  23.  
  24. // https://gist.githubusercontent.com/BrockA/2625891/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js
  25. /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
  26. that detects and handles AJAXed content.
  27.  
  28. Usage example:
  29.  
  30. waitForKeyElements (
  31. "div.comments"
  32. , commentCallbackFunction
  33. );
  34.  
  35. //--- Page-specific function to do what we want when the node is found.
  36. function commentCallbackFunction (jNode) {
  37. jNode.text ("This comment changed by waitForKeyElements().");
  38. }
  39.  
  40. IMPORTANT: This function requires your script to have loaded jQuery.
  41. */
  42. function waitForKeyElements (
  43. selectorTxt, /* Required: The jQuery selector string that
  44. specifies the desired element(s).
  45. */
  46. actionFunction, /* Required: The code to run when elements are
  47. found. It is passed a jNode to the matched
  48. element.
  49. */
  50. bWaitOnce, /* Optional: If false, will continue to scan for
  51. new elements even after the first match is
  52. found.
  53. */
  54. iframeSelector /* Optional: If set, identifies the iframe to
  55. search.
  56. */
  57. ) {
  58. var targetNodes, btargetsFound;
  59.  
  60. if (typeof iframeSelector == "undefined")
  61. targetNodes = $(selectorTxt);
  62. else
  63. targetNodes = $(iframeSelector).contents ()
  64. .find (selectorTxt);
  65.  
  66. if (targetNodes && targetNodes.length > 0) {
  67. btargetsFound = true;
  68. /*--- Found target node(s). Go through each and act if they
  69. are new.
  70. */
  71. targetNodes.each ( function () {
  72. var jThis = $(this);
  73. var alreadyFound = jThis.data ('alreadyFound') || false;
  74.  
  75. if (!alreadyFound) {
  76. //--- Call the payload function.
  77. var cancelFound = actionFunction (jThis);
  78. if (cancelFound)
  79. btargetsFound = false;
  80. else
  81. jThis.data ('alreadyFound', true);
  82. }
  83. } );
  84. }
  85. else {
  86. btargetsFound = false;
  87. }
  88.  
  89. //--- Get the timer-control variable for this selector.
  90. var controlObj = waitForKeyElements.controlObj || {};
  91. var controlKey = selectorTxt.replace (/[^\w]/g, "_");
  92. var timeControl = controlObj [controlKey];
  93.  
  94. //--- Now set or clear the timer as appropriate.
  95. if (btargetsFound && bWaitOnce && timeControl) {
  96. //--- The only condition where we need to clear the timer.
  97. clearInterval (timeControl);
  98. delete controlObj [controlKey]
  99. }
  100. else {
  101. //--- Set a timer, if needed.
  102. if ( ! timeControl) {
  103. timeControl = setInterval ( function () {
  104. waitForKeyElements ( selectorTxt,
  105. actionFunction,
  106. bWaitOnce,
  107. iframeSelector
  108. );
  109. },
  110. 300
  111. );
  112. controlObj [controlKey] = timeControl;
  113. }
  114. }
  115. waitForKeyElements.controlObj = controlObj;
  116. }
  117. /* End of Dependencies */
  118.  
  119.  
  120. const translations = {
  121. // Please submit a feedback on Greasyfork.com if your language is not in the list bellow
  122. 'en': {
  123. lang_name: 'English',
  124. like_title: 'Liked by',
  125. like_list_identifier: 'Timeline: Liked by',
  126. retweet_title: 'Retweeted by',
  127. retweet_list_identifier: 'Timeline: Retweeted by',
  128. block_btn: 'Block all',
  129. block_success: 'All users blocked!',
  130. mute_btn: 'Mute all',
  131. mute_success: 'All users muted!',
  132. include_original_tweeter: 'Include the original Tweeter',
  133. logs: 'Logs',
  134. list_members: 'List members',
  135. list_members_identifier: 'Timeline: List members',
  136. block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
  137. }
  138. }
  139. let i18n = translations.en
  140.  
  141. function get_theme_color (){
  142. const close_icon = $('div[aria-label] > div[dir="auto"] > svg[viewBox="0 0 24 24"]')[0]
  143. return window.getComputedStyle(close_icon).color
  144. }
  145.  
  146. function component_to_hex (c) {
  147. if (typeof(c) === 'string') c = Number(c)
  148. const hex = c.toString(16);
  149. return hex.length === 1 ? ("0" + hex) : hex;
  150. }
  151.  
  152. function rgb_to_hex (r, g, b) {
  153. return "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b);
  154. }
  155.  
  156. function get_cookie (cname) {
  157. let name = cname + '='
  158. let ca = document.cookie.split(';')
  159. for (let i = 0; i < ca.length; ++i) {
  160. let c = ca[i].trim()
  161. if (c.indexOf(name) === 0) {
  162. return c.substring(name.length, c.length)
  163. }
  164. }
  165. return ''
  166. }
  167.  
  168. function get_ancestor (dom, level) {
  169. for (let i = 0; i < level; ++i) {
  170. dom = dom.parent()
  171. }
  172. return dom
  173. }
  174.  
  175. const ajax = axios.create({
  176. baseURL: 'https://api.twitter.com',
  177. withCredentials: true,
  178. headers: {
  179. 'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  180. 'X-Twitter-Auth-Type': 'OAuth2Session',
  181. 'X-Twitter-Active-User': 'yes',
  182. 'X-Csrf-Token': get_cookie('ct0')
  183. }
  184. })
  185. // users this user is following
  186. async function get_friends (userId) {
  187. const cachedFriends = window.JSON.parse(sessionStorage.getItem('friends') || '[]')
  188. if (cachedFriends && cachedFriends.length) {
  189. return cachedFriends
  190. }
  191. const my_id = get_cookie('twid').replace('u%3D', '')
  192. const users = await ajax.get(`/1.1/friends/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
  193. res => res.data.ids
  194. )
  195. // console.log('get_friends', users)
  196. sessionStorage.setItem('friends', window.JSON.stringify(users))
  197. return users
  198. }
  199. // users follow this user
  200. async function get_followers (userId) {
  201. const cachedUsers = window.JSON.parse(sessionStorage.getItem('followers') || '[]')
  202. if (cachedUsers && cachedUsers.length) {
  203. return cachedUsers
  204. }
  205. const my_id = get_cookie('twid').replace('u%3D', '')
  206. const users = await ajax.get(`/1.1/followers/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
  207. res => res.data.ids
  208. )
  209. // console.log('get_followers', users)
  210. sessionStorage.setItem('followers', window.JSON.stringify(users))
  211. return users
  212. }
  213.  
  214. async function get_list_menber (listId) {
  215. const cachedUsers = window.JSON.parse(sessionStorage.getItem('ccpmember') || '[]')
  216. if (cachedUsers && cachedUsers.length) {
  217. return cachedUsers
  218. }
  219. const users = await ajax.get(`/1.1/lists/members.json?list_id=${listId}&count=5000`).then(
  220. res => res.data.users
  221. )
  222. // console.log('get_list_menber', users)
  223. const newUsers = (users || []).map(({ id_str }) => id_str)
  224. sessionStorage.setItem('ccpmember', window.JSON.stringify(newUsers))
  225. return newUsers
  226. }
  227.  
  228. function get_tweet_id () {
  229. // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
  230. return location.href.split('status/')[1].split('/')[0]
  231. }
  232.  
  233. function get_list_id () {
  234. // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
  235. return location.href.split('lists/')[1].split('/')[0]
  236. }
  237.  
  238. // fetch_likers and fetch_no_comment_retweeters need to be merged into one function
  239. async function fetch_likers (tweetId) {
  240. const users = await ajax.get(`/2/timeline/liked_by.json?tweet_id=${tweetId}`).then(
  241. res => res.data.globalObjects.users
  242. )
  243.  
  244. let likers = []
  245. Object.keys(users).forEach(user => likers.push(user)) // keys of users are id strings
  246. return likers
  247. }
  248.  
  249. async function fetch_no_comment_retweeters (tweetId) {
  250. const users = (await ajax.get(`/2/timeline/retweeted_by.json?tweet_id=${tweetId}`)).data.globalObjects.users
  251.  
  252. let targets = []
  253. Object.keys(users).forEach(user => targets.push(user))
  254. return targets
  255. }
  256.  
  257. async function fetch_list_members (listId) {
  258. const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users
  259. let members = []
  260. members = users.map(u => u.id_str)
  261. return members
  262. }
  263.  
  264. function block_user (id) {
  265. ajax.post('/1.1/blocks/create.json', Qs.stringify({
  266. user_id: id
  267. }), {
  268. headers: {
  269. 'Content-Type': 'application/x-www-form-urlencoded'
  270. }
  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. // block_all_liker and block_no_comment_retweeters need to be merged
  286. async function block_all_likers () {
  287. const tweetId = get_tweet_id()
  288. const [my_followers, my_friends, listMember] = await Promise.all([
  289. get_followers(),
  290. get_friends(),
  291. get_list_menber('1497432788634841089') // ccp propaganda list
  292. ])
  293.  
  294. // console.log('my_followers', my_followers)
  295. // console.log('my_friends', my_friends)
  296. // console.log('listMember', listMember)
  297. const likers = await fetch_likers(tweetId)
  298. console.log('likers', likers)
  299. const newLikers = likers.filter(id => {
  300. const flag_a = !my_followers.includes(id)
  301. const flag_b = !my_friends.includes(id)
  302. const flag_c = !listMember.includes(id)
  303. return flag_a && flag_b && flag_c
  304. })
  305. console.log('newLikers', newLikers)
  306. console.log('will not block ', likers.filter(id => !newLikers.includes(id)))
  307. likers.forEach(id => block_user(id))
  308. }
  309.  
  310.  
  311. async function block_no_comment_retweeters () {
  312. const tweetId = get_tweet_id()
  313. const retweeters = await fetch_no_comment_retweeters(tweetId)
  314. retweeters.forEach(id => block_user(id))
  315.  
  316. const tabName = location.href.split('retweets/')[1]
  317. if (tabName === 'with_comments') {
  318. if (!block_no_comment_retweeters.hasAlerted) {
  319. block_no_comment_retweeters.hasAlerted = true
  320. alert(i18n.block_rt_notice)
  321. }
  322. }
  323. }
  324.  
  325.  
  326.  
  327. async function block_list_members () {
  328. const listId = get_list_id()
  329. const members = await fetch_list_members(listId)
  330. members.forEach(id => block_user(id))
  331. }
  332.  
  333.  
  334. function success_notice (identifier, success_msg) {
  335. return _ => {
  336. const alertColor = 'rgb(224, 36, 94)'
  337. const container = $('div[aria-label="' + identifier + '"]')
  338. container.children().fadeOut(400, _ => {
  339. const notice = $(`
  340. <div style="
  341. color: ${alertColor};
  342. text-align: center;
  343. margin-top: 3em;
  344. font-size: x-large;
  345. ">
  346. <span>${success_msg}</span>
  347. </div>
  348. `)
  349. container.append(notice)
  350. })
  351. }
  352. }
  353.  
  354.  
  355.  
  356. function mount_button (parentDom, name, executer, success_notifier) {
  357. let themeColor = get_theme_color()
  358. const hoverColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.1)')
  359. const mousedownColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.2)')
  360. const btn_mousedown = 'bwl-btn-mousedown'
  361. const btn_hover = 'bwl-btn-hover'
  362.  
  363. $('head').append(`
  364. <style>
  365. .bwl-btn-base {
  366. min-height: 30px;
  367. padding-left: 1em;
  368. padding-right: 1em;
  369. border: 1px solid ${themeColor} !important;
  370. border-radius: 9999px;
  371. }
  372. .${btn_mousedown} {
  373. background-color: ${mousedownColor};
  374. cursor: pointer;
  375. }
  376. .${btn_hover} {
  377. background-color: ${hoverColor};
  378. cursor: pointer;
  379. }
  380. .bwl-btn-inner-wrapper {
  381. font-weight: bold;
  382. -webkit-box-align: center;
  383. align-items: center;
  384. -webkit-box-flex: 1;
  385. flex-grow: 1;
  386. color: ${themeColor};
  387. display: flex;
  388. }
  389. .bwl-text-font {
  390. font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
  391. color: ${themeColor};
  392. }
  393. </style>
  394. `)
  395.  
  396. const button = $(`
  397. <div
  398. aria-haspopup="true"
  399. role="button"
  400. data-focusable="true"
  401. class="bwl-btn-base"
  402. style="margin:3px"
  403. >
  404. <div class="bwl-btn-inner-wrapper">
  405. <span>
  406. <span class="bwl-text-font">${name}</span>
  407. </span>
  408. </div>
  409. </div>
  410. `)
  411. .addClass(parentDom.prop('classList')[0])
  412. .hover(function () {
  413. $(this).addClass(btn_hover)
  414. }, function () {
  415. $(this).removeClass(btn_hover)
  416. $(this).removeClass(btn_mousedown)
  417. })
  418. .on('selectstart', function () {
  419. return false
  420. })
  421. .mousedown(function () {
  422. $(this).removeClass(btn_hover)
  423. $(this).addClass(btn_mousedown)
  424. })
  425. .mouseup(function () {
  426. $(this).removeClass(btn_mousedown)
  427. if ($(this).is(':hover')) {
  428. $(this).addClass(btn_hover)
  429. }
  430. })
  431. .click(executer)
  432. .click(success_notifier)
  433.  
  434. parentDom.append(button)
  435. }
  436.  
  437. function main () {
  438. waitForKeyElements('h2:has(> span:contains(' + i18n.like_title + '))', dom => {
  439. const ancestor = get_ancestor(dom, 3)
  440. mount_button(ancestor, i18n.block_btn, block_all_likers, success_notice(i18n.like_list_identifier, i18n.block_success))
  441. })
  442.  
  443. waitForKeyElements('h2:has(> span:contains(' + i18n.retweet_title + '))', dom => {
  444. const ancestor = get_ancestor(dom, 3)
  445. mount_button(ancestor, i18n.block_btn, block_no_comment_retweeters, success_notice(i18n.retweet_list_identifier, i18n.block_success))
  446. })
  447.  
  448. waitForKeyElements('h2:has(> span:contains(' + i18n.list_members + '))', dom => {
  449. const ancestor = get_ancestor(dom, 3)
  450. mount_button(ancestor, i18n.block_btn, block_list_members, success_notice(i18n.list_members_identifier, i18n.block_success))
  451. })
  452. }
  453.  
  454. main()
  455. })()