sant0sk1-adhearsion 0.7.999

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/LICENSE +456 -0
  2. data/README.txt +5 -0
  3. data/Rakefile +75 -0
  4. data/adhearsion.gemspec +136 -0
  5. data/app_generators/ahn/USAGE +5 -0
  6. data/app_generators/ahn/ahn_generator.rb +77 -0
  7. data/app_generators/ahn/templates/.ahnrc +36 -0
  8. data/app_generators/ahn/templates/README +8 -0
  9. data/app_generators/ahn/templates/Rakefile +18 -0
  10. data/app_generators/ahn/templates/components/simon_game/configuration.rb +0 -0
  11. data/app_generators/ahn/templates/components/simon_game/lib/simon_game.rb +61 -0
  12. data/app_generators/ahn/templates/components/simon_game/test/test_helper.rb +14 -0
  13. data/app_generators/ahn/templates/components/simon_game/test/test_simon_game.rb +31 -0
  14. data/app_generators/ahn/templates/config/startup.rb +53 -0
  15. data/app_generators/ahn/templates/dialplan.rb +4 -0
  16. data/bin/ahn +28 -0
  17. data/bin/ahnctl +68 -0
  18. data/bin/jahn +32 -0
  19. data/lib/adhearsion.rb +32 -0
  20. data/lib/adhearsion/blank_slate.rb +5 -0
  21. data/lib/adhearsion/cli.rb +106 -0
  22. data/lib/adhearsion/component_manager.rb +277 -0
  23. data/lib/adhearsion/core_extensions/all.rb +9 -0
  24. data/lib/adhearsion/core_extensions/array.rb +0 -0
  25. data/lib/adhearsion/core_extensions/custom_daemonizer.rb +45 -0
  26. data/lib/adhearsion/core_extensions/global.rb +1 -0
  27. data/lib/adhearsion/core_extensions/hash.rb +0 -0
  28. data/lib/adhearsion/core_extensions/metaprogramming.rb +17 -0
  29. data/lib/adhearsion/core_extensions/numeric.rb +4 -0
  30. data/lib/adhearsion/core_extensions/proc.rb +0 -0
  31. data/lib/adhearsion/core_extensions/publishable.rb +73 -0
  32. data/lib/adhearsion/core_extensions/relationship_properties.rb +40 -0
  33. data/lib/adhearsion/core_extensions/string.rb +26 -0
  34. data/lib/adhearsion/core_extensions/thread.rb +13 -0
  35. data/lib/adhearsion/core_extensions/thread_safety.rb +7 -0
  36. data/lib/adhearsion/core_extensions/time.rb +0 -0
  37. data/lib/adhearsion/distributed/gateways/dbus_gateway.rb +0 -0
  38. data/lib/adhearsion/distributed/gateways/osa_gateway.rb +0 -0
  39. data/lib/adhearsion/distributed/gateways/rest_gateway.rb +9 -0
  40. data/lib/adhearsion/distributed/gateways/soap_gateway.rb +9 -0
  41. data/lib/adhearsion/distributed/gateways/xmlrpc_gateway.rb +9 -0
  42. data/lib/adhearsion/distributed/peer_finder.rb +0 -0
  43. data/lib/adhearsion/distributed/remote_cli.rb +0 -0
  44. data/lib/adhearsion/events_support.rb +26 -0
  45. data/lib/adhearsion/hooks.rb +57 -0
  46. data/lib/adhearsion/host_definitions.rb +63 -0
  47. data/lib/adhearsion/initializer.rb +246 -0
  48. data/lib/adhearsion/initializer/asterisk.rb +59 -0
  49. data/lib/adhearsion/initializer/configuration.rb +236 -0
  50. data/lib/adhearsion/initializer/database.rb +49 -0
  51. data/lib/adhearsion/initializer/drb.rb +25 -0
  52. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  53. data/lib/adhearsion/initializer/rails.rb +40 -0
  54. data/lib/adhearsion/logging.rb +92 -0
  55. data/lib/adhearsion/tasks.rb +15 -0
  56. data/lib/adhearsion/tasks/database.rb +5 -0
  57. data/lib/adhearsion/tasks/generating.rb +20 -0
  58. data/lib/adhearsion/tasks/lint.rb +4 -0
  59. data/lib/adhearsion/tasks/testing.rb +37 -0
  60. data/lib/adhearsion/version.rb +9 -0
  61. data/lib/adhearsion/voip/asterisk.rb +10 -0
  62. data/lib/adhearsion/voip/asterisk/agi_server.rb +81 -0
  63. data/lib/adhearsion/voip/asterisk/ami.rb +147 -0
  64. data/lib/adhearsion/voip/asterisk/ami/actions.rb +238 -0
  65. data/lib/adhearsion/voip/asterisk/ami/machine.rb +871 -0
  66. data/lib/adhearsion/voip/asterisk/ami/machine.rl +109 -0
  67. data/lib/adhearsion/voip/asterisk/ami/parser.rb +262 -0
  68. data/lib/adhearsion/voip/asterisk/commands.rb +1284 -0
  69. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  70. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  71. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  72. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  73. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  74. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  75. data/lib/adhearsion/voip/call.rb +436 -0
  76. data/lib/adhearsion/voip/call_routing.rb +64 -0
  77. data/lib/adhearsion/voip/commands.rb +9 -0
  78. data/lib/adhearsion/voip/constants.rb +39 -0
  79. data/lib/adhearsion/voip/conveniences.rb +18 -0
  80. data/lib/adhearsion/voip/dial_plan.rb +207 -0
  81. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  82. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  83. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  84. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  85. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
  86. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  87. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  88. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  89. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  90. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  91. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  92. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  93. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  94. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  95. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  96. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  97. metadata +167 -0
@@ -0,0 +1,109 @@
1
+ module Adhearsion
2
+ module VoIP
3
+ module Asterisk
4
+ class AMI
5
+ module Machine
6
+ %%{
7
+ machine ami;
8
+
9
+ cr = "\r";
10
+ lf = "\n";
11
+ crlf = cr lf;
12
+
13
+ action _key { mark("key") }
14
+ action key { set("key"); }
15
+ action _value { mark("value") }
16
+ action value { set("value"); }
17
+ Attr = [a-zA-Z\-]+ >_key %key ': ' (any* -- crlf) >_value %value crlf;
18
+ Privilege = "Privilege" >_key %key ': ' (any* -- crlf) >_value %value crlf;
19
+ ActionID = "ActionID" >_key %key ': ' (any* -- crlf) >_value %value crlf;
20
+
21
+ action _event { mark("event") }
22
+ action event { set("event"); @current_packet = EventPacket.new(@__ragel_event) }
23
+ Event = "Event: " alpha+ >_event %event crlf;
24
+
25
+ action _success { @current_packet = Packet.new; }
26
+ action _error { @current_packet = ErrorPacket.new; }
27
+ Response = "Response: ";
28
+ Success = Response "Success" >_success crlf;
29
+ Pong = Response "Pong" >_success crlf;
30
+ Error = Response "Error" >_error crlf;
31
+ Events = Response "Events " ("On" | "Off") >_success crlf;
32
+
33
+ action _follows { @current_packet = FollowsPacket.new; }
34
+ Follows = Response "Follows" >_follows crlf;
35
+ EndFollows = "--END COMMAND--" crlf;
36
+
37
+ # Capture the prompt. Signal any waiters.
38
+ Prompt = "Asterisk Call Manager/";
39
+ prompt := |*
40
+ graph+ >{ mark("version"); };
41
+ crlf >{ set("version"); @signal.signal } => { fgoto main; };
42
+ *|;
43
+
44
+ # For typical commands with responses with headers
45
+ response_normal := |*
46
+ Attr => { pair; };
47
+ crlf => { packet; fgoto main; };
48
+ *|;
49
+
50
+ # For immediate or raw commands
51
+ Raw = (any+ >{ mark_array("raw"); } -- lf) lf;
52
+
53
+ # For immediate or raw commands
54
+ Imm = (any+ >{ mark_array("raw") } -- crlf) crlf %{ insert("raw") };
55
+
56
+ # For raw commands
57
+ response_follows := |*
58
+ Privilege => { pair; };
59
+ ActionID => { pair; };
60
+ Raw => { insert("raw") };
61
+ EndFollows crlf => { packet; fgoto main; };
62
+ *|;
63
+
64
+ main := |*
65
+ Prompt @{ fgoto prompt; };
66
+ Success @{ fgoto response_normal; };
67
+ Pong @{ fgoto response_normal; };
68
+ Error @{ fgoto response_normal; };
69
+ Event @{ fgoto response_normal; };
70
+ Events @{ fgoto response_normal; };
71
+ Follows @{ fgoto response_follows; };
72
+
73
+ # Must also handle immediate responses with raw data
74
+ Imm crlf crlf => { @current_packet = ImmediatePacket.new; packet; };
75
+ *|;
76
+ }%%
77
+
78
+ class << self
79
+ def extended(base)
80
+ # Rename the Ragel variables. Not strictly necessary if
81
+ # we were to make accessors for them.
82
+ base.instance_eval do
83
+ %%{
84
+ variable p @__ragel_p;
85
+ variable pe @__ragel_pe;
86
+ variable cs @__ragel_cs;
87
+ variable act @__ragel_act;
88
+ variable data @__ragel_data;
89
+ variable tokstart @__ragel_tokstart;
90
+ variable tokend @__ragel_tokend;
91
+ write data nofinal;
92
+ }%%
93
+ end
94
+ end
95
+ end
96
+
97
+ private
98
+ def ragel_init
99
+ %% write init;
100
+ end
101
+
102
+ def ragel_exec
103
+ %% write exec;
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,262 @@
1
+ require 'drb'
2
+ require 'adhearsion/voip/asterisk/ami/machine'
3
+
4
+ module Adhearsion
5
+ module VoIP
6
+ module Asterisk
7
+ class AMI
8
+ class Packet < Hash
9
+ def error?
10
+ false
11
+ end
12
+
13
+ def raw?
14
+ false
15
+ end
16
+
17
+ def is_event?
18
+ false
19
+ end
20
+
21
+ # Return the hash, without the internal Action ID
22
+ def body
23
+ returning clone do |packet|
24
+ packet.delete 'ActionID'
25
+ end
26
+ end
27
+
28
+ def message
29
+ self['Message']
30
+ end
31
+ end
32
+
33
+ class EventPacket < Packet
34
+ attr_accessor :event
35
+ def initialize(event)
36
+ @event = event
37
+ super(false)
38
+ end
39
+
40
+ def is_event?
41
+ true
42
+ end
43
+ end
44
+
45
+ class ErrorPacket < Packet
46
+ def error?
47
+ true
48
+ end
49
+ end
50
+
51
+ class FollowsPacket < Packet
52
+ def raw?
53
+ true
54
+ end
55
+ end
56
+
57
+ class ImmediatePacket < Packet
58
+ def raw?
59
+ true
60
+ end
61
+ end
62
+
63
+ class Parser
64
+ # Size of the scanner buffer
65
+ BUFSIZE = 1024
66
+
67
+ attr_accessor :logger
68
+ attr_reader :events
69
+
70
+ def initialize
71
+ self.extend Machine
72
+
73
+ # Add the variables and accessors used for marking seen data
74
+ %w(event key value version).each do |name|
75
+ instance_eval <<-STR
76
+ class << self
77
+ send(:attr_accessor, "__ragel_mark_#{name}")
78
+ send(:attr_accessor, "__ragel_#{name}")
79
+ end
80
+ send("__ragel_mark_#{name}=", 0)
81
+ send("__ragel_#{name}=", nil)
82
+ STR
83
+ end
84
+
85
+ %w(raw).each do |name|
86
+ instance_eval <<-STR
87
+ class << self
88
+ send(:attr_accessor, "__ragel_mark_#{name}")
89
+ send(:attr_accessor, "__ragel_#{name}")
90
+ end
91
+ send("__ragel_mark_#{name}=", 0)
92
+ send("__ragel_#{name}=", [])
93
+ STR
94
+ end
95
+ @signal = ConditionVariable.new
96
+ @mutex = Mutex.new
97
+ @events = Queue.new
98
+ @current_packet = nil
99
+ @logger = Logger.new STDOUT
100
+ end
101
+
102
+ private
103
+
104
+ # Set the starting marker position
105
+ def mark(name)
106
+ send("__ragel_mark_#{name}=", @__ragel_p)
107
+ end
108
+
109
+ # Set the starting marker position for capturing raw data in an array
110
+ def mark_array(name)
111
+ send("__ragel_mark_#{name}=", @__ragel_p)
112
+ end
113
+
114
+ # Capture the marked data from the marker to the current position
115
+ def set(name)
116
+ mark = send("__ragel_mark_#{name}")
117
+ return if @__ragel_p == mark
118
+ send("__ragel_#{name}=", @__ragel_data[mark..@__ragel_p-1])
119
+ send("__ragel_mark_#{name}=", 0)
120
+ end
121
+
122
+ # Insert the data marked from the marker to the current position in the array
123
+ def insert(name)
124
+ mark = send("__ragel_mark_#{name}")
125
+ return if @__ragel_p == mark
126
+ var = send("__ragel_#{name}")
127
+ var << @__ragel_data[mark..@__ragel_p-1]
128
+ send("__ragel_#{name}=", var)
129
+ end
130
+
131
+ # Capture a key / value pair in a response packet
132
+ def pair
133
+ @current_packet[@__ragel_key] = @__ragel_value
134
+ end
135
+
136
+ # This method completes a packet. Add the current raw data to it if it
137
+ # is an immediate or raw response packet. If it has an action ID, it belongs
138
+ # to a command, so signal any waiters. If it does not, it is an asynchronous
139
+ # event, so add it to the event queue.
140
+ def packet
141
+ return if not @current_packet
142
+ @current_packet[:raw] = @__ragel_raw.join("\n") if @current_packet.raw?
143
+ action_id = nil
144
+ if not @current_packet.is_event? or @current_packet['ActionID']
145
+ action_id = @current_packet['ActionID'] || 0
146
+ end
147
+ logger.debug "Packet end: #{@__ragel_p}, #{@current_packet.class}, #{action_id.inspect}"
148
+ logger.debug "=====>#{@current_packet[:raw]}<=====" if @current_packet.raw?
149
+ if action_id
150
+ # Packets with IDs are associated with the action of the same ID
151
+ action = Actions::Action[action_id]
152
+ action << @current_packet
153
+ else
154
+ # Asynchronous events without IDs go into the event queue
155
+ @events.push(@current_packet)
156
+ end
157
+ @signal.broadcast
158
+ @current_packet = nil
159
+ @__ragel_raw = []
160
+ end
161
+
162
+ public
163
+ # Wait for any packets (including events) that have the specified Action ID.
164
+ # Do not stop waiting until all of the packets for the specified Action ID
165
+ # have been seen.
166
+ def wait(action)
167
+ logger.debug "Waiting for #{action.action_id.inspect}"
168
+ @mutex.synchronize do
169
+ loop do
170
+ action.check_error!
171
+ return action.packets! if action.done?
172
+ @signal.wait(@mutex)
173
+ end
174
+ end
175
+ end
176
+
177
+ # Receive an event packet from the event packet queue.
178
+ def receive
179
+ @events.pop
180
+ end
181
+
182
+ # Stop the scanner.
183
+ def stop
184
+ @mutex.synchronize do
185
+ @thread.kill if @thread
186
+ end
187
+ @thread = nil
188
+ end
189
+
190
+ # Run the scanner on the specified socket.
191
+ def run(socket)
192
+ @__ragel_eof = nil
193
+ @__ragel_data = " " * BUFSIZE
194
+ @__ragel_raw = []
195
+
196
+ ragel_init
197
+
198
+ # Synchronize, so we can wait for the command prompt before the
199
+ # scanner actually starts.
200
+ @mutex.synchronize do
201
+ @thread = Thread.new do
202
+ have = 0
203
+ loop do
204
+ # Grab as many bytes as we can for now.
205
+ space = BUFSIZE - have
206
+ raise RuntimeError, "No space" if space == 0
207
+ bytes = 0
208
+ begin
209
+ socket.synchronize do
210
+ if IO.select([socket], nil, nil, 1.0)
211
+ bytes = socket.read_nonblock(space)
212
+ else
213
+ retry
214
+ end
215
+ end
216
+ rescue Errno::EAGAIN
217
+ # Nothing available. Try again.
218
+ retry
219
+ rescue EOFError
220
+ # Socket closed. We are done.
221
+ break
222
+ end
223
+
224
+ # Adjust the pointers.
225
+ logger.debug "Got #{bytes.length} bytes, #{bytes.inspect}"
226
+ @__ragel_p = have
227
+ @__ragel_data[@__ragel_p..@__ragel_p + bytes.size - 1] = bytes
228
+ @__ragel_pe = @__ragel_p + bytes.size
229
+ logger.debug "P: #{@__ragel_p} PE: #{@__ragel_pe}"
230
+
231
+ # Run the scanner state machine.
232
+ @mutex.synchronize do
233
+ ragel_exec
234
+ end
235
+
236
+ if @__ragel_tokstart.nil? or @__ragel_tokstart == 0
237
+ have = 0
238
+ else
239
+ # Slide the window.
240
+ have = @__ragel_pe - @__ragel_tokstart
241
+ logger.debug "Sliding #{have} from #{@__ragel_tokstart} to 0 (tokend: #{@__ragel_tokend.inspect})"
242
+ @__ragel_data[0..have-1] = @__ragel_data[@__ragel_tokstart..@__ragel_tokstart + have - 1]
243
+ @__ragel_tokend -= @__ragel_tokstart if @__ragel_tokend
244
+ @__ragel_tokstart = 0
245
+ logger.debug "Data: #{@__ragel_data[0..have-1].inspect}"
246
+ end
247
+ end
248
+ @thread = nil
249
+ end
250
+ # Wait for the command prompt.
251
+ while @__ragel_version.blank?
252
+ @signal.wait(@mutex)
253
+ end
254
+ end
255
+ # Return the version number.
256
+ @__ragel_version
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,1284 @@
1
+
2
+ require 'adhearsion/voip/menu_state_machine/menu_class'
3
+
4
+ module Adhearsion
5
+ module VoIP
6
+ module Asterisk
7
+ module Commands
8
+
9
+ RESPONSE_PREFIX = "200 result=" unless defined? RESPONSE_PREFIX
10
+
11
+ # These are the status messages that asterisk will issue after a dial command is executed.
12
+ # More information here: http://www.voip-info.org/wiki/index.php?page=Asterisk+variable+DIALSTATUS
13
+ # Here is a current list of dial status messages which are not all necessarily supported by adhearsion:
14
+ #
15
+ # ANSWER: Call is answered. A successful dial. The caller reached the callee.
16
+ # BUSY: Busy signal. The dial command reached its number but the number is busy.
17
+ # NOANSWER: No answer. The dial command reached its number, the number rang for too long, then the dial timed out.
18
+ # CANCEL: Call is cancelled. The dial command reached its number but the caller hung up before the callee picked up.
19
+ # CONGESTION: Congestion. This status is usually a sign that the dialled number is not recognised.
20
+ # CHANUNAVAIL: Channel unavailable. On SIP, peer may not be registered.
21
+ # DONTCALL: Privacy mode, callee rejected the call
22
+ # TORTURE: Privacy mode, callee chose to send caller to torture menu
23
+ # INVALIDARGS: Error parsing Dial command arguments (added for Asterisk 1.4.1, SVN r53135-53136)
24
+ #
25
+ #
26
+ DIAL_STATUSES = Hash.new(:unknown).merge(:answer => :answered,
27
+ :congestion => :congested,
28
+ :busy => :busy,
29
+ :cancel => :cancelled,
30
+ :noanswer => :unanswered,
31
+ :cancelled => :cancelled,
32
+ :chanunavail => :channel_unavailable) unless defined? DIAL_STATUSES
33
+
34
+ DYNAMIC_FEATURE_EXTENSIONS = {
35
+ :attended_transfer => lambda do |options|
36
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
37
+ extend_dynamic_features_with "atxfer"
38
+ end,
39
+ :blind_transfer => lambda do
40
+ variable "TRANSFER_CONTEXT" => options[:context] if options && options.has_key?(:context)
41
+ extend_dynamic_features_with 'blindxfer'
42
+ end
43
+ } unless defined? DYNAMIC_FEATURE_EXTENSIONS
44
+
45
+ def write(message)
46
+ to_pbx.print(message)
47
+ end
48
+
49
+ def read
50
+ returning from_pbx.gets do |message|
51
+ ahn_log.agi.debug "<<< #{message}"
52
+ end
53
+ end
54
+
55
+ # This method is the underlying method executed by nearly all the command methods in this module.
56
+ # It is used to send the plaintext commands in the proper AGI format over TCP/IP back to an Asterisk server via the
57
+ # FAGI protocol.
58
+ # It is not recommended that you call this method directly unless you plan to write a new command method
59
+ # in which case use this method you to communicate directly with an Asterisk server via the FAGI protocol.
60
+ # For more information about FAGI visit: http://www.voip-info.org/wiki/view/Asterisk+FastAGI
61
+ def raw_response(message = nil)
62
+ ahn_log.agi.debug ">>> #{message}"
63
+ write message if message
64
+ read
65
+ end
66
+
67
+ # The answer command must be called first before any other commands can be issued.
68
+ # In typical adhearsion applications the answer command is called by default as soon
69
+ # as a call is transfered to a valid context in dialplan.rb.
70
+ # If you do not want your adhearsion application to automatically issue an answer command,
71
+ # then you must edit your startup.rb file and configure this setting.
72
+ # Keep in mind that you should not need to issue another answer command after
73
+ # an answer command has already been issued either explicitly by your code or implicitly
74
+ # by the standard adhearsion configuration.
75
+ def answer
76
+ raw_response "ANSWER"
77
+ true
78
+ end
79
+
80
+ # This asterisk dialplan command allows you to instruct Asterisk to start applications
81
+ # which are typically run from extensions.conf. For a complete list of these commands
82
+ # please visit: http://www.voip-info.org/wiki/view/Asterisk+-+documentation+of+application+commands
83
+ #
84
+ # The most common commands are already made available through the FAGI interface provided
85
+ # by this code base. For commands that do not fall into this category, then exec is what you
86
+ # should use.
87
+ #
88
+ # For example, if there are specific asterisk modules you have loaded that will not
89
+ # available through the standard commands provided through FAGI - then you can used EXEC.
90
+ #
91
+ # Example:
92
+ # execute 'SIPAddHeader', '"Call-Info: answer-after=0"
93
+ #
94
+ # Using execute in this way will add a header to an existing SIP call.
95
+ #
96
+ def execute(application, *arguments)
97
+ result = raw_response("EXEC #{application} #{arguments * '|'}")
98
+ return false if error?(result)
99
+ result
100
+ end
101
+
102
+ # Hangs up the current channel.
103
+ # After this command is issued, your application will stop executing.
104
+ # This should be used in the same way you would call the ruby exit() method to exit an application.
105
+ # If it is necessary to do some additional cleanup tasks before returning control back to asterisk, then
106
+ # make sure you have setup a begin...ensure block in the context of your adhearsion application dialplan.
107
+ def hangup
108
+ raw_response 'HANGUP'
109
+ end
110
+
111
+ # Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
112
+ # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. When playing
113
+ # numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
114
+ # is pronounced as "one hundred" instead of "one zero zero".
115
+ #
116
+ # Note: it's not necessary to supply a sound file extension; Asterisk will try to find a sound
117
+ # file encoded using the current channel's codec, if one exists. If not, it will transcode from
118
+ # the default codec (GSM). Asterisk stores its sound files in /var/lib/asterisk/sounds.
119
+ #
120
+ # Usage:
121
+ #
122
+ # play 'hello-world'
123
+ # play Time.now
124
+ # play %w"a-connect-charge-of 22 cents-per-minute will-apply"
125
+ # play "you-sound-cute", "what-are-you-wearing"
126
+ #
127
+ def play(*arguments)
128
+ arguments.flatten.each do |argument|
129
+ play_time(argument) || play_numeric(argument) || play_string(argument)
130
+ end
131
+ end
132
+
133
+ # Records a sound file with the given name. If no filename is specified a file named by Asterisk
134
+ # will be created and returned. Else the given filename will be returned. If a relative path is
135
+ # given, the file will be saved in the default Asterisk sound directory, /var/lib/spool/asterisk
136
+ # by default.
137
+ #
138
+ # Silence and maxduration is specified in seconds.
139
+ #
140
+ # Usage:
141
+ # record
142
+ # record '/path/to/my-file.gsm'
143
+ # record 'my-file.gsm', :silence => 5, :maxduration => 120
144
+ #
145
+ def record(*args)
146
+ options = args.last.kind_of?(Hash) ? args.pop : {}
147
+ filename = args.shift || "/tmp/recording_%d.gsm"
148
+ silence = options.delete(:silence) || 0
149
+ maxduration = options.delete(:maxduration) || 0
150
+
151
+ execute("Record", filename, silence, maxduration)
152
+
153
+ # If the user hangs up before the recording is entered, -1 is returned and RECORDED_FILE
154
+ # will not contain the name of the file, even though it IS in fact recorded.
155
+ filename.index("%d") ? get_variable('RECORDED_FILE') : filename
156
+ end
157
+
158
+ # Simulates pressing the specified digits over the current channel. Can be used to
159
+ # traverse a phone menu.
160
+ def dtmf(digits)
161
+ execute "SendDTMF", digits.to_s
162
+ end
163
+
164
+ def with_next_message(&block)
165
+ raise LocalJumpError, "Must supply a block" unless block_given?
166
+ block.call(next_message)
167
+ end
168
+
169
+ # This command shouled be used to advance to the next message in the Asterisk Comedian Voicemail application
170
+ def next_message
171
+ @call.inbox.pop
172
+ end
173
+
174
+ # This command should be used to check if a message is waiting on the Asterisk Comedian Voicemail application.
175
+ def messages_waiting?
176
+ not @call.inbox.empty?
177
+ end
178
+
179
+ # = Menu Command
180
+ #
181
+ # The following documentation was derived from this blog post on Jay Phillips' blog:
182
+ #
183
+ # http://jicksta.com/articles/2008/02/11/menu-command
184
+ #
185
+ # The menu() command solves the problem of building enormous input-fetching state machines in Ruby without first-class
186
+ # message passing facilities or an external DSL.
187
+ #
188
+ # Here is an example dialplan which uses the menu() command effectively.
189
+ #
190
+ # from_pstn {
191
+ # menu 'welcome', 'for-spanish-press-8', 'main-ivr',
192
+ # :timeout => 8.seconds, :tries => 3 do |link|
193
+ # link.shipment_status 1
194
+ # link.ordering 2
195
+ # link.representative 4
196
+ # link.spanish 8
197
+ # link.employee 900..999
198
+ #
199
+ # link.on_invalid { play 'invalid' }
200
+ #
201
+ # link.on_premature_timeout do |str|
202
+ # play 'sorry'
203
+ # end
204
+ #
205
+ # link.on_failure do
206
+ # play 'goodbye'
207
+ # hangup
208
+ # end
209
+ # end
210
+ # }
211
+ #
212
+ # shipment_status {
213
+ # # Fetch a tracking number and pass it to a web service.
214
+ # }
215
+ #
216
+ # ordering {
217
+ # # Enter another menu that lets them enter credit card
218
+ # # information and place their order over the phone.
219
+ # }
220
+ #
221
+ # representative {
222
+ # # Place the caller into a queue
223
+ # }
224
+ #
225
+ # spanish {
226
+ # # Special options for the spanish menu.
227
+ # }
228
+ #
229
+ # employee {
230
+ # dial "SIP/#{extension}" # Overly simplistic
231
+ # }
232
+ #
233
+ # The main detail to note is the declarations within the menu() command’s block. Each line seems to refer to a link object
234
+ # executing a seemingly arbitrary method with an argument that’s either a number or a Range of numbers. The +link+ object
235
+ # collects these arbitrary method invocations and assembles a set of rules. The seemingly arbitrary method name is the name
236
+ # of the context to which the menu should jump in case its argument (the pattern) is found to be a match.
237
+ #
238
+ # With these context names and patterns defined, the +menu()+ command plays in sequence the sound files you supply as
239
+ # arguments, stopping playback abruptly if the user enters a digit. If no digits were pressed when the files finish playing,
240
+ # it waits +:timeout+ seconds. If no digits are pressed after the timeout, it executes the +on_premature_timeout+ hook you
241
+ # define (if any) and then tries again a maximum of +:tries+ times. If digits are pressed that result in no possible match,
242
+ # it executes the +on_invalid+ hook. When/if all tries are exhausted with no positive match, it executes the +on_failure+
243
+ # hook after the other hook (e.g. +on_invalid+, then +on_failure+).
244
+ #
245
+ # When the +menu()+ state machine runs through the defined rules, it must distinguish between exact and potential matches.
246
+ # It’s important to understand the differences between these and how they affect the overall outcome:
247
+ #
248
+ # |---------------|-------------------|------------------------------------------------------|
249
+ # | exact matches | potential matches | result |
250
+ # |---------------|-------------------|------------------------------------------------------|
251
+ # | 0 | 0 | Fail and start over |
252
+ # | 1 | 0 | Match found! |
253
+ # | 0 | >0 | Get another digit |
254
+ # | >1 | 0 | Go with the first exact match |
255
+ # | 1 | >0 | Get another digit. If timeout, use exact match |
256
+ # | >1 | >0 | Get another digit. If timeout, use first exact match |
257
+ # |---------------|-------------------|------------------------------------------------------|
258
+ #
259
+ # == Database integration
260
+ #
261
+ # To do database integration, I recommend programatically executing methods on the link object within the block. For example:
262
+ #
263
+ # menu do |link|
264
+ # for employee in Employee.find(:all)
265
+ # link.internal employee.extension
266
+ # end
267
+ # end
268
+ #
269
+ # or this more efficient and Rubyish way
270
+ #
271
+ # menu do |link|
272
+ # link.internal *Employee.find(:all).map(&:extension)
273
+ # end
274
+ #
275
+ # If this second example seems like too much Ruby magic, let me explain — +Employee.find(:all)+ effectively does a “SELECT *
276
+ # FROM employees” on the database with ActiveRecord, returning (what you’d think is) an Array. The +map(&:extension)+ is
277
+ # fanciness that means “replace every instance in this Array with the result of calling extension on that object”. Now we
278
+ # have an Array of every extension in the database. The splat operator (*) before the argument changes the argument from
279
+ # being one argument (an Array) into a sequence of n arguments, where n is the number of items in the Array it’s “splatting”.
280
+ # Lastly, these arguments are passed to the internal method, the name of a context which will handle dialing this user if one
281
+ # of the supplied patterns matches.
282
+ #
283
+ # == Handling a successful pattern match
284
+ #
285
+ # Which brings me to another important note. Let’s say that the user’s input successfully matched one of the patterns
286
+ # returned by that Employe.find... magic. When it jumps to the internal context, that context can access the variable entered
287
+ # through the extension variable. This was a tricky design decision that I think, overall, works great. It makes the +menu()+
288
+ # command feel much more first-class in the Adhearsion dialplan grammar and decouples the receiving context from the menu
289
+ # that caused the jump. After all, the context doesn’t necessary need to be the endpoint from a menu; it can be its own entry
290
+ # point, making menu() effectively a pipeline of re-creating the call.
291
+ #
292
+ def menu(*args, &block)
293
+ options = args.last.kind_of?(Hash) ? args.pop : {}
294
+ sound_files = args.flatten
295
+
296
+ menu_instance = Menu.new(options, &block)
297
+
298
+ initial_digit_prompt = sound_files.any?
299
+
300
+ # This method is basically one big begin/rescue block. When we start the Menu state machine by continue()ing, the state
301
+ # machine will pass messages back to this method in the form of Exceptions. This decoupling allows the menu system to
302
+ # work on, say, Freeswitch and Asterisk both.
303
+ begin
304
+ if menu_instance.should_continue?
305
+ menu_instance.continue
306
+ else
307
+ menu_instance.execute_failure_hook
308
+ return :failed
309
+ end
310
+ rescue Menu::MenuResult => result_of_menu
311
+ case result_of_menu
312
+ when Menu::MenuResultInvalid
313
+ menu_instance.execute_invalid_hook
314
+ menu_instance.restart!
315
+ when Menu::MenuGetAnotherDigit
316
+
317
+ next_digit = play_sound_files_for_menu(menu_instance, sound_files)
318
+ if next_digit
319
+ menu_instance << next_digit
320
+ else
321
+ # The user timed out entering another digit!
322
+ case result_of_menu
323
+ when Menu::MenuGetAnotherDigitOrFinish
324
+ # This raises a ControlPassingException
325
+ jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
326
+ when Menu::MenuGetAnotherDigitOrTimeout
327
+ # This should execute premature_timeout AND reset if the number of retries
328
+ # has not been exhausted.
329
+ menu_instance.execute_timeout_hook
330
+ menu_instance.restart!
331
+ end
332
+ end
333
+ when Menu::MenuResultFound
334
+ jump_to result_of_menu.match_payload, :extension => result_of_menu.new_extension
335
+ else
336
+ raise "Unrecognized MenuResult! This may be a bug!"
337
+ end
338
+
339
+ # Retry will re-execute the begin block, preserving our changes to the menu_instance object.
340
+ retry
341
+
342
+ end
343
+ end
344
+
345
+ # This method is used to receive keypad input from the user. Digits are collected
346
+ # via DTMF (keypad) input until one of three things happens:
347
+ #
348
+ # 1. The number of digits you specify as the first argument is collected
349
+ # 2. The timeout you specify with the :timeout option elapses.
350
+ # 3. The "#" key (or the key you specify with :accept_key) is pressed
351
+ #
352
+ # Usage examples
353
+ #
354
+ # input # Receives digits until the caller presses the "#" key
355
+ # input 3 # Receives three digits. Can be 0-9, * or #
356
+ # input 5, :accept_key => "*" # Receive at most 5 digits, stopping if '*' is pressed
357
+ # input 1, :timeout => 1.minute # Receive a single digit, returning an empty
358
+ # string if the timeout is encountered
359
+ # input 9, :timeout => 7, :accept_key => "0" # Receives nine digits, returning
360
+ # # when the timeout is encountered
361
+ # # or when the "0" key is pressed.
362
+ # input 3, :play => "you-sound-cute"
363
+ # input :play => ["if-this-is-correct-press", 1, "otherwise-press", 2]
364
+ #
365
+ # When specifying files to play, the playback of the sequence of files will stop
366
+ # immediately when the user presses the first digit.
367
+ #
368
+ # The :timeout option works like a digit timeout, therefore each digit pressed
369
+ # causes the timer to reset. This is a much more user-friendly approach than an
370
+ # absolute timeout.
371
+ #
372
+ # Note that when you don't specify a digit limit, the :accept_key becomes "#"
373
+ # because there'd be no other way to end the collection of digits. You can
374
+ # obviously override this by passing in a new key with :accept_key.
375
+ def input(*args)
376
+ options = args.last.kind_of?(Hash) ? args.pop : {}
377
+ number_of_digits = args.shift
378
+
379
+ sound_files = Array options.delete(:play)
380
+ timeout = options.delete(:timeout)
381
+ terminating_key = options.delete(:accept_key)
382
+ terminating_key = if terminating_key
383
+ terminating_key.to_s
384
+ elsif number_of_digits.nil? && !terminating_key.equal?(false)
385
+ '#'
386
+ end
387
+
388
+ if number_of_digits && number_of_digits < 0
389
+ ahn_log.agi.warn "Giving -1 to input() is now deprecated. Don't specify a first " +
390
+ "argument to simulate unlimited digits." if number_of_digits == -1
391
+ raise ArgumentError, "The number of digits must be positive!"
392
+ end
393
+
394
+ buffer = ''
395
+ key = sound_files.any? ? interruptable_play(*sound_files) || '' : wait_for_digit(timeout || -1)
396
+ loop do
397
+ return buffer if key.nil?
398
+ if terminating_key
399
+ if key == terminating_key
400
+ return buffer
401
+ else
402
+ buffer << key
403
+ return buffer if number_of_digits && number_of_digits == buffer.length
404
+ end
405
+ else
406
+ buffer << key
407
+ return buffer if number_of_digits && number_of_digits == buffer.length
408
+ end
409
+ key = wait_for_digit timeout || -1
410
+ end
411
+ end
412
+
413
+ # An alternative to DialplanContextProc#+@. When jumping to a context, it will *not* resume executing
414
+ # the former context when the jumped-to context has finished executing. Make sure you don't have any
415
+ # +ensure+ closures which you expect to execute when the call has finished, as they will run when
416
+ # this method is called.
417
+ #
418
+ # You can optionally override certain dialplan variables when jumping to the context. A popular use of
419
+ # this is to redefine +extension+ (which this method automatically boxes with a PhoneNumber object) so
420
+ # you can effectively "restart" a call (from the perspective of the jumped-to context). When you override
421
+ # variables here, you're effectively blowing away the old variables. If you need them for some reason,
422
+ # you should assign the important ones to an instance variable first before calling this method.
423
+ def jump_to(context, overrides={})
424
+ context = lookup_context_with_name(context) if context.kind_of?(Symbol) || (context.kind_of?(String) && context =~ /^[\w_]+$/)
425
+ raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException unless context.kind_of?(Adhearsion::DialPlan::DialplanContextProc)
426
+
427
+ if overrides.any?
428
+ overrides = overrides.symbolize_keys
429
+ if overrides.has_key?(:extension) && !overrides[:extension].kind_of?(Adhearsion::VoIP::DSL::PhoneNumber)
430
+ overrides[:extension] = Adhearsion::VoIP::DSL::PhoneNumber.new overrides[:extension]
431
+ end
432
+
433
+ overrides.each_pair do |key, value|
434
+ meta_def(key) { value }
435
+ end
436
+ end
437
+
438
+ raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context)
439
+ end
440
+
441
+ def queue(queue_name)
442
+ queue_name = queue_name.to_s
443
+
444
+ @queue_proxy_hash_lock = Mutex.new unless defined? @queue_proxy_hash_lock
445
+ @queue_proxy_hash_lock.synchronize do
446
+ @queue_proxy_hash ||= {}
447
+ if @queue_proxy_hash.has_key? queue_name
448
+ return @queue_proxy_hash[queue_name]
449
+ else
450
+ proxy = @queue_proxy_hash[queue_name] = QueueProxy.new(queue_name, self)
451
+ return proxy
452
+ end
453
+ end
454
+ end
455
+
456
+ # Returns the status of the last dial(). Possible dial
457
+ # statuses include :answer, :busy, :no_answer, :cancelled,
458
+ # :congested, and :channel_unavailable. If :cancel is
459
+ # returned, the caller hung up before the callee picked
460
+ # up. If :congestion is returned, the dialed extension
461
+ # probably doesn't exist. If :channel_unavailable, the callee
462
+ # phone may not be registered.
463
+ def last_dial_status
464
+ DIAL_STATUSES[get_dial_status]
465
+ end
466
+
467
+ # Returns true if your last call to dial() finished with the ANSWER state, as reported
468
+ # by Asterisk. Returns false otherwise
469
+ def last_dial_successful?
470
+ last_dial_status == :answered
471
+ end
472
+
473
+ # Opposite of last_dial_successful?()
474
+ def last_dial_unsuccessful?
475
+ not last_dial_successful?
476
+ end
477
+
478
+ # This feature is presently experimental! Do not use it!
479
+ def speak(text, engine=:none)
480
+ engine = Adhearsion::Configuration::AsteriskConfiguration.speech_engine || engine
481
+ execute SpeechEngines.send(engine, text)
482
+ end
483
+
484
+ # This method is a high-level way of enabling features you create/uncomment from features.conf.
485
+ #
486
+ # Certain Symbol features you enable (as defined in DYNAMIC_FEATURE_EXTENSIONS) have optional
487
+ # arguments that you can also specify here. The usage examples show how to do this.
488
+ #
489
+ # Usage examples:
490
+ #
491
+ # enable_feature :attended_transfer # Enables "atxfer"
492
+ #
493
+ # enable_feature :attended_transfer, :context => "my_dial" # Enables "atxfer" and then
494
+ # # sets "TRANSFER_CONTEXT" to :context's value
495
+ #
496
+ # enable_feature :blind_transfer, :context => 'my_dial' # Enables 'blindxfer' and sets TRANSFER_CONTEXT
497
+ #
498
+ # enable_feature "foobar" # Enables "foobar"
499
+ #
500
+ # enable_feature("dup"); enable_feature("dup") # Enables "dup" only once.
501
+ def enable_feature(feature_name, optional_options=nil)
502
+ if DYNAMIC_FEATURE_EXTENSIONS.has_key? feature_name
503
+ instance_exec(optional_options, &DYNAMIC_FEATURE_EXTENSIONS[feature_name])
504
+ else
505
+ raise ArgumentError, "You cannot supply optional options when the feature name is " +
506
+ "not internally recognized!" if optional_options
507
+ extend_dynamic_features_with feature_name
508
+ end
509
+ end
510
+
511
+ # Disables a feature name specified in features.conf. If you're disabling it, it was probably
512
+ # set by enable_feature().
513
+ def disable_feature(feature_name)
514
+ enabled_features_variable = variable 'DYNAMIC_FEATURES'
515
+ enabled_features = enabled_features_variable.split('#')
516
+ if enabled_features.include? feature_name
517
+ enabled_features.delete feature_name
518
+ variable 'DYNAMIC_FEATURES' => enabled_features.join('#')
519
+ end
520
+ end
521
+
522
+ # Used to join a particular conference with the MeetMe application. To
523
+ # use MeetMe, be sure you have a proper timing device configured on your
524
+ # Asterisk box. MeetMe is Asterisk's built-in conferencing program.
525
+ # More info: http://www.voip-info.org/wiki-Asterisk+cmd+MeetMe
526
+ def join(conference_id, options={})
527
+ conference_id = conference_id.to_s.scan(/\w/).join
528
+ command_flags = options[:options].to_s # This is a passthrough string straight to Asterisk
529
+ pin = options[:pin]
530
+ raise ArgumentError, "A conference PIN number must be numerical!" if pin && pin.to_s !~ /^\d+$/
531
+ # The 'd' option of MeetMe creates conferences dynamically.
532
+ command_flags += 'd' unless command_flags.include? 'd'
533
+
534
+ execute "MeetMe", conference_id, command_flags, options[:pin]
535
+ end
536
+
537
+ # Issue this command to access a channel variable that exists in the asterisk dialplan (i.e. extensions.conf)
538
+ # A complete description is available here: http://www.voip-info.org/wiki/view/get+variable
539
+ # Use get_variable to pass information from other modules or high level configurations from the asterisk dialplan
540
+ # to the adhearsion dialplan.
541
+ def get_variable(variable_name)
542
+ result = raw_response("GET VARIABLE #{variable_name}")
543
+ case result
544
+ when "200 result=0"
545
+ return nil
546
+ when /^200 result=1 \((.*)\)$/
547
+ return $LAST_PAREN_MATCH
548
+ end
549
+ end
550
+
551
+ # Use set_variable to pass information back to the asterisk dial plan.
552
+ # A complete decription is available here: http://www.voip-info.org/wiki/view/set+variable
553
+ # Keep in mind that the variables are not global variables. These variables only exist for the channel
554
+ # related to the call that is being serviced by the particular instance of your adhearsion application.
555
+ # You will not be able to pass information back to the asterisk dialplan for other instances of your adhearsion
556
+ # application to share. Once the channel is "hungup" then the variables are cleared and their information is gone.
557
+ def set_variable(variable_name, value)
558
+ raw_response("SET VARIABLE %s %p" % [variable_name.to_s, value.to_s]) == "200 result=1"
559
+ end
560
+
561
+ def variable(*args)
562
+ if args.last.kind_of? Hash
563
+ assignments = args.pop
564
+ raise ArgumentError, "Can't mix variable setting and fetching!" if args.any?
565
+ assignments.each_pair do |key, value|
566
+ set_variable(key, value)
567
+ end
568
+ else
569
+ if args.size == 1
570
+ get_variable args.first
571
+ else
572
+ args.map { |var| get_variable(var) }
573
+ end
574
+ end
575
+ end
576
+
577
+ def voicemail(*args)
578
+ options_hash = args.last.kind_of?(Hash) ? args.pop : {}
579
+ mailbox_number = args.shift
580
+ greeting_option = options_hash.delete(:greeting)
581
+ skip_option = options_hash.delete(:skip)
582
+ raise ArgumentError, 'You supplied too many arguments!' if mailbox_number && options_hash.any?
583
+ greeting_option = case greeting_option
584
+ when :busy: 'b'
585
+ when :unavailable: 'u'
586
+ when nil: nil
587
+ else raise ArgumentError, "Unrecognized greeting #{greeting_option}"
588
+ end
589
+ skip_option &&= 's'
590
+ options = "#{greeting_option}#{skip_option}"
591
+
592
+ raise ArgumentError, "Mailbox cannot be blank!" if !mailbox_number.nil? && mailbox_number.blank?
593
+ number_with_context = if mailbox_number then mailbox_number else
594
+ raise ArgumentError, "You must supply ONE context name!" if options_hash.size != 1
595
+ context_name, mailboxes = options_hash.to_a.first
596
+ Array(mailboxes).map do |mailbox|
597
+ raise ArgumentError, "Mailbox numbers must be numerical!" unless mailbox.to_s =~ /^\d+$/
598
+ "#{mailbox}@#{context_name}"
599
+ end.join('&')
600
+ end
601
+ execute('voicemail', number_with_context, options)
602
+ case variable('VMSTATUS')
603
+ when 'SUCCESS': true
604
+ when 'USEREXIT': false
605
+ else nil
606
+ end
607
+ end
608
+
609
+ def voicemail_main(options={})
610
+ mailbox, context, folder = options.values_at :mailbox, :context, :folder
611
+ authenticate = options.has_key?(:authenticate) ? options[:authenticate] : true
612
+
613
+ folder = if folder
614
+ if folder.to_s =~ /^[\w_]+$/
615
+ "a(#{folder})"
616
+ else
617
+ raise ArgumentError, "Voicemail folder must be alphanumerical/underscore characters only!"
618
+ end
619
+ elsif folder == ''
620
+ raise "Folder name cannot be an empty String!"
621
+ else
622
+ nil
623
+ end
624
+
625
+ real_mailbox = ""
626
+ real_mailbox << "#{mailbox}" unless mailbox.blank?
627
+ real_mailbox << "@#{context}" unless context.blank?
628
+
629
+ real_options = ""
630
+ real_options << "s" if !authenticate
631
+ real_options << folder unless folder.blank?
632
+
633
+ command_args = [real_mailbox]
634
+ command_args << real_options unless real_options.blank?
635
+ command_args.clear if command_args == [""]
636
+
637
+ execute 'VoiceMailMain', *command_args
638
+ end
639
+
640
+ def check_voicemail
641
+ ahn_log.agi.warn "THE check_voicemail() DIALPLAN METHOD WILL SOON BE DEPRECATED! CHANGE THIS TO voicemail_main() INSTEAD"
642
+ voicemail_main
643
+ end
644
+
645
+ # Use this command to dial an extension i.e. "phone number" in asterisk
646
+ # This command maps to the Asterisk DIAL command in the asterisk dialplan: http://www.voip-info.org/wiki-Asterisk+cmd+Dial
647
+ #
648
+ # The first parameter, number, must be a string that represents the extension or "number" that asterisk should dial.
649
+ # Be careful to not just specify a number like 5001, 9095551001
650
+ # You must specify a properly formatted string as Asterisk would expect to use in order to understand
651
+ # whether the call should be dialed using SIP, IAX, or some other means.
652
+ # Examples:
653
+ #
654
+ # Make a call to the PSTN using my SIP provider for VoIP termination:
655
+ # dial("SIP/19095551001@my.sip.voip.terminator.us")
656
+ #
657
+ # Make 3 Simulataneous calls to the SIP extensions separated by & symbols, try for 15 seconds and use the callerid
658
+ # for this call specified by the variable my_callerid
659
+ # dial "SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => my_callerid
660
+ #
661
+ # Make a call using the IAX provider to the PSTN
662
+ # dial("IAX2/my.id@voipjet/19095551234", :name=>"John Doe", :caller_id=>"9095551234")
663
+ #
664
+ # Options Parameter:
665
+ # :caller_id - the caller id number to be used when the call is placed. It is advised you properly adhere to the
666
+ # policy of VoIP termination providers with respect to caller id values.
667
+ #
668
+ # :name - this is the name which should be passed with the caller ID information
669
+ # if :name=>"John Doe" and :caller_id => "444-333-1000" then the compelete CID and name would be "John Doe" <4443331000>
670
+ # support for caller id information varies from country to country and from one VoIP termination provider to another.
671
+ #
672
+ # :for - this option can be thought of best as a timeout. i.e. timeout after :for if no one answers the call
673
+ # For example, dial("SIP/jay-desk-650&SIP/jay-desk-601&SIP/jay-desk-601-2", :for => 15.seconds, :caller_id => callerid)
674
+ # this call will timeout after 15 seconds if 1 of the 3 extensions being dialed do not pick prior to the 15 second time limit
675
+ #
676
+ # :options - This is a string of options like "Tr" which are supported by the asterisk DIAL application.
677
+ # for a complete list of these options and their usage please visit: http://www.voip-info.org/wiki-Asterisk+cmd+Dial
678
+ #
679
+ # :confirm - ?
680
+ #
681
+ def dial(number, options={})
682
+ *recognized_options = :caller_id, :name, :for, :options, :confirm
683
+
684
+ unrecognized_options = options.keys - recognized_options
685
+ raise ArgumentError, "Unknown dial options: #{unrecognized_options.to_sentence}" if unrecognized_options.any?
686
+ set_caller_id_name options[:name]
687
+ set_caller_id_number options[:caller_id]
688
+ confirm_option = dial_macro_option_compiler options[:confirm]
689
+ all_options = options[:options]
690
+ all_options = all_options ? all_options + confirm_option : confirm_option
691
+ execute "Dial", number, options[:for], all_options
692
+ end
693
+
694
+
695
+ # This implementation of dial() uses the experimental call routing DSL.
696
+ #
697
+ # def dial(number, options={})
698
+ # rules = callable_routes_for number
699
+ # return :no_route if rules.empty?
700
+ # call_attempt_status = nil
701
+ # rules.each do |provider|
702
+ #
703
+ # response = execute "Dial",
704
+ # provider.format_number_for_platform(number),
705
+ # timeout_from_dial_options(options),
706
+ # asterisk_options_from_dial_options(options)
707
+ #
708
+ # call_attempt_status = last_dial_status
709
+ # break if call_attempt_status == :answered
710
+ # end
711
+ # call_attempt_status
712
+ # end
713
+
714
+
715
+ # Speaks the digits given as an argument. For example, "123" is spoken as "one two three".
716
+ def say_digits(digits)
717
+ execute "saydigits", validate_digits(digits)
718
+ end
719
+
720
+ # Returns the number of seconds the given block takes to execute as a Float. This
721
+ # is particularly useful in dialplans for tracking billable time. Note that
722
+ # if the call is hung up during the block, you will need to rescue the
723
+ # exception if you have some mission-critical logic after it with which
724
+ # you're recording this return-value.
725
+ def duration_of
726
+ start_time = Time.now
727
+ yield
728
+ Time.now - start_time
729
+ end
730
+
731
+ protected
732
+
733
+ def wait_for_digit(timeout=-1)
734
+ timeout *= 1_000 if timeout != -1
735
+ result = result_digit_from raw_response("WAIT FOR DIGIT #{timeout.to_i}")
736
+ (result == 0.chr) ? nil : result
737
+ end
738
+
739
+ def interruptable_play(*files)
740
+ files.flatten.each do |file|
741
+ result = result_digit_from raw_response("EXEC BACKGROUND #{file}")
742
+ return result if result != 0.chr
743
+ end
744
+ nil
745
+ end
746
+
747
+ def set_caller_id_number(caller_id)
748
+ return unless caller_id
749
+ raise ArgumentError, "Caller ID must be numerical" if caller_id.to_s !~ /^\d+$/
750
+ raw_response %(SET CALLERID %p) % caller_id
751
+ end
752
+
753
+ def set_caller_id_name(caller_id_name)
754
+ return unless caller_id_name
755
+ variable "CALLERID(name)" => caller_id_name
756
+ end
757
+
758
+ def timeout_from_dial_options(options)
759
+ options[:for] || options[:timeout]
760
+ end
761
+
762
+ def asterisk_options_from_dial_options(options)
763
+ # TODO: Will become much more sophisticated soon to handle callerid, etc
764
+ options[:options]
765
+ end
766
+
767
+ def dial_macro_option_compiler(confirm_argument_value)
768
+ defaults = { :macro => 'ahn_dial_confirmer',
769
+ :timeout => 20.seconds,
770
+ :play => "beep",
771
+ :key => '#' }
772
+
773
+ case confirm_argument_value
774
+ when true
775
+ DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument(defaults)
776
+ when false, nil
777
+ ''
778
+ when Proc
779
+ raise NotImplementedError, "Coming in the future, you can do :confirm => my_context."
780
+
781
+ when Hash
782
+ options = defaults.merge confirm_argument_value
783
+ if((confirm_argument_value.keys - defaults.keys).any?)
784
+ raise ArgumentError, "Known options: #{defaults.keys.to_sentence}"
785
+ end
786
+ raise ArgumentError, "Bad macro name!" unless options[:macro].to_s =~ /^[\w_]+$/
787
+ options[:timeout] = case options[:timeout]
788
+ when Fixnum, ActiveSupport::Duration
789
+ options[:timeout]
790
+ when String
791
+ raise ArgumentError, "Timeout must be numerical!" unless options[:timeout] =~ /^\d+$/
792
+ options[:timeout].to_i
793
+ when :none
794
+ 0
795
+ else
796
+ raise ArgumentError, "Unrecognized :timeout! #{options[:timeout].inspect}"
797
+ end
798
+ raise ArgumentError, "Unrecognized DTMF key: #{options[:key]}" unless options[:key].to_s =~ /^[\d#*]$/
799
+ options[:play] = Array(options[:play]).join('++')
800
+ DialPlan::ConfirmationManager.encode_hash_for_dial_macro_argument options
801
+
802
+ else
803
+ raise ArgumentError, "Unrecognized :confirm option: #{confirm_argument_value.inspect}!"
804
+ end
805
+ end
806
+
807
+ def result_digit_from(response_string)
808
+ raise ArgumentError, "Can't coerce nil into AGI response! This could be a bug!" unless response_string
809
+ digit = response_string[/^#{response_prefix}(-?\d+(\.\d+)?)/,1]
810
+ digit.to_i.chr if digit && digit.to_s != "-1"
811
+ end
812
+
813
+ def extract_input_from(result)
814
+ return false if error?(result)
815
+ # return false if input_timed_out?(result)
816
+
817
+ # This regexp doesn't match if there was a timeout with no
818
+ # inputted digits, therefore returning nil.
819
+
820
+ result[/^#{response_prefix}([\d*]+)/, 1]
821
+ end
822
+
823
+ def extract_variable_from(result)
824
+ return false if error?(result)
825
+ result[/^#{response_prefix}1 \((.+)\)/, 1]
826
+ end
827
+
828
+ def get_dial_status
829
+ dial_status = variable('DIALSTATUS')
830
+ dial_status ? dial_status.downcase.to_sym : :cancelled
831
+ end
832
+
833
+ def play_time(argument)
834
+ if argument.kind_of? Time
835
+ execute(:sayunixtime, argument.to_i)
836
+ end
837
+ end
838
+
839
+ def play_numeric(argument)
840
+ if argument.kind_of?(Numeric) || argument =~ /^\d+$/
841
+ execute(:saynumber, argument)
842
+ end
843
+ end
844
+
845
+ def play_string(argument)
846
+ execute(:playback, argument)
847
+ end
848
+
849
+ def play_sound_files_for_menu(menu_instance, sound_files)
850
+ digit = nil
851
+ if sound_files.any? && menu_instance.digit_buffer_empty?
852
+ digit = interruptable_play(*sound_files)
853
+ end
854
+ digit || wait_for_digit(menu_instance.timeout)
855
+ end
856
+
857
+ def extend_dynamic_features_with(feature_name)
858
+ current_variable = variable("DYNAMIC_FEATURES") || ''
859
+ enabled_features = current_variable.split '#'
860
+ unless enabled_features.include? feature_name
861
+ enabled_features << feature_name
862
+ variable "DYNAMIC_FEATURES" => enabled_features.join('#')
863
+ end
864
+ end
865
+
866
+ def jump_to_context_with_name(context_name)
867
+ context_lambda = lookup_context_with_name context_name
868
+ raise Adhearsion::VoIP::DSL::Dialplan::ControlPassingException.new(context_lambda)
869
+ end
870
+
871
+ def lookup_context_with_name(context_name)
872
+ begin
873
+ send context_name
874
+ rescue NameError
875
+ raise Adhearsion::VoIP::DSL::Dialplan::ContextNotFoundException
876
+ end
877
+ end
878
+
879
+ def redefine_extension_to_be(new_extension)
880
+ new_extension = Adhearsion::VoIP::DSL::PhoneNumber.new new_extension
881
+ meta_def(:extension) { new_extension }
882
+ end
883
+
884
+ def to_pbx
885
+ io
886
+ end
887
+
888
+ def from_pbx
889
+ io
890
+ end
891
+
892
+ def validate_digits(digits)
893
+ returning digits.to_s do |digits_as_string|
894
+ raise ArgumentError, "Can only be called with valid digits!" unless digits_as_string =~ /^\d+$/
895
+ end
896
+ end
897
+
898
+ def error?(result)
899
+ result.to_s[/^#{response_prefix}(?:-\d+|0)/]
900
+ end
901
+
902
+ # timeout with pressed digits: 200 result=<digits> (timeout)
903
+ # timeout without pressed digits: 200 result= (timeout)
904
+ # (http://www.voip-info.org/wiki/view/get+data)
905
+ def input_timed_out?(result)
906
+ result.starts_with?(response_prefix) && result.ends_with?('(timeout)')
907
+ end
908
+
909
+ def io
910
+ call.io
911
+ end
912
+
913
+ def response_prefix
914
+ RESPONSE_PREFIX
915
+ end
916
+
917
+ class QueueProxy
918
+
919
+ class << self
920
+
921
+ def format_join_hash_key_arguments(options)
922
+
923
+ bad_argument = lambda do |(key, value)|
924
+ raise ArgumentError, "Unrecognize value for #{key.inspect} -- #{value.inspect}"
925
+ end
926
+
927
+ # Direct Queue() arguments:
928
+ timeout = options.delete :timeout
929
+ announcement = options.delete :announce
930
+
931
+ # Terse single-character options
932
+ ring_style = options.delete :play
933
+ allow_hangup = options.delete :allow_hangup
934
+ allow_transfer = options.delete :allow_transfer
935
+
936
+ raise ArgumentError, "Unrecognized args to join!: #{options.inspect}" if options.any?
937
+
938
+ ring_style = case ring_style
939
+ when :ringing: 'r'
940
+ when :music: ''
941
+ when nil
942
+ else bad_argument[:play => ring_style]
943
+ end.to_s
944
+
945
+ allow_hangup = case allow_hangup
946
+ when :caller: 'H'
947
+ when :agent: 'h'
948
+ when :everyone: 'Hh'
949
+ when nil
950
+ else bad_argument[:allow_hangup => allow_hangup]
951
+ end.to_s
952
+
953
+ allow_transfer = case allow_transfer
954
+ when :caller: 'T'
955
+ when :agent: 't'
956
+ when :everyone: 'Tt'
957
+ when nil
958
+ else bad_argument[:allow_transfer => allow_transfer]
959
+ end.to_s
960
+
961
+ terse_character_options = ring_style + allow_transfer + allow_hangup
962
+
963
+ [terse_character_options, '', announcement, timeout].map(&:to_s)
964
+ end
965
+
966
+ end
967
+
968
+ attr_reader :name, :environment
969
+ def initialize(name, environment)
970
+ @name, @environment = name, environment
971
+ end
972
+
973
+ # Makes the current channel join the queue. Below are explanations of the recognized Hash-key
974
+ # arguments supported by this method.
975
+ #
976
+ # :timeout - The number of seconds to wait for an agent to answer
977
+ # :play - Can be :ringing or :music.
978
+ # :announce - A sound file to play instead of the normal queue announcement.
979
+ # :allow_transfer - Can be :caller, :agent, or :everyone. Allow someone to transfer the call.
980
+ # :allow_hangup - Can be :caller, :agent, or :everyone. Allow someone to hangup with the * key.
981
+ #
982
+ # Usage examples:
983
+ #
984
+ # - queue('sales').join!
985
+ # - queue('sales').join! :timeout => 1.minute
986
+ # - queue('sales').join! :play => :music
987
+ # - queue('sales').join! :play => :ringing
988
+ # - queue('sales').join! :announce => "custom/special-queue-announcement"
989
+ # - queue('sales').join! :allow_transfer => :caller
990
+ # - queue('sales').join! :allow_transfer => :agent
991
+ # - queue('sales').join! :allow_hangup => :caller
992
+ # - queue('sales').join! :allow_hangup => :agent
993
+ # - queue('sales').join! :allow_hangup => :everyone
994
+ # - queue('sales').join! :allow_transfer => :agent, :timeout => 30.seconds,
995
+ def join!(options={})
996
+ environment.execute("queue", name, *self.class.format_join_hash_key_arguments(options))
997
+ normalize_queue_status_variable environment.variable("QUEUESTATUS")
998
+ end
999
+
1000
+ def agents(options={})
1001
+ cached = options.has_key?(:cache) ? options.delete(:cache) : true
1002
+ raise ArgumentError, "Unrecognized arguments to agents(): #{options.inspect}" if options.keys.any?
1003
+ if cached
1004
+ @cached_proxy ||= QueueAgentsListProxy.new(self, true)
1005
+ else
1006
+ @uncached_proxy ||= QueueAgentsListProxy.new(self, false)
1007
+ end
1008
+ end
1009
+
1010
+ def waiting_count
1011
+ raise QueueDoesNotExistError.new(name) unless exists?
1012
+ environment.variable("QUEUE_WAITING_COUNT(#{name})").to_i
1013
+ end
1014
+
1015
+ def empty?
1016
+ waiting_count == 0
1017
+ end
1018
+
1019
+ def any?
1020
+ waiting_count > 0
1021
+ end
1022
+
1023
+ def exists?
1024
+ environment.execute('RemoveQueueMember', name, 'SIP/AdhearsionQueueExistenceCheck')
1025
+ environment.variable("RQMSTATUS") != 'NOSUCHQUEUE'
1026
+ end
1027
+
1028
+ private
1029
+
1030
+ def normalize_queue_status_variable(variable)
1031
+ returning variable.downcase.to_sym do |queue_status|
1032
+ raise QueueDoesNotExistError.new(name) if queue_status == :unknown
1033
+ end
1034
+ end
1035
+
1036
+ class QueueAgentsListProxy
1037
+
1038
+ include Enumerable
1039
+
1040
+ attr_reader :proxy, :agents
1041
+ def initialize(proxy, cached=false)
1042
+ @proxy = proxy
1043
+ @cached = cached
1044
+ end
1045
+
1046
+ def count
1047
+ if cached? && @cached_count
1048
+ @cached_count
1049
+ else
1050
+ @cached_count = proxy.environment.variable("QUEUE_MEMBER_COUNT(#{proxy.name})").to_i
1051
+ end
1052
+ end
1053
+ alias size count
1054
+ alias length count
1055
+
1056
+ # Supported Hash-key arguments are :penalty and :name. The :name value will be viewable in
1057
+ # the queue_log. The :penalty is the penalty assigned to this agent for answering calls on
1058
+ # this queue
1059
+ def new(*args)
1060
+
1061
+ options = args.last.kind_of?(Hash) ? args.pop : {}
1062
+ interface = args.shift || ''
1063
+
1064
+ raise ArgumentError, "You may only supply an interface and a Hash argument!" if args.any?
1065
+
1066
+ penalty = options.delete(:penalty) || ''
1067
+ name = options.delete(:name) || ''
1068
+
1069
+ raise ArgumentError, "Unrecognized argument(s): #{options.inspect}" if options.any?
1070
+
1071
+ proxy.environment.execute("AddQueueMember", proxy.name, interface, penalty, '', name)
1072
+
1073
+ case proxy.environment.variable("AQMSTATUS")
1074
+ when "ADDED" : true
1075
+ when "MEMBERALREADY" : false
1076
+ when "NOSUCHQUEUE" : raise QueueDoesNotExistError.new(proxy.name)
1077
+ else
1078
+ raise "UNRECOGNIZED AQMSTATUS VALUE!"
1079
+ end
1080
+
1081
+ # TODO: THIS SHOULD RETURN AN AGENT INSTANCE
1082
+ end
1083
+
1084
+ # Logs a pre-defined agent into this queue and waits for calls. Pass in :silent => true to stop
1085
+ # the message which says "Agent logged in".
1086
+ def login!(*args)
1087
+ options = args.last.kind_of?(Hash) ? args.pop : {}
1088
+
1089
+ silent = options.delete(:silent).equal?(false) ? '' : 's'
1090
+ id = args.shift
1091
+ id &&= AgentProxy.id_from_agent_channel(id)
1092
+ raise ArgumentError, "Unrecognized Hash options to login(): #{options.inspect}" if options.any?
1093
+ raise ArgumentError, "Unrecognized argument to login(): #{args.inspect}" if args.any?
1094
+
1095
+ proxy.environment.execute('AgentLogin', id, silent)
1096
+ end
1097
+
1098
+ # Removes the current channel from this queue
1099
+ def logout!
1100
+ # TODO: DRY this up. Repeated in the AgentProxy...
1101
+ proxy.environment.execute 'RemoveQueueMember', proxy.name
1102
+ case proxy.environment.variable("RQMSTATUS")
1103
+ when "REMOVED" : true
1104
+ when "NOTINQUEUE" : false
1105
+ when "NOSUCHQUEUE"
1106
+ raise QueueDoesNotExistError.new(proxy.name)
1107
+ else
1108
+ raise "Unrecognized RQMSTATUS variable!"
1109
+ end
1110
+ end
1111
+
1112
+ def each(&block)
1113
+ check_agent_cache!
1114
+ agents.each(&block)
1115
+ end
1116
+
1117
+ def first
1118
+ check_agent_cache!
1119
+ agents.first
1120
+ end
1121
+
1122
+ def last
1123
+ check_agent_cache!
1124
+ agents.last
1125
+ end
1126
+
1127
+ def cached?
1128
+ @cached
1129
+ end
1130
+
1131
+ def to_a
1132
+ check_agent_cache!
1133
+ @agents
1134
+ end
1135
+
1136
+ private
1137
+
1138
+ def check_agent_cache!
1139
+ if cached?
1140
+ load_agents! unless agents
1141
+ else
1142
+ load_agents!
1143
+ end
1144
+ end
1145
+
1146
+ def load_agents!
1147
+ raw_data = proxy.environment.variable "QUEUE_MEMBER_LIST(#{proxy.name})"
1148
+ @agents = raw_data.split(',').map(&:strip).reject(&:empty?).map do |agent|
1149
+ AgentProxy.new(agent, proxy)
1150
+ end
1151
+ @cached_count = @agents.size
1152
+ end
1153
+
1154
+ end
1155
+
1156
+ class AgentProxy
1157
+
1158
+ SUPPORTED_METADATA_NAMES = %w[status password name mohclass exten channel] unless defined? SUPPORTED_METADATA_NAMES
1159
+
1160
+ class << self
1161
+ def id_from_agent_channel(id)
1162
+ id = id.to_s
1163
+ id.starts_with?('Agent/') ? id[%r[^Agent/(.+)$],1] : id
1164
+ end
1165
+ end
1166
+
1167
+ attr_reader :interface, :proxy, :queue_name, :id
1168
+ def initialize(interface, proxy)
1169
+ @interface = interface
1170
+ @id = self.class.id_from_agent_channel interface
1171
+ @proxy = proxy
1172
+ @queue_name = proxy.name
1173
+ end
1174
+
1175
+ def remove!
1176
+ proxy.environment.execute 'RemoveQueueMember', queue_name, interface
1177
+ case proxy.environment.variable("RQMSTATUS")
1178
+ when "REMOVED" : true
1179
+ when "NOTINQUEUE" : false
1180
+ when "NOSUCHQUEUE"
1181
+ raise QueueDoesNotExistError.new(queue_name)
1182
+ else
1183
+ raise "Unrecognized RQMSTATUS variable!"
1184
+ end
1185
+ end
1186
+
1187
+ # Pauses the given agent for this queue only. If you wish to pause this agent
1188
+ # for all queues, pass in :everywhere => true. Returns true if the agent was
1189
+ # successfully paused and false if the agent was not found.
1190
+ def pause!(options={})
1191
+ everywhere = options.delete(:everywhere)
1192
+ args = [(everywhere ? nil : queue_name), interface]
1193
+ proxy.environment.execute('PauseQueueMember', *args)
1194
+ case proxy.environment.variable("PQMSTATUS")
1195
+ when "PAUSED" : true
1196
+ when "NOTFOUND" : false
1197
+ else
1198
+ raise "Unrecognized PQMSTATUS value!"
1199
+ end
1200
+ end
1201
+
1202
+ # Pauses the given agent for this queue only. If you wish to pause this agent
1203
+ # for all queues, pass in :everywhere => true. Returns true if the agent was
1204
+ # successfully paused and false if the agent was not found.
1205
+ def unpause!(options={})
1206
+ everywhere = options.delete(:everywhere)
1207
+ args = [(everywhere ? nil : queue_name), interface]
1208
+ proxy.environment.execute('UnpauseQueueMember', *args)
1209
+ case proxy.environment.variable("UPQMSTATUS")
1210
+ when "UNPAUSED" : true
1211
+ when "NOTFOUND" : false
1212
+ else
1213
+ raise "Unrecognized UPQMSTATUS value!"
1214
+ end
1215
+ end
1216
+
1217
+ # Returns true/false depending on whether this agent is logged in.
1218
+ def logged_in?
1219
+ status == 'LOGGEDIN'
1220
+ end
1221
+
1222
+ private
1223
+
1224
+ def status
1225
+ agent_metadata 'status'
1226
+ end
1227
+
1228
+ def agent_metadata(data_name)
1229
+ data_name = data_name.to_s.downcase
1230
+ raise ArgumentError, "unrecognized agent metadata name #{data_name}" unless SUPPORTED_METADATA_NAMES.include? data_name
1231
+ proxy.environment.variable "AGENT(#{id}:#{data_name})"
1232
+ end
1233
+
1234
+ end
1235
+
1236
+ class QueueDoesNotExistError < Exception
1237
+ def initialize(queue_name)
1238
+ super "Queue #{queue_name} does not exist!"
1239
+ end
1240
+ end
1241
+
1242
+ end
1243
+
1244
+ module MenuDigitResponse
1245
+ def timed_out?
1246
+ eql? 0.chr
1247
+ end
1248
+ end
1249
+
1250
+ module SpeechEngines
1251
+
1252
+ class InvalidSpeechEngine < Exception; end
1253
+
1254
+ class << self
1255
+ def cepstral(text)
1256
+ puts "in ceptral"
1257
+ puts escape(text)
1258
+ end
1259
+
1260
+ def festival(text)
1261
+ raise NotImplementedError
1262
+ end
1263
+
1264
+ def none(text)
1265
+ raise InvalidSpeechEngine, "No speech engine selected. You must specify one in your Adhearsion config file."
1266
+ end
1267
+
1268
+ def method_missing(engine_name, text)
1269
+ raise InvalidSpeechEngine, "Unsupported speech engine #{engine_name} for speaking '#{text}'"
1270
+ end
1271
+
1272
+ private
1273
+
1274
+ def escape(text)
1275
+ "%p" % text
1276
+ end
1277
+
1278
+ end
1279
+ end
1280
+
1281
+ end
1282
+ end
1283
+ end
1284
+ end