punchblock 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/lib/punchblock.rb +1 -1
  3. data/lib/punchblock/connection.rb +1 -0
  4. data/lib/punchblock/connection/asterisk.rb +0 -1
  5. data/lib/punchblock/connection/freeswitch.rb +49 -0
  6. data/lib/punchblock/event/offer.rb +1 -1
  7. data/lib/punchblock/translator.rb +5 -0
  8. data/lib/punchblock/translator/asterisk.rb +16 -28
  9. data/lib/punchblock/translator/asterisk/call.rb +4 -21
  10. data/lib/punchblock/translator/asterisk/component.rb +0 -5
  11. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +0 -3
  12. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +0 -1
  13. data/lib/punchblock/translator/asterisk/component/input.rb +7 -97
  14. data/lib/punchblock/translator/asterisk/component/output.rb +0 -4
  15. data/lib/punchblock/translator/asterisk/component/record.rb +0 -2
  16. data/lib/punchblock/translator/freeswitch.rb +153 -0
  17. data/lib/punchblock/translator/freeswitch/call.rb +265 -0
  18. data/lib/punchblock/translator/freeswitch/component.rb +92 -0
  19. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +57 -0
  20. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +17 -0
  21. data/lib/punchblock/translator/freeswitch/component/input.rb +29 -0
  22. data/lib/punchblock/translator/freeswitch/component/output.rb +56 -0
  23. data/lib/punchblock/translator/freeswitch/component/record.rb +79 -0
  24. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +26 -0
  25. data/lib/punchblock/translator/input_component.rb +108 -0
  26. data/lib/punchblock/version.rb +1 -1
  27. data/punchblock.gemspec +3 -2
  28. data/spec/punchblock/connection/freeswitch_spec.rb +90 -0
  29. data/spec/punchblock/translator/asterisk/call_spec.rb +23 -2
  30. data/spec/punchblock/translator/asterisk/component/input_spec.rb +3 -3
  31. data/spec/punchblock/translator/asterisk_spec.rb +1 -1
  32. data/spec/punchblock/translator/freeswitch/call_spec.rb +922 -0
  33. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +279 -0
  34. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +312 -0
  35. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +369 -0
  36. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +373 -0
  37. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +285 -0
  38. data/spec/punchblock/translator/freeswitch/component_spec.rb +118 -0
  39. data/spec/punchblock/translator/freeswitch_spec.rb +597 -0
  40. data/spec/punchblock_spec.rb +11 -0
  41. data/spec/spec_helper.rb +1 -0
  42. metadata +52 -7
@@ -0,0 +1,279 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Freeswitch
8
+ module Component
9
+ describe FliteOutput do
10
+ let(:connection) do
11
+ mock_connection_with_event_handler do |event|
12
+ original_command.add_event event
13
+ end
14
+ end
15
+ let(:media_engine) { :flite }
16
+ let(:default_voice) { nil }
17
+ let(:translator) { Punchblock::Translator::Freeswitch.new connection }
18
+ let(:mock_call) { Punchblock::Translator::Freeswitch::Call.new 'foo', translator }
19
+
20
+ let :original_command do
21
+ Punchblock::Component::Output.new command_options
22
+ end
23
+
24
+ let :ssml_doc do
25
+ RubySpeech::SSML.draw do
26
+ say_as(:interpret_as => :cardinal) { 'FOO' }
27
+ end
28
+ end
29
+
30
+ let :command_options do
31
+ { :ssml => ssml_doc }
32
+ end
33
+
34
+ def execute
35
+ subject.execute media_engine, default_voice
36
+ end
37
+
38
+ subject { described_class.new original_command, mock_call }
39
+
40
+ describe '#execute' do
41
+ before { original_command.request! }
42
+ def expect_playback(voice = :kal)
43
+ subject.wrapped_object.expects(:application).once.with :speak, "#{media_engine}|#{voice}|FOO"
44
+ end
45
+
46
+ let(:command_opts) { {} }
47
+
48
+ let :command_options do
49
+ { :ssml => ssml_doc }.merge(command_opts)
50
+ end
51
+
52
+ let :original_command do
53
+ Punchblock::Component::Output.new command_options
54
+ end
55
+
56
+ describe 'ssml' do
57
+ context 'unset' do
58
+ let(:command_opts) { { :ssml => nil } }
59
+ it "should return an error and not execute any actions" do
60
+ execute
61
+ error = ProtocolError.new.setup 'option error', 'An SSML document is required.'
62
+ original_command.response(0.1).should be == error
63
+ end
64
+ end
65
+
66
+ context 'with an SSML node' do
67
+ it 'should speak the document using the speak application' do
68
+ expect_playback
69
+ execute
70
+ end
71
+
72
+ it 'should send a complete event when the speak finishes' do
73
+ expect_playback.yields true
74
+ execute
75
+ subject.handle_es_event RubyFS::Event.new(nil, :event_name => "CHANNEL_EXECUTE_COMPLETE")
76
+ original_command.complete_event(0.1).reason.should be_a Punchblock::Component::Output::Complete::Success
77
+ end
78
+ end
79
+ end
80
+
81
+ describe 'start-offset' do
82
+ context 'unset' do
83
+ let(:command_opts) { { :start_offset => nil } }
84
+ it 'should not pass any options to Playback' do
85
+ expect_playback
86
+ execute
87
+ end
88
+ end
89
+
90
+ context 'set' do
91
+ let(:command_opts) { { :start_offset => 10 } }
92
+ it "should return an error and not execute any actions" do
93
+ execute
94
+ error = ProtocolError.new.setup 'option error', 'A start_offset value is unsupported.'
95
+ original_command.response(0.1).should be == error
96
+ end
97
+ end
98
+ end
99
+
100
+ describe 'start-paused' do
101
+ context 'false' do
102
+ let(:command_opts) { { :start_paused => false } }
103
+ it 'should not pass any options to Playback' do
104
+ expect_playback
105
+ execute
106
+ end
107
+ end
108
+
109
+ context 'true' do
110
+ let(:command_opts) { { :start_paused => true } }
111
+ it "should return an error and not execute any actions" do
112
+ execute
113
+ error = ProtocolError.new.setup 'option error', 'A start_paused value is unsupported.'
114
+ original_command.response(0.1).should be == error
115
+ end
116
+ end
117
+ end
118
+
119
+ describe 'repeat-interval' do
120
+ context 'unset' do
121
+ let(:command_opts) { { :repeat_interval => nil } }
122
+ it 'should not pass any options to Playback' do
123
+ expect_playback
124
+ execute
125
+ end
126
+ end
127
+
128
+ context 'set' do
129
+ let(:command_opts) { { :repeat_interval => 10 } }
130
+ it "should return an error and not execute any actions" do
131
+ execute
132
+ error = ProtocolError.new.setup 'option error', 'A repeat_interval value is unsupported.'
133
+ original_command.response(0.1).should be == error
134
+ end
135
+ end
136
+ end
137
+
138
+ describe 'repeat-times' do
139
+ context 'unset' do
140
+ let(:command_opts) { { :repeat_times => nil } }
141
+ it 'should not pass any options to Playback' do
142
+ expect_playback
143
+ execute
144
+ end
145
+ end
146
+
147
+ context 'set' do
148
+ let(:command_opts) { { :repeat_times => 2 } }
149
+ it "should return an error and not execute any actions" do
150
+ execute
151
+ error = ProtocolError.new.setup 'option error', 'A repeat_times value is unsupported.'
152
+ original_command.response(0.1).should be == error
153
+ end
154
+ end
155
+ end
156
+
157
+ describe 'max-time' do
158
+ context 'unset' do
159
+ let(:command_opts) { { :max_time => nil } }
160
+ it 'should not pass any options to Playback' do
161
+ expect_playback
162
+ execute
163
+ end
164
+ end
165
+
166
+ context 'set' do
167
+ let(:command_opts) { { :max_time => 30 } }
168
+ it "should return an error and not execute any actions" do
169
+ execute
170
+ error = ProtocolError.new.setup 'option error', 'A max_time value is unsupported.'
171
+ original_command.response(0.1).should be == error
172
+ end
173
+ end
174
+ end
175
+
176
+ describe 'voice' do
177
+ context 'unset' do
178
+ let(:command_opts) { { :voice => nil } }
179
+ it 'should use the default voice' do
180
+ expect_playback
181
+ execute
182
+ end
183
+ end
184
+
185
+ context 'set' do
186
+ let(:command_opts) { { :voice => 'alison' } }
187
+ it "should execute speak with the specified voice" do
188
+ expect_playback 'alison'
189
+ execute
190
+ end
191
+ end
192
+ end
193
+
194
+ describe 'interrupt_on' do
195
+ context "set to nil" do
196
+ let(:command_opts) { { :interrupt_on => nil } }
197
+ it "should not pass any digits to Playback" do
198
+ expect_playback
199
+ execute
200
+ end
201
+ end
202
+
203
+ context "set to :any" do
204
+ let(:command_opts) { { :interrupt_on => :any } }
205
+ it "should return an error and not execute any actions" do
206
+ execute
207
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of any is unsupported.'
208
+ original_command.response(0.1).should be == error
209
+ end
210
+ end
211
+
212
+ context "set to :dtmf" do
213
+ let(:command_opts) { { :interrupt_on => :dtmf } }
214
+ it "should return an error and not execute any actions" do
215
+ execute
216
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of dtmf is unsupported.'
217
+ original_command.response(0.1).should be == error
218
+ end
219
+ end
220
+
221
+ context "set to :speech" do
222
+ let(:command_opts) { { :interrupt_on => :speech } }
223
+ it "should return an error and not execute any actions" do
224
+ execute
225
+ error = ProtocolError.new.setup 'option error', 'An interrupt-on value of speech is unsupported.'
226
+ original_command.response(0.1).should be == error
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ describe "#execute_command" do
233
+ context "with a command it does not understand" do
234
+ let(:command) { Punchblock::Component::Output::Pause.new }
235
+
236
+ before { command.request! }
237
+
238
+ it "returns a ProtocolError response" do
239
+ subject.execute_command command
240
+ command.response(0.1).should be_a ProtocolError
241
+ end
242
+ end
243
+
244
+ context "with a Stop command" do
245
+ let(:command) { Punchblock::Component::Stop.new }
246
+ let(:reason) { original_command.complete_event(5).reason }
247
+
248
+ before do
249
+ command.request!
250
+ original_command.request!
251
+ original_command.execute!
252
+ end
253
+
254
+ it "sets the command response to true" do
255
+ subject.wrapped_object.expects(:application)
256
+ subject.execute_command command
257
+ command.response(0.1).should be == true
258
+ end
259
+
260
+ it "sends the correct complete event" do
261
+ subject.wrapped_object.expects(:application)
262
+ original_command.should_not be_complete
263
+ subject.execute_command command
264
+ reason.should be_a Punchblock::Event::Complete::Stop
265
+ original_command.should be_complete
266
+ end
267
+
268
+ it "breaks the current dialplan application" do
269
+ subject.wrapped_object.expects(:application).once.with 'break'
270
+ subject.execute_command command
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,312 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Punchblock
6
+ module Translator
7
+ class Freeswitch
8
+ module Component
9
+ describe Input do
10
+ let(:connection) do
11
+ mock_connection_with_event_handler do |event|
12
+ original_command.add_event event
13
+ end
14
+ end
15
+ let(:id) { Punchblock.new_uuid }
16
+ let(:translator) { Punchblock::Translator::Freeswitch.new connection }
17
+ let(:mock_stream) { mock('RubyFS::Stream') }
18
+ let(:call) { Punchblock::Translator::Freeswitch::Call.new id, translator, nil, mock_stream }
19
+
20
+ let(:original_command_options) { {} }
21
+
22
+ let :original_command do
23
+ Punchblock::Component::Input.new original_command_options
24
+ end
25
+
26
+ let :grammar do
27
+ RubySpeech::GRXML.draw :mode => 'dtmf', :root => 'pin' do
28
+ rule id: 'digit' do
29
+ one_of do
30
+ 0.upto(9) { |d| item { d.to_s } }
31
+ end
32
+ end
33
+
34
+ rule id: 'pin', scope: 'public' do
35
+ item repeat: '2' do
36
+ ruleref uri: '#digit'
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ subject { Input.new original_command, call }
43
+
44
+ describe '#execute' do
45
+ before { original_command.request! }
46
+
47
+ let(:original_command_opts) { {} }
48
+
49
+ let :original_command_options do
50
+ { :mode => :dtmf, :grammar => { :value => grammar } }.merge(original_command_opts)
51
+ end
52
+
53
+ def dtmf_event(digit)
54
+ RubyFS::Event.new nil, {
55
+ :event_name => 'DTMF',
56
+ :dtmf_digit => digit.to_s,
57
+ :dtmf_duration => "1600"
58
+ }
59
+ end
60
+
61
+ def send_dtmf(digit)
62
+ call.handle_es_event dtmf_event(digit)
63
+ end
64
+
65
+ let(:reason) { original_command.complete_event(5).reason }
66
+
67
+ describe "receiving DTMF events" do
68
+ before do
69
+ subject.execute
70
+ expected_event
71
+ end
72
+
73
+ context "when a match is found" do
74
+ before do
75
+ send_dtmf 1
76
+ send_dtmf 2
77
+ end
78
+
79
+ let :expected_event do
80
+ Punchblock::Component::Input::Complete::Success.new :mode => :dtmf,
81
+ :confidence => 1,
82
+ :utterance => '12',
83
+ :interpretation => 'dtmf-1 dtmf-2',
84
+ :component_id => subject.id,
85
+ :target_call_id => call.id
86
+ end
87
+
88
+ it "should send a success complete event with the relevant data" do
89
+ reason.should be == expected_event
90
+ end
91
+
92
+ it "should not process further dtmf events" do
93
+ subject.expects(:process_dtmf!).never
94
+ send_dtmf 3
95
+ end
96
+ end
97
+
98
+ context "when the match is invalid" do
99
+ before do
100
+ send_dtmf 1
101
+ send_dtmf '#'
102
+ end
103
+
104
+ let :expected_event do
105
+ Punchblock::Component::Input::Complete::NoMatch.new :component_id => subject.id,
106
+ :target_call_id => call.id
107
+ end
108
+
109
+ it "should send a nomatch complete event" do
110
+ reason.should be == expected_event
111
+ end
112
+ end
113
+ end
114
+
115
+ describe 'grammar' do
116
+ context 'unset' do
117
+ let(:original_command_opts) { { :grammar => nil } }
118
+ it "should return an error and not execute any actions" do
119
+ subject.execute
120
+ error = ProtocolError.new.setup 'option error', 'A grammar document is required.'
121
+ original_command.response(0.1).should be == error
122
+ end
123
+ end
124
+ end
125
+
126
+ describe 'mode' do
127
+ context 'unset' do
128
+ let(:original_command_opts) { { :mode => nil } }
129
+ it "should return an error and not execute any actions" do
130
+ subject.execute
131
+ error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.'
132
+ original_command.response(0.1).should be == error
133
+ end
134
+ end
135
+
136
+ context 'any' do
137
+ let(:original_command_opts) { { :mode => :any } }
138
+ it "should return an error and not execute any actions" do
139
+ subject.execute
140
+ error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.'
141
+ original_command.response(0.1).should be == error
142
+ end
143
+ end
144
+
145
+ context 'speech' do
146
+ let(:original_command_opts) { { :mode => :speech } }
147
+ it "should return an error and not execute any actions" do
148
+ subject.execute
149
+ error = ProtocolError.new.setup 'option error', 'A mode value other than DTMF is unsupported.'
150
+ original_command.response(0.1).should be == error
151
+ end
152
+ end
153
+ end
154
+
155
+ describe 'terminator' do
156
+ pending
157
+ end
158
+
159
+ describe 'recognizer' do
160
+ pending
161
+ end
162
+
163
+ describe 'initial-timeout' do
164
+ context 'a positive number' do
165
+ let(:original_command_opts) { { :initial_timeout => 1000 } }
166
+
167
+ it "should not cause a NoInput if first input is received in time" do
168
+ subject.execute
169
+ send_dtmf 1
170
+ sleep 1.5
171
+ send_dtmf 2
172
+ reason.should be_a Punchblock::Component::Input::Complete::Success
173
+ end
174
+
175
+ it "should cause a NoInput complete event to be sent after the timeout" do
176
+ subject.execute
177
+ sleep 1.5
178
+ send_dtmf 1
179
+ send_dtmf 2
180
+ reason.should be_a Punchblock::Component::Input::Complete::NoInput
181
+ end
182
+ end
183
+
184
+ context '-1' do
185
+ let(:original_command_opts) { { :initial_timeout => -1 } }
186
+
187
+ it "should not start a timer" do
188
+ subject.wrapped_object.expects(:begin_initial_timer).never
189
+ subject.execute
190
+ end
191
+ end
192
+
193
+ context 'unset' do
194
+ let(:original_command_opts) { { :initial_timeout => nil } }
195
+
196
+ it "should not start a timer" do
197
+ subject.wrapped_object.expects(:begin_initial_timer).never
198
+ subject.execute
199
+ end
200
+ end
201
+
202
+ context 'a negative number other than -1' do
203
+ let(:original_command_opts) { { :initial_timeout => -1000 } }
204
+
205
+ it "should return an error and not execute any actions" do
206
+ subject.execute
207
+ error = ProtocolError.new.setup 'option error', 'An initial timeout value that is negative (and not -1) is invalid.'
208
+ original_command.response(0.1).should be == error
209
+ end
210
+ end
211
+ end
212
+
213
+ describe 'inter-digit-timeout' do
214
+ context 'a positive number' do
215
+ let(:original_command_opts) { { :inter_digit_timeout => 1000 } }
216
+
217
+ it "should not prevent a Match if input is received in time" do
218
+ subject.execute
219
+ sleep 1.5
220
+ send_dtmf 1
221
+ sleep 0.5
222
+ send_dtmf 2
223
+ reason.should be_a Punchblock::Component::Input::Complete::Success
224
+ end
225
+
226
+ it "should cause a NoMatch complete event to be sent after the timeout" do
227
+ subject.execute
228
+ sleep 1.5
229
+ send_dtmf 1
230
+ sleep 1.5
231
+ send_dtmf 2
232
+ reason.should be_a Punchblock::Component::Input::Complete::NoMatch
233
+ end
234
+ end
235
+
236
+ context '-1' do
237
+ let(:original_command_opts) { { :inter_digit_timeout => -1 } }
238
+
239
+ it "should not start a timer" do
240
+ subject.wrapped_object.expects(:begin_inter_digit_timer).never
241
+ subject.execute
242
+ end
243
+ end
244
+
245
+ context 'unset' do
246
+ let(:original_command_opts) { { :inter_digit_timeout => nil } }
247
+
248
+ it "should not start a timer" do
249
+ subject.wrapped_object.expects(:begin_inter_digit_timer).never
250
+ subject.execute
251
+ end
252
+ end
253
+
254
+ context 'a negative number other than -1' do
255
+ let(:original_command_opts) { { :inter_digit_timeout => -1000 } }
256
+
257
+ it "should return an error and not execute any actions" do
258
+ subject.execute
259
+ error = ProtocolError.new.setup 'option error', 'An inter-digit timeout value that is negative (and not -1) is invalid.'
260
+ original_command.response(0.1).should be == error
261
+ end
262
+ end
263
+ end
264
+
265
+ describe 'sensitivity' do
266
+ pending
267
+ end
268
+
269
+ describe 'min-confidence' do
270
+ pending
271
+ end
272
+
273
+ describe 'max-silence' do
274
+ pending
275
+ end
276
+ end
277
+
278
+ describe "#execute_command" do
279
+ context "with a command it does not understand" do
280
+ let(:command) { Punchblock::Component::Output::Pause.new }
281
+
282
+ before { command.request! }
283
+
284
+ it "returns a ProtocolError response" do
285
+ subject.execute_command command
286
+ command.response(0.1).should be_a ProtocolError
287
+ end
288
+ end
289
+
290
+ context "with a Stop command" do
291
+ let(:command) { Punchblock::Component::Stop.new }
292
+ let(:reason) { original_command.complete_event(5).reason }
293
+
294
+ before do
295
+ command.request!
296
+ original_command.request!
297
+ original_command.execute!
298
+ end
299
+
300
+ it "sets the command response to true" do
301
+ subject.execute_command command
302
+ command.response(0.1).should be == true
303
+ reason.should be_a Punchblock::Event::Complete::Stop
304
+ end
305
+ end
306
+ end
307
+
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end