delete-twitter
change
commit 7cbdd5f382aaca9c07c44cb42e8cc3ffb2eb4ca9
author Alpha Chen <alpha@kejadlen.dev>
date
parent 80e8a1e8
diff --git a/delete-twitter/.gitignore b/delete-twitter/.gitignore
new file mode 100644
index 0000000..99fad44
--- /dev/null
+++ b/delete-twitter/.gitignore
@@ -0,0 +1 @@
+*.store
diff --git a/delete-twitter/Rakefile b/delete-twitter/Rakefile
new file mode 100644
index 0000000..f1b3ad3
--- /dev/null
+++ b/delete-twitter/Rakefile
@@ -0,0 +1,104 @@
+require "json"
+require "net/http"
+require "pstore"
+
+TWITTER_ARCHIVE = ENV.fetch("TWITTER_ARCHIVE")
+
+file "tweets.store" do
+  json = File.read("#{TWITTER_ARCHIVE}/data/tweets.js").sub(/^window.YTD.tweets.part0 = /,"")
+  tweets = JSON.load(json)
+
+  store = PStore.new("tweets.store")
+  store.transaction do
+    tweets.each do |tweet|
+      store[tweet.fetch("tweet").fetch("id_str")] = { is_deleted: false }
+    end
+  end
+end
+
+file "following.store" do
+  json = File.read("#{TWITTER_ARCHIVE}/data/following.js").sub(/^window.YTD.following.part0 = /,"")
+  followings = JSON.load(json)
+
+  store = PStore.new("following.store")
+  store.transaction do
+    followings.each do |following|
+      store[following.fetch("following").fetch("accountId")] = { is_deleted: false }
+    end
+  end
+end
+
+def delete_tweet(id)
+  uri = URI("https://twitter.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet")
+  headers = ENV.fetch("XITTER_DELETE_TWEET_HEADERS").lines(chomp: true).to_h { _1.split(": ", 2) }
+  data = JSON.dump({
+    variables: {
+      tweet_id:,
+      dark_request: false,
+    },
+    queryId: "VaenaVgh5q5ih7kvyVjgtg",
+  })
+  Net::HTTP.post(uri, data, headers)
+end
+
+def unfollow(id)
+  uri = URI("https://x.com/i/api/1.1/friendships/destroy.json")
+  headers = ENV.fetch("XITTER_UNFOLLOW_HEADERS").scan(/((?~')): ((?~'))/).to_h
+  Net::HTTP.post(uri, <<~DATA.lines(chomp: true).join(?&), headers)
+    include_profile_interstitial_type=1
+    include_blocking=1
+    include_blocked_by=1
+    include_followed_by=1
+    include_want_retweets=1
+    include_mute_edge=1
+    include_can_dm=1
+    include_can_media_tag=1
+    include_ext_is_blue_verified=1
+    include_ext_verified_type=1
+    include_ext_profile_image_shape=1
+    skip_status=1
+    user_id=#{id}
+  DATA
+end
+
+task delete_tweets: "tweets.store" do
+  store = PStore.new("tweets.store")
+  keys = store.transaction do
+    store.keys.reject {|tweet_id| store[tweet_id].fetch(:is_deleted) }
+  end
+  p "deleting #{keys.length} tweets"
+
+  keys.each do |tweet_id|
+    p "deleting #{tweet_id}"
+    resp = delete_tweet(tweet_id)
+    if resp.code == "200"
+      store.transaction do
+        store[tweet_id] = { is_deleted: true }
+      end
+    else
+      p "error deleting #{tweet_id}"
+    end
+  end
+end
+
+task unfollow: "following.store" do
+  store = PStore.new("following.store")
+  keys = store.transaction do
+    store.keys.reject {|account_id| store[account_id].fetch(:is_deleted) }
+  end
+  p "unfollowing #{keys.length} users"
+
+  keys.each do |account_id|
+    p "unfollowing #{account_id}"
+    resp = unfollow(account_id)
+    if resp.code == "200"
+      store.transaction do
+        store[account_id] = { is_deleted: true }
+      end
+    else
+      p "error unfollowing #{account_id}: #{resp.body}"
+    end
+  end
+end
+
+task default: %i[ delete_tweets unfollow ]