nuke button

kill 'em all

目前為 2025-03-13 提交的版本,檢視 最新版本

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

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址