ex_twitter 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 68eb499d7d42d9fa3351be383a820068509588b1
4
- data.tar.gz: 9a5977112d10a15feb4e4b5544ae164cd1f69d36
3
+ metadata.gz: 1e529d197b3ac12452f7d3864c2e00bb7e78822a
4
+ data.tar.gz: 2363dd11f96d395e19f68a4db3aa814240d54031
5
5
  SHA512:
6
- metadata.gz: 7dc179bd00f61fe17739bdfc60a99124193afb8372121869ceba71bfd6f8854cabb77491a38af577af4be6e6801b58e4af290b657dc384d837e481fe20d22ac1
7
- data.tar.gz: 361e9707b96c3d40c2ecc6a1d45f17d29901dce7a17af85acb2df5c773a051d7f16d4e463571ab11a5c1a94a6497a3e235424113704d6e85c6c8e196e9589cad
6
+ metadata.gz: 11012a0796bf7d3ab4ba4838803c03125425220b20d1ba777397f5f588141efb6fcd6bb8c1b4678d55a6b4ac3719172dc92cecbb52379776be315808abc15c8e
7
+ data.tar.gz: 67a189e3be3eeccfd934bebc9c37bcc039eb39340fe2b710b5a2eaad375087c5441ccfe15ebed2e9fed8daaff74ea6d893380101bf80d0d91444d948469c1415
@@ -4,6 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  Gem::Specification.new do |spec|
5
5
  spec.add_dependency 'twitter'
6
6
  spec.add_dependency 'activesupport'
7
+ spec.add_dependency 'hashie'
7
8
  spec.add_dependency 'parallel'
8
9
 
9
10
  spec.add_development_dependency 'bundler'
@@ -20,5 +21,5 @@ Gem::Specification.new do |spec|
20
21
  spec.require_paths = %w[lib]
21
22
  spec.summary = spec.description
22
23
  spec.test_files = Dir.glob('spec/**/*')
23
- spec.version = '0.0.4'
24
+ spec.version = '0.1.0'
24
25
  end
@@ -1,267 +1,806 @@
1
1
  require 'active_support'
2
- require 'active_support/core_ext'
2
+ require 'active_support/cache'
3
+ require 'active_support/core_ext/string'
4
+
5
+ require 'ex_twitter_subscriber'
6
+
3
7
  require 'twitter'
4
- require 'yaml'
5
- require 'active_support'
8
+ require 'hashie'
9
+ require 'memoist'
6
10
  require 'parallel'
7
- require 'logger'
8
11
 
9
- # extended twitter
10
- class ExTwitter < Twitter::REST::Client
11
- attr_accessor :cache, :cache_expires_in, :max_retries, :wait, :auto_paginate, :max_paginates, :logger
12
12
 
13
- MAX_RETRIES = 1
14
- WAIT = false
15
- # CACHE_EXPIRES_IN = 300
16
- AUTO_PAGINATE = true
17
- MAX_PAGINATES = 3
13
+
14
+ class ExTwitter < Twitter::REST::Client
15
+ extend Memoist
18
16
 
19
17
  def initialize(options = {})
20
- self.logger = Logger.new(STDOUT)
21
- self.logger.level = Logger::DEBUG
18
+ api_cache_prefix = options.has_key?(:api_cache_prefix) ? options.delete(:api_cache_prefix) : '%Y%m%d%H'
19
+ @cache = ActiveSupport::Cache::FileStore.new(File.join('tmp', 'api_cache', Time.now.strftime(api_cache_prefix)))
20
+ @uid = options[:uid]
21
+ @screen_name = options[:screen_name]
22
+ @authenticated_user = Hashie::Mash.new({uid: options[:uid].to_i, screen_name: options[:screen_name]})
23
+ @call_count = 0
24
+ ExTwitterSubscriber.attach_to :ex_twitter
25
+ super
26
+ end
22
27
 
23
- self.auto_paginate = AUTO_PAGINATE
24
- self.max_retries = MAX_RETRIES
25
- self.wait = WAIT
26
- self.max_paginates = MAX_PAGINATES
28
+ attr_accessor :call_count
29
+ attr_reader :cache, :authenticated_user
27
30
 
28
- # self.cache_expires_in = (config[:cache_expires_in] || CACHE_EXPIRES_IN)
29
- # self.cache = ActiveSupport::Cache::FileStore.new(File.join(Dir::pwd, 'ex_twitter_cache'),
30
- # {expires_in: self.cache_expires_in, race_condition_ttl: self.cache_expires_in})
31
+ INDENT = 4
31
32
 
32
- super
33
+ # for backward compatibility
34
+ def uid
35
+ @uid
36
+ end
37
+
38
+ def __uid
39
+ @uid
33
40
  end
34
41
 
35
- # def read(key)
36
- # self.cache.read(key)
37
- # rescue => e
38
- # logger.warn "in read #{key} #{e.inspect}"
39
- # nil
40
- # end
41
- #
42
- # def write(key, value)
43
- # self.cache.write(key, value)
44
- # rescue => e
45
- # logger.warn "in write #{key} #{value} #{e.inspect}"
46
- # false
47
- # end
48
-
49
- # githubでは、レスポンスが次のエンドポイントを知っているため、ブロックをデータの結合のみに使っている。引数ではAPI名を渡している。
50
- # 一方、twitterでは、レスポンスが次のエンドポイントを知らないため、ブロック内にAPI名を持ち、再帰的にブロックを呼び出している。
51
- # また、再帰を使うため、引数をコレクションの引き渡しに使ってしまっている。(この問題については通常のループを使えば解決する)
52
- #
53
- # APIは通常繰り返しの呼び出しになるため、API名は常に同じものを保持しておき、ブロックはデータの結合に使った方が無駄がなく自由度が高い。
54
- # また、再帰ではなく通常のループを使った方が分かりやすい。
55
- #
56
- # このgithub方式を実現するためには、メソッド名(例:user_timeline)を渡し、ループさせるか、user_timelineが内部で使っている
57
- # objects_from_response_with_userをループさせればよい。
58
- #
59
- # この2種類の方法を比較した場合、より外側のレイヤーを使った方が、使うライブラリの内部情報に依存しなくなるため、好ましい。
60
- #
61
- # twitterで再帰方式がとられている理由は、おそらく、一般ユーザー向けのサンプルでメソッド名を渡すようなリフレクションを避けるため、
62
- # なのかもしれない。
63
-
64
- # max_idを使って自動ページングを行う
65
- def collect_with_max_id(method_name, *args, &block)
42
+ def __uid_i
43
+ @uid.to_i
44
+ end
45
+
46
+ # for backward compatibility
47
+ def screen_name
48
+ @screen_name
49
+ end
50
+
51
+ def __screen_name
52
+ @screen_name
53
+ end
54
+
55
+ def logger
56
+ Dir.mkdir('log') unless File.exists?('log')
57
+ @logger ||= Logger.new('log/ex_twitter.log')
58
+ end
59
+
60
+ def instrument(operation)
61
+ ActiveSupport::Notifications.instrument('call.ex_twitter', name: operation) do
62
+ yield
63
+ end
64
+ end
65
+
66
+ def call_old_method(method_name, *args)
66
67
  options = args.extract_options!
67
- logger.info "#{method_name}, #{args.inspect} #{options.inspect}"
68
+ begin
69
+ start_t = Time.now
70
+ result = send(method_name, *args, options); self.call_count += 1
71
+ end_t = Time.now
72
+ logger.debug "#{method_name} #{args.inspect} #{options.inspect} (#{end_t - start_t}s)".indent(INDENT)
73
+ result
74
+ rescue Twitter::Error::TooManyRequests => e
75
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} Retry after #{e.rate_limit.reset_in} seconds."
76
+ raise e
77
+ rescue Twitter::Error::ServiceUnavailable => e
78
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
79
+ raise e
80
+ rescue Twitter::Error::InternalServerError => e
81
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
82
+ raise e
83
+ rescue Twitter::Error::Forbidden => e
84
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
85
+ raise e
86
+ rescue Twitter::Error::NotFound => e
87
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
88
+ raise e
89
+ rescue => e
90
+ logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
91
+ raise e
92
+ end
93
+ end
68
94
 
69
- max_paginates = options.delete(:max_paginates) || MAX_PAGINATES
70
- data = []
71
- last_response = send(method_name, *args, options)
72
- if block_given?
73
- last_response = yield(data, last_response)
74
- else
75
- data.concat(last_response) if last_response.is_a?(Array)
95
+ # user_timeline, search
96
+ def collect_with_max_id(method_name, *args)
97
+ options = args.extract_options!
98
+ options[:call_count] = 3 unless options.has_key?(:call_count)
99
+ last_response = call_old_method(method_name, *args, options)
100
+ last_response = yield(last_response) if block_given?
101
+ return_data = last_response
102
+ call_count = 1
103
+
104
+ while last_response.any? && call_count < options[:call_count]
105
+ options[:max_id] = last_response.last.kind_of?(Hash) ? last_response.last[:id] : last_response.last.id
106
+ last_response = call_old_method(method_name, *args, options)
107
+ last_response = yield(last_response) if block_given?
108
+ return_data += last_response
109
+ call_count += 1
76
110
  end
77
111
 
78
- if auto_paginate
79
- num_retries = 0
80
- (max_paginates - 1).times do
81
- break if last_response.blank?
82
-
83
- options[:max_id] = last_response.last[:id] - 1
84
-
85
- begin
86
- last_response = send(method_name, *args, options)
87
- logger.info "#{method_name}, #{args.inspect} #{options.inspect}"
88
- rescue Twitter::Error::TooManyRequests => e
89
- if num_retries <= MAX_RETRIES
90
- if WAIT
91
- sleep e.rate_limit.reset_in
92
- num_retries += 1
93
- retry
112
+ return_data.flatten
113
+ end
114
+
115
+ # friends, followers
116
+ def collect_with_cursor(method_name, *args)
117
+ options = args.extract_options!
118
+ last_response = call_old_method(method_name, *args, options).attrs
119
+ return_data = (last_response[:users] || last_response[:ids])
120
+
121
+ while (next_cursor = last_response[:next_cursor]) && next_cursor != 0
122
+ options[:cursor] = next_cursor
123
+ last_response = call_old_method(method_name, *args, options).attrs
124
+ return_data += (last_response[:users] || last_response[:ids])
125
+ end
126
+
127
+ return_data
128
+ end
129
+
130
+ require 'digest/md5'
131
+
132
+ # currently ignore options
133
+ def file_cache_key(method_name, user)
134
+ delim = ':'
135
+ identifier =
136
+ case
137
+ when method_name == :search
138
+ "str#{delim}#{user.to_s}"
139
+ when method_name == :mentions_timeline
140
+ "myself#{delim}#{user.to_s}"
141
+ when method_name == :home_timeline
142
+ "myself#{delim}#{user.to_s}"
143
+ when user.kind_of?(Integer)
144
+ "id#{delim}#{user.to_s}"
145
+ when user.kind_of?(Array) && user.first.kind_of?(Integer)
146
+ "ids#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
147
+ when user.kind_of?(Array) && user.first.kind_of?(String)
148
+ "sns#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
149
+ when user.kind_of?(String)
150
+ "sn#{delim}#{user}"
151
+ when user.kind_of?(Twitter::User)
152
+ "user#{delim}#{user.id.to_s}"
153
+ else raise "#{method_name.inspect} #{user.inspect}"
154
+ end
155
+
156
+ "#{method_name}#{delim}#{identifier}"
157
+ end
158
+
159
+ def namespaced_key(method_name, user)
160
+ file_cache_key(method_name, user)
161
+ end
162
+
163
+ PROFILE_SAVE_KEYS = %i(
164
+ id
165
+ name
166
+ screen_name
167
+ location
168
+ description
169
+ url
170
+ protected
171
+ followers_count
172
+ friends_count
173
+ listed_count
174
+ favourites_count
175
+ utc_offset
176
+ time_zone
177
+ geo_enabled
178
+ verified
179
+ statuses_count
180
+ lang
181
+ status
182
+ profile_image_url_https
183
+ profile_banner_url
184
+ profile_link_color
185
+ suspended
186
+ verified
187
+ entities
188
+ created_at
189
+ )
190
+
191
+ STATUS_SAVE_KEYS = %i(
192
+ created_at
193
+ id
194
+ text
195
+ source
196
+ truncated
197
+ coordinates
198
+ place
199
+ entities
200
+ user
201
+ contributors
202
+ is_quote_status
203
+ retweet_count
204
+ favorite_count
205
+ favorited
206
+ retweeted
207
+ possibly_sensitive
208
+ lang
209
+ )
210
+
211
+ # encode
212
+ def encode_json(obj, caller_name, options = {})
213
+ options[:reduce] = true unless options.has_key?(:reduce)
214
+ start_t = Time.now
215
+ result =
216
+ case caller_name
217
+ when :user_timeline, :home_timeline, :mentions_timeline, :favorites # Twitter::Tweet
218
+ JSON.pretty_generate(obj.map { |o| o.attrs })
219
+
220
+ when :search # Hash
221
+ data =
222
+ if options[:reduce]
223
+ obj.map { |o| o.to_hash.slice(*STATUS_SAVE_KEYS) }
94
224
  else
95
- logger.warn "retry #{e.rate_limit.reset_in} seconds later, #{e.inspect}"
225
+ obj.map { |o| o.to_hash }
96
226
  end
97
- else
98
- logger.warn "fail. num_retries > MAX_RETRIES(=#{MAX_RETRIES}), #{e.inspect}"
99
- end
100
- rescue => e
101
- if num_retries <= MAX_RETRIES
102
- logger.warn "retry till num_retries > MAX_RETRIES(=#{MAX_RETRIES}), #{e.inspect}"
103
- num_retries += 1
104
- retry
105
- else
106
- logger.warn "fail. num_retries > MAX_RETRIES(=#{MAX_RETRIES}), something error #{e.inspect}"
107
- end
108
- end
227
+ JSON.pretty_generate(data)
228
+
229
+ when :friends, :followers # Hash
230
+ data =
231
+ if options[:reduce]
232
+ obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
233
+ else
234
+ obj.map { |o| o.to_hash }
235
+ end
236
+ JSON.pretty_generate(data)
237
+
238
+ when :friend_ids, :follower_ids # Integer
239
+ JSON.pretty_generate(obj)
240
+
241
+ when :user # Twitter::User
242
+ JSON.pretty_generate(obj.to_hash.slice(*PROFILE_SAVE_KEYS))
243
+
244
+ when :users, :friends_advanced, :followers_advanced # Twitter::User
245
+ data =
246
+ if options[:reduce]
247
+ obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
248
+ else
249
+ obj.map { |o| o.to_hash }
250
+ end
251
+ JSON.pretty_generate(data)
252
+
253
+ when :user? # true or false
254
+ obj
255
+
256
+ when :friendship? # true or false
257
+ obj
109
258
 
110
- if block_given?
111
- last_response = yield(data, last_response)
112
259
  else
113
- data.concat(last_response) if last_response.is_a?(Array)
114
- end
260
+ raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
115
261
  end
116
- end
262
+ end_t = Time.now
263
+ logger.debug "#{__method__}: caller=#{caller_name} key=#{options[:key]} (#{end_t - start_t}s)".indent(INDENT)
264
+ result
265
+ end
266
+
267
+ # decode
268
+ def decode_json(json_str, caller_name, options = {})
269
+ start_t = Time.now
270
+ obj = json_str.kind_of?(String) ? JSON.parse(json_str) : json_str
271
+ result =
272
+ case
273
+ when obj.nil?
274
+ obj
117
275
 
118
- data
276
+ when obj.kind_of?(Array) && obj.first.kind_of?(Hash)
277
+ obj.map { |o| Hashie::Mash.new(o) }
278
+
279
+ when obj.kind_of?(Array) && obj.first.kind_of?(Integer)
280
+ obj
281
+
282
+ when obj.kind_of?(Hash)
283
+ Hashie::Mash.new(obj)
284
+
285
+ when obj === true || obj === false
286
+ obj
287
+
288
+ when obj.kind_of?(Array) && obj.empty?
289
+ obj
290
+
291
+ else
292
+ raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
293
+ end
294
+ end_t = Time.now
295
+ logger.debug "#{__method__}: caller=#{caller_name} key=#{options[:key]} (#{end_t - start_t}s)".indent(INDENT)
296
+ result
119
297
  end
120
298
 
121
- # cursorを使って自動ページングを行う
122
- def collect_with_cursor(method_name, *args, &block)
123
- options = args.extract_options!
124
- logger.info "#{method_name}, #{args.inspect} #{options.inspect}"
125
-
126
- max_paginates = options.delete(:max_paginates) || MAX_PAGINATES
127
- last_response = send(method_name, *args, options).attrs
128
- data = last_response[:users] || last_response[:ids]
129
-
130
- if auto_paginate
131
- num_retries = 0
132
- (max_paginates - 1).times do
133
- next_cursor = last_response[:next_cursor]
134
- break if !next_cursor || next_cursor == 0
135
-
136
- options[:cursor] = next_cursor
137
-
138
- begin
139
- last_response = send(method_name, *args, options).attrs
140
- logger.info "#{method_name}, #{args.inspect} #{options.inspect}"
141
- rescue Twitter::Error::TooManyRequests => e
142
- if num_retries <= MAX_RETRIES
143
- if WAIT
144
- sleep e.rate_limit.reset_in
145
- num_retries += 1
146
- retry
147
- else
148
- logger.warn "retry #{e.rate_limit.reset_in} seconds later, #{e.inspect}"
149
- end
150
- else
151
- logger.warn "fail. num_retries > MAX_RETRIES(=#{MAX_RETRIES}), #{e.inspect}"
152
- end
153
- rescue => e
154
- if num_retries <= MAX_RETRIES
155
- logger.warn "retry till num_retries > MAX_RETRIES(=#{MAX_RETRIES}), #{e.inspect}"
156
- num_retries += 1
157
- retry
158
- else
159
- logger.warn "fail. num_retries > MAX_RETRIES(=#{MAX_RETRIES}), something error #{e.inspect}"
160
- end
161
- end
299
+ def fetch_cache_or_call_api(method_name, user, options = {})
300
+ start_t = Time.now
301
+ key = namespaced_key(method_name, user)
302
+ options.update(key: key)
303
+
304
+ logger.debug "#{__method__}: caller=#{method_name} key=#{key}"
162
305
 
306
+ data =
307
+ if options[:cache] == :read
308
+ hit = 'force read'
309
+ cache.read(key)
310
+ else
163
311
  if block_given?
164
- yield(data, last_response)
312
+ hit = 'fetch(hit)'
313
+ cache.fetch(key, expires_in: 1.hour) do
314
+ hit = 'fetch(not hit)'
315
+ instrument(:encode_json) { encode_json(yield, method_name, options) }
316
+ end
165
317
  else
166
- items = last_response[:users] || last_response[:ids]
167
- data.concat(items) if items.is_a?(Array)
318
+ if cache.exist?(key)
319
+ hit = 'read(hit)'
320
+ cache.read(key)
321
+ else
322
+ hit = 'read(not hit)'
323
+ nil
324
+ end
168
325
  end
169
326
  end
170
- end
171
327
 
172
- data
328
+ result = decode_json(data, method_name, options)
329
+ logger.debug "#{__method__}: caller=#{method_name} key=#{key} #{hit} (#{Time.now - start_t}s)"
330
+ result
173
331
  end
174
332
 
175
- alias :old_user_timeline :user_timeline
176
- def user_timeline(*args)
177
- options = {count: 200, include_rts: true}.merge(args.extract_options!)
178
- collect_with_max_id(:old_user_timeline, *args, options)
333
+ alias :old_friendship? :friendship?
334
+ def friendship?(*args)
335
+ options = args.extract_options!
336
+ fetch_cache_or_call_api(:friendship?, args) {
337
+ call_old_method(:old_friendship?, *args, options)
338
+ }
339
+ end
340
+
341
+ alias :old_user? :user?
342
+ def user?(*args)
343
+ raise 'this method needs at least one param to use cache' if args.empty?
344
+ options = args.extract_options!
345
+ fetch_cache_or_call_api(:user?, args[0], options) {
346
+ call_old_method(:old_user?, args[0], options)
347
+ }
348
+ end
349
+
350
+ alias :old_user :user
351
+ def user(*args)
352
+ raise 'this method needs at least one param to use cache' if args.empty?
353
+ options = args.extract_options!
354
+ fetch_cache_or_call_api(:user, args[0], options) {
355
+ call_old_method(:old_user, args[0], options)
356
+ }
357
+ rescue => e
358
+ logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
359
+ raise e
360
+ end
361
+ # memoize :user
362
+
363
+ alias :old_friend_ids :friend_ids
364
+ def friend_ids(*args)
365
+ raise 'this method needs at least one param to use cache' if args.empty?
366
+ options = args.extract_options!
367
+ fetch_cache_or_call_api(:friend_ids, args[0], options) {
368
+ options = {count: 5000, cursor: -1}.merge(options)
369
+ collect_with_cursor(:old_friend_ids, *args, options)
370
+ }
179
371
  end
180
372
 
181
- def user_photos(*args)
182
- tweets = user_timeline(*args)
183
- tweets.select{|t| t.media? }.map{|t| t.media }.flatten
373
+ alias :old_follower_ids :follower_ids
374
+ def follower_ids(*args)
375
+ raise 'this method needs at least one param to use cache' if args.empty?
376
+ options = args.extract_options!
377
+ fetch_cache_or_call_api(:follower_ids, args[0], options) {
378
+ options = {count: 5000, cursor: -1}.merge(options)
379
+ collect_with_cursor(:old_follower_ids, *args, options)
380
+ }
184
381
  end
185
382
 
383
+ # specify reduce: false to use tweet for inactive_*
186
384
  alias :old_friends :friends
187
385
  def friends(*args)
188
- options = {count: 200, include_user_entities: true}.merge(args.extract_options!)
189
- collect_with_cursor(:old_friends, *args, options)
386
+ raise 'this method needs at least one param to use cache' if args.empty?
387
+ options = args.extract_options!
388
+ options[:reduce] = false unless options.has_key?(:reduce)
389
+ fetch_cache_or_call_api(:friends, args[0], options) {
390
+ options = {count: 200, include_user_entities: true, cursor: -1}.merge(options)
391
+ collect_with_cursor(:old_friends, *args, options)
392
+ }
190
393
  end
394
+ # memoize :friends
191
395
 
396
+ def friends_advanced(*args)
397
+ options = args.extract_options!
398
+ _friend_ids = friend_ids(*(args + [options]))
399
+ users(_friend_ids.map { |id| id.to_i }, options)
400
+ end
401
+
402
+ # specify reduce: false to use tweet for inactive_*
192
403
  alias :old_followers :followers
193
404
  def followers(*args)
194
- options = {count: 200, include_user_entities: true}.merge(args.extract_options!)
195
- collect_with_cursor(:old_followers, *args, options)
405
+ raise 'this method needs at least one param to use cache' if args.empty?
406
+ options = args.extract_options!
407
+ options[:reduce] = false unless options.has_key?(:reduce)
408
+ fetch_cache_or_call_api(:followers, args[0], options) {
409
+ options = {count: 200, include_user_entities: true, cursor: -1}.merge(options)
410
+ collect_with_cursor(:old_followers, *args, options)
411
+ }
196
412
  end
413
+ # memoize :followers
197
414
 
198
- alias :old_friend_ids :friend_ids
199
- def friend_ids(*args)
200
- options = {count: 5000}.merge(args.extract_options!)
201
- collect_with_cursor(:old_friend_ids, *args, options)
415
+ def followers_advanced(*args)
416
+ options = args.extract_options!
417
+ _follower_ids = follower_ids(*(args + [options]))
418
+ users(_follower_ids.map { |id| id.to_i }, options)
202
419
  end
203
420
 
204
- alias :old_follower_ids :follower_ids
205
- def follower_ids(*args)
206
- options = {count: 5000}.merge(args.extract_options!)
207
- collect_with_cursor(:old_follower_ids, *args, options)
421
+ def fetch_parallelly(signatures) # [{method: :friends, args: ['ts_3156', ...], {...}]
422
+ logger.debug "#{__method__} #{signatures.inspect}"
423
+ result = Array.new(signatures.size)
424
+
425
+ Parallel.each_with_index(signatures, in_threads: result.size) do |signature, i|
426
+ result[i] = send(signature[:method], *signature[:args])
427
+ end
428
+
429
+ result
430
+ end
431
+
432
+ def friends_and_followers(*args)
433
+ fetch_parallelly(
434
+ [
435
+ {method: 'friends_advanced', args: args},
436
+ {method: 'followers_advanced', args: args}])
437
+ end
438
+
439
+ def friends_followers_and_statuses(*args)
440
+ fetch_parallelly(
441
+ [
442
+ {method: 'friends_advanced', args: args},
443
+ {method: 'followers_advanced', args: args},
444
+ {method: 'user_timeline', args: args}])
445
+ end
446
+
447
+ def one_sided_following(me)
448
+ me.friends.to_a - me.followers.to_a
449
+ end
450
+
451
+ def one_sided_followers(me)
452
+ me.followers.to_a - me.friends.to_a
453
+ end
454
+
455
+ def mutual_friends(me)
456
+ me.friends.to_a & me.followers.to_a
208
457
  end
209
458
 
459
+ def common_friends(me, you)
460
+ me.friends.to_a & you.friends.to_a
461
+ end
462
+
463
+ def common_followers(me, you)
464
+ me.followers.to_a & you.followers.to_a
465
+ end
466
+
467
+ def removing(pre_me, cur_me)
468
+ pre_me.friends.to_a - cur_me.friends.to_a
469
+ end
470
+
471
+ def removed(pre_me, cur_me)
472
+ pre_me.followers.to_a - cur_me.followers.to_a
473
+ end
474
+
475
+ # use compact, not use sort and uniq
476
+ # specify reduce: false to use tweet for inactive_*
210
477
  alias :old_users :users
211
- def users(ids, options = {})
212
- ids_per_worker = ids.each_slice(100).to_a
478
+ def users(*args)
479
+ options = args.extract_options!
480
+ options[:reduce] = false
481
+ users_per_workers = args.first.compact.each_slice(100).to_a
213
482
  processed_users = []
214
483
 
215
- Parallel.each_with_index(ids_per_worker, in_threads: ids_per_worker.size) do |ids, i|
216
- _users = {i: i, users: old_users(ids, options)}
217
- processed_users << _users
484
+ Parallel.each_with_index(users_per_workers, in_threads: [users_per_workers.size, 10].min) do |users_per_worker, i|
485
+ _users = fetch_cache_or_call_api(:users, users_per_worker, options) {
486
+ call_old_method(:old_users, users_per_worker, options)
487
+ }
488
+
489
+ result = {i: i, users: _users}
490
+ processed_users << result
218
491
  end
219
492
 
220
- processed_users.sort_by{|p| p[:i] }.map{|p| p[:users] }.flatten
221
- end
222
-
223
- # Returns tweets that match a specified query.
224
- #
225
- # @see https://dev.twitter.com/docs/api/1.1/get/search/tweets
226
- # @see https://dev.twitter.com/docs/using-search
227
- # @note Please note that Twitter's search service and, by extension, the Search API is not meant to be an exhaustive source of Tweets. Not all Tweets will be indexed or made available via the search interface.
228
- # @rate_limited Yes
229
- # @authentication Requires user context
230
- # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
231
- # @param q [String] A search term. (from|to):hello min_retweets:3 OR min_faves:3 OR min_replies:3, #nhk @hello (in|ex)clude:retweets https://twitter.com/search-advanced
232
- # @param options [Hash] A customizable set of options.
233
- # @option options [String] :geocode Returns tweets by users located within a given radius of the given latitude/longitude. The location is preferentially taking from the Geotagging API, but will fall back to their Twitter profile. The parameter value is specified by "latitude,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly.
234
- # @option options [String] :lang Restricts tweets to the given language, given by an ISO 639-1 code.
235
- # @option options [String] :locale Specify the language of the query you are sending (only ja is currently effective). This is intended for language-specific clients and the default should work in the majority of cases.
236
- # @option options [String] :result_type Specifies what type of search results you would prefer to receive. Options are "mixed", "recent", and "popular". The current default is "mixed."
237
- # @option options [Integer] :count The number of tweets to return per page, up to a maximum of 100.
238
- # @option options [String] :until Optional. Returns tweets generated before the given date. Date should be formatted as YYYY-MM-DD.
239
- # @option options [Integer] :since_id Returns results with an ID greater than (that is, more recent than) the specified ID. There are limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has occured since the since_id, the since_id will be forced to the oldest ID available.
240
- # @option options [Integer] :max_id Returns results with an ID less than (that is, older than) or equal to the specified ID.
241
- # @return [Twitter::SearchResults] Return tweets that match a specified query with search metadata
493
+ processed_users.sort_by{|p| p[:i] }.map{|p| p[:users] }.flatten.compact
494
+ rescue => e
495
+ logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
496
+ raise e
497
+ end
498
+
499
+ def called_by_authenticated_user?(user)
500
+ authenticated_user = self.old_user; self.call_count += 1
501
+ if user.kind_of?(String)
502
+ authenticated_user.screen_name == user
503
+ elsif user.kind_of?(Integer)
504
+ authenticated_user.id.to_i == user
505
+ else
506
+ raise user.inspect
507
+ end
508
+ rescue => e
509
+ logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
510
+ raise e
511
+ end
512
+
513
+ # can't get tweets if you are not authenticated by specified user
514
+ alias :old_home_timeline :home_timeline
515
+ def home_timeline(*args)
516
+ raise 'this method needs at least one param to use cache' if args.empty?
517
+ raise 'this method must be called by authenticated user' unless called_by_authenticated_user?(args[0])
518
+ options = args.extract_options!
519
+ fetch_cache_or_call_api(:home_timeline, args[0], options) {
520
+ options = {count: 200, include_rts: true, call_count: 3}.merge(options)
521
+ collect_with_max_id(:old_home_timeline, options)
522
+ }
523
+ end
524
+
525
+ # can't get tweets if you are not authenticated by specified user
526
+ alias :old_user_timeline :user_timeline
527
+ def user_timeline(*args)
528
+ raise 'this method needs at least one param to use cache' if args.empty?
529
+ options = args.extract_options!
530
+ fetch_cache_or_call_api(:user_timeline, args[0], options) {
531
+ options = {count: 200, include_rts: true, call_count: 3}.merge(options)
532
+ collect_with_max_id(:old_user_timeline, *args, options)
533
+ }
534
+ end
535
+
536
+ # can't get tweets if you are not authenticated by specified user
537
+ alias :old_mentions_timeline :mentions_timeline
538
+ def mentions_timeline(*args)
539
+ raise 'this method needs at least one param to use cache' if args.empty?
540
+ raise 'this method must be called by authenticated user' unless called_by_authenticated_user?(args[0])
541
+ options = args.extract_options!
542
+ fetch_cache_or_call_api(:mentions_timeline, args[0], options) {
543
+ options = {count: 200, include_rts: true, call_count: 1}.merge(options)
544
+ collect_with_max_id(:old_mentions_timeline, options)
545
+ }
546
+ rescue => e
547
+ logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
548
+ raise e
549
+ end
550
+
551
+ def select_screen_names_replied(tweets, options = {})
552
+ result = tweets.map do |t|
553
+ $1 if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
554
+ end.compact
555
+ (options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq
556
+ end
557
+
558
+ # users which specified user is replying
559
+ # in_reply_to_user_id and in_reply_to_status_id is not used because of distinguishing mentions from replies
560
+ def replying(user, options = {})
561
+ tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user, options)
562
+ screen_names = select_screen_names_replied(tweets, options)
563
+ users(screen_names, options)
564
+ rescue Twitter::Error::NotFound => e
565
+ e.message == 'No user matches for specified terms.' ? [] : (raise e)
566
+ rescue => e
567
+ logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
568
+ raise e
569
+ end
570
+
242
571
  alias :old_search :search
243
572
  def search(*args)
244
- options = {count: 100, result_type: 'recent'}.merge(args.extract_options!)
245
- collect_with_max_id(:old_search, *args, options) do |data, last_response|
246
- statuses = last_response.attrs[:statuses]
247
- data.concat(statuses)
248
- statuses
573
+ raise 'this method needs at least one param to use cache' if args.empty?
574
+ options = args.extract_options!
575
+ options[:reduce] = false
576
+ fetch_cache_or_call_api(:search, args[0], options) {
577
+ options = {count: 100, result_type: :recent, call_count: 1}.merge(options)
578
+ collect_with_max_id(:old_search, *args, options) { |response| response.attrs[:statuses] }
579
+ }
580
+ rescue => e
581
+ logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
582
+ raise e
583
+ end
584
+
585
+ def select_uids_replying_to(tweets, options)
586
+ result = tweets.map do |t|
587
+ t.user.id.to_i if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
588
+ end.compact
589
+ (options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq
590
+ end
591
+
592
+ def select_replied_from_search(tweets, options = {})
593
+ return [] if tweets.empty?
594
+ uids = select_uids_replying_to(tweets, options)
595
+ uids.map { |u| tweets.find { |t| t.user.id.to_i == u.to_i } }.map { |t| t.user }
596
+ end
597
+
598
+ # users which specified user is replied
599
+ # when user is login you had better to call mentions_timeline
600
+ def replied(_user, options = {})
601
+ user = self.user(_user, options)
602
+ if user.id.to_i == __uid_i
603
+ mentions_timeline(__uid_i, options).uniq { |m| m.user.id }.map { |m| m.user }
604
+ else
605
+ select_replied_from_search(search('@' + user.screen_name, options))
249
606
  end
607
+ rescue => e
608
+ logger.warn "#{__method__} #{_user.inspect} #{e.class} #{e.message}"
609
+ raise e
250
610
  end
251
611
 
252
- # mentions_timeline is to fetch the timeline of Tweets mentioning the authenticated user
253
- # get_mentions is to fetch the Tweets mentioning the screen_name's user
254
- def get_mentions(screen_name)
255
- search("to:#{screen_name}")
612
+ def select_inactive_users(users, options = {})
613
+ options[:authorized] = false unless options.has_key?(:authorized)
614
+ two_weeks_ago = 2.weeks.ago.to_i
615
+ users.select do |u|
616
+ if options[:authorized] || !u.protected
617
+ (Time.parse(u.status.created_at).to_i < two_weeks_ago) rescue false
618
+ else
619
+ false
620
+ end
621
+ end
622
+ end
623
+
624
+ def inactive_friends(user)
625
+ select_inactive_users(friends_advanced(user))
256
626
  end
257
627
 
258
- def search_japanese_tweets(str)
259
- search(str, {lang: 'ja'})
628
+ def inactive_followers(user)
629
+ select_inactive_users(followers_advanced(user))
260
630
  end
261
631
 
262
- def search_tweets_except_rt(str)
263
- search("#{str} exclude:retweets")
632
+ def clusters_belong_to(text)
633
+ return [] if text.blank?
634
+
635
+ exclude_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_bad_words_path']))
636
+ special_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_good_words_path']))
637
+
638
+ # クラスタ用の単語の出現回数を記録
639
+ cluster_word_counter =
640
+ special_words.map { |sw| [sw, text.scan(sw)] }
641
+ .delete_if { |item| item[1].empty? }
642
+ .each_with_object(Hash.new(1)) { |item, memo| memo[item[0]] = item[1].size }
643
+
644
+ # 同一文字種の繰り返しを見付ける。漢字の繰り返し、ひらがなの繰り返し、カタカナの繰り返し、など
645
+ text.scan(/[一-龠〆ヵヶ々]+|[ぁ-んー~]+|[ァ-ヴー~]+|[a-zA-Z0-9]+|[、。!!??]+/).
646
+
647
+ # 複数回繰り返される文字を除去
648
+ map { |w| w.remove /[?!?!。、w]|(ー{2,})/ }.
649
+
650
+ # 文字数の少なすぎる単語、ひらがなだけの単語、除外単語を除去する
651
+ delete_if { |w| w.length <= 1 || (w.length <= 2 && w =~ /^[ぁ-んー~]+$/) || exclude_words.include?(w) }.
652
+
653
+ # 出現回数を記録
654
+ each { |w| cluster_word_counter[w] += 1 }
655
+
656
+ # 複数個以上見付かった単語のみを残し、出現頻度順にソート
657
+ cluster_word_counter.select { |_, v| v > 3 }.sort_by { |_, v| -v }.to_h
264
658
  end
265
- end
266
659
 
660
+ def clusters_assigned_to
267
661
 
662
+ end
663
+
664
+ def usage_stats_wday_series_data(times)
665
+ wday_count = times.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |time, memo|
666
+ memo[time.wday] += 1
667
+ end
668
+ wday_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
669
+ {name: key, y: value, drilldown: key}
670
+ end
671
+ end
672
+
673
+ def usage_stats_wday_drilldown_series(times)
674
+ hour_count =
675
+ (0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
676
+ wday_memo[wday] =
677
+ 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|
678
+ hour_memo[hour] += 1
679
+ end
680
+ end
681
+ hour_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
682
+ {name: key, id: key, data: value.to_a.map{|a| [a[0].to_s, a[1]] }}
683
+ end
684
+ end
685
+
686
+ def usage_stats_hour_series_data(times)
687
+ hour_count = times.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |time, memo|
688
+ memo[time.hour] += 1
689
+ end
690
+ hour_count.map do |key, value|
691
+ {name: key.to_s, y: value, drilldown: key.to_s}
692
+ end
693
+ end
694
+
695
+ def usage_stats_hour_drilldown_series(times)
696
+ wday_count =
697
+ (0..23).each_with_object((0..23).map { |n| [n, nil] }.to_h) do |hour, hour_memo|
698
+ hour_memo[hour] =
699
+ 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|
700
+ wday_memo[wday] += 1
701
+ end
702
+ end
703
+ wday_count.map do |key, value|
704
+ {name: key.to_s, id: key.to_s, data: value.to_a.map{|a| [I18n.t('date.abbr_day_names')[a[0]], a[1]] }}
705
+ end
706
+ end
707
+
708
+ def twitter_addiction_series(times)
709
+ five_mins = 5.minutes
710
+ wday_expended_seconds =
711
+ (0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
712
+ target_times = times.select { |t| t.wday == wday }
713
+ wday_memo[wday] = target_times.empty? ? nil : target_times.each_cons(2).map {|a, b| (a - b) < five_mins ? a - b : five_mins }.sum
714
+ end
715
+ days = times.map{|t| t.to_date.to_s(:long) }.uniq.size
716
+ weeks = (days > 7) ? days / 7.0 : 1.0
717
+ wday_expended_seconds.map { |k, v| [I18n.t('date.abbr_day_names')[k], (v.nil? ? nil : v / weeks / 60)] }.map do |key, value|
718
+ {name: key, y: value}
719
+ end
720
+ end
721
+
722
+ def usage_stats(user, options = {})
723
+ n_days_ago = options.has_key?(:days) ? options[:days].days.ago : 100.years.ago
724
+ tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user)
725
+ times =
726
+ # TODO Use user specific time zone
727
+ tweets.map { |t| ActiveSupport::TimeZone['Tokyo'].parse(t.created_at.to_s) }.
728
+ select { |t| t > n_days_ago }
729
+ [
730
+ usage_stats_wday_series_data(times),
731
+ usage_stats_wday_drilldown_series(times),
732
+ usage_stats_hour_series_data(times),
733
+ usage_stats_hour_drilldown_series(times),
734
+ twitter_addiction_series(times)
735
+ ]
736
+ end
737
+
738
+
739
+ alias :old_favorites :favorites
740
+ def favorites(*args)
741
+ raise 'this method needs at least one param to use cache' if args.empty?
742
+ options = args.extract_options!
743
+ fetch_cache_or_call_api(:favorites, args[0], options) {
744
+ options = {count: 100, call_count: 1}.merge(options)
745
+ collect_with_max_id(:old_favorites, *args, options)
746
+ }
747
+ end
748
+
749
+ def calc_scores_from_users(users, options)
750
+ min = options.has_key?(:min) ? options[:min] : 0
751
+ max = options.has_key?(:max) ? options[:max] : 1000
752
+ users.each_with_object(Hash.new(0)) { |u, memo| memo[u.id] += 1 }.
753
+ select { |_k, v| min <= v && v <= max }.
754
+ sort_by { |_, v| -v }.to_h
755
+ end
756
+
757
+ def calc_scores_from_tweets(tweets, options = {})
758
+ calc_scores_from_users(tweets.map { |t| t.user }, options)
759
+ end
760
+
761
+ def select_favoriting_from_favs(favs, options = {})
762
+ return [] if favs.empty?
763
+ uids = calc_scores_from_tweets(favs)
764
+ result = uids.map { |uid, score| f = favs.
765
+ find { |f| f.user.id.to_i == uid.to_i }; Array.new(score, f) }.flatten.map { |f| f.user }
766
+ (options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq { |u| u.id }
767
+ end
768
+
769
+ def favoriting(user, options= {})
770
+ favs = options.has_key?(:favorites) ? options.delete(:favorites) : favorites(user, options)
771
+ select_favoriting_from_favs(favs, options)
772
+ rescue => e
773
+ logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
774
+ raise e
775
+ end
776
+
777
+ def favorited_by(user)
778
+ end
779
+
780
+ def close_friends(_uid, options = {})
781
+ min = options.has_key?(:min) ? options[:min] : 0
782
+ max = options.has_key?(:max) ? options[:max] : 1000
783
+ uid_i = _uid.to_i
784
+ _replying = options.has_key?(:replying) ? options.delete(:replying) : replying(uid_i, options)
785
+ _replied = options.has_key?(:replied) ? options.delete(:replied) : replied(uid_i, options)
786
+ _favoriting = options.has_key?(:favoriting) ? options.delete(:favoriting) : favoriting(uid_i, options)
787
+
788
+ min_max = {min: min, max: max}
789
+ _users = _replying + _replied + _favoriting
790
+ return [] if _users.empty?
791
+
792
+ scores = calc_scores_from_users(_users, min_max)
793
+ replying_scores = calc_scores_from_users(_replying, min_max)
794
+ replied_scores = calc_scores_from_users(_replied, min_max)
795
+ favoriting_scores = calc_scores_from_users(_favoriting, min_max)
796
+
797
+ scores.keys.map { |uid| _users.find { |u| u.id.to_i == uid.to_i } }.
798
+ map do |u|
799
+ u[:score] = scores[u.id]
800
+ u[:replying_score] = replying_scores[u.id]
801
+ u[:replied_score] = replied_scores[u.id]
802
+ u[:favoriting_score] = favoriting_scores[u.id]
803
+ u
804
+ end
805
+ end
806
+ end
@@ -0,0 +1,8 @@
1
+ require 'active_support'
2
+ require 'active_support/subscriber'
3
+
4
+ class ExTwitterSubscriber < ActiveSupport::Subscriber
5
+ def call(event)
6
+ puts "#{event.payload[:name]} #{event.duration} ms"
7
+ end
8
+ end
@@ -6,38 +6,32 @@ describe ExTwitter do
6
6
  consumer_key: 'CK',
7
7
  consumer_secret: 'CS',
8
8
  access_token: 'AT',
9
- access_token_secret: 'ATS'
9
+ access_token_secret: 'ATS',
10
10
  }
11
11
  }
12
12
  let(:client) { ExTwitter.new(config) }
13
13
 
14
14
  describe '#initialize' do
15
- let(:default_value) { 3 }
16
- let(:value) { 100 }
15
+ let(:default_call_count) { 0 }
17
16
 
18
- context 'without params' do
19
- it "doesn't set max_paginates" do
20
- expect(ExTwitter.new.max_paginates).to eq(default_value)
21
- end
17
+ it 'sets call_count to 0' do
18
+ expect(client.call_count).to eq(default_call_count)
22
19
  end
23
20
 
24
- context 'with params' do
25
- it 'set max_paginates' do
26
- expect(ExTwitter.new(max_paginates: value).max_paginates).to eq(value)
27
- end
21
+ context 'without params' do
28
22
  end
29
23
 
30
- context 'with block' do
31
- it 'set max_paginates' do
32
- expect(ExTwitter.new {|config| config.max_paginates = value }.max_paginates).to eq(value)
33
- end
24
+ context 'with params' do
34
25
  end
35
26
  end
36
27
 
37
- describe '#read' do
28
+ describe '#logger' do
29
+ it 'has logger' do
30
+ expect(client.logger).to be_truthy
31
+ end
38
32
  end
39
33
 
40
- describe '#write' do
34
+ describe '#call_old_method' do
41
35
  end
42
36
 
43
37
  describe '#collect_with_max_id' do
@@ -46,45 +40,92 @@ describe ExTwitter do
46
40
  describe '#collect_with_cursor' do
47
41
  end
48
42
 
49
- # describe '#user_timeline' do
50
- # it 'call collect_with_max_id' do
51
- # expect(client).to receive(:collect_with_max_id)
52
- # client.user_timeline
53
- # end
54
- # end
55
- #
56
- # describe '#friends' do
57
- # it 'call collect_with_cursor' do
58
- # expect(client).to receive(:collect_with_cursor)
59
- # client.friends
60
- # end
61
- # end
62
- #
63
- # describe '#followers' do
64
- # it 'call collect_with_cursor' do
65
- # expect(client).to receive(:collect_with_cursor)
66
- # client.followers
67
- # end
68
- # end
69
- #
70
- # describe '#friend_ids' do
71
- # it 'call collect_with_cursor' do
72
- # expect(client).to receive(:collect_with_cursor)
73
- # client.friend_ids
74
- # end
75
- # end
76
- #
77
- # describe '#follower_ids' do
78
- # it 'call collect_with_cursor' do
79
- # expect(client).to receive(:collect_with_cursor)
80
- # client.follower_ids
81
- # end
82
- # end
83
- #
84
- # describe '#users' do
85
- # it 'call old_users' do
86
- # expect(client).to receive(:old_users)
87
- # client.users([1, 2, 3])
88
- # end
89
- # end
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
90
131
  end
@@ -5,4 +5,5 @@ RSpec.configure do |config|
5
5
  config.expect_with :rspec do |c|
6
6
  c.syntax = :expect
7
7
  end
8
+ config.order = 'random'
8
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ex_twitter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shinohara Teruki
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-29 00:00:00.000000000 Z
11
+ date: 2016-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: twitter
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: parallel
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -79,6 +93,7 @@ files:
79
93
  - ex_twitter.gemspec
80
94
  - lib/ex_stream.rb
81
95
  - lib/ex_twitter.rb
96
+ - lib/ex_twitter_subscriber.rb
82
97
  - spec/ex_twitter_spec.rb
83
98
  - spec/helper.rb
84
99
  homepage: http://github.com/ts-3156/ex-twitter/
@@ -101,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
116
  version: '0'
102
117
  requirements: []
103
118
  rubyforge_project:
104
- rubygems_version: 2.2.2
119
+ rubygems_version: 2.5.1
105
120
  signing_key:
106
121
  specification_version: 4
107
122
  summary: Add auto paginate feature to Twitter gem.