Skip to content

Instantly share code, notes, and snippets.

@tac-yacht
Last active February 10, 2024 11:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tac-yacht/0b656af7a9793bc7c08dd8a8d5e2e9d2 to your computer and use it in GitHub Desktop.
Save tac-yacht/0b656af7a9793bc7c08dd8a8d5e2e9d2 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name twitter/x spam blocking helper
// @version 1
// @grant none
// @include https://twitter.com/*
// @include https://x.com/*
// ==/UserScript==
//configuration
const EXCLUDE_PATH = [
'/home',
'/explore',
'/notifications',
'/messages',
'/settings',
]
/**
* 以下(<=)だとスパムと見なす
* 単位はms
*/
const SPAM_USER_JOIN_DURATION = 35 //日間
* 24 //日換算
* 60 //時換算
* 60 //分換算
* 1000 //秒換算
;
const SPAM_SCREEN_NAME_REGEX = /^[a-zA-Z_]+\d+/
//ユーティリティ類
// https://himenon.github.io/docs/javascript/wait/
const wait = async (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* 現在年月を取得
* @retrun {Date} 現在年月
*/
function getNowYearMonth() {
let now = new Date();
return new Date(
now.getFullYear(),
now.getMonth()
);
}
// --------------------------
// @see https://qiita.com/nanasi-1/items/dbaee5d609c2199e861b
let oldUrl = '';
const observer = new MutationObserver(() => {
if (oldUrl !== location.href) {
window.dispatchEvent(new CustomEvent('urlChange'));
oldUrl = location.href;
}
});
window.addEventListener('DOMContentLoaded', () => {
observer.observe(document.body, {
subtree: true,
childList: true,
attributes: true,
characterData: true
});
});
// --------------------------
// 画面内の情報拾う系
/**
* 利用開始日
* 関数としての入力パラメーターはないが、ユーザーのホームでないと動かない
* @retrun {Date} 年/月まで
*/
const USER_JOIN_DATE_PARSE_REGEX = /(?<year>\d{4}(?=年)).*(?<month>\d{1,2}(?=月))/
function getUserJoinDate() {
let rawText = document.querySelector('[data-testid="UserJoinDate"]').textContent;
let result = rawText.match(USER_JOIN_DATE_PARSE_REGEX);
return new Date(
result.groups.year,
result.groups.month - 1 /* monthIndexのため表示と1ずれる */
)
}
/**
* 利用開始日
* 関数としての入力パラメーターはないが、ユーザーのホームでないと動かない
* @retrun {string} スクリーンネーム
*/
function getScreenName() {
return new URL(location.href).pathname.substring(1);
}
//-------------------------------------------------
// ここからメイン処理
/**
* スパムか?
* 関数としての入力パラメーターはないが、ユーザーのホームでないと動かない
* @return {boolean} true:スパム(ぽい) false:非スパム
*/
//ふつうとは逆にセーフ判定を早期リターン
function isSpamSuspicion() {
if (Math.abs(getUserJoinDate().valueOf() - getNowYearMonth().valueOf()) > SPAM_USER_JOIN_DURATION) {
return false;
}
if (!getScreenName().match(SPAM_SCREEN_NAME_REGEX)) {
return false;
}
return true;
}
//twitterはSPAなので、includeで制御できない
/**
* 実際に有効にしたいURLであるか?
*/
function isIncludeURL() {
let pathname = new URL(location.href).pathname);
if (EXCLUDE_PATH.includes(pathname) {
return false;
}
//2階層以上パスあるからたぶん違う
if (pathname.match(/(\/.*){2,}/)) {
return false;
}
return true;
}
async function main() {
// 若干ラグがあるので、雑に待ち
await wait(1500);
try {
if (!isSpamSuspicion()) {
return;
}
} catch (e) {
console.warn('ページ解析に失敗', e);
return;
}
//ここからスパム判定でたときの処理
//報告画面オープン
document.querySelector('[data-testid="userActions"').click();
await wait(100);
document.evaluate('//*[@data-testid="Dropdown"]//*[contains(text(),"報告")]', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE)
.singleNodeValue.click();
await wait(500);
//スパムの選択肢クリック
document.evaluate('//*[@role="dialog"]//*[contains(text(),"スパム")]', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE)
.singleNodeValue.click();
//確定
document.querySelector('data-testid="ChoiceSelectionNextButton"').click();
await wait(500);
//ついでにブロック
document.evaluate('//*[@role="dialog"]//*[@role="button"]//*[contains(text(),"ブロック")]', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE).singleNodeValue.click()
}
window.addEventListener('urlChange', () => {
if (!isIncludeURL()) {
return;
}
main();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment