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.
- checksums.yaml +4 -4
- data/README.md +8 -18
- data/lib/twitter_with_auto_pagination.rb +42 -1
- data/lib/twitter_with_auto_pagination/log_subscriber.rb +2 -2
- data/lib/twitter_with_auto_pagination/rest/api.rb +31 -0
- data/lib/twitter_with_auto_pagination/rest/extension/clusters.rb +43 -0
- data/lib/twitter_with_auto_pagination/rest/extension/favoriting.rb +106 -0
- data/lib/twitter_with_auto_pagination/rest/extension/friends_and_followers.rb +131 -0
- data/lib/twitter_with_auto_pagination/rest/extension/replying.rb +90 -0
- data/lib/twitter_with_auto_pagination/rest/extension/unfollowing.rb +29 -0
- data/lib/twitter_with_auto_pagination/rest/favorites.rb +20 -0
- data/lib/twitter_with_auto_pagination/rest/friends_and_followers.rb +94 -0
- data/lib/twitter_with_auto_pagination/rest/search.rb +19 -0
- data/lib/twitter_with_auto_pagination/rest/timelines.rb +37 -0
- data/lib/twitter_with_auto_pagination/rest/uncategorized.rb +83 -0
- data/lib/twitter_with_auto_pagination/rest/users.rb +62 -0
- data/lib/twitter_with_auto_pagination/rest/utils.rb +303 -0
- data/spec/helper.rb +60 -1
- data/spec/twitter_with_auto_pagination/client_spec.rb +150 -0
- data/twitter_with_auto_pagination.gemspec +1 -1
- metadata +17 -8
- data/lib/twitter_with_auto_pagination/client.rb +0 -139
- data/lib/twitter_with_auto_pagination/existing_api.rb +0 -127
- data/lib/twitter_with_auto_pagination/new_api.rb +0 -337
- data/lib/twitter_with_auto_pagination/utils.rb +0 -303
- 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
|