birdgrinder 0.1.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -38,6 +38,7 @@ module BirdGrinder
38
38
  # Forwards a given message type (with options) to each handler,
39
39
  # storing the current id if changed.
40
40
  def receive_message(type, options = BirdGrinder::Nash.new)
41
+ options = options.to_nash if options.respond_to?(:to_nash)
41
42
  logger.debug "receiving message: #{type.inspect} - #{options.id? ? options.id : 'unknown id'}"
42
43
  dispatch(type.to_sym, options)
43
44
  update_stored_id_for(type, options.id) if options.id?
@@ -59,6 +60,13 @@ module BirdGrinder
59
60
  @tweeter.search(q, opts)
60
61
  end
61
62
 
63
+ # Returns the streaming api instance
64
+ #
65
+ # @see BirdGrinder::Tweeter#streaming
66
+ def streaming
67
+ @tweeter.streaming
68
+ end
69
+
62
70
  # Tweets some text as the current user
63
71
  #
64
72
  # @see BirdGrinder::Tweeter#tweet
@@ -3,9 +3,10 @@ module BirdGrinder
3
3
  class StreamProcessor
4
4
  is :loggable
5
5
 
6
- def initialize(parent, stream_name)
6
+ def initialize(parent, stream_name, stream_meta = {})
7
7
  @parent = parent
8
8
  @stream_name = stream_name.to_sym
9
+ @stream_meta = stream_meta.to_nash
9
10
  setup_parser
10
11
  end
11
12
 
@@ -27,6 +28,7 @@ module BirdGrinder
27
28
  end
28
29
  processed.stream_type = stream_type
29
30
  processed.streaming_source = @stream_name
31
+ processed.meta = @stream_meta
30
32
  @parent.delegate.receive_message(:incoming_stream, processed)
31
33
  end
32
34
 
@@ -1,4 +1,4 @@
1
- require 'bird_grinder/tweeter/stream_processor'
1
+ require 'bird_grinder/tweeter/streaming_request'
2
2
 
3
3
  module BirdGrinder
4
4
  class Tweeter
@@ -24,14 +24,14 @@ module BirdGrinder
24
24
  #
25
25
  # @param [Hash] opts extra options for the query
26
26
  def sample(opts = {})
27
- get(:sample, opts)
27
+ stream(:sample, opts)
28
28
  end
29
29
 
30
30
  # Start processing the filter stream
31
31
  #
32
32
  # @param [Hash] opts extra options for the query
33
33
  def filter(opts = {})
34
- get(:filter, opts)
34
+ stream(:filter, opts)
35
35
  end
36
36
 
37
37
  # Start processing the filter stream with a given follow
@@ -42,7 +42,7 @@ module BirdGrinder
42
42
  opts = args.extract_options!
43
43
  opts[:follow] = args.join(",")
44
44
  opts[:path] = :filter
45
- get(:follow, opts)
45
+ stream(:follow, opts)
46
46
  end
47
47
 
48
48
  # Starts tracking a specific query.
@@ -51,22 +51,20 @@ module BirdGrinder
51
51
  def track(query, opts = {})
52
52
  opts[:track] = query
53
53
  opts[:path] = :filter
54
- get(:track, opts)
54
+ stream(:track, opts)
55
55
  end
56
56
 
57
57
  protected
58
58
 
59
+ def stream(name, opts = {})
60
+ req = StreamingRequest.new(@parent, name, opts)
61
+ yield req if block_given?
62
+ req.perform
63
+ req
64
+ end
65
+
59
66
  def get(name, opts = {}, attempts = 0)
60
- logger.debug "Getting stream #{name} w/ options: #{opts.inspect}"
61
- path = opts.delete(:path)
62
- processor = StreamProcessor.new(@parent, name)
63
- http_opts = {
64
- :head => {'Authorization' => @parent.auth_credentials}
65
- }
66
- http_opts[:query] = opts if opts.present?
67
- url = streaming_base_url / api_version.to_s / "statuses" / "#{path || name}.json"
68
- http = EventMachine::HttpRequest.new(url).get(http_opts)
69
- http.stream(&processor.method(:receive_chunk))
67
+
70
68
  end
71
69
 
72
70
  end
@@ -0,0 +1,109 @@
1
+ require 'bird_grinder/tweeter/stream_processor'
2
+ require 'cgi'
3
+
4
+ module BirdGrinder
5
+ class Tweeter
6
+ class StreamingRequest
7
+ is :loggable
8
+
9
+ INITIAL_DELAYS = {:http => 10, :network => 0.25}
10
+ MAX_DELAYS = {:http => 240, :network => 16}
11
+ DELAY_CALCULATOR = {
12
+ :http => L { |v| v * 2 },
13
+ :network => L { |v| v + INITIAL_DELAYS[:network] }
14
+ }
15
+
16
+ def initialize(parent, name, options = {})
17
+ logger.debug "Creating stream '#{name}' with options: #{options.inspect}"
18
+ @parent = parent
19
+ @name = name
20
+ @path = options.delete(:path) || :name
21
+ @metadata = options.delete(:metadata) || {}
22
+ @options = options
23
+ @failure_delay = nil
24
+ @failure_count = 0
25
+ @failure_reason = nil
26
+ end
27
+
28
+ def perform
29
+ logger.debug "Preparing to start stream"
30
+ @stream_processor = nil
31
+ type = request_method
32
+ http = create_request.send(type, http_options(type))
33
+ # Handle failures correctly so we can back off
34
+ @current_request = http
35
+ http.errback { fail!(:network)}
36
+ http.callback { http.response_header.status > 299 ? fail!(:http) : perform }
37
+ http.stream { |c| receive_chunk(c) }
38
+ end
39
+
40
+ def fail!(type)
41
+ logger.debug "Streaming failed with #{type}"
42
+ if @failure_count == 0 || @failure_reason != type
43
+ logger.debug "Instantly restarting (#{@failure_count == 0 ? "First failure" : "Different type"})"
44
+ EM.next_tick { perform }
45
+ else
46
+ @failure_delay ||= INITIAL_DELAYS[type]
47
+ logger.debug "Restarting stream in #{@failure_delay} seconds"
48
+ logger.debug "Adding timer to restart in #{@failure_delay} seconds"
49
+ EM.add_timer(@failure_delay) { perform }
50
+ potential_new_delay = DELAY_CALCULATOR[type].call(@failure_delay)
51
+ @failure_delay = [potential_new_delay, MAX_DELAYS[type]].min
52
+ logger.debug "Next delay is #{@failure_delay}"
53
+ end
54
+ @failure_count += 1
55
+ @failure_reason = type
56
+ logger.debug "Failed #{@failure_count} times with #{@failure_reason}"
57
+ end
58
+
59
+ def create_request
60
+ EventMachine::HttpRequest.new(full_url)
61
+ end
62
+
63
+ def stream_processor
64
+ @stream_processor ||= StreamProcessor.new(@parent, @name, @metadata)
65
+ end
66
+
67
+ def receive_chunk(c)
68
+ return unless @current_request.response_header.status == 200
69
+ if !@failure_reason.nil?
70
+ @failure_reason = nil
71
+ @failure_delay = nil
72
+ @failure_count = 0
73
+ end
74
+ stream_processor.receive_chunk(c)
75
+ end
76
+
77
+ def default_request_options
78
+ {:head => {'Authorization' => @parent.auth_credentials}}
79
+ end
80
+
81
+ def http_options(type)
82
+ base = self.default_request_options
83
+ if @options.present?
84
+ if type == :get
85
+ base[:query] = @options
86
+ else
87
+ base[:head].merge! 'Content-Type' => "application/x-www-form-urlencoded"
88
+ base[:body] = body = {}
89
+ @options.each_pair { |k,v| body[CGI.escape(k.to_s)] = CGI.escape(v) }
90
+ end
91
+ end
92
+ base
93
+ end
94
+
95
+ def request_method
96
+ {:filter => :post,
97
+ :sample => :get,
98
+ :firehose => :get,
99
+ :retweet => :get
100
+ }.fetch(@path, :get)
101
+ end
102
+
103
+ def full_url
104
+ @full_url ||= (Streaming.streaming_base_url / Streaming.api_version.to_s / "statuses" / "#{@path}.json")
105
+ end
106
+
107
+ end
108
+ end
109
+ end
@@ -1,4 +1,5 @@
1
1
  require 'uri'
2
+ require 'cgi'
2
3
 
3
4
  module BirdGrinder
4
5
  # An asynchronous, delegate-based twitter client that uses
@@ -57,7 +58,7 @@ module BirdGrinder
57
58
  user = user.to_s.strip
58
59
  logger.info "Following '#{user}'"
59
60
  post("friendships/create.json", opts.merge(:screen_name => user)) do
60
- delegate.receive_message(:outgoing_follow, :user => user)
61
+ delegate.receive_message(:outgoing_follow, {:user => user}.to_nash)
61
62
  end
62
63
  end
63
64
 
@@ -69,7 +70,7 @@ module BirdGrinder
69
70
  user = user.to_s.strip
70
71
  logger.info "Unfollowing '#{user}'"
71
72
  post("friendships/destroy.json", opts.merge(:screen_name => user)) do
72
- delegate.receive_message(:outgoing_unfollow, :user => user)
73
+ delegate.receive_message(:outgoing_unfollow, {:user => user}.to_nash)
73
74
  end
74
75
  end
75
76
 
@@ -95,7 +96,7 @@ module BirdGrinder
95
96
  user = user.to_s.strip
96
97
  logger.debug "DM'ing #{user}: #{text}"
97
98
  post("direct_messages/new.json", opts.merge(:user => user, :text => text)) do
98
- delegate.receive_message(:outgoing_direct_message, :user => user, :text => text)
99
+ delegate.receive_message(:outgoing_direct_message, {:user => user, :text => text}.to_nash)
99
100
  end
100
101
  end
101
102
 
@@ -223,7 +224,7 @@ module BirdGrinder
223
224
 
224
225
  def post(path, params = {}, &blk)
225
226
  real_params = {}
226
- params.each_pair { |k,v| real_params[URI.encode(k.to_s)] = URI.encode(v) }
227
+ params.each_pair { |k,v| real_params[CGI.escape(k.to_s)] = CGI.escape(v) }
227
228
  http = request(path).post({
228
229
  :head => {
229
230
  'Authorization' => @auth_credentials,
data/lib/bird_grinder.rb CHANGED
@@ -8,7 +8,7 @@ require 'em-http'
8
8
  module BirdGrinder
9
9
  include Perennial
10
10
 
11
- VERSION = [0, 1, 1, 1]
11
+ VERSION = [0, 1, 2]
12
12
 
13
13
  def self.version(include_minor = false)
14
14
  VERSION[0, (include_minor ? 4 : 3)].join(".")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: birdgrinder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Darcy Laycock
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-10-19 00:00:00 +08:00
12
+ date: 2009-10-24 00:00:00 +08:00
13
13
  default_executable: birdgrinder
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -94,6 +94,7 @@ files:
94
94
  - lib/bird_grinder/tweeter/search.rb
95
95
  - lib/bird_grinder/tweeter/stream_processor.rb
96
96
  - lib/bird_grinder/tweeter/streaming.rb
97
+ - lib/bird_grinder/tweeter/streaming_request.rb
97
98
  - lib/bird_grinder/tweeter.rb
98
99
  - lib/bird_grinder.rb
99
100
  - lib/birdgrinder.rb