twitter_friendly 0.1.0 → 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.
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activesupport", "~> 4.2.10"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activesupport", "~> 5.1.5"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activesupport", "~> 5.2.1"
4
+
5
+ gemspec path: "../"
@@ -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
+