nuke button

kill 'em all

当前为 2025-03-13 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        nuke button
// @namespace   https://github.com/yassghn/nuke-button
// @version     1
// @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'

    // 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'
        }
    }

    // globals
    var gCurrentPage = ''
    var gObservers = {}
    var gProfile = ''
    var gPageChanged = false
    var gWhiteList = []

    // log
    function log(msg, err = false) {
        if (config.debug) {
            if (err) {
                console.error(msg)
            } else {
                console.log(msg)
            }
        }
    }

    // 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]
        }
    }

    // set current page
    function initCurrentPage() {
        gCurrentPage = window.location.href
    }

    // 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']
    }

    // init globals
    function initGlobals() {
        initCurrentPage()
        initObservers()
        initProfile()
        initWhiteList()
    }

    // disconnect observers
    function disconnectObservers() {
        for (let observer of gObservers) {
            observer.disconnect()
        }
    }

    // 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
        }
    }

    // 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
        }
    }

    // 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
        }
    }

    // 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 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 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
    }

    // 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')
        }
    }

    /**
     * @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()
        })
    }

    // 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
    }

    // 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 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
    }

    // 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
    }

    // 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
    }

    // insert css
    function insertCss() {
        let $style
        $style ??= appendStyle()
        $style.textContent = getNukeButtonCss()
    }

    // 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
    }

    // 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
    }

    // 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
        }
    }

    // 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()
    }

    // 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
    }

    // 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
    }

    // 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
            }
        }
    }

    // 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
    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)
        }
    }

    // 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
    }

    // rebind the nuke function
    function rebindNukeCommand(post) {
        $(post).find(config.selectors.nukeButton).on('click', nuke)
    }

    // 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) })
    }

    // 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) })
        }
    }

    // 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
        gPageChanged = false
        // create timeline observer
        observeTimeline(config.selectors.hometl)
        log('home nav changed')
    }

    // add navigation listener
    function addHomeNavigationListener() {
        // add event listener for timeline tabs
        $(config.selectors.nav).eq(1).on('mousedown', onHomeNavigationEvent)
    }

    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
    }

    // 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)
    }

    // 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
    }

    // 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
    }

    // 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
            gPageChanged = 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
            gPageChanged = 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
            gPageChanged = 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
            gPageChanged = updatePage ? true : false
            // wait for timeline to load in
            await getElement(config.selectors.searchtl)
            // observe timeline
            observeTimeline(config.selectors.searchtl)
        }
    }

    // after navigation
    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)
    }

    async function observeWindowHref() {
        setInterval(() => {
            // check if location changed
            if (gCurrentPage != window.location.href) {
                // update current page
                initCurrentPage()
                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
                initCurrentPage()
                onWindowHrefChange()
            }
        })
        gObservers.href.disconnect()
        gObservers.href = observer
        observer.observe(document, { childList: true, subtree: true })
    }

    // setup mutation observers
    function observeApp() {
        // add timeline observer
        processCurrentPage()
        // add window location poling
        observeWindowHref()
    }

    // 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()

})();