adhearsion-asterisk 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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