larynx 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +4 -0
- data/lib/larynx/call_handler.rb +40 -35
- data/lib/larynx/command.rb +5 -0
- data/lib/larynx/fields.rb +80 -16
- data/lib/larynx/version.rb +1 -1
- data/spec/fixtures/break.rb +51 -0
- data/spec/larynx/call_handler_spec.rb +44 -0
- data/spec/larynx/fields_spec.rb +18 -4
- metadata +61 -5
data/Rakefile
CHANGED
@@ -25,6 +25,10 @@ spec = Gem::Specification.new do |s|
|
|
25
25
|
s.require_path = 'lib'
|
26
26
|
s.autorequire = GEM_NAME
|
27
27
|
s.files = %w(MIT-LICENSE README.rdoc Rakefile) + Dir.glob("{lib,spec,examples}/**/*")
|
28
|
+
s.add_dependency "eventmachine", ">= 0.12.10"
|
29
|
+
s.add_dependency "active_support", ">= 2.3.5"
|
30
|
+
s.add_development_dependency "rspec", ">= 1.3.0"
|
31
|
+
s.add_development_dependency "em-spec", ">= 0.1.3"
|
28
32
|
end
|
29
33
|
|
30
34
|
desc 'Default: run specs.'
|
data/lib/larynx/call_handler.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# FIXME interrupted commands callback can be fired out of order if new command sent on break
|
2
1
|
module Larynx
|
3
2
|
class CallHandler < EventMachine::Protocols::HeaderAndContentProtocol
|
4
3
|
include Observable
|
@@ -36,16 +35,18 @@ module Larynx
|
|
36
35
|
@session[:caller_caller_id_number]
|
37
36
|
end
|
38
37
|
|
39
|
-
def interrupt_command
|
40
|
-
if @state == :executing && current_command.interruptable?
|
41
|
-
break!
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
38
|
def clear_input
|
46
39
|
@input = []
|
47
40
|
end
|
48
41
|
|
42
|
+
def current_command
|
43
|
+
@queue.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def next_command
|
47
|
+
@queue[1]
|
48
|
+
end
|
49
|
+
|
49
50
|
def execute(command, immediately=false)
|
50
51
|
log "Queued: #{command.name}"
|
51
52
|
if immediately
|
@@ -59,10 +60,13 @@ module Larynx
|
|
59
60
|
|
60
61
|
def timer(name, timeout, &block)
|
61
62
|
@timers[name] = [RestartableTimer.new(timeout) {
|
62
|
-
timer = @timers.delete(name)
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
if timer = @timers.delete(name)
|
64
|
+
timer[1].call if timer[1]
|
65
|
+
notify_observers :timed_out
|
66
|
+
send_next_command if @state == :ready
|
67
|
+
else
|
68
|
+
puts name
|
69
|
+
end
|
66
70
|
}, block]
|
67
71
|
end
|
68
72
|
|
@@ -91,17 +95,11 @@ module Larynx
|
|
91
95
|
end
|
92
96
|
end
|
93
97
|
|
94
|
-
def cleanup
|
95
|
-
break! if @state == :executing
|
96
|
-
cancel_all_timers
|
97
|
-
clear_observers!
|
98
|
-
end
|
99
|
-
|
100
98
|
def receive_request(header, content)
|
101
99
|
@response = Response.new(header, content)
|
102
100
|
|
103
101
|
case
|
104
|
-
when @response.reply? && !current_command.is_a?(AppCommand)
|
102
|
+
when @response.reply? && current_command && !current_command.is_a?(AppCommand)
|
105
103
|
log "Completed: #{current_command.name}"
|
106
104
|
finalize_command
|
107
105
|
@state = :ready
|
@@ -111,11 +109,13 @@ module Larynx
|
|
111
109
|
run_command_setup
|
112
110
|
@state = :executing
|
113
111
|
when @response.executed? && current_command
|
112
|
+
this_command = current_command
|
114
113
|
finalize_command
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
114
|
+
unless this_command.interrupted?
|
115
|
+
current_command.fire_callback(:after) if this_command.command == 'break'
|
116
|
+
@state = :ready
|
117
|
+
send_next_command
|
118
|
+
end
|
119
119
|
when @response.dtmf?
|
120
120
|
log "Button pressed: #{@response.body[:dtmf_digit]}"
|
121
121
|
handle_dtmf
|
@@ -136,16 +136,11 @@ module Larynx
|
|
136
136
|
send_next_command if @state == :ready
|
137
137
|
end
|
138
138
|
|
139
|
-
def
|
140
|
-
@
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
current_command && current_command.command == 'break'
|
145
|
-
end
|
146
|
-
|
147
|
-
def interrupted?
|
148
|
-
last_command && last_command.command == 'break'
|
139
|
+
def interrupt_command
|
140
|
+
if @state == :executing && current_command.interruptable?
|
141
|
+
current_command.interrupted = true
|
142
|
+
break!
|
143
|
+
end
|
149
144
|
end
|
150
145
|
|
151
146
|
def run_command_setup
|
@@ -154,18 +149,28 @@ module Larynx
|
|
154
149
|
|
155
150
|
def finalize_command
|
156
151
|
if command = @queue.shift
|
157
|
-
command.fire_callback
|
152
|
+
command.fire_callback(:after) unless command.interrupted?
|
158
153
|
@last_command = command
|
159
154
|
end
|
160
155
|
end
|
161
156
|
|
157
|
+
def command_to_send
|
158
|
+
current_command.try(:interrupted?) ? next_command : current_command
|
159
|
+
end
|
160
|
+
|
162
161
|
def send_next_command
|
163
|
-
if
|
162
|
+
if command = command_to_send
|
164
163
|
@state = :sending
|
165
|
-
send_data
|
164
|
+
send_data command.to_s
|
166
165
|
end
|
167
166
|
end
|
168
167
|
|
168
|
+
def cleanup
|
169
|
+
break! if @state == :executing
|
170
|
+
cancel_all_timers
|
171
|
+
clear_observers!
|
172
|
+
end
|
173
|
+
|
169
174
|
def log(msg)
|
170
175
|
LARYNX_LOGGER.info msg
|
171
176
|
end
|
data/lib/larynx/command.rb
CHANGED
@@ -2,6 +2,7 @@ module Larynx
|
|
2
2
|
class Command
|
3
3
|
include Callbacks
|
4
4
|
attr_reader :command
|
5
|
+
attr_accessor :interrupted
|
5
6
|
|
6
7
|
define_callback :before, :after
|
7
8
|
|
@@ -21,6 +22,10 @@ module Larynx
|
|
21
22
|
def interruptable?
|
22
23
|
false
|
23
24
|
end
|
25
|
+
|
26
|
+
def interrupted?
|
27
|
+
@interrupted
|
28
|
+
end
|
24
29
|
end
|
25
30
|
|
26
31
|
class CallCommand < Command
|
data/lib/larynx/fields.rb
CHANGED
@@ -38,6 +38,46 @@ module Larynx
|
|
38
38
|
|
39
39
|
end
|
40
40
|
|
41
|
+
module CallbacksWithMode
|
42
|
+
|
43
|
+
def setup(mode=:sync, &block)
|
44
|
+
block = app_scope(block)
|
45
|
+
callback = (mode == :async) ? lambda { EM.defer(block, lambda { send_next_command } ) } : block
|
46
|
+
super &callback
|
47
|
+
end
|
48
|
+
|
49
|
+
def success(mode=:sync, &block)
|
50
|
+
block = app_scope(block)
|
51
|
+
callback = (mode == :async) ? lambda { EM.defer(block, lambda { send_next_command } ) } : block
|
52
|
+
super &callback
|
53
|
+
end
|
54
|
+
|
55
|
+
def failure(mode=:sync, &block)
|
56
|
+
block = app_scope(block)
|
57
|
+
callback = (mode == :async) ? lambda { EM.defer(block, lambda { send_next_command } ) } : block
|
58
|
+
super &callback
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate(mode=:sync, &block)
|
62
|
+
block = app_scope(block)
|
63
|
+
if mode == :async
|
64
|
+
super &lambda { EM.defer(block, lambda {|result| evaluate_validity(result) } ) }
|
65
|
+
else
|
66
|
+
super &lambda { evaluate_validity(block.call) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def invalid(mode=:sync, &block)
|
71
|
+
block = app_scope(block)
|
72
|
+
if mode == :async
|
73
|
+
super &lambda { EM.defer(block, lambda {|result| invalid_input } ) }
|
74
|
+
else
|
75
|
+
super &lambda { block.call; invalid_input }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
41
81
|
class Field
|
42
82
|
include Callbacks
|
43
83
|
|
@@ -45,6 +85,8 @@ module Larynx
|
|
45
85
|
define_callback :setup, :validate, :invalid, :success, :failure
|
46
86
|
|
47
87
|
def initialize(name, options, &block)
|
88
|
+
class_eval { include CallbacksWithMode }
|
89
|
+
|
48
90
|
@name, @callbacks = name, {}
|
49
91
|
@options = options.reverse_merge(:attempts => 3)
|
50
92
|
@prompt_queue = []
|
@@ -82,38 +124,46 @@ module Larynx
|
|
82
124
|
|
83
125
|
def execute_prompt
|
84
126
|
call.execute current_prompt.command
|
127
|
+
send_next_command
|
85
128
|
end
|
86
129
|
|
87
130
|
def increment_attempts
|
88
131
|
@attempt += 1
|
89
132
|
end
|
90
133
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
134
|
+
def valid_length?
|
135
|
+
@value.size >= minimum_length
|
136
|
+
end
|
137
|
+
|
138
|
+
def evaluate_input
|
139
|
+
if valid_length?
|
140
|
+
fire_callback(:validate) { evaluate_validity(true) }
|
94
141
|
else
|
95
|
-
|
142
|
+
fire_callback(:invalid) { invalid_input }
|
96
143
|
end
|
97
144
|
end
|
98
145
|
|
99
|
-
def
|
100
|
-
|
146
|
+
def evaluate_validity(result)
|
147
|
+
if result
|
148
|
+
fire_callback(:success) { send_next_command }
|
149
|
+
else
|
150
|
+
fire_callback(:invalid) { invalid_input }
|
151
|
+
end
|
101
152
|
end
|
102
153
|
|
103
|
-
def
|
104
|
-
if
|
105
|
-
|
154
|
+
def invalid_input
|
155
|
+
if @attempt < @options[:attempts]
|
156
|
+
increment_attempts
|
157
|
+
execute_prompt
|
106
158
|
else
|
107
|
-
fire_callback(:
|
108
|
-
if @attempt < @options[:attempts]
|
109
|
-
increment_attempts
|
110
|
-
execute_prompt
|
111
|
-
else
|
112
|
-
fire_callback(:failure)
|
113
|
-
end
|
159
|
+
fire_callback(:failure) { send_next_command }
|
114
160
|
end
|
115
161
|
end
|
116
162
|
|
163
|
+
def send_next_command
|
164
|
+
call.send_next_command if call.state == :ready
|
165
|
+
end
|
166
|
+
|
117
167
|
def set_instance_variables(input)
|
118
168
|
@value = input
|
119
169
|
@app.send("#{@name}=", input)
|
@@ -137,6 +187,20 @@ module Larynx
|
|
137
187
|
def call
|
138
188
|
@app.call
|
139
189
|
end
|
190
|
+
|
191
|
+
def app_scope(block)
|
192
|
+
lambda { @app.instance_eval(&block) }
|
193
|
+
end
|
194
|
+
|
195
|
+
# fire callback with a default block if callback not defined
|
196
|
+
def fire_callback(callback, *args, &block)
|
197
|
+
if @callbacks && @callbacks[callback]
|
198
|
+
@callbacks[callback].call(*args)
|
199
|
+
else
|
200
|
+
yield if block_given?
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
140
204
|
end
|
141
205
|
|
142
206
|
end
|
data/lib/larynx/version.rb
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
RESPONSES[:break] = {
|
2
|
+
:header => [ "Content-Length: 1630", "Content-Type: text/event-plain" ],
|
3
|
+
:content => <<-DATA
|
4
|
+
Event-Name: CHANNEL_EXECUTE
|
5
|
+
Core-UUID: 8a4da428-37c0-11df-8380-0166d7747984
|
6
|
+
FreeSWITCH-Hostname: ubuntu
|
7
|
+
FreeSWITCH-IPv4: 172.16.150.131
|
8
|
+
FreeSWITCH-IPv6: %3A%3A1
|
9
|
+
Event-Date-Local: 2010-03-26%2018%3A11%3A13
|
10
|
+
Event-Date-GMT: Sat,%2027%20Mar%202010%2001%3A11%3A13%20GMT
|
11
|
+
Event-Date-Timestamp: 1269652273898632
|
12
|
+
Event-Calling-File: switch_core_session.c
|
13
|
+
Event-Calling-Function: switch_core_session_exec
|
14
|
+
Event-Calling-Line-Number: 1823
|
15
|
+
Channel-State: CS_EXECUTE
|
16
|
+
Channel-State-Number: 4
|
17
|
+
Channel-Name: sofia/internal/1000%40172.16.150.131
|
18
|
+
Unique-ID: 3e19d55a-394f-11df-8485-0166d7747984
|
19
|
+
Call-Direction: inbound
|
20
|
+
Presence-Call-Direction: inbound
|
21
|
+
Channel-Presence-ID: 1000%40172.16.150.131
|
22
|
+
Answer-State: answered
|
23
|
+
Channel-Read-Codec-Name: GSM
|
24
|
+
Channel-Read-Codec-Rate: 8000
|
25
|
+
Channel-Write-Codec-Name: GSM
|
26
|
+
Channel-Write-Codec-Rate: 8000
|
27
|
+
Caller-Username: 1000
|
28
|
+
Caller-Dialplan: XML
|
29
|
+
Caller-Caller-ID-Name: FreeSWITCH
|
30
|
+
Caller-Caller-ID-Number: 1000
|
31
|
+
Caller-Network-Addr: 172.16.150.1
|
32
|
+
Caller-ANI: 1000
|
33
|
+
Caller-Destination-Number: 502
|
34
|
+
Caller-Unique-ID: 3e19d55a-394f-11df-8485-0166d7747984
|
35
|
+
Caller-Source: mod_sofia
|
36
|
+
Caller-Context: default
|
37
|
+
Caller-Channel-Name: sofia/internal/1000%40172.16.150.131
|
38
|
+
Caller-Profile-Index: 1
|
39
|
+
Caller-Profile-Created-Time: 1269652267394234
|
40
|
+
Caller-Channel-Created-Time: 1269652267394234
|
41
|
+
Caller-Channel-Answered-Time: 1269652267509097
|
42
|
+
Caller-Channel-Progress-Time: 0
|
43
|
+
Caller-Channel-Progress-Media-Time: 0
|
44
|
+
Caller-Channel-Hangup-Time: 0
|
45
|
+
Caller-Channel-Transfer-Time: 0
|
46
|
+
Caller-Screen-Bit: true
|
47
|
+
Caller-Privacy-Hide-Name: false
|
48
|
+
Caller-Privacy-Hide-Number: false
|
49
|
+
Application: break
|
50
|
+
DATA
|
51
|
+
}
|
@@ -241,6 +241,50 @@ describe Larynx::CallHandler do
|
|
241
241
|
end
|
242
242
|
end
|
243
243
|
|
244
|
+
context "interrupting a command" do
|
245
|
+
before do
|
246
|
+
call.queue = []
|
247
|
+
@executing_command = call.speak('hello', :bargein => true) { call.speak 'next hello' }
|
248
|
+
call.send_next_command
|
249
|
+
call.send_response :execute
|
250
|
+
call.interrupt_command
|
251
|
+
end
|
252
|
+
|
253
|
+
it "should push break onto front of queue" do
|
254
|
+
call.queue[0].command.should == 'break'
|
255
|
+
call.queue[1].should == @executing_command
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should execute break immediately" do
|
259
|
+
call.sent_data.should match(/break/)
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should set executing command to interrupted" do
|
263
|
+
@executing_command.interrupted?.should be_true
|
264
|
+
end
|
265
|
+
|
266
|
+
it "should fire callback of interrupted command when break execute complete" do
|
267
|
+
@executing_command.should_receive(:fire_callback).with(:after).once
|
268
|
+
call.send_response :execute_complete
|
269
|
+
end
|
270
|
+
|
271
|
+
it "should leave executing command in queue" do
|
272
|
+
call.send_response :execute_complete
|
273
|
+
call.queue[0].should == @executing_command
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should execute next command after interrupted command" do
|
277
|
+
call.send_response :execute_complete
|
278
|
+
call.sent_data.should match(/next hello/)
|
279
|
+
end
|
280
|
+
|
281
|
+
it "should not fire callback of interrupted command once execute complete" do
|
282
|
+
call.send_response :execute_complete
|
283
|
+
@executing_command.should_not_receive(:fire_callback).with(:after)
|
284
|
+
call.send_response :execute_complete
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
244
288
|
context "timer" do
|
245
289
|
it "should add EM timer with name and timeout" do
|
246
290
|
Larynx::RestartableTimer.stub!(:new)
|
data/spec/larynx/fields_spec.rb
CHANGED
@@ -86,16 +86,18 @@ describe Larynx::Fields do
|
|
86
86
|
fld.current_prompt.message.should == 'second'
|
87
87
|
end
|
88
88
|
|
89
|
-
context '
|
89
|
+
context 'valid_length?' do
|
90
90
|
it 'should be false if input size less than minimum' do
|
91
91
|
fld = field(:guess) do
|
92
92
|
prompt :speak => 'first'
|
93
93
|
end
|
94
94
|
fld.run app
|
95
95
|
fld.current_prompt.finalise
|
96
|
-
fld.
|
96
|
+
fld.valid_length?.should be_false
|
97
97
|
end
|
98
|
+
end
|
98
99
|
|
100
|
+
context 'input evaluation' do
|
99
101
|
it 'should run validate callback if input minimum length' do
|
100
102
|
call_me = should_be_called
|
101
103
|
fld = field(:guess, :min_length => 1) do
|
@@ -106,9 +108,7 @@ describe Larynx::Fields do
|
|
106
108
|
call.input << '1'
|
107
109
|
fld.current_prompt.finalise
|
108
110
|
end
|
109
|
-
end
|
110
111
|
|
111
|
-
context 'input evaluation' do
|
112
112
|
it 'should run invalid callback if length not valid' do
|
113
113
|
call_me = should_be_called
|
114
114
|
fld = field(:guess) do
|
@@ -185,6 +185,20 @@ describe Larynx::Fields do
|
|
185
185
|
end
|
186
186
|
end
|
187
187
|
|
188
|
+
context "async callbacks" do
|
189
|
+
# it "should be run in thread" do
|
190
|
+
# em do
|
191
|
+
# fld = field(:guess) do
|
192
|
+
# prompt :speak => 'first'
|
193
|
+
# validate(:async) { sleep(0.25) }
|
194
|
+
# success { done }
|
195
|
+
# end.run(app)
|
196
|
+
# call.input << '1'
|
197
|
+
# end
|
198
|
+
# @callback.should be_nil
|
199
|
+
# end
|
200
|
+
end
|
201
|
+
|
188
202
|
end
|
189
203
|
|
190
204
|
def field(name, options={}, &block)
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
version: 0.1.
|
8
|
+
- 2
|
9
|
+
version: 0.1.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Adam Meehan
|
@@ -14,10 +14,65 @@ autorequire: larynx
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-03-
|
17
|
+
date: 2010-03-31 00:00:00 +11:00
|
18
18
|
default_executable:
|
19
|
-
dependencies:
|
20
|
-
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 12
|
30
|
+
- 10
|
31
|
+
version: 0.12.10
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: active_support
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 2
|
43
|
+
- 3
|
44
|
+
- 5
|
45
|
+
version: 2.3.5
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rspec
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 1
|
57
|
+
- 3
|
58
|
+
- 0
|
59
|
+
version: 1.3.0
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: em-spec
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
- 1
|
72
|
+
- 3
|
73
|
+
version: 0.1.3
|
74
|
+
type: :development
|
75
|
+
version_requirements: *id004
|
21
76
|
description: ""
|
22
77
|
email: adam.meehan@gmail.com
|
23
78
|
executables:
|
@@ -46,6 +101,7 @@ files:
|
|
46
101
|
- lib/larynx/version.rb
|
47
102
|
- lib/larynx.rb
|
48
103
|
- spec/fixtures/answer.rb
|
104
|
+
- spec/fixtures/break.rb
|
49
105
|
- spec/fixtures/channel_data.rb
|
50
106
|
- spec/fixtures/dtmf.rb
|
51
107
|
- spec/fixtures/execute.rb
|