adhearsion-asterisk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile +6 -0
  5. data/Guardfile +5 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +143 -0
  8. data/Rakefile +23 -0
  9. data/adhearsion-asterisk.gemspec +35 -0
  10. data/lib/adhearsion-asterisk.rb +1 -0
  11. data/lib/adhearsion/asterisk.rb +12 -0
  12. data/lib/adhearsion/asterisk/config_generator.rb +103 -0
  13. data/lib/adhearsion/asterisk/config_generator/agents.rb +138 -0
  14. data/lib/adhearsion/asterisk/config_generator/queues.rb +247 -0
  15. data/lib/adhearsion/asterisk/config_generator/voicemail.rb +238 -0
  16. data/lib/adhearsion/asterisk/config_manager.rb +60 -0
  17. data/lib/adhearsion/asterisk/plugin.rb +464 -0
  18. data/lib/adhearsion/asterisk/queue_proxy.rb +177 -0
  19. data/lib/adhearsion/asterisk/queue_proxy/agent_proxy.rb +81 -0
  20. data/lib/adhearsion/asterisk/queue_proxy/queue_agents_list_proxy.rb +132 -0
  21. data/lib/adhearsion/asterisk/version.rb +5 -0
  22. data/spec/adhearsion/asterisk/config_generators/agents_spec.rb +258 -0
  23. data/spec/adhearsion/asterisk/config_generators/queues_spec.rb +322 -0
  24. data/spec/adhearsion/asterisk/config_generators/voicemail_spec.rb +306 -0
  25. data/spec/adhearsion/asterisk/config_manager_spec.rb +125 -0
  26. data/spec/adhearsion/asterisk/plugin_spec.rb +618 -0
  27. data/spec/adhearsion/asterisk/queue_proxy/agent_proxy_spec.rb +90 -0
  28. data/spec/adhearsion/asterisk/queue_proxy/queue_agents_list_proxy_spec.rb +145 -0
  29. data/spec/adhearsion/asterisk/queue_proxy_spec.rb +156 -0
  30. data/spec/adhearsion/asterisk_spec.rb +9 -0
  31. data/spec/spec_helper.rb +23 -0
  32. data/spec/support/the_following_code.rb +3 -0
  33. metadata +229 -0
@@ -0,0 +1,60 @@
1
+ require 'enumerator'
2
+
3
+ module Adhearsion
4
+ module Asterisk
5
+ class ConfigurationManager
6
+
7
+ class << self
8
+ def normalize_configuration(file_contents)
9
+ # cat sip.conf | sed -e 's/\s*;.*$//g' | sed -e '/^;.*$/d' | sed -e '/^\s*$/d'
10
+ file_contents.split(/\n+/).map do |line|
11
+ line.sub(/;.+$/, '').strip
12
+ end.join("\n").squeeze("\n")
13
+ end
14
+ end
15
+
16
+ attr_reader :filename
17
+
18
+ def initialize(filename)
19
+ @filename = filename
20
+ end
21
+
22
+ def sections
23
+ @sections ||= read_configuration
24
+ end
25
+
26
+ def [](section_name)
27
+ result = sections.find { |(name, *rest)| section_name == name }
28
+ result.last if result
29
+ end
30
+
31
+ def delete_section(section_name)
32
+ sections.reject! { |(name, *rest)| section_name == name }
33
+ end
34
+
35
+ def new_section(name, properties={})
36
+ sections << [name, properties]
37
+ end
38
+
39
+ private
40
+
41
+ def read_configuration
42
+ normalized_file = self.class.normalize_configuration File.open(@filename, 'r'){|f| f.read}
43
+ sections = normalized_file.split(/^\[([-_\w]+)\]$/)[1..-1]
44
+ return [] if sections.nil?
45
+ sections.each_slice(2).map do |(name,properties)|
46
+ [name, hash_from_properties(properties)]
47
+ end
48
+ end
49
+
50
+ def hash_from_properties(properties)
51
+ properties.split(/\n+/).inject({}) do |property_hash,property|
52
+ all, name, value = *property.match(/^\s*([^=]+?)\s*=\s*(.+)\s*$/)
53
+ next property_hash unless name && value
54
+ property_hash[name] = value
55
+ property_hash
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,464 @@
1
+ module Adhearsion
2
+ module Asterisk
3
+ PLAYBACK_SUCCESS = 'SUCCESS' unless defined? PLAYBACK_SUCCESS
4
+
5
+
6
+ class Plugin < Adhearsion::Plugin
7
+ DYNAMIC_FEATURE_EXTENSIONS = {
8
+ :attended_transfer => lambda do |options|
9
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
10
+ extend_dynamic_features_with "atxfer"
11
+ end,
12
+ :blind_transfer => lambda do |options|
13
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
14
+ extend_dynamic_features_with 'blindxfer'
15
+ end
16
+ } unless defined? DYNAMIC_FEATURE_EXTENSIONS
17
+
18
+ dialplan :agi do |name, *params|
19
+ component = Punchblock::Component::Asterisk::AGI::Command.new :name => name, :params => params
20
+ execute_component_and_await_completion component
21
+ complete_reason = component.complete_event.resource.reason
22
+ [:code, :result, :data].map { |p| complete_reason.send p }
23
+ end
24
+
25
+ #
26
+ # This asterisk dialplan command allows you to instruct Asterisk to start applications
27
+ # which are typically run from extensions.conf and do not have AGI command equivalents.
28
+ #
29
+ # For example, if there are specific asterisk modules you have loaded that will not be
30
+ # available through the standard commands provided through FAGI - then you can use EXEC.
31
+ #
32
+ # @example Using execute in this way will add a header to an existing SIP call.
33
+ # execute 'SIPAddHeader', "Call-Info: answer-after=0"
34
+ #
35
+ # @see http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands Asterisk Dialplan Commands
36
+ #
37
+ dialplan :execute do |name, *params|
38
+ agi "EXEC #{name}", *params
39
+ end
40
+
41
+ #
42
+ # Sends a message to the console via the verbose message system.
43
+ #
44
+ # @param [String] message
45
+ # @param [Integer] level
46
+ #
47
+ # @return the result of the command
48
+ #
49
+ # @example Use this command to inform someone watching the Asterisk console
50
+ # of actions happening within Adhearsion.
51
+ # verbose 'Processing call with Adhearsion' 3
52
+ #
53
+ # @see http://www.voip-info.org/wiki/view/verbose
54
+ #
55
+ dialplan :verbose do |message, level = nil|
56
+ agi 'VERBOSE', message, level
57
+ end
58
+
59
+ # A high-level way of enabling features you create/uncomment from features.conf.
60
+ #
61
+ # Certain Symbol features you enable (as defined in DYNAMIC_FEATURE_EXTENSIONS) have optional
62
+ # arguments that you can also specify here. The usage examples show how to do this.
63
+ #
64
+ # Usage examples:
65
+ #
66
+ # enable_feature :attended_transfer # Enables "atxfer"
67
+ #
68
+ # enable_feature :attended_transfer, :context => "my_dial" # Enables "atxfer" and then
69
+ # # sets "TRANSFER_CONTEXT" to :context's value
70
+ #
71
+ # enable_feature :blind_transfer, :context => 'my_dial' # Enables 'blindxfer' and sets TRANSFER_CONTEXT
72
+ #
73
+ # enable_feature "foobar" # Enables "foobar"
74
+ #
75
+ # enable_feature("dup"); enable_feature("dup") # Enables "dup" only once.
76
+ #
77
+ # dialplan :voicemail do |*args|
78
+ # options_hash = args.last.kind_of?(Hash) ? args.pop : {}
79
+ # mailbox_number = args.shift
80
+ # greeting_option = options_hash.delete :greeting
81
+ #
82
+ dialplan :enable_feature do |*args|
83
+ feature_name, optional_options = args.flatten
84
+
85
+ if DYNAMIC_FEATURE_EXTENSIONS.has_key? feature_name
86
+ instance_exec(optional_options, &DYNAMIC_FEATURE_EXTENSIONS[feature_name])
87
+ else
88
+ unless optional_options.nil? or optional_options.empty?
89
+ raise ArgumentError, "You cannot supply optional options when the feature name is " +
90
+ "not internally recognized!"
91
+ end
92
+ extend_dynamic_features_with feature_name
93
+ end
94
+ end
95
+
96
+ # Disables a feature name specified in features.conf. If you're disabling it, it was probably
97
+ # set by enable_feature().
98
+ #
99
+ # @param [String] feature_name
100
+ dialplan :disable_feature do |feature_name|
101
+ enabled_features_variable = variable 'DYNAMIC_FEATURES'
102
+ enabled_features = enabled_features_variable.split('#')
103
+ if enabled_features.include? feature_name
104
+ enabled_features.delete feature_name
105
+ variable 'DYNAMIC_FEATURES' => enabled_features.join('#')
106
+ end
107
+ end
108
+
109
+ # helper method that should probably should private...
110
+ dialplan :extend_dynamic_features_with do |feature_name|
111
+ current_variable = variable("DYNAMIC_FEATURES") || ''
112
+ enabled_features = current_variable.split '#'
113
+ unless enabled_features.include? feature_name
114
+ enabled_features << feature_name
115
+ variable "DYNAMIC_FEATURES" => enabled_features.join('#')
116
+ end
117
+ end
118
+
119
+ #
120
+ # Issue this command to access a channel variable that exists in the asterisk dialplan (i.e. extensions.conf)
121
+ # Use get_variable to pass information from other modules or high level configurations from the asterisk dialplan
122
+ # to the adhearsion dialplan.
123
+ #
124
+ # @param [String] variable_name
125
+ #
126
+ # @see: http://www.voip-info.org/wiki/view/get+variable Asterisk Get Variable
127
+ #
128
+ dialplan :get_variable do |variable_name|
129
+ code, result, data = agi "GET VARIABLE", variable_name
130
+ data
131
+ end
132
+
133
+ #
134
+ # Pass information back to the asterisk dial plan.
135
+ #
136
+ # Keep in mind that the variables are not global variables. These variables only exist for the channel
137
+ # related to the call that is being serviced by the particular instance of your adhearsion application.
138
+ # You will not be able to pass information back to the asterisk dialplan for other instances of your adhearsion
139
+ # application to share. Once the channel is "hungup" then the variables are cleared and their information is gone.
140
+ #
141
+ # @param [String] variable_name
142
+ # @param [String] value
143
+ #
144
+ # @see http://www.voip-info.org/wiki/view/set+variable Asterisk Set Variable
145
+ #
146
+ dialplan :set_variable do |variable_name, value|
147
+ agi "SET VARIABLE", variable_name, value
148
+ end
149
+
150
+ #
151
+ # Issue the command to add a custom SIP header to the current call channel
152
+ # example use: sip_add_header("x-ahn-test", "rubyrox")
153
+ #
154
+ # @param[String] the name of the SIP header
155
+ # @param[String] the value of the SIP header
156
+ #
157
+ # @return [String] the Asterisk response
158
+ #
159
+ # @see http://www.voip-info.org/wiki/index.php?page=Asterisk+cmd+SIPAddHeader Asterisk SIPAddHeader
160
+ #
161
+ dialplan :sip_add_header do |header, value|
162
+ execute "SIPAddHeader", "#{header}: #{value}"
163
+ end
164
+
165
+ #
166
+ # Issue the command to fetch a SIP header from the current call channel
167
+ # example use: sip_get_header("x-ahn-test")
168
+ #
169
+ # @param[String] the name of the SIP header to get
170
+ #
171
+ # @return [String] the Asterisk response
172
+ #
173
+ # @see http://www.voip-info.org/wiki/index.php?page=Asterisk+cmd+SIPGetHeader Asterisk SIPGetHeader
174
+ #
175
+ dialplan :sip_get_header do |header|
176
+ get_variable "SIP_HEADER(#{header})"
177
+ end
178
+
179
+ #
180
+ # Allows you to either set or get a channel variable from Asterisk.
181
+ # The method takes a hash key/value pair if you would like to set a variable
182
+ # Or a single string with the variable to get from Asterisk
183
+ #
184
+ dialplan :variable do |*args|
185
+ if args.last.kind_of? Hash
186
+ assignments = args.pop
187
+ raise ArgumentError, "Can't mix variable setting and fetching!" if args.any?
188
+ assignments.each_pair do |key, value|
189
+ set_variable key, value
190
+ end
191
+ else
192
+ if args.size == 1
193
+ get_variable args.first
194
+ else
195
+ args.map { |var| get_variable var }
196
+ end
197
+ end
198
+ end
199
+
200
+ #
201
+ # Used to join a particular conference with the MeetMe application. To use MeetMe, be sure you
202
+ # have a proper timing device configured on your Asterisk box. MeetMe is Asterisk's built-in
203
+ # conferencing program.
204
+ #
205
+ # @param [String] conference_id
206
+ # @param [Hash] options
207
+ #
208
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe Asterisk Meetme Application Information
209
+ #
210
+ dialplan :meetme do |conference_id, options = {}|
211
+ conference_id = conference_id.to_s.scan(/\w/).join
212
+ command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
213
+ pin = options[:pin]
214
+ raise ArgumentError, "A conference PIN number must be numerical!" if pin && pin.to_s !~ /^\d+$/
215
+
216
+ # To disable dynamic conference creation set :use_static_conf => true
217
+ use_static_conf = options.has_key?(:use_static_conf) ? options[:use_static_conf] : false
218
+
219
+ # The 'd' option of MeetMe creates conferences dynamically.
220
+ command_flags += 'd' unless command_flags.include?('d') || use_static_conf
221
+
222
+ execute "MeetMe", conference_id, command_flags, options[:pin]
223
+ end
224
+
225
+ #
226
+ # Send a caller to a voicemail box to leave a message.
227
+ #
228
+ # The method takes the mailbox_number of the user to leave a message for and a
229
+ # greeting_option that will determine which message gets played to the caller.
230
+ #
231
+ # @see http://www.voip-info.org/tiki-index.php?page=Asterisk+cmd+VoiceMail Asterisk Voicemail
232
+ #
233
+ dialplan :voicemail do |*args|
234
+ options_hash = args.last.kind_of?(Hash) ? args.pop : {}
235
+ mailbox_number = args.shift
236
+ greeting_option = options_hash.delete :greeting
237
+ skip_option = options_hash.delete :skip
238
+ raise ArgumentError, 'You supplied too many arguments!' if mailbox_number && options_hash.any?
239
+
240
+ greeting_option = case greeting_option
241
+ when :busy then 'b'
242
+ when :unavailable then 'u'
243
+ when nil then nil
244
+ else raise ArgumentError, "Unrecognized greeting #{greeting_option}"
245
+ end
246
+ skip_option &&= 's'
247
+ options = "#{greeting_option}#{skip_option}"
248
+
249
+ raise ArgumentError, "Mailbox cannot be blank!" if !mailbox_number.nil? && mailbox_number.blank?
250
+ number_with_context = if mailbox_number then mailbox_number else
251
+ raise ArgumentError, "You must supply ONE context name!" unless options_hash.size == 1
252
+ context_name, mailboxes = options_hash.to_a.first
253
+ Array(mailboxes).map do |mailbox|
254
+ raise ArgumentError, "Mailbox numbers must be numerical!" unless mailbox.to_s =~ /^\d+$/
255
+ [mailbox, context_name].join '@'
256
+ end.join '&'
257
+ end
258
+
259
+ execute 'voicemail', number_with_context, options
260
+ case variable('VMSTATUS')
261
+ when 'SUCCESS' then true
262
+ when 'USEREXIT' then false
263
+ else nil
264
+ end
265
+ end
266
+
267
+ #
268
+ # The voicemail_main method puts a caller into the voicemail system to fetch their voicemail
269
+ # or set options for their voicemail box.
270
+ #
271
+ # @param [Hash] options
272
+ #
273
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+VoiceMailMain Asterisk VoiceMailMain Command
274
+ #
275
+ dialplan :voicemail_main do |options = {}|
276
+ mailbox, context, folder = options.values_at :mailbox, :context, :folder
277
+ authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
278
+
279
+ folder = if folder
280
+ if folder.to_s =~ /^[\w_]+$/
281
+ "a(#{folder})"
282
+ else
283
+ raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
284
+ end
285
+ elsif folder == ''
286
+ raise ArgumentError, "Folder name cannot be an empty String!"
287
+ else
288
+ nil
289
+ end
290
+
291
+ real_mailbox = ""
292
+ real_mailbox << "#{mailbox}" unless mailbox.blank?
293
+ real_mailbox << "@#{context}" unless context.blank?
294
+
295
+ real_options = ""
296
+ real_options << "s" if !authenticate
297
+ real_options << folder unless folder.blank?
298
+
299
+ command_args = [real_mailbox]
300
+ command_args << real_options unless real_options.blank?
301
+ command_args.clear if command_args == [""]
302
+
303
+ execute 'VoiceMailMain', *command_args
304
+ end
305
+
306
+ #
307
+ # Place a call in a queue to be answered by a registered agent. You must then call #join!
308
+ #
309
+ # @param [String] queue_name the queue name to place the caller in
310
+ # @return [Adhearsion::Asterisk::QueueProxy] a queue proxy object
311
+ #
312
+ # @see http://www.voip-info.org/wiki-Asterisk+cmd+Queue Full information on the Asterisk Queue
313
+ # @see Adhearsion::Asterisk":QueueProxy#join! for further details
314
+ #
315
+ dialplan :queue do |queue_name|
316
+ queue_name = queue_name.to_s
317
+
318
+ @queue_proxy_hash_lock ||= Mutex.new
319
+ @queue_proxy_hash_lock.synchronize do
320
+ @queue_proxy_hash ||= {}
321
+ if @queue_proxy_hash.has_key? queue_name
322
+ return @queue_proxy_hash[queue_name]
323
+ else
324
+ proxy = @queue_proxy_hash[queue_name] = QueueProxy.new(queue_name, self)
325
+ return proxy
326
+ end
327
+ end
328
+ end
329
+
330
+ # Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
331
+ # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. When playing
332
+ # numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
333
+ # is pronounced as "one hundred" instead of "one zero zero". To specify how the Date/Time objects are said
334
+ # pass in as an array with the first parameter as the Date/Time/DateTime object along with a hash with the
335
+ # additional options. See play_time for more information.
336
+ #
337
+ # Note: it is not necessary to supply a sound file extension; Asterisk will try to find a sound
338
+ # file encoded using the current channel's codec, if one exists. If not, it will transcode from
339
+ # the default codec (GSM). Asterisk stores its sound files in /var/lib/asterisk/sounds.
340
+ #
341
+ # @example Play file hello-world.???
342
+ # play 'hello-world'
343
+ # @example Speak current time
344
+ # play Time.now
345
+ # @example Speak today's date
346
+ # play Date.today
347
+ # @example Speak today's date in a specific format
348
+ # play [Date.today, {:format => 'BdY'}]
349
+ # @example Play sound file, speak number, play two more sound files
350
+ # play %w"a-connect-charge-of 22 cents-per-minute will-apply"
351
+ # @example Play two sound files
352
+ # play "you-sound-cute", "what-are-you-wearing"
353
+ #
354
+ # @return [Boolean] true is returned if everything was successful. Otherwise, false indicates that
355
+ # some sound file(s) could not be played.
356
+ dialplan :play do |*arguments|
357
+ begin
358
+ play! arguments
359
+ rescue Adhearsion::PlaybackError => e
360
+ return false
361
+ end
362
+ true
363
+ end
364
+
365
+ # Same as {#play}, but immediately raises an exception if a sound file cannot be played.
366
+ #
367
+ # @return [true]
368
+ # @raise [Adhearsion::VoIP::PlaybackError] If a sound file cannot be played
369
+ dialplan :play! do |*arguments|
370
+ result = true
371
+ unless play_time(arguments)
372
+ arguments.flatten.each do |argument|
373
+ result &= play_numeric(argument) || play_soundfile(argument)
374
+ end
375
+ end
376
+ raise Adhearsion::PlaybackError if !result
377
+ end
378
+
379
+ # Plays the given Date, Time, or Integer (seconds since epoch)
380
+ # using the given timezone and format.
381
+ #
382
+ # @param [Date|Time|DateTime] Time to be said.
383
+ # @param [Hash] Additional options to specify how exactly to say time specified.
384
+ #
385
+ # +:timezone+ - Sends a timezone to asterisk. See /usr/share/zoneinfo for a list. Defaults to the machine timezone.
386
+ # +:format+ - This is the format the time is to be said in. Defaults to "ABdY 'digits/at' IMp"
387
+ #
388
+ # @see http://www.voip-info.org/wiki/view/Asterisk+cmd+SayUnixTime
389
+ dialplan :play_time do |*args|
390
+ argument, options = args.flatten
391
+ options ||= {}
392
+
393
+ return false unless options.is_a? Hash
394
+
395
+ timezone = options.delete(:timezone) || ''
396
+ format = options.delete(:format) || ''
397
+ epoch = case argument
398
+ when Time || DateTime
399
+ argument.to_i
400
+ when Date
401
+ format = 'BdY' unless format.present?
402
+ argument.to_time.to_i
403
+ end
404
+
405
+ return false if epoch.nil?
406
+
407
+ execute "SayUnixTime", epoch, timezone, format
408
+ end
409
+
410
+ #Executes SayNumber with the passed argument.
411
+ #
412
+ # @param [Numeric|String] Numeric argument, or a string contanining numbers.
413
+ # @return [Boolean] Returns false if the argument could not be played.
414
+ dialplan :play_numeric do |argument|
415
+ if argument.kind_of?(Numeric) || argument =~ /^\d+$/
416
+ execute "SayNumber", argument
417
+ end
418
+ end
419
+
420
+ # Instruct Asterisk to play a sound file to the channel.
421
+ #
422
+ # @param [String] File name to play in the Asterisk convention, without extension.
423
+ # @return [Boolean] Returns false if the argument could not be played.
424
+ dialplan :play_soundfile do |argument|
425
+ execute "Playback", argument
426
+ get_variable('PLAYBACKSTATUS') == PLAYBACK_SUCCESS
427
+ end
428
+
429
+ #
430
+ # Plays a single output, not only files, accepting interruption by one of the digits specified
431
+ # Currently still stops execution, will be fixed soon in Punchblock
432
+ #
433
+ # @param [Object] String or Hash specifying output and options
434
+ # @param [String] String with the digits that are allowed to interrupt output
435
+ # @return [String|nil] The pressed digit, or nil if nothing was pressed
436
+ #
437
+ dialplan :stream_file do |argument, digits = '0123456789#*'|
438
+ begin
439
+ output_component = ::Punchblock::Component::Asterisk::AGI::Command.new :name => "STREAM FILE",
440
+ :params => [
441
+ argument,
442
+ digits
443
+ ]
444
+ execute_component_and_await_completion output_component
445
+
446
+ reason = output_component.complete_event.reason
447
+
448
+ case reason.result
449
+ when 0
450
+ raise Adhearsion::PlaybackError if reason.data == "endpos=0"
451
+ nil
452
+ when -1
453
+ raise Adhearsion::PlaybackError
454
+ else
455
+ [reason.result].pack 'U*'
456
+ end
457
+ rescue StandardError => e
458
+ raise Adhearsion::PlaybackError, "Output failed for argument #{argument.inspect}"
459
+ end
460
+ end
461
+
462
+ end#class
463
+ end#module
464
+ end#module