ex_twitter 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ex_twitter.rb CHANGED
@@ -1,806 +1,2 @@
1
- require 'active_support'
2
- require 'active_support/cache'
3
- require 'active_support/core_ext/string'
4
-
5
- require 'ex_twitter_subscriber'
6
-
7
- require 'twitter'
8
- require 'hashie'
9
- require 'memoist'
10
- require 'parallel'
11
-
12
-
13
-
14
- class ExTwitter < Twitter::REST::Client
15
- extend Memoist
16
-
17
- def initialize(options = {})
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
27
-
28
- attr_accessor :call_count
29
- attr_reader :cache, :authenticated_user
30
-
31
- INDENT = 4
32
-
33
- # for backward compatibility
34
- def uid
35
- @uid
36
- end
37
-
38
- def __uid
39
- @uid
40
- end
41
-
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)
67
- options = args.extract_options!
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
94
-
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
110
- end
111
-
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) }
224
- else
225
- obj.map { |o| o.to_hash }
226
- 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
258
-
259
- else
260
- raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
261
- 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
275
-
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
297
- end
298
-
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}"
305
-
306
- data =
307
- if options[:cache] == :read
308
- hit = 'force read'
309
- cache.read(key)
310
- else
311
- if block_given?
312
- hit = 'fetch(hit)'
313
- cache.fetch(key, expires_in: 1.hour, race_condition_ttl: 5.minutes) do
314
- hit = 'fetch(not hit)'
315
- instrument(:encode_json) { encode_json(yield, method_name, options) }
316
- end
317
- else
318
- if cache.exist?(key)
319
- hit = 'read(hit)'
320
- cache.read(key)
321
- else
322
- hit = 'read(not hit)'
323
- nil
324
- end
325
- end
326
- end
327
-
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
331
- end
332
-
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
- }
371
- end
372
-
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
- }
381
- end
382
-
383
- # specify reduce: false to use tweet for inactive_*
384
- alias :old_friends :friends
385
- def friends(*args)
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
- }
393
- end
394
- # memoize :friends
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_*
403
- alias :old_followers :followers
404
- def followers(*args)
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
- }
412
- end
413
- # memoize :followers
414
-
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)
419
- end
420
-
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
457
- end
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_*
477
- alias :old_users :users
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
482
- processed_users = []
483
-
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
491
- end
492
-
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
-
571
- alias :old_search :search
572
- def search(*args)
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))
606
- end
607
- rescue => e
608
- logger.warn "#{__method__} #{_user.inspect} #{e.class} #{e.message}"
609
- raise e
610
- end
611
-
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))
626
- end
627
-
628
- def inactive_followers(user)
629
- select_inactive_users(followers_advanced(user))
630
- end
631
-
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
658
- end
659
-
660
- def clusters_assigned_to
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
1
+ module ExTwitter
806
2
  end