jicksta-adhearsion 0.7.999

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