TweetXer

Delete all your Tweets for free.

  1. // ==UserScript==
  2. // @name TweetXer
  3. // @namespace https://github.com/lucahammer/tweetXer/
  4. // @version 0.10.0
  5. // @description Delete all your Tweets for free.
  6. // @author Luca,dbort,pReya,Micolithe,STrRedWolf
  7. // @license NoHarm-draft
  8. // @match https://x.com/*
  9. // @match https://mobile.x.com/*
  10. // @match https://twitter.com/*
  11. // @match https://mobile.twitter.com/*
  12. // @icon https://www.google.com/s2/favicons?domain=twitter.com
  13. // @grant none
  14. // @supportURL https://github.com/lucahammer/tweetXer/issues
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. let TweetsXer = {
  19. version: '0.10.0',
  20. TweetCount: 0,
  21. dId: "exportUpload",
  22. tIds: [],
  23. tId: "",
  24. ratelimitreset: 0,
  25. more: '[data-testid="tweet"] [data-testid="caret"]',
  26. skip: 0,
  27. total: 0,
  28. dCount: 0,
  29. deleteURL: '/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet',
  30. unfavURL: '/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet',
  31. deleteMessageURL: '/i/api/graphql/BJ6DtxA2llfjnRoRjaiIiw/DMMessageDeleteMutation',
  32. deleteConvoURL: '/i/api/1.1/dm/conversation/USER_ID-CONVERSATION_ID/delete.json',
  33. bookmarksURL: '/i/api/graphql/YnSSREbpZZHAaNdnEk4ycA/Bookmarks?',
  34. deleteDMsOneByOne: false,
  35. username: '',
  36. action: '',
  37. bookmarks: [],
  38. bookmarksNext: '',
  39. baseUrl: 'https://x.com',
  40. authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
  41. ct0: false,
  42. transaction_id: '',
  43.  
  44. async init() {
  45. this.baseUrl = `https://${window.location.hostname}`
  46. this.createUploadForm()
  47. await this.getTweetCount()
  48. this.ct0 = this.getCookie('ct0')
  49. this.username = document.location.href.split('/')[3].replace('#', '')
  50. },
  51.  
  52. sleep(ms) {
  53. return new Promise((resolve) => setTimeout(resolve, ms))
  54. },
  55.  
  56. getCookie(name) {
  57. const match = `; ${document.cookie}`.match(`;\\s*${name}=([^;]+)`)
  58. return match ? match[1] : null
  59. },
  60.  
  61.  
  62. updateTitle(text) {
  63. document.getElementById('tweetsXer_title').textContent = text
  64. },
  65.  
  66. updateInfo(text) {
  67. document.getElementById("info").textContent = text
  68. },
  69.  
  70. createProgressBar() {
  71. const progressbar = document.createElement("progress")
  72. progressbar.id = "progressbar"
  73. progressbar.value = this.dCount
  74. progressbar.max = this.total
  75. progressbar.style = 'width:100%'
  76.  
  77. document.getElementById(this.dId).appendChild(progressbar)
  78. },
  79.  
  80. updateProgressBar() {
  81. document.getElementById('progressbar').value = this.dCount
  82. this.updateInfo(`${this.dCount} deleted. ${this.tId}`)
  83. },
  84.  
  85. processFile() {
  86. const tn = document.getElementById(`${TweetsXer.dId}_file`)
  87. if (tn.files && tn.files[0]) {
  88. let fr = new FileReader()
  89. fr.onloadend = function (evt) {
  90. // window.YTD.tweet_headers.part0
  91. // window.YTD.tweets.part0
  92. // window.YTD.like.part0
  93. // window.YTD.direct_message_headers.part0
  94. let cutpoint = evt.target.result.indexOf('= ')
  95. let filestart = evt.target.result.slice(0, cutpoint)
  96. let json = JSON.parse(evt.target.result.slice(cutpoint + 1))
  97.  
  98. if (filestart.includes('.tweet_headers.')) {
  99. console.log('File contains Tweets.')
  100. TweetsXer.action = 'untweet'
  101. TweetsXer.tIds = json.map((x) => x.tweet.tweet_id)
  102. } else if (filestart.includes('.tweets.') || filestart.includes('.tweet.')) {
  103. console.log('File contains Tweets.')
  104. TweetsXer.action = 'untweet'
  105. TweetsXer.tIds = json.map((x) => x.tweet.id_str)
  106. } else if (filestart.includes('.like.')) {
  107. console.log('File contains Favs.')
  108. TweetsXer.action = 'unfav'
  109. TweetsXer.tIds = json.map((x) => x.like.tweetId)
  110. }
  111. else if (
  112. filestart.includes('.direct_message_headers.')
  113. || filestart.includes('.direct_message_group_headers.')
  114. || filestart.includes('.direct_messages.')
  115. || filestart.includes('.direct_message_groups.')) {
  116. console.log('File contains Direct Messages.')
  117. TweetsXer.action = 'undm'
  118. if (this.deleteDMsOneByOne) {
  119. TweetsXer.tIds = json.map((c) => c.dmConversation.messages.map((m) => m.messageCreate ? m.messageCreate.id : 0))
  120. TweetsXer.tIds = TweetsXer.tIds.flat()
  121. TweetsXer.tIds = TweetsXer.tIds.filter((i) => i != 0)
  122. }
  123. else {
  124. TweetsXer.tIds = json.map((c) => c.dmConversation.conversationId)
  125. }
  126.  
  127. } else {
  128. TweetsXer.updateInfo('File content not recognized. Please use a file from the Twitter data export.')
  129. console.log('File content not recognized. Please use a file from the Twitter data export.')
  130. }
  131.  
  132. if (TweetsXer.action.length > 0) {
  133. TweetsXer.total = TweetsXer.tIds.length
  134. document.getElementById(`${TweetsXer.dId}_file`).remove()
  135. TweetsXer.createProgressBar()
  136. }
  137.  
  138. if (TweetsXer.action == 'untweet') {
  139. if (document.getElementById('skipCount').value.length < 1) {
  140. // If there is no amount set to skip, automatically try to skip the amount
  141. // that has been deleted already. Difference of Tweeets in file to count on profile
  142. // 5% tolerance to prevent skipping too much
  143. TweetsXer.skip = TweetsXer.total - TweetsXer.TweetCount - parseInt(TweetsXer.total / 20)
  144. TweetsXer.skip = Math.max(0, TweetsXer.skip)
  145. }
  146. else {
  147. TweetsXer.skip = document.getElementById('skipCount').value
  148. }
  149. console.log(`Skipping oldest ${TweetsXer.skip} Tweets. Use advanced options to manually set how many to skip. Enter 0 to prevent the automatic calculation.`)
  150. TweetsXer.tIds.reverse()
  151. TweetsXer.tIds = TweetsXer.tIds.slice(TweetsXer.skip)
  152. TweetsXer.dCount = TweetsXer.skip
  153. TweetsXer.tIds.reverse()
  154. TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} Tweets`)
  155.  
  156. TweetsXer.deleteTweets()
  157. } else if (TweetsXer.action == 'unfav') {
  158. TweetsXer.skip = document.getElementById('skipCount').value.length > 0 ? document.getElementById('skipCount').value : 0
  159. console.log(`Skipping oldest ${TweetsXer.skip} Tweets`)
  160. TweetsXer.tIds = TweetsXer.tIds.slice(TweetsXer.skip)
  161. TweetsXer.dCount = TweetsXer.skip
  162. TweetsXer.tIds.reverse()
  163. TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} Favs`)
  164. TweetsXer.deleteFavs()
  165. } else if (TweetsXer.action == 'undm') {
  166. TweetsXer.skip = document.getElementById('skipCount').value.length > 0 ? document.getElementById('skipCount').value : 0
  167. console.log(`Skipping ${TweetsXer.skip} messages/convos`)
  168. TweetsXer.tIds = TweetsXer.tIds.slice(TweetsXer.skip)
  169. TweetsXer.dCount = TweetsXer.skip
  170. TweetsXer.tIds.reverse()
  171. if (this.deleteDMsOneByOne) {
  172. TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} DMs`)
  173. TweetsXer.deleteDMs()
  174. }
  175. else {
  176. TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} DM Conversations`)
  177. TweetsXer.deleteConvos()
  178. }
  179.  
  180. }
  181. else {
  182. TweetsXer.updateTitle(`TweetXer: Please try a different file`)
  183. }
  184.  
  185. }
  186. fr.readAsText(tn.files[0])
  187. }
  188. },
  189.  
  190. createUploadForm() {
  191. const h2Class = document.querySelectorAll("h2")[1]?.getAttribute("class") || ""
  192. const div = document.createElement("div")
  193. div.id = this.dId
  194. if (document.getElementById(this.dId)) { document.getElementById(this.dId).remove() }
  195. div.innerHTML = `
  196. <style>#${this.dId}{ z-index:99999; position: sticky; top:0px; left:0px; width:auto; margin:0 auto; padding: 20px 10%; background:#87CEFA; opacity:0.9; } #${this.dId} > *{padding:5px;}</style>
  197. <div style="color:black">
  198. <h2 class="${h2Class}" id="tweetsXer_title">TweetXer</h2>
  199. <p id="info">Please wait for your profile to load. If this message doesn't go away after some seconds, something isn't working.</p>
  200. <p id="start">
  201. <input type="file" value="" id="${this.dId}_file" />
  202. <a style="color:blue" href="#" id="toggleAdvanced">Advanced Options</a>
  203. <div id="advanced" style="display:none">
  204. <label for="skipCount">Enter how many Tweets to skip before selecting a file.</label>
  205. <input id="skipCount" type="number" value="" />
  206. <p>Supported files:
  207. <ul>
  208. <li>tweet-headers.js to delete Tweets (10.000 - 20.000 per hour)</li>
  209. <li>direct-message-header.js and direct-message-group-headers.js to delete DMs (around 800 per 15 minutes)</li>
  210. <li>like.js to delete Favs (500 per 15 minutes; only works for the most recent few thousands)</li>
  211. </ul>
  212. <p><strong>Export bookmarks</strong><br>
  213. Bookmarks are not included in the official data export. You can export them here.
  214. <input id="exportBookmarks" type="button" value="Export Bookmarks" />
  215. </p>
  216. <p><strong>No tweet-headers.js?</strong><br>
  217. If you are unable to get your data export, you can use the following option.<br>
  218. This option is much slower and less reliable. It can remove at most 4000 Tweets per hour.<br>
  219. <input id="slowDelete" type="button" value="Slow delete without file" />
  220. </p>
  221. <p><strong>Unfollow everyone</strong><br>
  222. It's time to let go. This will unfollow everyone you follow.<br>
  223. <input id="unfollowEveryone" type="button" value="Unfollow everyone" />
  224. </p>
  225. <p><a id="removeTweetXer" style="color:blue" href="#">Remove TweetXer</a></p>
  226. <p><small>${TweetsXer.version}</small></p>
  227. </div>
  228. </div>
  229. `
  230. document.body.insertBefore(div, document.body.firstChild)
  231. document.getElementById("toggleAdvanced").addEventListener("click", (() => {
  232. const adv = document.getElementById('advanced')
  233. if (adv.style.display == 'none') {
  234. adv.style.display = 'block'
  235. } else {
  236. adv.style.display = 'none'
  237. }
  238. }))
  239. document.getElementById(`${this.dId}_file`).addEventListener("change", this.processFile, false)
  240. document.getElementById("exportBookmarks").addEventListener("click", this.exportBookmarks, false)
  241. document.getElementById("slowDelete").addEventListener("click", this.slowDelete, false)
  242. document.getElementById("unfollowEveryone").addEventListener("click", this.unfollow, false)
  243. document.getElementById("removeTweetXer").addEventListener("click", this.removeTweetXer, false)
  244.  
  245. },
  246.  
  247. async exportBookmarks() {
  248. TweetsXer.updateTitle('TweetXer: Exporting bookmarks')
  249. let variables = ''
  250. while (TweetsXer.bookmarksNext.length > 0 || TweetsXer.bookmarks.length == 0) {
  251. if (TweetsXer.bookmarksNext.length > 0) {
  252. variables = `{"count":20,"cursor":"${TweetsXer.bookmarksNext}","includePromotedContent":false}`
  253. } else variables = '{"count":20,"includePromotedContent":false}'
  254.  
  255. let fetch_url = TweetsXer.baseUrl + TweetsXer.bookmarksURL + new URLSearchParams({
  256. variables: variables,
  257. features: '{"rweb_video_screen_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"premium_content_api_read_enabled":false,"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"responsive_web_grok_analyze_button_fetch_trends_enabled":false,"responsive_web_grok_analyze_post_followups_enabled":true,"responsive_web_jetfuel_frame":false,"responsive_web_grok_share_attachment_enabled":true,"articles_preview_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"responsive_web_grok_show_grok_translated_post":false,"responsive_web_grok_analysis_button_from_backend":true,"creator_subscriptions_quote_tweet_preview_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_grok_image_annotation_enabled":true,"responsive_web_enhance_cards_enabled":false}'
  258. })
  259. let transaction_id = await generateTID(fetch_url)
  260.  
  261. let response = await fetch(fetch_url, {
  262. "headers": {
  263. "authorization": TweetsXer.authorization,
  264. "content-type": "application/json",
  265. "x-twitter-auth-type": "OAuth2Session",
  266. "x-csrf-token": TweetsXer.ct0,
  267. "x-twitter-client-language": "en",
  268. "x-twitter-active-user": "yes",
  269. "x-client-transaction-id": transaction_id,
  270. "x-xp-forwarded-for": ''
  271. },
  272. "referrer": `${TweetsXer.baseUrl}/i/bookmarks`,
  273. "referrerPolicy": "strict-origin-when-cross-origin",
  274. "method": "GET",
  275. "mode": "cors",
  276. "credentials": "include"
  277. })
  278.  
  279. if (response.status == 200) {
  280. let data = await response.json()
  281. data.data.bookmark_timeline_v2.timeline.instructions[0].entries.forEach((item) => {
  282.  
  283. if (item.entryId.includes('tweet')) {
  284. TweetsXer.dCount++
  285. TweetsXer.bookmarks.push(item.content.itemContent.tweet_results.result)
  286. } else if (item.entryId.includes('cursor-bottom')) {
  287. if (TweetsXer.bookmarksNext != item.content.value) {
  288. TweetsXer.bookmarksNext = item.content.value
  289. } else {
  290. TweetsXer.bookmarksNext = ''
  291. }
  292. }
  293. })
  294. //document.getElementById('progressbar').setAttribute('value', TweetsXer.dCount)
  295. TweetsXer.updateInfo(`${TweetsXer.dCount} Bookmarks collected`)
  296. } else {
  297. console.log(response)
  298. break
  299. }
  300.  
  301. if (!response.headers.get('x-rate-limit-remaining') && response.headers.get('x-rate-limit-remaining') < 1) {
  302. console.log('rate limit hit')
  303. TweetsXer.ratelimitreset = response.headers.get('x-rate-limit-reset')
  304. let sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000)
  305. while (sleeptime > 0) {
  306. sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000)
  307. TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`)
  308. await TweetsXer.sleep(1000)
  309. }
  310. }
  311. }
  312. let download = new Blob([JSON.stringify(TweetsXer.bookmarks)], {
  313. type: 'text/plain'
  314. })
  315. let bookmarksDownload = document.createElement("a")
  316. bookmarksDownload.id = 'bookmarksDownload'
  317. bookmarksDownload.innerText = 'Download bookmarks'
  318. bookmarksDownload.href = window.URL.createObjectURL(download)
  319. bookmarksDownload.download = 'twitter-bookmarks.json'
  320. document.getElementById('advanced').appendChild(bookmarksDownload)
  321. TweetsXer.updateTitle('TweetXer')
  322. },
  323.  
  324. async sendRequest(
  325. url,
  326. body = `{\"variables\":{\"tweet_id\":\"${TweetsXer.tId}\",\"dark_request\":false},\"queryId\":\"${url.split('/')[6]}\"}`
  327. ) {
  328. return new Promise(async (resolve) => {
  329. try {
  330. let response = await fetch(url, {
  331. "headers": {
  332. "authorization": TweetsXer.authorization,
  333. "content-type": "application/json",
  334. "x-client-transaction-id": TweetsXer.transaction_id,
  335. "x-csrf-token": TweetsXer.ct0,
  336. "x-twitter-active-user": "yes",
  337. "x-twitter-auth-type": "OAuth2Session",
  338. "x-client-transaction-id": await generateTID(url),
  339. "x-xp-forwarded-for": ''
  340. },
  341. "referrer": `${TweetsXer.baseUrl}/${TweetsXer.username}/with_replies`,
  342. "referrerPolicy": "strict-origin-when-cross-origin",
  343. "body": body,
  344. "method": "POST",
  345. "mode": "cors",
  346. "credentials": "include",
  347. "signal": AbortSignal.timeout(5000)
  348. })
  349.  
  350.  
  351. if (response.status == 200) {
  352. TweetsXer.dCount++
  353. TweetsXer.updateProgressBar()
  354.  
  355. if (response.headers.get('x-rate-limit-remaining') != null && response.headers.get('x-rate-limit-remaining') < 1) {
  356. console.log('rate limit hit')
  357. console.log(response.headers.get('x-rate-limit-remaining'))
  358. TweetsXer.ratelimitreset = response.headers.get('x-rate-limit-reset')
  359. let sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000)
  360. while (sleeptime > 0) {
  361. sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000)
  362. TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`)
  363. await TweetsXer.sleep(1000)
  364. }
  365. resolve('deleted and waiting')
  366. }
  367. else {
  368. resolve('deleted')
  369. }
  370.  
  371.  
  372. }
  373. else if (response.status == 429) {
  374. TweetsXer.tIds.push(TweetsXer.tId)
  375. console.log('Received status code 429. Waiting for 1 second before trying again.')
  376. await TweetsXer.sleep(1000)
  377. }
  378. else {
  379. console.log(response)
  380. }
  381.  
  382. } catch (error) {
  383. if (error.Name === 'AbortError') {
  384. TweetsXer.tIds.push(TweetsXer.tId)
  385. console.log('Request timeout.')
  386. let sleeptime = 15
  387. while (sleeptime > 0) {
  388. sleeptime--
  389. TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`)
  390. await TweetsXer.sleep(1000)
  391. }
  392. resolve('error')
  393. }
  394. }
  395. })
  396. },
  397.  
  398. async deleteTweets() {
  399. while (this.tIds.length > 0) {
  400. this.tId = this.tIds.pop()
  401. await this.sendRequest(this.baseUrl + this.deleteURL)
  402. }
  403. this.tId = ''
  404. this.updateProgressBar()
  405. },
  406.  
  407. async deleteFavs() {
  408. this.updateTitle('TweetXer: Deleting Favs')
  409. // 500 unfavs per 15 Minutes
  410. // x-rate-limit-remaining
  411. // x-rate-limit-reset
  412.  
  413. while (this.tIds.length > 0) {
  414. this.tId = this.tIds.pop()
  415. await this.sendRequest(this.baseUrl + this.unfavURL)
  416. }
  417. this.tId = ''
  418. this.updateTitle('TweetXer')
  419. this.updateProgressBar()
  420. },
  421.  
  422. async deleteDMs() {
  423. while (this.tIds.length > 0) {
  424. this.tId = this.tIds.pop()
  425. await this.sendRequest(
  426. this.baseUrl + this.deleteMessageURL,
  427. body = `{\"variables\":{\"messageId\":\"${this.tId}\"},\"requestId\":\""}`
  428. )
  429. }
  430. this.tId = ''
  431. this.updateProgressBar()
  432. },
  433.  
  434. async deleteConvos() {
  435. while (this.tIds.length > 0) {
  436. this.tId = this.tIds.pop()
  437. url = this.baseUrl + this.deleteConvoURL.replace('USER_ID-CONVERSATION_ID', this.tId)
  438. let response = await fetch(url, {
  439. "headers": {
  440. "authorization": TweetsXer.authorization,
  441. "content-type": "application/x-www-form-urlencoded",
  442. "x-client-transaction-id": TweetsXer.transaction_id,
  443. "x-csrf-token": TweetsXer.ct0,
  444. "x-twitter-active-user": "yes",
  445. "x-twitter-auth-type": "OAuth2Session",
  446. "x-client-transaction-id": await generateTID(url),
  447. "x-xp-forwarded-for": ''
  448. },
  449. "referrer": `${TweetsXer.baseUrl}/messages`,
  450. "body": 'dm_secret_conversations_enabled=false&krs_registration_enabled=true&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_ext_limited_action_results=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&include_ext_views=true&dm_users=false&include_groups=true&include_inbox_timelines=true&include_ext_media_color=true&supports_reactions=true&supports_edit=true&include_conversation_info=true',
  451. "method": "POST",
  452. "mode": "cors",
  453. "credentials": "include",
  454. "signal": AbortSignal.timeout(5000)
  455. })
  456.  
  457.  
  458. if (response.status == 204) {
  459. TweetsXer.dCount++
  460. TweetsXer.updateProgressBar()
  461.  
  462. if (response.headers.get('x-rate-limit-remaining') != null && response.headers.get('x-rate-limit-remaining') < 1) {
  463. console.log('rate limit hit')
  464. console.log(response.headers.get('x-rate-limit-remaining'))
  465. TweetsXer.ratelimitreset = response.headers.get('x-rate-limit-reset')
  466. let sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000)
  467. while (sleeptime > 0) {
  468. sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000)
  469. TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`)
  470. await TweetsXer.sleep(1000)
  471. }
  472. }
  473. await TweetsXer.sleep(Math.floor(Math.random() * 200)) // send requests slightly slower and with random intervals
  474. }
  475. else if (response.status == 429 || response.status == 420) {
  476. TweetsXer.tIds.push(TweetsXer.tId)
  477. console.log(`Received status code ${response.status}. Waiting before trying again.`)
  478. let sleeptime = 60 * 5 // is that enough?
  479. while (sleeptime > 0) {
  480. sleeptime--
  481. TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`)
  482. await TweetsXer.sleep(1000)
  483. }
  484.  
  485. }
  486. else {
  487. console.log(response)
  488. }
  489. }
  490. this.tId = ''
  491. this.updateProgressBar()
  492. },
  493.  
  494. async getTweetCount() {
  495. await waitForElemToExist('header')
  496. await TweetsXer.sleep(1000)
  497. if (!document.querySelector('[data-testid="UserName"]')) {
  498. if (document.querySelector('[aria-label="Back"]')) {
  499. await TweetsXer.sleep(200)
  500. document.querySelector('[aria-label="Back"]').click()
  501. await TweetsXer.sleep(1000)
  502. }
  503. else if (document.querySelector('[data-testid="app-bar-back"]')) {
  504. document.querySelector('[data-testid="app-bar-back"]').click()
  505. await TweetsXer.sleep(1000)
  506. }
  507.  
  508. if (document.querySelector('[data-testid="AppTabBar_Profile_Link"]')) {
  509. await TweetsXer.sleep(200)
  510. document.querySelector('[data-testid="AppTabBar_Profile_Link"]').click()
  511. }
  512. else if (document.querySelector('[data-testid="DashButton_ProfileIcon_Link"]')) {
  513. await TweetsXer.sleep(100)
  514. document.querySelector('[data-testid="DashButton_ProfileIcon_Link"]').click()
  515. await TweetsXer.sleep(1000)
  516. document.querySelector('[data-testid="icon"').nextElementSibling.click()
  517. }
  518.  
  519. await waitForElemToExist('[data-testid="UserName"]')
  520. }
  521. await TweetsXer.sleep(1000)
  522.  
  523. function extractTweetCount(selector) {
  524. const element = document.querySelector(selector)
  525. if (!element) return null
  526.  
  527. const match = element.textContent.match(/((\d|,|\.|K)+) (\w+)$/)
  528. if (!match) return null
  529.  
  530. return match[1]
  531. .replace(/\.(\d+)K/, '$1'.padEnd(4, '0'))
  532. .replace('K', '000')
  533. .replace(',', '')
  534. .replace('.', '')
  535. }
  536.  
  537. try {
  538. TweetsXer.TweetCount = extractTweetCount('[data-testid="primaryColumn"]>div>div>div')
  539.  
  540. if (!TweetsXer.TweetCount) {
  541. TweetsXer.TweetCount = extractTweetCount('[data-testid="TopNavBar"]>div>div')
  542. }
  543.  
  544. if (!TweetsXer.TweetCount) {
  545. console.log("Wasn't able to find Tweet count on profile. Setting it to 1 million.")
  546. TweetsXer.TweetCount = 1000000
  547. }
  548.  
  549. } catch (error) {
  550. console.log("Wasn't able to find Tweet count on profile. Setting it to 1 million.")
  551. TweetsXer.TweetCount = 1000000 // prevents Tweets from being skipped because if tweet count of 0
  552.  
  553. }
  554. this.updateInfo('Select your tweet-headers.js from your Twitter Data Export to start the deletion of all your Tweets.')
  555. console.log(TweetsXer.TweetCount + " Tweets on profile.")
  556. console.log("You can close the console now to reduce the memory usage.")
  557. console.log("Reopen the console if there are issues to see if an error shows up.")
  558. },
  559.  
  560. async slowDelete() {
  561. //document.getElementById("toggleAdvanced").click()
  562. document.getElementById('start').remove()
  563. TweetsXer.total = TweetsXer.TweetCount
  564. TweetsXer.createProgressBar()
  565.  
  566. document.querySelectorAll('[data-testid="ScrollSnap-List"] a')[1].click()
  567. await TweetsXer.sleep(2000)
  568.  
  569. let unretweet, confirmURT, caret, menu, confirmation
  570.  
  571. const more = '[data-testid="tweet"] [data-testid="caret"]'
  572. while (document.querySelectorAll(more).length > 0) {
  573.  
  574. // give the Tweets a chance to load; increase/decrease if necessary
  575. // afaik the limit is 50 requests per minute
  576. await TweetsXer.sleep(1200)
  577.  
  578. // hide recommended profiles and stuff
  579. document.querySelectorAll('section [data-testid="cellInnerDiv"]>div>div>div').forEach(x => x.remove())
  580. document.querySelectorAll('section [data-testid="cellInnerDiv"]>div>div>[role="link"]').forEach(x => x.remove())
  581. document.querySelector(more).scrollIntoView({
  582. 'behavior': 'smooth'
  583. })
  584.  
  585. // if it is a Retweet, unretweet it
  586. unretweet = document.querySelector('[data-testid="unretweet"]')
  587. if (unretweet) {
  588. unretweet.click()
  589. confirmURT = await waitForElemToExist('[data-testid="unretweetConfirm"]')
  590. confirmURT.click()
  591. }
  592.  
  593. // delete Tweet
  594. else {
  595. caret = await waitForElemToExist(more)
  596. caret.click()
  597.  
  598.  
  599. menu = await waitForElemToExist('[role="menuitem"]')
  600. if (menu.textContent.includes('@')) {
  601. // don't unfollow people (because their Tweet is the reply tab)
  602. caret.click()
  603. document.querySelector('[data-testid="tweet"]').remove()
  604. } else {
  605. menu.click()
  606. confirmation = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]')
  607. if (confirmation) confirmation.click()
  608. }
  609. }
  610.  
  611. TweetsXer.dCount++
  612. TweetsXer.updateProgressBar()
  613.  
  614. // print to the console how many Tweets already got deleted
  615. // Change the 100 to how often you want an update.
  616. // 10 for every 10th Tweet, 1 for every Tweet, 100 for every 100th Tweet
  617. if (TweetsXer.dCount % 100 == 0) console.log(`${new Date().toUTCString()} Deleted ${TweetsXer.dCount} Tweets`)
  618.  
  619. }
  620.  
  621. console.log('No Tweets left. Please reload to confirm.')
  622. },
  623.  
  624. async unfollow() {
  625. //document.getElementById("toggleAdvanced").click()
  626. let unfollowCount = 0
  627. let next_unfollow, menu
  628.  
  629. document.querySelector('[href$="/following"]').click()
  630. await TweetsXer.sleep(1200)
  631.  
  632. const accounts = '[data-testid="UserCell"]'
  633. while (document.querySelectorAll('[data-testid="UserCell"] [data-testid$="-unfollow"]').length > 0) {
  634. next_unfollow = document.querySelectorAll(accounts)[0]
  635. next_unfollow.scrollIntoView({
  636. 'behavior': 'smooth'
  637. })
  638.  
  639. next_unfollow.querySelector('[data-testid$="-unfollow"]').click()
  640. menu = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]')
  641. menu.click()
  642. next_unfollow.remove()
  643. unfollowCount++
  644. if (unfollowCount % 10 == 0) console.log(`${new Date().toUTCString()} Unfollowed ${unfollowCount} accounts`)
  645. await TweetsXer.sleep(Math.floor(Math.random() * 200))
  646. }
  647.  
  648. console.log('No accounts left. Please reload to confirm.')
  649. },
  650. removeTweetXer() {
  651. document.getElementById('exportUpload').remove()
  652. }
  653. }
  654.  
  655. const waitForElemToExist = async (selector) => {
  656.  
  657. const elem = document.querySelector(selector)
  658. if (elem) return elem
  659.  
  660. return new Promise(resolve => {
  661. const observer = new MutationObserver(() => {
  662. const elem = document.querySelector(selector)
  663. if (elem) {
  664. resolve(elem)
  665. observer.disconnect()
  666. }
  667. })
  668.  
  669. observer.observe(document.body, {
  670. subtree: true,
  671. childList: true,
  672. })
  673. })
  674. }
  675.  
  676. TweetsXer.init()
  677.  
  678. // START CODE BY Ali HaSsan TaHir
  679. // https://greasyfork.org/en/scripts/536593-generate-x-client-transaction-id/code
  680. const savedFrames = [];
  681. const ADDITIONAL_RANDOM_NUMBER = 3;
  682. const DEFAULT_KEYWORD = "obfiowerehiring";
  683. let defaultRowIndex = null;
  684. let defaultKeyBytesIndices = null;
  685.  
  686. async function generateTID(api_path) {
  687. if (!defaultRowIndex || !defaultKeyBytesIndices) {
  688. const { firstIndex, remainingIndices } = await getIndices();
  689. defaultRowIndex = firstIndex;
  690. defaultKeyBytesIndices = remainingIndices;
  691. }
  692.  
  693. const method = "GET"
  694. const path = api_path
  695. const key = await getKey();
  696. const keyBytes = getKeyBytes(key);
  697. const animationKey = getAnimationKey(keyBytes);
  698. const xTID = await getTransactionID(method, path, key, keyBytes, animationKey)
  699. //console.log("Generated Transaction ID: ", xTID)
  700. return (xTID)
  701. }
  702.  
  703. const getFramesInterval = setInterval(() => {
  704. const nodes = document.querySelectorAll('[id^="loading-x-anim"]');
  705.  
  706. if (nodes.length === 0 && savedFrames.length !== 0) {
  707. clearInterval(getFramesInterval);
  708. const serialized = savedFrames.map(node => node.outerHTML);
  709. localStorage.setItem("savedFrames", JSON.stringify(serialized));
  710. return;
  711. }
  712.  
  713. nodes.forEach(removedNode => {
  714. if (!savedFrames.includes(removedNode)) {
  715. savedFrames.push(removedNode);
  716. }
  717. });
  718. }, 10);
  719.  
  720.  
  721. async function getIndices() {
  722. let url = null;
  723. const keyByteIndices = [];
  724. const targetFileMatch = document.documentElement.innerHTML.match(/"ondemand\.s":"([0-9a-f]+)"/);
  725.  
  726. if (targetFileMatch) {
  727. const hexString = targetFileMatch[1];
  728. url = `https://abs.twimg.com/responsive-web/client-web/ondemand.s.${hexString}a.js`;
  729. } else {
  730. throw new Error("Transaction ID generator needs an update.");
  731. }
  732.  
  733. const INDICES_REGEX = /\(\w{1}\[(\d{1,2})\],\s*16\)/g;
  734.  
  735. try {
  736. const response = await fetch(url);
  737. if (!response.ok) {
  738. throw new Error(`Failed to fetch indices file: ${response.statusText}`);
  739. }
  740.  
  741. const jsContent = await response.text();
  742. const keyByteIndicesMatch = [...jsContent.matchAll(INDICES_REGEX)];
  743.  
  744. keyByteIndicesMatch.forEach(item => {
  745. keyByteIndices.push(item[1]);
  746. });
  747.  
  748. if (keyByteIndices.length === 0) {
  749. throw new Error("Couldn't get KEY_BYTE indices from file content");
  750. }
  751.  
  752. const keyByteIndicesInt = keyByteIndices.map(Number);
  753. return {
  754. firstIndex: keyByteIndicesInt[0],
  755. remainingIndices: keyByteIndicesInt.slice(1),
  756. };
  757. } catch (error) {
  758. showError(error.message);
  759. return null;
  760. }
  761. }
  762.  
  763. async function getKey() {
  764. return new Promise(resolve => {
  765. const meta = document.querySelector('meta[name="twitter-site-verification"]');
  766. if (meta) resolve(meta.getAttribute("content"));
  767. });
  768. }
  769.  
  770. function getKeyBytes(key) {
  771. return Array.from(atob(key).split("").map(c => c.charCodeAt(0)));
  772. }
  773.  
  774. function getFrames() {
  775. const stored = localStorage.getItem("savedFrames");
  776. if (stored) {
  777. const frames = JSON.parse(stored);
  778. const parser = new DOMParser();
  779.  
  780. return frames.map(frame =>
  781. parser.parseFromString(frame, "text/html").body.firstChild
  782. );
  783. }
  784. return [];
  785. }
  786.  
  787. function get2DArray(keyBytes) {
  788. const frames = getFrames();
  789. const array = Array.from(
  790. frames[keyBytes[5] % 4].children[0].children[1]
  791. .getAttribute("d")
  792. .slice(9)
  793. .split("C")
  794. ).map(item =>
  795. item
  796. .replace(/[^\d]+/g, " ")
  797. .trim()
  798. .split(" ")
  799. .map(Number)
  800. );
  801. return array;
  802. }
  803.  
  804. function solve(value, minVal, maxVal, rounding) {
  805. const result = (value * (maxVal - minVal)) / 255 + minVal;
  806. return rounding ? Math.floor(result) : Math.round(result * 100) / 100;
  807. }
  808.  
  809. function animate(frames, targetTime) {
  810. const fromColor = frames.slice(0, 3).concat(1).map(Number);
  811. const toColor = frames.slice(3, 6).concat(1).map(Number);
  812. const fromRotation = [0.0];
  813. const toRotation = [solve(frames[6], 60.0, 360.0, true)];
  814. const remainingFrames = frames.slice(7);
  815. const curves = remainingFrames.map((item, index) =>
  816. solve(item, isOdd(index), 1.0, false)
  817. );
  818. const cubic = new Cubic(curves);
  819. const val = cubic.getValue(targetTime);
  820. const color = interpolate(fromColor, toColor, val).map(value =>
  821. value > 0 ? value : 0
  822. );
  823. const rotation = interpolate(fromRotation, toRotation, val);
  824. const matrix = convertRotationToMatrix(rotation[0]);
  825. const strArr = color.slice(0, -1).map(value =>
  826. Math.round(value).toString(16)
  827. );
  828.  
  829. for (const value of matrix) {
  830. let rounded = Math.round(value * 100) / 100;
  831. if (rounded < 0) {
  832. rounded = -rounded;
  833. }
  834. const hexValue = floatToHex(rounded);
  835. strArr.push(
  836. hexValue.startsWith(".")
  837. ? `0${hexValue}`.toLowerCase()
  838. : hexValue || "0"
  839. );
  840. }
  841.  
  842. const animationKey = strArr.join("").replace(/[.-]/g, "");
  843. return animationKey;
  844. }
  845.  
  846. function isOdd(num) {
  847. return num % 2 !== 0 ? -1.0 : 0.0;
  848. }
  849.  
  850. function getAnimationKey(keyBytes) {
  851. const totalTime = 4096;
  852.  
  853. if (typeof defaultRowIndex === "undefined" || typeof defaultKeyBytesIndices === "undefined") {
  854. throw new Error("Indices not initialized");
  855. }
  856.  
  857. const rowIndex = keyBytes[defaultRowIndex] % 16;
  858.  
  859. const frameTime = defaultKeyBytesIndices.reduce((acc, index) => {
  860. return acc * (keyBytes[index] % 16);
  861. }, 1);
  862.  
  863. const arr = get2DArray(keyBytes);
  864. if (!arr || !arr[rowIndex]) {
  865. throw new Error("Invalid frame data");
  866. }
  867.  
  868. const frameRow = arr[rowIndex];
  869. const targetTime = frameTime / totalTime;
  870. const animationKey = animate(frameRow, targetTime);
  871.  
  872. return animationKey;
  873. }
  874.  
  875. async function getTransactionID(method, path, key, keyBytes, animationKey) {
  876. if (!method || !path || !key || !animationKey) {
  877. return console.log("Invalid call.")
  878. }
  879. const timeNow = Math.floor((Date.now() - 1682924400 * 1000) / 1000);
  880. const timeNowBytes = [
  881. timeNow & 0xff,
  882. (timeNow >> 8) & 0xff,
  883. (timeNow >> 16) & 0xff,
  884. (timeNow >> 24) & 0xff,
  885. ];
  886.  
  887. const inputString = `${method}!${path}!${timeNow}${DEFAULT_KEYWORD}${animationKey}`;
  888. const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(inputString));
  889. const hashBytes = Array.from(new Uint8Array(hashBuffer));
  890. const randomNum = Math.floor(Math.random() * 256);
  891. const bytesArr = [
  892. ...keyBytes,
  893. ...timeNowBytes,
  894. ...hashBytes.slice(0, 16),
  895. ADDITIONAL_RANDOM_NUMBER,
  896. ];
  897. const out = new Uint8Array(bytesArr.length + 1);
  898. out[0] = randomNum;
  899. bytesArr.forEach((item, index) => {
  900. out[index + 1] = item ^ randomNum;
  901. });
  902. const transactionId = btoa(String.fromCharCode(...out)).replace(/=+$/, "");
  903. return transactionId;
  904. }
  905.  
  906. class Cubic {
  907. constructor(curves) {
  908. this.curves = curves;
  909. }
  910.  
  911. getValue(time) {
  912. let startGradient = 0;
  913. let endGradient = 0;
  914. let start = 0.0;
  915. let mid = 0.0;
  916. let end = 1.0;
  917.  
  918. if (time <= 0.0) {
  919. if (this.curves[0] > 0.0) {
  920. startGradient = this.curves[1] / this.curves[0];
  921. } else if (this.curves[1] === 0.0 && this.curves[2] > 0.0) {
  922. startGradient = this.curves[3] / this.curves[2];
  923. }
  924. return startGradient * time;
  925. }
  926.  
  927. if (time >= 1.0) {
  928. if (this.curves[2] < 1.0) {
  929. endGradient = (this.curves[3] - 1.0) / (this.curves[2] - 1.0);
  930. } else if (this.curves[2] === 1.0 && this.curves[0] < 1.0) {
  931. endGradient = (this.curves[1] - 1.0) / (this.curves[0] - 1.0);
  932. }
  933. return 1.0 + endGradient * (time - 1.0);
  934. }
  935.  
  936. while (start < end) {
  937. mid = (start + end) / 2;
  938. const xEst = this.calculate(this.curves[0], this.curves[2], mid);
  939. if (Math.abs(time - xEst) < 0.00001) {
  940. return this.calculate(this.curves[1], this.curves[3], mid);
  941. }
  942. if (xEst < time) {
  943. start = mid;
  944. } else {
  945. end = mid;
  946. }
  947. }
  948. return this.calculate(this.curves[1], this.curves[3], mid);
  949. }
  950.  
  951. calculate(a, b, m) {
  952. return (
  953. 3.0 * a * (1 - m) * (1 - m) * m +
  954. 3.0 * b * (1 - m) * m * m +
  955. m * m * m
  956. );
  957. }
  958. }
  959.  
  960. function interpolate(fromList, toList, f) {
  961. if (fromList.length !== toList.length) {
  962. throw new Error("Invalid list");
  963. }
  964. const out = [];
  965. for (let i = 0; i < fromList.length; i++) {
  966. out.push(interpolateNum(fromList[i], toList[i], f));
  967. }
  968. return out;
  969. }
  970.  
  971. function interpolateNum(fromVal, toVal, f) {
  972. if (typeof fromVal === "number" && typeof toVal === "number") {
  973. return fromVal * (1 - f) + toVal * f;
  974. }
  975. if (typeof fromVal === "boolean" && typeof toVal === "boolean") {
  976. return f < 0.5 ? fromVal : toVal;
  977. }
  978. }
  979.  
  980. function convertRotationToMatrix(degrees) {
  981. const radians = (degrees * Math.PI) / 180;
  982. const cos = Math.cos(radians);
  983. const sin = Math.sin(radians);
  984. return [cos, sin, -sin, cos, 0, 0];
  985. }
  986.  
  987. function floatToHex(x) {
  988. const result = [];
  989. let quotient = Math.floor(x);
  990. let fraction = x - quotient;
  991.  
  992. while (quotient > 0) {
  993. quotient = Math.floor(x / 16);
  994. const remainder = Math.floor(x - quotient * 16);
  995. if (remainder > 9) {
  996. result.unshift(String.fromCharCode(remainder + 55));
  997. } else {
  998. result.unshift(remainder.toString());
  999. }
  1000. x = quotient;
  1001. }
  1002.  
  1003. if (fraction === 0) {
  1004. return result.join("");
  1005. }
  1006.  
  1007. result.push(".");
  1008.  
  1009. while (fraction > 0) {
  1010. fraction *= 16;
  1011. const integer = Math.floor(fraction);
  1012. fraction -= integer;
  1013. if (integer > 9) {
  1014. result.push(String.fromCharCode(integer + 55));
  1015. } else {
  1016. result.push(integer.toString());
  1017. }
  1018. }
  1019.  
  1020. return result.join("");
  1021. }
  1022.  
  1023. function base64Encode(array) {
  1024. return btoa(String.fromCharCode.apply(null, array));
  1025. }
  1026.  
  1027. // END CODE BY Ali HaSsan TaHir
  1028.  
  1029.  
  1030.  
  1031. })()