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