punchblock 0.8.4 → 0.9.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.
@@ -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