ex_twitter 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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