140 lines
4.5 KiB
JavaScript
140 lines
4.5 KiB
JavaScript
// ==UserScript==
|
|
// @name X Auto-Block by Keyword
|
|
// @version 0.3
|
|
// @description Automatically block accounts whose tweets contain banned words
|
|
// @match https://x.com/*
|
|
// @grant none
|
|
// @run-at document-end
|
|
// ==/UserScript==
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// 1) Banned words/phrases (case-insensitive)
|
|
const bannedKeywords = [
|
|
'like to see my',
|
|
'wanna see my'
|
|
];
|
|
|
|
// 2) Banned patterns (RegEx)
|
|
const bannedRegexes = [
|
|
/(?:^|\s)https?:\/\/t\.me\/[^\s]+/i,
|
|
/(?:^|\s)t\.me\/[^\s]+/i
|
|
];
|
|
|
|
function escapeRegex(str) {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
// Build keyword regex
|
|
const keywordRegex = new RegExp(
|
|
`\\b(?:${bannedKeywords.map(escapeRegex).join('|')})\\b`,
|
|
'i'
|
|
);
|
|
|
|
function containsBannedRegex(text) {
|
|
return bannedRegexes.some(regex => regex.test(text));
|
|
}
|
|
|
|
// Avoid reprocessing the same tweet/container
|
|
const processed = new WeakSet();
|
|
|
|
function waitForSelector(selector, timeout = 3000) {
|
|
return new Promise((resolve, reject) => {
|
|
const el = document.querySelector(selector);
|
|
if (el) return resolve(el);
|
|
|
|
const obs = new MutationObserver((_, ob) => {
|
|
const found = document.querySelector(selector);
|
|
if (found) {
|
|
ob.disconnect();
|
|
resolve(found);
|
|
}
|
|
});
|
|
obs.observe(document.body, { childList: true, subtree: true });
|
|
|
|
setTimeout(() => {
|
|
obs.disconnect();
|
|
reject(new Error(`Timeout waiting for selector: ${selector}`));
|
|
}, timeout);
|
|
});
|
|
}
|
|
|
|
async function inspectAndBlock(textEl) {
|
|
if (processed.has(textEl)) return;
|
|
processed.add(textEl);
|
|
|
|
const text = textEl.innerText || textEl.textContent;
|
|
if (!keywordRegex.test(text) && !containsBannedRegex(text)) return;
|
|
|
|
let container = textEl;
|
|
while (container && container.getAttribute('data-testid') !== 'tweet') {
|
|
container = container.parentElement;
|
|
}
|
|
if (!container) return;
|
|
|
|
try {
|
|
const moreBtn = container.querySelector('button[data-testid="caret"]');
|
|
if (!moreBtn) return;
|
|
moreBtn.click();
|
|
|
|
const blockItem = await waitForSelector('div[data-testid="block"]');
|
|
if (/^\s*Block\s+@.+/i.test(blockItem.innerText)) {
|
|
blockItem.click();
|
|
} else {
|
|
const closeBtn = await waitForSelector('button[aria-label="Close"]');
|
|
closeBtn.click();
|
|
return;
|
|
}
|
|
|
|
const confirmBtn = await waitForSelector('button[data-testid="confirmationSheetConfirm"]');
|
|
confirmBtn.click();
|
|
|
|
let matchedBanTerm = null;
|
|
|
|
const wordMatch = text.match(keywordRegex);
|
|
if (wordMatch) {
|
|
matchedBanTerm = wordMatch[0];
|
|
} else {
|
|
matchedBanTerm = bannedRegexes
|
|
.map(rx => {
|
|
const m = text.match(rx);
|
|
return m ? m[0].trim() : null;
|
|
})
|
|
.find(Boolean);
|
|
}
|
|
|
|
console.log(`Blocked account for tweet containing banned term: "${matchedBanTerm}"`);
|
|
|
|
} catch (err) {
|
|
console.error('Error during block flow:', err);
|
|
}
|
|
}
|
|
|
|
const observer = new MutationObserver(mutations => {
|
|
for (const mut of mutations) {
|
|
mut.addedNodes.forEach(node => {
|
|
if (node.nodeType !== 1) return;
|
|
const texts = node.matches && (
|
|
node.matches('[data-testid="tweetText"]') ||
|
|
node.matches('[data-testid="card.layoutSmall.detail"]') ||
|
|
node.matches('[data-testid="User-Name"]')
|
|
)
|
|
? [node]
|
|
: Array.from(node.querySelectorAll(
|
|
'[data-testid="tweetText"], [data-testid="card.layoutSmall.detail"], [data-testid="User-Name"]'
|
|
));
|
|
|
|
texts.forEach(el => inspectAndBlock(el));
|
|
});
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
document.querySelectorAll(
|
|
'[data-testid="tweetText"], [data-testid="card.layoutSmall.detail"], [data-testid="User-Name"]'
|
|
).forEach(el => inspectAndBlock(el));
|
|
|
|
})();
|