├── VERSION ├── spec ├── spec.opts ├── spec_helper.rb └── tropo-webapi-ruby_spec.rb ├── .document ├── .gitignore ├── lib ├── tropo-webapi-ruby.rb └── tropo-webapi-ruby │ ├── object_patch.rb │ ├── tropo-webapi-ruby-helpers.rb │ └── tropo-webapi-ruby.rb ├── HISTORY.rdoc ├── LICENSE ├── Rakefile ├── tropo-webapi-ruby.gemspec ├── examples └── sinatra_server.rb └── README.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.10 2 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --loadby mtime 4 | --reverse 5 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | %w(rubygems tropo-webapi-ruby hashie rspec rspec/autorun).each { |lib| require lib } 4 | 5 | RSpec.configure do |config| 6 | 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | doc 21 | 22 | ## PROJECT::SPECIFIC 23 | 24 | *.gem -------------------------------------------------------------------------------- /lib/tropo-webapi-ruby.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path(File.dirname(__FILE__)) 2 | %w(uri json hashie time tropo-webapi-ruby/tropo-webapi-ruby-helpers tropo-webapi-ruby/tropo-webapi-ruby).each { |lib| require lib } 3 | 4 | # Add the instance_exec method to the object class for Ruby 1.8.6 support 5 | require 'tropo-webapi-ruby/object_patch' if RUBY_VERSION == '1.8.6' -------------------------------------------------------------------------------- /lib/tropo-webapi-ruby/object_patch.rb: -------------------------------------------------------------------------------- 1 | # Borrowed from here: http://eigenclass.org/hiki.rb?instance_exec 2 | class Object 3 | def instance_exec(*args, &block) 4 | mname = "__instance_exec_#{Thread.current.object_id.abs}" 5 | class << self; self end.class_eval{ define_method(mname, &block) } 6 | begin 7 | ret = send(mname, *args) 8 | ensure 9 | class << self; self end.class_eval{ undef_method(mname) } rescue nil 10 | end 11 | ret 12 | end 13 | end -------------------------------------------------------------------------------- /HISTORY.rdoc: -------------------------------------------------------------------------------- 1 | = Tropo Release History 2 | 3 | == Version 0.1.5 4 | 5 | - First public release 6 | 7 | == Version 0.1.6 8 | 9 | - Added the appropriate ActiveSupport dependency for 'instance_exec' for Ruby 1.8.6 support. 10 | - Added a dependency and support for Hashie. 11 | 12 | == Version 0.1.9 13 | 14 | - Fixed the start_recording so it does not require a 'name' parameter 15 | - Aliased start_call_recording -> start_recording 16 | - Aliased stop_call_recording -> stop_recording 17 | - Fixes to the README 18 | - Fixed the yardoc install 19 | - The Tropo::Generator.parse method will now take a JSON string or a Ruby hash. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Voxeo, Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'yard' 4 | 5 | begin 6 | require 'jeweler' 7 | Jeweler::Tasks.new do |gem| 8 | gem.name = "tropo-webapi-ruby" 9 | gem.summary = "Tropo Web API Ruby Gem" 10 | gem.description = "Ruby library for interacting with the Tropo Web API via REST & JSON" 11 | gem.email = "jsgoecke@voxeo.com" 12 | gem.homepage = "http://tropo.com" 13 | gem.authors = ["Jason Goecke"] 14 | gem.add_development_dependency "rspec", ">= 1.2.9" 15 | gem.files.include %w(lib/tropo-webapi-ruby.rb lib/tropo-webapi-ruby/tropo-webapi-ruby.rb lib/tropo-webapi-ruby/tropo-webapi-ruby-helpers.rb LICENSE VERSION README.markdown) 16 | #gem.add_dependency('json', '>= 1.2.0') 17 | gem.add_dependency('json_pure', '>= 1.2.0') 18 | gem.add_dependency('hashie', '>= 0.2.0') 19 | gem.required_ruby_version = '>= 1.8.6' 20 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 21 | end 22 | Jeweler::GemcutterTasks.new 23 | rescue LoadError 24 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 25 | end 26 | 27 | require 'spec/rake/spectask' 28 | Spec::Rake::SpecTask.new(:spec) do |spec| 29 | spec.libs << 'lib' << 'spec' 30 | spec.spec_files = FileList['spec/**/*_spec.rb'] 31 | end 32 | 33 | Spec::Rake::SpecTask.new(:rcov) do |spec| 34 | spec.libs << 'lib' << 'spec' 35 | spec.pattern = 'spec/**/*_spec.rb' 36 | spec.rcov = true 37 | end 38 | 39 | task :spec => :check_dependencies 40 | 41 | task :default => :spec 42 | 43 | require 'rake/rdoctask' 44 | Rake::RDocTask.new do |rdoc| 45 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 46 | 47 | rdoc.rdoc_dir = 'rdoc' 48 | rdoc.title = "tropo #{version}" 49 | rdoc.rdoc_files.include('README*') 50 | rdoc.rdoc_files.include('lib/**/*.rb') 51 | end 52 | 53 | YARD::Rake::YardocTask.new do |t| 54 | t.files = ['lib/tropo-webapi-ruby/*.rb', 'lib/*.rb', 'README'] 55 | end 56 | -------------------------------------------------------------------------------- /tropo-webapi-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{tropo-webapi-ruby} 8 | s.version = "0.1.10" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Jason Goecke"] 12 | s.date = %q{2010-05-14} 13 | s.description = %q{Ruby library for interacting with the Tropo Web API via REST & JSON} 14 | s.email = %q{jsgoecke@voxeo.com} 15 | s.extra_rdoc_files = [ 16 | "LICENSE", 17 | "README.rdoc" 18 | ] 19 | s.files = [ 20 | ".document", 21 | ".gitignore", 22 | "HISTORY.rdoc", 23 | "LICENSE", 24 | "README.rdoc", 25 | "Rakefile", 26 | "VERSION", 27 | "examples/sinatra_server.rb", 28 | "lib/tropo-webapi-ruby.rb", 29 | "lib/tropo-webapi-ruby/object_patch.rb", 30 | "lib/tropo-webapi-ruby/tropo-webapi-ruby-helpers.rb", 31 | "lib/tropo-webapi-ruby/tropo-webapi-ruby.rb", 32 | "spec/spec.opts", 33 | "spec/spec_helper.rb", 34 | "spec/tropo-webapi-ruby_spec.rb", 35 | "tropo-webapi-ruby.gemspec" 36 | ] 37 | s.homepage = %q{http://tropo.com} 38 | s.rdoc_options = ["--charset=UTF-8"] 39 | s.require_paths = ["lib"] 40 | s.required_ruby_version = Gem::Requirement.new(">= 1.8.6") 41 | s.rubygems_version = %q{1.3.5} 42 | s.summary = %q{Tropo Web API Ruby Gem} 43 | s.test_files = [ 44 | "spec/spec_helper.rb", 45 | "spec/tropo-webapi-ruby_spec.rb", 46 | "examples/sinatra_server.rb" 47 | ] 48 | 49 | if s.respond_to? :specification_version then 50 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 51 | s.specification_version = 3 52 | 53 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 54 | s.add_development_dependency(%q, [">= 1.2.9"]) 55 | s.add_runtime_dependency(%q, [">= 1.2.0"]) 56 | s.add_runtime_dependency(%q, [">= 0.2.0"]) 57 | else 58 | s.add_dependency(%q, [">= 1.2.9"]) 59 | s.add_dependency(%q, [">= 1.2.0"]) 60 | s.add_dependency(%q, [">= 0.2.0"]) 61 | end 62 | else 63 | s.add_dependency(%q, [">= 1.2.9"]) 64 | s.add_dependency(%q, [">= 1.2.0"]) 65 | s.add_dependency(%q, [">= 0.2.0"]) 66 | end 67 | end 68 | 69 | -------------------------------------------------------------------------------- /examples/sinatra_server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | require '../lib/tropo-webapi-ruby' 4 | 5 | enable :sessions 6 | 7 | post '/index.json' do 8 | tropo = Tropo::Generator.new do 9 | on :event => 'continue', :next => '/the_answer.json' 10 | ask({ :name => 'account_number', 11 | :bargein => 'true', 12 | :timeout => 30, 13 | :require => 'true' }) do 14 | say :value => 'Please enter your account number' 15 | choices :value => '[5 DIGITS]' 16 | end 17 | end 18 | tropo.response 19 | end 20 | 21 | post '/the_answer.json' do 22 | Tropo::Generator.hangup 23 | end 24 | 25 | post '/helloworld.json' do 26 | json_session = request.env["rack.input"].read 27 | p json_session 28 | tropo_session = Tropo::Generator.parse json_session 29 | Tropo::Generator.say :value => 'Hello World!' 30 | end 31 | 32 | # === 33 | # Ask answer example 34 | post '/ask.json' do 35 | tropo = Tropo::Generator.new do 36 | on :event => 'hangup', :next => '/hangup.json' 37 | on :event => 'continue', :next => '/answer.json' 38 | ask({ :name => 'account_number', 39 | :bargein => 'true', 40 | :timeout => 30, 41 | :require => 'true' }) do 42 | say :value => 'Please say your account number' 43 | choices :value => '[5 DIGITS]' 44 | end 45 | end 46 | tropo.response 47 | end 48 | 49 | post '/answer.json' do 50 | tropo_event = Tropo::Generator.parse request.env["rack.input"].read 51 | p tropo_event 52 | end 53 | 54 | post '/hangup.json' do 55 | tropo_event = Tropo::Generator.parse request.env["rack.input"].read 56 | p tropo_event 57 | end 58 | # === 59 | post '/say_goodbye.json' do 60 | Tropo::Generator.say :value => 'Thank you. Goodbye.' 61 | end 62 | 63 | post '/start.json' do 64 | tropo_session = Tropo::Generator.parse request.env["rack.input"].read 65 | session[:callid] = tropo_session[:session][:id] 66 | tropo = Tropo::Generator.new do 67 | say :value => 'Hello World!' 68 | on :event => 'hangup', :next => '/hangup.json' 69 | end 70 | tropo.response 71 | end 72 | 73 | post '/disconnect.json' do 74 | tropo = Tropo::Generator.hangup 75 | p tropo 76 | tropo 77 | end 78 | 79 | post '/hangup.json' do 80 | p 'Received a hangup response!' 81 | json_string = request.env["rack.input"].read 82 | tropo_session = Tropo::Generator.parse json_string 83 | p tropo_session 84 | end 85 | 86 | post '/conference.json' do 87 | tropo = Tropo::Generator.conference({ :name => 'foo', 88 | :id => '1234', 89 | :mute => false, 90 | :send_tones => false, 91 | :exit_tone => '#' }) do 92 | on(:event => 'join') { say :value => 'Welcome to the conference' } 93 | on(:event => 'leave') { say :value => 'Someone has left the conference' } 94 | end 95 | tropo 96 | end 97 | 98 | post '/result.json' do 99 | tropo_result = Tropo::Generator.parse request.env["rack.input"].read 100 | p tropo_result 101 | end 102 | 103 | post '/nomatch.json' do 104 | Tropo::Generator.say :value => 'Something went terribly wrong!' 105 | end 106 | 107 | post '/redirect.json' do 108 | response = Tropo::Generator.redirect(:to => 'sip:9991427589@sip.tropo.com') 109 | p response 110 | response 111 | end 112 | 113 | post '/reject.json' do 114 | response = Tropo::Generator.reject 115 | p response 116 | response 117 | end 118 | 119 | post '/total_recording.json' do 120 | tropo = Tropo::Generator.new do 121 | start_recording :name => 'ladeda', :url => 'http://postthis/mofo' 122 | say :value => 'I am now recording!' 123 | stop_recording 124 | end 125 | p tropo.response 126 | tropo.response 127 | end 128 | 129 | post '/record.json' do 130 | response = Tropo::Generator.record({ :name => 'foo', 131 | :url => 'http://sendme.com/tropo', 132 | :beep => true, 133 | :send_tones => false, 134 | :exit_tone => '#' }) do 135 | say :value => 'Please say your account number' 136 | choices :value => '[5 DIGITS]' 137 | end 138 | p response 139 | response 140 | end 141 | 142 | post '/session_scope.json' do 143 | session = Hash.new 144 | session[:foo] = 'bar' 145 | tropo = Tropo::Generator.new(session) do 146 | p session 147 | say 'Do we now see the session?' 148 | end 149 | tropo.response 150 | end 151 | 152 | post '/transfer_request.json' do 153 | tropo = Tropo::Generator.new do 154 | say 'Hello, about to transfer you' 155 | transfer :to => 'sip:9991427589@sip.tropo.com' 156 | say 'I like rocks!'*10 157 | end 158 | p tropo.response 159 | tropo.response 160 | end 161 | 162 | post '/ons.json' do 163 | tropo = Tropo::Generator.new 164 | p tropo.on :event => 'hangup', :next => '/hangup.json' 165 | p tropo.on :event => 'continue', :next => '/next_resource.json' 166 | tropo.say 'That was a boat load of ons...' 167 | tropo.response 168 | end 169 | 170 | post '/on_with_block.json' do 171 | tropo = Tropo::Generator.new 172 | p tropo.on :event => 'hangup', :next => '/hangup.json' 173 | p tropo.on :event => 'continue', :next => '/next_resource.json' do 174 | say 'About to send you to the next menu.' 175 | end 176 | tropo.say 'That was a boat load of ons...' 177 | tropo.response 178 | end 179 | 180 | post '/test_json.json' do 181 | t = Tropo::Generator.new 182 | t.on :event => 'error', :next => '/error.json' # For fatal programming errors. Log some details so we can fix it 183 | t.on :event => 'hangup', :next => '/hangup.json' # When a user hangs or call is done. We will want to log some details. 184 | t.on :event => 'continue', :next => '/next.json' 185 | t.say "Hello" 186 | t.start_recording(:name => 'recording', :url => "http://heroku-voip.marksilver.net/post_audio_to_s3?filename=foo.wav&unique_id=bar") 187 | # [From this point, until stop_recording(), we will record what the caller *and* the IVR say] 188 | t.say "You are now on the record." 189 | # Prompt the user to incriminate themselve on-the-record 190 | t.say "Go ahead, sing-along." 191 | t.say "http://denalidomain.com/music/keepers/HappyHappyBirthdaytoYou-Disney.mp3" 192 | t.response 193 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Tropo Web API Ruby Library 2 | 3 | A Ruby library for interaction with the Tropo Web API (http://tropo.com) using JSON. 4 | 5 | == Tropo Web API Overview 6 | 7 | The Tropo Remote API provides a RESTful JSON API for controlling realtime communications applications from 8 | your own web servers. 9 | 10 | == Requirements 11 | 12 | * Unit tests passed on: Ruby MRI v1.8.6/1.8.7 and JRuby v1.5.0 13 | * RubyGems 14 | 15 | Note: If using with ActiveSupport, v2.3.5 or better of ActiveSupport is required. 16 | 17 | == Installation 18 | 19 | $ sudo gem install tropo-webapi-ruby 20 | 21 | Optional, if you would like to use with Sinatra: 22 | 23 | $ sudo gem install sinatra 24 | 25 | == Generate Documentation 26 | 27 | === Developer 28 | 29 | $ gemserver 30 | 31 | === Project Developer 32 | 33 | $ sudo gem install yard 34 | 35 | From within the project: 36 | 37 | $ yardoc 38 | 39 | == Usage 40 | 41 | require 'rubygems' 42 | require 'tropo-webapi-ruby' 43 | 44 | # Will return the properly formatted JSON to pass to Tropo 45 | response = Tropo::Generator.ask({ :say => 'Hello world!' }) 46 | 47 | # Will return a Ruby Hash, with some transformations, from the JSON string received from Tropo 48 | response = Tropo::Generator.parse(json_string) 49 | 50 | # Will provide instance variables that will allow you to easily reference session type 51 | tropo = Tropo::Generator.new 52 | response = tropo.parse(json_string) 53 | p 'Hey, this is a voice session!' if tropo.voice_session 54 | p 'Hey, this is a text messaging session!' if tropo.text_session 55 | 56 | == Examples 57 | 58 | === Sinatra 59 | 60 | Using the great RESTful Web Services framework Sinatra for Ruby. 61 | 62 | ==== Hello World 63 | 64 | require 'rubygems' 65 | require 'sinatra' 66 | require 'tropo-webapi-ruby' 67 | 68 | post '/helloworld.json' do 69 | Tropo::Generator.say 'Hello World!' 70 | end 71 | 72 | post '/helloworld_block.json' do 73 | tropo = Tropo::Generator.new do 74 | say 'Hello World!' 75 | end 76 | tropo.response 77 | end 78 | 79 | post '/helloworld_twice.json' do 80 | tropo = Tropo::Generator.new 81 | tropo.say 'Hello World!' 82 | tropo.say 'Hello again.' 83 | tropo.response 84 | end 85 | 86 | ==== Getting Session Information 87 | 88 | # Produces a Ruby hash: 89 | # 90 | # { :session => 91 | # { :timestamp => Tue Jan 19 18:27:46 -0500 2010, 92 | # :user_type =>"HUMAN", 93 | # :initial_text => nil, 94 | # :account_id =>"0", 95 | # :headers => [{ "value" => "70", "key"=>"Max-Forwards" }, 96 | # { "value" => "385", "key"=>"Content-Length" }, 97 | # { "value" => "", "key"=>"Contact" }, 98 | # { "value" => "replaces", "key"=>"Supported" }, 99 | # { "value" => "", "key"=>"To" }, 100 | # { "value" => "1 INVITE", "key"=>"CSeq" }, 101 | # { "value" => "SJphone-M/1.65.382f (SJ Labs)", "key"=>"User-Agent" }, 102 | # { "value" => "SIP/2.0/UDP 127.0.0.1:49152;branch=z9000000a9;rport=49152", "key"=>"Via" }, 103 | # { "value" => "3FA7C70A1DD211B286B7A583D7B46DDD0xac106207", "key"=>"Call-ID" }, 104 | # { "value" => "application/sdp", "key"=>"Content-Type" }, 105 | # { "value" => "unknown ;tag=750b1b1648e9c876", "key"=>"From" }], 106 | # :id => "3FA7C70A1DD211B286B7A583D7B46DDD0xac106207", 107 | # :to => { :network => "PSTN", 108 | # :channel => "VOICE", 109 | # :name => "unknown", 110 | # :id => "sample.json"}, 111 | # :from => { :network => "PSTN", 112 | # :channel => "VOICE", 113 | # :name => "unknown", 114 | # :id => "unknown"}}} 115 | # 116 | post '/start_session.json' do 117 | tropo_session = Tropo::Generator.parse request.env["rack.input"].read 118 | p tropo_session 119 | end 120 | 121 | ==== Asking for input and receiving the response, or catching a hangup 122 | 123 | post '/ask.json' do 124 | tropo = Tropo::Generator.new do 125 | on :event => 'hangup', :next => '/hangup.json' 126 | on :event => 'continue', :next => '/answer.json' 127 | ask({ :name => 'account_number', 128 | :bargein => true, 129 | :timeout => 30, 130 | :require => 'true' }) do 131 | say :value => 'Please say your account number' 132 | choices :value => '[5 DIGITS]' 133 | end 134 | end 135 | tropo.response 136 | end 137 | 138 | # Produces a Ruby hash, if the user gives a response before hanging up: 139 | # 140 | # { :result => 141 | # { :actions => { :attempts => 1, 142 | # :disposition => "SUCCESS", 143 | # :interpretation => "12345", 144 | # :confidence => 100, 145 | # :name => "account_number", 146 | # :utterance => "1 2 3 4 5" }, 147 | # :session_duration => 3, 148 | # :error => nil, 149 | # :sequence => 1, 150 | # :session_id => "5325C262-1DD2-11B2-8F5B-C16F64C1D62E@127.0.0.1", 151 | # :state => "ANSWERED", 152 | # :complete => true } } 153 | # 154 | post '/answer.json' do 155 | tropo_event = Tropo::Generator.parse request.env["rack.input"].read 156 | p tropo_event 157 | end 158 | 159 | # Produces a Ruby hash, if the user hangs up before giving a reponse 160 | # { :result => 161 | # { :actions => {}, 162 | # :session_duration => 1, 163 | # :error => nil, 164 | # :sequence => 1, 165 | # :session_id => "812BEF50-1DD2-11B2-8F5B-C16F64C1D62E@127.0.0.1", 166 | # :state => "DISCONNECTED", 167 | # :complete => true } } 168 | # 169 | post '/hangup.json' do 170 | tropo_event = Tropo::Generator.parse request.env["rack.input"].read 171 | p tropo_event 172 | end 173 | 174 | ==== Redirect a call before answering 175 | 176 | # A redirect method 177 | post '/redirect.json' do 178 | Tropo::Generator.redirect({ :to => 'sip:1234', :from => '4155551212' }) 179 | end 180 | 181 | ==== Reject a call before answering 182 | 183 | # A reject method 184 | post '/reject.json' do 185 | Tropo::Generator.reject 186 | end 187 | 188 | ==== Setting a default voice for speech synthesis (text-to-speech/TTS) 189 | 190 | post '/speak.json' do 191 | t = Tropo::Generator.new(:voice => 'kate') 192 | t.say 'Hello!' # Will speak as kate now 193 | 194 | # or 195 | 196 | t = Tropo::Generator.new 197 | t.voice = 'kate' 198 | t.say 'Hello!' # Will speak as kate now 199 | end 200 | 201 | ==== Setting a default recognizer for speech recognition (ASR) 202 | 203 | post '/ask.json' do 204 | t = Tropo::Generator.new(:recognizer => 'fr-fr') 205 | t.ask({ :name => 'account_number', # Will now use the French speech recognition engine 206 | :bargein => true, 207 | :timeout => 30, 208 | :require => 'true' }) do 209 | say :value => "S'il vous plaît dire votre numéro de compte", :voice => 'florence' 210 | choices :value => '[5 DIGITS]' 211 | end 212 | 213 | # or 214 | 215 | t = Tropo::Generator.new 216 | t.recognizer = 'fr-fr' 217 | t.ask({ :name => 'account_number', # Will now use the French speech recognition engine 218 | :bargein => true, 219 | :timeout => 30, 220 | :require => 'true' }) do 221 | say :value => "S'il vous plaît dire votre numéro de compte", :voice => 'florence' 222 | choices :value => '[5 DIGITS]' 223 | end 224 | end 225 | 226 | === Additional Examples 227 | 228 | May be found by checking out the project from Github, and then looking in $PROJECT_HOME/examples and $PROJECT_HOME/spec/tropo-webapi-ruby_spec.rb. 229 | 230 | === Documentation 231 | 232 | * API Documentation: 233 | 234 | http://voxeo.github.com/tropo-webapi-ruby 235 | 236 | * Tropo Web API Documentation 237 | 238 | http://docs.tropo.com/webapi/2.0/home.htm 239 | 240 | == Notes 241 | 242 | In order to maintain compatibility between Ruby MRI and JRuby the gem requires 'json_pure'. If you are using the Ruby MRI and would prefer to use the C version of 'json', simply install the 'json' gem as follows: 243 | 244 | sudo gem install json 245 | 246 | == Copyright 247 | 248 | Copyright (c) 2010 Voxeo, Corporation. See LICENSE for details. -------------------------------------------------------------------------------- /lib/tropo-webapi-ruby/tropo-webapi-ruby-helpers.rb: -------------------------------------------------------------------------------- 1 | module Tropo 2 | module Helpers 3 | private 4 | 5 | ## 6 | # Method checks for presence of required elements and then builds the action 7 | # 8 | # @param [String] the name of the action to build 9 | # @param [Hash] the elements to be used to build the action 10 | # @return [Hash] provides the properply built hash for the action 11 | def build_action(action, params) 12 | raise ArgumentError, 'Action requires parameters' if params.nil? 13 | 14 | case action 15 | when 'choices' 16 | if params[:mode] 17 | if params[:mode] != 'dtmf' && params[:mode] != 'speech' 18 | raise ArgumentError, "If mode is provided, only 'dtmf', 'speech' or 'any' is supported" 19 | end 20 | end 21 | when 'conference' 22 | has_params?(params, 'conference', ['name', 'id']) 23 | when 'on' 24 | has_params?(params, 'on', 'event') 25 | when 'record' 26 | has_params?(params, 'record', ['name', 'url']) 27 | when 'start_recording' 28 | has_params?(params, 'start_recording', ['url']) 29 | 30 | # Camelcase this one to be Java friendly 31 | action = 'startRecording' 32 | when 'redirect' 33 | has_params?(params, 'redirect', 'to') 34 | raise ArgumentError, "Redirect should only be used alone and before the session is answered, use transfer instead" if @nested_hash 35 | when 'say' 36 | has_params?(params, 'say', 'value') 37 | return build_elements(params) 38 | when 'transfer' 39 | has_params?(params, 'transfer', 'to') 40 | end 41 | 42 | if action == 'on' 43 | build_elements(params) 44 | else 45 | { action.to_sym => build_elements(params) } 46 | end 47 | end 48 | 49 | ## 50 | # Checks to see if certain parameters are present, and if not raises an error 51 | # 52 | # @overload has_params?(params, action, names) 53 | # @param [Hash] the parameter hash to be checked for the presence of a parameter 54 | # @param [String] the action being checked 55 | # @param [String] the name of the key in the params that must be present 56 | # @overload has_params?(params, action, names) 57 | # @param [Hash] the parameter hash to be checked for the presence of a parameter 58 | # @param [String] the action being checked 59 | # @param [Array] a list of names of the keys in the params that must be present 60 | def has_params?(params, action, names) 61 | if names.kind_of? Array 62 | names.each { |name| raise ArgumentError, "A '#{name}' must be provided to a '#{action}' action" if params[name.to_sym].nil? } 63 | else 64 | raise ArgumentError, "A '#{names}' must be provided to a '#{action}' action" if params[names.to_sym].nil? 65 | end 66 | end 67 | 68 | # Takes a Ruby underscore string and converts to a Java friendly camelized string 69 | # 70 | # @param [String] the string to be camelized 71 | # @return [String] the Ruby string camelized 72 | def camelize(ruby_string) 73 | split_string = ruby_string.split('_') 74 | return_string = split_string[0] + split_string[1].capitalize 75 | return_string = return_string + split_string[2].capitalize if split_string[2] 76 | return_string 77 | end 78 | 79 | ## 80 | # Creates a nested hash when we have block within a block 81 | # 82 | # @param [String] the name of the action being built 83 | # @param [Hash] the parameters to be added to the action 84 | # @return [nil] 85 | def create_nested_hash(name, params) 86 | @nested_hash = build_action(name, params) 87 | @nested_name = name 88 | end 89 | 90 | ## 91 | # Creates a nested hash specfic to 'on', as an on may have an additional block 92 | # 93 | # @param [Hash] the parameters to be added to the instance of the 'on' action 94 | # @return [nil] 95 | def create_nested_on_hash(params) 96 | @nested_on_hash ||= { :on => Array.new } 97 | @nested_on_hash_cnt ||= 0 98 | @nested_on_hash[:on] << params 99 | end 100 | 101 | ## 102 | # Creates an on_hash for the on action 103 | # 104 | # @return [nil] 105 | def create_on_hash 106 | @on_hash ||= { :on => Array.new } 107 | end 108 | 109 | ## 110 | # Method builds the elements for each of the actions 111 | # 112 | # @param [Hash] the individual elements to be used to build the hash 113 | # @return [Hash] returns the elements properly formatted in a hash 114 | def build_elements(params) 115 | if params[:url] 116 | uri = URI.parse params[:url] 117 | # Check to see if it is a valid http address 118 | if uri.class != URI::HTTP 119 | # Check to see if it is a valid email address 120 | if params[:url].match(/^[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]$/) == false 121 | raise ArgumentError, "The 'url' paramater must be a valid URL" 122 | end 123 | end 124 | end 125 | 126 | hash = Hash.new 127 | params.each_pair do |k,v| 128 | if k.to_s.include? "_" 129 | k = camelize k.to_s 130 | k = k.to_sym if k 131 | end 132 | hash.merge!({ k => v }) 133 | end 134 | hash 135 | end 136 | 137 | ## 138 | # Takes a Java Camelized string and converts to an underscore string 139 | # 140 | # @param [String] the string to be de-camelized 141 | # @return [String] the Ruby string with an underscore and no capitals 142 | def decamelize(camel_string) 143 | camel_string.gsub(/[A-Z]/) { |char| '_' + char.downcase } 144 | end 145 | 146 | ## 147 | # Formats the @response instance variable to JSON before making it available to the accessor 148 | # 149 | # @return [nil]` 150 | def render_response 151 | @response.to_json 152 | end 153 | 154 | ## 155 | # Determines if there is a voice or recognizer specified, if not set it to the default specified and if not default leave it alone 156 | # this is for the speech synthesis and speech recognition language to use on a say/ask methods 157 | # 158 | # @params [Hash] the array of values to check if a voice and recognizer are present 159 | # @return [Hash] Will return the params with the appropriate voice/recognizer values set 160 | def set_language(params) 161 | params.merge!({ :recognizer => @recognizer }) if params[:recognizer].nil? && @recognizer 162 | params.merge!({ :voice => @voice }) if params[:voice].nil? && @voice 163 | params 164 | end 165 | 166 | ## 167 | # Returns an hash from a collapsed array, using the values of 'key' or 'name' as the collpassed hash key 168 | # 169 | # @param [Array] the array of values to collapse into a Hash 170 | # @return [Hash] the collapsed Hash 171 | def transform_array(array) 172 | transformed_to_hash = Hash.new 173 | 174 | array.each_with_index do |ele, i| 175 | # Set the key to the value of the respresentative key 176 | key = ele['key'] if ele['key'] 177 | key = ele['name'] if ele['name'] 178 | 179 | # Merge this new key into the hash 180 | transformed_to_hash.merge!({ key => Hash.new }) 181 | 182 | # Then add the corresponding key/values to this new hash 183 | ele.each_pair do |k, v| 184 | if k != 'key' && k != 'name' 185 | transformed_to_hash[key].merge!(transform_pair(k, v)) 186 | end 187 | end 188 | end 189 | 190 | transformed_to_hash 191 | end 192 | 193 | ## 194 | # Transforms a hash into the appropriatey formatted hash with no camelcase and keys as symbols 195 | # 196 | # @param [Hash] Hash to be transformed 197 | # @return [Hash] the transformed hash 198 | def transform_hash(hash) 199 | transformed_hash = Hash.new 200 | hash.each_pair { |k, v| transformed_hash.merge!(transform_pair(k, v)) } 201 | transformed_hash 202 | end 203 | 204 | ## 205 | # Transforms a single keypair into a decamelized symbol with the appropriate value. Also converts 206 | # any timestamps to a Ruby Time object 207 | # 208 | # @param[String] the key to be decamelized and symobolized 209 | # @param[Hash] the newly created hash that contins the properly formatted key 210 | def transform_pair(key, value) 211 | hash = { decamelize(key) => value } 212 | hash['timestamp'] = Time.parse(value) if hash['timestamp'] and hash['timestamp'].kind_of? String 213 | if hash['actions'] 214 | if hash['actions']['name'] 215 | key_name = hash['actions']['name'] 216 | hash['actions'].delete('name') 217 | hash['actions'] = { key_name => hash['actions'] } 218 | end 219 | end 220 | set_session_type(hash) if hash['channel'] 221 | hash 222 | end 223 | 224 | ## 225 | # Sets the session type instance variables of voice_session and text_session 226 | # 227 | # @param[Hash] the key, value pair of the channel 228 | # @return nil 229 | def set_session_type(hash) 230 | case hash['channel'] 231 | when "VOICE" 232 | @voice_session = true 233 | @text_session = false 234 | when "TEXT" 235 | @text_session = true 236 | @voice_session = false 237 | end 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /lib/tropo-webapi-ruby/tropo-webapi-ruby.rb: -------------------------------------------------------------------------------- 1 | # @author Jason Goecke 2 | module Tropo 3 | class Generator 4 | include Tropo::Helpers 5 | 6 | ## 7 | # Set a couple of Booleans to indicate the session type as a convenience 8 | # Set a default voice for speech synthesis 9 | # Set a default recognizer for speech recognition 10 | attr_reader :voice_session, :text_session, :voice, :recognizer 11 | 12 | ## 13 | # Defines the actions on self so that we may call them individually 14 | # 15 | # @return [nil] 16 | class << self 17 | def method_missing(method_id, *args, &block) 18 | g = Generator.new 19 | g.send(method_id, *args, &block) 20 | end 21 | end 22 | 23 | ## 24 | # Initializes the Generator class 25 | # 26 | # @overload initialize() 27 | # @overload initialize(params) 28 | # @param [String] voice sets the value of the default voice 29 | # @param [Object] pass in an object that may be accessed inside the block 30 | # @overload initialize(params, &block) 31 | # @param [Object] pass in an object that may be accessed inside the block 32 | # @param [String] voice sets the value of the default voice 33 | # @param [Block] a block of code to execute (optional) 34 | # @return [Object] a new Generator object 35 | def initialize(params={}, &block) 36 | @response = { :tropo => Array.new } 37 | @voice = params[:voice] if params[:voice] 38 | @recognizer = params[:recognizer] if params[:recognizer] 39 | 40 | if block_given? 41 | # Lets us know were are in the midst of building a block, so we only rendor the JSON 42 | # response at the end of executing the block, rather than at each action 43 | @building = true 44 | instance_exec(&block) 45 | render_response 46 | end 47 | end 48 | 49 | ## 50 | # Prompts the user (audio file or text to speech) and optionally waits for a response from the user. 51 | # If collected, responses may be in the form of DTMF, speech recognition or text using a grammar or 52 | # free-form text. 53 | # 54 | # @overload ask(params) 55 | # @param [Hash] params the options to create an ask action request with. 56 | # @option params [String] :name the name to assign to the result when returned to the application, default is true 57 | # @option params [optional, Integer] :attempts (1) the number of times to prompt the user for input 58 | # @option params [optional, Boolean] :bargein (true) allows a user to enter a key to stop the ask action 59 | # @option params [optional, Float] :min_confidence (.5) the minimum confidence by which to accept the response expressed from 0-1, as opposed to asking again 60 | # @option params [optional, Boolean] :required (true) if this is a field that must be completed by the user 61 | # @option params [optional, Integer] :timeout (30) the amount of time, in seconds, to wait for a response before moving on 62 | # @overload ask(params, &block) 63 | # @param [Hash] params the options to create an ask action request with. 64 | # @param [Block] takes a block so that you may trigger actions, such as a say, on a specific event 65 | # @option params [String] :name the name to assign to the result when returned to the application 66 | # @option params [optional, Integer] :attempts (1) the number of times to prompt the user for input 67 | # @option params [optional, Boolean] :bargein (true) allows a user to enter a key to stop the ask action 68 | # @option params [optional, Float] :min_confidence (.5) the minimum confidence by which to accept the response expressed from 0-1, as opposed to asking again 69 | # @option params [optional, Boolean] :required (true) if this is a field that must be completed by the user 70 | # @option params [optional, Integer] :timeout (30) the amount of time, in seconds, to wait for a response before moving on 71 | # @return [String, nil] the JSON string to be passed back to Tropo or nil 72 | # if the method has been called from inside a block 73 | def ask(params={}, &block) 74 | params = set_language(params) 75 | if block_given? 76 | create_nested_hash('ask', params) 77 | instance_exec(&block) 78 | @response[:tropo] << @nested_hash 79 | else 80 | hash = build_action('ask', params) 81 | @response[:tropo] << hash 82 | end 83 | render_response if @building.nil? 84 | end 85 | alias :prompt :ask 86 | 87 | ## 88 | # Prompts initiates a new call. May only be used when no call is active. 89 | # 90 | # @overload call(params) 91 | # @param [Hash] params the options to create a call action request with. 92 | # @option params [String] :to the destination of the call, may be a phone number, SMS number or IM address 93 | # @option params [optional, String] :from the phone number or IM address the call will come from 94 | # @option params [optional, String] :network which network the call will be initiated with, such as SMS 95 | # @option params [optional, String] :channel the channel the call will be initiated over, may be TEXT or VOICE 96 | # @option params [optional, Integer] :timeout (30) the amount of time, in seconds, to wait for a response before moving on 97 | # @option params [optional, Boolean] :answer_on_media (true) 98 | # @options params [optional, Hash] :headers A set of key/values to apply as customer SIP headers to the outgoing call 99 | # @options params [optional, Hash] :recording Refer to the recording method for paramaters in the hash 100 | # @overload ask(params, &block) 101 | # @param [Hash] params the options to create an message action request with. 102 | # @param [Block] takes a block so that you may trigger actions, such as a say, on a specific event 103 | # @option params [String] :to the destination of the call, may be a phone number, SMS number or IM address 104 | # @option params [optional, String] :from the phone number or IM address the call will come from 105 | # @option params [optional, String] :network which network the call will be initiated with, such as SMS 106 | # @option params [optional, String] :channel the channel the call will be initiated over, may be TEXT or VOICE 107 | # @option params [optional, Integer] :timeout (30) the amount of time, in seconds, to wait for a response before moving on 108 | # @option params [optional, Boolean] :answer_on_media (true) 109 | # @options params [optional, Hash] :headers A set of key/values to apply as customer SIP headers to the outgoing call 110 | # @options params [optional, Hash] :recording Refer to the recording method for paramaters in the hash 111 | # @return [String, nil] the JSON string to be passed back to Tropo or nil 112 | # if the method has been called from inside a block 113 | def call(params={}, &block) 114 | if block_given? 115 | create_nested_hash('call', params) 116 | instance_exec(&block) 117 | @response[:tropo] << @nested_hash 118 | else 119 | hash = build_action('call', params) 120 | @response[:tropo] << hash 121 | end 122 | render_response if @building.nil? 123 | end 124 | 125 | ## 126 | # Choices to give the user on input 127 | # 128 | # @param [Hash] params the options used to construct the grammar for the user 129 | # @option params [String] :value the name to assign to the result when returned to the app 130 | # @option params [optional, String] :mode (ANY) the mode to use when asking the user [DTMF, SPEECH or ANY] 131 | # @option params [optional, String] :term_char the user may enter a keypad entry to stop the request 132 | # @option params [optional, String] :type (simple/grammar) the type of grammar to use 133 | # @option [String, nil] the JSON string to be passed back to Tropo or nil 134 | # if the method has been called from inside a block 135 | def choices(params={}) 136 | hash = build_action('choices', params) 137 | 138 | if @nested_hash 139 | @nested_hash[@nested_name.to_sym].merge!(hash) 140 | else 141 | @response[:tropo] << hash 142 | render_response if @building.nil? 143 | end 144 | end 145 | 146 | ## 147 | # Creates a conference or pushes a user to an existing conference 148 | # 149 | # @overload conference(params) 150 | # @param [Hash] params the options to create a message with. 151 | # @option params [String] :name the name to assign to the conference room and to identify events back to the application 152 | # @option params [Integer] :id the number to assign to the conference room 153 | # @option params [optional, Boolean] :mute (false) whether to mute this caller in the conference 154 | # @option params [optional, Integer] :max_time the maximum time, in seconds, to allow this user to stay in conference 155 | # @option params [optional, Integer] :send_tones whether to send the DTMF a user may enter to the audio of the conference 156 | # @option params [optional, String] :exit_tone whether to play a beep when this user exits a conference 157 | # @overload conference(params, &block) 158 | # @param [Hash] params the options to create a message with. 159 | # @param [Block] takes a block so that you may trigger actions, such as a say, on a specific event 160 | # @option params [String] :name the name to assign to the conference room and to identify events back to the application 161 | # @option params [Integer] :id the number to assign to the conference room 162 | # @option params [optional, Boolean] :mute (false) whether to mute this caller in the conference 163 | # @option params [optional, Integer] :max_time the maximum time, in seconds, to allow this user to stay in conference 164 | # @option params [optional, Integer] :send_tones whether to send the DTMF a user may enter to the audio of the conference 165 | # @option params [optional, String] :exit_tone whether to play a beep when this user exits a conference 166 | # @return [String, nil] the JSON string to be passed back to Tropo or nil 167 | # if the method has been called from inside a block 168 | def conference(params={}, &block) 169 | if block_given? 170 | create_nested_hash('conference', params) 171 | instance_exec(&block) 172 | @response[:tropo] << @nested_hash 173 | else 174 | hash = build_action('conference', params) 175 | @response[:tropo] << hash 176 | end 177 | render_response if @building.nil? 178 | end 179 | 180 | ## 181 | # This function instructs Tropo to "hang-up" or disconnect the current session. 182 | # 183 | # May trigger these events: 184 | # - hangup 185 | # - error 186 | # 187 | # @return [String, nil] returns the JSON string to hangup/stop the current session or nil 188 | # if the method has been called from inside a block 189 | def hangup 190 | @response[:tropo] << { :hangup => nil } 191 | render_response if @building.nil? 192 | end 193 | alias :disconnect :hangup 194 | 195 | ## 196 | # Message initiates a new message to a destination and then hangs up on that destination. Also takes a say method 197 | # in order to deliver a message to that desintation and then hangup. 198 | # 199 | # @overload message(params) 200 | # @param [Hash] params the options to create a message action request with. 201 | # @option params [String] :to the destination of the call, may be a phone number, SMS number or IM address 202 | # @option params [optional, String] :from the phone number or IM address the call will come from 203 | # @option params [optional, String] :network which network the call will be initiated with, such as SMS 204 | # @option params [optional, String] :channel the channel the call will be initiated over, may be TEXT or VOICE 205 | # @option params [optional, Integer] :timeout (30) the amount of time, in seconds, to wait for a response before moving on 206 | # @option params [optional, Boolean] :answer_on_media (true) 207 | # @options params [optional, Hash] :headers A set of key/values to apply as customer SIP headers to the outgoing call 208 | # @options params [optional, Hash] :recording Refer to the recording method for paramaters in the hash 209 | # @overload ask(params, &block) 210 | # @param [Hash] params the options to create an message action request with. 211 | # @param [Block] takes a block so that you may trigger actions, such as a say, on a specific event 212 | # @option params [String] :to the destination of the call, may be a phone number, SMS number or IM address 213 | # @option params [optional, String] :from the phone number or IM address the call will come from 214 | # @option params [optional, String] :network which network the call will be initiated with, such as SMS 215 | # @option params [optional, String] :channel the channel the call will be initiated over, may be TEXT or VOICE 216 | # @option params [optional, Integer] :timeout (30) the amount of time, in seconds, to wait for a response before moving on 217 | # @option params [optional, Boolean] :answer_on_media (true) 218 | # @options params [optional, Hash] :headers A set of key/values to apply as customer SIP headers to the outgoing call 219 | # @options params [optional, Hash] :recording Refer to the recording method for paramaters in the hash 220 | # @return [String, nil] the JSON string to be passed back to Tropo or nil 221 | # if the method has been called from inside a block 222 | def message(params={}, &block) 223 | if block_given? 224 | create_nested_hash('message', params) 225 | instance_exec(&block) 226 | @response[:tropo] << @nested_hash 227 | else 228 | hash = build_action('message', params) 229 | @response[:tropo] << hash 230 | end 231 | render_response if @building.nil? 232 | end 233 | 234 | ## 235 | # Sets event handlers to call a REST resource when a particular event occurs 236 | # 237 | # @overload initialize(params) 238 | # @param [Hash] params the options to create a message with. 239 | # @option params [String] :event the event name that should trigger the callback 240 | # @option params [String] :next the resource to send the callback to, such as '/error.json' 241 | # @overload initialize(params, &block) 242 | # @param [Hash] params the options to create a message with. 243 | # @option params [String] :event the event name that should trigger the callback 244 | # @option params [String] :next the resource to send the callback to, such as '/error.json' 245 | # @param [Block] takes a block so that you may trigger actions, such as a say, on a specific event 246 | # @option [String, nil] the JSON string to be passed back to Tropo or nil 247 | # if the method has been called from inside a block 248 | def on(params={}, &block) 249 | if block_given? 250 | create_nested_on_hash(params) 251 | instance_exec(&block) 252 | if @nested_hash 253 | @nested_hash[@nested_name.to_sym].merge!(@nested_on_hash) 254 | end 255 | else 256 | create_on_hash 257 | hash = build_action('on', params) 258 | @on_hash[:on] << hash 259 | if @nested_hash 260 | @nested_hash[@nested_name.to_sym].merge!(@on_hash) 261 | else 262 | @response[:tropo] << { :on => hash } 263 | render_response if @building.nil? 264 | end 265 | end 266 | end 267 | 268 | ## 269 | # Parses the JSON string recieved from Tropo into a Ruby Hash, or 270 | # if already a Ruby Hash parses it with the nicities provided by 271 | # the gem 272 | # 273 | # @param [String or Hash] a JSON string or a Ruby Hash 274 | # @return [Hash] a Hash representing the formatted response from Tropo 275 | def parse(response) 276 | response = JSON.parse(response) if response.class == String 277 | 278 | # Check to see what type of response we are working with 279 | if response['session'] 280 | transformed_response = { 'session' => { } } 281 | 282 | response['session'].each_pair do |key, value| 283 | value = transform_hash value if value.kind_of? Hash 284 | transformed_response['session'].merge!(transform_pair(key, value)) 285 | end 286 | 287 | elsif response['result'] 288 | transformed_response = { 'result' => { } } 289 | 290 | response['result'].each_pair do |key, value| 291 | value = transform_hash value if value.kind_of? Hash 292 | value = transform_array value if value.kind_of? Array 293 | transformed_response['result'].merge!(transform_pair(key, value)) 294 | end 295 | end 296 | 297 | transformed_response = Hashie::Mash.new(transformed_response) 298 | end 299 | 300 | ## 301 | # Sets the default recognizer for the object 302 | # 303 | # @param [String] recognizer the value to set the default voice to 304 | def recognizer=(recognizer) 305 | @recognizer = recognizer 306 | end 307 | 308 | ## 309 | # Plays a prompt (audio file or text to speech) and optionally waits for a response from the caller that is recorded. 310 | # If collected, responses may be in the form of DTMF or speech recognition using a simple grammar format defined below. 311 | # The record funtion is really an alias of the prompt function, but one which forces the record option to true regardless of how it is (or is not) initially set. 312 | # At the conclusion of the recording, the audio file may be automatically sent to an external server via FTP or an HTTP POST/Multipart Form. 313 | # If specified, the audio file may also be transcribed and the text returned to you via an email address or HTTP POST/Multipart Form. 314 | # 315 | # @overload record(params) 316 | # @param [Hash] params the options to create a message with. 317 | # @option params [String] :name the event name that should trigger the callback 318 | # @option params [String] :url a valid URI, an HTTP, FTP or email address to POST the recording file to 319 | # @option params [optional, String] :format (audio/wav) the audio format to record in, either a wav or mp3 320 | # @option params [optional, String] :username if posting to FTP, the username for the FTP server 321 | # @option params [optional, String] :password if posting to FTP, the password for the FTP server 322 | # @option params [optional, Hash] :transcription parameters used to transcribe the recording 323 | # @overload record(params, &block) 324 | # @param [Hash] params the options to create a message with. 325 | # @param [Block] takes a block so that you may trigger actions, such as a say, on a specific event 326 | # @option params [String] :name the event name that should trigger the callback 327 | # @option params [String] :url a valid URI, an HTTP, FTP or email address to POST the recording file to 328 | # @option params [optional, String] :format (audio/wav) the audio format to record in, either a wav or mp3 329 | # @option params [optional, String] :username if posting to FTP, the username for the FTP server 330 | # @option params [optional, String] :password if posting to FTP, the password for the FTP server 331 | # @option params [optional, Hash] :transcription parameters used to transcribe the recording 332 | # @option [String, nil] the JSON string to be passed back to Tropo or nil 333 | # if the method has been called from inside a block 334 | def record(params={}, &block) 335 | if block_given? 336 | create_nested_hash('record', params) 337 | instance_exec(&block) 338 | @response[:tropo] << @nested_hash 339 | else 340 | hash = build_action('record', params) 341 | @response[:tropo] << hash 342 | end 343 | render_response if @building.nil? 344 | end 345 | 346 | ## 347 | # The redirect function forwards an incoming call to another destination / phone number before answering it. 348 | # The redirect function must be called before answer is called; redirect expects that a call be in the ringing or answering state. 349 | # Use transfer when working with active answered calls. 350 | # 351 | # tel: classic phone number (See RFC 2896), must be proceeded by a + and the country code (ie - +14155551212 for a US #) 352 | # sip: Session Initiation Protocol (SIP) address 353 | # 354 | # @param [Hash] params the options to create a message with. 355 | # @option params [required, String] :to where to redirect the session to 356 | # @option params [optional, String] :from set the from id for the session when redirecting 357 | # @return [String, nil] the JSON string to redirect the current session or nil 358 | # if the method has been called from inside a block 359 | def redirect(params={}) 360 | hash = build_action('redirect', params) 361 | @response[:tropo] << hash 362 | render_response if @building.nil? 363 | end 364 | 365 | ## 366 | # Allows Tropo applications to reject incoming calls before they are answered. 367 | # For example, an application could inspect the callerID variable to determine if the caller is known, 368 | # and then use the reject call accordingly. 369 | # 370 | # @return [String, nil] the JSON string to reject the current session or nil 371 | # if the method has been called from inside a block 372 | def reject 373 | @response[:tropo] << { :reject => nil } 374 | render_response if @building.nil? 375 | end 376 | 377 | ## 378 | # Renders the JSON string to be sent to Tropo to execute a set of actions 379 | # 380 | # @return [String] the JSON string to be sent to the Tropo Remote API 381 | def response 382 | @response.to_json 383 | end 384 | 385 | ## 386 | # Resets the action hash if one desires to reuse the same Generator object 387 | # 388 | # @return [nil] 389 | def reset 390 | @response = { :tropo => Array.new } 391 | @voice_session = false 392 | @text_session = false 393 | end 394 | 395 | ## 396 | # Plays a prompt (audio file, text to speech or text for IM/SMS). There is no ability to wait for a response from a user. 397 | # An audio file used for playback may be in one of the following two formats: 398 | # Wav 8bit 8khz Ulaw 399 | # MP3 400 | # 401 | # @overload say(params) 402 | # @param [Hash] params the options to create a message with. 403 | # @option params [String] :value the text or audio to be spoken or played back to the user 404 | # @option params [Boolean] :event assigns a callback when a particular event occurs 405 | # @option params [Integer] :as instructs the engine on how to handle the grammar 406 | # @option params [Boolean] :format instructs the engine on how to handle the grammar 407 | # @return [String, nil] the JSON string to be passed back to Tropo or nil 408 | # if the method has been called from inside a block 409 | # @overload say(value, params) 410 | # @param [String] the text or audio to be spoken or played back to the user 411 | # @param [Hash] params the options to create a message with. 412 | # @option params [Boolean] :event assigns a callback when a particular event occurs 413 | # @option params [Integer] :as instructs the engine on how to handle the grammar 414 | # @option params [Boolean] :format instructs the engine on how to handle the grammar 415 | # @return [String, nil] the JSON string to be passed back to Tropo or nil 416 | # if the method has been called from inside a block 417 | def say(value=nil, params={}) 418 | 419 | # This will allow a string to be passed to the say, as opposed to always having to specify a :value key/pair, 420 | # or still allow a hash or Array to be passed as well 421 | if value.kind_of? String 422 | params[:value] = value 423 | elsif value.kind_of? Hash 424 | params = value 425 | elsif value.kind_of? Array 426 | params = value 427 | else 428 | raise ArgumentError, "An invalid paramater type #{value.class} has been passed" 429 | end 430 | 431 | response = { :say => Array.new } 432 | 433 | if params.kind_of? Array 434 | params.each do |param| 435 | param = set_language(param) 436 | hash = build_action('say', param) 437 | response[:say] << hash 438 | end 439 | else 440 | params = set_language(params) 441 | hash = build_action('say', params) 442 | response[:say] << hash 443 | end 444 | 445 | if @nested_hash && @nested_on_hash.nil? 446 | @nested_hash[@nested_name.to_sym].merge!(response) 447 | elsif @nested_on_hash 448 | @nested_on_hash[:on][@nested_on_hash_cnt].merge!(response) 449 | @nested_on_hash_cnt += 1 450 | else 451 | @response[:tropo] << response 452 | render_response if @building.nil? 453 | end 454 | end 455 | 456 | ## 457 | # Allows Tropo applications to begin recording the current session. 458 | # The resulting recording may then be sent via FTP or an HTTP POST/Multipart Form. 459 | # 460 | # @param [Hash] params the options to create a message with. 461 | # @option params [String] :url a valid URI, an HTTP, FTP or email address to POST the recording file to 462 | # @option params [optional, String] :format (audio/wav) the audio format to record in, either a wav or mp3 463 | # @option params [optional, String] :username if posting to FTP, the username for the FTP server 464 | # @option params [optional, String] :password if posting to FTP, the password for the FTP server 465 | # @return [String, nil] returns the JSON string to start the recording of a session or nil 466 | # if the method has been called from inside a block 467 | def start_recording(params={}) 468 | if block_given? 469 | create_nested_hash('start_recording', params) 470 | instance_exec(&block) 471 | @response[:tropo] << @nested_hash 472 | else 473 | hash = build_action('start_recording', params) 474 | @response[:tropo] << hash 475 | end 476 | render_response if @building.nil? 477 | end 478 | alias :start_call_recording :start_recording 479 | 480 | ## 481 | # Stops the recording of the current session after startCallRecording has been called 482 | # 483 | # @return [String, nil] returns the JSON string to stop the recording of a session or nil 484 | # if the method has been called from inside a block 485 | def stop_recording 486 | @response[:tropo] << { :stopRecording => nil } 487 | render_response if @building.nil? 488 | end 489 | alias :stop_call_recording :stop_recording 490 | 491 | ## 492 | # Transfers an already answered call to another destination / phone number. 493 | # Call may be transferred to another phone number or SIP address, which is set through the "to" parameter and is in URL format. 494 | # Supported formats include: 495 | # tel: classic phone number (See RFC 2896), must be proceeded by a + and the country code (ie - +14155551212 for a US #) 496 | # sip: SIP protocol address 497 | # 498 | # When this method is called the following occurs: 499 | # The audio file specified in playvalue is played to the existing call. This could be "hold music", a ring-back sound, etc. The audio file is played up to playrepeat times. 500 | # While audio is playing, a new call is initiated to the specified "to" address using the callerID specified. 501 | # If answerOnMedia is true, the audio from the new call is connected to the existing call immediately. 502 | # The system waits for an answer or other event from the new call up to the timeout. 503 | # If the call successfully completes within the timeout, the existing call and new call will be connected, onSuccess will be called, and the transfer call will return a success event. 504 | # If the call fails before the timeout, onCallFailure will be called and the method will return an onCallFailure event. 505 | # If the call fails due to the timeout elapsing, onTimeout will be called and the method will return a timeout event 506 | # 507 | # @overload transfer(params) 508 | # @param [Hash] params the options to create a transfer action request with 509 | # @option params [String] :name the name to assign to the result when returned to the application, default is true 510 | # @option params [optional, Boolean] :answer_on_media ??? 511 | # @option params [optional, Integer] :answer_timeout the amount of time to ring the far side before giving up and going to the next step 512 | # @option params [optional, Boolean] :required (true) ??? 513 | # @option params [required, String] :to where to redirect the session to 514 | # @option params [optional, String] :from set the from id for the session when redirecting 515 | # @option params [optional, Integer] :ring_repeat ??? 516 | # @overload transfer(params, &block) 517 | # @param [Hash] params the options to create a transfer action request with 518 | # @option params [String] :name the name to assign to the result when returned to the application, default is true 519 | # @option params [optional, Boolean] :answer_on_media ??? 520 | # @option params [optional, Integer] :answer_timeout the amount of time to ring the far side before giving up and going to the next step 521 | # @option params [optional, Boolean] :required (true) ??? 522 | # @option params [required, String] :to where to redirect the session to 523 | # @option params [optional, String] :from set the from id for the session when redirecting 524 | # @option params [optional, Integer] :ring_repeat ??? 525 | # @return [nil, String] 526 | def transfer(params={}, &block) 527 | if block_given? 528 | create_nested_hash('transfer', params) 529 | instance_exec(&block) 530 | @response[:tropo] << @nested_hash 531 | else 532 | hash = build_action('transfer', params) 533 | @response[:tropo] << hash 534 | end 535 | render_response if @building.nil? 536 | end 537 | 538 | ## 539 | # Returns the current hash object of the response, as opposed to JSON 540 | # 541 | # @return [Hash] the current hash of the response 542 | def to_hash 543 | @response 544 | end 545 | 546 | ## 547 | # Sets the default voice for the object 548 | # 549 | # @param [String] voice the value to set the default voice to 550 | def voice=(voice) 551 | @voice = voice 552 | end 553 | end 554 | end -------------------------------------------------------------------------------- /spec/tropo-webapi-ruby_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "Tropo" do 4 | 5 | # Ask action tests (and alias Prompt) 6 | it "should generate a complete 'ask' JSON document" do 7 | response = Tropo::Generator.ask({ :name => 'foo', 8 | :bargein => 'true', 9 | :timeout => 30, 10 | :require => 'true' }) 11 | JSON.parse(response).should == { "tropo" => [{ "ask" => { "name" => "foo", "bargein" => "true", "timeout" => 30, "require" => "true" } }] } 12 | end 13 | 14 | it "should generate an 'ask' JSON document when a block is passed" do 15 | response = Tropo::Generator.ask({ :name => 'foo', 16 | :bargein => 'true', 17 | :timeout => 30, 18 | :require => 'true' }) do 19 | say :value => 'Please say your account number' 20 | choices :value => '[5 DIGITS]' 21 | end 22 | JSON.parse(response).should == {"tropo"=>[{"ask"=>{"name"=>"foo", "say"=>[{"value"=>"Please say your account number"}], "bargein"=>"true", "timeout"=>30, "require"=>"true", "choices"=>{"value"=>"[5 DIGITS]"}}}]} 23 | end 24 | 25 | # There is currently a feature request to support an on within an ask 26 | # 27 | # it "should generate an 'ask' JSON document when a block is passed with an 'on' action" do 28 | # response = Tropo::Generator.ask({ :name => 'foo', 29 | # :bargein => 'true', 30 | # :timeout => 30, 31 | # :require => 'true' }) do 32 | # say :value => 'Please say your account number' 33 | # choices :value => '[5 DIGITS]' 34 | # on :event => 'success', :next => '/result.json' 35 | # end 36 | # JSON.parse(response).should == {"tropo"=>[{"ask"=>{"name"=>"foo", "say"=>[{"value"=>"Please say your account number"}], "bargein"=>"true", "timeout"=>30, "require"=>"true", "on"=>[{"event"=>"success", "next"=>"/result.json"}], "choices"=>{"value"=>"[5 DIGITS]"}}}]} 37 | # end 38 | 39 | it "should generate an error if an 'ask' is passed without a 'name' parameter" do 40 | begin 41 | response = Tropo::Generator.ask({ :foo => 'bar' }) 42 | rescue => err 43 | err.to_s.should == "A 'name' must be provided to a 'ask' action" 44 | end 45 | end 46 | 47 | # Prompt 48 | it "should generate a complete 'prompt' JSON document" do 49 | response = Tropo::Generator.prompt({ :name => 'foo', 50 | :bargein => 'true', 51 | :timeout => 30, 52 | :require => 'true' }) 53 | JSON.parse(response).should == { "tropo" => [{ "ask" => { "name" => "foo", "bargein" => "true", "timeout" => 30, "require" => "true" } }] } 54 | end 55 | 56 | it "should generate an 'prompt' JSON document when a block is passed" do 57 | response = Tropo::Generator.prompt({ :name => 'foo', 58 | :bargein => 'true', 59 | :timeout => 30, 60 | :require => 'true' }) do 61 | say :value => 'Please say your account number' 62 | choices :value => '[5 DIGITS]' 63 | end 64 | JSON.parse(response).should == {"tropo"=>[{"ask"=>{"name"=>"foo", "say"=>[{"value"=>"Please say your account number"}], "bargein"=>"true", "timeout"=>30, "require"=>"true", "choices"=>{"value"=>"[5 DIGITS]"}}}]} 65 | end 66 | 67 | it "should generate an error if an 'prompt' is passed without a 'name' parameter" do 68 | begin 69 | response = Tropo::Generator.prompt({ :foo => 'bar' }) 70 | rescue => err 71 | err.to_s.should == "A 'name' must be provided to a 'ask' action" 72 | end 73 | end 74 | 75 | # Choices tests 76 | it "should generate a standard 'choices' JSON document" do 77 | response = Tropo::Generator.choices({ :value => '[5 DIGITS]' }) 78 | JSON.parse(response).should == { 'tropo' => [{ 'choices' => { 'value' => '[5 DIGITS]' } }] } 79 | end 80 | 81 | it "should raise an error if a 'choices' passes an unspported mode" do 82 | begin 83 | response = Tropo::Generator.choices({ :value => '[5 DIGITS]', :mode => 'frootloops' }) 84 | rescue => err 85 | err.to_s.should == "If mode is provided, only 'dtmf', 'speech' or 'any' is supported" 86 | end 87 | end 88 | 89 | it "should generate a standard 'choices' JSON document with a mode" do 90 | response = Tropo::Generator.choices({ :value => '[5 DIGITS]', :mode => 'dtmf' }) 91 | JSON.parse(response).should == { 'tropo' => [{ 'choices' => { 'value' => '[5 DIGITS]', 'mode' => 'dtmf' } }] } 92 | end 93 | 94 | # Conference action tests 95 | it "should generate a complete 'conference' JSON document" do 96 | response = Tropo::Generator.conference({ :name => 'foo', 97 | :id => '1234', 98 | :mute => false, 99 | :send_tones => false, 100 | :exit_tone => '#' }) 101 | JSON.parse(response).should == {"tropo"=>[{"conference"=>{"name"=>"foo", "mute"=>false, "sendTones"=>false, "id"=>"1234", "exitTone"=>"#"}}]} 102 | end 103 | 104 | it "should generate a complete 'conference' JSON document when a block is passed" do 105 | response = Tropo::Generator.conference({ :name => 'foo', 106 | :id => '1234', 107 | :mute => false, 108 | :send_tones => false, 109 | :exit_tone => '#' }) do 110 | on(:event => 'join') { say :value => 'Welcome to the conference' } 111 | on(:event => 'leave') { say :value => 'Someone has left the conference' } 112 | end 113 | JSON.parse(response).should == {"tropo"=>[{"conference"=>{"name"=>"foo", "mute"=>false, "id"=>"1234", "exitTone"=>"#", "sendTones"=>false, "on"=>[{"say"=>[{"value"=>"Welcome to the conference"}], "event"=>"join"}, {"say"=>[{"value"=>"Someone has left the conference"}], "event"=>"leave"}]}}]} 114 | end 115 | 116 | it "should generate an error if an 'conference' is passed without a 'name' parameter" do 117 | begin 118 | response = Tropo::Generator.conference({ :foo => 'bar' }) 119 | rescue => err 120 | err.to_s.should == "A 'name' must be provided to a 'conference' action" 121 | end 122 | end 123 | 124 | it "should generate an error if an 'conference' is passed without an 'id' parameter" do 125 | begin 126 | response = Tropo::Generator.conference({ :name => 'bar' }) 127 | rescue => err 128 | err.to_s.should == "A 'id' must be provided to a 'conference' action" 129 | end 130 | end 131 | 132 | # Hangup action tests and Disconnect alias 133 | it "should generate a JSON document with a 'hangup' action" do 134 | response = Tropo::Generator.hangup 135 | JSON.parse(response).should == {"tropo"=>[{"hangup"=>nil}]} 136 | end 137 | 138 | it "should generate a JSON document with a 'disconnect' action" do 139 | response = Tropo::Generator.disconnect 140 | JSON.parse(response).should == {"tropo"=>[{"hangup"=>nil}]} 141 | end 142 | 143 | it "should generate a standard 'on' JSON document" do 144 | response = Tropo::Generator.on({ :event => 'hangup', :next => 'myresource' }) 145 | JSON.parse(response).should == { "tropo" => [{ "on" =>{ "event" => "hangup", "next" => "myresource" } }] } 146 | end 147 | 148 | # On tests 149 | it "should generate a an error of an 'on' document does not pass an event param" do 150 | begin 151 | response = Tropo::Generator.on({ :foo => 'bar' }) 152 | rescue => err 153 | err.to_s.should == "A 'event' must be provided to a 'on' action" 154 | end 155 | end 156 | 157 | it "should generate a an error of an 'on' document does not pass an event param" do 158 | begin 159 | response = Tropo::Generator.on({ :event => 'bar' }) 160 | rescue => err 161 | err.to_s.should == "A 'next' resource must be provided" 162 | end 163 | end 164 | 165 | # Record action tests 166 | it "should generate a complete 'record' JSON document" do 167 | response = Tropo::Generator.record({ :name => 'foo', 168 | :url => 'http://sendme.com/tropo', 169 | :beep => true, 170 | :send_tones => false, 171 | :exit_tone => '#' }) 172 | JSON.parse(response).should == {"tropo"=>[{"record"=>{"name"=>"foo", "beep"=>true, "url"=>"http://sendme.com/tropo", "exitTone"=>"#", "sendTones"=>false}}]} 173 | end 174 | 175 | it "should generate a complete 'record' JSON document when a block is passed" do 176 | response = Tropo::Generator.record({ :name => 'foo', 177 | :url => 'http://sendme.com/tropo', 178 | :beep => true, 179 | :send_tones => false, 180 | :exit_tone => '#' }) do 181 | say :value => 'Please say your account number' 182 | choices :value => '[5 DIGITS]' 183 | end 184 | JSON.parse(response).should == {"tropo"=>[{"record"=>{"name"=>"foo", "say"=>[{"value"=>"Please say your account number"}], "beep"=>true, "url"=>"http://sendme.com/tropo", "sendTones"=>false, "exitTone"=>"#", "choices"=>{"value"=>"[5 DIGITS]"}}}]} 185 | end 186 | 187 | it "should generate an error if an 'record' is passed without a 'name' parameter" do 188 | begin 189 | response = Tropo::Generator.record({ :foo => 'bar' }) 190 | rescue => err 191 | err.to_s.should == "A 'name' must be provided to a 'record' action" 192 | end 193 | end 194 | 195 | it "should generate an error if an 'record' is passed without an 'url' parameter" do 196 | begin 197 | response = Tropo::Generator.record({ :name => 'bar' }) 198 | rescue => err 199 | err.to_s.should == "A 'url' must be provided to a 'record' action" 200 | end 201 | end 202 | 203 | it "should generate an error if an 'record' is passed without an invalid 'url' parameter" do 204 | begin 205 | response = Tropo::Generator.record({ :name => 'bar', 206 | :url => 'foobar' }) 207 | rescue => err 208 | err.to_s.should == "The 'url' paramater must be a valid URL" 209 | end 210 | end 211 | 212 | it "should accept a valid email address when a 'record' action is called" do 213 | response = Tropo::Generator.record({ :name => 'bar', 214 | :url => 'foo@bar.com' }) 215 | JSON.parse(response).should == JSON.parse("{\"tropo\":[{\"record\":{\"url\":\"foo@bar.com\",\"name\":\"bar\"}}]}") 216 | end 217 | 218 | # Redirect action tests 219 | it "should generate a JSON document with a 'redirect' action" do 220 | response = Tropo::Generator.redirect({ :to => 'sip:1234', :from => '4155551212' }) 221 | JSON.parse(response).should == {"tropo"=>[{"redirect"=>{"from"=>"4155551212", "to"=>"sip:1234"}}]} 222 | end 223 | 224 | it "should generate an error if a 'redirect' action is included in a block" do 225 | begin 226 | response = Tropo::Generator.conference(:name => 'foobar', :id => 1234) do 227 | redirect(:to => 'sip:1234', :from => '4155551212') 228 | end 229 | rescue => err 230 | err.to_s.should == 'Redirect should only be used alone and before the session is answered, use transfer instead' 231 | end 232 | end 233 | 234 | it "should generate an error when no 'to' is passed to a 'redirect' action" do 235 | begin 236 | response = Tropo::Generator.redirect 237 | rescue => err 238 | err.to_s.should == "A 'to' must be provided to a 'redirect' action" 239 | end 240 | end 241 | 242 | # Reject action tests 243 | it "should generate a JSON document with a 'reject' action" do 244 | response = Tropo::Generator.reject 245 | JSON.parse(response).should == {"tropo"=>[{"reject"=>nil}]} 246 | end 247 | 248 | # Say action tests 249 | it "should generate a standard 'say' JSON document when a stiring is passed" do 250 | JSON.parse(Tropo::Generator.say('1234')).should == { "tropo" => [{ "say" => [{ "value" => "1234" }] }] } 251 | end 252 | 253 | it "should generate an error if I try to pass an integer to a 'say' action" do 254 | begin 255 | Tropo::Generator.say(1234) 256 | rescue => err 257 | err.to_s.should == "An invalid paramater type Fixnum has been passed" 258 | end 259 | end 260 | 261 | it "should generate a standard 'say' JSON document" do 262 | JSON.parse(Tropo::Generator.say({ :value => '1234' })).should == { "tropo" => [{ "say" => [{ "value" => "1234" }] }] } 263 | end 264 | 265 | it "should generate a 'say' JSON document when an array of values is passed" do 266 | response = Tropo::Generator.say([{ :value => '1234' }, { :value => 'abcd', :event => 'nomatch:1' }]) 267 | JSON.parse(response).should == { "tropo" => [{ "say" => [{ "value" => "1234" }, { "value" => "abcd", "event"=>"nomatch:1" }] }] } 268 | end 269 | 270 | it "should generate an error if no 'value' key is passed to a 'say' request" do 271 | begin 272 | response = Tropo::Generator.say({ :name => 'foo' }) 273 | rescue => err 274 | err.to_s.should == "A 'value' must be provided to a 'say' action" 275 | end 276 | end 277 | 278 | it "should generate a JSON document with a 'say' and an 'on'" do 279 | result = Tropo::Generator.new do 280 | say :value => 'blah' 281 | on :event => 'error', :next => 'error.json' 282 | end 283 | JSON.parse(result.response).should == {"tropo"=>[{"say"=>[{"value"=>"blah"}]}, {"on"=>{"event"=>"error", "next"=>"error.json"}}]} 284 | end 285 | 286 | # Start & Stop Recording actions tests 287 | it "should generate a JSON document with a 'start_recording' action" do 288 | response = Tropo::Generator.start_recording(:url => 'http://postrecording.com/tropo') 289 | JSON.parse(response).should == {"tropo"=>[{"startRecording"=>{"url"=>"http://postrecording.com/tropo"}}]} 290 | end 291 | 292 | it "should generate a JSON document with a 'start_call_recording' action" do 293 | response = Tropo::Generator.start_call_recording(:url => 'http://postrecording.com/tropo') 294 | JSON.parse(response).should == {"tropo"=>[{"startRecording"=>{"url"=>"http://postrecording.com/tropo"}}]} 295 | end 296 | 297 | it "should generate a JSON document with a 'stopRecording' action" do 298 | response = Tropo::Generator.stop_call_recording 299 | JSON.parse(response).should == {"tropo"=>[{"stopRecording"=>nil}]} 300 | end 301 | 302 | it "should generate a JSON document with a 'stoprecording' action" do 303 | response = Tropo::Generator.stop_recording 304 | JSON.parse(response).should == {"tropo"=>[{"stopRecording"=>nil}]} 305 | end 306 | 307 | # Transfer action tests 308 | it "should generate a JSON document with a 'transfer' action" do 309 | response = Tropo::Generator.transfer(:to => 'tel:+14157044517') 310 | JSON.parse(response).should == {"tropo"=>[{"transfer"=>{"to"=>"tel:+14157044517"}}]} 311 | end 312 | 313 | # Transfer action tests 314 | it "should generate a JSON document with a 'transfer' action with an 'on' and 'choices' actions" do 315 | response = Tropo::Generator.transfer(:to => 'tel:+14157044517') do 316 | on :event => 'unbounded', :next => '/error.json' 317 | choices :value => '[5 DIGITS]' 318 | end 319 | JSON.parse(response).should == {"tropo"=>[{"transfer"=>{"to"=>"tel:+14157044517", "choices"=>{"value"=>"[5 DIGITS]"}, "on"=>[{"event"=>"unbounded", "next"=>"/error.json"}]}}]} 320 | end 321 | 322 | it "should generate an error if no 'to' key is passed to a 'transfer' request" do 323 | begin 324 | response = Tropo::Generator.transfer 325 | rescue => err 326 | err.to_s.should == "A 'to' must be provided to a 'transfer' action" 327 | end 328 | end 329 | 330 | # General tests 331 | it "should generate a JSON document when a block is passed" do 332 | result = Tropo::Generator.new do 333 | say [{ :value => '1234' }, { :value => 'abcd', :event => "nomatch:1" }] 334 | say [{ :value => '0987' }, { :value => 'zyxw', :event => "nomatch:2" }] 335 | end 336 | JSON.parse(result.response).should == {"tropo"=>[{"say"=>[{"value"=>"1234"}, {"value"=>"abcd", "event"=>"nomatch:1"}]}, {"say"=>[{"value"=>"0987"}, {"value"=>"zyxw", "event"=>"nomatch:2"}]}]} 337 | end 338 | 339 | it "should build a Ruby hash object when a session arrives in JSON" do 340 | json_session = "{\"session\":{\"id\":\"dih06n\",\"accountId\":\"33932\",\"timestamp\":\"2010-01-19T23:18:48.562Z\",\"userType\":\"HUMAN\",\"to\":{\"id\":\"tropomessaging@bot.im\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"},\"from\":{\"id\":\"john_doe@gmail.com\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"}}}" 341 | hash = Tropo::Generator.parse(json_session) 342 | hash[:session][:timestamp] == Time.parse('2010-01-19T18:27:46.852-05:00') 343 | end 344 | 345 | it "should build a Ruby hash object when a result arrives in JSON" do 346 | json_result = "{\"result\":{\"sessionId\":\"sessionId\",\"callState\":\"ANSWERED\",\"sessionDuration\":10,\"sequence\":1,\"complete\":true,\"error\":\"error\",\"properties\":[{\"key\":\"foo\",\"value\":\"bar\"},{\"key\":\"charlie\",\"value\":\"foxtrot\"}],\"actions\":{\"name\":\"pin\",\"attempts\":1,\"disposition\":\"SUCCESS\",\"confidence\":100,\"interpretation\":\"12345\",\"utterance\":\"1 2 3 4 5\"}}}" 347 | Tropo::Generator.parse(json_result).should == Hashie::Mash.new({:result=>{:session_id=>"sessionId", :properties=>{:foo=>{:value=>"bar"}, :charlie=>{:value=>"foxtrot"}}, :complete=>true, :call_state=>"ANSWERED", :actions=>{:pin=>{:disposition=>"SUCCESS", :utterance=>"1 2 3 4 5", :attempts=>1, :interpretation=>"12345", :confidence=>100}}, :session_duration=>10, :error=>"error", :sequence=>1}}) 348 | end 349 | 350 | it "should build a ruby hash object when a realworld JSON string arrives" do 351 | json_result = "{\"result\":{\"sessionId\":\"CCFD9C86-1DD1-11B2-B76D-B9B253E4B7FB@161.253.55.20\",\"callState\":\"ANSWERED\",\"sessionDuration\":2,\"sequence\":1,\"complete\":true,\"error\":null,\"actions\":[{\"name\":\"zip\",\"attempts\":1,\"disposition\":\"SUCCESS\",\"confidence\":100,\"interpretation\":\"12345\",\"utterance\":\"1 2 3 4 5\"},{\"name\":\"days\",\"attempts\":1,\"disposition\":\"SUCCESS\",\"confidence\":100,\"interpretation\":\"1\",\"utterance\":\"1\"}]}}" 352 | Tropo::Generator.parse(json_result).should == Hashie::Mash.new({:result=>{:call_state=>"ANSWERED", :complete=>true, :actions=>{:zip=>{:disposition=>"SUCCESS", :utterance=>"1 2 3 4 5", :attempts=>1, :interpretation=>"12345", :confidence=>100}, :days=>{:disposition=>"SUCCESS", :utterance=>"1", :attempts=>1, :interpretation=>"1", :confidence=>100}}, :session_duration=>2, :sequence=>1, :session_id=>"CCFD9C86-1DD1-11B2-B76D-B9B253E4B7FB@161.253.55.20", :error=>nil}}) 353 | end 354 | 355 | it "should see an object delcared outside of a block" do 356 | @@session = 'foobar' 357 | result = Tropo::Generator.new do 358 | @@new_session = @@session 359 | say :value => 'blah' 360 | on :event => 'error', :next => 'error.json' 361 | end 362 | @@new_session.should == 'foobar' 363 | end 364 | 365 | it "should see an object passed into the block" do 366 | session = 'foobar' 367 | result = Tropo::Generator.new(session) do 368 | session.should == 'foobar' 369 | say :value => 'blah' 370 | on :event => 'error', :next => 'error.json' 371 | end 372 | end 373 | 374 | it "should allow you to create a Tropo::Generator object and build up a JSON request with two says" do 375 | tropo = Tropo::Generator.new 376 | tropo.say('foo') 377 | tropo.say('bar') 378 | tropo.response.should == "{\"tropo\":[{\"say\":[{\"value\":\"foo\"}]},{\"say\":[{\"value\":\"bar\"}]}]}" 379 | end 380 | 381 | it "should allow you to create a Tropo::Generator object and build up a JSON request with: a say, an on and a record" do 382 | tropo = Tropo::Generator.new 383 | tropo.say 'Welcome to the app' 384 | tropo.on :event => 'hangup', :next => '/hangup.json' 385 | tropo.record({ :name => 'foo', 386 | :url => 'http://sendme.com/tropo', 387 | :beep => true, 388 | :send_tones => false, 389 | :exit_tone => '#' }) do 390 | say :value => 'Please say your account number' 391 | choices :value => '[5 DIGITS]' 392 | end 393 | JSON.parse(tropo.response).should == {"tropo"=>[{"say"=>[{"value"=>"Welcome to the app"}]}, {"on"=>{"event"=>"hangup", "next"=>"/hangup.json"}}, {"record"=>{"name"=>"foo", "say"=>[{"value"=>"Please say your account number"}], "beep"=>true, "url"=>"http://sendme.com/tropo", "sendTones"=>false, "exitTone"=>"#", "choices"=>{"value"=>"[5 DIGITS]"}}}]} 394 | end 395 | 396 | it "should allow you to reset the object to a fresh response after building a response first" do 397 | tropo = Tropo::Generator.new 398 | tropo.say 'Welcome to the app' 399 | tropo.on :event => 'hangup', :next => '/hangup.json' 400 | tropo.record({ :name => 'foo', 401 | :url => 'http://sendme.com/tropo', 402 | :beep => true, 403 | :send_tones => false, 404 | :exit_tone => '#' }) do 405 | say :value => 'Please say your account number' 406 | choices :value => '[5 DIGITS]' 407 | end 408 | JSON.parse(tropo.response).should == {"tropo"=>[{"say"=>[{"value"=>"Welcome to the app"}]}, {"on"=>{"event"=>"hangup", "next"=>"/hangup.json"}}, {"record"=>{"name"=>"foo", "say"=>[{"value"=>"Please say your account number"}], "beep"=>true, "url"=>"http://sendme.com/tropo", "sendTones"=>false, "exitTone"=>"#", "choices"=>{"value"=>"[5 DIGITS]"}}}]} 409 | tropo.reset 410 | tropo.response.should == "{\"tropo\":[]}" 411 | end 412 | 413 | it "should build a Ruby hash object when a session arrives in JSON with a proper Ruby Time object" do 414 | json_session = "{\"session\":{\"id\":\"dih06n\",\"accountId\":\"33932\",\"timestamp\":\"2010-01-19T23:18:48.562Z\",\"userType\":\"HUMAN\",\"to\":{\"id\":\"tropomessaging@bot.im\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"},\"from\":{\"id\":\"john_doe@gmail.com\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"}}}" 415 | hash = Tropo::Generator.parse(json_session) 416 | hash[:session][:timestamp].should == Time.parse("2010-01-19T23:18:48.562Z") 417 | end 418 | 419 | it "should build a Ruby hash object when a result arrives in JSON with one action returned in an array" do 420 | json_result = "{\"result\":{\"sessionId\":\"CCFD9C86-1DD1-11B2-B76D-B9B253E4B7FB@161.253.55.20\",\"callState\":\"ANSWERED\",\"sessionDuration\":2,\"sequence\":1,\"complete\":true,\"error\":null,\"actions\":{\"name\":\"zip\",\"attempts\":1,\"disposition\":\"SUCCESS\",\"confidence\":100,\"interpretation\":\"12345\",\"utterance\":\"1 2 3 4 5\"}}}" 421 | hash = Tropo::Generator.parse(json_result) 422 | hash.should == Hashie::Mash.new({:result=>{:call_state=>"ANSWERED", :complete=>true, :actions=>{:zip=>{:utterance=>"1 2 3 4 5", :attempts=>1, :interpretation=>"12345", :confidence=>100, :disposition=>"SUCCESS"}}, :session_id=>"CCFD9C86-1DD1-11B2-B76D-B9B253E4B7FB@161.253.55.20", :session_duration=>2, :error=>nil, :sequence=>1}}) 423 | end 424 | 425 | it "should build a Hashie object when a result arrives in JSON" do 426 | json_result = "{\"result\":{\"sessionId\":\"CCFD9C86-1DD1-11B2-B76D-B9B253E4B7FB@161.253.55.20\",\"callState\":\"ANSWERED\",\"sessionDuration\":2,\"sequence\":1,\"complete\":true,\"error\":null,\"actions\":{\"name\":\"zip\",\"attempts\":1,\"disposition\":\"SUCCESS\",\"confidence\":100,\"interpretation\":\"12345\",\"utterance\":\"1 2 3 4 5\"}}}" 427 | hash = Tropo::Generator.parse(json_result) 428 | hash.result.call_state.should == 'ANSWERED' 429 | hash[:result][:call_state].should == 'ANSWERED' 430 | hash['result']['call_state'].should == 'ANSWERED' 431 | end 432 | 433 | it "should generate valid JSON when a startRecording is used" do 434 | t = Tropo::Generator.new 435 | t.on :event => 'error', :next => '/error.json' # For fatal programming errors. Log some details so we can fix it 436 | t.on :event => 'hangup', :next => '/hangup.json' # When a user hangs or call is done. We will want to log some details. 437 | t.on :event => 'continue', :next => '/next.json' 438 | t.say "Hello" 439 | t.start_recording(:url => "http://heroku-voip.marksilver.net/post_audio_to_s3?filename=foo.wav&unique_id=bar") 440 | # [From this point, until stop_recording(), we will record what the caller *and* the IVR say] 441 | t.say "You are now on the record." 442 | # Prompt the user to incriminate themselve on-the-record 443 | t.say "Go ahead, sing-along." 444 | t.say "http://denalidomain.com/music/keepers/HappyHappyBirthdaytoYou-Disney.mp3" 445 | JSON.parse(t.response).should == {"tropo"=>[{"on"=>{"event"=>"error", "next"=>"/error.json"}}, {"on"=>{"event"=>"hangup", "next"=>"/hangup.json"}}, {"on"=>{"event"=>"continue", "next"=>"/next.json"}}, {"say"=>[{"value"=>"Hello"}]}, {"startRecording"=>{"url"=>"http://heroku-voip.marksilver.net/post_audio_to_s3?filename=foo.wav&unique_id=bar"}}, {"say"=>[{"value"=>"You are now on the record."}]}, {"say"=>[{"value"=>"Go ahead, sing-along."}]}, {"say"=>[{"value"=>"http://denalidomain.com/music/keepers/HappyHappyBirthdaytoYou-Disney.mp3"}]}]} 446 | end 447 | 448 | it "should generate a voice_session true if a JSON session is received that is a channel of 'VOICE'" do 449 | tropo = Tropo::Generator.new 450 | tropo.parse "{\"session\":{\"id\":\"0-13c4-4b563da3-7aecefda-46af-1d10bdd0\",\"accountId\":\"33932\",\"timestamp\":\"2010-01-19T23:18:00.854Z\",\"userType\":\"HUMAN\",\"to\":{\"id\":\"9991427589\",\"name\":\"unknown\",\"channel\":\"VOICE\",\"network\":\"PSTN\"},\"from\":{\"id\":\"jsgoecke\",\"name\":\"unknown\",\"channel\":\"VOICE\",\"network\":\"PSTN\"}}}" 451 | tropo.voice_session.should == true 452 | tropo.text_session.should == false 453 | end 454 | 455 | it "should generate a text_session true if a JSON session is received that is a channel of 'TEXT'" do 456 | tropo = Tropo::Generator.new 457 | tropo.parse "{\"session\":{\"id\":\"dih06n\",\"accountId\":\"33932\",\"timestamp\":\"2010-01-19T23:18:48.562Z\",\"userType\":\"HUMAN\",\"to\":{\"id\":\"tropomessaging@bot.im\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"},\"from\":{\"id\":\"john_doe@gmail.com\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"}}}" 458 | tropo.voice_session.should == false 459 | tropo.text_session.should == true 460 | end 461 | 462 | it "should generate a valid JSON string for a call method" do 463 | json_result = "{\"tropo\":[{\"call\":{\"recording\":{\"password\":\"passwd\",\"username\":\"jose\",\"method\":\"POST\",\"url\":\"http://foobar\",\"format\":\"audio/mp3\"},\"timeout\":10,\"network\":\"SMS\",\"channel\":\"TEXT\",\"to\":\"foo\",\"from\":\"bar\",\"headers\":{\"foo\":\"foo\",\"bar\":\"bar\"},\"answerOnMedia\":false}}]}" 464 | tropo = Tropo::Generator.call({ :to => 'foo', 465 | :from => 'bar', 466 | :network => 'SMS', 467 | :channel => 'TEXT', 468 | :timeout => 10, 469 | :answer_on_media => false, 470 | :headers => { :foo => 'foo', :bar => 'bar' }, 471 | :recording => { :url => 'http://foobar', 472 | :method => 'POST', 473 | :format => 'audio/mp3', 474 | :username => 'jose', 475 | :password => 'passwd' } }) 476 | JSON.parse(tropo).should == JSON.parse(json_result) 477 | end 478 | 479 | it "should generate a valid JSON string for a message method" do 480 | hash_result = {"tropo"=>[{"message"=>{"say"=>[{"value"=>"Please say your account number"}], "from"=>"bar", "timeout"=>10, "to"=>"foo", "network"=>"SMS", "answerOnMedia"=>false, "channel"=>"TEXT", "recording"=>{"format"=>"audio/mp3", "method"=>"POST", "url"=>"http://foobar", "username"=>"jose", "password"=>"passwd"}, "headers"=>{"foo"=>"foo", "bar"=>"bar"}}}]} 481 | tropo = Tropo::Generator.message({ :to => 'foo', 482 | :from => 'bar', 483 | :network => 'SMS', 484 | :channel => 'TEXT', 485 | :timeout => 10, 486 | :answer_on_media => false, 487 | :headers => { :foo => 'foo', :bar => 'bar' }, 488 | :recording => { :url => 'http://foobar', 489 | :method => 'POST', 490 | :format => 'audio/mp3', 491 | :username => 'jose', 492 | :password => 'passwd' } }) do 493 | say :value => 'Please say your account number' 494 | end 495 | JSON.parse(tropo).should == hash_result 496 | end 497 | 498 | it "should generate a valid JSON string for a record method with a transcription request" do 499 | hash_result = {"tropo"=>[{"record"=>{"name"=>"foo", "transcription"=>{"email_format"=>"encoded", "url"=>"mailto:jose@voxeo.com", "id"=>"bling"}, "say"=>[{"value"=>"Please say your account number"}], "beep"=>true, "url"=>"http://sendme.com/tropo", "exitTone"=>"#", "sendTones"=>false, "choices"=>{"value"=>"[5 DIGITS]"}}}]} 500 | tropo = Tropo::Generator.record({ :name => 'foo', 501 | :url => 'http://sendme.com/tropo', 502 | :beep => true, 503 | :send_tones => false, 504 | :transcription => { :id => 'bling', 505 | :url => 'mailto:jose@voxeo.com', 506 | :email_format => 'encoded' }, 507 | :exit_tone => '#' }) do 508 | say :value => 'Please say your account number' 509 | choices :value => '[5 DIGITS]' 510 | end 511 | JSON.parse(tropo).should == hash_result 512 | end 513 | 514 | it "should properly generate a JSON document when calling an ask with says as hash elements rather than as methods" do 515 | hash_result = {"tropo"=>[{"ask"=>{"name"=>"donate_to_id", "say"=>[{"event"=>"timeout", "value"=>"Sorry, I did not hear anything."}, {"event"=>"nomatch:1 nomatch:2 nomatch:3", "value"=>"Sorry, that wasn't a valid answer. You can press or say 1 for 'yes', or 2 for 'no'."}, {"value"=>"You chose organization foobar. Are you ready to donate to them? If you say no, I will tell you a little more about the organization."}, {"event"=>"nomatch:3", "value"=>"This is your last attempt."}], "bargein"=>true, "silenceTimeout"=>10, "timeout"=>10, "attempts"=>4, "choices"=>{"value"=>"true(1,yes,sure,affirmative), false(2,no,no thank you,negative), 0(0,help,i do not know, agent, operator, assistance, representative, real person, human), 9(9,quit,stop,shut up)"}}}]} 516 | help_stop_choices = "0(0,help,i do not know, agent, operator, assistance, representative, real person, human), 9(9,quit,stop,shut up)" 517 | yes_no_choices = "true(1,yes,sure,affirmative), false(2,no,no thank you,negative), " + help_stop_choices 518 | 519 | t = Tropo::Generator.new 520 | t.ask :name => 'donate_to_id', 521 | :bargein => true, 522 | :timeout => 10, 523 | :silence_timeout => 10, 524 | :attempts => 4, 525 | :say => [{:event => "timeout", :value => "Sorry, I did not hear anything."}, 526 | {:event => "nomatch:1 nomatch:2 nomatch:3", :value => "Sorry, that wasn't a valid answer. You can press or say 1 for 'yes', or 2 for 'no'."}, 527 | {:value => "You chose organization foobar. Are you ready to donate to them? If you say no, I will tell you a little more about the organization."}, 528 | {:event => "nomatch:3", :value => "This is your last attempt."}], 529 | :choices => { :value => yes_no_choices} 530 | JSON.parse(t.response).should == hash_result 531 | end 532 | 533 | it "should set the voice variable when called" do 534 | t = Tropo::Generator.new 535 | t.voice.should == nil 536 | 537 | t = Tropo::Generator.new(:voice => 'barnie') 538 | t.voice.should == 'barnie' 539 | 540 | t = Tropo::Generator.new 541 | t.voice = 'barnie' 542 | t.voice.should == 'barnie' 543 | end 544 | 545 | it "should handle the setting of the voice parameter based on defaults" do 546 | t = Tropo::Generator.new 547 | t.say 'Hi there!' 548 | JSON.parse(t.response)['tropo'][0]['say'][0]['voice'].should == nil 549 | 550 | t = Tropo::Generator.new 551 | t.say 'Hi there!', :voice => 'barnie' 552 | JSON.parse(t.response)['tropo'][0]['say'][0]['voice'].should == 'barnie' 553 | 554 | t = Tropo::Generator.new(:voice => 'barnie') 555 | t.say 'Hi there!' 556 | t.say 'Wow!' 557 | JSON.parse(t.response)['tropo'][0]['say'][0]['voice'].should == 'barnie' 558 | JSON.parse(t.response)['tropo'][1]['say'][0]['voice'].should == 'barnie' 559 | 560 | t = Tropo::Generator.new(:voice => 'barnie') 561 | t.say 'Hi there!' 562 | t.say 'Wow!', :voice => 'jack' 563 | JSON.parse(t.response)['tropo'][0]['say'][0]['voice'].should == 'barnie' 564 | JSON.parse(t.response)['tropo'][1]['say'][0]['voice'].should == 'jack' 565 | 566 | t = Tropo::Generator.new 567 | t.voice = 'barnie' 568 | t.say 'Hi there!' 569 | t.say 'Wow!', :voice => 'jack' 570 | JSON.parse(t.response)['tropo'][0]['say'][0]['voice'].should == 'barnie' 571 | JSON.parse(t.response)['tropo'][1]['say'][0]['voice'].should == 'jack' 572 | end 573 | 574 | it "should set the recognizer variable when called" do 575 | t = Tropo::Generator.new 576 | t.recognizer.should == nil 577 | 578 | t = Tropo::Generator.new(:recognizer => 'fr-fr') 579 | t.recognizer.should == 'fr-fr' 580 | 581 | t = Tropo::Generator.new 582 | t.recognizer = 'fr-fr' 583 | t.recognizer.should == 'fr-fr' 584 | end 585 | 586 | it "should handle the setting of the recognizer parameter based on defaults" do 587 | t = Tropo::Generator.new 588 | t.ask({ :name => 'foo', 589 | :bargein => 'true', 590 | :timeout => 30, 591 | :require => 'true' }) 592 | JSON.parse(t.response)['tropo'][0]['ask']['recognizer'].should == nil 593 | 594 | t = Tropo::Generator.new(:recognizer => 'fr-fr') 595 | t.ask({ :name => 'foo', 596 | :bargein => 'true', 597 | :timeout => 30, 598 | :require => 'true' }) 599 | JSON.parse(t.response)['tropo'][0]['ask']['recognizer'].should == 'fr-fr' 600 | 601 | t = Tropo::Generator.new 602 | t.recognizer = 'fr-fr' 603 | t.ask({ :name => 'foo', 604 | :bargein => 'true', 605 | :timeout => 30, 606 | :require => 'true' }) 607 | JSON.parse(t.response)['tropo'][0]['ask']['recognizer'].should == 'fr-fr' 608 | 609 | t = Tropo::Generator.new 610 | t.recognizer = 'fr-fr' 611 | t.ask({ :name => 'foo', 612 | :bargein => 'true', 613 | :timeout => 30, 614 | :require => 'true' }) 615 | t.ask({ :name => 'foo', 616 | :bargein => 'true', 617 | :timeout => 30, 618 | :require => 'true', 619 | :recognizer => 'de-de' }) 620 | JSON.parse(t.response)['tropo'][0]['ask']['recognizer'].should == 'fr-fr' 621 | JSON.parse(t.response)['tropo'][1]['ask']['recognizer'].should == 'de-de' 622 | end 623 | 624 | it "should parse a JSON string or a Ruby Hash the same" do 625 | json_session = "{\"session\":{\"id\":\"dih06n\",\"accountId\":\"33932\",\"timestamp\":\"2010-01-19T23:18:48.562Z\",\"userType\":\"HUMAN\",\"to\":{\"id\":\"tropomessaging@bot.im\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"},\"from\":{\"id\":\"john_doe@gmail.com\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"}}}" 626 | tropo = Tropo::Generator.parse json_session 627 | tropo.session.user_type.should == 'HUMAN' 628 | 629 | tropo = Tropo::Generator.parse(JSON.parse(json_session)) 630 | tropo.session.user_type.should == 'HUMAN' 631 | end 632 | 633 | it "should correctly parse a Ruby Hash with an instantiated Ruby Time object as timestamp" do 634 | json_session = "{\"session\":{\"id\":\"dih06n\",\"accountId\":\"33932\",\"timestamp\":\"2010-01-19T23:18:48.562Z\",\"userType\":\"HUMAN\",\"to\":{\"id\":\"tropomessaging@bot.im\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"},\"from\":{\"id\":\"john_doe@gmail.com\",\"name\":\"unknown\",\"channel\":\"TEXT\",\"network\":\"JABBER\"}}}" 635 | tropo = JSON.parse(json_session) 636 | tropo['session']['timestamp'] = Time.parse("2010-01-19T23:18:48.562Z") 637 | 638 | hash = Tropo::Generator.parse(tropo) 639 | hash[:session][:timestamp].should == Time.parse("2010-01-19T23:18:48.562Z") 640 | end 641 | 642 | it "should return a hash of the response object" do 643 | result = Tropo::Generator.new do 644 | say [{ :value => '1234' }, { :value => 'abcd', :event => "nomatch:1" }] 645 | say [{ :value => '0987' }, { :value => 'zyxw', :event => "nomatch:2" }] 646 | end 647 | result.to_hash.should == { :tropo => [{ :say => [{ :value => "1234" }, 648 | { :event => "nomatch:1", :value => "abcd" }] }, 649 | { :say => [{ :value => "0987" }, 650 | { :event => "nomatch:2", :value => "zyxw" }] }] } 651 | end 652 | 653 | it "should not require a name in an ask" do 654 | t = Tropo::Generator.new 655 | t.ask({ :bargein => 'true', 656 | :timeout => 30, 657 | :require => 'true' }) 658 | JSON.parse(t.response)['tropo'][0]['ask']['timeout'].should == 30 659 | end 660 | end 661 | --------------------------------------------------------------------------------