您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto fire to Impression Zombies with Japanese Twitter!
// ==UserScript== // @name Impression Zombie Buster(mod) // @namespace http://tampermonkey.net/ // @version 2025-09-18 // @description Auto fire to Impression Zombies with Japanese Twitter! // @author Ganohr, @rmc_km // @match https://x.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com // @grant none // @license CC BY-SA 4.0 // @license url https://creativecommons.org/licenses/by-sa/4.0/deed.ja // @source page https://ganohr.net/blog/a-monkey-soldier-who-automatically-exterminates-the-zombies-infesting-x/ // ==/UserScript== (function () { "use strict"; setInterval(() => { const menuGet = (e) => e.parentElement.parentElement.querySelector( "button[aria-haspopup='menu']" ); document .querySelectorAll( "div[data-testid='Tweet-User-Avatar']:not(:has(button))" ) .forEach((e) => { if (!menuGet(e)) { return; } const button = document.createElement("button"); button.style = "font-size:7pt;height:30pt;"; button.textContent = "block"; button.onclick = () => { const menuButton = menuGet(e); menuButton.click(); const i1 = setInterval(() => { const blockButton = document.querySelector( "div[role='menuitem'][data-testid='block']" ); if (!blockButton) { return; } clearInterval(i1); blockButton.click(); const i2 = setInterval(() => { const confirmButton = document.querySelector( "button[data-testid='confirmationSheetConfirm']" ); if (!confirmButton) { return; } clearInterval(i2); confirmButton.click(); }, 100); }, 100); }; e.append(button); }); }, 1000); const urlReg = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; const emojiReg = /[\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF]/g; const punctuationReg = /[ \$\uFFE5\^\+=`~<>{}\[\]|\u3000-\u303F!-#%-\x2A,-/:;\x3F@\x5B-\x5D_\x7B}\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E3B\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]+/g; const getPostedDate = (e) => { if (!e) { return new Date(); } const time = e.querySelector("time[datetime]"); return time ? new Date(time.attributes.datetime.value) : null; }; const getPostedText = (e, plain = false) => { if (!e) { return null; } const tweet = e.querySelector('div[data-testid="tweetText"]'); const plainer = (text) => plain ? text : text.replace(punctuationReg, ""); return tweet ? plainer(tweet.innerText.trim()) : null; }; const getPostedAccount = (e) => { if (!e) { return null; } const account = e.querySelector( "div[data-testid='User-Name']>div:last-child" ); return account ? account.innerText.split("\n")[0].trim() : null; }; const getPostedAccountName = (e) => { if (!e) { return null; } const accountName = e.querySelector( "div[data-testid='User-Name'] span" ); return accountName ? accountName.innerText.split("\n")[0].trim() : null; }; const checkZombieCarrier = (e) => { const firstFoundCarrier = e.querySelector( "svg[aria-label='認証済みアカウント']" ); const rePostingCarrier = e.querySelector( "div[role='link'] svg[aria-label='認証済みアカウント']" ); if ( firstFoundCarrier && rePostingCarrier && firstFoundCarrier !== rePostingCarrier ) { return true; } if (firstFoundCarrier && !rePostingCarrier) { return true; } return false; }; const hasEmoji = (e) => { let hasEmoji = false; e.querySelectorAll("img[alt]").forEach((img) => { if (hasEmoji) { return; } const html = img.outerHTML; if (emojiReg.test(html) || /emoji/.test(html)) { hasEmoji = true; } }); return hasEmoji; }; const checkEmojiOnlyPost = (e) => { if (!e) { return false; } const text = getPostedText(e); if (text) { return text.replace(emojiReg, "").trim().length === 0; } return hasEmoji(e); }; const checkRePostOnlyPost = (e) => { if (!e) { return false; } const text = e.innerText; if (/ブロックしているアカウントによるポストです。/g.test(text)) { return true; } if (!/引用/g.test(text)) { return false; } const textArray = e.innerText.split("\n"); const NOT_FOUND = -1; const START_INDEX = 4; let searchIndex = NOT_FOUND; let startOffset = 0; textArray.forEach((line, index) => { if (index === 0 && line === "Block") { startOffset = 1; } if (searchIndex !== NOT_FOUND) { return; } if (line === "引用") { searchIndex = index; } }); if (searchIndex === START_INDEX + startOffset) { return true; } let post = ""; for ( let index = START_INDEX + startOffset; index < searchIndex; index++ ) { post += textArray[index]; } return ( post .replace(urlReg, "") .replace(emojiReg, "") .replace(punctuationReg, "") .replace(/[\s\r\n ]/g, "").length === 0 ); }; const checkSpamTweet = (e) => { const innerText = e.innerText; if (/(dmm\.co\.jp|app\.link|a\.r10\.to)/.test(innerText)) { return true; } // ドメイン検出を最初に行う(innerTextとtextの両方をチェック) const suspiciousDomainReg = /\.(?:xyz|tk|ml|ga|cf|live|site|click|top|info|biz|pw|win)(?:$|\/|[:?#])|[a-z0-9]{8,}\..*kabu.*\.xyz|news[a-z0-9]{6,}\.[a-z0-9]+\.xyz|[a-z0-9]{10,}\.[a-z0-9]+\.(?:xyz|tk|live)|[a-z0-9]{6,}\.(?:tosayo|nayoto|newekuk|yilife|liveright)\.xyz|[a-z0-9]{6,}\.fangp\.top|[a-z0-9]{6,}\.yuizoo[a-z]{2,}\.xyz/gi; if (suspiciousDomainReg.test(innerText)) { return true; } const text = getPostedText(e, true); // textでもドメインチェック if (suspiciousDomainReg.test(text)) { return true; } if ( /(^| |\n|\r)(\$BEYOND|\$PARAM|\$BUBBLE|\@ricyofficial|\$RICY|\$XTER|\$COOKIE|\$BUBBLE|\$LOL|@Cookie3_com)($| |\n|\r)/gi.test( text ) ) { return true; } if ( /打扰楼主帖子|发个推广|借楼主宝|价不亏|的点击主页联系|此推特不|作任何回|([良よ](かったら|ければ))?(プロフ(ィール)?|ぷろふ)[のを]?(URL|リンク)?(から)?([き来]て|[見み]て|確認|かくにん|チェック|ちぇっく|check)|[気き]になったから(リプ|りぷ)(ライ|らい)?し|今フォローした.+?資産.+倍|NUDES\s*IN\s*PROFILE|NUDES\s*IN\s*PROFILE|月超絶材料[↓⬇︎▼▽⤋⤓⇩⇊⤸]{0,3}|株主優待制度.+?(拡充|の|を).+?(発表|拡充)|来週は.?S高.?(行く|いく)|S高.?(行く|いく).?の.?かい.???|明日は.?(ストップ高|S高)|ストップ高.?(期待|狙い|確実|決まり|買い気配)|爆益|爆上げ|テンバガー|業績修正|決算発表.+?(ストップ高|S高)|株式分割.+?(好感|材料)|自己株式.+?取得/gi.test( text ) ) { return true; } if ( /\s*[→⇒]\s*@/g.test(text) && hasEmoji(e) && /(プロフ(ィール)?(URL)?から|ぷろふ(ぃーる)?(URL)から|こんにち[はわ]|こんばん[はわ]|連絡(してね?)?|絡みましょう?|絡もう?|からもう?|お?話しし?ま(しょ|せんか|よう|する)|こっち|ここ)/gi.test( text ) && /よろしくね?|よろしくおねがいします|お?返事(待ってるね?|待ってます|してね?|ちょうだい|楽しみ|たのしみ|ください|下さい)/gi.test( text ) ) { return true; } if ( (/(りぷ|リプ)頂戴|お?(話|はな)しし?ましょう?|(ぷろふ(ぃー?る)?|プロフ(ィー?ル))?から([来き]て|よろしく|宜しく)|profみて|qr(コード)?を(スマホで?|すまほで)?([読よ]んでね?|読み?[こ込]んで)|♡と(ふぉろー?|フォロー)|(ふぉろー?|フォロー?)(らぶ|ラブ)?(りつ|リツ)\s*(して|で)/i.test( text ) || /(おな|オナ|オナ|すか|スカ|スカ|マン|マン|まん|パイ|ぱい|パイ|チン|チン|ちん)凸|無修正|(スカトロ|すかとろ)|レズ(ビアン)?|([おぉオォ][なナナ][二に][いぃーイィ]?)|ハメ撮り|のくぱぁ欲しい人|初めての人優先でDM送ります|発情期|おな凸|R18|18以上だけだよ|依存相手募集中|[MMSS][女男]|裏(アカ|あか)[男女]|写メ|おじさんすき/i.test( text )) && hasEmoji(e) && text.length > "おはなししましょう🙌🙌🙌".length ) { return true; } if ( /#pr|即現?金|即.+?円|総額.+?円|本日限定|稼[が-ご]|登録|報酬|#ad/i.test( text ) && /ポイ活|tiktok\s*lite|ポイント|マイル|クーポン|GET|プレゼント|ゲット[!!しすせだ]|paypay|追加報酬|過去最高|登録/i.test( text ) && text.length > 64 ) { return true; } const accountName = getPostedAccountName(e); const spamSiteReg = /bokuao-antena\.antenam\.jp|kinmirainews\.com|bnc\.lt/g; const sensitiveNGWordReg = /オフパコ|セフレ|おかず|オカズ/g; const followMeReg = /(follow|フォロー?|ふぉろー?)しても(OK|いい|良い|大丈夫)(かな|ですか?)?/g; if ( false || spamSiteReg.test(text) || (true && (false || sensitiveNGWordReg.test(text) || sensitiveNGWordReg.test(accountName)) && followMeReg.test(text)) || (true && followMeReg.test(text) && (false || hasEmoji(e) || emojiReg.test(text))) ) { return true; } return false; }; const zombieQueue = {}; const targettingZombie = (e) => { const zombie = getPostedAccount(e); if (zombieQueue.hasOwnProperty(zombie)) { return; } zombieQueue[zombie] = e; }; const postQueue = []; const NEED_POST_LENGTH = 8; const checkNearString = (a, b) => { // https://qiita.com/gomaoaji/items/603904e31f965d759293 // https://www.k-intl.co.jp/blog/B_200729A // thanx KIマーケティングチーム-川村インターナショナル, @gomaoaji-Qiita const getToNgram = (text, n = 3) => { let ret = {}; for (var m = 0; m < n; m++) { for (var i = 0; i < text.length - m; i++) { const c = text.substring(i, i + m + 1); ret[c] = ret[c] ? ret[c] + 1 : 1; } } return ret; }; const getValuesSum = (obj) => { return Object.values(obj).reduce( (prev, current) => prev + current, 0 ); }; const calculate = (a, b) => { const aGram = getToNgram(a); const bGram = getToNgram(b); const keyOfAGram = Object.keys(aGram); const keyOfBGram = Object.keys(bGram); // aGramとbGramに共通するN-gramのkeyの配列 const abKey = keyOfAGram.filter((n) => keyOfBGram.includes(n)); // aGramとbGramの内積(0と1の掛け算のため、小さいほうの値を足し算すれば終わる。) let dot = abKey.reduce( (prev, key) => prev + Math.min(aGram[key], bGram[key]), 0 ); // 長さの積(平方根の積は積の平方根) const abLengthMul = Math.sqrt( getValuesSum(aGram) * getValuesSum(bGram) ); return dot / abLengthMul; }; return calculate(a, b) * 100; }; const checkSamePost = (text, date, e, isZombieCarrier) => { const postText = text.replace(emojiReg, "").toLowerCase().trim(); if (postText.length < NEED_POST_LENGTH) { return false; } let isSamePost = false; let targetElement = null; const removeIndex = []; postQueue.forEach((post, i) => { const postedText = post.text; const postedDate = post.date; const postedIsCarrier = post.isCarrier; targetElement = post.element; if (isSamePost) { return; } if (postText != postedText) { if (checkNearString(postText, postedText) <= 90) { return; } } if (date < postedDate) { if ( (postedIsCarrier && targetElement) || (!postedIsCarrier && targetElement && postedText.length >= 15) ) { targettingZombie(targetElement); } removeIndex.push(i); return; } isSamePost = true; }); removeIndex .sort() .reverse() .forEach((i) => { postQueue.splice(i, 1); }); if (!isSamePost) { postQueue.push({ text: postText, date: date, element: e, isCarrier: isZombieCarrier, }); return false; } return targetElement; }; const checkedPostQueue = {}; const checkAlreadyCheckedPost = (post, date, account) => { const key = account + date; if (!checkedPostQueue.hasOwnProperty(key)) { checkedPostQueue[key] = post; return false; } if (checkedPostQueue[key] !== post) { return false; } return true; }; const checkMoreReplyLoadded = () => { let hasMoreReplyLoadded = false; document .querySelectorAll('div[data-testid="cellInnerDiv"] h2 span') .forEach((e) => { if (hasMoreReplyLoadded) { return; } if (/返信をさらに表示/.test(e.innerText)) { hasMoreReplyLoadded = true; } }); return hasMoreReplyLoadded; }; const checkAndClickMoreReply = () => { if (checkMoreReplyLoadded()) { return false; } let hasMoreReply = false; document .querySelectorAll('div[data-testid="cellInnerDiv"] span') .forEach((e) => { if (hasMoreReply) { return; } if (/返信をさらに表示/.test(e.innerText)) { e.click(); hasMoreReply = true; } else if (/さらに返信を表示する/.test(e.innerText)) { const button = e.parentElement.parentElement.parentElement.querySelector( "button[role='button']:has(span span)" ); if (button) { button.click(); hasMoreReply = true; } } }); return hasMoreReply; }; const checkZombie = (e) => { const post = getPostedText(e); const date = getPostedDate(e); const account = getPostedAccount(e); if (post === null || !date) { return false; } if (checkAlreadyCheckedPost(post, date, account)) { return false; } if (postAuthor && account && postAuthor === account) { return false; } if (checkSpamTweet(e)) { return true; } if (!checkZombieCarrier(e)) { checkSamePost(post, date, e, false); return false; } return ( false || checkEmojiOnlyPost(e) || checkSamePost(post, date, e, true) || checkRePostOnlyPost(e) ); }; const loggingZombie = (message, e) => console.log("Can't bust Zombie! " + message, e); const SHOOT_MODE_MENU = 1; const SHOOT_MODE_BLOCK = 2; const SHOOT_MODE_CONFIRM = 3; const SHOOT_WAIT = 20; let shootMode = SHOOT_MODE_MENU; let shootWait = SHOOT_WAIT; const initShoot = () => { shootMode = SHOOT_MODE_MENU; shootWait = SHOOT_WAIT; }; const removeZombieFromQueue = (zombie) => { delete zombieQueue[zombie]; initShoot(); }; const LS_KEY_KILLED_ZOMBIE = "ganohrs_izb_killed"; const getKilledZombieCount = () => { const num = Number(localStorage.getItem(LS_KEY_KILLED_ZOMBIE)); if (Number.isNaN(num)) { return 0; } return num; }; const LS_KEY_KILLED_LIST = "ganohrs_izb_list"; const getKilledZombieList = () => { const csv = localStorage.getItem(LS_KEY_KILLED_LIST); if (!csv) { return []; } return csv.split(","); }; const killedZombie = (zombie) => { const count = getKilledZombieCount() + 1; localStorage.setItem(LS_KEY_KILLED_ZOMBIE, count); const list = getKilledZombieList(); list.push(zombie); localStorage.setItem(LS_KEY_KILLED_LIST, list); return count; }; const shootZombie = () => { const entries = Object.entries(zombieQueue); if (!entries || entries.length === 0) { return; } const zombie = entries[0][0]; const e = entries[0][1]; const post = getPostedText(e, true); const date = getPostedDate(e); shootWait--; if (shootWait <= 0) { loggingZombie( "Can't bust Zombie! zombie named: [" + zombie + "], mode = " + shootMode, e ); removeZombieFromQueue(zombie); return; } const fireZombie = (nextMode, needRemove, e) => { if (e) { e.click(); shootWait = SHOOT_WAIT; shootMode = nextMode; if (needRemove) { removeZombieFromQueue(zombie); } } }; switch (shootMode) { case SHOOT_MODE_MENU: { fireZombie( SHOOT_MODE_BLOCK, false, e.querySelector( 'button[aria-expanded="false"][aria-haspopup="menu"][aria-label="もっと見る"]' ) ); return; } case SHOOT_MODE_BLOCK: { const topMenuItem = document.querySelector( "div[data-testid='Dropdown'] div[role='menuitem']" ); if (!topMenuItem) { return; } if (/さんのフォローを解除$/.test(topMenuItem.innerText)) { debugger; e.querySelector( 'div[aria-expanded="true"][aria-haspopup="menu"][aria-label="もっと見る"]' ).click(); removeZombieFromQueue(zombie); shootMode = SHOOT_MODE_MENU; return; } fireZombie( SHOOT_MODE_CONFIRM, false, document.querySelector( 'div[role="menuitem"][data-testid="block"]' ) ); return; } case SHOOT_MODE_CONFIRM: { fireZombie( SHOOT_MODE_MENU, true, document.querySelector( 'button[role="button"][data-testid="confirmationSheetConfirm"]' ) ); const killedZombieCount = killedZombie(zombie); console.log( "killed zombie named: [" + zombie + "], total: " + killedZombieCount ); console.log( "\tpost is : [" + post + "], datetime : [" + date.toLocaleString() + "]" ); return; } default: { removeZombieFromQueue(zombie); return; } } }; let postAuthor = ""; let nowLocation = location.href; const clearProperties = (o) => { Object.keys(o).forEach((k) => { delete o[k]; }); }; const needSkip = () => /\/(home|notifications|explore|messages|lists)/.test(nowLocation); const isEndOfTimeLine = () => { let foundMore = false; document .querySelectorAll("div[data-testid='cellInnerDiv']") .forEach((e) => { if (foundMore && e.clientHeight > 0) { return; } if ( /もっと見つける/.test(e.innerText) && /Xから/.test(e.innerText) ) { foundMore = true; } }); return foundMore; }; let loadingMoreReply = false; const cooldown = () => { nowLocation = location.href; clearProperties(zombieQueue); clearProperties(postQueue); postAuthor = ""; loadingMoreReply = false; initShoot(); }; const searchZombie = () => { if (nowLocation !== location.href) { cooldown(); } if (needSkip()) { return; } if (loadingMoreReply) { if (checkMoreReplyLoadded()) { loadingMoreReply = false; } } else if (checkAndClickMoreReply()) { loadingMoreReply = true; return; } document .querySelectorAll( "article[data-testid='tweet'] div[aria-labelledby][id^='id'] button.redblock-btn" + ", article[data-testid='tweet']:has(button.redblock-btn):has(div[data-testid='tweetPhoto']) button.redblock-btn" + ", article[data-testid='tweet']:has(button.redblock-btn):has(div[data-testid^='card']) button.redblock-btn" + ", article[data-testid='tweet']:has(article .r-x572qd) button.redblock-btn" // + ", article[data-testid='tweet']:has(button.redblock-btn):has(img[src^='https://abs-0.twimg.com/emoji/v2/svg/']) button.redblock-btn" // + ", article[data-testid='tweet']:has(button.redblock-btn):has(svg[aria-label='認証済みアカウント']) button.redblock-btn" ) .forEach((e) => { e.style.display = "block"; }); document.querySelectorAll("time[datetime]").forEach((t) => { t.innerText = new Date(new Date(t.dateTime) * 1 + 9 * 3600 * 1000) .toISOString() .substring(2, 16) .replace(/T/, " "); }); if (isEndOfTimeLine()) { return; } document .querySelectorAll("article[data-testid='tweet']") .forEach((e) => { postAuthor = getPostedAccount(document); if (checkZombie(e)) { targettingZombie(e); } }); }; cooldown(); setInterval(shootZombie, 50); setInterval(searchZombie, 1000); })();