punchblock 0.8.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,116 +0,0 @@
1
- module Punchblock
2
- module Translator
3
- class Asterisk
4
- module Component
5
- module Asterisk
6
- class Input < Component
7
-
8
- attr_reader :grammar, :buffer
9
-
10
- def setup
11
- @media_engine = call.translator.media_engine
12
- @buffer = ""
13
- end
14
-
15
- def execute
16
- initial_timeout = @component_node.initial_timeout || -1
17
- @inter_digit_timeout = @component_node.inter_digit_timeout || -1
18
-
19
- return with_error 'option error', 'A grammar document is required.' unless @component_node.grammar
20
- return with_error 'option error', 'A mode value other than DTMF is unsupported on Asterisk.' unless @component_node.mode == :dtmf
21
- return with_error 'option error', 'An initial timeout value that is negative (and not -1) is invalid.' unless initial_timeout >= -1
22
- return with_error 'option error', 'An inter-digit timeout value that is negative (and not -1) is invalid.' unless @inter_digit_timeout >= -1
23
-
24
- send_ref
25
-
26
- case @media_engine
27
- when :asterisk, nil
28
- @grammar = @component_node.grammar.value.clone
29
- grammar.inline!
30
- grammar.tokenize!
31
- grammar.normalize_whitespace
32
-
33
- begin_initial_timer initial_timeout/1000 unless initial_timeout == -1
34
-
35
- component = current_actor
36
-
37
- @active = true
38
-
39
- call.register_handler :ami, :name => 'DTMF' do |event|
40
- component.process_dtmf! event['Digit'] if event['End'] == 'Yes'
41
- end
42
- end
43
- end
44
-
45
- def process_dtmf(digit)
46
- return unless @active
47
- pb_logger.trace "Processing incoming DTMF digit #{digit}"
48
- buffer << digit
49
- cancel_initial_timer
50
- case (match = grammar.match buffer.dup)
51
- when RubySpeech::GRXML::Match
52
- pb_logger.trace "Found a match against buffer #{buffer}"
53
- complete success_reason(match)
54
- when RubySpeech::GRXML::NoMatch
55
- pb_logger.trace "Buffer #{buffer} does not match grammar"
56
- complete Punchblock::Component::Input::Complete::NoMatch.new
57
- when RubySpeech::GRXML::PotentialMatch
58
- pb_logger.trace "Buffer #{buffer} potentially matches grammar. Waiting..."
59
- reset_inter_digit_timer
60
- end
61
- end
62
-
63
- private
64
-
65
- def begin_initial_timer(timeout)
66
- pb_logger.trace "Setting initial timer for #{timeout} seconds"
67
- @initial_timer = after timeout do
68
- pb_logger.trace "Initial timer expired."
69
- complete Punchblock::Component::Input::Complete::NoInput.new
70
- end
71
- end
72
-
73
- def cancel_initial_timer
74
- return unless @initial_timer
75
- @initial_timer.cancel
76
- @initial_timer = nil
77
- end
78
-
79
- def reset_inter_digit_timer
80
- return if @inter_digit_timeout == -1
81
- @inter_digit_timer ||= begin
82
- pb_logger.trace "Setting inter-digit timer for #{@inter_digit_timeout/1000} seconds"
83
- after @inter_digit_timeout/1000 do
84
- pb_logger.trace "Inter digit-timer expired."
85
- complete Punchblock::Component::Input::Complete::NoMatch.new
86
- end
87
- end
88
- pb_logger.trace "Resetting inter-digit timer"
89
- @inter_digit_timer.reset
90
- end
91
-
92
- def cancel_inter_digit_timer
93
- return unless @inter_digit_timer
94
- @inter_digit_timer.cancel
95
- @inter_digit_timer = nil
96
- end
97
-
98
- def success_reason(match)
99
- Punchblock::Component::Input::Complete::Success.new :mode => match.mode,
100
- :confidence => match.confidence,
101
- :utterance => match.utterance,
102
- :interpretation => match.interpretation
103
- end
104
-
105
- def complete(reason)
106
- @active = false
107
- cancel_initial_timer
108
- cancel_inter_digit_timer
109
- send_complete_event reason
110
- end
111
- end
112
- end
113
- end
114
- end
115
- end
116
- end
@@ -1,94 +0,0 @@
1
- require 'active_support/core_ext/string/filters'
2
-
3
- module Punchblock
4
- module Translator
5
- class Asterisk
6
- module Component
7
- module Asterisk
8
- class Output < Component
9
-
10
- def setup
11
- @media_engine = @call.translator.media_engine
12
- end
13
-
14
- def execute
15
- return with_error 'option error', 'An SSML document is required.' unless @component_node.ssml
16
-
17
- return with_error 'option error', 'An interrupt-on value of speech is unsupported.' if @component_node.interrupt_on == :speech
18
-
19
- [:start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time].each do |opt|
20
- return with_error 'option error', "A #{opt} value is unsupported on Asterisk." if @component_node.send opt
21
- end
22
-
23
- case @media_engine
24
- when :asterisk, nil
25
- return with_error 'option error', "A voice value is unsupported on Asterisk." if @component_node.voice
26
-
27
- @execution_elements = @component_node.ssml.children.map do |node|
28
- case node
29
- when RubySpeech::SSML::Audio
30
- lambda { current_actor.play_audio! node.src }
31
- end
32
- end.compact
33
-
34
- @pending_actions = @execution_elements.count
35
-
36
- send_ref
37
-
38
- @interrupt_digits = '0123456789*#' if [:any, :dtmf].include? @component_node.interrupt_on
39
-
40
- @execution_elements.each do |element|
41
- element.call
42
- wait :continue
43
- process_playback_completion
44
- end
45
- when :unimrcp
46
- doc = @component_node.ssml.to_s.squish.gsub(/["\\]/) { |m| "\\#{m}" }
47
- send_ref
48
- @call.send_agi_action! 'EXEC MRCPSynth', doc, mrcpsynth_options do |complete_event|
49
- pb_logger.debug "MRCPSynth completed with #{complete_event}."
50
- send_complete_event success_reason
51
- end
52
- end
53
- end
54
-
55
- def process_playback_completion
56
- @pending_actions -= 1
57
- pb_logger.debug "Received action completion. Now waiting on #{@pending_actions} actions."
58
- if @pending_actions < 1
59
- pb_logger.debug "Sending complete event"
60
- send_complete_event success_reason
61
- end
62
- end
63
-
64
- def continue(event = nil)
65
- signal :continue, event
66
- end
67
-
68
- def play_audio(path)
69
- pb_logger.debug "Playing an audio file (#{path}) via STREAM FILE"
70
- op = current_actor
71
- @call.send_agi_action! 'STREAM FILE', path, @interrupt_digits do |complete_event|
72
- pb_logger.debug "STREAM FILE completed with #{complete_event}. Signalling to continue execution."
73
- op.continue! complete_event
74
- end
75
- end
76
-
77
- private
78
-
79
- def mrcpsynth_options
80
- [].tap do |opts|
81
- opts << 'i=any' if [:any, :dtmf].include? @component_node.interrupt_on
82
- opts << "v=#{@component_node.voice}" if @component_node.voice
83
- end.join '&'
84
- end
85
-
86
- def success_reason
87
- Punchblock::Component::Output::Complete::Success.new
88
- end
89
- end
90
- end
91
- end
92
- end
93
- end
94
- end
@@ -1,361 +0,0 @@
1
- require 'spec_helper'
2
-
3
- module Punchblock
4
- module Component
5
- module Tropo
6
- describe Conference do
7
- it 'registers itself' do
8
- RayoNode.class_from_registration(:conference, 'urn:xmpp:tropo:conference:1').should == Conference
9
- end
10
-
11
- describe "when setting options in initializer" do
12
- subject do
13
- Conference.new :name => '1234',
14
- :terminator => '#',
15
- :moderator => true,
16
- :tone_passthrough => true,
17
- :mute => false,
18
- :announcement => {:text => "Welcome to Rayo", :voice => 'shelly'},
19
- :music => {:text => "The moderator how not yet joined.. Listen to this awesome music while you wait.", :voice => 'frank'}
20
- end
21
-
22
- its(:name) { should == '1234' }
23
- its(:mute) { should == false }
24
- its(:terminator) { should == '#' }
25
- its(:tone_passthrough) { should == true }
26
- its(:moderator) { should == true }
27
- its(:announcement) { should == Conference::Announcement.new(:text => "Welcome to Rayo", :voice => 'shelly') }
28
- its(:music) { should == Conference::Music.new(:text => "The moderator how not yet joined.. Listen to this awesome music while you wait.", :voice => 'frank') }
29
- end
30
-
31
- its(:mute_status_name) { should == :unknown_mute }
32
- its(:hold_status_name) { should == :unknown_hold }
33
-
34
- describe "#==" do
35
- subject { Conference.new :name => 'the-conference' }
36
- let(:conference2) { Conference.new :name => 'the-conference' }
37
- let(:conference3) { Conference.new :name => 'other-conference' }
38
-
39
- it { should == conference2 }
40
- it { should_not == conference3 }
41
- end
42
-
43
- describe "#add_event" do
44
- describe "with an on-hold" do
45
- it "should call #onhold!" do
46
- subject.expects(:onhold!).once
47
- subject.add_event Conference::OnHold.new
48
- end
49
- end
50
-
51
- describe "with an off-hold" do
52
- it "should call #offhold!" do
53
- subject.expects(:offhold!).once
54
- subject.add_event Conference::OffHold.new
55
- end
56
- end
57
- end
58
-
59
- describe "#requested" do
60
- context "when requesting to be muted" do
61
- subject { Conference.new :mute => true }
62
- before { subject.request! }
63
- its(:mute_status_name) { should == :muted }
64
- end
65
-
66
- context "when requesting not to be muted" do
67
- subject { Conference.new :mute => false }
68
- before { subject.request! }
69
- its(:mute_status_name) { should == :unmuted }
70
- end
71
- end
72
-
73
- describe "#onhold!" do
74
- before do
75
- subject.onhold!
76
- end
77
-
78
- its(:hold_status_name) { should == :onhold }
79
-
80
- it "should raise a StateMachine::InvalidTransition when received a second time" do
81
- lambda { subject.onhold! }.should raise_error(StateMachine::InvalidTransition)
82
- end
83
- end
84
-
85
- describe "#offhold!" do
86
- before do
87
- subject.onhold!
88
- subject.offhold!
89
- end
90
-
91
- its(:hold_status_name) { should == :offhold }
92
-
93
- it "should raise a StateMachine::InvalidTransition when received a second time" do
94
- lambda { subject.offhold! }.should raise_error(StateMachine::InvalidTransition)
95
- end
96
- end
97
-
98
- describe "actions" do
99
- let(:mock_client) { mock 'Client' }
100
- let(:conference) { Conference.new :name => '1234' }
101
-
102
- before do
103
- conference.component_id = 'abc123'
104
- conference.call_id = '123abc'
105
- conference.client = mock_client
106
- end
107
-
108
- describe '#mute_action' do
109
- subject { conference.mute_action }
110
-
111
- it { should be_a Command::Mute }
112
- its(:component_id) { should == 'abc123' }
113
- its(:call_id) { should == '123abc' }
114
- end
115
-
116
- describe '#mute!' do
117
- describe "when unmuted" do
118
- before do
119
- conference.request!
120
- conference.execute!
121
- end
122
-
123
- it "should send its command properly" do
124
- mock_client.expects(:execute_command).with(conference.mute_action, :call_id => '123abc', :component_id => 'abc123').returns true
125
- conference.expects :muted!
126
- conference.mute!
127
- end
128
- end
129
-
130
- describe "when muted" do
131
- before { conference.muted! }
132
-
133
- it "should raise an error" do
134
- lambda { conference.mute! }.should raise_error(InvalidActionError, "Cannot mute a Conference that is already muted")
135
- end
136
- end
137
- end
138
-
139
- describe "#muted!" do
140
- before do
141
- subject.request!
142
- subject.execute!
143
- subject.muted!
144
- end
145
-
146
- its(:mute_status_name) { should == :muted }
147
-
148
- it "should raise a StateMachine::InvalidTransition when received a second time" do
149
- lambda { subject.muted! }.should raise_error(StateMachine::InvalidTransition)
150
- end
151
- end
152
-
153
- describe '#unmute_action' do
154
- subject { conference.unmute_action }
155
-
156
- it { should be_a Command::Unmute }
157
- its(:component_id) { should == 'abc123' }
158
- its(:call_id) { should == '123abc' }
159
- end
160
-
161
- describe '#unmute!' do
162
- before do
163
- conference.request!
164
- conference.execute!
165
- end
166
-
167
- describe "when muted" do
168
- before do
169
- conference.muted!
170
- end
171
-
172
- it "should send its command properly" do
173
- mock_client.expects(:execute_command).with(conference.unmute_action, :call_id => '123abc', :component_id => 'abc123').returns true
174
- conference.expects :unmuted!
175
- conference.unmute!
176
- end
177
- end
178
-
179
- describe "when unmuted" do
180
- it "should raise an error" do
181
- lambda { conference.unmute! }.should raise_error(InvalidActionError, "Cannot unmute a Conference that is not muted")
182
- end
183
- end
184
- end
185
-
186
- describe "#unmuted!" do
187
- before do
188
- subject.request!
189
- subject.execute!
190
- subject.muted!
191
- subject.unmuted!
192
- end
193
-
194
- its(:mute_status_name) { should == :unmuted }
195
-
196
- it "should raise a StateMachine::InvalidTransition when received a second time" do
197
- lambda { subject.unmuted! }.should raise_error(StateMachine::InvalidTransition)
198
- end
199
- end
200
-
201
- describe '#stop_action' do
202
- subject { conference.stop_action }
203
-
204
- its(:to_xml) { should == '<stop xmlns="urn:xmpp:rayo:1"/>' }
205
- its(:component_id) { should == 'abc123' }
206
- its(:call_id) { should == '123abc' }
207
- end
208
-
209
- describe '#stop!' do
210
- describe "when the command is executing" do
211
- before do
212
- conference.request!
213
- conference.execute!
214
- end
215
-
216
- it "should send its command properly" do
217
- mock_client.expects(:execute_command).with(conference.stop_action, :call_id => '123abc', :component_id => 'abc123')
218
- conference.stop!
219
- end
220
- end
221
-
222
- describe "when the command is not executing" do
223
- it "should raise an error" do
224
- lambda { conference.stop! }.should raise_error(InvalidActionError, "Cannot stop a Conference that is not executing")
225
- end
226
- end
227
- end # describe #stop!
228
-
229
- describe '#kick_action' do
230
- subject { conference.kick_action :message => 'bye!' }
231
-
232
- its(:to_xml) { should == '<kick xmlns="urn:xmpp:tropo:conference:1">bye!</kick>' }
233
- its(:component_id) { should == 'abc123' }
234
- its(:call_id) { should == '123abc' }
235
- end
236
-
237
- describe '#kick!' do
238
- describe "when the command is executing" do
239
- before do
240
- conference.request!
241
- conference.execute!
242
- end
243
-
244
- it "should send its command properly" do
245
- mock_client.expects(:execute_command).with(conference.kick_action(:message => 'bye!'), :call_id => '123abc', :component_id => 'abc123')
246
- conference.kick! :message => 'bye!'
247
- end
248
- end
249
-
250
- describe "when the command is not executing" do
251
- it "should raise an error" do
252
- lambda { conference.kick! }.should raise_error(InvalidActionError, "Cannot kick a Conference that is not executing")
253
- end
254
- end
255
- end # describe #kick!
256
- end
257
-
258
- describe Conference::OnHold do
259
- it 'registers itself' do
260
- RayoNode.class_from_registration(:'on-hold', 'urn:xmpp:tropo:conference:1').should == Conference::OnHold
261
- end
262
-
263
- describe "from a stanza" do
264
- let(:stanza) { "<on-hold xmlns='urn:xmpp:tropo:conference:1'/>" }
265
-
266
- subject { RayoNode.import parse_stanza(stanza).root, '9f00061', '1' }
267
-
268
- it { should be_instance_of Conference::OnHold }
269
-
270
- it_should_behave_like 'event'
271
- end
272
- end
273
-
274
- describe Conference::OffHold do
275
- it 'registers itself' do
276
- RayoNode.class_from_registration(:'off-hold', 'urn:xmpp:tropo:conference:1').should == Conference::OffHold
277
- end
278
-
279
- describe "from a stanza" do
280
- let(:stanza) { "<off-hold xmlns='urn:xmpp:tropo:conference:1'/>" }
281
-
282
- subject { RayoNode.import parse_stanza(stanza).root, '9f00061', '1' }
283
-
284
- it { should be_instance_of Conference::OffHold }
285
-
286
- it_should_behave_like 'event'
287
- end
288
- end
289
-
290
- describe Conference::Speaking do
291
- it 'registers itself' do
292
- RayoNode.class_from_registration(:speaking, 'urn:xmpp:tropo:conference:1').should == Conference::Speaking
293
- end
294
-
295
- describe "from a stanza" do
296
- let(:stanza) { "<speaking xmlns='urn:xmpp:tropo:conference:1' call-id='abc123'/>" }
297
-
298
- subject { RayoNode.import parse_stanza(stanza).root, '9f00061', '1' }
299
-
300
- it { should be_instance_of Conference::Speaking }
301
-
302
- it_should_behave_like 'event'
303
-
304
- its(:speaking_call_id) { should == 'abc123' }
305
- end
306
- end
307
-
308
- describe Conference::FinishedSpeaking do
309
- it 'registers itself' do
310
- RayoNode.class_from_registration(:'finished-speaking', 'urn:xmpp:tropo:conference:1').should == Conference::FinishedSpeaking
311
- end
312
-
313
- describe "from a stanza" do
314
- let(:stanza) { "<finished-speaking xmlns='urn:xmpp:tropo:conference:1' call-id='abc123'/>" }
315
-
316
- subject { RayoNode.import parse_stanza(stanza).root, '9f00061', '1' }
317
-
318
- it { should be_instance_of Conference::FinishedSpeaking }
319
-
320
- it_should_behave_like 'event'
321
-
322
- its(:speaking_call_id) { should == 'abc123' }
323
- end
324
- end
325
-
326
- describe Conference::Complete::Kick do
327
- let :stanza do
328
- <<-MESSAGE
329
- <complete xmlns='urn:xmpp:rayo:ext:1'>
330
- <kick xmlns='urn:xmpp:tropo:conference:complete:1'>wouldn't stop talking</kick>
331
- </complete>
332
- MESSAGE
333
- end
334
-
335
- subject { RayoNode.import(parse_stanza(stanza).root).reason }
336
-
337
- it { should be_instance_of Conference::Complete::Kick }
338
-
339
- its(:name) { should == :kick }
340
- its(:details) { should == "wouldn't stop talking" }
341
- end
342
-
343
- describe Conference::Complete::Terminator do
344
- let :stanza do
345
- <<-MESSAGE
346
- <complete xmlns='urn:xmpp:rayo:ext:1'>
347
- <terminator xmlns='urn:xmpp:tropo:conference:complete:1' />
348
- </complete>
349
- MESSAGE
350
- end
351
-
352
- subject { RayoNode.import(parse_stanza(stanza).root).reason }
353
-
354
- it { should be_instance_of Conference::Complete::Terminator }
355
-
356
- its(:name) { should == :terminator }
357
- end
358
- end
359
- end
360
- end
361
- end # Punchblock