Last active
July 5, 2026 06:42
-
-
Save kyohsuke/5a1915b10c2ccd12b781a85b4a2f0f0a to your computer and use it in GitHub Desktop.
X Reply Filter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name X Reply Filter | |
| // @version 1.1.0 | |
| // @description Filter replies on X | |
| // @match https://x.com/* | |
| // @match https://twitter.com/* | |
| // @homepageURL https://gist.github.com/kyohsuke/5a1915b10c2ccd12b781a85b4a2f0f0a | |
| // @updateURL https://raw.github.com/gist/kyohsuke/5a1915b10c2ccd12b781a85b4a2f0f0a/raw/twrefresh.user.js | |
| // @downloadURL https://raw.github.com/gist/kyohsuke/5a1915b10c2ccd12b781a85b4a2f0f0a/raw/twrefresh.user.js | |
| // @icon https://help.twitter.com/content/dam/help-twitter/brand/logo.png | |
| // @run-at document-start | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // 二重インストール防止 | |
| if (window.__X_REPLY_FILTER_INSTALLED__) { | |
| return; | |
| } | |
| window.__X_REPLY_FILTER_INSTALLED__ = true; | |
| const CONFIG = { | |
| KEEP_FOLLOWING: GM_getValue("KEEP_FOLLOWING", true), | |
| FILTER_WORD: GM_getValue("FILTER_WORD", true), | |
| FILTER_WORD_LIST: GM_getValue("FILTER_WORD_LIST", "").split(",").filter(Boolean), | |
| FILTER_BLUE: GM_getValue("FILTER_BLUE", true), | |
| MIN_FOLLOWER_GAP: GM_getValue("MIN_FOLLOWER_GAP", 1000), | |
| DEBUG_MODE: GM_getValue("DEBUG_MODE", false), | |
| }; | |
| GM_registerMenuCommand(`フォロー中を常に表示: ${CONFIG.KEEP_FOLLOWING ? "ON" : "OFF"}`, () => { | |
| GM_setValue("KEEP_FOLLOWING", !CONFIG.KEEP_FOLLOWING); | |
| const toggleTo = !CONFIG.KEEP_FOLLOWING ? "ON" : "OFF"; | |
| alert(`${toggleTo} に変更しました。\nページを再読み込みします。`); | |
| location.reload(); | |
| }); | |
| GM_registerMenuCommand(`特定ワードでフィルタ: ${CONFIG.FILTER_WORD ? "ON" : "OFF"}`, () => { | |
| GM_setValue("FILTER_WORD", !CONFIG.FILTER_WORD); | |
| const toggleTo = !CONFIG.FILTER_WORD ? "ON" : "OFF"; | |
| alert(`${toggleTo} に変更しました。\nページを再読み込みします。`); | |
| location.reload(); | |
| }); | |
| GM_registerMenuCommand(`フィルタワード (現在${CONFIG.FILTER_WORD_LIST.length}件登録)`, () => { | |
| const value = prompt("フィルタワード (カンマ区切り)", CONFIG.FILTER_WORD_LIST); | |
| if (value === null) return; | |
| GM_setValue("FILTER_WORD_LIST", value); | |
| alert("保存しました。\nページを再読み込みします。"); | |
| location.reload(); | |
| }); | |
| GM_registerMenuCommand(`Blueをフォロワー差でフィルタ: ${CONFIG.FILTER_BLUE ? "ON" : "OFF"}`, () => { | |
| GM_setValue("FILTER_BLUE", !CONFIG.FILTER_BLUE); | |
| const toggleTo = !CONFIG.FILTER_BLUE ? "ON" : "OFF"; | |
| alert(`${toggleTo} に変更しました。\nページを再読み込みします。`); | |
| location.reload(); | |
| }); | |
| GM_registerMenuCommand(`フォロワー差 (${CONFIG.MIN_FOLLOWER_GAP})`, () => { | |
| const prevGap = CONFIG.MIN_FOLLOWER_GAP; | |
| const value = prompt("フォロワー数 - フォロー数", prevGap); | |
| if (value === null) return; | |
| const num = Number(value); | |
| if (!Number.isFinite(num) || num < 0) { | |
| alert("数値を入力してください"); | |
| return; | |
| } | |
| if (prevGap == num) return; | |
| GM_setValue("MIN_FOLLOWER_GAP", num); | |
| alert(`${prevGap} -> ${num}に変更しました。\nページを再読み込みします。`); | |
| location.reload(); | |
| }); | |
| GM_registerMenuCommand(`デバッグモード: ${CONFIG.DEBUG_MODE ? "ON" : "OFF"}`, () => { | |
| GM_setValue("DEBUG_MODE", !CONFIG.DEBUG_MODE); | |
| const toggleTo = !CONFIG.DEBUG_MODE ? "ON" : "OFF"; | |
| alert(`${toggleTo} に変更しました。\nページを再読み込みします。`); | |
| location.reload(); | |
| }); | |
| GM_registerMenuCommand("設定を表示", () => { | |
| alert(JSON.stringify(CONFIG, null, 2)); | |
| }); | |
| GM_registerMenuCommand("設定を初期化", () => { | |
| GM_setValue("KEEP_FOLLOWING", true); | |
| GM_setValue("FILTER_WORD", true); | |
| GM_setValue("FILTER_WORD_LIST", "web3,crypto"); | |
| GM_setValue("FILTER_BLUE", true); | |
| GM_setValue("MIN_FOLLOWER_GAP", 1000); | |
| GM_setValue("DEBUG_MODE", false); | |
| location.reload(); | |
| }); | |
| const pageCode = ` | |
| (() => { | |
| if (window.__X_REPLY_FILTER_PAGE__) return; | |
| window.__X_REPLY_FILTER_PAGE__ = true; | |
| const CONFIG = ${JSON.stringify(CONFIG)}; | |
| const logger = CONFIG.DEBUG_MODE ? console : new Proxy({}, { | |
| get() { | |
| return () => {}; | |
| } | |
| }); | |
| function cleanupTweetInstructions(instructions) { | |
| for (const instruction of instructions) { | |
| if (instruction.type !== "TimelineAddEntries") { | |
| continue; | |
| } | |
| instruction.entries = instruction.entries.filter(entry => { | |
| try { | |
| if(entry.content?.entryType !== "TimelineTimelineModule") { | |
| return true; | |
| } | |
| const userResult = entry.content?.items?.[0]?.item?.itemContent?.tweet_results?.result?.core?.user_results?.result; | |
| if(!userResult) { | |
| return true; | |
| } | |
| logger.groupCollapsed("ReplyFilterEntryCheck"); | |
| logger.debug("UserResult:", userResult); | |
| const isFollowing = userResult.relationship_perspectives?.following; | |
| logger.debug("UserName:", userResult.core.name, "(", userResult.core.screen_name, ")"); | |
| logger.debug("Following:", isFollowing); | |
| // フォロー中なら表示 | |
| if(CONFIG.KEEP_FOLLOWING && isFollowing) { | |
| return true; | |
| } | |
| const blueVerified = userResult.is_blue_verified; | |
| logger.debug("BlueVerified:", blueVerified); | |
| // Blue人数差フィルタ | |
| if(CONFIG.FILTER_BLUE && blueVerified) { | |
| const followers = userResult.legacy?.followers_count ?? 0; | |
| const following = userResult.legacy?.friends_count ?? 0; | |
| logger.debug("Followers:", followers); | |
| logger.debug("Following:", following); | |
| logger.debug("Remove by followers filter:", ((followers - following) <= CONFIG.MIN_FOLLOWER_GAP)); | |
| // 人数差に満たなければ非表示 | |
| if((followers - following) <= CONFIG.MIN_FOLLOWER_GAP) { | |
| return false; | |
| } | |
| } | |
| // ワードフィルタ | |
| if(CONFIG.FILTER_WORD && 0 < CONFIG.FILTER_WORD_LIST.length) { | |
| const currentBio = userResult.profile_bio.description.toLowerCase(); | |
| const result = CONFIG.FILTER_WORD_LIST.some(word => { | |
| // Bio に特定文字列が含まれていたら非表示 | |
| const checkResult = currentBio.includes(word); | |
| if(checkResult) { | |
| logger.debug("Hit filter word:", word); | |
| } | |
| return checkResult; | |
| }); | |
| console.log("Word Check Result:", result); | |
| if(result) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } finally { | |
| logger.groupEnd(); | |
| } | |
| }); | |
| } | |
| return instructions; | |
| } | |
| const originalParse = JSON.parse; | |
| JSON.parse = function(text, reviver) { | |
| const obj = originalParse.call(this, text, reviver); | |
| const instructions = obj?.data?.threaded_conversation_with_injections_v2?.instructions; | |
| if (instructions) { | |
| cleanupTweetInstructions(instructions); | |
| logger.debug("[ReplyFilter] TweetDetail"); | |
| } | |
| return obj; | |
| }; | |
| logger.info("[ReplyFilter] Hook Installed"); | |
| })(); | |
| `; | |
| const script = document.createElement("script"); | |
| script.src = "data:text/javascript;charset=utf-8," + encodeURIComponent(pageCode); | |
| (document.head || document.documentElement).appendChild(script); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment