twitter_friendly 0.3.0 → 1.0.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 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