您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
kill 'em all
// ==UserScript== // @name nuke button // @namespace https://github.com/yassghn/nuke-button // @version 2025-04-10 // @description kill 'em all // @icon https://www.svgrepo.com/download/528868/bomb-emoji.svg // @author yassghn // @match https://twitter.com/* // @match https://mobile.twitter.com/* // @match https://x.com/* // @match https://mobile.x.com/* // @run-at document-start // @grant none // @license OUI // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // ==/UserScript== /* global $ */ (function () { 'use strict'; /** * nuke-button * * config.mjs */ // config object const config = { projectName: 'nuke-button', debug: false, mobile: (window.location.href.startsWith('https://mobile')) ? true : false, behavior: { newTabOnError: false }, selectors: { nukeButton: 'a[class="nuke-button"]', posts: 'div[data-testid="User-Name"]', hometl: 'div[aria-label="Timeline: Your Home Timeline"]', tl: 'div[aria-label*="Timeline:"]', statustl: 'div[aria-label="Timeline: Conversation"]', searchtl: 'div[aria-label="Timeline: Search timeline"]', status: 'article[data-testid="tweet"]', postHref: 'a[href*="status"]', avatar: 'div[data-testid="Tweet-User-Avatar"]', nav: 'div [role="navigation"]', profile: 'a[aria-label="Profile"]', kbd: 'a[href="/i/keyboard_shortcuts"]', communities: 'a[aria-label="Communities"]' }, static: { icon: '💣', checkMark: '✔️', redCross: '❌' }, features: { rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, rweb_video_timestamps_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_enhance_cards_enabled: false, blue_business_profile_image_shape_enabled: false, tweetypie_unmention_optimization_enabled: true, responsive_web_text_conversations_enabled: true, vibe_api_enabled: true, responsive_web_twitter_blue_verified_badge_is_enabled: false, interactive_text_enabled: true, longform_notetweets_richtext_consumption_enabled: true, premium_content_api_read_enabled: true, profile_label_improvements_pcf_label_in_post_enabled: true, responsive_web_grok_analyze_post_followups_enabled: false, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_share_attachment_enabled: false }, fieldToggles: { count: 1000, rankingMode: "Relevance", withSafetyModeUserFields: true, includePromotedContent: true, withQuickPromoteEligibilityTweetFields: true, withVoice: true, withV2Timeline: true, withDownvotePerspective: false, withBirdwatchNotes: true, withCommunity: true, withSuperFollowsUserFields: true, withReactionsMetadata: false, withReactionsPerspective: false, withSuperFollowsTweetFields: true, isMetatagsQuery: false, withReplays: true, withClientEventToken: false, withAttachments: true, withConversationQueryHighlights: true, withMessageQueryHighlights: true, withMessages: true, with_rux_injections: false }, apiEndpoints: { tweetDetail: 'https://x.com/i/api/graphql/nBS-WpgA6ZG0CyNHD517JQ/TweetDetail', following: 'https://x.com/i/api/graphql/eWTmcJY3EMh-dxIR7CYTKw/Following', followers: 'https://x.com/i/api/graphql/pd8Tt1qUz1YWrICegqZ8cw/Followers', retweeters: 'https://x.com/i/api/graphql/0BoJlKAxoNPQUHRftlwZ2w/Retweeters', verifiedFollowers: 'https://x.com/i/api/graphql/srYtCtUs5BuBPbYj7agW6A/BlueVerifiedFollowers', userid: 'https://x.com/i/api/graphql/sLVLhk0bGj3MVFEKTdax1w/UserByScreenName', blockUser: 'https://x.com/i/api/1.1/blocks/create.json' } }; /** * nuke-button * * globals.mjs */ // globals var gCurrentPage = ''; var gObservers = {}; var gProfile = ''; var gPageChanged = false; var gWhiteList = []; // init profile href function initProfile() { // check if we're on a mobile device if (!config.mobile) { gProfile = $(config.selectors.profile).attr('href').split('/')[1]; } else { gProfile = $(config.selectors.communities).attr('href').split('/')[1]; } } // init observers function initObservers() { gObservers = { href: new MutationObserver(() => { }), timeline: new MutationObserver(() => { }) }; } // init white list function initWhiteList() { // array of usernames to whitelist gWhiteList = ['boryshn', 'yassghn_', 'commet_w']; } // set current page function initCurrentPage() { gCurrentPage = window.location.href; } // update current page function setCurrentPage(page) { gCurrentPage = page; } function setPageChanged(changed) { gPageChanged = changed; } // init globals function initGlobals() { initCurrentPage(); initObservers(); initProfile(); initWhiteList(); } // disconnect observers function disconnectObservers() { for (let observer of gObservers) { observer.disconnect(); } } /** * nuke-button * * log.mjs */ // log function log(msg, err = false) { if (config.debug) { if (err) { console.error(msg); } else { console.log(msg); } } } function logObj(obj, depth = null) { if (config.debug) { console.dir(obj, { depth: depth }); } } /** * nuke-button * * fight-react.mjs */ // delete react state /* function deleteReactState() { if ($('div')[0].firstElementChild['wrappedJSObject']) { delete ($('div')[0].firstElementChild['wrappedJSObject']) return '' } if ($('div')[0].firstElementChild) { delete ($('div')[0].firstElementChild) } //const reactPropsKey = Object.keys(wrapped).find(key => key.startsWith('__reactProps')) // //const state = wrapped[reactPropsKey].children?.props?.children?.props?.store?.getState() // //delete(state) } */ /** * * notes: * reactProps.children.props.history ??? */ /* async function removePostsReactProps(post, href) { const wrapped = $('div')[0].firstElementChild['wrappedJSObject'] || $('div')[0].firstElementChild const reactPropsKey = Object.keys(wrapped).find(key => key.startsWith('__reactProps')) const reactFiberKey = Object.keys(wrapped).find(key => key.startsWith('__reactFiber')) const reactProps = wrapped[reactPropsKey] const reactFiber = wrapped[reactFiberKey] logObj(reactFiber) logObj(reactProps) delete(reactFiber.memorizedProps) delete(reactFiber.stateNode) delete(reactProps.children.props.children.props) //delete(wrapped[key]) //wrapped[key] = {} //removeReactObjects(href) const article = $(post).find('article')[0].firstElementChild['wrappedJSObject'] const keys = Object.keys(article).filter(key => key.startsWith('__react')) //console.dir(article, { depth: null }) logObj(keys) keys.forEach((key) => { //logObj(article[key]) if (article[key]) { delete (article[key]) } }) //logObj(article) } */ // get react state function getReactState() { const wrapped = $('div')[0].firstElementChild['wrappedJSObject'] || $('div')[0].firstElementChild; const reactPropsKey = Object.keys(wrapped).find(key => key.startsWith('__reactProps')); const state = wrapped[reactPropsKey].children?.props?.children?.props?.store?.getState(); return state } // remvoe post react object function removeReactObjects(href) { // todo: not sure how to approach hacking at the react obejcts // need to stop them from repopulating the timeline with removed posts // also causes page to crash needing to reload // get react state const reactState = getReactState(); const statusId = href.split('/')[3]; // get in-reply-to if it exists const inReply = reactState.entities.tweets.entities[statusId].in_reply_to_status_id_str; // remove react objects if (reactState.entities.tweets.entities[statusId]) { //delete(reactState.entities.tweets.entities[statusId]) reactState.entities.tweets.entities[statusId].conversation_id_str = "1234"; } if (reactState.entities.tweets.fetchStatus[statusId]) { delete (reactState.entities.tweets.fetchStatus[statusId]); } for (const key in reactState.urt) { if (reactState.urt[key].entries) { for (let i = 0; i < reactState.urt[key].entries.length; i++) { if (reactState.urt[key].entries[i]) { if (reactState.urt[key].entries[i].entryId.includes(statusId)) { //delete(reactState.urt[key].entries[i]) reactState.urt[key].entries[i].entryId = 'noid-0000'; reactState.urt[key].entries[i].sortIndex = "1234"; reactState.urt[key].entries[i].type = 'nonexistingType'; } } } } } for (const key in reactState.audio.conversationLookup) { if (reactState.audio.conversationLookup[key]) { for (let i = 0; i < reactState.audio.conversationLookup[key].length; i++) { delete (reactState.audio.conversationLookup[key][i]); } } } } // hide post from timeline function hidePost(post) { // hide html from timeline $(post).html(''); $(post).hide(); } /** * nuke-button * * polling.mjs */ /** * @param {string} selector * @param {{ * name?: string * stopIf?: () => boolean * timeout?: number * context?: Document | HTMLElement * }?} options * @returns {Promise<HTMLElement | null>} */ function getElement(selector, { name = null, stopIf = null, timeout = Infinity, context = document, } = {}) { return new Promise((resolve) => { let startTime = Date.now(); let rafId; let timeoutId; function stop($element, reason) { resolve($element); } if (timeout !== Infinity) { timeoutId = setTimeout(stop, timeout, null, `${timeout}ms timeout`); } function queryElement() { let $element = context.querySelector(selector); if ($element) { log(`found element with selector: ${selector}`); stop($element); } else if (stopIf?.() === true) { stop(null, 'stopIf condition met'); } else { log(`waiting for element with selector: ${selector}`); rafId = requestAnimationFrame(queryElement); } } queryElement(); }) } // poll for react state async function pollReactState() { // new promise const promise = new Promise((resolve) => { // interval id let intervalId = 0; // function to return react state function returnReactState(reactState) { log('found react state'); logObj(reactState); // resolve react state resolve(reactState); } // poll for react state function poll() { // use set interval to poll intervalId = setInterval(function () { try { const reactState = getReactState(); // clear interval clearInterval(intervalId); // resolve returnReactState(reactState); } catch (error) { log('waiting for react state...'); } }, 1000); } // start polling poll(); }); return promise } // wait for user to login if necessary async function isLoggedIn(reactState) { // new promise const promise = new Promise((resolve) => { // interval id let intervalId = 0; // resolve promise function resolved() { log('user logged in'); // resolve resolve(true); } // poll checking if user is logged in function pollLoggedIn() { // poll with set interval intervalId = setInterval(function () { //check href and window vars if (!(window?.__META_DATA__?.isLoggedIn == false) && !window.location.href.includes('/i/flow/login')) { // clear interval clearInterval(intervalId); // resolve resolved(); } else { // keep polling log('waiting for user login'); } }, 1000); } // start polling pollLoggedIn(); }); // return promise return promise } /** * nuke-button * * api-data.mjs */ // extract item content from tweet responses function extractTweetResponseItemContent(entry) { if (entry.content.entryType === 'TimelineTimelineItem') return [entry.content.itemContent] if (entry.content.entryType === 'TimelineTimelineModule') return entry.content.items.map((item) => item.item.itemContent) return [] } // check if item content is a tweet entry function isTweetEntry(itemContent) { return ( itemContent.itemType === 'TimelineTweet' && itemContent.tweet_results.result.__typename !== 'TweetWithVisibilityResults' && itemContent.tweet_results.result.__typename !== 'TweetTombstone' ) } // extract user id from data function extractUserId(data) { return data?.data?.user?.result?.rest_id } // extract user response data from instructions function extractUserResponseData(instructions) { const data = instructions .flatMap((instr) => instr.entries || []) .filter( (entry) => entry.content.entryType === "TimelineTimelineItem" && entry.content.itemContent.user_results.result && entry.content.itemContent.user_results.result.__typename !== "UserUnavailable" ) .map((entry) => ({ username: entry.content.itemContent.user_results.result.legacy?.screen_name, isBlocked: entry.content.itemContent.user_results.result.legacy.blocking ?? entry.content.itemContent.user_results.result.smart_blocking ?? false, userId: entry.content.itemContent.user_results.result?.rest_id })) || []; return data } // extract user response data from instructions function extractTweetResponseData(instructions) { const entries = instructions.flatMap((instr) => instr.entries || []); // collect targets let responseTargets = []; // iterate instructions for (const entry of entries) { // get item contents const itemContents = extractTweetResponseItemContent(entry); // iterate item contents for (const itemContent of itemContents) { if (isTweetEntry(itemContent)) { const userId = itemContent.tweet_results.result.legacy.user_id_str; const username = itemContent.tweet_results.result.core.user_results.result.legacy.screen_name; const responseTarget = { username: username, isBlocked: false, userId: userId }; responseTargets.push(responseTarget); } } } return responseTargets } /** * nuke-button * * browser.mjs */ // open href in new tab function openHrefInNewTab(href) { // complete url const url = `https://x.com${href}`; log(`opening url: ${url}`); // open new tab window.open(url, '_blank'); //window.focus() } /** * nuke-button * * whitelist.mjs */ // filter block list function filterDedupWhiteList(item, index, arr) { // do not include white listed accounts if (gWhiteList.indexOf(item.username) > -1) { return false } // dedup based username and userid const i = arr.findIndex((item2) => ['username', 'userId'].every((key) => item2[key] === item[key])); return i === index } /** * nuke-button * * api-request.mjs */ // get cookie function getCookie(cname) { const name = cname + '='; const ca = document.cookie.split(';'); for (let i = 0; i < ca.length; ++i) { const c = ca[i].trim(); if (c.indexOf(name) === 0) { return c.substring(name.length, c.length) } } return '' } // api request headers function createRequestHeaders() { const headers = { Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', 'X-Twitter-Auth-Type': 'OAuth2Session', 'X-Twitter-Active-User': 'yes', 'X-Csrf-Token': getCookie('ct0') }; return headers } // Universal API request function for making requests async function apiRequest(url, method = 'GET', body = null) { const options = { headers: createRequestHeaders(), method, credentials: 'include' }; if (body) { options.body = body; options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; } try { const response = await fetch(url, options); // check for errors if (response.ok) { // return data const data = await response.json(); return data } else { // throw response error const errors = await response.json(); throw errors } } catch (error) { // add url to error error.url = url; //throw error throw error } } // build api url function buildUrl(endpoint, variables) { // start with endpoint const url = `${endpoint}` + `?variables=${encodeURIComponent(JSON.stringify(Object.assign(variables, config.fieldToggles)))}` + `&features=${encodeURIComponent(JSON.stringify(config.features))}`; return url } // Fetches the list of users a given user is following async function fetchUserFollowing(userId) { const variables = { userId }; const url = buildUrl(config.apiEndpoints.following, variables); try { const data = await apiRequest(url); return data } catch (e) { throw e } } // fetch users followers async function fetchUserFollowers(userId) { const variables = { userId }; const url = buildUrl(config.apiEndpoints.followers, variables); try { const data = await apiRequest(url); return data } catch (e) { throw e } } // fetch verified followers async function fetchVerifiedFollowers(userId) { const variables = { userId: userId }; const url = buildUrl(config.apiEndpoints.verifiedFollowers, variables); try { const data = await apiRequest(url); return data } catch (e) { throw e } } // Fetches responses for a tweet, with optional pagination cursor async function fetchTweetResponses(tweetId, cursor = null) { const variables = { focalTweetId: tweetId, cursor }; const url = buildUrl(config.apiEndpoints.tweetDetail, variables); try { const data = await apiRequest(url); return data } catch (e) { throw e } } // fetch retweeters async function fetchTweetRetweeters(tweetId, cursor = null) { const variables = { tweetId: tweetId }; const url = buildUrl(config.apiEndpoints.retweeters, variables); try { const data = await apiRequest(url); return data } catch (e) { throw e } } // blocks a user with the given user ID async function blockUser(userId) { try { const data = await apiRequest(config.apiEndpoints.blockUser, 'POST', `user_id=${userId}`); return data } catch (error) { throw error } } // fetch user id from username async function fetchUserId(username) { const variables = { screen_name: username }; const url = buildUrl(config.apiEndpoints.userid, variables); try { const data = await apiRequest(url); return data } catch (e) { throw e } } // get block list async function getBlockList(userId, username, tweetId) { try { // get data const followingData = await fetchUserFollowing(userId); const following = extractUserResponseData(followingData?.data?.user?.result?.timeline?.timeline?.instructions); const followersData = await fetchUserFollowers(userId); const followers = extractUserResponseData(followersData?.data?.user?.result?.timeline?.timeline?.instructions); const verifiedFollowersData = await fetchVerifiedFollowers(userId); const verifiedFollowers = extractUserResponseData(verifiedFollowersData?.data?.user?.result?.timeline?.timeline?.instructions); const responsesData = await fetchTweetResponses(tweetId); const responses = extractTweetResponseData(responsesData?.data?.threaded_conversation_with_injections_v2?.instructions); const retweetersData = await fetchTweetRetweeters(tweetId); const retweeters = extractUserResponseData(retweetersData?.data?.retweeters_timeline?.timeline?.instructions); // add target user to front of array const target = [{ username: username, isBlocking: false, userId: userId }]; // combine data const blockList = [].concat(target, following, followers, verifiedFollowers, responses, retweeters); // filter blocklist based on username and userid const filteredBlockList = blockList.filter(filterDedupWhiteList); // return block list return filteredBlockList } catch (e) { throw e } } // block a block list async function blockBlockList(blockList, href) { // init return let blockedTally = 0; let openedHref = false; // iterate block list for (const item of blockList) { // check if user is blocked already if (!item.isBlocked) { try { // block user const ret = await blockUser(item.userId); // increment blocked tally blockedTally += 1; } catch (error) { // log error log(`${error.name}, ${error.message}, ${error.cause}`, true); // something's going wrong, open post in new tab to finish nuke-ing later // probably got logged out (401), or api timeout (429) if (!openedHref && config.behavior.newTabOnError) { openHrefInNewTab(href); openedHref = true; } } } } // return success return blockedTally } /** * nuke-button * * html-css.mjs */ // add processing html/css /* function addProcessingElement(post, href, style) { // create processing html const processingHtml = $(getProcessingHtml(href)) // make timeline item div wrappers const divWrapper = $('<div/>') const outterDivWrapper = $('<div/>') const separatorDiv = $('<div/>') const divWrapperClasses = $(post).children().eq(0).attr('class') const outterDivWrapperClasses = $(post).attr('class') const separatorClasses = $(post).find('div[role="separator"]').attr('class') const transformCss = $(post).css('transform') log(transformCss) $(divWrapper).attr('class', divWrapperClasses) $(outterDivWrapper).attr('class', outterDivWrapperClasses) $(outterDivWrapper).attr('style', `transform: ${transformCss}; position: relative; width: 100%;`) $(outterDivWrapper).attr('data-testid', 'cellInnerDiv') $(separatorDiv).attr('class', separatorClasses) $(separatorDiv).attr('role', 'separator') // wrap divs $(processingHtml).attr('class', outterDivWrapperClasses) $(processingHtml).find('div').attr('class', outterDivWrapperClasses) // add css to elements $(processingHtml).css(getProcessingCss()) $(processingHtml).wrap($(outterDivWrapper)).wrap($(divWrapper)) const finalDiv = $(processingHtml).parents().eq(1) $(finalDiv).append($(separatorDiv)) log($(separatorDiv)) log(finalDiv) // add to dom $(finalDiv).insertBefore($(post)) // return outter most div wrapper return finalDiv } */ // append style element to head function appendStyle() { let $style = document.createElement('style'); $style.dataset.insertedBy = config.projectName; $style.dataset.role = 'features'; document.head.appendChild($style); return $style } // get twitter theme colors function getThemeColors() { // get body style const style = window.getComputedStyle($('body')[0]); // try to get theme color from two different elements const themeColor1 = style.getPropertyValue('--theme-color'); const themeColor2 = $(config.selectors.kbd).css('color'); return { color: (themeColor1 != '') ? themeColor1 : themeColor2, bg: style.getPropertyValue('background-color') } } // nuke button css function getNukeButtonCss() { const theme = getThemeColors(); const css = `a.nuke-button { z-index: 1; position: absolute; width: 30px; height: 30px; top: 45px; text-decoration: none; text-align: center; user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; -o-user-select: none; } a.nuke-button:hover { border-radius: 5px; background-color: ${theme.color}; } #nuke-button { width: 100%; height: 100%; line-height: 30px; } #nuke-button-text { margin: 0 auto; } #processing-text { -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; -o-user-select: none; } #processing-text span { display: inline-block; text-transform: uppercase; animation: flip 2s infinite; animation-delay: calc(.11s * var(--i)); } #nuke-confirmation { height: 100px; width: 100%; text-align: center; justify-content: center; align-items: center; text-transform: uppercase; padding-top: 30px; border: 2px ${theme.color} solid; border-radius: 2px; box-shadow: inset 0 0 2px ${theme.bg}, inset 0 0 7px ${theme.bg}, inset 0 0 14px ${theme.color}, inset 0 0 21px ${theme.color}, inset 0 0 28px ${theme.color}, inset 0 0 35px ${theme.color}; animation: glow 0.9s infinite alternate; } #nuke-confirmation-title { -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; -o-user-select: none; } #nuke-confirmation-title span { display: inline-block; animation: flip 2s infinite; animation-delay: calc(.5s * var(--i)); } .nuke-confirmation-button { display: inline-block; padding-left: 30px; padding-right: 30px; } .nuke-confirmation-button button { height: 50px; width: 80px; cursor: pointer; text-transform: uppercase; } @keyframes glow { 100% { box-shadow: inset 0 0 3px ${theme.bg}, inset 0 0 10px ${theme.bg}, inset 0 0 20px ${theme.color}, inset 0 0 40px ${theme.color}, inset 0 0 70px ${theme.color}, inset 0 0 89px ${theme.color}; } } @keyframes flip { 0%,80% { transform: rotateY(360deg); } }`; return css } // nuke button html function getNukeButtonHtml() { const nukeButtonHtml = `<a class="nuke-button" data-testid="block"> <div id="nuke-button"> <div id="nuke-button-text"> <span id="nuke-emoji">${config.static.icon}</span> </div> </div> </a>`; return nukeButtonHtml } // nuke confirmation html function getNukeConfirmationHtml() { const nukeConfirmationHtml = `<div id="nuke-confirmation"> <div id="nuke-confirmation-title"> <span style="--i:1;">are</span> <span style="--i:2;">you</span> <span style="--i:3;">sure</span> <span style="--i:4;">you</span> <span style="--i:5;">want</span> <span style="--i:6;">to</span> <span style="--i:7;">nuke</span> <span style="--i:8;">this</span> <span style="--i:9;">thread?</span> </div> <br/> <div class="nuke-confirmation-button"> <button name="yes" type="button" value="true"> <span>${config.static.checkMark}</span> <span>yes</span> </button> </div> <div class="nuke-confirmation-button"> <button name="no" type="button" value="false"> <span>${config.static.redCross}</span> <span>no</span> </button> </div> </div>`; return nukeConfirmationHtml } // get post href function getPostHref(post) { // get target links attached to post const links = $(post).find(config.selectors.postHref); // iterate links for (let i = 0; i < links.length; i++) { // get href const href = $(links[i]).attr('href'); // toss out incorrect links if (href.includes('analytics') || href.includes('photo') || href.includes('history') || href.includes('retweets')) { const arr = href.split('/'); const ret = `/${arr[1]}/${arr[2]}/${arr[3]}`; return ret } else if (i == links.length - 1) { // return last link if none found return href } } } // processing css function getProcessingCss() { const theme = getThemeColors(); const css = { 'height': `100px`, 'text-align': 'center', 'justify-content': 'center', 'align-items': 'center', 'padding-top': '30px', 'border': `2px ${theme.color} solid`, 'border-radius': '2px', 'box-shadow': `inset 0 0 2px ${theme.bg}, inset 0 0 7px ${theme.bg}, inset 0 0 14px ${theme.color}, inset 0 0 21px ${theme.color}, inset 0 0 28px ${theme.color}, inset 0 0 35px ${theme.color}`, 'animation': 'glow 0.7s infinite alternate' }; return css } // insert css function insertCss() { let $style; $style ??= appendStyle(); $style.textContent = getNukeButtonCss(); } // edit status view css async function editStatusViewCss() { // create css const css = { 'z-index': -1, 'top': '17px' }; // wait for element to load in await getElement(config.selectors.status); // add new style $(config.selectors.status).find('div').eq(1).children().eq(2).css(css); } // html to display while processing is happening function getProcessingHtml(href) { // remove leading slash from href const info = href.substring(1, href.length); // make processing text html const processingTextHtml = `<div id="processing-text"> <span style="--i:1;">n</span> <span style="--i:2;">u</span> <span style="--i:3;">k</span> <span style="--i:4;">e</span> <span style="--i:5;">-</span> <span style="--i:6;">i</span> <span style="--i:7;">n</span> <span style="--i:8;">g</span> <span style="--i:10;">.</span> <span style="--i:11;">.</span> <span style="--i:12;">.</span> </div>`; // combine and return const processingHtml = `<div id="processing"><article role="article" tabindex="0" data-testid="tweet">` + `${processingTextHtml}<br/>` + `<span id="processing-info-text">${info}</span></article></div>`; return processingHtml } /** * nuke-button * * nuke-button.mjs */ // kill 'em all async function killEmAll(href) { // get username from href const targetUsername = href.split('/')[1]; const tweetId = href.split('/')[3]; try { // get user data const userData = await fetchUserId(targetUsername); // check for error if (userData.message) { // throw error throw userData.message } // extract user id const targetUserId = extractUserId(userData); const blockList = await getBlockList(targetUserId, targetUsername, tweetId); const result = await blockBlockList(blockList, href); log(`processing finished for ${href}: blocked ${result} accounts`); } catch (e) { // log error url if (e.url) { log(`api request error for url: ${e.url}`, true); } // log error log(e, true); return undefined } // return href on success return href } // rebind the nuke function function rebindNukeCommand(post) { $(post).find(config.selectors.nukeButton).on('click', nuke); } // check for quote tweet function isQuoteTweet(post) { // quoted tweets have two classes if ($(post).attr('class').split(/\s+/).length > 1) { const quote = $(post).parents().eq(6).find('span:contains("Quote")'); return quote.length > 0 } return false } // should add nuke button function shouldAddNukeButton(post) { // check if post is null if (post != null) { // do not add nuke button to self's own posts let profile = ''; // check for quote tweet if (isQuoteTweet(post)) { profile = $(post).parents().eq(1).find('span:contains("@")').first().text().split('@')[1]; } else { profile = $(post).parents().eq(1).find('a').first().attr('href').split('/')[1]; } // white list check const whiteListed = gWhiteList.find((username) => username.toString() === profile.toString()); // check against global profile variable and whitelist if (gProfile != profile && !whiteListed) { return true } } // default return return false } // append nuke button html to post function appendNukeButtonHtml(post) { // check if buke button should be added if (shouldAddNukeButton(post)) { // apend html $(post).parents().first().parents().first().append(getNukeButtonHtml()); // arm nuke $(post).parents().eq(1).find(config.selectors.nukeButton).on('click', nuke); } } // insert nuke button html function addNukeButton() { // todo: breaks opening post in new tab, link at the end, probably react is looking for last link $(config.selectors.avatar).each((index, post) => { appendNukeButtonHtml(post); }); } // nuke confirmation async function nukeConfirmation(post) { // init return let ret = false; // store post html const postHtml = $(post).html(); // set confirmation html $(post).html(getNukeConfirmationHtml()); // add even listeners to buttons const promise = new Promise((resolve) => { // yes button $(post).find('button[name="yes"]').on('click', function (event) { resolve(true); }); // no button $(post).find('button[name="no"]').on('click', function (event) { resolve(false); }); }); // await confirmation response await promise.then((result) => { // store return value ret = result; // reset post html $(post).html(postHtml); }); // return result return ret } // nuke async function nuke(event) { // get upper context const post = $(this).parents().eq(6); // get post href const href = getPostHref(post); // confirm nuke-ing if (await nukeConfirmation(post)) { // log log('NUKE-ing!: ...' + href); // append processing html $(post).html(getProcessingHtml(href)); // add css to elements $(post).find('#processing').css(getProcessingCss()); // start nuking! await killEmAll(href); // remove react object removeReactObjects(href); // todo: error reporting, syncing with view // hide post from timeline hidePost(post); } else { rebindNukeCommand(post); } } /** * nuke-button * * observe.mjs */ // on timeline change function onTimelineChange(mutations) { for (const mutation of mutations) { // append nuke button $(mutation.addedNodes).find(config.selectors.avatar).each((index, post) => { appendNukeButtonHtml(post); }); } } function isUserPage() { // try to get username const username = window.location.href.split('/')[3]; const url = `/${username}/header_photo`; // test for user page if ($('div').find(`a[href="${url}"]`).length == 1) { log(':is user page:'); return true } // return false return false } // build userpage timeline selector function getUserPageTimelineSelector() { // get user display name const displayName = $('div[data-testid="UserName"]').find('span').first().text(); const selector = `div[aria-label="Timeline: ${displayName}’s posts"]`; return selector } // observe timeline async function observeTimeline(selector) { // create observer with callback const observer = new MutationObserver((mutations) => { onTimelineChange(mutations); }); // disconnect old observer // todo: not sure if this is necessary, maybe modals disconnect it anyway? gObservers.timeline.disconnect(); gObservers.timeline = observer; // wait for timeline to load in await getElement(config.selectors.status); observer.observe($(selector).children().first()[0], { childList: true }); // check if page has changed //if (!gPageChanged) { // add nuke button to initial posts addNukeButton(); //} } // when home timeline navigation event is propagated function onHomeNavigationEvent() { // reset page changed if it was changed setPageChanged(false); // create timeline observer observeTimeline(config.selectors.hometl); log('home nav changed'); } async function observeWindowHref() { setInterval(() => { // check if location changed if (gCurrentPage != window.location.href) { // update current page setCurrentPage(window.location.href); onWindowHrefChange(); } }, 1000); } // observe for location changes function observeWindowHrefasdf() { // observer with callback const observer = new MutationObserver(() => { // check if location changed if (gCurrentPage != window.location.href) { // update current page setCurrentPage(window.location.href); onWindowHrefChange(); } }); gObservers.href.disconnect(); gObservers.href = observer; observer.observe(document, { childList: true, subtree: true }); } // process current page async function processCurrentPage(updatePage = false) { // check href location if (window.location.href.endsWith('home')) { // check for home // todo: work out updating this value after more back and forth browsing // todo: work out race conditions where polling fails // update page changed setPageChanged(updatePage ? true : false); addHomeNavigationListener(); // wait for timeline to load in await getElement(config.selectors.hometl); // todo: dethrottle polling when no posts are loading observeTimeline(config.selectors.hometl); } else if (isUserPage()) { // check for userpage // update page changed setPageChanged(updatePage ? true : false); // get userpage timeline selector const selector = getUserPageTimelineSelector(); // wait for timeline to load in await getElement(selector); observeTimeline(selector); } else if (window.location.href.includes('status')) { // check for status (post) view page // todo: editstatusview does not update correctly sometimes // todo: if you need to use 'view' for the post nuke-button does not populate to the main status //update page changed setPageChanged(updatePage ? true : false); // wait for timeline to load in await getElement(config.selectors.statustl); // change status view css editStatusViewCss(); // obvserve timeline observeTimeline(config.selectors.statustl); } else if (window.location.href.includes('search?q=')) { // check for search page //update page changed setPageChanged(updatePage ? true : false); // wait for timeline to load in await getElement(config.selectors.searchtl); // observe timeline observeTimeline(config.selectors.searchtl); } } async function onWindowHrefChange() { log(`window href changed: ${window.location.href}`); // wait for react state const reactState = await pollReactState(); // wait for login if necessary await isLoggedIn(reactState); // process current page processCurrentPage(true); } // add navigation listener function addHomeNavigationListener() { // add event listener for timeline tabs $(config.selectors.nav).eq(1).on('mousedown', onHomeNavigationEvent); } // setup mutation observers function observeApp() { // add timeline observer processCurrentPage(); // add window location poling observeWindowHref(); } /** * nuke-button * * nuke-button.user.js */ (function () { 'use strict'; // main async function main() { // wait for react state const reactState = await pollReactState(); // wait for login if necessary await isLoggedIn(reactState); // wait for timeline to load in await getElement(config.selectors.tl); // init globals initGlobals(); // insert css insertCss(); // observe observeApp(); } // run script window.onload = main(); })(); })();