nuke button

kill 'em all

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