Twitter Block With Love

Block all users who love a certain tweet

目前為 2020-07-19 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Twitter Block With Love
  3. // @namespace https://www.eolstudy.com
  4. // @version 2.1
  5. // @description Block all users who love a certain tweet
  6. // @author Eol, OverflowCat
  7. // @run-at document-end
  8. // @match https://twitter.com/*
  9. // @match https://mobile.twitter.com/*
  10. // @exclude https://twitter.com/account/*
  11. // @require https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js
  12. // @require https://cdn.jsdelivr.net/npm/qs/dist/qs.min.js
  13. // @require https://code.jquery.com/jquery-3.4.1.min.js
  14. // @require https://greasyfork.org/scripts/2199-waitforkeyelements/code/waitForKeyElements.js?version=6349
  15. // ==/UserScript==
  16.  
  17. /* global axios $ Qs waitForKeyElements*/
  18.  
  19. (_ => {
  20. let lang = document.documentElement.lang
  21. const translations = {
  22. // Please submit a feedback on Greasyfork.com if your language is not in the list bellow
  23. 'en': {
  24. lang_name: 'English',
  25. like_title: 'Liked by',
  26. like_list_identifier: 'Timeline: Liked by', // aria-label
  27. retweet_title: 'Retweets',
  28. mini_retweet_title: 'Retweeted by',
  29. retweet_list_identifier: 'Timeline: Retweeted by',
  30. btn: 'Block All',
  31. success: 'All Users Blocked!'
  32. },
  33. 'en-GB': {
  34. lang_name: 'British English',
  35. like_title: 'Liked by',
  36. like_list_identifier: 'Timeline: Liked by',
  37. retweet_title: 'Retweets',
  38. mini_retweet_title: 'Retweeted by',
  39. retweet_list_identifier: 'Timeline: Retweeted by',
  40. btn: 'Block All',
  41. success: 'All Users Blocked!'
  42. },
  43. 'zh': {
  44. lang_name: '简体中文',
  45. like_title: '喜欢者',
  46. like_list_identifier: '时间线:喜欢者',
  47. retweet_title: '转推',
  48. retweet_list_identifier: '时间线:转推者',
  49. btn: '全部屏蔽',
  50. success: '列表用户已全部屏蔽!'
  51. },
  52. 'zh-Hant': {
  53. lang_name: '正體中文',
  54. like_title: '已被喜歡',
  55. like_list_identifier: '時間軸:已被喜歡',
  56. retweet_title: '轉推',
  57. retweet_list_identifier: '時間軸:已被轉推',
  58. btn: '全部封鎖',
  59. success: '列表用戶已全部封鎖!'
  60. },
  61. 'ja': {
  62. lang_name: '日本語',
  63. like_list_identifier: 'タイムライン: いいねしたユーザー',
  64. like_title: 'いいねしたユーザー',
  65. retweet_list_identifier: 'タイムライン: リツイートしたユーザー',
  66. retweet_title: 'リツイート',
  67. btn: '<span style="font-size: small">全部ブロックする<span>',
  68. success: '全てブロックしました!'
  69. },
  70. 'vi': {
  71. lang_name: 'Tiếng Việt',
  72. like_list_identifier: 'Dòng thời gian: Được thích bởi',
  73. like_title: 'Được thích bởi',
  74. retweet_list_identifier: 'Dòng thời gian: Được Tweet lại bởi',
  75. retweet_title: 'Được Tweet lại bởi',
  76. btn: 'Chặn tất cả',
  77. success: 'Tất cả tài khoản đã bị chặn!'
  78. // translated by Ly Hương
  79. }
  80. }
  81. let i18n = translations[lang]
  82. // lang is empty in some error pages, so check lang first
  83. if (lang && !i18n) {
  84. let langnames = []
  85. Object.values(translations).forEach(language => langnames.push(language.lang_name))
  86. langnames = langnames.join(', ')
  87. let issue = confirm(
  88. 'Twitter Block With Love userscript does not support your language (language code: "' + lang + '").\n' +
  89. 'Please send feedback at Greasyfork.com or open an issue at Github.com.\n' +
  90. 'Before that, you can edit the userscript yourself or just switch the language of Twitter Web App to any of the following languages: ' +
  91. langnames + '.\n\nDo you want to open an issue?'
  92. )
  93. if (issue) window.location.replace("https://github.com/E011011101001/Twitter-Block-With-Love/issues/new/")
  94. }
  95.  
  96. function get_cookie (cname) {
  97. let name = cname + '='
  98. let ca = document.cookie.split(';')
  99. for(let i=0; i<ca.length; i++) {
  100. let c = ca[i].trim()
  101. if (c.indexOf(name)==0) {
  102. return c.substring(name.length,c.length)
  103. }
  104. }
  105. return ''
  106. }
  107.  
  108. function get_ancestor (dom, level) {
  109. for (let i = 0; i < level; ++i) {
  110. dom = dom.parent()
  111. }
  112. return dom
  113. }
  114.  
  115. const ajax = axios.create({
  116. baseURL: 'https://api.twitter.com',
  117. withCredentials: true,
  118. headers: {
  119. 'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  120. 'X-Twitter-Auth-Type': 'OAuth2Session',
  121. 'X-Twitter-Active-User': 'yes',
  122. 'X-Csrf-Token': get_cookie('ct0')
  123. }
  124. })
  125.  
  126. function get_tweet_id () {
  127. // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
  128. return location.href.split('status/')[1].split('/')[0]
  129. }
  130.  
  131. // fetch_likers and fetch_no_comment_retweeters need to be merged into one function
  132. async function fetch_likers (tweetId) {
  133. const users = await ajax.get(`/2/timeline/liked_by.json?tweet_id=${tweetId}`).then(
  134. res => res.data.globalObjects.users
  135. )
  136.  
  137. let likers = []
  138. Object.keys(users).forEach(user => likers.push(user)) // keys of users are id strings
  139. return likers
  140. }
  141.  
  142. async function fetch_no_comment_retweeters (tweetId) {
  143. const users = await ajax.get(`/2/timeline/retweeted_by.json?tweet_id=${tweetId}`).then(
  144. res => res.data.globalObjects.users
  145. )
  146.  
  147. let targets = []
  148. Object.keys(users).forEach(user => targets.push(user))
  149. return targets
  150. }
  151.  
  152. function block_user (id) {
  153. ajax.post('/1.1/blocks/create.json', Qs.stringify({
  154. user_id: id
  155. }), {
  156. headers: {
  157. 'Content-Type':'application/x-www-form-urlencoded'
  158. }
  159. })
  160. }
  161.  
  162. // block_all_liker and block_no_comment_retweeters need to be merged
  163. async function block_all_likers () {
  164. const tweetId = get_tweet_id()
  165. const likers = await fetch_likers(tweetId)
  166. likers.forEach(id => block_user(id))
  167. }
  168.  
  169. async function block_no_comment_retweeters () {
  170. const tweetId = get_tweet_id()
  171. const retweeters = await fetch_no_comment_retweeters(tweetId)
  172. retweeters.forEach(id => block_user(id))
  173.  
  174. const tabName = location.href.split('retweets/')[1]
  175. if (tabName === 'with_comments') {
  176. if (!block_no_comment_retweeters.hasAlerted) {
  177. block_no_comment_retweeters.hasAlerted = true
  178. alert('TBWL has only blocked users that retweeted without comments.\n Please block users with comments manually.')
  179. }
  180. }
  181. }
  182.  
  183. function success_notice (identifier) {
  184. return _ => {
  185. const container = $('div[aria-label="'+ identifier + '"]')
  186. container.children().fadeOut(400, _ => {
  187. const notice = $(`
  188. <div style="
  189. color: ${themeColor};
  190. text-align: center;
  191. margin-top: 3em;
  192. font-size: x-large;
  193. ">
  194. <span>${i18n.success}</span>
  195. </div>
  196. `)
  197. container.append(notice)
  198. })
  199. }
  200. }
  201.  
  202. function mount_block_button (dom, executer, success_notifier) {
  203. const btn_mousedown = 'bwl-btn-mousedown'
  204. const btn_hover = 'bwl-btn-hover'
  205.  
  206. let close_icon = $('div[aria-label] > div[dir="auto"] > svg[viewBox="0 0 24 24"]')[0]
  207. let themeColor = window.getComputedStyle(close_icon).color
  208. const hoverColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.1)')
  209. const mousedownColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.2)')
  210.  
  211. $('head').append(`
  212. <style>
  213. .bwl-btn-base {
  214. min-height: 30px;
  215. padding-left: 1em;
  216. padding-right: 1em;
  217. border: 1px solid ${themeColor} !important;
  218. border-radius: 9999px;
  219. }
  220. .${btn_mousedown} {
  221. background-color: ${mousedownColor};
  222. cursor: pointer;
  223. }
  224. .${btn_hover} {
  225. background-color: ${hoverColor};
  226. cursor: pointer;
  227. }
  228. .bwl-btn-inner-wrapper {
  229. font-weight: bold;
  230. -webkit-box-align: center;
  231. align-items: center;
  232. -webkit-box-flex: 1;
  233. flex-grow: 1;
  234. color: ${themeColor};
  235. display: flex;
  236. }
  237. .bwl-text-font {
  238. font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
  239. }
  240. </style>
  241. `)
  242.  
  243. const button = $(`
  244. <div
  245. aria-haspopup="true"
  246. role="button"
  247. data-focusable="true"
  248. class="bwl-btn-base"
  249. >
  250. <div class="bwl-btn-inner-wrapper">
  251. <span>
  252. <span class="bwl-text-font">${i18n.btn}</span>
  253. </span>
  254. </div>
  255. </div>
  256. `)
  257. .addClass(dom.prop('classList')[0])
  258. .hover(function () {
  259. $(this).addClass(btn_hover)
  260. }, function () {
  261. $(this).removeClass(btn_hover)
  262. $(this).removeClass(btn_mousedown)
  263. })
  264. .on('selectstart', function () {
  265. return false
  266. })
  267. .mousedown(function () {
  268. $(this).removeClass(btn_hover)
  269. $(this).addClass(btn_mousedown)
  270. })
  271. .mouseup(function () {
  272. $(this).removeClass(btn_mousedown)
  273. if ($(this).is(':hover')) {
  274. $(this).addClass(btn_hover)
  275. }
  276. })
  277. .click(executer)
  278. .click(success_notifier)
  279.  
  280. dom.append(button)
  281. }
  282.  
  283. function main () {
  284. waitForKeyElements('h2:has(> span:contains(' + i18n.like_title + '))', dom => {
  285. mount_block_button(get_ancestor(dom, 3), block_all_likers, success_notice(i18n.like_list_identifier))
  286. })
  287.  
  288. waitForKeyElements('h2:has(> span:contains(' + i18n.retweet_title + '))', dom => {
  289. mount_block_button(get_ancestor(dom, 3), block_no_comment_retweeters, success_notice(i18n.retweet_list_identifier))
  290. })
  291.  
  292. // some languages do not need the 'mini' version
  293. if (i18n.mini_retweet_title) {
  294. waitForKeyElements('h2:has(> span:contains(' + i18n.mini_retweet_title + '))', dom => {
  295. mount_block_button(get_ancestor(dom, 3), block_no_comment_retweeters, success_notice(i18n.retweet_list_identifier))
  296. })
  297. }
  298. }
  299.  
  300. main()
  301. })()