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 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 user
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 user just pushed
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"
@@ -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
- myevents {
22
- linger
23
- answer {
24
- log 'Answered call'
25
- Larynx.fire_callback(:answer, self)
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 timer(name, timeout, &block)
54
+ def add_timer(name, timeout, &block)
62
55
  @timers[name] = [RestartableTimer.new(timeout) {
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
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.delete(name)
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.delete(name)
86
- timer[0].cancel
87
- timer[1].call if timer[1]
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 && !current_command.is_a?(AppCommand)
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
- run_command_setup
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.fire_callback(:after) if this_command.command == 'break'
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.fire_callback(:after) unless command.interrupted?
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
@@ -26,6 +26,14 @@ module Larynx
26
26
  def interrupted?
27
27
  @interrupted
28
28
  end
29
+
30
+ def setup
31
+ fire_callback :before
32
+ end
33
+
34
+ def finalize
35
+ fire_callback :after
36
+ end
29
37
  end
30
38
 
31
39
  class CallCommand < Command
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.send :include, InstanceMethods
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
- @fields ||= []
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 ||= 0
26
- @current_field = index_of_field(field_name) if field_name
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 index_of_field(name)
35
- field = self.class.fields.find {|f| f.name == name }
36
- self.class.fields.index(field)
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 Callbacks
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
- class_eval { include CallbacksWithMode }
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 = ([:play, :speak, :phrase] & options.keys).first
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
- def valid_length?
135
- @value.size >= minimum_length
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
- if valid_length?
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
- if result
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) { send_next_command }
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 maximum_length
173
- @options[:max_length] || @options[:length]
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 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
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.timer(:digit, @options[:interdigit_timeout]) {
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.timer(:input, @options[:timeout]) {
100
+ call.add_timer(:input, timeout) {
83
101
  call.cancel_timer :digit
84
102
  finalise
85
103
  }
@@ -10,6 +10,10 @@ module Larynx
10
10
  end
11
11
  end
12
12
 
13
+ def answered?
14
+ executed? && command_name == 'answer' #event_name == 'CHANNEL_ANSWER'
15
+ end
16
+
13
17
  def reply?
14
18
  @header[:content_type] == 'command/reply'
15
19
  end
@@ -1,3 +1,3 @@
1
1
  module Larynx
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end
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 => './larynx.pid',
33
- :log_file => './larynx.log'
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 tool to develop FreeSWITCH IVR applications in Ruby."
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
- LARYNX_LOGGER.info "Shutting down Larynx"
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
- LARYNX_LOGGER.info "Larynx starting up on #{@options[:ip]}:#{@options[:port]}"
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 { @callback = true }
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
- Larynx.connect {|call| @callback = true }
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 "on session start" do
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 myevents" do
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 set event lingering on after filter events" do
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 call after filter events" do
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 = mock('session', :unique_id => '123')
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
- Larynx.answer {|call| @callback = true }
191
- answer_call
192
- @callback.should be_true
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.start_session
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.timer(:test, 0.1)
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.timer(:test, 0.2) { @callback = true; done }
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.timer(:test, 1) { @callback = true }
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.timer(:test, 0.2) { @callback = true }
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.timer(:test, 1) { done }
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
@@ -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 = Larynx::Application.new(@call)
10
+ @app = define_app.new(@call)
9
11
  end
10
12
 
11
13
  context 'module' do
12
- include Larynx::Fields
14
+ before do
15
+ @app_class = define_app
16
+ end
13
17
 
14
18
  it 'should add field class method' do
15
- self.class.should respond_to(:field)
19
+ @app_class.should respond_to(:field)
16
20
  end
17
21
 
18
22
  it 'should add instance accessor for field name' do
19
- self.class.field(:guess) { prompt :speak => 'hello' }
20
- self.methods.include?(:guess)
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
- include Larynx::Fields
26
- field(:field1) { prompt :speak => 'hello' }
27
- field(:field2) { prompt :speak => 'hello' }
28
- field(:field3) { prompt :speak => 'hello' }
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
@@ -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
- call.should_not_receive(:timer).with(:digit, anything())
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
- call.should_receive(:timer).with(:digit, anything())
113
- call.should_receive(:timer).with(:input, anything())
114
- after_callback new_prompt
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!(:timer)
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 = '1'}
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
- - 2
9
- version: 0.1.2
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-03-31 00:00:00 +11:00
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