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 +7 -0
- data/.env.sample +9 -0
- data/.env.test +9 -0
- data/README.md +119 -0
- data/bin/i_delete_my_tweets +17 -0
- data/i_delete_my_tweets.gemspec +33 -0
- data/lib/i_delete_my_tweets/api.rb +145 -0
- data/lib/i_delete_my_tweets/ascii.rb +52 -0
- data/lib/i_delete_my_tweets/auth.rb +50 -0
- data/lib/i_delete_my_tweets/cli.rb +138 -0
- data/lib/i_delete_my_tweets/config.rb +76 -0
- data/lib/i_delete_my_tweets/presenter.rb +61 -0
- data/lib/i_delete_my_tweets/version.rb +71 -0
- data/lib/i_delete_my_tweets.rb +7 -0
- data/tweets.csv +1 -0
- metadata +246 -0
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
data/.env.test
ADDED
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: []
|