t 0.9.9 → 1.0.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/README.md CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  [icon]: https://github.com/sferik/t/raw/master/icon/t.png
4
4
 
5
- # Twitter CLI [![Build Status](https://secure.travis-ci.org/sferik/t.png?branch=master)][travis] [![Dependency Status](https://gemnasium.com/sferik/t.png?travis)][gemnasium] [![Click here to make a donation to T](http://www.pledgie.com/campaigns/17330.png)][pledgie]
5
+ # Twitter CLI
6
+ [![Build Status](https://secure.travis-ci.org/sferik/t.png?branch=master)][travis]
7
+ [![Dependency Status](https://gemnasium.com/sferik/t.png?travis)][gemnasium]
8
+ [![Click here to make a donation to T](http://www.pledgie.com/campaigns/17330.png)][pledgie]
6
9
 
7
10
  ### A command-line power tool for Twitter.
8
11
 
@@ -15,28 +18,53 @@ offers vastly more commands and capabilities than are available via SMS.
15
18
  [sms]: https://support.twitter.com/articles/14020-twitter-sms-command
16
19
 
17
20
  ## Installation
18
- gem install t # Requires Ruby :)
21
+
22
+ First, make sure you have Ruby installed.
23
+
24
+ **On a Mac**, open `/Applications/Utilities/Terminal.app` and type:
25
+
26
+ ruby -v
27
+
28
+ If the output looks something like this, you're in good shape:
29
+
30
+ ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-darwin12.0.0]
31
+
32
+ If the output looks more like this, you need to [install Ruby][ruby]:
33
+
34
+ ruby: command not found
35
+
36
+ **On Windows**, you can install Ruby with [RubyInstaller][].
37
+
38
+ Once you've verified that Ruby is installed:
39
+
40
+ gem install t
41
+
42
+ [ruby]: http://www.ruby-lang.org/en/downloads/
43
+ [rubyinstaller]: http://rubyinstaller.org/
19
44
 
20
45
  ## Configuration
21
46
 
22
- Twitter requires OAuth for most of its functionality, so you'll need to
23
- register a new application at <http://dev.twitter.com/apps/new>. Once you
24
- create your application, make sure to set your application's Access Level to
25
- "Read, Write and Access direct messages", otherwise you may receive an error
26
- that looks something like this:
47
+ Twitter requires OAuth for most of its functionality, so you'll need a
48
+ registered Twitter application. If you've never registered a Twitter
49
+ application before, it's easy! Just sign-in using your Twitter account and the
50
+ fill out the short form at <http://dev.twitter.com/apps/new>. If you've
51
+ previously registered a Twitter application, it should be listed at
52
+ <http://dev.twitter.com/apps>. Once you've registered an application, make sure
53
+ to set your application's Access Level to "Read, Write and Access direct
54
+ messages", otherwise you'll receive an error that looks like this:
27
55
 
28
56
  Read-only application cannot POST
29
57
 
30
- Once you've successfully registered your application, you'll be given a
31
- consumer key and secret, which you can use to authorize your Twitter account.
58
+ Now, you're ready to authorize a Twitter account with your application. To
59
+ proceed, type the following command at the prompt and follow the instructions:
32
60
 
33
- t authorize -c YOUR_CONSUMER_KEY -s YOUR_CONSUMER_SECRET
61
+ t authorize
34
62
 
35
- This command directs you to a URL where you can sign-in to Twitter and then
36
- enter the returned PIN back into the terminal. If you type the PIN correctly,
37
- you should now be authorized to use `t` as that user. To authorize multiple
38
- accounts, simply repeat the last step, signing into Twitter as a different
39
- user.
63
+ This command will direct you to a URL where you can sign-in to Twitter,
64
+ authorize the application, and then enter the returned PIN back into the
65
+ terminal. If you type the PIN correctly, you should now be authorized to use
66
+ `t` as that user. To authorize multiple accounts, simply repeat the last step,
67
+ signing into Twitter as a different user.
40
68
 
41
69
  You can see a list of all the accounts you've authorized by typing the command:
42
70
 
@@ -108,14 +136,17 @@ example, send a user a direct message only if he already follows you:
108
136
  t lists -l
109
137
 
110
138
  ### List all your friends, in long format, ordered by number of followers
111
- t friends -lf
139
+ t friends -l --sort=followers
112
140
 
113
141
  ### List all your leaders (people you follow who don't follow you back)
114
- t leaders -lf
142
+ t leaders -l --sort=followers
115
143
 
116
144
  ### Unfollow everyone you follow who doesn't follow you back
117
145
  t leaders | xargs t unfollow
118
146
 
147
+ ### Unfollow 10 people who haven't tweeted in the longest time
148
+ t followings -l --sort=tweeted | head -10 | awk '{print $1}' | xargs t unfollow
149
+
119
150
  ### Twitter roulette: randomly follow someone who follows you (who you don't already follow)
120
151
  t groupies | shuf | head -1 | xargs t follow
121
152
 
@@ -154,12 +185,12 @@ example, send a user a direct message only if he already follows you:
154
185
 
155
186
  ## Features
156
187
  * Deep search: Instead of using the Twitter Search API, [which only only goes
157
- back 6-9 days][index], `t search` fetches up to 3,200 tweets via the REST API
188
+ back 6-9 days][search], `t search` fetches up to 3,200 tweets via the REST API
158
189
  and then checks each one against a regular expression.
159
- * Multithreaded: Whenever possible, Twitter API requests are made in parallel,
190
+ * Multi-threaded: Whenever possible, Twitter API requests are made in parallel,
160
191
  resulting in faster performance for bulk operations.
161
192
  * Designed for Unix: Output is designed to be piped to other Unix utilities,
162
- like grep, cut, awk, bc, wc, and xargs for advanced text processing.
193
+ like grep, comm, cut, awk, bc, wc, and xargs for advanced text processing.
163
194
  * Generate spreadsheets: Convert the output of any command to CSV format simply
164
195
  by adding the `--csv` flag.
165
196
  * 95% C0 Code Coverage: Well tested, with a 2.5:1 test-to-code ratio.
data/bin/t CHANGED
@@ -18,7 +18,8 @@ rescue OAuth::Unauthorized
18
18
  pute "Authorization failed"
19
19
  exit 1
20
20
  rescue Twitter::Error::Unauthorized => error
21
- pute "#{error.message} Run `#{$0} authorize --consumer-key=CONSUMER_KEY --consumer-secret=CONSUMER_SECRET` to authorize."
21
+ pute error.message
22
+ pute "Run `#{$0} authorize` to authorize."
22
23
  exit 1
23
24
  rescue Twitter::Error => error
24
25
  pute error.message
data/lib/t.rb CHANGED
@@ -1,16 +1,9 @@
1
- require 'active_support/string_inquirer'
2
1
  require 't/cli'
3
2
  require 'time'
4
3
 
5
4
  module T
6
5
  class << self
7
6
 
8
- attr_reader :env
9
-
10
- def env=(environment)
11
- @env = ActiveSupport::StringInquirer.new(environment)
12
- end
13
-
14
7
  # Convert time to local time by applying the `utc_offset` setting.
15
8
  def local_time(time)
16
9
  utc_offset ? (time.utc + utc_offset) : time.localtime
data/lib/t/cli.rb CHANGED
@@ -1,29 +1,28 @@
1
+ # encoding: utf-8
2
+ require 'oauth'
1
3
  require 'thor'
2
4
  require 'twitter'
5
+ require 't/collectable'
6
+ require 't/delete'
7
+ require 't/list'
8
+ require 't/printable'
9
+ require 't/rcfile'
10
+ require 't/requestable'
11
+ require 't/search'
12
+ require 't/set'
13
+ require 't/stream'
14
+ require 't/utils'
3
15
 
4
16
  module T
5
- autoload :Authorizable, 't/authorizable'
6
- autoload :Collectable, 't/collectable'
7
- autoload :Delete, 't/delete'
8
- autoload :FormatHelpers, 't/format_helpers'
9
- autoload :List, 't/list'
10
- autoload :Printable, 't/printable'
11
- autoload :RCFile, 't/rcfile'
12
- autoload :Requestable, 't/requestable'
13
- autoload :Search, 't/search'
14
- autoload :Set, 't/set'
15
- autoload :Stream, 't/stream'
16
- autoload :Version, 't/version'
17
17
  class CLI < Thor
18
- include T::Authorizable
19
18
  include T::Collectable
20
19
  include T::Printable
21
20
  include T::Requestable
22
- include T::FormatHelpers
21
+ include T::Utils
23
22
 
23
+ DEFAULT_HOST = 'api.twitter.com'
24
+ DEFAULT_PROTOCOL = 'https'
24
25
  DEFAULT_NUM_RESULTS = 20
25
- MAX_SCREEN_NAME_SIZE = 20
26
- MAX_USERS_PER_REQUEST = 100
27
26
  DIRECT_MESSAGE_HEADINGS = ["ID", "Posted at", "Screen name", "Text"]
28
27
  TREND_HEADINGS = ["WOEID", "Parent ID", "Type", "Name", "Country"]
29
28
 
@@ -32,11 +31,11 @@ module T
32
31
  option "host", :aliases => "-H", :type => :string, :default => DEFAULT_HOST, :desc => "Twitter API server"
33
32
  option "no-color", :aliases => "-N", :type => :boolean, :desc => "Disable colorization in output"
34
33
  option "no-ssl", :aliases => "-U", :type => :boolean, :default => false, :desc => "Disable SSL"
35
- option "profile", :aliases => "-P", :type => :string, :default => File.join(File.expand_path("~"), RCFile::FILE_NAME), :desc => "Path to RC file", :banner => "FILE"
34
+ option "profile", :aliases => "-P", :type => :string, :default => File.join(File.expand_path("~"), T::RCFile::FILE_NAME), :desc => "Path to RC file", :banner => "FILE"
36
35
 
37
36
  def initialize(*)
37
+ @rcfile = T::RCFile.instance
38
38
  super
39
- @rcfile = RCFile.instance
40
39
  end
41
40
 
42
41
  desc "accounts", "List accounts"
@@ -51,63 +50,75 @@ module T
51
50
  end
52
51
 
53
52
  desc "authorize", "Allows an application to request user authorization"
54
- method_option "consumer-key", :aliases => "-c", :required => true, :desc => "This can be found at https://dev.twitter.com/apps", :banner => "KEY"
55
- method_option "consumer-secret", :aliases => "-s", :required => true, :desc => "This can be found at https://dev.twitter.com/apps", :banner => "SECRET"
56
53
  method_option "display-url", :aliases => "-d", :type => :boolean, :default => false, :desc => "Display the authorization URL instead of attempting to open it."
57
- method_option "prompt", :aliases => "-p", :type => :boolean, :default => true
58
54
  def authorize
59
- request_token = consumer.get_request_token
60
- url = generate_authorize_url(request_token)
61
- if options['prompt']
62
- say "In a moment, you will be directed to the Twitter app authorization page."
63
- say "Perform the following steps to complete the authorization process:"
64
- say " 1. Sign in to Twitter"
65
- say " 2. Press \"Authorize app\""
66
- say " 3. Copy or memorize the supplied PIN"
67
- say " 4. Return to the terminal to enter the PIN"
55
+ @rcfile.path = options['profile'] if options['profile']
56
+ if @rcfile.empty?
57
+ say "Welcome! Before you can use t, you'll first need to register an"
58
+ say "application with Twitter. Just follow the steps below:"
59
+ say " 1. Sign in to the Twitter Developer site and click"
60
+ say " \"Create a new application\"."
61
+ say " 2. Complete the required fields and submit the form."
62
+ say " Note: Your application must have a unique name."
63
+ say " We recommend: \"<your handle>/t\"."
64
+ say " 3. Go to the Settings tab of your application, and change the"
65
+ say " Access setting to \"Read, Write and Access direct messages\"."
66
+ say " 4. Go to the Details tab to view the consumer key and secret,"
67
+ say " which you'll need to copy and paste below when prompted."
68
68
  say
69
- ask "Press [Enter] to open the Twitter app authorization page."
69
+ ask "Press [Enter] to open the Twitter Developer site."
70
+ say
71
+ else
72
+ say "It looks like you've already registered an application with Twitter."
73
+ say "To authorize a new account, just follow the steps below:"
74
+ say " 1. Sign in to the Twitter Developer site."
75
+ say " 2. Select the application for which you'd like to authorize an account."
76
+ say " 3. Copy and paste the consumer key and secret below when prompted."
77
+ say
78
+ ask "Press [Enter] to open the Twitter Developer site."
70
79
  say
71
80
  end
72
81
  require 'launchy'
82
+ Launchy.open("https://dev.twitter.com/apps", :dry_run => options['display-url'])
83
+ key = ask "Enter your consumer key:"
84
+ secret = ask "Enter your consumer secret:"
85
+ consumer = OAuth::Consumer.new(key, secret, :site => base_url)
86
+ request_token = consumer.get_request_token
87
+ url = generate_authorize_url(consumer, request_token)
88
+ say
89
+ say "In a moment, you will be directed to the Twitter app authorization page."
90
+ say "Perform the following steps to complete the authorization process:"
91
+ say " 1. Sign in to Twitter."
92
+ say " 2. Press \"Authorize app\"."
93
+ say " 3. Copy and paste the supplied PIN below when prompted."
94
+ say
95
+ ask "Press [Enter] to open the Twitter app authorization page."
96
+ say
73
97
  Launchy.open(url, :dry_run => options['display-url'])
74
- pin = ask "Paste in the supplied PIN:"
98
+ pin = ask "Enter the supplied PIN:"
75
99
  access_token = request_token.get_access_token(:oauth_verifier => pin.chomp)
76
100
  oauth_response = access_token.get('/1/account/verify_credentials.json')
77
101
  screen_name = oauth_response.body.match(/"screen_name"\s*:\s*"(.*?)"/).captures.first
78
- @rcfile.path = options['profile'] if options['profile']
79
102
  @rcfile[screen_name] = {
80
- options['consumer-key'] => {
103
+ key => {
81
104
  'username' => screen_name,
82
- 'consumer_key' => options['consumer-key'],
83
- 'consumer_secret' => options['consumer-secret'],
105
+ 'consumer_key' => key,
106
+ 'consumer_secret' => secret,
84
107
  'token' => access_token.token,
85
108
  'secret' => access_token.secret,
86
109
  }
87
110
  }
88
- @rcfile.active_profile = {'username' => screen_name, 'consumer_key' => options['consumer-key']}
111
+ @rcfile.active_profile = {'username' => screen_name, 'consumer_key' => key}
89
112
  say "Authorization successful."
90
113
  end
91
114
 
92
115
  desc "block USER [USER...]", "Block users."
93
116
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify input as Twitter user IDs instead of screen names."
94
117
  def block(user, *users)
95
- users.unshift(user)
96
- require 't/core_ext/string'
97
- if options['id']
98
- users.map!(&:to_i)
99
- else
100
- users.map!(&:strip_ats)
101
- end
102
- require 't/core_ext/enumerable'
103
- require 'retryable'
104
- users = users.threaded_map do |user|
105
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
106
- client.block(user)
107
- end
118
+ users, number = fetch_users(users.unshift(user), options) do |users|
119
+ client.block(users)
108
120
  end
109
- number = users.length
110
- say "@#{@rcfile.active_profile[0]} blocked #{number} #{number == 1 ? 'user' : 'users'}."
121
+ say "@#{@rcfile.active_profile[0]} blocked #{pluralize(number, 'user')}."
111
122
  say
112
123
  say "Run `#{File.basename($0)} delete block #{users.map{|user| "@#{user.screen_name}"}.join(' ')}` to unblock."
113
124
  end
@@ -139,13 +150,13 @@ module T
139
150
  print_table_with_headings(array, DIRECT_MESSAGE_HEADINGS, format)
140
151
  else
141
152
  direct_messages.each do |direct_message|
142
- 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)"
153
+ print_message(direct_message.sender.screen_name, direct_message.text)
143
154
  end
144
155
  end
145
156
  end
146
157
  map %w(directmessages dms) => :direct_messages
147
158
 
148
- desc "direct_messages_sent", "Returns the #{DEFAULT_NUM_RESULTS} most recent Direct Messages sent to you."
159
+ desc "direct_messages_sent", "Returns the #{DEFAULT_NUM_RESULTS} most recent Direct Messages you've sent."
149
160
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
150
161
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
151
162
  method_option "number", :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS, :desc => "Limit the number of results."
@@ -172,7 +183,7 @@ module T
172
183
  print_table_with_headings(array, DIRECT_MESSAGE_HEADINGS, format)
173
184
  else
174
185
  direct_messages.each do |direct_message|
175
- 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)"
186
+ print_message(direct_message.recipient.screen_name, direct_message.text)
176
187
  end
177
188
  end
178
189
  end
@@ -180,15 +191,10 @@ module T
180
191
 
181
192
  desc "groupies [USER]", "Returns the list of people who follow you but you don't follow back."
182
193
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
183
- method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
184
- method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
185
- method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
186
194
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
187
- method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
188
195
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
189
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
190
196
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
191
- method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
197
+ 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"
192
198
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
193
199
  def groupies(user=nil)
194
200
  if user
@@ -199,21 +205,21 @@ module T
199
205
  user.strip_ats
200
206
  end
201
207
  end
202
- follower_ids = collect_with_cursor do |cursor|
203
- client.follower_ids(user, :cursor => cursor)
208
+ follower_ids = Thread.new do
209
+ collect_with_cursor do |cursor|
210
+ client.follower_ids(user, :cursor => cursor)
211
+ end
204
212
  end
205
- following_ids = collect_with_cursor do |cursor|
206
- client.friend_ids(user, :cursor => cursor)
213
+ following_ids = Thread.new do
214
+ collect_with_cursor do |cursor|
215
+ client.friend_ids(user, :cursor => cursor)
216
+ end
207
217
  end
208
- disciple_ids = (follower_ids - following_ids)
209
- require 'active_support/core_ext/array/grouping'
210
- require 't/core_ext/enumerable'
218
+ disciple_ids = (follower_ids.value - following_ids.value)
211
219
  require 'retryable'
212
- users = disciple_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |disciple_id_group|
213
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
214
- client.users(disciple_id_group)
215
- end
216
- end.flatten
220
+ users = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
221
+ client.users(disciple_ids)
222
+ end
217
223
  print_users(users)
218
224
  end
219
225
  map %w(disciples) => :groupies
@@ -228,25 +234,14 @@ module T
228
234
  user.strip_ats
229
235
  end
230
236
  direct_message = client.direct_message_create(user, message)
231
- say "Direct Message sent from @#{@rcfile.active_profile[0]} to @#{direct_message.recipient.screen_name} (#{time_ago_in_words(direct_message.created_at)} ago)."
237
+ say "Direct Message sent from @#{@rcfile.active_profile[0]} to @#{direct_message.recipient.screen_name}."
232
238
  end
233
239
  map %w(d m) => :dm
234
240
 
235
241
  desc "does_contain [USER/]LIST USER", "Find out whether a list contains a user."
236
242
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
237
243
  def does_contain(list, user=nil)
238
- owner, list = list.split('/')
239
- if list.nil?
240
- list = owner
241
- owner = @rcfile.active_profile[0]
242
- else
243
- require 't/core_ext/string'
244
- owner = if options['id']
245
- client.user(owner.to_i).screen_name
246
- else
247
- owner.strip_ats
248
- end
249
- end
244
+ owner, list = extract_owner(list, options)
250
245
  if user.nil?
251
246
  user = @rcfile.active_profile[0]
252
247
  else
@@ -258,9 +253,9 @@ module T
258
253
  end
259
254
  end
260
255
  if client.list_member?(owner, list, user)
261
- say "Yes, @#{owner}/#{list} contains @#{user}."
256
+ say "Yes, #{list} contains @#{user}."
262
257
  else
263
- say "No, @#{owner}/#{list} does not contain @#{user}."
258
+ say "No, #{list} does not contain @#{user}."
264
259
  exit 1
265
260
  end
266
261
  end
@@ -297,15 +292,12 @@ module T
297
292
  def favorite(status_id, *status_ids)
298
293
  status_ids.unshift(status_id)
299
294
  status_ids.map!(&:to_i)
300
- require 't/core_ext/enumerable'
301
295
  require 'retryable'
302
- favorites = status_ids.threaded_map do |status_id|
303
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
304
- client.favorite(status_id)
305
- end
296
+ favorites = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
297
+ client.favorite(status_ids)
306
298
  end
307
299
  number = favorites.length
308
- say "@#{@rcfile.active_profile[0]} favorited #{number} #{number == 1 ? 'tweet' : 'tweets'}."
300
+ say "@#{@rcfile.active_profile[0]} favorited #{pluralize(number, 'tweet')}."
309
301
  say
310
302
  say "Run `#{File.basename($0)} delete favorite #{status_ids.join(' ')}` to unfavorite."
311
303
  end
@@ -337,37 +329,20 @@ module T
337
329
  desc "follow USER [USER...]", "Allows you to start following users."
338
330
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify input as Twitter user IDs instead of screen names."
339
331
  def follow(user, *users)
340
- users.unshift(user)
341
- require 't/core_ext/string'
342
- if options['id']
343
- users.map!(&:to_i)
344
- else
345
- users.map!(&:strip_ats)
346
- end
347
- require 't/core_ext/enumerable'
348
- require 'retryable'
349
- users = users.threaded_map do |user|
350
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
351
- client.follow(user)
352
- end
332
+ users, number = fetch_users(users.unshift(user), options) do |users|
333
+ client.follow(users)
353
334
  end
354
- number = users.length
355
- say "@#{@rcfile.active_profile[0]} is now following #{number} more #{number == 1 ? 'user' : 'users'}."
335
+ say "@#{@rcfile.active_profile[0]} is now following #{pluralize(number, 'more user')}."
356
336
  say
357
337
  say "Run `#{File.basename($0)} unfollow #{users.map{|user| "@#{user.screen_name}"}.join(' ')}` to stop."
358
338
  end
359
339
 
360
340
  desc "followings [USER]", "Returns a list of the people you follow on Twitter."
361
341
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
362
- method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
363
- method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
364
- method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
365
342
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
366
- method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
367
343
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
368
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
369
344
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
370
- method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
345
+ 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"
371
346
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
372
347
  def followings(user=nil)
373
348
  if user
@@ -381,28 +356,19 @@ module T
381
356
  following_ids = collect_with_cursor do |cursor|
382
357
  client.friend_ids(user, :cursor => cursor)
383
358
  end
384
- require 'active_support/core_ext/array/grouping'
385
- require 't/core_ext/enumerable'
386
359
  require 'retryable'
387
- users = following_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |following_id_group|
388
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
389
- client.users(following_id_group)
390
- end
391
- end.flatten
360
+ users = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
361
+ client.users(following_ids)
362
+ end
392
363
  print_users(users)
393
364
  end
394
365
 
395
366
  desc "followers [USER]", "Returns a list of the people who follow you on Twitter."
396
367
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
397
- method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
398
- method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
399
- method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
400
368
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
401
- method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
402
369
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
403
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
404
370
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
405
- method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
371
+ 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"
406
372
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
407
373
  def followers(user=nil)
408
374
  if user
@@ -416,28 +382,19 @@ module T
416
382
  follower_ids = collect_with_cursor do |cursor|
417
383
  client.follower_ids(user, :cursor => cursor)
418
384
  end
419
- require 'active_support/core_ext/array/grouping'
420
- require 't/core_ext/enumerable'
421
385
  require 'retryable'
422
- users = follower_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |follower_id_group|
423
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
424
- client.users(follower_id_group)
425
- end
426
- end.flatten
386
+ users = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
387
+ client.users(follower_ids)
388
+ end
427
389
  print_users(users)
428
390
  end
429
391
 
430
392
  desc "friends [USER]", "Returns the list of people who you follow and follow you back."
431
393
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
432
- method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
433
- method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
434
- method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
435
394
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
436
- method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
437
395
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
438
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
439
396
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
440
- method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
397
+ 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"
441
398
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
442
399
  def friends(user=nil)
443
400
  if user
@@ -448,35 +405,30 @@ module T
448
405
  user.strip_ats
449
406
  end
450
407
  end
451
- following_ids = collect_with_cursor do |cursor|
452
- client.friend_ids(user, :cursor => cursor)
408
+ following_ids = Thread.new do
409
+ collect_with_cursor do |cursor|
410
+ client.friend_ids(user, :cursor => cursor)
411
+ end
453
412
  end
454
- follower_ids = collect_with_cursor do |cursor|
455
- client.follower_ids(user, :cursor => cursor)
413
+ follower_ids = Thread.new do
414
+ collect_with_cursor do |cursor|
415
+ client.follower_ids(user, :cursor => cursor)
416
+ end
456
417
  end
457
- friend_ids = (following_ids & follower_ids)
458
- require 'active_support/core_ext/array/grouping'
459
- require 't/core_ext/enumerable'
418
+ friend_ids = (following_ids.value & follower_ids.value)
460
419
  require 'retryable'
461
- users = friend_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |friend_id_group|
462
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
463
- client.users(friend_id_group)
464
- end
465
- end.flatten
420
+ users = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
421
+ client.users(friend_ids)
422
+ end
466
423
  print_users(users)
467
424
  end
468
425
 
469
426
  desc "leaders [USER]", "Returns the list of people who you follow but don't follow you back."
470
427
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
471
- method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
472
- method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
473
- method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
474
428
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
475
- method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
476
429
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
477
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
478
430
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
479
- method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
431
+ 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"
480
432
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
481
433
  def leaders(user=nil)
482
434
  if user
@@ -487,21 +439,21 @@ module T
487
439
  user.strip_ats
488
440
  end
489
441
  end
490
- following_ids = collect_with_cursor do |cursor|
491
- client.friend_ids(user, :cursor => cursor)
442
+ following_ids = Thread.new do
443
+ collect_with_cursor do |cursor|
444
+ client.friend_ids(user, :cursor => cursor)
445
+ end
492
446
  end
493
- follower_ids = collect_with_cursor do |cursor|
494
- client.follower_ids(user, :cursor => cursor)
447
+ follower_ids = Thread.new do
448
+ collect_with_cursor do |cursor|
449
+ client.follower_ids(user, :cursor => cursor)
450
+ end
495
451
  end
496
- leader_ids = (following_ids - follower_ids)
497
- require 'active_support/core_ext/array/grouping'
498
- require 't/core_ext/enumerable'
452
+ leader_ids = (following_ids.value - follower_ids.value)
499
453
  require 'retryable'
500
- users = leader_ids.in_groups_of(MAX_USERS_PER_REQUEST, false).threaded_map do |leader_id_group|
501
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
502
- client.users(leader_id_group)
503
- end
504
- end.flatten
454
+ users = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
455
+ client.users(leader_ids)
456
+ end
505
457
  print_users(users)
506
458
  end
507
459
 
@@ -509,11 +461,8 @@ module T
509
461
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
510
462
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
511
463
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
512
- method_option "members", :aliases => "-m", :type => :boolean, :default => false, :desc => "Sort by number of members."
513
- method_option "mode", :aliases => "-o", :type => :boolean, :default => false, :desc => "Sort by mode."
514
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter list was posted."
515
464
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
516
- method_option "subscribers", :aliases => "-s", :type => :boolean, :default => false, :desc => "Sort by number of subscribers."
465
+ method_option "sort", :aliases => "-s", :type => :string, :enum => %w(members mode posted slug subscribers), :default => "slug", :desc => "Specify the order of the results.", :banner => "ORDER"
517
466
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
518
467
  def lists(user=nil)
519
468
  if user
@@ -593,11 +542,7 @@ module T
593
542
  status = client.status(status_id.to_i, :include_my_retweet => false)
594
543
  users = Array(status.from_user)
595
544
  if options['all']
596
- # twitter-text requires $KCODE to be set to UTF8 on Ruby versions < 1.8
597
- major, minor, patch = RUBY_VERSION.split('.')
598
- $KCODE='u' if major.to_i == 1 && minor.to_i < 9
599
- require 'twitter-text'
600
- users += Twitter::Extractor.extract_mentioned_screen_names(status.full_text)
545
+ users += extract_mentioned_screen_names(status.full_text)
601
546
  users.uniq!
602
547
  end
603
548
  require 't/core_ext/string'
@@ -605,7 +550,7 @@ module T
605
550
  opts = {:in_reply_to_status_id => status.id, :trim_user => true}
606
551
  opts.merge!(:lat => location.lat, :long => location.lng) if options['location']
607
552
  reply = client.update("#{users.join(' ')} #{message}", opts)
608
- say "Reply created by @#{@rcfile.active_profile[0]} to #{users.join(' ')} (#{time_ago_in_words(reply.created_at)} ago)."
553
+ say "Reply posted by @#{@rcfile.active_profile[0]} to #{users.join(' ')}."
609
554
  say
610
555
  say "Run `#{File.basename($0)} delete status #{reply.id}` to delete."
611
556
  end
@@ -613,22 +558,10 @@ module T
613
558
  desc "report_spam USER [USER...]", "Report users for spam."
614
559
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify input as Twitter user IDs instead of screen names."
615
560
  def report_spam(user, *users)
616
- users.unshift(user)
617
- require 't/core_ext/string'
618
- if options['id']
619
- users.map!(&:to_i)
620
- else
621
- users.map!(&:strip_ats)
622
- end
623
- require 't/core_ext/enumerable'
624
- require 'retryable'
625
- users = users.threaded_map do |user|
626
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
627
- client.report_spam(user)
628
- end
561
+ users, number = fetch_users(users.unshift(user), options) do |users|
562
+ client.report_spam(users)
629
563
  end
630
- number = users.length
631
- say "@#{@rcfile.active_profile[0]} reported #{number} #{number == 1 ? 'user' : 'users'}."
564
+ say "@#{@rcfile.active_profile[0]} reported #{pluralize(number, 'user')}."
632
565
  end
633
566
  map %w(report reportspam spam) => :report_spam
634
567
 
@@ -636,15 +569,12 @@ module T
636
569
  def retweet(status_id, *status_ids)
637
570
  status_ids.unshift(status_id)
638
571
  status_ids.map!(&:to_i)
639
- require 't/core_ext/enumerable'
640
572
  require 'retryable'
641
- retweets = status_ids.threaded_map do |status_id|
642
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
643
- client.retweet(status_id, :trim_user => true)
644
- end
573
+ retweets = retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
574
+ client.retweet(status_ids, :trim_user => true)
645
575
  end
646
576
  number = retweets.length
647
- say "@#{@rcfile.active_profile[0]} retweeted #{number} #{number == 1 ? 'tweet' : 'tweets'}."
577
+ say "@#{@rcfile.active_profile[0]} retweeted #{pluralize(number, 'tweet')}."
648
578
  say
649
579
  say "Run `#{File.basename($0)} delete status #{retweets.map(&:id).join(' ')}` to undo."
650
580
  end
@@ -657,17 +587,21 @@ module T
657
587
  method_option "number", :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS, :desc => "Limit the number of results."
658
588
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
659
589
  def retweets(user=nil)
660
- if user
590
+ count = options['number'] || DEFAULT_NUM_RESULTS
591
+ statuses = if user
661
592
  require 't/core_ext/string'
662
593
  user = if options['id']
663
594
  user.to_i
664
595
  else
665
596
  user.strip_ats
666
597
  end
667
- end
668
- count = options['number'] || DEFAULT_NUM_RESULTS
669
- statuses = collect_with_count(count) do |opts|
670
- client.retweeted_by(user, opts)
598
+ collect_with_count(count) do |opts|
599
+ client.retweeted_by_user(user, opts)
600
+ end
601
+ else
602
+ collect_with_count(count) do |opts|
603
+ client.retweeted_by_me(opts)
604
+ end
671
605
  end
672
606
  print_statuses(statuses)
673
607
  end
@@ -683,12 +617,12 @@ module T
683
617
  def status(status_id)
684
618
  status = client.status(status_id.to_i, :include_my_retweet => false)
685
619
  location = if status.place
686
- if status.place.name && status.place.attributes && status.place.attributes['street_address'] && status.place.attributes['locality'] && status.place.attributes['region'] && status.place.country
687
- [status.place.name, status.place.attributes['street_address'], status.place.attributes['locality'], status.place.attributes['region'], status.place.country].join(", ")
688
- elsif status.place.name && status.place.attributes && status.place.attributes['locality'] && status.place.attributes['region'] && status.place.country
689
- [status.place.name, status.place.attributes['locality'], status.place.attributes['region'], status.place.country].join(", ")
690
- elsif status.place.full_name && status.place.attributes && status.place.attributes['region'] && status.place.country
691
- [status.place.full_name, status.place.attributes['region'], status.place.country].join(", ")
620
+ if status.place.name && status.place.attributes && status.place.attributes[:street_address] && status.place.attributes[:locality] && status.place.attributes[:region] && status.place.country
621
+ [status.place.name, status.place.attributes[:street_address], status.place.attributes[:locality], status.place.attributes[:region], status.place.country].join(", ")
622
+ elsif status.place.name && status.place.attributes && status.place.attributes[:locality] && status.place.attributes[:region] && status.place.country
623
+ [status.place.name, status.place.attributes[:locality], status.place.attributes[:region], status.place.country].join(", ")
624
+ elsif status.place.full_name && status.place.attributes && status.place.attributes[:region] && status.place.country
625
+ [status.place.full_name, status.place.attributes[:region], status.place.country].join(", ")
692
626
  elsif status.place.full_name && status.place.country
693
627
  [status.place.full_name, status.place.country].join(", ")
694
628
  elsif status.place.full_name
@@ -721,16 +655,11 @@ module T
721
655
 
722
656
  desc "suggest [USER]", "Returns a listing of Twitter users' accounts you might enjoy following."
723
657
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
724
- method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
725
- method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
726
- method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
727
658
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
728
- method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
729
659
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
730
660
  method_option "number", :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS, :desc => "Limit the number of results."
731
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
732
661
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
733
- method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
662
+ 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"
734
663
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
735
664
  def suggest(user=nil)
736
665
  if user
@@ -813,22 +742,10 @@ module T
813
742
  desc "unfollow USER [USER...]", "Allows you to stop following users."
814
743
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify input as Twitter user IDs instead of screen names."
815
744
  def unfollow(user, *users)
816
- users.unshift(user)
817
- require 't/core_ext/string'
818
- if options['id']
819
- users.map!(&:to_i)
820
- else
821
- users.map!(&:strip_ats)
745
+ users, number = fetch_users(users.unshift(user), options) do |users|
746
+ client.unfollow(users)
822
747
  end
823
- require 't/core_ext/enumerable'
824
- require 'retryable'
825
- users = users.threaded_map do |user|
826
- retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
827
- client.unfollow(user)
828
- end
829
- end
830
- number = users.length
831
- say "@#{@rcfile.active_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
748
+ say "@#{@rcfile.active_profile[0]} is no longer following #{pluralize(number, 'user')}."
832
749
  say
833
750
  say "Run `#{File.basename($0)} follow #{users.map{|user| "@#{user.screen_name}"}.join(' ')}` to follow again."
834
751
  end
@@ -839,7 +756,7 @@ module T
839
756
  opts = {:trim_user => true}
840
757
  opts.merge!(:lat => location.lat, :long => location.lng) if options['location']
841
758
  status = client.update(message, opts)
842
- say "Tweet created by @#{@rcfile.active_profile[0]} (#{time_ago_in_words(status.created_at)} ago)."
759
+ say "Tweet posted by @#{@rcfile.active_profile[0]}."
843
760
  say
844
761
  say "Run `#{File.basename($0)} delete status #{status.id}` to delete."
845
762
  end
@@ -847,15 +764,10 @@ module T
847
764
 
848
765
  desc "users USER [USER...]", "Returns a list of users you specify."
849
766
  method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
850
- method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
851
- method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
852
- method_option "friends", :aliases => "-e", :type => :boolean, :default => false, :desc => "Sort by number of friends."
853
767
  method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify input as Twitter user IDs instead of screen names."
854
- method_option "listed", :aliases => "-d", :type => :boolean, :default => false, :desc => "Sort by number of list memberships."
855
768
  method_option "long", :aliases => "-l", :type => :boolean, :default => false, :desc => "Output in long format."
856
- method_option "posted", :aliases => "-p", :type => :boolean, :default => false, :desc => "Sort by the time when Twitter account was posted."
857
769
  method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
858
- method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
770
+ 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"
859
771
  method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
860
772
  def users(user, *users)
861
773
  users.unshift(user)
@@ -872,6 +784,7 @@ module T
872
784
 
873
785
  desc "version", "Show version."
874
786
  def version
787
+ require 't/version'
875
788
  say T::Version
876
789
  end
877
790
  map %w(-v --version) => :version
@@ -930,6 +843,40 @@ module T
930
843
 
931
844
  private
932
845
 
846
+ def extract_mentioned_screen_names(text)
847
+ valid_mention_preceding_chars = /(?:[^a-zA-Z0-9_!#\$%&*@@]|^|RT:?)/o
848
+ at_signs = /[@@]/
849
+ valid_mentions = /
850
+ (#{valid_mention_preceding_chars}) # $1: Preceeding character
851
+ (#{at_signs}) # $2: At mark
852
+ ([a-zA-Z0-9_]{1,20}) # $3: Screen name
853
+ /ox
854
+
855
+ return [] if text !~ at_signs
856
+
857
+ text.to_s.scan(valid_mentions).map do |before, at, screen_name|
858
+ screen_name
859
+ end
860
+ end
861
+
862
+ def base_url
863
+ "#{protocol}://#{host}"
864
+ end
865
+
866
+ def generate_authorize_url(consumer, request_token)
867
+ request = consumer.create_signed_request(:get, consumer.authorize_path, request_token, pin_auth_parameters)
868
+ params = request['Authorization'].sub(/^OAuth\s+/, '').split(/,\s+/).map do |param|
869
+ key, value = param.split('=')
870
+ value =~ /"(.*?)"/
871
+ "#{key}=#{CGI::escape($1)}"
872
+ end.join('&')
873
+ "#{base_url}#{request.path}?#{params}"
874
+ end
875
+
876
+ def pin_auth_parameters
877
+ {:oauth_callback => 'oob'}
878
+ end
879
+
933
880
  def location
934
881
  return @location if @location
935
882
  require 'geokit'