nuke button

kill 'em all

目前为 2025-03-13 提交的版本。查看 最新版本

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