t 0.9.9 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +51 -20
- data/bin/t +2 -1
- data/lib/t.rb +0 -7
- data/lib/t/cli.rb +188 -241
- data/lib/t/collectable.rb +11 -3
- data/lib/t/core_ext/string.rb +1 -1
- data/lib/t/delete.rb +36 -30
- data/lib/t/list.rb +19 -82
- data/lib/t/printable.rb +43 -38
- data/lib/t/rcfile.rb +76 -70
- data/lib/t/search.rb +57 -44
- data/lib/t/set.rb +3 -3
- data/lib/t/stream.rb +9 -9
- data/lib/t/{format_helpers.rb → utils.rb} +40 -3
- data/lib/t/version.rb +3 -3
- data/spec/cli_spec.rb +918 -436
- data/spec/delete_spec.rb +4 -4
- data/spec/fixtures/sferik.json +46 -62
- data/spec/fixtures/status_no_attributes.json +4 -4
- data/spec/fixtures/status_no_country.json +4 -4
- data/spec/fixtures/status_no_full_name.json +4 -4
- data/spec/fixtures/status_no_locality.json +4 -4
- data/spec/fixtures/status_no_street_address.json +4 -4
- data/spec/fixtures/users.json +105 -75
- data/spec/fixtures/users_list.json +105 -75
- data/spec/helper.rb +0 -1
- data/spec/list_spec.rb +125 -49
- data/spec/rcfile_spec.rb +28 -27
- data/spec/search_spec.rb +272 -249
- data/spec/set_spec.rb +24 -24
- data/spec/{format_helpers_spec.rb → utils_spec.rb} +7 -7
- data/t.gemspec +3 -5
- metadata +12 -54
- data/lib/t/authorizable.rb +0 -38
- data/lib/t/core_ext/enumerable.rb +0 -19
- data/spec/t_spec.rb +0 -31
data/lib/t/rcfile.rb
CHANGED
@@ -1,95 +1,101 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
module T
|
4
|
+
class RCFile
|
5
|
+
FILE_NAME = '.trc'
|
6
|
+
attr_reader :path
|
6
7
|
|
7
|
-
|
8
|
+
include Singleton
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
def initialize
|
11
|
+
@path = File.join(File.expand_path("~"), FILE_NAME)
|
12
|
+
@data = load
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def [](username)
|
16
|
+
profiles[username]
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
def []=(username, profile)
|
20
|
+
profiles[username] ||= {}
|
21
|
+
profiles[username].merge!(profile)
|
22
|
+
write
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
def configuration
|
26
|
+
@data['configuration']
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def active_consumer_key
|
30
|
+
profiles[active_profile[0]][active_profile[1]]['consumer_key'] if active_profile?
|
31
|
+
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
def active_consumer_secret
|
34
|
+
profiles[active_profile[0]][active_profile[1]]['consumer_secret'] if active_profile?
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
def active_profile
|
38
|
+
configuration['default_profile']
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
def active_profile=(profile)
|
42
|
+
configuration['default_profile'] = [profile['username'], profile['consumer_key']]
|
43
|
+
write
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
def active_secret
|
47
|
+
profiles[active_profile[0]][active_profile[1]]['secret'] if active_profile?
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
def active_token
|
51
|
+
profiles[active_profile[0]][active_profile[1]]['token'] if active_profile?
|
52
|
+
end
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
def delete
|
55
|
+
File.delete(@path) if File.exist?(@path)
|
56
|
+
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
def empty?
|
59
|
+
@data == default_structure
|
60
|
+
end
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
62
|
+
def load
|
63
|
+
require 'yaml'
|
64
|
+
YAML.load_file(@path)
|
65
|
+
rescue Errno::ENOENT
|
66
|
+
default_structure
|
67
|
+
end
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
69
|
+
def path=(path)
|
70
|
+
@path = path
|
71
|
+
@data = load
|
72
|
+
@path
|
73
|
+
end
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
75
|
+
def profiles
|
76
|
+
@data['profiles']
|
77
|
+
end
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
def reset
|
80
|
+
self.send(:initialize)
|
81
|
+
end
|
81
82
|
|
82
|
-
private
|
83
|
+
private
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
def active_profile?
|
86
|
+
active_profile && profiles[active_profile[0]] && profiles[active_profile[0]][active_profile[1]]
|
87
|
+
end
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
def default_structure
|
90
|
+
{'configuration' => {}, 'profiles' => {}}
|
91
|
+
end
|
92
|
+
|
93
|
+
def write
|
94
|
+
require 'yaml'
|
95
|
+
File.open(@path, File::RDWR|File::TRUNC|File::CREAT, 0600) do |rcfile|
|
96
|
+
rcfile.write @data.to_yaml
|
97
|
+
end
|
92
98
|
end
|
93
|
-
end
|
94
99
|
|
100
|
+
end
|
95
101
|
end
|
data/lib/t/search.rb
CHANGED
@@ -1,26 +1,26 @@
|
|
1
1
|
require 'thor'
|
2
2
|
require 'twitter'
|
3
|
+
require 't/collectable'
|
4
|
+
require 't/printable'
|
5
|
+
require 't/rcfile'
|
6
|
+
require 't/requestable'
|
7
|
+
require 't/utils'
|
3
8
|
|
4
9
|
module T
|
5
|
-
autoload :Collectable, 't/collectable'
|
6
|
-
autoload :Printable, 't/printable'
|
7
|
-
autoload :RCFile, 't/rcfile'
|
8
|
-
autoload :Requestable, 't/requestable'
|
9
10
|
class Search < Thor
|
10
11
|
include T::Collectable
|
11
12
|
include T::Printable
|
12
13
|
include T::Requestable
|
14
|
+
include T::Utils
|
13
15
|
|
14
16
|
DEFAULT_NUM_RESULTS = 20
|
15
17
|
MAX_NUM_RESULTS = 200
|
16
|
-
MAX_SCREEN_NAME_SIZE = 20
|
17
|
-
MAX_USERS_PER_REQUEST = 20
|
18
18
|
|
19
19
|
check_unknown_options!
|
20
20
|
|
21
21
|
def initialize(*)
|
22
|
+
@rcfile = T::RCFile.instance
|
22
23
|
super
|
23
|
-
@rcfile = RCFile.instance
|
24
24
|
end
|
25
25
|
|
26
26
|
desc "all QUERY", "Returns the #{DEFAULT_NUM_RESULTS} most recent Tweets that match the specified query."
|
@@ -30,8 +30,9 @@ module T
|
|
30
30
|
def all(query)
|
31
31
|
rpp = options['number'] || DEFAULT_NUM_RESULTS
|
32
32
|
statuses = collect_with_rpp(rpp) do |opts|
|
33
|
-
client.search(query, opts)
|
33
|
+
client.search(query, opts).results
|
34
34
|
end
|
35
|
+
statuses.reverse! if options['reverse']
|
35
36
|
require 'htmlentities'
|
36
37
|
if options['csv']
|
37
38
|
require 'csv'
|
@@ -49,19 +50,35 @@ module T
|
|
49
50
|
else
|
50
51
|
say unless statuses.empty?
|
51
52
|
statuses.each do |status|
|
52
|
-
|
53
|
+
print_message(status.from_user, status.full_text)
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
57
|
-
desc "favorites QUERY", "Returns Tweets you've favorited that match the specified query."
|
58
|
+
desc "favorites [USER] QUERY", "Returns Tweets you've favorited that match the specified query."
|
58
59
|
method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
|
60
|
+
method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
|
59
61
|
method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
|
60
|
-
def favorites(
|
62
|
+
def favorites(*args)
|
61
63
|
opts = {:count => MAX_NUM_RESULTS}
|
62
|
-
|
63
|
-
|
64
|
-
|
64
|
+
query = args.pop
|
65
|
+
user = args.pop
|
66
|
+
if user
|
67
|
+
require 't/core_ext/string'
|
68
|
+
user = if options['id']
|
69
|
+
user.to_i
|
70
|
+
else
|
71
|
+
user.strip_ats
|
72
|
+
end
|
73
|
+
statuses = collect_with_max_id do |max_id|
|
74
|
+
opts[:max_id] = max_id unless max_id.nil?
|
75
|
+
client.favorites(user, opts)
|
76
|
+
end
|
77
|
+
else
|
78
|
+
statuses = collect_with_max_id do |max_id|
|
79
|
+
opts[:max_id] = max_id unless max_id.nil?
|
80
|
+
client.favorites(opts)
|
81
|
+
end
|
65
82
|
end
|
66
83
|
statuses = statuses.select do |status|
|
67
84
|
/#{query}/i.match(status.full_text)
|
@@ -75,18 +92,7 @@ module T
|
|
75
92
|
method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
|
76
93
|
method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
|
77
94
|
def list(list, query)
|
78
|
-
owner, list = list
|
79
|
-
if list.nil?
|
80
|
-
list = owner
|
81
|
-
owner = @rcfile.active_profile[0]
|
82
|
-
else
|
83
|
-
require 't/core_ext/string'
|
84
|
-
owner = if options['id']
|
85
|
-
owner.to_i
|
86
|
-
else
|
87
|
-
owner.strip_ats
|
88
|
-
end
|
89
|
-
end
|
95
|
+
owner, list = extract_owner(list, options)
|
90
96
|
opts = {:count => MAX_NUM_RESULTS}
|
91
97
|
statuses = collect_with_max_id do |max_id|
|
92
98
|
opts[:max_id] = max_id unless max_id.nil?
|
@@ -114,14 +120,30 @@ module T
|
|
114
120
|
end
|
115
121
|
map %w(replies) => :mentions
|
116
122
|
|
117
|
-
desc "retweets QUERY", "Returns Tweets you've retweeted that match the specified query."
|
123
|
+
desc "retweets [USER] QUERY", "Returns Tweets you've retweeted that match the specified query."
|
118
124
|
method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
|
125
|
+
method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
|
119
126
|
method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
|
120
|
-
def retweets(
|
127
|
+
def retweets(*args)
|
121
128
|
opts = {:count => MAX_NUM_RESULTS}
|
122
|
-
|
123
|
-
|
124
|
-
|
129
|
+
query = args.pop
|
130
|
+
user = args.pop
|
131
|
+
if user
|
132
|
+
require 't/core_ext/string'
|
133
|
+
user = if options['id']
|
134
|
+
user.to_i
|
135
|
+
else
|
136
|
+
user.strip_ats
|
137
|
+
end
|
138
|
+
statuses = collect_with_max_id do |max_id|
|
139
|
+
opts[:max_id] = max_id unless max_id.nil?
|
140
|
+
client.retweeted_by_user(user, opts)
|
141
|
+
end
|
142
|
+
else
|
143
|
+
statuses = collect_with_max_id do |max_id|
|
144
|
+
opts[:max_id] = max_id unless max_id.nil?
|
145
|
+
client.retweeted_by_me(opts)
|
146
|
+
end
|
125
147
|
end
|
126
148
|
statuses = statuses.select do |status|
|
127
149
|
/#{query}/i.match(status.full_text)
|
@@ -164,23 +186,14 @@ module T
|
|
164
186
|
|
165
187
|
desc "users QUERY", "Returns users that match the specified query."
|
166
188
|
method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
|
167
|
-
method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
|
168
|
-
method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
|
169
|
-
method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
|
170
|
-
method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
|
171
189
|
method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
|
172
|
-
method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
|
173
190
|
method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
|
174
|
-
method_option "
|
191
|
+
method_option "sort", :aliases => "-s", :type => :string, :enum => %w(favorites followers friends listed screen_name since tweets tweeted), :default => "screen_name", :desc => "Specify the order of the results.", :banner => "ORDER"
|
175
192
|
method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
|
176
193
|
def users(query)
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
181
|
-
client.user_search(query, :page => page, :per_page => MAX_USERS_PER_REQUEST)
|
182
|
-
end
|
183
|
-
end.flatten
|
194
|
+
users = collect_with_page do |page|
|
195
|
+
client.user_search(query, :page => page)
|
196
|
+
end
|
184
197
|
print_users(users)
|
185
198
|
end
|
186
199
|
|
data/lib/t/set.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
require 'thor'
|
2
|
+
require 't/rcfile'
|
3
|
+
require 't/requestable'
|
2
4
|
|
3
5
|
module T
|
4
|
-
autoload :RCFile, 't/rcfile'
|
5
|
-
autoload :Requestable, 't/requestable'
|
6
6
|
class Set < Thor
|
7
7
|
include T::Requestable
|
8
8
|
|
9
9
|
check_unknown_options!
|
10
10
|
|
11
11
|
def initialize(*)
|
12
|
+
@rcfile = T::RCFile.instance
|
12
13
|
super
|
13
|
-
@rcfile = RCFile.instance
|
14
14
|
end
|
15
15
|
|
16
16
|
desc "active SCREEN_NAME [CONSUMER_KEY]", "Set your active account."
|
data/lib/t/stream.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
require 'thor'
|
2
|
+
require 't/printable'
|
3
|
+
require 't/rcfile'
|
2
4
|
|
3
5
|
module T
|
4
|
-
autoload :CLI, 't/cli'
|
5
|
-
autoload :Printable, 't/printable'
|
6
|
-
autoload :RCFile, 't/rcfile'
|
7
|
-
autoload :Search, 't/search'
|
8
6
|
class Stream < Thor
|
9
7
|
include T::Printable
|
10
8
|
|
@@ -16,8 +14,8 @@ module T
|
|
16
14
|
]
|
17
15
|
|
18
16
|
def initialize(*)
|
17
|
+
@rcfile = T::RCFile.instance
|
19
18
|
super
|
20
|
-
@rcfile = RCFile.instance
|
21
19
|
end
|
22
20
|
|
23
21
|
desc "all", "Stream a random sample of all Tweets (Control-C to stop)"
|
@@ -46,7 +44,7 @@ module T
|
|
46
44
|
end
|
47
45
|
print_table([array], :truncate => STDOUT.tty?)
|
48
46
|
else
|
49
|
-
|
47
|
+
print_message(status.user.screen_name, status.text)
|
50
48
|
end
|
51
49
|
end
|
52
50
|
client.sample
|
@@ -67,6 +65,7 @@ module T
|
|
67
65
|
def search(keyword, *keywords)
|
68
66
|
keywords.unshift(keyword)
|
69
67
|
require 'tweetstream'
|
68
|
+
require 't/search'
|
70
69
|
client.on_inited do
|
71
70
|
search = T::Search.new
|
72
71
|
search.options = search.options.merge(options)
|
@@ -83,7 +82,7 @@ module T
|
|
83
82
|
end
|
84
83
|
print_table([array], :truncate => STDOUT.tty?)
|
85
84
|
else
|
86
|
-
|
85
|
+
print_message(status.user.screen_name, status.text)
|
87
86
|
end
|
88
87
|
end
|
89
88
|
client.track(keywords)
|
@@ -94,6 +93,7 @@ module T
|
|
94
93
|
method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
|
95
94
|
def timeline
|
96
95
|
require 'tweetstream'
|
96
|
+
require 't/cli'
|
97
97
|
client.on_inited do
|
98
98
|
cli = T::CLI.new
|
99
99
|
cli.options = cli.options.merge(options)
|
@@ -110,7 +110,7 @@ module T
|
|
110
110
|
end
|
111
111
|
print_table([array], :truncate => STDOUT.tty?)
|
112
112
|
else
|
113
|
-
|
113
|
+
print_message(status.user.screen_name, status.text)
|
114
114
|
end
|
115
115
|
end
|
116
116
|
client.userstream
|
@@ -144,7 +144,7 @@ module T
|
|
144
144
|
end
|
145
145
|
print_table([array], :truncate => STDOUT.tty?)
|
146
146
|
else
|
147
|
-
|
147
|
+
print_message(status.user.screen_name, status.text)
|
148
148
|
end
|
149
149
|
end
|
150
150
|
client.follow(user_ids)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module T
|
2
|
-
module
|
2
|
+
module Utils
|
3
3
|
private
|
4
4
|
|
5
5
|
# https://github.com/rails/rails/blob/bd8a970/actionpack/lib/action_view/helpers/date_helper.rb
|
@@ -47,16 +47,53 @@ module T
|
|
47
47
|
alias :time_ago_in_words :distance_of_time_in_words
|
48
48
|
alias :time_from_now_in_words :distance_of_time_in_words
|
49
49
|
|
50
|
+
def fetch_users(users, options, &block)
|
51
|
+
format_users!(users, options)
|
52
|
+
require 'retryable'
|
53
|
+
users = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
54
|
+
yield users
|
55
|
+
end
|
56
|
+
[users, users.length]
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_users!(users, options)
|
60
|
+
require 't/core_ext/string'
|
61
|
+
if options['id']
|
62
|
+
users.map!(&:to_i)
|
63
|
+
else
|
64
|
+
users.map!(&:strip_ats)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_owner(list, options)
|
69
|
+
owner, list = list.split('/')
|
70
|
+
if list.nil?
|
71
|
+
list = owner
|
72
|
+
owner = @rcfile.active_profile[0]
|
73
|
+
else
|
74
|
+
require 't/core_ext/string'
|
75
|
+
owner = if options['id']
|
76
|
+
owner.to_i
|
77
|
+
else
|
78
|
+
owner.strip_ats
|
79
|
+
end
|
80
|
+
end
|
81
|
+
[owner, list]
|
82
|
+
end
|
83
|
+
|
50
84
|
def strip_tags(html)
|
51
85
|
html.gsub(/<.+?>/, '')
|
52
86
|
end
|
53
87
|
|
54
88
|
def number_with_delimiter(number, delimiter=",")
|
55
89
|
digits = number.to_s.split(//)
|
56
|
-
|
57
|
-
groups = digits.reverse.in_groups_of(3).map{|g| g.join('')}
|
90
|
+
groups = digits.reverse.each_slice(3).map{|g| g.join('')}
|
58
91
|
groups.join(delimiter).reverse
|
59
92
|
end
|
60
93
|
|
94
|
+
def pluralize(count, singular, plural=nil)
|
95
|
+
"#{count || 0} " + ((count == 1 || count =~ /^1(\.0+)?$/) ? singular : (plural || "#{singular}s"))
|
96
|
+
end
|
97
|
+
|
61
98
|
end
|
62
99
|
end
|