larynx 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +60 -6
- data/Rakefile +1 -1
- data/lib/larynx/call_handler.rb +33 -34
- data/lib/larynx/callbacks_with_async.rb +60 -0
- data/lib/larynx/command.rb +8 -0
- data/lib/larynx/fields.rb +49 -92
- data/lib/larynx/prompt.rb +21 -3
- data/lib/larynx/response.rb +4 -0
- data/lib/larynx/version.rb +1 -1
- data/lib/larynx.rb +15 -5
- data/spec/fixtures/answered.rb +131 -0
- data/spec/larynx/call_handler_spec.rb +30 -30
- data/spec/larynx/fields_spec.rb +39 -24
- data/spec/larynx/prompt_spec.rb +26 -10
- data/spec/spec_helper.rb +23 -1
- metadata +7 -5
data/README.rdoc
CHANGED
@@ -104,7 +104,7 @@ A more sophisticated example using the Form class
|
|
104
104
|
|
105
105
|
Larynx.answer {|call| Guess.run(call) }
|
106
106
|
|
107
|
-
The Form class wraps up many handy conventions into a pleasant DSL in which allows you to control the
|
107
|
+
The Form class wraps up many handy conventions into a pleasant DSL in which allows you to control the caller
|
108
108
|
interaction more easily.
|
109
109
|
|
110
110
|
Save your app into file and run larynx comand to start the app server ready to receive calls.
|
@@ -167,10 +167,6 @@ The class method initialises some things for you and then calls <tt>run</tt> on
|
|
167
167
|
there its up to you. You can use all the commands directly rather than call them on the call
|
168
168
|
instance.
|
169
169
|
|
170
|
-
== Form Class
|
171
|
-
|
172
|
-
|
173
|
-
|
174
170
|
== Event Hooks
|
175
171
|
|
176
172
|
The Application and Form classes have a couple of useful event hook methods available which are
|
@@ -180,7 +176,7 @@ The Application and Form classes have a couple of useful event hook methods avai
|
|
180
176
|
end
|
181
177
|
|
182
178
|
def dtmf_received(input)
|
183
|
-
# input is the button the
|
179
|
+
# input is the button the caller just pushed
|
184
180
|
end
|
185
181
|
|
186
182
|
def hungup
|
@@ -188,4 +184,62 @@ The Application and Form classes have a couple of useful event hook methods avai
|
|
188
184
|
end
|
189
185
|
end
|
190
186
|
|
187
|
+
== Form Class
|
188
|
+
|
189
|
+
The Form class is a sublcass of the Application class with some added goodness. You can use the field
|
190
|
+
DSL to abstract away a lot of the reptitive work and more difficult asynchronous stuff you would have
|
191
|
+
to handle yourself.
|
192
|
+
|
193
|
+
When collection input from the caller you are usually needing to do one or more of a few things.
|
194
|
+
|
195
|
+
These are:
|
196
|
+
1. Set-up something before the caller is prompted for the information.
|
197
|
+
2. Repeat the prompt for input if the caller doesn't enter anything or does not enter enough digits.
|
198
|
+
3. Validate the input for constraints beyond length such as a range or finding a matching database record.
|
199
|
+
4. Perform some task, perhaps say something, if the input is invalid but before the prompt is repeated.
|
200
|
+
5. Handle the next action if the input is valid
|
201
|
+
6. Handle the next action if the caller fails enter valid input after a number of attempts.
|
202
|
+
|
203
|
+
You define these tasks as callbacks for each type of action, be it setup, validate, invalid, success or failure.
|
204
|
+
These callbacks are run in the scope of the form class instance each time so that they can access user
|
205
|
+
defined methods or instance variables you need.
|
206
|
+
|
207
|
+
Each field defined becomes an attribute on the form class instance which you can use at any time. The value
|
208
|
+
is set to the caller input after each prompt, just use the field attribute method to retrieve it. A form may
|
209
|
+
have as many fields defined you as need. Though the idea would be to group fields with a particular purpose
|
210
|
+
together and define another form for unrelated fields.
|
211
|
+
|
212
|
+
Lets look at a simple Form class with empty callbacks in place.
|
213
|
+
|
214
|
+
class MyApp < Larynx::Form
|
215
|
+
field(:my_field, :attempts => 3, :length => 1) do
|
216
|
+
prompt :speak => 'Please enter a value.'
|
217
|
+
|
218
|
+
setup do
|
219
|
+
# run once when the field is starts
|
220
|
+
end
|
221
|
+
|
222
|
+
validate do
|
223
|
+
# run when the input is of a valid length
|
224
|
+
end
|
225
|
+
|
226
|
+
invalid do
|
227
|
+
# run when the input is not a valid length or the validate block returns false
|
228
|
+
end
|
229
|
+
|
230
|
+
success do
|
231
|
+
# run when the input is a valid length the validate block, if defined, returns true
|
232
|
+
end
|
233
|
+
|
234
|
+
failure do
|
235
|
+
# run when the maximum attempts has been reached and valid input has not been entered
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
This form will do the following when run:
|
241
|
+
* Define a field attribute method called <tt>my_field</tt>
|
242
|
+
* Atempt to obtain the field value up to 3 times if the caller fails to successfully enter it
|
243
|
+
* Accept a single digit value as complete and valid intput
|
244
|
+
|
191
245
|
|
data/Rakefile
CHANGED
@@ -16,7 +16,7 @@ spec = Gem::Specification.new do |s|
|
|
16
16
|
s.has_rdoc = true
|
17
17
|
s.extra_rdoc_files = ["README.rdoc"]
|
18
18
|
s.executables = ["larynx"]
|
19
|
-
s.summary = ""
|
19
|
+
s.summary = "An evented application framework for the FreeSWITCH telephony platform"
|
20
20
|
s.description = s.summary
|
21
21
|
s.author = "Adam Meehan"
|
22
22
|
s.email = "adam.meehan@gmail.com"
|
data/lib/larynx/call_handler.rb
CHANGED
@@ -8,23 +8,16 @@ module Larynx
|
|
8
8
|
# EM hook which is run when call is received
|
9
9
|
def post_init
|
10
10
|
@queue, @input, @timers = [], [], {}
|
11
|
-
connect {
|
12
|
-
@session = Session.new(@response.header)
|
13
|
-
log "Call received from #{caller_id}"
|
14
|
-
Larynx.fire_callback(:connect, self)
|
15
|
-
start_session
|
16
|
-
}
|
11
|
+
connect { start_session }
|
17
12
|
send_next_command
|
18
13
|
end
|
19
14
|
|
20
15
|
def start_session
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
}
|
27
|
-
}
|
16
|
+
@session = Session.new(@response.header)
|
17
|
+
log "Call received from #{caller_id}"
|
18
|
+
myevents
|
19
|
+
linger
|
20
|
+
Larynx.fire_callback(:connect, self)
|
28
21
|
end
|
29
22
|
|
30
23
|
def called_number
|
@@ -58,33 +51,35 @@ module Larynx
|
|
58
51
|
command
|
59
52
|
end
|
60
53
|
|
61
|
-
def
|
54
|
+
def add_timer(name, timeout, &block)
|
62
55
|
@timers[name] = [RestartableTimer.new(timeout) {
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
else
|
68
|
-
puts name
|
69
|
-
end
|
56
|
+
timer = @timers.delete(name)
|
57
|
+
timer[1].call if timer[1]
|
58
|
+
notify_observers :timed_out
|
59
|
+
send_next_command if @state == :ready
|
70
60
|
}, block]
|
71
61
|
end
|
72
62
|
|
73
63
|
def cancel_timer(name)
|
74
|
-
if timer = @timers
|
64
|
+
if timer = @timers[name]
|
75
65
|
timer[0].cancel
|
66
|
+
@timers.delete(name)
|
76
67
|
send_next_command if @state == :ready
|
77
68
|
end
|
78
69
|
end
|
79
70
|
|
80
71
|
def cancel_all_timers
|
81
72
|
@timers.values.each {|t| t[0].cancel }
|
73
|
+
@timers = {}
|
82
74
|
end
|
83
75
|
|
84
76
|
def stop_timer(name)
|
85
|
-
if timer = @timers
|
86
|
-
|
87
|
-
timer[
|
77
|
+
if timer = @timers[name]
|
78
|
+
# only run callback if it was actually cancelled (i.e. returns false)
|
79
|
+
if timer[0].cancel == false && timer[1]
|
80
|
+
timer[1].call
|
81
|
+
end
|
82
|
+
@timers.delete(name)
|
88
83
|
send_next_command if @state == :ready
|
89
84
|
end
|
90
85
|
end
|
@@ -97,29 +92,37 @@ module Larynx
|
|
97
92
|
|
98
93
|
def receive_request(header, content)
|
99
94
|
@response = Response.new(header, content)
|
95
|
+
handle_request
|
96
|
+
end
|
100
97
|
|
98
|
+
def handle_request
|
101
99
|
case
|
102
|
-
when @response.reply? && current_command
|
100
|
+
when @response.reply? && current_command.is_a?(CallCommand)
|
103
101
|
log "Completed: #{current_command.name}"
|
104
102
|
finalize_command
|
105
103
|
@state = :ready
|
106
104
|
send_next_command
|
105
|
+
when @response.answered?
|
106
|
+
log 'Answered call'
|
107
|
+
finalize_command
|
108
|
+
@state = :ready
|
109
|
+
Larynx.fire_callback(:answer, self)
|
110
|
+
send_next_command
|
107
111
|
when @response.executing?
|
108
112
|
log "Executing: #{current_command.name}"
|
109
|
-
|
113
|
+
current_command.setup
|
110
114
|
@state = :executing
|
111
115
|
when @response.executed? && current_command
|
112
116
|
this_command = current_command
|
113
117
|
finalize_command
|
114
118
|
unless this_command.interrupted?
|
115
|
-
current_command.
|
119
|
+
current_command.finalize if this_command.command == 'break'
|
116
120
|
@state = :ready
|
117
121
|
send_next_command
|
118
122
|
end
|
119
123
|
when @response.dtmf?
|
120
124
|
log "Button pressed: #{@response.body[:dtmf_digit]}"
|
121
125
|
handle_dtmf
|
122
|
-
when @response.speech?
|
123
126
|
when @response.disconnect?
|
124
127
|
log "Disconnected."
|
125
128
|
cleanup
|
@@ -143,13 +146,9 @@ module Larynx
|
|
143
146
|
end
|
144
147
|
end
|
145
148
|
|
146
|
-
def run_command_setup
|
147
|
-
current_command.fire_callback :before
|
148
|
-
end
|
149
|
-
|
150
149
|
def finalize_command
|
151
150
|
if command = @queue.shift
|
152
|
-
command.
|
151
|
+
command.finalize unless command.interrupted?
|
153
152
|
@last_command = command
|
154
153
|
end
|
155
154
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Larynx
|
2
|
+
# Allows you to set the :async flag on the back to the callback in with
|
3
|
+
# EM.defer method. Its useful for long running tasks like database calls.
|
4
|
+
module CallbacksWithAsync
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.cattr_accessor :callback_options
|
8
|
+
base.callback_options = {}
|
9
|
+
base.class_eval do
|
10
|
+
include InstanceMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def define_callback(*callbacks)
|
17
|
+
options = callbacks.extract_options!
|
18
|
+
callbacks.each do |callback|
|
19
|
+
self.callback_options[callback] = options
|
20
|
+
class_eval <<-DEF
|
21
|
+
def #{callback}(mode=:sync, &block)
|
22
|
+
@callbacks ||= {}
|
23
|
+
@callbacks[:#{callback}] = [block, mode]
|
24
|
+
self
|
25
|
+
end
|
26
|
+
DEF
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
|
34
|
+
def fire_callback(callback)
|
35
|
+
if @callbacks && @callbacks[callback]
|
36
|
+
block, mode = *@callbacks[callback]
|
37
|
+
scope = self.class.callback_options[callback][:scope]
|
38
|
+
if mode == :async
|
39
|
+
EM.defer(scope_callback(block, scope), lambda {|result| callback_complete(callback, result) })
|
40
|
+
else
|
41
|
+
callback_complete(callback, scope_callback(block, scope).call)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
callback_complete(callback, nil)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Scope takes the callback block and a method symbol which is used
|
49
|
+
# to return an object that scopes the block evaluation.
|
50
|
+
def scope_callback(block, scope=nil)
|
51
|
+
scope ? lambda { send(scope).instance_eval(&block) } : block
|
52
|
+
end
|
53
|
+
|
54
|
+
def callback_complete(callback, result)
|
55
|
+
# Override in class to handle post callback result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/larynx/command.rb
CHANGED
data/lib/larynx/fields.rb
CHANGED
@@ -5,15 +5,18 @@ module Larynx
|
|
5
5
|
|
6
6
|
def self.included(base)
|
7
7
|
base.extend ClassMethods
|
8
|
-
base.
|
8
|
+
base.class_eval do
|
9
|
+
include InstanceMethods
|
10
|
+
class_inheritable_accessor :field_definitions
|
11
|
+
self.field_definitions = []
|
12
|
+
attr_accessor :fields
|
13
|
+
end
|
9
14
|
end
|
10
15
|
|
11
16
|
module ClassMethods
|
12
|
-
attr_accessor :fields
|
13
17
|
|
14
18
|
def field(name, options={}, &block)
|
15
|
-
|
16
|
-
@fields << Field.new(name, options, &block)
|
19
|
+
self.field_definitions << {:name => name, :options => options, :block => block}
|
17
20
|
attr_accessor name
|
18
21
|
end
|
19
22
|
|
@@ -21,73 +24,36 @@ module Larynx
|
|
21
24
|
|
22
25
|
module InstanceMethods
|
23
26
|
|
27
|
+
def initialize(*args, &block)
|
28
|
+
@fields = self.class.field_definitions.map {|field| Field.new(field[:name], field[:options], &field[:block]) }
|
29
|
+
@current_field = 0
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
24
33
|
def next_field(field_name=nil)
|
25
|
-
@current_field
|
26
|
-
|
27
|
-
if field = self.class.fields[@current_field]
|
34
|
+
@current_field = field_index(field_name) if field_name
|
35
|
+
if field = @fields[@current_field]
|
28
36
|
field.run(self)
|
29
37
|
@current_field += 1
|
30
38
|
field
|
31
39
|
end
|
32
40
|
end
|
33
41
|
|
34
|
-
def
|
35
|
-
field =
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
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
|
42
|
+
def field_index(name)
|
43
|
+
field = @fields.find {|f| f.name == name }
|
44
|
+
@fields.index(field)
|
77
45
|
end
|
78
46
|
|
79
47
|
end
|
80
48
|
|
81
49
|
class Field
|
82
|
-
include
|
50
|
+
include CallbacksWithAsync
|
83
51
|
|
84
|
-
attr_reader :name
|
85
|
-
define_callback :setup, :validate, :invalid, :success, :failure
|
52
|
+
attr_reader :name, :app
|
53
|
+
define_callback :setup, :validate, :invalid, :success, :failure, :scope => :app
|
86
54
|
|
87
55
|
def initialize(name, options, &block)
|
88
|
-
|
89
|
-
|
90
|
-
@name, @callbacks = name, {}
|
56
|
+
@name = name
|
91
57
|
@options = options.reverse_merge(:attempts => 3)
|
92
58
|
@prompt_queue = []
|
93
59
|
|
@@ -105,19 +71,20 @@ module Larynx
|
|
105
71
|
end
|
106
72
|
|
107
73
|
def add_prompt(options)
|
74
|
+
options.assert_valid_keys(:play, :speak, :phrase, :bargein, :repeats, :interdigit_timeout, :timeout)
|
108
75
|
repeats = options.delete(:repeats) || 1
|
109
|
-
options.merge!(@options.slice(:length, :min_length, :max_length))
|
76
|
+
options.merge!(@options.slice(:length, :min_length, :max_length, :interdigit_timeout, :timeout))
|
110
77
|
@prompt_queue += ([options] * repeats)
|
111
78
|
end
|
112
79
|
|
113
80
|
def current_prompt
|
114
81
|
options = (@prompt_queue[@attempt-1] || @prompt_queue.last).dup
|
115
|
-
method = (
|
82
|
+
method = command_from_options(options)
|
116
83
|
message = options[method].is_a?(Symbol) ? @app.send(options[method]) : options[method]
|
117
84
|
options[method] = message
|
118
85
|
|
119
|
-
Prompt.new(call, options) {|input|
|
120
|
-
set_instance_variables(input)
|
86
|
+
Prompt.new(call, options) {|input, result|
|
87
|
+
set_instance_variables(input, result)
|
121
88
|
evaluate_input
|
122
89
|
}
|
123
90
|
end
|
@@ -131,24 +98,25 @@ module Larynx
|
|
131
98
|
@attempt += 1
|
132
99
|
end
|
133
100
|
|
134
|
-
|
135
|
-
|
101
|
+
# hook called when callback is complete
|
102
|
+
def callback_complete(callback, result)
|
103
|
+
case callback
|
104
|
+
when :validate
|
105
|
+
result = result.nil? ? true : result
|
106
|
+
evaluate_validity(result)
|
107
|
+
when :invalid
|
108
|
+
invalid_input
|
109
|
+
when :success, :failure
|
110
|
+
finalize
|
111
|
+
end
|
136
112
|
end
|
137
113
|
|
138
114
|
def evaluate_input
|
139
|
-
|
140
|
-
fire_callback(:validate) { evaluate_validity(true) }
|
141
|
-
else
|
142
|
-
fire_callback(:invalid) { invalid_input }
|
143
|
-
end
|
115
|
+
@valid_length ? fire_callback(:validate) : fire_callback(:invalid)
|
144
116
|
end
|
145
117
|
|
146
118
|
def evaluate_validity(result)
|
147
|
-
|
148
|
-
fire_callback(:success) { send_next_command }
|
149
|
-
else
|
150
|
-
fire_callback(:invalid) { invalid_input }
|
151
|
-
end
|
119
|
+
result ? fire_callback(:success) : fire_callback(:invalid)
|
152
120
|
end
|
153
121
|
|
154
122
|
def invalid_input
|
@@ -156,7 +124,7 @@ module Larynx
|
|
156
124
|
increment_attempts
|
157
125
|
execute_prompt
|
158
126
|
else
|
159
|
-
fire_callback(:failure)
|
127
|
+
fire_callback(:failure)
|
160
128
|
end
|
161
129
|
end
|
162
130
|
|
@@ -164,22 +132,19 @@ module Larynx
|
|
164
132
|
call.send_next_command if call.state == :ready
|
165
133
|
end
|
166
134
|
|
167
|
-
def set_instance_variables(input)
|
168
|
-
@value = input
|
135
|
+
def set_instance_variables(input, result)
|
136
|
+
@value, @valid_length = input, result
|
169
137
|
@app.send("#{@name}=", input)
|
170
138
|
end
|
171
139
|
|
172
|
-
def
|
173
|
-
|
174
|
-
end
|
175
|
-
|
176
|
-
def minimum_length
|
177
|
-
@options[:min_length] || @options[:length] || 1
|
140
|
+
def command_from_options(options)
|
141
|
+
([:play, :speak, :phrase] & options.keys).first
|
178
142
|
end
|
179
143
|
|
180
144
|
def run(app)
|
181
145
|
@app = app
|
182
146
|
@attempt = 1
|
147
|
+
call.add_observer self
|
183
148
|
fire_callback(:setup)
|
184
149
|
execute_prompt
|
185
150
|
end
|
@@ -188,17 +153,9 @@ module Larynx
|
|
188
153
|
@app.call
|
189
154
|
end
|
190
155
|
|
191
|
-
def
|
192
|
-
|
193
|
-
|
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
|
156
|
+
def finalize
|
157
|
+
call.remove_observer self
|
158
|
+
send_next_command
|
202
159
|
end
|
203
160
|
|
204
161
|
end
|
data/lib/larynx/prompt.rb
CHANGED
@@ -45,10 +45,22 @@ module Larynx
|
|
45
45
|
@options[:termchar]
|
46
46
|
end
|
47
47
|
|
48
|
+
def minimum_length
|
49
|
+
@options[:min_length] || @options[:length] || 1
|
50
|
+
end
|
51
|
+
|
48
52
|
def maximum_length
|
49
53
|
@options[:max_length] || @options[:length]
|
50
54
|
end
|
51
55
|
|
56
|
+
def interdigit_timeout
|
57
|
+
@options[:interdigit_timeout]
|
58
|
+
end
|
59
|
+
|
60
|
+
def timeout
|
61
|
+
@options[:timeout]
|
62
|
+
end
|
63
|
+
|
52
64
|
def command_name
|
53
65
|
([:play, :speak, :phrase] & @options.keys).first.to_s
|
54
66
|
end
|
@@ -57,9 +69,15 @@ module Larynx
|
|
57
69
|
@options[command_name.to_sym]
|
58
70
|
end
|
59
71
|
|
72
|
+
def valid_length?
|
73
|
+
length = input.size
|
74
|
+
length >= minimum_length && length <= (maximum_length || length)
|
75
|
+
end
|
76
|
+
|
60
77
|
def finalise
|
61
78
|
call.remove_observer self
|
62
|
-
@block.call(input)
|
79
|
+
@block.arity == 2 ? @block.call(input, valid_length?) : @block.call(input)
|
80
|
+
call.clear_input
|
63
81
|
end
|
64
82
|
|
65
83
|
def dtmf_received(digit)
|
@@ -72,14 +90,14 @@ module Larynx
|
|
72
90
|
end
|
73
91
|
|
74
92
|
def add_digit_timer
|
75
|
-
call.
|
93
|
+
call.add_timer(:digit, interdigit_timeout) {
|
76
94
|
call.cancel_timer :input
|
77
95
|
finalise
|
78
96
|
}
|
79
97
|
end
|
80
98
|
|
81
99
|
def add_input_timer
|
82
|
-
call.
|
100
|
+
call.add_timer(:input, timeout) {
|
83
101
|
call.cancel_timer :digit
|
84
102
|
finalise
|
85
103
|
}
|
data/lib/larynx/response.rb
CHANGED
data/lib/larynx/version.rb
CHANGED
data/lib/larynx.rb
CHANGED
@@ -8,6 +8,7 @@ require 'larynx/version'
|
|
8
8
|
require 'larynx/logger'
|
9
9
|
require 'larynx/observable'
|
10
10
|
require 'larynx/callbacks'
|
11
|
+
require 'larynx/callbacks_with_async'
|
11
12
|
require 'larynx/session'
|
12
13
|
require 'larynx/response'
|
13
14
|
require 'larynx/command'
|
@@ -29,13 +30,13 @@ module Larynx
|
|
29
30
|
@options = {
|
30
31
|
:ip => "0.0.0.0",
|
31
32
|
:port => 8084,
|
32
|
-
:pid_file =>
|
33
|
-
:log_file =>
|
33
|
+
:pid_file => "./larynx.pid",
|
34
|
+
:log_file => "./larynx.log"
|
34
35
|
}
|
35
36
|
opts = OptionParser.new
|
36
37
|
opts.banner = "Usage: larynx [options]"
|
37
38
|
opts.separator ''
|
38
|
-
opts.separator "Larynx is a
|
39
|
+
opts.separator "Larynx is a framework to develop FreeSWITCH IVR applications in Ruby."
|
39
40
|
opts.on('-i', '--ip IP', 'Listen for connections on this IP') {|ip| @options[:ip] = ip }
|
40
41
|
opts.on('-p', '--port PORT', 'Listen on this port', Integer) {|port| @options[:port] = port }
|
41
42
|
opts.on('-d', '--daemonize', 'Run as daemon') { @options[:daemonize] = true }
|
@@ -53,7 +54,10 @@ module Larynx
|
|
53
54
|
end
|
54
55
|
|
55
56
|
def graceful_exit
|
56
|
-
|
57
|
+
msg = "Shutting down Larynx"
|
58
|
+
$stderr.puts msg unless @options[:daemon]
|
59
|
+
LARYNX_LOGGER.info msg
|
60
|
+
|
57
61
|
EM.stop_server @em_signature
|
58
62
|
@em_signature = nil
|
59
63
|
remove_pid_file if @options[:daemonize]
|
@@ -85,7 +89,10 @@ module Larynx
|
|
85
89
|
end
|
86
90
|
|
87
91
|
def start_server
|
88
|
-
|
92
|
+
msg = "Larynx starting up on #{@options[:ip]}:#{@options[:port]}"
|
93
|
+
$stderr.puts msg unless @options[:daemon]
|
94
|
+
LARYNX_LOGGER.info msg
|
95
|
+
|
89
96
|
EM::run {
|
90
97
|
@em_signature = EM::start_server @options[:ip], @options[:port], Larynx::CallHandler
|
91
98
|
}
|
@@ -104,6 +111,9 @@ module Larynx
|
|
104
111
|
!@em_signature.nil?
|
105
112
|
end
|
106
113
|
end
|
114
|
+
|
115
|
+
# Default connect callback is to answer call
|
116
|
+
connect {|call| call.answer }
|
107
117
|
end
|
108
118
|
|
109
119
|
Larynx.run unless defined?(TEST)
|
@@ -0,0 +1,131 @@
|
|
1
|
+
RESPONSES[:answered] = {
|
2
|
+
:header => [ "Content-Length: 5793", "Content-Type: text/event-plain" ],
|
3
|
+
:content => <<-DATA
|
4
|
+
Event-Name: CHANNEL_EXECUTE_COMPLETE
|
5
|
+
Core-UUID: 0a3bd7a4-2a47-11df-8f89-ddea2631ab78
|
6
|
+
FreeSWITCH-Hostname: ubuntu
|
7
|
+
FreeSWITCH-IPv4: 172.16.150.131
|
8
|
+
FreeSWITCH-IPv6: %3A%3A1
|
9
|
+
Event-Date-Local: 2010-03-07%2016%3A39%3A02
|
10
|
+
Event-Date-GMT: Mon,%2008%20Mar%202010%2000%3A39%3A02%20GMT
|
11
|
+
Event-Date-Timestamp: 1268008742244659
|
12
|
+
Event-Calling-File: switch_core_session.c
|
13
|
+
Event-Calling-Function: switch_core_session_exec
|
14
|
+
Event-Calling-Line-Number: 1824
|
15
|
+
Channel-State: CS_EXECUTE
|
16
|
+
Channel-State-Number: 4
|
17
|
+
Channel-Name: sofia/internal/1000%40172.16.150.131
|
18
|
+
Unique-ID: f81a3fbc-2a4a-11df-8f9d-ddea2631ab78
|
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: f81a3fbc-2a4a-11df-8f9d-ddea2631ab78
|
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: 1268008732274835
|
40
|
+
Caller-Channel-Created-Time: 1268008732274835
|
41
|
+
Caller-Channel-Answered-Time: 1268008732325458
|
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
|
+
variable_uuid: f81a3fbc-2a4a-11df-8f9d-ddea2631ab78
|
50
|
+
variable_sip_network_ip: 172.16.150.1
|
51
|
+
variable_sip_network_port: 61230
|
52
|
+
variable_sip_received_ip: 172.16.150.1
|
53
|
+
variable_sip_received_port: 61230
|
54
|
+
variable_sip_via_protocol: udp
|
55
|
+
variable_sip_authorized: true
|
56
|
+
variable_sip_number_alias: 1000
|
57
|
+
variable_sip_auth_username: 1000
|
58
|
+
variable_sip_auth_realm: 172.16.150.131
|
59
|
+
variable_number_alias: 1000
|
60
|
+
variable_user_name: 1000
|
61
|
+
variable_domain_name: 172.16.150.131
|
62
|
+
variable_toll_allow: domestic,international,local
|
63
|
+
variable_accountcode: 1000
|
64
|
+
variable_user_context: default
|
65
|
+
variable_effective_caller_id_name: Extension%201000
|
66
|
+
variable_effective_caller_id_number: 1000
|
67
|
+
variable_outbound_caller_id_name: FreeSWITCH
|
68
|
+
variable_outbound_caller_id_number: 0000000000
|
69
|
+
variable_callgroup: techsupport
|
70
|
+
variable_record_stereo: true
|
71
|
+
variable_default_gateway: example.com
|
72
|
+
variable_default_areacode: 918
|
73
|
+
variable_transfer_fallback_extension: operator
|
74
|
+
variable_sip_from_user: 1000
|
75
|
+
variable_sip_from_uri: 1000%40172.16.150.131
|
76
|
+
variable_sip_from_host: 172.16.150.131
|
77
|
+
variable_sip_from_user_stripped: 1000
|
78
|
+
variable_sofia_profile_name: internal
|
79
|
+
variable_sip_full_via: SIP/2.0/UDP%20192.168.20.2%3A61230%3Brport%3D61230%3Bbranch%3Dz9hG4bKPjywNOvUq.sdNaFl0hFfl2j92kst3ChkRT%3Breceived%3D172.16.150.1
|
80
|
+
variable_sip_req_user: 502
|
81
|
+
variable_sip_req_uri: 502%40172.16.150.131
|
82
|
+
variable_sip_req_host: 172.16.150.131
|
83
|
+
variable_sip_to_user: 502
|
84
|
+
variable_sip_to_uri: 502%40172.16.150.131
|
85
|
+
variable_sip_to_host: 172.16.150.131
|
86
|
+
variable_sip_contact_user: 1000
|
87
|
+
variable_sip_contact_port: 61230
|
88
|
+
variable_sip_contact_uri: 1000%40192.168.20.2%3A61230
|
89
|
+
variable_sip_contact_host: 192.168.20.2
|
90
|
+
variable_channel_name: sofia/internal/1000%40172.16.150.131
|
91
|
+
variable_sip_user_agent: Telephone%200.14.3
|
92
|
+
variable_sip_via_host: 192.168.20.2
|
93
|
+
variable_sip_via_port: 61230
|
94
|
+
variable_sip_via_rport: 61230
|
95
|
+
variable_max_forwards: 70
|
96
|
+
variable_presence_id: 1000%40172.16.150.131
|
97
|
+
variable_switch_r_sdp: v%3D0%0D%0Ao%3D-%203476997537%203476997537%20IN%20IP4%20192.168.20.2%0D%0As%3Dpjmedia%0D%0Ac%3DIN%20IP4%20192.168.20.2%0D%0At%3D0%200%0D%0Aa%3DX-nat%3A0%0D%0Am%3Daudio%204010%20RTP/AVP%20103%20102%20104%20117%203%200%208%209%20101%0D%0Aa%3Drtpmap%3A103%20speex/16000%0D%0Aa%3Drtpmap%3A102%20speex/8000%0D%0Aa%3Drtpmap%3A104%20speex/32000%0D%0Aa%3Drtpmap%3A117%20iLBC/8000%0D%0Aa%3Dfmtp%3A117%20mode%3D30%0D%0Aa%3Drtpmap%3A3%20GSM/8000%0D%0Aa%3Drtpmap%3A0%20PCMU/8000%0D%0Aa%3Drtpmap%3A8%20PCMA/8000%0D%0Aa%3Drtpmap%3A9%20G722/8000%0D%0Aa%3Drtpmap%3A101%20telephone-event/8000%0D%0Aa%3Dfmtp%3A101%200-15%0D%0Aa%3Drtcp%3A4011%20IN%20IP4%20192.168.20.2%0D%0A
|
98
|
+
variable_sip_use_codec_name: GSM
|
99
|
+
variable_sip_use_codec_rate: 8000
|
100
|
+
variable_sip_use_codec_ptime: 20
|
101
|
+
variable_read_codec: GSM
|
102
|
+
variable_read_rate: 8000
|
103
|
+
variable_write_codec: GSM
|
104
|
+
variable_write_rate: 8000
|
105
|
+
variable_tts_engine: cepstral
|
106
|
+
variable_tts_voice: allison
|
107
|
+
variable_socket_host: 172.16.150.1
|
108
|
+
variable_sip_local_sdp_str: v%3D0%0Ao%3DFreeSWITCH%201267985798%201267985799%20IN%20IP4%20172.16.150.131%0As%3DFreeSWITCH%0Ac%3DIN%20IP4%20172.16.150.131%0At%3D0%200%0Am%3Daudio%2022934%20RTP/AVP%203%20101%0Aa%3Drtpmap%3A3%20GSM/8000%0Aa%3Drtpmap%3A101%20telephone-event/8000%0Aa%3Dfmtp%3A101%200-16%0Aa%3DsilenceSupp%3Aoff%20-%20-%20-%20-%0Aa%3Dptime%3A20%0Aa%3Dsendrecv%0A
|
109
|
+
variable_local_media_ip: 172.16.150.131
|
110
|
+
variable_local_media_port: 22934
|
111
|
+
variable_sip_use_pt: 3
|
112
|
+
variable_rtp_use_ssrc: 2898819412
|
113
|
+
variable_sip_nat_detected: true
|
114
|
+
variable_endpoint_disposition: ANSWER
|
115
|
+
variable_sip_to_tag: gDK3D6pXtFm3e
|
116
|
+
variable_sip_from_tag: elWUmu5et-auCRPijiEAiLkCgci1M67D
|
117
|
+
variable_sip_cseq: 14587
|
118
|
+
variable_sip_call_id: bjGKd9gAHJVCxd1uVy9knG.iFWn-n2h4
|
119
|
+
variable_sip_from_display: FreeSWITCH
|
120
|
+
variable_sip_full_from: %22FreeSWITCH%22%20%3Csip%3A1000%40172.16.150.131%3E%3Btag%3DelWUmu5et-auCRPijiEAiLkCgci1M67D
|
121
|
+
variable_sip_full_to: %3Csip%3A502%40172.16.150.131%3E%3Btag%3DgDK3D6pXtFm3e
|
122
|
+
variable_remote_media_ip_reported: 192.168.20.2
|
123
|
+
variable_remote_media_ip: 172.16.150.1
|
124
|
+
variable_remote_media_port_reported: 4010
|
125
|
+
variable_remote_media_port: 4010
|
126
|
+
variable_rtp_auto_adjust: true
|
127
|
+
variable_current_application: answer
|
128
|
+
Application: answer
|
129
|
+
Application-Response: _none_
|
130
|
+
DATA
|
131
|
+
}
|
@@ -64,9 +64,8 @@ describe Larynx::CallHandler do
|
|
64
64
|
end
|
65
65
|
|
66
66
|
it "should run app command before callback" do
|
67
|
-
call.speak('hello world').before
|
67
|
+
call.speak('hello world').before &should_be_called
|
68
68
|
call.send_response :execute
|
69
|
-
@callback.should be_true
|
70
69
|
end
|
71
70
|
|
72
71
|
it "should change state to executing" do
|
@@ -132,9 +131,19 @@ describe Larynx::CallHandler do
|
|
132
131
|
end
|
133
132
|
|
134
133
|
it "should fire global connect callback" do
|
135
|
-
|
134
|
+
with_global_callback(:connect, should_be_called) do
|
135
|
+
connect_call
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should subscribe to myevents" do
|
140
|
+
call.should_receive(:myevents)
|
141
|
+
connect_call
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should set event lingering on after filter events" do
|
145
|
+
call.should_receive(:linger)
|
136
146
|
connect_call
|
137
|
-
@callback.should be_true
|
138
147
|
end
|
139
148
|
|
140
149
|
it "should start the call session" do
|
@@ -146,39 +155,36 @@ describe Larynx::CallHandler do
|
|
146
155
|
call.send_response :channel_data
|
147
156
|
end
|
148
157
|
|
149
|
-
after do
|
150
|
-
Larynx.connect {|call|}
|
151
|
-
end
|
152
158
|
end
|
153
159
|
|
154
|
-
context "
|
160
|
+
context "#session_start" do
|
155
161
|
before do
|
156
162
|
call.queue = []
|
157
163
|
call.session = mock('session', :unique_id => '123')
|
164
|
+
call.response = mock('response', :header => {})
|
158
165
|
end
|
159
166
|
|
160
|
-
it "should subscribe to
|
167
|
+
it "should subscribe to events" do
|
161
168
|
call.should_receive(:myevents)
|
162
169
|
call.start_session
|
163
170
|
end
|
164
171
|
|
165
|
-
it "should
|
172
|
+
it "should execute linger command" do
|
166
173
|
call.should_receive(:linger)
|
167
174
|
call.start_session
|
168
|
-
call.send_response :reply_ok
|
169
175
|
end
|
170
176
|
|
171
|
-
it "should answer
|
177
|
+
it "should run answer command in connect callback be default" do
|
172
178
|
call.should_receive(:answer)
|
173
179
|
call.start_session
|
174
|
-
call.send_response :reply_ok
|
175
180
|
end
|
176
181
|
end
|
177
182
|
|
178
183
|
context "on answer" do
|
179
184
|
before do
|
180
|
-
call.queue
|
181
|
-
call.session
|
185
|
+
call.queue = []
|
186
|
+
call.session = mock('session', :unique_id => '123')
|
187
|
+
call.response = mock('response', :header => {})
|
182
188
|
end
|
183
189
|
|
184
190
|
it "should change state to ready" do
|
@@ -187,17 +193,13 @@ describe Larynx::CallHandler do
|
|
187
193
|
end
|
188
194
|
|
189
195
|
it "should fire global answer callback" do
|
190
|
-
|
191
|
-
|
192
|
-
|
196
|
+
with_global_callback(:answer, should_be_called) do
|
197
|
+
answer_call
|
198
|
+
end
|
193
199
|
end
|
194
200
|
|
195
201
|
def answer_call
|
196
|
-
call.
|
197
|
-
call.send_response :reply_ok
|
198
|
-
call.send_response :reply_ok
|
199
|
-
call.send_response :reply_ok
|
200
|
-
call.send_response :execute_complete
|
202
|
+
call.send_response :answered
|
201
203
|
end
|
202
204
|
end
|
203
205
|
|
@@ -288,13 +290,13 @@ describe Larynx::CallHandler do
|
|
288
290
|
context "timer" do
|
289
291
|
it "should add EM timer with name and timeout" do
|
290
292
|
Larynx::RestartableTimer.stub!(:new)
|
291
|
-
call.
|
293
|
+
call.add_timer(:test, 0.1)
|
292
294
|
call.timers[:test].should_not be_nil
|
293
295
|
end
|
294
296
|
|
295
297
|
it "should run callback on timeout" do
|
296
298
|
em do
|
297
|
-
call.
|
299
|
+
call.add_timer(:test, 0.2) { @callback = true; done }
|
298
300
|
end
|
299
301
|
@callback.should be_true
|
300
302
|
end
|
@@ -303,20 +305,18 @@ describe Larynx::CallHandler do
|
|
303
305
|
context "stop_timer" do
|
304
306
|
it "should run callback on timeout" do
|
305
307
|
em do
|
306
|
-
call.
|
308
|
+
call.add_timer(:test, 1, &should_be_called)
|
307
309
|
EM::Timer.new(0.1) { call.stop_timer :test; done }
|
308
310
|
end
|
309
|
-
@callback.should be_true
|
310
311
|
end
|
311
312
|
end
|
312
313
|
|
313
314
|
context "cancel_timer" do
|
314
315
|
it "should not run callback" do
|
315
316
|
em do
|
316
|
-
call.
|
317
|
+
call.add_timer(:test, 0.2, &should_not_be_called)
|
317
318
|
EM::Timer.new(0.1) { call.cancel_timer :test; done }
|
318
319
|
end
|
319
|
-
@callback.should_not be_true
|
320
320
|
end
|
321
321
|
end
|
322
322
|
|
@@ -325,7 +325,7 @@ describe Larynx::CallHandler do
|
|
325
325
|
start = Time.now
|
326
326
|
em do
|
327
327
|
EM::Timer.new(0.5) { call.restart_timer :test }
|
328
|
-
call.
|
328
|
+
call.add_timer(:test, 1) { done }
|
329
329
|
end
|
330
330
|
(Time.now-start).should be_close(1.5, 0.2)
|
331
331
|
end
|
data/spec/larynx/fields_spec.rb
CHANGED
@@ -1,40 +1,47 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
|
+
class TestApp < Larynx::Application; end
|
4
|
+
|
3
5
|
describe Larynx::Fields do
|
4
6
|
attr_reader :call, :app
|
5
7
|
|
6
8
|
before do
|
7
9
|
@call = TestCallHandler.new(1)
|
8
|
-
@app
|
10
|
+
@app = define_app.new(@call)
|
9
11
|
end
|
10
12
|
|
11
13
|
context 'module' do
|
12
|
-
|
14
|
+
before do
|
15
|
+
@app_class = define_app
|
16
|
+
end
|
13
17
|
|
14
18
|
it 'should add field class method' do
|
15
|
-
|
19
|
+
@app_class.should respond_to(:field)
|
16
20
|
end
|
17
21
|
|
18
22
|
it 'should add instance accessor for field name' do
|
19
|
-
|
20
|
-
|
23
|
+
@app_class.field(:guess) { prompt :speak => 'hello' }
|
24
|
+
@app_class.methods.include?(:guess)
|
21
25
|
end
|
22
26
|
end
|
23
27
|
|
24
28
|
context 'next_field' do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
+
before do
|
30
|
+
@app = define_app do
|
31
|
+
field(:field1) { prompt :speak => 'hello' }
|
32
|
+
field(:field2) { prompt :speak => 'hello' }
|
33
|
+
field(:field3) { prompt :speak => 'hello' }
|
34
|
+
end.new(call)
|
35
|
+
end
|
29
36
|
|
30
37
|
it 'should iterate over defined fields' do
|
31
|
-
next_field.name.should == :field1
|
32
|
-
next_field.name.should == :field2
|
33
|
-
next_field.name.should == :field3
|
38
|
+
app.next_field.name.should == :field1
|
39
|
+
app.next_field.name.should == :field2
|
40
|
+
app.next_field.name.should == :field3
|
34
41
|
end
|
35
42
|
|
36
43
|
it 'should jump to field name if supplied' do
|
37
|
-
next_field(:field2).name.should == :field2
|
44
|
+
app.next_field(:field2).name.should == :field2
|
38
45
|
end
|
39
46
|
end
|
40
47
|
|
@@ -52,6 +59,18 @@ describe Larynx::Fields do
|
|
52
59
|
fld.run app
|
53
60
|
end
|
54
61
|
|
62
|
+
it 'should pass timeout and length options to the prompt object' do
|
63
|
+
fld = field(:guess, :length => 1, :min_length => 1, :max_length => 2, :interdigit_timeout => 1, :timeout => 2) do
|
64
|
+
prompt :speak => 'first'
|
65
|
+
end
|
66
|
+
fld.run(app)
|
67
|
+
prompt = fld.current_prompt
|
68
|
+
prompt.interdigit_timeout.should == 1
|
69
|
+
prompt.timeout.should == 2
|
70
|
+
prompt.minimum_length.should == 1
|
71
|
+
prompt.maximum_length.should == 2
|
72
|
+
end
|
73
|
+
|
55
74
|
it 'should return same prompt all attempts if single prompt' do
|
56
75
|
fld = field(:guess) do
|
57
76
|
prompt :speak => 'first'
|
@@ -86,17 +105,6 @@ describe Larynx::Fields do
|
|
86
105
|
fld.current_prompt.message.should == 'second'
|
87
106
|
end
|
88
107
|
|
89
|
-
context 'valid_length?' do
|
90
|
-
it 'should be false if input size less than minimum' do
|
91
|
-
fld = field(:guess) do
|
92
|
-
prompt :speak => 'first'
|
93
|
-
end
|
94
|
-
fld.run app
|
95
|
-
fld.current_prompt.finalise
|
96
|
-
fld.valid_length?.should be_false
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
108
|
context 'input evaluation' do
|
101
109
|
it 'should run validate callback if input minimum length' do
|
102
110
|
call_me = should_be_called
|
@@ -205,4 +213,11 @@ describe Larynx::Fields do
|
|
205
213
|
@app.class.class_eval { attr_accessor name }
|
206
214
|
Larynx::Fields::Field.new(name, options, &block)
|
207
215
|
end
|
216
|
+
|
217
|
+
def define_app(&block)
|
218
|
+
reset_class(TestApp) do
|
219
|
+
include Larynx::Fields
|
220
|
+
instance_eval &block if block_given?
|
221
|
+
end
|
222
|
+
end
|
208
223
|
end
|
data/spec/larynx/prompt_spec.rb
CHANGED
@@ -85,17 +85,18 @@ describe Larynx::Prompt do
|
|
85
85
|
context "after callback" do
|
86
86
|
context "input completed" do
|
87
87
|
it "should not add timers if reached length" do
|
88
|
+
prompt = new_prompt
|
89
|
+
prompt.should_not_receive(:add_digit_timer)
|
90
|
+
prompt.should_not_receive(:add_input_timer)
|
88
91
|
call.input << '1'
|
89
|
-
|
90
|
-
call.should_not_receive(:timer).with(:input, anything())
|
91
|
-
after_callback new_prompt
|
92
|
+
after_callback prompt
|
92
93
|
end
|
93
94
|
|
94
95
|
it "should not add timers if termchar input" do
|
95
96
|
prompt = new_prompt(:speak => 'hello', :length => 2, :termchar => '#')
|
97
|
+
prompt.should_not_receive(:add_digit_timer)
|
98
|
+
prompt.should_not_receive(:add_input_timer)
|
96
99
|
call.input << '#'
|
97
|
-
call.should_not_receive(:timer).with(:digit, anything())
|
98
|
-
call.should_not_receive(:timer).with(:input, anything())
|
99
100
|
after_callback prompt
|
100
101
|
end
|
101
102
|
|
@@ -109,14 +110,15 @@ describe Larynx::Prompt do
|
|
109
110
|
|
110
111
|
context "input not completed" do
|
111
112
|
it "should add timers" do
|
112
|
-
|
113
|
-
|
114
|
-
|
113
|
+
prompt = new_prompt
|
114
|
+
prompt.should_receive(:add_digit_timer)
|
115
|
+
prompt.should_receive(:add_input_timer)
|
116
|
+
after_callback prompt
|
115
117
|
end
|
116
118
|
|
117
119
|
it "should add itself as call observer" do
|
118
120
|
prompt = new_prompt
|
119
|
-
call.stub!(:
|
121
|
+
call.stub!(:add_timer)
|
120
122
|
after_callback prompt
|
121
123
|
call.observers.should include(prompt)
|
122
124
|
end
|
@@ -142,15 +144,29 @@ describe Larynx::Prompt do
|
|
142
144
|
call.should_receive(:remove_observer).with(prompt)
|
143
145
|
prompt.finalise
|
144
146
|
end
|
147
|
+
|
148
|
+
it "should clear input" do
|
149
|
+
prompt = new_prompt
|
150
|
+
call.should_receive(:clear_input)
|
151
|
+
prompt.finalise
|
152
|
+
end
|
145
153
|
end
|
146
154
|
|
147
155
|
context "user callback" do
|
148
156
|
it "should be passed input argument equal to call input" do
|
149
|
-
prompt = new_prompt {|input| @callback =
|
157
|
+
prompt = new_prompt {|input| @callback = input }
|
150
158
|
call.input << '1'
|
151
159
|
after_callback prompt
|
152
160
|
@callback.should == '1'
|
153
161
|
end
|
162
|
+
|
163
|
+
it "should be passed input and result arguments if block arity is 2" do
|
164
|
+
prompt = new_prompt(:speak => '', :length => 1) {|input, result| @input = input; @result = result }
|
165
|
+
call.input << '1'
|
166
|
+
after_callback prompt
|
167
|
+
@input.should == '1'
|
168
|
+
@result.should be_true
|
169
|
+
end
|
154
170
|
end
|
155
171
|
|
156
172
|
context "interdigit timeout" do
|
data/spec/spec_helper.rb
CHANGED
@@ -11,8 +11,14 @@ LARYNX_LOGGER = Logger.new(STDOUT)
|
|
11
11
|
RESPONSES = {}
|
12
12
|
Dir['spec/fixtures/*.rb'].each {|file| require file }
|
13
13
|
|
14
|
+
Larynx.module_eval do
|
15
|
+
def self.callbacks
|
16
|
+
@callbacks
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
14
20
|
class TestCallHandler < Larynx::CallHandler
|
15
|
-
attr_accessor :sent_data, :session, :state, :queue, :input, :timers
|
21
|
+
attr_accessor :sent_data, :session, :response, :state, :queue, :input, :timers
|
16
22
|
|
17
23
|
def send_data(msg)
|
18
24
|
@sent_data = msg
|
@@ -40,6 +46,22 @@ module SpecHelper
|
|
40
46
|
proc.should_not_receive(:call).instance_eval(&(block || lambda {}))
|
41
47
|
lambda { |*args| proc.call(*args) }
|
42
48
|
end
|
49
|
+
|
50
|
+
def with_global_callback(name, test_callback)
|
51
|
+
default = Larynx.callbacks[name]
|
52
|
+
Larynx.send(name, &test_callback)
|
53
|
+
yield
|
54
|
+
Larynx.send(name, &default)
|
55
|
+
end
|
56
|
+
|
57
|
+
def reset_class(klass, &block)
|
58
|
+
name = klass.name.to_sym
|
59
|
+
Object.send(:remove_const, name)
|
60
|
+
eval "class #{klass}#{' < ' + klass.superclass.to_s if klass.superclass != Class}; end", TOPLEVEL_BINDING
|
61
|
+
new_klass = Object.const_get(name)
|
62
|
+
new_klass.class_eval &block if block_given?
|
63
|
+
new_klass
|
64
|
+
end
|
43
65
|
end
|
44
66
|
|
45
67
|
Spec::Runner.configure do |config|
|
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
|
+
- 3
|
9
|
+
version: 0.1.3
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Adam Meehan
|
@@ -14,7 +14,7 @@ autorequire: larynx
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-04-29 00:00:00 +10:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -73,7 +73,7 @@ dependencies:
|
|
73
73
|
version: 0.1.3
|
74
74
|
type: :development
|
75
75
|
version_requirements: *id004
|
76
|
-
description:
|
76
|
+
description: An evented application framework for the FreeSWITCH telephony platform
|
77
77
|
email: adam.meehan@gmail.com
|
78
78
|
executables:
|
79
79
|
- larynx
|
@@ -88,6 +88,7 @@ files:
|
|
88
88
|
- lib/larynx/application.rb
|
89
89
|
- lib/larynx/call_handler.rb
|
90
90
|
- lib/larynx/callbacks.rb
|
91
|
+
- lib/larynx/callbacks_with_async.rb
|
91
92
|
- lib/larynx/command.rb
|
92
93
|
- lib/larynx/commands.rb
|
93
94
|
- lib/larynx/fields.rb
|
@@ -101,6 +102,7 @@ files:
|
|
101
102
|
- lib/larynx/version.rb
|
102
103
|
- lib/larynx.rb
|
103
104
|
- spec/fixtures/answer.rb
|
105
|
+
- spec/fixtures/answered.rb
|
104
106
|
- spec/fixtures/break.rb
|
105
107
|
- spec/fixtures/channel_data.rb
|
106
108
|
- spec/fixtures/dtmf.rb
|
@@ -147,6 +149,6 @@ rubyforge_project: larynx
|
|
147
149
|
rubygems_version: 1.3.6
|
148
150
|
signing_key:
|
149
151
|
specification_version: 3
|
150
|
-
summary:
|
152
|
+
summary: An evented application framework for the FreeSWITCH telephony platform
|
151
153
|
test_files: []
|
152
154
|
|