Skip to content

Instantly share code, notes, and snippets.

@kyohsuke
Last active August 20, 2026 05:38
Show Gist options
  • Select an option

  • Save kyohsuke/5a1915b10c2ccd12b781a85b4a2f0f0a to your computer and use it in GitHub Desktop.

Select an option

Save kyohsuke/5a1915b10c2ccd12b781a85b4a2f0f0a to your computer and use it in GitHub Desktop.
X Reply Filter
// ==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