eric-adhearsion 0.7.999

Sign up to get free protection for your applications and to get access to all the features.
Files changed (150) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +456 -0
  3. data/Manifest.txt +149 -0
  4. data/README.txt +6 -0
  5. data/Rakefile +48 -0
  6. data/ahn_generators/component/USAGE +5 -0
  7. data/ahn_generators/component/component_generator.rb +57 -0
  8. data/ahn_generators/component/templates/configuration.rb +0 -0
  9. data/ahn_generators/component/templates/lib/lib.rb.erb +3 -0
  10. data/ahn_generators/component/templates/test/test.rb.erb +12 -0
  11. data/ahn_generators/component/templates/test/test_helper.rb +14 -0
  12. data/app_generators/ahn/USAGE +5 -0
  13. data/app_generators/ahn/ahn_generator.rb +76 -0
  14. data/app_generators/ahn/templates/.ahnrc +12 -0
  15. data/app_generators/ahn/templates/README +8 -0
  16. data/app_generators/ahn/templates/Rakefile +3 -0
  17. data/app_generators/ahn/templates/components/simon_game/configuration.rb +0 -0
  18. data/app_generators/ahn/templates/components/simon_game/lib/simon_game.rb +61 -0
  19. data/app_generators/ahn/templates/components/simon_game/test/test_helper.rb +14 -0
  20. data/app_generators/ahn/templates/components/simon_game/test/test_simon_game.rb +31 -0
  21. data/app_generators/ahn/templates/config/startup.rb +53 -0
  22. data/app_generators/ahn/templates/dialplan.rb +4 -0
  23. data/bin/ahn +28 -0
  24. data/bin/ahnctl +68 -0
  25. data/bin/jahn +32 -0
  26. data/lib/adhearsion/blank_slate.rb +5 -0
  27. data/lib/adhearsion/cli.rb +106 -0
  28. data/lib/adhearsion/component_manager.rb +277 -0
  29. data/lib/adhearsion/core_extensions/all.rb +9 -0
  30. data/lib/adhearsion/core_extensions/array.rb +0 -0
  31. data/lib/adhearsion/core_extensions/custom_daemonizer.rb +45 -0
  32. data/lib/adhearsion/core_extensions/global.rb +1 -0
  33. data/lib/adhearsion/core_extensions/guid.rb +5 -0
  34. data/lib/adhearsion/core_extensions/hash.rb +0 -0
  35. data/lib/adhearsion/core_extensions/metaprogramming.rb +17 -0
  36. data/lib/adhearsion/core_extensions/numeric.rb +4 -0
  37. data/lib/adhearsion/core_extensions/proc.rb +0 -0
  38. data/lib/adhearsion/core_extensions/pseudo_uuid.rb +11 -0
  39. data/lib/adhearsion/core_extensions/publishable.rb +73 -0
  40. data/lib/adhearsion/core_extensions/relationship_properties.rb +40 -0
  41. data/lib/adhearsion/core_extensions/string.rb +26 -0
  42. data/lib/adhearsion/core_extensions/thread.rb +13 -0
  43. data/lib/adhearsion/core_extensions/thread_safety.rb +7 -0
  44. data/lib/adhearsion/core_extensions/time.rb +0 -0
  45. data/lib/adhearsion/distributed/gateways/dbus_gateway.rb +0 -0
  46. data/lib/adhearsion/distributed/gateways/osa_gateway.rb +0 -0
  47. data/lib/adhearsion/distributed/gateways/rest_gateway.rb +9 -0
  48. data/lib/adhearsion/distributed/gateways/soap_gateway.rb +9 -0
  49. data/lib/adhearsion/distributed/gateways/xmlrpc_gateway.rb +9 -0
  50. data/lib/adhearsion/distributed/peer_finder.rb +0 -0
  51. data/lib/adhearsion/distributed/remote_cli.rb +0 -0
  52. data/lib/adhearsion/hooks.rb +57 -0
  53. data/lib/adhearsion/host_definitions.rb +63 -0
  54. data/lib/adhearsion/initializer/asterisk.rb +59 -0
  55. data/lib/adhearsion/initializer/configuration.rb +202 -0
  56. data/lib/adhearsion/initializer/database.rb +92 -0
  57. data/lib/adhearsion/initializer/drb.rb +25 -0
  58. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  59. data/lib/adhearsion/initializer/paths.rb +55 -0
  60. data/lib/adhearsion/initializer/rails.rb +40 -0
  61. data/lib/adhearsion/initializer.rb +217 -0
  62. data/lib/adhearsion/logging.rb +92 -0
  63. data/lib/adhearsion/services/scheduler.rb +5 -0
  64. data/lib/adhearsion/tasks/database.rb +5 -0
  65. data/lib/adhearsion/tasks/generating.rb +20 -0
  66. data/lib/adhearsion/tasks/lint.rb +4 -0
  67. data/lib/adhearsion/tasks/testing.rb +37 -0
  68. data/lib/adhearsion/tasks.rb +15 -0
  69. data/lib/adhearsion/version.rb +9 -0
  70. data/lib/adhearsion/voip/asterisk/agi_server.rb +78 -0
  71. data/lib/adhearsion/voip/asterisk/ami/actions.rb +238 -0
  72. data/lib/adhearsion/voip/asterisk/ami/machine.rb +871 -0
  73. data/lib/adhearsion/voip/asterisk/ami/machine.rl +109 -0
  74. data/lib/adhearsion/voip/asterisk/ami/parser.rb +262 -0
  75. data/lib/adhearsion/voip/asterisk/ami.rb +147 -0
  76. data/lib/adhearsion/voip/asterisk/commands.rb +1182 -0
  77. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  78. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  79. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  80. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  81. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  82. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  83. data/lib/adhearsion/voip/asterisk.rb +4 -0
  84. data/lib/adhearsion/voip/call.rb +391 -0
  85. data/lib/adhearsion/voip/call_routing.rb +64 -0
  86. data/lib/adhearsion/voip/commands.rb +9 -0
  87. data/lib/adhearsion/voip/constants.rb +39 -0
  88. data/lib/adhearsion/voip/conveniences.rb +18 -0
  89. data/lib/adhearsion/voip/dial_plan.rb +205 -0
  90. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  91. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  92. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  93. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  94. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +75 -0
  95. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  96. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  97. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  98. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  99. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  100. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  101. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  102. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  103. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  104. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  105. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  106. data/lib/adhearsion.rb +31 -0
  107. data/script/destroy +14 -0
  108. data/script/generate +14 -0
  109. data/spec/fixtures/dialplan.rb +3 -0
  110. data/spec/initializer/test_configuration.rb +267 -0
  111. data/spec/initializer/test_loading.rb +162 -0
  112. data/spec/initializer/test_paths.rb +43 -0
  113. data/spec/silence.rb +10 -0
  114. data/spec/test_ahn_command.rb +149 -0
  115. data/spec/test_code_quality.rb +87 -0
  116. data/spec/test_component_manager.rb +97 -0
  117. data/spec/test_constants.rb +8 -0
  118. data/spec/test_drb.rb +104 -0
  119. data/spec/test_helper.rb +94 -0
  120. data/spec/test_hooks.rb +37 -0
  121. data/spec/test_host_definitions.rb +79 -0
  122. data/spec/test_initialization.rb +105 -0
  123. data/spec/test_logging.rb +80 -0
  124. data/spec/test_relationship_properties.rb +54 -0
  125. data/spec/voip/asterisk/ami_response_definitions.rb +23 -0
  126. data/spec/voip/asterisk/config_file_generators/test_agents.rb +253 -0
  127. data/spec/voip/asterisk/config_file_generators/test_queues.rb +325 -0
  128. data/spec/voip/asterisk/config_file_generators/test_voicemail.rb +306 -0
  129. data/spec/voip/asterisk/menu_command/test_calculated_match.rb +111 -0
  130. data/spec/voip/asterisk/menu_command/test_matchers.rb +98 -0
  131. data/spec/voip/asterisk/mock_ami_server.rb +176 -0
  132. data/spec/voip/asterisk/test_agi_server.rb +451 -0
  133. data/spec/voip/asterisk/test_ami.rb +227 -0
  134. data/spec/voip/asterisk/test_commands.rb +2006 -0
  135. data/spec/voip/asterisk/test_config_manager.rb +129 -0
  136. data/spec/voip/dsl/dispatcher_spec_helper.rb +45 -0
  137. data/spec/voip/dsl/test_dialing_dsl.rb +268 -0
  138. data/spec/voip/dsl/test_dispatcher.rb +82 -0
  139. data/spec/voip/dsl/test_parser.rb +87 -0
  140. data/spec/voip/freeswitch/test_basic_connection_manager.rb +39 -0
  141. data/spec/voip/freeswitch/test_inbound_connection_manager.rb +39 -0
  142. data/spec/voip/freeswitch/test_oes_server.rb +9 -0
  143. data/spec/voip/test_call_routing.rb +127 -0
  144. data/spec/voip/test_dialplan_manager.rb +372 -0
  145. data/spec/voip/test_numerical_string.rb +48 -0
  146. data/spec/voip/test_phone_number.rb +36 -0
  147. data/test/test_ahn_generator.rb +59 -0
  148. data/test/test_component_generator.rb +52 -0
  149. data/test/test_generator_helper.rb +20 -0
  150. metadata +254 -0
@@ -0,0 +1,1182 @@
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
+ DIAL_STATUSES = Hash.new(:unknown).merge(:answer => :answered,
11
+ :congestion => :congested,
12
+ :busy => :busy,
13
+ :cancel => :cancelled,
14
+ :noanswer => :unanswered,
15
+ :cancelled => :cancelled,
16
+ :chanunavail => :channel_unavailable) unless defined? DIAL_STATUSES
17
+
18
+ DYNAMIC_FEATURE_EXTENSIONS = {
19
+ :attended_transfer => lambda do |options|
20
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
21
+ extend_dynamic_features_with "atxfer"
22
+ end,
23
+ :blind_transfer => lambda do
24
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
25
+ extend_dynamic_features_with 'blindxfer'
26
+ end
27
+ } unless defined? DYNAMIC_FEATURE_EXTENSIONS
28
+
29
+ def write(message)
30
+ to_pbx.print(message)
31
+ end
32
+
33
+ def read
34
+ returning from_pbx.gets do |message|
35
+ ahn_log.agi.debug "<<< #{message}"
36
+ end
37
+ end
38
+
39
+ def raw_response(message = nil)
40
+ ahn_log.agi.debug ">>> #{message}"
41
+ write message if message
42
+ read
43
+ end
44
+
45
+ def answer
46
+ raw_response "ANSWER"
47
+ true
48
+ end
49
+
50
+ def execute(application, *arguments)
51
+ result = raw_response("EXEC #{application} #{arguments * '|'}")
52
+ return false if error?(result)
53
+ result
54
+ end
55
+
56
+ # Hangs up the current channel.
57
+ def hangup
58
+ raw_response 'HANGUP'
59
+ end
60
+
61
+ # Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
62
+ # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. When playing
63
+ # numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
64
+ # is pronounced as "one hundred" instead of "one zero zero".
65
+ #
66
+ # Note: it's not necessary to supply a sound file extension; Asterisk will try to find a sound
67
+ # file encoded using the current channel's codec, if one exists. If not, it will transcode from
68
+ # the default codec (GSM). Asterisk stores its sound files in /var/lib/asterisk/sounds.
69
+ #
70
+ # Usage:
71
+ #
72
+ # play 'hello-world'
73
+ # play Time.now
74
+ # play %w"a-connect-charge-of 22 cents-per-minute will-apply"
75
+ # play "you-sound-cute", "what-are-you-wearing"
76
+ #
77
+ def play(*arguments)
78
+ arguments.flatten.each do |argument|
79
+ play_time(argument) || play_numeric(argument) || play_string(argument)
80
+ end
81
+ end
82
+
83
+ # Records a sound file with the given name. If no filename is specified a file named by Asterisk
84
+ # will be created and returned. Else the given filename will be returned. If a relative path is
85
+ # given, the file will be saved in the default Asterisk sound directory, /var/lib/spool/asterisk
86
+ # by default.
87
+ #
88
+ # Silence and maxduration is specified in seconds.
89
+ #
90
+ # Usage:
91
+ # record
92
+ # record '/path/to/my-file.gsm'
93
+ # record 'my-file.gsm', :silence => 5, :maxduration => 120
94
+ #
95
+ def record(*args)
96
+ options = args.last.kind_of?(Hash) ? args.pop : {}
97
+ filename = args.shift || "/tmp/recording_%d.gsm"
98
+ silence = options.delete(:silence) || 0
99
+ maxduration = options.delete(:maxduration) || 0
100
+
101
+ execute("Record", filename, silence, maxduration)
102
+
103
+ # If the user hangs up before the recording is entered, -1 is returned and RECORDED_FILE
104
+ # will not contain the name of the file, even though it IS in fact recorded.
105
+ filename.index("%d") ? get_variable('RECORDED_FILE') : filename
106
+ end
107
+
108
+ # Simulates pressing the specified digits over the current channel. Can be used to
109
+ # traverse a phone menu.
110
+ def dtmf(digits)
111
+ execute "SendDTMF", digits.to_s
112
+ end
113
+
114
+ def with_next_message(&block)
115
+ raise LocalJumpError, "Must supply a block" unless block_given?
116
+ block.call @call.inbox.pop
117
+ end
118
+
119
+ def messages_waiting?
120
+ not @call.inbox.empty?
121
+ end
122
+
123
+ # = Menu Command
124
+ #
125
+ # The following documentation was derived from this blog post on Jay Phillips' blog:
126
+ #
127
+ # http://jicksta.com/articles/2008/02/11/menu-command
128
+ #
129
+ # The menu() command solves the problem of building enormous input-fetching state machines in Ruby without first-class
130
+ # message passing facilities or an external DSL.
131
+ #
132
+ # Here is an example dialplan which uses the menu() command effectively.
133
+ #
134
+ # from_pstn {
135
+ # menu 'welcome', 'for-spanish-press-8', 'main-ivr',
136
+ # :timeout => 8.seconds, :tries => 3 do |link|
137
+ # link.shipment_status 1
138
+ # link.ordering 2
139
+ # link.representative 4
140
+ # link.spanish 8
141
+ # link.employee 900..999
142
+ #
143
+ # link.on_invalid { play 'invalid' }
144
+ #
145
+ # link.on_premature_timeout do |str|
146
+ # play 'sorry'
147
+ # end
148
+ #
149
+ # link.on_failure do
150
+ # play 'goodbye'
151
+ # hangup
152
+ # end
153
+ # end
154
+ # }
155
+ #
156
+ # shipment_status {
157
+ # # Fetch a tracking number and pass it to a web service.
158
+ # }
159
+ #
160
+ # ordering {
161
+ # # Enter another menu that lets them enter credit card
162
+ # # information and place their order over the phone.
163
+ # }
164
+ #
165
+ # representative {
166
+ # # Place the caller into a queue
167
+ # }
168
+ #
169
+ # spanish {
170
+ # # Special options for the spanish menu.
171
+ # }
172
+ #
173
+ # employee {
174
+ # dial "SIP/#{extension}" # Overly simplistic
175
+ # }
176
+ #
177
+ # The main detail to note is the declarations within the menu() command’s block. Each line seems to refer to a link object
178
+ # executing a seemingly arbitrary method with an argument that’s either a number or a Range of numbers. The +link+ object
179
+ # collects these arbitrary method invocations and assembles a set of rules. The seemingly arbitrary method name is the name
180
+ # of the context to which the menu should jump in case its argument (the pattern) is found to be a match.
181
+ #
182
+ # With these context names and patterns defined, the +menu()+ command plays in sequence the sound files you supply as
183
+ # arguments, stopping playback abruptly if the user enters a digit. If no digits were pressed when the files finish playing,
184
+ # it waits +:timeout+ seconds. If no digits are pressed after the timeout, it executes the +on_premature_timeout+ hook you
185
+ # define (if any) and then tries again a maximum of +:tries+ times. If digits are pressed that result in no possible match,
186
+ # it executes the +on_invalid+ hook. When/if all tries are exhausted with no positive match, it executes the +on_failure+
187
+ # hook after the other hook (e.g. +on_invalid+, then +on_failure+).
188
+ #
189
+ # When the +menu()+ state machine runs through the defined rules, it must distinguish between exact and potential matches.
190
+ # It’s important to understand the differences between these and how they affect the overall outcome:
191
+ #
192
+ # |---------------|-------------------|------------------------------------------------------|
193
+ # | exact matches | potential matches | result |
194
+ # |---------------|-------------------|------------------------------------------------------|
195
+ # | 0 | 0 | Fail and start over |
196
+ # | 1 | 0 | Match found! |
197
+ # | 0 | >0 | Get another digit |
198
+ # | >1 | 0 | Go with the first exact match |
199
+ # | 1 | >0 | Get another digit. If timeout, use exact match |
200
+ # | >1 | >0 | Get another digit. If timeout, use first exact match |
201
+ # |---------------|-------------------|------------------------------------------------------|
202
+ #
203
+ # == Database integration
204
+ #
205
+ # To do database integration, I recommend programatically executing methods on the link object within the block. For example:
206
+ #
207
+ # menu do |link|
208
+ # for employee in Employee.find(:all)
209
+ # link.internal employee.extension
210
+ # end
211
+ # end
212
+ #
213
+ # or this more efficient and Rubyish way
214
+ #
215
+ # menu do |link|
216
+ # link.internal *Employee.find(:all).map(&:extension)
217
+ # end
218
+ #
219
+ # If this second example seems like too much Ruby magic, let me explain — +Employee.find(:all)+ effectively does a “SELECT *
220
+ # FROM employees” on the database with ActiveRecord, returning (what you’d think is) an Array. The +map(&:extension)+ is
221
+ # fanciness that means “replace every instance in this Array with the result of calling extension on that object”. Now we
222
+ # have an Array of every extension in the database. The splat operator (*) before the argument changes the argument from
223
+ # being one argument (an Array) into a sequence of n arguments, where n is the number of items in the Array it’s “splatting”.
224
+ # Lastly, these arguments are passed to the internal method, the name of a context which will handle dialing this user if one
225
+ # of the supplied patterns matches.
226
+ #
227
+ # == Handling a successful pattern match
228
+ #
229
+ # Which brings me to another important note. Let’s say that the user’s input successfully matched one of the patterns
230
+ # returned by that Employe.find... magic. When it jumps to the internal context, that context can access the variable entered
231
+ # through the extension variable. This was a tricky design decision that I think, overall, works great. It makes the +menu()+
232
+ # command feel much more first-class in the Adhearsion dialplan grammar and decouples the receiving context from the menu
233
+ # 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
234
+ # point, making menu() effectively a pipeline of re-creating the call.
235
+ #
236
+ def menu(*args, &block)
237
+ options = args.last.kind_of?(Hash) ? args.pop : {}
238
+ sound_files = args.flatten
239
+
240
+ menu_instance = Menu.new(options, &block)
241
+
242
+ initial_digit_prompt = sound_files.any?
243
+
244
+ # This method is basically one big begin/rescue block. When we start the Menu state machine by continue()ing, the state
245
+ # machine will pass messages back to this method in the form of Exceptions. This decoupling allows the menu system to
246
+ # work on, say, Freeswitch and Asterisk both.
247
+ begin
248
+ if menu_instance.should_continue?
249
+ menu_instance.continue
250
+ else
251
+ menu_instance.execute_failure_hook
252
+ return :failed
253
+ end
254
+ rescue Menu::MenuResult => result_of_menu
255
+ case result_of_menu
256
+ when Menu::MenuResultInvalid
257
+ menu_instance.execute_invalid_hook
258
+ menu_instance.restart!
259
+ when Menu::MenuGetAnotherDigit
260
+
261
+ next_digit = play_sound_files_for_menu(menu_instance, sound_files)
262
+ if next_digit
263
+ menu_instance << next_digit
264
+ else
265
+ # The user timed out entering another digit!
266
+ case result_of_menu
267
+ when Menu::MenuGetAnotherDigitOrFinish
268
+ # This raises a ControlPassingException
269
+ jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
270
+ when Menu::MenuGetAnotherDigitOrTimeout
271
+ # This should execute premature_timeout AND reset if the number of retries
272
+ # has not been exhausted.
273
+ menu_instance.execute_timeout_hook
274
+ menu_instance.restart!
275
+ end
276
+ end
277
+ when Menu::MenuResultFound
278
+ jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
279
+ else
280
+ raise "Unrecognized MenuResult! This may be a bug!"
281
+ end
282
+
283
+ # Retry will re-execute the begin block, preserving our changes to the menu_instance object.
284
+ retry
285
+
286
+ end
287
+ end
288
+
289
+ # This method is used to receive keypad input from the user. Digits are collected
290
+ # via DTMF (keypad) input until one of three things happens:
291
+ #
292
+ # 1. The number of digits you specify as the first argument is collected
293
+ # 2. The timeout you specify with the :timeout option elapses.
294
+ # 3. The "#" key (or the key you specify with :accept_key) is pressed
295
+ #
296
+ # Usage examples
297
+ #
298
+ # input # Receives digits until the caller presses the "#" key
299
+ # input 3 # Receives three digits. Can be 0-9, * or #
300
+ # input 5, :accept_key => "*" # Receive at most 5 digits, stopping if '*' is pressed
301
+ # input 1, :timeout => 1.minute # Receive a single digit, returning an empty
302
+ # string if the timeout is encountered
303
+ # input 9, :timeout => 7, :accept_key => "0" # Receives nine digits, returning
304
+ # # when the timeout is encountered
305
+ # # or when the "0" key is pressed.
306
+ # input 3, :play => "you-sound-cute"
307
+ # input :play => ["if-this-is-correct-press", 1, "otherwise-press", 2]
308
+ #
309
+ # When specifying files to play, the playback of the sequence of files will stop
310
+ # immediately when the user presses the first digit.
311
+ #
312
+ # The :timeout option works like a digit timeout, therefore each digit pressed
313
+ # causes the timer to reset. This is a much more user-friendly approach than an
314
+ # absolute timeout.
315
+ #
316
+ # Note that when you don't specify a digit limit, the :accept_key becomes "#"
317
+ # because there'd be no other way to end the collection of digits. You can
318
+ # obviously override this by passing in a new key with :accept_key.
319
+ def input(*args)
320
+ options = args.last.kind_of?(Hash) ? args.pop : {}
321
+ number_of_digits = args.shift
322
+
323
+ sound_files = Array options.delete(:play)
324
+ timeout = options.delete(:timeout)
325
+ terminating_key = options.delete(:accept_key)
326
+ terminating_key = if terminating_key
327
+ terminating_key.to_s
328
+ elsif number_of_digits.nil? && !terminating_key.equal?(false)
329
+ '#'
330
+ end
331
+
332
+ if number_of_digits && number_of_digits < 0
333
+ ahn_log.agi.warn "Giving -1 to input() is now deprecated. Don't specify a first " +
334
+ "argument to simulate unlimited digits." if number_of_digits == -1
335
+ raise ArgumentError, "The number of digits must be positive!"
336
+ end
337
+
338
+ buffer = ''
339
+ key = sound_files.any? ? interruptable_play(*sound_files) || '' : wait_for_digit(timeout || -1)
340
+ loop do
341
+ return buffer if key.nil?
342
+ if terminating_key
343
+ if key == terminating_key
344
+ return buffer
345
+ else
346
+ buffer << key
347
+ return buffer if number_of_digits && number_of_digits == buffer.length
348
+ end
349
+ else
350
+ buffer << key
351
+ return buffer if number_of_digits && number_of_digits == buffer.length
352
+ end
353
+ key = wait_for_digit timeout || -1
354
+ end
355
+ end
356
+
357
+ # An alternative to DialplanContextProc#+@. When jumping to a context, it will *not* resume executing
358
+ # the former context when the jumped-to context has finished executing. Make sure you don't have any
359
+ # +ensure+ closures which you expect to execute when the call has finished, as they will run when
360
+ # this method is called.
361
+ #
362
+ # You can optionally override certain dialplan variables when jumping to the context. A popular use of
363
+ # this is to redefine +extension+ (which this method automatically boxes with a PhoneNumber object) so
364
+ # you can effectively "restart" a call (from the perspective of the jumped-to context). When you override
365
+ # variables here, you're effectively blowing away the old variables. If you need them for some reason,
366
+ # you should assign the important ones to an instance variable first before calling this method.
367
+ def jump_to(context, overrides={})
368
+ context = lookup_context_with_name(context) if context.kind_of?(Symbol) || (context.kind_of?(String) && context =~ /^[\w_]+$/)
369
+ raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException unless context.kind_of?(Adhearsion::DialPlan::DialplanContextProc)
370
+
371
+ if overrides.any?
372
+ overrides = overrides.symbolize_keys
373
+ if overrides.has_key?(:extension) && !overrides[:extension].kind_of?(Adhearsion::VoIP::DSL::PhoneNumber)
374
+ overrides[:extension] = Adhearsion::VoIP::DSL::PhoneNumber.new overrides[:extension]
375
+ end
376
+
377
+ overrides.each_pair do |key, value|
378
+ meta_def(key) { value }
379
+ end
380
+ end
381
+
382
+ raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context)
383
+ end
384
+
385
+ def queue(queue_name)
386
+ queue_name = queue_name.to_s
387
+
388
+ @queue_proxy_hash_lock = Mutex.new unless defined? @queue_proxy_hash_lock
389
+ @queue_proxy_hash_lock.synchronize do
390
+ @queue_proxy_hash ||= {}
391
+ if @queue_proxy_hash.has_key? queue_name
392
+ return @queue_proxy_hash[queue_name]
393
+ else
394
+ proxy = @queue_proxy_hash[queue_name] = QueueProxy.new(queue_name, self)
395
+ return proxy
396
+ end
397
+ end
398
+ end
399
+
400
+ # Returns the status of the last dial(). Possible dial
401
+ # statuses include :answer, :busy, :no_answer, :cancelled,
402
+ # :congested, and :channel_unavailable. If :cancel is
403
+ # returned, the caller hung up before the callee picked
404
+ # up. If :congestion is returned, the dialed extension
405
+ # probably doesn't exist. If :channel_unavailable, the callee
406
+ # phone may not be registered.
407
+ def last_dial_status
408
+ DIAL_STATUSES[get_dial_status]
409
+ end
410
+
411
+ # Returns true if your last call to dial() finished with the ANSWER state, as reported
412
+ # by Asterisk. Returns false otherwise
413
+ def last_dial_successful?
414
+ last_dial_status == :answered
415
+ end
416
+
417
+ # Opposite of last_dial_successful?()
418
+ def last_dial_unsuccessful?
419
+ not last_dial_successful?
420
+ end
421
+
422
+ # This feature is presently experimental! Do not use it!
423
+ def speak(text, engine=:none)
424
+ engine = Adhearsion::Configuration::AsteriskConfiguration.speech_engine || engine
425
+ execute SpeechEngines.send(engine, text)
426
+ end
427
+
428
+ # This method is a high-level way of enabling features you create/uncomment from features.conf.
429
+ #
430
+ # Certain Symbol features you enable (as defined in DYNAMIC_FEATURE_EXTENSIONS) have optional
431
+ # arguments that you can also specify here. The usage examples show how to do this.
432
+ #
433
+ # Usage examples:
434
+ #
435
+ # enable_feature :attended_transfer # Enables "atxfer"
436
+ #
437
+ # enable_feature :attended_transfer, :context => "my_dial" # Enables "atxfer" and then
438
+ # # sets "TRANSFER_CONTEXT" to :context's value
439
+ #
440
+ # enable_feature :blind_transfer, :context => 'my_dial' # Enables 'blindxfer' and sets TRANSFER_CONTEXT
441
+ #
442
+ # enable_feature "foobar" # Enables "foobar"
443
+ #
444
+ # enable_feature("dup"); enable_feature("dup") # Enables "dup" only once.
445
+ def enable_feature(feature_name, optional_options=nil)
446
+ if DYNAMIC_FEATURE_EXTENSIONS.has_key? feature_name
447
+ instance_exec(optional_options, &DYNAMIC_FEATURE_EXTENSIONS[feature_name])
448
+ else
449
+ raise ArgumentError, "You cannot supply optional options when the feature name is " +
450
+ "not internally recognized!" if optional_options
451
+ extend_dynamic_features_with feature_name
452
+ end
453
+ end
454
+
455
+ # Disables a feature name specified in features.conf. If you're disabling it, it was probably
456
+ # set by enable_feature().
457
+ def disable_feature(feature_name)
458
+ enabled_features_variable = variable 'DYNAMIC_FEATURES'
459
+ enabled_features = enabled_features_variable.split('#')
460
+ if enabled_features.include? feature_name
461
+ enabled_features.delete feature_name
462
+ variable 'DYNAMIC_FEATURES' => enabled_features.join('#')
463
+ end
464
+ end
465
+
466
+ # Used to join a particular conference with the MeetMe application. To
467
+ # use MeetMe, be sure you have a proper timing device configured on your
468
+ # Asterisk box. MeetMe is Asterisk's built-in conferencing program.
469
+ # More info: http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe
470
+ def join(conference_id, options={})
471
+ conference_id = conference_id.to_s.scan(/\w/).join
472
+ command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
473
+ pin = options[:pin]
474
+ raise ArgumentError, "A conference PIN number must be numerical!" if pin && pin.to_s !~ /^\d+$/
475
+ # The 'd' option of MeetMe creates conferences dynamically.
476
+ command_flags += 'd' unless command_flags.include? 'd'
477
+
478
+ execute "MeetMe", conference_id, command_flags, options[:pin]
479
+ end
480
+
481
+ def get_variable(variable_name)
482
+ result = raw_response("GET VARIABLE #{variable_name}")
483
+ case result
484
+ when "200 result=0"
485
+ return nil
486
+ when /^200 result=1 \((.*)\)$/
487
+ return $LAST_PAREN_MATCH
488
+ end
489
+ end
490
+
491
+ def set_variable(variable_name, value)
492
+ raw_response("SET VARIABLE %s %p" % [variable_name.to_s, value.to_s]) == "200 result=1"
493
+ end
494
+
495
+ def variable(*args)
496
+ if args.last.kind_of? Hash
497
+ assignments = args.pop
498
+ raise ArgumentError, "Can't mix variable setting and fetching!" if args.any?
499
+ assignments.each_pair do |key, value|
500
+ set_variable(key, value)
501
+ end
502
+ else
503
+ if args.size == 1
504
+ get_variable args.first
505
+ else
506
+ args.map { |var| get_variable(var) }
507
+ end
508
+ end
509
+ end
510
+
511
+ def voicemail(*args)
512
+ options_hash = args.last.kind_of?(Hash) ? args.pop : {}
513
+ mailbox_number = args.shift
514
+ greeting_option = options_hash.delete(:greeting)
515
+ skip_option = options_hash.delete(:skip)
516
+ raise ArgumentError, 'You supplied too many arguments!' if mailbox_number && options_hash.any?
517
+ greeting_option = case greeting_option
518
+ when :busy: 'b'
519
+ when :unavailable: 'u'
520
+ when nil: nil
521
+ else raise ArgumentError, "Unrecognized greeting #{greeting_option}"
522
+ end
523
+ skip_option &&= 's'
524
+ options = "#{greeting_option}#{skip_option}"
525
+
526
+ raise ArgumentError, "Mailbox cannot be blank!" if !mailbox_number.nil? && mailbox_number.blank?
527
+ number_with_context = if mailbox_number then mailbox_number else
528
+ raise ArgumentError, "You must supply ONE context name!" if options_hash.size != 1
529
+ context_name, mailboxes = options_hash.to_a.first
530
+ Array(mailboxes).map do |mailbox|
531
+ raise ArgumentError, "Mailbox numbers must be numerical!" unless mailbox.to_s =~ /^\d+$/
532
+ "#{mailbox}@#{context_name}"
533
+ end.join('&')
534
+ end
535
+ execute('voicemail', number_with_context, options)
536
+ case variable('VMSTATUS')
537
+ when 'SUCCESS': true
538
+ when 'USEREXIT': false
539
+ else nil
540
+ end
541
+ end
542
+
543
+ def voicemail_main(options={})
544
+ mailbox, context, folder = options.values_at :mailbox, :context, :folder
545
+ authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
546
+
547
+ folder = if folder
548
+ if folder.to_s =~ /^[\w_]+$/
549
+ "a(#{folder})"
550
+ else
551
+ raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
552
+ end
553
+ elsif folder == ''
554
+ raise "Folder name cannot be an empty String!"
555
+ else
556
+ nil
557
+ end
558
+
559
+ real_mailbox = ""
560
+ real_mailbox << "#{mailbox}" unless mailbox.blank?
561
+ real_mailbox << "@#{context}" unless context.blank?
562
+
563
+ real_options = ""
564
+ real_options << "s" if !authenticate
565
+ real_options << folder unless folder.blank?
566
+
567
+ command_args = [real_mailbox]
568
+ command_args << real_options unless real_options.blank?
569
+ command_args.clear if command_args == [""]
570
+
571
+ execute 'VoiceMailMain', *command_args
572
+ end
573
+
574
+ def check_voicemail
575
+ ahn_log.agi.warn "THE check_voicemail() DIALPLAN METHOD WILL SOON BE DEPRECATED! CHANGE THIS TO voicemail_main() INSTEAD"
576
+ voicemail_main
577
+ end
578
+
579
+ def dial(number, options={})
580
+ *recognized_options = :caller_id, :name, :for, :options, :confirm
581
+
582
+ unrecognized_options = options.keys - recognized_options
583
+ raise ArgumentError, "Unknown dial options: #{unrecognized_options.to_sentence}" if unrecognized_options.any?
584
+ set_caller_id_name options[:name]
585
+ set_caller_id_number options[:caller_id]
586
+ confirm_option = dial_macro_option_compiler options[:confirm]
587
+ all_options = options[:options]
588
+ all_options = all_options ? all_options + confirm_option : confirm_option
589
+ execute "Dial", number, options[:for], all_options
590
+ end
591
+
592
+
593
+ # This implementation of dial() uses the experimental call routing DSL.
594
+ #
595
+ # def dial(number, options={})
596
+ # rules = callable_routes_for number
597
+ # return :no_route if rules.empty?
598
+ # call_attempt_status = nil
599
+ # rules.each do |provider|
600
+ #
601
+ # response = execute "Dial",
602
+ # provider.format_number_for_platform(number),
603
+ # timeout_from_dial_options(options),
604
+ # asterisk_options_from_dial_options(options)
605
+ #
606
+ # call_attempt_status = last_dial_status
607
+ # break if call_attempt_status == :answered
608
+ # end
609
+ # call_attempt_status
610
+ # end
611
+
612
+
613
+ # Speaks the digits given as an argument. For example, "123" is spoken as "one two three".
614
+ def say_digits(digits)
615
+ execute "saydigits", validate_digits(digits)
616
+ end
617
+
618
+ # Returns the number of seconds the given block takes to execute as a Float. This
619
+ # is particularly useful in dialplans for tracking billable time. Note that
620
+ # if the call is hung up during the block, you will need to rescue the
621
+ # exception if you have some mission-critical logic after it with which
622
+ # you're recording this return-value.
623
+ def duration_of
624
+ start_time = Time.now
625
+ yield
626
+ Time.now - start_time
627
+ end
628
+
629
+ protected
630
+
631
+ def wait_for_digit(timeout=-1)
632
+ timeout *= 1_000 if timeout != -1
633
+ result = result_digit_from raw_response("WAIT FOR DIGIT #{timeout.to_i}")
634
+ (result == 0.chr) ? nil : result
635
+ end
636
+
637
+ def interruptable_play(*files)
638
+ files.each do |file|
639
+ result = result_digit_from raw_response("EXEC BACKGROUND #{file}")
640
+ return result if result != 0.chr
641
+ end
642
+ nil
643
+ end
644
+
645
+ def set_caller_id_number(caller_id)
646
+ return unless caller_id
647
+ raise ArgumentError, "Caller ID must be numerical" if caller_id.to_s !~ /^\d+$/
648
+ raw_response %(SET CALLERID %p) % caller_id
649
+ end
650
+
651
+ def set_caller_id_name(caller_id_name)
652
+ return unless caller_id_name
653
+ variable "CALLERID(name)" => caller_id_name
654
+ end
655
+
656
+ def timeout_from_dial_options(options)
657
+ options[:for] || options[:timeout]
658
+ end
659
+
660
+ def asterisk_options_from_dial_options(options)
661
+ # TODO: Will become much more sophisticated soon to handle callerid, etc
662
+ options[:options]
663
+ end
664
+
665
+ def dial_macro_option_compiler(confirm_argument_value)
666
+ defaults = { :macro => 'ahn_dial_confirmer',
667
+ :timeout => 20.seconds,
668
+ :play => "beep",
669
+ :key => '#' }
670
+
671
+ case confirm_argument_value
672
+ when true
673
+ DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument(defaults)
674
+ when false, nil
675
+ ''
676
+ when Proc
677
+ raise NotImplementedError, "Coming in the future, you can do :confirm => my_context."
678
+
679
+ when Hash
680
+ options = defaults.merge confirm_argument_value
681
+ if((confirm_argument_value.keys - defaults.keys).any?)
682
+ raise ArgumentError, "Known options: #{defaults.keys.to_sentence}"
683
+ end
684
+ raise ArgumentError, "Bad macro name!" unless options[:macro].to_s =~ /^[\w_]+$/
685
+ options[:timeout] = case options[:timeout]
686
+ when Fixnum, ActiveSupport::Duration
687
+ options[:timeout]
688
+ when String
689
+ raise ArgumentError, "Timeout must be numerical!" unless options[:timeout] =~ /^\d+$/
690
+ options[:timeout].to_i
691
+ when :none
692
+ 0
693
+ else
694
+ raise ArgumentError, "Unrecognized :timeout! #{options[:timeout].inspect}"
695
+ end
696
+ raise ArgumentError, "Unrecognized DTMF key: #{options[:key]}" unless options[:key].to_s =~ /^[\d#*]$/
697
+ options[:play] = Array(options[:play]).join('++')
698
+ DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument options
699
+
700
+ else
701
+ raise ArgumentError, "Unrecognized :confirm option: #{confirm_argument_value.inspect}!"
702
+ end
703
+ end
704
+
705
+ def result_digit_from(response_string)
706
+ raise ArgumentError, "Can't coerce nil into AGI response! This could be a bug!" unless response_string
707
+ digit = response_string[/^#{response_prefix}(-?\d+(\.\d+)?)/,1]
708
+ digit.to_i.chr if digit && digit.to_s != "-1"
709
+ end
710
+
711
+ def extract_input_from(result)
712
+ return false if error?(result)
713
+ # return false if input_timed_out?(result)
714
+
715
+ # This regexp doesn't match if there was a timeout with no
716
+ # inputted digits, therefore returning nil.
717
+
718
+ result[/^#{response_prefix}([\d*]+)/, 1]
719
+ end
720
+
721
+ def extract_variable_from(result)
722
+ return false if error?(result)
723
+ result[/^#{response_prefix}1 \((.+)\)/, 1]
724
+ end
725
+
726
+ def get_dial_status
727
+ dial_status = variable('DIALSTATUS')
728
+ dial_status ? dial_status.downcase.to_sym : :cancelled
729
+ end
730
+
731
+ def play_time(argument)
732
+ if argument.kind_of? Time
733
+ execute(:sayunixtime, argument.to_i)
734
+ end
735
+ end
736
+
737
+ def play_numeric(argument)
738
+ if argument.kind_of?(Numeric) || argument =~ /^\d+$/
739
+ execute(:saynumber, argument)
740
+ end
741
+ end
742
+
743
+ def play_string(argument)
744
+ execute(:playback, argument)
745
+ end
746
+
747
+ def play_sound_files_for_menu(menu_instance, sound_files)
748
+ digit = nil
749
+ if sound_files.any? && menu_instance.digit_buffer_empty?
750
+ digit = interruptable_play(*sound_files)
751
+ end
752
+ digit || wait_for_digit(menu_instance.timeout)
753
+ end
754
+
755
+ def extend_dynamic_features_with(feature_name)
756
+ current_variable = variable("DYNAMIC_FEATURES") || ''
757
+ enabled_features = current_variable.split '#'
758
+ unless enabled_features.include? feature_name
759
+ enabled_features << feature_name
760
+ variable "DYNAMIC_FEATURES" => enabled_features.join('#')
761
+ end
762
+ end
763
+
764
+ def jump_to_context_with_name(context_name)
765
+ context_lambda = lookup_context_with_name context_name
766
+ raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context_lambda)
767
+ end
768
+
769
+ def lookup_context_with_name(context_name)
770
+ begin
771
+ send context_name
772
+ rescue NameError
773
+ raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException
774
+ end
775
+ end
776
+
777
+ def redefine_extension_to_be(new_extension)
778
+ new_extension = Adhearsion::VoIP::DSL::PhoneNumber.new new_extension
779
+ meta_def(:extension) { new_extension }
780
+ end
781
+
782
+ def to_pbx
783
+ io
784
+ end
785
+
786
+ def from_pbx
787
+ io
788
+ end
789
+
790
+ def validate_digits(digits)
791
+ returning digits.to_s do |digits_as_string|
792
+ raise ArgumentError, "Can only be called with valid digits!" unless digits_as_string =~ /^\d+$/
793
+ end
794
+ end
795
+
796
+ def error?(result)
797
+ result.to_s[/^#{response_prefix}(?:-\d+|0)/]
798
+ end
799
+
800
+ # timeout with pressed digits: 200 result=<digits> (timeout)
801
+ # timeout without pressed digits: 200 result= (timeout)
802
+ # (http://www.voip-info.org/wiki/view/get+data)
803
+ def input_timed_out?(result)
804
+ result.starts_with?(response_prefix) && result.ends_with?('(timeout)')
805
+ end
806
+
807
+ def io
808
+ call.io
809
+ end
810
+
811
+ def response_prefix
812
+ RESPONSE_PREFIX
813
+ end
814
+
815
+ class QueueProxy
816
+
817
+ class << self
818
+
819
+ def format_join_hash_key_arguments(options)
820
+
821
+ bad_argument = lambda do |(key, value)|
822
+ raise ArgumentError, "Unrecognize value for #{key.inspect} -- #{value.inspect}"
823
+ end
824
+
825
+ # Direct Queue() arguments:
826
+ timeout = options.delete :timeout
827
+ announcement = options.delete :announce
828
+
829
+ # Terse single-character options
830
+ ring_style = options.delete :play
831
+ allow_hangup = options.delete :allow_hangup
832
+ allow_transfer = options.delete :allow_transfer
833
+
834
+ raise ArgumentError, "Unrecognized args to join!: #{options.inspect}" if options.any?
835
+
836
+ ring_style = case ring_style
837
+ when :ringing: 'r'
838
+ when :music: ''
839
+ when nil
840
+ else bad_argument[:play => ring_style]
841
+ end.to_s
842
+
843
+ allow_hangup = case allow_hangup
844
+ when :caller: 'H'
845
+ when :agent: 'h'
846
+ when :everyone: 'Hh'
847
+ when nil
848
+ else bad_argument[:allow_hangup => allow_hangup]
849
+ end.to_s
850
+
851
+ allow_transfer = case allow_transfer
852
+ when :caller: 'T'
853
+ when :agent: 't'
854
+ when :everyone: 'Tt'
855
+ when nil
856
+ else bad_argument[:allow_transfer => allow_transfer]
857
+ end.to_s
858
+
859
+ terse_character_options = ring_style + allow_transfer + allow_hangup
860
+
861
+ [terse_character_options, '', announcement, timeout].map(&:to_s)
862
+ end
863
+
864
+ end
865
+
866
+ attr_reader :name, :environment
867
+ def initialize(name, environment)
868
+ @name, @environment = name, environment
869
+ end
870
+
871
+ # Makes the current channel join the queue. Below are explanations of the recognized Hash-key
872
+ # arguments supported by this method.
873
+ #
874
+ # :timeout - The number of seconds to wait for an agent to answer
875
+ # :play - Can be :ringing or :music.
876
+ # :announce - A sound file to play instead of the normal queue announcement.
877
+ # :allow_transfer - Can be :caller, :agent, or :everyone. Allow someone to transfer the call.
878
+ # :allow_hangup - Can be :caller, :agent, or :everyone. Allow someone to hangup with the * key.
879
+ #
880
+ # Usage examples:
881
+ #
882
+ # - queue('sales').join!
883
+ # - queue('sales').join! :timeout => 1.minute
884
+ # - queue('sales').join! :play => :music
885
+ # - queue('sales').join! :play => :ringing
886
+ # - queue('sales').join! :announce => "custom/special-queue-announcement"
887
+ # - queue('sales').join! :allow_transfer => :caller
888
+ # - queue('sales').join! :allow_transfer => :agent
889
+ # - queue('sales').join! :allow_hangup => :caller
890
+ # - queue('sales').join! :allow_hangup => :agent
891
+ # - queue('sales').join! :allow_hangup => :everyone
892
+ # - queue('sales').join! :allow_transfer => :agent, :timeout => 30.seconds,
893
+ def join!(options={})
894
+ environment.execute("queue", name, *self.class.format_join_hash_key_arguments(options))
895
+ normalize_queue_status_variable environment.variable("QUEUESTATUS")
896
+ end
897
+
898
+ def agents(options={})
899
+ cached = options.has_key?(:cache) ? options.delete(:cache) : true
900
+ raise ArgumentError, "Unrecognized arguments to agents(): #{options.inspect}" if options.keys.any?
901
+ if cached
902
+ @cached_proxy ||= QueueAgentsListProxy.new(self, true)
903
+ else
904
+ @uncached_proxy ||= QueueAgentsListProxy.new(self, false)
905
+ end
906
+ end
907
+
908
+ def waiting_count
909
+ raise QueueDoesNotExistError.new(name) unless exists?
910
+ environment.variable("QUEUE_WAITING_COUNT(#{name})").to_i
911
+ end
912
+
913
+ def empty?
914
+ waiting_count == 0
915
+ end
916
+
917
+ def any?
918
+ waiting_count > 0
919
+ end
920
+
921
+ def exists?
922
+ environment.execute('RemoveQueueMember', name, 'SIP/AdhearsionQueueExistenceCheck')
923
+ environment.variable("RQMSTATUS") != 'NOSUCHQUEUE'
924
+ end
925
+
926
+ private
927
+
928
+ def normalize_queue_status_variable(variable)
929
+ returning variable.downcase.to_sym do |queue_status|
930
+ raise QueueDoesNotExistError.new(name) if queue_status == :unknown
931
+ end
932
+ end
933
+
934
+ class QueueAgentsListProxy
935
+
936
+ include Enumerable
937
+
938
+ attr_reader :proxy, :agents
939
+ def initialize(proxy, cached=false)
940
+ @proxy = proxy
941
+ @cached = cached
942
+ end
943
+
944
+ def count
945
+ if cached? && @cached_count
946
+ @cached_count
947
+ else
948
+ @cached_count = proxy.environment.variable("QUEUE_MEMBER_COUNT(#{proxy.name})").to_i
949
+ end
950
+ end
951
+ alias size count
952
+ alias length count
953
+
954
+ # Supported Hash-key arguments are :penalty and :name. The :name value will be viewable in
955
+ # the queue_log. The :penalty is the penalty assigned to this agent for answering calls on
956
+ # this queue
957
+ def new(*args)
958
+
959
+ options = args.last.kind_of?(Hash) ? args.pop : {}
960
+ interface = args.shift || ''
961
+
962
+ raise ArgumentError, "You may only supply an interface and a Hash argument!" if args.any?
963
+
964
+ penalty = options.delete(:penalty) || ''
965
+ name = options.delete(:name) || ''
966
+
967
+ raise ArgumentError, "Unrecognized argument(s): #{options.inspect}" if options.any?
968
+
969
+ proxy.environment.execute("AddQueueMember", proxy.name, interface, penalty, '', name)
970
+
971
+ case proxy.environment.variable("AQMSTATUS")
972
+ when "ADDED" : true
973
+ when "MEMBERALREADY" : false
974
+ when "NOSUCHQUEUE" : raise QueueDoesNotExistError.new(proxy.name)
975
+ else
976
+ raise "UNRECOGNIZED AQMSTATUS VALUE!"
977
+ end
978
+
979
+ # TODO: THIS SHOULD RETURN AN AGENT INSTANCE
980
+ end
981
+
982
+ # Logs a pre-defined agent into this queue and waits for calls. Pass in :silent => true to stop
983
+ # the message which says "Agent logged in".
984
+ def login!(*args)
985
+ options = args.last.kind_of?(Hash) ? args.pop : {}
986
+
987
+ silent = options.delete(:silent).equal?(false) ? '' : 's'
988
+ id = args.shift
989
+ id &&= AgentProxy.id_from_agent_channel(id)
990
+ raise ArgumentError, "Unrecognized Hash options to login(): #{options.inspect}" if options.any?
991
+ raise ArgumentError, "Unrecognized argument to login(): #{args.inspect}" if args.any?
992
+
993
+ proxy.environment.execute('AgentLogin', id, silent)
994
+ end
995
+
996
+ # Removes the current channel from this queue
997
+ def logout!
998
+ # TODO: DRY this up. Repeated in the AgentProxy...
999
+ proxy.environment.execute 'RemoveQueueMember', proxy.name
1000
+ case proxy.environment.variable("RQMSTATUS")
1001
+ when "REMOVED" : true
1002
+ when "NOTINQUEUE" : false
1003
+ when "NOSUCHQUEUE"
1004
+ raise QueueDoesNotExistError.new(proxy.name)
1005
+ else
1006
+ raise "Unrecognized RQMSTATUS variable!"
1007
+ end
1008
+ end
1009
+
1010
+ def each(&block)
1011
+ check_agent_cache!
1012
+ agents.each(&block)
1013
+ end
1014
+
1015
+ def first
1016
+ check_agent_cache!
1017
+ agents.first
1018
+ end
1019
+
1020
+ def last
1021
+ check_agent_cache!
1022
+ agents.last
1023
+ end
1024
+
1025
+ def cached?
1026
+ @cached
1027
+ end
1028
+
1029
+ def to_a
1030
+ check_agent_cache!
1031
+ @agents
1032
+ end
1033
+
1034
+ private
1035
+
1036
+ def check_agent_cache!
1037
+ if cached?
1038
+ load_agents! unless agents
1039
+ else
1040
+ load_agents!
1041
+ end
1042
+ end
1043
+
1044
+ def load_agents!
1045
+ raw_data = proxy.environment.variable "QUEUE_MEMBER_LIST(#{proxy.name})"
1046
+ @agents = raw_data.split(',').map(&:strip).reject(&:empty?).map do |agent|
1047
+ AgentProxy.new(agent, proxy)
1048
+ end
1049
+ @cached_count = @agents.size
1050
+ end
1051
+
1052
+ end
1053
+
1054
+ class AgentProxy
1055
+
1056
+ SUPPORTED_METADATA_NAMES = %w[status password name mohclass exten channel] unless defined? SUPPORTED_METADATA_NAMES
1057
+
1058
+ class << self
1059
+ def id_from_agent_channel(id)
1060
+ id = id.to_s
1061
+ id.starts_with?('Agent/') ? id[%r[^Agent/(.+)$],1] : id
1062
+ end
1063
+ end
1064
+
1065
+ attr_reader :interface, :proxy, :queue_name, :id
1066
+ def initialize(interface, proxy)
1067
+ @interface = interface
1068
+ @id = self.class.id_from_agent_channel interface
1069
+ @proxy = proxy
1070
+ @queue_name = proxy.name
1071
+ end
1072
+
1073
+ def remove!
1074
+ proxy.environment.execute 'RemoveQueueMember', queue_name, interface
1075
+ case proxy.environment.variable("RQMSTATUS")
1076
+ when "REMOVED" : true
1077
+ when "NOTINQUEUE" : false
1078
+ when "NOSUCHQUEUE"
1079
+ raise QueueDoesNotExistError.new(queue_name)
1080
+ else
1081
+ raise "Unrecognized RQMSTATUS variable!"
1082
+ end
1083
+ end
1084
+
1085
+ # Pauses the given agent for this queue only. If you wish to pause this agent
1086
+ # for all queues, pass in :everywhere => true. Returns true if the agent was
1087
+ # successfully paused and false if the agent was not found.
1088
+ def pause!(options={})
1089
+ everywhere = options.delete(:everywhere)
1090
+ args = [(everywhere ? nil : queue_name), interface]
1091
+ proxy.environment.execute('PauseQueueMember', *args)
1092
+ case proxy.environment.variable("PQMSTATUS")
1093
+ when "PAUSED" : true
1094
+ when "NOTFOUND" : false
1095
+ else
1096
+ raise "Unrecognized PQMSTATUS value!"
1097
+ end
1098
+ end
1099
+
1100
+ # Pauses the given agent for this queue only. If you wish to pause this agent
1101
+ # for all queues, pass in :everywhere => true. Returns true if the agent was
1102
+ # successfully paused and false if the agent was not found.
1103
+ def unpause!(options={})
1104
+ everywhere = options.delete(:everywhere)
1105
+ args = [(everywhere ? nil : queue_name), interface]
1106
+ proxy.environment.execute('UnpauseQueueMember', *args)
1107
+ case proxy.environment.variable("UPQMSTATUS")
1108
+ when "UNPAUSED" : true
1109
+ when "NOTFOUND" : false
1110
+ else
1111
+ raise "Unrecognized UPQMSTATUS value!"
1112
+ end
1113
+ end
1114
+
1115
+ # Returns true/false depending on whether this agent is logged in.
1116
+ def logged_in?
1117
+ status == 'LOGGEDIN'
1118
+ end
1119
+
1120
+ private
1121
+
1122
+ def status
1123
+ agent_metadata 'status'
1124
+ end
1125
+
1126
+ def agent_metadata(data_name)
1127
+ data_name = data_name.to_s.downcase
1128
+ raise ArgumentError, "unrecognized agent metadata name #{data_name}" unless SUPPORTED_METADATA_NAMES.include? data_name
1129
+ proxy.environment.variable "AGENT(#{id}:#{data_name})"
1130
+ end
1131
+
1132
+ end
1133
+
1134
+ class QueueDoesNotExistError < Exception
1135
+ def initialize(queue_name)
1136
+ super "Queue #{queue_name} does not exist!"
1137
+ end
1138
+ end
1139
+
1140
+ end
1141
+
1142
+ module MenuDigitResponse
1143
+ def timed_out?
1144
+ eql? 0.chr
1145
+ end
1146
+ end
1147
+
1148
+ module SpeechEngines
1149
+
1150
+ class InvalidSpeechEngine < Exception; end
1151
+
1152
+ class << self
1153
+ def cepstral(text)
1154
+ puts "in ceptral"
1155
+ puts escape(text)
1156
+ end
1157
+
1158
+ def festival(text)
1159
+ raise NotImplementedError
1160
+ end
1161
+
1162
+ def none(text)
1163
+ raise InvalidSpeechEngine, "No speech engine selected. You must specify one in your Adhearsion config file."
1164
+ end
1165
+
1166
+ def method_missing(engine_name, text)
1167
+ raise InvalidSpeechEngine, "Unsupported speech engine #{engine_name} for speaking '#{text}'"
1168
+ end
1169
+
1170
+ private
1171
+
1172
+ def escape(text)
1173
+ "%p" % text
1174
+ end
1175
+
1176
+ end
1177
+ end
1178
+
1179
+ end
1180
+ end
1181
+ end
1182
+ end