twitter_with_auto_pagination 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +7 -0
- data/README.md +52 -0
- data/Rakefile +9 -0
- data/lib/twitter_with_auto_pagination.rb +4 -0
- data/lib/twitter_with_auto_pagination/client.rb +137 -0
- data/lib/twitter_with_auto_pagination/existing_api.rb +127 -0
- data/lib/twitter_with_auto_pagination/log_subscriber.rb +82 -0
- data/lib/twitter_with_auto_pagination/new_api.rb +331 -0
- data/lib/twitter_with_auto_pagination/utils.rb +303 -0
- data/spec/helper.rb +9 -0
- data/spec/twitter_with_auto_pagination_spec.rb +131 -0
- data/twitter_with_auto_pagination.gemspec +25 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5a766fdb1cdee5bb05a9a5a075d2249637284338
|
4
|
+
data.tar.gz: a493b330a56148deb5abf3408b3252c492d56be9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8238fe4d1f500572e1993891ecd861f1cb6f90526732bf37defe460fd5e130b1605fded19fd951dc68901c62eead6d69325b71790b9cb552393203ceb402e355
|
7
|
+
data.tar.gz: 9fd76e42c73f82b200ce110eb9efeba3097150e29fd6bbb273d84da4a0d267698bb7d2bba2e6e6b65f5a78880b65b523febde6841a9d51a29a45bbd8dee76573
|
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2014 Shinohara Teruki
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
ex-twitter
|
2
|
+
==========
|
3
|
+
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/ex_twitter.png)](https://badge.fury.io/rb/ex_twitter)
|
5
|
+
[![Build Status](https://travis-ci.org/ts-3156/ex-twitter.svg?branch=master)](https://travis-ci.org/ts-3156/ex-twitter)
|
6
|
+
|
7
|
+
Add auto paginate feature to Twitter gem.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
### Gem
|
12
|
+
|
13
|
+
```
|
14
|
+
gem install ex_twitter
|
15
|
+
```
|
16
|
+
|
17
|
+
### Rails
|
18
|
+
|
19
|
+
Add ex_twitter to your Gemfile, and bundle.
|
20
|
+
|
21
|
+
## Features
|
22
|
+
|
23
|
+
* Auto paginate feature
|
24
|
+
|
25
|
+
## Configuration
|
26
|
+
|
27
|
+
You can pass configuration options as a block to `ExTwitter.new` just like `Twitter::REST::Client.new`.
|
28
|
+
|
29
|
+
```
|
30
|
+
client = ExTwitter.new do |config|
|
31
|
+
config.consumer_key = "YOUR_CONSUMER_KEY"
|
32
|
+
config.consumer_secret = "YOUR_CONSUMER_SECRET"
|
33
|
+
config.access_token = "YOUR_ACCESS_TOKEN"
|
34
|
+
config.access_token_secret = "YOUR_ACCESS_SECRET"
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
You can pass advanced configuration options as a block to `ExTwitter.new`.
|
39
|
+
|
40
|
+
```
|
41
|
+
client = ExTwitter.new do |config|
|
42
|
+
config.auto_paginate = true
|
43
|
+
config.max_retries = 1
|
44
|
+
config.max_paginates = 3
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
## Usage Examples
|
49
|
+
|
50
|
+
```
|
51
|
+
client.user_timeline
|
52
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/cache'
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
|
5
|
+
require 'twitter_with_auto_pagination/log_subscriber'
|
6
|
+
require 'twitter_with_auto_pagination/utils'
|
7
|
+
require 'twitter_with_auto_pagination/existing_api'
|
8
|
+
require 'twitter_with_auto_pagination/new_api'
|
9
|
+
|
10
|
+
require 'twitter'
|
11
|
+
require 'hashie'
|
12
|
+
require 'parallel'
|
13
|
+
|
14
|
+
module TwitterWithAutoPagination
|
15
|
+
class Client < Twitter::REST::Client
|
16
|
+
def initialize(options = {})
|
17
|
+
@cache = ActiveSupport::Cache::FileStore.new(File.join('tmp', 'api_cache'))
|
18
|
+
@call_count = 0
|
19
|
+
|
20
|
+
@uid = options.has_key?(:uid) ? options.delete(:uid).to_i : nil
|
21
|
+
@screen_name = options.has_key?(:screen_name) ? options.delete(:screen_name).to_s : nil
|
22
|
+
|
23
|
+
@@logger = @logger =
|
24
|
+
if options[:logger]
|
25
|
+
options.delete(:logger)
|
26
|
+
else
|
27
|
+
Dir.mkdir('log') unless File.exists?('log')
|
28
|
+
Logger.new('log/twitter_with_auto_pagination.log')
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.logger
|
35
|
+
@@logger
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_accessor :call_count
|
39
|
+
attr_reader :cache, :authenticated_user, :logger
|
40
|
+
|
41
|
+
INDENT = 4
|
42
|
+
|
43
|
+
include TwitterWithAutoPagination::Utils
|
44
|
+
|
45
|
+
alias :old_verify_credentials :verify_credentials
|
46
|
+
alias :old_friendship? :friendship?
|
47
|
+
alias :old_user? :user?
|
48
|
+
alias :old_user :user
|
49
|
+
alias :old_friend_ids :friend_ids
|
50
|
+
alias :old_follower_ids :follower_ids
|
51
|
+
alias :old_friends :friends
|
52
|
+
alias :old_followers :followers
|
53
|
+
alias :old_users :users
|
54
|
+
alias :old_home_timeline :home_timeline
|
55
|
+
alias :old_user_timeline :user_timeline
|
56
|
+
alias :old_mentions_timeline :mentions_timeline
|
57
|
+
alias :old_favorites :favorites
|
58
|
+
alias :old_search :search
|
59
|
+
|
60
|
+
include TwitterWithAutoPagination::ExistingApi
|
61
|
+
include TwitterWithAutoPagination::NewApi
|
62
|
+
|
63
|
+
def usage_stats_wday_series_data(times)
|
64
|
+
wday_count = times.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |time, memo|
|
65
|
+
memo[time.wday] += 1
|
66
|
+
end
|
67
|
+
wday_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
|
68
|
+
{name: key, y: value, drilldown: key}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def usage_stats_wday_drilldown_series(times)
|
73
|
+
hour_count =
|
74
|
+
(0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
|
75
|
+
wday_memo[wday] =
|
76
|
+
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|
|
77
|
+
hour_memo[hour] += 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
hour_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
|
81
|
+
{name: key, id: key, data: value.to_a.map{|a| [a[0].to_s, a[1]] }}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def usage_stats_hour_series_data(times)
|
86
|
+
hour_count = times.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |time, memo|
|
87
|
+
memo[time.hour] += 1
|
88
|
+
end
|
89
|
+
hour_count.map do |key, value|
|
90
|
+
{name: key.to_s, y: value, drilldown: key.to_s}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def usage_stats_hour_drilldown_series(times)
|
95
|
+
wday_count =
|
96
|
+
(0..23).each_with_object((0..23).map { |n| [n, nil] }.to_h) do |hour, hour_memo|
|
97
|
+
hour_memo[hour] =
|
98
|
+
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|
|
99
|
+
wday_memo[wday] += 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
wday_count.map do |key, value|
|
103
|
+
{name: key.to_s, id: key.to_s, data: value.to_a.map{|a| [I18n.t('date.abbr_day_names')[a[0]], a[1]] }}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def twitter_addiction_series(times)
|
108
|
+
five_mins = 5.minutes
|
109
|
+
wday_expended_seconds =
|
110
|
+
(0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
|
111
|
+
target_times = times.select { |t| t.wday == wday }
|
112
|
+
wday_memo[wday] = target_times.empty? ? nil : target_times.each_cons(2).map {|a, b| (a - b) < five_mins ? a - b : five_mins }.sum
|
113
|
+
end
|
114
|
+
days = times.map{|t| t.to_date.to_s(:long) }.uniq.size
|
115
|
+
weeks = (days > 7) ? days / 7.0 : 1.0
|
116
|
+
wday_expended_seconds.map { |k, v| [I18n.t('date.abbr_day_names')[k], (v.nil? ? nil : v / weeks / 60)] }.map do |key, value|
|
117
|
+
{name: key, y: value}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def usage_stats(user, options = {})
|
122
|
+
n_days_ago = options.has_key?(:days) ? options[:days].days.ago : 100.years.ago
|
123
|
+
tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user)
|
124
|
+
times =
|
125
|
+
# TODO Use user specific time zone
|
126
|
+
tweets.map { |t| ActiveSupport::TimeZone['Tokyo'].parse(t.created_at.to_s) }.
|
127
|
+
select { |t| t > n_days_ago }
|
128
|
+
[
|
129
|
+
usage_stats_wday_series_data(times),
|
130
|
+
usage_stats_wday_drilldown_series(times),
|
131
|
+
usage_stats_hour_series_data(times),
|
132
|
+
usage_stats_hour_drilldown_series(times),
|
133
|
+
twitter_addiction_series(times)
|
134
|
+
]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module TwitterWithAutoPagination
|
2
|
+
module ExistingApi
|
3
|
+
def verify_credentials(*args)
|
4
|
+
options = {skip_status: true}.merge(args.extract_options!)
|
5
|
+
fetch_cache_or_call_api(__method__, args) {
|
6
|
+
call_old_method("old_#{__method__}", *args, options)
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
def friendship?(*args)
|
11
|
+
options = args.extract_options!
|
12
|
+
fetch_cache_or_call_api(__method__, args) {
|
13
|
+
call_old_method("old_#{__method__}", *args, options)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def user?(*args)
|
18
|
+
options = args.extract_options!
|
19
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
20
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
21
|
+
call_old_method("old_#{__method__}", args[0], options)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def user(*args)
|
26
|
+
options = args.extract_options!
|
27
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
28
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
29
|
+
call_old_method("old_#{__method__}", args[0], options)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def friend_ids(*args)
|
34
|
+
options = {count: 5000, cursor: -1}.merge(args.extract_options!)
|
35
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
36
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
37
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def follower_ids(*args)
|
42
|
+
options = {count: 5000, cursor: -1}.merge(args.extract_options!)
|
43
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
44
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
45
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# specify reduce: false to use tweet for inactive_*
|
50
|
+
def friends(*args)
|
51
|
+
options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
|
52
|
+
options[:reduce] = false unless options.has_key?(:reduce)
|
53
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
54
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
55
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# specify reduce: false to use tweet for inactive_*
|
60
|
+
def followers(*args)
|
61
|
+
options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
|
62
|
+
options[:reduce] = false unless options.has_key?(:reduce)
|
63
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
64
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
65
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# use compact, not use sort and uniq
|
70
|
+
# specify reduce: false to use tweet for inactive_*
|
71
|
+
# TODO Perhaps `old_users` automatically merges result...
|
72
|
+
def users(*args)
|
73
|
+
options = args.extract_options!
|
74
|
+
options[:reduce] = false
|
75
|
+
users_per_workers = args.first.compact.each_slice(100).to_a
|
76
|
+
processed_users = []
|
77
|
+
|
78
|
+
Parallel.each_with_index(users_per_workers, in_threads: [users_per_workers.size, 10].min) do |users_per_worker, i|
|
79
|
+
_users = fetch_cache_or_call_api(__method__, users_per_worker, options) {
|
80
|
+
call_old_method("old_#{__method__}", users_per_worker, options)
|
81
|
+
}
|
82
|
+
|
83
|
+
processed_users << {i: i, users: _users}
|
84
|
+
end
|
85
|
+
|
86
|
+
processed_users.sort_by{|p| p[:i] }.map{|p| p[:users] }.flatten.compact
|
87
|
+
end
|
88
|
+
|
89
|
+
def home_timeline(*args)
|
90
|
+
options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
|
91
|
+
fetch_cache_or_call_api(__method__, user.id, options) {
|
92
|
+
collect_with_max_id("old_#{__method__}", options)
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def user_timeline(*args)
|
97
|
+
options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
|
98
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
99
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
100
|
+
collect_with_max_id("old_#{__method__}", *args, options)
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def mentions_timeline(*args)
|
105
|
+
options = {count: 200, include_rts: true, call_limit: 1}.merge(args.extract_options!)
|
106
|
+
fetch_cache_or_call_api(__method__, user.id, options) {
|
107
|
+
collect_with_max_id("old_#{__method__}", options)
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def favorites(*args)
|
112
|
+
options = {count: 100, call_count: 1}.merge(args.extract_options!)
|
113
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
114
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
115
|
+
collect_with_max_id("old_#{__method__}", *args, options)
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def search(*args)
|
120
|
+
options = {count: 100, result_type: :recent, call_limit: 1}.merge(args.extract_options!)
|
121
|
+
options[:reduce] = false
|
122
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
123
|
+
collect_with_max_id("old_#{__method__}", *args, options) { |response| response.attrs[:statuses] }
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
|
4
|
+
|
5
|
+
module TwitterWithAutoPagination
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@odd = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def cache_any(event)
|
14
|
+
return unless logger.debug?
|
15
|
+
|
16
|
+
payload = event.payload
|
17
|
+
name = "#{payload.delete(:name)} (#{event.duration.round(1)}ms)"
|
18
|
+
name = colorize_payload_name(name, payload[:name], AS: true)
|
19
|
+
debug { "#{name} #{(payload.inspect)}" }
|
20
|
+
end
|
21
|
+
|
22
|
+
%w(read write fetch_hit generate delete exist?).each do |operation|
|
23
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
24
|
+
def cache_#{operation}(event)
|
25
|
+
event.payload[:name] = '#{operation}'
|
26
|
+
cache_any(event)
|
27
|
+
end
|
28
|
+
METHOD
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(event)
|
32
|
+
return unless logger.debug?
|
33
|
+
|
34
|
+
payload = event.payload
|
35
|
+
name = "#{payload.delete(:operation)} (#{event.duration.round(1)}ms)"
|
36
|
+
|
37
|
+
name = colorize_payload_name(name, payload[:name])
|
38
|
+
# sql = color(sql, sql_color(sql), true)
|
39
|
+
|
40
|
+
key = payload.delete(:key)
|
41
|
+
debug { "#{name} #{key} #{(payload.inspect)}" }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def colorize_payload_name(name, payload_name, options = {})
|
47
|
+
if options[:AS]
|
48
|
+
color(name, MAGENTA, true)
|
49
|
+
else
|
50
|
+
color(name, CYAN, true)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def sql_color(sql)
|
55
|
+
case sql
|
56
|
+
when /\A\s*rollback/mi
|
57
|
+
RED
|
58
|
+
when /select .*for update/mi, /\A\s*lock/mi
|
59
|
+
WHITE
|
60
|
+
when /\A\s*select/i
|
61
|
+
BLUE
|
62
|
+
when /\A\s*insert/i
|
63
|
+
GREEN
|
64
|
+
when /\A\s*update/i
|
65
|
+
YELLOW
|
66
|
+
when /\A\s*delete/i
|
67
|
+
RED
|
68
|
+
when /transaction\s*\Z/i
|
69
|
+
CYAN
|
70
|
+
else
|
71
|
+
MAGENTA
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def logger
|
76
|
+
TwitterWithAutoPagination::Client.logger
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
TwitterWithAutoPagination::LogSubscriber.attach_to :twitter_with_auto_pagination
|
82
|
+
TwitterWithAutoPagination::LogSubscriber.attach_to :active_support
|
@@ -0,0 +1,331 @@
|
|
1
|
+
module TwitterWithAutoPagination
|
2
|
+
module NewApi
|
3
|
+
def friends_parallelly(*args)
|
4
|
+
options = {super_operation: __method__}.merge(args.extract_options!)
|
5
|
+
_friend_ids = friend_ids(*(args + [options]))
|
6
|
+
users(_friend_ids.map { |id| id.to_i }, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def followers_parallelly(*args)
|
10
|
+
options = {super_operation: __method__}.merge(args.extract_options!)
|
11
|
+
_follower_ids = follower_ids(*(args + [options]))
|
12
|
+
users(_follower_ids.map { |id| id.to_i }, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def _fetch_parallelly(signatures) # [{method: :friends, args: ['ts_3156', ...], {...}]
|
16
|
+
result = Array.new(signatures.size)
|
17
|
+
|
18
|
+
Parallel.each_with_index(signatures, in_threads: result.size) do |signature, i|
|
19
|
+
result[i] = send(signature[:method], *signature[:args])
|
20
|
+
end
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
def friends_and_followers(*args)
|
26
|
+
_fetch_parallelly(
|
27
|
+
[
|
28
|
+
{method: :friends_parallelly, args: args},
|
29
|
+
{method: :followers_parallelly, args: args}])
|
30
|
+
end
|
31
|
+
|
32
|
+
def friends_followers_and_statuses(*args)
|
33
|
+
_fetch_parallelly(
|
34
|
+
[
|
35
|
+
{method: :friends_parallelly, args: args},
|
36
|
+
{method: :followers_parallelly, args: args},
|
37
|
+
{method: :user_timeline, args: args}])
|
38
|
+
end
|
39
|
+
|
40
|
+
def one_sided_following(me)
|
41
|
+
if uid_or_screen_name?(me)
|
42
|
+
# TODO use friends_and_followers
|
43
|
+
friends_parallelly(me).to_a - followers_parallelly(me).to_a
|
44
|
+
elsif me.respond_to?(:friends) && me.respond_to?(:followers)
|
45
|
+
me.friends.to_a - me.followers.to_a
|
46
|
+
else
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def one_sided_followers(me)
|
52
|
+
if uid_or_screen_name?(me)
|
53
|
+
# TODO use friends_and_followers
|
54
|
+
followers_parallelly(me).to_a - friends_parallelly(me).to_a
|
55
|
+
elsif me.respond_to?(:friends) && me.respond_to?(:followers)
|
56
|
+
me.followers.to_a - me.friends.to_a
|
57
|
+
else
|
58
|
+
raise
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def mutual_friends(me)
|
63
|
+
if uid_or_screen_name?(me)
|
64
|
+
# TODO use friends_and_followers
|
65
|
+
friends_parallelly(me).to_a & followers_parallelly(me).to_a
|
66
|
+
elsif me.respond_to?(:friends) && me.respond_to?(:followers)
|
67
|
+
me.friends.to_a & me.followers.to_a
|
68
|
+
else
|
69
|
+
raise
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def common_friends(me, you)
|
74
|
+
if uid_or_screen_name?(me) && uid_or_screen_name?(you)
|
75
|
+
friends_parallelly(me).to_a & friends_parallelly(you).to_a
|
76
|
+
elsif me.respond_to?(:friends) && you.respond_to?(:friends)
|
77
|
+
me.friends.to_a & you.friends.to_a
|
78
|
+
else
|
79
|
+
raise
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def common_followers(me, you)
|
84
|
+
if uid_or_screen_name?(me) && uid_or_screen_name?(you)
|
85
|
+
followers_parallelly(me).to_a & followers_parallelly(you).to_a
|
86
|
+
elsif me.respond_to?(:followers) && you.respond_to?(:followers)
|
87
|
+
me.followers.to_a & you.followers.to_a
|
88
|
+
else
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def removing(pre_me, cur_me)
|
94
|
+
if uid_or_screen_name?(pre_me) && uid_or_screen_name?(cur_me)
|
95
|
+
friends_parallelly(pre_me).to_a - friends_parallelly(cur_me).to_a
|
96
|
+
elsif pre_me.respond_to?(:friends) && cur_me.respond_to?(:friends)
|
97
|
+
pre_me.friends.to_a - cur_me.friends.to_a
|
98
|
+
else
|
99
|
+
raise
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def removed(pre_me, cur_me)
|
104
|
+
if uid_or_screen_name?(pre_me) && uid_or_screen_name?(cur_me)
|
105
|
+
followers_parallelly(pre_me).to_a - followers_parallelly(cur_me).to_a
|
106
|
+
elsif pre_me.respond_to?(:followers) && cur_me.respond_to?(:followers)
|
107
|
+
pre_me.followers.to_a - cur_me.followers.to_a
|
108
|
+
else
|
109
|
+
raise
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def _extract_screen_names(tweets)
|
114
|
+
tweets.map do |t|
|
115
|
+
$1 if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
|
116
|
+
end.compact
|
117
|
+
end
|
118
|
+
|
119
|
+
# users which specified user is replying
|
120
|
+
# in_reply_to_user_id and in_reply_to_status_id is not used because of distinguishing mentions from replies
|
121
|
+
def replying(*args)
|
122
|
+
options = args.extract_options!
|
123
|
+
tweets =
|
124
|
+
if args.empty?
|
125
|
+
user_timeline(options)
|
126
|
+
elsif uid_or_screen_name?(args[0])
|
127
|
+
user_timeline(args[0], options)
|
128
|
+
elsif args[0].kind_of?(Array) && args[0].all? { |t| t.respond_to?(:text) }
|
129
|
+
args[0]
|
130
|
+
else
|
131
|
+
raise
|
132
|
+
end
|
133
|
+
|
134
|
+
screen_names = _extract_screen_names(tweets)
|
135
|
+
result = users(screen_names, {super_operation: __method__}.merge(options))
|
136
|
+
if options.has_key?(:uniq) && !options[:uniq]
|
137
|
+
screen_names.map { |sn| result.find { |r| r.screen_name == sn } }.compact
|
138
|
+
else
|
139
|
+
result.uniq { |r| r.id }
|
140
|
+
end
|
141
|
+
rescue Twitter::Error::NotFound => e
|
142
|
+
e.message == 'No user matches for specified terms.' ? [] : (raise e)
|
143
|
+
rescue => e
|
144
|
+
logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
|
145
|
+
raise e
|
146
|
+
end
|
147
|
+
|
148
|
+
def _extract_uids(tweets)
|
149
|
+
tweets.map do |t|
|
150
|
+
t.user.id.to_i if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
|
151
|
+
end.compact
|
152
|
+
end
|
153
|
+
|
154
|
+
def _extract_users(tweets, uids)
|
155
|
+
uids.map { |uid| tweets.find { |t| t.user.id.to_i == uid.to_i } }.map { |t| t.user }.compact
|
156
|
+
end
|
157
|
+
|
158
|
+
# users which specified user is replied
|
159
|
+
# when user is login you had better to call mentions_timeline
|
160
|
+
def replied(*args)
|
161
|
+
options = args.extract_options!
|
162
|
+
|
163
|
+
result =
|
164
|
+
if args.empty? || (uid_or_screen_name?(args[0]) && authenticating_user?(args[0]))
|
165
|
+
mentions_timeline.map { |m| m.user }
|
166
|
+
else
|
167
|
+
searched_result = search('@' + user(args[0]).screen_name, options)
|
168
|
+
uids = _extract_uids(searched_result)
|
169
|
+
_extract_users(searched_result, uids)
|
170
|
+
end
|
171
|
+
|
172
|
+
if options.has_key?(:uniq) && !options[:uniq]
|
173
|
+
result
|
174
|
+
else
|
175
|
+
result.uniq { |r| r.id }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def _count_users_with_two_sided_threshold(users, options)
|
180
|
+
min = options.has_key?(:min) ? options[:min] : 0
|
181
|
+
max = options.has_key?(:max) ? options[:max] : 1000
|
182
|
+
users.each_with_object(Hash.new(0)) { |u, memo| memo[u.id] += 1 }.
|
183
|
+
select { |_k, v| min <= v && v <= max }.
|
184
|
+
sort_by { |_, v| -v }.to_h
|
185
|
+
end
|
186
|
+
|
187
|
+
def _extract_favorite_users(favs, options = {})
|
188
|
+
counted_value = _count_users_with_two_sided_threshold(favs.map { |t| t.user }, options)
|
189
|
+
counted_value.map do |uid, cnt|
|
190
|
+
fav = favs.find { |f| f.user.id.to_i == uid.to_i }
|
191
|
+
Array.new(cnt, fav.user)
|
192
|
+
end.flatten
|
193
|
+
end
|
194
|
+
|
195
|
+
def favoriting(*args)
|
196
|
+
options = args.extract_options!
|
197
|
+
|
198
|
+
favs =
|
199
|
+
if args.empty?
|
200
|
+
favorites(options)
|
201
|
+
elsif uid_or_screen_name?(args[0])
|
202
|
+
favorites(args[0], options)
|
203
|
+
elsif args[0].kind_of?(Array) && args[0].all? { |t| t.respond_to?(:text) }
|
204
|
+
args[0]
|
205
|
+
else
|
206
|
+
raise
|
207
|
+
end
|
208
|
+
|
209
|
+
result = _extract_favorite_users(favs, options)
|
210
|
+
if options.has_key?(:uniq) && !options[:uniq]
|
211
|
+
result
|
212
|
+
else
|
213
|
+
result.uniq { |r| r.id }
|
214
|
+
end
|
215
|
+
rescue => e
|
216
|
+
logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
|
217
|
+
raise e
|
218
|
+
end
|
219
|
+
|
220
|
+
def _extract_inactive_users(users, options = {})
|
221
|
+
authorized = options.delete(:authorized)
|
222
|
+
two_weeks_ago = 2.weeks.ago.to_i
|
223
|
+
users.select do |u|
|
224
|
+
if authorized
|
225
|
+
(Time.parse(u.status.created_at).to_i < two_weeks_ago) rescue false
|
226
|
+
else
|
227
|
+
false
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def favorited_by(*args)
|
233
|
+
end
|
234
|
+
|
235
|
+
def close_friends(*args)
|
236
|
+
options = {uniq: false}.merge(args.extract_options!)
|
237
|
+
min_max = {
|
238
|
+
min: options.has_key?(:min) ? options.delete(:min) : 0,
|
239
|
+
max: options.has_key?(:max) ? options.delete(:max) : 1000
|
240
|
+
}
|
241
|
+
|
242
|
+
_replying, _replied, _favoriting =
|
243
|
+
if args.empty?
|
244
|
+
[replying(options), replied(options), favoriting(options)]
|
245
|
+
elsif uid_or_screen_name?(args[0])
|
246
|
+
[replying(args[0], options), replied(args[0], options), favoriting(args[0], options)]
|
247
|
+
elsif (m_names = %i(replying replied favoriting)).all? { |m_name| args[0].respond_to?(m_name) }
|
248
|
+
m_names.map { |mn| args[0].send(mn) }
|
249
|
+
else
|
250
|
+
raise
|
251
|
+
end
|
252
|
+
|
253
|
+
_users = _replying + _replied + _favoriting
|
254
|
+
return [] if _users.empty?
|
255
|
+
|
256
|
+
scores = _count_users_with_two_sided_threshold(_users, min_max)
|
257
|
+
replying_scores = _count_users_with_two_sided_threshold(_replying, min_max)
|
258
|
+
replied_scores = _count_users_with_two_sided_threshold(_replied, min_max)
|
259
|
+
favoriting_scores = _count_users_with_two_sided_threshold(_favoriting, min_max)
|
260
|
+
|
261
|
+
scores.keys.map { |uid| _users.find { |u| u.id.to_i == uid.to_i } }.
|
262
|
+
map do |u|
|
263
|
+
u[:score] = scores[u.id]
|
264
|
+
u[:replying_score] = replying_scores[u.id]
|
265
|
+
u[:replied_score] = replied_scores[u.id]
|
266
|
+
u[:favoriting_score] = favoriting_scores[u.id]
|
267
|
+
u
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def inactive_friends(user = nil)
|
272
|
+
if user.blank?
|
273
|
+
_extract_inactive_users(friends_parallelly, authorized: true)
|
274
|
+
elsif uid_or_screen_name?(user)
|
275
|
+
authorized = authenticating_user?(user) || authorized_user?(user)
|
276
|
+
_extract_inactive_users(friends_parallelly(user), authorized: authorized)
|
277
|
+
elsif user.respond_to?(:friends)
|
278
|
+
authorized = authenticating_user?(user.uid.to_i) || authorized_user?(user.uid.to_i)
|
279
|
+
_extract_inactive_users(user.friends, authorized: authorized)
|
280
|
+
else
|
281
|
+
raise
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def inactive_followers(user = nil)
|
286
|
+
if user.blank?
|
287
|
+
_extract_inactive_users(followers_parallelly, authorized: true)
|
288
|
+
elsif uid_or_screen_name?(user)
|
289
|
+
authorized = authenticating_user?(user) || authorized_user?(user)
|
290
|
+
_extract_inactive_users(followers_parallelly(user), authorized: authorized)
|
291
|
+
elsif user.respond_to?(:followers)
|
292
|
+
authorized = authenticating_user?(user.uid.to_i) || authorized_user?(user.uid.to_i)
|
293
|
+
_extract_inactive_users(user.followers, authorized: authorized)
|
294
|
+
else
|
295
|
+
raise
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def clusters_belong_to(text)
|
300
|
+
return [] if text.blank?
|
301
|
+
|
302
|
+
exclude_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_bad_words_path']))
|
303
|
+
special_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_good_words_path']))
|
304
|
+
|
305
|
+
# クラスタ用の単語の出現回数を記録
|
306
|
+
cluster_word_counter =
|
307
|
+
special_words.map { |sw| [sw, text.scan(sw)] }
|
308
|
+
.delete_if { |item| item[1].empty? }
|
309
|
+
.each_with_object(Hash.new(1)) { |item, memo| memo[item[0]] = item[1].size }
|
310
|
+
|
311
|
+
# 同一文字種の繰り返しを見付ける。漢字の繰り返し、ひらがなの繰り返し、カタカナの繰り返し、など
|
312
|
+
text.scan(/[一-龠〆ヵヶ々]+|[ぁ-んー~]+|[ァ-ヴー~]+|[a-zA-Z0-9]+|[、。!!??]+/).
|
313
|
+
|
314
|
+
# 複数回繰り返される文字を除去
|
315
|
+
map { |w| w.remove /[?!?!。、w]|(ー{2,})/ }.
|
316
|
+
|
317
|
+
# 文字数の少なすぎる単語、ひらがなだけの単語、除外単語を除去する
|
318
|
+
delete_if { |w| w.length <= 1 || (w.length <= 2 && w =~ /^[ぁ-んー~]+$/) || exclude_words.include?(w) }.
|
319
|
+
|
320
|
+
# 出現回数を記録
|
321
|
+
each { |w| cluster_word_counter[w] += 1 }
|
322
|
+
|
323
|
+
# 複数個以上見付かった単語のみを残し、出現頻度順にソート
|
324
|
+
cluster_word_counter.select { |_, v| v > 3 }.sort_by { |_, v| -v }.to_h
|
325
|
+
end
|
326
|
+
|
327
|
+
def clusters_assigned_to
|
328
|
+
raise NotImplementedError.new
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
module TwitterWithAutoPagination
|
2
|
+
module Utils
|
3
|
+
# for backward compatibility
|
4
|
+
def uid
|
5
|
+
@uid || user.id.to_i
|
6
|
+
end
|
7
|
+
|
8
|
+
def __uid
|
9
|
+
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
|
10
|
+
`TwitterWithAutoPagination::Utils#__uid` is deprecated.
|
11
|
+
MESSAGE
|
12
|
+
uid
|
13
|
+
end
|
14
|
+
|
15
|
+
def __uid_i
|
16
|
+
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
|
17
|
+
`TwitterWithAutoPagination::Utils#__uid_i` is deprecated.
|
18
|
+
MESSAGE
|
19
|
+
uid
|
20
|
+
end
|
21
|
+
|
22
|
+
# for backward compatibility
|
23
|
+
def screen_name
|
24
|
+
@screen_name || user.screen_name
|
25
|
+
end
|
26
|
+
|
27
|
+
def __screen_name
|
28
|
+
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
|
29
|
+
`TwitterWithAutoPagination::Utils#__screen_name` is deprecated.
|
30
|
+
MESSAGE
|
31
|
+
screen_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def uid_or_screen_name?(object)
|
35
|
+
object.kind_of?(String) || object.kind_of?(Integer)
|
36
|
+
end
|
37
|
+
|
38
|
+
def authenticating_user?(target)
|
39
|
+
user.id.to_i == user(target).id.to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
def authorized_user?(target)
|
43
|
+
target_user = user(target)
|
44
|
+
!target_user.protected? || friendship?(user.id.to_i, target_user.id.to_i)
|
45
|
+
end
|
46
|
+
|
47
|
+
def instrument(operation, key, options = nil)
|
48
|
+
payload = {operation: operation, key: key}
|
49
|
+
payload.merge!(options) if options.is_a?(Hash)
|
50
|
+
ActiveSupport::Notifications.instrument('call.twitter_with_auto_pagination', payload) { yield(payload) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def call_old_method(method_name, *args)
|
54
|
+
options = args.extract_options!
|
55
|
+
begin
|
56
|
+
self.call_count += 1
|
57
|
+
_options = {method_name: method_name, call_count: self.call_count, args: args}.merge(options)
|
58
|
+
instrument('api call', args[0], _options) { send(method_name, *args, options) }
|
59
|
+
rescue Twitter::Error::TooManyRequests => e
|
60
|
+
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} Retry after #{e.rate_limit.reset_in} seconds."
|
61
|
+
raise e
|
62
|
+
rescue Twitter::Error::ServiceUnavailable => e
|
63
|
+
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
64
|
+
raise e
|
65
|
+
rescue Twitter::Error::InternalServerError => e
|
66
|
+
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
67
|
+
raise e
|
68
|
+
rescue Twitter::Error::Forbidden => e
|
69
|
+
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
70
|
+
raise e
|
71
|
+
rescue Twitter::Error::NotFound => e
|
72
|
+
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
73
|
+
raise e
|
74
|
+
rescue => e
|
75
|
+
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
76
|
+
raise e
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# user_timeline, search
|
81
|
+
def collect_with_max_id(method_name, *args)
|
82
|
+
options = args.extract_options!
|
83
|
+
call_limit = options.delete(:call_limit) || 3
|
84
|
+
last_response = call_old_method(method_name, *args, options)
|
85
|
+
last_response = yield(last_response) if block_given?
|
86
|
+
return_data = last_response
|
87
|
+
call_count = 1
|
88
|
+
|
89
|
+
while last_response.any? && call_count < call_limit
|
90
|
+
options[:max_id] = last_response.last.kind_of?(Hash) ? last_response.last[:id] : last_response.last.id
|
91
|
+
last_response = call_old_method(method_name, *args, options)
|
92
|
+
last_response = yield(last_response) if block_given?
|
93
|
+
return_data += last_response
|
94
|
+
call_count += 1
|
95
|
+
end
|
96
|
+
|
97
|
+
return_data.flatten
|
98
|
+
end
|
99
|
+
|
100
|
+
# friends, followers
|
101
|
+
def collect_with_cursor(method_name, *args)
|
102
|
+
options = args.extract_options!
|
103
|
+
last_response = call_old_method(method_name, *args, options).attrs
|
104
|
+
return_data = (last_response[:users] || last_response[:ids])
|
105
|
+
|
106
|
+
while (next_cursor = last_response[:next_cursor]) && next_cursor != 0
|
107
|
+
options[:cursor] = next_cursor
|
108
|
+
last_response = call_old_method(method_name, *args, options).attrs
|
109
|
+
return_data += (last_response[:users] || last_response[:ids])
|
110
|
+
end
|
111
|
+
|
112
|
+
return_data
|
113
|
+
end
|
114
|
+
|
115
|
+
require 'digest/md5'
|
116
|
+
|
117
|
+
def file_cache_key(method_name, user, options = {})
|
118
|
+
delim = ':'
|
119
|
+
identifier =
|
120
|
+
case
|
121
|
+
when method_name == :verify_credentials
|
122
|
+
"object-id#{delim}#{object_id}"
|
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
|
+
options.update(key: key)
|
289
|
+
|
290
|
+
data =
|
291
|
+
if options[:cache] == :read
|
292
|
+
instrument('Cache Read(Force)', key, caller: method_name) { cache.read(key) }
|
293
|
+
else
|
294
|
+
cache.fetch(key, expires_in: 1.hour, race_condition_ttl: 5.minutes) do
|
295
|
+
_d = yield
|
296
|
+
instrument('serialize', key, caller: method_name) { encode_json(_d, method_name, options) }
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
instrument('deserialize', key, caller: method_name) { decode_json(data, method_name, options) }
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe TwitterWithAutoPagination do
|
4
|
+
let(:config) {
|
5
|
+
{
|
6
|
+
consumer_key: 'CK',
|
7
|
+
consumer_secret: 'CS',
|
8
|
+
access_token: 'AT',
|
9
|
+
access_token_secret: 'ATS',
|
10
|
+
}
|
11
|
+
}
|
12
|
+
let(:client) { TwitterWithAutoPagination::Client.new(config) }
|
13
|
+
|
14
|
+
describe '#initialize' do
|
15
|
+
let(:default_call_count) { 0 }
|
16
|
+
|
17
|
+
it 'sets call_count to 0' do
|
18
|
+
expect(client.call_count).to eq(default_call_count)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'without params' do
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with params' do
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#logger' do
|
29
|
+
it 'has logger' do
|
30
|
+
expect(client.logger).to be_truthy
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#call_old_method' do
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#collect_with_max_id' do
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#collect_with_cursor' do
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#file_cache_key' do
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#namespaced_key' do
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#encode_json' do
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#decode_json' do
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#fetch_cache_or_call_api' do
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#user_timeline' do
|
59
|
+
it 'calls old_user_timeline' do
|
60
|
+
expect(client).to receive(:old_user_timeline)
|
61
|
+
client.user_timeline
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'calls collect_with_max_id' do
|
65
|
+
expect(client).to receive(:collect_with_max_id)
|
66
|
+
client.user_timeline
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#user_photos' do
|
71
|
+
it 'calls user_timeline' do
|
72
|
+
expect(client).to receive(:user_timeline)
|
73
|
+
client.user_photos
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#friends' do
|
78
|
+
it 'calls old_friends' do
|
79
|
+
expect(client).to receive(:old_friends)
|
80
|
+
client.friends
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'calls collect_with_cursor' do
|
84
|
+
expect(client).to receive(:collect_with_cursor)
|
85
|
+
client.friends
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#followers' do
|
90
|
+
it 'calls old_followers' do
|
91
|
+
expect(client).to receive(:old_followers)
|
92
|
+
client.followers
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'calls collect_with_cursor' do
|
96
|
+
expect(client).to receive(:collect_with_cursor)
|
97
|
+
client.followers
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#friend_ids' do
|
102
|
+
it 'calls old_friend_ids' do
|
103
|
+
expect(client).to receive(:old_friend_ids)
|
104
|
+
client.friend_ids
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'calls collect_with_cursor' do
|
108
|
+
expect(client).to receive(:collect_with_cursor)
|
109
|
+
client.friend_ids
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#follower_ids' do
|
114
|
+
it 'calls old_follower_ids' do
|
115
|
+
expect(client).to receive(:old_follower_ids)
|
116
|
+
client.follower_ids
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'calls collect_with_cursor' do
|
120
|
+
expect(client).to receive(:collect_with_cursor)
|
121
|
+
client.follower_ids
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '#users' do
|
126
|
+
it 'calls old_users' do
|
127
|
+
expect(client).to receive(:old_users)
|
128
|
+
client.users([1, 2, 3])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.add_dependency 'twitter'
|
6
|
+
spec.add_dependency 'activesupport'
|
7
|
+
spec.add_dependency 'hashie'
|
8
|
+
spec.add_dependency 'parallel'
|
9
|
+
|
10
|
+
spec.add_development_dependency 'bundler'
|
11
|
+
|
12
|
+
spec.authors = ['Shinohara Teruki']
|
13
|
+
spec.description = %q(Add auto paginate feature to Twitter gem.)
|
14
|
+
spec.email = %w[ts_3156@yahoo.co.jp]
|
15
|
+
spec.files = %w[LICENSE.md README.md Rakefile twitter_with_auto_pagination.gemspec]
|
16
|
+
spec.files += Dir.glob('lib/**/*.rb')
|
17
|
+
spec.files += Dir.glob('spec/**/*')
|
18
|
+
spec.homepage = 'http://github.com/ts-3156/twitter_with_auto_pagination/'
|
19
|
+
spec.licenses = %w[MIT]
|
20
|
+
spec.name = 'twitter_with_auto_pagination'
|
21
|
+
spec.require_paths = %w[lib]
|
22
|
+
spec.summary = spec.description
|
23
|
+
spec.test_files = Dir.glob('spec/**/*')
|
24
|
+
spec.version = '0.4.0'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: twitter_with_auto_pagination
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shinohara Teruki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: twitter
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: hashie
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: parallel
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Add auto paginate feature to Twitter gem.
|
84
|
+
email:
|
85
|
+
- ts_3156@yahoo.co.jp
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- LICENSE.md
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- lib/twitter_with_auto_pagination.rb
|
94
|
+
- lib/twitter_with_auto_pagination/client.rb
|
95
|
+
- lib/twitter_with_auto_pagination/existing_api.rb
|
96
|
+
- lib/twitter_with_auto_pagination/log_subscriber.rb
|
97
|
+
- lib/twitter_with_auto_pagination/new_api.rb
|
98
|
+
- lib/twitter_with_auto_pagination/utils.rb
|
99
|
+
- spec/helper.rb
|
100
|
+
- spec/twitter_with_auto_pagination_spec.rb
|
101
|
+
- twitter_with_auto_pagination.gemspec
|
102
|
+
homepage: http://github.com/ts-3156/twitter_with_auto_pagination/
|
103
|
+
licenses:
|
104
|
+
- MIT
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 2.5.1
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: Add auto paginate feature to Twitter gem.
|
126
|
+
test_files:
|
127
|
+
- spec/helper.rb
|
128
|
+
- spec/twitter_with_auto_pagination_spec.rb
|