nuke button

kill 'em all

当前为 2025-02-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name nuke button
  3. // @namespace https://github.com/yassghn/nuke-button
  4. // @version 2025-02-28
  5. // @description kill 'em all
  6. // @icon https://www.svgrepo.com/download/528868/bomb-emoji.svg
  7. // @author yassghn
  8. // @match https://twitter.com/*
  9. // @match https://mobile.twitter.com/*
  10. // @match https://x.com/*
  11. // @match https://mobile.x.com/*
  12. // @run-at document-start
  13. // @grant none
  14. // @license OUI
  15. // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
  16. // ==/UserScript==
  17.  
  18. /* global $ */
  19.  
  20. (function () {
  21. 'use strict'
  22.  
  23. // config object
  24. const config = {
  25. projectName: 'nuke-button',
  26. debug: false,
  27. mobile: (window.location.href.startsWith('https://mobile')) ? true : false,
  28. behavior: {
  29. newTabOnError: false
  30. },
  31. selectors: {
  32. nukeButton: 'a[class="nuke-button"]',
  33. posts: 'div[data-testid="User-Name"]',
  34. hometl: 'div[aria-label="Timeline: Your Home Timeline"]',
  35. tl: 'div[aria-label*="Timeline:"]',
  36. statustl: 'div[aria-label="Timeline: Conversation"]',
  37. searchtl: 'div[aria-label="Timeline: Search timeline"]',
  38. status: 'article[data-testid="tweet"]',
  39. postHref: 'a[href*="status"]',
  40. avatar: 'div[data-testid="Tweet-User-Avatar"]',
  41. nav: 'div [role="navigation"]',
  42. profile: 'a[aria-label="Profile"]',
  43. kbd: 'a[href="/i/keyboard_shortcuts"]',
  44. communities: 'a[aria-label="Communities"]'
  45. },
  46. static: {
  47. icon: '💣',
  48. checkMark: '✔️',
  49. redCross: '❌'
  50. },
  51. features: {
  52. rweb_tipjar_consumption_enabled: true,
  53. responsive_web_graphql_exclude_directive_enabled: true,
  54. verified_phone_label_enabled: false,
  55. creator_subscriptions_tweet_preview_api_enabled: true,
  56. responsive_web_graphql_timeline_navigation_enabled: true,
  57. responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
  58. communities_web_enable_tweet_community_results_fetch: true,
  59. c9s_tweet_anatomy_moderator_badge_enabled: true,
  60. articles_preview_enabled: true,
  61. responsive_web_edit_tweet_api_enabled: true,
  62. graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
  63. view_counts_everywhere_api_enabled: true,
  64. longform_notetweets_consumption_enabled: true,
  65. responsive_web_twitter_article_tweet_consumption_enabled: true,
  66. tweet_awards_web_tipping_enabled: false,
  67. creator_subscriptions_quote_tweet_preview_enabled: false,
  68. freedom_of_speech_not_reach_fetch_enabled: true,
  69. standardized_nudges_misinfo: true,
  70. tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
  71. rweb_video_timestamps_enabled: true,
  72. longform_notetweets_rich_text_read_enabled: true,
  73. longform_notetweets_inline_media_enabled: true,
  74. responsive_web_enhance_cards_enabled: false,
  75. blue_business_profile_image_shape_enabled: false,
  76. tweetypie_unmention_optimization_enabled: true,
  77. responsive_web_text_conversations_enabled: true,
  78. vibe_api_enabled: true,
  79. responsive_web_twitter_blue_verified_badge_is_enabled: false,
  80. interactive_text_enabled: true,
  81. longform_notetweets_richtext_consumption_enabled: true,
  82. premium_content_api_read_enabled: true,
  83. profile_label_improvements_pcf_label_in_post_enabled: true,
  84. responsive_web_grok_analyze_post_followups_enabled: false,
  85. responsive_web_grok_analyze_button_fetch_trends_enabled: false,
  86. responsive_web_grok_share_attachment_enabled: false
  87. },
  88. fieldToggles: {
  89. count: 1000,
  90. rankingMode: "Relevance",
  91. withSafetyModeUserFields: true,
  92. includePromotedContent: true,
  93. withQuickPromoteEligibilityTweetFields: true,
  94. withVoice: true,
  95. withV2Timeline: true,
  96. withDownvotePerspective: false,
  97. withBirdwatchNotes: true,
  98. withCommunity: true,
  99. withSuperFollowsUserFields: true,
  100. withReactionsMetadata: false,
  101. withReactionsPerspective: false,
  102. withSuperFollowsTweetFields: true,
  103. isMetatagsQuery: false,
  104. withReplays: true,
  105. withClientEventToken: false,
  106. withAttachments: true,
  107. withConversationQueryHighlights: true,
  108. withMessageQueryHighlights: true,
  109. withMessages: true,
  110. with_rux_injections: false
  111. },
  112. apiEndpoints: {
  113. tweetDetail: 'https://x.com/i/api/graphql/nBS-WpgA6ZG0CyNHD517JQ/TweetDetail',
  114. following: 'https://x.com/i/api/graphql/eWTmcJY3EMh-dxIR7CYTKw/Following',
  115. followers: 'https://x.com/i/api/graphql/pd8Tt1qUz1YWrICegqZ8cw/Followers',
  116. retweeters: 'https://x.com/i/api/graphql/0BoJlKAxoNPQUHRftlwZ2w/Retweeters',
  117. verifiedFollowers: 'https://x.com/i/api/graphql/srYtCtUs5BuBPbYj7agW6A/BlueVerifiedFollowers',
  118. userid: 'https://x.com/i/api/graphql/sLVLhk0bGj3MVFEKTdax1w/UserByScreenName',
  119. blockUser: 'https://x.com/i/api/1.1/blocks/create.json'
  120. }
  121. }
  122.  
  123. // globals
  124. var gCurrentPage = ''
  125. var gObservers = {}
  126. var gProfile = ''
  127. var gPageChanged = false
  128. var gWhiteList = []
  129.  
  130. // log
  131. function log(msg, err = false) {
  132. if (config.debug) {
  133. if (err) {
  134. console.error(msg)
  135. } else {
  136. console.log(msg)
  137. }
  138. }
  139. }
  140.  
  141. // init profile href
  142. function initProfile() {
  143. // check if we're on a mobile device
  144. if (!config.mobile) {
  145. gProfile = $(config.selectors.profile).attr('href').split('/')[1]
  146. } else {
  147. gProfile = $(config.selectors.communities).attr('href').split('/')[1]
  148. }
  149. }
  150.  
  151. // set current page
  152. function initCurrentPage() {
  153. gCurrentPage = window.location.href
  154. }
  155.  
  156. // init observers
  157. function initObservers() {
  158. gObservers = { href: new MutationObserver(() => { }), timeline: new MutationObserver(() => { }) }
  159. }
  160.  
  161. // init white list
  162. function initWhiteList() {
  163. // array of usernames to whitelist
  164. gWhiteList = ['boryshn', 'yassghn_', 'commet_w']
  165. }
  166.  
  167. // init globals
  168. function initGlobals() {
  169. initCurrentPage()
  170. initObservers()
  171. initProfile()
  172. initWhiteList()
  173. }
  174.  
  175. // disconnect observers
  176. function disconnectObservers() {
  177. for (let observer of gObservers) {
  178. observer.disconnect()
  179. }
  180. }
  181.  
  182. // get cookie
  183. function getCookie(cname) {
  184. const name = cname + '='
  185. const ca = document.cookie.split(';')
  186. for (let i = 0; i < ca.length; ++i) {
  187. const c = ca[i].trim()
  188. if (c.indexOf(name) === 0) {
  189. return c.substring(name.length, c.length)
  190. }
  191. }
  192. return ''
  193. }
  194.  
  195. // api request headers
  196. function createRequestHeaders() {
  197. const headers = {
  198. Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  199. 'X-Twitter-Auth-Type': 'OAuth2Session',
  200. 'X-Twitter-Active-User': 'yes',
  201. 'X-Csrf-Token': getCookie('ct0')
  202. }
  203. return headers
  204. }
  205.  
  206. // Universal API request function for making requests
  207. async function apiRequest(url, method = 'GET', body = null) {
  208. const options = {
  209. headers: createRequestHeaders(),
  210. method,
  211. credentials: 'include'
  212. }
  213. if (body) {
  214. options.body = body
  215. options.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  216. }
  217. try {
  218. const response = await fetch(url, options)
  219. // check for errors
  220. if (response.ok) {
  221. // return data
  222. const data = await response.json()
  223. return data
  224. } else {
  225. // throw response error
  226. const errors = await response.json()
  227. throw errors
  228. }
  229. } catch (error) {
  230. // add url to error
  231. error.url = url
  232. //throw error
  233. throw error
  234. }
  235. }
  236.  
  237. // build api url
  238. function buildUrl(endpoint, variables) {
  239. // start with endpoint
  240. const url = `${endpoint}` +
  241. `?variables=${encodeURIComponent(JSON.stringify(Object.assign(variables, config.fieldToggles)))}` +
  242. `&features=${encodeURIComponent(JSON.stringify(config.features))}`
  243. return url
  244. }
  245.  
  246. // Fetches the list of users a given user is following
  247. async function fetchUserFollowing(userId) {
  248. const variables = {
  249. userId
  250. }
  251. const url = buildUrl(config.apiEndpoints.following, variables)
  252. try {
  253. const data = await apiRequest(url)
  254. return data
  255. } catch (e) {
  256. throw e
  257. }
  258. }
  259.  
  260. // fetch users followers
  261. async function fetchUserFollowers(userId) {
  262. const variables = {
  263. userId
  264. }
  265. const url = buildUrl(config.apiEndpoints.followers, variables)
  266. try {
  267. const data = await apiRequest(url)
  268. return data
  269. } catch (e) {
  270. throw e
  271. }
  272. }
  273.  
  274. // fetch verified followers
  275. async function fetchVerifiedFollowers(userId) {
  276. const variables = {
  277. userId: userId
  278. }
  279. const url = buildUrl(config.apiEndpoints.verifiedFollowers, variables)
  280. try {
  281. const data = await apiRequest(url)
  282. return data
  283. } catch (e) {
  284. throw e
  285. }
  286. }
  287.  
  288. // Fetches responses for a tweet, with optional pagination cursor
  289. async function fetchTweetResponses(tweetId, cursor = null) {
  290. const variables = {
  291. focalTweetId: tweetId,
  292. cursor
  293. }
  294. const url = buildUrl(config.apiEndpoints.tweetDetail, variables)
  295. try {
  296. const data = await apiRequest(url)
  297. return data
  298. } catch (e) {
  299. throw e
  300. }
  301. }
  302.  
  303. // fetch retweeters
  304. async function fetchTweetRetweeters(tweetId, cursor = null) {
  305. const variables = {
  306. tweetId: tweetId
  307. }
  308. const url = buildUrl(config.apiEndpoints.retweeters, variables)
  309. try {
  310. const data = await apiRequest(url)
  311. return data
  312. } catch (e) {
  313. throw e
  314. }
  315. }
  316.  
  317. // fetch user id from username
  318. async function fetchUserId(username) {
  319. const variables = { screen_name: username }
  320. const url = buildUrl(config.apiEndpoints.userid, variables)
  321. try {
  322. const data = await apiRequest(url)
  323. return data
  324. } catch (e) {
  325. throw e
  326. }
  327. }
  328.  
  329. // blocks a user with the given user ID
  330. async function blockUser(userId) {
  331. try {
  332. const data = await apiRequest(config.apiEndpoints.blockUser, 'POST', `user_id=${userId}`)
  333. return data
  334. } catch (error) {
  335. throw error
  336. }
  337. }
  338.  
  339. // extract user id from data
  340. function extractUserId(data) {
  341. return data?.data?.user?.result?.rest_id
  342. }
  343.  
  344. // extract user response data from instructions
  345. function extractUserResponseData(instructions) {
  346. const data = instructions
  347. .flatMap((instr) => instr.entries || [])
  348. .filter(
  349. (entry) =>
  350. entry.content.entryType === "TimelineTimelineItem" &&
  351. entry.content.itemContent.user_results.result &&
  352. entry.content.itemContent.user_results.result.__typename !== "UserUnavailable"
  353. )
  354. .map((entry) => ({
  355. username: entry.content.itemContent.user_results.result.legacy?.screen_name,
  356. isBlocked: entry.content.itemContent.user_results.result.legacy.blocking ??
  357. entry.content.itemContent.user_results.result.smart_blocking ??
  358. false,
  359. userId: entry.content.itemContent.user_results.result?.rest_id
  360. })) || []
  361. return data
  362. }
  363.  
  364. // extract item content from tweet responses
  365. function extractTweetResponseItemContent(entry) {
  366. if (entry.content.entryType === 'TimelineTimelineItem')
  367. return [entry.content.itemContent]
  368. if (entry.content.entryType === 'TimelineTimelineModule')
  369. return entry.content.items.map((item) => item.item.itemContent)
  370. return []
  371. }
  372.  
  373. // check if item content is a tweet entry
  374. function isTweetEntry(itemContent) {
  375. return (
  376. itemContent.itemType === 'TimelineTweet' &&
  377. itemContent.tweet_results.result.__typename !== 'TweetWithVisibilityResults' &&
  378. itemContent.tweet_results.result.__typename !== 'TweetTombstone'
  379. )
  380. }
  381.  
  382. // extract user response data from instructions
  383. function extractTweetResponseData(instructions) {
  384. const entries = instructions.flatMap((instr) => instr.entries || [])
  385. // collect targets
  386. let responseTargets = []
  387. // iterate instructions
  388. for (const entry of entries) {
  389. // get item contents
  390. const itemContents = extractTweetResponseItemContent(entry)
  391. // iterate item contents
  392. for (const itemContent of itemContents) {
  393. if (isTweetEntry(itemContent)) {
  394. const userId = itemContent.tweet_results.result.legacy.user_id_str
  395. const username = itemContent.tweet_results.result.core.user_results.result.legacy.screen_name
  396. const responseTarget = { username: username, isBlocked: false, userId: userId }
  397. responseTargets.push(responseTarget)
  398. }
  399. }
  400. }
  401. return responseTargets
  402. }
  403.  
  404. // append style element to head
  405. function appendStyle() {
  406. let $style = document.createElement('style')
  407. $style.dataset.insertedBy = config.projectName
  408. $style.dataset.role = 'features'
  409. document.head.appendChild($style)
  410. return $style
  411. }
  412.  
  413. // get twitter theme colors
  414. function getThemeColors() {
  415. // get body style
  416. const style = window.getComputedStyle($('body')[0])
  417. // try to get theme color from two different elements
  418. const themeColor1 = style.getPropertyValue('--theme-color')
  419. const themeColor2 = $(config.selectors.kbd).css('color')
  420.  
  421. return {
  422. color: (themeColor1 != '') ? themeColor1 : themeColor2,
  423. bg: style.getPropertyValue('background-color')
  424. }
  425. }
  426.  
  427. /**
  428. * @param {string} selector
  429. * @param {{
  430. * name?: string
  431. * stopIf?: () => boolean
  432. * timeout?: number
  433. * context?: Document | HTMLElement
  434. * }?} options
  435. * @returns {Promise<HTMLElement | null>}
  436. */
  437. function getElement(selector, {
  438. name = null,
  439. stopIf = null,
  440. timeout = Infinity,
  441. context = document,
  442. } = {}) {
  443. return new Promise((resolve) => {
  444. let startTime = Date.now()
  445. let rafId
  446. let timeoutId
  447.  
  448. function stop($element, reason) {
  449. resolve($element)
  450. }
  451.  
  452. if (timeout !== Infinity) {
  453. timeoutId = setTimeout(stop, timeout, null, `${timeout}ms timeout`)
  454. }
  455.  
  456. function queryElement() {
  457. let $element = context.querySelector(selector)
  458. if ($element) {
  459. log(`found element with selector: ${selector}`)
  460. stop($element)
  461. }
  462. else if (stopIf?.() === true) {
  463. stop(null, 'stopIf condition met')
  464. }
  465. else {
  466. log(`waiting for element with selector: ${selector}`)
  467. rafId = requestAnimationFrame(queryElement)
  468. }
  469. }
  470.  
  471. queryElement()
  472. })
  473. }
  474.  
  475. // nuke button html
  476. function getNukeButtonHtml() {
  477. const nukeButtonHtml =
  478. `<a class="nuke-button" data-testid="block">
  479. <div id="nuke-button">
  480. <div id="nuke-button-text">
  481. <span id="nuke-emoji">${config.static.icon}</span>
  482. </div>
  483. </div>
  484. </a>`
  485. return nukeButtonHtml
  486. }
  487.  
  488. // html to display while processing is happening
  489. function getProcessingHtml(href) {
  490. // remove leading slash from href
  491. const info = href.substring(1, href.length)
  492. // make processing text html
  493. const processingTextHtml =
  494. `<div id="processing-text">
  495. <span style="--i:1;">n</span>
  496. <span style="--i:2;">u</span>
  497. <span style="--i:3;">k</span>
  498. <span style="--i:4;">e</span>
  499. <span style="--i:5;">-</span>
  500. <span style="--i:6;">i</span>
  501. <span style="--i:7;">n</span>
  502. <span style="--i:8;">g</span>
  503. <span style="--i:10;">.</span>
  504. <span style="--i:11;">.</span>
  505. <span style="--i:12;">.</span>
  506. </div>`
  507. // combine and return
  508. const processingHtml =
  509. `<div id="processing"><article role="article" tabindex="0" data-testid="tweet">` +
  510. `${processingTextHtml}<br/>` +
  511. `<span id="processing-info-text">${info}</span></article></div>`
  512. return processingHtml
  513. }
  514.  
  515. // nuke confirmation html
  516. function getNukeConfirmationHtml() {
  517. const nukeConfirmationHtml =
  518. `<div id="nuke-confirmation">
  519. <div id="nuke-confirmation-title">
  520. <span style="--i:1;">are</span>
  521. <span style="--i:2;">you</span>
  522. <span style="--i:3;">sure</span>
  523. <span style="--i:4;">you</span>
  524. <span style="--i:5;">want</span>
  525. <span style="--i:6;">to</span>
  526. <span style="--i:7;">nuke</span>
  527. <span style="--i:8;">this</span>
  528. <span style="--i:9;">thread?</span>
  529. </div>
  530. <br/>
  531. <div class="nuke-confirmation-button">
  532. <button name="yes" type="button" value="true">
  533. <span>${config.static.checkMark}</span>
  534. <span>yes</span>
  535. </button>
  536. </div>
  537. <div class="nuke-confirmation-button">
  538. <button name="no" type="button" value="false">
  539. <span>${config.static.redCross}</span>
  540. <span>no</span>
  541. </button>
  542. </div>
  543. </div>`
  544. return nukeConfirmationHtml
  545. }
  546.  
  547. // processing css
  548. function getProcessingCss() {
  549. const theme = getThemeColors()
  550. const css = {
  551. 'height': `100px`,
  552. 'text-align': 'center',
  553. 'justify-content': 'center',
  554. 'align-items': 'center',
  555. 'padding-top': '30px',
  556. 'border': `2px ${theme.color} solid`,
  557. 'border-radius': '2px',
  558. 'box-shadow': `inset 0 0 2px ${theme.bg},
  559. inset 0 0 7px ${theme.bg},
  560. inset 0 0 14px ${theme.color},
  561. inset 0 0 21px ${theme.color},
  562. inset 0 0 28px ${theme.color},
  563. inset 0 0 35px ${theme.color}`,
  564. 'animation': 'glow 0.7s infinite alternate'
  565. }
  566. return css
  567. }
  568.  
  569. // nuke button css
  570. function getNukeButtonCss() {
  571. const theme = getThemeColors()
  572. const css =
  573. `a.nuke-button {
  574. z-index: 1;
  575. position: absolute;
  576. width: 30px;
  577. height: 30px;
  578. top: 45px;
  579. text-decoration: none;
  580. text-align: center;
  581. user-select: none;
  582. -moz-user-select: none;
  583. -khtml-user-select: none;
  584. -webkit-user-select: none;
  585. -o-user-select: none;
  586. }
  587. a.nuke-button:hover {
  588. border-radius: 5px;
  589. background-color: ${theme.color};
  590. }
  591. #nuke-button {
  592. width: 100%;
  593. height: 100%;
  594. line-height: 30px;
  595. }
  596. #nuke-button-text {
  597. margin: 0 auto;
  598. }
  599. #processing-text {
  600. -moz-user-select: none;
  601. -khtml-user-select: none;
  602. -webkit-user-select: none;
  603. -o-user-select: none;
  604. }
  605. #processing-text span {
  606. display: inline-block;
  607. text-transform: uppercase;
  608. animation: flip 2s infinite;
  609. animation-delay: calc(.11s * var(--i));
  610. }
  611. #nuke-confirmation {
  612. height: 100px;
  613. width: 100%;
  614. text-align: center;
  615. justify-content: center;
  616. align-items: center;
  617. text-transform: uppercase;
  618. padding-top: 30px;
  619. border: 2px ${theme.color} solid;
  620. border-radius: 2px;
  621. box-shadow: inset 0 0 2px ${theme.bg},
  622. inset 0 0 7px ${theme.bg},
  623. inset 0 0 14px ${theme.color},
  624. inset 0 0 21px ${theme.color},
  625. inset 0 0 28px ${theme.color},
  626. inset 0 0 35px ${theme.color};
  627. animation: glow 0.9s infinite alternate;
  628. }
  629. #nuke-confirmation-title {
  630. -moz-user-select: none;
  631. -khtml-user-select: none;
  632. -webkit-user-select: none;
  633. -o-user-select: none;
  634. }
  635. #nuke-confirmation-title span {
  636. display: inline-block;
  637. animation: flip 2s infinite;
  638. animation-delay: calc(.5s * var(--i));
  639. }
  640. .nuke-confirmation-button {
  641. display: inline-block;
  642. padding-left: 30px;
  643. padding-right: 30px;
  644. }
  645. .nuke-confirmation-button button {
  646. height: 50px;
  647. width: 80px;
  648. cursor: pointer;
  649. text-transform: uppercase;
  650. }
  651. @keyframes glow {
  652. 100% {
  653. box-shadow:
  654. inset 0 0 3px ${theme.bg},
  655. inset 0 0 10px ${theme.bg},
  656. inset 0 0 20px ${theme.color},
  657. inset 0 0 40px ${theme.color},
  658. inset 0 0 70px ${theme.color},
  659. inset 0 0 89px ${theme.color};
  660. }
  661. }
  662. @keyframes flip {
  663. 0%,80% {
  664. transform: rotateY(360deg);
  665. }
  666. }`
  667. return css
  668. }
  669.  
  670. // insert css
  671. function insertCss() {
  672. let $style
  673. $style ??= appendStyle()
  674. $style.textContent = getNukeButtonCss()
  675. }
  676.  
  677. // nuke confirmation
  678. async function nukeConfirmation(post) {
  679. // init return
  680. let ret = false
  681. // store post html
  682. const postHtml = $(post).html()
  683. // set confirmation html
  684. $(post).html(getNukeConfirmationHtml())
  685. // add even listeners to buttons
  686. const promise = new Promise((resolve) => {
  687. // yes button
  688. $(post).find('button[name="yes"]').on('click', function (event) {
  689. resolve(true)
  690. })
  691. // no button
  692. $(post).find('button[name="no"]').on('click', function (event) {
  693. resolve(false)
  694. })
  695. })
  696. // await confirmation response
  697. await promise.then((result) => {
  698. // store return value
  699. ret = result
  700. // reset post html
  701. $(post).html(postHtml)
  702. })
  703. // return result
  704. return ret
  705. }
  706.  
  707. // filter block list
  708. function filterDedupWhiteList(item, index, arr) {
  709. // do not include white listed accounts
  710. if (gWhiteList.indexOf(item.username) > -1) {
  711. return false
  712. }
  713. // dedup based username and userid
  714. const i = arr.findIndex((item2) => ['username', 'userId'].every((key) => item2[key] === item[key]))
  715. return i === index
  716. }
  717.  
  718. // get block list
  719. async function getBlockList(userId, username, tweetId) {
  720. try {
  721. // get data
  722. const followingData = await fetchUserFollowing(userId)
  723. const following = extractUserResponseData(followingData?.data?.user?.result?.timeline?.timeline?.instructions)
  724. const followersData = await fetchUserFollowers(userId)
  725. const followers = extractUserResponseData(followersData?.data?.user?.result?.timeline?.timeline?.instructions)
  726. const verifiedFollowersData = await fetchVerifiedFollowers(userId)
  727. const verifiedFollowers = extractUserResponseData(verifiedFollowersData?.data?.user?.result?.timeline?.timeline?.instructions)
  728. const responsesData = await fetchTweetResponses(tweetId)
  729. const responses = extractTweetResponseData(responsesData?.data?.threaded_conversation_with_injections_v2?.instructions)
  730. const retweetersData = await fetchTweetRetweeters(tweetId)
  731. const retweeters = extractUserResponseData(retweetersData?.data?.retweeters_timeline?.timeline?.instructions)
  732. // add target user to front of array
  733. const target = [{ username: username, isBlocking: false, userId: userId }]
  734. // combine data
  735. const blockList = [].concat(target, following, followers, verifiedFollowers, responses, retweeters)
  736. // filter blocklist based on username and userid
  737. const filteredBlockList = blockList.filter(filterDedupWhiteList)
  738. // return block list
  739. return filteredBlockList
  740. } catch (e) {
  741. throw e
  742. }
  743. }
  744.  
  745. // open href in new tab
  746. function openHrefInNewTab(href) {
  747. // complete url
  748. const url = `https://x.com${href}`
  749. log(`opening url: ${url}`)
  750. // open new tab
  751. window.open(url, '_blank')
  752. //window.focus()
  753. }
  754.  
  755. // block a block list
  756. async function blockBlockList(blockList, href) {
  757. // init return
  758. let blockedTally = 0
  759. let openedHref = false
  760. // iterate block list
  761. for (const item of blockList) {
  762. // check if user is blocked already
  763. if (!item.isBlocked) {
  764. try {
  765. // block user
  766. const ret = await blockUser(item.userId)
  767. // increment blocked tally
  768. blockedTally += 1;
  769. } catch (error) {
  770. // log error
  771. log(`${error.name}, ${error.message}, ${error.cause}`, true)
  772. // something's going wrong, open post in new tab to finish nuke-ing later
  773. // probably got logged out (401), or api timeout (429)
  774. if (!openedHref && config.behavior.newTabOnError) {
  775. openHrefInNewTab(href)
  776. openedHref = true
  777. }
  778. }
  779. }
  780. }
  781. // return success
  782. return blockedTally
  783. }
  784.  
  785. // kill 'em all
  786. async function killEmAll(href) {
  787. // get username from href
  788. const targetUsername = href.split('/')[1]
  789. const tweetId = href.split('/')[3]
  790. try {
  791. // get user data
  792. const userData = await fetchUserId(targetUsername)
  793. // check for error
  794. if (userData.message) {
  795. // throw error
  796. throw userData.message
  797. }
  798. // extract user id
  799. const targetUserId = extractUserId(userData)
  800. const blockList = await getBlockList(targetUserId, targetUsername, tweetId)
  801. const result = await blockBlockList(blockList, href)
  802. log(`processing finished for ${href}: blocked ${result} accounts`)
  803. } catch (e) {
  804. // log error url
  805. if (e.url) {
  806. log(`api request error for url: ${e.url}`, true)
  807. }
  808. // log error
  809. log(e, true)
  810. return undefined
  811. }
  812. // return href on success
  813. return href
  814. }
  815.  
  816. // get post href
  817. function getPostHref(post) {
  818. // get target links attached to post
  819. const links = $(post).find(config.selectors.postHref);
  820. // iterate links
  821. for (let i = 0; i < links.length; i++) {
  822. // get href
  823. const href = $(links[i]).attr('href')
  824. // toss out incorrect links
  825. if (href.includes('analytics') || href.includes('photo') || href.includes('history') || href.includes('retweets')) {
  826. const arr = href.split('/')
  827. const ret = `/${arr[1]}/${arr[2]}/${arr[3]}`
  828. return ret
  829. } else if (i == links.length - 1) {
  830. // return last link if none found
  831. return href
  832. }
  833. }
  834. }
  835.  
  836. // remvoe post react object
  837. function removeReactObjects(href) {
  838. // todo: not sure how to approach hacking at the react obejcts
  839. // need to stop them from repopulating the timeline with removed posts
  840. // also causes page to crash needing to reload
  841. // get react state
  842. const reactState = getReactState()
  843. const statusId = href.split('/')[3]
  844. // get in-reply-to if it exists
  845. const inReply = reactState.entities.tweets.entities[statusId].in_reply_to_status_id_str
  846. // remove react objects
  847. if (reactState.entities.tweets.entities[statusId]) {
  848. //delete(reactState.entities.tweets.entities[statusId])
  849. reactState.entities.tweets.entities[statusId].conversation_id_str = "1234"
  850. }
  851. if (reactState.entities.tweets.fetchStatus[statusId]) {
  852. delete (reactState.entities.tweets.fetchStatus[statusId])
  853. }
  854. for (const key in reactState.urt) {
  855. if (reactState.urt[key].entries) {
  856. for (let i = 0; i < reactState.urt[key].entries.length; i++) {
  857. if (reactState.urt[key].entries[i]) {
  858. if (reactState.urt[key].entries[i].entryId.includes(statusId)) {
  859. //delete(reactState.urt[key].entries[i])
  860. reactState.urt[key].entries[i].entryId = 'noid-0000'
  861. reactState.urt[key].entries[i].sortIndex = "1234"
  862. reactState.urt[key].entries[i].type = 'nonexistingType'
  863. }
  864. }
  865. }
  866. }
  867. }
  868. for (const key in reactState.audio.conversationLookup) {
  869. if (reactState.audio.conversationLookup[key]) {
  870. for (let i = 0; i < reactState.audio.conversationLookup[key].length; i++) {
  871. delete (reactState.audio.conversationLookup[key][i])
  872. }
  873. }
  874. }
  875. }
  876.  
  877. // hide post from timeline
  878. function hidePost(post) {
  879. // hide html from timeline
  880. $(post).html('')
  881. $(post).hide()
  882. }
  883.  
  884. // nuke
  885. async function nuke(event) {
  886. // get upper context
  887. const post = $(this).parents().eq(6)
  888. // get post href
  889. const href = getPostHref(post)
  890. // confirm nuke-ing
  891. if (await nukeConfirmation(post)) {
  892. // log
  893. log('NUKE-ing!: ...' + href)
  894. // append processing html
  895. $(post).html(getProcessingHtml(href))
  896. // add css to elements
  897. $(post).find('#processing').css(getProcessingCss())
  898. // start nuking!
  899. await killEmAll(href)
  900. // remove react object
  901. removeReactObjects(href)
  902. // todo: error reporting, syncing with view
  903. // hide post from timeline
  904. hidePost(post)
  905. } else {
  906. rebindNukeCommand(post)
  907. }
  908. }
  909.  
  910. // check for quote tweet
  911. function isQuoteTweet(post) {
  912. // quoted tweets have two classes
  913. if ($(post).attr('class').split(/\s+/).length > 1) {
  914. const quote = $(post).parents().eq(6).find('span:contains("Quote")')
  915. return quote.length > 0
  916. }
  917. return false
  918. }
  919.  
  920. // rebind the nuke function
  921. function rebindNukeCommand(post) {
  922. $(post).find(config.selectors.nukeButton).on('click', nuke)
  923. }
  924.  
  925. // should add nuke button
  926. function shouldAddNukeButton(post) {
  927. // check if post is null
  928. if (post != null) {
  929. // do not add nuke button to self's own posts
  930. let profile = ''
  931. // check for quote tweet
  932. if (isQuoteTweet(post)) {
  933. profile = $(post).parents().eq(1).find('span:contains("@")').first().text().split('@')[1]
  934. } else {
  935. profile = $(post).parents().eq(1).find('a').first().attr('href').split('/')[1]
  936. }
  937. // white list check
  938. const whiteListed = gWhiteList.find((username) => username.toString() === profile.toString())
  939. // check against global profile variable and whitelist
  940. if (gProfile != profile && !whiteListed) {
  941. return true
  942. }
  943. }
  944. // default return
  945. return false
  946. }
  947.  
  948. // append nuke button html to post
  949. function appendNukeButtonHtml(post) {
  950. // check if buke button should be added
  951. if (shouldAddNukeButton(post)) {
  952. // apend html
  953. $(post).parents().first().parents().first().append(getNukeButtonHtml())
  954. // arm nuke
  955. $(post).parents().eq(1).find(config.selectors.nukeButton).on('click', nuke)
  956. }
  957. }
  958.  
  959. // insert nuke button html
  960. function addNukeButton() {
  961. // todo: breaks opening post in new tab, link at the end, probably react is looking for last link
  962. $(config.selectors.avatar).each((index, post) => { appendNukeButtonHtml(post) })
  963. }
  964.  
  965. // on timeline change
  966. function onTimelineChange(mutations) {
  967. for (const mutation of mutations) {
  968. // append nuke button
  969. $(mutation.addedNodes).find(config.selectors.avatar).each((index, post) => { appendNukeButtonHtml(post) })
  970. }
  971. }
  972.  
  973. // observe timeline
  974. async function observeTimeline(selector) {
  975. // create observer with callback
  976. const observer = new MutationObserver((mutations) => { onTimelineChange(mutations) })
  977. // disconnect old observer
  978. // todo: not sure if this is necessary, maybe modals disconnect it anyway?
  979. gObservers.timeline.disconnect()
  980. gObservers.timeline = observer
  981. // wait for timeline to load in
  982. await getElement(config.selectors.status)
  983. observer.observe($(selector).children().first()[0], { childList: true })
  984. // check if page has changed
  985. //if (!gPageChanged) {
  986. // add nuke button to initial posts
  987. addNukeButton()
  988. //}
  989. }
  990.  
  991. // when home timeline navigation event is propagated
  992. function onHomeNavigationEvent() {
  993. // reset page changed if it was changed
  994. gPageChanged = false
  995. // create timeline observer
  996. observeTimeline(config.selectors.hometl)
  997. log('home nav changed')
  998. }
  999.  
  1000. // add navigation listener
  1001. function addHomeNavigationListener() {
  1002. // add event listener for timeline tabs
  1003. $(config.selectors.nav).eq(1).on('mousedown', onHomeNavigationEvent)
  1004. }
  1005.  
  1006. function isUserPage() {
  1007. // try to get username
  1008. const username = window.location.href.split('/')[3]
  1009. const url = `/${username}/header_photo`
  1010. // test for user page
  1011. if ($('div').find(`a[href="${url}"]`).length == 1) {
  1012. log(':is user page:')
  1013. return true
  1014. }
  1015. // return false
  1016. return false
  1017. }
  1018.  
  1019. // build userpage timeline selector
  1020. function getUserPageTimelineSelector() {
  1021. // get user display name
  1022. const displayName = $('div[data-testid="UserName"]').find('span').first().text()
  1023. const selector = `div[aria-label="Timeline: ${displayName}’s posts"]`
  1024. return selector
  1025. }
  1026.  
  1027. // edit status view css
  1028. async function editStatusViewCss() {
  1029. // create css
  1030. const css = {
  1031. 'z-index': -1,
  1032. 'top': '17px'
  1033. }
  1034. // wait for element to load in
  1035. await getElement(config.selectors.status)
  1036. // add new style
  1037. $(config.selectors.status).find('div').eq(1).children().eq(2).css(css)
  1038. }
  1039.  
  1040. // get react state
  1041. function getReactState() {
  1042. const wrapped = $('div')[0].firstElementChild['wrappedJSObject'] || $('div')[0].firstElementChild
  1043. const reactPropsKey = Object.keys(wrapped).find(key => key.startsWith('__reactProps'))
  1044. const state = wrapped[reactPropsKey].children?.props?.children?.props?.store?.getState()
  1045. return state
  1046. }
  1047.  
  1048. // poll for react state
  1049. async function pollReactState() {
  1050. // new promise
  1051. const promise = new Promise((resolve) => {
  1052. // interval id
  1053. let intervalId = 0
  1054. // function to return react state
  1055. function returnReactState(reactState) {
  1056. log('found react state')
  1057. log(reactState)
  1058. // resolve react state
  1059. resolve(reactState)
  1060. }
  1061. // poll for react state
  1062. function poll() {
  1063. // use set interval to poll
  1064. intervalId = setInterval(function () {
  1065. try {
  1066. const reactState = getReactState()
  1067. // clear interval
  1068. clearInterval(intervalId)
  1069. // resolve
  1070. returnReactState(reactState)
  1071. } catch (error) {
  1072. log('waiting for react state...')
  1073. }
  1074. }, 1000)
  1075. }
  1076. // start polling
  1077. poll()
  1078. })
  1079. return promise
  1080. }
  1081.  
  1082. // wait for user to login if necessary
  1083. async function isLoggedIn(reactState) {
  1084. // new promise
  1085. const promise = new Promise((resolve) => {
  1086. // interval id
  1087. let intervalId = 0
  1088. // resolve promise
  1089. function resolved() {
  1090. log('user logged in')
  1091. // resolve
  1092. resolve(true)
  1093. }
  1094. // poll checking if user is logged in
  1095. function pollLoggedIn() {
  1096. // poll with set interval
  1097. intervalId = setInterval(function () {
  1098. //check href and window vars
  1099. if (!(window?.__META_DATA__?.isLoggedIn == false) &&
  1100. !window.location.href.includes('/i/flow/login')) {
  1101. // clear interval
  1102. clearInterval(intervalId)
  1103. // resolve
  1104. resolved()
  1105. } else {
  1106. // keep polling
  1107. log('waiting for user login')
  1108. }
  1109. }, 1000)
  1110. }
  1111. // start polling
  1112. pollLoggedIn()
  1113. })
  1114. // return promise
  1115. return promise
  1116. }
  1117.  
  1118. // process current page
  1119. async function processCurrentPage(updatePage = false) {
  1120. // check href location
  1121. if (window.location.href.endsWith('home')) {
  1122. // check for home
  1123. // todo: work out updating this value after more back and forth browsing
  1124. // todo: work out race conditions where polling fails
  1125. // update page changed
  1126. gPageChanged = updatePage ? true : false
  1127. addHomeNavigationListener()
  1128. // wait for timeline to load in
  1129. await getElement(config.selectors.hometl)
  1130. // todo: dethrottle polling when no posts are loading
  1131. observeTimeline(config.selectors.hometl)
  1132. } else if (isUserPage()) {
  1133. // check for userpage
  1134. // update page changed
  1135. gPageChanged = updatePage ? true : false
  1136. // get userpage timeline selector
  1137. const selector = getUserPageTimelineSelector()
  1138. // wait for timeline to load in
  1139. await getElement(selector)
  1140. observeTimeline(selector)
  1141. } else if (window.location.href.includes('status')) {
  1142. // check for status (post) view page
  1143. // todo: editstatusview does not update correctly sometimes
  1144. // todo: if you need to use 'view' for the post nuke-button does not populate to the main status
  1145. //update page changed
  1146. gPageChanged = updatePage ? true : false
  1147. // wait for timeline to load in
  1148. await getElement(config.selectors.statustl)
  1149. // change status view css
  1150. editStatusViewCss()
  1151. // obvserve timeline
  1152. observeTimeline(config.selectors.statustl)
  1153. } else if (window.location.href.includes('search?q=')) {
  1154. // check for search page
  1155. //update page changed
  1156. gPageChanged = updatePage ? true : false
  1157. // wait for timeline to load in
  1158. await getElement(config.selectors.searchtl)
  1159. // observe timeline
  1160. observeTimeline(config.selectors.searchtl)
  1161. }
  1162. }
  1163.  
  1164. // after navigation
  1165. async function onWindowHrefChange() {
  1166. log(`window href changed: ${window.location.href}`)
  1167. // wait for react state
  1168. const reactState = await pollReactState()
  1169. // wait for login if necessary
  1170. await isLoggedIn(reactState)
  1171. // process current page
  1172. processCurrentPage(true)
  1173. }
  1174.  
  1175. async function observeWindowHref() {
  1176. setInterval(() => {
  1177. // check if location changed
  1178. if (gCurrentPage != window.location.href) {
  1179. // update current page
  1180. initCurrentPage()
  1181. onWindowHrefChange()
  1182. }
  1183. }, 1000)
  1184. }
  1185.  
  1186. // observe for location changes
  1187. function observeWindowHrefasdf() {
  1188. // observer with callback
  1189. const observer = new MutationObserver(() => {
  1190. // check if location changed
  1191. if (gCurrentPage != window.location.href) {
  1192. // update current page
  1193. initCurrentPage()
  1194. onWindowHrefChange()
  1195. }
  1196. })
  1197. gObservers.href.disconnect()
  1198. gObservers.href = observer
  1199. observer.observe(document, { childList: true, subtree: true })
  1200. }
  1201.  
  1202. // setup mutation observers
  1203. function observeApp() {
  1204. // add timeline observer
  1205. processCurrentPage()
  1206. // add window location poling
  1207. observeWindowHref()
  1208. }
  1209.  
  1210. // main
  1211. async function main() {
  1212. // wait for react state
  1213. const reactState = await pollReactState()
  1214. // wait for login if necessary
  1215. await isLoggedIn(reactState)
  1216. // wait for timeline to load in
  1217. await getElement(config.selectors.tl)
  1218. // init globals
  1219. initGlobals()
  1220. // insert css
  1221. insertCss()
  1222. // observe
  1223. observeApp()
  1224. }
  1225.  
  1226. // run script
  1227. window.onload = main()
  1228.  
  1229. })();