Last active
April 4, 2025 03:23
-
-
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.
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 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