Twitter Block With Love

封鎖或靜音所有轉推或喜歡某則推文的推特使用者

  1. // ==UserScript==
  2. // @name Twitter Block With Love
  3. // @namespace https://www.eolstudy.com
  4. // @homepage https://github.com/E011011101001/Twitter-Block-With-Love
  5. // @icon https://raw.githubusercontent.com/E011011101001/Twitter-Block-With-Love/master/imgs/icon.svg
  6. // @version 2025.04.04
  7. // @description Block or mute all the Twitter users who like or repost a specific post(Tweet), with love.
  8. // @description:fr Bloque ou mute tous les utilisateurs de Twitter qui aiment ou repostent un post spécifique (Tweet), avec amour.
  9. // @description:zh-CN 屏蔽或隐藏所有转发或点赞某条推文的推特用户
  10. // @description:zh-TW 封鎖或靜音所有轉推或喜歡某則推文的推特使用者
  11. // @description:ja あるツイートに「いいね」や「リツイート」をしたTwitterユーザー全員をブロックまたはミュートする機能を追加する
  12. // @description:ko 특정 트윗을 좋아하거나 리트윗하는 모든 트위터 사용자 차단 또는 음소거
  13. // @description:de Blockieren Sie alle Twitter-Nutzer, denen ein bestimmter Tweet gefällt oder die ihn retweeten, oder schalten Sie sie stumm - mit Liebe.
  14. // @author Eol, OverflowCat, yuanLeeMidori, nwxz
  15. // @license MIT
  16. // @run-at document-end
  17. // @grant GM_registerMenuCommand
  18. // @match https://twitter.com/*
  19. // @match https://x.com/*
  20. // @match https://mobile.twitter.com/*
  21. // @match https://tweetdeck.twitter.com/*
  22. // @exclude https://twitter.com/account/*
  23. // @require https://cdn.jsdelivr.net/npm/axios@0.25.0/dist/axios.min.js
  24. // @require https://cdn.jsdelivr.net/npm/qs@6.10.3/dist/qs.min.js
  25. // @require https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js
  26. // ==/UserScript==
  27.  
  28. /* global axios $ Qs */
  29.  
  30. (_ => {
  31. /* Begin of Dependencies */
  32. /* eslint-disable */
  33.  
  34. // https://gist.githubusercontent.com/BrockA/2625891/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js
  35. /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
  36. that detects and handles AJAXed content.
  37.  
  38. Usage example:
  39.  
  40. waitForKeyElements (
  41. "div.comments"
  42. , commentCallbackFunction
  43. );
  44.  
  45. //--- Page-specific function to do what we want when the node is found.
  46. function commentCallbackFunction (jNode) {
  47. jNode.text ("This comment changed by waitForKeyElements().");
  48. }
  49.  
  50. IMPORTANT: This function requires your script to have loaded jQuery.
  51. */
  52. function waitForKeyElements (
  53. selectorTxt, /* Required: The jQuery selector string that
  54. specifies the desired element(s).
  55. */
  56. actionFunction, /* Required: The code to run when elements are
  57. found. It is passed a jNode to the matched
  58. element.
  59. */
  60. bWaitOnce, /* Optional: If false, will continue to scan for
  61. new elements even after the first match is
  62. found.
  63. */
  64. iframeSelector /* Optional: If set, identifies the iframe to
  65. search.
  66. */
  67. ) {
  68. var targetNodes, btargetsFound;
  69.  
  70. if (typeof iframeSelector == "undefined")
  71. targetNodes = $(selectorTxt);
  72. else
  73. targetNodes = $(iframeSelector).contents ()
  74. .find (selectorTxt);
  75.  
  76. if (targetNodes && targetNodes.length > 0) {
  77. btargetsFound = true;
  78. /*--- Found target node(s). Go through each and act if they
  79. are new.
  80. */
  81. targetNodes.each ( function () {
  82. var jThis = $(this);
  83. var alreadyFound = jThis.data ('alreadyFound') || false;
  84.  
  85. if (!alreadyFound) {
  86. //--- Call the payload function.
  87. var cancelFound = actionFunction (jThis);
  88. if (cancelFound)
  89. btargetsFound = false;
  90. else
  91. jThis.data ('alreadyFound', true);
  92. }
  93. } );
  94. }
  95. else {
  96. btargetsFound = false;
  97. }
  98.  
  99. //--- Get the timer-control variable for this selector.
  100. var controlObj = waitForKeyElements.controlObj || {};
  101. var controlKey = selectorTxt.replace (/[^\w]/g, "_");
  102. var timeControl = controlObj [controlKey];
  103.  
  104. //--- Now set or clear the timer as appropriate.
  105. if (btargetsFound && bWaitOnce && timeControl) {
  106. //--- The only condition where we need to clear the timer.
  107. clearInterval (timeControl);
  108. delete controlObj [controlKey]
  109. }
  110. else {
  111. //--- Set a timer, if needed.
  112. if ( ! timeControl) {
  113. timeControl = setInterval ( function () {
  114. waitForKeyElements ( selectorTxt,
  115. actionFunction,
  116. bWaitOnce,
  117. iframeSelector
  118. );
  119. },
  120. 300
  121. );
  122. controlObj [controlKey] = timeControl;
  123. }
  124. }
  125. waitForKeyElements.controlObj = controlObj;
  126. }
  127. /* eslint-enable */
  128. /* End of Dependencies */
  129.  
  130. let lang = document.documentElement.lang
  131. if (lang == 'en-US') {
  132. lang = 'en' // TweetDeck
  133. }
  134. const translations = {
  135. // Please submit a feedback on Greasyfork.com if your language is not in the list bellow
  136. en: {
  137. lang_name: 'English',
  138. like_title: 'Liked by',
  139. like_list_identifier: 'Timeline: Liked by',
  140. retweet_title: 'Retweeted by',
  141. retweet_list_identifier: 'Timeline: Retweeted by',
  142. block_btn: 'Block all',
  143. block_success: 'All users blocked!',
  144. mute_btn: 'Mute all',
  145. mute_success: 'All users muted!',
  146. include_original_tweeter: 'Include the original Tweeter',
  147. logs: 'Logs',
  148. list_members: 'List members',
  149. list_members_identifier: 'Timeline: List members',
  150. block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
  151. },
  152. 'en-GB': {
  153. lang_name: 'British English',
  154. like_title: 'Liked by',
  155. like_list_identifier: 'Timeline: Liked by',
  156. retweet_title: 'Retweeted by',
  157. retweet_list_identifier: 'Timeline: Retweeted by',
  158. block_btn: 'Block all',
  159. block_success: 'All users blocked!',
  160. mute_btn: 'Mute all',
  161. mute_success: 'All users muted!',
  162. include_original_tweeter: 'Include the original Tweeter',
  163. logs: 'Logs',
  164. list_members: 'List members',
  165. list_members_identifier: 'Timeline: List members',
  166. block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
  167. },
  168. zh: {
  169. lang_name: '简体中文',
  170. like_title: '喜欢者',
  171. like_list_identifier: '时间线:喜欢者',
  172. retweet_title: '转推',
  173. retweet_list_identifier: '时间线:转推者',
  174. block_btn: '全部屏蔽',
  175. mute_btn: '全部隐藏',
  176. block_success: '列表用户已全部屏蔽!',
  177. mute_success: '列表用户已全部隐藏!',
  178. include_original_tweeter: '包括推主',
  179. logs: '操作记录',
  180. list_members: '列表成员',
  181. list_members_identifier: '时间线:列表成员',
  182. block_retweets_notice: 'Twitter Block with Love 仅屏蔽了不带评论转推的用户。\n请手动屏蔽引用推文的用户。'
  183. },
  184. 'zh-Hant': {
  185. lang_name: '正體中文',
  186. like_title: '已被喜歡',
  187. like_list_identifier: '時間軸:已被喜歡',
  188. retweet_title: '轉推',
  189. retweet_list_identifier: '時間軸:已被轉推',
  190. block_btn: '全部封鎖',
  191. mute_btn: '全部靜音',
  192. block_success: '列表用戶已全部封鎖!',
  193. mute_success: '列表用戶已全部靜音!',
  194. include_original_tweeter: '包括推主',
  195. logs: '活動記錄',
  196. list_members: '列表成員',
  197. list_members_identifier: '時間軸:列表成員',
  198. block_retweets_notice: 'Twitter Block with Love 僅封鎖了不帶評論轉推的使用者。\n請手動封鎖引用推文的使用者。'
  199. },
  200. ja: {
  201. lang_name: '日本語',
  202. like_list_identifier: 'タイムライン: いいねしたユーザー',
  203. like_title: 'いいねしたユーザー',
  204. retweet_list_identifier: 'タイムライン: リツイートしたユーザー',
  205. retweet_title: 'リツイート',
  206. block_btn: '全部ブロック',
  207. mute_btn: '全部ミュート',
  208. block_success: '全てブロックしました!',
  209. mute_success: '全てミュートしました!',
  210. include_original_tweeter: 'スレ主',
  211. logs: '操作履歴を表示',
  212. list_members: 'リストに追加されているユーザー',
  213. list_members_identifier: 'タイムライン: リストに追加されているユーザー',
  214. block_retweets_notice: 'TBWLは、コメントなしでリツイートしたユーザーのみをブロックしました。\n引用ツイートしたユーザーを手動でブロックしてください。'
  215. },
  216. vi: {
  217. // translation by Ly Hương
  218. lang_name: 'Tiếng Việt',
  219. like_list_identifier: 'Dòng thời gian: Được thích bởi',
  220. like_title: 'Được thích bởi',
  221. retweet_list_identifier: 'Dòng thời gian: Được Tweet lại bởi',
  222. retweet_title: 'Được Tweet lại bởi',
  223. block_btn: 'Chặn tất cả',
  224. mute_btn: 'Tắt tiếng tất cả',
  225. block_success: 'Tất cả tài khoản đã bị chặn!',
  226. mute_success: 'Tất cả tài khoản đã bị tắt tiếng!',
  227. include_original_tweeter: 'Tweeter gốc',
  228. logs: 'Lịch sử',
  229. list_members: 'Thành viên trong danh sách',
  230. list_members_identifier: 'Dòng thời gian: Thành viên trong danh sách',
  231. block_retweets_notice: 'TBWL chỉ chặn tài khoản đã retweet không bình luận. Những tài khoản retweet bằng bình luận thì xin hãy chặn bằng tay.'
  232. },
  233. ko: {
  234. // translation by hellojo011
  235. lang_name: '한국어',
  236. like_list_identifier: '타임라인: 마음에 들어 함',
  237. like_title: '마음에 들어 함',
  238. retweet_list_identifier: '타임라인: 리트윗함',
  239. retweet_title: '리트윗',
  240. block_btn: '모두 차단',
  241. mute_btn: '모두 뮤트',
  242. block_success: '모두 차단했습니다!',
  243. mute_success: '모두 뮤트했습니다!',
  244. include_original_tweeter: '글쓴이',
  245. logs: '활동',
  246. list_members: '리스트 멤버',
  247. list_members_identifier: '타임라인: 리스트 멤버',
  248. block_retweets_notice: '저희는 리트윗하신 사용자분들을 차단 했으나 트윗 인용하신 사용자분들은 직접 차단하셔야 합니다.'
  249. },
  250. de: {
  251. // translation by Wassermäuserich Lúcio
  252. lang_name: 'Deutsch',
  253. like_title: 'Gefällt',
  254. like_list_identifier: 'Timeline: Gefällt',
  255. retweet_title: 'Retweetet von',
  256. retweet_list_identifier: 'Timeline: Retweetet von',
  257. block_btn: 'Alle blockieren',
  258. mute_btn: 'Alle stummschalten',
  259. block_success: 'Alle wurden blockiert!',
  260. mute_success: 'Alle wurden stummgeschaltet!',
  261. include_original_tweeter: 'Original-Hochlader einschließen',
  262. logs: 'Betriebsaufzeichnung',
  263. list_members: 'Listenmitglieder',
  264. list_members_identifier: 'Timeline: Listenmitglieder',
  265. block_retweets_notice: 'TBWL hat nur Benutzer blockiert, die ohne Kommentare retweetet haben.\nBitte blockieren Sie Benutzer, die mit Kommentaren retweetet haben, manuell.',
  266. enabled: 'Aktiviert!',
  267. disabled: 'Behindert!',
  268. },
  269. fr: {
  270. lang_name: 'French',
  271. like_title: 'Aimé par',
  272. like_list_identifier: 'Fil d\'actualités : Aimé par',
  273. retweet_title: 'Retweeté par',
  274. retweet_list_identifier: 'Fil d\'actualités : Retweeté par',
  275. block_btn: 'Bloquer tous',
  276. block_success: 'Tous les utilisateurs sont bloqués !',
  277. mute_btn: 'Masquer tous',
  278. mute_success: 'Tous les utilisateurs sont masqués !',
  279. include_original_tweeter: 'Inclure l’auteur original',
  280. logs: 'Logs',
  281. list_members: 'Membres de la liste',
  282. list_members_identifier: 'Fil d\'actualités : Membres de la liste',
  283. block_retweets_notice: 'TBWL a seulement bloqué les utilisateurs qui ont retweeté sans commenter.\n Vous devez bloquer manuellement les retweets avec commentaire.'
  284. },
  285. }
  286. let i18n = translations[lang]
  287. // lang is empty in some error pages, so check lang first
  288. if (lang && !i18n) {
  289. i18n = translations.en
  290. if (false) {
  291. let langnames = []
  292. Object.values(translations).forEach(language => langnames.push(language.lang_name))
  293. langnames = langnames.join(', ')
  294. confirm(
  295. 'Twitter Block With Love userscript does not support your language (language code: "' + lang + '").\n' +
  296. 'Please send feedback at Greasyfork.com or open an issue at Github.com.\n' +
  297. 'Before that, you can edit the userscript yourself or just switch the language of Twitter Web App to any of the following languages: ' +
  298. langnames + '.\n\nDo you want to open an issue?'
  299. ) && window.location.replace('https://github.com/E011011101001/Twitter-Block-With-Love/issues/new/')
  300. }
  301. }
  302.  
  303. function rgba_to_hex (rgba_str, force_remove_alpha) {
  304. return '#' + rgba_str.replace(/^rgba?\(|\s+|\)$/g, '') // Get's rgba / rgb string values
  305. .split(',') // splits them at ","
  306. .filter((_, index) => !force_remove_alpha || index !== 3)
  307. .map(string => parseFloat(string)) // Converts them to numbers
  308. .map((number, index) => index === 3 ? Math.round(number * 255) : number) // Converts alpha to 255 number
  309. .map(number => number.toString(16)) // Converts numbers to hex
  310. .map(string => string.length === 1 ? '0' + string : string) // Adds 0 when length of one number is 1
  311. .join('')
  312. .toUpperCase()
  313. }
  314.  
  315. function hex_to_rgb (hex_str) {
  316. const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i.exec(hex_str)
  317. return result ? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})` : ''
  318. }
  319.  
  320. function invert_hex (hex) {
  321. return '#' + (Number(`0x1${hex.substring(1)}`) ^ 0xFFFFFF).toString(16).substring(1).toUpperCase()
  322. }
  323.  
  324. function get_theme_color () {
  325. const FALLBACK_COLOR = 'rgb(128, 128, 128)'
  326. let bgColor = FALLBACK_COLOR
  327. // try {
  328. // bgColor = getComputedStyle(document.querySelector('h2 > span')).color
  329. // } catch (e) {
  330. // console.info('[TBWL] bgColor not found. Falling back to default.')
  331. // }
  332. let buttonTextColor = hex_to_rgb(invert_hex(rgba_to_hex(bgColor)))
  333. for (const ele of document.querySelectorAll('div[role=\'button\']')) {
  334. const color = ele?.style?.backgroundColor
  335. if (color != '') {
  336. bgColor = color
  337. const span = ele.querySelector('span')
  338. buttonTextColor = getComputedStyle(span)?.color || buttonTextColor
  339. }
  340. }
  341.  
  342. return {
  343. bgColor,
  344. buttonTextColor,
  345. plainTextColor: $('span').css('color'),
  346. hoverColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.9)'),
  347. mousedownColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.8)')
  348. }
  349. }
  350.  
  351. function get_cookie (cname) {
  352. const name = cname + '='
  353. const ca = document.cookie.split(';')
  354. for (let i = 0; i < ca.length; ++i) {
  355. const c = ca[i].trim()
  356. if (c.indexOf(name) === 0) {
  357. return c.substring(name.length, c.length)
  358. }
  359. }
  360. return ''
  361. }
  362.  
  363. function get_ancestor (dom, level) {
  364. for (let i = 0; i < level; ++i) {
  365. dom = dom.parent()
  366. }
  367. return dom
  368. }
  369.  
  370. const ajax = axios.create({
  371. baseURL: 'https://api.x.com',
  372. withCredentials: true,
  373. headers: {
  374. Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  375. 'X-Twitter-Auth-Type': 'OAuth2Session',
  376. 'X-Twitter-Active-User': 'yes',
  377. 'X-Csrf-Token': get_cookie('ct0')
  378. }
  379. })
  380.  
  381. function get_tweet_id () {
  382. // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
  383. return location.href.split('status/')[1].split('/')[0]
  384. }
  385.  
  386. function get_list_id () {
  387. // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
  388. return location.href.split('lists/')[1].split('/')[0]
  389. }
  390.  
  391. const paramsREQ = `features=%7B%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D`
  392.  
  393. // fetch current followers
  394. async function fetch_followers(userName, count) {
  395. const paramsUserExtReq = `features=%7B%22hidden_profile_subscriptions_enabled%22%3Atrue%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22subscriptions_verification_info_is_identity_verified_enabled%22%3Atrue%2C%22subscriptions_verification_info_verified_since_enabled%22%3Atrue%2C%22highlights_tweets_tab_ui_enabled%22%3Atrue%2C%22responsive_web_twitter_article_notes_tab_enabled%22%3Atrue%2C%22subscriptions_feature_can_gift_premium%22%3Atrue%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Afalse%2C%22longform_notetweets_rich_text_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Afalse%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22articles_preview_enabled%22%3Afalse%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Afalse%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22standardized_nudges_misinfo%22%3Afalse%2C%22view_counts_everywhere_api_enabled%22%3Afalse%2C%22rweb_video_timestamps_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Afalse%2C%22longform_notetweets_consumption_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Afalse%2C%22responsive_web_grok_image_annotation_enabled%22%3Afalse%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Afalse%2C%22responsive_web_edit_tweet_api_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Afalse%7D%26fieldToggles%3D%7B%22withAuxiliaryUserLabels%22%3Afalse%7D`
  396. const userIdResponse = await ajax.get(`https://x.com/i/api/graphql/32pL5BWe9WKeSK1MoPvFQQ/UserByScreenName?variables=%7B%22screen_name%22%3A%22${userName}%22%7D&${paramsUserExtReq}`);
  397. const userId = userIdResponse.data["data"]["user"]["result"]["rest_id"]
  398. console.log('user rest id', userId);
  399.  
  400. const paramsExtREQ = `features=%7B%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D%26fieldToggles%3D%7B%22withAuxiliaryUserLabels%22%3Afalse%7D`
  401. const response = await ajax.get(`https://x.com/i/api/graphql/OGScL-RC4DFMsRGOCjPR6g/Followers?variables=%7B%22userId%22%3A%22${userId}%22%2C%22count%22%3A${count}%2C%22includePromotedContent%22%3Afalse%7D&${paramsExtREQ}`);
  402. const data = response.data;
  403.  
  404. const users = data["data"]["user"]["result"]["timeline"]["timeline"]["instructions"].reduce((acc, instruction) => {
  405. if (instruction.type === 'TimelineAddEntries') {
  406. instruction.entries.forEach(entry => {
  407. if (entry.content && entry.content.entryType === 'TimelineTimelineItem' && entry.content.itemContent && entry.content.itemContent.itemType === 'TimelineUser') {
  408. if (entry.content.itemContent.user_results && entry.content.itemContent.user_results.result && typeof entry.content.itemContent.user_results.result.rest_id !== "undefined") {
  409. const restId = entry.content.itemContent.user_results.result.rest_id;
  410. acc[restId] = true;
  411. }
  412. }
  413. });
  414. }
  415. return acc;
  416. }, {});
  417.  
  418. const followers = Object.keys(users);
  419. return followers;
  420. }
  421.  
  422. // fetch_likers and fetch_no_comment_reposters need to be merged into one function
  423. async function fetch_likers (tweetId) {
  424. const response = await ajax.get(`https://x.com/i/api/graphql/-A3YSkEdbCV0rpHTkYZXCA/Favoriters?variables=%7B%22tweetId%22%3A%22${tweetId}%22%2C%22includePromotedContent%22%3Atrue%7D&${paramsREQ}`);
  425. const data = response.data;
  426.  
  427. const users = data["data"]["favoriters_timeline"]["timeline"]["instructions"].reduce((acc, instruction) => {
  428. if (instruction.type === 'TimelineAddEntries') {
  429. instruction.entries.forEach(entry => {
  430. if (entry.content && entry.content.entryType === 'TimelineTimelineItem' && entry.content.itemContent && entry.content.itemContent.itemType === 'TimelineUser') {
  431. if (entry.content.itemContent.user_results && entry.content.itemContent.user_results.result && typeof entry.content.itemContent.user_results.result.rest_id !== "undefined") {
  432. const restId = entry.content.itemContent.user_results.result.rest_id;
  433. acc[restId] = true;
  434. }
  435. }
  436. });
  437. }
  438. return acc;
  439. }, {});
  440.  
  441. const likers = Object.keys(users);
  442. return likers;
  443. }
  444.  
  445. async function fetch_no_comment_reposters (tweetId) {
  446. const response = await ajax.get(`https://x.com/i/api/graphql/s6LwzbPawe8J04NldDYrQQ/Retweeters?variables=%7B%22tweetId%22%3A%22${tweetId}%22%2C%22includePromotedContent%22%3Atrue%7D&${paramsREQ}`);
  447. const data = response.data;
  448.  
  449. const users = data["data"]["retweeters_timeline"]["timeline"]["instructions"].reduce((acc, instruction) => {
  450. if (instruction.type === 'TimelineAddEntries') {
  451. instruction.entries.forEach(entry => {
  452. if (entry.content && entry.content.entryType === 'TimelineTimelineItem' && entry.content.itemContent && entry.content.itemContent.itemType === 'TimelineUser') {
  453. if (entry.content.itemContent.user_results && entry.content.itemContent.user_results.result && typeof entry.content.itemContent.user_results.result.rest_id !== "undefined") {
  454. const restId = entry.content.itemContent.user_results.result.rest_id;
  455. acc[restId] = true;
  456. }
  457. }
  458. });
  459. }
  460. return acc;
  461. }, {});
  462.  
  463. const reposters = Object.keys(users);
  464. return reposters;
  465. }
  466.  
  467.  
  468.  
  469. async function fetch_list_members (listId) {
  470. const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users
  471. const members = users.map(u => u.id_str)
  472. return members
  473. }
  474.  
  475. function block_user (id) {
  476. ajax.post('/1.1/blocks/create.json', Qs.stringify({
  477. user_id: id
  478. }), {
  479. headers: {
  480. 'Content-Type': 'application/x-www-form-urlencoded'
  481. }
  482. })
  483. }
  484.  
  485. function mute_user (id) {
  486. ajax.post('/1.1/mutes/users/create.json', Qs.stringify({
  487. user_id: id
  488. }), {
  489. headers: {
  490. 'Content-Type': 'application/x-www-form-urlencoded'
  491. }
  492. })
  493. }
  494.  
  495. async function get_tweeter (tweetId) {
  496. const screen_name = location.href.split('x.com/')[1].split('/')[0]
  497. const tweetData = (await ajax.get(`/2/timeline/conversation/${tweetId}.json`)).data
  498. // Find the tweeter by username
  499. const users = tweetData.globalObjects.users
  500. for (const key in users) {
  501. if (users[key].screen_name === screen_name) {
  502. return key
  503. }
  504. }
  505. return undefined
  506. }
  507.  
  508. function is_poster_included () {
  509. return $('#bwl-include-tweeter').prop('checked')
  510. }
  511.  
  512. // block_all_liker and block_no_comment_reposters need to be merged
  513. async function block_all_likers () {
  514. const tweetId = get_tweet_id()
  515. const likers = await fetch_likers(tweetId)
  516. if (is_poster_included()) {
  517. const tweeter = await get_tweeter(tweetId)
  518. if (tweeter) {
  519. likers.push(tweeter)
  520. }
  521. }
  522. likers.forEach(block_user)
  523. }
  524.  
  525. async function mute_all_likers () {
  526. const tweetId = get_tweet_id()
  527. const likers = await fetch_likers(tweetId)
  528. if (is_poster_included()) {
  529. const tweeter = await get_tweeter(tweetId)
  530. if (tweeter) {
  531. likers.push(tweeter)
  532. }
  533. }
  534. likers.forEach(mute_user)
  535. }
  536.  
  537. async function block_followers () {
  538. const userName = window.location.href.match(/http.*\/(\w+)\/followers/)[1]
  539. const followers = await fetch_followers(userName, 10000)
  540.  
  541. followers.forEach(block_user)
  542. }
  543.  
  544. async function block_reposters () {
  545. const tweetId = get_tweet_id()
  546. const reposters = await fetch_no_comment_reposters(tweetId)
  547. if (is_poster_included()) {
  548. const tweeter = await get_tweeter(tweetId)
  549. if (tweeter) {
  550. reposters.push(tweeter)
  551. }
  552. }
  553. reposters.forEach(block_user)
  554. }
  555.  
  556. async function mute_reposters () {
  557. const tweetId = get_tweet_id()
  558. const reposters = await fetch_no_comment_reposters(tweetId)
  559. if (is_poster_included()) {
  560. const tweeter = await get_tweeter(tweetId)
  561. if (tweeter) {
  562. reposters.push(tweeter)
  563. }
  564. }
  565. reposters.forEach(mute_user)
  566. }
  567.  
  568. async function block_list_members () {
  569. const listId = get_list_id()
  570. const members = await fetch_list_members(listId)
  571. members.forEach(block_user)
  572. }
  573.  
  574. async function mute_list_members () {
  575. const listId = get_list_id()
  576. const members = await fetch_list_members(listId)
  577. members.forEach(mute_user)
  578. }
  579.  
  580. async function mute () {
  581. const url = window.location.href
  582. if (url.endsWith('/likes')) {
  583. mute_all_likers()
  584. } else if (url.endsWith('/retweets')) {
  585. mute_reposters()
  586. } else {
  587. console.error('Mute is not implemented on this page.')
  588. }
  589. }
  590.  
  591. async function block () {
  592. const url = window.location.href
  593. if (url.endsWith('/likes')) {
  594. block_all_likers()
  595. } else if (url.endsWith('/retweets')) {
  596. block_reposters()
  597. } else if (url.endsWith('/followers')) {
  598. block_followers()
  599. } else {
  600. console.error('Block is not implemented on this page.')
  601. }
  602. }
  603.  
  604. function get_notifier_of (msg) {
  605. return _ => {
  606. const banner = $(`
  607. <div id="bwl-notice" style="right:0px; position:fixed; left:0px; bottom:0px; display:flex; flex-direction:column;">
  608. <div class="tbwl-notice">
  609. <span>${msg}</span>
  610. </div>
  611. </div>
  612. `)
  613. const closeButton = $(`
  614. <span id="bwl-close-button" style="font-weight:700; margin-left:12px; margin-right:12px; cursor:pointer;">
  615. Close
  616. </span>
  617. `)
  618. closeButton.click(_ => banner.remove())
  619. $(banner).children('.tbwl-notice').append(closeButton)
  620.  
  621. $('#layers').append(banner)
  622.  
  623. // TODO: after the hiding, the tab gets sluggish if revisited
  624. $('div[data-testid="cellInnerDiv"]:has(div[role="button"])').parent().hide()
  625. setTimeout(() => banner.remove(), 5000)
  626. }
  627. }
  628.  
  629. function mount_switch (parentDom, name) {
  630. const button = $(`
  631. <div class="container">
  632. <div class="checkbox">
  633. <input type="checkbox" id="bwl-include-tweeter" name="" value="">
  634. <label for="bwl-include-tweeter"><span>${name}</span></label>
  635. </div>
  636. </div>
  637. `)
  638.  
  639. parentDom.append(button)
  640. }
  641.  
  642. function mount_button (parentDom, name, executer, success_notifier) {
  643. const btn_mousedown = 'bwl-btn-mousedown'
  644. const btn_hover = 'bwl-btn-hover'
  645.  
  646. const button = $(`
  647. <div
  648. aria-haspopup="true"
  649. role="button"
  650. data-focusable="true"
  651. class="bwl-btn-base"
  652. style="margin:3px"
  653. >
  654. <div class="bwl-btn-inner-wrapper">
  655. <span>
  656. <span class="bwl-text-font">${name}</span>
  657. </span>
  658. </div>
  659. </div>
  660. `).addClass(parentDom.prop('classList')[0])
  661. .hover(function () {
  662. $(this).addClass(btn_hover)
  663. }, function () {
  664. $(this).removeClass(btn_hover)
  665. $(this).removeClass(btn_mousedown)
  666. })
  667. .on('selectstart', function () {
  668. return false
  669. })
  670. .mousedown(function () {
  671. $(this).removeClass(btn_hover)
  672. $(this).addClass(btn_mousedown)
  673. })
  674. .mouseup(function () {
  675. $(this).removeClass(btn_mousedown)
  676. if ($(this).is(':hover')) {
  677. $(this).addClass(btn_hover)
  678. }
  679. })
  680. .click(executer)
  681. .click(success_notifier)
  682.  
  683. parentDom.append(button)
  684. }
  685.  
  686. function insert_css () {
  687. const FALLBACK_FONT_FAMILY = 'TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, "Noto Sans CJK SC", "Noto Sans CJK TC", "Noto Sans CJK JP", Arial, sans-serif;'
  688. function get_font_family () {
  689. for (const ele of document.querySelectorAll('div[role=\'button\']')) {
  690. const font_family = getComputedStyle(ele).fontFamily
  691. if (font_family) {
  692. return font_family + ', ' + FALLBACK_FONT_FAMILY
  693. }
  694. }
  695. return FALLBACK_FONT_FAMILY
  696. }
  697.  
  698. const colors = get_theme_color()
  699.  
  700. // switch related
  701. $('head').append(`<style>
  702. </style>`)
  703.  
  704. // TODO: reduce repeated styles
  705. $('head').append(`<style>
  706. #tbwl-panel {
  707. display:flex; align-items:center; justify-content:center; border-bottom-width:1px;border-bottom-style: solid;
  708. border-color: rgb(239, 243, 244);
  709. padding-top: 3px;
  710. padding-bottom: 3px;
  711. }
  712. .tbwl-notice {
  713. align-self: center;
  714. display: flex;
  715. flex-direction: row;
  716. padding: 12px;
  717. margin-bottom: 32px;
  718. border-radius: 4px;
  719. color:rgb(255, 255, 255);
  720. background-color: rgb(29, 155, 240);
  721. font-family: ${FALLBACK_FONT_FAMILY};
  722. font-size: 15px;
  723. line-height: 20px;
  724. overflow-wrap: break-word;
  725. }
  726. .bwl-btn-base {
  727. min-height: 30px;
  728. padding-left: 1em;
  729. padding-right: 1em;
  730. border: 1px solid ${colors.bgColor} !important;
  731. border-radius: 9999px;
  732. background-color: ${colors.bgColor};
  733. display: flex;
  734. align-items: center
  735. }
  736. .bwl-btn-mousedown {
  737. background-color: ${colors.mousedownColor};
  738. cursor: pointer;
  739. }
  740. .bwl-btn-hover {
  741. background-color: ${colors.hoverColor};
  742. cursor: pointer;
  743. }
  744. .bwl-btn-inner-wrapper {
  745. font-weight: bold;
  746. -webkit-box-align: center;
  747. align-items: center;
  748. -webkit-box-flex: 1;
  749. flex-grow: 1;
  750. color: ${colors.bgColor};
  751. display: flex;
  752. }
  753. .bwl-text-font {
  754. font-family: ${get_font_family()};
  755. color: ${colors.buttonTextColor};
  756. font-size: 14px;
  757. }
  758. .container {
  759. margin-top: 0px;
  760. margin-left: 0px;
  761. margin-right: 5px;
  762. }
  763. .checkbox {
  764. width: 100%;
  765. margin: 0px auto;
  766. position: relative;
  767. display: block;
  768. color: ${colors.plainTextColor};
  769. }
  770. .checkbox input[type="checkbox"] {
  771. width: auto;
  772. opacity: 0.00000001;
  773. position: absolute;
  774. left: 0;
  775. margin-left: 0px;
  776. }
  777. .checkbox label:before {
  778. content: '';
  779. position: absolute;
  780. left: 0;
  781. top: -5px;
  782. margin: 0px;
  783. width: 22px;
  784. height: 22px;
  785. transition: transform 0.2s ease;
  786. border-radius: 3px;
  787. border: 2px solid ${colors.bgColor};
  788. }
  789. .checkbox label:after {
  790. content: '';
  791. display: block;
  792. width: 10px;
  793. height: 5px;
  794. border-bottom: 2px solid ${colors.bgColor};
  795. border-left: 2px solid ${colors.bgColor};
  796. -webkit-transform: rotate(-45deg) scale(0);
  797. transform: rotate(-45deg) scale(0);
  798. transition: transform ease 0.2s;
  799. will-change: transform;
  800. position: absolute;
  801. top: 3px;
  802. left: 8px;
  803. }
  804. .checkbox input[type="checkbox"]:checked ~ label::before {
  805. color: ${colors.bgColor};
  806. }
  807. .checkbox input[type="checkbox"]:checked ~ label::after {
  808. -webkit-transform: rotate(-45deg) scale(1);
  809. transform: rotate(-45deg) scale(1);
  810. }
  811. .checkbox label {
  812. position: relative;
  813. display: block;
  814. padding-left: 31px;
  815. margin-bottom: 0;
  816. font-weight: normal;
  817. cursor: pointer;
  818. vertical-align: sub;
  819. width:fit-content;
  820. width:-webkit-fit-content;
  821. width:-moz-fit-content;
  822. }
  823. .checkbox label span {
  824. position: relative;
  825. top: 50%;
  826. -webkit-transform: translateY(-50%);
  827. transform: translateY(-50%);
  828. }
  829. .checkbox input[type="checkbox"]:focus + label::before {
  830. outline: 0;
  831. }
  832. </style>`)
  833. }
  834.  
  835. function compose_panel () {
  836. const notice_block_success = get_notifier_of('Successfully blocked.')
  837. const notice_mute_success = get_notifier_of('Successfully muted.')
  838.  
  839. const TBWLPanel = $(`<div id="tbwl-panel" style="
  840. "></div>`)
  841. mount_switch(TBWLPanel, i18n.include_original_tweeter)
  842. mount_button(TBWLPanel, i18n.mute_btn, mute, notice_mute_success)
  843. mount_button(TBWLPanel, i18n.block_btn, block, notice_block_success)
  844. return TBWLPanel.hide()
  845. }
  846.  
  847. function main () {
  848. const sleepTime = 700 // ms
  849.  
  850. insert_css()
  851. const TBWLPanel = compose_panel()
  852.  
  853. let prevURL = undefined
  854. setInterval(_ => {
  855. const currentURL = window.location.href
  856. if (prevURL !== currentURL) {
  857. prevURL = currentURL
  858.  
  859. // Attention: /retweets may change to /reposts at any time.
  860. // Good job, Elon.
  861. // ♪ CEO, entrepreneur, born in 1971, Elon~~ Elon Reeve Musk~~ ♪♪
  862. if (currentURL.endsWith('/likes') || currentURL.endsWith('/retweets') || currentURL.endsWith('/followers')) {
  863. if ($('#tbwl-panel').length) {
  864. TBWLPanel.slideDown('fast')
  865. } else {
  866. waitForKeyElements('div[data-testid="primaryColumn"] section, div[data-testid="primaryColumn"] div[data-testid="emptyState"]', ele => {
  867. TBWLPanel.insertBefore(ele)
  868. TBWLPanel.slideDown()
  869. }, true)
  870. }
  871. } else {
  872. TBWLPanel.slideUp('fast')
  873. }
  874. }
  875. }, sleepTime)
  876.  
  877. // TODO: merge into the above way
  878. // need a way to hide the include_original_tweeter option
  879. waitForKeyElements('h2#modal-header[aria-level="2"][role="heading"]', ele => {
  880. const ancestor = get_ancestor(ele, 3)
  881. const currentURL = window.location.href
  882. if (/\/lists\/[0-9]+\/members$/.test(currentURL)) {
  883. mount_switch(ancestor, i18n.include_original_tweeter)
  884.  
  885. const notice_block_success = get_notifier_of('Successfully blocked.')
  886. const notice_mute_success = get_notifier_of('Successfully muted.')
  887. mount_button(ancestor, i18n.mute_btn, mute_list_members, notice_mute_success)
  888. mount_button(ancestor, i18n.block_btn, block_list_members, notice_block_success)
  889. }
  890. })
  891. }
  892.  
  893. main()
  894. })()