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 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ unless defined?(Gem)
4
+ $:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
5
+ end
6
+
7
+ require 'larynx'
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