t 0.4.0 → 0.5.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.
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