i_delete_my_tweets 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3ff348ad213ee9c945f3446ddcfc262ddd765b365c2ba822659ce23c50c13031
4
+ data.tar.gz: 5b9e89d7c7f20425ace5030692ee2691aab1635f758bf9740b3c2554d869b1eb
5
+ SHA512:
6
+ metadata.gz: 958a1f50a5fac1cde0361bf7f0cc0a17294ab8fc28a00164d0a8b6d7786b938f89fb81b350c3c3883971ad60f5d6d9e7d549fae3ecfdecff54c7b3beb6b108b8
7
+ data.tar.gz: faef72a69487e4e84b0b0d95dc4675c7bc9f487e84b2decbcc2e29e5a55efcee345b03e7503a6cbee490ca67c57e0933adb22afd648501f68d5d29a57b8dbeff
data/.env.sample ADDED
@@ -0,0 +1,9 @@
1
+ CONSUMER_KEY=
2
+ CONSUMER_SECRET=
3
+ ACCESS_TOKEN=
4
+ ACCESS_TOKEN_SECRET=
5
+ OLDER_THAN="2022-04-28 21:20:47 -0300"
6
+ PATH_TO_CSV='./tweets.csv'
7
+ FAVE_THRESHOLD=3
8
+ RT_THRESHOLD=5
9
+ SCREEN_NAME=jack
data/.env.test ADDED
@@ -0,0 +1,9 @@
1
+ CONSUMER_KEY=12121122
2
+ CONSUMER_SECRET=12321312312
3
+ ACCESS_TOKEN=12312312321321
4
+ ACCESS_TOKEN_SECRET=123123123123123
5
+ OLDER_THAN="2022-04-28 21:20:47 -0300"
6
+ PATH_TO_CSV='./tweets.csv'
7
+ FAVE_THRESHOLD=3
8
+ RT_THRESHOLD=5
9
+ SCREEN_NAME=jack
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # I Delete My Tweets
2
+
3
+ A **CLI** (as in Command Line Interface) to delete your tweets based on faves, RTs, and time.
4
+
5
+ There are some services out there with a friendly web interface, but this is not one of them.
6
+ You must know the basics of working with a UNIX terminal and configuring a Twitter API app, as this
7
+ will only work if you have a Twitter Developer account.
8
+
9
+ Due to the irrevocable nature of tweet deletion, all delete commands are `dry-run` true, meaning
10
+ you must call all of them with a `--dry-run=false` flag if you want them to really do something.
11
+
12
+ Called with `--dry-run=false`, there is no way to revoke tweet deletion. They are just gone, disappeared into the ether (or the stashed in the Twitter-owned secret place you have no access to without a mandate since nothing gets _really_ deleted from the web these days, folks).
13
+
14
+ This tool won't delete all of your tweets in one fell swoop; it is more of a way to delete your old tweets from time to time. The [Twitter API rate limits](https://developer.twitter.com/en/docs/twitter-api/rate-limits) are relatively complicated, and I don't even wanna go there, but if you do intend on deleting all of your tweets, you can do it with this CLI and some perseverance. I did delete more than 100k of mine by using this script every day for a couple of weeks. The more tweets you delete, the fewer of them you have, and with time the rate limits won't be that much of a problem.
15
+
16
+ I Delete My Tweets (IDMT) can delete your tweets by fetching them via API using an APP you will have to set up yourself. Still, it can also delete tweets from an CSV (comma-separated file) that you can generate from the archive you can request from twitter.com by going to Settings and privacy > Your Account > Download an archive of your data. It is out of the scope of this CLI to generate the CSV (at the moment) but [there are scripts out there](https://gist.github.com/jessefogarty/b0f2d4ea6bdd770e5e9e94d54154c751) that can do this for you.
17
+
18
+ > TIP: You can find an example of the CSV header in the project's root folder.
19
+
20
+ These are the keys/values that make the script work. They are read from and written to
21
+ a `.i_delete_my_tweets` env file in your user directory (~/). You can fill
22
+ the values yourself or work with the <config store> commands (see Usage) to do that interactively.
23
+
24
+ | KEY | VALUE | DESCRIPTION |
25
+ | ------------------- | --------------------------- | ---------------------------------------- |
26
+ | CONSUMER_KEY | String | Your Twitter App key |
27
+ | CONSUMER_SECRET | String | Your Twitter App secret |
28
+ | ACCESS_TOKEN | String | Account access token |
29
+ | ACCESS_TOKEN_SECRET | String | Access token secret |
30
+ | OLDER_THAN | "2022-04-28 21:20:47 -0300" | A timestamp |
31
+ | PATH_TO_CSV | './tweets.csv' | A path to a CSV file |
32
+ | FAVE_THRESHOLD | 3 | Minimum number of faves to skip deletion |
33
+ | RT_THRESHOLD | 5 | Minimum number of RTs to skip deletion |
34
+ | SCREEN_NAME | jack | The account screen_name |
35
+
36
+ Since you have to call commands with `--dry-run=false` for them to really take action, just play around with the skip rules before using `--dry-run=false` and see what works for you.
37
+
38
+ ## Install
39
+
40
+ ```sh
41
+ $ gem install i_delete_my_tweets
42
+ ```
43
+
44
+ IDMT is compatible with ruby-2.6.5 up.
45
+
46
+ ## Usage
47
+
48
+ ```sh
49
+ $ i_delete_my_tweets -h
50
+ ```
51
+
52
+ Will print all commands and options.
53
+
54
+ ```sh
55
+ $ i_delete_my_tweets -v
56
+ ```
57
+
58
+ Gives you the version.
59
+
60
+ ### Commands
61
+
62
+ ```sh
63
+ $ i_delete_my_tweets config store key value
64
+ ```
65
+
66
+ ### Set-up
67
+
68
+ - [Create a Twitter Developer Account](https://developer.twitter.com/en/apply) if you don't already have one.
69
+
70
+ You have to wait for the account to be reviewed and approved.
71
+
72
+ - [Create a Twitter App](https://developer.twitter.com/en/apps/create) with read and write permission
73
+ - Take note of the app's `CONSUMER_KEY` and `CONSUMER_SECRET`
74
+
75
+ #### Config
76
+
77
+ ```sh
78
+ $ i_delete_my_tweets config store CONSUMER_KEY 9183921819809283910f
79
+ $ i_delete_my_tweets config store CONSUMER_SECRET 0293090239-2039209302-238392839
80
+ $ i_delete_my_tweets config store RT_THRESHOLD 2
81
+ $ i_delete_my_tweets config store FAVE_THRESHOLD 2
82
+ $ i_delete_my_tweets config store OLDER_THAN 2021-11-02
83
+ $ i_delete_my_tweets config store SCREEN_NAME mytwitterhandle
84
+ ```
85
+
86
+ IDMT can generate an `ACCESS_TOKEN` and `ACCESS_TOKEN_SECRET` for you using a PIN provided
87
+ that you do have a Twitter App setup and `CONSUMER_KEY` and `CONSUMER_SECRET`.
88
+
89
+ You can bypass most of the configuration by doing a
90
+
91
+ ```sh
92
+ $ i_delete_my_tweets config authorize_with_pin <consumer-key> <consumer-value>
93
+ ```
94
+
95
+ It will generate a URL that will take you to Twitter and issue a PIN. Then IDMT will
96
+ configure `ACCESS_TOKEN`, `ACCESS_TOKEN_SECRET`, and `SCREEN_NAME` for you.
97
+
98
+ At any point, you can check if all keys/values are good to go with
99
+
100
+ ```sh
101
+ $ i_delete_my_tweets config check
102
+ ```
103
+
104
+ #### Delete
105
+
106
+ ```sh
107
+ $ i_delete_my_tweets delete start
108
+ ```
109
+
110
+ Will start traversing the API for your tweets and applying the `OLDER_THAN`, `FAVE_THRESHOLD`, and `RT_THRESHOLD` rules (they are applied in this order). The rules are NOT combined. If the first one matches the data in the tweet, the tweet is skipped, next one.
111
+
112
+ Pass in `--dry-run=false` if you REALLY want to delete them otherwise this command will just output the tweets it would delete but doesn't because
113
+ the default flag for delete commands is `--dry-run=true`.
114
+
115
+ ```sh
116
+ $ i_delete_my_tweets delete from_csv
117
+ ```
118
+
119
+ Will use the tweets from the CSV file and not fetch the API for them. This is a nice option if you want to avoid some of the API rate limits and will be a little faster since it will not do the initial tweet-fetching over HTTP.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'semver'
7
+ require 'dotenv'
8
+ require 'csv'
9
+ require 'twitter'
10
+ require 'oauth'
11
+ require 'active_support/all'
12
+ require 'terminal-table'
13
+ require 'byebug'
14
+ require 'i_delete_my_tweets'
15
+
16
+ Dotenv.overload("~/.i_delete_my_tweets")
17
+ IDeleteMyTweets::CLI.start(ARGV)
@@ -0,0 +1,33 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'i_delete_my_tweets/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.add_dependency 'activesupport', '~> 6.1.5.1'
7
+ spec.add_dependency 'csv', '~> 3.0.9'
8
+ spec.add_dependency 'dotenv', '~> 2.7.6'
9
+ spec.add_dependency 'oauth', '~> 0.4'
10
+ spec.add_dependency 'terminal-table', '~> 3.0.2'
11
+ spec.add_dependency 'thor', '~> 1.2.1'
12
+ spec.add_dependency 'twitter', '~> 7.0.0'
13
+ spec.add_development_dependency 'bundler', '>= 1.0', '< 3'
14
+ spec.add_development_dependency 'rubocop', '~> 1.29'
15
+ spec.add_development_dependency 'rubocop-performance', '~> 1.13.3'
16
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.10.0'
17
+ spec.add_development_dependency 'semver2', '~> 3.4.2'
18
+ spec.authors = %w('Fabio Mont Alegre')
19
+ spec.platform = Gem::Platform::RUBY
20
+ spec.description = IDeleteMyTweets::Description
21
+ spec.email = %w(spiceee@gmail.com)
22
+ spec.homepage = 'https://github.com/spiceee/i_delete_my_tweets'
23
+ spec.licenses = %w(MIT)
24
+ spec.executables = %w(i_delete_my_tweets)
25
+ spec.name = 'i_delete_my_tweets'
26
+ spec.require_paths = %w(lib)
27
+ spec.required_ruby_version = '>= 2.6.5'
28
+ spec.required_rubygems_version = '>= 1.3.5'
29
+ spec.summary = 'A CLI to delete your tweets based on faves, RTs, and time.'
30
+ spec.files = Dir["*.md", "bin/*", "lib/**/*.rb"] + %w(.env.sample .env.test tweets.csv i_delete_my_tweets.gemspec)
31
+ spec.version = IDeleteMyTweets::Version
32
+ spec.metadata['rubygems_mfa_required'] = 'true'
33
+ end
@@ -0,0 +1,145 @@
1
+ module IDeleteMyTweets
2
+ class Api
3
+ include Presenter
4
+ attr_reader :config, :verbose, :log, :dry_run, :a_bit
5
+ attr_accessor :delete_count, :skipped_count, :not_found_count
6
+
7
+ def initialize(opts = {})
8
+ @config = opts[:config] || Config.new
9
+ @verbose, @log, @dry_run = opts[:verbose], opts[:logger], opts[:dry_run]
10
+ @delete_count, @skipped_count, @not_found_count = 0, 0, 0
11
+ @a_bit = opts[:a_bit] || 5
12
+ end
13
+
14
+ def traverse_api!
15
+ all_tweets.each do |tweet|
16
+ if can_be_destroyed?(tweet)
17
+ destroy_with_retry(tweet)
18
+ sleep a_bit
19
+ else
20
+ self.skipped_count += 1
21
+ end
22
+ end
23
+ rescue IOError
24
+ do_log(" 💥 Oops, there was a connection error! ", color: :red)
25
+ ensure
26
+ do_log summary delete_count, skipped_count, not_found_count, dry_run
27
+ end
28
+
29
+ def traverse_csv!
30
+ CSV.foreach(config.path_to_csv, headers: true) do |tweet|
31
+ if to_time(tweet["timestamp"]) >= config.older_than
32
+ self.skipped_count += 1
33
+ next
34
+ end
35
+
36
+ tw_data = fetch_tweet(tweet["tweet_id"])
37
+ next if tw_data.nil?
38
+
39
+ if can_be_destroyed?(tw_data)
40
+ destroy_with_retry(tw_data)
41
+ sleep a_bit
42
+ else
43
+ self.skipped_count += 1
44
+ end
45
+ end
46
+ end
47
+
48
+ def client
49
+ @client ||= Twitter::REST::Client.new do |c|
50
+ c.consumer_key = config.consumer_key
51
+ c.consumer_secret = config.consumer_secret
52
+ c.access_token = config.access_token
53
+ c.access_token_secret = config.access_token_secret
54
+ end
55
+ end
56
+
57
+ def verify_credentials
58
+ client.verify_credentials(skip_status: true)
59
+ do_log(" 🎉 We could verify your ❝#{config.screen_name}❞ credentials with Twitter and it looks good! ", color: :white, force_new_line: false)
60
+ true
61
+ rescue Twitter::Error => e
62
+ if e.is_a?(Twitter::Error::Forbidden)
63
+ do_log(" 🚫 Oops, Twitter cannot verify the crendentials for #{config.screen_name} ", color: :red)
64
+ else
65
+ do_log(" 🚫 Oops, something bad happened: #{e.message} ", color: :red)
66
+ end
67
+ false
68
+ end
69
+
70
+ private
71
+
72
+ def destroy_with_retry(tweet)
73
+ destroy_tweet!(tweet.id)
74
+ rescue HTTP::ConnectionError
75
+ do_log "💥 == trying again =="
76
+ destroy_tweet!(tweet.id)
77
+ rescue Twitter::Error::TooManyRequests => e
78
+ do_log "💥 == we've reached a rate limit, sleeping it off =="
79
+ sleep e.rate_limit.reset_in + 1
80
+ retry
81
+ ensure
82
+ do_log tweet_presenter(tweet, dry_run, verbose: verbose), force_new_line: verbose
83
+ end
84
+
85
+ def destroy_tweet!(id)
86
+ client.destroy_status(id) unless dry_run
87
+ self.delete_count += 1
88
+ end
89
+
90
+ def do_log(message, color: nil, force_new_line: true)
91
+ log&.call("", nil, true) if force_new_line
92
+ log&.call(message, color || COLORS.sample, force_new_line)
93
+ end
94
+
95
+ def fetch_tweet(id)
96
+ client.status(id)
97
+ rescue Twitter::Error::NotFound
98
+ do_log tweet_not_found(id, dry_run, verbose: verbose)
99
+ self.not_found_count += 1
100
+ nil
101
+ end
102
+
103
+ def satisfies_older_than?(tweet)
104
+ tweet.created_at < config.older_than
105
+ end
106
+
107
+ def bellow_fave_threshold?(tweet)
108
+ return true if config.fave_threshold.to_i == 0
109
+ return true unless tweet.favorite_count > 0
110
+
111
+ tweet.favorite_count < config.fave_threshold.to_i
112
+ end
113
+
114
+ def bellow_rt_threshold?(tweet)
115
+ return true if config.rt_threshold.to_i == 0
116
+ return true unless tweet.retweet_count > 0
117
+
118
+ tweet.retweet_count < config.rt_threshold.to_i
119
+ end
120
+
121
+ def can_be_destroyed?(tweet)
122
+ return false unless satisfies_older_than? tweet
123
+ return false unless bellow_fave_threshold? tweet
124
+ return false unless bellow_rt_threshold? tweet
125
+
126
+ true
127
+ end
128
+
129
+ def collect_with_max_id(collection = [], max_id = nil, &block)
130
+ response = yield(max_id)
131
+ collection += response
132
+ response.empty? ? collection.flatten : collect_with_max_id(collection, response.last.id - 1, &block)
133
+ end
134
+
135
+ def all_tweets
136
+ @all_tweets ||= collect_with_max_id do |max_id|
137
+ options = {count: 200, include_rts: true}
138
+ options[:max_id] = max_id unless max_id.nil?
139
+ client.user_timeline(config.screen_name, options)
140
+ end
141
+ rescue HTTP::ConnectionError
142
+ do_log(" 💥 Oops, there was a connection error fetching your tweets! ", color: :red)
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,52 @@
1
+ module IDeleteMyTweets
2
+ module Ascii
3
+ def show_face
4
+ <<~'FACE'
5
+
6
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
7
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
8
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#BPJ!~~~^^^^!7?JYPB&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
9
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&BP?~:.. ... .:!P@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
10
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@P~. ........ ... :5&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
11
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@G. .:P@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
12
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@7 .~P&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
13
+ @@@@@@@@@@@@@@@@@@@@@@@@@&: ..::^~!!!77777???7!~^:.. .?@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
14
+ @@@@@@@@@@@@@@@@@@@@@@@@&. .:~!?JY5PPPGGGGGGGGGGGPP55Y?!~. 7@@@@@@@@@@@@@@@@@@@@@@@@@@@@
15
+ @@@@@@@@@@@@@@@@@@@@@@@@7 .^!?JY5PPPGGGGGBBBGGGGGGPPPPP555Y?~. ?@@@@@@@@@@@@@@@@@@@@@@@@@@@
16
+ @@@@@@@@@@@@@@@@@@@@@@@@! .!?JYY5PPPGGGGBBBBBBBBBGGGGPP55555YYJ~. ^P&@@@@@@@@@@@@@@@@@@@@@@@@
17
+ @@@@@@@@@@@@@@@@@@@@@@@@: ^!7JYYY5PPGGGBBB#BBBBBBBGGGGPPP55YYYYYJ~.. ... .G@@@@@@@@@@@@@@@@@@@@@@@
18
+ @@@@@@@@@@@@@@@@@@@@@@@P ^7?JYY55PPGGGGBBBBBBBBBBBGGGGGPPP5YYYYYJ7^..... J@@@@@@@@@@@@@@@@@@@@@@
19
+ @@@@@@@@@@@@@@@@@@@@@@@: ^7?YYY5PPPGGGGBBBBBBBBBBBBGGGGGPPP555YYJ?!^..... G@@@@@@@@@@@@@@@@@@@@@
20
+ @@@@@@@@@@@@@@@@@@@@@@# :!7?JJY5PGGBBBBBBBBBBBBBBBBBBGGGGGPP555YJ7~^.. 7@@@@@@@@@@@@@@@@@@@@@
21
+ @@@@@@@@@@@@@@@@@@@@@@G ~~~~^^^^7JPGGBBB###BB###BGPYJY5PGGPPP555J?~:. !@@@@@@@@@@@@@@@@@@@@@
22
+ @@@@@@@@@@@@@@@@@@@@@@B.~~~7?JJ?77?Y5PPGGBBGGGP5?7~~~~~!?Y55555YYY?^. ~@@@@@@@@@@@@@@@@@@@@@
23
+ @@@@@@@@@@@@@@@@@@@@@@&:!!!77?JY555??J5PPPPPP5YY5PPPP55YJ?7?YY5YYJYJ~. .B@@@@@@@@@@@@@@@@@@@@@
24
+ @@@@@@@@@@@@@@@@@@@@@@&^77!~^^.^?Y??77YGGGP5YJYJJJJJY55Y55Y?JYYYYYJJ!. .#@@@@@@@@@@@@@@@@@@@@@@
25
+ @@@@@@@@@@@@@@@@@@@@@@#~???7?JJY55JYJ7YPPP5YJJ77?^^^7!7JJJYYYYYYYJ??~. ~&@@@@@@@@@@@@@@@@@@@@@@@
26
+ @@@@@@@@@@@@@@@@@@@@@@B7JJYYJJJY5555??Y555JY5Y??J5555J??J55555YYJ??7^... ~@@@@@@@@@@@@@@@@@@@@@@@@@
27
+ @@@@@@@@@@@@@@@@@@@@@@57?JY55PPGGGPJ7J555YYY5PP5YYY555PPPGPP55J?7??~:...^^^7@@@@@@@@@@@@@@@@@@@@@@@@
28
+ @@@@@@@@@@@@@@@@@@@@@&77?Y55PGGGGPJ7?YP5YJJYPGGGGGGGGGGGGPPP5Y?7777^..:!?!~7@@@@@@@@@@@@@@@@@@@@@@@@
29
+ @@@@@@@@@@@@@@@@@@@@@G~7J555PPPP5J??YGGPY??Y55PGBBBBBGGP5YYYYJ7!!7!::^:7J??B@@@@@@@@@@@@@@@@@@@@@@@@
30
+ @@@@@@@@@@@@@@@@@@@@@Y~7?YY55YYYJ!^!JPPY??Y5YJ55PGBBBBGGP5YYJY7!!!^^!!:75?Y@@@@@@@@@@@@@@@@@@@@@@@@@
31
+ @@@@@@@@@@@@@@@@@@@@@Y^~7?J?7777?!~^^^!^:^~!7?55YYPGGGGPP5YJJ?7!!~~7?!!Y57&@@@@@@@@@@@@@@@@@@@@@@@@@
32
+ @@@@@@@@@@@@@@@@@@@@@B^^~!!!!7777???!~^~7JYYYY5YY??Y5555YJJ?77!!~!!77?JJ?&@@@@@@@@@@@@@@@@@@@@@@@@@@
33
+ @@@@@@@@@@@@@@@@@@@@@&!~!???!~!7??JY5PPPPPPP555Y5Y?!!???77777!!~!!!7YYYP&@@@@@@@@@@@@@@@@@@@@@@@@@@@
34
+ @@@@@@@@@@@@@@@@@@@@@@Y~!YY7~^..:~7!?7??!!77?7??JJJ?7!7!!!!777!~!!!YY5&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
35
+ @@@@@@@@@@@@@@@@@@@@@@&!!?P5J7: :?PB#BB#PG5Y?~:..:!7JYYJ?7?777!~~^JB#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
36
+ @@@@@@@@@@@@@@@@@@@@@@@P~!YGPY7^....::^~^^:::. :?5YYP5Y?J??7!!~^7@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
37
+ @@@@@@@@@@@@@@@@@@@@@@@@!^!5PY7~!!!~^^:::^^^~~!J5PPPP5J?JJ7!!~~^^&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
38
+ @@@@@@@@@@@@@@@@@@@@@@@@&!~75Y?~~!?JYP5YYJJJ??Y5PPPP5?7??7!~~^:~#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
39
+ @@@@@@@@@@@@@@@@@@@@@@@@@&!~YYJ7~!!77???JJ???J5P5PP5??J?7!~~::J@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
40
+ @@@@@@@@@@@@@@@@@@@@@@@@@@#^!YYYJ77777????JJY5PPPGPJ?J?7~^::~B@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
41
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@B.!Y5P5YY5555PPPPPGGPG5?!77~^:.~G@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
42
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@#^^7YY55PPPPPGGPGP5J7~^^^:..^G@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
43
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@J::~!?JYYYYYJ??7!:.....:7B@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
44
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&Y^...::::.... ..:!Y#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
45
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&B5?~^^^^~!?5B&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
46
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
47
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
48
+
49
+ FACE
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,50 @@
1
+ require "oauth"
2
+
3
+ module IDeleteMyTweets
4
+ module Auth
5
+ def consumer(client)
6
+ OAuth::Consumer.new(
7
+ client.consumer_key,
8
+ client.consumer_secret,
9
+ site: Twitter::REST::Request::BASE_URL,
10
+ )
11
+ end
12
+
13
+ def pin_auth_parameters
14
+ {oauth_callback: "oob"}
15
+ end
16
+
17
+ def get_request_token(client)
18
+ consumer(client).get_request_token
19
+ rescue StandardError => e
20
+ say_error set_color " 🚫 Oops, something bad happened: #{e.message} ", :white, :on_red, :bold
21
+ end
22
+
23
+ def get_access_credentials(request_token, pin)
24
+ access_token = request_token.get_access_token(oauth_verifier: pin.chomp)
25
+ {oauth_token: access_token.token,
26
+ oauth_token_secret: access_token.secret,
27
+ screen_name: access_token.params[:screen_name]}
28
+ rescue StandardError => e
29
+ say_error set_color " 🚫 Oops, something bad happened: #{e.message} ", :white, :on_red, :bold
30
+ end
31
+
32
+ def generate_authorize_url(client, request_token)
33
+ oauth = consumer(client)
34
+ request = oauth.create_signed_request(:get, oauth.authorize_path, request_token, pin_auth_parameters)
35
+ build_path(request, build_headers(request))
36
+ end
37
+
38
+ def build_path(request, params)
39
+ "#{Twitter::REST::Request::BASE_URL}#{request.path}?#{params}"
40
+ end
41
+
42
+ def build_headers(request)
43
+ request["Authorization"].sub(/^OAuth\s+/, "").split(/,\s+/).map { |p|
44
+ k, v = p.split("=")
45
+ v =~ /"(.*?)"/
46
+ "#{k}=#{CGI.escape($1)}"
47
+ }.join("&")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,138 @@
1
+ require 'thor'
2
+
3
+ module IDeleteMyTweets
4
+ class CommandDelete < Thor
5
+ class_option :dry_run, type: :boolean, default: true
6
+
7
+ include Ascii
8
+ desc "start", "Starts the deleting process"
9
+ def start
10
+ say show_face, :yellow if options[:verbose]
11
+ say set_color api.config.to_table, :green, :bold
12
+ api.traverse_api!
13
+ end
14
+
15
+ desc "from_csv", "Starts the deleting process using a csv file"
16
+ def from_csv
17
+ say show_face, :yellow if options[:verbose]
18
+ say set_color api.config.to_table, :green, :bold
19
+ api.traverse_csv!
20
+ end
21
+
22
+ private
23
+
24
+ def api
25
+ @api ||= IDeleteMyTweets::Api.new(verbose: options[:verbose], logger: method(:say), dry_run: options[:dry_run])
26
+ end
27
+ end
28
+
29
+ class CommandConfig < Thor
30
+ include Thor::Actions
31
+ include Auth
32
+ class_option :dry_run, type: :boolean, default: false
33
+
34
+ desc "check", "Checks if the configuration is ok"
35
+ def check
36
+ bad_configs = cli_config.empty_values
37
+
38
+ if bad_configs.empty?
39
+ api = IDeleteMyTweets::Api.new(logger: method(:say))
40
+ api.verify_credentials
41
+ say set_color " ✅ You're all set! ", :white, :on_green, :bold
42
+ say set_color cli_config.to_table, :green
43
+ else
44
+ say_error set_color " 🚫 Oops, #{bad_configs.join(', ')} #{bad_configs.size == 1 ? 'is' : 'are'} missing! ", :white, :on_red, :bold
45
+ end
46
+ end
47
+
48
+ desc "store <key> <value>", "Stores the configuration <key> with value <value>"
49
+ def store(key, value)
50
+ raise Error, "ERROR: #{key} does not exist" unless CONFIG_VARS.include?(key.strip)
51
+
52
+ conf = cli_config
53
+ conf.send("#{key.downcase.to_sym}=", value);
54
+
55
+ if options[:dry_run]
56
+ say set_color conf.to_table, :green, :bold
57
+ else
58
+ dump_to_file!(conf)
59
+ end
60
+ end
61
+
62
+ desc "authorize_with_pin <consumer-key> <consumer-secret>", "Generates a URL to auth the app"
63
+ map %w[-a --authorize-with-pin] => :authorize_with_pin
64
+ def authorize_with_pin(consumer_key, consumer_secret)
65
+ conf = cli_config
66
+ conf.consumer_key, conf.consumer_secret = consumer_key, consumer_secret
67
+ api = IDeleteMyTweets::Api.new(config: conf)
68
+
69
+ request_token = get_request_token(api.client)
70
+ auth_url = generate_authorize_url(api.client, request_token)
71
+
72
+ pin = ask set_color interactive_pin_message(auth_url), :green, :bold
73
+ credentials = get_access_credentials(request_token, pin)
74
+ update_and_rewrite_conf!(conf, credentials)
75
+ end
76
+
77
+ private
78
+
79
+ def update_and_rewrite_conf!(config, new_creds)
80
+ config.access_token, config.access_token_secret = new_creds[:oauth_token], new_creds[:oauth_token_secret]
81
+ config.screen_name = new_creds[:screen_name]
82
+
83
+ if options[:dry_run]
84
+ say set_color config.to_table, :green, :bold
85
+ else
86
+ dump_to_file!(config)
87
+ end
88
+ end
89
+
90
+ def interactive_pin_message(auth_url)
91
+ <<~MESSAGE
92
+ 👋 Open this URL in a browser, preferably the one you're logged into the
93
+ Twitter account you want to delete tweets from:
94
+
95
+ ✂️------------------------------------------------------------------
96
+
97
+ #{auth_url}
98
+
99
+ ------------------------------------------------------------------✂️
100
+
101
+ Then copy and paste below the PIN you'll receive once you've authorized the
102
+ Twitter app you've set up for this ⬇️
103
+ MESSAGE
104
+ end
105
+
106
+ def dump_to_file!(conf)
107
+ path = File.expand_path(PATH_TO_ENV)
108
+ File.exist?(path) ? comment_lines(path, /^[A-Z]/) : create_file(PATH_TO_ENV)
109
+
110
+ insert_into_file(path) { ["\n", conf.to_env].join("\n") }
111
+ end
112
+
113
+ def cli_config
114
+ @cli_config ||= IDeleteMyTweets::Config.new
115
+ end
116
+ end
117
+
118
+ class CLI < Thor
119
+ check_unknown_options!
120
+ class_option :verbose, type: :boolean, default: true
121
+
122
+ def self.exit_on_failure?
123
+ true
124
+ end
125
+
126
+ desc "version", "Displays version"
127
+ map %w[-v --version] => :version
128
+ def version
129
+ say Gem.loaded_specs["i_delete_my_tweets"]&.version || Version
130
+ end
131
+
132
+ desc "delete command ...ARGS", "Deletes your tweets based on time, faves and RTs"
133
+ subcommand "delete", CommandDelete
134
+
135
+ desc "config command ...ARGS", "Configures the Twitter app credentials"
136
+ subcommand "config", CommandConfig
137
+ end
138
+ end
@@ -0,0 +1,76 @@
1
+ require 'thor'
2
+
3
+ module IDeleteMyTweets
4
+ CONFIG_VARS = [
5
+ "CONSUMER_KEY",
6
+ "CONSUMER_SECRET",
7
+ "ACCESS_TOKEN",
8
+ "ACCESS_TOKEN_SECRET",
9
+ "OLDER_THAN",
10
+ "PATH_TO_CSV",
11
+ "FAVE_THRESHOLD",
12
+ "RT_THRESHOLD",
13
+ "SCREEN_NAME"
14
+ ].freeze
15
+ PATH_TO_ENV = "~/.i_delete_my_tweets".freeze
16
+ OBFUSCATE_WORDS = %w(secret token key).freeze
17
+
18
+ class Config
19
+ attr_accessor :consumer_key,
20
+ :consumer_secret,
21
+ :access_token,
22
+ :access_token_secret,
23
+ :older_than,
24
+ :path_to_csv,
25
+ :fave_threshold,
26
+ :rt_threshold,
27
+ :screen_name
28
+
29
+ def initialize(opts = {})
30
+ @consumer_key = opts[:consumer_key] || ENV.fetch("CONSUMER_KEY", nil)
31
+ @consumer_secret = opts[:consumer_secret] || ENV.fetch("CONSUMER_SECRET", nil)
32
+ @access_token = opts[:access_token] || ENV.fetch("ACCESS_TOKEN", nil)
33
+ @access_token_secret = opts[:access_token_secret] || ENV.fetch("ACCESS_TOKEN_SECRET", nil)
34
+ @older_than = opts[:older_than] || Time.parse(ENV.fetch("OLDER_THAN", 1.week.ago).to_s)
35
+ @path_to_csv = opts[:path_to_csv] || ENV.fetch("PATH_TO_CSV", "./")
36
+ @fave_threshold = opts[:fave_threshold] || ENV.fetch("FAVE_THRESHOLD", 0)
37
+ @rt_threshold = opts[:rt_threshold] || ENV.fetch("RT_THRESHOLD", 0)
38
+ @screen_name = opts[:screen_name] || ENV.fetch("SCREEN_NAME", "")
39
+ end
40
+
41
+ def zipped
42
+ values = CONFIG_VARS.map do |k|
43
+ normalized_key = k.downcase
44
+ value = send(normalized_key.to_sym)
45
+ OBFUSCATE_WORDS.any? { |word| normalized_key.include?(word) } ? obfuscate_token(value) : value
46
+ end
47
+ CONFIG_VARS.zip(values)
48
+ end
49
+
50
+ def to_table
51
+ Terminal::Table.new do |table|
52
+ table.title = "[I Delete My Tweets] Configuration"
53
+ table.headings = ["KEY", "VALUE"]
54
+ table.rows = zipped
55
+ table.style = Presenter::TABLE_STYLE
56
+ end
57
+ end
58
+
59
+ def to_env
60
+ CONFIG_VARS.map { |k| "#{k}='#{send(k.downcase.to_sym).to_s.gsub("'"){ "\\'" }}'" }
61
+ # ^ Escaping with single quotes because some shells are not nice with ruby unquoted timestamps
62
+ end
63
+
64
+ def empty_values
65
+ zipped.map { |tuples| tuples.second.to_s.empty? ? tuples.first : nil }.compact
66
+ end
67
+
68
+ private
69
+
70
+ def obfuscate_token(token)
71
+ return if token.nil?
72
+
73
+ token.size > 3 ? token.gsub(/^.+(.{3,})$/m, '*********************\1') : "***"
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,61 @@
1
+ module IDeleteMyTweets
2
+ module Presenter
3
+ COLORS = [
4
+ :red,
5
+ :green,
6
+ :yellow,
7
+ :blue,
8
+ :magenta,
9
+ :cyan
10
+ ].freeze
11
+
12
+ TABLE_STYLE = {border: Terminal::Table::UnicodeRoundBorder.new}.freeze
13
+
14
+ def summary(delete_count, skipped_count, not_found_count, dry_run)
15
+ Terminal::Table.new do |table|
16
+ table.title = "Summary"
17
+ table.headings = ["Deleted", "Skipped", "Not Found", "Dry Run?"]
18
+ table.rows = [[delete_count, skipped_count, not_found_count, dry_run.present?]]
19
+ table.style = TABLE_STYLE
20
+ end
21
+ end
22
+
23
+ def tweet_presenter(tweet, dry_run, verbose: true)
24
+ if verbose
25
+ Terminal::Table.new do |table|
26
+ table.title = "🐤 Deleted Tweet 🚽"
27
+ table.headings = ["Text", "Date", "Faves", "RTs", "Dry Run?"]
28
+ table.rows = [[truncate(tweet.text), to_human_time(tweet.created_at), tweet.favorite_count, tweet.retweet_count, dry_run.present?]]
29
+ table.style = TABLE_STYLE
30
+ end
31
+ else
32
+ ".🐤 "
33
+ end
34
+ end
35
+
36
+ def tweet_not_found(tweet_id, dry_run, verbose: true)
37
+ if verbose
38
+ Terminal::Table.new do |table|
39
+ table.title = "💥 Tweet Not Found 💥"
40
+ table.headings = ["ID", "Dry Run?"]
41
+ table.rows = [[tweet_id, dry_run.present?]]
42
+ table.style = TABLE_STYLE
43
+ end
44
+ else
45
+ ".💥 "
46
+ end
47
+ end
48
+
49
+ def truncate(text)
50
+ text.gsub(/^(.{40,}?).*$/m, '\1...')
51
+ end
52
+
53
+ def to_time(timestamp)
54
+ Date.parse timestamp
55
+ end
56
+
57
+ def to_human_time(time)
58
+ time.strftime("%Y-%m-%d %H:%M")
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,71 @@
1
+ require 'semver'
2
+
3
+ module IDeleteMyTweets
4
+ module Version
5
+ module_function
6
+
7
+ def parsed_version
8
+ @parsed_version ||= SemVer.find
9
+ end
10
+
11
+ # @return [Integer]
12
+ def major
13
+ parsed_version.major
14
+ end
15
+
16
+ # @return [Integer]
17
+ def minor
18
+ parsed_version.minor
19
+ end
20
+
21
+ # @return [Integer]
22
+ def patch
23
+ parsed_version.patch
24
+ end
25
+
26
+ # @return [Integer, NilClass]
27
+ def pre
28
+ parsed_version.prerelease.empty? ? nil : parsed_version.prerelease
29
+ end
30
+
31
+ # @return [Hash]
32
+ def to_h
33
+ {
34
+ major: major,
35
+ minor: minor,
36
+ patch: patch,
37
+ pre: pre,
38
+ }
39
+ end
40
+
41
+ # @return [Array]
42
+ def to_a
43
+ [major, minor, patch, pre].compact
44
+ end
45
+
46
+ # @return [String]
47
+ def to_s
48
+ to_a.join('.')
49
+ end
50
+ end
51
+
52
+ module Description
53
+ module_function
54
+
55
+ def to_s
56
+ <<~'DESCRIPTION'
57
+ A CLI (as in Command Line Interface) to delete your tweets based on faves, RTs, and time.
58
+
59
+ There are some services out there with a friendly web interface, but this is not one of them. You must know the basics of working with a UNIX terminal and configuring a Twitter API app, as this will only work if you have a Twitter Developer account.
60
+
61
+ Due to the irrevocable nature of tweet deletion, all delete commands are dry-run true, meaning you must call all of them with a --dry-run=false flag if you want them to really do something.
62
+
63
+ Called with --dry-run=false, there is no way to revoke tweet deletion. They are just gone, disappeared into the ether (or the stashed in the Twitter-owned secret place you have no access to without a mandate since nothing gets really deleted from the web these days, folks).
64
+
65
+ This tool won't delete all of your tweets in one fell swoop; it is more of a way to delete your old tweets from time to time. The Twitter API rate limits are relatively complicated, and I don't even wanna go there, but if you do intend on deleting all of your tweets, you can do it with this CLI and some perseverance. I did delete more than 100k of mine by using this script every day for a couple of weeks. The more tweets you delete, the fewer of them you have, and with time the rate limits won't be that much of a problem.
66
+
67
+ I Delete My Tweets (IDMT) can delete your tweets by fetching them via API using an APP you will have to set up yourself. Still, it can also delete tweets from an CSV (comma-separated file) that you can generate from the archive you can request from twitter.com by going to Settings and privacy > Your Account > Download an archive of your data. It is out of the scope of this CLI to generate the CSV (at the moment) but there are scripts out there that can do this for you.
68
+ DESCRIPTION
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'i_delete_my_tweets/version'
2
+ require_relative 'i_delete_my_tweets/config'
3
+ require_relative 'i_delete_my_tweets/presenter'
4
+ require_relative 'i_delete_my_tweets/api'
5
+ require_relative 'i_delete_my_tweets/ascii'
6
+ require_relative 'i_delete_my_tweets/auth'
7
+ require_relative 'i_delete_my_tweets/cli'
data/tweets.csv ADDED
@@ -0,0 +1 @@
1
+ "tweet_id","in_reply_to_status_id","in_reply_to_user_id","timestamp","source","text","retweeted_status_id","retweeted_status_user_id","retweeted_status_timestamp","expanded_urls"
metadata ADDED
@@ -0,0 +1,246 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: i_delete_my_tweets
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - "'Fabio"
8
+ - Mont
9
+ - Alegre'
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2022-05-11 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: 6.1.5.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: 6.1.5.1
29
+ - !ruby/object:Gem::Dependency
30
+ name: csv
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: 3.0.9
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: 3.0.9
43
+ - !ruby/object:Gem::Dependency
44
+ name: dotenv
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: 2.7.6
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: 2.7.6
57
+ - !ruby/object:Gem::Dependency
58
+ name: oauth
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '0.4'
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '0.4'
71
+ - !ruby/object:Gem::Dependency
72
+ name: terminal-table
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: 3.0.2
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: 3.0.2
85
+ - !ruby/object:Gem::Dependency
86
+ name: thor
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: 1.2.1
92
+ type: :runtime
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: 1.2.1
99
+ - !ruby/object:Gem::Dependency
100
+ name: twitter
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: 7.0.0
106
+ type: :runtime
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: 7.0.0
113
+ - !ruby/object:Gem::Dependency
114
+ name: bundler
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '1.0'
120
+ - - "<"
121
+ - !ruby/object:Gem::Version
122
+ version: '3'
123
+ type: :development
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '1.0'
130
+ - - "<"
131
+ - !ruby/object:Gem::Version
132
+ version: '3'
133
+ - !ruby/object:Gem::Dependency
134
+ name: rubocop
135
+ requirement: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '1.29'
140
+ type: :development
141
+ prerelease: false
142
+ version_requirements: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '1.29'
147
+ - !ruby/object:Gem::Dependency
148
+ name: rubocop-performance
149
+ requirement: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: 1.13.3
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: 1.13.3
161
+ - !ruby/object:Gem::Dependency
162
+ name: rubocop-rspec
163
+ requirement: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: 2.10.0
168
+ type: :development
169
+ prerelease: false
170
+ version_requirements: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: 2.10.0
175
+ - !ruby/object:Gem::Dependency
176
+ name: semver2
177
+ requirement: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - "~>"
180
+ - !ruby/object:Gem::Version
181
+ version: 3.4.2
182
+ type: :development
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - "~>"
187
+ - !ruby/object:Gem::Version
188
+ version: 3.4.2
189
+ description: |
190
+ A CLI (as in Command Line Interface) to delete your tweets based on faves, RTs, and time.
191
+
192
+ There are some services out there with a friendly web interface, but this is not one of them. You must know the basics of working with a UNIX terminal and configuring a Twitter API app, as this will only work if you have a Twitter Developer account.
193
+
194
+ Due to the irrevocable nature of tweet deletion, all delete commands are dry-run true, meaning you must call all of them with a --dry-run=false flag if you want them to really do something.
195
+
196
+ Called with --dry-run=false, there is no way to revoke tweet deletion. They are just gone, disappeared into the ether (or the stashed in the Twitter-owned secret place you have no access to without a mandate since nothing gets really deleted from the web these days, folks).
197
+
198
+ This tool won't delete all of your tweets in one fell swoop; it is more of a way to delete your old tweets from time to time. The Twitter API rate limits are relatively complicated, and I don't even wanna go there, but if you do intend on deleting all of your tweets, you can do it with this CLI and some perseverance. I did delete more than 100k of mine by using this script every day for a couple of weeks. The more tweets you delete, the fewer of them you have, and with time the rate limits won't be that much of a problem.
199
+
200
+ I Delete My Tweets (IDMT) can delete your tweets by fetching them via API using an APP you will have to set up yourself. Still, it can also delete tweets from an CSV (comma-separated file) that you can generate from the archive you can request from twitter.com by going to Settings and privacy > Your Account > Download an archive of your data. It is out of the scope of this CLI to generate the CSV (at the moment) but there are scripts out there that can do this for you.
201
+ email:
202
+ - spiceee@gmail.com
203
+ executables:
204
+ - i_delete_my_tweets
205
+ extensions: []
206
+ extra_rdoc_files: []
207
+ files:
208
+ - ".env.sample"
209
+ - ".env.test"
210
+ - README.md
211
+ - bin/i_delete_my_tweets
212
+ - i_delete_my_tweets.gemspec
213
+ - lib/i_delete_my_tweets.rb
214
+ - lib/i_delete_my_tweets/api.rb
215
+ - lib/i_delete_my_tweets/ascii.rb
216
+ - lib/i_delete_my_tweets/auth.rb
217
+ - lib/i_delete_my_tweets/cli.rb
218
+ - lib/i_delete_my_tweets/config.rb
219
+ - lib/i_delete_my_tweets/presenter.rb
220
+ - lib/i_delete_my_tweets/version.rb
221
+ - tweets.csv
222
+ homepage: https://github.com/spiceee/i_delete_my_tweets
223
+ licenses:
224
+ - MIT
225
+ metadata:
226
+ rubygems_mfa_required: 'true'
227
+ post_install_message:
228
+ rdoc_options: []
229
+ require_paths:
230
+ - lib
231
+ required_ruby_version: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: 2.6.5
236
+ required_rubygems_version: !ruby/object:Gem::Requirement
237
+ requirements:
238
+ - - ">="
239
+ - !ruby/object:Gem::Version
240
+ version: 1.3.5
241
+ requirements: []
242
+ rubygems_version: 3.3.13
243
+ signing_key:
244
+ specification_version: 4
245
+ summary: A CLI to delete your tweets based on faves, RTs, and time.
246
+ test_files: []