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.
@@ -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