t 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,6 +1,5 @@
1
1
  *.gem
2
2
  *.rbc
3
- .DS_Store
4
3
  .bundle
5
4
  .rvmrc
6
5
  .yardoc
data/.travis.yml CHANGED
@@ -3,6 +3,5 @@ rvm:
3
3
  - 1.8.7
4
4
  - 1.9.2
5
5
  - 1.9.3
6
- - jruby-18mode
7
6
  - rbx-18mode
8
7
  - rbx-19mode
data/README.md CHANGED
@@ -12,7 +12,6 @@ however it offers many more commands than are available via SMS.
12
12
  ## <a name="installation"></a>Installation
13
13
  gem install t
14
14
 
15
-
16
15
  ## <a name="configuration"></a>Configuration
17
16
 
18
17
  Because Twitter requires OAuth for most of its functionality, you'll need to
@@ -49,134 +48,66 @@ username and consumer key pair, like so:
49
48
  Account information is stored in the YAML-formatted file `~/.trc`.
50
49
 
51
50
  ## <a name="examples"></a>Usage Examples
52
-
53
51
  Typing `t help` will give you a list of all the available commands. You can
54
52
  type `t help TASK` to get help for a specific command.
55
53
 
56
54
  t help
57
55
 
58
56
  ### <a name="update"></a>Update your status
59
-
60
57
  t update "I'm tweeting from the command line. Isn't that special?"
61
58
 
62
- ### <a name="dm"></a>Send a direct message
63
-
64
- t dm sferik "Want to get dinner tonight?"
65
-
66
- ### <a name="location"></a>Update the location field in your profile
67
-
68
- t set location "San Francisco"
69
-
70
- ### <a name="whois"></a>Retrieve profile information for a user
71
-
72
- t whois sferik
73
-
74
- ### <a name="stats"></a>Retrieve stats about a user
75
-
76
- t stats sferik
77
-
78
- ### <a name="suggest"></a>Return a user you might enjoy following
79
-
80
- t suggest
59
+ ### <a name="stats"></a>Retrieve stats about users
60
+ t users -l sferik gem
81
61
 
82
- ### <a name="follow-users"></a>Start following users
62
+ ### <a name="follow"></a>Follow users
63
+ t follow sferik gem
83
64
 
84
- t follow users sferik gem
65
+ ### <a name="friends"></a>List your friends (ordered by number of followers)
66
+ t friends -lf
85
67
 
86
- ### <a name="follow-followers"></a>Follow all followers (i.e. follow back)
68
+ ### <a name="leaders"></a>List your leaders (people you follow who don't follow you back)
69
+ t leaders -lf
87
70
 
88
- t follow followers
71
+ ### <a name="unfollow"></a>Unfollow everyone you follow who doesn't follow you back
72
+ t leaders | xargs t unfollow
89
73
 
90
- ### <a name="unfollow-users"></a>Stop following users
91
-
92
- t unfollow users sferik gem
93
-
94
- ### <a name="unfollow-nonfollowers"></a>Unfollow all non-followers
95
-
96
- t unfollow nonfollowers
74
+ ### Follow back everyone who follows you
75
+ t followers | xargs t follow
97
76
 
98
77
  ### <a name="list-create"></a>Create a list
99
-
100
78
  t list create presidents
101
79
 
102
- ### <a name="list-add-followers"></a>Add users to a list
103
-
104
- t list add users presidents BarackObama Jasonfinn
105
-
106
- ### <a name="list-add-friends"></a>Add all friends to a list
107
-
108
- t list add friends presidents
109
-
110
- ### <a name="list-add-followers"></a>Add all followers to a list
111
-
112
- t list add followers presidents
113
-
114
- ### <a name="list-add-followers"></a>Add all members of one list to another
115
-
116
- t list add listed democrats presidents
80
+ ### <a name="list-add"></a>Add users to a list
81
+ t list add presidents BarackObama Jasonfinn
117
82
 
118
- ### <a name="follow-all-listed"></a>Follow all members of a list
83
+ ### <a name="following"></a>Create a list that contains today's date in the name
84
+ date "+following-%Y-%m-%d" | xargs t list create
119
85
 
120
- t follow listed presidents
86
+ ### Add everyone you're following to a list
87
+ t followings | xargs t list add following-`date "+%Y-%m-%d"`
121
88
 
122
- ### <a name="unfollow-all-listed"></a>Unfollow all members of a list
89
+ ### <a name="members"></a>Display members of a list
90
+ t members following-`date "+%Y-%m-%d"`
123
91
 
124
- t unfollow listed presidents
125
-
126
- ### <a name="list-timeline"></a>Retrieve the timeline of status updates from a list
127
-
128
- t list timeline presidents
129
-
130
- ### <a name="timeline"></a>Retrieve the timeline of status updates posted by you and the users you follow
131
-
132
- t timeline
133
-
134
- ### <a name="timeline-user"></a>Retrieve the timeline of status updates posted by a user
135
-
136
- t timeline sferik
137
-
138
- ### <a name="mentions"></a>Retrieve the timeline of status updates that mention you
139
-
140
- t mentions
141
-
142
- ### <a name="favorites"></a>Retrieve the timeline of status updates that you favorited
143
-
144
- t favorites
145
-
146
- ### <a name="reply"></a>Reply to a Tweet
147
-
148
- t reply sferik "Thanks Erik"
149
-
150
- ### <a name="retweet"></a>Send another user's latest Tweet to your followers
151
-
152
- t retweet sferik
153
-
154
- ### <a name="favorite"></a>Mark a user's latest Tweet as one of your favorites
155
-
156
- t favorite sferik
157
-
158
- ### <a name="search-all"></a>Retrieve the 20 most recent Tweets that match a specified query
92
+ ### Count the number of Twitter employees
93
+ t members twitter team | wc -l
159
94
 
95
+ ### <a name="search-all"></a>Search Twitter for the 20 most recent Tweets that match a specified query
160
96
  t search all "query"
161
97
 
162
- ### <a name="search-retweets"></a>Returns Tweets you've favorited that mach a specified query
163
-
98
+ ### <a name="search-retweets"></a>Search Tweets you've favorited that match a specified query
164
99
  t search favorites "query"
165
100
 
166
- ### <a name="search-mentions"></a>Returns Tweets mentioning you that mach a specified query
167
-
101
+ ### <a name="search-mentions"></a>Search Tweets mentioning you that match a specified query
168
102
  t search mentions "query"
169
103
 
170
- ### <a name="search-retweets"></a>Returns Tweets you've retweeted that mach a specified query
171
-
104
+ ### <a name="search-retweets"></a>Search Tweets you've retweeted that match a specified query
172
105
  t search retweets "query"
173
106
 
174
- ### <a name="search-timeline"></a>Retrieve Tweets in your timeline that match a specified query
175
-
107
+ ### <a name="search-timeline"></a>Search Tweets in your timeline that match a specified query
176
108
  t search timeline "query"
177
109
 
178
- ### <a name="search-user"></a>Retrieve Tweets in a user's timeline that match a specified query
179
-
110
+ ### <a name="search-user"></a>Search Tweets in a user's timeline that match a specified query
180
111
  t search user sferik "query"
181
112
 
182
113
  ## <a name="history"></a>History
@@ -240,10 +171,8 @@ implementations:
240
171
  * Ruby 1.8.7
241
172
  * Ruby 1.9.2
242
173
  * Ruby 1.9.3
243
- * [JRuby][]
244
174
  * [Rubinius][]
245
175
 
246
- [jruby]: http://www.jruby.org/
247
176
  [rubinius]: http://rubini.us/
248
177
 
249
178
  If something doesn't work on one of these interpreters, it should be considered
data/lib/t.rb CHANGED
@@ -1,14 +1 @@
1
- require 'active_support/string_inquirer'
2
1
  require 't/cli'
3
-
4
- module T
5
- class << self
6
- def env
7
- @env ||= ActiveSupport::StringInquirer.new("development")
8
- end
9
-
10
- def env=(environment)
11
- @env = ActiveSupport::StringInquirer.new(environment)
12
- end
13
- end
14
- end
data/lib/t/cli.rb CHANGED
@@ -1,9 +1,19 @@
1
1
  require 'action_view'
2
+ require 'active_support/core_ext/array/grouping'
3
+ require 'active_support/core_ext/date/calculations'
4
+ require 'active_support/core_ext/integer/time'
5
+ require 'active_support/core_ext/numeric/time'
6
+ require 'highline'
2
7
  require 'launchy'
3
8
  require 'oauth'
4
- require 'pager'
9
+ require 't/collectable'
5
10
  require 't/core_ext/string'
11
+ require 't/delete'
12
+ require 't/list'
6
13
  require 't/rcfile'
14
+ require 't/search'
15
+ require 't/set'
16
+ require 't/version'
7
17
  require 'thor'
8
18
  require 'time'
9
19
  require 'twitter'
@@ -13,12 +23,13 @@ module T
13
23
  class CLI < Thor
14
24
  include ActionView::Helpers::DateHelper
15
25
  include ActionView::Helpers::NumberHelper
16
- include Pager
26
+ include T::Collectable
17
27
 
18
28
  DEFAULT_HOST = 'api.twitter.com'
19
29
  DEFAULT_PROTOCOL = 'https'
20
30
  DEFAULT_NUM_RESULTS = 20
21
31
  MAX_SCREEN_NAME_SIZE = 20
32
+ MAX_USERS_PER_REQUEST = 100
22
33
 
23
34
  check_unknown_options!
24
35
 
@@ -77,86 +88,248 @@ module T
77
88
  }
78
89
  }
79
90
  @rcfile.default_profile = {'username' => screen_name, 'consumer_key' => options['consumer_key']}
80
- say "Authorization successful"
91
+ say "Authorization successful."
81
92
  end
82
93
 
83
- desc "block SCREEN_NAME", "Block a user."
84
- def block(screen_name)
85
- screen_name = screen_name.strip_at
86
- user = client.block(screen_name, :include_entities => false)
87
- say "@#{@rcfile.default_profile[0]} blocked @#{user.screen_name}"
94
+ desc "block SCREEN_NAME [SCREEN_NAME...]", "Block users."
95
+ def block(screen_name, *screen_names)
96
+ screen_names.unshift(screen_name)
97
+ screen_names.threaded_each do |screen_name|
98
+ screen_name.strip_at
99
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
100
+ client.block(screen_name, :include_entities => false)
101
+ end
102
+ end
103
+ say "@#{@rcfile.default_profile[0]} blocked @#{screen_names.join(' ')}."
88
104
  say
89
- say "Run `#{File.basename($0)} delete block #{user.screen_name}` to unblock."
105
+ say "Run `#{File.basename($0)} delete block #{screen_names.join(' ')}` to unblock."
90
106
  end
91
107
 
92
108
  desc "direct_messages", "Returns the #{DEFAULT_NUM_RESULTS} most recent Direct Messages sent to you."
109
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
93
110
  method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
111
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
94
112
  def direct_messages
95
- defaults = {:include_entities => false}
96
- defaults.merge!(:count => options['number']) if options['number']
97
- page unless T.env.test?
98
- client.direct_messages(defaults).each do |direct_message|
99
- say "#{direct_message.sender.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{direct_message.text} (#{time_ago_in_words(direct_message.created_at)} ago)"
113
+ count = options['number'] || DEFAULT_NUM_RESULTS
114
+ direct_messages = client.direct_messages(:count => count, :include_entities => false)
115
+ direct_messages.reverse! if options['reverse']
116
+ if options['long']
117
+ array = direct_messages.map do |direct_message|
118
+ created_at = direct_message.created_at > 6.months.ago ? direct_message.created_at.strftime("%b %e %H:%M") : direct_message.created_at.strftime("%b %e %Y")
119
+ [direct_message.id.to_s, created_at, direct_message.sender.screen_name, direct_message.text.gsub(/\n+/, ' ')]
120
+ end
121
+ if STDOUT.tty?
122
+ headings = ["ID", "Created at", "Screen name", "Text"]
123
+ array.unshift(headings)
124
+ end
125
+ print_table(array)
126
+ else
127
+ direct_messages.each do |direct_message|
128
+ say "#{direct_message.sender.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{direct_message.text.gsub(/\n+/, ' ')} (#{time_ago_in_words(direct_message.created_at)} ago)"
129
+ end
100
130
  end
101
131
  end
102
132
  map %w(dms) => :direct_messages
103
133
 
134
+ desc "direct_messages_sent", "Returns the #{DEFAULT_NUM_RESULTS} most recent Direct Messages sent to you."
135
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
136
+ method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
137
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
138
+ def direct_messages_sent
139
+ count = options['number'] || DEFAULT_NUM_RESULTS
140
+ direct_messages = client.direct_messages_sent(:count => count, :include_entities => false)
141
+ direct_messages.reverse! if options['reverse']
142
+ if options['long']
143
+ array = direct_messages.map do |direct_message|
144
+ created_at = direct_message.created_at > 6.months.ago ? direct_message.created_at.strftime("%b %e %H:%M") : direct_message.created_at.strftime("%b %e %Y")
145
+ [direct_message.id.to_s, created_at, direct_message.recipient.screen_name, direct_message.text.gsub(/\n+/, ' ')]
146
+ end
147
+ if STDOUT.tty?
148
+ headings = ["ID", "Created at", "Screen name", "Text"]
149
+ array.unshift(headings)
150
+ end
151
+ print_table(array)
152
+ else
153
+ direct_messages.each do |direct_message|
154
+ say "#{direct_message.recipient.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{direct_message.text.gsub(/\n+/, ' ')} (#{time_ago_in_words(direct_message.created_at)} ago)"
155
+ end
156
+ end
157
+ end
158
+ map %w(sent_messages sms) => :direct_messages_sent
159
+
104
160
  desc "dm SCREEN_NAME MESSAGE", "Sends that person a Direct Message."
105
161
  def dm(screen_name, message)
106
162
  screen_name = screen_name.strip_at
107
163
  direct_message = client.direct_message_create(screen_name, message, :include_entities => false)
108
- say "Direct Message sent from @#{@rcfile.default_profile[0]} to @#{direct_message.recipient.screen_name} (#{time_ago_in_words(direct_message.created_at)} ago)"
164
+ say "Direct Message sent from @#{@rcfile.default_profile[0]} to @#{direct_message.recipient.screen_name} (#{time_ago_in_words(direct_message.created_at)} ago)."
109
165
  end
110
- map %w(m) => :dm
111
-
112
- desc "favorite SCREEN_NAME", "Marks that user's last Tweet as one of your favorites."
113
- def favorite(screen_name)
114
- screen_name = screen_name.strip_at
115
- user = client.user(screen_name, :include_entities => false)
116
- if user.status
117
- client.favorite(user.status.id, :include_entities => false)
118
- say "@#{@rcfile.default_profile[0]} favorited @#{user.screen_name}'s latest status: \"#{user.status.text}\""
119
- say
120
- say "Run `#{File.basename($0)} delete favorite` to unfavorite."
121
- else
122
- raise Thor::Error, "Tweet not found"
166
+ map %w(d m) => :dm
167
+
168
+ desc "favorite STATUS_ID [STATUS_ID...]", "Marks Tweets as favorites."
169
+ def favorite(status_id, *status_ids)
170
+ status_ids.unshift(status_id)
171
+ favorites = status_ids.threaded_map do |status_id|
172
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
173
+ client.favorite(status_id, :include_entities => false)
174
+ end
123
175
  end
124
- rescue Twitter::Error::Forbidden => error
125
- if error.message =~ /You have already favorited this status\./
126
- say "@#{@rcfile.default_profile[0]} favorited @#{user.screen_name}'s latest status: \"#{user.status.text}\""
127
- else
128
- raise
176
+ favorites.each do |status|
177
+ say "@#{@rcfile.default_profile[0]} favorited @#{status.user.screen_name}'s status: \"#{status.text.gsub(/\n+/, ' ')}\""
129
178
  end
179
+ say
180
+ say "Run `#{File.basename($0)} delete favorite #{status_ids.join(' ')}` to unfavorite."
130
181
  end
131
182
  map %w(fave) => :favorite
132
183
 
133
184
  desc "favorites", "Returns the #{DEFAULT_NUM_RESULTS} most recent Tweets you favorited."
185
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
134
186
  method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
135
187
  method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
136
188
  def favorites
137
- defaults = {:include_entities => false}
138
- defaults.merge!(:count => options['number']) if options['number']
139
- timeline = client.favorites(defaults)
140
- timeline.reverse! if options['reverse']
141
- page unless T.env.test?
142
- timeline.each do |status|
143
- say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
144
- end
189
+ count = options['number'] || DEFAULT_NUM_RESULTS
190
+ statuses = client.favorites(:count => count, :include_entities => false)
191
+ print_status_list(statuses)
145
192
  end
146
193
  map %w(faves) => :favorites
147
194
 
195
+ desc "follow SCREEN_NAME [SCREEN_NAME...]", "Allows you to start following users."
196
+ def follow(screen_name, *screen_names)
197
+ screen_names.unshift(screen_name)
198
+ screen_names.threaded_each do |screen_name|
199
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
200
+ client.follow(screen_name, :include_entities => false)
201
+ end
202
+ end
203
+ number = screen_names.length
204
+ say "@#{@rcfile.default_profile[0]} is now following #{number} more #{number == 1 ? 'user' : 'users'}."
205
+ say
206
+ say "Run `#{File.basename($0)} unfollow users #{screen_names.join(' ')}` to stop."
207
+ end
208
+
209
+ desc "followings", "Returns a list of the people you follow on Twitter."
210
+ method_option :created, :aliases => "-c", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter acount was created."
211
+ method_option :friends, :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by total number of friends."
212
+ method_option :followers, :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by total number of followers."
213
+ method_option :listed, :aliases => "-i", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
214
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
215
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
216
+ method_option :tweets, :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by total number of Tweets."
217
+ method_option :unsorted, :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
218
+ method_option :favorites, :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by total number of favorites."
219
+ def followings
220
+ following_ids = collect_with_cursor do |cursor|
221
+ client.friend_ids(:cursor => cursor)
222
+ end
223
+ users = following_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |following_id_group|
224
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
225
+ client.users(following_id_group, :include_entities => false)
226
+ end
227
+ end.flatten
228
+ print_user_list(users)
229
+ end
230
+
231
+ desc "followers", "Returns a list of the people who follow you on Twitter."
232
+ method_option :created, :aliases => "-c", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter acount was created."
233
+ method_option :friends, :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by total number of friends."
234
+ method_option :followers, :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by total number of followers."
235
+ method_option :listed, :aliases => "-i", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
236
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
237
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
238
+ method_option :tweets, :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by total number of Tweets."
239
+ method_option :unsorted, :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
240
+ method_option :favorites, :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by total number of favorites."
241
+ def followers
242
+ follower_ids = collect_with_cursor do |cursor|
243
+ client.follower_ids(:cursor => cursor)
244
+ end
245
+ users = follower_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |follower_id_group|
246
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
247
+ client.users(follower_id_group, :include_entities => false)
248
+ end
249
+ end.flatten
250
+ print_user_list(users)
251
+ end
252
+
253
+ desc "friends", "Returns the list of people who you follow and follow you back."
254
+ method_option :created, :aliases => "-c", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter acount was created."
255
+ method_option :friends, :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by total number of friends."
256
+ method_option :followers, :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by total number of followers."
257
+ method_option :listed, :aliases => "-i", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
258
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
259
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
260
+ method_option :tweets, :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by total number of Tweets."
261
+ method_option :unsorted, :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
262
+ method_option :favorites, :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by total number of favorites."
263
+ def friends
264
+ following_ids = collect_with_cursor do |cursor|
265
+ client.friend_ids(:cursor => cursor)
266
+ end
267
+ follower_ids = collect_with_cursor do |cursor|
268
+ client.follower_ids(:cursor => cursor)
269
+ end
270
+ friend_ids = (following_ids & follower_ids)
271
+ users = friend_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |friend_id_group|
272
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
273
+ client.users(friend_id_group, :include_entities => false)
274
+ end
275
+ end.flatten
276
+ print_user_list(users)
277
+ end
278
+
279
+ desc "leaders", "Returns the list of people who you follow but don't follow you back."
280
+ method_option :created, :aliases => "-c", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter acount was created."
281
+ method_option :friends, :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by total number of friends."
282
+ method_option :followers, :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by total number of followers."
283
+ method_option :listed, :aliases => "-i", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
284
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
285
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
286
+ method_option :tweets, :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by total number of Tweets."
287
+ method_option :unsorted, :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
288
+ method_option :favorites, :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by total number of favorites."
289
+ def leaders
290
+ following_ids = collect_with_cursor do |cursor|
291
+ client.friend_ids(:cursor => cursor)
292
+ end
293
+ follower_ids = collect_with_cursor do |cursor|
294
+ client.follower_ids(:cursor => cursor)
295
+ end
296
+ leader_ids = (following_ids - follower_ids)
297
+ users = leader_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |leader_id_group|
298
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
299
+ client.users(leader_id_group, :include_entities => false)
300
+ end
301
+ end.flatten
302
+ print_user_list(users)
303
+ end
304
+
305
+ desc "members [SCREEN_NAME] LIST_NAME", "Returns the members of a Twitter list."
306
+ method_option :created, :aliases => "-c", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter acount was created."
307
+ method_option :friends, :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by total number of friends."
308
+ method_option :followers, :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by total number of followers."
309
+ method_option :listed, :aliases => "-i", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
310
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
311
+ method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
312
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
313
+ method_option :tweets, :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by total number of Tweets."
314
+ method_option :unsorted, :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
315
+ method_option :favorites, :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by total number of favorites."
316
+ def members(*args)
317
+ list = args.pop
318
+ owner = args.pop || @rcfile.default_profile[0]
319
+ users = collect_with_cursor do |cursor|
320
+ client.list_members(owner, list, :cursor => cursor, :include_entities => false, :skip_status => true)
321
+ end
322
+ print_user_list(users)
323
+ end
324
+
148
325
  desc "mentions", "Returns the #{DEFAULT_NUM_RESULTS} most recent Tweets mentioning you."
326
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
149
327
  method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
150
328
  method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
151
329
  def mentions
152
- defaults = {:include_entities => false}
153
- defaults.merge!(:count => options['number']) if options['number']
154
- timeline = client.mentions(defaults)
155
- timeline.reverse! if options['reverse']
156
- page unless T.env.test?
157
- timeline.each do |status|
158
- say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
159
- end
330
+ count = options['number'] || DEFAULT_NUM_RESULTS
331
+ statuses = client.mentions(:count => count, :include_entities => false)
332
+ print_status_list(statuses)
160
333
  end
161
334
  map %w(replies) => :mentions
162
335
 
@@ -167,126 +340,136 @@ module T
167
340
  Launchy.open("https://twitter.com/#{screen_name}", :dry_run => options.fetch('dry_run', false))
168
341
  end
169
342
 
170
- desc "reply SCREEN_NAME MESSAGE", "Post your Tweet as a reply directed at another person."
343
+ desc "reply STATUS_ID MESSAGE", "Post your Tweet as a reply directed at another person."
171
344
  method_option :location, :aliases => "-l", :type => :boolean, :default => false
172
- def reply(screen_name, message)
173
- screen_name = screen_name.strip_at
174
- defaults = {:include_entities => false, :trim_user => true}
175
- defaults.merge!(:lat => location.lat, :long => location.lng) if options['location']
176
- user = client.user(screen_name, :include_entities => false)
177
- defaults.merge!(:in_reply_to_status_id => user.status.id) if user.status
178
- status = client.update("@#{user.screen_name} #{message}", defaults)
179
- say "Reply created by @#{@rcfile.default_profile[0]} to @#{user.screen_name} (#{time_ago_in_words(status.created_at)} ago)"
345
+ def reply(status_id, message)
346
+ status = client.status(status_id, :include_entities => false, :include_my_retweet => false, :trim_user => true)
347
+ opts = {:in_reply_to_status_id => status.id, :include_entities => false, :trim_user => true}
348
+ opts.merge!(:lat => location.lat, :long => location.lng) if options['location']
349
+ reply = client.update("@#{status.user.screen_name} #{message}", opts)
350
+ say "Reply created by @#{@rcfile.default_profile[0]} to @#{status.user.screen_name} (#{time_ago_in_words(reply.created_at)} ago)."
180
351
  say
181
- say "Run `#{File.basename($0)} delete status` to delete."
352
+ say "Run `#{File.basename($0)} delete status #{reply.id}` to delete."
182
353
  end
183
354
 
184
- desc "retweet SCREEN_NAME", "Sends that user's latest Tweet to your followers."
185
- def retweet(screen_name)
186
- screen_name = screen_name.strip_at
187
- user = client.user(screen_name, :include_entities => false)
188
- if user.status
189
- client.retweet(user.status.id, :include_entities => false, :trim_user => true)
190
- say "@#{@rcfile.default_profile[0]} retweeted @#{user.screen_name}'s latest status: \"#{user.status.text}\""
191
- say
192
- say "Run `#{File.basename($0)} delete status` to undo."
193
- else
194
- raise Thor::Error, "Tweet not found"
355
+ desc "report_spam SCREEN_NAME [SCREEN_NAME...]", "Report users for spam."
356
+ def report_spam(screen_name, *screen_names)
357
+ screen_names.unshift(screen_name)
358
+ screen_names.threaded_each do |screen_name|
359
+ screen_name.strip_at
360
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
361
+ client.report_spam(screen_name, :include_entities => false)
362
+ end
195
363
  end
196
- rescue Twitter::Error::Forbidden => error
197
- if error.message =~ /sharing is not permissable for this status \(Share validations failed\)/
198
- say "@#{@rcfile.default_profile[0]} retweeted @#{user.screen_name}'s latest status: \"#{user.status.text}\""
199
- else
200
- raise
364
+ say "@#{@rcfile.default_profile[0]} reported @#{screen_names.join(' ')}."
365
+ end
366
+ map %w(report spam) => :report_spam
367
+
368
+ desc "retweet STATUS_ID [STATUS_ID...]", "Sends Tweets to your followers."
369
+ def retweet(status_id, *status_ids)
370
+ status_ids.unshift(status_id)
371
+ retweets = status_ids.threaded_map do |status_id|
372
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
373
+ client.retweet(status_id, :include_entities => false, :trim_user => true)
374
+ end
375
+ end
376
+ retweets.each do |status|
377
+ say "@#{@rcfile.default_profile[0]} retweeted @#{status.user.screen_name}'s status: \"#{status.text.gsub(/\n+/, ' ')}\""
201
378
  end
379
+ say
380
+ say "Run `#{File.basename($0)} delete status #{status_ids.join(' ')}` to undo."
202
381
  end
203
382
  map %w(rt) => :retweet
204
383
 
205
384
  desc "retweets [SCREEN_NAME]", "Returns the #{DEFAULT_NUM_RESULTS} most recent Retweets by a user."
385
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
206
386
  method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
207
387
  method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
208
388
  def retweets(screen_name=nil)
209
389
  screen_name = screen_name.strip_at if screen_name
210
- defaults = {:include_entities => false}
211
- defaults.merge!(:count => options['number']) if options['number']
212
- timeline = client.retweeted_by(screen_name, defaults)
213
- timeline.reverse! if options['reverse']
214
- page unless T.env.test?
215
- timeline.each do |status|
216
- say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
217
- end
390
+ count = options['number'] || DEFAULT_NUM_RESULTS
391
+ statuses = client.retweeted_by(screen_name, :count => count, :include_entities => false)
392
+ print_status_list(statuses)
218
393
  end
219
394
  map %w(rts) => :retweets
220
395
 
221
- desc "sent_messages", "Returns the #{DEFAULT_NUM_RESULTS} most recent Direct Messages sent to you."
222
- method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
223
- def sent_messages
224
- defaults = {:include_entities => false}
225
- defaults.merge!(:count => options['number']) if options['number']
226
- page unless T.env.test?
227
- client.direct_messages_sent(defaults).each do |direct_message|
228
- say "#{direct_message.recipient.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{direct_message.text} (#{time_ago_in_words(direct_message.created_at)} ago)"
229
- end
230
- end
231
- map %w(sms) => :sent_messages
232
-
233
- desc "stats SCREEN_NAME", "Retrieves the given user's number of followers and how many people they're following."
234
- def stats(screen_name)
235
- screen_name = screen_name.strip_at
236
- user = client.user(screen_name, :include_entities => false)
237
- say "Tweets: #{number_with_delimiter(user.statuses_count)}"
238
- say "Following: #{number_with_delimiter(user.friends_count)}"
239
- say "Followers: #{number_with_delimiter(user.followers_count)}"
240
- say "Favorites: #{number_with_delimiter(user.favorites_count)}"
241
- say "Listed: #{number_with_delimiter(user.listed_count)}"
242
- say
243
- say "Run `#{File.basename($0)} whois #{user.screen_name}` to view profile."
244
- end
245
-
246
396
  desc "status MESSAGE", "Post a Tweet."
247
397
  method_option :location, :aliases => "-l", :type => :boolean, :default => false
248
398
  def status(message)
249
- defaults = {:include_entities => false, :trim_user => true}
250
- defaults.merge!(:lat => location.lat, :long => location.lng) if options['location']
251
- status = client.update(message, defaults)
252
- say "Tweet created by @#{@rcfile.default_profile[0]} (#{time_ago_in_words(status.created_at)} ago)"
399
+ opts = {:include_entities => false, :trim_user => true}
400
+ opts.merge!(:lat => location.lat, :long => location.lng) if options['location']
401
+ status = client.update(message, opts)
402
+ say "Tweet created by @#{@rcfile.default_profile[0]} (#{time_ago_in_words(status.created_at)} ago)."
253
403
  say
254
- say "Run `#{File.basename($0)} delete status` to delete."
404
+ say "Run `#{File.basename($0)} delete status #{status.id}` to delete."
255
405
  end
256
406
  map %w(post tweet update) => :status
257
407
 
258
408
  desc "suggest", "This command returns a listing of Twitter users' accounts we think you might enjoy following."
409
+ method_option :created, :aliases => "-c", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter acount was created."
410
+ method_option :friends, :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by total number of friends."
411
+ method_option :listed, :aliases => "-i", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
412
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
413
+ method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
414
+ method_option :followers, :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by total number of followers."
415
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
416
+ method_option :tweets, :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by total number of Tweets."
417
+ method_option :unsorted, :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
418
+ method_option :favorites, :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by total number of favorites."
259
419
  def suggest
260
- recommendation = client.recommendations(:limit => 1, :include_entities => false).first
261
- if recommendation
262
- say "Try following @#{recommendation.screen_name}."
263
- say
264
- say "Run `#{File.basename($0)} follow #{recommendation.screen_name}` to follow."
265
- say "Run `#{File.basename($0)} whois #{recommendation.screen_name}` for profile."
266
- say "Run `#{File.basename($0)} suggest` for another recommendation."
267
- end
420
+ limit = options['number'] || DEFAULT_NUM_RESULTS
421
+ users = client.recommendations(:limit => limit, :include_entities => false)
422
+ print_user_list(users)
268
423
  end
269
424
 
270
425
  desc "timeline [SCREEN_NAME]", "Returns the #{DEFAULT_NUM_RESULTS} most recent Tweets posted by a user."
426
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
271
427
  method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
272
428
  method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
273
429
  def timeline(screen_name=nil)
274
- defaults = {:include_entities => false}
275
- defaults.merge!(:count => options['number']) if options['number']
430
+ count = options['number'] || DEFAULT_NUM_RESULTS
276
431
  if screen_name
277
432
  screen_name = screen_name.strip_at
278
- timeline = client.user_timeline(screen_name, defaults)
433
+ statuses = client.user_timeline(screen_name, :count => count, :include_entities => false)
279
434
  else
280
- timeline = client.home_timeline(defaults)
281
- end
282
- timeline.reverse! if options['reverse']
283
- page unless T.env.test?
284
- timeline.each do |status|
285
- say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
435
+ statuses = client.home_timeline(:count => count, :include_entities => false)
286
436
  end
437
+ print_status_list(statuses)
287
438
  end
288
439
  map %w(tl) => :timeline
289
440
 
441
+ desc "unfollow SCREEN_NAME [SCREEN_NAME...]", "Allows you to stop following users."
442
+ def unfollow(screen_name, *screen_names)
443
+ screen_names.unshift(screen_name)
444
+ screen_names.threaded_each do |screen_name|
445
+ retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
446
+ client.unfollow(screen_name, :include_entities => false)
447
+ end
448
+ end
449
+ number = screen_names.length
450
+ say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
451
+ say
452
+ say "Run `#{File.basename($0)} follow users #{screen_names.join(' ')}` to follow again."
453
+ end
454
+
455
+ desc "users SCREEN_NAME [SCREEN_NAME...]", "Returns a list of users you specify."
456
+ method_option :created, :aliases => "-c", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter acount was created."
457
+ method_option :friends, :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by total number of friends."
458
+ method_option :followers, :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by total number of followers."
459
+ method_option :listed, :aliases => "-i", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
460
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
461
+ method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
462
+ method_option :reverse, :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
463
+ method_option :tweets, :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by total number of Tweets."
464
+ method_option :unsorted, :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
465
+ method_option :favorites, :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by total number of favorites."
466
+ def users(screen_name, *screen_names)
467
+ screen_names.unshift(screen_name)
468
+ users = client.users(screen_names, :include_entities => false)
469
+ print_user_list(users)
470
+ end
471
+ map %w(stats) => :users
472
+
290
473
  desc "version", "Show version."
291
474
  def version
292
475
  say T::Version
@@ -304,30 +487,19 @@ module T
304
487
  say "web: #{user.url}"
305
488
  end
306
489
 
307
- require 't/cli/delete'
308
490
  desc "delete SUBCOMMAND ...ARGS", "Delete Tweets, Direct Messages, etc."
309
- method_option :force, :aliases => "-f", :type => :boolean
310
- subcommand 'delete', CLI::Delete
491
+ method_option :force, :aliases => "-f", :type => :boolean, :default => false
492
+ subcommand 'delete', T::Delete
311
493
 
312
- require 't/cli/follow'
313
- desc "follow SUBCOMMAND ...ARGS", "Follow users."
314
- subcommand 'follow', CLI::Follow
315
-
316
- require 't/cli/list'
317
494
  desc "list SUBCOMMAND ...ARGS", "Do various things with lists."
318
- subcommand 'list', CLI::List
495
+ subcommand 'list', T::List
319
496
 
320
- require 't/cli/search'
321
497
  desc "search SUBCOMMAND ...ARGS", "Search through Tweets."
322
- subcommand 'search', CLI::Search
498
+ method_option :long, :aliases => "-l", :type => :boolean, :default => false, :desc => "List in long format."
499
+ subcommand 'search', T::Search
323
500
 
324
- require 't/cli/set'
325
501
  desc "set SUBCOMMAND ...ARGS", "Change various account settings."
326
- subcommand 'set', CLI::Set
327
-
328
- require 't/cli/unfollow'
329
- desc "unfollow SUBCOMMAND ...ARGS", "Unfollow users."
330
- subcommand 'unfollow', CLI::Unfollow
502
+ subcommand 'set', T::Set
331
503
 
332
504
  private
333
505
 
@@ -383,6 +555,72 @@ module T
383
555
  {:oauth_callback => 'oob'}
384
556
  end
385
557
 
558
+ def print_in_columns(array)
559
+ cols = HighLine::SystemExtensions.terminal_size[0]
560
+ width = (array.map{|el| el.to_s.size}.max || 0) + 2
561
+ array.each_with_index do |value, index|
562
+ puts if (((index) % (cols / width))).zero? && !index.zero?
563
+ printf("%-#{width}s", value)
564
+ end
565
+ puts
566
+ end
567
+
568
+ def print_status_list(statuses)
569
+ statuses.reverse! if options['reverse']
570
+ if options['long']
571
+ array = statuses.map do |status|
572
+ created_at = status.created_at > 6.months.ago ? status.created_at.strftime("%b %e %H:%M") : status.created_at.strftime("%b %e %Y")
573
+ [status.id.to_s, created_at, status.user.screen_name, status.text.gsub(/\n+/, ' ')]
574
+ end
575
+ if STDOUT.tty?
576
+ headings = ["ID", "Created at", "Screen name", "Text"]
577
+ array.unshift(headings)
578
+ end
579
+ print_table(array)
580
+ else
581
+ statuses.each do |status|
582
+ say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text.gsub(/\n+/, ' ')} (#{time_ago_in_words(status.created_at)} ago)"
583
+ end
584
+ end
585
+ end
586
+
587
+ def print_user_list(users)
588
+ users = users.sort_by{|user| user.screen_name.downcase} unless options['unsorted']
589
+ if options['created']
590
+ users = users.sort_by{|user| user.created_at}
591
+ elsif options['favorites']
592
+ users = users.sort_by{|user| user.favourites_count}
593
+ elsif options['followers']
594
+ users = users.sort_by{|user| user.followers_count}
595
+ elsif options['friends']
596
+ users = users.sort_by{|user| user.friends_count}
597
+ elsif options['listed']
598
+ users = users.sort_by{|user| user.listed_count}
599
+ elsif options['tweets']
600
+ users = users.sort_by{|user| user.statuses_count}
601
+ end
602
+ users.reverse! if options['reverse']
603
+ if options['long']
604
+ array = users.map do |user|
605
+ created_at = user.created_at > 6.months.ago ? user.created_at.strftime("%b %e %H:%M") : user.created_at.strftime("%b %e %Y")
606
+ [user.id, created_at, user.statuses_count, user.friends_count, user.followers_count, user.favourites_count, user.screen_name, user.name]
607
+ end
608
+ if STDOUT.tty?
609
+ headings = ["ID", "Created at", "Tweets", "Following", "Followers", "Favorites", "Listed", "Screen name", "Name"]
610
+ array.unshift(headings)
611
+ end
612
+ print_table(array)
613
+ else
614
+ if STDOUT.tty?
615
+ print_in_columns(users.map(&:screen_name))
616
+ else
617
+ users.map(&:screen_name).each do |user|
618
+ say user
619
+ end
620
+ end
621
+ end
622
+ end
623
+
386
624
  def protocol
387
625
  options['no_ssl'] ? 'http' : DEFAULT_PROTOCOL
388
626
  end