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.
@@ -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,11 @@
1
+ require 'digest/md5'
2
+
3
+ module TwitterFriendly
4
+ module REST
5
+ module Utils
6
+ def credentials_hash
7
+ Digest::MD5.hexdigest(access_token + access_token_secret + consumer_key + consumer_secret)
8
+ end
9
+ end
10
+ end
11
+ 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