Last active
August 20, 2026 05:38
-
-
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.4.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 = { | |
| ADD_COUNT_BADGE: GM_getValue("ADD_COUNT_BADGE", true), | |
| KEEP_FOLLOWING: GM_getValue("KEEP_FOLLOWING", true), | |
| KEEP_GOLD: GM_getValue("KEEP_GOLD", true), | |
| FILTER_MEDIA_ONLY_REPLY: GM_getValue("FILTER_MEDIA_ONLY_REPLY", false), | |
| 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.ADD_COUNT_BADGE ? "ON" : "OFF"}`, () => { | |
| GM_setValue("ADD_COUNT_BADGE", !CONFIG.ADD_COUNT_BADGE); | |
| const toggleTo = !CONFIG.ADD_COUNT_BADGE ? "ON" : "OFF"; | |
| alert(`${toggleTo} に変更しました。\nページを再読み込みします。`); | |
| location.reload(); | |
| }); | |
| 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.KEEP_GOLD ? "ON" : "OFF"}`, () => { | |
| GM_setValue("KEEP_GOLD", !CONFIG.KEEP_GOLD); | |
| const toggleTo = !CONFIG.KEEP_GOLD ? "ON" : "OFF"; | |
| alert(`${toggleTo} に変更しました。\nページを再読み込みします。`); | |
| location.reload(); | |
| }); | |
| GM_registerMenuCommand(`メディアのみのリプライをフィルタ: ${CONFIG.FILTER_MEDIA_ONLY_REPLY ? "ON" : "OFF"}`, () => { | |
| GM_setValue("FILTER_MEDIA_ONLY_REPLY", !CONFIG.FILTER_MEDIA_ONLY_REPLY); | |
| const toggleTo = !CONFIG.FILTER_MEDIA_ONLY_REPLY ? "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("KEEP_GOLD", true); | |
| GM_setValue("FILTER_MEDIA_ONLY_REPLY", false); | |
| 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 showBadge(count) { | |
| // 1. ロゴ要素を取得 (aria-label="X" または aria-label="ホーム" のリンク内SVG) | |
| const logo = document.querySelector('h1 [aria-label="X"], a[aria-label="ホーム"] svg, [data-testid="AppTabBar_Home_Link"] svg'); | |
| if (!logo) { | |
| console.log("ロゴが見つかりませんでした。"); | |
| return; | |
| } | |
| // 2. 親要素を相対位置指定 (position: relative) にする | |
| const container = logo.closest('a') || logo.parentElement; | |
| container.style.position = 'relative'; | |
| // 3. すでにバッジがあれば削除(重複防止) | |
| const existingBadge = container.querySelector('.x-logo-badge'); | |
| if (existingBadge) existingBadge.remove(); | |
| // 4. 数字バッジの要素を作成 | |
| const badge = document.createElement('span'); | |
| badge.className = 'x-logo-badge'; | |
| badge.textContent = count; // 表示したい数字 | |
| // 5. スタイル(位置や見た目)を設定 | |
| Object.assign(badge.style, { | |
| position: 'absolute', | |
| top: '2px', | |
| right: '4px', | |
| backgroundColor: '#f91880', // Xのアクセントカラー(ピンク/赤) | |
| color: '#ffffff', | |
| fontSize: '11px', | |
| fontWeight: 'bold', | |
| padding: '0 4px', | |
| height: '16px', | |
| minWidth: '16px', | |
| borderRadius: '8px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| zIndex: '9999', | |
| pointerEvents: 'none', | |
| boxSizing: 'border-box' | |
| }); | |
| // 6. コンテナに追加 | |
| container.appendChild(badge); | |
| } | |
| function cleanupTweetInstructions(instructions) { | |
| for (const instruction of instructions) { | |
| if (instruction.type !== "TimelineAddEntries") { | |
| continue; | |
| } | |
| let beforeCount = instruction.entries.length; | |
| instruction.entries = instruction.entries.filter(entry => { | |
| try { | |
| if(entry.content?.entryType !== "TimelineTimelineModule") { | |
| return true; | |
| } | |
| logger.groupCollapsed("ReplyFilterEntryCheck"); | |
| const tweetResult = entry.content?.items?.[0]?.item?.itemContent?.tweet_results?.result; | |
| if(!tweetResult) { | |
| return true; | |
| } | |
| logger.debug("TweetResult:", tweetResult); | |
| const userResult = tweetResult.core?.user_results?.result; | |
| if(!userResult) { | |
| return true; | |
| } | |
| 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 imageShape = userResult?.profile_image_shape; | |
| logger.debug("ImageShape:", imageShape); | |
| if(CONFIG.KEEP_GOLD && imageShape == "Square") { | |
| return true; | |
| } | |
| const blueVerified = userResult.is_blue_verified; | |
| logger.debug("BlueVerified:", blueVerified); | |
| // Blue人数差フィルタ | |
| if(CONFIG.FILTER_BLUE && blueVerified) { | |
| const followers = userResult.relationship_counts?.followers ?? 0; | |
| const following = userResult.relationship_counts?.following ?? 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) { | |
| logger.debug("Filter by blue follower difference"); | |
| return false; | |
| } | |
| } | |
| // メディアのみのリプライは非表示 | |
| const tweetLang = tweetResult?.legacy?.lang; | |
| if(CONFIG.FILTER_MEDIA_ONLY_REPLY && tweetLang == "qme") { | |
| logger.debug("Filter by Media Only Tweet"); | |
| return false; | |
| } | |
| // ワードフィルタ | |
| if(CONFIG.FILTER_WORD && 0 < CONFIG.FILTER_WORD_LIST.length) { | |
| const currentBio = userResult.profile_bio.description.toLowerCase(); | |
| const currentTweet = tweetResult?.legacy?.full_text; | |
| const currentURLs = tweetResult?.legacy?.entities?.urls; | |
| const result = CONFIG.FILTER_WORD_LIST.some(word => { | |
| const checkBioResult = currentBio.includes(word); // Bio に特定文字列が含まれていたら非表示 | |
| const checkTweetResult = currentTweet.includes(word); // Tweet に特定文字列が含まれていたら非表示 | |
| const checkUrlResult = currentURLs?.some(url => { | |
| return url.expanded_url.includes(word); | |
| }); | |
| logger.debug("checkBio:", checkBioResult); | |
| logger.debug("checkTweet:", checkTweetResult); | |
| logger.debug("checkURL:", checkUrlResult); | |
| const checkResult = checkBioResult || checkTweetResult || checkUrlResult; | |
| if(checkResult) { | |
| logger.debug("Hit filter word:", word); | |
| } | |
| return checkResult; | |
| }); | |
| logger.debug("Word Check Result:", result); | |
| if(result) { | |
| logger.debug("Filter by NG word"); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } finally { | |
| logger.groupEnd(); | |
| } | |
| }); | |
| let afterCount = instruction.entries.length; | |
| if(CONFIG.ADD_COUNT_BADGE) { | |
| showBadge(beforeCount - afterCount); | |
| } | |
| } | |
| 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