freec 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Manifest ADDED
@@ -0,0 +1,17 @@
1
+ lib/call_variables.rb
2
+ lib/freec.rb
3
+ lib/listener.rb
4
+ lib/freec_base.rb
5
+ lib/freec_logger.rb
6
+ lib/freeswitch_applications.rb
7
+ lib/tools.rb
8
+ Manifest
9
+ Rakefile
10
+ README.rdoc
11
+ spec/event_parser_spec.rb
12
+ spec/freec_base_spec.rb
13
+ spec/freeswitch_applications_spec.rb
14
+ spec/spec_helper.rb
15
+ spec/data/event.rb
16
+ spec/data/event_with_body.rb
17
+
data/README.rdoc ADDED
@@ -0,0 +1,73 @@
1
+ = About
2
+ Freec is a framework you can build voice applications on top of. It makes use of the Event socket outbound API of Freeswitch.
3
+
4
+ = Installation
5
+ sudo gem install jankubr-freec
6
+
7
+ = Usage
8
+ 1. Install Freeswitch and point a chosen extension to the IP where your app will run:
9
+
10
+ <extension name="telfa">
11
+ <condition field="destination_number" expression="^.*$">
12
+ <action application="socket" data="127.0.0.1:8084 async full" />
13
+ </condition>
14
+ </extension>
15
+
16
+ 2. Create a file with the main class of your application (e.g. Jukebox) and make it a subclass of Freec. Name the file according to the class name (e.g. jukebox.rb). Implement
17
+ a step method instructions for which will follow. Simple example:
18
+
19
+ #jukebox.rb
20
+ require 'rubygems'
21
+ require 'freec'
22
+ class Jukebox < Freec
23
+ def step
24
+ @step ||= 1
25
+ case @step
26
+ when 1
27
+ answer
28
+ when 2
29
+ playback('music/8000/danza-espanola-op-37-h-142-xii-arabesca.wav')
30
+ when 3
31
+ return nil
32
+ end
33
+ @step += 1
34
+ end
35
+ end
36
+
37
+ 3. Run it with:
38
+ ruby jukebox.rb
39
+
40
+ = How does it work
41
+ The step method is called after each even is finished (e.g. file is played, recording is finished). Here is what you can do:
42
+ * read call variables from the hash in call_vars
43
+ * call one of the following methods (Freeswitch apps):
44
+ * answer
45
+ * playback(path_to_file)
46
+ * bridge(number_or_numbers)
47
+ * record(path_to_file)
48
+ * read(path_to_file)
49
+ * set_variable(name, value)
50
+ * any other via execute_app(app_name, params)
51
+ If you return nil or false from the step method, the call will be hungup.
52
+
53
+ = Cool stuff
54
+ * Your application's main file as well as everything you might have in the lib directory will be reloaded on each step in development mode.
55
+ * Passing -d will daemonize the application and automatically implies production mode.
56
+ * The log directory contains the log file and the pid file.
57
+ * There is a hidden feature.
58
+
59
+ = Stinks?
60
+ What Freec can do at the moment was enough for me to implement Telfa (http://telfapbx.com). If it doesn't fit your needs, you can:
61
+ 1. Complain to hi@jankubr.com .
62
+ 2. Fork it and change yourself.
63
+ 3. Check Liverpie (http://www.liverpie.com/) or Telegraph (http://code.google.com/p/telegraph/), Freec is inspired by them.
64
+ 4. Write yours :-)
65
+
66
+ = License
67
+ Copyright © 2009 Jan Kubr
68
+
69
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
70
+
71
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('freec', '0.2.0') do |p|
6
+ p.description = "Layer between your Ruby voice app and Freeswitch."
7
+ p.url = "http://github.com/jankubr/freec"
8
+ p.author = "Jan Kubr"
9
+ p.email = "hi@jankubr.com"
10
+ p.ignore_pattern = []
11
+ p.runtime_dependencies = ['daemons']
12
+ p.development_dependencies = ['rspec']
13
+ end
14
+
15
+ desc 'Clean up files.'
16
+ task :clean do |t|
17
+ FileUtils.rm_rf "tmp"
18
+ FileUtils.rm_rf "pkg"
19
+ end
20
+
21
+ spec = Gem::Specification.new do |s|
22
+ s.name = "freec"
23
+ s.version = '0.2.0'
24
+ s.author = "Jan Kubr"
25
+ s.email = "hi@jankubr.com"
26
+ s.homepage = "http://github.com/jankubr/freec"
27
+ s.platform = Gem::Platform::RUBY
28
+ s.summary = "Layer between your Ruby voice app and Freeswitch."
29
+ s.files = FileList["README*",
30
+ "Rakefile",
31
+ "{lib,spec}/**/*"].to_a
32
+ s.require_path = "lib"
33
+ s.rubyforge_project = "freec"
34
+ s.has_rdoc = false
35
+ s.extra_rdoc_files = FileList["README*"].to_a
36
+ s.rdoc_options << '--line-numbers' << '--inline-source'
37
+ s.requirements << "daemons"
38
+ s.add_development_dependency 'rspec'
39
+ end
40
+
41
+ desc "Generate a gemspec file for GitHub"
42
+ task :gemspec do
43
+ File.open("#{spec.name}.gemspec", 'w') do |f|
44
+ f.write spec.to_ruby
45
+ end
46
+ end
47
+
48
+ require 'spec/rake/spectask'
49
+
50
+ desc "Run all specs"
51
+ Spec::Rake::SpecTask.new('spec') do |t|
52
+ t.spec_files = FileList['spec/**/*.rb']
53
+ end
data/freec.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{freec}
5
+ s.version = "0.2.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jan Kubr"]
9
+ s.date = %q{2009-10-26}
10
+ s.description = %q{Layer between your Ruby voice app and Freeswitch.}
11
+ s.email = %q{hi@jankubr.com}
12
+ s.extra_rdoc_files = ["lib/call_variables.rb", "lib/freec.rb", "lib/listener.rb", "lib/freec_base.rb", "lib/freec_logger.rb", "lib/freeswitch_applications.rb", "lib/tools.rb", "README.rdoc"]
13
+ s.files = ["lib/call_variables.rb", "lib/freec.rb", "lib/listener.rb", "lib/freec_base.rb", "lib/freec_logger.rb", "lib/freeswitch_applications.rb", "lib/tools.rb", "Manifest", "Rakefile", "README.rdoc", "spec/event_parser_spec.rb", "spec/freec_base_spec.rb", "spec/freeswitch_applications_spec.rb", "spec/spec_helper.rb", "spec/data/event.rb", "spec/data/event_with_body.rb", "freec.gemspec"]
14
+ s.homepage = %q{http://github.com/jankubr/freec}
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Freec", "--main", "README.rdoc"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{freec}
18
+ s.rubygems_version = %q{1.3.5}
19
+ s.summary = %q{Layer between your Ruby voice app and Freeswitch.}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<daemons>, [">= 0"])
27
+ s.add_development_dependency(%q<rspec>, [">= 0"])
28
+ else
29
+ s.add_dependency(%q<daemons>, [">= 0"])
30
+ s.add_dependency(%q<rspec>, [">= 0"])
31
+ end
32
+ else
33
+ s.add_dependency(%q<daemons>, [">= 0"])
34
+ s.add_dependency(%q<rspec>, [">= 0"])
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ module CallVariables
2
+
3
+ # Value of the variable_sip_from_user variable
4
+ # This is the callee username/number
5
+ def sip_from_user
6
+ call_vars[:variable_sip_from_user]
7
+ end
8
+
9
+ # Value of the variable_sip_to_user variable
10
+ # This is the called username/number
11
+ def sip_to_user
12
+ call_vars[:variable_sip_to_user]
13
+ end
14
+
15
+ # Value of the channel_destination_number variable
16
+ # This is the extension this call was routed to in Freeswitch
17
+ def channel_destination_number
18
+ call_vars[:channel_destination_number]
19
+ end
20
+
21
+ end
data/lib/freec.rb ADDED
@@ -0,0 +1,68 @@
1
+ lib_dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
3
+ require "freec_base"
4
+ require 'listener'
5
+
6
+ require 'fileutils'
7
+ require 'daemons/daemonize'
8
+ include Daemonize
9
+
10
+ def freec_app_file_name
11
+ $0.sub(/\.[^\.]*$/, '')
12
+ end
13
+
14
+ def freec_app_class_name
15
+ freec_app_file_name.split('_').map{|w| w.capitalize}.join
16
+ end
17
+
18
+ def freec_app_log_dir
19
+ "#{ROOT}/log"
20
+ end
21
+
22
+ def create_freec_app_log_dir
23
+ FileUtils.mkdir_p(freec_app_log_dir)
24
+ end
25
+
26
+ def freec_app_log_file
27
+ @@log_file ||= "#{freec_app_log_dir}/#{freec_app_file_name}.log"
28
+ end
29
+
30
+ def freec_app_pid_file
31
+ "#{freec_app_log_dir}/#{freec_app_file_name}.pid"
32
+ end
33
+
34
+ def load_freec_app_config
35
+ if File.exist?(freec_app_configuration_file)
36
+ @@config = YAML.load_file(freec_app_configuration_file)
37
+ else
38
+ @@config = {}
39
+ end
40
+ @@config['listen_port'] ||= '8084'
41
+ end
42
+
43
+ def freec_app_configuration_file
44
+ "#{ROOT}/config/config.yml"
45
+ end
46
+
47
+ unless defined?(TEST)
48
+ at_exit do
49
+ ROOT = File.expand_path(File.dirname($0))
50
+ ENVIRONMENT = ARGV[0] == '-d' ? 'production' : 'development'
51
+ create_freec_app_log_dir
52
+ load_freec_app_config
53
+ if ARGV[0] == '-d'
54
+ puts 'Daemonizing...'
55
+ daemonize(freec_app_log_file)
56
+ end
57
+ open(freec_app_pid_file, "w") {|f| f.write(Process.pid) }
58
+
59
+ server = Listener.new('0.0.0.0', @@config['listen_port'].to_i, freec_app_class_name)
60
+ server.audit = true
61
+ server.start
62
+ puts "Listening on port #{@@config['listen_port']}"
63
+ loop do
64
+ break if server.stopped?
65
+ sleep(1)
66
+ end
67
+ end
68
+ end
data/lib/freec_base.rb ADDED
@@ -0,0 +1,188 @@
1
+ require 'gserver'
2
+ require 'rubygems'
3
+ require 'uri'
4
+
5
+ require 'tools'
6
+ require "freeswitch_applications"
7
+ require "call_variables"
8
+
9
+ class Freec
10
+ include FreeswitchApplications
11
+ include CallVariables
12
+
13
+ attr_reader :call_vars, :event_body, :log
14
+
15
+ def initialize(io, log) #:nodoc:
16
+ @call_vars ||= {}
17
+ @last_app_executed = 'initial_step'
18
+ @io = io
19
+ @log = log
20
+ end
21
+
22
+ def handle_call #:nodoc:
23
+ call_initialization
24
+ loop do
25
+ subscribe_to_new_channel_events
26
+ if last_event_dtmf? && respond_to?(:on_dtmf)
27
+ callback(:on_dtmf, call_vars[:dtmf_digit])
28
+ elsif waiting_for_this_response? || execute_completed?
29
+ reset_wait_for if waiting_for_this_response?
30
+ reload_application_code
31
+ break if disconnect_notice? || !callback(:step)
32
+ end
33
+ read_response
34
+ parse_response
35
+ end
36
+ callback(:on_hangup)
37
+ hangup unless @io.closed?
38
+ send_and_read('exit') unless @io.closed?
39
+ end
40
+
41
+ def wait_for(key, value)
42
+ @waiting_for_key = key && key.to_sym
43
+ @waiting_for_value = value
44
+ end
45
+
46
+ def reset_wait_for
47
+ wait_for(nil, nil)
48
+ true
49
+ end
50
+
51
+ def execute_completed?
52
+ channel_execute_complete? || channel_destroyed_after_bridge? || disconnect_notice?
53
+ end
54
+
55
+ private
56
+
57
+ def call_initialization
58
+ connect_to_freeswitch
59
+ subscribe_to_events
60
+ end
61
+
62
+ def channel_execute_complete?
63
+ return true if @last_app_executed == 'initial_step'
64
+ complete = call_vars[:content_type] == 'text/event-plain' &&
65
+ call_vars[:event_name] == 'CHANNEL_EXECUTE_COMPLETE' &&
66
+ @last_app_executed == call_vars[:application]
67
+ @last_app_executed = nil if complete
68
+ complete
69
+ end
70
+
71
+ def channel_destroyed_after_bridge?
72
+ call_vars[:application] == 'bridge' && call_vars[:event_name] == 'CHANNEL_DESTROY'
73
+ end
74
+
75
+ def disconnect_notice?
76
+ call_vars[:content_type] == 'text/disconnect-notice'
77
+ end
78
+
79
+ def callback(callback_name, *args)
80
+ send(callback_name, *args) if respond_to?(callback_name)
81
+ rescue StandardError => e
82
+ log.error e.message
83
+ e.backtrace.each {|trace_line| log.error(trace_line)}
84
+ end
85
+
86
+ def reload_application_code
87
+ return unless ENVIRONMENT == 'development'
88
+ load($0)
89
+ lib_dir = "#{ROOT}/lib"
90
+ return unless File.exist?(lib_dir)
91
+ Dir.open(lib_dir).each do |file|
92
+ full_file_name = File.join(lib_dir, file)
93
+ next unless File.file?(full_file_name)
94
+ load(full_file_name)
95
+ end
96
+ end
97
+
98
+ def connect_to_freeswitch
99
+ send_and_read('connect')
100
+ parse_response
101
+ end
102
+
103
+ def subscribe_to_events
104
+ send_and_read('events plain all')
105
+ parse_response
106
+ send_and_read("filter Unique-ID #{@unique_id}")
107
+ parse_response
108
+ end
109
+
110
+ def subscribe_to_new_channel_events
111
+ return unless call_vars[:event_name] == 'CHANNEL_BRIDGE'
112
+ send_and_read("filter Unique-ID #{call_vars[:other_leg_unique_id]}")
113
+ end
114
+
115
+ def waiting_for_this_response?
116
+ @waiting_for_key && @waiting_for_value && call_vars[@waiting_for_key] == @waiting_for_value
117
+ end
118
+
119
+ def last_event_dtmf?
120
+ call_vars[:content_type] == 'text/event-plain' && call_vars[:event_name] == 'DTMF'
121
+ end
122
+
123
+ def send_data(data)
124
+ log.debug "Sending: #{data}"
125
+ @io.write("#{data}\n\n") unless disconnect_notice?
126
+ end
127
+
128
+ def send_and_read(data)
129
+ send_data(data)
130
+ read_response
131
+ end
132
+
133
+ def read_response
134
+ return if disconnect_notice?
135
+ read_response_info
136
+ read_event_header
137
+ read_event_body
138
+ end
139
+
140
+ def read_response_info
141
+ @response = ''
142
+ begin
143
+ line = @io.gets.to_s
144
+ @response += line
145
+ end until @response[-2..-1] == "\n\n"
146
+ end
147
+
148
+ def read_event_header
149
+ header_length = @response.sub(/^Content-Length: ([0-9]+)$.*/m, '\1').to_i
150
+ return if header_length == 0
151
+ header = ''
152
+ begin
153
+ line = @io.gets.to_s
154
+ header += line.to_s
155
+ end until header[-2..-1] == "\n\n"
156
+ @response += header
157
+ end
158
+
159
+ def read_event_body
160
+ body_length = @response.sub(/^Content-Length.*^Content-Length: ([0-9]+)$.*/m, '\1').to_i
161
+ return if body_length == 0
162
+ body = ''
163
+ begin
164
+ line = @io.read(body_length).to_s
165
+ body += line.to_s
166
+ end until body.length == body_length
167
+ @response += body
168
+ end
169
+
170
+ def parse_response
171
+ hash = {}
172
+ if @response =~ /^Content-Length.*^Content-Length/m
173
+ @event_body = @response.sub(/.*\n\n.*\n\n(.*)/m, '\1').strip
174
+ else
175
+ @event_body = nil
176
+ end
177
+ @response.split("\n").each do |line|
178
+ k,v = line.split(/\s*:\s*/)
179
+ hash[k.strip.gsub('-', '_').downcase.to_sym] = URI.unescape(v).strip if k && v
180
+ end
181
+ call_vars.merge!(hash)
182
+ @unique_id ||= call_vars[:unique_id]
183
+ raise call_vars[:reply_text] if call_vars[:reply_text] =~ /^-ERR/
184
+ log.debug "\n\tUnique ID: #{call_vars[:unique_id]}\n\tContent-type: #{call_vars[:content_type]}\n\tEvent name: #{call_vars[:event_name]}"
185
+ @response = ''
186
+ end
187
+
188
+ end
@@ -0,0 +1,12 @@
1
+ require 'logger'
2
+
3
+ class FreecLogger < Logger
4
+ def format_message(severity, timestamp, progname, msg) #:nodoc:
5
+ "#{timestamp.strftime("%Y-%m-%d %H:%M:%S")}.#{(timestamp.usec / 1000.0).round} #{severity} #{msg}\n"
6
+ end
7
+
8
+ # Prints the message with twenty #'s before and after it.
9
+ def highlighted_info(message)
10
+ info("#{'#'*20} #{message} #{'#'*20}")
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ module FreeswitchApplications
2
+
3
+ # Answers the call.
4
+ def answer
5
+ execute_app('answer')
6
+ end
7
+
8
+ # Plays the file in file_name
9
+ # file_name is either an absolute path or path relative
10
+ # to the sound_prefix variable set in Freeswitch's vars.xml configuration file.
11
+ def playback(file_name)
12
+ execute_app('playback', file_name)
13
+ end
14
+
15
+ # Spells the string
16
+ def spell(string)
17
+ execute_app('phrase', "spell,#{string}")
18
+ end
19
+
20
+ # Says the given string
21
+ # Don't forget to set up your TTS engine and set variables tts_engine and tts_voice accordingly
22
+ # See e.g.: http://wiki.freeswitch.org/wiki/Mod_flite
23
+ def speak(string)
24
+ execute_app('speak', string)
25
+ end
26
+
27
+ # Bridges the call to the given number or numbers (this param can be a number or an array of numbers).
28
+ def bridge(number_or_numbers, options = {})
29
+ number_or_numbers = number_or_numbers.join(",") if number_or_numbers.is_a?(Array)
30
+ execute_app("bridge", "#{number_or_numbers}")
31
+ end
32
+
33
+ # Transfers the call to the given extension
34
+ def transfer(extension)
35
+ execute_app("transfer", "#{extension}")
36
+ end
37
+
38
+ # Records the call to a file with the given file_name
39
+ # file_name is either an absolute path or path relative
40
+ # to the sound_prefix variable set in Freeswitch's vars.xml configuration file.
41
+ #
42
+ # Options:
43
+ # * <tt>:time_limit_secs</tt> overrides the default timeout, which is 600 seconds
44
+ def record(file_name, options = {})
45
+ options = {:time_limit_secs => 600}.merge(options) #no reverse_merge, no fun :-)
46
+ execute_app("record", "#{file_name} #{options[:time_limit_secs]}")
47
+ end
48
+
49
+ # Plays the file in file_name and reads input (key presses) from the user.
50
+ #
51
+ # Options:
52
+ # * <tt>:terminators</tt> option to set a different terminator or terminators (defaults to '#')
53
+ # * <tt>:variable</tt> to set the variable where the results is put (defaults to call_vars[:input])
54
+ # * <tt>:timeout</tt> to override the default timeout value (which is 10 seconds)
55
+ # * <tt>:min and :max</tt> options to override the default maximum and minimum of characters that will be read (both default to 1)
56
+ def read(file_name, options = {})
57
+ options[:terminators] = [options[:terminators]] if options[:terminators].is_a?(String)
58
+ options = {:timeout => 10, :variable => 'input', :min => 1, :max => 1, :terminators => ['#']}.merge(options)
59
+ execute_app("read", "#{options[:min]} #{options[:max]} #{file_name} #{options[:variable]} #{options[:timeout] * 1000} #{options[:terminators].join(',')}")
60
+ end
61
+
62
+ # Starts recording the call in file in file_name
63
+ #
64
+ def start_recording(file_name)
65
+ execute_app('record_session', file_name)
66
+ end
67
+
68
+ # Stops recording the call in file in file_name
69
+ #
70
+ def stop_recording(file_name)
71
+ execute_app('stop_record_session', file_name)
72
+ end
73
+
74
+ # Sets a variable with the give name to the given value.
75
+ def set_variable(name, value)
76
+ execute_app('set', "#{name}=#{value}")
77
+ end
78
+
79
+ # Hangs up the call.
80
+ def hangup
81
+ execute_app('hangup')
82
+ end
83
+
84
+ # Executes an app using the sendmsg command of Freeswitch.
85
+ # Use this if there is no method for the application you want to run.
86
+ #
87
+ # Params:
88
+ # * <tt>app</tt> is the application name
89
+ # * <tt>pars</tt> is a string of arguments of the app
90
+ # * <tt>lock</tt> can be set to false so Freeswitch won't wait for this app to finish before running the next one
91
+ def execute_app(app, pars = '', lock = true, unique_id = nil)
92
+ @last_app_executed = app
93
+ unique_id = @unique_id unless unique_id
94
+ cmd = "sendmsg #{unique_id}"
95
+ cmd << "\ncall-command: execute"
96
+ cmd << "\nexecute-app-name: #{app}"
97
+ cmd << "\nexecute-app-arg: #{pars}" unless pars.blank?
98
+ cmd << "\nevent-lock:#{lock}"
99
+ send_data cmd
100
+ end
101
+ end
data/lib/listener.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'gserver'
2
+ require 'freec_logger'
3
+
4
+ class Listener < GServer
5
+ def initialize(host, port, application_class_name) #:nodoc:
6
+ @application_class_name = application_class_name
7
+ @logger = FreecLogger.new(dev_or_test? ? STDOUT : @@log_file)
8
+ @logger.level = Logger::INFO unless dev_or_test?
9
+ super(port, host, (1.0/0.0))
10
+ self.audit = true
11
+ connect_to_database
12
+ end
13
+
14
+ def serve(io) #:nodoc:
15
+ app = Kernel.const_get(@application_class_name).new(io, @logger)
16
+ app.handle_call
17
+ rescue StandardError => e
18
+ @logger.error e.message
19
+ e.backtrace.each {|trace_line| @logger.error(trace_line)}
20
+ end
21
+
22
+ private
23
+
24
+ def connect_to_database
25
+ return unless @@config['database'] && @@config['database'][ENVIRONMENT]
26
+ require 'active_record'
27
+ ActiveRecord::Base.establish_connection(@@config['database'][ENVIRONMENT])
28
+ end
29
+
30
+ def dev_or_test?
31
+ ['development', 'test'].include?(ENVIRONMENT)
32
+ end
33
+
34
+ end
data/lib/tools.rb ADDED
@@ -0,0 +1,5 @@
1
+ class Object
2
+ def blank?
3
+ respond_to?(:empty?) ? empty? : !self
4
+ end
5
+ end
@@ -0,0 +1,56 @@
1
+ unless defined?(EVENT)
2
+ EVENT=<<STRING
3
+ Content-Length: 1693
4
+ Content-Type: text/event-plain
5
+
6
+ Event-Name: CHANNEL_EXECUTE
7
+ Core-UUID: 4a78c88d-38cc-4e35-92af-c155def76f9a
8
+ FreeSWITCH-Hostname: air.local
9
+ FreeSWITCH-IPv4: 10.0.1.2
10
+ FreeSWITCH-IPv6: %3A%3A1
11
+ Event-Date-Local: 2009-05-14%2019%3A29%3A09
12
+ Event-Date-GMT: Thu,%2014%20May%202009%2017%3A29%3A09%20GMT
13
+ Event-Date-Timestamp: 1242322149917313
14
+ Event-Calling-File: switch_core_session.c
15
+ Event-Calling-Function: switch_core_session_exec
16
+ Event-Calling-Line-Number: 1460
17
+ Channel-State: CS_EXECUTE
18
+ Channel-State-Number: 4
19
+ Channel-Name: sofia/internal/jan.kubr.gmail.com%4010.0.1.2
20
+ Channel-Destination-Number: 886
21
+ Unique-ID: f3c2d5ee-d064-4f55-9280-5be2a65867e8
22
+ Call-Direction: inbound
23
+ Presence-Call-Direction: inbound
24
+ Answer-State: answered
25
+ Channel-Read-Codec-Name: GSM
26
+ Channel-Read-Codec-Rate: 8000
27
+ Channel-Write-Codec-Name: GSM
28
+ Channel-Write-Codec-Rate: 8000
29
+ Caller-Username: jan.kubr.gmail.com
30
+ Caller-Dialplan: XML
31
+ Caller-Caller-ID-Name: Jan%20Local
32
+ Caller-Caller-ID-Number: jan.kubr.gmail.com
33
+ Caller-Network-Addr: 10.0.1.2
34
+ Caller-Destination-Number: 0
35
+ Caller-Unique-ID: f3c2d5ee-d064-4f55-9280-5be2a65867e8
36
+ Caller-Source: mod_sofia
37
+ Caller-Context: default
38
+ Caller-Channel-Name: sofia/internal/jan.kubr.gmail.com%4010.0.1.2
39
+ Caller-Profile-Index: 1
40
+ Caller-Profile-Created-Time: 1242322142643329
41
+ Caller-Channel-Created-Time: 1242322142643329
42
+ Caller-Channel-Answered-Time: 1242322143798188
43
+ Caller-Channel-Progress-Time: 0
44
+ Caller-Channel-Progress-Media-Time: 1242322142657064
45
+ Caller-Channel-Hangup-Time: 0
46
+ Caller-Channel-Transfer-Time: 0
47
+ Caller-Screen-Bit: true
48
+ Caller-Privacy-Hide-Name: false
49
+ Caller-Privacy-Hide-Number: false
50
+ variable_sip_to_user: 886
51
+ variable_sip_from_user: 1001
52
+ Application: set
53
+ Application-Data: continue_on_fail%3Dtrue
54
+
55
+ STRING
56
+ end
@@ -0,0 +1,25 @@
1
+ unless defined?(EVENT_WITH_BODY)
2
+ EVENT_WITH_BODY=<<STRING
3
+ Content-Length: 460
4
+ Content-Type: text/event-plain
5
+
6
+ Event-Name: DETECTED_SPEECH
7
+ Core-UUID: b61467b4-6d7c-4a6f-ba87-32ade7b79951
8
+ FreeSWITCH-Hostname: air.local
9
+ FreeSWITCH-IPv4: 10.0.1.2
10
+ FreeSWITCH-IPv6: %3A%3A1
11
+ Event-Date-Local: 2009-05-04%2020%3A40%3A48
12
+ Event-Date-GMT: Mon,%2004%20May%202009%2018%3A40%3A48%20GMT
13
+ Event-Date-Timestamp: 1241462448174337
14
+ Event-Calling-File: switch_ivr_async.c
15
+ Event-Calling-Function: speech_thread
16
+ Event-Calling-Line-Number: 1878
17
+ Speech-Type: detected-speech
18
+ Content-Length: 132
19
+
20
+ <interpretation grammar="battle" score="100">
21
+ <result name="match">FREESTYLE</result>
22
+ <input>FREESTYLE</input>
23
+ </interpretation>
24
+ STRING
25
+ end
@@ -0,0 +1,56 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ describe Freec do
4
+ before do
5
+ io = stub(:io)
6
+ log = FreecLogger.new(STDOUT)
7
+ log.level = Logger::FATAL
8
+ @freec = Freec.new(io, log)
9
+ end
10
+
11
+ describe "parses body-less event" do
12
+
13
+ before do
14
+ @freec.instance_variable_set(:@response, EVENT)
15
+ @freec.send :parse_response
16
+ end
17
+
18
+ it "should read unique id from response" do
19
+ @freec.instance_variable_get(:@unique_id).should == 'f3c2d5ee-d064-4f55-9280-5be2a65867e8'
20
+ end
21
+
22
+ it "should parse variables from response" do
23
+ @freec.call_vars[:call_direction].should == 'inbound'
24
+ @freec.call_vars[:caller_context].should == 'default'
25
+ @freec.call_vars[:application].should == 'set'
26
+ end
27
+
28
+ it "should make the value of the sip_from_user variable available as a method" do
29
+ @freec.sip_from_user.should == '1001'
30
+ end
31
+
32
+ it "should make the value of the sip_to_user variable available as a method" do
33
+ @freec.sip_to_user.should == '886'
34
+ end
35
+
36
+ it "should make the value of the channel_destination_number variable available as a method" do
37
+ @freec.channel_destination_number.should == '886'
38
+ end
39
+ end
40
+
41
+ describe "parses an event with a body" do
42
+ before do
43
+ @freec.instance_variable_set(:@response, EVENT_WITH_BODY)
44
+ @freec.send :parse_response
45
+ end
46
+
47
+ it "should parse the variables from the event header" do
48
+ @freec.call_vars[:event_name].should == 'DETECTED_SPEECH'
49
+ end
50
+
51
+ it "makes the body of the response available as a public method" do
52
+ @freec.event_body.should =~ /<\/interpretation>$/
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,176 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ module FreecSpecHelper
4
+ def event_parts(event=EVENT)
5
+ @parts = event.split("\n\n").map {|p| "#{p}\n\n"}
6
+ end
7
+
8
+ def initial_response
9
+ @initial_response ||= EVENT.split("\n\n")[1] + "\n\n"
10
+ end
11
+
12
+ end
13
+
14
+ describe Freec do
15
+ include FreecSpecHelper
16
+
17
+ before do
18
+ @io = stub(:io)
19
+ log = FreecLogger.new(STDOUT)
20
+ log.level = Logger::FATAL
21
+ @freec = Freec.new(@io, log)
22
+ end
23
+
24
+ describe "call initialization" do
25
+
26
+ it "sends 'connect' command to Freeswitch" do
27
+ @freec.should_receive(:send_data).with("connect")
28
+ @freec.should_receive(:send_data)
29
+ @freec.should_receive(:send_data)
30
+ @io.should_receive(:gets).exactly(3).and_return(initial_response)
31
+ @freec.send :call_initialization
32
+ end
33
+
34
+ it "subscribes to events" do
35
+ @freec.should_receive(:send_data)
36
+ @freec.should_receive(:send_data).with('events plain all')
37
+ @freec.should_receive(:send_data).with('filter Unique-ID f3c2d5ee-d064-4f55-9280-5be2a65867e8')
38
+ @io.should_receive(:gets).exactly(3).and_return(initial_response)
39
+ @freec.send :call_initialization
40
+ end
41
+
42
+ end
43
+
44
+ describe "response reader" do
45
+
46
+ it "reads the full body-less event" do
47
+ @io.should_receive(:gets).and_return(event_parts(EVENT)[0], event_parts(EVENT)[1])
48
+ @freec.send(:read_response)
49
+ end
50
+
51
+ it "reads the full event with body" do
52
+ @io.should_receive(:gets).and_return(event_parts(EVENT_WITH_BODY)[0],
53
+ event_parts(EVENT_WITH_BODY)[1])
54
+ @io.should_receive(:read).and_return(event_parts(EVENT_WITH_BODY)[2].strip.chomp)
55
+ @freec.send(:read_response)
56
+ end
57
+
58
+ end
59
+
60
+ describe "event recognition" do
61
+
62
+ it "should subscribe to events of other leg channel after bridge" do
63
+ bridge_event = EVENT.sub('CHANNEL_EXECUTE', 'CHANNEL_BRIDGE').sub('Caller-Profile-Index: 1', 'Other-Leg-Unique-ID: 6c75cb42-e72d-48bf-9ecf-d71bd4b60617')
64
+ @freec.instance_variable_set(:@response, bridge_event)
65
+ @freec.send(:parse_response)
66
+ @freec.should_receive(:send_and_read).with("filter Unique-ID 6c75cb42-e72d-48bf-9ecf-d71bd4b60617")
67
+ @freec.send(:subscribe_to_new_channel_events)
68
+ end
69
+
70
+ it "should recognize last event was DTMF to call the on_dtmf callback" do
71
+ dtmf_event = EVENT.sub('CHANNEL_EXECUTE', 'DTMF')
72
+ @freec.instance_variable_set(:@response, dtmf_event)
73
+ @freec.send(:parse_response)
74
+ @freec.send(:last_event_dtmf?).should be_true
75
+ end
76
+
77
+ it "should recognize an event as a non-DTMF one" do
78
+ @freec.instance_variable_set(:@response, EVENT)
79
+ @freec.send(:parse_response)
80
+ @freec.send(:last_event_dtmf?).should be_false
81
+ end
82
+
83
+ it "should recognize that app execution has been completed for the last run app" do
84
+ @freec.instance_variable_set(:@last_app_executed, 'set')
85
+ set_executed_event = EVENT.sub('CHANNEL_EXECUTE', 'CHANNEL_EXECUTE_COMPLETE')
86
+ @freec.instance_variable_set(:@response, set_executed_event)
87
+ @freec.send(:parse_response)
88
+
89
+ @freec.send(:execute_completed?).should be_true
90
+ @freec.instance_variable_get(:@last_app_executed).should be_nil
91
+ end
92
+
93
+ it "should recognize that app execution has been completed but not for the last run one" do
94
+ @freec.instance_variable_set(:@last_app_executed, 'different')
95
+ set_executed_event = EVENT.sub('CHANNEL_EXECUTE', 'CHANNEL_EXECUTE_COMPLETE')
96
+ @freec.instance_variable_set(:@response, set_executed_event)
97
+ @freec.send(:parse_response)
98
+
99
+ @freec.send(:execute_completed?).should be_false
100
+ @freec.instance_variable_get(:@last_app_executed).should == 'different'
101
+ end
102
+
103
+ it "should hangup the call, send exit command to Freeswitch and disconnect from it when step callback returns nil" do
104
+ @freec.should_receive(:send_data).with("connect")
105
+ @freec.should_receive(:send_data).with("events plain all")
106
+ @freec.should_receive(:send_data).with('filter Unique-ID f3c2d5ee-d064-4f55-9280-5be2a65867e8')
107
+
108
+ @io.should_receive(:gets).and_return(initial_response, initial_response, initial_response, "bye\n\n")
109
+ @io.should_receive(:closed?).twice.and_return(false)
110
+ @freec.should_receive(:step).and_return(nil)
111
+ @freec.should_receive(:execute_app).with('hangup')
112
+ @freec.should_receive(:on_hangup)
113
+ @freec.should_receive(:send_data).with("exit")
114
+ @freec.handle_call
115
+ end
116
+
117
+ it "should exit when Freeswitch disconnects (e.g. caller hangs up) and neither call the step callback nor send any other data to Freeswitch" do
118
+ disconnect_event = EVENT.sub('text/event-plain', 'text/disconnect-notice')
119
+ @freec.should_receive(:send_data).with("connect")
120
+ @freec.should_receive(:send_data).with("events plain all")
121
+ @freec.should_receive(:send_data).with('filter Unique-ID f3c2d5ee-d064-4f55-9280-5be2a65867e8')
122
+ @io.should_receive(:gets).and_return(initial_response, initial_response, event_parts(disconnect_event)[0], event_parts(disconnect_event)[1])
123
+ @io.should_receive(:closed?).twice.and_return(true)
124
+ @freec.should_receive(:step).never
125
+ @freec.should_receive(:on_hangup)
126
+ @freec.should_receive(:execute_app).never
127
+ @freec.should_receive(:send_data).never
128
+ @freec.handle_call
129
+ end
130
+
131
+ end
132
+
133
+ describe "callback exception handling" do
134
+
135
+ it "should catch and log any exception occurred in a callback" do
136
+ @freec.should_receive(:callback_name).and_raise(RuntimeError)
137
+ @freec.log.should_receive(:error).with('RuntimeError')
138
+ @freec.log.should_receive(:error).at_least(1).times #backtrace
139
+ lambda { @freec.send(:callback, :callback_name) }.should_not raise_error(Exception)
140
+ end
141
+
142
+ end
143
+
144
+ describe "custom waiting conditions" do
145
+
146
+ # it "should return true from waiting_for_this_response? when the conditions for the response are met" do
147
+ # @freec.wait_for(:content_type, 'command/reply')
148
+ # @freec.instance_variable_set(:@response, EVENT)
149
+ # @freec.send(:parse_response)
150
+ # @freec.send(:waiting_for_this_response?).should be_true
151
+ # end
152
+ #
153
+ # it "should return false from waiting_for_this_response? when the conditions for the response are not met" do
154
+ # @freec.wait_for(:content_type, 'text/event-plain')
155
+ # @freec.instance_variable_set(:@response, EVENT)
156
+ # @freec.send(:parse_response)
157
+ # @freec.send(:waiting_for_this_response?).should be_false
158
+ # end
159
+ #
160
+ # it "should reset the waiting conditions after they have been met" do
161
+ # @freec.wait_for(:content_type, 'command/reply')
162
+ # @freec.should_receive(:send_data).with("connect")
163
+ # @freec.should_receive(:send_data).with("myevents")
164
+ # @io.should_receive(:gets).and_return(EVENT, EVENT, "bye\n\n")
165
+ # @freec.instance_variable_set(:@last_app_executed, 'set')
166
+ # @freec.should_receive(:step).and_return(nil)
167
+ # @freec.should_receive(:execute_app).with('hangup')
168
+ # @freec.should_receive(:on_hangup)
169
+ # @freec.should_receive(:send_data).with("exit")
170
+ # @freec.handle_call
171
+ # @freec.send(:waiting_for_this_response?).should be_nil
172
+ # end
173
+ #
174
+ end
175
+
176
+ end
@@ -0,0 +1,128 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ module FreeswitchApplicationsSpecHelper
4
+ def freeswitch_command(app, pars = nil)
5
+ cmd = "sendmsg "
6
+ cmd += "\ncall-command: execute"
7
+ cmd += "\nexecute-app-name: #{app}"
8
+ cmd += "\nexecute-app-arg: #{pars}" unless pars.blank?
9
+ cmd += "\nevent-lock:true"
10
+ cmd
11
+ end
12
+ end
13
+
14
+ describe Freec do
15
+
16
+ before do
17
+ io = stub(:io)
18
+ log = FreecLogger.new(STDOUT)
19
+ log.level = Logger::FATAL
20
+ @freec = Freec.new(io, log)
21
+ end
22
+
23
+ describe "executes Freeswitch applications" do
24
+ include FreeswitchApplicationsSpecHelper
25
+
26
+ it "should set the last_app_executed variable to last run app" do
27
+ @freec.should_receive(:send_data).with(freeswitch_command('answer'))
28
+ @freec.answer
29
+ @freec.instance_variable_get(:@last_app_executed).should == 'answer'
30
+ end
31
+
32
+ it "should execute the answer app when called the answer method" do
33
+ @freec.should_receive(:send_data).with(freeswitch_command('answer'))
34
+ @freec.answer
35
+ end
36
+
37
+ it "should execute the playback app when called the playback method" do
38
+ @freec.should_receive(:send_data).with(freeswitch_command('playback', 'sounds/file.wav'))
39
+ @freec.playback('sounds/file.wav')
40
+ end
41
+
42
+ it "should execute the phrase app with spell option when called the spell method" do
43
+ @freec.should_receive(:send_data).with(freeswitch_command('phrase', 'spell,abcd'))
44
+ @freec.spell('abcd')
45
+ end
46
+
47
+ it "should execute the speak app when called the speak method" do
48
+ @freec.should_receive(:send_data).with(freeswitch_command('speak', 'hello'))
49
+ @freec.speak('hello')
50
+ end
51
+
52
+ it "should execute the bridge app when called the bridge method" do
53
+ @freec.should_receive(:send_data).with(freeswitch_command('bridge', 'user/brian@10.0.1.2'))
54
+ @freec.bridge('user/brian@10.0.1.2')
55
+ end
56
+
57
+ it "should execute the transfer app when called the transfer method" do
58
+ @freec.should_receive(:send_data).with(freeswitch_command('transfer', '1000'))
59
+ @freec.transfer('1000')
60
+ end
61
+
62
+ it "should pass all numbers passed to the bridge method as params of the bridge separated by comma (thus numbers are called simultaneously)" do
63
+ @freec.should_receive(:send_data).with(freeswitch_command('bridge', 'user/brian@10.0.1.2,user/karl@10.0.1.2'))
64
+ @freec.bridge(['user/brian@10.0.1.2', 'user/karl@10.0.1.2'])
65
+ end
66
+
67
+ it "should execute the record app when called the record method" do
68
+ @freec.should_receive(:send_data).with(freeswitch_command('record', 'recordings/file.mp3 600'))
69
+ @freec.record('recordings/file.mp3')
70
+ end
71
+
72
+ it "should set the time_limit_secs option send to the record method as the max length of the recording" do
73
+ @freec.should_receive(:send_data).with(freeswitch_command('record', 'recordings/file.mp3 120'))
74
+ @freec.record('recordings/file.mp3', :time_limit_secs => 120)
75
+ end
76
+
77
+ it "should execute the read app when called the input method" do
78
+ @freec.should_receive(:send_data).with(freeswitch_command('read', '1 1 sounds/file.mp3 input 10000 #'))
79
+ @freec.read('sounds/file.mp3')
80
+ end
81
+
82
+ it "should allow to pass the minimum and maximum digits to be read" do
83
+ @freec.should_receive(:send_data).with(freeswitch_command('read', '2 5 sounds/file.mp3 input 10000 #'))
84
+ @freec.read('sounds/file.mp3', :min => 2, :max => 5)
85
+ end
86
+
87
+ it "should allow to pass the timeout in seconds before the read apps before it times out" do
88
+ @freec.should_receive(:send_data).with(freeswitch_command('read', '1 1 sounds/file.mp3 input 5000 #'))
89
+ @freec.read('sounds/file.mp3', :timeout => 5)
90
+ end
91
+
92
+ it "should allow to pass the terminator for the read app" do
93
+ @freec.should_receive(:send_data).with(freeswitch_command('read', '1 1 sounds/file.mp3 input 10000 *'))
94
+ @freec.read('sounds/file.mp3', :terminators => '*')
95
+ end
96
+
97
+ it "should allow to pass the terminators as an array for the read app" do
98
+ @freec.should_receive(:send_data).with(freeswitch_command('read', '1 1 sounds/file.mp3 input 10000 #,*'))
99
+ @freec.read('sounds/file.mp3', :terminators => ['#', '*'])
100
+ end
101
+
102
+ it "should allow to specify the variable name the input is read by the read app" do
103
+ @freec.should_receive(:send_data).with(freeswitch_command('read', '1 1 sounds/file.mp3 name 10000 #'))
104
+ @freec.read('sounds/file.mp3', :variable => 'name')
105
+ end
106
+
107
+ it "should execute the record_session app when called the start_recording method" do
108
+ @freec.should_receive(:send_data).with(freeswitch_command('record_session', 'file.wav'))
109
+ @freec.start_recording('file.wav')
110
+ end
111
+
112
+ it "should execute the stop_record_session app when called the start_recording method" do
113
+ @freec.should_receive(:send_data).with(freeswitch_command('stop_record_session', 'file.wav'))
114
+ @freec.stop_recording('file.wav')
115
+ end
116
+
117
+ it "should call the set app to set a variable" do
118
+ @freec.should_receive(:send_data).with(freeswitch_command('set', 'name=value'))
119
+ @freec.set_variable('name', 'value')
120
+ end
121
+
122
+ it "should call the hangup app when caled the hangup method" do
123
+ @freec.should_receive(:send_data).with(freeswitch_command('hangup'))
124
+ @freec.hangup
125
+ end
126
+
127
+ end
128
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ $:.unshift(File.join(File.dirname(__FILE__), "..", 'lib')).unshift(File.dirname(__FILE__)).
5
+ unshift(File.join(File.dirname(__FILE__), 'data'))
6
+ ROOT = File.join(File.dirname(__FILE__), '..') unless defined?(ROOT)
7
+ @@config = ''
8
+ ENVIRONMENT = 'test' unless defined?(ENVIRONMENT)
9
+ TEST = true unless defined?(TEST)
10
+ require 'freec'
11
+ require 'event'
12
+ require 'event_with_body'
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: freec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Jan Kubr
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-26 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: daemons
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: Layer between your Ruby voice app and Freeswitch.
36
+ email: hi@jankubr.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - lib/call_variables.rb
43
+ - lib/freec.rb
44
+ - lib/listener.rb
45
+ - lib/freec_base.rb
46
+ - lib/freec_logger.rb
47
+ - lib/freeswitch_applications.rb
48
+ - lib/tools.rb
49
+ - README.rdoc
50
+ files:
51
+ - lib/call_variables.rb
52
+ - lib/freec.rb
53
+ - lib/listener.rb
54
+ - lib/freec_base.rb
55
+ - lib/freec_logger.rb
56
+ - lib/freeswitch_applications.rb
57
+ - lib/tools.rb
58
+ - Manifest
59
+ - Rakefile
60
+ - README.rdoc
61
+ - spec/event_parser_spec.rb
62
+ - spec/freec_base_spec.rb
63
+ - spec/freeswitch_applications_spec.rb
64
+ - spec/spec_helper.rb
65
+ - spec/data/event.rb
66
+ - spec/data/event_with_body.rb
67
+ - freec.gemspec
68
+ has_rdoc: true
69
+ homepage: http://github.com/jankubr/freec
70
+ licenses: []
71
+
72
+ post_install_message:
73
+ rdoc_options:
74
+ - --line-numbers
75
+ - --inline-source
76
+ - --title
77
+ - Freec
78
+ - --main
79
+ - README.rdoc
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "1.2"
93
+ version:
94
+ requirements: []
95
+
96
+ rubyforge_project: freec
97
+ rubygems_version: 1.3.5
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: Layer between your Ruby voice app and Freeswitch.
101
+ test_files: []
102
+