twitter_friendly 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd3aedc3bbfeb41ae92a95b3ca8a4a654c03d1332540396d85ff883871456f8d
4
- data.tar.gz: 2554445ef8a39dce0be467d006f9f1fea19aa6ce265a0612e71350f27aca3007
3
+ metadata.gz: 01532d5c3cc176a719499e7680cb7df3127c45a4f745ece3f05d703158aa4dcc
4
+ data.tar.gz: 04cc82c2474a4d7e63d232f13750aeed2a2da072654605fb90c3ab2fc43ed6c4
5
5
  SHA512:
6
- metadata.gz: 69ce0e002321247a430df087a6efad1868c5fd7397c86940f8c4bf9043e089c8d7077acda3699926d29b8b69d9b24cc511313e072f515e2897c62cbdc6df3927
7
- data.tar.gz: a2b958451a785d063b23becc9bde00c3fb52c89f25e646a95b4c98126c2969771ad3a8b7fed3ee80e64cc8618ba0f1f99dd3fdd9121f01c3f1075071e564789a
6
+ metadata.gz: e8f28b5369b73869cdf39cc6165550b8a75d7fe5acff766ee0f1cfd9e6d965acb15c9bd00281cc3b1a1d3424a829e1ad6c005b2befcb55e4944faaa2d7e7ed95
7
+ data.tar.gz: e905e22b816779c8ce367e1c3ed5028a957289b15b1ba7b44d42d4bfe67457f1cbafa7febb1b0c00b99b20029d1b81d44af11310329d939c0f50ac642a57c6e8
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- twitter_friendly (0.3.0)
4
+ twitter_friendly (1.0.0)
5
5
  activesupport (>= 4.2, < 6.0)
6
6
  oj (~> 3.7.6)
7
7
  parallel (~> 1.12.1)
data/README.md CHANGED
@@ -3,7 +3,32 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/twitter_friendly.png)](https://badge.fury.io/rb/twitter_friendly)
4
4
  [![Build Status](https://travis-ci.org/ts-3156/twitter_friendly.svg?branch=master)](https://travis-ci.org/ts-3156/twitter_friendly)
5
5
 
6
- A twitter-friendly Ruby interface to the Twitter API. This twitter_friendly gem provides multiple features.
6
+ The twitter_friendly is a gem to crawl many friends/followers with minimal code. When you want to get a list of friends/followers for a user, all you need to write is the below.
7
+
8
+ ```
9
+ require 'twitter_friendly'
10
+
11
+ client =
12
+ TwitterFriendly::Client.new(
13
+ consumer_key: 'CONSUMER_KEY',
14
+ consumer_secret: 'CONSUMER_SECRET',
15
+ access_token: 'ACCESS_TOKEN',
16
+ access_token_secret: 'ACCESS_TOKEN_SECRET',
17
+ expires_in: 86400 # 1day
18
+ )
19
+
20
+ ids = []
21
+
22
+ begin
23
+ ids = client.follower_ids('yousuck2020')
24
+ rescue Twitter::Error::TooManyRequests => e
25
+ sleep client.rate_limit.follower_ids[:reset_in]
26
+ retry
27
+ end
28
+
29
+ puts "ids #{ids.size}"
30
+ File.write('ids.txt', ids.join("\n"))
31
+ ```
7
32
 
8
33
  - Auto pagination
9
34
  - Auto caching
@@ -156,30 +181,16 @@ client.followers
156
181
  Fetch the timeline of Tweets (by screen name or user ID, or by implicit authenticated user)
157
182
 
158
183
  ```ruby
159
- client.user_timeline('gem')
160
- client.user_timeline(213747670)
161
- client.user_timeline
184
+ tweets = client.user_timeline('screen_name')
162
185
 
163
- result.size
186
+ tweets.size
164
187
  # => 588
165
188
 
166
- result.first.text
189
+ tweets[0][:text]
167
190
  # => "Your tweet text..."
168
191
 
169
- result.first.user.screen_name
170
- # => "your_screen_name"
171
- ```
172
-
173
- Fetch the timeline of Tweets from the authenticated user's home page
174
-
175
- ```ruby
176
- client.home_timeline
177
- ```
178
-
179
- Fetch the timeline of Tweets mentioning the authenticated user
180
-
181
- ```ruby
182
- client.mentions_timeline
192
+ tweets[0][:user][:screen_name]
193
+ # => "screen_name"
183
194
  ```
184
195
 
185
196
  ## Contributing
@@ -2,16 +2,28 @@ require 'digest/md5'
2
2
 
3
3
  module TwitterFriendly
4
4
  class CacheKey
5
- DELIM = ':'
5
+ DELIM = '__'
6
6
  VERSION = '1'
7
7
 
8
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)
9
+ def gen(method_name, args, cache_options = {})
10
+ args_array = args.dup
11
+ options = args_array.extract_options!
12
+ user = method_name == :friendship? ? args_array[0, 2] : args_array[0]
13
+
14
+ key =
15
+ [version,
16
+ method_name,
17
+ method_identifier(method_name, user, options, cache_options),
18
+ options_identifier(method_name, options, cache_options)
19
+ ].compact.join(DELIM)
20
+
21
+ if ENV['SAVE_CACHE_KEY']
22
+ $last_cache_key = key
23
+ puts key
24
+ end
25
+
26
+ key
15
27
  end
16
28
 
17
29
  private
@@ -20,17 +32,19 @@ module TwitterFriendly
20
32
  'v' + VERSION
21
33
  end
22
34
 
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(extract_super_operation(options), user, options)
29
- when method == :collect_with_cursor then method_identifier(extract_super_operation(options), user, options)
30
- when user.nil? && options[:hash].present? then "token-hash#{DELIM}#{options[:hash]}"
31
- else user_identifier(user)
32
- end
35
+ def method_identifier(method, user, options, cache_options)
36
+ raise ArgumentError.new('You must specify method.') unless method
37
+ case
38
+ when method == :search then "query#{DELIM}#{user}"
39
+ when method == :friendship? then "from#{DELIM}#{user[0]}#{DELIM}to#{DELIM}#{user[1]}"
40
+ when method == :list_members then "list_id#{DELIM}#{user}"
41
+ when method == :collect_with_max_id then super_operation_identifier(cache_options[:super_operation], user, options, cache_options)
42
+ when method == :collect_with_cursor then super_operation_identifier(cache_options[:super_operation], user, options, cache_options)
43
+ when user.nil? && cache_options[:hash] then "token-hash#{DELIM}#{options[:hash]}"
44
+ else user_identifier(user)
45
+ end
33
46
  end
47
+ alias_method :super_operation_identifier, :method_identifier
34
48
 
35
49
  def user_identifier(user)
36
50
  case
@@ -43,28 +57,20 @@ module TwitterFriendly
43
57
  end
44
58
  end
45
59
 
46
- def options_identifier(method, options)
60
+ def options_identifier(method, options, cache_options)
47
61
  # TODO 内部的な値はすべてprefix _tf_ をつける
48
62
  opt = options.except(:hash, :call_count, :call_limit, :super_operation, :super_super_operation, :recursive, :parallel)
49
- opt[:in] = extract_super_operation(options) if %i(collect_with_max_id collect_with_cursor).include?(method)
63
+ opt[:in] = cache_options[:super_operation] if %i(collect_with_max_id collect_with_cursor).include?(method)
64
+ delim = '_'
50
65
 
51
66
  if opt.empty?
52
67
  nil
53
68
  else
54
- str = opt.map {|k, v| "#{k}=#{v}"}.join('&')
69
+ str = opt.map {|k, v| "#{k}#{delim}#{v}"}.join(delim)
55
70
  "options#{DELIM}#{str}"
56
71
  end
57
72
  end
58
73
 
59
- def extract_super_operation(options)
60
- raise ArgumentError.new('You must specify :super_operation.') unless options[:super_operation]
61
- if options[:super_operation].is_a?(Array)
62
- options[:super_operation][0]
63
- else
64
- options[:super_operation]
65
- end
66
- end
67
-
68
74
  def hexdigest(ary)
69
75
  Digest::MD5.hexdigest(ary.join(','))
70
76
  end
@@ -0,0 +1,80 @@
1
+ module TwitterFriendly
2
+ module Caching
3
+ # 他のメソッドと違い再帰的に呼ばれるため、全体をキャッシュすると、すべてを再帰的にキャッシュしてしまう。
4
+ # それを防ぐために、特別にここでキャッシュの処理を登録している。
5
+
6
+ def caching_users
7
+ method_name = :users
8
+
9
+ define_method(method_name) do |*args|
10
+ if args[0].size <= TwitterFriendly::REST::Users::MAX_USERS_PER_REQUEST
11
+ options = args.dup.extract_options!
12
+ TwitterFriendly::CachingAndLogging::Instrumenter.start_processing(method_name, options)
13
+
14
+ TwitterFriendly::CachingAndLogging::Instrumenter.complete_processing(method_name, options) do
15
+
16
+ key = CacheKey.gen(method_name, args, hash: credentials_hash)
17
+ @cache.fetch(key, args: [method_name, options]) do
18
+ TwitterFriendly::CachingAndLogging::Instrumenter.perform_request(method_name, options) {super(*args)}
19
+ end
20
+ end
21
+ else
22
+ super(*args)
23
+ end
24
+ end
25
+ end
26
+
27
+ def caching_tweets_with_max_id(*method_names)
28
+ method_names.each do |method_name|
29
+ max_count =
30
+ case method_name
31
+ when :home_timeline then TwitterFriendly::REST::Timelines::MAX_TWEETS_PER_REQUEST
32
+ when :user_timeline then TwitterFriendly::REST::Timelines::MAX_TWEETS_PER_REQUEST
33
+ when :mentions_timeline then TwitterFriendly::REST::Timelines::MAX_TWEETS_PER_REQUEST
34
+ when :favorites then TwitterFriendly::REST::Favorites::MAX_TWEETS_PER_REQUEST
35
+ when :search then TwitterFriendly::REST::Search::MAX_TWEETS_PER_REQUEST
36
+ else raise "Unknown method #{method_name}"
37
+ end
38
+
39
+ define_method(method_name) do |*args|
40
+ options = {count: max_count}.merge(args.extract_options!)
41
+ args << options
42
+
43
+ if options[:count] <= max_count
44
+ TwitterFriendly::CachingAndLogging::Instrumenter.start_processing(method_name, options)
45
+
46
+ TwitterFriendly::CachingAndLogging::Instrumenter.complete_processing(method_name, options) do
47
+ key = CacheKey.gen(method_name, args, hash: credentials_hash)
48
+ @cache.fetch(key, args: [method_name, options]) do
49
+ TwitterFriendly::CachingAndLogging::Instrumenter.perform_request(method_name, options) {super(*args)}
50
+ end
51
+ end
52
+ else
53
+ super(*args)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def caching_resources_with_cursor(*method_names)
60
+ method_names.each do |method_name|
61
+ define_method(method_name) do |*args|
62
+ options = args.dup.extract_options!
63
+
64
+ if options.has_key?(:cursor)
65
+ TwitterFriendly::CachingAndLogging::Instrumenter.start_processing(method_name, options)
66
+
67
+ TwitterFriendly::CachingAndLogging::Instrumenter.complete_processing(method_name, options) do
68
+ key = CacheKey.gen(method_name, args, hash: credentials_hash)
69
+ @cache.fetch(key, args: [method_name, options]) do
70
+ TwitterFriendly::CachingAndLogging::Instrumenter.perform_request(method_name, options) {super(*args)}
71
+ end
72
+ end
73
+ else
74
+ super(*args)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -5,22 +5,18 @@ module TwitterFriendly
5
5
 
6
6
  # TODO 1つのメソッドに対して1回しか実行されないようにする
7
7
  # 全体をキャッシュさせ、さらにロギングを行う
8
- def caching(*root_args)
9
- root_args.each do |method_name|
8
+ def caching(*method_names)
9
+ method_names.each do |method_name|
10
+
10
11
  define_method(method_name) do |*args|
11
- options = args.extract_options!
12
+ options = args.dup.extract_options!
12
13
  Instrumenter.start_processing(method_name, options)
13
14
 
14
15
  Instrumenter.complete_processing(method_name, options) do
15
- do_request =
16
- Proc.new {Instrumenter.perform_request(method_name, options) {options.empty? ? super(*args) : super(*args, options)}}
17
16
 
18
- if Utils.cache_disabled?(options)
19
- do_request.call
20
- else
21
- user = (method_name == :friendship?) ? args[0, 2] : args[0]
22
- key = CacheKey.gen(method_name, user, options.merge(hash: credentials_hash))
23
- @cache.fetch(key, args: [method_name, options], &do_request)
17
+ key = CacheKey.gen(method_name, args, hash: credentials_hash)
18
+ @cache.fetch(key, args: [method_name, options]) do
19
+ Instrumenter.perform_request(method_name, options) {super(*args)}
24
20
  end
25
21
  end
26
22
  end
@@ -31,12 +27,10 @@ module TwitterFriendly
31
27
  def logging(*root_args)
32
28
  root_args.each do |method_name|
33
29
  define_method(method_name) do |*args|
34
- options = args.extract_options!
30
+ options = args.dup.extract_options!
35
31
  Instrumenter.start_processing(method_name, options)
36
32
 
37
- Instrumenter.complete_processing(method_name, options) do
38
- options.empty? ? super(*args) : super(*args, options)
39
- end
33
+ Instrumenter.complete_processing(method_name, options) {super(*args)}
40
34
  end
41
35
  end
42
36
  end
@@ -60,14 +54,5 @@ module TwitterFriendly
60
54
  ::ActiveSupport::Notifications.instrument('request.twitter_friendly', payload) { yield(payload) }
61
55
  end
62
56
  end
63
-
64
- module Utils
65
-
66
- module_function
67
-
68
- def cache_disabled?(options)
69
- options.is_a?(Hash) && options.has_key?(:cache) && !options[:cache]
70
- end
71
- end
72
57
  end
73
58
  end
@@ -1,6 +1,7 @@
1
1
  require 'forwardable'
2
2
 
3
3
  require 'twitter_friendly/caching_and_logging'
4
+ require 'twitter_friendly/caching'
4
5
  require 'twitter_friendly/rest/api'
5
6
  require 'twitter_friendly/utils'
6
7
 
@@ -15,8 +16,13 @@ module TwitterFriendly
15
16
 
16
17
  extend TwitterFriendly::CachingAndLogging
17
18
  caching :user, :friendship?, :verify_credentials, :user?, :blocked_ids
18
- logging :favorites, :friend_ids, :follower_ids, :friends, :followers, :friend_ids_and_follower_ids, :friends_and_followers,
19
- :home_timeline, :user_timeline, :mentions_timeline, :search, :memberships, :list_members, :retweeters_ids
19
+ logging :friends, :followers, :friend_ids_and_follower_ids, :friends_and_followers, :retweeters_ids
20
+
21
+
22
+ extend TwitterFriendly::Caching
23
+ caching_users
24
+ caching_tweets_with_max_id :home_timeline, :user_timeline, :mentions_timeline, :favorites, :search
25
+ caching_resources_with_cursor :friend_ids, :follower_ids, :memberships, :list_members
20
26
 
21
27
  def initialize(*args)
22
28
  options = args.extract_options!
@@ -49,6 +55,11 @@ module TwitterFriendly
49
55
  @twitter
50
56
  end
51
57
 
58
+ def twitter
59
+ logger.warn "DEPRECATION WARNING: Use #internal_client instead of #twitter"
60
+ internal_client
61
+ end
62
+
52
63
  def subscriber_attached?
53
64
  @@subscriber_attached ||= false
54
65
  end
@@ -14,19 +14,13 @@ module TwitterFriendly
14
14
  {args: args}.merge(payload.except(:args)).inspect
15
15
  end
16
16
 
17
- INDENT = ' '
18
-
19
- def indentation(payload)
20
- sp = payload[:super_operation]&.is_a?(Array) ? (INDENT * payload[:super_operation].size) : ''
21
- sp + (payload[:name] == 'write' ? INDENT : '')
22
- end
23
-
24
17
  module_function
25
18
 
26
19
  def logger
27
20
  @@logger
28
21
  end
29
22
 
23
+ # Because TwitterFriendly::Logging is not inherited, passing an instance of logger via module function.
30
24
  def logger=(logger)
31
25
  @@logger = logger
32
26
  end
@@ -38,7 +32,7 @@ module TwitterFriendly
38
32
  def start_processing(event)
39
33
  debug do
40
34
  payload = event.payload
41
- name = "#{indentation(payload)}TF::Started #{payload[:operation]}"
35
+ name = "TF::Started #{payload[:operation]}"
42
36
 
43
37
  if payload[:super_operation]
44
38
  "#{name} in #{payload[:super_operation][0]} at #{Time.now}"
@@ -53,7 +47,7 @@ module TwitterFriendly
53
47
  payload = event.payload
54
48
  name = "TF::Completed #{payload[:operation]} in #{event.duration.round(1)}ms"
55
49
 
56
- "#{indentation(payload)}#{name}#{" #{truncated_payload(payload)}" unless payload.empty?}"
50
+ "#{name}#{" #{truncated_payload(payload)}" unless payload.empty?}"
57
51
  end
58
52
  end
59
53
 
@@ -62,9 +56,9 @@ module TwitterFriendly
62
56
  payload = event.payload
63
57
  payload.delete(:name)
64
58
  operation = payload.delete(:operation)
65
- name = " TW::#{operation.capitalize} #{payload[:args].last[:super_operation][0]} in #{payload[:args][0]} (#{event.duration.round(1)}ms)"
59
+ name = " TW::#{operation.capitalize} #{payload[:args].last[:super_operation]} in #{payload[:args][0]} (#{event.duration.round(1)}ms)"
66
60
  name = color(name, BLUE, true)
67
- " #{indentation(payload)}#{name}#{" #{payload[:args][1]}" unless payload.empty?}"
61
+ " #{name}"
68
62
  end
69
63
  end
70
64
 
@@ -73,10 +67,10 @@ module TwitterFriendly
73
67
  payload = event.payload
74
68
  payload.delete(:name)
75
69
  operation = payload.delete(:operation)
76
- name = " TW::#{operation.capitalize} #{payload[:args][0]} (#{event.duration.round(1)}ms)"
70
+ name = " TW::#{operation.capitalize} #{payload[:args][0] if payload[:args]&.is_a?(Array)} (#{event.duration.round(1)}ms)"
77
71
  c = (%i(encode decode).include?(operation.to_sym)) ? YELLOW : CYAN
78
72
  name = color(name, c, true)
79
- " #{indentation(payload)}#{name}#{" #{payload[:args][1]}" unless payload.empty?}"
73
+ " #{name}#{" #{payload[:args][1] if payload[:args]&.is_a?(Array)}" unless payload.empty?}"
80
74
  end
81
75
  end
82
76
 
@@ -98,10 +92,10 @@ module TwitterFriendly
98
92
  payload = event.payload
99
93
  operation = payload[:super_operation] == :fetch ? :fetch : payload[:name]
100
94
  hit = %i(read fetch).include?(operation.to_sym) && payload[:hit] ? ' (Hit)' : ''
101
- name = " AS::#{operation.capitalize}#{hit} #{payload[:key].split(':')[1]} (#{event.duration.round(1)}ms)"
95
+ name = " AS::#{operation.capitalize}#{hit} #{payload[:key].split('__')[1]} (#{event.duration.round(1)}ms)"
102
96
  name = color(name, MAGENTA, true)
103
97
  # :name, :expires_in, :super_operation, :hit, :race_condition_ttl, :tf_super_operation, :tf_super_super_operation
104
- "#{indentation(payload)}#{name} #{(payload.slice(:key).inspect)}"
98
+ "#{name} #{(payload.slice(:key).inspect)}"
105
99
  end
106
100
  end
107
101
 
@@ -5,7 +5,7 @@ require 'logger'
5
5
  module TwitterFriendly
6
6
  class Logger
7
7
  extend Forwardable
8
- def_delegators :@logger, :debug, :info, :warn, :level
8
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal, :level
9
9
 
10
10
  def initialize(options = {})
11
11
  path = options[:log_dir] || File.join('.twitter_friendly')
@@ -15,4 +15,4 @@ module TwitterFriendly
15
15
  @logger.level = options[:log_level] || :debug
16
16
  end
17
17
  end
18
- end
18
+ end
@@ -1,7 +1,6 @@
1
1
  require 'twitter_friendly/rest/utils'
2
2
  require 'twitter_friendly/rest/collector'
3
3
  require "twitter_friendly/rest/parallel"
4
- require "twitter_friendly/rest/base"
5
4
  require 'twitter_friendly/rest/friends_and_followers'
6
5
  require 'twitter_friendly/rest/users'
7
6
  require 'twitter_friendly/rest/timelines'
@@ -20,7 +19,6 @@ module TwitterFriendly
20
19
  include TwitterFriendly::REST::Utils
21
20
  include TwitterFriendly::REST::Collector
22
21
  include TwitterFriendly::REST::Parallel
23
- include TwitterFriendly::REST::Base
24
22
  include TwitterFriendly::REST::FriendsAndFollowers
25
23
  include TwitterFriendly::REST::Users
26
24
  include TwitterFriendly::REST::Timelines
@@ -31,9 +29,6 @@ module TwitterFriendly
31
29
 
32
30
  include TwitterFriendly::REST::Extension::Clusters
33
31
  include TwitterFriendly::REST::Extension::Timelines
34
-
35
- include TwitterFriendly::REST::Collector::Caching
36
- include TwitterFriendly::REST::Users::Caching
37
32
  end
38
33
  end
39
34
  end
@@ -1,22 +1,52 @@
1
1
  module TwitterFriendly
2
2
  module REST
3
3
  module Collector
4
- def collect_with_max_id(user, collection, max_id, options, collect_options, &block)
5
- key = CacheKey.gen(__method__, user, options.merge(max_id: max_id, hash: credentials_hash, super_operation: collect_options[:super_operation]))
4
+ def fetch_tweets_with_max_id(method_name, max_count, *args)
5
+ options = args.dup.extract_options!
6
6
 
7
- # TODO Handle {cache: false} option
8
- tweets =
9
- @cache.fetch(key, args: [__method__, options]) do
10
- Instrumenter.perform_request(__method__, options) {yield(max_id)}
7
+ total_count = options.delete(:count) || max_count
8
+ call_count = total_count / max_count + (total_count % max_count == 0 ? 0 : 1)
9
+ options[:count] = [max_count, total_count].min
10
+ collect_options = {call_count: call_count, total_count: total_count}
11
+
12
+ collect_with_max_id([], nil, collect_options) do |max_id|
13
+ options[:max_id] = max_id unless max_id.nil?
14
+ result = send(method_name, *args)
15
+
16
+ if method_name == :search
17
+ result.attrs[:statuses]
18
+ else
19
+ if result.is_a?(Array) && result[0].respond_to?(:attrs)
20
+ result.map(&:attrs)
21
+ else
22
+ result
11
23
  end
24
+ end
25
+ end
26
+ end
27
+
28
+ # @param method_name [Symbol]
29
+ # @param user [Integer, String, nil]
30
+ #
31
+ # @option options [Integer] :count
32
+ def fetch_resources_with_cursor(method_name, *args)
33
+ options = args.dup.extract_options!
34
+
35
+ collect_with_cursor([], -1) do |next_cursor|
36
+ options[:cursor] = next_cursor unless next_cursor.nil?
37
+ send(method_name, *args)
38
+ end
39
+ end
40
+
41
+ def collect_with_max_id(collection, max_id, collect_options, &block)
42
+ tweets = yield(max_id)
12
43
  return collection if tweets.nil?
13
44
 
14
45
  collection.concat tweets
15
46
  if tweets.empty? || (collect_options[:call_count] -= 1) < 1
16
47
  collection.flatten
17
48
  else
18
- options[:recursive] = true
19
- collect_with_max_id(user, collection, tweets.last[:id] - 1, options, collect_options, &block)
49
+ collect_with_max_id(collection, tweets.last[:id] - 1, collect_options, &block)
20
50
  end
21
51
  end
22
52
 
@@ -25,55 +55,13 @@ module TwitterFriendly
25
55
  # @param cursor [Integer]
26
56
  #
27
57
  # @option options [Integer] :count
28
- # @option options [String] :super_operation
29
- # @option options [String] :super_super_operation
30
- def collect_with_cursor(user, collection, cursor, options, &block)
31
- key = CacheKey.gen(__method__, user, options.merge(cursor: cursor, hash: credentials_hash))
32
-
33
- # TODO Handle {cache: false} option
34
- response =
35
- @cache.fetch(key, args: [__method__, options]) do
36
- Instrumenter.perform_request(__method__, options) {yield(cursor).attrs}
37
- end
58
+ def collect_with_cursor(collection, cursor, &block)
59
+ response = yield(cursor)
38
60
  return collection if response.nil?
39
61
 
40
- options[:recursive] = true
41
-
42
62
  # Notice: If you call response.to_a, it automatically fetch all results and the results are not cached.
43
63
  collection.concat (response[:ids] || response[:users] || response[:lists])
44
- response[:next_cursor].zero? ? collection.flatten : collect_with_cursor(user, collection, response[:next_cursor], options, &block)
45
- end
46
-
47
- module Instrumenter
48
-
49
- module_function
50
-
51
- # 他のメソッドと違い再帰的に呼ばれるため、全体をキャッシュすると、すべてを再帰的にキャッシュしてしまう。
52
- # それを防ぐために、特別にここでキャッシュの処理を登録している。
53
-
54
- def perform_request(method_name, options, &block)
55
- payload = {operation: 'collect', args: [method_name, options.slice(:max_id, :cursor, :super_operation)]}
56
- ::ActiveSupport::Notifications.instrument('collect.twitter_friendly', payload) { yield(payload) }
57
- end
58
- end
59
-
60
- module Caching
61
- %i(
62
- collect_with_max_id
63
- collect_with_cursor
64
- ).each do |name|
65
- define_method(name) do |*args, &block|
66
- options = args.extract_options!
67
- do_request = Proc.new { options.empty? ? super(*args, &block) : super(*args, options, &block) }
68
-
69
- if options[:recursive]
70
- do_request.call
71
- else
72
- TwitterFriendly::CachingAndLogging::Instrumenter.start_processing(name, options)
73
- TwitterFriendly::CachingAndLogging::Instrumenter.complete_processing(name, options, &do_request)
74
- end
75
- end
76
- end
64
+ response[:next_cursor].zero? ? collection.flatten : collect_with_cursor(collection, response[:next_cursor], &block)
77
65
  end
78
66
  end
79
67
  end
@@ -5,9 +5,14 @@ module TwitterFriendly
5
5
  MAX_TWEETS_PER_REQUEST = 100
6
6
 
7
7
  def favorites(*args)
8
- options = {result_type: :recent}.merge(args.extract_options!)
9
- push_operations(options, __method__)
10
- fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, args[0], options)
8
+ options = {count: MAX_TWEETS_PER_REQUEST}.merge(args.extract_options!)
9
+ args << options
10
+
11
+ if options[:count] <= MAX_TWEETS_PER_REQUEST
12
+ @twitter.favorites(*args)&.map(&:attrs)
13
+ else
14
+ fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, *args)
15
+ end
11
16
  end
12
17
  end
13
18
  end
@@ -1,3 +1,5 @@
1
+ require 'parallel'
2
+
1
3
  module TwitterFriendly
2
4
  module REST
3
5
  module FriendsAndFollowers
@@ -17,14 +19,24 @@ module TwitterFriendly
17
19
  # @option options [Integer] :count The number of tweets to return per page, up to a maximum of 5000.
18
20
  def friend_ids(*args)
19
21
  options = {count: MAX_IDS_PER_REQUEST}.merge(args.extract_options!)
20
- push_operations(options, __method__)
21
- fetch_resources_with_cursor(__method__, args[0], options)
22
+ args << options
23
+
24
+ if options.has_key?(:cursor)
25
+ @twitter.friend_ids(*args)&.attrs
26
+ else
27
+ fetch_resources_with_cursor(__method__, *args)
28
+ end
22
29
  end
23
30
 
24
31
  def follower_ids(*args)
25
32
  options = {count: MAX_IDS_PER_REQUEST}.merge(args.extract_options!)
26
- push_operations(options, __method__)
27
- fetch_resources_with_cursor(__method__, args[0], options)
33
+ args << options
34
+
35
+ if options.has_key?(:cursor)
36
+ @twitter.follower_ids(*args)&.attrs
37
+ else
38
+ fetch_resources_with_cursor(__method__, *args)
39
+ end
28
40
  end
29
41
 
30
42
  # @return [Hash]
@@ -36,47 +48,26 @@ module TwitterFriendly
36
48
  #
37
49
  # @option options [Bool] :parallel
38
50
  def friends(*args)
39
- options = {parallel: true}.merge(args.extract_options!)
40
- push_operations(options, __method__)
41
- ids = friend_ids(*args, options.except(:parallel))
42
- users(ids, options)
51
+ ids = friend_ids(*args)
52
+ users(ids)
43
53
  end
44
54
 
45
55
  def followers(*args)
46
- options = {parallel: true}.merge(args.extract_options!)
47
- push_operations(options, __method__)
48
- ids = follower_ids(*args, options.except(:parallel))
49
- users(ids, options)
56
+ ids = follower_ids(*args)
57
+ users(ids)
50
58
  end
51
59
 
52
60
  def friend_ids_and_follower_ids(*args)
53
- options = { parallel: true}.merge(args.extract_options!)
54
- is_parallel = options.delete(:parallel)
55
-
56
- if is_parallel
57
- require 'parallel'
58
-
59
- parallel(in_threads: 2) do |batch|
60
- batch.friend_ids(*args, options.merge(super_operation: [__method__]))
61
- batch.follower_ids(*args, options.merge(super_operation: [__method__]))
62
- end
63
- else
64
- [friend_ids(*args, options), follower_ids(*args, options)]
61
+ parallel(in_threads: 2) do |batch|
62
+ batch.friend_ids(*args)
63
+ batch.follower_ids(*args)
65
64
  end
66
65
  end
67
66
 
68
67
  def friends_and_followers(*args)
69
- options = args.extract_options!.merge(super_operation: :friends_and_followers)
70
-
71
- following_ids, followed_ids = friend_ids_and_follower_ids(*args, options)
72
- unique_ids = (following_ids + followed_ids).uniq
73
- people = _users(unique_ids).index_by { |u| u[:id] }
68
+ following_ids, followed_ids = friend_ids_and_follower_ids(*args)
69
+ people = users((following_ids + followed_ids).uniq).index_by { |u| u[:id] }
74
70
  [people.slice(*following_ids).values, people.slice(*followed_ids).values]
75
-
76
- # parallel(in_threads: 2) do |batch|
77
- # batch.friends(*args, options)
78
- # batch.followers(*args, options)
79
- # end
80
71
  end
81
72
  end
82
73
  end
@@ -2,6 +2,10 @@ module TwitterFriendly
2
2
  module REST
3
3
  module Lists
4
4
 
5
+ def list(*args)
6
+ @twitter.list(*args)&.to_hash
7
+ end
8
+
5
9
  MAX_LISTS_PER_REQUEST = 1000
6
10
 
7
11
  # @return [Hash] The lists the specified user has been added to.
@@ -14,8 +18,13 @@ module TwitterFriendly
14
18
  # @option options [Integer] :count The number of tweets to return per page, up to a maximum of 5000.
15
19
  def memberships(*args)
16
20
  options = {count: MAX_LISTS_PER_REQUEST}.merge(args.extract_options!)
17
- push_operations(options, __method__)
18
- fetch_resources_with_cursor(__method__, args[0], options)
21
+ args << options
22
+
23
+ if options.has_key?(:cursor)
24
+ @twitter.memberships(*args)&.attrs
25
+ else
26
+ fetch_resources_with_cursor(__method__, *args)
27
+ end
19
28
  end
20
29
 
21
30
  MAX_MEMBERS_PER_REQUEST = 5000
@@ -30,8 +39,13 @@ module TwitterFriendly
30
39
  # @option options [Integer] :count The number of tweets to return per page, up to a maximum of 5000.
31
40
  def list_members(*args)
32
41
  options = {count: MAX_MEMBERS_PER_REQUEST, skip_status: 1}.merge(args.extract_options!)
33
- push_operations(options, __method__)
34
- fetch_resources_with_cursor(__method__, args[0], options)
42
+ args << options
43
+
44
+ if options.has_key?(:cursor)
45
+ @twitter.list_members(*args)&.attrs
46
+ else
47
+ fetch_resources_with_cursor(__method__, *args)
48
+ end
35
49
  end
36
50
  end
37
51
  end
@@ -6,9 +6,13 @@ module TwitterFriendly
6
6
 
7
7
  def search(query, options = {})
8
8
  raise ArgumentError.new('You must specify a search query.') unless query.is_a?(String)
9
- options = {result_type: :recent}.merge(options)
10
- push_operations(options, __method__)
11
- fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, query, options)
9
+ options = {result_type: 'recent'}.merge(options)
10
+
11
+ if options[:count] <= MAX_TWEETS_PER_REQUEST
12
+ @twitter.search(query, options)&.attrs&.fetch(:statuses)
13
+ else
14
+ fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, query, options)
15
+ end
12
16
  end
13
17
  end
14
18
  end
@@ -5,21 +5,31 @@ module TwitterFriendly
5
5
  MAX_TWEETS_PER_REQUEST = 200
6
6
 
7
7
  def home_timeline(options = {})
8
- options = {include_rts: true}.merge(options)
9
- push_operations(options, __method__)
10
- fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, nil, options)
8
+ options = {include_rts: true, count: MAX_TWEETS_PER_REQUEST}.merge(options)
9
+ if options[:count] <= MAX_TWEETS_PER_REQUEST
10
+ @twitter.home_timeline(options)&.map(&:attrs)
11
+ else
12
+ fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, options)
13
+ end
11
14
  end
12
15
 
13
16
  def user_timeline(*args)
14
- options = {include_rts: true}.merge(args.extract_options!)
15
- push_operations(options, __method__)
16
- fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, args[0], options)
17
+ options = {include_rts: true, count: MAX_TWEETS_PER_REQUEST}.merge(args.extract_options!)
18
+ args << options
19
+ if options[:count] <= MAX_TWEETS_PER_REQUEST
20
+ @twitter.user_timeline(*args)&.map(&:attrs)
21
+ else
22
+ fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, *args)
23
+ end
17
24
  end
18
25
 
19
26
  def mentions_timeline(options = {})
20
- options = {include_rts: true}.merge(options)
21
- push_operations(options, __method__)
22
- fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, nil, options)
27
+ options = {include_rts: true, count: MAX_TWEETS_PER_REQUEST}.merge(options)
28
+ if options[:count] <= MAX_TWEETS_PER_REQUEST
29
+ @twitter.mentions_timeline(options)&.map(&:attrs)
30
+ else
31
+ fetch_tweets_with_max_id(__method__, MAX_TWEETS_PER_REQUEST, options)
32
+ end
23
33
  end
24
34
  end
25
35
  end
@@ -14,8 +14,8 @@ module TwitterFriendly
14
14
  def retweeters_ids(*args)
15
15
  # このメソッドではページングができない
16
16
  options = {count: MAX_IDS_PER_REQUEST}.merge(args.extract_options!)
17
- push_operations(options, __method__)
18
- fetch_resources_with_cursor(__method__, args[0], options)
17
+ args << options
18
+ @twitter.retweeters_ids(*args)
19
19
  end
20
20
  end
21
21
  end
@@ -1,3 +1,5 @@
1
+ require 'parallel'
2
+
1
3
  module TwitterFriendly
2
4
  module REST
3
5
  module Users
@@ -5,8 +7,8 @@ module TwitterFriendly
5
7
  @twitter.verify_credentials({skip_status: true}.merge(options))&.to_hash
6
8
  end
7
9
 
8
- def user?(user, options = {})
9
- @twitter.user?(user, options)
10
+ def user?(*args)
11
+ @twitter.user?(*args)
10
12
  end
11
13
 
12
14
  def user(*args)
@@ -17,71 +19,17 @@ module TwitterFriendly
17
19
 
18
20
  def users(values, options = {})
19
21
  if values.size <= MAX_USERS_PER_REQUEST
20
- key = CacheKey.gen(__method__, values, options.merge(hash: credentials_hash))
21
-
22
- @cache.fetch(key, args: [__method__, options]) do
23
- Instrumenter.perform_request(args: [__method__, super_operation: options[:super_operation]]) do
24
- @twitter.send(__method__, values, options.except(:parallel, :super_operation, :recursive))&.compact&.map(&:to_hash)
25
- end
26
- end
22
+ @twitter.users(values, options)
27
23
  else
28
- options[:recursive] = true
29
- _users(values, options)
24
+ parallel(in_threads: 10) do |batch|
25
+ values.each_slice(MAX_USERS_PER_REQUEST) { |targets| batch.users(targets, options) }
26
+ end.flatten
30
27
  end
31
28
  end
32
29
 
33
30
  def blocked_ids(*args)
34
31
  @twitter.blocked_ids(*args)&.attrs&.fetch(:ids)
35
32
  end
36
-
37
- module Instrumenter
38
-
39
- module_function
40
-
41
- # 他のメソッドと違い再帰的に呼ばれるため、全体をキャッシュすると、すべてを再帰的にキャッシュしてしまう。
42
- # それを防ぐために、特別にここでキャッシュの処理を登録している。
43
-
44
- def perform_request(options, &block)
45
- payload = {operation: 'request', args: options[:args]}
46
- ::ActiveSupport::Notifications.instrument('request.twitter_friendly', payload) { yield(payload) }
47
- end
48
- end
49
-
50
- module Caching
51
- %i(
52
- users
53
- ).each do |name|
54
- define_method(name) do |*args, &block|
55
- options = args.extract_options!
56
- do_request = Proc.new { options.empty? ? super(*args, &block) : super(*args, options, &block) }
57
-
58
- if options[:recursive]
59
- do_request.call
60
- else
61
- TwitterFriendly::CachingAndLogging::Instrumenter.start_processing(name, options)
62
- TwitterFriendly::CachingAndLogging::Instrumenter.complete_processing(name, options, &do_request)
63
- end
64
- end
65
- end
66
- end
67
-
68
- private
69
-
70
- def _users(values, options = {})
71
- options = {super_operation: :users, parallel: true}.merge(options)
72
-
73
- if options[:parallel]
74
- require 'parallel'
75
-
76
- parallel(in_threads: 10) do |batch|
77
- values.each_slice(MAX_USERS_PER_REQUEST) { |targets| batch.users(targets, options) }
78
- end.flatten
79
- else
80
- values.each_slice(MAX_USERS_PER_REQUEST).map do |targets|
81
- users(targets, options)
82
- end
83
- end&.flatten&.compact&.map(&:to_hash)
84
- end
85
33
  end
86
34
  end
87
35
  end
@@ -6,12 +6,6 @@ module TwitterFriendly
6
6
  def credentials_hash
7
7
  Digest::MD5.hexdigest(access_token + access_token_secret + consumer_key + consumer_secret)
8
8
  end
9
-
10
- def push_operations(options, operation)
11
- options[:super_operation] = [] unless options[:super_operation]
12
- options[:super_operation] = [options[:super_operation]] unless options[:super_operation].is_a?(Array)
13
- options[:super_operation].prepend operation
14
- end
15
9
  end
16
10
  end
17
11
  end
@@ -1,3 +1,3 @@
1
1
  module TwitterFriendly
2
- VERSION = "0.3.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twitter_friendly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ts-3156
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-10 00:00:00.000000000 Z
11
+ date: 2019-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -180,13 +180,13 @@ files:
180
180
  - lib/twitter_friendly.rb
181
181
  - lib/twitter_friendly/cache.rb
182
182
  - lib/twitter_friendly/cache_key.rb
183
+ - lib/twitter_friendly/caching.rb
183
184
  - lib/twitter_friendly/caching_and_logging.rb
184
185
  - lib/twitter_friendly/client.rb
185
186
  - lib/twitter_friendly/log_subscriber.rb
186
187
  - lib/twitter_friendly/logger.rb
187
188
  - lib/twitter_friendly/rate_limit.rb
188
189
  - lib/twitter_friendly/rest/api.rb
189
- - lib/twitter_friendly/rest/base.rb
190
190
  - lib/twitter_friendly/rest/collector.rb
191
191
  - lib/twitter_friendly/rest/extension/clusters.rb
192
192
  - lib/twitter_friendly/rest/extension/timelines.rb
@@ -1,32 +0,0 @@
1
- module TwitterFriendly
2
- module REST
3
- module Base
4
- def fetch_tweets_with_max_id(method_name, max_count, user, options)
5
- total_count = options.delete(:count) || max_count
6
- call_count = total_count / max_count + (total_count % max_count == 0 ? 0 : 1)
7
- options[:count] = [max_count, total_count].min
8
- super_operation = options.delete(:super_operation)
9
- collect_options = {call_count: call_count, total_count: total_count, super_operation: super_operation}
10
-
11
- collect_with_max_id(user, [], nil, options, collect_options) do |max_id|
12
- options[:max_id] = max_id unless max_id.nil?
13
-
14
- result = @twitter.send(method_name, *[user, options].compact)
15
- (method_name == :search) ? result.attrs[:statuses] : result.map(&:attrs)
16
- end
17
- end
18
-
19
- # @param method_name [Symbol]
20
- # @param user [Integer, String, nil]
21
- #
22
- # @option options [Integer] :count
23
- # @option options [String] :super_super_operation
24
- def fetch_resources_with_cursor(method_name, user, options)
25
- collect_with_cursor(user, [], -1, options) do |next_cursor|
26
- options[:cursor] = next_cursor unless next_cursor.nil?
27
- @twitter.send(method_name, user, options.except(:super_operation))
28
- end
29
- end
30
- end
31
- end
32
- end