rene-adhearsion 0.8.6

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