birdgrinder 0.1.1.1 → 0.1.2

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