twitter_friendly 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +41 -0
- data/Gemfile.lock +44 -5
- data/README.md +163 -11
- data/bin/test/friends_and_followers.rb +95 -0
- data/gemfiles/ar_4.2.gemfile +5 -0
- data/gemfiles/ar_5.1.gemfile +5 -0
- data/gemfiles/ar_5.2.gemfile +5 -0
- data/lib/twitter_friendly.rb +12 -0
- data/lib/twitter_friendly/cache.rb +48 -0
- data/lib/twitter_friendly/cache_key.rb +64 -0
- data/lib/twitter_friendly/caching.rb +86 -0
- data/lib/twitter_friendly/client.rb +52 -0
- data/lib/twitter_friendly/log_subscriber.rb +109 -0
- data/lib/twitter_friendly/logger.rb +18 -0
- data/lib/twitter_friendly/rate_limit.rb +81 -0
- data/lib/twitter_friendly/rest/api.rb +34 -0
- data/lib/twitter_friendly/rest/base.rb +32 -0
- data/lib/twitter_friendly/rest/collector.rb +78 -0
- data/lib/twitter_friendly/rest/favorites.rb +15 -0
- data/lib/twitter_friendly/rest/friends_and_followers.rb +64 -0
- data/lib/twitter_friendly/rest/lists.rb +22 -0
- data/lib/twitter_friendly/rest/parallel.rb +30 -0
- data/lib/twitter_friendly/rest/search.rb +16 -0
- data/lib/twitter_friendly/rest/timelines.rb +15 -0
- data/lib/twitter_friendly/rest/tweets.rb +13 -0
- data/lib/twitter_friendly/rest/users.rb +49 -0
- data/lib/twitter_friendly/rest/utils.rb +11 -0
- data/lib/twitter_friendly/serializer.rb +79 -0
- data/lib/twitter_friendly/utils.rb +6 -0
- data/lib/twitter_friendly/version.rb +1 -1
- data/twitter_friendly.gemspec +11 -2
- metadata +127 -10
@@ -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
|