punchblock 1.3.0 → 1.4.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.
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