larynx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|