t 0.9.6 → 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +29 -10
- data/bin/t +4 -25
- data/lib/t/cli.rb +24 -23
- data/lib/t/core_ext/kernel.rb +15 -0
- data/lib/t/core_ext/string.rb +4 -2
- data/lib/t/delete.rb +13 -13
- data/lib/t/format_helpers.rb +62 -0
- data/lib/t/list.rb +4 -4
- data/lib/t/printable.rb +27 -24
- data/lib/t/rcfile.rb +1 -1
- data/lib/t/requestable.rb +0 -4
- data/lib/t/search.rb +10 -11
- data/lib/t/stream.rb +57 -21
- data/lib/t/version.rb +1 -1
- data/lib/t.rb +22 -3
- data/spec/cli_spec.rb +53 -45
- data/spec/fixtures/501_ids.json +509 -1
- data/spec/fixtures/501_users_list.json +17543 -1
- data/spec/fixtures/direct_message.json +80 -1
- data/spec/fixtures/direct_messages.json +802 -1
- data/spec/fixtures/empty_cursor.json +7 -1
- data/spec/fixtures/favorites.json +1099 -1
- data/spec/fixtures/followers_ids.json +10 -1
- data/spec/fixtures/friends_ids.json +9 -1
- data/spec/fixtures/gem.json +61 -1
- data/spec/fixtures/list.json +54 -1
- data/spec/fixtures/lists.json +116 -1
- data/spec/fixtures/locations.json +57 -1
- data/spec/fixtures/not_found.json +4 -1
- data/spec/fixtures/rate_limit_status.json +6 -1
- data/spec/fixtures/recommendations.json +364 -1
- data/spec/fixtures/retweet.json +112 -1
- data/spec/fixtures/search.json +346 -1
- data/spec/fixtures/settings.json +30 -1
- data/spec/fixtures/sferik.json +76 -1
- data/spec/fixtures/status.json +109 -1
- data/spec/fixtures/status_no_attributes.json +104 -1
- data/spec/fixtures/status_no_country.json +102 -1
- data/spec/fixtures/status_no_full_name.json +101 -1
- data/spec/fixtures/status_no_locality.json +107 -1
- data/spec/fixtures/status_no_street_address.json +108 -1
- data/spec/fixtures/statuses.json +1105 -1
- data/spec/fixtures/trends.json +35 -1
- data/spec/fixtures/users.json +92 -1
- data/spec/fixtures/users_list.json +98 -1
- data/spec/list_spec.rb +13 -5
- data/spec/search_spec.rb +110 -102
- data/t.gemspec +1 -2
- metadata +6 -32
data/README.md
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
+
# [![Application icon](https://github.com/sferik/t/raw/master/icon/t.png)][icon]
|
2
|
+
|
3
|
+
[icon]: https://github.com/sferik/t/raw/master/icon/t.png
|
4
|
+
|
1
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]
|
6
|
+
|
2
7
|
### A command-line power tool for Twitter.
|
3
8
|
|
4
9
|
The CLI takes syntactic cues from the [Twitter SMS commands][sms], however it
|
@@ -7,12 +12,10 @@ offers vastly more commands and capabilities than are available via SMS.
|
|
7
12
|
[travis]: http://travis-ci.org/sferik/t
|
8
13
|
[gemnasium]: https://gemnasium.com/sferik/t
|
9
14
|
[pledgie]: http://www.pledgie.com/campaigns/17330
|
10
|
-
[gem]: https://rubygems.org/gems/twitter
|
11
15
|
[sms]: https://support.twitter.com/articles/14020-twitter-sms-command
|
12
16
|
|
13
17
|
## Installation
|
14
|
-
# Requires Ruby :)
|
15
|
-
gem install t
|
18
|
+
gem install t # Requires Ruby :)
|
16
19
|
|
17
20
|
## Configuration
|
18
21
|
|
@@ -56,7 +59,7 @@ and consumer key pair, like this:
|
|
56
59
|
|
57
60
|
Account information is stored in a YAML-formatted file located at `~/.trc`.
|
58
61
|
|
59
|
-
**Note**: Anyone with access to this file can
|
62
|
+
**Note**: Anyone with access to this file can impersonate you on Twitter, so
|
60
63
|
it's important to keep it secure, just as you would treat your SSH private key.
|
61
64
|
For this reason, the file is hidden and has the permission bits set to `0600`.
|
62
65
|
|
@@ -114,7 +117,7 @@ example, send a user a direct message only if he already follows you:
|
|
114
117
|
t leaders | xargs t unfollow
|
115
118
|
|
116
119
|
### Twitter roulette: randomly follow someone who follows you (who you don't already follow)
|
117
|
-
t
|
120
|
+
t groupies | shuf | head -1 | xargs t follow
|
118
121
|
|
119
122
|
### Favorite the last 10 tweets that mention you
|
120
123
|
t mentions -n 10 -l | awk '{print $1}' | xargs t favorite
|
@@ -163,6 +166,23 @@ example, send a user a direct message only if he already follows you:
|
|
163
166
|
|
164
167
|
[search]: https://dev.twitter.com/docs/using-search
|
165
168
|
|
169
|
+
# Using T for Backup
|
170
|
+
|
171
|
+
[@jphpsf][jphpsf] wrote a [blog post][blog] explaining how to use `t` to backup
|
172
|
+
your Twitter account.
|
173
|
+
|
174
|
+
[jphpsf]: https://github.com/jphpsf
|
175
|
+
[blog]: http://blog.jphpsf.com/2012/05/07/backing-up-your-twitter-account-with-t/
|
176
|
+
|
177
|
+
`t` was also mentioned on [an episode of the Ruby 5 podcast][ruby5].
|
178
|
+
|
179
|
+
[ruby5]: http://ruby5.envylabs.com/episodes/273-episode-269-may-4th-2012/stories/2400-t-command-line-power-tool-for-twitter
|
180
|
+
|
181
|
+
If you discuss `t` in a blog post or podcast, [let me know][email] and I'll
|
182
|
+
link it here.
|
183
|
+
|
184
|
+
[email]: mailto:sferik@gmail.com
|
185
|
+
|
166
186
|
## Relationship Terminology
|
167
187
|
|
168
188
|
There is some ambiguity in the terminology used to describe relationships on
|
@@ -176,7 +196,7 @@ used by `t`:
|
|
176
196
|
| YOU FOLLOW THEM | YOU DON'T FOLLOW THEM |
|
177
197
|
_________________________|_________________________|_________________________|_________________________
|
178
198
|
| | | | |
|
179
|
-
| THEY FOLLOW YOU | friends |
|
199
|
+
| THEY FOLLOW YOU | friends | groupies | followers |
|
180
200
|
|_________________________|_________________________|_________________________|_________________________|
|
181
201
|
| | |
|
182
202
|
| THEY DON'T FOLLOW YOU | leaders |
|
@@ -195,6 +215,7 @@ version 0.5.0, when it was [removed][]. This project is offered as a sucessor
|
|
195
215
|
to that effort, however it is a clean room implementation that contains none of
|
196
216
|
the original code.
|
197
217
|
|
218
|
+
[gem]: https://rubygems.org/gems/twitter
|
198
219
|
[removed]: https://github.com/jnunemaker/twitter/commit/dd2445e3e2c97f38b28a3f32ea902536b3897adf
|
199
220
|
![History](https://github.com/sferik/t/raw/master/screenshots/history.png)
|
200
221
|
|
@@ -253,9 +274,6 @@ implementations:
|
|
253
274
|
* Ruby 1.8.7
|
254
275
|
* Ruby 1.9.2
|
255
276
|
* Ruby 1.9.3
|
256
|
-
* [Rubinius][]
|
257
|
-
|
258
|
-
[rubinius]: http://rubini.us/
|
259
277
|
|
260
278
|
If something doesn't work on one of these Ruby versions, it's a bug.
|
261
279
|
|
@@ -272,5 +290,6 @@ of a major release, support for that Ruby version may be dropped.
|
|
272
290
|
|
273
291
|
## Copyright
|
274
292
|
Copyright (c) 2011 Erik Michaels-Ober. See [LICENSE][] for details.
|
275
|
-
|
293
|
+
Application icon by [@nvk][nvk].
|
276
294
|
[license]: https://github.com/sferik/t/blob/master/LICENSE.md
|
295
|
+
[nvk]: http://rodolfonovak.com
|
data/bin/t
CHANGED
@@ -12,34 +12,13 @@ begin
|
|
12
12
|
rescue Interrupt
|
13
13
|
pute "Quitting..."
|
14
14
|
exit 1
|
15
|
-
rescue Twitter::Error::BadRequest => error
|
16
|
-
pute error.message
|
17
|
-
exit 400
|
18
15
|
rescue OAuth::Unauthorized
|
19
16
|
pute "Authorization failed"
|
20
|
-
exit
|
17
|
+
exit 1
|
21
18
|
rescue Twitter::Error::Unauthorized => error
|
22
19
|
pute "#{error.message} Run `#{$0} authorize --consumer-key=CONSUMER_KEY --consumer-secret=CONSUMER_SECRET` to authorize."
|
23
|
-
exit
|
24
|
-
rescue Twitter::Error
|
25
|
-
pute error.message
|
26
|
-
exit 403
|
27
|
-
rescue Twitter::Error::NotFound => error
|
28
|
-
pute error.message
|
29
|
-
exit 404
|
30
|
-
rescue Twitter::Error::NotAcceptable => error
|
31
|
-
pute error.message
|
32
|
-
exit 406
|
33
|
-
rescue Twitter::Error::EnhanceYourCalm => error
|
34
|
-
pute error.message
|
35
|
-
exit 420
|
36
|
-
rescue Twitter::Error::InternalServerError => error
|
37
|
-
pute error.message
|
38
|
-
exit 500
|
39
|
-
rescue Twitter::Error::BadGateway => error
|
40
|
-
pute error.message
|
41
|
-
exit 502
|
42
|
-
rescue Twitter::Error::ServiceUnavailable => error
|
20
|
+
exit 1
|
21
|
+
rescue Twitter::Error => error
|
43
22
|
pute error.message
|
44
|
-
exit
|
23
|
+
exit 1
|
45
24
|
end
|
data/lib/t/cli.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require 'action_view'
|
2
1
|
require 'active_support/core_ext/array/grouping'
|
3
2
|
require 'active_support/core_ext/date/calculations'
|
4
3
|
require 'active_support/core_ext/integer/time'
|
5
4
|
require 'active_support/core_ext/numeric/time'
|
5
|
+
require 't/format_helpers'
|
6
6
|
require 'csv'
|
7
7
|
# 'fastercsv' required on Ruby versions < 1.9
|
8
8
|
require 'fastercsv' unless Array.new.respond_to?(:to_csv)
|
@@ -27,17 +27,17 @@ require 'yaml'
|
|
27
27
|
|
28
28
|
module T
|
29
29
|
class CLI < Thor
|
30
|
-
include ActionView::Helpers::DateHelper
|
31
|
-
include ActionView::Helpers::NumberHelper
|
32
|
-
include ActionView::Helpers::TextHelper
|
33
30
|
include T::Authorizable
|
34
31
|
include T::Collectable
|
35
32
|
include T::Printable
|
36
33
|
include T::Requestable
|
34
|
+
include T::FormatHelpers
|
37
35
|
|
38
36
|
DEFAULT_NUM_RESULTS = 20
|
39
37
|
MAX_SCREEN_NAME_SIZE = 20
|
40
38
|
MAX_USERS_PER_REQUEST = 100
|
39
|
+
DIRECT_MESSAGE_HEADINGS = ["ID", "Posted at", "Screen name", "Text"]
|
40
|
+
TREND_HEADINGS = ["WOEID", "Parent ID", "Type", "Name", "Country"]
|
41
41
|
|
42
42
|
check_unknown_options!
|
43
43
|
|
@@ -141,7 +141,8 @@ module T
|
|
141
141
|
array = direct_messages.map do |direct_message|
|
142
142
|
[direct_message.id, ls_formatted_time(direct_message), "@#{direct_message.sender.screen_name}", HTMLEntities.new.decode(direct_message.text).gsub(/\n+/, ' ')]
|
143
143
|
end
|
144
|
-
|
144
|
+
format = options['format'] || DIRECT_MESSAGE_HEADINGS.size.times.map{"%s"}
|
145
|
+
print_table_with_headings(array, DIRECT_MESSAGE_HEADINGS, format)
|
145
146
|
else
|
146
147
|
direct_messages.each do |direct_message|
|
147
148
|
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)"
|
@@ -170,7 +171,8 @@ module T
|
|
170
171
|
array = direct_messages.map do |direct_message|
|
171
172
|
[direct_message.id, ls_formatted_time(direct_message), "@#{direct_message.recipient.screen_name}", HTMLEntities.new.decode(direct_message.text).gsub(/\n+/, ' ')]
|
172
173
|
end
|
173
|
-
|
174
|
+
format = options['format'] || DIRECT_MESSAGE_HEADINGS.size.times.map{"%s"}
|
175
|
+
print_table_with_headings(array, DIRECT_MESSAGE_HEADINGS, format)
|
174
176
|
else
|
175
177
|
direct_messages.each do |direct_message|
|
176
178
|
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)"
|
@@ -179,7 +181,7 @@ module T
|
|
179
181
|
end
|
180
182
|
map %w(directmessagessent sent_messages sentmessages sms) => :direct_messages_sent
|
181
183
|
|
182
|
-
desc "
|
184
|
+
desc "groupies [USER]", "Returns the list of people who follow you but you don't follow back."
|
183
185
|
method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
|
184
186
|
method_option "favorites", :aliases => "-v", :type => :boolean, :default => false, :desc => "Sort by number of favorites."
|
185
187
|
method_option "followers", :aliases => "-f", :type => :boolean, :default => false, :desc => "Sort by number of followers."
|
@@ -191,7 +193,7 @@ module T
|
|
191
193
|
method_option "reverse", :aliases => "-r", :type => :boolean, :default => false, :desc => "Reverse the order of the sort."
|
192
194
|
method_option "tweets", :aliases => "-t", :type => :boolean, :default => false, :desc => "Sort by number of Tweets."
|
193
195
|
method_option "unsorted", :aliases => "-u", :type => :boolean, :default => false, :desc => "Output is not sorted."
|
194
|
-
def
|
196
|
+
def groupies(user=nil)
|
195
197
|
if user
|
196
198
|
user = if options['id']
|
197
199
|
user.to_i
|
@@ -213,6 +215,7 @@ module T
|
|
213
215
|
end.flatten
|
214
216
|
print_users(users)
|
215
217
|
end
|
218
|
+
map %w(disciples) => :groupies
|
216
219
|
|
217
220
|
desc "dm USER MESSAGE", "Sends that person a Direct Message."
|
218
221
|
method_option "id", :aliases => "-i", :type => "boolean", :default => false, :desc => "Specify user via ID instead of screen name."
|
@@ -288,10 +291,10 @@ module T
|
|
288
291
|
desc "favorite STATUS_ID [STATUS_ID...]", "Marks Tweets as favorites."
|
289
292
|
def favorite(status_id, *status_ids)
|
290
293
|
status_ids.unshift(status_id)
|
291
|
-
status_ids.map!(&:
|
294
|
+
status_ids.map!(&:to_i)
|
292
295
|
favorites = status_ids.threaded_map do |status_id|
|
293
296
|
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
294
|
-
client.favorite(status_id
|
297
|
+
client.favorite(status_id)
|
295
298
|
end
|
296
299
|
end
|
297
300
|
number = favorites.length
|
@@ -546,7 +549,7 @@ module T
|
|
546
549
|
array = []
|
547
550
|
array << ["Hourly limit", number_with_delimiter(rate_limit_status.hourly_limit)]
|
548
551
|
array << ["Remaining hits", number_with_delimiter(rate_limit_status.remaining_hits)]
|
549
|
-
array << ["Reset time", ls_formatted_time(rate_limit_status, :reset_time)]
|
552
|
+
array << ["Reset time", "#{ls_formatted_time(rate_limit_status, :reset_time)} (#{distance_of_time_in_words_to_now(rate_limit_status.reset_time)} from now)"]
|
550
553
|
print_table(array)
|
551
554
|
end
|
552
555
|
end
|
@@ -556,7 +559,6 @@ module T
|
|
556
559
|
method_option "all", :aliases => "-a", :type => "boolean", :default => false, :desc => "Reply to all users mentioned in the Tweet."
|
557
560
|
method_option "location", :aliases => "-l", :type => :boolean, :default => false
|
558
561
|
def reply(status_id, message)
|
559
|
-
status_id = status_id.strip_commas
|
560
562
|
status = client.status(status_id.to_i, :include_my_retweet => false)
|
561
563
|
users = Array(status.from_user)
|
562
564
|
if options['all']
|
@@ -564,7 +566,7 @@ module T
|
|
564
566
|
major, minor, patch = RUBY_VERSION.split('.')
|
565
567
|
$KCODE='u' if major.to_i == 1 && minor.to_i < 9
|
566
568
|
require 'twitter-text'
|
567
|
-
users += Twitter::Extractor.extract_mentioned_screen_names(status.
|
569
|
+
users += Twitter::Extractor.extract_mentioned_screen_names(status.full_text)
|
568
570
|
users.uniq!
|
569
571
|
end
|
570
572
|
users.map!(&:prepend_at)
|
@@ -598,16 +600,16 @@ module T
|
|
598
600
|
desc "retweet STATUS_ID [STATUS_ID...]", "Sends Tweets to your followers."
|
599
601
|
def retweet(status_id, *status_ids)
|
600
602
|
status_ids.unshift(status_id)
|
601
|
-
status_ids.map!(&:
|
603
|
+
status_ids.map!(&:to_i)
|
602
604
|
retweets = status_ids.threaded_map do |status_id|
|
603
605
|
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
604
|
-
client.retweet(status_id
|
606
|
+
client.retweet(status_id, :trim_user => true)
|
605
607
|
end
|
606
608
|
end
|
607
609
|
number = retweets.length
|
608
610
|
say "@#{@rcfile.active_profile[0]} retweeted #{number} #{number == 1 ? 'tweet' : 'tweets'}."
|
609
611
|
say
|
610
|
-
say "Run `#{File.basename($0)} delete status #{
|
612
|
+
say "Run `#{File.basename($0)} delete status #{retweets.map(&:id).join(' ')}` to undo."
|
611
613
|
end
|
612
614
|
map %w(rt) => :retweet
|
613
615
|
|
@@ -641,7 +643,6 @@ module T
|
|
641
643
|
desc "status STATUS_ID", "Retrieves detailed information about a Tweet."
|
642
644
|
method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
|
643
645
|
def status(status_id)
|
644
|
-
status_id = status_id.strip_commas
|
645
646
|
status = client.status(status_id.to_i, :include_my_retweet => false)
|
646
647
|
location = if status.place
|
647
648
|
if status.place.name && status.place.attributes && status.place.attributes['street_address'] && status.place.attributes['locality'] && status.place.attributes['region'] && status.place.country
|
@@ -662,14 +663,13 @@ module T
|
|
662
663
|
end
|
663
664
|
if options['csv']
|
664
665
|
say ["ID", "Text", "Screen name", "Posted at", "Location", "Retweets", "Source", "URL"].to_csv
|
665
|
-
say [status.id, HTMLEntities.new.decode(status.
|
666
|
+
say [status.id, HTMLEntities.new.decode(status.full_text), status.from_user, csv_formatted_time(status), location, status.retweet_count, strip_tags(status.source), "https://twitter.com/#{status.from_user}/status/#{status.id}"].to_csv
|
666
667
|
else
|
667
668
|
array = []
|
668
669
|
array << ["ID", status.id.to_s]
|
669
|
-
array << ["Text", HTMLEntities.new.decode(status.
|
670
|
+
array << ["Text", HTMLEntities.new.decode(status.full_text).gsub(/\n+/, ' ')]
|
670
671
|
array << ["Screen name", "@#{status.from_user}"]
|
671
|
-
|
672
|
-
array << ["Posted at", posted_at]
|
672
|
+
array << ["Posted at", "#{ls_formatted_time(status)} (#{time_ago_in_words(status.created_at)} ago)"]
|
673
673
|
array << ["Location", location] unless location.nil?
|
674
674
|
array << ["Retweets", number_with_delimiter(status.retweet_count)]
|
675
675
|
array << ["Source", strip_tags(status.source)]
|
@@ -757,7 +757,8 @@ module T
|
|
757
757
|
array = places.map do |place|
|
758
758
|
[place.woeid, place.parent_id, place.place_type, place.name, place.country]
|
759
759
|
end
|
760
|
-
|
760
|
+
format = options['format'] || TREND_HEADINGS.size.times.map{"%s"}
|
761
|
+
print_table_with_headings(array, TREND_HEADINGS, format)
|
761
762
|
else
|
762
763
|
print_attribute(places, :name)
|
763
764
|
end
|
@@ -847,7 +848,7 @@ module T
|
|
847
848
|
array << ["Location", user.location] unless user.location.nil?
|
848
849
|
array << ["Status", user.following ? "Following" : "Not following"]
|
849
850
|
array << ["Last update", "#{HTMLEntities.new.decode(user.status.text).gsub(/\n+/, ' ')} (#{time_ago_in_words(user.status.created_at)} ago)"] unless user.status.nil?
|
850
|
-
array << ["Since", ls_formatted_time(user)]
|
851
|
+
array << ["Since", "#{ls_formatted_time(user)} (#{time_ago_in_words(user.created_at)} ago)"]
|
851
852
|
array << ["Tweets", number_with_delimiter(user.statuses_count)]
|
852
853
|
array << ["Favorites", number_with_delimiter(user.favourites_count)]
|
853
854
|
array << ["Listed", number_with_delimiter(user.listed_count)]
|
data/lib/t/core_ext/string.rb
CHANGED
data/lib/t/delete.rb
CHANGED
@@ -41,13 +41,13 @@ module T
|
|
41
41
|
method_option "force", :aliases => "-f", :type => :boolean, :default => false
|
42
42
|
def dm(direct_message_id, *direct_message_ids)
|
43
43
|
direct_message_ids.unshift(direct_message_id)
|
44
|
-
direct_message_ids.map!(&:
|
44
|
+
direct_message_ids.map!(&:to_i)
|
45
45
|
direct_message_ids.each do |direct_message_id|
|
46
46
|
unless options['force']
|
47
|
-
direct_message = client.direct_message(direct_message_id
|
47
|
+
direct_message = client.direct_message(direct_message_id)
|
48
48
|
return unless yes? "Are you sure you want to permanently delete the direct message to @#{direct_message.recipient.screen_name}: \"#{direct_message.text}\"? [y/N]"
|
49
49
|
end
|
50
|
-
direct_message = client.direct_message_destroy(direct_message_id
|
50
|
+
direct_message = client.direct_message_destroy(direct_message_id)
|
51
51
|
say "@#{@rcfile.active_profile[0]} deleted the direct message sent to @#{direct_message.recipient.screen_name}: \"#{direct_message.text}\""
|
52
52
|
end
|
53
53
|
end
|
@@ -57,14 +57,14 @@ module T
|
|
57
57
|
method_option "force", :aliases => "-f", :type => :boolean, :default => false
|
58
58
|
def favorite(status_id, *status_ids)
|
59
59
|
status_ids.unshift(status_id)
|
60
|
-
status_ids.map!(&:
|
60
|
+
status_ids.map!(&:to_i)
|
61
61
|
status_ids.each do |status_id|
|
62
62
|
unless options['force']
|
63
|
-
status = client.status(status_id
|
64
|
-
return unless yes? "Are you sure you want to remove @#{status.from_user}'s status: \"#{status.
|
63
|
+
status = client.status(status_id, :include_my_retweet => false, :trim_user => true)
|
64
|
+
return unless yes? "Are you sure you want to remove @#{status.from_user}'s status: \"#{status.full_text}\" from your favorites? [y/N]"
|
65
65
|
end
|
66
|
-
status = client.unfavorite(status_id
|
67
|
-
say "@#{@rcfile.active_profile[0]} unfavorited @#{status.from_user}'s status: \"#{status.
|
66
|
+
status = client.unfavorite(status_id)
|
67
|
+
say "@#{@rcfile.active_profile[0]} unfavorited @#{status.from_user}'s status: \"#{status.full_text}\""
|
68
68
|
end
|
69
69
|
end
|
70
70
|
map %w(fave favourite) => :favorite
|
@@ -86,14 +86,14 @@ module T
|
|
86
86
|
method_option "force", :aliases => "-f", :type => :boolean, :default => false
|
87
87
|
def status(status_id, *status_ids)
|
88
88
|
status_ids.unshift(status_id)
|
89
|
-
status_ids.map!(&:
|
89
|
+
status_ids.map!(&:to_i)
|
90
90
|
status_ids.each do |status_id|
|
91
91
|
unless options['force']
|
92
|
-
status = client.status(status_id
|
93
|
-
return unless yes? "Are you sure you want to permanently delete @#{status.from_user}'s status: \"#{status.
|
92
|
+
status = client.status(status_id, :include_my_retweet => false, :trim_user => true)
|
93
|
+
return unless yes? "Are you sure you want to permanently delete @#{status.from_user}'s status: \"#{status.full_text}\"? [y/N]"
|
94
94
|
end
|
95
|
-
status = client.status_destroy(status_id
|
96
|
-
say "@#{@rcfile.active_profile[0]} deleted the status: \"#{status.
|
95
|
+
status = client.status_destroy(status_id, :trim_user => true)
|
96
|
+
say "@#{@rcfile.active_profile[0]} deleted the status: \"#{status.full_text}\""
|
97
97
|
end
|
98
98
|
end
|
99
99
|
map %w(post tweet update) => :status
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module T
|
4
|
+
module FormatHelpers
|
5
|
+
private
|
6
|
+
|
7
|
+
# https://github.com/rails/rails/blob/bd8a970/actionpack/lib/action_view/helpers/date_helper.rb
|
8
|
+
def distance_of_time_in_words_to_now(from_time)
|
9
|
+
to_time = Time.now
|
10
|
+
seconds = (to_time - from_time).abs
|
11
|
+
case (minutes = seconds / 60)
|
12
|
+
when 0 then 'less than a minute'
|
13
|
+
when 1 then '1 minute'
|
14
|
+
when 2..44 then '%d minutes' % minutes
|
15
|
+
when 45..89 then 'about 1 hour'
|
16
|
+
when 90..1439 then 'about %d hours' % (minutes.to_f / 60.0).round
|
17
|
+
when 1440..2519 then '1 day'
|
18
|
+
when 2520..43199 then '%d days' % (minutes.to_f / 1440.0).round
|
19
|
+
when 43200..86399 then 'about 1 month'
|
20
|
+
when 86400..525599 then '%d months' % (minutes.to_f / 43200.0).round
|
21
|
+
else
|
22
|
+
fyear = from_time.year
|
23
|
+
fyear += 1 if from_time.month >= 3
|
24
|
+
tyear = to_time.year
|
25
|
+
tyear -= 1 if to_time.month < 3
|
26
|
+
leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| Date.leap?(x)}
|
27
|
+
minute_offset_for_leap_year = leap_years * 1440
|
28
|
+
minutes_with_offset = minutes - minute_offset_for_leap_year
|
29
|
+
remainder = (minutes_with_offset % 525600)
|
30
|
+
distance_in_years = (minutes_with_offset / 525600)
|
31
|
+
if remainder < 131400
|
32
|
+
pluralize distance_in_years, 'about %d year'
|
33
|
+
elsif remainder < 394200
|
34
|
+
pluralize distance_in_years, 'over %d year'
|
35
|
+
else
|
36
|
+
pluralize distance_in_years + 1, 'almost %d year'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias time_ago_in_words distance_of_time_in_words_to_now
|
41
|
+
|
42
|
+
def pluralize(count, word)
|
43
|
+
word += 's' if count.to_i > 1
|
44
|
+
if word.include? '%'
|
45
|
+
word % count
|
46
|
+
else
|
47
|
+
"%d #{word}" % count
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def strip_tags(html)
|
52
|
+
html.gsub(/<.+?>/, '')
|
53
|
+
end
|
54
|
+
|
55
|
+
def number_with_delimiter(num)
|
56
|
+
digits = num.to_s.split(//)
|
57
|
+
groups = digits.reverse.in_groups_of(3).map {|g| g.join('') }
|
58
|
+
groups.join(',').reverse
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/t/list.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require 'action_view'
|
2
1
|
require 'active_support/core_ext/array/grouping'
|
2
|
+
require 't/format_helpers'
|
3
3
|
require 'csv'
|
4
4
|
# 'fastercsv' required on Ruby versions < 1.9
|
5
5
|
require 'fastercsv' unless Array.new.respond_to?(:to_csv)
|
@@ -14,10 +14,10 @@ require 'thor'
|
|
14
14
|
|
15
15
|
module T
|
16
16
|
class List < Thor
|
17
|
-
include ActionView::Helpers::DateHelper
|
18
17
|
include T::Collectable
|
19
18
|
include T::Printable
|
20
19
|
include T::Requestable
|
20
|
+
include T::FormatHelpers
|
21
21
|
|
22
22
|
DEFAULT_NUM_RESULTS = 20
|
23
23
|
MAX_SCREEN_NAME_SIZE = 20
|
@@ -88,7 +88,7 @@ module T
|
|
88
88
|
array << ["Description", list.description] unless list.description.nil?
|
89
89
|
array << ["Slug", list.slug]
|
90
90
|
array << ["Screen name", "@#{list.user.screen_name}"]
|
91
|
-
array << ["Created at", ls_formatted_time(list)]
|
91
|
+
array << ["Created at", "#{ls_formatted_time(list)} (#{time_ago_in_words(list.created_at)} ago)"]
|
92
92
|
array << ["Members", number_with_delimiter(list.member_count)]
|
93
93
|
array << ["Subscribers", number_with_delimiter(list.subscriber_count)]
|
94
94
|
array << ["Status", list.following ? "Following" : "Not following"]
|
@@ -97,7 +97,7 @@ module T
|
|
97
97
|
print_table(array)
|
98
98
|
end
|
99
99
|
end
|
100
|
-
map %w(
|
100
|
+
map %w(details) => :information
|
101
101
|
|
102
102
|
desc "members [USER/]LIST", "Returns the members of a Twitter list."
|
103
103
|
method_option "csv", :aliases => "-c", :type => :boolean, :default => false, :desc => "Output in CSV format."
|
data/lib/t/printable.rb
CHANGED
@@ -1,47 +1,43 @@
|
|
1
|
-
require 'action_view'
|
2
1
|
require 'csv'
|
3
2
|
# 'fastercsv' required on Ruby versions < 1.9
|
4
3
|
require 'fastercsv' unless Array.new.respond_to?(:to_csv)
|
5
4
|
require 'htmlentities'
|
5
|
+
require 't/core_ext/kernel'
|
6
6
|
require 'thor/shell/color'
|
7
7
|
require 'time'
|
8
8
|
|
9
9
|
module T
|
10
10
|
module Printable
|
11
11
|
MAX_SCREEN_NAME_SIZE = 20
|
12
|
-
|
13
|
-
LIST_HEADINGS =["ID", "Created at", "Screen name", "Slug", "Members", "Subscribers", "Mode", "Description"]
|
12
|
+
LIST_HEADINGS = ["ID", "Created at", "Screen name", "Slug", "Members", "Subscribers", "Mode", "Description"]
|
14
13
|
STATUS_HEADINGS = ["ID", "Posted at", "Screen name", "Text"]
|
15
|
-
TREND_HEADINGS = ["WOEID", "Parent ID", "Type", "Name", "Country"]
|
16
14
|
USER_HEADINGS = ["ID", "Since", "Tweets", "Favorites", "Listed", "Following", "Followers", "Screen name", "Name"]
|
17
15
|
|
18
|
-
include ActionView::Helpers::NumberHelper
|
19
|
-
|
20
|
-
def self.included(base)
|
21
|
-
|
22
16
|
private
|
23
17
|
|
24
18
|
def build_long_list(list)
|
25
|
-
[list.id, ls_formatted_time(list), "@#{list.user.screen_name}", list.slug,
|
19
|
+
[list.id, ls_formatted_time(list), "@#{list.user.screen_name}", list.slug, list.member_count, list.subscriber_count, list.mode, list.description]
|
26
20
|
end
|
27
21
|
|
28
22
|
def build_long_status(status)
|
29
|
-
[status.id, ls_formatted_time(status), "@#{status.from_user}", HTMLEntities.new.decode(status.
|
23
|
+
[status.id, ls_formatted_time(status), "@#{status.from_user}", HTMLEntities.new.decode(status.full_text).gsub(/\n+/, ' ')]
|
30
24
|
end
|
31
25
|
|
32
26
|
def build_long_user(user)
|
33
|
-
[user.id, ls_formatted_time(user),
|
27
|
+
[user.id, ls_formatted_time(user), user.statuses_count, user.favourites_count, user.listed_count, user.friends_count, user.followers_count, "@#{user.screen_name}", user.name]
|
34
28
|
end
|
35
29
|
|
36
30
|
def csv_formatted_time(object, key=:created_at)
|
37
|
-
|
31
|
+
time = object.send(key.to_sym)
|
32
|
+
time.utc.strftime("%Y-%m-%d %H:%M:%S %z")
|
38
33
|
end
|
39
34
|
|
40
35
|
def ls_formatted_time(object, key=:created_at)
|
41
|
-
|
42
|
-
|
36
|
+
time = T.local_time object.send(key.to_sym)
|
37
|
+
if time > 6.months.ago
|
38
|
+
time.strftime("%b %e %H:%M")
|
43
39
|
else
|
44
|
-
|
40
|
+
time.strftime("%b %e %Y")
|
45
41
|
end
|
46
42
|
end
|
47
43
|
|
@@ -50,7 +46,7 @@ module T
|
|
50
46
|
end
|
51
47
|
|
52
48
|
def print_csv_status(status)
|
53
|
-
say [status.id, csv_formatted_time(status), status.from_user, HTMLEntities.new.decode(status.
|
49
|
+
say [status.id, csv_formatted_time(status), status.from_user, HTMLEntities.new.decode(status.full_text)].to_csv
|
54
50
|
end
|
55
51
|
|
56
52
|
def print_csv_user(user)
|
@@ -78,7 +74,8 @@ module T
|
|
78
74
|
array = lists.map do |list|
|
79
75
|
build_long_list(list)
|
80
76
|
end
|
81
|
-
|
77
|
+
format = options['format'] || LIST_HEADINGS.size.times.map{"%s"}
|
78
|
+
print_table_with_headings(array, LIST_HEADINGS, format)
|
82
79
|
else
|
83
80
|
print_attribute(lists, :full_name)
|
84
81
|
end
|
@@ -94,9 +91,15 @@ module T
|
|
94
91
|
end
|
95
92
|
end
|
96
93
|
|
97
|
-
def print_table_with_headings(array, headings)
|
94
|
+
def print_table_with_headings(array, headings, format)
|
95
|
+
return if array.flatten.empty?
|
98
96
|
if STDOUT.tty?
|
99
|
-
array.unshift(headings)
|
97
|
+
array.unshift(headings)
|
98
|
+
array.map! do |row|
|
99
|
+
row.each_with_index.map do |element, index|
|
100
|
+
Kernel.send(element.class.name.to_sym, format[index] % element)
|
101
|
+
end
|
102
|
+
end
|
100
103
|
print_table(array, :truncate => true)
|
101
104
|
else
|
102
105
|
print_table(array)
|
@@ -109,7 +112,7 @@ module T
|
|
109
112
|
else
|
110
113
|
say(" @#{status.from_user}")
|
111
114
|
end
|
112
|
-
print_wrapped(HTMLEntities.new.decode(status.
|
115
|
+
print_wrapped(HTMLEntities.new.decode(status.full_text), :indent => 3)
|
113
116
|
say
|
114
117
|
end
|
115
118
|
|
@@ -124,7 +127,8 @@ module T
|
|
124
127
|
array = statuses.map do |status|
|
125
128
|
build_long_status(status)
|
126
129
|
end
|
127
|
-
|
130
|
+
format = options['format'] || STATUS_HEADINGS.size.times.map{"%s"}
|
131
|
+
print_table_with_headings(array, STATUS_HEADINGS, format)
|
128
132
|
else
|
129
133
|
statuses.each do |status|
|
130
134
|
print_status(status)
|
@@ -157,13 +161,12 @@ module T
|
|
157
161
|
array = users.map do |user|
|
158
162
|
build_long_user(user)
|
159
163
|
end
|
160
|
-
|
164
|
+
format = options['format'] || USER_HEADINGS.size.times.map{"%s"}
|
165
|
+
print_table_with_headings(array, USER_HEADINGS, format)
|
161
166
|
else
|
162
167
|
print_attribute(users, :screen_name)
|
163
168
|
end
|
164
169
|
end
|
165
170
|
|
166
|
-
end
|
167
|
-
|
168
171
|
end
|
169
172
|
end
|
data/lib/t/rcfile.rb
CHANGED
data/lib/t/requestable.rb
CHANGED