// ==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.3.4 // @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 // @match https://twitter.com/* // @match https://mobile.twitter.com/* // @match https://tweetdeck.twitter.com/* // @exclude https://twitter.com/account/* // @require https://cdn.jsdelivr.net/npm/axios@0.25.0/dist/axios.min.js // @require https://cdn.jsdelivr.net/npm/qs@6.10.3/dist/qs.min.js // @require https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js // @namespace https://greasyfork.org/users/1121182 // @downloadURL none // ==/UserScript== /* global axios $ Qs */ const menu_command_list = GM_registerMenuCommand('打开共享黑名单 ①', function () { const url = 'https://twitter.com/i/lists/1677334530754248706/members' GM_openInTab(url, {active: true}) }, ''); const menu_command_member = GM_registerMenuCommand('打开共享黑名单 ②', function () { const url = 'https://twitter.com/i/lists/1683810394287079426/members' 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() }); (_ => { /* 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 result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i.exec(hex_str) return result ? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[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_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 '' } function get_ancestor (dom, level) { for (let i = 0; i < level; ++i) { dom = dom.parent() } return dom } const ajax = axios.create({ baseURL: 'https://api.twitter.com', withCredentials: true, headers: { Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', 'X-Twitter-Auth-Type': 'OAuth2Session', 'X-Twitter-Active-User': 'yes', 'X-Csrf-Token': get_cookie('ct0') } }) function get_list_id () { // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567 return location.href.split('lists/')[1].split('/')[0] } async function fetch_list_members_id(listId) { let cursor = -1; let allMembers = []; while (cursor != 0) { let response = await ajax.get(`/1.1/lists/members.json?list_id=${listId}&cursor=${cursor}`); let users = response.data.users; let members = users.map(u => u.id_str); allMembers = allMembers.concat(members); cursor = response.data.next_cursor; } return allMembers; } async function fetch_list_members_info(listId) { let cursor = -1; let allMembers = []; while (cursor != 0) { let response = await ajax.get(`/1.1/lists/members.json?list_id=${listId}&cursor=${cursor}`); let users = response.data.users; allMembers = allMembers.concat(users); cursor = response.data.next_cursor; } return allMembers; } function block_user (id) { ajax.post('/1.1/blocks/create.json', Qs.stringify({ user_id: id }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) } async function block_list_test_members () { const listId = get_list_id() const members = await fetch_list_members_id(listId) members.slice(0, 10).forEach(block_user) } async function block_list_members () { const listId = get_list_id() const members = await fetch_list_members_id(listId) // 要拉黑的 id 包括: // - 列表: https://twitter.com/i/lists/1677334530754248706 // - 列表: https://twitter.com/i/lists/1683810394287079426 // - 加急名单: 特别活跃/拉黑我/来挑衅的黄推 const special_scammers = [ "1585644302381694976", "1580799004983508992", "1578298585514668032", "824376009029992456", "1687816807766523905", "593711290", "1511380196", "1562212902207033345", "1684743661853229056", "1083844806", "175911002", "310749736", "1191513095774048256", "1639847615981568001", "1683326796488671232", "1413745607466885121", "1676435725942661121", "1684583392665550850", "1583905468178567168", "1371653074217963522", "1650736618293133313", "1684659885726916609", "1665333981951172608", "1592573920905166849", "1489964205310570498", "1683330763167780864", "1673741619634241536" ] // 去重 const unique_scammers = [...new Set(special_scammers)]; members.concat(unique_scammers) .slice(0, 1000) .forEach(block_user) } async function export_list_members () { const listId = get_list_id(); const members = await fetch_list_members_info(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 = "members.json"; link.click(); } function get_notifier_of (msg) { return _ => { const banner = $(`