t 3.1.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/LICENSE.md +1 -1
- data/README.md +8 -9
- data/bin/t +21 -21
- data/lib/t/cli.rb +470 -463
- data/lib/t/collectable.rb +9 -8
- data/lib/t/core_ext/kernel.rb +3 -3
- data/lib/t/core_ext/string.rb +2 -2
- data/lib/t/delete.rb +44 -41
- data/lib/t/editor.rb +5 -5
- data/lib/t/identicon.rb +1 -1
- data/lib/t/list.rb +51 -51
- data/lib/t/printable.rb +69 -65
- data/lib/t/rcfile.rb +18 -17
- data/lib/t/requestable.rb +3 -2
- data/lib/t/search.rb +82 -82
- data/lib/t/set.rb +21 -21
- data/lib/t/stream.rb +67 -60
- data/lib/t/utils.rb +25 -25
- data/lib/t/version.rb +2 -2
- data/lib/t.rb +2 -2
- data/t.gemspec +22 -21
- metadata +29 -49
data/lib/t/printable.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module T
|
2
|
-
module Printable # rubocop:disable ModuleLength
|
3
|
-
LIST_HEADINGS = [
|
4
|
-
TWEET_HEADINGS = [
|
5
|
-
USER_HEADINGS = [
|
2
|
+
module Printable # rubocop:disable Metrics/ModuleLength
|
3
|
+
LIST_HEADINGS = ["ID", "Created at", "Screen name", "Slug", "Members", "Subscribers", "Mode", "Description"].freeze
|
4
|
+
TWEET_HEADINGS = ["ID", "Posted at", "Screen name", "Text"].freeze
|
5
|
+
USER_HEADINGS = ["ID", "Since", "Last tweeted at", "Tweets", "Favorites", "Listed", "Following", "Followers", "Screen name", "Name", "Verified", "Protected", "Bio", "Status", "Location", "URL"].freeze
|
6
6
|
MONTH_IN_SECONDS = 2_592_000
|
7
7
|
|
8
8
|
private
|
@@ -12,73 +12,75 @@ module T
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def build_long_tweet(tweet)
|
15
|
-
[tweet.id, ls_formatted_time(tweet), "@#{tweet.user.screen_name}", decode_full_text(tweet, options[
|
15
|
+
[tweet.id, ls_formatted_time(tweet), "@#{tweet.user.screen_name}", decode_full_text(tweet, options["decode_uris"]).gsub(/\n+/, " ")]
|
16
16
|
end
|
17
17
|
|
18
18
|
def build_long_user(user)
|
19
|
-
[user.id, ls_formatted_time(user), ls_formatted_time(user.status), user.statuses_count, user.favorites_count, user.listed_count, user.friends_count, user.followers_count, "@#{user.screen_name}", user.name, user.verified? ?
|
19
|
+
[user.id, ls_formatted_time(user), ls_formatted_time(user.status), user.statuses_count, user.favorites_count, user.listed_count, user.friends_count, user.followers_count, "@#{user.screen_name}", user.name, user.verified? ? "Yes" : "No", user.protected? ? "Yes" : "No", user.description.gsub(/\n+/, " "), user.status? ? decode_full_text(user.status, options["decode_uris"]).gsub(/\n+/, " ") : nil, user.location, user.website.to_s]
|
20
20
|
end
|
21
21
|
|
22
22
|
def csv_formatted_time(object, key = :created_at)
|
23
23
|
return nil if object.nil?
|
24
|
+
|
24
25
|
time = object.send(key.to_sym).dup
|
25
|
-
time.utc.strftime(
|
26
|
+
time.utc.strftime("%Y-%m-%d %H:%M:%S %z")
|
26
27
|
end
|
27
28
|
|
28
29
|
def ls_formatted_time(object, key = :created_at, allow_relative = true)
|
29
|
-
return
|
30
|
+
return "" if object.nil?
|
31
|
+
|
30
32
|
time = T.local_time(object.send(key.to_sym))
|
31
|
-
if allow_relative && options[
|
32
|
-
distance_of_time_in_words(time)
|
33
|
-
elsif time > Time.now - MONTH_IN_SECONDS * 6
|
34
|
-
time.strftime(
|
33
|
+
if allow_relative && options["relative_dates"]
|
34
|
+
"#{distance_of_time_in_words(time)} ago"
|
35
|
+
elsif time > Time.now - (MONTH_IN_SECONDS * 6)
|
36
|
+
time.strftime("%b %e %H:%M")
|
35
37
|
else
|
36
|
-
time.strftime(
|
38
|
+
time.strftime("%b %e %Y")
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
40
42
|
def print_csv_list(list)
|
41
|
-
require
|
43
|
+
require "csv"
|
42
44
|
say [list.id, csv_formatted_time(list), list.user.screen_name, list.slug, list.member_count, list.subscriber_count, list.mode, list.description].to_csv
|
43
45
|
end
|
44
46
|
|
45
47
|
def print_csv_tweet(tweet)
|
46
|
-
require
|
47
|
-
say [tweet.id, csv_formatted_time(tweet), tweet.user.screen_name, decode_full_text(tweet, options[
|
48
|
+
require "csv"
|
49
|
+
say [tweet.id, csv_formatted_time(tweet), tweet.user.screen_name, decode_full_text(tweet, options["decode_uris"])].to_csv
|
48
50
|
end
|
49
51
|
|
50
52
|
def print_csv_user(user)
|
51
|
-
require
|
53
|
+
require "csv"
|
52
54
|
say [user.id, csv_formatted_time(user), csv_formatted_time(user.status), user.statuses_count, user.favorites_count, user.listed_count, user.friends_count, user.followers_count, user.screen_name, user.name, user.verified?, user.protected?, user.description, user.status? ? user.status.full_text : nil, user.location, user.website].to_csv
|
53
55
|
end
|
54
56
|
|
55
57
|
def print_lists(lists)
|
56
|
-
unless options[
|
57
|
-
lists = case options[
|
58
|
-
when
|
58
|
+
unless options["unsorted"]
|
59
|
+
lists = case options["sort"]
|
60
|
+
when "members"
|
59
61
|
lists.sort_by(&:member_count)
|
60
|
-
when
|
62
|
+
when "mode"
|
61
63
|
lists.sort_by(&:mode)
|
62
|
-
when
|
64
|
+
when "since"
|
63
65
|
lists.sort_by(&:created_at)
|
64
|
-
when
|
66
|
+
when "subscribers"
|
65
67
|
lists.sort_by(&:subscriber_count)
|
66
68
|
else
|
67
69
|
lists.sort_by { |list| list.slug.downcase }
|
68
70
|
end
|
69
71
|
end
|
70
|
-
lists.reverse! if options[
|
71
|
-
if options[
|
72
|
-
require
|
72
|
+
lists.reverse! if options["reverse"]
|
73
|
+
if options["csv"]
|
74
|
+
require "csv"
|
73
75
|
say LIST_HEADINGS.to_csv unless lists.empty?
|
74
76
|
lists.each do |list|
|
75
77
|
print_csv_list(list)
|
76
78
|
end
|
77
|
-
elsif options[
|
79
|
+
elsif options["long"]
|
78
80
|
array = lists.collect do |list|
|
79
81
|
build_long_list(list)
|
80
82
|
end
|
81
|
-
format = options[
|
83
|
+
format = options["format"] || Array.new(LIST_HEADINGS.size) { "%s" }
|
82
84
|
print_table_with_headings(array, LIST_HEADINGS, format)
|
83
85
|
else
|
84
86
|
print_attribute(lists, :full_name)
|
@@ -97,12 +99,14 @@ module T
|
|
97
99
|
|
98
100
|
def print_table_with_headings(array, headings, format)
|
99
101
|
return if array.flatten.empty?
|
102
|
+
|
100
103
|
if STDOUT.tty?
|
101
104
|
array.unshift(headings)
|
102
|
-
require
|
105
|
+
require "t/core_ext/kernel"
|
103
106
|
array.collect! do |row|
|
104
107
|
row.each_with_index.collect do |element, index|
|
105
108
|
next if element.nil?
|
109
|
+
|
106
110
|
Kernel.send(element.class.name.to_sym, format[index] % element)
|
107
111
|
end
|
108
112
|
end
|
@@ -114,14 +118,14 @@ module T
|
|
114
118
|
end
|
115
119
|
|
116
120
|
def print_message(from_user, message)
|
117
|
-
require
|
121
|
+
require "htmlentities"
|
118
122
|
|
119
|
-
case options[
|
120
|
-
when
|
123
|
+
case options["color"]
|
124
|
+
when "icon"
|
121
125
|
print_identicon(from_user, message)
|
122
126
|
say
|
123
|
-
when
|
124
|
-
say(" @#{from_user}", [
|
127
|
+
when "auto"
|
128
|
+
say(" @#{from_user}", %i[bold yellow])
|
125
129
|
print_wrapped(HTMLEntities.new.decode(message), indent: 3)
|
126
130
|
else
|
127
131
|
say(" @#{from_user}")
|
@@ -131,30 +135,30 @@ module T
|
|
131
135
|
end
|
132
136
|
|
133
137
|
def print_identicon(from_user, message)
|
134
|
-
require
|
135
|
-
require
|
138
|
+
require "htmlentities"
|
139
|
+
require "t/identicon"
|
136
140
|
icon = Identicon.for_user_name(from_user)
|
137
141
|
|
138
142
|
# Save 6 chars for icon, ensure at least 3 lines long
|
139
|
-
lines = wrapped(HTMLEntities.new.decode(message), indent: 2, width: terminal_width - (6 + 5))
|
143
|
+
lines = wrapped(HTMLEntities.new.decode(message), indent: 2, width: Thor::Shell::Terminal.terminal_width - (6 + 5))
|
140
144
|
lines.unshift(set_color(" @#{from_user}", :bold, :yellow))
|
141
|
-
lines.concat(Array.new([3 - lines.length, 0].max) {
|
145
|
+
lines.concat(Array.new([3 - lines.length, 0].max) { "" })
|
142
146
|
|
143
|
-
$stdout.puts
|
147
|
+
$stdout.puts(lines.zip(icon.lines).map { |x, i| " #{i || ' '}#{x}" })
|
144
148
|
end
|
145
149
|
|
146
150
|
def wrapped(message, options = {})
|
147
151
|
indent = options[:indent] || 0
|
148
|
-
width = options[:width] || terminal_width - indent
|
152
|
+
width = options[:width] || (Thor::Shell::Terminal.terminal_width - indent)
|
149
153
|
paras = message.split("\n\n")
|
150
154
|
|
151
155
|
paras.map! do |unwrapped|
|
152
|
-
unwrapped.strip.squeeze(
|
156
|
+
unwrapped.strip.squeeze(" ").gsub(/.{1,#{width}}(?:\s|\Z)/) { (::Regexp.last_match(0) + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n") }
|
153
157
|
end
|
154
158
|
|
155
159
|
lines = paras.inject([]) do |memo, para|
|
156
|
-
memo.concat(para.split("\n").map { |line| line.insert(0,
|
157
|
-
memo.push
|
160
|
+
memo.concat(para.split("\n").map { |line| line.insert(0, " " * indent) })
|
161
|
+
memo.push ""
|
158
162
|
end
|
159
163
|
|
160
164
|
lines.pop
|
@@ -162,59 +166,59 @@ module T
|
|
162
166
|
end
|
163
167
|
|
164
168
|
def print_tweets(tweets)
|
165
|
-
tweets.reverse! if options[
|
166
|
-
if options[
|
167
|
-
require
|
169
|
+
tweets.reverse! if options["reverse"]
|
170
|
+
if options["csv"]
|
171
|
+
require "csv"
|
168
172
|
say TWEET_HEADINGS.to_csv unless tweets.empty?
|
169
173
|
tweets.each do |tweet|
|
170
174
|
print_csv_tweet(tweet)
|
171
175
|
end
|
172
|
-
elsif options[
|
176
|
+
elsif options["long"]
|
173
177
|
array = tweets.collect do |tweet|
|
174
178
|
build_long_tweet(tweet)
|
175
179
|
end
|
176
|
-
format = options[
|
180
|
+
format = options["format"] || Array.new(TWEET_HEADINGS.size) { "%s" }
|
177
181
|
print_table_with_headings(array, TWEET_HEADINGS, format)
|
178
182
|
else
|
179
183
|
tweets.each do |tweet|
|
180
|
-
print_message(tweet.user.screen_name, decode_uris(tweet.full_text, options[
|
184
|
+
print_message(tweet.user.screen_name, decode_uris(tweet.full_text, options["decode_uris"] ? tweet.uris : nil))
|
181
185
|
end
|
182
186
|
end
|
183
187
|
end
|
184
188
|
|
185
|
-
def print_users(users) # rubocop:disable CyclomaticComplexity
|
186
|
-
unless options[
|
187
|
-
users = case options[
|
188
|
-
when
|
189
|
+
def print_users(users) # rubocop:disable Metrics/CyclomaticComplexity
|
190
|
+
unless options["unsorted"]
|
191
|
+
users = case options["sort"]
|
192
|
+
when "favorites"
|
189
193
|
users.sort_by { |user| user.favorites_count.to_i }
|
190
|
-
when
|
194
|
+
when "followers"
|
191
195
|
users.sort_by { |user| user.followers_count.to_i }
|
192
|
-
when
|
196
|
+
when "friends"
|
193
197
|
users.sort_by { |user| user.friends_count.to_i }
|
194
|
-
when
|
198
|
+
when "listed"
|
195
199
|
users.sort_by { |user| user.listed_count.to_i }
|
196
|
-
when
|
200
|
+
when "since"
|
197
201
|
users.sort_by(&:created_at)
|
198
|
-
when
|
202
|
+
when "tweets"
|
199
203
|
users.sort_by { |user| user.statuses_count.to_i }
|
200
|
-
when
|
201
|
-
users.sort_by { |user| user.status? ? user.status.created_at : Time.at(0) } # rubocop:disable BlockNesting
|
204
|
+
when "tweeted"
|
205
|
+
users.sort_by { |user| user.status? ? user.status.created_at : Time.at(0) } # rubocop:disable Metrics/BlockNesting
|
202
206
|
else
|
203
207
|
users.sort_by { |user| user.screen_name.downcase }
|
204
208
|
end
|
205
209
|
end
|
206
|
-
users.reverse! if options[
|
207
|
-
if options[
|
208
|
-
require
|
210
|
+
users.reverse! if options["reverse"]
|
211
|
+
if options["csv"]
|
212
|
+
require "csv"
|
209
213
|
say USER_HEADINGS.to_csv unless users.empty?
|
210
214
|
users.each do |user|
|
211
215
|
print_csv_user(user)
|
212
216
|
end
|
213
|
-
elsif options[
|
217
|
+
elsif options["long"]
|
214
218
|
array = users.collect do |user|
|
215
219
|
build_long_user(user)
|
216
220
|
end
|
217
|
-
format = options[
|
221
|
+
format = options["format"] || Array.new(USER_HEADINGS.size) { "%s" }
|
218
222
|
print_table_with_headings(array, USER_HEADINGS, format)
|
219
223
|
else
|
220
224
|
print_attribute(users, :screen_name)
|
data/lib/t/rcfile.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
require
|
1
|
+
require "singleton"
|
2
2
|
|
3
3
|
module T
|
4
4
|
class RCFile
|
5
5
|
include Singleton
|
6
6
|
attr_reader :path
|
7
|
-
|
7
|
+
|
8
|
+
FILE_NAME = ".trc".freeze
|
8
9
|
|
9
10
|
def initialize
|
10
|
-
@path = File.join(File.expand_path(
|
11
|
+
@path = File.join(File.expand_path("~"), FILE_NAME)
|
11
12
|
@data = load_file
|
12
13
|
end
|
13
14
|
|
@@ -17,7 +18,8 @@ module T
|
|
17
18
|
|
18
19
|
def find(username)
|
19
20
|
possibilities = Array(find_case_insensitive_match(username) || find_case_insensitive_possibilities(username))
|
20
|
-
raise(ArgumentError.new("Username #{username} is #{possibilities.empty? ? 'not found.' :
|
21
|
+
raise(ArgumentError.new("Username #{username} is #{possibilities.empty? ? 'not found.' : "ambiguous, matching #{possibilities.join(', ')}"}")) unless possibilities.size == 1
|
22
|
+
|
21
23
|
possibilities.first
|
22
24
|
end
|
23
25
|
|
@@ -36,36 +38,36 @@ module T
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def configuration
|
39
|
-
@data[
|
41
|
+
@data["configuration"]
|
40
42
|
end
|
41
43
|
|
42
44
|
def active_consumer_key
|
43
|
-
profiles[active_profile[0]][active_profile[1]][
|
45
|
+
profiles[active_profile[0]][active_profile[1]]["consumer_key"] if active_profile?
|
44
46
|
end
|
45
47
|
|
46
48
|
def active_consumer_secret
|
47
|
-
profiles[active_profile[0]][active_profile[1]][
|
49
|
+
profiles[active_profile[0]][active_profile[1]]["consumer_secret"] if active_profile?
|
48
50
|
end
|
49
51
|
|
50
52
|
def active_profile
|
51
|
-
configuration[
|
53
|
+
configuration["default_profile"]
|
52
54
|
end
|
53
55
|
|
54
56
|
def active_profile=(profile)
|
55
|
-
configuration[
|
57
|
+
configuration["default_profile"] = [profile["username"], profile["consumer_key"]]
|
56
58
|
write
|
57
59
|
end
|
58
60
|
|
59
61
|
def active_secret
|
60
|
-
profiles[active_profile[0]][active_profile[1]][
|
62
|
+
profiles[active_profile[0]][active_profile[1]]["secret"] if active_profile?
|
61
63
|
end
|
62
64
|
|
63
65
|
def active_token
|
64
|
-
profiles[active_profile[0]][active_profile[1]][
|
66
|
+
profiles[active_profile[0]][active_profile[1]]["token"] if active_profile?
|
65
67
|
end
|
66
68
|
|
67
69
|
def delete
|
68
|
-
|
70
|
+
FileUtils.rm_f(@path)
|
69
71
|
end
|
70
72
|
|
71
73
|
def empty?
|
@@ -73,7 +75,7 @@ module T
|
|
73
75
|
end
|
74
76
|
|
75
77
|
def load_file
|
76
|
-
require
|
78
|
+
require "yaml"
|
77
79
|
YAML.load_file(@path)
|
78
80
|
rescue Errno::ENOENT
|
79
81
|
default_structure
|
@@ -82,11 +84,10 @@ module T
|
|
82
84
|
def path=(path)
|
83
85
|
@path = path
|
84
86
|
@data = load_file
|
85
|
-
@path
|
86
87
|
end
|
87
88
|
|
88
89
|
def profiles
|
89
|
-
@data[
|
90
|
+
@data["profiles"]
|
90
91
|
end
|
91
92
|
|
92
93
|
def reset
|
@@ -110,11 +111,11 @@ module T
|
|
110
111
|
end
|
111
112
|
|
112
113
|
def default_structure
|
113
|
-
{
|
114
|
+
{"configuration" => {}, "profiles" => {}}
|
114
115
|
end
|
115
116
|
|
116
117
|
def write
|
117
|
-
require
|
118
|
+
require "yaml"
|
118
119
|
File.open(@path, File::RDWR | File::TRUNC | File::CREAT, 0o0600) do |rcfile|
|
119
120
|
rcfile.write @data.to_yaml
|
120
121
|
end
|
data/lib/t/requestable.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "twitter"
|
2
2
|
|
3
3
|
module T
|
4
4
|
module Requestable
|
@@ -6,7 +6,8 @@ module T
|
|
6
6
|
|
7
7
|
def client
|
8
8
|
return @client if @client
|
9
|
-
|
9
|
+
|
10
|
+
@rcfile.path = options["profile"] if options["profile"]
|
10
11
|
@client = Twitter::REST::Client.new do |config|
|
11
12
|
config.consumer_key = @rcfile.active_consumer_key
|
12
13
|
config.consumer_secret = @rcfile.active_consumer_secret
|