larynx 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ module Larynx
2
+ class Command
3
+ include Callbacks
4
+ attr_reader :command
5
+
6
+ define_callback :before, :after
7
+
8
+ def initialize(command, params=nil, &block)
9
+ @command, @params, @callbacks = command, params, {}
10
+ after(&block) if block_given?
11
+ end
12
+
13
+ def to_s
14
+ @command
15
+ end
16
+
17
+ def name
18
+ @command
19
+ end
20
+
21
+ def interruptable?
22
+ false
23
+ end
24
+ end
25
+
26
+ class CallCommand < Command
27
+ def name
28
+ "#{@command}#{" #{@params}" if @params}"
29
+ end
30
+
31
+ def to_s
32
+ cmd = "#{@command}"
33
+ cmd << " #{@params}" if @params
34
+ cmd << "\n\n"
35
+ end
36
+ end
37
+
38
+ class ApiCommand < Command
39
+ def name
40
+ "#{@command}#{" #{@params}" if @params}"
41
+ end
42
+
43
+ def to_s
44
+ cmd = "api #{@command}"
45
+ cmd << " #{@params}" if @params
46
+ cmd << "\n\n"
47
+ end
48
+ end
49
+
50
+ class AppCommand < Command
51
+ def initialize(command, params=nil, options={}, &block)
52
+ super command, params, &block
53
+ @options = options.reverse_merge(:bargein => true)
54
+ end
55
+
56
+ def name
57
+ "#{@command}#{" '#{@params}'" if @params}"
58
+ end
59
+
60
+ def to_s
61
+ cmd = "sendmsg\n"
62
+ cmd << "call-command: execute\n"
63
+ cmd << "execute-app-name: #{@command}\n"
64
+ cmd << "execute-app-arg: #{@params}\n" if @params
65
+ cmd << "event-lock: #{@options[:lock]}\n" if @options[:lock]
66
+ cmd << "\n"
67
+ end
68
+
69
+ def interruptable?
70
+ @options[:bargein]
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,88 @@
1
+ module Larynx
2
+ module Commands
3
+
4
+ def connect(&block)
5
+ execute CallCommand.new('connect', &block)
6
+ end
7
+
8
+ def myevents(&block)
9
+ execute CallCommand.new('myevents', &block)
10
+ end
11
+
12
+ def filter(type, &block)
13
+ execute CallCommand.new('filter', type, &block)
14
+ end
15
+
16
+ def linger(&block)
17
+ execute CallCommand.new('linger', &block)
18
+ end
19
+
20
+ def answer(&block)
21
+ execute AppCommand.new('answer', &block)
22
+ end
23
+
24
+ def hangup(&block)
25
+ execute AppCommand.new('hangup', &block)
26
+ end
27
+
28
+ def playback(data, options={}, &block)
29
+ execute AppCommand.new('playback', data, options, &block)
30
+ end
31
+ alias_method :play, :playback
32
+
33
+ def speak(data, options={}, &block)
34
+ execute AppCommand.new('speak', data, options, &block)
35
+ end
36
+
37
+ def phrase(data, options={}, &block)
38
+ execute AppCommand.new('phrase', data, options, &block)
39
+ end
40
+
41
+
42
+ # Executes read command with some default values.
43
+ # Allows length option which expands into minimum and maximum length values. Length can be a range.
44
+ # Passes user input into callback block.
45
+ #
46
+ # Defaults:
47
+ # timeout: 5000 or 5 seconds
48
+ # termchar: #
49
+ #
50
+ # Example:
51
+ #
52
+ # read(:minimum => 1, :maximum => 2, :sound_file => 'en/us/callie/conference/8000/conf-pin.wav') {|input|
53
+ # speak "You entered #{input}"
54
+ # }
55
+ #
56
+ # Or
57
+ #
58
+ # read(:length => 1..2, :sound_file => 'en/us/callie/conference/8000/conf-pin.wav') {|input|
59
+ # speak "You entered #{input}"
60
+ # }
61
+ #
62
+ def read(options={}, &block)
63
+ options.reverse_merge!(:timeout => 5000, :var_name => 'read_result', :termchar => '#')
64
+ options[:bargein] = false
65
+
66
+ if length = options.delete(:length)
67
+ values = length.is_a?(Range) ? [length.first, length.last] : [length, length]
68
+ options.merge!(:minimum => values[0], :maximum => values[1])
69
+ end
70
+
71
+ order = [:minimum, :maximum, :sound_file, :var_name, :timeout, :termchar]
72
+ data = order.inject('') {|data, key| data += " #{options[key]}"; data }.strip
73
+
74
+ execute AppCommand.new('read', data, options).after {
75
+ block.call(response.body[:variable_read_result])
76
+ }
77
+ end
78
+
79
+ def prompt(options={}, &block)
80
+ execute Prompt.new(self, options, &block).command
81
+ end
82
+
83
+ def break!
84
+ execute AppCommand.new('break'), true
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,143 @@
1
+ module Larynx
2
+ class NoPromptDefined < StandardError; end
3
+
4
+ module Fields
5
+
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ attr_accessor :fields
13
+
14
+ def field(name, options={}, &block)
15
+ @fields ||= []
16
+ @fields << Field.new(name, options, &block)
17
+ attr_accessor name
18
+ end
19
+
20
+ end
21
+
22
+ module InstanceMethods
23
+
24
+ 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]
28
+ field.run(self)
29
+ @current_field += 1
30
+ field
31
+ end
32
+ end
33
+
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
+ class Field
42
+ include Callbacks
43
+
44
+ attr_reader :name
45
+ define_callback :setup, :validate, :invalid, :success, :failure
46
+
47
+ def initialize(name, options, &block)
48
+ @name, @callbacks = name, {}
49
+ @options = options.reverse_merge(:attempts => 3)
50
+ @prompt_queue = []
51
+
52
+ instance_eval(&block)
53
+ raise(Larynx::NoPromptDefined, 'A field requires a prompt to be defined') if @prompt_queue.empty?
54
+ end
55
+
56
+ def prompt(options)
57
+ add_prompt(options)
58
+ end
59
+
60
+ def reprompt(options)
61
+ raise 'A reprompt can only be used after a prompt' if @prompt_queue.empty?
62
+ add_prompt(options)
63
+ end
64
+
65
+ def add_prompt(options)
66
+ repeats = options.delete(:repeats) || 1
67
+ options.merge!(@options.slice(:length, :min_length, :max_length))
68
+ @prompt_queue += ([options] * repeats)
69
+ end
70
+
71
+ def current_prompt
72
+ options = (@prompt_queue[@attempt-1] || @prompt_queue.last).dup
73
+ method = ([:play, :speak, :phrase] & options.keys).first
74
+ message = options[method].is_a?(Symbol) ? @app.send(options[method]) : options[method]
75
+ options[method] = message
76
+
77
+ Prompt.new(call, options) {|input|
78
+ set_instance_variables(input)
79
+ evaluate_input
80
+ }
81
+ end
82
+
83
+ def execute_prompt
84
+ call.execute current_prompt.command
85
+ end
86
+
87
+ def increment_attempts
88
+ @attempt += 1
89
+ end
90
+
91
+ def fire_callback(callback)
92
+ if block = @callbacks[callback]
93
+ @app.instance_eval(&block)
94
+ else
95
+ true
96
+ end
97
+ end
98
+
99
+ def valid?
100
+ @value.size >= minimum_length && fire_callback(:validate)
101
+ end
102
+
103
+ def evaluate_input
104
+ if valid?
105
+ fire_callback(:success)
106
+ else
107
+ fire_callback(:invalid)
108
+ if @attempt < @options[:attempts]
109
+ increment_attempts
110
+ execute_prompt
111
+ else
112
+ fire_callback(:failure)
113
+ end
114
+ end
115
+ end
116
+
117
+ def set_instance_variables(input)
118
+ @value = input
119
+ @app.send("#{@name}=", input)
120
+ end
121
+
122
+ def maximum_length
123
+ @options[:max_length] || @options[:length]
124
+ end
125
+
126
+ def minimum_length
127
+ @options[:min_length] || @options[:length] || 1
128
+ end
129
+
130
+ def run(app)
131
+ @app = app
132
+ @attempt = 1
133
+ fire_callback(:setup)
134
+ execute_prompt
135
+ end
136
+
137
+ def call
138
+ @app.call
139
+ end
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,19 @@
1
+ module Larynx
2
+ class Form < Application
3
+ include Fields
4
+
5
+ def self.setup(&block)
6
+ @@setup = block
7
+ end
8
+
9
+ def run
10
+ instance_eval &@@setup if @@setup
11
+ next_field
12
+ end
13
+
14
+ def restart_form
15
+ @current_field = 0
16
+ run
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ module Larynx
2
+ class Logger < ::Logger
3
+ def format_message(severity, timestamp, progname, msg)
4
+ time = timestamp.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d" % timestamp.usec
5
+ "[%s] %5s: %s\n" % [time, severity, msg]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,35 @@
1
+ module Larynx
2
+ module Observable
3
+
4
+ def add_observer(object)
5
+ @observers ||= []
6
+ @observers << object
7
+ end
8
+
9
+ def remove_observer(object)
10
+ @observers && @observers.delete(object)
11
+ end
12
+
13
+ def clear_observers!
14
+ @observers = []
15
+ end
16
+
17
+ # Like an observer stack which only notifies top observer
18
+ def notify_current_observer(event, data=nil)
19
+ return unless @observers
20
+ obs = @observers.last
21
+ if obs.respond_to?(event)
22
+ data ? obs.send(event, data) : obs.send(event)
23
+ end
24
+ end
25
+
26
+ def notify_observers(event, data=nil)
27
+ return unless @observers
28
+ @observers.each do |obs|
29
+ next unless obs.respond_to?(event)
30
+ data ? obs.send(event, data) : obs.send(event)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,88 @@
1
+ module Larynx
2
+ class NoPromptCommandValue < StandardError; end
3
+
4
+ # The prompt class neatly wraps up a convention where you prompt for input of
5
+ # certain length. The prompt waits until the required input length is reached,
6
+ # the user presses the terminator button or the time runs out. Think of the
7
+ # play_and_get_digits command except it works for speak as well. It also
8
+ # provides a bargein option to allow or prevent the user from interrupting
9
+ # the speech or playback.
10
+ #
11
+ # Pass a block to the method as a callback which receives input as an argument.
12
+ #
13
+ class Prompt
14
+ attr_reader :call
15
+
16
+ def initialize(call, options, &block)
17
+ @call, @options, @block = call, options, block
18
+ @options.reverse_merge!(:bargein => true, :timeout => 10, :interdigit_timeout => 3, :termchar => '#')
19
+ raise NoPromptCommandValue, "No output command value supplied. Use one of playback, speak or phrase keys." if command_name.blank?
20
+ end
21
+
22
+ def command
23
+ @command ||= AppCommand.new(command_name, message, :bargein => @options[:bargein]).
24
+ before { call.clear_input }.
25
+ after {
26
+ if prompt_finished?
27
+ finalise
28
+ else
29
+ call.add_observer self
30
+ add_digit_timer
31
+ add_input_timer
32
+ end
33
+ }
34
+ end
35
+
36
+ def input
37
+ (call.input.last == termchar ? call.input[0..-2] : call.input).join
38
+ end
39
+
40
+ def prompt_finished?
41
+ call.input.last == termchar || call.input.size == maximum_length
42
+ end
43
+
44
+ def termchar
45
+ @options[:termchar]
46
+ end
47
+
48
+ def maximum_length
49
+ @options[:max_length] || @options[:length]
50
+ end
51
+
52
+ def command_name
53
+ ([:play, :speak, :phrase] & @options.keys).first.to_s
54
+ end
55
+
56
+ def message
57
+ @options[command_name.to_sym]
58
+ end
59
+
60
+ def finalise
61
+ call.remove_observer self
62
+ @block.call(input)
63
+ end
64
+
65
+ def dtmf_received(digit)
66
+ if prompt_finished?
67
+ call.stop_timer(:input)
68
+ call.cancel_timer(:digit)
69
+ else
70
+ call.restart_timer(:digit)
71
+ end
72
+ end
73
+
74
+ def add_digit_timer
75
+ call.timer(:digit, @options[:interdigit_timeout]) {
76
+ call.cancel_timer :input
77
+ finalise
78
+ }
79
+ end
80
+
81
+ def add_input_timer
82
+ call.timer(:input, @options[:timeout]) {
83
+ call.cancel_timer :digit
84
+ finalise
85
+ }
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,57 @@
1
+ module Larynx
2
+ class Response
3
+ attr_reader :header, :body
4
+
5
+ def initialize(header, body)
6
+ @header = CallHandler.headers_2_hash(header)
7
+ if body
8
+ @body = body.match(/:/) ? CallHandler.headers_2_hash(body) : body
9
+ @body.each {|k,v| v.chomp! if v.is_a?(String)}
10
+ end
11
+ end
12
+
13
+ def reply?
14
+ @header[:content_type] == 'command/reply'
15
+ end
16
+
17
+ def event?
18
+ @header[:content_type] == 'text/event-plain'
19
+ end
20
+
21
+ def ok?
22
+ @header[:reply_text] =~ /\+OK/
23
+ end
24
+
25
+ def error?
26
+ @header[:reply_text] =~ /ERR/
27
+ end
28
+
29
+ def executing?
30
+ event_name == 'CHANNEL_EXECUTE'
31
+ end
32
+
33
+ def executed?
34
+ event_name == 'CHANNEL_EXECUTE_COMPLETE'
35
+ end
36
+
37
+ def command_name
38
+ @body[:application] if @body
39
+ end
40
+
41
+ def event_name
42
+ @body[:event_name] if @body
43
+ end
44
+
45
+ def dtmf?
46
+ @body[:event_name] == 'DTMF' if @body
47
+ end
48
+
49
+ def speech?
50
+ @body[:event_name] == 'DETECTED_SPEECH' if @body
51
+ end
52
+
53
+ def disconnect?
54
+ @header[:content_type] == 'text/disconnect-notice'
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ module Larynx
2
+ # Adds restart to EM timer class. Implementation influenced by EM::PeriodicTimer class
3
+ # so hopefully it should not cause any issues.
4
+ class RestartableTimer < EM::Timer
5
+ def initialize(interval, callback=nil, &block)
6
+ @interval = interval
7
+ @code = callback || block
8
+ schedule
9
+ end
10
+
11
+ # Restart the timer
12
+ def restart
13
+ cancel
14
+ schedule
15
+ end
16
+
17
+ def schedule
18
+ @signature = EM::add_timer(@interval, method(:fire))
19
+ end
20
+
21
+ def fire
22
+ @code.call
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Larynx
2
+ class Session
3
+ attr_reader :variables
4
+
5
+ def initialize(data)
6
+ @variables = data
7
+ end
8
+
9
+ def method_missing(method, *args, &block)
10
+ if @variables.has_key?(method.to_sym)
11
+ @variables[method.to_sym]
12
+ end
13
+ end
14
+
15
+ def [](key)
16
+ @variables[key]
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module Larynx
2
+ VERSION = '0.1.0'
3
+ end
data/lib/larynx.rb ADDED
@@ -0,0 +1,109 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'active_support'
4
+ require 'logger'
5
+ require 'daemons/daemonize'
6
+
7
+ require 'larynx/version'
8
+ require 'larynx/logger'
9
+ require 'larynx/observable'
10
+ require 'larynx/callbacks'
11
+ require 'larynx/session'
12
+ require 'larynx/response'
13
+ require 'larynx/command'
14
+ require 'larynx/commands'
15
+ require 'larynx/prompt'
16
+ require 'larynx/application'
17
+ require 'larynx/fields'
18
+ require 'larynx/form'
19
+ require 'larynx/restartable_timer'
20
+ require 'larynx/call_handler'
21
+
22
+ module Larynx
23
+ class << self
24
+ include Callbacks
25
+
26
+ define_callback :connect, :answer, :hungup
27
+
28
+ def parse_options(args=ARGV)
29
+ @options = {
30
+ :ip => "0.0.0.0",
31
+ :port => 8084,
32
+ :pid_file => './larynx.pid',
33
+ :log_file => './larynx.log'
34
+ }
35
+ opts = OptionParser.new
36
+ opts.banner = "Usage: larynx [options]"
37
+ opts.separator ''
38
+ opts.separator "Larynx is a tool to develop FreeSWITCH IVR applications in Ruby."
39
+ opts.on('-i', '--ip IP', 'Listen for connections on this IP') {|ip| @options[:ip] = ip }
40
+ opts.on('-p', '--port PORT', 'Listen on this port', Integer) {|port| @options[:port] = port }
41
+ opts.on('-d', '--daemonize', 'Run as daemon') { @options[:daemonize] = true }
42
+ opts.on('-l', '--log-file FILE', 'Defaults to /app/root/larynx.log') {|log| @options[:log_file] = log }
43
+ opts.on( '--pid-file FILE', 'Defaults to /app/root/larynx.pid') {|pid| @options[:pid_file] = pid }
44
+ opts.on('-h', '--help', 'This is it') { $stderr.puts opts; exit 0 }
45
+ opts.on('-v', '--version') { $stderr.puts "Larynx version #{Larynx::VERSION}"; exit 0 }
46
+ opts.parse!(args)
47
+ end
48
+
49
+ def setup_logger
50
+ logger = Larynx::Logger.new(@options[:log_file])
51
+ logger.level = Logger::INFO
52
+ Object.const_set "LARYNX_LOGGER", logger
53
+ end
54
+
55
+ def graceful_exit
56
+ LARYNX_LOGGER.info "Shutting down Larynx"
57
+ EM.stop_server @em_signature
58
+ @em_signature = nil
59
+ remove_pid_file if @options[:daemonize]
60
+ exit 130
61
+ end
62
+
63
+ def daemonize
64
+ Daemonize.daemonize
65
+ Dir.chdir LARYNX_ROOT
66
+ File.open(@options[:pid_file], 'w+') {|f| f.write("#{Process.pid}\n") }
67
+ end
68
+
69
+ def remove_pid_file
70
+ File.delete @options[:pid_file]
71
+ end
72
+
73
+ def trap_signals
74
+ trap('TERM') { graceful_exit }
75
+ trap('INT') { graceful_exit }
76
+ end
77
+
78
+ def setup_app
79
+ if ARGV[0].nil?
80
+ $stderr.puts "You must specify an application file"
81
+ exit -1
82
+ end
83
+ Object.const_set "LARYNX_ROOT", File.expand_path(File.dirname(ARGV[0]))
84
+ require File.expand_path(ARGV[0])
85
+ end
86
+
87
+ def start_server
88
+ LARYNX_LOGGER.info "Larynx starting up on #{@options[:ip]}:#{@options[:port]}"
89
+ EM::run {
90
+ @em_signature = EM::start_server @options[:ip], @options[:port], Larynx::CallHandler
91
+ }
92
+ end
93
+
94
+ def run
95
+ parse_options(ARGV)
96
+ setup_app
97
+ daemonize if @options[:daemonize]
98
+ setup_logger
99
+ trap_signals
100
+ start_server
101
+ end
102
+
103
+ def running?
104
+ !@em_signature.nil?
105
+ end
106
+ end
107
+ end
108
+
109
+ Larynx.run unless defined?(TEST)