Twitter Block Porn

One-click block all the yellow scammers in the comment area.

  1. // ==UserScript==
  2. // @name Twitter Block Porn
  3. // @homepage https://github.com/daymade/Twitter-Block-Porn
  4. // @icon https://raw.githubusercontent.com/daymade/Twitter-Block-Porn/master/imgs/icon.svg
  5. // @version 1.6.1
  6. // @description One-click block all the yellow scammers in the comment area.
  7. // @description:zh-CN 共享黑名单, 一键拉黑所有黄推诈骗犯
  8. // @description:zh-TW 一鍵封鎖評論區的黃色詐騙犯
  9. // @description:ja コメントエリアのイエロースキャマーを一括ブロック
  10. // @description:ko 댓글 영역의 노란색 사기꾼을 한 번에 차단
  11. // @description:de Alle gelben Betrüger im Kommentarbereich mit einem Klick blockieren.
  12. // @author daymade
  13. // @source forked from https://github.com/E011011101001/Twitter-Block-With-Love
  14. // @license MIT
  15. // @run-at document-end
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_openInTab
  18. // @grant GM_addStyle
  19. // @grant GM_setValue
  20. // @grant GM_getValue
  21. // @grant GM_log
  22. // @grant GM_xmlhttpRequest
  23. // @match https://twitter.com/*
  24. // @match https://mobile.twitter.com/*
  25. // @match https://tweetdeck.twitter.com/*
  26. // @match https://x.com/*
  27. // @match https://mobile.x.com/*
  28. // @match https://tweetdeck.x.com/*
  29. // @exclude https://twitter.com/account/*
  30. // @exclude https://x.com/account/*
  31. // @connect raw.githubusercontent.com
  32. // @require https://cdn.jsdelivr.net/npm/axios@0.25.0/dist/axios.min.js
  33. // @require https://cdn.jsdelivr.net/npm/qs@6.10.3/dist/qs.min.js
  34. // @require https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js
  35. // @namespace https://greasyfork.org/users/1121182
  36. // ==/UserScript==
  37.  
  38. const pageBaseUrlMap = {
  39. 'twitter.com': 'twitter.com',
  40. 'x.com': 'x.com',
  41. };
  42.  
  43.  
  44. function getPageBaseUrl() {
  45. const hostname = window.location.hostname;
  46. return pageBaseUrlMap[hostname] || 'twitter.com'; // 默认使用 Twitter 的页面 URL
  47. }
  48.  
  49. const basePageUrl = getPageBaseUrl();
  50.  
  51. /* global axios $ Qs */
  52. const menu_command_list1 = GM_registerMenuCommand('🔗 打开共享黑名单 ①...', function () {
  53. const url = `https://${basePageUrl}/i/lists/1677334530754248706/members`
  54. GM_openInTab(url, {active: true})
  55. }, '');
  56.  
  57. const menu_command_list2 = GM_registerMenuCommand('🔗 打开共享黑名单 ②...', function () {
  58. const url = `https://${basePageUrl}/i/lists/1683810394287079426/members`
  59. GM_openInTab(url, {active: true})
  60. }, '');
  61.  
  62. const menu_command_list3 = GM_registerMenuCommand('🔗 打开共享黑名单 ③...', function () {
  63. const url = `https://${basePageUrl}/i/lists/1699049983159259593/members`
  64. GM_openInTab(url, {active: true})
  65. }, '');
  66.  
  67. const menu_command_list4 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑤...', function () {
  68. const url = `https://${basePageUrl}/i/lists/1702721627287371907/members`
  69. GM_openInTab(url, {active: true})
  70. }, '');
  71.  
  72. const menu_command_list5 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑥...', function () {
  73. const url = `https://${basePageUrl}/i/lists/1704104182913843610/members`
  74. GM_openInTab(url, {active: true})
  75. }, '');
  76.  
  77. const menu_command_list6 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑦...', function () {
  78. const url = `https://${basePageUrl}/i/lists/1696907125090586845/members`
  79. GM_openInTab(url, {active: true})
  80. }, '');
  81.  
  82. const menu_command_list7 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑧...', function () {
  83. const url = `https://${basePageUrl}/i/lists/1770809941151896014/members`
  84. GM_openInTab(url, {active: true})
  85. }, '');
  86.  
  87. const menu_command_special_list = GM_registerMenuCommand('🚫 拉黑加急名单', function () {
  88. if (window.confirm("加急名单里有一些爱主动拉黑人的诈骗犯,要一键屏蔽吗?")) {
  89. block_special_list()
  90. } else {
  91. GM_log('user cancelled block special scammers')
  92. }
  93. }, '');
  94.  
  95. const menu_command_all_list = GM_registerMenuCommand('🔗 查看全部名单(举报前请先搜索)...', function () {
  96. const url = 'https://raw.githubusercontent.com/daymade/Twitter-Block-Porn/master/lists/all.json'
  97. GM_openInTab(url, {active: true})
  98. }, '');
  99.  
  100. const menu_command_report = GM_registerMenuCommand('🔗 我要举报...', function () {
  101. const url = 'https://github.com/daymade/Twitter-Block-Porn/issues'
  102. GM_openInTab(url, {active: true})
  103. }, '');
  104.  
  105. const menu_command_sponsor = GM_registerMenuCommand('🥤 请我喝蜜雪冰城...', function () {
  106. const url = 'https://buymeacoffee.com/finetuning'
  107. GM_openInTab(url, {active: true})
  108. }, '');
  109.  
  110. const ChangeLogo = GM_getValue('change_logo', true)
  111. GM_registerMenuCommand(`${ChangeLogo?'✅ 已将 Logo 还原为小蓝鸟, 点击可使用 \uD835\uDD4F':'🐤 点击唤回小蓝鸟'}`, function () {
  112. GM_setValue('change_logo', !ChangeLogo)
  113. location.reload()
  114. });
  115.  
  116. function get_cookie (cname) {
  117. const name = cname + '='
  118. const ca = document.cookie.split(';')
  119. for (let i = 0; i < ca.length; ++i) {
  120. const c = ca[i].trim()
  121. if (c.indexOf(name) === 0) {
  122. return c.substring(name.length, c.length)
  123. }
  124. }
  125. return ''
  126. }
  127.  
  128. const apiBaseUrlMap = {
  129. 'twitter.com': 'https://api.twitter.com',
  130. 'x.com': 'https://api.x.com',
  131. };
  132.  
  133. function getApiBaseUrl() {
  134. const hostname = window.location.hostname;
  135. return apiBaseUrlMap[hostname] || 'https://api.twitter.com'; // 默认使用 Twitter 的 API URL
  136. }
  137.  
  138. const apiClient = axios.create({
  139. withCredentials: true,
  140. headers: {
  141. Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  142. 'X-Twitter-Auth-Type': 'OAuth2Session',
  143. 'X-Twitter-Active-User': 'yes',
  144. 'X-Csrf-Token': get_cookie('ct0')
  145. }
  146. });
  147.  
  148. // 在发起请求前,设置正确的 baseURL
  149. apiClient.interceptors.request.use(config => {
  150. config.baseURL = getApiBaseUrl();
  151. return config;
  152. });
  153.  
  154.  
  155. // extract list id in url
  156. function parseListId (url) {
  157. // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
  158. return url.split('lists/')[1].split('/')[0]
  159. }
  160.  
  161. async function fetch_list_members_id(listId) {
  162. let users = await fetch_list_members_info(listId)
  163. return users.map(u => u.id_str);
  164. }
  165.  
  166. async function fetch_list_members_info(listId) {
  167. const merged = await fetchAndMergeLists(listId);
  168. console.log(`merged: ${JSON.stringify(merged)}}`);
  169. return merged;
  170. }
  171.  
  172. async function fetchRemoteList(listId) {
  173. return new Promise((resolve, reject) => {
  174. GM_xmlhttpRequest({
  175. method: "GET",
  176. url: `https://raw.githubusercontent.com/daymade/Twitter-Block-Porn/master/lists/${listId}.json`,
  177. onload: function(response) {
  178. if (response.status === 200) {
  179. resolve(JSON.parse(response.responseText));
  180. } else {
  181. console.warn(`Remote list for listId ${listId} not found.`);
  182. resolve([]);
  183. }
  184. },
  185. onerror: function() {
  186. console.warn(`Error fetching remote list for listId ${listId}.`);
  187. resolve([]);
  188. }
  189. });
  190. });
  191. }
  192.  
  193. async function fetchTwitterListMembers(listId) {
  194. let cursor = -1;
  195. let allMembers = [];
  196. while (cursor && cursor !== 0) {
  197. // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/api-reference/get-lists-members
  198. // https://api.twitter.com/1.1/lists/members.json
  199. // Endpoint | Requests per user | Requests per app
  200. // GET lists/members | 900/15min | 75/15min
  201. let response = await apiClient.get(`/1.1/lists/members.json?list_id=${listId}&cursor=${cursor}`);
  202. if (!response.data.users) {
  203. GM_log(`fetchTwitterListMembers errors: ${JSON.stringify(response.data)}`)
  204. return allMembers;
  205. }
  206. let users = response.data.users;
  207. allMembers = allMembers.concat(users);
  208. cursor = response.data.next_cursor;
  209. }
  210. return allMembers;
  211. }
  212.  
  213. async function fetchAndMergeLists(listId) {
  214. let [remoteList, twitterList] = await Promise.all([
  215. fetchRemoteList(listId),
  216. fetchTwitterListMembers(listId)
  217. ]).catch(err => {
  218. console.error('Promise.all error:', err)
  219. });
  220.  
  221. // Merge lists. Ensure uniqueness by 'id_str'.
  222. let merged = [...twitterList, ...remoteList];
  223. let uniqueMembers = Array.from(new Map(merged.map(item => [item["id_str"], item])).values());
  224. return uniqueMembers;
  225. }
  226.  
  227. async function block_user (id, listId) {
  228. try {
  229. // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/mute-block-report-users/api-reference/post-blocks-create
  230. // https://api.twitter.com/1.1/blocks/create.json
  231. // Endpoint | Requests per user | Requests per app
  232. // not mentioned in doc!!
  233. await apiClient.post('/1.1/blocks/create.json', Qs.stringify({
  234. user_id: id
  235. }), {
  236. headers: {
  237. 'Content-Type': 'application/x-www-form-urlencoded'
  238. }
  239. });
  240. // Update blocked IDs list in GM storage
  241. let blocked = GM_getValue('blockedIdsMapping', {});
  242. if (!blocked[listId]) {
  243. blocked[listId] = [];
  244. }
  245. blocked[listId].push(id);
  246. GM_setValue('blockedIdsMapping', blocked);
  247. } catch (err) {
  248. GM_log(`error occurs when block_user: ${err}`);
  249. throw err;
  250. }
  251. }
  252.  
  253. async function block_by_ids (member_ids, listId) {
  254. let blocked = GM_getValue('blockedIdsMapping', {});
  255. GM_log(`block_by_ids: already blocked accounts: ${JSON.stringify(blocked)}`);
  256.  
  257. let suspended = await fetchRemoteList('suspended');
  258. GM_log(`block_by_ids: suspended accounts to be ignored: ${JSON.stringify(suspended)}`);
  259.  
  260. let toBlock = member_ids
  261. .filter(id => !blocked[listId] || !blocked[listId].includes(id))
  262. .filter(id => !suspended.includes(id));
  263.  
  264. const ids = [...new Set(toBlock)];
  265.  
  266. GM_log(`block_by_ids: about to block ${ids.length} scammers, detail: ${ids}`)
  267.  
  268. // Number of requests per batch
  269. const batchSize = 10;
  270. // 5s delay between batches
  271. const delay = 5000;
  272.  
  273. let failedIds = [];
  274.  
  275. for (let i = 0; i < Math.ceil(ids.length / batchSize); i++) {
  276. const batch = ids.slice(i * batchSize, (i + 1) * batchSize);
  277. const results = await Promise.allSettled(batch.map(id => block_user(id, listId)));
  278.  
  279. for (const [index, result] of results.entries()) {
  280. if (result.status === 'rejected' && result.reason.response.status === 404) {
  281. // Keep track of failed IDs
  282. failedIds.push(batch[index]);
  283. }
  284. }
  285.  
  286. if (i < Math.ceil(ids.length / batchSize) - 1) {
  287. await new Promise(resolve => setTimeout(resolve, delay));
  288. }
  289. }
  290.  
  291. if (failedIds.length > 0) {
  292. GM_log(`block_by_ids: failed to block these scammers: ${failedIds.join(', ')}`)
  293. }
  294. }
  295.  
  296. async function block_list_test_members () {
  297. const listId = parseListId(location.href)
  298. const members = await fetch_list_members_id(listId)
  299.  
  300. block_by_ids(members.slice(0, 10), listId)
  301. }
  302.  
  303. async function block_list_members () {
  304. const listId = parseListId(location.href)
  305. const members = await fetch_list_members_id(listId)
  306.  
  307. block_by_ids(members, listId)
  308. }
  309.  
  310. async function block_special_list () {
  311. // 加急名单: 特别活跃/拉黑我/来挑衅的黄推
  312. const special_scammers = [
  313. "1626262000547377153",
  314. "1083844806",
  315. "1191513095774048256",
  316. "1240432468995473411",
  317. "1260746375010177033",
  318. "1287994653388623875",
  319. "1356533851250548737",
  320. "1367769524657786883",
  321. "1371653074217963522",
  322. "1413745607466885121",
  323. "1428063931130646531",
  324. "1450954075084713986",
  325. "1455789588392202241",
  326. "1484792482357338113",
  327. "1489964205310570498",
  328. "1511380196",
  329. "1559311298986393603",
  330. "1562212902207033345",
  331. "1572580165900767234",
  332. "1578298585514668032",
  333. "1580799004983508992",
  334. "1583905468178567168",
  335. "1585644302381694976",
  336. "1589228466658250752",
  337. "1592249768771977216",
  338. "1592260043490983936",
  339. "1592573920905166849",
  340. "1592691909805604870",
  341. "1593155334742618112",
  342. "1593157357059837954",
  343. "1593620049704628224",
  344. "1593948516594102272",
  345. "1602373747344048128",
  346. "1616646252",
  347. "1616785364462743553",
  348. "1622437704159100929",
  349. "1623320122978099201",
  350. "1624527915663896576",
  351. "1626772507910082562",
  352. "1639847615981568001",
  353. "1650736618293133313",
  354. "1655041846790467585",
  355. "1655821795272937472",
  356. "1656797676820725761",
  357. "1658357788249321472",
  358. "1660685299930759168",
  359. "1665333981951172608",
  360. "1670444198586363904",
  361. "1671941042776702976",
  362. "1672954423663038466",
  363. "1673741619634241536",
  364. "1675179645870768128",
  365. "1676435725942661121",
  366. "1676489900021915648",
  367. "1679909362335354880",
  368. "1683326796488671232",
  369. "1683330763167780864",
  370. "1684583392665550850",
  371. "1684659885726916609",
  372. "1684743661853229056",
  373. "1684859751119847424",
  374. "1685367331013476352",
  375. "1685884844885295104",
  376. "1686221264023957504",
  377. "1687807818831986688",
  378. "1687816807766523905",
  379. "1688494198331420672",
  380. "1689943762531753985",
  381. "1690427325001904128",
  382. "175911002",
  383. "2802758389",
  384. "310749736",
  385. "3183558127",
  386. "532085468",
  387. "593711290",
  388. "769695361656991744",
  389. "824376009029992456",
  390. "976566332111179778",
  391. "769695361656991744",
  392. "3183558127",
  393. "1690427325001904128",
  394. "1675179645870768128",
  395. "1572106376",
  396. "1695160338780409856",
  397. "1637733191673950208",
  398. "1683682718863724544",
  399. "1399167832001241088",
  400. "1401414397021417472",
  401. "1387838616202788865",
  402. "1687365559355121665",
  403. "1399167832001241088",
  404. "1689962125169680384",
  405. "1387838616202788865",
  406. "1459187911329345538",
  407. "771777233878933504",
  408. "732529176805318661",
  409. "1593953486592303104",
  410. "1269873849568382981",
  411. "1631995677742907393",
  412. "837242544",
  413. "1642503707165364225",
  414. "1626262000547377153",
  415. "1624507776432242688",
  416. "358108318",
  417. "1612286141893595137",
  418. "1456084225791299586",
  419. "1687938257831514112",
  420. "1673189721445629953",
  421. "249563694",
  422. "1412358780872921093",
  423. "1688885999265361920"
  424. ]
  425.  
  426. // `block` is a reserved listId for those sacmmers who has blocked me
  427. // see block.json in `lists` folder
  428. let scammers = await fetchRemoteList("block")
  429. let blockedIds = scammers.map(u => u.id_str)
  430.  
  431. block_by_ids(special_scammers.concat(blockedIds), "block")
  432. }
  433.  
  434. async function export_list_members () {
  435. const listId = parseListId(location.href);
  436. const members = await fetchTwitterListMembers(listId);
  437.  
  438. // 创建一个 Blob 实例,包含 JSON 字符串的成员信息
  439. const blob = new Blob([JSON.stringify(members, null, 2)], {type : 'application/json'});
  440.  
  441. // 创建一个下载链接并点击它来下载文件
  442. const link = document.createElement("a");
  443. link.href = URL.createObjectURL(blob);
  444. link.download = `${listId}-${Date.now()}.json`;
  445. link.click();
  446. }
  447.  
  448. (_ => {
  449. /* Begin of Dependencies */
  450. /* eslint-disable */
  451.  
  452. // https://gist.githubusercontent.com/BrockA/2625891/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js
  453. /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
  454. that detects and handles AJAXed content.
  455.  
  456. Usage example:
  457.  
  458. waitForKeyElements (
  459. "div.comments"
  460. , commentCallbackFunction
  461. );
  462.  
  463. //--- Page-specific function to do what we want when the node is found.
  464. function commentCallbackFunction (jNode) {
  465. jNode.text ("This comment changed by waitForKeyElements().");
  466. }
  467.  
  468. IMPORTANT: This function requires your script to have loaded jQuery.
  469. */
  470. function waitForKeyElements (
  471. selectorTxt, /* Required: The jQuery selector string that
  472. specifies the desired element(s).
  473. */
  474. actionFunction, /* Required: The code to run when elements are
  475. found. It is passed a jNode to the matched
  476. element.
  477. */
  478. bWaitOnce, /* Optional: If false, will continue to scan for
  479. new elements even after the first match is
  480. found.
  481. */
  482. iframeSelector /* Optional: If set, identifies the iframe to
  483. search.
  484. */
  485. ) {
  486. var targetNodes, btargetsFound;
  487.  
  488. if (typeof iframeSelector == "undefined")
  489. targetNodes = $(selectorTxt);
  490. else
  491. targetNodes = $(iframeSelector).contents ()
  492. .find (selectorTxt);
  493.  
  494. if (targetNodes && targetNodes.length > 0) {
  495. btargetsFound = true;
  496. /*--- Found target node(s). Go through each and act if they
  497. are new.
  498. */
  499. targetNodes.each ( function () {
  500. var jThis = $(this);
  501. var alreadyFound = jThis.data ('alreadyFound') || false;
  502.  
  503. if (!alreadyFound) {
  504. //--- Call the payload function.
  505. var cancelFound = actionFunction (jThis);
  506. if (cancelFound)
  507. btargetsFound = false;
  508. else
  509. jThis.data ('alreadyFound', true);
  510. }
  511. } );
  512. }
  513. else {
  514. btargetsFound = false;
  515. }
  516.  
  517. //--- Get the timer-control variable for this selector.
  518. var controlObj = waitForKeyElements.controlObj || {};
  519. var controlKey = selectorTxt.replace (/[^\w]/g, "_");
  520. var timeControl = controlObj [controlKey];
  521.  
  522. //--- Now set or clear the timer as appropriate.
  523. if (btargetsFound && bWaitOnce && timeControl) {
  524. //--- The only condition where we need to clear the timer.
  525. clearInterval (timeControl);
  526. delete controlObj [controlKey]
  527. }
  528. else {
  529. //--- Set a timer, if needed.
  530. if ( ! timeControl) {
  531. timeControl = setInterval ( function () {
  532. waitForKeyElements ( selectorTxt,
  533. actionFunction,
  534. bWaitOnce,
  535. iframeSelector
  536. );
  537. },
  538. 300
  539. );
  540. controlObj [controlKey] = timeControl;
  541. }
  542. }
  543. waitForKeyElements.controlObj = controlObj;
  544. }
  545. /* eslint-enable */
  546. /* End of Dependencies */
  547.  
  548. let lang = document.documentElement.lang
  549. if (lang == 'en-US') {
  550. lang = 'en' // TweetDeck
  551. }
  552. if (lang == 'zh-CN') {
  553. lang = 'zh'
  554. }
  555. const translations = {
  556. en: {
  557. lang_name: 'English',
  558. block_btn: 'Block all Scammers',
  559. block_test_btn: 'Test block top 10 Scammers',
  560. block_success: 'All scammers blocked!',
  561. block_test_success: 'Top 10 scammers test blocked successfully!',
  562. export_btn: 'Export',
  563. export_success: 'Export successful!',
  564. },
  565. 'en-GB': {
  566. lang_name: 'British English',
  567. block_btn: 'Block all Scammers',
  568. block_test_btn: 'Test block top 10 Scammers',
  569. block_success: 'All scammers blocked!',
  570. block_test_success: 'Top 10 scammers test blocked successfully!',
  571. export_btn: 'Export',
  572. export_success: 'Export successful!',
  573. },
  574. zh: {
  575. lang_name: '简体中文',
  576. block_btn: '屏蔽所有诈骗犯',
  577. block_test_btn: '屏蔽前10名',
  578. block_success: '诈骗犯已全部被屏蔽!',
  579. block_test_success: '前10名诈骗犯测试屏蔽成功!',
  580. export_btn: '导出',
  581. export_success: '导出成功!',
  582. },
  583. 'zh-Hant': {
  584. lang_name: '正體中文',
  585. block_btn: '封鎖所有詐騙犯',
  586. block_test_btn: '測試封鎖前10名詐騙犯',
  587. block_success: '詐騙犯已全部被封鎖!',
  588. block_test_success: '前10名詐騙犯測試封鎖成功!',
  589. export_btn: '導出',
  590. export_success: '導出成功!',
  591. },
  592. ja: {
  593. lang_name: '日本語',
  594. block_btn: 'すべての詐欺師をブロック',
  595. block_test_btn: 'トップ10詐欺師をテストブロック',
  596. block_success: 'すべての詐欺師がブロックされました!',
  597. block_test_success: 'トップ10の詐欺師がテストブロックされました!',
  598. export_btn: 'エクスポート',
  599. export_success: 'エクスポート成功!',
  600. },
  601. vi: {
  602. lang_name: 'Tiếng Việt',
  603. block_btn: 'Chặn tất cả scammers',
  604. block_test_btn: 'Thử chặn top 10 scammers',
  605. block_success: 'Tất cả scammers đã bị chặn!',
  606. block_test_success: 'Đã thành công chặn thử top 10 scammers!',
  607. export_btn: 'Xuất',
  608. export_success: 'Xuất thành công!',
  609. },
  610. ko: {
  611. lang_name: '한국어',
  612. block_btn: '모든 사기꾼을 차단',
  613. block_test_btn: '테스트 차단 사기꾼 상위 10',
  614. block_success: '모든 사기꾼이 차단되었습니다!',
  615. block_test_success: '상위 10 사기꾼 테스트 차단 성공!',
  616. export_btn: '내보내기',
  617. export_success: '내보내기 성공!',
  618. },
  619. de: {
  620. lang_name: 'Deutsch',
  621. block_btn: 'Alle Betrüger blockieren',
  622. block_test_btn: 'Testblock Top 10 Betrüger',
  623. block_success: 'Alle Betrüger wurden blockiert!',
  624. block_test_success: 'Top 10 Betrüger erfolgreich getestet und blockiert!',
  625. export_btn: 'Exportieren',
  626. export_success: 'Export erfolgreich!',
  627. },
  628. fr: {
  629. lang_name: 'French',
  630. block_btn: 'Bloquer tous les escrocs',
  631. block_test_btn: 'Test de blocage top 10 escrocs',
  632. block_success: 'Tous les escrocs sont bloqués !',
  633. block_test_success: 'Test de blocage des 10 premiers escrocs réussi !',
  634. export_btn: 'Exporter',
  635. export_success: 'Exportation réussie !',
  636. },
  637. }
  638.  
  639. let i18n = translations[lang]
  640.  
  641. function rgba_to_hex (rgba_str, force_remove_alpha) {
  642. return '#' + rgba_str.replace(/^rgba?\(|\s+|\)$/g, '') // Get's rgba / rgb string values
  643. .split(',') // splits them at ","
  644. .filter((_, index) => !force_remove_alpha || index !== 3)
  645. .map(string => parseFloat(string)) // Converts them to numbers
  646. .map((number, index) => index === 3 ? Math.round(number * 255) : number) // Converts alpha to 255 number
  647. .map(number => number.toString(16)) // Converts numbers to hex
  648. .map(string => string.length === 1 ? '0' + string : string) // Adds 0 when length of one number is 1
  649. .join('')
  650. .toUpperCase()
  651. }
  652.  
  653. function hex_to_rgb (hex_str) {
  654. const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i.exec(hex_str)
  655. return result ? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})` : ''
  656. }
  657.  
  658. function invert_hex (hex) {
  659. return '#' + (Number(`0x1${hex.substring(1)}`) ^ 0xFFFFFF).toString(16).substring(1).toUpperCase()
  660. }
  661.  
  662. function get_theme_color () {
  663. const FALLBACK_COLOR = 'rgb(128, 128, 128)'
  664. let bgColor = getComputedStyle(document.querySelector('#modal-header > span')).color || FALLBACK_COLOR
  665. let buttonTextColor = hex_to_rgb(invert_hex(rgba_to_hex(bgColor)))
  666. for (const ele of document.querySelectorAll('div[role=\'button\']')) {
  667. const color = ele?.style?.backgroundColor
  668. if (color != '') {
  669. bgColor = color
  670. const span = ele.querySelector('span')
  671. buttonTextColor = getComputedStyle(span)?.color || buttonTextColor
  672. }
  673. }
  674.  
  675. return {
  676. bgColor,
  677. buttonTextColor,
  678. plainTextColor: $('span').css('color'),
  679. hoverColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.9)'),
  680. mousedownColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.8)')
  681. }
  682. }
  683.  
  684. function get_ancestor (dom, level) {
  685. for (let i = 0; i < level; ++i) {
  686. dom = dom.parent()
  687. }
  688. return dom
  689. }
  690.  
  691. function get_notifier_of (msg) {
  692. return _ => {
  693. const banner = $(`
  694. <div id="bwl-notice" style="right:0px; position:fixed; left:0px; bottom:0px; display:flex; flex-direction:column;">
  695. <div class="tbwl-notice">
  696. <span>${msg}</span>
  697. </div>
  698. </div>
  699. `)
  700. const closeButton = $(`
  701. <span id="bwl-close-button" style="font-weight:700; margin-left:12px; margin-right:12px; cursor:pointer;">
  702. Close
  703. </span>
  704. `)
  705. closeButton.click(_ => banner.remove())
  706. $(banner).children('.tbwl-notice').append(closeButton)
  707.  
  708. $('#layers').append(banner)
  709. setTimeout(() => banner.remove(), 5000)
  710. $('div[data-testid="app-bar-close"]').click()
  711. }
  712. }
  713.  
  714. function mount_button (parentDom, name, executer, success_notifier) {
  715. const btn_mousedown = 'bwl-btn-mousedown'
  716. const btn_hover = 'bwl-btn-hover'
  717.  
  718. const button = $(`
  719. <div
  720. aria-haspopup="true"
  721. role="button"
  722. data-focusable="true"
  723. class="bwl-btn-base"
  724. style="margin:3px"
  725. >
  726. <div class="bwl-btn-inner-wrapper">
  727. <span>
  728. <span class="bwl-text-font">${name}</span>
  729. </span>
  730. </div>
  731. </div>
  732. `).addClass(parentDom.prop('classList')[0])
  733. .hover(function () {
  734. $(this).addClass(btn_hover)
  735. }, function () {
  736. $(this).removeClass(btn_hover)
  737. $(this).removeClass(btn_mousedown)
  738. })
  739. .on('selectstart', function () {
  740. return false
  741. })
  742. .mousedown(function () {
  743. $(this).removeClass(btn_hover)
  744. $(this).addClass(btn_mousedown)
  745. })
  746. .mouseup(function () {
  747. $(this).removeClass(btn_mousedown)
  748. if ($(this).is(':hover')) {
  749. $(this).addClass(btn_hover)
  750. }
  751. })
  752. .click(async () => await executer())
  753. .click(success_notifier)
  754.  
  755. parentDom.append(button)
  756. }
  757.  
  758. function insert_css () {
  759. 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;'
  760. function get_font_family () {
  761. for (const ele of document.querySelectorAll('div[role=\'button\']')) {
  762. const font_family = getComputedStyle(ele).fontFamily
  763. if (font_family) {
  764. return font_family + ', ' + FALLBACK_FONT_FAMILY
  765. }
  766. }
  767. return FALLBACK_FONT_FAMILY
  768. }
  769.  
  770. const colors = get_theme_color()
  771.  
  772. // switch related
  773. $('head').append(`<style>
  774. </style>`)
  775.  
  776. // TODO: reduce repeated styles
  777. $('head').append(`<style>
  778. .tbwl-notice {
  779. align-self: center;
  780. display: flex;
  781. flex-direction: row;
  782. padding: 12px;
  783. margin-bottom: 32px;
  784. border-radius: 4px;
  785. color:rgb(255, 255, 255);
  786. background-color: rgb(29, 155, 240);
  787. font-family: ${FALLBACK_FONT_FAMILY};
  788. font-size:15px;
  789. line-height:20px;
  790. overflow-wrap: break-word;
  791. }
  792. .bwl-btn-base {
  793. min-height: 30px;
  794. padding-left: 1em;
  795. padding-right: 1em;
  796. border: 1px solid ${colors.bgColor} !important;
  797. border-radius: 9999px;
  798. background-color: ${colors.bgColor};
  799. }
  800. .bwl-btn-mousedown {
  801. background-color: ${colors.mousedownColor};
  802. cursor: pointer;
  803. }
  804. .bwl-btn-hover {
  805. background-color: ${colors.hoverColor};
  806. cursor: pointer;
  807. }
  808. .bwl-btn-inner-wrapper {
  809. font-weight: bold;
  810. -webkit-box-align: center;
  811. align-items: center;
  812. -webkit-box-flex: 1;
  813. flex-grow: 1;
  814. color: ${colors.bgColor};
  815. display: flex;
  816. }
  817. .bwl-text-font {
  818. font-family: ${get_font_family()};
  819. color: ${colors.buttonTextColor};
  820. }
  821. .container {
  822. margin-top: 0px;
  823. margin-left: 0px;
  824. margin-right: 5px;
  825. }
  826. .checkbox {
  827. width: 100%;
  828. margin: 0px auto;
  829. position: relative;
  830. display: block;
  831. color: ${colors.plainTextColor};
  832. }
  833. .checkbox input[type="checkbox"] {
  834. width: auto;
  835. opacity: 0.00000001;
  836. position: absolute;
  837. left: 0;
  838. margin-left: 0px;
  839. }
  840. .checkbox label:before {
  841. content: '';
  842. position: absolute;
  843. left: 0;
  844. top: 0;
  845. margin: 0px;
  846. width: 22px;
  847. height: 22px;
  848. transition: transform 0.2s ease;
  849. border-radius: 3px;
  850. border: 2px solid ${colors.bgColor};
  851. }
  852. .checkbox label:after {
  853. content: '';
  854. display: block;
  855. width: 10px;
  856. height: 5px;
  857. border-bottom: 2px solid ${colors.bgColor};
  858. border-left: 2px solid ${colors.bgColor};
  859. -webkit-transform: rotate(-45deg) scale(0);
  860. transform: rotate(-45deg) scale(0);
  861. transition: transform ease 0.2s;
  862. will-change: transform;
  863. position: absolute;
  864. top: 8px;
  865. left: 6px;
  866. }
  867. .checkbox input[type="checkbox"]:checked ~ label::before {
  868. color: ${colors.bgColor};
  869. }
  870. .checkbox input[type="checkbox"]:checked ~ label::after {
  871. -webkit-transform: rotate(-45deg) scale(1);
  872. transform: rotate(-45deg) scale(1);
  873. }
  874. .checkbox label {
  875. position: relative;
  876. display: block;
  877. padding-left: 31px;
  878. margin-bottom: 0;
  879. font-weight: normal;
  880. cursor: pointer;
  881. vertical-align: sub;
  882. width:fit-content;
  883. width:-webkit-fit-content;
  884. width:-moz-fit-content;
  885. }
  886. .checkbox label span {
  887. position: relative;
  888. top: 50%;
  889. -webkit-transform: translateY(-50%);
  890. transform: translateY(-50%);
  891. }
  892. .checkbox input[type="checkbox"]:focus + label::before {
  893. outline: 0;
  894. }
  895. </style>`)
  896. }
  897.  
  898. function main () {
  899. let inited = false
  900.  
  901. const notice_export_success = get_notifier_of(i18n.export_success)
  902. const notice_block_test_success = get_notifier_of(i18n.block_test_success)
  903. const notice_block_success = get_notifier_of(`${i18n.block_success}, 为了安全起见, 每次最多拉黑 300 个`)
  904.  
  905. waitForKeyElements('h2#modal-header[aria-level="2"][role="heading"]', ele => {
  906. if (!inited) {
  907. insert_css()
  908. inited = true
  909. }
  910. const ancestor = get_ancestor(ele, 3)
  911. const currentURL = window.location.href
  912. if (/\/lists\/[0-9]+\/members$/.test(currentURL)) {
  913. mount_button(ancestor, i18n.export_btn, export_list_members, notice_export_success)
  914. mount_button(ancestor, i18n.block_test_btn, block_list_test_members, notice_block_test_success)
  915. mount_button(ancestor, i18n.block_btn, block_list_members, notice_block_success)
  916. }
  917. })
  918. }
  919.  
  920. // 这个函数名字来自 @albaz64
  921. (function makeBlueBirdGreatAgain() {
  922. if(!ChangeLogo) return;
  923.  
  924. // Twitter logo
  925. const SVG = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 335 276' fill='%233ba9ee'%3E%3Cpath d='m302 70a195 195 0 0 1 -299 175 142 142 0 0 0 97 -30 70 70 0 0 1 -58 -47 70 70 0 0 0 31 -2 70 70 0 0 1 -57 -66 70 70 0 0 0 28 5 70 70 0 0 1 -18 -90 195 195 0 0 0 141 72 67 67 0 0 1 116 -62 117 117 0 0 0 43 -17 65 65 0 0 1 -31 38 117 117 0 0 0 39 -11 65 65 0 0 1 -32 35'/%3E%3C/svg%3E"
  926.  
  927. // Function to reset favicon
  928. document.querySelector(`head>link[rel="shortcut icon"]`).href = `//abs.twimg.com/favicons/twitter.ico`
  929.  
  930. // Add style
  931. GM_addStyle(
  932. `header h1 a[href="/home"] {
  933. margin: 6px 4px 2px;
  934. }
  935. header h1 a[href="/home"] div {
  936. background-image: url("${SVG}");
  937. background-size: contain;
  938. background-position: center;
  939. background-repeat: no-repeat;
  940. margin: 4px;
  941. }
  942. header h1 a[href="/home"] div svg {
  943. display: none;
  944. }
  945. header h1 a[href="/home"] :hover :after {
  946. content: "已被 Twitter-Block-Porn 替换";
  947. font: message-box;
  948. color: gray;
  949. position: absolute;
  950. left: 48px;
  951. }`
  952. )
  953. })()
  954.  
  955. main()
  956. })()