/
call_controller_methods.rb
456 lines (411 loc) · 18 KB
/
call_controller_methods.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
module Adhearsion
module Asterisk
PLAYBACK_SUCCESS = 'SUCCESS' unless defined? PLAYBACK_SUCCESS
module CallControllerMethods
DYNAMIC_FEATURE_EXTENSIONS = {
:attended_transfer => lambda do |options|
variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
extend_dynamic_features_with "atxfer"
end,
:blind_transfer => lambda do |options|
variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
extend_dynamic_features_with 'blindxfer'
end
} unless defined? DYNAMIC_FEATURE_EXTENSIONS
def agi(name, *params)
component = Punchblock::Component::Asterisk::AGI::Command.new :name => name, :params => params
execute_component_and_await_completion component
complete_reason = component.complete_event.reason
raise Adhearsion::Call::Hangup if complete_reason.is_a?(Punchblock::Event::Complete::Hangup)
[:code, :result, :data].map { |p| complete_reason.send p }
end
#
# This asterisk dialplan command allows you to instruct Asterisk to start applications
# which are typically run from extensions.conf and do not have AGI command equivalents.
#
# For example, if there are specific asterisk modules you have loaded that will not be
# available through the standard commands provided through FAGI - then you can use EXEC.
#
# @example Using execute in this way will add a header to an existing SIP call.
# execute 'SIPAddHeader', "Call-Info: answer-after=0"
#
# @see http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands Asterisk Dialplan Commands
#
def execute(name, *params)
agi "EXEC #{name}", params.join(',')
end
#
# Sends a message to the console via the verbose message system.
#
# @param [String] message
# @param [Integer] level
#
# @return the result of the command
#
# @example Use this command to inform someone watching the Asterisk console
# of actions happening within Adhearsion.
# verbose 'Processing call with Adhearsion' 3
#
# @see http://www.voip-info.org/wiki/view/verbose
#
def verbose(message, level = nil)
agi 'VERBOSE', message, level
end
# A high-level way of enabling features you create/uncomment from features.conf.
#
# Certain Symbol features you enable (as defined in DYNAMIC_FEATURE_EXTENSIONS) have optional
# arguments that you can also specify here. The usage examples show how to do this.
#
# Usage examples:
#
# enable_feature :attended_transfer # Enables "atxfer"
#
# enable_feature :attended_transfer, :context => "my_dial" # Enables "atxfer" and then
# # sets "TRANSFER_CONTEXT" to :context's value
#
# enable_feature :blind_transfer, :context => 'my_dial' # Enables 'blindxfer' and sets TRANSFER_CONTEXT
#
# enable_feature "foobar" # Enables "foobar"
#
# enable_feature("dup"); enable_feature("dup") # Enables "dup" only once.
#
# def voicemail(*args)
# options_hash = args.last.kind_of?(Hash) ? args.pop : {}
# mailbox_number = args.shift
# greeting_option = options_hash.delete :greeting
#
def enable_feature(*args)
feature_name, optional_options = args.flatten
if DYNAMIC_FEATURE_EXTENSIONS.has_key? feature_name
instance_exec(optional_options, &DYNAMIC_FEATURE_EXTENSIONS[feature_name])
else
unless optional_options.nil? or optional_options.empty?
raise ArgumentError, "You cannot supply optional options when the feature name is " +
"not internally recognized!"
end
extend_dynamic_features_with feature_name
end
end
# Disables a feature name specified in features.conf. If you're disabling it, it was probably
# set by enable_feature().
#
# @param [String] feature_name
def disable_feature(feature_name)
enabled_features_variable = variable 'DYNAMIC_FEATURES'
enabled_features = enabled_features_variable.split('#')
if enabled_features.include? feature_name
enabled_features.delete feature_name
variable 'DYNAMIC_FEATURES' => enabled_features.join('#')
end
end
# helper method that should probably should private...
def extend_dynamic_features_with(feature_name)
current_variable = variable("DYNAMIC_FEATURES") || ''
enabled_features = current_variable.split '#'
unless enabled_features.include? feature_name
enabled_features << feature_name
variable "DYNAMIC_FEATURES" => enabled_features.join('#')
end
end
#
# Issue this command to access a channel variable that exists in the asterisk dialplan (i.e. extensions.conf)
# Use get_variable to pass information from other modules or high level configurations from the asterisk dialplan
# to the adhearsion dialplan.
#
# @param [String] variable_name
#
# @see: http://www.voip-info.org/wiki/view/get+variable Asterisk Get Variable
#
def get_variable(variable_name)
code, result, data = agi "GET VARIABLE", variable_name
data
end
#
# Pass information back to the asterisk dial plan.
#
# Keep in mind that the variables are not global variables. These variables only exist for the channel
# related to the call that is being serviced by the particular instance of your adhearsion application.
# You will not be able to pass information back to the asterisk dialplan for other instances of your adhearsion
# application to share. Once the channel is "hungup" then the variables are cleared and their information is gone.
#
# @param [String] variable_name
# @param [String] value
#
# @see http://www.voip-info.org/wiki/view/set+variable Asterisk Set Variable
#
def set_variable(variable_name, value)
agi "SET VARIABLE", variable_name, value
end
#
# Issue the command to add a custom SIP header to the current call channel
# example use: sip_add_header("x-ahn-test", "rubyrox")
#
# @param[String] the name of the SIP header
# @param[String] the value of the SIP header
#
# @return [String] the Asterisk response
#
# @see http://www.voip-info.org/wiki/index.php?page=Asterisk+cmd+SIPAddHeader Asterisk SIPAddHeader
#
def sip_add_header(header, value)
execute "SIPAddHeader", "#{header}: #{value}"
end
#
# Issue the command to fetch a SIP header from the current call channel
# example use: sip_get_header("x-ahn-test")
#
# @param[String] the name of the SIP header to get
#
# @return [String] the Asterisk response
#
# @see http://www.voip-info.org/wiki/index.php?page=Asterisk+cmd+SIPGetHeader Asterisk SIPGetHeader
#
def sip_get_header(header)
get_variable "SIP_HEADER(#{header})"
end
#
# Allows you to either set or get a channel variable from Asterisk.
# The method takes a hash key/value pair if you would like to set a variable
# Or a single string with the variable to get from Asterisk
#
def variable(*args)
if args.last.kind_of? Hash
assignments = args.pop
raise ArgumentError, "Can't mix variable setting and fetching!" if args.any?
assignments.each_pair do |key, value|
set_variable key, value
end
else
if args.size == 1
get_variable args.first
else
args.map { |var| get_variable var }
end
end
end
#
# Used to join a particular conference with the MeetMe application. To use MeetMe, be sure you
# have a proper timing device configured on your Asterisk box. MeetMe is Asterisk's built-in
# conferencing program.
#
# @param [String] conference_id
# @param [Hash] options
#
# @see http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe Asterisk Meetme Application Information
#
def meetme(conference_id, options = {})
conference_id = conference_id.to_s.scan(/\w/).join
command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
pin = options[:pin]
raise ArgumentError, "A conference PIN number must be numerical!" if pin && pin.to_s !~ /^\d+$/
# To disable dynamic conference creation set :use_static_conf => true
use_static_conf = options.has_key?(:use_static_conf) ? options[:use_static_conf] : false
# The 'd' option of MeetMe creates conferences dynamically.
command_flags += 'd' unless command_flags.include?('d') || use_static_conf
execute "MeetMe", conference_id, command_flags, options[:pin]
end
#
# Send a caller to a voicemail box to leave a message.
#
# The method takes the mailbox_number of the user to leave a message for and a
# greeting_option that will determine which message gets played to the caller.
#
# @see http://www.voip-info.org/tiki-index.php?page=Asterisk+cmd+VoiceMail Asterisk Voicemail
#
def voicemail(*args)
options_hash = args.last.kind_of?(Hash) ? args.pop : {}
mailbox_number = args.shift
greeting_option = options_hash.delete :greeting
skip_option = options_hash.delete :skip
raise ArgumentError, 'You supplied too many arguments!' if mailbox_number && options_hash.any?
greeting_option = case greeting_option
when :busy then 'b'
when :unavailable then 'u'
when nil then nil
else raise ArgumentError, "Unrecognized greeting #{greeting_option}"
end
skip_option &&= 's'
options = "#{greeting_option}#{skip_option}"
raise ArgumentError, "Mailbox cannot be blank!" if !mailbox_number.nil? && mailbox_number.blank?
number_with_context = if mailbox_number then mailbox_number else
raise ArgumentError, "You must supply ONE context name!" unless options_hash.size == 1
context_name, mailboxes = options_hash.to_a.first
Array(mailboxes).map do |mailbox|
raise ArgumentError, "Mailbox numbers must be numerical!" unless mailbox.to_s =~ /^\d+$/
[mailbox, context_name].join '@'
end.join '&'
end
execute 'voicemail', number_with_context, options
case variable('VMSTATUS')
when 'SUCCESS' then true
when 'USEREXIT' then false
else nil
end
end
#
# The voicemail_main method puts a caller into the voicemail system to fetch their voicemail
# or set options for their voicemail box.
#
# @param [Hash] options
#
# @see http://www.voip-info.org/wiki-Asterisk+cmd+VoiceMailMain Asterisk VoiceMailMain Command
#
def voicemail_main(options = {})
mailbox, context, folder = options.values_at :mailbox, :context, :folder
authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
folder = if folder
if folder.to_s =~ /^[\w_]+$/
"a(#{folder})"
else
raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
end
elsif folder == ''
raise ArgumentError, "Folder name cannot be an empty String!"
else
nil
end
real_mailbox = ""
real_mailbox << "#{mailbox}" unless mailbox.blank?
real_mailbox << "@#{context}" unless context.blank?
real_options = ""
real_options << "s" if !authenticate
real_options << folder unless folder.blank?
command_args = [real_mailbox]
command_args << real_options unless real_options.blank?
command_args.clear if command_args == [""]
execute 'VoiceMailMain', *command_args
end
#
# Place a call in a queue to be answered by a registered agent. You must then call #join!
#
# @param [String] queue_name the queue name to place the caller in
# @return [Adhearsion::Asterisk::QueueProxy] a queue proxy object
#
# @see http://www.voip-info.org/wiki-Asterisk+cmd+Queue Full information on the Asterisk Queue
# @see Adhearsion::Asterisk":QueueProxy#join! for further details
#
def queue(queue_name)
queue_name = queue_name.to_s
@queue_proxy_hash_lock ||= Mutex.new
@queue_proxy_hash_lock.synchronize do
@queue_proxy_hash ||= {}
if @queue_proxy_hash.has_key? queue_name
return @queue_proxy_hash[queue_name]
else
proxy = @queue_proxy_hash[queue_name] = QueueProxy.new(queue_name, self)
return proxy
end
end
end
# Plays the given Date, Time, or Integer (seconds since epoch)
# using the given timezone and format.
#
# @param [Date|Time|DateTime] Time to be said.
# @param [Hash] Additional options to specify how exactly to say time specified.
#
# +:timezone+ - Sends a timezone to asterisk. See /usr/share/zoneinfo for a list. Defaults to the machine timezone.
# +:format+ - This is the format the time is to be said in. Defaults to "ABdY 'digits/at' IMp"
#
# @see http://www.voip-info.org/wiki/view/Asterisk+cmd+SayUnixTime
def play_time(*args)
argument, options = args.flatten
options ||= {}
return false unless options.is_a? Hash
timezone = options.delete(:timezone) || ''
format = options.delete(:format) || ''
epoch = case argument
when Time || DateTime
argument.to_i
when Date
format = 'BdY' unless format.present?
argument.to_time.to_i
end
return false if epoch.nil?
execute "SayUnixTime", epoch, timezone, format
end
#Executes SayNumber with the passed argument.
#
# @param [Numeric|String] Numeric argument, or a string contanining numbers.
def play_numeric(argument)
execute "SayNumber", argument
end
#Executes SayDigits with the passed argument.
#
# @param [Numeric|String] Numeric argument, or a string contanining numbers.
def play_digits(argument)
execute "SayDigits", argument
end
#Executes Playtones with the passed argument.
#
# @param [String|Array] Array or comma-separated string of tones.
# @param [Boolean] Whether to wait for the tones to finish (defaults to false).
def play_tones(argument, wait = false)
tones = [*argument].join(",")
execute("Playtones", tones).tap do
sleep tones.scan(/(?<=\/)\d+/).map(&:to_i).sum.to_f / 1000 if wait
end
end
# Instruct Asterisk to play a sound file to the channel.
#
# @param [String] File name to play in the Asterisk convention, without extension.
# @return [Boolean] Returns false if the argument could not be played.
def play_soundfile(argument)
execute "Playback", argument
get_variable('PLAYBACKSTATUS') == PLAYBACK_SUCCESS
end
#
# Generates silence in the background, just once until some other sound is generated, or
# continuously for the duration of a given block. Silence is normally only generated under
# specific circumstances but this method will explicitly generate it, which can be useful
# in some scenarios.
#
# Note that the Playtones command must be available and the transmit_silence option must be
# enabled in asterisk.conf. Also note that the given block is executed using instance_eval
# and that imposes one important restriction. If the silence is interrupted outside the scope
# of the block (e.g. calling play in another method) then it won't be restarted until
# execution returns to the scope. However, it is safe to call generate_silence again when
# outside the scope. Instance variables may be used as they are copied and copied back but be
# careful handling immutable objects outside the scope. If you're unsure, don't use a block.
#
def generate_silence(&block)
component = Punchblock::Component::Asterisk::AGI::Command.new :name => "EXEC Playtones", :params => ["0"]
execute_component_and_await_completion component
GenerateSilenceProxy.proxy_for(self, &block) if block_given?
end
#
# Go to a specified context, extension and priority
# This requires us to relinquish control of the call.
# Execution will continue until the user hangs up, but the channel will be no longer available
#
def goto(context, extension = :nothing, priority = :nothing)
call[:ahn_prevent_hangup] = true
args = ['Goto', context, extension, priority].reject { |v| v == :nothing }
execute *args
set_variable 'PUNCHBLOCK_END_ON_ASYNCAGI_BREAK', 'true'
agi "ASYNCAGI BREAK"
end
class GenerateSilenceProxy
def self.proxy_for(target, &block)
proxy = new(target)
ivs = target.instance_variables
ivs.each { |iv| proxy.instance_variable_set iv, target.instance_variable_get(iv) }
proxy.instance_eval(&block).tap do
ivs = proxy.instance_variables - [:@_target]
ivs.each { |iv| target.instance_variable_set iv, proxy.instance_variable_get(iv) }
end
end
def initialize(target)
@_target = target
end
def method_missing(*args)
@_target.send(*args).tap do
@_target.generate_silence
end
end
def respond_to_missing?(*args)
@_target.respond_to?(*args)
end
end
end
end
end