nuke button

kill 'em all

// ==UserScript==
// @name        nuke button
// @namespace   https://github.com/yassghn/nuke-button
// @version     2025-03-12
// @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);
			}
		}
	}

	/**
	 * 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');
				log(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$1() {
	    // 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();

	})();

})();