Skip to content

Instantly share code, notes, and snippets.

@jordyvandomselaar
Last active April 4, 2025 03:23
Show Gist options
  • Save jordyvandomselaar/dc61585498c93aed2755b6a38c358ce1 to your computer and use it in GitHub Desktop.
Save jordyvandomselaar/dc61585498c93aed2755b6a38c358ce1 to your computer and use it in GitHub Desktop.
Remove all message requests on Twitter that appear to be spam, paste it in an Arc boost or something idk.
// ==UserScript==
// @name Twitter spam DM deleter
// @namespace http://tampermonkey.net/
// @version 2025-04-01
// @description try to take over the world!
// @author You
// @match https://x.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=x.com
// @grant none
// ==/UserScript==
async function run() {
const bearerToken = await getBearerToken();
const csrfToken = await getCSRFToken();
const dmInfo = await backoff(getDMInfo.bind(undefined, bearerToken, csrfToken));
const spamMessageRequests = getSpamMessageRequests(dmInfo)
const [successCount, failCount] = await deleteSpamMessageRequests(spamMessageRequests, csrfToken, bearerToken)
if(successCount === 0 && failCount === 0) {
return;
}
alert(`Removed ${successCount} spam message requests. ${failCount} failures.`);
const savedSuccessCount = parseInt(window.localStorage.getItem("twitter_spam_deleter_success_count") ?? "0");
const newSuccessCount = savedSuccessCount + successCount;
window.localStorage.setItem("twitter_spam_deleter_success_count", newSuccessCount.toString());
alert(`Success count so far: ${newSuccessCount}`)
}
function getSpamMessageRequests(dmInfo) {
const requests = Object.values(dmInfo.inbox_initial_state.conversations).filter(c => c.trusted === false)
const messages = dmInfo.inbox_initial_state.entries
const messagesGroupedByConversation = messages.reduce((acc, message) => {
if (!message.message) {
return acc;
}
const conversationId = message.message.conversation_id
if(!acc[conversationId]) {
acc[conversationId] = []
}
acc[conversationId].push(message.message.message_data.text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim())
return acc;
}, {})
const requestsWithMessages = requests.map(r => {
return {
...r,
messages: messagesGroupedByConversation[r.conversation_id]
}
})
const spamRequests = requestsWithMessages.filter(request => {
const mutualCount = request.social_proof?.find(sp => sp.proof_type === "mutual_friends")?.total ?? 0
// More than one mutual is unlikely to be spam.
if(mutualCount > 1) {
return false;
}
const messages = request.messages
const messagesAsString = messages.join(' ');
const spamWords = [
"watch for free",
"increase followers",
"new clients",
"add my whatsapp",
"promote your account",
"I want to meet some friends",
"real results",
"I guarantee",
"beauties from all",
"we're building",
"instagram tool",
"memecoin",
"bitcoin",
"solana",
"crypto",
"hack",
"How old"
]
if(spamWords.some(word => messagesAsString.toLowerCase().includes(word.toLowerCase()))) {
return true;
}
// If all messages combined sans emojis are just a single word, it's probably spam.
if(messagesAsString.toLowerCase().replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}]/gu, '').split(' ').filter(Boolean).length === 1) {
return true;
}
// sub-optimal check to see if there are only one or two messages and at least one of them contains a link
// Not all links in my DMs start with http(s):// so we gotta do some weird shit here
if(messages.length < 3 && messagesAsString.match(/.+?\..+?\//)) {
return true;
}
return false;
})
return spamRequests;
}
async function deleteSpamMessageRequests(spamMessageRequests, csrfToken, bearerToken) {
let removeCount = 0;
let failCount = 0;
for(const spamMessageRequest of spamMessageRequests){
if(!confirm(`Remove message ${spamMessageRequest.messages.join("|")}}?`)) continue;
try {
const response = await fetch(`https://x.com/i/api/1.1/dm/conversation/${spamMessageRequest.conversation_id}/delete.json`, {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"authorization": `Bearer ${bearerToken}`,
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": "\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"130\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-csrf-token": csrfToken,
"x-twitter-active-user": "yes",
"x-twitter-auth-type": "OAuth2Session",
"x-twitter-client-language": "en"
},
"body": "dm_secret_conversations_enabled=false&krs_registration_enabled=true&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_ext_limited_action_results=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&include_ext_views=true&dm_users=false&include_groups=true&include_inbox_timelines=true&include_ext_media_color=true&supports_reactions=true&supports_edit=true&include_conversation_info=true",
"method": "POST",
"mode": "cors",
"credentials": "include"
});
if(response.ok) {
removeCount++;
} else {
failCount++;
}
} catch (e) {
failCount++;
}
}
return [removeCount, failCount]
}
async function getCSRFToken() {
const cookie = await window.cookieStore.get("ct0");
if(!cookie) {
throw new Error("Couldn't get CSRF cookie")
}
return cookie.value
}
async function getBearerToken() {
const mainScriptUrl = await backoff(getMainScriptUrl);
const mainScriptCode = await backoff(getMainScriptCode.bind(undefined, mainScriptUrl));
const bearerToken = mainScriptCode.match(/Bearer (.+?)\"/)?.[1]
if(!bearerToken) {
throw new Error("Couldn't find bearer token");
}
return bearerToken;
}
async function getMainScriptUrl() {
const element = document.querySelector("[href^='https://abs.twimg.com/responsive-web/client-web/main.']")
if(!element){
throw new Error("Unable to find main script element")
}
return element.getAttribute("href");
}
async function getMainScriptCode(url) {
const response = await fetch(url);
if(!response.ok) {
throw new Error("Failed to fetch DM info.")
}
const body = await response.text();
return body;
}
async function getDMInfo(bearerToken, csrfToken) {
const response = await fetch("https://x.com/i/api/1.1/dm/inbox_initial_state.json?nsfw_filtering_enabled=false&filter_low_quality=true&include_quality=all&include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_is_blue_verified=1&include_ext_verified_type=1&include_ext_profile_image_shape=1&skip_status=1&dm_secret_conversations_enabled=false&krs_registration_enabled=true&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_ext_limited_action_results=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&include_ext_views=true&dm_users=true&include_groups=true&include_inbox_timelines=true&include_ext_media_color=true&supports_reactions=true&supports_edit=true&include_ext_edit_control=true&include_ext_business_affiliations_label=true&ext=mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle", {
"headers": {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"authorization": `Bearer ${bearerToken}`,
"cache-control": "no-cache",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": "\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"130\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-csrf-token": csrfToken,
"x-twitter-active-user": "yes",
"x-twitter-auth-type": "OAuth2Session",
"x-twitter-client-language": "en"
},
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
});
if(!response.ok) {
throw new Error("Failed to fetch DM info.")
}
const body = await response.json();
return body;
}
async function backoff(callback, options = {}) {
const { maxTries = 3, delayFn = (tries) => tries * 5000 } = options;
const attempt = async (tries) => {
try {
return await callback();
} catch (error) {
if (tries >= maxTries) {
throw error;
}
return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const result = await attempt(tries + 1);
resolve(result);
} catch (error) {
reject(error);
}
}, delayFn(tries));
});
}
};
return attempt(1);
}
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment