twitter_with_auto_pagination 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5a766fdb1cdee5bb05a9a5a075d2249637284338
4
+ data.tar.gz: a493b330a56148deb5abf3408b3252c492d56be9
5
+ SHA512:
6
+ metadata.gz: 8238fe4d1f500572e1993891ecd861f1cb6f90526732bf37defe460fd5e130b1605fded19fd951dc68901c62eead6d69325b71790b9cb552393203ceb402e355
7
+ data.tar.gz: 9fd76e42c73f82b200ce110eb9efeba3097150e29fd6bbb273d84da4a0d267698bb7d2bba2e6e6b65f5a78880b65b523febde6841a9d51a29a45bbd8dee76573
data/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2014 Shinohara Teruki
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ ex-twitter
2
+ ==========
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/ex_twitter.png)](https://badge.fury.io/rb/ex_twitter)
5
+ [![Build Status](https://travis-ci.org/ts-3156/ex-twitter.svg?branch=master)](https://travis-ci.org/ts-3156/ex-twitter)
6
+
7
+ Add auto paginate feature to Twitter gem.
8
+
9
+ ## Installation
10
+
11
+ ### Gem
12
+
13
+ ```
14
+ gem install ex_twitter
15
+ ```
16
+
17
+ ### Rails
18
+
19
+ Add ex_twitter to your Gemfile, and bundle.
20
+
21
+ ## Features
22
+
23
+ * Auto paginate feature
24
+
25
+ ## Configuration
26
+
27
+ You can pass configuration options as a block to `ExTwitter.new` just like `Twitter::REST::Client.new`.
28
+
29
+ ```
30
+ client = ExTwitter.new do |config|
31
+ config.consumer_key = "YOUR_CONSUMER_KEY"
32
+ config.consumer_secret = "YOUR_CONSUMER_SECRET"
33
+ config.access_token = "YOUR_ACCESS_TOKEN"
34
+ config.access_token_secret = "YOUR_ACCESS_SECRET"
35
+ end
36
+ ```
37
+
38
+ You can pass advanced configuration options as a block to `ExTwitter.new`.
39
+
40
+ ```
41
+ client = ExTwitter.new do |config|
42
+ config.auto_paginate = true
43
+ config.max_retries = 1
44
+ config.max_paginates = 3
45
+ end
46
+ ```
47
+
48
+ ## Usage Examples
49
+
50
+ ```
51
+ client.user_timeline
52
+ ```
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :test => :spec
8
+
9
+ task default: [:spec]
@@ -0,0 +1,4 @@
1
+ require 'twitter_with_auto_pagination/client'
2
+
3
+ module TwitterWithAutoPagination
4
+ end
@@ -0,0 +1,137 @@
1
+ require 'active_support'
2
+ require 'active_support/cache'
3
+ require 'active_support/core_ext/string'
4
+
5
+ require 'twitter_with_auto_pagination/log_subscriber'
6
+ require 'twitter_with_auto_pagination/utils'
7
+ require 'twitter_with_auto_pagination/existing_api'
8
+ require 'twitter_with_auto_pagination/new_api'
9
+
10
+ require 'twitter'
11
+ require 'hashie'
12
+ require 'parallel'
13
+
14
+ module TwitterWithAutoPagination
15
+ class Client < Twitter::REST::Client
16
+ def initialize(options = {})
17
+ @cache = ActiveSupport::Cache::FileStore.new(File.join('tmp', 'api_cache'))
18
+ @call_count = 0
19
+
20
+ @uid = options.has_key?(:uid) ? options.delete(:uid).to_i : nil
21
+ @screen_name = options.has_key?(:screen_name) ? options.delete(:screen_name).to_s : nil
22
+
23
+ @@logger = @logger =
24
+ if options[:logger]
25
+ options.delete(:logger)
26
+ else
27
+ Dir.mkdir('log') unless File.exists?('log')
28
+ Logger.new('log/twitter_with_auto_pagination.log')
29
+ end
30
+
31
+ super
32
+ end
33
+
34
+ def self.logger
35
+ @@logger
36
+ end
37
+
38
+ attr_accessor :call_count
39
+ attr_reader :cache, :authenticated_user, :logger
40
+
41
+ INDENT = 4
42
+
43
+ include TwitterWithAutoPagination::Utils
44
+
45
+ alias :old_verify_credentials :verify_credentials
46
+ alias :old_friendship? :friendship?
47
+ alias :old_user? :user?
48
+ alias :old_user :user
49
+ alias :old_friend_ids :friend_ids
50
+ alias :old_follower_ids :follower_ids
51
+ alias :old_friends :friends
52
+ alias :old_followers :followers
53
+ alias :old_users :users
54
+ alias :old_home_timeline :home_timeline
55
+ alias :old_user_timeline :user_timeline
56
+ alias :old_mentions_timeline :mentions_timeline
57
+ alias :old_favorites :favorites
58
+ alias :old_search :search
59
+
60
+ include TwitterWithAutoPagination::ExistingApi
61
+ include TwitterWithAutoPagination::NewApi
62
+
63
+ def usage_stats_wday_series_data(times)
64
+ wday_count = times.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |time, memo|
65
+ memo[time.wday] += 1
66
+ end
67
+ wday_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
68
+ {name: key, y: value, drilldown: key}
69
+ end
70
+ end
71
+
72
+ def usage_stats_wday_drilldown_series(times)
73
+ hour_count =
74
+ (0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
75
+ wday_memo[wday] =
76
+ times.select { |t| t.wday == wday }.map { |t| t.hour }.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |hour, hour_memo|
77
+ hour_memo[hour] += 1
78
+ end
79
+ end
80
+ hour_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
81
+ {name: key, id: key, data: value.to_a.map{|a| [a[0].to_s, a[1]] }}
82
+ end
83
+ end
84
+
85
+ def usage_stats_hour_series_data(times)
86
+ hour_count = times.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |time, memo|
87
+ memo[time.hour] += 1
88
+ end
89
+ hour_count.map do |key, value|
90
+ {name: key.to_s, y: value, drilldown: key.to_s}
91
+ end
92
+ end
93
+
94
+ def usage_stats_hour_drilldown_series(times)
95
+ wday_count =
96
+ (0..23).each_with_object((0..23).map { |n| [n, nil] }.to_h) do |hour, hour_memo|
97
+ hour_memo[hour] =
98
+ times.select { |t| t.hour == hour }.map { |t| t.wday }.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |wday, wday_memo|
99
+ wday_memo[wday] += 1
100
+ end
101
+ end
102
+ wday_count.map do |key, value|
103
+ {name: key.to_s, id: key.to_s, data: value.to_a.map{|a| [I18n.t('date.abbr_day_names')[a[0]], a[1]] }}
104
+ end
105
+ end
106
+
107
+ def twitter_addiction_series(times)
108
+ five_mins = 5.minutes
109
+ wday_expended_seconds =
110
+ (0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
111
+ target_times = times.select { |t| t.wday == wday }
112
+ wday_memo[wday] = target_times.empty? ? nil : target_times.each_cons(2).map {|a, b| (a - b) < five_mins ? a - b : five_mins }.sum
113
+ end
114
+ days = times.map{|t| t.to_date.to_s(:long) }.uniq.size
115
+ weeks = (days > 7) ? days / 7.0 : 1.0
116
+ wday_expended_seconds.map { |k, v| [I18n.t('date.abbr_day_names')[k], (v.nil? ? nil : v / weeks / 60)] }.map do |key, value|
117
+ {name: key, y: value}
118
+ end
119
+ end
120
+
121
+ def usage_stats(user, options = {})
122
+ n_days_ago = options.has_key?(:days) ? options[:days].days.ago : 100.years.ago
123
+ tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user)
124
+ times =
125
+ # TODO Use user specific time zone
126
+ tweets.map { |t| ActiveSupport::TimeZone['Tokyo'].parse(t.created_at.to_s) }.
127
+ select { |t| t > n_days_ago }
128
+ [
129
+ usage_stats_wday_series_data(times),
130
+ usage_stats_wday_drilldown_series(times),
131
+ usage_stats_hour_series_data(times),
132
+ usage_stats_hour_drilldown_series(times),
133
+ twitter_addiction_series(times)
134
+ ]
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,127 @@
1
+ module TwitterWithAutoPagination
2
+ module ExistingApi
3
+ def verify_credentials(*args)
4
+ options = {skip_status: true}.merge(args.extract_options!)
5
+ fetch_cache_or_call_api(__method__, args) {
6
+ call_old_method("old_#{__method__}", *args, options)
7
+ }
8
+ end
9
+
10
+ def friendship?(*args)
11
+ options = args.extract_options!
12
+ fetch_cache_or_call_api(__method__, args) {
13
+ call_old_method("old_#{__method__}", *args, options)
14
+ }
15
+ end
16
+
17
+ def user?(*args)
18
+ options = args.extract_options!
19
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
20
+ fetch_cache_or_call_api(__method__, args[0], options) {
21
+ call_old_method("old_#{__method__}", args[0], options)
22
+ }
23
+ end
24
+
25
+ def user(*args)
26
+ options = args.extract_options!
27
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
28
+ fetch_cache_or_call_api(__method__, args[0], options) {
29
+ call_old_method("old_#{__method__}", args[0], options)
30
+ }
31
+ end
32
+
33
+ def friend_ids(*args)
34
+ options = {count: 5000, cursor: -1}.merge(args.extract_options!)
35
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
36
+ fetch_cache_or_call_api(__method__, args[0], options) {
37
+ collect_with_cursor("old_#{__method__}", *args, options)
38
+ }
39
+ end
40
+
41
+ def follower_ids(*args)
42
+ options = {count: 5000, cursor: -1}.merge(args.extract_options!)
43
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
44
+ fetch_cache_or_call_api(__method__, args[0], options) {
45
+ collect_with_cursor("old_#{__method__}", *args, options)
46
+ }
47
+ end
48
+
49
+ # specify reduce: false to use tweet for inactive_*
50
+ def friends(*args)
51
+ options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
52
+ options[:reduce] = false unless options.has_key?(:reduce)
53
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
54
+ fetch_cache_or_call_api(__method__, args[0], options) {
55
+ collect_with_cursor("old_#{__method__}", *args, options)
56
+ }
57
+ end
58
+
59
+ # specify reduce: false to use tweet for inactive_*
60
+ def followers(*args)
61
+ options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
62
+ options[:reduce] = false unless options.has_key?(:reduce)
63
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
64
+ fetch_cache_or_call_api(__method__, args[0], options) {
65
+ collect_with_cursor("old_#{__method__}", *args, options)
66
+ }
67
+ end
68
+
69
+ # use compact, not use sort and uniq
70
+ # specify reduce: false to use tweet for inactive_*
71
+ # TODO Perhaps `old_users` automatically merges result...
72
+ def users(*args)
73
+ options = args.extract_options!
74
+ options[:reduce] = false
75
+ users_per_workers = args.first.compact.each_slice(100).to_a
76
+ processed_users = []
77
+
78
+ Parallel.each_with_index(users_per_workers, in_threads: [users_per_workers.size, 10].min) do |users_per_worker, i|
79
+ _users = fetch_cache_or_call_api(__method__, users_per_worker, options) {
80
+ call_old_method("old_#{__method__}", users_per_worker, options)
81
+ }
82
+
83
+ processed_users << {i: i, users: _users}
84
+ end
85
+
86
+ processed_users.sort_by{|p| p[:i] }.map{|p| p[:users] }.flatten.compact
87
+ end
88
+
89
+ def home_timeline(*args)
90
+ options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
91
+ fetch_cache_or_call_api(__method__, user.id, options) {
92
+ collect_with_max_id("old_#{__method__}", options)
93
+ }
94
+ end
95
+
96
+ def user_timeline(*args)
97
+ options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
98
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
99
+ fetch_cache_or_call_api(__method__, args[0], options) {
100
+ collect_with_max_id("old_#{__method__}", *args, options)
101
+ }
102
+ end
103
+
104
+ def mentions_timeline(*args)
105
+ options = {count: 200, include_rts: true, call_limit: 1}.merge(args.extract_options!)
106
+ fetch_cache_or_call_api(__method__, user.id, options) {
107
+ collect_with_max_id("old_#{__method__}", options)
108
+ }
109
+ end
110
+
111
+ def favorites(*args)
112
+ options = {count: 100, call_count: 1}.merge(args.extract_options!)
113
+ args[0] = verify_credentials(skip_status: true).id if args.empty?
114
+ fetch_cache_or_call_api(__method__, args[0], options) {
115
+ collect_with_max_id("old_#{__method__}", *args, options)
116
+ }
117
+ end
118
+
119
+ def search(*args)
120
+ options = {count: 100, result_type: :recent, call_limit: 1}.merge(args.extract_options!)
121
+ options[:reduce] = false
122
+ fetch_cache_or_call_api(__method__, args[0], options) {
123
+ collect_with_max_id("old_#{__method__}", *args, options) { |response| response.attrs[:statuses] }
124
+ }
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+
4
+
5
+ module TwitterWithAutoPagination
6
+ class LogSubscriber < ActiveSupport::LogSubscriber
7
+
8
+ def initialize
9
+ super
10
+ @odd = false
11
+ end
12
+
13
+ def cache_any(event)
14
+ return unless logger.debug?
15
+
16
+ payload = event.payload
17
+ name = "#{payload.delete(:name)} (#{event.duration.round(1)}ms)"
18
+ name = colorize_payload_name(name, payload[:name], AS: true)
19
+ debug { "#{name} #{(payload.inspect)}" }
20
+ end
21
+
22
+ %w(read write fetch_hit generate delete exist?).each do |operation|
23
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
24
+ def cache_#{operation}(event)
25
+ event.payload[:name] = '#{operation}'
26
+ cache_any(event)
27
+ end
28
+ METHOD
29
+ end
30
+
31
+ def call(event)
32
+ return unless logger.debug?
33
+
34
+ payload = event.payload
35
+ name = "#{payload.delete(:operation)} (#{event.duration.round(1)}ms)"
36
+
37
+ name = colorize_payload_name(name, payload[:name])
38
+ # sql = color(sql, sql_color(sql), true)
39
+
40
+ key = payload.delete(:key)
41
+ debug { "#{name} #{key} #{(payload.inspect)}" }
42
+ end
43
+
44
+ private
45
+
46
+ def colorize_payload_name(name, payload_name, options = {})
47
+ if options[:AS]
48
+ color(name, MAGENTA, true)
49
+ else
50
+ color(name, CYAN, true)
51
+ end
52
+ end
53
+
54
+ def sql_color(sql)
55
+ case sql
56
+ when /\A\s*rollback/mi
57
+ RED
58
+ when /select .*for update/mi, /\A\s*lock/mi
59
+ WHITE
60
+ when /\A\s*select/i
61
+ BLUE
62
+ when /\A\s*insert/i
63
+ GREEN
64
+ when /\A\s*update/i
65
+ YELLOW
66
+ when /\A\s*delete/i
67
+ RED
68
+ when /transaction\s*\Z/i
69
+ CYAN
70
+ else
71
+ MAGENTA
72
+ end
73
+ end
74
+
75
+ def logger
76
+ TwitterWithAutoPagination::Client.logger
77
+ end
78
+ end
79
+ end
80
+
81
+ TwitterWithAutoPagination::LogSubscriber.attach_to :twitter_with_auto_pagination
82
+ TwitterWithAutoPagination::LogSubscriber.attach_to :active_support
@@ -0,0 +1,331 @@
1
+ module TwitterWithAutoPagination
2
+ module NewApi
3
+ def friends_parallelly(*args)
4
+ options = {super_operation: __method__}.merge(args.extract_options!)
5
+ _friend_ids = friend_ids(*(args + [options]))
6
+ users(_friend_ids.map { |id| id.to_i }, options)
7
+ end
8
+
9
+ def followers_parallelly(*args)
10
+ options = {super_operation: __method__}.merge(args.extract_options!)
11
+ _follower_ids = follower_ids(*(args + [options]))
12
+ users(_follower_ids.map { |id| id.to_i }, options)
13
+ end
14
+
15
+ def _fetch_parallelly(signatures) # [{method: :friends, args: ['ts_3156', ...], {...}]
16
+ result = Array.new(signatures.size)
17
+
18
+ Parallel.each_with_index(signatures, in_threads: result.size) do |signature, i|
19
+ result[i] = send(signature[:method], *signature[:args])
20
+ end
21
+
22
+ result
23
+ end
24
+
25
+ def friends_and_followers(*args)
26
+ _fetch_parallelly(
27
+ [
28
+ {method: :friends_parallelly, args: args},
29
+ {method: :followers_parallelly, args: args}])
30
+ end
31
+
32
+ def friends_followers_and_statuses(*args)
33
+ _fetch_parallelly(
34
+ [
35
+ {method: :friends_parallelly, args: args},
36
+ {method: :followers_parallelly, args: args},
37
+ {method: :user_timeline, args: args}])
38
+ end
39
+
40
+ def one_sided_following(me)
41
+ if uid_or_screen_name?(me)
42
+ # TODO use friends_and_followers
43
+ friends_parallelly(me).to_a - followers_parallelly(me).to_a
44
+ elsif me.respond_to?(:friends) && me.respond_to?(:followers)
45
+ me.friends.to_a - me.followers.to_a
46
+ else
47
+ raise
48
+ end
49
+ end
50
+
51
+ def one_sided_followers(me)
52
+ if uid_or_screen_name?(me)
53
+ # TODO use friends_and_followers
54
+ followers_parallelly(me).to_a - friends_parallelly(me).to_a
55
+ elsif me.respond_to?(:friends) && me.respond_to?(:followers)
56
+ me.followers.to_a - me.friends.to_a
57
+ else
58
+ raise
59
+ end
60
+ end
61
+
62
+ def mutual_friends(me)
63
+ if uid_or_screen_name?(me)
64
+ # TODO use friends_and_followers
65
+ friends_parallelly(me).to_a & followers_parallelly(me).to_a
66
+ elsif me.respond_to?(:friends) && me.respond_to?(:followers)
67
+ me.friends.to_a & me.followers.to_a
68
+ else
69
+ raise
70
+ end
71
+ end
72
+
73
+ def common_friends(me, you)
74
+ if uid_or_screen_name?(me) && uid_or_screen_name?(you)
75
+ friends_parallelly(me).to_a & friends_parallelly(you).to_a
76
+ elsif me.respond_to?(:friends) && you.respond_to?(:friends)
77
+ me.friends.to_a & you.friends.to_a
78
+ else
79
+ raise
80
+ end
81
+ end
82
+
83
+ def common_followers(me, you)
84
+ if uid_or_screen_name?(me) && uid_or_screen_name?(you)
85
+ followers_parallelly(me).to_a & followers_parallelly(you).to_a
86
+ elsif me.respond_to?(:followers) && you.respond_to?(:followers)
87
+ me.followers.to_a & you.followers.to_a
88
+ else
89
+ raise
90
+ end
91
+ end
92
+
93
+ def removing(pre_me, cur_me)
94
+ if uid_or_screen_name?(pre_me) && uid_or_screen_name?(cur_me)
95
+ friends_parallelly(pre_me).to_a - friends_parallelly(cur_me).to_a
96
+ elsif pre_me.respond_to?(:friends) && cur_me.respond_to?(:friends)
97
+ pre_me.friends.to_a - cur_me.friends.to_a
98
+ else
99
+ raise
100
+ end
101
+ end
102
+
103
+ def removed(pre_me, cur_me)
104
+ if uid_or_screen_name?(pre_me) && uid_or_screen_name?(cur_me)
105
+ followers_parallelly(pre_me).to_a - followers_parallelly(cur_me).to_a
106
+ elsif pre_me.respond_to?(:followers) && cur_me.respond_to?(:followers)
107
+ pre_me.followers.to_a - cur_me.followers.to_a
108
+ else
109
+ raise
110
+ end
111
+ end
112
+
113
+ def _extract_screen_names(tweets)
114
+ tweets.map do |t|
115
+ $1 if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
116
+ end.compact
117
+ end
118
+
119
+ # users which specified user is replying
120
+ # in_reply_to_user_id and in_reply_to_status_id is not used because of distinguishing mentions from replies
121
+ def replying(*args)
122
+ options = args.extract_options!
123
+ tweets =
124
+ if args.empty?
125
+ user_timeline(options)
126
+ elsif uid_or_screen_name?(args[0])
127
+ user_timeline(args[0], options)
128
+ elsif args[0].kind_of?(Array) && args[0].all? { |t| t.respond_to?(:text) }
129
+ args[0]
130
+ else
131
+ raise
132
+ end
133
+
134
+ screen_names = _extract_screen_names(tweets)
135
+ result = users(screen_names, {super_operation: __method__}.merge(options))
136
+ if options.has_key?(:uniq) && !options[:uniq]
137
+ screen_names.map { |sn| result.find { |r| r.screen_name == sn } }.compact
138
+ else
139
+ result.uniq { |r| r.id }
140
+ end
141
+ rescue Twitter::Error::NotFound => e
142
+ e.message == 'No user matches for specified terms.' ? [] : (raise e)
143
+ rescue => e
144
+ logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
145
+ raise e
146
+ end
147
+
148
+ def _extract_uids(tweets)
149
+ tweets.map do |t|
150
+ t.user.id.to_i if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
151
+ end.compact
152
+ end
153
+
154
+ def _extract_users(tweets, uids)
155
+ uids.map { |uid| tweets.find { |t| t.user.id.to_i == uid.to_i } }.map { |t| t.user }.compact
156
+ end
157
+
158
+ # users which specified user is replied
159
+ # when user is login you had better to call mentions_timeline
160
+ def replied(*args)
161
+ options = args.extract_options!
162
+
163
+ result =
164
+ if args.empty? || (uid_or_screen_name?(args[0]) && authenticating_user?(args[0]))
165
+ mentions_timeline.map { |m| m.user }
166
+ else
167
+ searched_result = search('@' + user(args[0]).screen_name, options)
168
+ uids = _extract_uids(searched_result)
169
+ _extract_users(searched_result, uids)
170
+ end
171
+
172
+ if options.has_key?(:uniq) && !options[:uniq]
173
+ result
174
+ else
175
+ result.uniq { |r| r.id }
176
+ end
177
+ end
178
+
179
+ def _count_users_with_two_sided_threshold(users, options)
180
+ min = options.has_key?(:min) ? options[:min] : 0
181
+ max = options.has_key?(:max) ? options[:max] : 1000
182
+ users.each_with_object(Hash.new(0)) { |u, memo| memo[u.id] += 1 }.
183
+ select { |_k, v| min <= v && v <= max }.
184
+ sort_by { |_, v| -v }.to_h
185
+ end
186
+
187
+ def _extract_favorite_users(favs, options = {})
188
+ counted_value = _count_users_with_two_sided_threshold(favs.map { |t| t.user }, options)
189
+ counted_value.map do |uid, cnt|
190
+ fav = favs.find { |f| f.user.id.to_i == uid.to_i }
191
+ Array.new(cnt, fav.user)
192
+ end.flatten
193
+ end
194
+
195
+ def favoriting(*args)
196
+ options = args.extract_options!
197
+
198
+ favs =
199
+ if args.empty?
200
+ favorites(options)
201
+ elsif uid_or_screen_name?(args[0])
202
+ favorites(args[0], options)
203
+ elsif args[0].kind_of?(Array) && args[0].all? { |t| t.respond_to?(:text) }
204
+ args[0]
205
+ else
206
+ raise
207
+ end
208
+
209
+ result = _extract_favorite_users(favs, options)
210
+ if options.has_key?(:uniq) && !options[:uniq]
211
+ result
212
+ else
213
+ result.uniq { |r| r.id }
214
+ end
215
+ rescue => e
216
+ logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
217
+ raise e
218
+ end
219
+
220
+ def _extract_inactive_users(users, options = {})
221
+ authorized = options.delete(:authorized)
222
+ two_weeks_ago = 2.weeks.ago.to_i
223
+ users.select do |u|
224
+ if authorized
225
+ (Time.parse(u.status.created_at).to_i < two_weeks_ago) rescue false
226
+ else
227
+ false
228
+ end
229
+ end
230
+ end
231
+
232
+ def favorited_by(*args)
233
+ end
234
+
235
+ def close_friends(*args)
236
+ options = {uniq: false}.merge(args.extract_options!)
237
+ min_max = {
238
+ min: options.has_key?(:min) ? options.delete(:min) : 0,
239
+ max: options.has_key?(:max) ? options.delete(:max) : 1000
240
+ }
241
+
242
+ _replying, _replied, _favoriting =
243
+ if args.empty?
244
+ [replying(options), replied(options), favoriting(options)]
245
+ elsif uid_or_screen_name?(args[0])
246
+ [replying(args[0], options), replied(args[0], options), favoriting(args[0], options)]
247
+ elsif (m_names = %i(replying replied favoriting)).all? { |m_name| args[0].respond_to?(m_name) }
248
+ m_names.map { |mn| args[0].send(mn) }
249
+ else
250
+ raise
251
+ end
252
+
253
+ _users = _replying + _replied + _favoriting
254
+ return [] if _users.empty?
255
+
256
+ scores = _count_users_with_two_sided_threshold(_users, min_max)
257
+ replying_scores = _count_users_with_two_sided_threshold(_replying, min_max)
258
+ replied_scores = _count_users_with_two_sided_threshold(_replied, min_max)
259
+ favoriting_scores = _count_users_with_two_sided_threshold(_favoriting, min_max)
260
+
261
+ scores.keys.map { |uid| _users.find { |u| u.id.to_i == uid.to_i } }.
262
+ map do |u|
263
+ u[:score] = scores[u.id]
264
+ u[:replying_score] = replying_scores[u.id]
265
+ u[:replied_score] = replied_scores[u.id]
266
+ u[:favoriting_score] = favoriting_scores[u.id]
267
+ u
268
+ end
269
+ end
270
+
271
+ def inactive_friends(user = nil)
272
+ if user.blank?
273
+ _extract_inactive_users(friends_parallelly, authorized: true)
274
+ elsif uid_or_screen_name?(user)
275
+ authorized = authenticating_user?(user) || authorized_user?(user)
276
+ _extract_inactive_users(friends_parallelly(user), authorized: authorized)
277
+ elsif user.respond_to?(:friends)
278
+ authorized = authenticating_user?(user.uid.to_i) || authorized_user?(user.uid.to_i)
279
+ _extract_inactive_users(user.friends, authorized: authorized)
280
+ else
281
+ raise
282
+ end
283
+ end
284
+
285
+ def inactive_followers(user = nil)
286
+ if user.blank?
287
+ _extract_inactive_users(followers_parallelly, authorized: true)
288
+ elsif uid_or_screen_name?(user)
289
+ authorized = authenticating_user?(user) || authorized_user?(user)
290
+ _extract_inactive_users(followers_parallelly(user), authorized: authorized)
291
+ elsif user.respond_to?(:followers)
292
+ authorized = authenticating_user?(user.uid.to_i) || authorized_user?(user.uid.to_i)
293
+ _extract_inactive_users(user.followers, authorized: authorized)
294
+ else
295
+ raise
296
+ end
297
+ end
298
+
299
+ def clusters_belong_to(text)
300
+ return [] if text.blank?
301
+
302
+ exclude_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_bad_words_path']))
303
+ special_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_good_words_path']))
304
+
305
+ # クラスタ用の単語の出現回数を記録
306
+ cluster_word_counter =
307
+ special_words.map { |sw| [sw, text.scan(sw)] }
308
+ .delete_if { |item| item[1].empty? }
309
+ .each_with_object(Hash.new(1)) { |item, memo| memo[item[0]] = item[1].size }
310
+
311
+ # 同一文字種の繰り返しを見付ける。漢字の繰り返し、ひらがなの繰り返し、カタカナの繰り返し、など
312
+ text.scan(/[一-龠〆ヵヶ々]+|[ぁ-んー~]+|[ァ-ヴー~]+|[a-zA-Z0-9]+|[、。!!??]+/).
313
+
314
+ # 複数回繰り返される文字を除去
315
+ map { |w| w.remove /[?!?!。、w]|(ー{2,})/ }.
316
+
317
+ # 文字数の少なすぎる単語、ひらがなだけの単語、除外単語を除去する
318
+ delete_if { |w| w.length <= 1 || (w.length <= 2 && w =~ /^[ぁ-んー~]+$/) || exclude_words.include?(w) }.
319
+
320
+ # 出現回数を記録
321
+ each { |w| cluster_word_counter[w] += 1 }
322
+
323
+ # 複数個以上見付かった単語のみを残し、出現頻度順にソート
324
+ cluster_word_counter.select { |_, v| v > 3 }.sort_by { |_, v| -v }.to_h
325
+ end
326
+
327
+ def clusters_assigned_to
328
+ raise NotImplementedError.new
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,303 @@
1
+ module TwitterWithAutoPagination
2
+ module Utils
3
+ # for backward compatibility
4
+ def uid
5
+ @uid || user.id.to_i
6
+ end
7
+
8
+ def __uid
9
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
10
+ `TwitterWithAutoPagination::Utils#__uid` is deprecated.
11
+ MESSAGE
12
+ uid
13
+ end
14
+
15
+ def __uid_i
16
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
17
+ `TwitterWithAutoPagination::Utils#__uid_i` is deprecated.
18
+ MESSAGE
19
+ uid
20
+ end
21
+
22
+ # for backward compatibility
23
+ def screen_name
24
+ @screen_name || user.screen_name
25
+ end
26
+
27
+ def __screen_name
28
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
29
+ `TwitterWithAutoPagination::Utils#__screen_name` is deprecated.
30
+ MESSAGE
31
+ screen_name
32
+ end
33
+
34
+ def uid_or_screen_name?(object)
35
+ object.kind_of?(String) || object.kind_of?(Integer)
36
+ end
37
+
38
+ def authenticating_user?(target)
39
+ user.id.to_i == user(target).id.to_i
40
+ end
41
+
42
+ def authorized_user?(target)
43
+ target_user = user(target)
44
+ !target_user.protected? || friendship?(user.id.to_i, target_user.id.to_i)
45
+ end
46
+
47
+ def instrument(operation, key, options = nil)
48
+ payload = {operation: operation, key: key}
49
+ payload.merge!(options) if options.is_a?(Hash)
50
+ ActiveSupport::Notifications.instrument('call.twitter_with_auto_pagination', payload) { yield(payload) }
51
+ end
52
+
53
+ def call_old_method(method_name, *args)
54
+ options = args.extract_options!
55
+ begin
56
+ self.call_count += 1
57
+ _options = {method_name: method_name, call_count: self.call_count, args: args}.merge(options)
58
+ instrument('api call', args[0], _options) { send(method_name, *args, options) }
59
+ rescue Twitter::Error::TooManyRequests => e
60
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} Retry after #{e.rate_limit.reset_in} seconds."
61
+ raise e
62
+ rescue Twitter::Error::ServiceUnavailable => e
63
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
64
+ raise e
65
+ rescue Twitter::Error::InternalServerError => e
66
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
67
+ raise e
68
+ rescue Twitter::Error::Forbidden => e
69
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
70
+ raise e
71
+ rescue Twitter::Error::NotFound => e
72
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
73
+ raise e
74
+ rescue => e
75
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
76
+ raise e
77
+ end
78
+ end
79
+
80
+ # user_timeline, search
81
+ def collect_with_max_id(method_name, *args)
82
+ options = args.extract_options!
83
+ call_limit = options.delete(:call_limit) || 3
84
+ last_response = call_old_method(method_name, *args, options)
85
+ last_response = yield(last_response) if block_given?
86
+ return_data = last_response
87
+ call_count = 1
88
+
89
+ while last_response.any? && call_count < call_limit
90
+ options[:max_id] = last_response.last.kind_of?(Hash) ? last_response.last[:id] : last_response.last.id
91
+ last_response = call_old_method(method_name, *args, options)
92
+ last_response = yield(last_response) if block_given?
93
+ return_data += last_response
94
+ call_count += 1
95
+ end
96
+
97
+ return_data.flatten
98
+ end
99
+
100
+ # friends, followers
101
+ def collect_with_cursor(method_name, *args)
102
+ options = args.extract_options!
103
+ last_response = call_old_method(method_name, *args, options).attrs
104
+ return_data = (last_response[:users] || last_response[:ids])
105
+
106
+ while (next_cursor = last_response[:next_cursor]) && next_cursor != 0
107
+ options[:cursor] = next_cursor
108
+ last_response = call_old_method(method_name, *args, options).attrs
109
+ return_data += (last_response[:users] || last_response[:ids])
110
+ end
111
+
112
+ return_data
113
+ end
114
+
115
+ require 'digest/md5'
116
+
117
+ def file_cache_key(method_name, user, options = {})
118
+ delim = ':'
119
+ identifier =
120
+ case
121
+ when method_name == :verify_credentials
122
+ "object-id#{delim}#{object_id}"
123
+ when method_name == :search
124
+ "str#{delim}#{user.to_s}"
125
+ when method_name == :mentions_timeline
126
+ "#{user.kind_of?(Integer) ? 'id' : 'sn'}#{delim}#{user.to_s}"
127
+ when method_name == :home_timeline
128
+ "#{user.kind_of?(Integer) ? 'id' : 'sn'}#{delim}#{user.to_s}"
129
+ when method_name.in?([:users, :replying]) && options[:super_operation].present?
130
+ case
131
+ when user.kind_of?(Array) && user.first.kind_of?(Integer)
132
+ "#{options[:super_operation]}-ids#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
133
+ when user.kind_of?(Array) && user.first.kind_of?(String)
134
+ "#{options[:super_operation]}-sns#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
135
+ else raise "#{method_name.inspect} #{user.inspect}"
136
+ end
137
+ when user.kind_of?(Integer)
138
+ "id#{delim}#{user.to_s}"
139
+ when user.kind_of?(Array) && user.first.kind_of?(Integer)
140
+ "ids#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
141
+ when user.kind_of?(Array) && user.first.kind_of?(String)
142
+ "sns#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
143
+ when user.kind_of?(String)
144
+ "sn#{delim}#{user}"
145
+ when user.kind_of?(Twitter::User)
146
+ "user#{delim}#{user.id.to_s}"
147
+ else raise "#{method_name.inspect} #{user.inspect}"
148
+ end
149
+
150
+ "#{method_name}#{delim}#{identifier}"
151
+ end
152
+
153
+ def namespaced_key(method_name, user, options = {})
154
+ file_cache_key(method_name, user, options)
155
+ end
156
+
157
+ PROFILE_SAVE_KEYS = %i(
158
+ id
159
+ name
160
+ screen_name
161
+ location
162
+ description
163
+ url
164
+ protected
165
+ followers_count
166
+ friends_count
167
+ listed_count
168
+ favourites_count
169
+ utc_offset
170
+ time_zone
171
+ geo_enabled
172
+ verified
173
+ statuses_count
174
+ lang
175
+ status
176
+ profile_image_url_https
177
+ profile_banner_url
178
+ profile_link_color
179
+ suspended
180
+ verified
181
+ entities
182
+ created_at
183
+ )
184
+
185
+ STATUS_SAVE_KEYS = %i(
186
+ created_at
187
+ id
188
+ text
189
+ source
190
+ truncated
191
+ coordinates
192
+ place
193
+ entities
194
+ user
195
+ contributors
196
+ is_quote_status
197
+ retweet_count
198
+ favorite_count
199
+ favorited
200
+ retweeted
201
+ possibly_sensitive
202
+ lang
203
+ )
204
+
205
+ # encode
206
+ def encode_json(obj, caller_name, options = {})
207
+ options[:reduce] = true unless options.has_key?(:reduce)
208
+ case caller_name
209
+ when :user_timeline, :home_timeline, :mentions_timeline, :favorites # Twitter::Tweet
210
+ JSON.pretty_generate(obj.map { |o| o.attrs })
211
+
212
+ when :search # Hash
213
+ data =
214
+ if options[:reduce]
215
+ obj.map { |o| o.to_hash.slice(*STATUS_SAVE_KEYS) }
216
+ else
217
+ obj.map { |o| o.to_hash }
218
+ end
219
+ JSON.pretty_generate(data)
220
+
221
+ when :friends, :followers # Hash
222
+ data =
223
+ if options[:reduce]
224
+ obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
225
+ else
226
+ obj.map { |o| o.to_hash }
227
+ end
228
+ JSON.pretty_generate(data)
229
+
230
+ when :friend_ids, :follower_ids # Integer
231
+ JSON.pretty_generate(obj)
232
+
233
+ when :verify_credentials # Twitter::User
234
+ JSON.pretty_generate(obj.to_hash.slice(*PROFILE_SAVE_KEYS))
235
+
236
+ when :user # Twitter::User
237
+ JSON.pretty_generate(obj.to_hash.slice(*PROFILE_SAVE_KEYS))
238
+
239
+ when :users, :friends_parallelly, :followers_parallelly # Twitter::User
240
+ data =
241
+ if options[:reduce]
242
+ obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
243
+ else
244
+ obj.map { |o| o.to_hash }
245
+ end
246
+ JSON.pretty_generate(data)
247
+
248
+ when :user? # true or false
249
+ obj
250
+
251
+ when :friendship? # true or false
252
+ obj
253
+
254
+ else
255
+ raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
256
+ end
257
+ end
258
+
259
+ # decode
260
+ def decode_json(json_str, caller_name, options = {})
261
+ obj = json_str.kind_of?(String) ? JSON.parse(json_str) : json_str
262
+ case
263
+ when obj.nil?
264
+ obj
265
+
266
+ when obj.kind_of?(Array) && obj.first.kind_of?(Hash)
267
+ obj.map { |o| Hashie::Mash.new(o) }
268
+
269
+ when obj.kind_of?(Array) && obj.first.kind_of?(Integer)
270
+ obj
271
+
272
+ when obj.kind_of?(Hash)
273
+ Hashie::Mash.new(obj)
274
+
275
+ when obj === true || obj === false
276
+ obj
277
+
278
+ when obj.kind_of?(Array) && obj.empty?
279
+ obj
280
+
281
+ else
282
+ raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
283
+ end
284
+ end
285
+
286
+ def fetch_cache_or_call_api(method_name, user, options = {})
287
+ key = namespaced_key(method_name, user, options)
288
+ options.update(key: key)
289
+
290
+ data =
291
+ if options[:cache] == :read
292
+ instrument('Cache Read(Force)', key, caller: method_name) { cache.read(key) }
293
+ else
294
+ cache.fetch(key, expires_in: 1.hour, race_condition_ttl: 5.minutes) do
295
+ _d = yield
296
+ instrument('serialize', key, caller: method_name) { encode_json(_d, method_name, options) }
297
+ end
298
+ end
299
+
300
+ instrument('deserialize', key, caller: method_name) { decode_json(data, method_name, options) }
301
+ end
302
+ end
303
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'twitter_with_auto_pagination'
2
+ require 'rspec'
3
+
4
+ RSpec.configure do |config|
5
+ config.expect_with :rspec do |c|
6
+ c.syntax = :expect
7
+ end
8
+ config.order = 'random'
9
+ end
@@ -0,0 +1,131 @@
1
+ require 'helper'
2
+
3
+ describe TwitterWithAutoPagination do
4
+ let(:config) {
5
+ {
6
+ consumer_key: 'CK',
7
+ consumer_secret: 'CS',
8
+ access_token: 'AT',
9
+ access_token_secret: 'ATS',
10
+ }
11
+ }
12
+ let(:client) { TwitterWithAutoPagination::Client.new(config) }
13
+
14
+ describe '#initialize' do
15
+ let(:default_call_count) { 0 }
16
+
17
+ it 'sets call_count to 0' do
18
+ expect(client.call_count).to eq(default_call_count)
19
+ end
20
+
21
+ context 'without params' do
22
+ end
23
+
24
+ context 'with params' do
25
+ end
26
+ end
27
+
28
+ describe '#logger' do
29
+ it 'has logger' do
30
+ expect(client.logger).to be_truthy
31
+ end
32
+ end
33
+
34
+ describe '#call_old_method' do
35
+ end
36
+
37
+ describe '#collect_with_max_id' do
38
+ end
39
+
40
+ describe '#collect_with_cursor' do
41
+ end
42
+
43
+ describe '#file_cache_key' do
44
+ end
45
+
46
+ describe '#namespaced_key' do
47
+ end
48
+
49
+ describe '#encode_json' do
50
+ end
51
+
52
+ describe '#decode_json' do
53
+ end
54
+
55
+ describe '#fetch_cache_or_call_api' do
56
+ end
57
+
58
+ describe '#user_timeline' do
59
+ it 'calls old_user_timeline' do
60
+ expect(client).to receive(:old_user_timeline)
61
+ client.user_timeline
62
+ end
63
+
64
+ it 'calls collect_with_max_id' do
65
+ expect(client).to receive(:collect_with_max_id)
66
+ client.user_timeline
67
+ end
68
+ end
69
+
70
+ describe '#user_photos' do
71
+ it 'calls user_timeline' do
72
+ expect(client).to receive(:user_timeline)
73
+ client.user_photos
74
+ end
75
+ end
76
+
77
+ describe '#friends' do
78
+ it 'calls old_friends' do
79
+ expect(client).to receive(:old_friends)
80
+ client.friends
81
+ end
82
+
83
+ it 'calls collect_with_cursor' do
84
+ expect(client).to receive(:collect_with_cursor)
85
+ client.friends
86
+ end
87
+ end
88
+
89
+ describe '#followers' do
90
+ it 'calls old_followers' do
91
+ expect(client).to receive(:old_followers)
92
+ client.followers
93
+ end
94
+
95
+ it 'calls collect_with_cursor' do
96
+ expect(client).to receive(:collect_with_cursor)
97
+ client.followers
98
+ end
99
+ end
100
+
101
+ describe '#friend_ids' do
102
+ it 'calls old_friend_ids' do
103
+ expect(client).to receive(:old_friend_ids)
104
+ client.friend_ids
105
+ end
106
+
107
+ it 'calls collect_with_cursor' do
108
+ expect(client).to receive(:collect_with_cursor)
109
+ client.friend_ids
110
+ end
111
+ end
112
+
113
+ describe '#follower_ids' do
114
+ it 'calls old_follower_ids' do
115
+ expect(client).to receive(:old_follower_ids)
116
+ client.follower_ids
117
+ end
118
+
119
+ it 'calls collect_with_cursor' do
120
+ expect(client).to receive(:collect_with_cursor)
121
+ client.follower_ids
122
+ end
123
+ end
124
+
125
+ describe '#users' do
126
+ it 'calls old_users' do
127
+ expect(client).to receive(:old_users)
128
+ client.users([1, 2, 3])
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,25 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.add_dependency 'twitter'
6
+ spec.add_dependency 'activesupport'
7
+ spec.add_dependency 'hashie'
8
+ spec.add_dependency 'parallel'
9
+
10
+ spec.add_development_dependency 'bundler'
11
+
12
+ spec.authors = ['Shinohara Teruki']
13
+ spec.description = %q(Add auto paginate feature to Twitter gem.)
14
+ spec.email = %w[ts_3156@yahoo.co.jp]
15
+ spec.files = %w[LICENSE.md README.md Rakefile twitter_with_auto_pagination.gemspec]
16
+ spec.files += Dir.glob('lib/**/*.rb')
17
+ spec.files += Dir.glob('spec/**/*')
18
+ spec.homepage = 'http://github.com/ts-3156/twitter_with_auto_pagination/'
19
+ spec.licenses = %w[MIT]
20
+ spec.name = 'twitter_with_auto_pagination'
21
+ spec.require_paths = %w[lib]
22
+ spec.summary = spec.description
23
+ spec.test_files = Dir.glob('spec/**/*')
24
+ spec.version = '0.4.0'
25
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twitter_with_auto_pagination
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Shinohara Teruki
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: twitter
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hashie
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: parallel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Add auto paginate feature to Twitter gem.
84
+ email:
85
+ - ts_3156@yahoo.co.jp
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - LICENSE.md
91
+ - README.md
92
+ - Rakefile
93
+ - lib/twitter_with_auto_pagination.rb
94
+ - lib/twitter_with_auto_pagination/client.rb
95
+ - lib/twitter_with_auto_pagination/existing_api.rb
96
+ - lib/twitter_with_auto_pagination/log_subscriber.rb
97
+ - lib/twitter_with_auto_pagination/new_api.rb
98
+ - lib/twitter_with_auto_pagination/utils.rb
99
+ - spec/helper.rb
100
+ - spec/twitter_with_auto_pagination_spec.rb
101
+ - twitter_with_auto_pagination.gemspec
102
+ homepage: http://github.com/ts-3156/twitter_with_auto_pagination/
103
+ licenses:
104
+ - MIT
105
+ metadata: {}
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.5.1
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Add auto paginate feature to Twitter gem.
126
+ test_files:
127
+ - spec/helper.rb
128
+ - spec/twitter_with_auto_pagination_spec.rb