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
data/lib/ex_twitter.rb
CHANGED
@@ -1,806 +1,2 @@
|
|
1
|
-
|
2
|
-
require 'active_support/cache'
|
3
|
-
require 'active_support/core_ext/string'
|
4
|
-
|
5
|
-
require 'ex_twitter_subscriber'
|
6
|
-
|
7
|
-
require 'twitter'
|
8
|
-
require 'hashie'
|
9
|
-
require 'memoist'
|
10
|
-
require 'parallel'
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
class ExTwitter < Twitter::REST::Client
|
15
|
-
extend Memoist
|
16
|
-
|
17
|
-
def initialize(options = {})
|
18
|
-
api_cache_prefix = options.has_key?(:api_cache_prefix) ? options.delete(:api_cache_prefix) : '%Y%m%d%H'
|
19
|
-
@cache = ActiveSupport::Cache::FileStore.new(File.join('tmp', 'api_cache', Time.now.strftime(api_cache_prefix)))
|
20
|
-
@uid = options[:uid]
|
21
|
-
@screen_name = options[:screen_name]
|
22
|
-
@authenticated_user = Hashie::Mash.new({uid: options[:uid].to_i, screen_name: options[:screen_name]})
|
23
|
-
@call_count = 0
|
24
|
-
ExTwitterSubscriber.attach_to :ex_twitter
|
25
|
-
super
|
26
|
-
end
|
27
|
-
|
28
|
-
attr_accessor :call_count
|
29
|
-
attr_reader :cache, :authenticated_user
|
30
|
-
|
31
|
-
INDENT = 4
|
32
|
-
|
33
|
-
# for backward compatibility
|
34
|
-
def uid
|
35
|
-
@uid
|
36
|
-
end
|
37
|
-
|
38
|
-
def __uid
|
39
|
-
@uid
|
40
|
-
end
|
41
|
-
|
42
|
-
def __uid_i
|
43
|
-
@uid.to_i
|
44
|
-
end
|
45
|
-
|
46
|
-
# for backward compatibility
|
47
|
-
def screen_name
|
48
|
-
@screen_name
|
49
|
-
end
|
50
|
-
|
51
|
-
def __screen_name
|
52
|
-
@screen_name
|
53
|
-
end
|
54
|
-
|
55
|
-
def logger
|
56
|
-
Dir.mkdir('log') unless File.exists?('log')
|
57
|
-
@logger ||= Logger.new('log/ex_twitter.log')
|
58
|
-
end
|
59
|
-
|
60
|
-
def instrument(operation)
|
61
|
-
ActiveSupport::Notifications.instrument('call.ex_twitter', name: operation) do
|
62
|
-
yield
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def call_old_method(method_name, *args)
|
67
|
-
options = args.extract_options!
|
68
|
-
begin
|
69
|
-
start_t = Time.now
|
70
|
-
result = send(method_name, *args, options); self.call_count += 1
|
71
|
-
end_t = Time.now
|
72
|
-
logger.debug "#{method_name} #{args.inspect} #{options.inspect} (#{end_t - start_t}s)".indent(INDENT)
|
73
|
-
result
|
74
|
-
rescue Twitter::Error::TooManyRequests => e
|
75
|
-
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} Retry after #{e.rate_limit.reset_in} seconds."
|
76
|
-
raise e
|
77
|
-
rescue Twitter::Error::ServiceUnavailable => e
|
78
|
-
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
79
|
-
raise e
|
80
|
-
rescue Twitter::Error::InternalServerError => e
|
81
|
-
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
82
|
-
raise e
|
83
|
-
rescue Twitter::Error::Forbidden => e
|
84
|
-
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
85
|
-
raise e
|
86
|
-
rescue Twitter::Error::NotFound => e
|
87
|
-
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
88
|
-
raise e
|
89
|
-
rescue => e
|
90
|
-
logger.warn "#{__method__}: call=#{method_name} #{args.inspect} #{e.class} #{e.message}"
|
91
|
-
raise e
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# user_timeline, search
|
96
|
-
def collect_with_max_id(method_name, *args)
|
97
|
-
options = args.extract_options!
|
98
|
-
options[:call_count] = 3 unless options.has_key?(:call_count)
|
99
|
-
last_response = call_old_method(method_name, *args, options)
|
100
|
-
last_response = yield(last_response) if block_given?
|
101
|
-
return_data = last_response
|
102
|
-
call_count = 1
|
103
|
-
|
104
|
-
while last_response.any? && call_count < options[:call_count]
|
105
|
-
options[:max_id] = last_response.last.kind_of?(Hash) ? last_response.last[:id] : last_response.last.id
|
106
|
-
last_response = call_old_method(method_name, *args, options)
|
107
|
-
last_response = yield(last_response) if block_given?
|
108
|
-
return_data += last_response
|
109
|
-
call_count += 1
|
110
|
-
end
|
111
|
-
|
112
|
-
return_data.flatten
|
113
|
-
end
|
114
|
-
|
115
|
-
# friends, followers
|
116
|
-
def collect_with_cursor(method_name, *args)
|
117
|
-
options = args.extract_options!
|
118
|
-
last_response = call_old_method(method_name, *args, options).attrs
|
119
|
-
return_data = (last_response[:users] || last_response[:ids])
|
120
|
-
|
121
|
-
while (next_cursor = last_response[:next_cursor]) && next_cursor != 0
|
122
|
-
options[:cursor] = next_cursor
|
123
|
-
last_response = call_old_method(method_name, *args, options).attrs
|
124
|
-
return_data += (last_response[:users] || last_response[:ids])
|
125
|
-
end
|
126
|
-
|
127
|
-
return_data
|
128
|
-
end
|
129
|
-
|
130
|
-
require 'digest/md5'
|
131
|
-
|
132
|
-
# currently ignore options
|
133
|
-
def file_cache_key(method_name, user)
|
134
|
-
delim = ':'
|
135
|
-
identifier =
|
136
|
-
case
|
137
|
-
when method_name == :search
|
138
|
-
"str#{delim}#{user.to_s}"
|
139
|
-
when method_name == :mentions_timeline
|
140
|
-
"myself#{delim}#{user.to_s}"
|
141
|
-
when method_name == :home_timeline
|
142
|
-
"myself#{delim}#{user.to_s}"
|
143
|
-
when user.kind_of?(Integer)
|
144
|
-
"id#{delim}#{user.to_s}"
|
145
|
-
when user.kind_of?(Array) && user.first.kind_of?(Integer)
|
146
|
-
"ids#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
|
147
|
-
when user.kind_of?(Array) && user.first.kind_of?(String)
|
148
|
-
"sns#{delim}#{Digest::MD5.hexdigest(user.join(','))}"
|
149
|
-
when user.kind_of?(String)
|
150
|
-
"sn#{delim}#{user}"
|
151
|
-
when user.kind_of?(Twitter::User)
|
152
|
-
"user#{delim}#{user.id.to_s}"
|
153
|
-
else raise "#{method_name.inspect} #{user.inspect}"
|
154
|
-
end
|
155
|
-
|
156
|
-
"#{method_name}#{delim}#{identifier}"
|
157
|
-
end
|
158
|
-
|
159
|
-
def namespaced_key(method_name, user)
|
160
|
-
file_cache_key(method_name, user)
|
161
|
-
end
|
162
|
-
|
163
|
-
PROFILE_SAVE_KEYS = %i(
|
164
|
-
id
|
165
|
-
name
|
166
|
-
screen_name
|
167
|
-
location
|
168
|
-
description
|
169
|
-
url
|
170
|
-
protected
|
171
|
-
followers_count
|
172
|
-
friends_count
|
173
|
-
listed_count
|
174
|
-
favourites_count
|
175
|
-
utc_offset
|
176
|
-
time_zone
|
177
|
-
geo_enabled
|
178
|
-
verified
|
179
|
-
statuses_count
|
180
|
-
lang
|
181
|
-
status
|
182
|
-
profile_image_url_https
|
183
|
-
profile_banner_url
|
184
|
-
profile_link_color
|
185
|
-
suspended
|
186
|
-
verified
|
187
|
-
entities
|
188
|
-
created_at
|
189
|
-
)
|
190
|
-
|
191
|
-
STATUS_SAVE_KEYS = %i(
|
192
|
-
created_at
|
193
|
-
id
|
194
|
-
text
|
195
|
-
source
|
196
|
-
truncated
|
197
|
-
coordinates
|
198
|
-
place
|
199
|
-
entities
|
200
|
-
user
|
201
|
-
contributors
|
202
|
-
is_quote_status
|
203
|
-
retweet_count
|
204
|
-
favorite_count
|
205
|
-
favorited
|
206
|
-
retweeted
|
207
|
-
possibly_sensitive
|
208
|
-
lang
|
209
|
-
)
|
210
|
-
|
211
|
-
# encode
|
212
|
-
def encode_json(obj, caller_name, options = {})
|
213
|
-
options[:reduce] = true unless options.has_key?(:reduce)
|
214
|
-
start_t = Time.now
|
215
|
-
result =
|
216
|
-
case caller_name
|
217
|
-
when :user_timeline, :home_timeline, :mentions_timeline, :favorites # Twitter::Tweet
|
218
|
-
JSON.pretty_generate(obj.map { |o| o.attrs })
|
219
|
-
|
220
|
-
when :search # Hash
|
221
|
-
data =
|
222
|
-
if options[:reduce]
|
223
|
-
obj.map { |o| o.to_hash.slice(*STATUS_SAVE_KEYS) }
|
224
|
-
else
|
225
|
-
obj.map { |o| o.to_hash }
|
226
|
-
end
|
227
|
-
JSON.pretty_generate(data)
|
228
|
-
|
229
|
-
when :friends, :followers # Hash
|
230
|
-
data =
|
231
|
-
if options[:reduce]
|
232
|
-
obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
|
233
|
-
else
|
234
|
-
obj.map { |o| o.to_hash }
|
235
|
-
end
|
236
|
-
JSON.pretty_generate(data)
|
237
|
-
|
238
|
-
when :friend_ids, :follower_ids # Integer
|
239
|
-
JSON.pretty_generate(obj)
|
240
|
-
|
241
|
-
when :user # Twitter::User
|
242
|
-
JSON.pretty_generate(obj.to_hash.slice(*PROFILE_SAVE_KEYS))
|
243
|
-
|
244
|
-
when :users, :friends_advanced, :followers_advanced # Twitter::User
|
245
|
-
data =
|
246
|
-
if options[:reduce]
|
247
|
-
obj.map { |o| o.to_hash.slice(*PROFILE_SAVE_KEYS) }
|
248
|
-
else
|
249
|
-
obj.map { |o| o.to_hash }
|
250
|
-
end
|
251
|
-
JSON.pretty_generate(data)
|
252
|
-
|
253
|
-
when :user? # true or false
|
254
|
-
obj
|
255
|
-
|
256
|
-
when :friendship? # true or false
|
257
|
-
obj
|
258
|
-
|
259
|
-
else
|
260
|
-
raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
|
261
|
-
end
|
262
|
-
end_t = Time.now
|
263
|
-
logger.debug "#{__method__}: caller=#{caller_name} key=#{options[:key]} (#{end_t - start_t}s)".indent(INDENT)
|
264
|
-
result
|
265
|
-
end
|
266
|
-
|
267
|
-
# decode
|
268
|
-
def decode_json(json_str, caller_name, options = {})
|
269
|
-
start_t = Time.now
|
270
|
-
obj = json_str.kind_of?(String) ? JSON.parse(json_str) : json_str
|
271
|
-
result =
|
272
|
-
case
|
273
|
-
when obj.nil?
|
274
|
-
obj
|
275
|
-
|
276
|
-
when obj.kind_of?(Array) && obj.first.kind_of?(Hash)
|
277
|
-
obj.map { |o| Hashie::Mash.new(o) }
|
278
|
-
|
279
|
-
when obj.kind_of?(Array) && obj.first.kind_of?(Integer)
|
280
|
-
obj
|
281
|
-
|
282
|
-
when obj.kind_of?(Hash)
|
283
|
-
Hashie::Mash.new(obj)
|
284
|
-
|
285
|
-
when obj === true || obj === false
|
286
|
-
obj
|
287
|
-
|
288
|
-
when obj.kind_of?(Array) && obj.empty?
|
289
|
-
obj
|
290
|
-
|
291
|
-
else
|
292
|
-
raise "#{__method__}: caller=#{caller_name} key=#{options[:key]} obj=#{obj.inspect}"
|
293
|
-
end
|
294
|
-
end_t = Time.now
|
295
|
-
logger.debug "#{__method__}: caller=#{caller_name} key=#{options[:key]} (#{end_t - start_t}s)".indent(INDENT)
|
296
|
-
result
|
297
|
-
end
|
298
|
-
|
299
|
-
def fetch_cache_or_call_api(method_name, user, options = {})
|
300
|
-
start_t = Time.now
|
301
|
-
key = namespaced_key(method_name, user)
|
302
|
-
options.update(key: key)
|
303
|
-
|
304
|
-
logger.debug "#{__method__}: caller=#{method_name} key=#{key}"
|
305
|
-
|
306
|
-
data =
|
307
|
-
if options[:cache] == :read
|
308
|
-
hit = 'force read'
|
309
|
-
cache.read(key)
|
310
|
-
else
|
311
|
-
if block_given?
|
312
|
-
hit = 'fetch(hit)'
|
313
|
-
cache.fetch(key, expires_in: 1.hour, race_condition_ttl: 5.minutes) do
|
314
|
-
hit = 'fetch(not hit)'
|
315
|
-
instrument(:encode_json) { encode_json(yield, method_name, options) }
|
316
|
-
end
|
317
|
-
else
|
318
|
-
if cache.exist?(key)
|
319
|
-
hit = 'read(hit)'
|
320
|
-
cache.read(key)
|
321
|
-
else
|
322
|
-
hit = 'read(not hit)'
|
323
|
-
nil
|
324
|
-
end
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
|
-
result = decode_json(data, method_name, options)
|
329
|
-
logger.debug "#{__method__}: caller=#{method_name} key=#{key} #{hit} (#{Time.now - start_t}s)"
|
330
|
-
result
|
331
|
-
end
|
332
|
-
|
333
|
-
alias :old_friendship? :friendship?
|
334
|
-
def friendship?(*args)
|
335
|
-
options = args.extract_options!
|
336
|
-
fetch_cache_or_call_api(:friendship?, args) {
|
337
|
-
call_old_method(:old_friendship?, *args, options)
|
338
|
-
}
|
339
|
-
end
|
340
|
-
|
341
|
-
alias :old_user? :user?
|
342
|
-
def user?(*args)
|
343
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
344
|
-
options = args.extract_options!
|
345
|
-
fetch_cache_or_call_api(:user?, args[0], options) {
|
346
|
-
call_old_method(:old_user?, args[0], options)
|
347
|
-
}
|
348
|
-
end
|
349
|
-
|
350
|
-
alias :old_user :user
|
351
|
-
def user(*args)
|
352
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
353
|
-
options = args.extract_options!
|
354
|
-
fetch_cache_or_call_api(:user, args[0], options) {
|
355
|
-
call_old_method(:old_user, args[0], options)
|
356
|
-
}
|
357
|
-
rescue => e
|
358
|
-
logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
|
359
|
-
raise e
|
360
|
-
end
|
361
|
-
# memoize :user
|
362
|
-
|
363
|
-
alias :old_friend_ids :friend_ids
|
364
|
-
def friend_ids(*args)
|
365
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
366
|
-
options = args.extract_options!
|
367
|
-
fetch_cache_or_call_api(:friend_ids, args[0], options) {
|
368
|
-
options = {count: 5000, cursor: -1}.merge(options)
|
369
|
-
collect_with_cursor(:old_friend_ids, *args, options)
|
370
|
-
}
|
371
|
-
end
|
372
|
-
|
373
|
-
alias :old_follower_ids :follower_ids
|
374
|
-
def follower_ids(*args)
|
375
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
376
|
-
options = args.extract_options!
|
377
|
-
fetch_cache_or_call_api(:follower_ids, args[0], options) {
|
378
|
-
options = {count: 5000, cursor: -1}.merge(options)
|
379
|
-
collect_with_cursor(:old_follower_ids, *args, options)
|
380
|
-
}
|
381
|
-
end
|
382
|
-
|
383
|
-
# specify reduce: false to use tweet for inactive_*
|
384
|
-
alias :old_friends :friends
|
385
|
-
def friends(*args)
|
386
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
387
|
-
options = args.extract_options!
|
388
|
-
options[:reduce] = false unless options.has_key?(:reduce)
|
389
|
-
fetch_cache_or_call_api(:friends, args[0], options) {
|
390
|
-
options = {count: 200, include_user_entities: true, cursor: -1}.merge(options)
|
391
|
-
collect_with_cursor(:old_friends, *args, options)
|
392
|
-
}
|
393
|
-
end
|
394
|
-
# memoize :friends
|
395
|
-
|
396
|
-
def friends_advanced(*args)
|
397
|
-
options = args.extract_options!
|
398
|
-
_friend_ids = friend_ids(*(args + [options]))
|
399
|
-
users(_friend_ids.map { |id| id.to_i }, options)
|
400
|
-
end
|
401
|
-
|
402
|
-
# specify reduce: false to use tweet for inactive_*
|
403
|
-
alias :old_followers :followers
|
404
|
-
def followers(*args)
|
405
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
406
|
-
options = args.extract_options!
|
407
|
-
options[:reduce] = false unless options.has_key?(:reduce)
|
408
|
-
fetch_cache_or_call_api(:followers, args[0], options) {
|
409
|
-
options = {count: 200, include_user_entities: true, cursor: -1}.merge(options)
|
410
|
-
collect_with_cursor(:old_followers, *args, options)
|
411
|
-
}
|
412
|
-
end
|
413
|
-
# memoize :followers
|
414
|
-
|
415
|
-
def followers_advanced(*args)
|
416
|
-
options = args.extract_options!
|
417
|
-
_follower_ids = follower_ids(*(args + [options]))
|
418
|
-
users(_follower_ids.map { |id| id.to_i }, options)
|
419
|
-
end
|
420
|
-
|
421
|
-
def fetch_parallelly(signatures) # [{method: :friends, args: ['ts_3156', ...], {...}]
|
422
|
-
logger.debug "#{__method__} #{signatures.inspect}"
|
423
|
-
result = Array.new(signatures.size)
|
424
|
-
|
425
|
-
Parallel.each_with_index(signatures, in_threads: result.size) do |signature, i|
|
426
|
-
result[i] = send(signature[:method], *signature[:args])
|
427
|
-
end
|
428
|
-
|
429
|
-
result
|
430
|
-
end
|
431
|
-
|
432
|
-
def friends_and_followers(*args)
|
433
|
-
fetch_parallelly(
|
434
|
-
[
|
435
|
-
{method: 'friends_advanced', args: args},
|
436
|
-
{method: 'followers_advanced', args: args}])
|
437
|
-
end
|
438
|
-
|
439
|
-
def friends_followers_and_statuses(*args)
|
440
|
-
fetch_parallelly(
|
441
|
-
[
|
442
|
-
{method: 'friends_advanced', args: args},
|
443
|
-
{method: 'followers_advanced', args: args},
|
444
|
-
{method: 'user_timeline', args: args}])
|
445
|
-
end
|
446
|
-
|
447
|
-
def one_sided_following(me)
|
448
|
-
me.friends.to_a - me.followers.to_a
|
449
|
-
end
|
450
|
-
|
451
|
-
def one_sided_followers(me)
|
452
|
-
me.followers.to_a - me.friends.to_a
|
453
|
-
end
|
454
|
-
|
455
|
-
def mutual_friends(me)
|
456
|
-
me.friends.to_a & me.followers.to_a
|
457
|
-
end
|
458
|
-
|
459
|
-
def common_friends(me, you)
|
460
|
-
me.friends.to_a & you.friends.to_a
|
461
|
-
end
|
462
|
-
|
463
|
-
def common_followers(me, you)
|
464
|
-
me.followers.to_a & you.followers.to_a
|
465
|
-
end
|
466
|
-
|
467
|
-
def removing(pre_me, cur_me)
|
468
|
-
pre_me.friends.to_a - cur_me.friends.to_a
|
469
|
-
end
|
470
|
-
|
471
|
-
def removed(pre_me, cur_me)
|
472
|
-
pre_me.followers.to_a - cur_me.followers.to_a
|
473
|
-
end
|
474
|
-
|
475
|
-
# use compact, not use sort and uniq
|
476
|
-
# specify reduce: false to use tweet for inactive_*
|
477
|
-
alias :old_users :users
|
478
|
-
def users(*args)
|
479
|
-
options = args.extract_options!
|
480
|
-
options[:reduce] = false
|
481
|
-
users_per_workers = args.first.compact.each_slice(100).to_a
|
482
|
-
processed_users = []
|
483
|
-
|
484
|
-
Parallel.each_with_index(users_per_workers, in_threads: [users_per_workers.size, 10].min) do |users_per_worker, i|
|
485
|
-
_users = fetch_cache_or_call_api(:users, users_per_worker, options) {
|
486
|
-
call_old_method(:old_users, users_per_worker, options)
|
487
|
-
}
|
488
|
-
|
489
|
-
result = {i: i, users: _users}
|
490
|
-
processed_users << result
|
491
|
-
end
|
492
|
-
|
493
|
-
processed_users.sort_by{|p| p[:i] }.map{|p| p[:users] }.flatten.compact
|
494
|
-
rescue => e
|
495
|
-
logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
|
496
|
-
raise e
|
497
|
-
end
|
498
|
-
|
499
|
-
def called_by_authenticated_user?(user)
|
500
|
-
authenticated_user = self.old_user; self.call_count += 1
|
501
|
-
if user.kind_of?(String)
|
502
|
-
authenticated_user.screen_name == user
|
503
|
-
elsif user.kind_of?(Integer)
|
504
|
-
authenticated_user.id.to_i == user
|
505
|
-
else
|
506
|
-
raise user.inspect
|
507
|
-
end
|
508
|
-
rescue => e
|
509
|
-
logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
|
510
|
-
raise e
|
511
|
-
end
|
512
|
-
|
513
|
-
# can't get tweets if you are not authenticated by specified user
|
514
|
-
alias :old_home_timeline :home_timeline
|
515
|
-
def home_timeline(*args)
|
516
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
517
|
-
raise 'this method must be called by authenticated user' unless called_by_authenticated_user?(args[0])
|
518
|
-
options = args.extract_options!
|
519
|
-
fetch_cache_or_call_api(:home_timeline, args[0], options) {
|
520
|
-
options = {count: 200, include_rts: true, call_count: 3}.merge(options)
|
521
|
-
collect_with_max_id(:old_home_timeline, options)
|
522
|
-
}
|
523
|
-
end
|
524
|
-
|
525
|
-
# can't get tweets if you are not authenticated by specified user
|
526
|
-
alias :old_user_timeline :user_timeline
|
527
|
-
def user_timeline(*args)
|
528
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
529
|
-
options = args.extract_options!
|
530
|
-
fetch_cache_or_call_api(:user_timeline, args[0], options) {
|
531
|
-
options = {count: 200, include_rts: true, call_count: 3}.merge(options)
|
532
|
-
collect_with_max_id(:old_user_timeline, *args, options)
|
533
|
-
}
|
534
|
-
end
|
535
|
-
|
536
|
-
# can't get tweets if you are not authenticated by specified user
|
537
|
-
alias :old_mentions_timeline :mentions_timeline
|
538
|
-
def mentions_timeline(*args)
|
539
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
540
|
-
raise 'this method must be called by authenticated user' unless called_by_authenticated_user?(args[0])
|
541
|
-
options = args.extract_options!
|
542
|
-
fetch_cache_or_call_api(:mentions_timeline, args[0], options) {
|
543
|
-
options = {count: 200, include_rts: true, call_count: 1}.merge(options)
|
544
|
-
collect_with_max_id(:old_mentions_timeline, options)
|
545
|
-
}
|
546
|
-
rescue => e
|
547
|
-
logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
|
548
|
-
raise e
|
549
|
-
end
|
550
|
-
|
551
|
-
def select_screen_names_replied(tweets, options = {})
|
552
|
-
result = tweets.map do |t|
|
553
|
-
$1 if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
|
554
|
-
end.compact
|
555
|
-
(options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq
|
556
|
-
end
|
557
|
-
|
558
|
-
# users which specified user is replying
|
559
|
-
# in_reply_to_user_id and in_reply_to_status_id is not used because of distinguishing mentions from replies
|
560
|
-
def replying(user, options = {})
|
561
|
-
tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user, options)
|
562
|
-
screen_names = select_screen_names_replied(tweets, options)
|
563
|
-
users(screen_names, options)
|
564
|
-
rescue Twitter::Error::NotFound => e
|
565
|
-
e.message == 'No user matches for specified terms.' ? [] : (raise e)
|
566
|
-
rescue => e
|
567
|
-
logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
|
568
|
-
raise e
|
569
|
-
end
|
570
|
-
|
571
|
-
alias :old_search :search
|
572
|
-
def search(*args)
|
573
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
574
|
-
options = args.extract_options!
|
575
|
-
options[:reduce] = false
|
576
|
-
fetch_cache_or_call_api(:search, args[0], options) {
|
577
|
-
options = {count: 100, result_type: :recent, call_count: 1}.merge(options)
|
578
|
-
collect_with_max_id(:old_search, *args, options) { |response| response.attrs[:statuses] }
|
579
|
-
}
|
580
|
-
rescue => e
|
581
|
-
logger.warn "#{__method__} #{args.inspect} #{e.class} #{e.message}"
|
582
|
-
raise e
|
583
|
-
end
|
584
|
-
|
585
|
-
def select_uids_replying_to(tweets, options)
|
586
|
-
result = tweets.map do |t|
|
587
|
-
t.user.id.to_i if t.text =~ /^(?:\.)?@(\w+)( |\W)/ # include statuses starts with .
|
588
|
-
end.compact
|
589
|
-
(options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq
|
590
|
-
end
|
591
|
-
|
592
|
-
def select_replied_from_search(tweets, options = {})
|
593
|
-
return [] if tweets.empty?
|
594
|
-
uids = select_uids_replying_to(tweets, options)
|
595
|
-
uids.map { |u| tweets.find { |t| t.user.id.to_i == u.to_i } }.map { |t| t.user }
|
596
|
-
end
|
597
|
-
|
598
|
-
# users which specified user is replied
|
599
|
-
# when user is login you had better to call mentions_timeline
|
600
|
-
def replied(_user, options = {})
|
601
|
-
user = self.user(_user, options)
|
602
|
-
if user.id.to_i == __uid_i
|
603
|
-
mentions_timeline(__uid_i, options).uniq { |m| m.user.id }.map { |m| m.user }
|
604
|
-
else
|
605
|
-
select_replied_from_search(search('@' + user.screen_name, options))
|
606
|
-
end
|
607
|
-
rescue => e
|
608
|
-
logger.warn "#{__method__} #{_user.inspect} #{e.class} #{e.message}"
|
609
|
-
raise e
|
610
|
-
end
|
611
|
-
|
612
|
-
def select_inactive_users(users, options = {})
|
613
|
-
options[:authorized] = false unless options.has_key?(:authorized)
|
614
|
-
two_weeks_ago = 2.weeks.ago.to_i
|
615
|
-
users.select do |u|
|
616
|
-
if options[:authorized] || !u.protected
|
617
|
-
(Time.parse(u.status.created_at).to_i < two_weeks_ago) rescue false
|
618
|
-
else
|
619
|
-
false
|
620
|
-
end
|
621
|
-
end
|
622
|
-
end
|
623
|
-
|
624
|
-
def inactive_friends(user)
|
625
|
-
select_inactive_users(friends_advanced(user))
|
626
|
-
end
|
627
|
-
|
628
|
-
def inactive_followers(user)
|
629
|
-
select_inactive_users(followers_advanced(user))
|
630
|
-
end
|
631
|
-
|
632
|
-
def clusters_belong_to(text)
|
633
|
-
return [] if text.blank?
|
634
|
-
|
635
|
-
exclude_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_bad_words_path']))
|
636
|
-
special_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_good_words_path']))
|
637
|
-
|
638
|
-
# クラスタ用の単語の出現回数を記録
|
639
|
-
cluster_word_counter =
|
640
|
-
special_words.map { |sw| [sw, text.scan(sw)] }
|
641
|
-
.delete_if { |item| item[1].empty? }
|
642
|
-
.each_with_object(Hash.new(1)) { |item, memo| memo[item[0]] = item[1].size }
|
643
|
-
|
644
|
-
# 同一文字種の繰り返しを見付ける。漢字の繰り返し、ひらがなの繰り返し、カタカナの繰り返し、など
|
645
|
-
text.scan(/[一-龠〆ヵヶ々]+|[ぁ-んー~]+|[ァ-ヴー~]+|[a-zA-Z0-9]+|[、。!!??]+/).
|
646
|
-
|
647
|
-
# 複数回繰り返される文字を除去
|
648
|
-
map { |w| w.remove /[?!?!。、w]|(ー{2,})/ }.
|
649
|
-
|
650
|
-
# 文字数の少なすぎる単語、ひらがなだけの単語、除外単語を除去する
|
651
|
-
delete_if { |w| w.length <= 1 || (w.length <= 2 && w =~ /^[ぁ-んー~]+$/) || exclude_words.include?(w) }.
|
652
|
-
|
653
|
-
# 出現回数を記録
|
654
|
-
each { |w| cluster_word_counter[w] += 1 }
|
655
|
-
|
656
|
-
# 複数個以上見付かった単語のみを残し、出現頻度順にソート
|
657
|
-
cluster_word_counter.select { |_, v| v > 3 }.sort_by { |_, v| -v }.to_h
|
658
|
-
end
|
659
|
-
|
660
|
-
def clusters_assigned_to
|
661
|
-
|
662
|
-
end
|
663
|
-
|
664
|
-
def usage_stats_wday_series_data(times)
|
665
|
-
wday_count = times.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |time, memo|
|
666
|
-
memo[time.wday] += 1
|
667
|
-
end
|
668
|
-
wday_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
|
669
|
-
{name: key, y: value, drilldown: key}
|
670
|
-
end
|
671
|
-
end
|
672
|
-
|
673
|
-
def usage_stats_wday_drilldown_series(times)
|
674
|
-
hour_count =
|
675
|
-
(0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
|
676
|
-
wday_memo[wday] =
|
677
|
-
times.select { |t| t.wday == wday }.map { |t| t.hour }.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |hour, hour_memo|
|
678
|
-
hour_memo[hour] += 1
|
679
|
-
end
|
680
|
-
end
|
681
|
-
hour_count.map { |k, v| [I18n.t('date.abbr_day_names')[k], v] }.map do |key, value|
|
682
|
-
{name: key, id: key, data: value.to_a.map{|a| [a[0].to_s, a[1]] }}
|
683
|
-
end
|
684
|
-
end
|
685
|
-
|
686
|
-
def usage_stats_hour_series_data(times)
|
687
|
-
hour_count = times.each_with_object((0..23).map { |n| [n, 0] }.to_h) do |time, memo|
|
688
|
-
memo[time.hour] += 1
|
689
|
-
end
|
690
|
-
hour_count.map do |key, value|
|
691
|
-
{name: key.to_s, y: value, drilldown: key.to_s}
|
692
|
-
end
|
693
|
-
end
|
694
|
-
|
695
|
-
def usage_stats_hour_drilldown_series(times)
|
696
|
-
wday_count =
|
697
|
-
(0..23).each_with_object((0..23).map { |n| [n, nil] }.to_h) do |hour, hour_memo|
|
698
|
-
hour_memo[hour] =
|
699
|
-
times.select { |t| t.hour == hour }.map { |t| t.wday }.each_with_object((0..6).map { |n| [n, 0] }.to_h) do |wday, wday_memo|
|
700
|
-
wday_memo[wday] += 1
|
701
|
-
end
|
702
|
-
end
|
703
|
-
wday_count.map do |key, value|
|
704
|
-
{name: key.to_s, id: key.to_s, data: value.to_a.map{|a| [I18n.t('date.abbr_day_names')[a[0]], a[1]] }}
|
705
|
-
end
|
706
|
-
end
|
707
|
-
|
708
|
-
def twitter_addiction_series(times)
|
709
|
-
five_mins = 5.minutes
|
710
|
-
wday_expended_seconds =
|
711
|
-
(0..6).each_with_object((0..6).map { |n| [n, nil] }.to_h) do |wday, wday_memo|
|
712
|
-
target_times = times.select { |t| t.wday == wday }
|
713
|
-
wday_memo[wday] = target_times.empty? ? nil : target_times.each_cons(2).map {|a, b| (a - b) < five_mins ? a - b : five_mins }.sum
|
714
|
-
end
|
715
|
-
days = times.map{|t| t.to_date.to_s(:long) }.uniq.size
|
716
|
-
weeks = (days > 7) ? days / 7.0 : 1.0
|
717
|
-
wday_expended_seconds.map { |k, v| [I18n.t('date.abbr_day_names')[k], (v.nil? ? nil : v / weeks / 60)] }.map do |key, value|
|
718
|
-
{name: key, y: value}
|
719
|
-
end
|
720
|
-
end
|
721
|
-
|
722
|
-
def usage_stats(user, options = {})
|
723
|
-
n_days_ago = options.has_key?(:days) ? options[:days].days.ago : 100.years.ago
|
724
|
-
tweets = options.has_key?(:tweets) ? options.delete(:tweets) : user_timeline(user)
|
725
|
-
times =
|
726
|
-
# TODO Use user specific time zone
|
727
|
-
tweets.map { |t| ActiveSupport::TimeZone['Tokyo'].parse(t.created_at.to_s) }.
|
728
|
-
select { |t| t > n_days_ago }
|
729
|
-
[
|
730
|
-
usage_stats_wday_series_data(times),
|
731
|
-
usage_stats_wday_drilldown_series(times),
|
732
|
-
usage_stats_hour_series_data(times),
|
733
|
-
usage_stats_hour_drilldown_series(times),
|
734
|
-
twitter_addiction_series(times)
|
735
|
-
]
|
736
|
-
end
|
737
|
-
|
738
|
-
|
739
|
-
alias :old_favorites :favorites
|
740
|
-
def favorites(*args)
|
741
|
-
raise 'this method needs at least one param to use cache' if args.empty?
|
742
|
-
options = args.extract_options!
|
743
|
-
fetch_cache_or_call_api(:favorites, args[0], options) {
|
744
|
-
options = {count: 100, call_count: 1}.merge(options)
|
745
|
-
collect_with_max_id(:old_favorites, *args, options)
|
746
|
-
}
|
747
|
-
end
|
748
|
-
|
749
|
-
def calc_scores_from_users(users, options)
|
750
|
-
min = options.has_key?(:min) ? options[:min] : 0
|
751
|
-
max = options.has_key?(:max) ? options[:max] : 1000
|
752
|
-
users.each_with_object(Hash.new(0)) { |u, memo| memo[u.id] += 1 }.
|
753
|
-
select { |_k, v| min <= v && v <= max }.
|
754
|
-
sort_by { |_, v| -v }.to_h
|
755
|
-
end
|
756
|
-
|
757
|
-
def calc_scores_from_tweets(tweets, options = {})
|
758
|
-
calc_scores_from_users(tweets.map { |t| t.user }, options)
|
759
|
-
end
|
760
|
-
|
761
|
-
def select_favoriting_from_favs(favs, options = {})
|
762
|
-
return [] if favs.empty?
|
763
|
-
uids = calc_scores_from_tweets(favs)
|
764
|
-
result = uids.map { |uid, score| f = favs.
|
765
|
-
find { |f| f.user.id.to_i == uid.to_i }; Array.new(score, f) }.flatten.map { |f| f.user }
|
766
|
-
(options.has_key?(:uniq) && !options[:uniq]) ? result : result.uniq { |u| u.id }
|
767
|
-
end
|
768
|
-
|
769
|
-
def favoriting(user, options= {})
|
770
|
-
favs = options.has_key?(:favorites) ? options.delete(:favorites) : favorites(user, options)
|
771
|
-
select_favoriting_from_favs(favs, options)
|
772
|
-
rescue => e
|
773
|
-
logger.warn "#{__method__} #{user.inspect} #{e.class} #{e.message}"
|
774
|
-
raise e
|
775
|
-
end
|
776
|
-
|
777
|
-
def favorited_by(user)
|
778
|
-
end
|
779
|
-
|
780
|
-
def close_friends(_uid, options = {})
|
781
|
-
min = options.has_key?(:min) ? options[:min] : 0
|
782
|
-
max = options.has_key?(:max) ? options[:max] : 1000
|
783
|
-
uid_i = _uid.to_i
|
784
|
-
_replying = options.has_key?(:replying) ? options.delete(:replying) : replying(uid_i, options)
|
785
|
-
_replied = options.has_key?(:replied) ? options.delete(:replied) : replied(uid_i, options)
|
786
|
-
_favoriting = options.has_key?(:favoriting) ? options.delete(:favoriting) : favoriting(uid_i, options)
|
787
|
-
|
788
|
-
min_max = {min: min, max: max}
|
789
|
-
_users = _replying + _replied + _favoriting
|
790
|
-
return [] if _users.empty?
|
791
|
-
|
792
|
-
scores = calc_scores_from_users(_users, min_max)
|
793
|
-
replying_scores = calc_scores_from_users(_replying, min_max)
|
794
|
-
replied_scores = calc_scores_from_users(_replied, min_max)
|
795
|
-
favoriting_scores = calc_scores_from_users(_favoriting, min_max)
|
796
|
-
|
797
|
-
scores.keys.map { |uid| _users.find { |u| u.id.to_i == uid.to_i } }.
|
798
|
-
map do |u|
|
799
|
-
u[:score] = scores[u.id]
|
800
|
-
u[:replying_score] = replying_scores[u.id]
|
801
|
-
u[:replied_score] = replied_scores[u.id]
|
802
|
-
u[:favoriting_score] = favoriting_scores[u.id]
|
803
|
-
u
|
804
|
-
end
|
805
|
-
end
|
1
|
+
module ExTwitter
|
806
2
|
end
|