Twitter Block Porn (TestFlight)

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

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