larynx 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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