sevenscale-adhearsion 0.7.1000

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