adhearsion-asterisk 0.1.2 → 0.1.3

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