punchblock 0.4.3 → 0.5.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,161 @@
1
+ %w{
2
+ timeout
3
+ blather/client/dsl
4
+ punchblock/core_ext/blather/stanza
5
+ punchblock/core_ext/blather/stanza/presence
6
+ }.each { |f| require f }
7
+
8
+ module Punchblock
9
+ module Connection
10
+ class XMPP
11
+ include Blather::DSL
12
+ attr_accessor :event_handler
13
+
14
+ ##
15
+ # Initialize the required connection attributes
16
+ #
17
+ # @param [Hash] options
18
+ # @option options [String] :username client JID
19
+ # @option options [String] :password XMPP password
20
+ # @option options [String] :rayo_domain the domain on which Rayo is running
21
+ # @option options [Logger] :wire_logger to which all XMPP transactions will be logged
22
+ # @option options [Boolean, Optional] :auto_reconnect whether or not to auto reconnect
23
+ # @option options [Numeric, Optional] :write_timeout for which to wait on a command response
24
+ # @option options [Numeric, nil, Optional] :ping_period interval in seconds on which to ping the server. Nil or false to disable
25
+ #
26
+ def initialize(options = {})
27
+ raise ArgumentError unless (@username = options[:username]) && options[:password]
28
+
29
+ setup *[:username, :password, :host, :port, :certs].map { |key| options.delete key }
30
+
31
+ @rayo_domain = options[:rayo_domain] || Blather::JID.new(@username).domain
32
+
33
+ @callmap = {} # This hash maps call IDs to their XMPP domain.
34
+
35
+ @auto_reconnect = !!options[:auto_reconnect]
36
+ @reconnect_attempts = 0
37
+
38
+ @ping_period = options.has_key?(:ping_period) ? options[:ping_period] : 60
39
+
40
+ @event_handler = lambda { |event| raise 'No event handler set' }
41
+
42
+ Blather.logger = options.delete(:wire_logger) if options.has_key?(:wire_logger)
43
+ end
44
+
45
+ def write(command, options = {})
46
+ iq = prep_command_for_execution command, options
47
+ client.write_with_handler iq do |response|
48
+ if response.result?
49
+ handle_iq_result response, command
50
+ elsif response.error?
51
+ handle_error response, command
52
+ end
53
+ end
54
+ command.request!
55
+ end
56
+
57
+ def prep_command_for_execution(command, options = {})
58
+ call_id, component_id = options.values_at :call_id, :component_id
59
+ command.connection = self
60
+ command.call_id = call_id
61
+ jid = command.is_a?(Command::Dial) ? @rayo_domain : "#{call_id}@#{@callmap[call_id]}"
62
+ jid << "/#{component_id}" if component_id
63
+ create_iq(jid).tap do |iq|
64
+ @logger.debug "Sending IQ ID #{iq.id} #{command.inspect} to #{jid}" if @logger
65
+ iq << command
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Fire up the connection
71
+ #
72
+ def run
73
+ register_handlers
74
+ connect
75
+ end
76
+
77
+ def connect
78
+ begin
79
+ EM.run { client.run }
80
+ rescue Blather::SASLError, Blather::StreamError => e
81
+ raise ProtocolError.new(e.class.to_s, e.message)
82
+ end
83
+ end
84
+
85
+ def stop
86
+ @reconnect_attempts = nil
87
+ client.close
88
+ end
89
+
90
+ def connected?
91
+ client.connected?
92
+ end
93
+
94
+ private
95
+
96
+ def handle_presence(p)
97
+ throw :pass unless p.rayo_event? && p.from.domain == @rayo_domain
98
+ @logger.info "Receiving event for call ID #{p.call_id}" if @logger
99
+ @callmap[p.call_id] = p.from.domain
100
+ @logger.debug p.inspect if @logger
101
+ event = p.event
102
+ event.connection = self
103
+ event_handler.call event
104
+ end
105
+
106
+ def handle_iq_result(iq, command)
107
+ # FIXME: Do we need to raise a warning if the domain changes?
108
+ @callmap[iq.from.node] = iq.from.domain
109
+ @logger.debug "Command #{iq.id} completed successfully" if @logger
110
+ command.response = iq.rayo_node.is_a?(Ref) ? iq.rayo_node : true
111
+ end
112
+
113
+ def handle_error(iq, command = nil)
114
+ e = Blather::StanzaError.import iq
115
+ protocol_error = ProtocolError.new e.name, e.text, iq.call_id, iq.component_id
116
+ command.response = protocol_error if command
117
+ end
118
+
119
+ def register_handlers
120
+ # Push a message to the queue and the log that we connected
121
+ when_ready do
122
+ event_handler.call Connected
123
+ @logger.info "Connected to XMPP as #{@username}" if @logger
124
+ @reconnect_attempts = 0
125
+ @rayo_ping = EM::PeriodicTimer.new(@ping_period) { ping_rayo } if @ping_period
126
+ end
127
+
128
+ disconnected do
129
+ @rayo_ping.cancel if @rayo_ping
130
+ if @auto_reconnect && @reconnect_attempts
131
+ timer = 30 * 2 ** @reconnect_attempts
132
+ @logger.warn "XMPP disconnected. Tried to reconnect #{@reconnect_attempts} times. Reconnecting in #{timer}s." if @logger
133
+ sleep timer
134
+ @logger.info "Trying to reconnect..." if @logger
135
+ @reconnect_attempts += 1
136
+ connect
137
+ end
138
+ end
139
+
140
+ # Read/handle presence requests. This is how we get events.
141
+ presence do |msg|
142
+ handle_presence msg
143
+ end
144
+ end
145
+
146
+ def ping_rayo
147
+ client.write_with_handler Blather::Stanza::Iq::Ping.new(:set, @rayo_domain) do |response|
148
+ begin
149
+ handle_error response if response.is_a? Blather::BlatherError
150
+ rescue ProtocolError => e
151
+ raise e unless e.name == :feature_not_implemented
152
+ end
153
+ end
154
+ end
155
+
156
+ def create_iq(jid = nil)
157
+ Blather::Stanza::Iq.new :set, jid || @call_id
158
+ end
159
+ end
160
+ end
161
+ end
@@ -4,8 +4,8 @@
4
4
  # THIS IS IMPERMANENT AND WILL DISAPPEAR
5
5
  module Punchblock
6
6
  class DSL
7
- def initialize(connection, call, queue) # :nodoc:
8
- @connection, @call, @queue = connection, call, queue
7
+ def initialize(client, call_id, queue) # :nodoc:
8
+ @client, @call_id, @queue = client, call_id, queue
9
9
  end
10
10
 
11
11
  def accept # :nodoc:
@@ -38,8 +38,8 @@ module Punchblock
38
38
  component.complete_event.resource
39
39
  end
40
40
 
41
- def write(msg) # :nodoc:
42
- @connection.write @call, msg
41
+ def write(command) # :nodoc:
42
+ @client.execute_command command, :call_id => @call_id, :async => false
43
43
  end
44
44
  end
45
45
  end
@@ -7,7 +7,7 @@ module Punchblock
7
7
 
8
8
  class_attribute :registered_ns, :registered_name
9
9
 
10
- attr_accessor :call_id, :component_id, :connection, :original_component
10
+ attr_accessor :call_id, :component_id, :connection, :client, :original_component
11
11
 
12
12
  # Register a new stanza class to a name and/or namespace
13
13
  #
@@ -78,7 +78,7 @@ module Punchblock
78
78
  # @return [RayoNode] the original command issued that lead to this event
79
79
  #
80
80
  def source
81
- @source ||= connection.original_component_from_id component_id if connection && component_id
81
+ @source ||= client.find_component_by_id component_id if client && component_id
82
82
  @source ||= original_component
83
83
  end
84
84
 
@@ -1,3 +1,3 @@
1
1
  module Punchblock
2
- VERSION = "0.4.3"
2
+ VERSION = "0.5.0"
3
3
  end
data/punchblock.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
28
28
  s.add_runtime_dependency %q<activesupport>, [">= 2.1.0"]
29
29
  s.add_runtime_dependency %q<state_machine>, [">= 1.0.1"]
30
30
  s.add_runtime_dependency %q<future-resource>, [">= 0.0.2"]
31
- s.add_runtime_dependency %q<has-guarded-handlers>, [">= 0.0.3"]
31
+ s.add_runtime_dependency %q<has-guarded-handlers>, [">= 0.1.0"]
32
32
 
33
33
  s.add_development_dependency %q<bundler>, ["~> 1.0.0"]
34
34
  s.add_development_dependency %q<rspec>, ["~> 2.3.0"]
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ class Client
5
+ describe ComponentRegistry do
6
+ let(:component_id) { 'abc123' }
7
+ let(:component) { stub 'Component', :component_id => component_id }
8
+
9
+ it 'should store components and allow lookup by ID' do
10
+ subject << component
11
+ subject.find_by_id(component_id).should be component
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ describe Client do
5
+ let(:connection) { Connection::XMPP.new :username => '1@call.rayo.net', :password => 1 }
6
+
7
+ subject { Client.new :connection => connection }
8
+
9
+ its(:event_queue) { should be_a Queue }
10
+ its(:connection) { should be connection }
11
+ its(:component_registry) { should be_a Client::ComponentRegistry }
12
+
13
+ let(:call_id) { 'abc123' }
14
+ let(:mock_event) { stub_everything 'Event' }
15
+ let(:component_id) { 'abc123' }
16
+ let(:mock_component) { stub 'Component', :component_id => component_id }
17
+ let(:mock_command) { stub 'Command' }
18
+
19
+ describe '#run' do
20
+ it 'should start up the connection' do
21
+ connection.expects(:run).once
22
+ subject.run
23
+ end
24
+ end
25
+
26
+ describe '#stop' do
27
+ it 'should stop the connection' do
28
+ connection.expects(:stop).once
29
+ subject.stop
30
+ end
31
+ end
32
+
33
+ it 'should handle connection events' do
34
+ subject.expects(:handle_event).with(mock_event).once
35
+ connection.event_handler.call mock_event
36
+ end
37
+
38
+ describe '#handle_event' do
39
+ it "sets the event's client" do
40
+ event = Event::Offer.new
41
+ subject.handle_event event
42
+ event.client.should be subject
43
+ end
44
+
45
+ context 'if the event can be associated with a source component' do
46
+ before do
47
+ mock_event.stubs(:source).returns mock_component
48
+ mock_component.expects(:add_event).with mock_event
49
+ end
50
+
51
+ it 'should not queue up the event' do
52
+ subject.handle_event mock_event
53
+ subject.event_queue.should be_empty
54
+ end
55
+
56
+ it 'should not call event handlers' do
57
+ handler = mock 'handler'
58
+ handler.expects(:call).never
59
+ subject.register_event_handler do |event|
60
+ handler.call event
61
+ throw :halt
62
+ end
63
+ subject.handle_event mock_event
64
+ end
65
+ end
66
+
67
+ context 'if the event cannot be associated with a source component' do
68
+ context 'if event handlers have been set' do
69
+ it 'should call the event handler and not queue up the event' do
70
+ handler = mock 'handler'
71
+ handler.expects(:call).once.with mock_event
72
+ subject.register_event_handler do |event|
73
+ handler.call event
74
+ throw :halt
75
+ end
76
+ subject.handle_event mock_event
77
+ subject.event_queue.should be_empty
78
+ end
79
+ end
80
+
81
+ context 'if event handlers have not been set' do
82
+ it 'should queue up the event' do
83
+ subject.handle_event mock_event
84
+ subject.event_queue.pop(true).should == mock_event
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ it 'should be able to register and retrieve components' do
91
+ subject.register_component mock_component
92
+ subject.find_component_by_id(component_id).should be mock_component
93
+ end
94
+
95
+ describe '#execute_command' do
96
+ let(:component) { Component::Output.new }
97
+ let(:event) { Event::Complete.new }
98
+
99
+ before do
100
+ connection.expects(:write).once.with component, :call_id => call_id
101
+ end
102
+
103
+ let :execute_command do
104
+ subject.execute_command component, :call_id => call_id
105
+ end
106
+
107
+ it 'should write the command to the connection' do
108
+ execute_command
109
+ end
110
+
111
+ it "should set the command's client" do
112
+ execute_command
113
+ component.client.should be subject
114
+ end
115
+
116
+ it "should handle a component's events" do
117
+ subject.expects(:trigger_handler).with(:event, event).once
118
+ execute_command
119
+ component.request!
120
+ component.execute!
121
+ component.add_event event
122
+ end
123
+ end
124
+ end # describe Client
125
+ end # Punchblock
@@ -37,15 +37,8 @@ module Punchblock
37
37
  end
38
38
  end
39
39
 
40
- let :iq do
41
- Blather::Stanza::Iq.new(:result, 'blah').tap do |iq|
42
- iq.from = "call.rayo.net"
43
- iq << ref
44
- end
45
- end
46
-
47
40
  it "should set the call ID from the ref" do
48
- subject.response = iq
41
+ subject.response = ref
49
42
  subject.call_id.should == call_id
50
43
  end
51
44
  end
@@ -92,12 +92,13 @@ module Punchblock
92
92
  end
93
93
 
94
94
  describe "actions" do
95
+ let(:mock_client) { mock 'Client' }
95
96
  let(:command) { Input.new :grammar => '[5 DIGITS]' }
96
97
 
97
98
  before do
98
99
  command.component_id = 'abc123'
99
100
  command.call_id = '123abc'
100
- command.connection = Connection.new :username => '123', :password => '123'
101
+ command.client = mock_client
101
102
  end
102
103
 
103
104
  describe '#stop_action' do
@@ -116,7 +117,7 @@ module Punchblock
116
117
  end
117
118
 
118
119
  it "should send its command properly" do
119
- Connection.any_instance.expects(:write).with('123abc', command.stop_action, 'abc123')
120
+ mock_client.expects(:execute_command).with(command.stop_action, :call_id => '123abc', :component_id => 'abc123')
120
121
  command.stop!
121
122
  end
122
123
  end
@@ -51,12 +51,13 @@ module Punchblock
51
51
  end
52
52
 
53
53
  describe "actions" do
54
+ let(:mock_client) { mock 'Client' }
54
55
  let(:command) { Output.new :text => 'Once upon a time there was a message...', :voice => 'kate' }
55
56
 
56
57
  before do
57
58
  command.component_id = 'abc123'
58
59
  command.call_id = '123abc'
59
- command.connection = Connection.new :username => '123', :password => '123'
60
+ command.client = mock_client
60
61
  end
61
62
 
62
63
  describe '#pause_action' do
@@ -75,7 +76,7 @@ module Punchblock
75
76
  end
76
77
 
77
78
  it "should send its command properly" do
78
- Connection.any_instance.expects(:write).with('123abc', command.pause_action, 'abc123').returns true
79
+ mock_client.expects(:execute_command).with(command.pause_action, :call_id => '123abc', :component_id => 'abc123').returns true
79
80
  command.expects :paused!
80
81
  command.pause!
81
82
  end
@@ -119,7 +120,7 @@ module Punchblock
119
120
  end
120
121
 
121
122
  it "should send its command properly" do
122
- Connection.any_instance.expects(:write).with('123abc', command.resume_action, 'abc123').returns true
123
+ mock_client.expects(:execute_command).with(command.resume_action, :call_id => '123abc', :component_id => 'abc123').returns true
123
124
  command.expects :resumed!
124
125
  command.resume!
125
126
  end
@@ -163,7 +164,7 @@ module Punchblock
163
164
  end
164
165
 
165
166
  it "should send its command properly" do
166
- Connection.any_instance.expects(:write).with('123abc', command.stop_action, 'abc123')
167
+ mock_client.expects(:execute_command).with(command.stop_action, :call_id => '123abc', :component_id => 'abc123')
167
168
  command.stop!
168
169
  end
169
170
  end
@@ -196,7 +197,7 @@ module Punchblock
196
197
  it "should send its command properly" do
197
198
  seek_action = command.seek_action seek_options
198
199
  command.stubs(:seek_action).returns seek_action
199
- Connection.any_instance.expects(:write).with('123abc', seek_action, 'abc123').returns true
200
+ mock_client.expects(:execute_command).with(seek_action, :call_id => '123abc', :component_id => 'abc123').returns true
200
201
  command.expects :seeking!
201
202
  command.expects :stopped_seeking!
202
203
  command.seek! seek_options
@@ -263,7 +264,7 @@ module Punchblock
263
264
  it "should send its command properly" do
264
265
  speed_up_action = command.speed_up_action
265
266
  command.stubs(:speed_up_action).returns speed_up_action
266
- Connection.any_instance.expects(:write).with('123abc', speed_up_action, 'abc123').returns true
267
+ mock_client.expects(:execute_command).with(speed_up_action, :call_id => '123abc', :component_id => 'abc123').returns true
267
268
  command.expects :speeding_up!
268
269
  command.expects :stopped_speeding!
269
270
  command.speed_up!
@@ -321,7 +322,7 @@ module Punchblock
321
322
  it "should send its command properly" do
322
323
  slow_down_action = command.slow_down_action
323
324
  command.stubs(:slow_down_action).returns slow_down_action
324
- Connection.any_instance.expects(:write).with('123abc', slow_down_action, 'abc123').returns true
325
+ mock_client.expects(:execute_command).with(slow_down_action, :call_id => '123abc', :component_id => 'abc123').returns true
325
326
  command.expects :slowing_down!
326
327
  command.expects :stopped_speeding!
327
328
  command.slow_down!
@@ -396,7 +397,7 @@ module Punchblock
396
397
  it "should send its command properly" do
397
398
  volume_up_action = command.volume_up_action
398
399
  command.stubs(:volume_up_action).returns volume_up_action
399
- Connection.any_instance.expects(:write).with('123abc', volume_up_action, 'abc123').returns true
400
+ mock_client.expects(:execute_command).with(volume_up_action, :call_id => '123abc', :component_id => 'abc123').returns true
400
401
  command.expects :voluming_up!
401
402
  command.expects :stopped_voluming!
402
403
  command.volume_up!
@@ -454,7 +455,7 @@ module Punchblock
454
455
  it "should send its command properly" do
455
456
  volume_down_action = command.volume_down_action
456
457
  command.stubs(:volume_down_action).returns volume_down_action
457
- Connection.any_instance.expects(:write).with('123abc', volume_down_action, 'abc123').returns true
458
+ mock_client.expects(:execute_command).with(volume_down_action, :call_id => '123abc', :component_id => 'abc123').returns true
458
459
  command.expects :voluming_down!
459
460
  command.expects :stopped_voluming!
460
461
  command.volume_down!