mtrudel-adhearsion 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/CHANGELOG +26 -0
  2. data/EVENTS +11 -0
  3. data/LICENSE +456 -0
  4. data/Rakefile +127 -0
  5. data/adhearsion.gemspec +149 -0
  6. data/app_generators/ahn/USAGE +5 -0
  7. data/app_generators/ahn/ahn_generator.rb +91 -0
  8. data/app_generators/ahn/templates/.ahnrc +34 -0
  9. data/app_generators/ahn/templates/README +8 -0
  10. data/app_generators/ahn/templates/Rakefile +25 -0
  11. data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
  12. data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
  13. data/app_generators/ahn/templates/components/disabled/restful_rpc/README.markdown +11 -0
  14. data/app_generators/ahn/templates/components/disabled/restful_rpc/example-client.rb +48 -0
  15. data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb +87 -0
  16. data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.yml +34 -0
  17. data/app_generators/ahn/templates/components/disabled/restful_rpc/spec/restful_rpc_spec.rb +263 -0
  18. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +104 -0
  19. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.yml +2 -0
  20. data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
  21. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
  22. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.yml +12 -0
  23. data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
  24. data/app_generators/ahn/templates/config/startup.rb +50 -0
  25. data/app_generators/ahn/templates/dialplan.rb +3 -0
  26. data/app_generators/ahn/templates/events.rb +32 -0
  27. data/bin/ahn +28 -0
  28. data/bin/ahnctl +68 -0
  29. data/bin/jahn +42 -0
  30. data/examples/asterisk_manager_interface/standalone.rb +51 -0
  31. data/lib/adhearsion.rb +37 -0
  32. data/lib/adhearsion/cli.rb +223 -0
  33. data/lib/adhearsion/component_manager.rb +207 -0
  34. data/lib/adhearsion/component_manager/component_tester.rb +55 -0
  35. data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
  36. data/lib/adhearsion/events_support.rb +84 -0
  37. data/lib/adhearsion/foundation/all.rb +9 -0
  38. data/lib/adhearsion/foundation/blank_slate.rb +5 -0
  39. data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
  40. data/lib/adhearsion/foundation/event_socket.rb +203 -0
  41. data/lib/adhearsion/foundation/future_resource.rb +36 -0
  42. data/lib/adhearsion/foundation/global.rb +1 -0
  43. data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
  44. data/lib/adhearsion/foundation/numeric.rb +13 -0
  45. data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
  46. data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
  47. data/lib/adhearsion/foundation/string.rb +26 -0
  48. data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
  49. data/lib/adhearsion/foundation/thread_safety.rb +7 -0
  50. data/lib/adhearsion/host_definitions.rb +67 -0
  51. data/lib/adhearsion/initializer.rb +373 -0
  52. data/lib/adhearsion/initializer/asterisk.rb +81 -0
  53. data/lib/adhearsion/initializer/configuration.rb +254 -0
  54. data/lib/adhearsion/initializer/database.rb +50 -0
  55. data/lib/adhearsion/initializer/drb.rb +31 -0
  56. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  57. data/lib/adhearsion/initializer/rails.rb +41 -0
  58. data/lib/adhearsion/logging.rb +92 -0
  59. data/lib/adhearsion/tasks.rb +16 -0
  60. data/lib/adhearsion/tasks/database.rb +5 -0
  61. data/lib/adhearsion/tasks/deprecations.rb +59 -0
  62. data/lib/adhearsion/tasks/generating.rb +20 -0
  63. data/lib/adhearsion/tasks/lint.rb +4 -0
  64. data/lib/adhearsion/tasks/testing.rb +37 -0
  65. data/lib/adhearsion/version.rb +9 -0
  66. data/lib/adhearsion/voip/asterisk.rb +4 -0
  67. data/lib/adhearsion/voip/asterisk/agi_server.rb +84 -0
  68. data/lib/adhearsion/voip/asterisk/commands.rb +1314 -0
  69. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  70. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  71. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  72. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  73. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  74. data/lib/adhearsion/voip/asterisk/manager_interface.rb +597 -0
  75. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1589 -0
  76. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
  77. data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
  78. data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
  79. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  80. data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
  81. data/lib/adhearsion/voip/call.rb +453 -0
  82. data/lib/adhearsion/voip/call_routing.rb +64 -0
  83. data/lib/adhearsion/voip/commands.rb +9 -0
  84. data/lib/adhearsion/voip/constants.rb +39 -0
  85. data/lib/adhearsion/voip/conveniences.rb +18 -0
  86. data/lib/adhearsion/voip/dial_plan.rb +218 -0
  87. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  88. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  89. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  90. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  91. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
  92. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  93. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  94. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  95. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  96. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  97. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  98. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  99. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  100. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  101. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  102. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  103. data/lib/theatre.rb +151 -0
  104. data/lib/theatre/README.markdown +64 -0
  105. data/lib/theatre/callback_definition_loader.rb +84 -0
  106. data/lib/theatre/guid.rb +23 -0
  107. data/lib/theatre/invocation.rb +121 -0
  108. data/lib/theatre/namespace_manager.rb +153 -0
  109. data/lib/theatre/version.rb +2 -0
  110. metadata +182 -0
@@ -0,0 +1,92 @@
1
+ require 'log4r'
2
+
3
+ module Adhearsion
4
+ module Logging
5
+
6
+ @@logging_level_lock = Mutex.new
7
+
8
+ class << self
9
+
10
+ def silence!
11
+ self.logging_level = :fatal
12
+ end
13
+
14
+ def unsilence!
15
+ self.logging_level = :info
16
+ end
17
+
18
+ def logging_level=(new_logging_level)
19
+ new_logging_level = Log4r.const_get(new_logging_level.to_s.upcase)
20
+ @@logging_level_lock.synchronize do
21
+ @@logging_level = new_logging_level
22
+ Log4r::Logger.each_logger do |logger|
23
+ logger.level = new_logging_level
24
+ end
25
+ end
26
+ end
27
+
28
+ def logging_level
29
+ @@logging_level_lock.synchronize do
30
+ return @@logging_level ||= Log4r::INFO
31
+ end
32
+ end
33
+ end
34
+
35
+ class AdhearsionLogger < Log4r::Logger
36
+
37
+ @@outputters = [Log4r::Outputter.stdout]
38
+
39
+ class << self
40
+ def outputters
41
+ @@outputters
42
+ end
43
+
44
+ def outputters=(other)
45
+ @@outputters = other
46
+ end
47
+ end
48
+
49
+ def initialize(*args)
50
+ super
51
+ redefine_outputters
52
+ end
53
+
54
+ def redefine_outputters
55
+ self.outputters = @@outputters
56
+ end
57
+
58
+ def method_missing(logger_name, *args, &block)
59
+ define_logging_method(logger_name, self.class.new(logger_name.to_s))
60
+ send(logger_name, *args, &block)
61
+ end
62
+
63
+ private
64
+
65
+ def define_logging_method(name, logger)
66
+ # Can't use Module#define_method() because blocks in Ruby 1.8.x can't
67
+ # have their own block arguments.
68
+ self.class.class_eval(<<-CODE, __FILE__, __LINE__)
69
+ def #{name}(*args, &block)
70
+ logger = Log4r::Logger['#{name}']
71
+ if args.any? || block_given?
72
+ logger.info(*args, &block)
73
+ else
74
+ logger
75
+ end
76
+ end
77
+ CODE
78
+ end
79
+ end
80
+
81
+ DefaultAdhearsionLogger = AdhearsionLogger.new 'ahn'
82
+
83
+ end
84
+ end
85
+
86
+ def ahn_log(*args)
87
+ if args.any?
88
+ Adhearsion::Logging::DefaultAdhearsionLogger.info(*args)
89
+ else
90
+ Adhearsion::Logging::DefaultAdhearsionLogger
91
+ end
92
+ end
@@ -0,0 +1,16 @@
1
+ require 'rake/testtask'
2
+ require 'adhearsion'
3
+ require 'adhearsion/tasks/database'
4
+ require 'adhearsion/tasks/testing'
5
+ require 'adhearsion/tasks/generating'
6
+ require 'adhearsion/tasks/lint'
7
+ require 'adhearsion/tasks/deprecations'
8
+
9
+ namespace :adhearsion do
10
+ desc "Dump useful information about this application's adhearsion environment"
11
+ task :about do
12
+ puts "Adhearsion version: #{Adhearsion::VERSION::STRING}"
13
+ end
14
+ end
15
+
16
+ task :default => "adhearsion:about"
@@ -0,0 +1,5 @@
1
+ task:migrate do
2
+ require 'active_record'
3
+ %w.db/migrate db/ahn..each
4
+ ActiveRecord::Migrator.migrate 'db/migrate', ENV['VERSION'] ? ENV['VERSION'].to_i : nil
5
+ end
@@ -0,0 +1,59 @@
1
+ namespace :deprecations do
2
+ desc <<-DESC
3
+ Older versions of Adhearsion had an .ahnrc "paths" section similar to the following...
4
+
5
+ paths:
6
+ models:
7
+ directory: models
8
+ pattern: *.rb
9
+
10
+ This has been deprecated. The new format is this:
11
+
12
+ paths:
13
+ models: {models,gui/app/models}/*.rb
14
+
15
+ This Rake task will fix your .ahnrc if you have
16
+ DESC
17
+ task :fix_ahnrc_path_format do
18
+ puts "\nThis will remove all comments from your .ahnrc file. A backup will be created as .ahnrc.backup."
19
+ puts "If you wish to do this manually to preserve your comments, simply overwrite .ahnrc with .ahnrc.backup"
20
+ puts "and apply the change manually."
21
+ puts
22
+
23
+ require 'fileutils'
24
+ require 'yaml'
25
+
26
+ ahnrc_file = File.expand_path(".ahnrc")
27
+
28
+ FileUtils.cp ahnrc_file, ahnrc_file + ".backup"
29
+ ahnrc_contents = YAML.load_file ahnrc_file
30
+
31
+ abort '.ahnrc does not have a "paths" section!' unless ahnrc_contents.has_key? "paths"
32
+
33
+ paths = ahnrc_contents["paths"]
34
+ paths.clone.each_pair do |key,value|
35
+ if value.kind_of?(Hash)
36
+ if value.has_key?("directory") || value.has_key?("pattern")
37
+ directory, pattern = value.values_at "directory", "pattern"
38
+ new_path = "#{directory}/#{pattern}"
39
+
40
+ puts "!!! CHANGING KEY #{key.inspect}!"
41
+ puts "!!! NEW: #{new_path.inspect}"
42
+ puts "!!! OLD:\n#{{key => value}.to_yaml.sub("---", "")}\n\n"
43
+
44
+ paths[key] = new_path
45
+ end
46
+ end
47
+ end
48
+
49
+ ahnrc_contents["paths"] = paths
50
+ new_yaml = ahnrc_contents.to_yaml.gsub("--- \n", "")
51
+
52
+ puts "New .ahnrc file:\n" + ("#" * 25) + "\n"
53
+ puts new_yaml
54
+ puts '#' * 25
55
+
56
+ File.open(ahnrc_file, "w") { |file| file.puts new_yaml }
57
+ puts "Wrote to .ahnrc. Done!"
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ namespace:create do
2
+
3
+ task:war do
4
+ # Hmm, this will is a tough one
5
+ end
6
+
7
+ task:rails_plugin do
8
+
9
+ end
10
+
11
+ task:migration do
12
+ name = ARGV.shift
13
+ end
14
+ end
15
+
16
+ namespace:delete do
17
+ task:migration do
18
+ # Take arg.underscore and remove it
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ task :sanity do
2
+ puts "Performing many checks on your Adhearsion application!"
3
+ # TODO: Anything that should be brought to the user's attention should be placed here!
4
+ end
@@ -0,0 +1,37 @@
1
+ namespace:test do
2
+ desc "Run tests for a component specified by COMPONENT=<component_name>. If no component is specified, tests will be executed for all components"
3
+ task :component do
4
+ component = ENV['COMPONENT']
5
+ components_to_test = component.nil? ? all_component_directories : [full_path_for(component)]
6
+ components_to_test.each do |component_name|
7
+ setup_and_execute(component_name)
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def setup_and_execute(component_path)
14
+ task = create_test_task_for(component_path)
15
+ Rake::Task[task.name].execute
16
+ end
17
+
18
+ def create_test_task_for(component_path)
19
+ Rake::TestTask.new(task_name_for(component_path)) do |t|
20
+ t.libs = ["lib", "test"].map{|subdir| File.join(component_path, subdir)}
21
+ t.test_files = FileList["#{component_path}/test/test_*.rb"]
22
+ t.verbose = true
23
+ end
24
+ end
25
+
26
+ def task_name_for(component_path)
27
+ "test_#{component_path.split(/\//).last}"
28
+ end
29
+
30
+ def all_component_directories
31
+ Dir['components/*']
32
+ end
33
+
34
+ def full_path_for(component)
35
+ component =~ /^components\// ? component : File.join("components", component)
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ module Adhearsion #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0 unless defined? MAJOR
4
+ MINOR = 8 unless defined? MINOR
5
+ TINY = 3 unless defined? TINY
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.') unless defined? STRING
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ require File.dirname(__FILE__) + "/dsl/numerical_string"
2
+ require File.dirname(__FILE__) + "/asterisk/agi_server"
3
+ require File.dirname(__FILE__) + "/asterisk/manager_interface"
4
+ require File.dirname(__FILE__) + "/asterisk/commands"
@@ -0,0 +1,84 @@
1
+ require 'gserver'
2
+ module Adhearsion
3
+ module VoIP
4
+ module Asterisk
5
+ module AGI
6
+ class Server
7
+
8
+ class RubyServer < GServer
9
+
10
+ def initialize(port, host)
11
+ super(port, host, (1.0/0.0)) # (1.0/0.0) == Infinity
12
+ end
13
+
14
+ def serve(io)
15
+ call = Adhearsion.receive_call_from(io)
16
+ Events.trigger_immediately([:asterisk, :before_call], call)
17
+ ahn_log.agi "Handling call with variables #{call.variables.inspect}"
18
+
19
+ return DialPlan::ConfirmationManager.handle(call) if DialPlan::ConfirmationManager.confirmation_call?(call)
20
+
21
+ # This is what happens 99.9% of the time.
22
+
23
+ DialPlan::Manager.handle call
24
+ rescue Hangup
25
+ ahn_log.agi "HANGUP event for call with uniqueid #{call.variables[:uniqueid].inspect} and channel #{call.variables[:channel].inspect}"
26
+ call.hangup!
27
+ rescue DialPlan::Manager::NoContextError => e
28
+ ahn_log.agi e.message
29
+ call.hangup!
30
+ rescue FailedExtensionCallException => failed_call
31
+ begin
32
+ ahn_log.agi "Received \"failed\" meta-call with :failed_reason => #{failed_call.call.failed_reason.inspect}. Executing Executing /asterisk/failed_call event callbacks."
33
+ Events.trigger [:asterisk, :failed_call], failed_call.call
34
+ call.hangup!
35
+ rescue => e
36
+ ahn_log.agi.error e
37
+ end
38
+ rescue HungupExtensionCallException => hungup_call
39
+ begin
40
+ ahn_log.agi "Received \"h\" meta-call. Executing /asterisk/hungup_call event callbacks."
41
+ Events.trigger [:asterisk, :hungup_call], hungup_call.call
42
+ call.hangup!
43
+ rescue => e
44
+ ahn_log.agi.error e
45
+ end
46
+ rescue UselessCallException
47
+ ahn_log.agi "Ignoring meta-AGI request"
48
+ call.hangup!
49
+ # TBD: (may have more hooks than what Jay has defined in hooks.rb)
50
+ rescue => e
51
+ ahn_log.agi.error e.inspect
52
+ ahn_log.agi.error e.backtrace.map { |s| " " * 5 + s }.join("\n")
53
+ ensure
54
+ Adhearsion.remove_inactive_call call rescue nil
55
+ end
56
+
57
+ end
58
+
59
+ DEFAULT_OPTIONS = { :server_class => RubyServer, :port => 4573, :host => "0.0.0.0" } unless defined? DEFAULT_OPTIONS
60
+ attr_reader :host, :port, :server_class, :server
61
+
62
+ def initialize(options = {})
63
+ options = DEFAULT_OPTIONS.merge options
64
+ @host, @port, @server_class = options.values_at(:host, :port, :server_class)
65
+ @server = server_class.new(port, host)
66
+ end
67
+
68
+ def start
69
+ server.start
70
+ end
71
+
72
+ def shutdown
73
+ server.stop
74
+ end
75
+
76
+ def join
77
+ server.join
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,1314 @@
1
+
2
+ require 'adhearsion/voip/menu_state_machine/menu_class'
3
+
4
+ module Adhearsion
5
+ module VoIP
6
+ module Asterisk
7
+ module Commands
8
+
9
+ RESPONSE_PREFIX = "200 result=" unless defined? RESPONSE_PREFIX
10
+
11
+ # These are the status messages that asterisk will issue after a dial command is executed.
12
+ # More information here: http://www.voip-info.org/wiki/index.php?page=Asterisk+variable+DIALSTATUS
13
+ # Here is a current list of dial status messages which are not all necessarily supported by adhearsion:
14
+ #
15
+ # ANSWER: Call is answered. A successful dial. The caller reached the callee.
16
+ # BUSY: Busy signal. The dial command reached its number but the number is busy.
17
+ # NOANSWER: No answer. The dial command reached its number, the number rang for too long, then the dial timed out.
18
+ # CANCEL: Call is cancelled. The dial command reached its number but the caller hung up before the callee picked up.
19
+ # CONGESTION: Congestion. This status is usually a sign that the dialled number is not recognised.
20
+ # CHANUNAVAIL: Channel unavailable. On SIP, peer may not be registered.
21
+ # DONTCALL: Privacy mode, callee rejected the call
22
+ # TORTURE: Privacy mode, callee chose to send caller to torture menu
23
+ # INVALIDARGS: Error parsing Dial command arguments (added for Asterisk 1.4.1, SVN r53135-53136)
24
+ #
25
+ #
26
+ DIAL_STATUSES = Hash.new(:unknown).merge(:answer => :answered,
27
+ :congestion => :congested,
28
+ :busy => :busy,
29
+ :cancel => :cancelled,
30
+ :noanswer => :unanswered,
31
+ :cancelled => :cancelled,
32
+ :chanunavail => :channel_unavailable) unless defined? DIAL_STATUSES
33
+
34
+ DYNAMIC_FEATURE_EXTENSIONS = {
35
+ :attended_transfer => lambda do |options|
36
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
37
+ extend_dynamic_features_with "atxfer"
38
+ end,
39
+ :blind_transfer => lambda do
40
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
41
+ extend_dynamic_features_with 'blindxfer'
42
+ end
43
+ } unless defined? DYNAMIC_FEATURE_EXTENSIONS
44
+
45
+ def write(message)
46
+ to_pbx.print(message)
47
+ end
48
+
49
+ def read
50
+ returning from_pbx.gets do |message|
51
+ raise Hangup if message.nil?
52
+ ahn_log.agi.debug "<<< #{message}"
53
+ end
54
+ end
55
+
56
+ # This method is the underlying method executed by nearly all the command methods in this module.
57
+ # It is used to send the plaintext commands in the proper AGI format over TCP/IP back to an Asterisk server via the
58
+ # FAGI protocol.
59
+ # It is not recommended that you call this method directly unless you plan to write a new command method
60
+ # in which case use this method you to communicate directly with an Asterisk server via the FAGI protocol.
61
+ # For more information about FAGI visit: http://www.voip-info.org/wiki/view/Asterisk+FastAGI
62
+ def raw_response(message = nil)
63
+ ahn_log.agi.debug ">>> #{message}"
64
+ write message if message
65
+ read
66
+ end
67
+
68
+ # The answer command must be called first before any other commands can be issued.
69
+ # In typical adhearsion applications the answer command is called by default as soon
70
+ # as a call is transfered to a valid context in dialplan.rb.
71
+ # If you do not want your adhearsion application to automatically issue an answer command,
72
+ # then you must edit your startup.rb file and configure this setting.
73
+ # Keep in mind that you should not need to issue another answer command after
74
+ # an answer command has already been issued either explicitly by your code or implicitly
75
+ # by the standard adhearsion configuration.
76
+ def answer
77
+ raw_response "ANSWER"
78
+ true
79
+ end
80
+
81
+ # This asterisk dialplan command allows you to instruct Asterisk to start applications
82
+ # which are typically run from extensions.conf. For a complete list of these commands
83
+ # please visit: http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands
84
+ #
85
+ # The most common commands are already made available through the FAGI interface provided
86
+ # by this code base. For commands that do not fall into this category, then exec is what you
87
+ # should use.
88
+ #
89
+ # For example, if there are specific asterisk modules you have loaded that will not
90
+ # available through the standard commands provided through FAGI - then you can used EXEC.
91
+ #
92
+ # Example:
93
+ # execute 'SIPAddHeader', '"Call-Info: answer-after=0"
94
+ #
95
+ # Using execute in this way will add a header to an existing SIP call.
96
+ #
97
+ def execute(application, *arguments)
98
+ result = raw_response("EXEC #{application} #{arguments * '|'}")
99
+ return false if error?(result)
100
+ result
101
+ end
102
+
103
+ ##
104
+ # Hangs up the current channel. After this command is issued, you will not be able to send any more AGI
105
+ # commands but the dialplan Thread will still continue, allowing you to do any post-call work.
106
+ #
107
+ def hangup
108
+ raw_response 'HANGUP'
109
+ end
110
+
111
+ # Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
112
+ # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. When playing
113
+ # numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
114
+ # is pronounced as "one hundred" instead of "one zero zero".
115
+ #
116
+ # Note: it's not necessary to supply a sound file extension; Asterisk will try to find a sound
117
+ # file encoded using the current channel's codec, if one exists. If not, it will transcode from
118
+ # the default codec (GSM). Asterisk stores its sound files in /var/lib/asterisk/sounds.
119
+ #
120
+ # Usage:
121
+ #
122
+ # play 'hello-world'
123
+ # play Time.now
124
+ # play %w"a-connect-charge-of 22 cents-per-minute will-apply"
125
+ # play "you-sound-cute", "what-are-you-wearing"
126
+ #
127
+ def play(*arguments)
128
+ arguments.flatten.each do |argument|
129
+ play_time(argument) || play_numeric(argument) || play_string(argument)
130
+ end
131
+ end
132
+
133
+ # Records a sound file with the given name. If no filename is specified a file named by Asterisk
134
+ # will be created and returned. Else the given filename will be returned. If a relative path is
135
+ # given, the file will be saved in the default Asterisk sound directory, /var/lib/spool/asterisk
136
+ # by default.
137
+ #
138
+ # Silence and maxduration is specified in seconds.
139
+ #
140
+ # Usage:
141
+ # record
142
+ # record '/path/to/my-file.gsm'
143
+ # record 'my-file.gsm', :silence => 5, :maxduration => 120
144
+ #
145
+ def record(*args)
146
+ options = args.last.kind_of?(Hash) ? args.pop : {}
147
+ filename = args.shift || "/tmp/recording_%d.gsm"
148
+ silence = options.delete(:silence) || 0
149
+ maxduration = options.delete(:maxduration) || 0
150
+
151
+ execute("Record", filename, silence, maxduration)
152
+
153
+ # If the user hangs up before the recording is entered, -1 is returned and RECORDED_FILE
154
+ # will not contain the name of the file, even though it IS in fact recorded.
155
+ filename.index("%d") ? get_variable('RECORDED_FILE') : filename
156
+ end
157
+
158
+ # Simulates pressing the specified digits over the current channel. Can be used to
159
+ # traverse a phone menu.
160
+ def dtmf(digits)
161
+ execute "SendDTMF", digits.to_s
162
+ end
163
+
164
+ # The with_next_message method...
165
+ def with_next_message(&block)
166
+ raise LocalJumpError, "Must supply a block" unless block_given?
167
+ block.call(next_message)
168
+ end
169
+
170
+ # This command should be used to advance to the next message in the Asterisk Comedian Voicemail application
171
+ def next_message
172
+ @call.inbox.pop
173
+ end
174
+
175
+ # This command should be used to check if a message is waiting on the Asterisk Comedian Voicemail application.
176
+ def messages_waiting?
177
+ not @call.inbox.empty?
178
+ end
179
+
180
+ # = Menu Command
181
+ #
182
+ # The following documentation was derived from this blog post on Jay Phillips' blog:
183
+ #
184
+ # http://jicksta.com/articles/2008/02/11/menu-command
185
+ #
186
+ # The menu() command solves the problem of building enormous input-fetching state machines in Ruby without first-class
187
+ # message passing facilities or an external DSL.
188
+ #
189
+ # Here is an example dialplan which uses the menu() command effectively.
190
+ #
191
+ # from_pstn {
192
+ # menu 'welcome', 'for-spanish-press-8', 'main-ivr',
193
+ # :timeout => 8.seconds, :tries => 3 do |link|
194
+ # link.shipment_status 1
195
+ # link.ordering 2
196
+ # link.representative 4
197
+ # link.spanish 8
198
+ # link.employee 900..999
199
+ #
200
+ # link.on_invalid { play 'invalid' }
201
+ #
202
+ # link.on_premature_timeout do |str|
203
+ # play 'sorry'
204
+ # end
205
+ #
206
+ # link.on_failure do
207
+ # play 'goodbye'
208
+ # hangup
209
+ # end
210
+ # end
211
+ # }
212
+ #
213
+ # shipment_status {
214
+ # # Fetch a tracking number and pass it to a web service.
215
+ # }
216
+ #
217
+ # ordering {
218
+ # # Enter another menu that lets them enter credit card
219
+ # # information and place their order over the phone.
220
+ # }
221
+ #
222
+ # representative {
223
+ # # Place the caller into a queue
224
+ # }
225
+ #
226
+ # spanish {
227
+ # # Special options for the spanish menu.
228
+ # }
229
+ #
230
+ # employee {
231
+ # dial "SIP/#{extension}" # Overly simplistic
232
+ # }
233
+ #
234
+ # The main detail to note is the declarations within the menu() command’s block. Each line seems to refer to a link object
235
+ # executing a seemingly arbitrary method with an argument that’s either a number or a Range of numbers. The +link+ object
236
+ # collects these arbitrary method invocations and assembles a set of rules. The seemingly arbitrary method name is the name
237
+ # of the context to which the menu should jump in case its argument (the pattern) is found to be a match.
238
+ #
239
+ # With these context names and patterns defined, the +menu()+ command plays in sequence the sound files you supply as
240
+ # arguments, stopping playback abruptly if the user enters a digit. If no digits were pressed when the files finish playing,
241
+ # it waits +:timeout+ seconds. If no digits are pressed after the timeout, it executes the +on_premature_timeout+ hook you
242
+ # define (if any) and then tries again a maximum of +:tries+ times. If digits are pressed that result in no possible match,
243
+ # it executes the +on_invalid+ hook. When/if all tries are exhausted with no positive match, it executes the +on_failure+
244
+ # hook after the other hook (e.g. +on_invalid+, then +on_failure+).
245
+ #
246
+ # When the +menu()+ state machine runs through the defined rules, it must distinguish between exact and potential matches.
247
+ # It’s important to understand the differences between these and how they affect the overall outcome:
248
+ #
249
+ # |---------------|-------------------|------------------------------------------------------|
250
+ # | exact matches | potential matches | result |
251
+ # |---------------|-------------------|------------------------------------------------------|
252
+ # | 0 | 0 | Fail and start over |
253
+ # | 1 | 0 | Match found! |
254
+ # | 0 | >0 | Get another digit |
255
+ # | >1 | 0 | Go with the first exact match |
256
+ # | 1 | >0 | Get another digit. If timeout, use exact match |
257
+ # | >1 | >0 | Get another digit. If timeout, use first exact match |
258
+ # |---------------|-------------------|------------------------------------------------------|
259
+ #
260
+ # == Database integration
261
+ #
262
+ # To do database integration, I recommend programatically executing methods on the link object within the block. For example:
263
+ #
264
+ # menu do |link|
265
+ # for employee in Employee.find(:all)
266
+ # link.internal employee.extension
267
+ # end
268
+ # end
269
+ #
270
+ # or this more efficient and Rubyish way
271
+ #
272
+ # menu do |link|
273
+ # link.internal *Employee.find(:all).map(&:extension)
274
+ # end
275
+ #
276
+ # If this second example seems like too much Ruby magic, let me explain — +Employee.find(:all)+ effectively does a “SELECT *
277
+ # FROM employees” on the database with ActiveRecord, returning (what you’d think is) an Array. The +map(&:extension)+ is
278
+ # fanciness that means “replace every instance in this Array with the result of calling extension on that object”. Now we
279
+ # have an Array of every extension in the database. The splat operator (*) before the argument changes the argument from
280
+ # being one argument (an Array) into a sequence of n arguments, where n is the number of items in the Array it’s “splatting”.
281
+ # Lastly, these arguments are passed to the internal method, the name of a context which will handle dialing this user if one
282
+ # of the supplied patterns matches.
283
+ #
284
+ # == Handling a successful pattern match
285
+ #
286
+ # Which brings me to another important note. Let’s say that the user’s input successfully matched one of the patterns
287
+ # returned by that Employe.find... magic. When it jumps to the internal context, that context can access the variable entered
288
+ # through the extension variable. This was a tricky design decision that I think, overall, works great. It makes the +menu()+
289
+ # command feel much more first-class in the Adhearsion dialplan grammar and decouples the receiving context from the menu
290
+ # that caused the jump. After all, the context doesn’t necessary need to be the endpoint from a menu; it can be its own entry
291
+ # point, making menu() effectively a pipeline of re-creating the call.
292
+ #
293
+ def menu(*args, &block)
294
+ options = args.last.kind_of?(Hash) ? args.pop : {}
295
+ sound_files = args.flatten
296
+
297
+ menu_instance = Menu.new(options, &block)
298
+
299
+ initial_digit_prompt = sound_files.any?
300
+
301
+ # This method is basically one big begin/rescue block. When we start the Menu state machine by continue()ing, the state
302
+ # machine will pass messages back to this method in the form of Exceptions. This decoupling allows the menu system to
303
+ # work on, say, Freeswitch and Asterisk both.
304
+ begin
305
+ if menu_instance.should_continue?
306
+ menu_instance.continue
307
+ else
308
+ menu_instance.execute_failure_hook
309
+ return :failed
310
+ end
311
+ rescue Menu::MenuResult => result_of_menu
312
+ case result_of_menu
313
+ when Menu::MenuResultInvalid
314
+ menu_instance.execute_invalid_hook
315
+ menu_instance.restart!
316
+ when Menu::MenuGetAnotherDigit
317
+
318
+ next_digit = play_sound_files_for_menu(menu_instance, sound_files)
319
+ if next_digit
320
+ menu_instance << next_digit
321
+ else
322
+ # The user timed out entering another digit!
323
+ case result_of_menu
324
+ when Menu::MenuGetAnotherDigitOrFinish
325
+ # This raises a ControlPassingException
326
+ jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
327
+ when Menu::MenuGetAnotherDigitOrTimeout
328
+ # This should execute premature_timeout AND reset if the number of retries
329
+ # has not been exhausted.
330
+ menu_instance.execute_timeout_hook
331
+ menu_instance.restart!
332
+ end
333
+ end
334
+ when Menu::MenuResultFound
335
+ jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
336
+ else
337
+ raise "Unrecognized MenuResult! This may be a bug!"
338
+ end
339
+
340
+ # Retry will re-execute the begin block, preserving our changes to the menu_instance object.
341
+ retry
342
+
343
+ end
344
+ end
345
+
346
+ # This method is used to receive keypad input from the user. Digits are collected
347
+ # via DTMF (keypad) input until one of three things happens:
348
+ #
349
+ # 1. The number of digits you specify as the first argument is collected
350
+ # 2. The timeout you specify with the :timeout option elapses.
351
+ # 3. The "#" key (or the key you specify with :accept_key) is pressed
352
+ #
353
+ # Usage examples
354
+ #
355
+ # input # Receives digits until the caller presses the "#" key
356
+ # input 3 # Receives three digits. Can be 0-9, * or #
357
+ # input 5, :accept_key => "*" # Receive at most 5 digits, stopping if '*' is pressed
358
+ # input 1, :timeout => 1.minute # Receive a single digit, returning an empty
359
+ # string if the timeout is encountered
360
+ # input 9, :timeout => 7, :accept_key => "0" # Receives nine digits, returning
361
+ # # when the timeout is encountered
362
+ # # or when the "0" key is pressed.
363
+ # input 3, :play => "you-sound-cute"
364
+ # input :play => ["if-this-is-correct-press", 1, "otherwise-press", 2]
365
+ #
366
+ # When specifying files to play, the playback of the sequence of files will stop
367
+ # immediately when the user presses the first digit.
368
+ #
369
+ # The :timeout option works like a digit timeout, therefore each digit pressed
370
+ # causes the timer to reset. This is a much more user-friendly approach than an
371
+ # absolute timeout.
372
+ #
373
+ # Note that when you don't specify a digit limit, the :accept_key becomes "#"
374
+ # because there'd be no other way to end the collection of digits. You can
375
+ # obviously override this by passing in a new key with :accept_key.
376
+ def input(*args)
377
+ options = args.last.kind_of?(Hash) ? args.pop : {}
378
+ number_of_digits = args.shift
379
+
380
+ sound_files = Array options.delete(:play)
381
+ timeout = options.delete(:timeout)
382
+ terminating_key = options.delete(:accept_key)
383
+ terminating_key = if terminating_key
384
+ terminating_key.to_s
385
+ elsif number_of_digits.nil? && !terminating_key.equal?(false)
386
+ '#'
387
+ end
388
+
389
+ if number_of_digits && number_of_digits < 0
390
+ ahn_log.agi.warn "Giving -1 to input() is now deprecated. Don't specify a first " +
391
+ "argument to simulate unlimited digits." if number_of_digits == -1
392
+ raise ArgumentError, "The number of digits must be positive!"
393
+ end
394
+
395
+ buffer = ''
396
+ key = sound_files.any? ? interruptable_play(*sound_files) || '' : wait_for_digit(timeout || -1)
397
+ loop do
398
+ return buffer if key.nil?
399
+ if terminating_key
400
+ if key == terminating_key
401
+ return buffer
402
+ else
403
+ buffer << key
404
+ return buffer if number_of_digits && number_of_digits == buffer.length
405
+ end
406
+ else
407
+ buffer << key
408
+ return buffer if number_of_digits && number_of_digits == buffer.length
409
+ end
410
+ key = wait_for_digit(timeout || -1)
411
+ end
412
+ end
413
+
414
+ # An alternative to DialplanContextProc#+@. When jumping to a context, it will *not* resume executing
415
+ # the former context when the jumped-to context has finished executing. Make sure you don't have any
416
+ # +ensure+ closures which you expect to execute when the call has finished, as they will run when
417
+ # this method is called.
418
+ #
419
+ # You can optionally override certain dialplan variables when jumping to the context. A popular use of
420
+ # this is to redefine +extension+ (which this method automatically boxes with a PhoneNumber object) so
421
+ # you can effectively "restart" a call (from the perspective of the jumped-to context). When you override
422
+ # variables here, you're effectively blowing away the old variables. If you need them for some reason,
423
+ # you should assign the important ones to an instance variable first before calling this method.
424
+ def jump_to(context, overrides={})
425
+ context = lookup_context_with_name(context) if context.kind_of?(Symbol) || (context.kind_of?(String) && context =~ /^[\w_]+$/)
426
+ raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException unless context.kind_of?(Adhearsion::DialPlan::DialplanContextProc)
427
+
428
+ if overrides.any?
429
+ overrides = overrides.symbolize_keys
430
+ if overrides.has_key?(:extension) && !overrides[:extension].kind_of?(Adhearsion::VoIP::DSL::PhoneNumber)
431
+ overrides[:extension] = Adhearsion::VoIP::DSL::PhoneNumber.new overrides[:extension]
432
+ end
433
+
434
+ overrides.each_pair do |key, value|
435
+ meta_def(key) { value }
436
+ end
437
+ end
438
+
439
+ raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context)
440
+ end
441
+
442
+ # The queue method puts a call into a call queue to be answered by an agent registered with that queue.
443
+ # A full description may be found here: http://www.voip-info.org/wiki-Asterisk+cmd+Queue
444
+ # The queue method takes a queue_name as an argument to place the caller in the appropriate queue.
445
+ def queue(queue_name)
446
+ queue_name = queue_name.to_s
447
+
448
+ @queue_proxy_hash_lock = Mutex.new unless defined? @queue_proxy_hash_lock
449
+ @queue_proxy_hash_lock.synchronize do
450
+ @queue_proxy_hash ||= {}
451
+ if @queue_proxy_hash.has_key? queue_name
452
+ return @queue_proxy_hash[queue_name]
453
+ else
454
+ proxy = @queue_proxy_hash[queue_name] = QueueProxy.new(queue_name, self)
455
+ return proxy
456
+ end
457
+ end
458
+ end
459
+
460
+ # Returns the status of the last dial(). Possible dial
461
+ # statuses include :answer, :busy, :no_answer, :cancelled,
462
+ # :congested, and :channel_unavailable. If :cancel is
463
+ # returned, the caller hung up before the callee picked
464
+ # up. If :congestion is returned, the dialed extension
465
+ # probably doesn't exist. If :channel_unavailable, the callee
466
+ # phone may not be registered.
467
+ def last_dial_status
468
+ DIAL_STATUSES[get_dial_status]
469
+ end
470
+
471
+ # Returns true if your last call to dial() finished with the ANSWER state, as reported
472
+ # by Asterisk. Returns false otherwise
473
+ def last_dial_successful?
474
+ last_dial_status == :answered
475
+ end
476
+
477
+ # Opposite of last_dial_successful?()
478
+ def last_dial_unsuccessful?
479
+ not last_dial_successful?
480
+ end
481
+
482
+ # This feature is presently experimental! Do not use it!
483
+ def speak(text, engine=:none)
484
+ engine = Adhearsion::Configuration::AsteriskConfiguration.speech_engine || engine
485
+ execute SpeechEngines.send(engine, text)
486
+ end
487
+
488
+ # This method is a high-level way of enabling features you create/uncomment from features.conf.
489
+ #
490
+ # Certain Symbol features you enable (as defined in DYNAMIC_FEATURE_EXTENSIONS) have optional
491
+ # arguments that you can also specify here. The usage examples show how to do this.
492
+ #
493
+ # Usage examples:
494
+ #
495
+ # enable_feature :attended_transfer # Enables "atxfer"
496
+ #
497
+ # enable_feature :attended_transfer, :context => "my_dial" # Enables "atxfer" and then
498
+ # # sets "TRANSFER_CONTEXT" to :context's value
499
+ #
500
+ # enable_feature :blind_transfer, :context => 'my_dial' # Enables 'blindxfer' and sets TRANSFER_CONTEXT
501
+ #
502
+ # enable_feature "foobar" # Enables "foobar"
503
+ #
504
+ # enable_feature("dup"); enable_feature("dup") # Enables "dup" only once.
505
+ def enable_feature(feature_name, optional_options=nil)
506
+ if DYNAMIC_FEATURE_EXTENSIONS.has_key? feature_name
507
+ instance_exec(optional_options, &DYNAMIC_FEATURE_EXTENSIONS[feature_name])
508
+ else
509
+ raise ArgumentError, "You cannot supply optional options when the feature name is " +
510
+ "not internally recognized!" if optional_options
511
+ extend_dynamic_features_with feature_name
512
+ end
513
+ end
514
+
515
+ # Disables a feature name specified in features.conf. If you're disabling it, it was probably
516
+ # set by enable_feature().
517
+ def disable_feature(feature_name)
518
+ enabled_features_variable = variable 'DYNAMIC_FEATURES'
519
+ enabled_features = enabled_features_variable.split('#')
520
+ if enabled_features.include? feature_name
521
+ enabled_features.delete feature_name
522
+ variable 'DYNAMIC_FEATURES' => enabled_features.join('#')
523
+ end
524
+ end
525
+
526
+ # Used to join a particular conference with the MeetMe application. To
527
+ # use MeetMe, be sure you have a proper timing device configured on your
528
+ # Asterisk box. MeetMe is Asterisk's built-in conferencing program.
529
+ # More info: http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe
530
+ def join(conference_id, options={})
531
+ conference_id = conference_id.to_s.scan(/\w/).join
532
+ command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
533
+ pin = options[:pin]
534
+ raise ArgumentError, "A conference PIN number must be numerical!" if pin && pin.to_s !~ /^\d+$/
535
+ # The 'd' option of MeetMe creates conferences dynamically.
536
+ command_flags += 'd' unless command_flags.include? 'd'
537
+
538
+ execute "MeetMe", conference_id, command_flags, options[:pin]
539
+ end
540
+
541
+ # Issue this command to access a channel variable that exists in the asterisk dialplan (i.e. extensions.conf)
542
+ # A complete description is available here: http://www.voip-info.org/wiki/view/get+variable
543
+ # Use get_variable to pass information from other modules or high level configurations from the asterisk dialplan
544
+ # to the adhearsion dialplan.
545
+ def get_variable(variable_name)
546
+ result = raw_response("GET VARIABLE #{variable_name}")
547
+ case result
548
+ when "200 result=0"
549
+ return nil
550
+ when /^200 result=1 \((.*)\)$/
551
+ return $LAST_PAREN_MATCH
552
+ end
553
+ end
554
+
555
+ # Use set_variable to pass information back to the asterisk dial plan.
556
+ # A complete decription is available here: http://www.voip-info.org/wiki/view/set+variable
557
+ # Keep in mind that the variables are not global variables. These variables only exist for the channel
558
+ # related to the call that is being serviced by the particular instance of your adhearsion application.
559
+ # You will not be able to pass information back to the asterisk dialplan for other instances of your adhearsion
560
+ # application to share. Once the channel is "hungup" then the variables are cleared and their information is gone.
561
+ def set_variable(variable_name, value)
562
+ raw_response("SET VARIABLE %s %p" % [variable_name.to_s, value.to_s]) == "200 result=1"
563
+ end
564
+
565
+ # The variable method allows you to either set or get a channel variable from Asterisk
566
+ # The method takes a hash key/value pair if you would like to set a variable
567
+ # Or a single string with the variable to get from Asterisk
568
+ def variable(*args)
569
+ if args.last.kind_of? Hash
570
+ assignments = args.pop
571
+ raise ArgumentError, "Can't mix variable setting and fetching!" if args.any?
572
+ assignments.each_pair do |key, value|
573
+ set_variable(key, value)
574
+ end
575
+ else
576
+ if args.size == 1
577
+ get_variable args.first
578
+ else
579
+ args.map { |var| get_variable(var) }
580
+ end
581
+ end
582
+ end
583
+
584
+ # Use the voicemail method to send a caller to a voicemail box to leave a message.
585
+ # A complete description is avilable at:
586
+ # http://www.voip-info.org/tiki-index.php?page=Asterisk+cmd+VoiceMail
587
+ # The method takes the mailbox_number of the user to leave a message for and a
588
+ # greeting_option that will determine which message gets played to the caller.
589
+ def voicemail(*args)
590
+ options_hash = args.last.kind_of?(Hash) ? args.pop : {}
591
+ mailbox_number = args.shift
592
+ greeting_option = options_hash.delete(:greeting)
593
+ skip_option = options_hash.delete(:skip)
594
+ raise ArgumentError, 'You supplied too many arguments!' if mailbox_number && options_hash.any?
595
+ greeting_option = case greeting_option
596
+ when :busy: 'b'
597
+ when :unavailable: 'u'
598
+ when nil: nil
599
+ else raise ArgumentError, "Unrecognized greeting #{greeting_option}"
600
+ end
601
+ skip_option &&= 's'
602
+ options = "#{greeting_option}#{skip_option}"
603
+
604
+ raise ArgumentError, "Mailbox cannot be blank!" if !mailbox_number.nil? && mailbox_number.blank?
605
+ number_with_context = if mailbox_number then mailbox_number else
606
+ raise ArgumentError, "You must supply ONE context name!" if options_hash.size != 1
607
+ context_name, mailboxes = options_hash.to_a.first
608
+ Array(mailboxes).map do |mailbox|
609
+ raise ArgumentError, "Mailbox numbers must be numerical!" unless mailbox.to_s =~ /^\d+$/
610
+ "#{mailbox}@#{context_name}"
611
+ end.join('&')
612
+ end
613
+ execute('voicemail', number_with_context, options)
614
+ case variable('VMSTATUS')
615
+ when 'SUCCESS': true
616
+ when 'USEREXIT': false
617
+ else nil
618
+ end
619
+ end
620
+
621
+ # The voicemail_main method puts a caller into the voicemail system to fetch their voicemail
622
+ # or set options for their voicemail box. A full description may be found here:
623
+ # http://www.voip-info.org/wiki-Asterisk+cmd+VoiceMailMain
624
+ def voicemail_main(options={})
625
+ mailbox, context, folder = options.values_at :mailbox, :context, :folder
626
+ authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
627
+
628
+ folder = if folder
629
+ if folder.to_s =~ /^[\w_]+$/
630
+ "a(#{folder})"
631
+ else
632
+ raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
633
+ end
634
+ elsif folder == ''
635
+ raise "Folder name cannot be an empty String!"
636
+ else
637
+ nil
638
+ end
639
+
640
+ real_mailbox = ""
641
+ real_mailbox << "#{mailbox}" unless mailbox.blank?
642
+ real_mailbox << "@#{context}" unless context.blank?
643
+
644
+ real_options = ""
645
+ real_options << "s" if !authenticate
646
+ real_options << folder unless folder.blank?
647
+
648
+ command_args = [real_mailbox]
649
+ command_args << real_options unless real_options.blank?
650
+ command_args.clear if command_args == [""]
651
+
652
+ execute 'VoiceMailMain', *command_args
653
+ end
654
+
655
+ def check_voicemail
656
+ ahn_log.agi.warn "THE check_voicemail() DIALPLAN METHOD WILL SOON BE DEPRECATED! CHANGE THIS TO voicemail_main() INSTEAD"
657
+ voicemail_main
658
+ end
659
+
660
+ # Use this command to dial an extension i.e. "phone number" in asterisk
661
+ # This command maps to the Asterisk DIAL command in the asterisk dialplan: http://www.voip-info.org/wiki-Asterisk+cmd+Dial
662
+ #
663
+ # The first parameter, number, must be a string that represents the extension or "number" that asterisk should dial.
664
+ # Be careful to not just specify a number like 5001, 9095551001
665
+ # You must specify a properly formatted string as Asterisk would expect to use in order to understand
666
+ # whether the call should be dialed using SIP, IAX, or some other means.
667
+ # Examples:
668
+ #
669
+ # Make a call to the PSTN using my SIP provider for VoIP termination:
670
+ # dial("SIP/19095551001@my.sip.voip.terminator.us")
671
+ #
672
+ # Make 3 Simulataneous calls to the SIP extensions separated by & symbols, try for 15 seconds and use the callerid
673
+ # for this call specified by the variable my_callerid
674
+ # dial "SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => my_callerid
675
+ #
676
+ # Make a call using the IAX provider to the PSTN
677
+ # dial("IAX2/my.id@voipjet/19095551234", :name=>"John Doe", :caller_id=>"9095551234")
678
+ #
679
+ # Options Parameter:
680
+ # :caller_id - the caller id number to be used when the call is placed. It is advised you properly adhere to the
681
+ # policy of VoIP termination providers with respect to caller id values.
682
+ #
683
+ # :name - this is the name which should be passed with the caller ID information
684
+ # if :name=>"John Doe" and :caller_id => "444-333-1000" then the compelete CID and name would be "John Doe" <4443331000>
685
+ # support for caller id information varies from country to country and from one VoIP termination provider to another.
686
+ #
687
+ # :for - this option can be thought of best as a timeout. i.e. timeout after :for if no one answers the call
688
+ # For example, dial("SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => callerid)
689
+ # this call will timeout after 15 seconds if 1 of the 3 extensions being dialed do not pick prior to the 15 second time limit
690
+ #
691
+ # :options - This is a string of options like "Tr" which are supported by the asterisk DIAL application.
692
+ # for a complete list of these options and their usage please visit: http://www.voip-info.org/wiki-Asterisk+cmd+Dial
693
+ #
694
+ # :confirm - ?
695
+ #
696
+ def dial(number, options={})
697
+ *recognized_options = :caller_id, :name, :for, :options, :confirm
698
+
699
+ unrecognized_options = options.keys - recognized_options
700
+ raise ArgumentError, "Unknown dial options: #{unrecognized_options.to_sentence}" if unrecognized_options.any?
701
+ set_caller_id_name options[:name]
702
+ set_caller_id_number options[:caller_id]
703
+ confirm_option = dial_macro_option_compiler options[:confirm]
704
+ all_options = options[:options]
705
+ all_options = all_options ? all_options + confirm_option : confirm_option
706
+ execute "Dial", number, options[:for], all_options
707
+ end
708
+
709
+
710
+ # This implementation of dial() uses the experimental call routing DSL.
711
+ #
712
+ # def dial(number, options={})
713
+ # rules = callable_routes_for number
714
+ # return :no_route if rules.empty?
715
+ # call_attempt_status = nil
716
+ # rules.each do |provider|
717
+ #
718
+ # response = execute "Dial",
719
+ # provider.format_number_for_platform(number),
720
+ # timeout_from_dial_options(options),
721
+ # asterisk_options_from_dial_options(options)
722
+ #
723
+ # call_attempt_status = last_dial_status
724
+ # break if call_attempt_status == :answered
725
+ # end
726
+ # call_attempt_status
727
+ # end
728
+
729
+
730
+ # Speaks the digits given as an argument. For example, "123" is spoken as "one two three".
731
+ def say_digits(digits)
732
+ execute "saydigits", validate_digits(digits)
733
+ end
734
+
735
+ # Returns the number of seconds the given block takes to execute as a Float. This
736
+ # is particularly useful in dialplans for tracking billable time. Note that
737
+ # if the call is hung up during the block, you will need to rescue the
738
+ # exception if you have some mission-critical logic after it with which
739
+ # you're recording this return-value.
740
+ def duration_of
741
+ start_time = Time.now
742
+ yield
743
+ Time.now - start_time
744
+ end
745
+
746
+ ##
747
+ # This will play a sequence of files, stopping the playback if a digit is pressed. If a digit is pressed, it will be
748
+ # returned as a String. If the files played with no keypad input, nil will be returned.
749
+ #
750
+ def interruptible_play(*files)
751
+ files.flatten.each do |file|
752
+ result = result_digit_from raw_response("EXEC BACKGROUND #{file}")
753
+ return result if result != 0.chr
754
+ end
755
+ nil
756
+ end
757
+
758
+ protected
759
+
760
+ # wait_for_digits waits for the input of digits based on the number of milliseconds
761
+ def wait_for_digit(timeout=-1)
762
+ timeout *= 1_000 if timeout != -1
763
+ result = result_digit_from raw_response("WAIT FOR DIGIT #{timeout.to_i}")
764
+ (result == 0.chr) ? nil : result
765
+ end
766
+
767
+ ##
768
+ # Deprecated name of interruptible_play(). This is a misspelling!
769
+ #
770
+ def interruptable_play(*files)
771
+ ahn_log.deprecation.warn 'Please change your code to use interruptible_play() instead. "interruptable" is a misspelling! interruptable_play() will work for now but will be deprecated in the future!'
772
+ interruptible_play(*files)
773
+ end
774
+
775
+ # set_callier_id_number method allows setting of the callerid number of the call
776
+ def set_caller_id_number(caller_id)
777
+ return unless caller_id
778
+ raise ArgumentError, "Caller ID must be numerical" if caller_id.to_s !~ /^\d+$/
779
+ raw_response %(SET CALLERID %p) % caller_id
780
+ end
781
+
782
+ # set_caller_id_name method allows the setting of the callerid name of the call
783
+ def set_caller_id_name(caller_id_name)
784
+ return unless caller_id_name
785
+ variable "CALLERID(name)" => caller_id_name
786
+ end
787
+
788
+ def timeout_from_dial_options(options)
789
+ options[:for] || options[:timeout]
790
+ end
791
+
792
+ def asterisk_options_from_dial_options(options)
793
+ # TODO: Will become much more sophisticated soon to handle callerid, etc
794
+ options[:options]
795
+ end
796
+
797
+ def dial_macro_option_compiler(confirm_argument_value)
798
+ defaults = { :macro => 'ahn_dial_confirmer',
799
+ :timeout => 20.seconds,
800
+ :play => "beep",
801
+ :key => '#' }
802
+
803
+ case confirm_argument_value
804
+ when true
805
+ DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument(defaults)
806
+ when false, nil
807
+ ''
808
+ when Proc
809
+ raise NotImplementedError, "Coming in the future, you can do :confirm => my_context."
810
+
811
+ when Hash
812
+ options = defaults.merge confirm_argument_value
813
+ if((confirm_argument_value.keys - defaults.keys).any?)
814
+ raise ArgumentError, "Known options: #{defaults.keys.to_sentence}"
815
+ end
816
+ raise ArgumentError, "Bad macro name!" unless options[:macro].to_s =~ /^[\w_]+$/
817
+ options[:timeout] = case options[:timeout]
818
+ when Fixnum, ActiveSupport::Duration
819
+ options[:timeout]
820
+ when String
821
+ raise ArgumentError, "Timeout must be numerical!" unless options[:timeout] =~ /^\d+$/
822
+ options[:timeout].to_i
823
+ when :none
824
+ 0
825
+ else
826
+ raise ArgumentError, "Unrecognized :timeout! #{options[:timeout].inspect}"
827
+ end
828
+ raise ArgumentError, "Unrecognized DTMF key: #{options[:key]}" unless options[:key].to_s =~ /^[\d#*]$/
829
+ options[:play] = Array(options[:play]).join('++')
830
+ DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument options
831
+
832
+ else
833
+ raise ArgumentError, "Unrecognized :confirm option: #{confirm_argument_value.inspect}!"
834
+ end
835
+ end
836
+
837
+ def result_digit_from(response_string)
838
+ raise ArgumentError, "Can't coerce nil into AGI response! This could be a bug!" unless response_string
839
+ digit = response_string[/^#{response_prefix}(-?\d+(\.\d+)?)/,1]
840
+ digit.to_i.chr if digit && digit.to_s != "-1"
841
+ end
842
+
843
+ def extract_input_from(result)
844
+ return false if error?(result)
845
+ # return false if input_timed_out?(result)
846
+
847
+ # This regexp doesn't match if there was a timeout with no
848
+ # inputted digits, therefore returning nil.
849
+
850
+ result[/^#{response_prefix}([\d*]+)/, 1]
851
+ end
852
+
853
+ def extract_variable_from(result)
854
+ return false if error?(result)
855
+ result[/^#{response_prefix}1 \((.+)\)/, 1]
856
+ end
857
+
858
+ def get_dial_status
859
+ dial_status = variable('DIALSTATUS')
860
+ dial_status ? dial_status.downcase.to_sym : :cancelled
861
+ end
862
+
863
+ def play_time(argument)
864
+ if argument.kind_of? Time
865
+ execute(:sayunixtime, argument.to_i)
866
+ end
867
+ end
868
+
869
+ def play_numeric(argument)
870
+ if argument.kind_of?(Numeric) || argument =~ /^\d+$/
871
+ execute(:saynumber, argument)
872
+ end
873
+ end
874
+
875
+ def play_string(argument)
876
+ execute(:playback, argument)
877
+ end
878
+
879
+ def play_sound_files_for_menu(menu_instance, sound_files)
880
+ digit = nil
881
+ if sound_files.any? && menu_instance.digit_buffer_empty?
882
+ digit = interruptable_play(*sound_files)
883
+ end
884
+ digit || wait_for_digit(menu_instance.timeout)
885
+ end
886
+
887
+ def extend_dynamic_features_with(feature_name)
888
+ current_variable = variable("DYNAMIC_FEATURES") || ''
889
+ enabled_features = current_variable.split '#'
890
+ unless enabled_features.include? feature_name
891
+ enabled_features << feature_name
892
+ variable "DYNAMIC_FEATURES" => enabled_features.join('#')
893
+ end
894
+ end
895
+
896
+ def jump_to_context_with_name(context_name)
897
+ context_lambda = lookup_context_with_name context_name
898
+ raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context_lambda)
899
+ end
900
+
901
+ def lookup_context_with_name(context_name)
902
+ begin
903
+ send context_name
904
+ rescue NameError
905
+ raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException
906
+ end
907
+ end
908
+
909
+ def redefine_extension_to_be(new_extension)
910
+ new_extension = Adhearsion::VoIP::DSL::PhoneNumber.new new_extension
911
+ meta_def(:extension) { new_extension }
912
+ end
913
+
914
+ def to_pbx
915
+ io
916
+ end
917
+
918
+ def from_pbx
919
+ io
920
+ end
921
+
922
+ def validate_digits(digits)
923
+ returning digits.to_s do |digits_as_string|
924
+ raise ArgumentError, "Can only be called with valid digits!" unless digits_as_string =~ /^[0-9*#-]+$/
925
+ end
926
+ end
927
+
928
+ def error?(result)
929
+ result.to_s[/^#{response_prefix}(?:-\d+|0)/]
930
+ end
931
+
932
+ # timeout with pressed digits: 200 result=<digits> (timeout)
933
+ # timeout without pressed digits: 200 result= (timeout)
934
+ # (http://www.voip-info.org/wiki/view/get+data)
935
+ def input_timed_out?(result)
936
+ result.starts_with?(response_prefix) && result.ends_with?('(timeout)')
937
+ end
938
+
939
+ def io
940
+ call.io
941
+ end
942
+
943
+ def response_prefix
944
+ RESPONSE_PREFIX
945
+ end
946
+
947
+ class QueueProxy
948
+
949
+ class << self
950
+
951
+ def format_join_hash_key_arguments(options)
952
+
953
+ bad_argument = lambda do |(key, value)|
954
+ raise ArgumentError, "Unrecognize value for #{key.inspect} -- #{value.inspect}"
955
+ end
956
+
957
+ # Direct Queue() arguments:
958
+ timeout = options.delete :timeout
959
+ announcement = options.delete :announce
960
+
961
+ # Terse single-character options
962
+ ring_style = options.delete :play
963
+ allow_hangup = options.delete :allow_hangup
964
+ allow_transfer = options.delete :allow_transfer
965
+
966
+ raise ArgumentError, "Unrecognized args to join!: #{options.inspect}" if options.any?
967
+
968
+ ring_style = case ring_style
969
+ when :ringing: 'r'
970
+ when :music: ''
971
+ when nil
972
+ else bad_argument[:play => ring_style]
973
+ end.to_s
974
+
975
+ allow_hangup = case allow_hangup
976
+ when :caller: 'H'
977
+ when :agent: 'h'
978
+ when :everyone: 'Hh'
979
+ when nil
980
+ else bad_argument[:allow_hangup => allow_hangup]
981
+ end.to_s
982
+
983
+ allow_transfer = case allow_transfer
984
+ when :caller: 'T'
985
+ when :agent: 't'
986
+ when :everyone: 'Tt'
987
+ when nil
988
+ else bad_argument[:allow_transfer => allow_transfer]
989
+ end.to_s
990
+
991
+ terse_character_options = ring_style + allow_transfer + allow_hangup
992
+
993
+ [terse_character_options, '', announcement, timeout].map(&:to_s)
994
+ end
995
+
996
+ end
997
+
998
+ attr_reader :name, :environment
999
+ def initialize(name, environment)
1000
+ @name, @environment = name, environment
1001
+ end
1002
+
1003
+ # Makes the current channel join the queue. Below are explanations of the recognized Hash-key
1004
+ # arguments supported by this method.
1005
+ #
1006
+ # :timeout - The number of seconds to wait for an agent to answer
1007
+ # :play - Can be :ringing or :music.
1008
+ # :announce - A sound file to play instead of the normal queue announcement.
1009
+ # :allow_transfer - Can be :caller, :agent, or :everyone. Allow someone to transfer the call.
1010
+ # :allow_hangup - Can be :caller, :agent, or :everyone. Allow someone to hangup with the * key.
1011
+ #
1012
+ # Usage examples:
1013
+ #
1014
+ # - queue('sales').join!
1015
+ # - queue('sales').join! :timeout => 1.minute
1016
+ # - queue('sales').join! :play => :music
1017
+ # - queue('sales').join! :play => :ringing
1018
+ # - queue('sales').join! :announce => "custom/special-queue-announcement"
1019
+ # - queue('sales').join! :allow_transfer => :caller
1020
+ # - queue('sales').join! :allow_transfer => :agent
1021
+ # - queue('sales').join! :allow_hangup => :caller
1022
+ # - queue('sales').join! :allow_hangup => :agent
1023
+ # - queue('sales').join! :allow_hangup => :everyone
1024
+ # - queue('sales').join! :allow_transfer => :agent, :timeout => 30.seconds,
1025
+ def join!(options={})
1026
+ environment.execute("queue", name, *self.class.format_join_hash_key_arguments(options))
1027
+ normalize_queue_status_variable environment.variable("QUEUESTATUS")
1028
+ end
1029
+
1030
+ def agents(options={})
1031
+ cached = options.has_key?(:cache) ? options.delete(:cache) : true
1032
+ raise ArgumentError, "Unrecognized arguments to agents(): #{options.inspect}" if options.keys.any?
1033
+ if cached
1034
+ @cached_proxy ||= QueueAgentsListProxy.new(self, true)
1035
+ else
1036
+ @uncached_proxy ||= QueueAgentsListProxy.new(self, false)
1037
+ end
1038
+ end
1039
+
1040
+ def waiting_count
1041
+ raise QueueDoesNotExistError.new(name) unless exists?
1042
+ environment.variable("QUEUE_WAITING_COUNT(#{name})").to_i
1043
+ end
1044
+
1045
+ def empty?
1046
+ waiting_count == 0
1047
+ end
1048
+
1049
+ def any?
1050
+ waiting_count > 0
1051
+ end
1052
+
1053
+ def exists?
1054
+ environment.execute('RemoveQueueMember', name, 'SIP/AdhearsionQueueExistenceCheck')
1055
+ environment.variable("RQMSTATUS") != 'NOSUCHQUEUE'
1056
+ end
1057
+
1058
+ private
1059
+
1060
+ def normalize_queue_status_variable(variable)
1061
+ returning variable.downcase.to_sym do |queue_status|
1062
+ raise QueueDoesNotExistError.new(name) if queue_status == :unknown
1063
+ end
1064
+ end
1065
+
1066
+ class QueueAgentsListProxy
1067
+
1068
+ include Enumerable
1069
+
1070
+ attr_reader :proxy, :agents
1071
+ def initialize(proxy, cached=false)
1072
+ @proxy = proxy
1073
+ @cached = cached
1074
+ end
1075
+
1076
+ def count
1077
+ if cached? && @cached_count
1078
+ @cached_count
1079
+ else
1080
+ @cached_count = proxy.environment.variable("QUEUE_MEMBER_COUNT(#{proxy.name})").to_i
1081
+ end
1082
+ end
1083
+ alias size count
1084
+ alias length count
1085
+
1086
+ # Supported Hash-key arguments are :penalty and :name. The :name value will be viewable in
1087
+ # the queue_log. The :penalty is the penalty assigned to this agent for answering calls on
1088
+ # this queue
1089
+ def new(*args)
1090
+
1091
+ options = args.last.kind_of?(Hash) ? args.pop : {}
1092
+ interface = args.shift || ''
1093
+
1094
+ raise ArgumentError, "You may only supply an interface and a Hash argument!" if args.any?
1095
+
1096
+ penalty = options.delete(:penalty) || ''
1097
+ name = options.delete(:name) || ''
1098
+
1099
+ raise ArgumentError, "Unrecognized argument(s): #{options.inspect}" if options.any?
1100
+
1101
+ proxy.environment.execute("AddQueueMember", proxy.name, interface, penalty, '', name)
1102
+
1103
+ case proxy.environment.variable("AQMSTATUS")
1104
+ when "ADDED" : true
1105
+ when "MEMBERALREADY" : false
1106
+ when "NOSUCHQUEUE" : raise QueueDoesNotExistError.new(proxy.name)
1107
+ else
1108
+ raise "UNRECOGNIZED AQMSTATUS VALUE!"
1109
+ end
1110
+
1111
+ # TODO: THIS SHOULD RETURN AN AGENT INSTANCE
1112
+ end
1113
+
1114
+ # Logs a pre-defined agent into this queue and waits for calls. Pass in :silent => true to stop
1115
+ # the message which says "Agent logged in".
1116
+ def login!(*args)
1117
+ options = args.last.kind_of?(Hash) ? args.pop : {}
1118
+
1119
+ silent = options.delete(:silent).equal?(false) ? '' : 's'
1120
+ id = args.shift
1121
+ id &&= AgentProxy.id_from_agent_channel(id)
1122
+ raise ArgumentError, "Unrecognized Hash options to login(): #{options.inspect}" if options.any?
1123
+ raise ArgumentError, "Unrecognized argument to login(): #{args.inspect}" if args.any?
1124
+
1125
+ proxy.environment.execute('AgentLogin', id, silent)
1126
+ end
1127
+
1128
+ # Removes the current channel from this queue
1129
+ def logout!
1130
+ # TODO: DRY this up. Repeated in the AgentProxy...
1131
+ proxy.environment.execute 'RemoveQueueMember', proxy.name
1132
+ case proxy.environment.variable("RQMSTATUS")
1133
+ when "REMOVED" : true
1134
+ when "NOTINQUEUE" : false
1135
+ when "NOSUCHQUEUE"
1136
+ raise QueueDoesNotExistError.new(proxy.name)
1137
+ else
1138
+ raise "Unrecognized RQMSTATUS variable!"
1139
+ end
1140
+ end
1141
+
1142
+ def each(&block)
1143
+ check_agent_cache!
1144
+ agents.each(&block)
1145
+ end
1146
+
1147
+ def first
1148
+ check_agent_cache!
1149
+ agents.first
1150
+ end
1151
+
1152
+ def last
1153
+ check_agent_cache!
1154
+ agents.last
1155
+ end
1156
+
1157
+ def cached?
1158
+ @cached
1159
+ end
1160
+
1161
+ def to_a
1162
+ check_agent_cache!
1163
+ @agents
1164
+ end
1165
+
1166
+ private
1167
+
1168
+ def check_agent_cache!
1169
+ if cached?
1170
+ load_agents! unless agents
1171
+ else
1172
+ load_agents!
1173
+ end
1174
+ end
1175
+
1176
+ def load_agents!
1177
+ raw_data = proxy.environment.variable "QUEUE_MEMBER_LIST(#{proxy.name})"
1178
+ @agents = raw_data.split(',').map(&:strip).reject(&:empty?).map do |agent|
1179
+ AgentProxy.new(agent, proxy)
1180
+ end
1181
+ @cached_count = @agents.size
1182
+ end
1183
+
1184
+ end
1185
+
1186
+ class AgentProxy
1187
+
1188
+ SUPPORTED_METADATA_NAMES = %w[status password name mohclass exten channel] unless defined? SUPPORTED_METADATA_NAMES
1189
+
1190
+ class << self
1191
+ def id_from_agent_channel(id)
1192
+ id = id.to_s
1193
+ id.starts_with?('Agent/') ? id[%r[^Agent/(.+)$],1] : id
1194
+ end
1195
+ end
1196
+
1197
+ attr_reader :interface, :proxy, :queue_name, :id
1198
+ def initialize(interface, proxy)
1199
+ @interface = interface
1200
+ @id = self.class.id_from_agent_channel interface
1201
+ @proxy = proxy
1202
+ @queue_name = proxy.name
1203
+ end
1204
+
1205
+ def remove!
1206
+ proxy.environment.execute 'RemoveQueueMember', queue_name, interface
1207
+ case proxy.environment.variable("RQMSTATUS")
1208
+ when "REMOVED" : true
1209
+ when "NOTINQUEUE" : false
1210
+ when "NOSUCHQUEUE"
1211
+ raise QueueDoesNotExistError.new(queue_name)
1212
+ else
1213
+ raise "Unrecognized RQMSTATUS variable!"
1214
+ end
1215
+ end
1216
+
1217
+ # Pauses the given agent for this queue only. If you wish to pause this agent
1218
+ # for all queues, pass in :everywhere => true. Returns true if the agent was
1219
+ # successfully paused and false if the agent was not found.
1220
+ def pause!(options={})
1221
+ everywhere = options.delete(:everywhere)
1222
+ args = [(everywhere ? nil : queue_name), interface]
1223
+ proxy.environment.execute('PauseQueueMember', *args)
1224
+ case proxy.environment.variable("PQMSTATUS")
1225
+ when "PAUSED" : true
1226
+ when "NOTFOUND" : false
1227
+ else
1228
+ raise "Unrecognized PQMSTATUS value!"
1229
+ end
1230
+ end
1231
+
1232
+ # Pauses the given agent for this queue only. If you wish to pause this agent
1233
+ # for all queues, pass in :everywhere => true. Returns true if the agent was
1234
+ # successfully paused and false if the agent was not found.
1235
+ def unpause!(options={})
1236
+ everywhere = options.delete(:everywhere)
1237
+ args = [(everywhere ? nil : queue_name), interface]
1238
+ proxy.environment.execute('UnpauseQueueMember', *args)
1239
+ case proxy.environment.variable("UPQMSTATUS")
1240
+ when "UNPAUSED" : true
1241
+ when "NOTFOUND" : false
1242
+ else
1243
+ raise "Unrecognized UPQMSTATUS value!"
1244
+ end
1245
+ end
1246
+
1247
+ # Returns true/false depending on whether this agent is logged in.
1248
+ def logged_in?
1249
+ status == 'LOGGEDIN'
1250
+ end
1251
+
1252
+ private
1253
+
1254
+ def status
1255
+ agent_metadata 'status'
1256
+ end
1257
+
1258
+ def agent_metadata(data_name)
1259
+ data_name = data_name.to_s.downcase
1260
+ raise ArgumentError, "unrecognized agent metadata name #{data_name}" unless SUPPORTED_METADATA_NAMES.include? data_name
1261
+ proxy.environment.variable "AGENT(#{id}:#{data_name})"
1262
+ end
1263
+
1264
+ end
1265
+
1266
+ class QueueDoesNotExistError < Exception
1267
+ def initialize(queue_name)
1268
+ super "Queue #{queue_name} does not exist!"
1269
+ end
1270
+ end
1271
+
1272
+ end
1273
+
1274
+ module MenuDigitResponse
1275
+ def timed_out?
1276
+ eql? 0.chr
1277
+ end
1278
+ end
1279
+
1280
+ module SpeechEngines
1281
+
1282
+ class InvalidSpeechEngine < Exception; end
1283
+
1284
+ class << self
1285
+ def cepstral(text)
1286
+ puts "in ceptral"
1287
+ puts escape(text)
1288
+ end
1289
+
1290
+ def festival(text)
1291
+ raise NotImplementedError
1292
+ end
1293
+
1294
+ def none(text)
1295
+ raise InvalidSpeechEngine, "No speech engine selected. You must specify one in your Adhearsion config file."
1296
+ end
1297
+
1298
+ def method_missing(engine_name, text)
1299
+ raise InvalidSpeechEngine, "Unsupported speech engine #{engine_name} for speaking '#{text}'"
1300
+ end
1301
+
1302
+ private
1303
+
1304
+ def escape(text)
1305
+ "%p" % text
1306
+ end
1307
+
1308
+ end
1309
+ end
1310
+
1311
+ end
1312
+ end
1313
+ end
1314
+ end