t 0.2.1 → 0.3.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 +29 -13
- data/lib/t/cli.rb +51 -59
- data/lib/t/cli/follow.rb +47 -6
- data/lib/t/cli/list.rb +10 -8
- data/lib/t/cli/list/add.rb +98 -4
- data/lib/t/cli/list/remove.rb +99 -6
- data/lib/t/cli/search.rb +128 -0
- data/lib/t/cli/set.rb +1 -1
- data/lib/t/cli/unfollow.rb +90 -6
- data/lib/t/collectable.rb +12 -0
- data/lib/t/version.rb +2 -2
- data/spec/cli/follow_spec.rb +192 -3
- data/spec/cli/list/add_spec.rb +407 -14
- data/spec/cli/list/remove_spec.rb +392 -2
- data/spec/cli/search_spec.rb +205 -0
- data/spec/cli/unfollow_spec.rb +362 -3
- data/spec/cli_spec.rb +72 -99
- data/spec/rcfile_spec.rb +15 -17
- data/t.gemspec +1 -0
- metadata +45 -42
- data/lib/t/cli/follow/all.rb +0 -97
- data/lib/t/cli/list/add/all.rb +0 -169
- data/lib/t/cli/list/remove/all.rb +0 -163
- data/lib/t/cli/unfollow/all.rb +0 -148
- data/spec/cli/follow/all_spec.rb +0 -158
- data/spec/cli/list/add/all_spec.rb +0 -435
- data/spec/cli/list/remove/all_spec.rb +0 -315
- data/spec/cli/unfollow/all_spec.rb +0 -292
data/lib/t/cli/list/remove.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
require 'retryable'
|
1
2
|
require 't/core_ext/enumerable'
|
2
3
|
require 't/core_ext/string'
|
4
|
+
require 't/collectable'
|
3
5
|
require 't/rcfile'
|
4
6
|
require 'thor'
|
5
7
|
require 'twitter'
|
@@ -8,6 +10,8 @@ module T
|
|
8
10
|
class CLI
|
9
11
|
class List
|
10
12
|
class Remove < Thor
|
13
|
+
include T::Collectable
|
14
|
+
|
11
15
|
DEFAULT_HOST = 'api.twitter.com'
|
12
16
|
DEFAULT_PROTOCOL = 'https'
|
13
17
|
|
@@ -18,12 +22,105 @@ module T
|
|
18
22
|
@rcfile = RCFile.instance
|
19
23
|
end
|
20
24
|
|
25
|
+
desc "friends LIST_NAME", "Remove all friends from a list."
|
26
|
+
def friends(list_name)
|
27
|
+
list_member_ids = collect_with_cursor do |cursor|
|
28
|
+
client.list_members(list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
|
29
|
+
end
|
30
|
+
friend_ids = collect_with_cursor do |cursor|
|
31
|
+
client.friend_ids(:cursor => cursor)
|
32
|
+
end
|
33
|
+
list_member_ids_to_remove = (friend_ids - list_member_ids)
|
34
|
+
number = list_member_ids_to_remove.length
|
35
|
+
if number.zero?
|
36
|
+
return say "None of @#{@rcfile.default_profile[0]}'s friends are members of the list \"#{list_name}\"."
|
37
|
+
else
|
38
|
+
return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'friend' : 'friends'} from the list \"#{list_name}\"?"
|
39
|
+
end
|
40
|
+
list_member_ids_to_remove.threaded_map do |list_member_id|
|
41
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
42
|
+
client.list_remove_member(list_name, list_member_id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'friend' : 'friends'} from the list \"#{list_name}\"."
|
46
|
+
say
|
47
|
+
say "Run `#{$0} list add all friends #{list_name}` to undo."
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "followers LIST_NAME", "Remove all followers from a list."
|
51
|
+
def followers(list_name)
|
52
|
+
list_member_ids = collect_with_cursor do |cursor|
|
53
|
+
client.list_members(list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
|
54
|
+
end
|
55
|
+
follower_ids = collect_with_cursor do |cursor|
|
56
|
+
client.follower_ids(:cursor => cursor)
|
57
|
+
end
|
58
|
+
list_member_ids_to_remove = (follower_ids - list_member_ids)
|
59
|
+
number = list_member_ids_to_remove.length
|
60
|
+
if number.zero?
|
61
|
+
return say "None of @#{@rcfile.default_profile[0]}'s followers are members of the list \"#{list_name}\"."
|
62
|
+
else
|
63
|
+
return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'follower' : 'followers'} from the list \"#{list_name}\"?"
|
64
|
+
end
|
65
|
+
list_member_ids_to_remove.threaded_map do |list_member_id|
|
66
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
67
|
+
client.list_remove_member(list_name, list_member_id)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'follower' : 'followers'} from the list \"#{list_name}\"."
|
71
|
+
say
|
72
|
+
say "Run `#{$0} list add all followers #{list_name}` to undo."
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "listed FROM_LIST_NAME TO_LIST_NAME", "Remove all list members from a list."
|
76
|
+
def listed(from_list_name, to_list_name)
|
77
|
+
to_list_members = collect_with_cursor do |cursor|
|
78
|
+
client.list_members(to_list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
|
79
|
+
end
|
80
|
+
from_list_members = collect_with_cursor do |cursor|
|
81
|
+
client.list_members(from_list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
|
82
|
+
end
|
83
|
+
list_member_ids_to_remove = (from_list_members.collect(&:id) - to_list_members.collect(&:id))
|
84
|
+
number = list_member_ids_to_remove.length
|
85
|
+
if number.zero?
|
86
|
+
return say "None of the members of the list \"#{from_list_name}\" are members of the list \"#{to_list_name}\"."
|
87
|
+
else
|
88
|
+
return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{to_list_name}\"?"
|
89
|
+
end
|
90
|
+
list_member_ids_to_remove.threaded_map do |list_member_id|
|
91
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
92
|
+
client.list_remove_member(to_list_name, list_member_id)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{to_list_name}\"."
|
96
|
+
say
|
97
|
+
say "Run `#{$0} list add all listed #{from_list_name} #{to_list_name}` to undo."
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "members LIST_NAME", "Remove all members from a list."
|
101
|
+
def members(list_name)
|
102
|
+
list_members = collect_with_cursor do |cursor|
|
103
|
+
client.list_members(list_name, :cursor => cursor, :skip_status => true, :include_entities => false)
|
104
|
+
end
|
105
|
+
number = list_members.length
|
106
|
+
return say "The list \"#{list_name}\" doesn't have any members." if number.zero?
|
107
|
+
return unless yes? "Are you sure you want to remove #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{list_name}\"?"
|
108
|
+
list_members.collect(&:id).threaded_map do |list_member_id|
|
109
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
110
|
+
client.list_remove_member(list_name, list_member_id)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'member' : 'members'} from the list \"#{list_name}\"."
|
114
|
+
end
|
115
|
+
map %w(all) => :members
|
116
|
+
|
21
117
|
desc "users LIST_NAME SCREEN_NAME [SCREEN_NAME...]", "Remove users from a list."
|
22
118
|
def users(list_name, screen_name, *screen_names)
|
23
119
|
screen_names.unshift(screen_name)
|
24
120
|
screen_names.threaded_map do |screen_name|
|
25
|
-
|
26
|
-
|
121
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
122
|
+
client.list_remove_member(list_name, screen_name)
|
123
|
+
end
|
27
124
|
end
|
28
125
|
number = screen_names.length
|
29
126
|
say "@#{@rcfile.default_profile[0]} removed #{number} #{number == 1 ? 'user' : 'users'} from the list \"#{list_name}\"."
|
@@ -31,10 +128,6 @@ module T
|
|
31
128
|
say "Run `#{$0} list add users #{list_name} #{screen_names.join(' ')}` to undo."
|
32
129
|
end
|
33
130
|
|
34
|
-
desc "all SUBCOMMAND ...ARGS", "Remove all users to a list."
|
35
|
-
require 't/cli/list/remove/all'
|
36
|
-
subcommand 'all', CLI::List::Remove::All
|
37
|
-
|
38
131
|
private
|
39
132
|
|
40
133
|
def base_url
|
data/lib/t/cli/search.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'retryable'
|
3
|
+
require 't/core_ext/enumerable'
|
4
|
+
require 't/rcfile'
|
5
|
+
require 'thor'
|
6
|
+
require 'twitter'
|
7
|
+
|
8
|
+
module T
|
9
|
+
class CLI
|
10
|
+
class Search < Thor
|
11
|
+
include ActionView::Helpers::DateHelper
|
12
|
+
|
13
|
+
DEFAULT_HOST = 'api.twitter.com'
|
14
|
+
DEFAULT_PROTOCOL = 'https'
|
15
|
+
DEFAULT_NUM_RESULTS = 20
|
16
|
+
MAX_PAGES = 16
|
17
|
+
MAX_NUM_RESULTS = 200
|
18
|
+
MAX_SCREEN_NAME_SIZE = 20
|
19
|
+
|
20
|
+
check_unknown_options!
|
21
|
+
|
22
|
+
def initialize(*)
|
23
|
+
super
|
24
|
+
@rcfile = RCFile.instance
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "all QUERY", "Returns the #{DEFAULT_NUM_RESULTS} most recent Tweets that match a specified query."
|
28
|
+
method_option :number, :aliases => "-n", :type => :numeric, :default => DEFAULT_NUM_RESULTS
|
29
|
+
method_option :reverse, :aliases => "-r", :type => :boolean, :default => false
|
30
|
+
def all(query)
|
31
|
+
defaults = {:include_entities => false}
|
32
|
+
defaults.merge!(:rpp => options['number']) if options['number']
|
33
|
+
timeline = client.search(query, defaults)
|
34
|
+
timeline.reverse! if options['reverse']
|
35
|
+
run_pager
|
36
|
+
timeline.each do |status|
|
37
|
+
say "#{status.from_user.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "timeline QUERY", "Returns Tweets in your timeline that match a specified query."
|
42
|
+
def timeline(query)
|
43
|
+
timeline = 1.upto(MAX_PAGES).threaded_map do |page|
|
44
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
45
|
+
client.home_timeline(:page => page, :count => MAX_NUM_RESULTS).map do |status|
|
46
|
+
status if /#{query}/i.match(status.text)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
run_pager
|
51
|
+
timeline.flatten.compact.each do |status|
|
52
|
+
say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
map %w(tl) => :timeline
|
56
|
+
|
57
|
+
desc "user SCREEN_NAME QUERY", "Returns Tweets in a user's timeline that match a specified query."
|
58
|
+
def user(screen_name, query)
|
59
|
+
screen_name = screen_name.strip_at
|
60
|
+
timeline = 1.upto(MAX_PAGES).threaded_map do |page|
|
61
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
62
|
+
client.user_timeline(screen_name, :page => page, :count => MAX_NUM_RESULTS).map do |status|
|
63
|
+
status if /#{query}/i.match(status.text)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
run_pager
|
68
|
+
timeline.flatten.compact.each do |status|
|
69
|
+
say "#{status.user.screen_name.rjust(MAX_SCREEN_NAME_SIZE)}: #{status.text} (#{time_ago_in_words(status.created_at)} ago)"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def base_url
|
76
|
+
"#{protocol}://#{host}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def client
|
80
|
+
return @client if @client
|
81
|
+
@rcfile.path = parent_options['profile'] if parent_options['profile']
|
82
|
+
@client = Twitter::Client.new(
|
83
|
+
:endpoint => base_url,
|
84
|
+
:consumer_key => @rcfile.default_consumer_key,
|
85
|
+
:consumer_secret => @rcfile.default_consumer_secret,
|
86
|
+
:oauth_token => @rcfile.default_token,
|
87
|
+
:oauth_token_secret => @rcfile.default_secret
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def host
|
92
|
+
parent_options['host'] || DEFAULT_HOST
|
93
|
+
end
|
94
|
+
|
95
|
+
def protocol
|
96
|
+
parent_options['no_ssl'] ? 'http' : DEFAULT_PROTOCOL
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_pager
|
100
|
+
return if RUBY_PLATFORM =~ /win32/
|
101
|
+
return if ENV["T_ENV"] == "test"
|
102
|
+
return unless STDOUT.tty?
|
103
|
+
|
104
|
+
read, write = IO.pipe
|
105
|
+
|
106
|
+
unless Kernel.fork # Child process
|
107
|
+
STDOUT.reopen(write)
|
108
|
+
STDERR.reopen(write) if STDERR.tty?
|
109
|
+
read.close
|
110
|
+
write.close
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
# Parent process, become pager
|
115
|
+
STDIN.reopen(read)
|
116
|
+
read.close
|
117
|
+
write.close
|
118
|
+
|
119
|
+
ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
|
120
|
+
|
121
|
+
Kernel.select [STDIN] # Wait until we have input before we start the pager
|
122
|
+
pager = ENV['PAGER'] || 'less'
|
123
|
+
exec pager rescue exec "/bin/sh", "-c", pager
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/t/cli/set.rb
CHANGED
@@ -26,7 +26,7 @@ module T
|
|
26
26
|
def default(screen_name, consumer_key=nil)
|
27
27
|
screen_name = screen_name.strip_at
|
28
28
|
@rcfile.path = parent_options['profile'] if parent_options['profile']
|
29
|
-
consumer_key = rcfile[screen_name].keys.last if consumer_key.nil?
|
29
|
+
consumer_key = @rcfile[screen_name].keys.last if consumer_key.nil?
|
30
30
|
@rcfile.default_profile = {'username' => screen_name, 'consumer_key' => consumer_key}
|
31
31
|
say "Default account has been updated."
|
32
32
|
end
|
data/lib/t/cli/unfollow.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
require 'retryable'
|
1
2
|
require 't/core_ext/enumerable'
|
2
3
|
require 't/core_ext/string'
|
4
|
+
require 't/collectable'
|
3
5
|
require 't/rcfile'
|
4
6
|
require 'thor'
|
5
7
|
require 'twitter'
|
@@ -7,6 +9,8 @@ require 'twitter'
|
|
7
9
|
module T
|
8
10
|
class CLI
|
9
11
|
class Unfollow < Thor
|
12
|
+
include T::Collectable
|
13
|
+
|
10
14
|
DEFAULT_HOST = 'api.twitter.com'
|
11
15
|
DEFAULT_PROTOCOL = 'https'
|
12
16
|
|
@@ -17,12 +21,96 @@ module T
|
|
17
21
|
@rcfile = RCFile.instance
|
18
22
|
end
|
19
23
|
|
24
|
+
desc "listed LIST_NAME", "Unfollow all members of a list."
|
25
|
+
def listed(list_name)
|
26
|
+
list_member_collection = collect_with_cursor do |cursor|
|
27
|
+
client.list_members(list_name, :cursor => cursor, :include_entities => false, :skip_status => true)
|
28
|
+
end
|
29
|
+
number = list_member_collection.length
|
30
|
+
return say "@#{@rcfile.default_profile[0]} is already not following any list members." if number.zero?
|
31
|
+
return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
|
32
|
+
list_member_collection.threaded_map do |list_member|
|
33
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
34
|
+
client.unfollow(list_member.id, :include_entities => false)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
|
38
|
+
say
|
39
|
+
say "Run `#{$0} follow all listed #{list_name}` to follow again."
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "followers", "Unfollow all followers."
|
43
|
+
def followers
|
44
|
+
follower_ids = collect_with_cursor do |cursor|
|
45
|
+
client.follower_ids(:cursor => cursor)
|
46
|
+
end
|
47
|
+
friend_ids = collect_with_cursor do |cursor|
|
48
|
+
friends = client.friend_ids(:cursor => cursor)
|
49
|
+
end
|
50
|
+
follow_ids = (follower_ids - friend_ids)
|
51
|
+
number = follow_ids.length
|
52
|
+
return say "@#{@rcfile.default_profile[0]} is already not following any followers." if number.zero?
|
53
|
+
return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
|
54
|
+
screen_names = follow_ids.threaded_map do |follow_id|
|
55
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
56
|
+
client.unfollow(follow_id, :include_entities => false)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
|
60
|
+
say
|
61
|
+
say "Run `#{$0} follow all followers` to stop."
|
62
|
+
end
|
63
|
+
|
64
|
+
desc "friends", "Unfollow all friends."
|
65
|
+
def friends
|
66
|
+
friend_ids = collect_with_cursor do |cursor|
|
67
|
+
client.friend_ids(:cursor => cursor)
|
68
|
+
end
|
69
|
+
number = friend_ids.length
|
70
|
+
return say "@#{@rcfile.default_profile[0]} is already not following anyone." if number.zero?
|
71
|
+
return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
|
72
|
+
screen_names = friend_ids.threaded_map do |friend_id|
|
73
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
74
|
+
user = client.unfollow(friend_id, :include_entities => false)
|
75
|
+
user.screen_name
|
76
|
+
end
|
77
|
+
end
|
78
|
+
say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
|
79
|
+
say
|
80
|
+
say "Run `#{$0} follow users #{screen_names.join(' ')}` to follow again."
|
81
|
+
end
|
82
|
+
map %w(everyone everybody) => :friends
|
83
|
+
|
84
|
+
desc "nonfollowers", "Unfollow all non-followers."
|
85
|
+
def nonfollowers
|
86
|
+
friend_ids = collect_with_cursor do |cursor|
|
87
|
+
client.friend_ids(:cursor => cursor)
|
88
|
+
end
|
89
|
+
follower_ids = collect_with_cursor do |cursor|
|
90
|
+
client.follower_ids(:cursor => cursor)
|
91
|
+
end
|
92
|
+
unfollow_ids = (friend_ids - follower_ids)
|
93
|
+
number = unfollow_ids.length
|
94
|
+
return say "@#{@rcfile.default_profile[0]} is already not following any non-followers." if number.zero?
|
95
|
+
return unless yes? "Are you sure you want to unfollow #{number} #{number == 1 ? 'user' : 'users'}?"
|
96
|
+
screen_names = unfollow_ids.threaded_map do |unfollow_id|
|
97
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
98
|
+
user = client.unfollow(unfollow_id, :include_entities => false)
|
99
|
+
user.screen_name
|
100
|
+
end
|
101
|
+
end
|
102
|
+
say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
|
103
|
+
say
|
104
|
+
say "Run `#{$0} follow users #{screen_names.join(' ')}` to follow again."
|
105
|
+
end
|
106
|
+
|
20
107
|
desc "users SCREEN_NAME [SCREEN_NAME...]", "Allows you to stop following users."
|
21
108
|
def users(screen_name, *screen_names)
|
22
109
|
screen_names.unshift(screen_name)
|
23
110
|
screen_names.threaded_map do |screen_name|
|
24
|
-
|
25
|
-
|
111
|
+
retryable(:tries => 3, :on => Twitter::Error::ServerError, :sleep => 0) do
|
112
|
+
client.unfollow(screen_name, :include_entities => false)
|
113
|
+
end
|
26
114
|
end
|
27
115
|
number = screen_names.length
|
28
116
|
say "@#{@rcfile.default_profile[0]} is no longer following #{number} #{number == 1 ? 'user' : 'users'}."
|
@@ -30,10 +118,6 @@ module T
|
|
30
118
|
say "Run `#{$0} follow users #{screen_names.join(' ')}` to follow again."
|
31
119
|
end
|
32
120
|
|
33
|
-
desc "all SUBCOMMAND ...ARGS", "Follow all users."
|
34
|
-
require 't/cli/unfollow/all'
|
35
|
-
subcommand 'all', CLI::Unfollow::All
|
36
|
-
|
37
121
|
private
|
38
122
|
|
39
123
|
def base_url
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module T
|
2
|
+
module Collectable
|
3
|
+
|
4
|
+
def collect_with_cursor(collection=[], cursor=-1, &block)
|
5
|
+
return collection if cursor == 0
|
6
|
+
object = yield cursor
|
7
|
+
collection += object.collection
|
8
|
+
collect_with_cursor(collection, object.next_cursor)
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
data/lib/t/version.rb
CHANGED
data/spec/cli/follow_spec.rb
CHANGED
@@ -16,6 +16,181 @@ describe T::CLI::Follow do
|
|
16
16
|
$stdout = @old_stdout
|
17
17
|
end
|
18
18
|
|
19
|
+
describe "#followers" do
|
20
|
+
before do
|
21
|
+
@t.options = @t.options.merge(:profile => fixture_path + "/.trc")
|
22
|
+
end
|
23
|
+
context "no followers" do
|
24
|
+
before do
|
25
|
+
stub_get("/1/followers/ids.json").
|
26
|
+
with(:query => {:cursor => "-1"}).
|
27
|
+
to_return(:body => fixture("friends_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
28
|
+
stub_get("/1/friends/ids.json").
|
29
|
+
with(:query => {:cursor => "-1"}).
|
30
|
+
to_return(:body => fixture("friends_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
31
|
+
end
|
32
|
+
it "should request the correct resource" do
|
33
|
+
@t.follow("followers")
|
34
|
+
a_get("/1/followers/ids.json").
|
35
|
+
with(:query => {:cursor => "-1"}).
|
36
|
+
should have_been_made
|
37
|
+
a_get("/1/friends/ids.json").
|
38
|
+
with(:query => {:cursor => "-1"}).
|
39
|
+
should have_been_made
|
40
|
+
end
|
41
|
+
it "should have the correct output" do
|
42
|
+
@t.follow("followers")
|
43
|
+
$stdout.string.chomp.should == "@testcli is already following all followers."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
context "one follower" do
|
47
|
+
before do
|
48
|
+
stub_get("/1/followers/ids.json").
|
49
|
+
with(:query => {:cursor => "-1"}).
|
50
|
+
to_return(:body => fixture("friends_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
51
|
+
stub_get("/1/friends/ids.json").
|
52
|
+
with(:query => {:cursor => "-1"}).
|
53
|
+
to_return(:body => fixture("followers_ids.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
54
|
+
end
|
55
|
+
it "should request the correct resource" do
|
56
|
+
stub_post("/1/friendships/create.json").
|
57
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
58
|
+
to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
59
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
60
|
+
$stdin.should_receive(:gets).and_return("yes")
|
61
|
+
@t.follow("followers")
|
62
|
+
a_get("/1/followers/ids.json").
|
63
|
+
with(:query => {:cursor => "-1"}).
|
64
|
+
should have_been_made
|
65
|
+
a_get("/1/friends/ids.json").
|
66
|
+
with(:query => {:cursor => "-1"}).
|
67
|
+
should have_been_made
|
68
|
+
a_post("/1/friendships/create.json").
|
69
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
70
|
+
should have_been_made
|
71
|
+
end
|
72
|
+
context "yes" do
|
73
|
+
it "should have the correct output" do
|
74
|
+
stub_post("/1/friendships/create.json").
|
75
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
76
|
+
to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
77
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
78
|
+
$stdin.should_receive(:gets).and_return("yes")
|
79
|
+
@t.follow("followers")
|
80
|
+
$stdout.string.should =~ /^@testcli is now following 1 more user\.$/
|
81
|
+
end
|
82
|
+
end
|
83
|
+
context "no" do
|
84
|
+
it "should have the correct output" do
|
85
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
86
|
+
$stdin.should_receive(:gets).and_return("no")
|
87
|
+
@t.follow("followers")
|
88
|
+
$stdout.string.chomp.should == ""
|
89
|
+
end
|
90
|
+
end
|
91
|
+
context "Twitter is down" do
|
92
|
+
it "should retry 3 times and then raise an error" do
|
93
|
+
stub_post("/1/friendships/create.json").
|
94
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
95
|
+
to_return(:status => 502)
|
96
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
97
|
+
$stdin.should_receive(:gets).and_return("yes")
|
98
|
+
lambda do
|
99
|
+
@t.follow("followers")
|
100
|
+
end.should raise_error("Twitter is down or being upgraded.")
|
101
|
+
a_post("/1/friendships/create.json").
|
102
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
103
|
+
should have_been_made.times(3)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "#listed" do
|
110
|
+
before do
|
111
|
+
@t.options = @t.options.merge(:profile => fixture_path + "/.trc")
|
112
|
+
stub_get("/1/account/verify_credentials.json").
|
113
|
+
to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
114
|
+
end
|
115
|
+
context "no users" do
|
116
|
+
before do
|
117
|
+
stub_get("/1/lists/members.json").
|
118
|
+
with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
|
119
|
+
to_return(:body => fixture("empty_cursor.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
120
|
+
end
|
121
|
+
it "should request the correct resource" do
|
122
|
+
@t.follow("listed", "presidents")
|
123
|
+
a_get("/1/account/verify_credentials.json").
|
124
|
+
should have_been_made
|
125
|
+
a_get("/1/lists/members.json").
|
126
|
+
with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
|
127
|
+
should have_been_made
|
128
|
+
end
|
129
|
+
it "should have the correct output" do
|
130
|
+
@t.follow("listed", "presidents")
|
131
|
+
$stdout.string.chomp.should == "@testcli is already following all list members."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
context "one user" do
|
135
|
+
before do
|
136
|
+
@t.options = @t.options.merge(:profile => fixture_path + "/.trc")
|
137
|
+
stub_get("/1/lists/members.json").
|
138
|
+
with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
|
139
|
+
to_return(:body => fixture("users_list.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
140
|
+
end
|
141
|
+
it "should request the correct resource" do
|
142
|
+
stub_post("/1/friendships/create.json").
|
143
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
144
|
+
to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
145
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
146
|
+
$stdin.should_receive(:gets).and_return("yes")
|
147
|
+
@t.follow("listed", "presidents")
|
148
|
+
a_get("/1/account/verify_credentials.json").
|
149
|
+
should have_been_made
|
150
|
+
a_get("/1/lists/members.json").
|
151
|
+
with(:query => {:cursor => "-1", :include_entities => "false", :owner_screen_name => "sferik", :skip_status => "true", :slug => "presidents"}).
|
152
|
+
should have_been_made
|
153
|
+
a_post("/1/friendships/create.json").
|
154
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
155
|
+
should have_been_made
|
156
|
+
end
|
157
|
+
context "yes" do
|
158
|
+
it "should have the correct output" do
|
159
|
+
stub_post("/1/friendships/create.json").
|
160
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
161
|
+
to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
162
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
163
|
+
$stdin.should_receive(:gets).and_return("yes")
|
164
|
+
@t.follow("listed", "presidents")
|
165
|
+
$stdout.string.should =~ /^@testcli is now following 1 more user\.$/
|
166
|
+
end
|
167
|
+
end
|
168
|
+
context "no" do
|
169
|
+
it "should have the correct output" do
|
170
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
171
|
+
$stdin.should_receive(:gets).and_return("no")
|
172
|
+
@t.follow("listed", "presidents")
|
173
|
+
$stdout.string.chomp.should == ""
|
174
|
+
end
|
175
|
+
end
|
176
|
+
context "Twitter is down" do
|
177
|
+
it "should retry 3 times and then raise an error" do
|
178
|
+
stub_post("/1/friendships/create.json").
|
179
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
180
|
+
to_return(:status => 502)
|
181
|
+
$stdout.should_receive(:print).with("Are you sure you want to follow 1 user? ")
|
182
|
+
$stdin.should_receive(:gets).and_return("yes")
|
183
|
+
lambda do
|
184
|
+
@t.follow("listed", "presidents")
|
185
|
+
end.should raise_error("Twitter is down or being upgraded.")
|
186
|
+
a_post("/1/friendships/create.json").
|
187
|
+
with(:body => {:user_id => "7505382", :include_entities => "false"}).
|
188
|
+
should have_been_made.times(3)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
19
194
|
describe "#users" do
|
20
195
|
before do
|
21
196
|
@t.options = @t.options.merge(:profile => fixture_path + "/.trc")
|
@@ -28,21 +203,35 @@ describe T::CLI::Follow do
|
|
28
203
|
end
|
29
204
|
end
|
30
205
|
context "one user" do
|
31
|
-
|
206
|
+
it "should request the correct resource" do
|
32
207
|
stub_post("/1/friendships/create.json").
|
33
208
|
with(:body => {:screen_name => "sferik", :include_entities => "false"}).
|
34
209
|
to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
35
|
-
end
|
36
|
-
it "should request the correct resource" do
|
37
210
|
@t.follow("users", "sferik")
|
38
211
|
a_post("/1/friendships/create.json").
|
39
212
|
with(:body => {:screen_name => "sferik", :include_entities => "false"}).
|
40
213
|
should have_been_made
|
41
214
|
end
|
42
215
|
it "should have the correct output" do
|
216
|
+
stub_post("/1/friendships/create.json").
|
217
|
+
with(:body => {:screen_name => "sferik", :include_entities => "false"}).
|
218
|
+
to_return(:body => fixture("sferik.json"), :headers => {:content_type => "application/json; charset=utf-8"})
|
43
219
|
@t.follow("users", "sferik")
|
44
220
|
$stdout.string.should =~ /^@testcli is now following 1 more user\.$/
|
45
221
|
end
|
222
|
+
context "Twitter is down" do
|
223
|
+
it "should retry 3 times and then raise an error" do
|
224
|
+
stub_post("/1/friendships/create.json").
|
225
|
+
with(:body => {:screen_name => "sferik", :include_entities => "false"}).
|
226
|
+
to_return(:status => 502)
|
227
|
+
lambda do
|
228
|
+
@t.follow("users", "sferik")
|
229
|
+
end.should raise_error("Twitter is down or being upgraded.")
|
230
|
+
a_post("/1/friendships/create.json").
|
231
|
+
with(:body => {:screen_name => "sferik", :include_entities => "false"}).
|
232
|
+
should have_been_made.times(3)
|
233
|
+
end
|
234
|
+
end
|
46
235
|
end
|
47
236
|
end
|
48
237
|
|