共享黑名单, 一键拉黑所有黄推诈骗犯
// ==UserScript== // @name Twitter Block Porn // @homepage https://github.com/daymade/Twitter-Block-Porn // @icon https://raw.githubusercontent.com/daymade/Twitter-Block-Porn/master/imgs/icon.svg // @version 1.6.1 // @description One-click block all the yellow scammers in the comment area. // @description:zh-CN 共享黑名单, 一键拉黑所有黄推诈骗犯 // @description:zh-TW 一鍵封鎖評論區的黃色詐騙犯 // @description:ja コメントエリアのイエロースキャマーを一括ブロック // @description:ko 댓글 영역의 노란색 사기꾼을 한 번에 차단 // @description:de Alle gelben Betrüger im Kommentarbereich mit einem Klick blockieren. // @author daymade // @source forked from https://github.com/E011011101001/Twitter-Block-With-Love // @license MIT // @run-at document-end // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_log // @grant GM_xmlhttpRequest // @match https://twitter.com/* // @match https://mobile.twitter.com/* // @match https://tweetdeck.twitter.com/* // @match https://x.com/* // @match https://mobile.x.com/* // @match https://tweetdeck.x.com/* // @exclude https://twitter.com/account/* // @exclude https://x.com/account/* // @connect raw.githubusercontent.com // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/qs.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @namespace https://greasyfork.org/users/1121182 // ==/UserScript== const pageBaseUrlMap = { 'twitter.com': 'twitter.com', 'x.com': 'x.com', }; function getPageBaseUrl() { const hostname = window.location.hostname; return pageBaseUrlMap[hostname] || 'twitter.com'; // 默认使用 Twitter 的页面 URL } const basePageUrl = getPageBaseUrl(); /* global axios $ Qs */ const menu_command_list1 = GM_registerMenuCommand('🔗 打开共享黑名单 ①...', function () { const url = `https://${basePageUrl}/i/lists/1677334530754248706/members` GM_openInTab(url, {active: true}) }, ''); const menu_command_list2 = GM_registerMenuCommand('🔗 打开共享黑名单 ②...', function () { const url = `https://${basePageUrl}/i/lists/1683810394287079426/members` GM_openInTab(url, {active: true}) }, ''); const menu_command_list3 = GM_registerMenuCommand('🔗 打开共享黑名单 ③...', function () { const url = `https://${basePageUrl}/i/lists/1699049983159259593/members` GM_openInTab(url, {active: true}) }, ''); const menu_command_list4 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑤...', function () { const url = `https://${basePageUrl}/i/lists/1702721627287371907/members` GM_openInTab(url, {active: true}) }, ''); const menu_command_list5 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑥...', function () { const url = `https://${basePageUrl}/i/lists/1704104182913843610/members` GM_openInTab(url, {active: true}) }, ''); const menu_command_list6 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑦...', function () { const url = `https://${basePageUrl}/i/lists/1696907125090586845/members` GM_openInTab(url, {active: true}) }, ''); const menu_command_list7 = GM_registerMenuCommand('🔗 打开共享黑名单 ⑧...', function () { const url = `https://${basePageUrl}/i/lists/1770809941151896014/members` GM_openInTab(url, {active: true}) }, ''); const menu_command_special_list = GM_registerMenuCommand('🚫 拉黑加急名单', function () { if (window.confirm("加急名单里有一些爱主动拉黑人的诈骗犯,要一键屏蔽吗?")) { block_special_list() } else { GM_log('user cancelled block special scammers') } }, ''); const menu_command_all_list = GM_registerMenuCommand('🔗 查看全部名单(举报前请先搜索)...', function () { const url = 'https://raw.githubusercontent.com/daymade/Twitter-Block-Porn/master/lists/all.json' GM_openInTab(url, {active: true}) }, ''); const menu_command_report = GM_registerMenuCommand('🔗 我要举报...', function () { const url = 'https://github.com/daymade/Twitter-Block-Porn/issues' GM_openInTab(url, {active: true}) }, ''); const menu_command_sponsor = GM_registerMenuCommand('🥤 请我喝蜜雪冰城...', function () { const url = 'https://buymeacoffee.com/finetuning' GM_openInTab(url, {active: true}) }, ''); const ChangeLogo = GM_getValue('change_logo', true) GM_registerMenuCommand(`${ChangeLogo?'✅ 已将 Logo 还原为小蓝鸟, 点击可使用 \uD835\uDD4F':'🐤 点击唤回小蓝鸟'}`, function () { GM_setValue('change_logo', !ChangeLogo) location.reload() }); function get_cookie (cname) { const name = cname + '=' const ca = document.cookie.split(';') for (let i = 0; i < ca.length; ++i) { const c = ca[i].trim() if (c.indexOf(name) === 0) { return c.substring(name.length, c.length) } } return '' } const apiBaseUrlMap = { 'twitter.com': 'https://api.twitter.com', 'x.com': 'https://api.x.com', }; function getApiBaseUrl() { const hostname = window.location.hostname; return apiBaseUrlMap[hostname] || 'https://api.twitter.com'; // 默认使用 Twitter 的 API URL } const apiClient = axios.create({ withCredentials: true, headers: { Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', 'X-Twitter-Auth-Type': 'OAuth2Session', 'X-Twitter-Active-User': 'yes', 'X-Csrf-Token': get_cookie('ct0') } }); // 在发起请求前,设置正确的 baseURL apiClient.interceptors.request.use(config => { config.baseURL = getApiBaseUrl(); return config; }); // extract list id in url function parseListId (url) { // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567 return url.split('lists/')[1].split('/')[0] } async function fetch_list_members_id(listId) { let users = await fetch_list_members_info(listId) return users.map(u => u.id_str); } async function fetch_list_members_info(listId) { const merged = await fetchAndMergeLists(listId); console.log(`merged: ${JSON.stringify(merged)}}`); return merged; } async function fetchRemoteList(listId) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://raw.githubusercontent.com/daymade/Twitter-Block-Porn/master/lists/${listId}.json`, onload: function(response) { if (response.status === 200) { resolve(JSON.parse(response.responseText)); } else { console.warn(`Remote list for listId ${listId} not found.`); resolve([]); } }, onerror: function() { console.warn(`Error fetching remote list for listId ${listId}.`); resolve([]); } }); }); } async function fetchTwitterListMembers(listId) { let cursor = -1; let allMembers = []; while (cursor && cursor !== 0) { // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/api-reference/get-lists-members // https://api.twitter.com/1.1/lists/members.json // Endpoint | Requests per user | Requests per app // GET lists/members | 900/15min | 75/15min let response = await apiClient.get(`/1.1/lists/members.json?list_id=${listId}&cursor=${cursor}`); if (!response.data.users) { GM_log(`fetchTwitterListMembers errors: ${JSON.stringify(response.data)}`) return allMembers; } let users = response.data.users; allMembers = allMembers.concat(users); cursor = response.data.next_cursor; } return allMembers; } async function fetchAndMergeLists(listId) { let [remoteList, twitterList] = await Promise.all([ fetchRemoteList(listId), fetchTwitterListMembers(listId) ]).catch(err => { console.error('Promise.all error:', err) }); // Merge lists. Ensure uniqueness by 'id_str'. let merged = [...twitterList, ...remoteList]; let uniqueMembers = Array.from(new Map(merged.map(item => [item["id_str"], item])).values()); return uniqueMembers; } async function block_user (id, listId) { try { // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/mute-block-report-users/api-reference/post-blocks-create // https://api.twitter.com/1.1/blocks/create.json // Endpoint | Requests per user | Requests per app // not mentioned in doc!! await apiClient.post('/1.1/blocks/create.json', Qs.stringify({ user_id: id }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); // Update blocked IDs list in GM storage let blocked = GM_getValue('blockedIdsMapping', {}); if (!blocked[listId]) { blocked[listId] = []; } blocked[listId].push(id); GM_setValue('blockedIdsMapping', blocked); } catch (err) { GM_log(`error occurs when block_user: ${err}`); throw err; } } async function block_by_ids (member_ids, listId) { let blocked = GM_getValue('blockedIdsMapping', {}); GM_log(`block_by_ids: already blocked accounts: ${JSON.stringify(blocked)}`); let suspended = await fetchRemoteList('suspended'); GM_log(`block_by_ids: suspended accounts to be ignored: ${JSON.stringify(suspended)}`); let toBlock = member_ids .filter(id => !blocked[listId] || !blocked[listId].includes(id)) .filter(id => !suspended.includes(id)); const ids = [...new Set(toBlock)]; GM_log(`block_by_ids: about to block ${ids.length} scammers, detail: ${ids}`) // Number of requests per batch const batchSize = 10; // 5s delay between batches const delay = 5000; let failedIds = []; for (let i = 0; i < Math.ceil(ids.length / batchSize); i++) { const batch = ids.slice(i * batchSize, (i + 1) * batchSize); const r###lts = await Promise.allSettled(batch.map(id => block_user(id, listId))); for (const [index, r###lt] of r###lts.entries()) { if (r###lt.status === 'rejected' && r###lt.reason.response.status === 404) { // Keep track of failed IDs failedIds.push(batch[index]); } } if (i < Math.ceil(ids.length / batchSize) - 1) { await new Promise(resolve => setTimeout(resolve, delay)); } } if (failedIds.length > 0) { GM_log(`block_by_ids: failed to block these scammers: ${failedIds.join(', ')}`) } } async function block_list_test_members () { const listId = parseListId(location.href) const members = await fetch_list_members_id(listId) block_by_ids(members.slice(0, 10), listId) } async function block_list_members () { const listId = parseListId(location.href) const members = await fetch_list_members_id(listId) block_by_ids(members, listId) } async function block_special_list () { // 加急名单: 特别活跃/拉黑我/来挑衅的黄推 const special_scammers = [ "1626262000547377153", "1083844806", "1191513095774048256", "1240432468995473411", "1260746375010177033", "1287994653388623875", "1356533851250548737", "1367769524657786883", "1371653074217963522", "1413745607466885121", "1428063931130646531", "####954075084713986", "1455789588392202241", "1484792482357338113", "148###4205310570498", "1511380196", "1559311298986393603", "1562212902207033345", "1572580165900767234", "1578298585514668032", "1580799004983508992", "1583905468178567168", "1585644302381694976", "1589228466658250752", "1592249768771977216", "1592260043490983936", "1592573920905166849", "1592691909805604870", "1593155334742618112", "1593157357059837954", "1593620049704628224", "1593948516594102272", "1602373747344048128", "1616646252", "1616785364462743553", "1622437704159100929", "1623320122978099201", "1624527915663896576", "1626772507910082562", "1639847615981568001", "1650736618293133313", "1655041846790467585", "1655821795272937472", "1656797676820725761", "1658357788249321472", "1660685299930759168", "1665333981951172608", "1670444198586363904", "1671941042776702976", "1672954423663038466", "1673741619634241536", "1675179645870768128", "1676435725942661121", "1676489900021915648", "1679909362335354880", "1683326796488671232", "1683330763167780864", "1684583392665550850", "1684659885726916609", "1684743661853229056", "1684859751119847424", "1685367331013476352", "1685884844885295104", "1686221264023957504", "1687807818831986688", "1687816807766523905", "1688494198331420672", "1689943762531753985", "1690427325001904128", "175911002", "2802758389", "310749736", "3183558127", "532085468", "593711290", "769695361656991744", "824376009029992456", "976566332111179778", "769695361656991744", "3183558127", "1690427325001904128", "1675179645870768128", "1572106376", "1695160338780409856", "1637733191673950208", "1683682718863724544", "1399167832001241088", "1401414397021417472", "1387838616202788865", "1687365559355121665", "1399167832001241088", "168###2125169680384", "1387838616202788865", "1459187911329345538", "771777233878933504", "732529176805318661", "1593953486592303104", "1269873849568382981", "1631995677742907393", "837242544", "1642503707165364225", "1626262000547377153", "1624507776432242688", "358108318", "1612286141893595137", "1456084225791299586", "1687938257831514112", "1673189721445629953", "249563694", "1412358780872921093", "1688885999265361920" ] // `block` is a reserved listId for those sacmmers who has blocked me // see block.json in `lists` folder let scammers = await fetchRemoteList("block") let blockedIds = scammers.map(u => u.id_str) block_by_ids(special_scammers.concat(blockedIds), "block") } async function export_list_members () { const listId = parseListId(location.href); const members = await fetchTwitterListMembers(listId); // 创建一个 Blob 实例,包含 JSON 字符串的成员信息 const blob = new Blob([JSON.stringify(members, null, 2)], {type : 'application/json'}); // 创建一个下载链接并点击它来下载文件 const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `${listId}-${Date.now()}.json`; link.click(); } (_ => { /* Begin of Dependencies */ /* eslint-disable */ // https://gist.githubusercontent.com/BrockA/2625891/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts, that detects and handles AJAXed content. Usage example: waitForKeyElements ( "div.comments" , commentCallbackFunction ); //--- Page-specific function to do what we want when the node is found. function commentCallbackFunction (jNode) { jNode.text ("This comment changed by waitForKeyElements()."); } IMPORTANT: This function requires your script to have loaded jQuery. */ function waitForKeyElements ( selectorTxt, /* Required: The jQuery selector string that specifies the desired element(s). */ actionFunction, /* Required: The code to run when elements are found. It is passed a jNode to the matched element. */ bWaitOnce, /* Optional: If false, will continue to scan for new elements even after the first match is found. */ iframeSelector /* Optional: If set, identifies the iframe to search. */ ) { var targetNodes, btargetsFound; if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt); else targetNodes = $(iframeSelector).contents () .find (selectorTxt); if (targetNodes && targetNodes.length > 0) { btargetsFound = true; /*--- Found target node(s). Go through each and act if they are new. */ targetNodes.each ( function () { var jThis = $(this); var alreadyFound = jThis.data ('alreadyFound') || false; if (!alreadyFound) { //--- Call the payload function. var cancelFound = actionFunction (jThis); if (cancelFound) btargetsFound = false; else jThis.data ('alreadyFound', true); } } ); } else { btargetsFound = false; } //--- Get the timer-control variable for this selector. var controlObj = waitForKeyElements.controlObj || {}; var controlKey = selectorTxt.replace (/[^\w]/g, "_"); var timeControl = controlObj [controlKey]; //--- Now set or clear the timer as appropriate. if (btargetsFound && bWaitOnce && timeControl) { //--- The only condition where we need to clear the timer. clearInterval (timeControl); delete controlObj [controlKey] } else { //--- Set a timer, if needed. if ( ! timeControl) { timeControl = setInterval ( function () { waitForKeyElements ( selectorTxt, actionFunction, bWaitOnce, iframeSelector ); }, 300 ); controlObj [controlKey] = timeControl; } } waitForKeyElements.controlObj = controlObj; } /* eslint-enable */ /* End of Dependencies */ let lang = document.documentElement.lang if (lang == 'en-US') { lang = 'en' // TweetDeck } if (lang == 'zh-CN') { lang = 'zh' } const translations = { en: { lang_name: 'English', block_btn: 'Block all Scammers', block_test_btn: 'Test block top 10 Scammers', block_success: 'All scammers blocked!', block_test_success: 'Top 10 scammers test blocked successfully!', export_btn: 'Export', export_success: 'Export successful!', }, 'en-GB': { lang_name: 'British English', block_btn: 'Block all Scammers', block_test_btn: 'Test block top 10 Scammers', block_success: 'All scammers blocked!', block_test_success: 'Top 10 scammers test blocked successfully!', export_btn: 'Export', export_success: 'Export successful!', }, zh: { lang_name: '简体中文', block_btn: '屏蔽所有诈骗犯', block_test_btn: '屏蔽前10名', block_success: '诈骗犯已全部被屏蔽!', block_test_success: '前10名诈骗犯测试屏蔽成功!', export_btn: '导出', export_success: '导出成功!', }, 'zh-Hant': { lang_name: '正體中文', block_btn: '封鎖所有詐騙犯', block_test_btn: '測試封鎖前10名詐騙犯', block_success: '詐騙犯已全部被封鎖!', block_test_success: '前10名詐騙犯測試封鎖成功!', export_btn: '導出', export_success: '導出成功!', }, ja: { lang_name: '日本語', block_btn: 'すべての詐欺師をブロック', block_test_btn: 'トップ10詐欺師をテストブロック', block_success: 'すべての詐欺師がブロックされました!', block_test_success: 'トップ10の詐欺師がテストブロックされました!', export_btn: 'エクスポート', export_success: 'エクスポート成功!', }, vi: { lang_name: 'Tiếng Việt', block_btn: 'Chặn tất cả scammers', block_test_btn: 'Thử chặn top 10 scammers', block_success: 'Tất cả scammers đã bị chặn!', block_test_success: 'Đã thành công chặn thử top 10 scammers!', export_btn: 'Xuất', export_success: 'Xuất thành công!', }, ko: { lang_name: '한국어', block_btn: '모든 사기꾼을 차단', block_test_btn: '테스트 차단 사기꾼 상위 10', block_success: '모든 사기꾼이 차단되었습니다!', block_test_success: '상위 10 사기꾼 테스트 차단 성공!', export_btn: '내보내기', export_success: '내보내기 성공!', }, de: { lang_name: 'Deutsch', block_btn: 'Alle Betrüger blockieren', block_test_btn: 'Testblock Top 10 Betrüger', block_success: 'Alle Betrüger wurden blockiert!', block_test_success: 'Top 10 Betrüger erfolgreich getestet und blockiert!', export_btn: 'Exportieren', export_success: 'Export erfolgreich!', }, fr: { lang_name: 'French', block_btn: 'Bloquer tous les escrocs', block_test_btn: 'Test de blocage top 10 escrocs', block_success: 'Tous les escrocs sont bloqués !', block_test_success: 'Test de blocage des 10 premiers escrocs réussi !', export_btn: 'Exporter', export_success: 'Exportation réussie !', }, } let i18n = translations[lang] function rgba_to_hex (rgba_str, force_remove_alpha) { return '#' + rgba_str.replace(/^rgba?\(|\s+|\)$/g, '') // Get's rgba / rgb string values .split(',') // splits them at "," .filter((_, index) => !force_remove_alpha || index !== 3) .map(string => parseFloat(string)) // Converts them to numbers .map((number, index) => index === 3 ? Math.round(number * 255) : number) // Converts alpha to 255 number .map(number => number.toString(16)) // Converts numbers to hex .map(string => string.length === 1 ? '0' + string : string) // Adds 0 when length of one number is 1 .join('') .toUpperCase() } function hex_to_rgb (hex_str) { const r###lt = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i.exec(hex_str) return r###lt ? `rgb(${parseInt(r###lt[1], 16)}, ${parseInt(r###lt[2], 16)}, ${parseInt(r###lt[3], 16)})` : '' } function invert_hex (hex) { return '#' + (Number(`0x1${hex.substring(1)}`) ^ 0xFFFFFF).toString(16).substring(1).toUpperCase() } function get_theme_color () { const FALLBACK_COLOR = 'rgb(128, 128, 128)' let bgColor = getComputedStyle(document.querySelector('#modal-header > span')).color || FALLBACK_COLOR let buttonTextColor = hex_to_rgb(invert_hex(rgba_to_hex(bgColor))) for (const ele of document.querySelectorAll('div[role=\'button\']')) { const color = ele?.style?.backgroundColor if (color != '') { bgColor = color const span = ele.querySelector('span') buttonTextColor = getComputedStyle(span)?.color || buttonTextColor } } return { bgColor, buttonTextColor, plainTextColor: $('span').css('color'), hoverColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.9)'), mousedownColor: bgColor.replace(/rgb/i, 'rgba').replace(/\)/, ', 0.8)') } } function get_ancestor (dom, level) { for (let i = 0; i < level; ++i) { dom = dom.parent() } return dom } function get_notifier_of (msg) { return _ => { const banner = $(` <div id="bwl-notice" style="right:0px; position:fixed; left:0px; bottom:0px; display:flex; flex-direction:column;"> <div class="tbwl-notice"> <span>${msg}</span> </div> </div> `) const closeButton = $(` <span id="bwl-close-button" style="font-weight:700; margin-left:12px; margin-right:12px; cursor:pointer;"> Close </span> `) closeButton.click(_ => banner.remove()) $(banner).children('.tbwl-notice').append(closeButton) $('#layers').append(banner) setTimeout(() => banner.remove(), 5000) $('div[data-testid="app-bar-close"]').click() } } function mount_button (parentDom, name, executer, success_notifier) { const btn_mousedown = 'bwl-btn-mousedown' const btn_hover = 'bwl-btn-hover' const button = $(` <div aria-haspopup="true" role="button" data-focusable="true" class="bwl-btn-base" style="margin:3px" > <div class="bwl-btn-inner-wrapper"> <span> <span class="bwl-text-font">${name}</span> </span> </div> </div> `).addClass(parentDom.prop('classList')[0]) .hover(function () { $(this).addClass(btn_hover) }, function () { $(this).removeClass(btn_hover) $(this).removeClass(btn_mousedown) }) .on('selectstart', function () { return false }) .mousedown(function () { $(this).removeClass(btn_hover) $(this).addClass(btn_mousedown) }) .mouseup(function () { $(this).removeClass(btn_mousedown) if ($(this).is(':hover')) { $(this).addClass(btn_hover) } }) .click(async () => await executer()) .click(success_notifier) parentDom.append(button) } function insert_css () { 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;' function get_font_family () { for (const ele of document.querySelectorAll('div[role=\'button\']')) { const font_family = getComputedStyle(ele).fontFamily if (font_family) { return font_family + ', ' + FALLBACK_FONT_FAMILY } } return FALLBACK_FONT_FAMILY } const colors = get_theme_color() // switch related $('head').append(`<style> </style>`) // TODO: reduce repeated styles $('head').append(`<style> .tbwl-notice { align-self: center; display: flex; flex-direction: row; padding: 12px; margin-bottom: 32px; border-radius: 4px; color:rgb(255, 255, 255); background-color: rgb(29, 155, 240); font-family: ${FALLBACK_FONT_FAMILY}; font-size:15px; line-height:20px; overflow-wrap: break-word; } .bwl-btn-base { min-height: 30px; padding-left: 1em; padding-right: 1em; border: 1px solid ${colors.bgColor} !important; border-radius: 9999px; background-color: ${colors.bgColor}; } .bwl-btn-mousedown { background-color: ${colors.mousedownColor}; cursor: pointer; } .bwl-btn-hover { background-color: ${colors.hoverColor}; cursor: pointer; } .bwl-btn-inner-wrapper { font-weight: bold; -webkit-box-align: center; align-items: center; -webkit-box-flex: 1; flex-grow: 1; color: ${colors.bgColor}; display: flex; } .bwl-text-font { font-family: ${get_font_family()}; color: ${colors.buttonTextColor}; } .container { margin-top: 0px; margin-left: 0px; margin-right: 5px; } .checkbox { width: 100%; margin: 0px auto; position: relative; display: block; color: ${colors.plainTextColor}; } .checkbox input[type="checkbox"] { width: auto; opacity: 0.00000001; position: absolute; left: 0; margin-left: 0px; } .checkbox label:before { content: ''; position: absolute; left: 0; top: 0; margin: 0px; width: 22px; height: 22px; transition: transform 0.2s ease; border-radius: 3px; border: 2px solid ${colors.bgColor}; } .checkbox label:after { content: ''; display: block; width: 10px; height: 5px; border-bottom: 2px solid ${colors.bgColor}; border-left: 2px solid ${colors.bgColor}; -webkit-transform: rotate(-45deg) scale(0); transform: rotate(-45deg) scale(0); transition: transform ease 0.2s; will-change: transform; position: absolute; top: 8px; left: 6px; } .checkbox input[type="checkbox"]:checked ~ label::before { color: ${colors.bgColor}; } .checkbox input[type="checkbox"]:checked ~ label::after { -webkit-transform: rotate(-45deg) scale(1); transform: rotate(-45deg) scale(1); } .checkbox label { position: relative; display: block; padding-left: 31px; margin-bottom: 0; font-weight: normal; cursor: pointer; vertical-align: sub; width:fit-content; width:-webkit-fit-content; width:-moz-fit-content; } .checkbox label span { position: relative; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); } .checkbox input[type="checkbox"]:focus + label::before { outline: 0; } </style>`) } function main () { let inited = false const notice_export_success = get_notifier_of(i18n.export_success) const notice_block_test_success = get_notifier_of(i18n.block_test_success) const notice_block_success = get_notifier_of(`${i18n.block_success}, 为了安全起见, 每次最多拉黑 300 个`) waitForKeyElements('h2#modal-header[aria-level="2"][role="heading"]', ele => { if (!inited) { insert_css() inited = true } const ancestor = get_ancestor(ele, 3) const currentURL = window.location.href if (/\/lists\/[0-9]+\/members$/.test(currentURL)) { mount_button(ancestor, i18n.export_btn, export_list_members, notice_export_success) mount_button(ancestor, i18n.block_test_btn, block_list_test_members, notice_block_test_success) mount_button(ancestor, i18n.block_btn, block_list_members, notice_block_success) } }) } // 这个函数名字来自 @albaz64 (function makeBlueBirdGreatAgain() { if(!ChangeLogo) return; // Twitter logo 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" // Function to reset favicon document.querySelector(`head>link[rel="shortcut icon"]`).href = `//abs.twimg.com/favicons/twitter.ico` // Add style GM_addStyle( `header h1 a[href="/home"] { margin: 6px 4px 2px; } header h1 a[href="/home"] div { background-image: url("${SVG}"); background-size: contain; background-position: center; background-repeat: no-repeat; margin: 4px; } header h1 a[href="/home"] div svg { display: none; } header h1 a[href="/home"] :hover :after { content: "已被 Twitter-Block-Porn 替换"; font: message-box; color: gray; position: absolute; left: 48px; }` ) })() main() })()