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.
- 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
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'twitter_friendly/rest/utils'
|
2
|
+
require 'twitter_friendly/rest/collector'
|
3
|
+
require "twitter_friendly/rest/parallel"
|
4
|
+
require "twitter_friendly/rest/base"
|
5
|
+
require 'twitter_friendly/rest/friends_and_followers'
|
6
|
+
require 'twitter_friendly/rest/users'
|
7
|
+
require 'twitter_friendly/rest/timelines'
|
8
|
+
require 'twitter_friendly/rest/search'
|
9
|
+
require 'twitter_friendly/rest/favorites'
|
10
|
+
require 'twitter_friendly/rest/lists'
|
11
|
+
require 'twitter_friendly/rest/tweets'
|
12
|
+
|
13
|
+
require 'twitter_friendly/caching'
|
14
|
+
|
15
|
+
module TwitterFriendly
|
16
|
+
module REST
|
17
|
+
module API
|
18
|
+
include TwitterFriendly::REST::Utils
|
19
|
+
include TwitterFriendly::REST::Collector
|
20
|
+
include TwitterFriendly::REST::Parallel
|
21
|
+
include TwitterFriendly::REST::Base
|
22
|
+
include TwitterFriendly::REST::FriendsAndFollowers
|
23
|
+
include TwitterFriendly::REST::Users
|
24
|
+
include TwitterFriendly::REST::Timelines
|
25
|
+
include TwitterFriendly::REST::Search
|
26
|
+
include TwitterFriendly::REST::Favorites
|
27
|
+
include TwitterFriendly::REST::Lists
|
28
|
+
include TwitterFriendly::REST::Tweets
|
29
|
+
|
30
|
+
include TwitterFriendly::Caching
|
31
|
+
include TwitterFriendly::REST::Collector::Caching
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Base
|
4
|
+
def fetch_tweets_with_max_id(name, args, max_count)
|
5
|
+
options = args.extract_options!
|
6
|
+
total_count = options.delete(:count) || max_count
|
7
|
+
call_count = total_count / max_count + (total_count % max_count == 0 ? 0 : 1)
|
8
|
+
options[:count] = max_count
|
9
|
+
|
10
|
+
collect_with_max_id(args[0], [], nil, {super_operation: name}.merge(options)) do |max_id|
|
11
|
+
options[:max_id] = max_id unless max_id.nil?
|
12
|
+
if (call_count -= 1) >= 0
|
13
|
+
if name == :search
|
14
|
+
@twitter.send(name, *args, options).attrs[:statuses]
|
15
|
+
else
|
16
|
+
@twitter.send(name, *args, options).map(&:attrs)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch_resources_with_cursor(name, args)
|
23
|
+
options = args.extract_options!
|
24
|
+
|
25
|
+
collect_with_cursor(args[0], [], -1, {super_operation: name}.merge(options)) do |next_cursor|
|
26
|
+
options[:cursor] = next_cursor unless next_cursor.nil?
|
27
|
+
@twitter.send(name, *args, options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Collector
|
4
|
+
def collect_with_max_id(user, collection, max_id, options, &block)
|
5
|
+
fetch_options = options.dup
|
6
|
+
fetch_options[:max_id] = max_id
|
7
|
+
fetch_options.merge!(args: [__method__, fetch_options], hash: credentials_hash)
|
8
|
+
|
9
|
+
# TODO Handle {cache: false} option
|
10
|
+
tweets =
|
11
|
+
@cache.fetch(__method__, user, fetch_options) do
|
12
|
+
Instrumenter.perform_request(args: [__method__, max_id: max_id, super_operation: options[:super_operation]]) do
|
13
|
+
yield(max_id)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
return collection if tweets.nil?
|
17
|
+
|
18
|
+
options[:recursive] = true
|
19
|
+
|
20
|
+
collection.concat tweets
|
21
|
+
tweets.empty? ? collection.flatten : collect_with_max_id(user, collection, tweets.last[:id] - 1, options, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def collect_with_cursor(user, collection, cursor, options, &block)
|
25
|
+
fetch_options = options.dup
|
26
|
+
fetch_options[:cursor] = cursor
|
27
|
+
fetch_options.merge!(args: [__method__, fetch_options], hash: credentials_hash)
|
28
|
+
|
29
|
+
# TODO Handle {cache: false} option
|
30
|
+
response =
|
31
|
+
@cache.fetch(__method__, user, fetch_options) do
|
32
|
+
Instrumenter.perform_request(args: [__method__, cursor: cursor, super_operation: options[:super_operation]]) do
|
33
|
+
yield(cursor).attrs
|
34
|
+
end
|
35
|
+
end
|
36
|
+
return collection if response.nil?
|
37
|
+
|
38
|
+
options[:recursive] = true
|
39
|
+
|
40
|
+
# Notice: If you call response.to_a, it automatically fetch all results and the results are not cached.
|
41
|
+
collection.concat (response[:ids] || response[:users] || response[:lists])
|
42
|
+
response[:next_cursor].zero? ? collection.flatten : collect_with_cursor(user, collection, response[:next_cursor], options, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
module Instrumenter
|
46
|
+
|
47
|
+
module_function
|
48
|
+
|
49
|
+
# 他のメソッドと違い再帰的に呼ばれるため、全体をキャッシュすると、すべてを再帰的にキャッシュしてしまう。
|
50
|
+
# それを防ぐために、特別にここでキャッシュの処理を登録している。
|
51
|
+
|
52
|
+
def perform_request(options, &block)
|
53
|
+
payload = {operation: 'collect', args: options[:args]}
|
54
|
+
::ActiveSupport::Notifications.instrument('collect.twitter_friendly', payload) { yield(payload) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module Caching
|
59
|
+
%i(
|
60
|
+
collect_with_max_id
|
61
|
+
collect_with_cursor
|
62
|
+
).each do |name|
|
63
|
+
define_method(name) do |*args, &block|
|
64
|
+
options = args.extract_options!
|
65
|
+
do_request = Proc.new { options.empty? ? super(*args, &block) : super(*args, options, &block) }
|
66
|
+
|
67
|
+
if options[:recursive]
|
68
|
+
do_request.call
|
69
|
+
else
|
70
|
+
TwitterFriendly::Caching::Instrumenter.start_processing(name, options)
|
71
|
+
TwitterFriendly::Caching::Instrumenter.complete_processing(name, options, &do_request)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Favorites
|
4
|
+
|
5
|
+
MAX_TWEETS_PER_REQUEST = 100
|
6
|
+
|
7
|
+
%i(favorites).each do |name|
|
8
|
+
define_method(name) do |*args|
|
9
|
+
args << {result_type: :recent}.merge(args.extract_options!)
|
10
|
+
fetch_tweets_with_max_id(name, args, MAX_TWEETS_PER_REQUEST)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module FriendsAndFollowers
|
4
|
+
def friendship?(from, to, options = {})
|
5
|
+
@twitter.send(__method__, from, to, options)
|
6
|
+
end
|
7
|
+
|
8
|
+
MAX_IDS_PER_REQUEST = 5000
|
9
|
+
|
10
|
+
%i(friend_ids follower_ids).each do |name|
|
11
|
+
define_method(name) do |*args|
|
12
|
+
options = {count: MAX_IDS_PER_REQUEST}.merge(args.extract_options!)
|
13
|
+
if options[:super_operation]
|
14
|
+
options[:super_super_operation] = options[:super_operation]
|
15
|
+
options[:super_operation] = name
|
16
|
+
end
|
17
|
+
args << options
|
18
|
+
fetch_resources_with_cursor(name, args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def friends(*args)
|
23
|
+
options = args.extract_options!.merge(super_operation: :friends)
|
24
|
+
ids = friend_ids(*args, options)
|
25
|
+
users(ids, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def followers(*args)
|
29
|
+
options = args.extract_options!.merge(super_operation: :followers)
|
30
|
+
ids = follower_ids(*args, options)
|
31
|
+
users(ids, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def friend_ids_and_follower_ids(*args)
|
35
|
+
options = {super_operation: :friend_ids_and_follower_ids, parallel: true}.merge(args.extract_options!)
|
36
|
+
|
37
|
+
if options[:parallel]
|
38
|
+
require 'parallel'
|
39
|
+
|
40
|
+
parallel(in_threads: 2) do |batch|
|
41
|
+
batch.friend_ids(*args, options)
|
42
|
+
batch.follower_ids(*args, options)
|
43
|
+
end
|
44
|
+
else
|
45
|
+
[friend_ids(*args, options), follower_ids(*args, options)]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def friends_and_followers(*args)
|
50
|
+
options = args.extract_options!.merge(super_operation: :friends_and_followers)
|
51
|
+
|
52
|
+
following_ids, followed_ids = friend_ids_and_follower_ids(*args, options)
|
53
|
+
unique_ids = (following_ids + followed_ids).uniq
|
54
|
+
people = _users(unique_ids).index_by { |u| u[:id] }
|
55
|
+
[people.slice(*following_ids).values, people.slice(*followed_ids).values]
|
56
|
+
|
57
|
+
# parallel(in_threads: 2) do |batch|
|
58
|
+
# batch.friends(*args, options)
|
59
|
+
# batch.followers(*args, options)
|
60
|
+
# end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Lists
|
4
|
+
|
5
|
+
MAX_LISTS_PER_REQUEST = 1000
|
6
|
+
|
7
|
+
# Returns the lists the specified user has been added to.
|
8
|
+
def memberships(*args)
|
9
|
+
args << {count: MAX_LISTS_PER_REQUEST}.merge(args.extract_options!)
|
10
|
+
fetch_resources_with_cursor(__method__, args)
|
11
|
+
end
|
12
|
+
|
13
|
+
MAX_MEMBERS_PER_REQUEST = 5000
|
14
|
+
|
15
|
+
# Returns the members of the specified list.
|
16
|
+
def list_members(*args)
|
17
|
+
args << {count: MAX_MEMBERS_PER_REQUEST, skip_status: 1}.merge(args.extract_options!)
|
18
|
+
fetch_resources_with_cursor(__method__, args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Parallel
|
4
|
+
def parallel(options = {}, &block)
|
5
|
+
batch = Arguments.new
|
6
|
+
yield(batch)
|
7
|
+
|
8
|
+
in_threads = options.fetch(:in_threads, batch.size)
|
9
|
+
|
10
|
+
::Parallel.map_with_index(batch, in_threads: in_threads) do |args, i|
|
11
|
+
{i: i, result: send(*args)} # Cached here
|
12
|
+
end.sort_by { |q| q[:i] }.map { |q| q[:result] }
|
13
|
+
end
|
14
|
+
|
15
|
+
class Arguments < Array
|
16
|
+
%i(
|
17
|
+
users
|
18
|
+
friend_ids
|
19
|
+
follower_ids
|
20
|
+
friends
|
21
|
+
followers
|
22
|
+
).each do |name|
|
23
|
+
define_method(name) do |*args|
|
24
|
+
send(:<< , [name, *args])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Search
|
4
|
+
|
5
|
+
MAX_TWEETS_PER_REQUEST = 100
|
6
|
+
|
7
|
+
%i(search).each do |name|
|
8
|
+
define_method(name) do |query, options = {}|
|
9
|
+
raise ArgumentError.new('You must specify a search query.') unless query.is_a?(String)
|
10
|
+
args = [query, {result_type: :recent}.merge(options)]
|
11
|
+
fetch_tweets_with_max_id(name, args, MAX_TWEETS_PER_REQUEST)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Timelines
|
4
|
+
|
5
|
+
MAX_TWEETS_PER_REQUEST = 200
|
6
|
+
|
7
|
+
%i(home_timeline user_timeline mentions_timeline).each do |name|
|
8
|
+
define_method(name) do |*args|
|
9
|
+
args << {include_rts: true}.merge(args.extract_options!)
|
10
|
+
fetch_tweets_with_max_id(name, args, MAX_TWEETS_PER_REQUEST)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Tweets
|
4
|
+
|
5
|
+
MAX_IDS_PER_REQUEST = 100
|
6
|
+
|
7
|
+
def retweeters_ids(*args)
|
8
|
+
args << {count: MAX_IDS_PER_REQUEST}.merge(args.extract_options!)
|
9
|
+
fetch_resources_with_cursor(__method__, args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module TwitterFriendly
|
2
|
+
module REST
|
3
|
+
module Users
|
4
|
+
def verify_credentials(options = {})
|
5
|
+
@twitter.send(__method__, {skip_status: true}.merge(options))&.to_hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def user?(*args)
|
9
|
+
@twitter.send(__method__, *args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def user(*args)
|
13
|
+
@twitter.send(__method__, *args)&.to_hash
|
14
|
+
end
|
15
|
+
|
16
|
+
MAX_USERS_PER_REQUEST = 100
|
17
|
+
|
18
|
+
def users(values, options = {})
|
19
|
+
if values.size <= MAX_USERS_PER_REQUEST
|
20
|
+
@twitter.send(__method__, values, options)&.compact&.map(&:to_hash)
|
21
|
+
else
|
22
|
+
_users(values, options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def blocked_ids(*args)
|
27
|
+
@twitter.send(__method__, *args)&.attrs&.fetch(:ids)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def _users(values, options = {})
|
33
|
+
options = {super_operation: :users, parallel: true}.merge(options)
|
34
|
+
|
35
|
+
if options[:parallel]
|
36
|
+
require 'parallel'
|
37
|
+
|
38
|
+
parallel(in_threads: 10) do |batch|
|
39
|
+
values.each_slice(MAX_USERS_PER_REQUEST) { |targets| batch.users(targets, options) }
|
40
|
+
end.flatten
|
41
|
+
else
|
42
|
+
values.each_slice(MAX_USERS_PER_REQUEST).map do |targets|
|
43
|
+
@twitter.send(:users, targets, options)
|
44
|
+
end
|
45
|
+
end&.flatten&.compact&.map(&:to_hash)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'oj'
|
3
|
+
|
4
|
+
module TwitterFriendly
|
5
|
+
class Serializer
|
6
|
+
class << self
|
7
|
+
def encode(obj, options = {})
|
8
|
+
Instrumenter.perform_encode(options) do
|
9
|
+
(!!obj == obj) ? obj : coder.encode(obj)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def decode(str, options = {})
|
14
|
+
Instrumenter.perform_decode(options) do
|
15
|
+
str.kind_of?(String) ? coder.decode(str) : str
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def coder
|
20
|
+
@@coder ||= Coder.instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def coder=(coder)
|
24
|
+
@@coder = Coder.instance(coder)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
module Instrumenter
|
31
|
+
|
32
|
+
module_function
|
33
|
+
|
34
|
+
def perform_encode(options, &block)
|
35
|
+
payload = {operation: 'encode', args: options[:args]}
|
36
|
+
::ActiveSupport::Notifications.instrument('encode.twitter_friendly', payload) { yield(payload) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def perform_decode(options, &block)
|
40
|
+
payload = {operation: 'decode', args: options[:args]}
|
41
|
+
::ActiveSupport::Notifications.instrument('decode.twitter_friendly', payload) { yield(payload) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Coder
|
46
|
+
def initialize(coder)
|
47
|
+
@coder = coder
|
48
|
+
end
|
49
|
+
|
50
|
+
def encode(obj)
|
51
|
+
@coder.dump(obj)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.instance(coder = nil)
|
55
|
+
if coder.nil? && defined?(Oj)
|
56
|
+
OjCoder.new(Oj)
|
57
|
+
else
|
58
|
+
JsonCoder.new(coder)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class JsonCoder < Coder
|
64
|
+
def decode(str)
|
65
|
+
@coder.parse(str, symbolize_names: true)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class OjCoder < Coder
|
70
|
+
def encode(obj)
|
71
|
+
@coder.dump(obj, mode: :compat)
|
72
|
+
end
|
73
|
+
|
74
|
+
def decode(str)
|
75
|
+
@coder.load(str, symbol_keys: true)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|