nuke button

kill 'em all

Stan na 10-04-2025. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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: true,
		mobile: (window.location.href.startsWith('https://mobile')) ? true : false,
		behavior: {
			newTabOnError: true
		},
		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();

	})();

})();