larynx 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +191 -0
- data/Rakefile +55 -0
- data/bin/larynx +7 -0
- data/examples/guess.rb +39 -0
- data/examples/guess_form.rb +34 -0
- data/examples/multiple_apps.rb +63 -0
- data/lib/larynx/application.rb +24 -0
- data/lib/larynx/call_handler.rb +174 -0
- data/lib/larynx/callbacks.rb +32 -0
- data/lib/larynx/command.rb +73 -0
- data/lib/larynx/commands.rb +88 -0
- data/lib/larynx/fields.rb +143 -0
- data/lib/larynx/form.rb +19 -0
- data/lib/larynx/logger.rb +8 -0
- data/lib/larynx/observable.rb +35 -0
- data/lib/larynx/prompt.rb +88 -0
- data/lib/larynx/response.rb +57 -0
- data/lib/larynx/restartable_timer.rb +26 -0
- data/lib/larynx/session.rb +20 -0
- data/lib/larynx/version.rb +3 -0
- data/lib/larynx.rb +109 -0
- data/spec/fixtures/answer.rb +125 -0
- data/spec/fixtures/channel_data.rb +147 -0
- data/spec/fixtures/dtmf.rb +52 -0
- data/spec/fixtures/execute.rb +52 -0
- data/spec/fixtures/execute_complete.rb +133 -0
- data/spec/fixtures/reply_ok.rb +6 -0
- data/spec/larynx/call_handler_spec.rb +290 -0
- data/spec/larynx/command_spec.rb +76 -0
- data/spec/larynx/eventmachince_spec.rb +14 -0
- data/spec/larynx/fields_spec.rb +194 -0
- data/spec/larynx/prompt_spec.rb +222 -0
- data/spec/larynx_spec.rb +4 -0
- data/spec/spec_helper.rb +47 -0
- metadata +96 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Adam Meehan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
= Larynx
|
2
|
+
|
3
|
+
A framework to develop IVR applications in Ruby for the FreeSWITCH (FS) telephony platform. It is used
|
4
|
+
with the FS event socket module to easily develop IVR applications in an asynchronous fashion.
|
5
|
+
|
6
|
+
It offer some useful functions and classes on top the default FreeSWITCH dialplan commands to make
|
7
|
+
application development easier.
|
8
|
+
|
9
|
+
Larynx currently implements an 'outbound' socket listener for incoming calls to be handled. An 'inbound'
|
10
|
+
module will probably follow soon enough.
|
11
|
+
|
12
|
+
== Install
|
13
|
+
|
14
|
+
On Rubygems.org:
|
15
|
+
|
16
|
+
sudo gem install larynx
|
17
|
+
|
18
|
+
You will need to have the FreeSWITCH server installed somewhere you can control.
|
19
|
+
|
20
|
+
== Example
|
21
|
+
|
22
|
+
Simplest possible
|
23
|
+
|
24
|
+
Larynx.answer {|call|
|
25
|
+
call.speak 'Hello world! Or whoever you are.'
|
26
|
+
}
|
27
|
+
|
28
|
+
Using the bare Application class, below is a guessing game.
|
29
|
+
|
30
|
+
class Guess < Larynx::Application
|
31
|
+
def run
|
32
|
+
@number = rand(9) + 1
|
33
|
+
@guess = ''
|
34
|
+
@guesses = 0
|
35
|
+
get_guess
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_guess
|
39
|
+
if @guesses < 3
|
40
|
+
speak(guess_prompt) { @guesses += 1 }
|
41
|
+
else
|
42
|
+
speak "Sorry you didn't guess it. It was #{@number}. Try again soon.", :bargein => false
|
43
|
+
hangup
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def guess_prompt
|
48
|
+
@guesses == 0 ? 'Guess a number between 1 and 9.' : 'Have another guess.'
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_guess
|
52
|
+
if @guess.to_i == @number
|
53
|
+
speak "You got it! It was #{@guess}. It took you #{@guesses} guesses.", :bargein => false
|
54
|
+
speak "Thanks for playing."
|
55
|
+
hangup
|
56
|
+
else
|
57
|
+
speak "No it's not #{@guess}."
|
58
|
+
get_guess
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def dtmf_received(input)
|
63
|
+
@guess = input
|
64
|
+
check_guess
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Larynx.answer {|call| Guess.run(call) }
|
69
|
+
|
70
|
+
A more sophisticated example using the Form class
|
71
|
+
|
72
|
+
class Guess < Larynx::Form
|
73
|
+
field(:guess, :attempts => 3, :length => 1) do
|
74
|
+
prompt :speak => 'Guess a number between 1 and 9.', :interdigit_timeout => 6
|
75
|
+
reprompt :speak => 'Have another guess.', :interdigit_timeout => 6
|
76
|
+
|
77
|
+
setup do
|
78
|
+
@number = rand(9) + 1
|
79
|
+
@guesses = 0
|
80
|
+
end
|
81
|
+
|
82
|
+
validate do
|
83
|
+
@guesses += 1 if guess.size > 0
|
84
|
+
@number == guess.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
invalid do
|
88
|
+
if guess.size > 0
|
89
|
+
speak "No, it's not #{guess}.", :bargein => false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
success do
|
94
|
+
speak "You got it! It was #{guess}. It took you #{@guesses} guesses.", :bargein => false
|
95
|
+
hangup
|
96
|
+
end
|
97
|
+
|
98
|
+
failure do
|
99
|
+
speak "Sorry you didn't guess it. It was #{@number}. Try again soon.", :bargein => false
|
100
|
+
hangup
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
Larynx.answer {|call| Guess.run(call) }
|
106
|
+
|
107
|
+
The Form class wraps up many handy conventions into a pleasant DSL in which allows you to control the user
|
108
|
+
interaction more easily.
|
109
|
+
|
110
|
+
Save your app into file and run larynx comand to start the app server ready to receive calls.
|
111
|
+
|
112
|
+
$ larynx app.rb
|
113
|
+
|
114
|
+
Now make a call to extension 2000 with a SIP phone. Your app should start.
|
115
|
+
|
116
|
+
|
117
|
+
== Configure FreeSWTICH
|
118
|
+
|
119
|
+
To set up a dialplan which connects to your app read http://wiki.freeswitch.org/wiki/Event_Socket
|
120
|
+
|
121
|
+
Also take a look at the http://wiki.freeswitch.org/wiki/Event_socket_outbound for background.
|
122
|
+
|
123
|
+
Example socket diaplan:
|
124
|
+
|
125
|
+
<include>
|
126
|
+
<extension name="outbound_socket">
|
127
|
+
<condition field="destination_number" expression="^2000$">
|
128
|
+
<action application="socket" data="localhost:8084 async full" />
|
129
|
+
</condition>
|
130
|
+
</extension>
|
131
|
+
</include>
|
132
|
+
|
133
|
+
Which connects calls to destination number 2000 to your event socket app.
|
134
|
+
|
135
|
+
|
136
|
+
== Global Hooks
|
137
|
+
|
138
|
+
Larynx provides three globals hooks you can use to perform some action at each point.
|
139
|
+
The are:
|
140
|
+
|
141
|
+
Larynx.connect {|call|
|
142
|
+
# you can choose to hangup the call here if you wish
|
143
|
+
}
|
144
|
+
|
145
|
+
Larynx.answer {|call|
|
146
|
+
# call is answered and ready to interact with the caller
|
147
|
+
}
|
148
|
+
|
149
|
+
Larynx.hungup {|call|
|
150
|
+
# finish off any logging or some such
|
151
|
+
}
|
152
|
+
|
153
|
+
Mainly you just use the answer hook. From the examples you can see can start sending commands or
|
154
|
+
start an application class running. You write an app just in this block but you don't want to.
|
155
|
+
|
156
|
+
|
157
|
+
== Application Class
|
158
|
+
|
159
|
+
The application adds a sprinkling of convenience for handling a call, plus you can store instance
|
160
|
+
variables and create methods for structuring you app better.
|
161
|
+
|
162
|
+
The application should define a run instance method which is used to kick it off when you call
|
163
|
+
|
164
|
+
MyApp.run(call)
|
165
|
+
|
166
|
+
The class method initialises some things for you and then calls <tt>run</tt> on the instance. From
|
167
|
+
there its up to you. You can use all the commands directly rather than call them on the call
|
168
|
+
instance.
|
169
|
+
|
170
|
+
== Form Class
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
== Event Hooks
|
175
|
+
|
176
|
+
The Application and Form classes have a couple of useful event hook methods available which are
|
177
|
+
|
178
|
+
class MyApp < Larynx::Application
|
179
|
+
def run
|
180
|
+
end
|
181
|
+
|
182
|
+
def dtmf_received(input)
|
183
|
+
# input is the button the user just pushed
|
184
|
+
end
|
185
|
+
|
186
|
+
def hungup
|
187
|
+
# application specific handling of a hangup
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rubygems/specification'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'lib/larynx/version'
|
7
|
+
|
8
|
+
GEM_NAME = "larynx"
|
9
|
+
GEM_VERSION = Larynx::VERSION
|
10
|
+
|
11
|
+
spec = Gem::Specification.new do |s|
|
12
|
+
s.name = GEM_NAME
|
13
|
+
s.version = GEM_VERSION
|
14
|
+
s.platform = Gem::Platform::RUBY
|
15
|
+
s.rubyforge_project = GEM_NAME
|
16
|
+
s.has_rdoc = true
|
17
|
+
s.extra_rdoc_files = ["README.rdoc"]
|
18
|
+
s.executables = ["larynx"]
|
19
|
+
s.summary = ""
|
20
|
+
s.description = s.summary
|
21
|
+
s.author = "Adam Meehan"
|
22
|
+
s.email = "adam.meehan@gmail.com"
|
23
|
+
s.homepage = "http://github.com/adzap/larynx"
|
24
|
+
|
25
|
+
s.require_path = 'lib'
|
26
|
+
s.autorequire = GEM_NAME
|
27
|
+
s.files = %w(MIT-LICENSE README.rdoc Rakefile) + Dir.glob("{lib,spec,examples}/**/*")
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'Default: run specs.'
|
31
|
+
task :default => :spec
|
32
|
+
|
33
|
+
spec_files = Rake::FileList["spec/**/*_spec.rb"]
|
34
|
+
|
35
|
+
desc "Run specs"
|
36
|
+
Spec::Rake::SpecTask.new do |t|
|
37
|
+
t.spec_files = spec_files
|
38
|
+
t.spec_opts = ["-c"]
|
39
|
+
end
|
40
|
+
|
41
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
42
|
+
pkg.gem_spec = spec
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "install the gem locally"
|
46
|
+
task :install => [:package] do
|
47
|
+
sh %{gem install pkg/#{GEM_NAME}-#{GEM_VERSION}}
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "create a gemspec file"
|
51
|
+
task :make_spec do
|
52
|
+
File.open("#{GEM_NAME}.gemspec", "w") do |file|
|
53
|
+
file.puts spec.to_ruby
|
54
|
+
end
|
55
|
+
end
|
data/bin/larynx
ADDED
data/examples/guess.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
class Guess < Larynx::Application
|
2
|
+
def run
|
3
|
+
@number = rand(9) + 1
|
4
|
+
@guess = ''
|
5
|
+
@guesses = 0
|
6
|
+
get_guess
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_guess
|
10
|
+
if @guesses < 3
|
11
|
+
speak(guess_prompt) { @guesses += 1 }
|
12
|
+
else
|
13
|
+
speak "Sorry you didn't guess it. It was #{@number}. Try again soon.", :bargein => false
|
14
|
+
hangup
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def guess_prompt
|
19
|
+
@guesses == 0 ? 'Guess a number between 1 and 9.' : 'Have another guess.'
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_guess
|
23
|
+
if @guess.to_i == @number
|
24
|
+
speak "You got it! It was #{@guess}. It took you #{@guesses} guesses.", :bargein => false
|
25
|
+
speak "Thanks for playing."
|
26
|
+
hangup
|
27
|
+
else
|
28
|
+
speak "No it's not #{@guess}."
|
29
|
+
get_guess
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def dtmf_received(input)
|
34
|
+
@guess = input
|
35
|
+
check_guess
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Larynx.answer {|call| Guess.run(call) }
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Guess < Larynx::Form
|
2
|
+
field(:guess, :attempts => 3, :length => 1) do
|
3
|
+
prompt :speak => 'Guess a number between 1 and 9.', :interdigit_timeout => 6
|
4
|
+
reprompt :speak => 'Have another guess.', :interdigit_timeout => 6
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@number = rand(9) + 1
|
8
|
+
@guesses = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
validate do
|
12
|
+
@guesses += 1 if guess.size > 0
|
13
|
+
@number == guess.to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
invalid do
|
17
|
+
if guess.size > 0
|
18
|
+
speak "No, it's not #{guess}.", :bargein => false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
success do
|
23
|
+
speak "You got it! It was #{guess}. It took you #{@guesses} guesses.", :bargein => false
|
24
|
+
hangup
|
25
|
+
end
|
26
|
+
|
27
|
+
failure do
|
28
|
+
speak "Sorry you didn't guess it. It was #{@number}. Try again soon.", :bargein => false
|
29
|
+
hangup
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Larynx.answer {|call| Guess.run(call) }
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Login < Larynx::Form
|
2
|
+
setup do
|
3
|
+
@user_id = '1234'
|
4
|
+
@pin = '4321'
|
5
|
+
@attempts = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
field(:enter_id, :attempts => 3, :length => 4) do
|
9
|
+
prompt :speak => 'Please enter your 4 digit user ID.', :bargein => true
|
10
|
+
|
11
|
+
success do
|
12
|
+
next_field
|
13
|
+
end
|
14
|
+
|
15
|
+
failure do
|
16
|
+
speak "You have been unable to enter your user ID. Goodbye."
|
17
|
+
hangup
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
field(:enter_pin, :attempts => 3, :length => 4) do
|
22
|
+
prompt :speak => 'Now enter your 4 digit pin.', :bargein => true
|
23
|
+
|
24
|
+
success do
|
25
|
+
if valid_credentials?
|
26
|
+
speak "Credentials accepted."
|
27
|
+
Party.run(call)
|
28
|
+
else
|
29
|
+
failed_login
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
failure do
|
34
|
+
speak "You have been unable to enter your pin. Goodbye."
|
35
|
+
hangup
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def valid_credentials?
|
40
|
+
enter_id == @user_id && enter_pin == @pin
|
41
|
+
end
|
42
|
+
|
43
|
+
def failed_login
|
44
|
+
@attempts += 1
|
45
|
+
if @attempts < 3
|
46
|
+
speak "Those credentials are invalid. Try again."
|
47
|
+
next_field :enter_id
|
48
|
+
else
|
49
|
+
speak "You have been able to login. Goodbye."
|
50
|
+
hangup
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Party < Larynx::Application
|
56
|
+
def run
|
57
|
+
speak 'Time to party!'
|
58
|
+
speak 'But all on your own. Goodbye.'
|
59
|
+
hangup
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Larynx.answer {|call| Login.run(call) }
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Larynx
|
2
|
+
class Application
|
3
|
+
attr_reader :call
|
4
|
+
delegate *Commands.instance_methods << {:to => :call}
|
5
|
+
|
6
|
+
def self.run(call)
|
7
|
+
app = self.new(call)
|
8
|
+
call.add_observer app
|
9
|
+
app.run
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(call)
|
13
|
+
@call = call
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
#override for setup
|
18
|
+
end
|
19
|
+
|
20
|
+
def log(msg)
|
21
|
+
app.call.log(msg)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# FIXME interrupted commands callback can be fired out of order if new command sent on break
|
2
|
+
module Larynx
|
3
|
+
class CallHandler < EventMachine::Protocols::HeaderAndContentProtocol
|
4
|
+
include Observable
|
5
|
+
include Commands
|
6
|
+
|
7
|
+
attr_reader :state, :session, :response, :input, :observers, :last_command
|
8
|
+
|
9
|
+
# EM hook which is run when call is received
|
10
|
+
def post_init
|
11
|
+
@queue, @input, @timers = [], [], {}
|
12
|
+
connect {
|
13
|
+
@session = Session.new(@response.header)
|
14
|
+
log "Call received from #{caller_id}"
|
15
|
+
Larynx.fire_callback(:connect, self)
|
16
|
+
start_session
|
17
|
+
}
|
18
|
+
send_next_command
|
19
|
+
end
|
20
|
+
|
21
|
+
def start_session
|
22
|
+
myevents {
|
23
|
+
linger
|
24
|
+
answer {
|
25
|
+
log 'Answered call'
|
26
|
+
Larynx.fire_callback(:answer, self)
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def called_number
|
32
|
+
@session[:caller_destination_number]
|
33
|
+
end
|
34
|
+
|
35
|
+
def caller_id
|
36
|
+
@session[:caller_caller_id_number]
|
37
|
+
end
|
38
|
+
|
39
|
+
def interrupt_command
|
40
|
+
if @state == :executing && current_command.interruptable?
|
41
|
+
break!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def clear_input
|
46
|
+
@input = []
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute(command, immediately=false)
|
50
|
+
log "Queued: #{command.name}"
|
51
|
+
if immediately
|
52
|
+
@queue.unshift command
|
53
|
+
send_next_command
|
54
|
+
else
|
55
|
+
@queue << command
|
56
|
+
end
|
57
|
+
command
|
58
|
+
end
|
59
|
+
|
60
|
+
def timer(name, timeout, &block)
|
61
|
+
@timers[name] = [RestartableTimer.new(timeout) {
|
62
|
+
timer = @timers.delete(name)
|
63
|
+
timer[1].call if timer[1]
|
64
|
+
notify_observers :timed_out
|
65
|
+
send_next_command if @state == :ready
|
66
|
+
}, block]
|
67
|
+
end
|
68
|
+
|
69
|
+
def cancel_timer(name)
|
70
|
+
if timer = @timers.delete(name)
|
71
|
+
timer[0].cancel
|
72
|
+
send_next_command if @state == :ready
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def cancel_all_timers
|
77
|
+
@timers.values.each {|t| t[0].cancel }
|
78
|
+
end
|
79
|
+
|
80
|
+
def stop_timer(name)
|
81
|
+
if timer = @timers.delete(name)
|
82
|
+
timer[0].cancel
|
83
|
+
timer[1].call if timer[1]
|
84
|
+
send_next_command if @state == :ready
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def restart_timer(name)
|
89
|
+
if timer = @timers[name]
|
90
|
+
timer[0].restart
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def cleanup
|
95
|
+
break! if @state == :executing
|
96
|
+
cancel_all_timers
|
97
|
+
clear_observers!
|
98
|
+
end
|
99
|
+
|
100
|
+
def receive_request(header, content)
|
101
|
+
@response = Response.new(header, content)
|
102
|
+
|
103
|
+
case
|
104
|
+
when @response.reply? && !current_command.is_a?(AppCommand)
|
105
|
+
log "Completed: #{current_command.name}"
|
106
|
+
finalize_command
|
107
|
+
@state = :ready
|
108
|
+
send_next_command
|
109
|
+
when @response.executing?
|
110
|
+
log "Executing: #{current_command.name}"
|
111
|
+
run_command_setup
|
112
|
+
@state = :executing
|
113
|
+
when @response.executed? && current_command
|
114
|
+
finalize_command
|
115
|
+
unless interrupted?
|
116
|
+
@state = :ready
|
117
|
+
send_next_command
|
118
|
+
end
|
119
|
+
when @response.dtmf?
|
120
|
+
log "Button pressed: #{@response.body[:dtmf_digit]}"
|
121
|
+
handle_dtmf
|
122
|
+
when @response.speech?
|
123
|
+
when @response.disconnect?
|
124
|
+
log "Disconnected."
|
125
|
+
cleanup
|
126
|
+
notify_observers :hungup
|
127
|
+
Larynx.fire_callback(:hungup, self)
|
128
|
+
@state = :waiting
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def handle_dtmf
|
133
|
+
@input << @response.body[:dtmf_digit]
|
134
|
+
interrupt_command
|
135
|
+
notify_observers :dtmf_received, @response.body[:dtmf_digit]
|
136
|
+
send_next_command if @state == :ready
|
137
|
+
end
|
138
|
+
|
139
|
+
def current_command
|
140
|
+
@queue.first
|
141
|
+
end
|
142
|
+
|
143
|
+
def interrupting?
|
144
|
+
current_command && current_command.command == 'break'
|
145
|
+
end
|
146
|
+
|
147
|
+
def interrupted?
|
148
|
+
last_command && last_command.command == 'break'
|
149
|
+
end
|
150
|
+
|
151
|
+
def run_command_setup
|
152
|
+
current_command.fire_callback :before
|
153
|
+
end
|
154
|
+
|
155
|
+
def finalize_command
|
156
|
+
if command = @queue.shift
|
157
|
+
command.fire_callback :after
|
158
|
+
@last_command = command
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def send_next_command
|
163
|
+
if current_command
|
164
|
+
@state = :sending
|
165
|
+
send_data current_command.to_s
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def log(msg)
|
170
|
+
LARYNX_LOGGER.info msg
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Larynx
|
2
|
+
module Callbacks
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.class_eval do
|
7
|
+
include InstanceMethods
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def define_callback(*callbacks)
|
13
|
+
callbacks.each do |callback|
|
14
|
+
class_eval <<-DEF
|
15
|
+
def #{callback}(&block)
|
16
|
+
@callbacks ||= {}
|
17
|
+
@callbacks[:#{callback}] = block
|
18
|
+
self
|
19
|
+
end
|
20
|
+
DEF
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
def fire_callback(callback, *args)
|
27
|
+
@callbacks && @callbacks[callback] && @callbacks[callback].call(*args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|