twitter_friendly 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +41 -0
- data/Gemfile.lock +44 -5
- data/README.md +163 -11
- data/bin/test/friends_and_followers.rb +95 -0
- data/gemfiles/ar_4.2.gemfile +5 -0
- data/gemfiles/ar_5.1.gemfile +5 -0
- data/gemfiles/ar_5.2.gemfile +5 -0
- data/lib/twitter_friendly.rb +12 -0
- data/lib/twitter_friendly/cache.rb +48 -0
- data/lib/twitter_friendly/cache_key.rb +64 -0
- data/lib/twitter_friendly/caching.rb +86 -0
- data/lib/twitter_friendly/client.rb +52 -0
- data/lib/twitter_friendly/log_subscriber.rb +109 -0
- data/lib/twitter_friendly/logger.rb +18 -0
- data/lib/twitter_friendly/rate_limit.rb +81 -0
- data/lib/twitter_friendly/rest/api.rb +34 -0
- data/lib/twitter_friendly/rest/base.rb +32 -0
- data/lib/twitter_friendly/rest/collector.rb +78 -0
- data/lib/twitter_friendly/rest/favorites.rb +15 -0
- data/lib/twitter_friendly/rest/friends_and_followers.rb +64 -0
- data/lib/twitter_friendly/rest/lists.rb +22 -0
- data/lib/twitter_friendly/rest/parallel.rb +30 -0
- data/lib/twitter_friendly/rest/search.rb +16 -0
- data/lib/twitter_friendly/rest/timelines.rb +15 -0
- data/lib/twitter_friendly/rest/tweets.rb +13 -0
- data/lib/twitter_friendly/rest/users.rb +49 -0
- data/lib/twitter_friendly/rest/utils.rb +11 -0
- data/lib/twitter_friendly/serializer.rb +79 -0
- data/lib/twitter_friendly/utils.rb +6 -0
- data/lib/twitter_friendly/version.rb +1 -1
- data/twitter_friendly.gemspec +11 -2
- metadata +127 -10
data/lib/twitter_friendly.rb
CHANGED
@@ -1,4 +1,16 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'twitter'
|
4
|
+
|
1
5
|
require "twitter_friendly/version"
|
6
|
+
require "twitter_friendly/utils"
|
7
|
+
require "twitter_friendly/logger"
|
8
|
+
require "twitter_friendly/log_subscriber"
|
9
|
+
require "twitter_friendly/serializer"
|
10
|
+
require "twitter_friendly/cache_key"
|
11
|
+
require "twitter_friendly/cache"
|
12
|
+
require "twitter_friendly/rate_limit"
|
13
|
+
require "twitter_friendly/client"
|
2
14
|
|
3
15
|
module TwitterFriendly
|
4
16
|
class Error < StandardError; end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module TwitterFriendly
|
5
|
+
class Cache
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@client, :clear, :cleanup
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
options = args.extract_options!
|
11
|
+
|
12
|
+
path = options[:cache_dir] || File.join('.twitter_friendly', 'cache')
|
13
|
+
FileUtils.mkdir_p(path) unless File.exists?(path)
|
14
|
+
@client = ::ActiveSupport::Cache::FileStore.new(path, expires_in: 1.hour, race_condition_ttl: 5.minutes)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(method, user, options = {}, &block)
|
18
|
+
key = CacheKey.gen(method, user, options.except(:args))
|
19
|
+
super_operation = options[:args].length >= 2 && options[:args][1][:super_operation]
|
20
|
+
|
21
|
+
block_result = nil
|
22
|
+
blk =
|
23
|
+
Proc.new do
|
24
|
+
block_result = yield
|
25
|
+
encode(block_result, args: options[:args])
|
26
|
+
end
|
27
|
+
|
28
|
+
fetch_result =
|
29
|
+
if super_operation
|
30
|
+
@client.fetch(key, tf_super_operation: super_operation, &blk)
|
31
|
+
else
|
32
|
+
@client.fetch(key, &blk)
|
33
|
+
end
|
34
|
+
|
35
|
+
block_result ? block_result : decode(fetch_result, args: options[:args])
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def encode(obj, options)
|
41
|
+
Serializer.encode(obj, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def decode(str, options)
|
45
|
+
Serializer.decode(str, options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module TwitterFriendly
|
4
|
+
class CacheKey
|
5
|
+
DELIM = ':'
|
6
|
+
VERSION = '1'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def gen(method, user, options = {})
|
10
|
+
[version,
|
11
|
+
method,
|
12
|
+
method_identifier(method, user, options),
|
13
|
+
options_identifier(method, options)
|
14
|
+
].compact.join(DELIM)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def version
|
20
|
+
'v' + VERSION
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_identifier(method, user, options)
|
24
|
+
case
|
25
|
+
when method == :search then "query#{DELIM}#{user}"
|
26
|
+
when method == :friendship? then "from#{DELIM}#{user[0]}#{DELIM}to#{DELIM}#{user[1]}"
|
27
|
+
when method == :list_members then "list_id#{DELIM}#{user}"
|
28
|
+
when method == :collect_with_max_id then method_identifier(options[:super_operation], user, options)
|
29
|
+
when method == :collect_with_cursor then method_identifier(options[:super_operation], user, options)
|
30
|
+
when user.nil? && options[:hash].present? then "token-hash#{DELIM}#{options[:hash]}"
|
31
|
+
else user_identifier(user)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def user_identifier(user)
|
36
|
+
case
|
37
|
+
when user.kind_of?(Integer) then "id#{DELIM}#{user}"
|
38
|
+
when user.kind_of?(String) then "screen_name#{DELIM}#{user}"
|
39
|
+
when user.kind_of?(Array) && user.empty? then 'The_#users_is_called_with_an_empty_array'
|
40
|
+
when user.kind_of?(Array) && user[0].kind_of?(Integer) then "ids#{DELIM}#{user.size}-#{hexdigest(user)}"
|
41
|
+
when user.kind_of?(Array) && user[0].kind_of?(String) then "screen_names#{DELIM}#{user.size}-#{hexdigest(user)}"
|
42
|
+
else raise "#{__method__}: No matches #{user.inspect}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def options_identifier(method, options)
|
47
|
+
# TODO 内部的な値はすべてprefix _tf_ をつける
|
48
|
+
opt = options.except(:hash, :call_count, :call_limit, :super_operation, :super_super_operation, :recursive, :parallel)
|
49
|
+
opt[:in] = options[:super_operation] if %i(collect_with_max_id collect_with_cursor).include?(method)
|
50
|
+
|
51
|
+
if opt.empty?
|
52
|
+
nil
|
53
|
+
else
|
54
|
+
str = opt.map {|k, v| "#{k}=#{v}"}.join('&')
|
55
|
+
"options#{DELIM}#{str}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def hexdigest(ary)
|
60
|
+
Digest::MD5.hexdigest(ary.join(','))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module Caching
|
3
|
+
%i(
|
4
|
+
verify_credentials
|
5
|
+
user?
|
6
|
+
user
|
7
|
+
users
|
8
|
+
blocked_ids
|
9
|
+
friendship?
|
10
|
+
).each do |name|
|
11
|
+
define_method(name) do |*args|
|
12
|
+
options = args.extract_options!
|
13
|
+
Instrumenter.start_processing(name, options)
|
14
|
+
|
15
|
+
Instrumenter.complete_processing(name, options) do
|
16
|
+
do_request =
|
17
|
+
Proc.new {Instrumenter.perform_request(name, options) {options.empty? ? super(*args) : super(*args, options)}}
|
18
|
+
|
19
|
+
if Utils.cache_disabled?(options)
|
20
|
+
do_request.call
|
21
|
+
else
|
22
|
+
user = (name == :friendship?) ? args[0, 2] : args[0]
|
23
|
+
@cache.fetch(name, user, options.merge(args: [name, options], hash: credentials_hash), &do_request)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# これらのメソッドは、内部で呼ぶメソッドをキャッシュするため、
|
30
|
+
# 全体をキャッシュすることはない。
|
31
|
+
%i(
|
32
|
+
friend_ids
|
33
|
+
follower_ids
|
34
|
+
friends
|
35
|
+
followers
|
36
|
+
friend_ids_and_follower_ids
|
37
|
+
friends_and_followers
|
38
|
+
search
|
39
|
+
favorites
|
40
|
+
home_timeline
|
41
|
+
user_timeline
|
42
|
+
mentions_timeline
|
43
|
+
memberships
|
44
|
+
list_members
|
45
|
+
retweeters_ids
|
46
|
+
).each do |name|
|
47
|
+
define_method(name) do |*args|
|
48
|
+
options = args.extract_options!
|
49
|
+
Instrumenter.start_processing(name, options)
|
50
|
+
|
51
|
+
Instrumenter.complete_processing(name, options) do
|
52
|
+
options.empty? ? super(*args) : super(*args, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module Instrumenter
|
58
|
+
|
59
|
+
module_function
|
60
|
+
|
61
|
+
def start_processing(operation, options)
|
62
|
+
payload = {operation: operation}.merge(options)
|
63
|
+
::ActiveSupport::Notifications.instrument('start_processing.twitter_friendly', payload) {}
|
64
|
+
end
|
65
|
+
|
66
|
+
def complete_processing(operation, options)
|
67
|
+
payload = {operation: operation}.merge(options)
|
68
|
+
::ActiveSupport::Notifications.instrument('complete_processing.twitter_friendly', payload) { yield(payload) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def perform_request(caller, options, &block)
|
72
|
+
payload = {operation: 'request', args: [caller, options]}
|
73
|
+
::ActiveSupport::Notifications.instrument('request.twitter_friendly', payload) { yield(payload) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module Utils
|
78
|
+
|
79
|
+
module_function
|
80
|
+
|
81
|
+
def cache_disabled?(options)
|
82
|
+
options.is_a?(Hash) && options.has_key?(:cache) && !options[:cache]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
require 'twitter_friendly/rest/api'
|
4
|
+
|
5
|
+
module TwitterFriendly
|
6
|
+
class Client
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@twitter, :access_token, :access_token_secret, :consumer_key, :consumer_secret
|
9
|
+
|
10
|
+
include TwitterFriendly::REST::API
|
11
|
+
include TwitterFriendly::RateLimit
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
options = args.extract_options!
|
15
|
+
|
16
|
+
@logger = TwitterFriendly::Logger.new(options)
|
17
|
+
|
18
|
+
unless subscriber_attached?
|
19
|
+
if @logger.level == ::Logger::DEBUG
|
20
|
+
@@subscriber_attached = true
|
21
|
+
TwitterFriendly::Logging.logger = @logger
|
22
|
+
TwitterFriendly::TFLogSubscriber.attach_to :twitter_friendly
|
23
|
+
TwitterFriendly::ASLogSubscriber.attach_to :active_support
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@cache = TwitterFriendly::Cache.new(options)
|
28
|
+
@twitter = Twitter::REST::Client.new(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def cache
|
32
|
+
@cache
|
33
|
+
end
|
34
|
+
|
35
|
+
def logger
|
36
|
+
@logger
|
37
|
+
end
|
38
|
+
|
39
|
+
def internal_client
|
40
|
+
@twitter
|
41
|
+
end
|
42
|
+
|
43
|
+
def subscriber_attached?
|
44
|
+
@@subscriber_attached ||= false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def cache(cache_dir = nil)
|
49
|
+
TwitterFriendly::Cache.new(cache_dir: cache_dir)
|
50
|
+
end
|
51
|
+
module_function :cache
|
52
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module Logging
|
3
|
+
def truncated_payload(payload)
|
4
|
+
return payload.inspect if !payload.has_key?(:args) || !payload[:args].is_a?(Array) || payload[:args].empty? || !payload[:args][0].is_a?(Array)
|
5
|
+
|
6
|
+
args = payload[:args].dup
|
7
|
+
args[0] =
|
8
|
+
if args[0].size > 3
|
9
|
+
"[#{args[0].take(3).join(', ')} ... #{args[0].size}]"
|
10
|
+
else
|
11
|
+
args[0].inspect
|
12
|
+
end
|
13
|
+
|
14
|
+
{args: args}.merge(payload.except(:args)).inspect
|
15
|
+
end
|
16
|
+
|
17
|
+
def nested_indent(payload)
|
18
|
+
(payload[:super_operation] ? ' ' : '') + (payload[:super_super_operation] ? ' ' : '') + (payload[:tf_super_operation] ? ' ' : '')
|
19
|
+
end
|
20
|
+
|
21
|
+
module_function
|
22
|
+
|
23
|
+
def logger
|
24
|
+
@@logger
|
25
|
+
end
|
26
|
+
|
27
|
+
def logger=(logger)
|
28
|
+
@@logger = logger
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class TFLogSubscriber < ::ActiveSupport::LogSubscriber
|
33
|
+
include Logging
|
34
|
+
|
35
|
+
def start_processing(event)
|
36
|
+
payload = event.payload
|
37
|
+
name = "#{nested_indent(payload)}TF::Started #{payload.delete(:operation)}"
|
38
|
+
debug do
|
39
|
+
if payload[:super_operation]
|
40
|
+
"#{name} in #{payload[:super_operation]} at #{Time.now}"
|
41
|
+
else
|
42
|
+
"#{name} at #{Time.now}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def complete_processing(event)
|
48
|
+
payload = event.payload
|
49
|
+
name = "TF::Completed #{payload.delete(:operation)} in #{event.duration.round(1)}ms"
|
50
|
+
debug do
|
51
|
+
"#{nested_indent(payload)}#{name}#{" #{truncated_payload(payload)}" unless payload.empty?}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def twitter_friendly_any(event)
|
56
|
+
payload = event.payload
|
57
|
+
payload.delete(:name)
|
58
|
+
operation = payload.delete(:operation)
|
59
|
+
name =
|
60
|
+
if operation.to_sym == :collect
|
61
|
+
" TW::#{operation.capitalize} #{payload[:args].last[:super_operation]} in #{payload[:args][0]} (#{event.duration.round(1)}ms)"
|
62
|
+
else
|
63
|
+
" TW::#{operation.capitalize} #{payload[:args][0]} (#{event.duration.round(1)}ms)"
|
64
|
+
end
|
65
|
+
c =
|
66
|
+
if %i(encode decode).include?(operation.to_sym)
|
67
|
+
YELLOW
|
68
|
+
elsif %i(collect).include?(operation.to_sym)
|
69
|
+
BLUE
|
70
|
+
else
|
71
|
+
CYAN
|
72
|
+
end
|
73
|
+
name = color(name, c, true)
|
74
|
+
debug { " #{nested_indent(payload)}#{name}#{" #{truncated_payload(payload)}" unless payload.empty?}" }
|
75
|
+
end
|
76
|
+
|
77
|
+
%w(request encode decode collect).each do |operation|
|
78
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
79
|
+
def #{operation}(event)
|
80
|
+
event.payload[:name] = '#{operation}'
|
81
|
+
twitter_friendly_any(event)
|
82
|
+
end
|
83
|
+
METHOD
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class ASLogSubscriber < ::ActiveSupport::LogSubscriber
|
88
|
+
include Logging
|
89
|
+
|
90
|
+
def cache_any(event)
|
91
|
+
payload = event.payload
|
92
|
+
operation = payload[:super_operation] == :fetch ? :fetch : payload[:name]
|
93
|
+
hit = %i(read fetch).include?(operation.to_sym) && payload[:hit]
|
94
|
+
name = " AS::#{operation.capitalize}#{' (Hit)' if hit} #{payload[:key].split(':')[1]} (#{event.duration.round(1)}ms)"
|
95
|
+
name = color(name, MAGENTA, true)
|
96
|
+
debug { "#{nested_indent(payload)}#{name} #{(payload.except(:name, :expires_in, :super_operation, :hit, :race_condition_ttl, :tf_super_operation).inspect)}" }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Ignore generate and fetch_hit
|
100
|
+
%w(read write delete exist?).each do |operation|
|
101
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
102
|
+
def cache_#{operation}(event)
|
103
|
+
event.payload[:name] = '#{operation}'
|
104
|
+
cache_any(event)
|
105
|
+
end
|
106
|
+
METHOD
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module TwitterFriendly
|
6
|
+
class Logger
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@logger, :debug, :info, :warn, :level
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
path = options[:log_dir] || File.join('.twitter_friendly')
|
12
|
+
FileUtils.mkdir_p(path) unless File.exists?(path)
|
13
|
+
|
14
|
+
@logger = ::Logger.new(File.join(path, 'twitter_friendly.log'))
|
15
|
+
@logger.level = options[:log_level] || :debug
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module RateLimit
|
3
|
+
def rate_limit
|
4
|
+
RateLimit.new(perform_get('/1.1/application/rate_limit_status.json'))
|
5
|
+
rescue => e
|
6
|
+
logger.warn e.inspect
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def perform_get(*args)
|
13
|
+
internal_client.send(:perform_get, *args)
|
14
|
+
end
|
15
|
+
|
16
|
+
class RateLimit
|
17
|
+
def initialize(status)
|
18
|
+
@status = status
|
19
|
+
end
|
20
|
+
|
21
|
+
def resources
|
22
|
+
@status[:resources]
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify_credentials
|
26
|
+
extract_remaining_and_reset_in(resources[:account][:'/account/verify_credentials'])
|
27
|
+
end
|
28
|
+
|
29
|
+
def friend_ids
|
30
|
+
extract_remaining_and_reset_in(resources[:friends][:'/friends/ids'])
|
31
|
+
end
|
32
|
+
|
33
|
+
def follower_ids
|
34
|
+
extract_remaining_and_reset_in(resources[:followers][:'/followers/ids'])
|
35
|
+
end
|
36
|
+
|
37
|
+
def users
|
38
|
+
extract_remaining_and_reset_in(resources[:users][:'/users/lookup'])
|
39
|
+
end
|
40
|
+
|
41
|
+
def friends(parallel: true)
|
42
|
+
if parallel
|
43
|
+
{friend_ids: friend_ids, users: users}
|
44
|
+
else
|
45
|
+
extract_remaining_and_reset_in(resources[:friends][:'/friends/list'])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def followers(parallel: true)
|
50
|
+
if parallel
|
51
|
+
{follower_ids: follower_ids, users: users}
|
52
|
+
else
|
53
|
+
extract_remaining_and_reset_in(resources[:followers][:'/followers/list'])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_h
|
58
|
+
{
|
59
|
+
verify_credentials: verify_credentials,
|
60
|
+
friend_ids: friend_ids,
|
61
|
+
follower_ids: follower_ids,
|
62
|
+
users: users
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
'verify_credentials ' + verify_credentials.inspect +
|
68
|
+
' friend_ids ' + friend_ids.inspect +
|
69
|
+
' follower_ids ' + follower_ids.inspect +
|
70
|
+
' users ' + users.inspect
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def extract_remaining_and_reset_in(limit)
|
76
|
+
{remaining: limit[:remaining], reset_in: (Time.at(limit[:reset]) - Time.now).round}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|