twitter_with_auto_pagination 0.6.2 → 0.7.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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -18
  3. data/lib/twitter_with_auto_pagination.rb +42 -1
  4. data/lib/twitter_with_auto_pagination/log_subscriber.rb +2 -2
  5. data/lib/twitter_with_auto_pagination/rest/api.rb +31 -0
  6. data/lib/twitter_with_auto_pagination/rest/extension/clusters.rb +43 -0
  7. data/lib/twitter_with_auto_pagination/rest/extension/favoriting.rb +106 -0
  8. data/lib/twitter_with_auto_pagination/rest/extension/friends_and_followers.rb +131 -0
  9. data/lib/twitter_with_auto_pagination/rest/extension/replying.rb +90 -0
  10. data/lib/twitter_with_auto_pagination/rest/extension/unfollowing.rb +29 -0
  11. data/lib/twitter_with_auto_pagination/rest/favorites.rb +20 -0
  12. data/lib/twitter_with_auto_pagination/rest/friends_and_followers.rb +94 -0
  13. data/lib/twitter_with_auto_pagination/rest/search.rb +19 -0
  14. data/lib/twitter_with_auto_pagination/rest/timelines.rb +37 -0
  15. data/lib/twitter_with_auto_pagination/rest/uncategorized.rb +83 -0
  16. data/lib/twitter_with_auto_pagination/rest/users.rb +62 -0
  17. data/lib/twitter_with_auto_pagination/rest/utils.rb +303 -0
  18. data/spec/helper.rb +60 -1
  19. data/spec/twitter_with_auto_pagination/client_spec.rb +150 -0
  20. data/twitter_with_auto_pagination.gemspec +1 -1
  21. metadata +17 -8
  22. data/lib/twitter_with_auto_pagination/client.rb +0 -139
  23. data/lib/twitter_with_auto_pagination/existing_api.rb +0 -127
  24. data/lib/twitter_with_auto_pagination/new_api.rb +0 -337
  25. data/lib/twitter_with_auto_pagination/utils.rb +0 -303
  26. data/spec/twitter_with_auto_pagination_spec.rb +0 -131
@@ -0,0 +1,90 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+
3
+ module TwitterWithAutoPagination
4
+ module REST
5
+ module Extension
6
+ module Replying
7
+ include TwitterWithAutoPagination::REST::Utils
8
+
9
+ def _extract_screen_names(tweets)
10
+ tweets.map do |t|
11
+ $1 if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
12
+ end.compact
13
+ end
14
+
15
+ def _retrieve_user_timeline(*args)
16
+ options = args.extract_options!
17
+ if args.empty?
18
+ user_timeline(options)
19
+ elsif uid_or_screen_name?(args[0])
20
+ user_timeline(args[0], options)
21
+ elsif args[0].kind_of?(Array) && args[0].all? { |t| t.respond_to?(:text) }
22
+ args[0]
23
+ else
24
+ raise ArgumentError
25
+ end
26
+ end
27
+
28
+ # users which specified user is replying
29
+ # in_reply_to_user_id and in_reply_to_status_id is not used because of distinguishing mentions from replies
30
+ def users_which_you_replied_to(*args)
31
+ options = args.extract_options!
32
+ instrument(__method__, nil, options) do
33
+ tweets = _retrieve_user_timeline(*args, options)
34
+ screen_names = _extract_screen_names(tweets)
35
+ result = users(screen_names, {super_operation: __method__}.merge(options))
36
+ if options.has_key?(:uniq) && !options[:uniq]
37
+ screen_names.map { |sn| result.find { |u| u.screen_name == sn } }.compact
38
+ else
39
+ result.uniq { |u| u.id }
40
+ end
41
+ end
42
+ rescue Twitter::Error::NotFound => e
43
+ e.message == 'No user matches for specified terms.' ? [] : (raise e)
44
+ rescue => e
45
+ logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
46
+ raise e
47
+ end
48
+
49
+ alias replying users_which_you_replied_to
50
+
51
+ def _extract_uids(tweets)
52
+ tweets.map do |t|
53
+ t.user.id.to_i if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
54
+ end.compact
55
+ end
56
+
57
+ def _extract_users(tweets, uids)
58
+ uids.map { |uid| tweets.find { |t| t.user.id.to_i == uid.to_i } }.map { |t| t.user }.compact
59
+ end
60
+
61
+ def _retrieve_users_from_mentions_timeline(*args)
62
+ options = args.extract_options!
63
+ if args.empty? || (uid_or_screen_name?(args[0]) && authenticating_user?(args[0]))
64
+ mentions_timeline.map { |m| m.user }
65
+ else
66
+ searched_result = search('@' + user(args[0]).screen_name, options)
67
+ uids = _extract_uids(searched_result)
68
+ _extract_users(searched_result, uids)
69
+ end
70
+ end
71
+
72
+ # users which specified user is replied
73
+ # when user is login you had better to call mentions_timeline
74
+ def users_who_replied_to_you(*args)
75
+ options = args.extract_options!
76
+ instrument(__method__, nil, options) do
77
+ result = _retrieve_users_from_mentions_timeline(*args, options)
78
+ if options.has_key?(:uniq) && !options[:uniq]
79
+ result
80
+ else
81
+ result.uniq { |r| r.id }
82
+ end
83
+ end
84
+ end
85
+
86
+ alias replied users_who_replied_to_you
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,29 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+
3
+ module TwitterWithAutoPagination
4
+ module REST
5
+ module Extension
6
+ module Unfollowing
7
+ include TwitterWithAutoPagination::REST::Utils
8
+
9
+ def users_which_you_removed(past_me, cur_me)
10
+ instrument(__method__, nil) do
11
+ past_friends, cur_friends = _retrieve_friends(past_me, cur_me)
12
+ past_friends.to_a - cur_friends.to_a
13
+ end
14
+ end
15
+
16
+ alias unfollowing users_which_you_removed
17
+
18
+ def users_who_removed_you(past_me, cur_me)
19
+ instrument(__method__, nil) do
20
+ past_followers, cur_followers = _retrieve_followers(past_me, cur_me)
21
+ past_followers.to_a - cur_followers.to_a
22
+ end
23
+ end
24
+
25
+ alias unfollowed users_who_removed_you
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+
3
+ module TwitterWithAutoPagination
4
+ module REST
5
+ module Favorites
6
+ include TwitterWithAutoPagination::REST::Utils
7
+
8
+ def favorites(*args)
9
+ # TODO call_count bug fix
10
+ options = {count: 100, call_count: 1}.merge(args.extract_options!)
11
+ args[0] = verify_credentials.id if args.empty?
12
+ instrument(__method__, nil, options) do
13
+ fetch_cache_or_call_api(__method__, args[0], options) do
14
+ collect_with_max_id(method(__method__).super_method, *args, options)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,94 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+
3
+ module TwitterWithAutoPagination
4
+ module REST
5
+ module FriendsAndFollowers
6
+ include TwitterWithAutoPagination::REST::Utils
7
+
8
+ def friendship?(*args)
9
+ options = args.extract_options!
10
+ instrument(__method__, nil, options) do
11
+ fetch_cache_or_call_api(__method__, args) do
12
+ call_api(method(__method__).super_method, *args, options)
13
+ end
14
+ end
15
+ end
16
+
17
+ def friend_ids(*args)
18
+ options = {count: 5000, cursor: -1}.merge(args.extract_options!)
19
+ args[0] = verify_credentials.id if args.empty?
20
+ instrument(__method__, nil, options) do
21
+ fetch_cache_or_call_api(__method__, args[0], options) do
22
+ collect_with_cursor(method(__method__).super_method, *args, options)
23
+ end
24
+ end
25
+ end
26
+
27
+ def follower_ids(*args)
28
+ options = {count: 5000, cursor: -1}.merge(args.extract_options!)
29
+ args[0] = verify_credentials.id if args.empty?
30
+ instrument(__method__, nil, options) do
31
+ fetch_cache_or_call_api(__method__, args[0], options) do
32
+ collect_with_cursor(method(__method__).super_method, *args, options)
33
+ end
34
+ end
35
+ end
36
+
37
+ # specify reduce: false to use tweet for inactive_*
38
+ def friends(*args)
39
+ options = args.extract_options!
40
+ if options.delete(:serial)
41
+ _friends_serially(*args, options)
42
+ else
43
+ _friends_parallelly(*args, options)
44
+ end
45
+ end
46
+
47
+ def _friends_serially(*args)
48
+ options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
49
+ options[:reduce] = false unless options.has_key?(:reduce)
50
+ args[0] = verify_credentials.id if args.empty?
51
+ instrument(__method__, nil, options) do
52
+ fetch_cache_or_call_api(:friends, args[0], options) do
53
+ collect_with_cursor(method(:friends).super_method, *args, options)
54
+ end
55
+ end
56
+ end
57
+
58
+ def _friends_parallelly(*args)
59
+ options = {super_operation: __method__}.merge(args.extract_options!)
60
+ instrument(__method__, nil, options) do
61
+ users(friend_ids(*args, options).map { |id| id.to_i }, options)
62
+ end
63
+ end
64
+
65
+ # specify reduce: false to use tweet for inactive_*
66
+ def followers(*args)
67
+ options = args.extract_options!
68
+ if options.delete(:serial)
69
+ _followers_serially(*args, options)
70
+ else
71
+ _followers_parallelly(*args, options)
72
+ end
73
+ end
74
+
75
+ def _followers_serially(*args)
76
+ options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
77
+ options[:reduce] = false unless options.has_key?(:reduce)
78
+ args[0] = verify_credentials.id if args.empty?
79
+ instrument(__method__, nil, options) do
80
+ fetch_cache_or_call_api(:followers, args[0], options) do
81
+ collect_with_cursor(method(:followers).super_method, *args, options)
82
+ end
83
+ end
84
+ end
85
+
86
+ def _followers_parallelly(*args)
87
+ options = {super_operation: __method__}.merge(args.extract_options!)
88
+ instrument(__method__, nil, options) do
89
+ users(follower_ids(*args, options).map { |id| id.to_i }, options)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,19 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+
3
+ module TwitterWithAutoPagination
4
+ module REST
5
+ module Search
6
+ include TwitterWithAutoPagination::REST::Utils
7
+
8
+ def search(*args)
9
+ options = {count: 100, result_type: :recent, call_limit: 1}.merge(args.extract_options!)
10
+ options[:reduce] = false
11
+ instrument(__method__, nil, options) do
12
+ fetch_cache_or_call_api(__method__, args[0], options) do
13
+ collect_with_max_id(method(__method__).super_method, *args, options) { |response| response.attrs[:statuses] }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+
3
+ module TwitterWithAutoPagination
4
+ module REST
5
+ module Timelines
6
+ include TwitterWithAutoPagination::REST::Utils
7
+
8
+ def home_timeline(*args)
9
+ options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
10
+ instrument(__method__, nil, options) do
11
+ fetch_cache_or_call_api(__method__, verify_credentials.id, options) do
12
+ collect_with_max_id(method(__method__).super_method, options)
13
+ end
14
+ end
15
+ end
16
+
17
+ def user_timeline(*args)
18
+ options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
19
+ args[0] = verify_credentials.id if args.empty?
20
+ instrument(__method__, nil, options) do
21
+ fetch_cache_or_call_api(__method__, args[0], options) do
22
+ collect_with_max_id(method(__method__).super_method, *args, options)
23
+ end
24
+ end
25
+ end
26
+
27
+ def mentions_timeline(*args)
28
+ options = {count: 200, include_rts: true, call_limit: 1}.merge(args.extract_options!)
29
+ instrument(__method__, nil, options) do
30
+ fetch_cache_or_call_api(__method__, verify_credentials.id, options) do
31
+ collect_with_max_id(method(__method__).super_method, options)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,83 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+
3
+ module TwitterWithAutoPagination
4
+ module REST
5
+ module Uncategorized
6
+ include TwitterWithAutoPagination::REST::Utils
7
+
8
+ def usage_stats_wday_series_data(times)
9
+ wday_count = times.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |time, memo|
10
+ memo[time.wday] += 1
11
+ end
12
+ wday_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
13
+ {name: key, y: value, drilldown: key}
14
+ end
15
+ end
16
+
17
+ def usage_stats_wday_drilldown_series(times)
18
+ hour_count =
19
+ (0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
20
+ wday_memo[wday] =
21
+ 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|
22
+ hour_memo[hour] += 1
23
+ end
24
+ end
25
+ hour_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
26
+ {name: key, id: key, data: value.to_a.map{|a| [a[0].to_s, a[1]] }}
27
+ end
28
+ end
29
+
30
+ def usage_stats_hour_series_data(times)
31
+ hour_count = times.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |time, memo|
32
+ memo[time.hour] += 1
33
+ end
34
+ hour_count.map do |key, value|
35
+ {name: key.to_s, y: value, drilldown: key.to_s}
36
+ end
37
+ end
38
+
39
+ def usage_stats_hour_drilldown_series(times)
40
+ wday_count =
41
+ (0..23).each_with_object((0..23).map { |n| [n, nil] }.to_h) do |hour, hour_memo|
42
+ hour_memo[hour] =
43
+ 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|
44
+ wday_memo[wday] += 1
45
+ end
46
+ end
47
+ wday_count.map do |key, value|
48
+ {name: key.to_s, id: key.to_s, data: value.to_a.map{|a| [I18n.t('date.abbr_day_names')[a[0]], a[1]] }}
49
+ end
50
+ end
51
+
52
+ def twitter_addiction_series(times)
53
+ five_mins = 5.minutes
54
+ wday_expended_seconds =
55
+ (0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
56
+ target_times = times.select { |t| t.wday == wday }
57
+ wday_memo[wday] = target_times.empty? ? nil : target_times.each_cons(2).map {|a, b| (a - b) < five_mins ? a - b : five_mins }.sum
58
+ end
59
+ days = times.map{|t| t.to_date.to_s(:long) }.uniq.size
60
+ weeks = (days > 7) ? days / 7.0 : 1.0
61
+ wday_expended_seconds.map { |k, v| [I18n.t('date.abbr_day_names')[k], (v.nil? ? nil : v / weeks / 60)] }.map do |key, value|
62
+ {name: key, y: value}
63
+ end
64
+ end
65
+
66
+ def usage_stats(user, options = {})
67
+ n_days_ago = options.has_key?(:days) ? options[:days].days.ago : 100.years.ago
68
+ tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user)
69
+ times =
70
+ # TODO Use user specific time zone
71
+ tweets.map { |t| ActiveSupport::TimeZone['Tokyo'].parse(t.created_at.to_s) }.
72
+ select { |t| t > n_days_ago }
73
+ [
74
+ usage_stats_wday_series_data(times),
75
+ usage_stats_wday_drilldown_series(times),
76
+ usage_stats_hour_series_data(times),
77
+ usage_stats_hour_drilldown_series(times),
78
+ twitter_addiction_series(times)
79
+ ]
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,62 @@
1
+ require 'twitter_with_auto_pagination/rest/utils'
2
+ require 'parallel'
3
+
4
+ module TwitterWithAutoPagination
5
+ module REST
6
+ module Users
7
+ include TwitterWithAutoPagination::REST::Utils
8
+
9
+ def verify_credentials(*args)
10
+ options = {skip_status: true}.merge(args.extract_options!)
11
+ instrument(__method__, nil, options) do
12
+ fetch_cache_or_call_api(__method__, args) do
13
+ call_api(method(__method__).super_method, *args, options)
14
+ end
15
+ end
16
+ end
17
+
18
+ def user?(*args)
19
+ options = args.extract_options!
20
+ args[0] = verify_credentials.id if args.empty?
21
+ instrument(__method__, nil, options) do
22
+ fetch_cache_or_call_api(__method__, args[0], options) do
23
+ call_api(method(__method__).super_method, *args, options)
24
+ end
25
+ end
26
+ end
27
+
28
+ def user(*args)
29
+ options = args.extract_options!
30
+ args[0] = verify_credentials.id if args.empty?
31
+ instrument(__method__, nil, options) do
32
+ fetch_cache_or_call_api(__method__, args[0], options) do
33
+ call_api(method(__method__).super_method, *args, options)
34
+ end
35
+ end
36
+ end
37
+
38
+ # use compact, not use sort and uniq
39
+ # specify reduce: false to use tweet for inactive_*
40
+ # TODO Perhaps `old_users` automatically merges result...
41
+ def users(*args)
42
+ options = args.extract_options!
43
+ options[:reduce] = false
44
+ users_per_workers = args.first.compact.each_slice(100).to_a
45
+ processed_users = []
46
+ thread_size = [users_per_workers.size, 10].min
47
+
48
+ instrument(__method__, nil, options) do
49
+ Parallel.each_with_index(users_per_workers, in_threads: thread_size) do |users_per_worker, i|
50
+ _users = fetch_cache_or_call_api(__method__, users_per_worker, options) do
51
+ call_api(method(__method__).super_method, users_per_worker, options)
52
+ end
53
+
54
+ processed_users << {i: i, users: _users}
55
+ end
56
+ end
57
+
58
+ processed_users.sort_by { |p| p[:i] }.map { |p| p[:users] }.flatten.compact
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,303 @@
1
+ require 'hashie'
2
+ require 'digest/md5'
3
+
4
+ module TwitterWithAutoPagination
5
+ module REST
6
+ module Utils
7
+ # for backward compatibility
8
+ def uid
9
+ @uid || user.id.to_i
10
+ end
11
+
12
+ def __uid
13
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
14
+ `TwitterWithAutoPagination::Utils##{__method__}` is deprecated.
15
+ MESSAGE
16
+ uid
17
+ end
18
+
19
+ def __uid_i
20
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
21
+ `TwitterWithAutoPagination::Utils##{__method__}` is deprecated.
22
+ MESSAGE
23
+ uid
24
+ end
25
+
26
+ # for backward compatibility
27
+ def screen_name
28
+ @screen_name || user.screen_name
29
+ end
30
+
31
+ def __screen_name
32
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
33
+ `TwitterWithAutoPagination::Utils##{__method__}` is deprecated.
34
+ MESSAGE
35
+ screen_name
36
+ end
37
+
38
+ def uid_or_screen_name?(object)
39
+ object.kind_of?(String) || object.kind_of?(Integer)
40
+ end
41
+
42
+ def authenticating_user?(target)
43
+ user.id.to_i == user(target).id.to_i
44
+ end
45
+
46
+ def authorized_user?(target)
47
+ target_user = user(target)
48
+ !target_user.protected? || friendship?(user.id.to_i, target_user.id.to_i)
49
+ end
50
+
51
+ def credentials_hash
52
+ str = access_token + access_token_secret + consumer_key + consumer_secret
53
+ Digest::MD5.hexdigest(str)
54
+ end
55
+
56
+ def instrument(operation, key, options = nil)
57
+ payload = {operation: operation, key: key}
58
+ payload.merge!(options) if options.is_a?(Hash)
59
+ ActiveSupport::Notifications.instrument('call.twitter_with_auto_pagination', payload) { yield(payload) }
60
+ end
61
+
62
+ def call_api(method_obj, *args)
63
+ api_options = args.extract_options!
64
+ begin
65
+ self.call_count += 1
66
+ # TODO call without reduce, call_count
67
+ options = {method_name: method_obj.name, call_count: self.call_count, args: [*args, api_options]}
68
+ instrument('request', args[0], options) { method_obj.call(*args, api_options) }
69
+ rescue Twitter::Error::TooManyRequests => e
70
+ logger.warn "#{__method__}: #{options.inspect} #{e.class} Retry after #{e.rate_limit.reset_in} seconds."
71
+ raise e
72
+ rescue Twitter::Error::ServiceUnavailable, Twitter::Error::InternalServerError,
73
+ Twitter::Error::Forbidden, Twitter::Error::NotFound => e
74
+ logger.warn "#{__method__}: #{options.inspect} #{e.class} #{e.message}"
75
+ raise e
76
+ rescue => e
77
+ logger.warn "NEED TO CATCH! #{__method__}: #{options.inspect} #{e.class} #{e.message}"
78
+ raise e
79
+ end
80
+ end
81
+
82
+ # user_timeline, search
83
+ def collect_with_max_id(method_obj, *args)
84
+ options = args.extract_options!
85
+ call_limit = options.delete(:call_limit) || 3
86
+ last_response = call_api(method_obj, *args, options)
87
+ last_response = yield(last_response) if block_given?
88
+ return_data = last_response
89
+ call_count = 1
90
+
91
+ while last_response.any? && call_count < call_limit
92
+ options[:max_id] = last_response.last.kind_of?(Hash) ? last_response.last[:id] : last_response.last.id
93
+ last_response = call_api(method_obj, *args, options)
94
+ last_response = yield(last_response) if block_given?
95
+ return_data += last_response
96
+ call_count += 1
97
+ end
98
+
99
+ return_data.flatten
100
+ end
101
+
102
+ # friends, followers
103
+ def collect_with_cursor(method_obj, *args)
104
+ options = args.extract_options!
105
+ last_response = call_api(method_obj, *args, options).attrs
106
+ return_data = (last_response[:users] || last_response[:ids])
107
+
108
+ while (next_cursor = last_response[:next_cursor]) && next_cursor != 0
109
+ options[:cursor] = next_cursor
110
+ last_response = call_api(method_obj, *args, options).attrs
111
+ return_data += (last_response[:users] || last_response[:ids])
112
+ end
113
+
114
+ return_data
115
+ end
116
+
117
+ def file_cache_key(method_name, user, options = {})
118
+ delim = ':'
119
+ identifier =
120
+ case
121
+ when method_name == :verify_credentials
122
+ "hash-str#{delim}#{credentials_hash}"
123
+ when method_name == :search
124
+ "str#{delim}#{user.to_s}"
125
+ when method_name == :mentions_timeline
126
+ "#{user.kind_of?(Integer) ? 'id' : 'sn'}#{delim}#{user.to_s}"
127
+ when method_name == :home_timeline
128
+ "#{user.kind_of?(Integer) ? 'id' : 'sn'}#{delim}#{user.to_s}"
129
+ when method_name.in?([:users, :replying]) && options[:super_operation].present?
130
+ case
131
+ when user.kind_of?(Array) && user.first.kind_of?(Integer)
132
+ "#{options[:super_operation]}-ids#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
133
+ when user.kind_of?(Array) && user.first.kind_of?(String)
134
+ "#{options[:super_operation]}-sns#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
135
+ else raise "#{method_name.inspect} #{user.inspect}"
136
+ end
137
+ when user.kind_of?(Integer)
138
+ "id#{delim}#{user.to_s}"
139
+ when user.kind_of?(Array) && user.first.kind_of?(Integer)
140
+ "ids#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
141
+ when user.kind_of?(Array) && user.first.kind_of?(String)
142
+ "sns#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
143
+ when user.kind_of?(String)
144
+ "sn#{delim}#{user}"
145
+ when user.kind_of?(Twitter::User)
146
+ "user#{delim}#{user.id.to_s}"
147
+ else raise "#{method_name.inspect} #{user.inspect}"
148
+ end
149
+
150
+ "#{method_name}#{delim}#{identifier}"
151
+ end
152
+
153
+ def namespaced_key(method_name, user, options = {})
154
+ file_cache_key(method_name, user, options)
155
+ end
156
+
157
+ PROFILE_SAVE_KEYS = %i(
158
+ id
159
+ name
160
+ screen_name
161
+ location
162
+ description
163
+ url
164
+ protected
165
+ followers_count
166
+ friends_count
167
+ listed_count
168
+ favourites_count
169
+ utc_offset
170
+ time_zone
171
+ geo_enabled
172
+ verified
173
+ statuses_count
174
+ lang
175
+ status
176
+ profile_image_url_https
177
+ profile_banner_url
178
+ profile_link_color
179
+ suspended
180
+ verified
181
+ entities
182
+ created_at
183
+ )
184
+
185
+ STATUS_SAVE_KEYS = %i(
186
+ created_at
187
+ id
188
+ text
189
+ source
190
+ truncated
191
+ coordinates
192
+ place
193
+ entities
194
+ user
195
+ contributors
196
+ is_quote_status
197
+ retweet_count
198
+ favorite_count
199
+ favorited
200
+ retweeted
201
+ possibly_sensitive
202
+ lang
203
+ )
204
+
205
+ # encode
206
+ def encode_json(obj, caller_name, options = {})
207
+ options[:reduce] = true unless options.has_key?(:reduce)
208
+ case caller_name
209
+ when :user_timeline, :home_timeline, :mentions_timeline, :favorites # Twitter::Tweet
210
+ JSON.pretty_generate(obj.map { |o| o.attrs })
211
+
212
+ when :search # Hash
213
+ data =
214
+ if options[:reduce]
215
+ obj.map { |o| o.to_hash.slice(*STATUS_SAVE_KEYS) }
216
+ else
217
+ obj.map { |o| o.to_hash }
218
+ end
219
+ JSON.pretty_generate(data)
220
+
221
+ when :friends, :followers # Hash
222
+ data =
223
+ if options[:reduce]
224
+ obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
225
+ else
226
+ obj.map { |o| o.to_hash }
227
+ end
228
+ JSON.pretty_generate(data)
229
+
230
+ when :friend_ids, :follower_ids # Integer
231
+ JSON.pretty_generate(obj)
232
+
233
+ when :verify_credentials # Twitter::User
234
+ JSON.pretty_generate(obj.to_hash.slice(*PROFILE_SAVE_KEYS))
235
+
236
+ when :user # Twitter::User
237
+ JSON.pretty_generate(obj.to_hash.slice(*PROFILE_SAVE_KEYS))
238
+
239
+ when :users, :friends_parallelly, :followers_parallelly # Twitter::User
240
+ data =
241
+ if options[:reduce]
242
+ obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
243
+ else
244
+ obj.map { |o| o.to_hash }
245
+ end
246
+ JSON.pretty_generate(data)
247
+
248
+ when :user? # true or false
249
+ obj
250
+
251
+ when :friendship? # true or false
252
+ obj
253
+
254
+ else
255
+ raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
256
+ end
257
+ end
258
+
259
+ # decode
260
+ def decode_json(json_str, caller_name, options = {})
261
+ obj = json_str.kind_of?(String) ? JSON.parse(json_str) : json_str
262
+ case
263
+ when obj.nil?
264
+ obj
265
+
266
+ when obj.kind_of?(Array) && obj.first.kind_of?(Hash)
267
+ obj.map { |o| Hashie::Mash.new(o) }
268
+
269
+ when obj.kind_of?(Array) && obj.first.kind_of?(Integer)
270
+ obj
271
+
272
+ when obj.kind_of?(Hash)
273
+ Hashie::Mash.new(obj)
274
+
275
+ when obj === true || obj === false
276
+ obj
277
+
278
+ when obj.kind_of?(Array) && obj.empty?
279
+ obj
280
+
281
+ else
282
+ raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
283
+ end
284
+ end
285
+
286
+ def fetch_cache_or_call_api(method_name, user, options = {})
287
+ key = namespaced_key(method_name, user, options)
288
+
289
+ fetch_result =
290
+ if options[:cache] == :read
291
+ instrument('Cache Read(Force)', key, caller: method_name) { cache.read(key) }
292
+ else
293
+ cache.fetch(key, expires_in: 1.hour, race_condition_ttl: 5.minutes) do
294
+ block_result = yield
295
+ instrument('serialize', key, caller: method_name) { encode_json(block_result, method_name, options) }
296
+ end
297
+ end
298
+
299
+ instrument('deserialize', key, caller: method_name) { decode_json(fetch_result, method_name, options) }
300
+ end
301
+ end
302
+ end
303
+ end