ex_twitter 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +0 -8
- data/ex_twitter.gemspec +1 -1
- data/lib/ex_twitter/client.rb +194 -0
- data/lib/ex_twitter/existing_api.rb +122 -0
- data/lib/ex_twitter/log_subscriber.rb +79 -0
- data/lib/ex_twitter/new_api.rb +238 -0
- data/lib/ex_twitter/utils.rb +293 -0
- data/lib/ex_twitter.rb +1 -805
- metadata +7 -4
- data/lib/ex_stream.rb +0 -68
- data/lib/ex_twitter_subscriber.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fae8a4c6b259764373fb52ca806eb66f8c38a472
|
4
|
+
data.tar.gz: b2c3d986eb8ed42504f00ae2841b8dab8e302436
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7c77240b80287e4d73935813b8185658eeac95b7ccaa93540a72ac01ec3b2cac751c12098cda953ff4b2bd0fc8cbcbb8d8f87f078be941845b6982d1e02fdaf
|
7
|
+
data.tar.gz: 28a1dc726814e7e503b6d179945ef214b3b025e6cfe0650c56e54855f0f46c057d11e68a10b4e30ef2edd74aeb07c5fb4d6c6f5b69af2e0ce3fff101d19578ce
|
data/README.md
CHANGED
data/ex_twitter.gemspec
CHANGED
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/cache'
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
|
5
|
+
require 'ex_twitter/log_subscriber'
|
6
|
+
require 'ex_twitter/utils'
|
7
|
+
require 'ex_twitter/existing_api'
|
8
|
+
require 'ex_twitter/new_api'
|
9
|
+
|
10
|
+
require 'twitter'
|
11
|
+
require 'hashie'
|
12
|
+
require 'parallel'
|
13
|
+
|
14
|
+
module ExTwitter
|
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
|
+
ExTwitter::LogSubscriber.attach_to :ex_twitter
|
21
|
+
ExTwitter::LogSubscriber.attach_to :active_support
|
22
|
+
@@logger = @logger =
|
23
|
+
if options[:logger]
|
24
|
+
options.delete(:logger)
|
25
|
+
else
|
26
|
+
Dir.mkdir('log') unless File.exists?('log')
|
27
|
+
Logger.new('log/ex_twitter.log')
|
28
|
+
end
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.logger
|
34
|
+
@@logger
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :call_count
|
38
|
+
attr_reader :cache, :authenticated_user, :logger
|
39
|
+
|
40
|
+
INDENT = 4
|
41
|
+
|
42
|
+
include ExTwitter::Utils
|
43
|
+
|
44
|
+
alias :old_friendship? :friendship?
|
45
|
+
alias :old_user? :user?
|
46
|
+
alias :old_user :user
|
47
|
+
alias :old_friend_ids :friend_ids
|
48
|
+
alias :old_follower_ids :follower_ids
|
49
|
+
alias :old_friends :friends
|
50
|
+
alias :old_followers :followers
|
51
|
+
alias :old_users :users
|
52
|
+
alias :old_home_timeline :home_timeline
|
53
|
+
alias :old_user_timeline :user_timeline
|
54
|
+
alias :old_mentions_timeline :mentions_timeline
|
55
|
+
alias :old_favorites :favorites
|
56
|
+
alias :old_search :search
|
57
|
+
|
58
|
+
include ExTwitter::ExistingApi
|
59
|
+
include ExTwitter::NewApi
|
60
|
+
|
61
|
+
def usage_stats_wday_series_data(times)
|
62
|
+
wday_count = times.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |time, memo|
|
63
|
+
memo[time.wday] += 1
|
64
|
+
end
|
65
|
+
wday_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
|
66
|
+
{name: key, y: value, drilldown: key}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def usage_stats_wday_drilldown_series(times)
|
71
|
+
hour_count =
|
72
|
+
(0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
|
73
|
+
wday_memo[wday] =
|
74
|
+
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|
|
75
|
+
hour_memo[hour] += 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
hour_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
|
79
|
+
{name: key, id: key, data: value.to_a.map{|a| [a[0].to_s, a[1]] }}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def usage_stats_hour_series_data(times)
|
84
|
+
hour_count = times.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |time, memo|
|
85
|
+
memo[time.hour] += 1
|
86
|
+
end
|
87
|
+
hour_count.map do |key, value|
|
88
|
+
{name: key.to_s, y: value, drilldown: key.to_s}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def usage_stats_hour_drilldown_series(times)
|
93
|
+
wday_count =
|
94
|
+
(0..23).each_with_object((0..23).map { |n| [n, nil] }.to_h) do |hour, hour_memo|
|
95
|
+
hour_memo[hour] =
|
96
|
+
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|
|
97
|
+
wday_memo[wday] += 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
wday_count.map do |key, value|
|
101
|
+
{name: key.to_s, id: key.to_s, data: value.to_a.map{|a| [I18n.t('date.abbr_day_names')[a[0]], a[1]] }}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def twitter_addiction_series(times)
|
106
|
+
five_mins = 5.minutes
|
107
|
+
wday_expended_seconds =
|
108
|
+
(0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
|
109
|
+
target_times = times.select { |t| t.wday == wday }
|
110
|
+
wday_memo[wday] = target_times.empty? ? nil : target_times.each_cons(2).map {|a, b| (a - b) < five_mins ? a - b : five_mins }.sum
|
111
|
+
end
|
112
|
+
days = times.map{|t| t.to_date.to_s(:long) }.uniq.size
|
113
|
+
weeks = (days > 7) ? days / 7.0 : 1.0
|
114
|
+
wday_expended_seconds.map { |k, v| [I18n.t('date.abbr_day_names')[k], (v.nil? ? nil : v / weeks / 60)] }.map do |key, value|
|
115
|
+
{name: key, y: value}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def usage_stats(user, options = {})
|
120
|
+
n_days_ago = options.has_key?(:days) ? options[:days].days.ago : 100.years.ago
|
121
|
+
tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user)
|
122
|
+
times =
|
123
|
+
# TODO Use user specific time zone
|
124
|
+
tweets.map { |t| ActiveSupport::TimeZone['Tokyo'].parse(t.created_at.to_s) }.
|
125
|
+
select { |t| t > n_days_ago }
|
126
|
+
[
|
127
|
+
usage_stats_wday_series_data(times),
|
128
|
+
usage_stats_wday_drilldown_series(times),
|
129
|
+
usage_stats_hour_series_data(times),
|
130
|
+
usage_stats_hour_drilldown_series(times),
|
131
|
+
twitter_addiction_series(times)
|
132
|
+
]
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def calc_scores_from_users(users, options)
|
137
|
+
min = options.has_key?(:min) ? options[:min] : 0
|
138
|
+
max = options.has_key?(:max) ? options[:max] : 1000
|
139
|
+
users.each_with_object(Hash.new(0)) { |u, memo| memo[u.id] += 1 }.
|
140
|
+
select { |_k, v| min <= v && v <= max }.
|
141
|
+
sort_by { |_, v| -v }.to_h
|
142
|
+
end
|
143
|
+
|
144
|
+
def calc_scores_from_tweets(tweets, options = {})
|
145
|
+
calc_scores_from_users(tweets.map { |t| t.user }, options)
|
146
|
+
end
|
147
|
+
|
148
|
+
def select_favoriting_from_favs(favs, options = {})
|
149
|
+
return [] if favs.empty?
|
150
|
+
uids = calc_scores_from_tweets(favs)
|
151
|
+
result = uids.map { |uid, score| f = favs.
|
152
|
+
find { |f| f.user.id.to_i == uid.to_i }; Array.new(score, f) }.flatten.map { |f| f.user }
|
153
|
+
(options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq { |u| u.id }
|
154
|
+
end
|
155
|
+
|
156
|
+
def favoriting(user, options= {})
|
157
|
+
favs = options.has_key?(:favorites) ? options.delete(:favorites) : favorites(user, options)
|
158
|
+
select_favoriting_from_favs(favs, options)
|
159
|
+
rescue => e
|
160
|
+
logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
|
161
|
+
raise e
|
162
|
+
end
|
163
|
+
|
164
|
+
def favorited_by(user)
|
165
|
+
end
|
166
|
+
|
167
|
+
def close_friends(_uid, options = {})
|
168
|
+
min = options.has_key?(:min) ? options[:min] : 0
|
169
|
+
max = options.has_key?(:max) ? options[:max] : 1000
|
170
|
+
uid_i = _uid.to_i
|
171
|
+
_replying = options.has_key?(:replying) ? options.delete(:replying) : replying(uid_i, options)
|
172
|
+
_replied = options.has_key?(:replied) ? options.delete(:replied) : replied(uid_i, options)
|
173
|
+
_favoriting = options.has_key?(:favoriting) ? options.delete(:favoriting) : favoriting(uid_i, options)
|
174
|
+
|
175
|
+
min_max = {min: min, max: max}
|
176
|
+
_users = _replying + _replied + _favoriting
|
177
|
+
return [] if _users.empty?
|
178
|
+
|
179
|
+
scores = calc_scores_from_users(_users, min_max)
|
180
|
+
replying_scores = calc_scores_from_users(_replying, min_max)
|
181
|
+
replied_scores = calc_scores_from_users(_replied, min_max)
|
182
|
+
favoriting_scores = calc_scores_from_users(_favoriting, min_max)
|
183
|
+
|
184
|
+
scores.keys.map { |uid| _users.find { |u| u.id.to_i == uid.to_i } }.
|
185
|
+
map do |u|
|
186
|
+
u[:score] = scores[u.id]
|
187
|
+
u[:replying_score] = replying_scores[u.id]
|
188
|
+
u[:replied_score] = replied_scores[u.id]
|
189
|
+
u[:favoriting_score] = favoriting_scores[u.id]
|
190
|
+
u
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module ExTwitter
|
2
|
+
module ExistingApi
|
3
|
+
def friendship?(*args)
|
4
|
+
options = 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 user?(*args)
|
11
|
+
options = args.extract_options!
|
12
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
13
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
14
|
+
call_old_method("old_#{__method__}", args[0], options)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def user(*args)
|
19
|
+
options = args.extract_options!
|
20
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
21
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
22
|
+
call_old_method("old_#{__method__}", args[0], options)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def friend_ids(*args)
|
27
|
+
options = {count: 5000, cursor: -1}.merge(args.extract_options!)
|
28
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
29
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
30
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def follower_ids(*args)
|
35
|
+
options = {count: 5000, cursor: -1}.merge(args.extract_options!)
|
36
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
37
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
38
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# specify reduce: false to use tweet for inactive_*
|
43
|
+
def friends(*args)
|
44
|
+
options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
|
45
|
+
options[:reduce] = false unless options.has_key?(:reduce)
|
46
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
47
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
48
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
# specify reduce: false to use tweet for inactive_*
|
53
|
+
def followers(*args)
|
54
|
+
options = {count: 200, include_user_entities: true, cursor: -1}.merge(args.extract_options!)
|
55
|
+
options[:reduce] = false unless options.has_key?(:reduce)
|
56
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
57
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
58
|
+
collect_with_cursor("old_#{__method__}", *args, options)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
# use compact, not use sort and uniq
|
63
|
+
# specify reduce: false to use tweet for inactive_*
|
64
|
+
def users(*args)
|
65
|
+
options = args.extract_options!
|
66
|
+
options[:reduce] = false
|
67
|
+
users_per_workers = args.first.compact.each_slice(100).to_a
|
68
|
+
processed_users = []
|
69
|
+
|
70
|
+
Parallel.each_with_index(users_per_workers, in_threads: [users_per_workers.size, 10].min) do |users_per_worker, i|
|
71
|
+
_users = fetch_cache_or_call_api(__method__, users_per_worker, options) {
|
72
|
+
call_old_method("old_#{__method__}", users_per_worker, options)
|
73
|
+
}
|
74
|
+
|
75
|
+
processed_users << {i: i, users: _users}
|
76
|
+
end
|
77
|
+
|
78
|
+
processed_users.sort_by{|p| p[:i] }.map{|p| p[:users] }.flatten.compact
|
79
|
+
rescue => e # debug
|
80
|
+
logger.warn "#{__method__}: #{args.inspect} #{e.class} #{e.message}"
|
81
|
+
raise e
|
82
|
+
end
|
83
|
+
|
84
|
+
def home_timeline(*args)
|
85
|
+
options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
|
86
|
+
fetch_cache_or_call_api(__method__, user.screen_name, options) {
|
87
|
+
collect_with_max_id("old_#{__method__}", options)
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def user_timeline(*args)
|
92
|
+
options = {count: 200, include_rts: true, call_limit: 3}.merge(args.extract_options!)
|
93
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
94
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
95
|
+
collect_with_max_id("old_#{__method__}", *args, options)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def mentions_timeline(*args)
|
100
|
+
options = {count: 200, include_rts: true, call_limit: 1}.merge(args.extract_options!)
|
101
|
+
fetch_cache_or_call_api(__method__, user.screen_name, options) {
|
102
|
+
collect_with_max_id("old_#{__method__}", options)
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def favorites(*args)
|
107
|
+
options = {count: 100, call_count: 1}.merge(args.extract_options!)
|
108
|
+
args[0] = verify_credentials(skip_status: true).id if args.empty?
|
109
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
110
|
+
collect_with_max_id("old_#{__method__}", *args, options)
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def search(*args)
|
115
|
+
options = {count: 100, result_type: :recent, call_limit: 1}.merge(args.extract_options!)
|
116
|
+
options[:reduce] = false
|
117
|
+
fetch_cache_or_call_api(__method__, args[0], options) {
|
118
|
+
collect_with_max_id("old_#{__method__}", *args, options) { |response| response.attrs[:statuses] }
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
|
4
|
+
|
5
|
+
module ExTwitter
|
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
|
+
ExTwitter::Client.logger
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
module ExTwitter
|
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, options = {})
|
114
|
+
result = tweets.map do |t|
|
115
|
+
$1 if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
|
116
|
+
end.compact
|
117
|
+
(options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq
|
118
|
+
end
|
119
|
+
|
120
|
+
# users which specified user is replying
|
121
|
+
# in_reply_to_user_id and in_reply_to_status_id is not used because of distinguishing mentions from replies
|
122
|
+
def replying(*args)
|
123
|
+
options = args.extract_options!
|
124
|
+
tweets =
|
125
|
+
if args.empty?
|
126
|
+
user_timeline(user.screen_name, options)
|
127
|
+
elsif uid_or_screen_name?(args[0])
|
128
|
+
user_timeline(args[0], options)
|
129
|
+
else
|
130
|
+
raise
|
131
|
+
end
|
132
|
+
screen_names = _extract_screen_names(tweets, options)
|
133
|
+
users(screen_names, {super_operation: __method__}.merge(options))
|
134
|
+
rescue Twitter::Error::NotFound => e
|
135
|
+
e.message == 'No user matches for specified terms.' ? [] : (raise e)
|
136
|
+
rescue => e
|
137
|
+
logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
|
138
|
+
raise e
|
139
|
+
end
|
140
|
+
|
141
|
+
def _extract_uids(tweets, options)
|
142
|
+
result = tweets.map do |t|
|
143
|
+
t.user.id.to_i if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
|
144
|
+
end.compact
|
145
|
+
(options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq
|
146
|
+
end
|
147
|
+
|
148
|
+
def _extract_users(tweets, uids, options = {})
|
149
|
+
uids.map { |u| tweets.find { |t| t.user.id.to_i == u.to_i } }.map { |t| t.user }
|
150
|
+
end
|
151
|
+
|
152
|
+
# users which specified user is replied
|
153
|
+
# when user is login you had better to call mentions_timeline
|
154
|
+
def replied(*args)
|
155
|
+
options = args.extract_options!
|
156
|
+
|
157
|
+
if args.empty? || (uid_or_screen_name?(args[0]) && authenticating_user?(args[0]))
|
158
|
+
mentions_timeline.uniq { |m| m.user.id }.map { |m| m.user }
|
159
|
+
else
|
160
|
+
searched_result = search('@' + user(args[0]).screen_name, options)
|
161
|
+
uids = _extract_uids(searched_result, options)
|
162
|
+
_extract_users(searched_result, uids, options)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def _extract_inactive_users(users, options = {})
|
167
|
+
authorized = options.delete(:authorized)
|
168
|
+
two_weeks_ago = 2.weeks.ago.to_i
|
169
|
+
users.select do |u|
|
170
|
+
if authorized
|
171
|
+
(Time.parse(u.status.created_at).to_i < two_weeks_ago) rescue false
|
172
|
+
else
|
173
|
+
false
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def inactive_friends(user = nil)
|
179
|
+
if user.blank?
|
180
|
+
_extract_inactive_users(friends_parallelly, authorized: true)
|
181
|
+
elsif uid_or_screen_name?(user)
|
182
|
+
authorized = authenticating_user?(user) || authorized_user?(user)
|
183
|
+
_extract_inactive_users(friends_parallelly(user), authorized: authorized)
|
184
|
+
elsif user.respond_to?(:friends)
|
185
|
+
authorized = authenticating_user?(user.uid.to_i) || authorized_user?(user.uid.to_i)
|
186
|
+
_extract_inactive_users(user.friends, authorized: authorized)
|
187
|
+
else
|
188
|
+
raise
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def inactive_followers(user = nil)
|
193
|
+
if user.blank?
|
194
|
+
_extract_inactive_users(followers_parallelly, authorized: true)
|
195
|
+
elsif uid_or_screen_name?(user)
|
196
|
+
authorized = authenticating_user?(user) || authorized_user?(user)
|
197
|
+
_extract_inactive_users(followers_parallelly(user), authorized: authorized)
|
198
|
+
elsif user.respond_to?(:followers)
|
199
|
+
authorized = authenticating_user?(user.uid.to_i) || authorized_user?(user.uid.to_i)
|
200
|
+
_extract_inactive_users(user.followers, authorized: authorized)
|
201
|
+
else
|
202
|
+
raise
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def clusters_belong_to(text)
|
207
|
+
return [] if text.blank?
|
208
|
+
|
209
|
+
exclude_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_bad_words_path']))
|
210
|
+
special_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_good_words_path']))
|
211
|
+
|
212
|
+
# クラスタ用の単語の出現回数を記録
|
213
|
+
cluster_word_counter =
|
214
|
+
special_words.map { |sw| [sw, text.scan(sw)] }
|
215
|
+
.delete_if { |item| item[1].empty? }
|
216
|
+
.each_with_object(Hash.new(1)) { |item, memo| memo[item[0]] = item[1].size }
|
217
|
+
|
218
|
+
# 同一文字種の繰り返しを見付ける。漢字の繰り返し、ひらがなの繰り返し、カタカナの繰り返し、など
|
219
|
+
text.scan(/[一-龠〆ヵヶ々]+|[ぁ-んー~]+|[ァ-ヴー~]+|[a-zA-Z0-9]+|[、。!!??]+/).
|
220
|
+
|
221
|
+
# 複数回繰り返される文字を除去
|
222
|
+
map { |w| w.remove /[?!?!。、w]|(ー{2,})/ }.
|
223
|
+
|
224
|
+
# 文字数の少なすぎる単語、ひらがなだけの単語、除外単語を除去する
|
225
|
+
delete_if { |w| w.length <= 1 || (w.length <= 2 && w =~ /^[ぁ-んー~]+$/) || exclude_words.include?(w) }.
|
226
|
+
|
227
|
+
# 出現回数を記録
|
228
|
+
each { |w| cluster_word_counter[w] += 1 }
|
229
|
+
|
230
|
+
# 複数個以上見付かった単語のみを残し、出現頻度順にソート
|
231
|
+
cluster_word_counter.select { |_, v| v > 3 }.sort_by { |_, v| -v }.to_h
|
232
|
+
end
|
233
|
+
|
234
|
+
def clusters_assigned_to
|
235
|
+
raise NotImplementedError.new
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|