larynx 0.1.1 → 0.1.2
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.
- 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
|