您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Delete all your Tweets for free.
当前为
// ==UserScript== // @name TweetXer // @namespace https://github.com/lucahammer/tweetXer/ // @version 0.10.0 // @description Delete all your Tweets for free. // @author Luca,dbort,pReya,Micolithe,STrRedWolf // @license NoHarm-draft // @match https://x.com/* // @match https://mobile.x.com/* // @match https://twitter.com/* // @match https://mobile.twitter.com/* // @icon https://www.google.com/s2/favicons?domain=twitter.com // @grant none // @supportURL https://github.com/lucahammer/tweetXer/issues // ==/UserScript== (function () { let TweetsXer = { version: '0.10.0', TweetCount: 0, dId: "exportUpload", tIds: [], tId: "", ratelimitreset: 0, more: '[data-testid="tweet"] [data-testid="caret"]', skip: 0, total: 0, dCount: 0, deleteURL: '/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet', unfavURL: '/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet', deleteMessageURL: '/i/api/graphql/BJ6DtxA2llfjnRoRjaiIiw/DMMessageDeleteMutation', deleteConvoURL: '/i/api/1.1/dm/conversation/USER_ID-CONVERSATION_ID/delete.json', bookmarksURL: '/i/api/graphql/YnSSREbpZZHAaNdnEk4ycA/Bookmarks?', deleteDMsOneByOne: false, username: '', action: '', bookmarks: [], bookmarksNext: '', baseUrl: 'https://x.com', authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', ct0: false, transaction_id: '', async init() { this.baseUrl = `https://${window.location.hostname}` this.createUploadForm() await this.getTweetCount() this.ct0 = this.getCookie('ct0') this.username = document.location.href.split('/')[3].replace('#', '') }, sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) }, getCookie(name) { const match = `; ${document.cookie}`.match(`;\\s*${name}=([^;]+)`) return match ? match[1] : null }, updateTitle(text) { document.getElementById('tweetsXer_title').textContent = text }, updateInfo(text) { document.getElementById("info").textContent = text }, createProgressBar() { const progressbar = document.createElement("progress") progressbar.id = "progressbar" progressbar.value = this.dCount progressbar.max = this.total progressbar.style = 'width:100%' document.getElementById(this.dId).appendChild(progressbar) }, updateProgressBar() { document.getElementById('progressbar').value = this.dCount this.updateInfo(`${this.dCount} deleted. ${this.tId}`) }, processFile() { const tn = document.getElementById(`${TweetsXer.dId}_file`) if (tn.files && tn.files[0]) { let fr = new FileReader() fr.onloadend = function (evt) { // window.YTD.tweet_headers.part0 // window.YTD.tweets.part0 // window.YTD.like.part0 // window.YTD.direct_message_headers.part0 let cutpoint = evt.target.result.indexOf('= ') let filestart = evt.target.result.slice(0, cutpoint) let json = JSON.parse(evt.target.result.slice(cutpoint + 1)) if (filestart.includes('.tweet_headers.')) { console.log('File contains Tweets.') TweetsXer.action = 'untweet' TweetsXer.tIds = json.map((x) => x.tweet.tweet_id) } else if (filestart.includes('.tweets.') || filestart.includes('.tweet.')) { console.log('File contains Tweets.') TweetsXer.action = 'untweet' TweetsXer.tIds = json.map((x) => x.tweet.id_str) } else if (filestart.includes('.like.')) { console.log('File contains Favs.') TweetsXer.action = 'unfav' TweetsXer.tIds = json.map((x) => x.like.tweetId) } else if ( filestart.includes('.direct_message_headers.') || filestart.includes('.direct_message_group_headers.') || filestart.includes('.direct_messages.') || filestart.includes('.direct_message_groups.')) { console.log('File contains Direct Messages.') TweetsXer.action = 'undm' if (this.deleteDMsOneByOne) { TweetsXer.tIds = json.map((c) => c.dmConversation.messages.map((m) => m.messageCreate ? m.messageCreate.id : 0)) TweetsXer.tIds = TweetsXer.tIds.flat() TweetsXer.tIds = TweetsXer.tIds.filter((i) => i != 0) } else { TweetsXer.tIds = json.map((c) => c.dmConversation.conversationId) } } else { TweetsXer.updateInfo('File content not recognized. Please use a file from the Twitter data export.') console.log('File content not recognized. Please use a file from the Twitter data export.') } if (TweetsXer.action.length > 0) { TweetsXer.total = TweetsXer.tIds.length document.getElementById(`${TweetsXer.dId}_file`).remove() TweetsXer.createProgressBar() } if (TweetsXer.action == 'untweet') { if (document.getElementById('skipCount').value.length < 1) { // If there is no amount set to skip, automatically try to skip the amount // that has been deleted already. Difference of Tweeets in file to count on profile // 5% tolerance to prevent skipping too much TweetsXer.skip = TweetsXer.total - TweetsXer.TweetCount - parseInt(TweetsXer.total / 20) TweetsXer.skip = Math.max(0, TweetsXer.skip) } else { TweetsXer.skip = document.getElementById('skipCount').value } console.log(`Skipping oldest ${TweetsXer.skip} Tweets. Use advanced options to manually set how many to skip. Enter 0 to prevent the automatic calculation.`) TweetsXer.tIds.reverse() TweetsXer.tIds = TweetsXer.tIds.slice(TweetsXer.skip) TweetsXer.dCount = TweetsXer.skip TweetsXer.tIds.reverse() TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} Tweets`) TweetsXer.deleteTweets() } else if (TweetsXer.action == 'unfav') { TweetsXer.skip = document.getElementById('skipCount').value.length > 0 ? document.getElementById('skipCount').value : 0 console.log(`Skipping oldest ${TweetsXer.skip} Tweets`) TweetsXer.tIds = TweetsXer.tIds.slice(TweetsXer.skip) TweetsXer.dCount = TweetsXer.skip TweetsXer.tIds.reverse() TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} Favs`) TweetsXer.deleteFavs() } else if (TweetsXer.action == 'undm') { TweetsXer.skip = document.getElementById('skipCount').value.length > 0 ? document.getElementById('skipCount').value : 0 console.log(`Skipping ${TweetsXer.skip} messages/convos`) TweetsXer.tIds = TweetsXer.tIds.slice(TweetsXer.skip) TweetsXer.dCount = TweetsXer.skip TweetsXer.tIds.reverse() if (this.deleteDMsOneByOne) { TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} DMs`) TweetsXer.deleteDMs() } else { TweetsXer.updateTitle(`TweetXer: Deleting ${TweetsXer.total} DM Conversations`) TweetsXer.deleteConvos() } } else { TweetsXer.updateTitle(`TweetXer: Please try a different file`) } } fr.readAsText(tn.files[0]) } }, createUploadForm() { const h2Class = document.querySelectorAll("h2")[1]?.getAttribute("class") || "" const div = document.createElement("div") div.id = this.dId if (document.getElementById(this.dId)) { document.getElementById(this.dId).remove() } div.innerHTML = ` <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> <div style="color:black"> <h2 class="${h2Class}" id="tweetsXer_title">TweetXer</h2> <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> <p id="start"> <input type="file" value="" id="${this.dId}_file" /> <a style="color:blue" href="#" id="toggleAdvanced">Advanced Options</a> <div id="advanced" style="display:none"> <label for="skipCount">Enter how many Tweets to skip before selecting a file.</label> <input id="skipCount" type="number" value="" /> <p>Supported files: <ul> <li>tweet-headers.js to delete Tweets (10.000 - 20.000 per hour)</li> <li>direct-message-header.js and direct-message-group-headers.js to delete DMs (around 800 per 15 minutes)</li> <li>like.js to delete Favs (500 per 15 minutes; only works for the most recent few thousands)</li> </ul> <p><strong>Export bookmarks</strong><br> Bookmarks are not included in the official data export. You can export them here. <input id="exportBookmarks" type="button" value="Export Bookmarks" /> </p> <p><strong>No tweet-headers.js?</strong><br> If you are unable to get your data export, you can use the following option.<br> This option is much slower and less reliable. It can remove at most 4000 Tweets per hour.<br> <input id="slowDelete" type="button" value="Slow delete without file" /> </p> <p><strong>Unfollow everyone</strong><br> It's time to let go. This will unfollow everyone you follow.<br> <input id="unfollowEveryone" type="button" value="Unfollow everyone" /> </p> <p><a id="removeTweetXer" style="color:blue" href="#">Remove TweetXer</a></p> <p><small>${TweetsXer.version}</small></p> </div> </div> ` document.body.insertBefore(div, document.body.firstChild) document.getElementById("toggleAdvanced").addEventListener("click", (() => { const adv = document.getElementById('advanced') if (adv.style.display == 'none') { adv.style.display = 'block' } else { adv.style.display = 'none' } })) document.getElementById(`${this.dId}_file`).addEventListener("change", this.processFile, false) document.getElementById("exportBookmarks").addEventListener("click", this.exportBookmarks, false) document.getElementById("slowDelete").addEventListener("click", this.slowDelete, false) document.getElementById("unfollowEveryone").addEventListener("click", this.unfollow, false) document.getElementById("removeTweetXer").addEventListener("click", this.removeTweetXer, false) }, async exportBookmarks() { TweetsXer.updateTitle('TweetXer: Exporting bookmarks') let variables = '' while (TweetsXer.bookmarksNext.length > 0 || TweetsXer.bookmarks.length == 0) { if (TweetsXer.bookmarksNext.length > 0) { variables = `{"count":20,"cursor":"${TweetsXer.bookmarksNext}","includePromotedContent":false}` } else variables = '{"count":20,"includePromotedContent":false}' let fetch_url = TweetsXer.baseUrl + TweetsXer.bookmarksURL + new URLSearchParams({ variables: variables, 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}' }) let transaction_id = await generateTID(fetch_url) let response = await fetch(fetch_url, { "headers": { "authorization": TweetsXer.authorization, "content-type": "application/json", "x-twitter-auth-type": "OAuth2Session", "x-csrf-token": TweetsXer.ct0, "x-twitter-client-language": "en", "x-twitter-active-user": "yes", "x-client-transaction-id": transaction_id, "x-xp-forwarded-for": '' }, "referrer": `${TweetsXer.baseUrl}/i/bookmarks`, "referrerPolicy": "strict-origin-when-cross-origin", "method": "GET", "mode": "cors", "credentials": "include" }) if (response.status == 200) { let data = await response.json() data.data.bookmark_timeline_v2.timeline.instructions[0].entries.forEach((item) => { if (item.entryId.includes('tweet')) { TweetsXer.dCount++ TweetsXer.bookmarks.push(item.content.itemContent.tweet_results.result) } else if (item.entryId.includes('cursor-bottom')) { if (TweetsXer.bookmarksNext != item.content.value) { TweetsXer.bookmarksNext = item.content.value } else { TweetsXer.bookmarksNext = '' } } }) //document.getElementById('progressbar').setAttribute('value', TweetsXer.dCount) TweetsXer.updateInfo(`${TweetsXer.dCount} Bookmarks collected`) } else { console.log(response) break } if (!response.headers.get('x-rate-limit-remaining') && response.headers.get('x-rate-limit-remaining') < 1) { console.log('rate limit hit') TweetsXer.ratelimitreset = response.headers.get('x-rate-limit-reset') let sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000) while (sleeptime > 0) { sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000) TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`) await TweetsXer.sleep(1000) } } } let download = new Blob([JSON.stringify(TweetsXer.bookmarks)], { type: 'text/plain' }) let bookmarksDownload = document.createElement("a") bookmarksDownload.id = 'bookmarksDownload' bookmarksDownload.innerText = 'Download bookmarks' bookmarksDownload.href = window.URL.createObjectURL(download) bookmarksDownload.download = 'twitter-bookmarks.json' document.getElementById('advanced').appendChild(bookmarksDownload) TweetsXer.updateTitle('TweetXer') }, async sendRequest( url, body = `{\"variables\":{\"tweet_id\":\"${TweetsXer.tId}\",\"dark_request\":false},\"queryId\":\"${url.split('/')[6]}\"}` ) { return new Promise(async (resolve) => { try { let response = await fetch(url, { "headers": { "authorization": TweetsXer.authorization, "content-type": "application/json", "x-client-transaction-id": TweetsXer.transaction_id, "x-csrf-token": TweetsXer.ct0, "x-twitter-active-user": "yes", "x-twitter-auth-type": "OAuth2Session", "x-client-transaction-id": await generateTID(url), "x-xp-forwarded-for": '' }, "referrer": `${TweetsXer.baseUrl}/${TweetsXer.username}/with_replies`, "referrerPolicy": "strict-origin-when-cross-origin", "body": body, "method": "POST", "mode": "cors", "credentials": "include", "signal": AbortSignal.timeout(5000) }) if (response.status == 200) { TweetsXer.dCount++ TweetsXer.updateProgressBar() if (response.headers.get('x-rate-limit-remaining') != null && response.headers.get('x-rate-limit-remaining') < 1) { console.log('rate limit hit') console.log(response.headers.get('x-rate-limit-remaining')) TweetsXer.ratelimitreset = response.headers.get('x-rate-limit-reset') let sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000) while (sleeptime > 0) { sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000) TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`) await TweetsXer.sleep(1000) } resolve('deleted and waiting') } else { resolve('deleted') } } else if (response.status == 429) { TweetsXer.tIds.push(TweetsXer.tId) console.log('Received status code 429. Waiting for 1 second before trying again.') await TweetsXer.sleep(1000) } else { console.log(response) } } catch (error) { if (error.Name === 'AbortError') { TweetsXer.tIds.push(TweetsXer.tId) console.log('Request timeout.') let sleeptime = 15 while (sleeptime > 0) { sleeptime-- TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`) await TweetsXer.sleep(1000) } resolve('error') } } }) }, async deleteTweets() { while (this.tIds.length > 0) { this.tId = this.tIds.pop() await this.sendRequest(this.baseUrl + this.deleteURL) } this.tId = '' this.updateProgressBar() }, async deleteFavs() { this.updateTitle('TweetXer: Deleting Favs') // 500 unfavs per 15 Minutes // x-rate-limit-remaining // x-rate-limit-reset while (this.tIds.length > 0) { this.tId = this.tIds.pop() await this.sendRequest(this.baseUrl + this.unfavURL) } this.tId = '' this.updateTitle('TweetXer') this.updateProgressBar() }, async deleteDMs() { while (this.tIds.length > 0) { this.tId = this.tIds.pop() await this.sendRequest( this.baseUrl + this.deleteMessageURL, body = `{\"variables\":{\"messageId\":\"${this.tId}\"},\"requestId\":\""}` ) } this.tId = '' this.updateProgressBar() }, async deleteConvos() { while (this.tIds.length > 0) { this.tId = this.tIds.pop() url = this.baseUrl + this.deleteConvoURL.replace('USER_ID-CONVERSATION_ID', this.tId) let response = await fetch(url, { "headers": { "authorization": TweetsXer.authorization, "content-type": "application/x-www-form-urlencoded", "x-client-transaction-id": TweetsXer.transaction_id, "x-csrf-token": TweetsXer.ct0, "x-twitter-active-user": "yes", "x-twitter-auth-type": "OAuth2Session", "x-client-transaction-id": await generateTID(url), "x-xp-forwarded-for": '' }, "referrer": `${TweetsXer.baseUrl}/messages`, "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', "method": "POST", "mode": "cors", "credentials": "include", "signal": AbortSignal.timeout(5000) }) if (response.status == 204) { TweetsXer.dCount++ TweetsXer.updateProgressBar() if (response.headers.get('x-rate-limit-remaining') != null && response.headers.get('x-rate-limit-remaining') < 1) { console.log('rate limit hit') console.log(response.headers.get('x-rate-limit-remaining')) TweetsXer.ratelimitreset = response.headers.get('x-rate-limit-reset') let sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000) while (sleeptime > 0) { sleeptime = TweetsXer.ratelimitreset - Math.floor(Date.now() / 1000) TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`) await TweetsXer.sleep(1000) } } await TweetsXer.sleep(Math.floor(Math.random() * 200)) // send requests slightly slower and with random intervals } else if (response.status == 429 || response.status == 420) { TweetsXer.tIds.push(TweetsXer.tId) console.log(`Received status code ${response.status}. Waiting before trying again.`) let sleeptime = 60 * 5 // is that enough? while (sleeptime > 0) { sleeptime-- TweetsXer.updateInfo(`Ratelimited. Waiting ${sleeptime} seconds. ${TweetsXer.dCount} deleted.`) await TweetsXer.sleep(1000) } } else { console.log(response) } } this.tId = '' this.updateProgressBar() }, async getTweetCount() { await waitForElemToExist('header') await TweetsXer.sleep(1000) if (!document.querySelector('[data-testid="UserName"]')) { if (document.querySelector('[aria-label="Back"]')) { await TweetsXer.sleep(200) document.querySelector('[aria-label="Back"]').click() await TweetsXer.sleep(1000) } else if (document.querySelector('[data-testid="app-bar-back"]')) { document.querySelector('[data-testid="app-bar-back"]').click() await TweetsXer.sleep(1000) } if (document.querySelector('[data-testid="AppTabBar_Profile_Link"]')) { await TweetsXer.sleep(200) document.querySelector('[data-testid="AppTabBar_Profile_Link"]').click() } else if (document.querySelector('[data-testid="DashButton_ProfileIcon_Link"]')) { await TweetsXer.sleep(100) document.querySelector('[data-testid="DashButton_ProfileIcon_Link"]').click() await TweetsXer.sleep(1000) document.querySelector('[data-testid="icon"').nextElementSibling.click() } await waitForElemToExist('[data-testid="UserName"]') } await TweetsXer.sleep(1000) function extractTweetCount(selector) { const element = document.querySelector(selector) if (!element) return null const match = element.textContent.match(/((\d|,|\.|K)+) (\w+)$/) if (!match) return null return match[1] .replace(/\.(\d+)K/, '$1'.padEnd(4, '0')) .replace('K', '000') .replace(',', '') .replace('.', '') } try { TweetsXer.TweetCount = extractTweetCount('[data-testid="primaryColumn"]>div>div>div') if (!TweetsXer.TweetCount) { TweetsXer.TweetCount = extractTweetCount('[data-testid="TopNavBar"]>div>div') } if (!TweetsXer.TweetCount) { console.log("Wasn't able to find Tweet count on profile. Setting it to 1 million.") TweetsXer.TweetCount = 1000000 } } catch (error) { console.log("Wasn't able to find Tweet count on profile. Setting it to 1 million.") TweetsXer.TweetCount = 1000000 // prevents Tweets from being skipped because if tweet count of 0 } this.updateInfo('Select your tweet-headers.js from your Twitter Data Export to start the deletion of all your Tweets.') console.log(TweetsXer.TweetCount + " Tweets on profile.") console.log("You can close the console now to reduce the memory usage.") console.log("Reopen the console if there are issues to see if an error shows up.") }, async slowDelete() { //document.getElementById("toggleAdvanced").click() document.getElementById('start').remove() TweetsXer.total = TweetsXer.TweetCount TweetsXer.createProgressBar() document.querySelectorAll('[data-testid="ScrollSnap-List"] a')[1].click() await TweetsXer.sleep(2000) let unretweet, confirmURT, caret, menu, confirmation const more = '[data-testid="tweet"] [data-testid="caret"]' while (document.querySelectorAll(more).length > 0) { // give the Tweets a chance to load; increase/decrease if necessary // afaik the limit is 50 requests per minute await TweetsXer.sleep(1200) // hide recommended profiles and stuff document.querySelectorAll('section [data-testid="cellInnerDiv"]>div>div>div').forEach(x => x.remove()) document.querySelectorAll('section [data-testid="cellInnerDiv"]>div>div>[role="link"]').forEach(x => x.remove()) document.querySelector(more).scrollIntoView({ 'behavior': 'smooth' }) // if it is a Retweet, unretweet it unretweet = document.querySelector('[data-testid="unretweet"]') if (unretweet) { unretweet.click() confirmURT = await waitForElemToExist('[data-testid="unretweetConfirm"]') confirmURT.click() } // delete Tweet else { caret = await waitForElemToExist(more) caret.click() menu = await waitForElemToExist('[role="menuitem"]') if (menu.textContent.includes('@')) { // don't unfollow people (because their Tweet is the reply tab) caret.click() document.querySelector('[data-testid="tweet"]').remove() } else { menu.click() confirmation = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]') if (confirmation) confirmation.click() } } TweetsXer.dCount++ TweetsXer.updateProgressBar() // print to the console how many Tweets already got deleted // Change the 100 to how often you want an update. // 10 for every 10th Tweet, 1 for every Tweet, 100 for every 100th Tweet if (TweetsXer.dCount % 100 == 0) console.log(`${new Date().toUTCString()} Deleted ${TweetsXer.dCount} Tweets`) } console.log('No Tweets left. Please reload to confirm.') }, async unfollow() { //document.getElementById("toggleAdvanced").click() let unfollowCount = 0 let next_unfollow, menu document.querySelector('[href$="/following"]').click() await TweetsXer.sleep(1200) const accounts = '[data-testid="UserCell"]' while (document.querySelectorAll('[data-testid="UserCell"] [data-testid$="-unfollow"]').length > 0) { next_unfollow = document.querySelectorAll(accounts)[0] next_unfollow.scrollIntoView({ 'behavior': 'smooth' }) next_unfollow.querySelector('[data-testid$="-unfollow"]').click() menu = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]') menu.click() next_unfollow.remove() unfollowCount++ if (unfollowCount % 10 == 0) console.log(`${new Date().toUTCString()} Unfollowed ${unfollowCount} accounts`) await TweetsXer.sleep(Math.floor(Math.random() * 200)) } console.log('No accounts left. Please reload to confirm.') }, removeTweetXer() { document.getElementById('exportUpload').remove() } } const waitForElemToExist = async (selector) => { const elem = document.querySelector(selector) if (elem) return elem return new Promise(resolve => { const observer = new MutationObserver(() => { const elem = document.querySelector(selector) if (elem) { resolve(elem) observer.disconnect() } }) observer.observe(document.body, { subtree: true, childList: true, }) }) } TweetsXer.init() // START CODE BY Ali HaSsan TaHir // https://gf.qytechs.cn/en/scripts/536593-generate-x-client-transaction-id/code const savedFrames = []; const ADDITIONAL_RANDOM_NUMBER = 3; const DEFAULT_KEYWORD = "obfiowerehiring"; let defaultRowIndex = null; let defaultKeyBytesIndices = null; async function generateTID(api_path) { if (!defaultRowIndex || !defaultKeyBytesIndices) { const { firstIndex, remainingIndices } = await getIndices(); defaultRowIndex = firstIndex; defaultKeyBytesIndices = remainingIndices; } const method = "GET" const path = api_path const key = await getKey(); const keyBytes = getKeyBytes(key); const animationKey = getAnimationKey(keyBytes); const xTID = await getTransactionID(method, path, key, keyBytes, animationKey) //console.log("Generated Transaction ID: ", xTID) return (xTID) } const getFramesInterval = setInterval(() => { const nodes = document.querySelectorAll('[id^="loading-x-anim"]'); if (nodes.length === 0 && savedFrames.length !== 0) { clearInterval(getFramesInterval); const serialized = savedFrames.map(node => node.outerHTML); localStorage.setItem("savedFrames", JSON.stringify(serialized)); return; } nodes.forEach(removedNode => { if (!savedFrames.includes(removedNode)) { savedFrames.push(removedNode); } }); }, 10); async function getIndices() { let url = null; const keyByteIndices = []; const targetFileMatch = document.documentElement.innerHTML.match(/"ondemand\.s":"([0-9a-f]+)"/); if (targetFileMatch) { const hexString = targetFileMatch[1]; url = `https://abs.twimg.com/responsive-web/client-web/ondemand.s.${hexString}a.js`; } else { throw new Error("Transaction ID generator needs an update."); } const INDICES_REGEX = /\(\w{1}\[(\d{1,2})\],\s*16\)/g; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch indices file: ${response.statusText}`); } const jsContent = await response.text(); const keyByteIndicesMatch = [...jsContent.matchAll(INDICES_REGEX)]; keyByteIndicesMatch.forEach(item => { keyByteIndices.push(item[1]); }); if (keyByteIndices.length === 0) { throw new Error("Couldn't get KEY_BYTE indices from file content"); } const keyByteIndicesInt = keyByteIndices.map(Number); return { firstIndex: keyByteIndicesInt[0], remainingIndices: keyByteIndicesInt.slice(1), }; } catch (error) { showError(error.message); return null; } } async function getKey() { return new Promise(resolve => { const meta = document.querySelector('meta[name="twitter-site-verification"]'); if (meta) resolve(meta.getAttribute("content")); }); } function getKeyBytes(key) { return Array.from(atob(key).split("").map(c => c.charCodeAt(0))); } function getFrames() { const stored = localStorage.getItem("savedFrames"); if (stored) { const frames = JSON.parse(stored); const parser = new DOMParser(); return frames.map(frame => parser.parseFromString(frame, "text/html").body.firstChild ); } return []; } function get2DArray(keyBytes) { const frames = getFrames(); const array = Array.from( frames[keyBytes[5] % 4].children[0].children[1] .getAttribute("d") .slice(9) .split("C") ).map(item => item .replace(/[^\d]+/g, " ") .trim() .split(" ") .map(Number) ); return array; } function solve(value, minVal, maxVal, rounding) { const result = (value * (maxVal - minVal)) / 255 + minVal; return rounding ? Math.floor(result) : Math.round(result * 100) / 100; } function animate(frames, targetTime) { const fromColor = frames.slice(0, 3).concat(1).map(Number); const toColor = frames.slice(3, 6).concat(1).map(Number); const fromRotation = [0.0]; const toRotation = [solve(frames[6], 60.0, 360.0, true)]; const remainingFrames = frames.slice(7); const curves = remainingFrames.map((item, index) => solve(item, isOdd(index), 1.0, false) ); const cubic = new Cubic(curves); const val = cubic.getValue(targetTime); const color = interpolate(fromColor, toColor, val).map(value => value > 0 ? value : 0 ); const rotation = interpolate(fromRotation, toRotation, val); const matrix = convertRotationToMatrix(rotation[0]); const strArr = color.slice(0, -1).map(value => Math.round(value).toString(16) ); for (const value of matrix) { let rounded = Math.round(value * 100) / 100; if (rounded < 0) { rounded = -rounded; } const hexValue = floatToHex(rounded); strArr.push( hexValue.startsWith(".") ? `0${hexValue}`.toLowerCase() : hexValue || "0" ); } const animationKey = strArr.join("").replace(/[.-]/g, ""); return animationKey; } function isOdd(num) { return num % 2 !== 0 ? -1.0 : 0.0; } function getAnimationKey(keyBytes) { const totalTime = 4096; if (typeof defaultRowIndex === "undefined" || typeof defaultKeyBytesIndices === "undefined") { throw new Error("Indices not initialized"); } const rowIndex = keyBytes[defaultRowIndex] % 16; const frameTime = defaultKeyBytesIndices.reduce((acc, index) => { return acc * (keyBytes[index] % 16); }, 1); const arr = get2DArray(keyBytes); if (!arr || !arr[rowIndex]) { throw new Error("Invalid frame data"); } const frameRow = arr[rowIndex]; const targetTime = frameTime / totalTime; const animationKey = animate(frameRow, targetTime); return animationKey; } async function getTransactionID(method, path, key, keyBytes, animationKey) { if (!method || !path || !key || !animationKey) { return console.log("Invalid call.") } const timeNow = Math.floor((Date.now() - 1682924400 * 1000) / 1000); const timeNowBytes = [ timeNow & 0xff, (timeNow >> 8) & 0xff, (timeNow >> 16) & 0xff, (timeNow >> 24) & 0xff, ]; const inputString = `${method}!${path}!${timeNow}${DEFAULT_KEYWORD}${animationKey}`; const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(inputString)); const hashBytes = Array.from(new Uint8Array(hashBuffer)); const randomNum = Math.floor(Math.random() * 256); const bytesArr = [ ...keyBytes, ...timeNowBytes, ...hashBytes.slice(0, 16), ADDITIONAL_RANDOM_NUMBER, ]; const out = new Uint8Array(bytesArr.length + 1); out[0] = randomNum; bytesArr.forEach((item, index) => { out[index + 1] = item ^ randomNum; }); const transactionId = btoa(String.fromCharCode(...out)).replace(/=+$/, ""); return transactionId; } class Cubic { constructor(curves) { this.curves = curves; } getValue(time) { let startGradient = 0; let endGradient = 0; let start = 0.0; let mid = 0.0; let end = 1.0; if (time <= 0.0) { if (this.curves[0] > 0.0) { startGradient = this.curves[1] / this.curves[0]; } else if (this.curves[1] === 0.0 && this.curves[2] > 0.0) { startGradient = this.curves[3] / this.curves[2]; } return startGradient * time; } if (time >= 1.0) { if (this.curves[2] < 1.0) { endGradient = (this.curves[3] - 1.0) / (this.curves[2] - 1.0); } else if (this.curves[2] === 1.0 && this.curves[0] < 1.0) { endGradient = (this.curves[1] - 1.0) / (this.curves[0] - 1.0); } return 1.0 + endGradient * (time - 1.0); } while (start < end) { mid = (start + end) / 2; const xEst = this.calculate(this.curves[0], this.curves[2], mid); if (Math.abs(time - xEst) < 0.00001) { return this.calculate(this.curves[1], this.curves[3], mid); } if (xEst < time) { start = mid; } else { end = mid; } } return this.calculate(this.curves[1], this.curves[3], mid); } calculate(a, b, m) { return ( 3.0 * a * (1 - m) * (1 - m) * m + 3.0 * b * (1 - m) * m * m + m * m * m ); } } function interpolate(fromList, toList, f) { if (fromList.length !== toList.length) { throw new Error("Invalid list"); } const out = []; for (let i = 0; i < fromList.length; i++) { out.push(interpolateNum(fromList[i], toList[i], f)); } return out; } function interpolateNum(fromVal, toVal, f) { if (typeof fromVal === "number" && typeof toVal === "number") { return fromVal * (1 - f) + toVal * f; } if (typeof fromVal === "boolean" && typeof toVal === "boolean") { return f < 0.5 ? fromVal : toVal; } } function convertRotationToMatrix(degrees) { const radians = (degrees * Math.PI) / 180; const cos = Math.cos(radians); const sin = Math.sin(radians); return [cos, sin, -sin, cos, 0, 0]; } function floatToHex(x) { const result = []; let quotient = Math.floor(x); let fraction = x - quotient; while (quotient > 0) { quotient = Math.floor(x / 16); const remainder = Math.floor(x - quotient * 16); if (remainder > 9) { result.unshift(String.fromCharCode(remainder + 55)); } else { result.unshift(remainder.toString()); } x = quotient; } if (fraction === 0) { return result.join(""); } result.push("."); while (fraction > 0) { fraction *= 16; const integer = Math.floor(fraction); fraction -= integer; if (integer > 9) { result.push(String.fromCharCode(integer + 55)); } else { result.push(integer.toString()); } } return result.join(""); } function base64Encode(array) { return btoa(String.fromCharCode.apply(null, array)); } // END CODE BY Ali HaSsan TaHir })()
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址