adhearsion-asterisk 0.1.0

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 (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