punchblock 0.6.1 → 0.6.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.
@@ -0,0 +1,84 @@
1
+ require 'uri'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ module Component
7
+ module Asterisk
8
+ class AGICommand < Component
9
+ attr_reader :action
10
+
11
+ def initialize(component_node, call)
12
+ @component_node, @call = component_node, call
13
+ @id = UUIDTools::UUID.random_create.to_s
14
+ @action = create_action
15
+ pb_logger.debug "Starting up..."
16
+ end
17
+
18
+ def execute
19
+ @call.send_ami_action! @action
20
+ end
21
+
22
+ def handle_ami_event(event)
23
+ pb_logger.debug "Handling AMI event: #{event.inspect}"
24
+ if event.name == 'AsyncAGI'
25
+ if event['SubEvent'] == 'Exec'
26
+ pb_logger.debug "Received AsyncAGI:Exec event, sending complete event."
27
+ send_event complete_event(success_reason(event))
28
+ end
29
+ end
30
+ end
31
+
32
+ def parse_agi_result(result)
33
+ match = URI.decode(result).chomp.match(/^(\d{3}) result=(-?\d*) ?(\(?.*\)?)?$/)
34
+ if match
35
+ data = match[3] ? match[3].gsub(/(^\()|(\)$)/, '') : nil
36
+ [match[1].to_i, match[2].to_i, data]
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def create_action
43
+ RubyAMI::Action.new 'AGI', 'Channel' => @call.channel, 'Command' => @component_node.name, 'CommandID' => id do |response|
44
+ handle_response response
45
+ end
46
+ end
47
+
48
+ def handle_response(response)
49
+ pb_logger.debug "Handling response: #{response.inspect}"
50
+ case response
51
+ when RubyAMI::Error
52
+ set_node_response false
53
+ when RubyAMI::Response
54
+ set_node_response Ref.new :id => id
55
+ end
56
+ end
57
+
58
+ def set_node_response(value)
59
+ pb_logger.debug "Setting response on component node to #{value}"
60
+ @component_node.response = value
61
+ end
62
+
63
+ def success_reason(event)
64
+ code, result, data = parse_agi_result event['Result']
65
+ Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new :code => code, :result => result, :data => data
66
+ end
67
+
68
+ def complete_event(reason)
69
+ Punchblock::Event::Complete.new.tap do |c|
70
+ c.reason = reason
71
+ end
72
+ end
73
+
74
+ def send_event(event)
75
+ event.component_id = id
76
+ pb_logger.debug "Sending event #{event.inspect}"
77
+ @component_node.add_event event
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,96 @@
1
+ module Punchblock
2
+ module Translator
3
+ class Asterisk
4
+ module Component
5
+ module Asterisk
6
+ class AMIAction < Component
7
+ attr_reader :action
8
+
9
+ def initialize(component_node, translator)
10
+ @component_node, @translator = component_node, translator
11
+ @action = create_action
12
+ @id = @action.action_id
13
+ pb_logger.debug "Starting up..."
14
+ end
15
+
16
+ def execute
17
+ send_action
18
+ send_ref
19
+ end
20
+
21
+ private
22
+
23
+ def create_action
24
+ headers = {}
25
+ @component_node.params_hash.each_pair do |key, value|
26
+ headers[key.to_s.capitalize] = value
27
+ end
28
+ RubyAMI::Action.new @component_node.name, headers do |response|
29
+ handle_response response
30
+ end
31
+ end
32
+
33
+ def send_action
34
+ @translator.send_ami_action! @action
35
+ end
36
+
37
+ def send_ref
38
+ @component_node.response = Ref.new :id => @action.action_id
39
+ end
40
+
41
+ def handle_response(response)
42
+ pb_logger.debug "Handling response #{response.inspect}"
43
+ case response
44
+ when RubyAMI::Error
45
+ send_event complete_event(error_reason(response))
46
+ when RubyAMI::Response
47
+ send_events
48
+ send_event complete_event(success_reason(response))
49
+ end
50
+ end
51
+
52
+ def error_reason(response)
53
+ Punchblock::Event::Complete::Error.new :details => response.message
54
+ end
55
+
56
+ def success_reason(response)
57
+ headers = response.headers
58
+ headers.merge! @extra_complete_attributes if @extra_complete_attributes
59
+ headers.delete 'ActionID'
60
+ Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new :message => headers.delete('Message'), :attributes => headers
61
+ end
62
+
63
+ def complete_event(reason)
64
+ Punchblock::Event::Complete.new.tap do |c|
65
+ c.reason = reason
66
+ end
67
+ end
68
+
69
+ def send_events
70
+ return unless @action.has_causal_events?
71
+ @action.events.each do |e|
72
+ if e.name.downcase == @action.causal_event_terminator_name
73
+ @extra_complete_attributes = e.headers
74
+ else
75
+ send_event pb_event_from_ami_event(e)
76
+ end
77
+ end
78
+ end
79
+
80
+ def pb_event_from_ami_event(ami_event)
81
+ headers = ami_event.headers
82
+ headers.delete 'ActionID'
83
+ Event::Asterisk::AMI::Event.new :name => ami_event.name, :attributes => headers
84
+ end
85
+
86
+ def send_event(event)
87
+ event.component_id = id
88
+ pb_logger.debug "Sending event #{event}"
89
+ @component_node.add_event event
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,3 +1,3 @@
1
1
  module Punchblock
2
- VERSION = "0.6.1"
2
+ VERSION = "0.6.2"
3
3
  end
@@ -19,8 +19,10 @@ module Punchblock
19
19
  end
20
20
 
21
21
  it 'should properly set the Blather logger' do
22
- XMPP.new :wire_logger => :foo, :username => 1, :password => 1
22
+ Punchblock.logger = :foo
23
+ XMPP.new :username => '1@call.rayo.net', :password => 1
23
24
  Blather.logger.should be :foo
25
+ Punchblock.reset_logger
24
26
  end
25
27
 
26
28
  it "looking up original command by command ID" do
@@ -4,6 +4,66 @@ module Punchblock
4
4
  module Translator
5
5
  class Asterisk
6
6
  describe Call do
7
+ let(:channel) { 'SIP/foo' }
8
+ let(:translator) { stub_everything 'Translator::Asterisk' }
9
+ let(:env) { "agi_request%3A%20async%0Aagi_channel%3A%20SIP%2F1234-00000000%0Aagi_language%3A%20en%0Aagi_type%3A%20SIP%0Aagi_uniqueid%3A%201320835995.0%0Aagi_version%3A%201.8.4.1%0Aagi_callerid%3A%205678%0Aagi_calleridname%3A%20Jane%20Smith%0Aagi_callingpres%3A%200%0Aagi_callingani2%3A%200%0Aagi_callington%3A%200%0Aagi_callingtns%3A%200%0Aagi_dnid%3A%201000%0Aagi_rdnis%3A%20unknown%0Aagi_context%3A%20default%0Aagi_extension%3A%201000%0Aagi_priority%3A%201%0Aagi_enhanced%3A%200.0%0Aagi_accountcode%3A%20%0Aagi_threadid%3A%204366221312%0A%0A" }
10
+ let(:agi_env) do
11
+ {
12
+ :agi_request => 'async',
13
+ :agi_channel => 'SIP/1234-00000000',
14
+ :agi_language => 'en',
15
+ :agi_type => 'SIP',
16
+ :agi_uniqueid => '1320835995.0',
17
+ :agi_version => '1.8.4.1',
18
+ :agi_callerid => '5678',
19
+ :agi_calleridname => 'Jane Smith',
20
+ :agi_callingpres => '0',
21
+ :agi_callingani2 => '0',
22
+ :agi_callington => '0',
23
+ :agi_callingtns => '0',
24
+ :agi_dnid => '1000',
25
+ :agi_rdnis => 'unknown',
26
+ :agi_context => 'default',
27
+ :agi_extension => '1000',
28
+ :agi_priority => '1',
29
+ :agi_enhanced => '0.0',
30
+ :agi_accountcode => '',
31
+ :agi_threadid => '4366221312'
32
+ }
33
+ end
34
+
35
+ let :sip_headers do
36
+ {
37
+ :x_agi_request => 'async',
38
+ :x_agi_channel => 'SIP/1234-00000000',
39
+ :x_agi_language => 'en',
40
+ :x_agi_type => 'SIP',
41
+ :x_agi_uniqueid => '1320835995.0',
42
+ :x_agi_version => '1.8.4.1',
43
+ :x_agi_callerid => '5678',
44
+ :x_agi_calleridname => 'Jane Smith',
45
+ :x_agi_callingpres => '0',
46
+ :x_agi_callingani2 => '0',
47
+ :x_agi_callington => '0',
48
+ :x_agi_callingtns => '0',
49
+ :x_agi_dnid => '1000',
50
+ :x_agi_rdnis => 'unknown',
51
+ :x_agi_context => 'default',
52
+ :x_agi_extension => '1000',
53
+ :x_agi_priority => '1',
54
+ :x_agi_enhanced => '0.0',
55
+ :x_agi_accountcode => '',
56
+ :x_agi_threadid => '4366221312'
57
+ }
58
+ end
59
+
60
+ subject { Call.new channel, translator, env }
61
+
62
+ its(:id) { should be_a String }
63
+ its(:channel) { should == channel }
64
+ its(:translator) { should be translator }
65
+ its(:agi_env) { should == agi_env }
66
+
7
67
  describe '#register_component' do
8
68
  it 'should make the component accessible by ID' do
9
69
  component_id = 'abc123'
@@ -12,6 +72,152 @@ module Punchblock
12
72
  subject.component_with_id(component_id).should be component
13
73
  end
14
74
  end
75
+
76
+ describe '#send_offer' do
77
+ it 'sends an offer to the translator' do
78
+ expected_offer = Punchblock::Event::Offer.new :call_id => subject.id,
79
+ :to => '1000',
80
+ :from => 'sip:5678',
81
+ :headers => sip_headers
82
+ translator.expects(:handle_pb_event!).with expected_offer
83
+ subject.send_offer
84
+ end
85
+ end
86
+
87
+ describe '#process_ami_event' do
88
+ context 'with a Hangup event' do
89
+ let :ami_event do
90
+ RubyAMI::Event.new('Hangup').tap do |e|
91
+ e['Uniqueid'] = "1320842458.8"
92
+ e['Calleridnum'] = "5678"
93
+ e['Calleridname'] = "Jane Smith"
94
+ e['Cause'] = "0"
95
+ e['Cause-txt'] = "Unknown"
96
+ e['Channel'] = "SIP/1234-00000000"
97
+ end
98
+ end
99
+
100
+ it 'should send an end event to the translator' do
101
+ expected_end_event = Punchblock::Event::End.new :reason => :hangup,
102
+ :call_id => subject.id
103
+ translator.expects(:handle_pb_event!).with expected_end_event
104
+ subject.process_ami_event ami_event
105
+ end
106
+ end
107
+
108
+ context 'with an event for a known AGI command component' do
109
+ let(:mock_component_node) { mock 'Punchblock::Component::Asterisk::AGI::Command', :name => 'EXEC ANSWER' }
110
+ let :component do
111
+ Component::Asterisk::AGICommand.new mock_component_node, subject.translator
112
+ end
113
+
114
+ let(:ami_event) do
115
+ RubyAMI::Event.new("AGIExec").tap do |e|
116
+ e["SubEvent"] = "End"
117
+ e["Channel"] = "SIP/1234-00000000"
118
+ e["CommandId"] = component.id
119
+ e["Command"] = "EXEC ANSWER"
120
+ e["ResultCode"] = "200"
121
+ e["Result"] = "Success"
122
+ e["Data"] = "FOO"
123
+ end
124
+ end
125
+
126
+ before do
127
+ subject.register_component component
128
+ end
129
+
130
+ it 'should send the event to the component' do
131
+ component.expects(:handle_ami_event!).once.with ami_event
132
+ subject.process_ami_event ami_event
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '#execute_command' do
138
+ let :expected_agi_complete_event do
139
+ Punchblock::Event::Complete.new.tap do |c|
140
+ c.reason = Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new :code => 200,
141
+ :result => 'Success',
142
+ :data => 'FOO'
143
+ end
144
+ end
145
+
146
+ before do
147
+ command.request!
148
+ end
149
+
150
+ context 'with an accept command' do
151
+ let(:command) { Command::Accept.new }
152
+
153
+ it "should send an EXEC RINGING AGI command and set the command's response" do
154
+ subject.execute_command command
155
+ agi_command = subject.actor_subject.instance_variable_get(:'@current_agi_command')
156
+ agi_command.name.should == "EXEC RINGING"
157
+ agi_command.execute!
158
+ agi_command.add_event expected_agi_complete_event
159
+ command.response(0.5).should be true
160
+ end
161
+ end
162
+
163
+ context 'with an answer command' do
164
+ let(:command) { Command::Answer.new }
165
+
166
+ it "should send an EXEC ANSWER AGI command and set the command's response" do
167
+ subject.execute_command command
168
+ agi_command = subject.actor_subject.instance_variable_get(:'@current_agi_command')
169
+ agi_command.name.should == "EXEC ANSWER"
170
+ agi_command.execute!
171
+ agi_command.add_event expected_agi_complete_event
172
+ command.response(0.5).should be true
173
+ end
174
+ end
175
+
176
+ context 'with a hangup command' do
177
+ let(:command) { Command::Hangup.new }
178
+
179
+ it "should send a Hangup AMI command and set the command's response" do
180
+ subject.execute_command command
181
+ ami_action = subject.actor_subject.instance_variable_get(:'@current_ami_action')
182
+ ami_action.name.should == "hangup"
183
+ ami_action << RubyAMI::Response.new
184
+ command.response(0.5).should be true
185
+ end
186
+ end
187
+
188
+ context 'with a component' do
189
+ let :command do
190
+ Punchblock::Component::Asterisk::AGI::Command.new :name => 'Answer'
191
+ end
192
+
193
+ let(:mock_action) { mock 'Component::Asterisk::AGI::Command', :id => 'foo' }
194
+
195
+ it 'should create a component actor and execute it asynchronously' do
196
+ Component::Asterisk::AGICommand.expects(:new).once.with(command, subject).returns mock_action
197
+ mock_action.expects(:execute!).once
198
+ subject.execute_command command
199
+ end
200
+ end
201
+ end
202
+
203
+ describe '#send_agi_action' do
204
+ it 'should send an appropriate AsyncAGI AMI action' do
205
+ pending
206
+ subject.actor_subject.expects(:send_ami_action).once.with('AGI', 'Command' => 'FOO', 'Channel' => subject.channel)
207
+ subject.send_agi_action 'FOO'
208
+ end
209
+ end
210
+
211
+ describe '#send_ami_action' do
212
+ let(:component_id) { UUIDTools::UUID.random_create }
213
+ before { UUIDTools::UUID.stubs :random_create => component_id }
214
+
215
+ it 'should send the action to the AMI client' do
216
+ action = RubyAMI::Action.new 'foo', :foo => :bar
217
+ translator.expects(:send_ami_action!).once.with action
218
+ subject.send_ami_action 'foo', :foo => :bar
219
+ end
220
+ end
15
221
  end
16
222
  end
17
223
  end
@@ -0,0 +1,143 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ module Component
7
+ module Asterisk
8
+ describe AGICommand do
9
+ let(:channel) { 'SIP/foo' }
10
+ let(:mock_call) { mock 'Call', :channel => channel }
11
+ let(:component_id) { UUIDTools::UUID.random_create }
12
+
13
+ before { UUIDTools::UUID.stubs :random_create => component_id }
14
+
15
+ let :command do
16
+ Punchblock::Component::Asterisk::AGI::Command.new :name => 'EXEC ANSWER'
17
+ end
18
+
19
+ subject { AGICommand.new command, mock_call }
20
+
21
+ let :expected_action do
22
+ RubyAMI::Action.new 'AGI', 'Channel' => channel, 'Command' => 'EXEC ANSWER', 'CommandID' => component_id
23
+ end
24
+
25
+ context 'initial execution' do
26
+ it 'should send the appropriate RubyAMI::Action' do
27
+ mock_call.expects(:send_ami_action!).once.with(expected_action).returns(expected_action)
28
+ subject.execute
29
+ end
30
+ end
31
+
32
+ context 'when the AMI action completes' do
33
+ before do
34
+ command.request!
35
+ command.execute!
36
+ end
37
+
38
+ let :expected_response do
39
+ Ref.new :id => component_id
40
+ end
41
+
42
+ let :response do
43
+ RubyAMI::Response.new.tap do |r|
44
+ r['ActionID'] = "552a9d9f-46d7-45d8-a257-06fe95f48d99"
45
+ r['Message'] = 'Added AGI command to queue'
46
+ end
47
+ end
48
+
49
+ it 'should send the component node a ref with the action ID' do
50
+ command.expects(:response=).once.with(expected_response)
51
+ subject.action << response
52
+ end
53
+
54
+ context 'with an error' do
55
+ let :error do
56
+ RubyAMI::Error.new.tap { |e| e.message = 'Action failed' }
57
+ end
58
+
59
+ it 'should send the component node false' do
60
+ command.expects(:response=).once.with false
61
+ subject.action << error
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'when receiving an AsyncAGI event' do
67
+ before do
68
+ command.request!
69
+ command.execute!
70
+ end
71
+
72
+ context 'of type start'
73
+
74
+ context 'of type Exec' do
75
+ let(:ami_event) do
76
+ RubyAMI::Event.new("AsyncAGI").tap do |e|
77
+ e["SubEvent"] = "Exec"
78
+ e["Channel"] = channel
79
+ e["CommandId"] = component_id
80
+ e["Command"] = "EXEC ANSWER"
81
+ e["Result"] = "200%20result=123%20(timeout)%0A"
82
+ end
83
+ end
84
+
85
+ let :expected_complete_reason do
86
+ Punchblock::Component::Asterisk::AGI::Command::Complete::Success.new :code => 200,
87
+ :result => 123,
88
+ :data => 'timeout'
89
+ end
90
+
91
+ it 'should send a complete event' do
92
+ subject.handle_ami_event ami_event
93
+
94
+ command.should be_complete
95
+
96
+ complete_event = command.complete_event.resource(0.5)
97
+
98
+ complete_event.component_id.should == subject.id
99
+ complete_event.reason.should == expected_complete_reason
100
+ end
101
+ end
102
+ end
103
+
104
+ describe '#parse_agi_result' do
105
+ context 'with a simple result with no data' do
106
+ let(:result_string) { "200%20result=123%0A" }
107
+
108
+ it 'should provide the code and result' do
109
+ code, result, data = subject.parse_agi_result result_string
110
+ code.should == 200
111
+ result.should == 123
112
+ data.should == ''
113
+ end
114
+ end
115
+
116
+ context 'with a result and data in parens' do
117
+ let(:result_string) { "200%20result=-123%20(timeout)%0A" }
118
+
119
+ it 'should provide the code and result' do
120
+ code, result, data = subject.parse_agi_result result_string
121
+ code.should == 200
122
+ result.should == -123
123
+ data.should == 'timeout'
124
+ end
125
+ end
126
+
127
+ context 'with a result and key-value data' do
128
+ let(:result_string) { "200%20result=123%20foo=bar%0A" }
129
+
130
+ it 'should provide the code and result' do
131
+ code, result, data = subject.parse_agi_result result_string
132
+ code.should == 200
133
+ result.should == 123
134
+ data.should == 'foo=bar'
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end