kenwiesner-adhearsioncw 0.8.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +26 -0
- data/EVENTS +11 -0
- data/LICENSE +456 -0
- data/Rakefile +127 -0
- data/adhearsion.gemspec +149 -0
- data/app_generators/ahn/USAGE +5 -0
- data/app_generators/ahn/ahn_generator.rb +91 -0
- data/app_generators/ahn/templates/.ahnrc +34 -0
- data/app_generators/ahn/templates/README +8 -0
- data/app_generators/ahn/templates/Rakefile +25 -0
- data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
- data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/README.markdown +11 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/example-client.rb +48 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb +87 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.yml +34 -0
- data/app_generators/ahn/templates/components/disabled/restful_rpc/spec/restful_rpc_spec.rb +263 -0
- data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +104 -0
- data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.yml +2 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.yml +12 -0
- data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
- data/app_generators/ahn/templates/config/startup.rb +50 -0
- data/app_generators/ahn/templates/dialplan.rb +3 -0
- data/app_generators/ahn/templates/events.rb +32 -0
- data/bin/ahn +28 -0
- data/bin/ahnctl +68 -0
- data/bin/jahn +42 -0
- data/examples/asterisk_manager_interface/standalone.rb +51 -0
- data/lib/adhearsion/cli.rb +223 -0
- data/lib/adhearsion/component_manager/component_tester.rb +55 -0
- data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
- data/lib/adhearsion/component_manager.rb +207 -0
- data/lib/adhearsion/events_support.rb +84 -0
- data/lib/adhearsion/foundation/all.rb +9 -0
- data/lib/adhearsion/foundation/blank_slate.rb +5 -0
- data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
- data/lib/adhearsion/foundation/event_socket.rb +203 -0
- data/lib/adhearsion/foundation/future_resource.rb +36 -0
- data/lib/adhearsion/foundation/global.rb +1 -0
- data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
- data/lib/adhearsion/foundation/numeric.rb +13 -0
- data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
- data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
- data/lib/adhearsion/foundation/string.rb +26 -0
- data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
- data/lib/adhearsion/foundation/thread_safety.rb +7 -0
- data/lib/adhearsion/host_definitions.rb +67 -0
- data/lib/adhearsion/initializer/asterisk.rb +81 -0
- data/lib/adhearsion/initializer/configuration.rb +254 -0
- data/lib/adhearsion/initializer/database.rb +50 -0
- data/lib/adhearsion/initializer/drb.rb +31 -0
- data/lib/adhearsion/initializer/freeswitch.rb +22 -0
- data/lib/adhearsion/initializer/rails.rb +41 -0
- data/lib/adhearsion/initializer.rb +373 -0
- data/lib/adhearsion/logging.rb +92 -0
- data/lib/adhearsion/tasks/database.rb +5 -0
- data/lib/adhearsion/tasks/deprecations.rb +59 -0
- data/lib/adhearsion/tasks/generating.rb +20 -0
- data/lib/adhearsion/tasks/lint.rb +4 -0
- data/lib/adhearsion/tasks/testing.rb +37 -0
- data/lib/adhearsion/tasks.rb +16 -0
- data/lib/adhearsion/version.rb +9 -0
- data/lib/adhearsion/voip/asterisk/agi_server.rb +84 -0
- data/lib/adhearsion/voip/asterisk/commands.rb +1314 -0
- data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
- data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
- data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
- data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
- data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1589 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
- data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +597 -0
- data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
- data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
- data/lib/adhearsion/voip/asterisk.rb +4 -0
- data/lib/adhearsion/voip/call.rb +453 -0
- data/lib/adhearsion/voip/call_routing.rb +64 -0
- data/lib/adhearsion/voip/commands.rb +9 -0
- data/lib/adhearsion/voip/constants.rb +39 -0
- data/lib/adhearsion/voip/conveniences.rb +18 -0
- data/lib/adhearsion/voip/dial_plan.rb +218 -0
- data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
- data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
- data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
- data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
- data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
- data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
- data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
- data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
- data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
- data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
- data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
- data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
- data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
- data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
- data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
- data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
- data/lib/adhearsion.rb +37 -0
- data/lib/theatre/README.markdown +64 -0
- data/lib/theatre/callback_definition_loader.rb +84 -0
- data/lib/theatre/guid.rb +23 -0
- data/lib/theatre/invocation.rb +121 -0
- data/lib/theatre/namespace_manager.rb +153 -0
- data/lib/theatre/version.rb +2 -0
- data/lib/theatre.rb +151 -0
- metadata +182 -0
@@ -0,0 +1,597 @@
|
|
1
|
+
require 'adhearsion/voip/asterisk/manager_interface/ami_lexer'
|
2
|
+
|
3
|
+
module Adhearsion
|
4
|
+
module VoIP
|
5
|
+
module Asterisk
|
6
|
+
|
7
|
+
##
|
8
|
+
# Sorry, this AMI class has been deprecated. Please see http://docs.adhearsion.com/Asterisk_Manager_Interface for
|
9
|
+
# documentation on the new way of handling AMI. This new version is much better and should not require an enormous
|
10
|
+
# migration on your part.
|
11
|
+
#
|
12
|
+
class AMI
|
13
|
+
def initialize
|
14
|
+
raise "Sorry, this AMI class has been deprecated. Please see http://docs.adhearsion.com/Asterisk_Manager_Interface for documentation on the new way of handling AMI. This new version is much better and should not require an enormous migration on your part."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
mattr_accessor :manager_interface
|
19
|
+
|
20
|
+
module Manager
|
21
|
+
|
22
|
+
##
|
23
|
+
# This class abstracts a connection to the Asterisk Manager Interface. Its purpose is, first and foremost, to make
|
24
|
+
# the protocol consistent. Though the classes employed to assist this class (ManagerInterfaceAction,
|
25
|
+
# ManagerInterfaceResponse, ManagerInterfaceError, etc.) are relatively user-friendly, they're designed to be a
|
26
|
+
# building block on which to build higher-level abstractions of the Asterisk Manager Interface.
|
27
|
+
#
|
28
|
+
# For a higher-level abstraction of the Asterisk Manager Interface, see the SuperManager class.
|
29
|
+
#
|
30
|
+
class ManagerInterface
|
31
|
+
|
32
|
+
class << self
|
33
|
+
|
34
|
+
def connect(*args)
|
35
|
+
returning new(*args) do |connection|
|
36
|
+
connection.connect!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def replies_with_action_id?(name, headers={})
|
41
|
+
name = name.to_s.downcase
|
42
|
+
# TODO: Expand this case statement
|
43
|
+
case name
|
44
|
+
when "queues", "iaxpeers"
|
45
|
+
false
|
46
|
+
else
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# When sending an action with "causal events" (i.e. events which must be collected to form a proper
|
53
|
+
# response), AMI should send a particular event which instructs us that no more events will be sent.
|
54
|
+
# This event is called the "causal event terminator".
|
55
|
+
#
|
56
|
+
# Note: you must supply both the name of the event and any headers because it's possible that some uses of an
|
57
|
+
# action (i.e. same name, different headers) have causal events while other uses don't.
|
58
|
+
#
|
59
|
+
# @param [String] name the name of the event
|
60
|
+
# @param [Hash] the headers associated with this event
|
61
|
+
# @return [String] the downcase()'d name of the event name for which to wait
|
62
|
+
#
|
63
|
+
def has_causal_events?(name, headers={})
|
64
|
+
name = name.to_s.downcase
|
65
|
+
case name
|
66
|
+
when "queuestatus", "sippeers", "parkedcalls", "status"
|
67
|
+
true
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Used to determine the event name for an action which has causal events.
|
75
|
+
#
|
76
|
+
# @param [String] action_name
|
77
|
+
# @return [String] The corresponding event name which signals the completion of the causal event sequence.
|
78
|
+
#
|
79
|
+
def causal_event_terminator_name_for(action_name)
|
80
|
+
return nil unless has_causal_events?(action_name)
|
81
|
+
action_name = action_name.to_s.downcase
|
82
|
+
case action_name
|
83
|
+
when "queuestatus", 'parkedcalls', "status"
|
84
|
+
action_name + "complete"
|
85
|
+
when "sippeers"
|
86
|
+
"peerlistcomplete"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
DEFAULT_SETTINGS = {
|
93
|
+
:host => "localhost",
|
94
|
+
:port => 5038,
|
95
|
+
:username => "admin",
|
96
|
+
:password => "secret",
|
97
|
+
:events => true
|
98
|
+
}.freeze unless defined? DEFAULT_SETTINGS
|
99
|
+
|
100
|
+
attr_reader *DEFAULT_SETTINGS.keys
|
101
|
+
|
102
|
+
##
|
103
|
+
# Creates a new Asterisk Manager Interface connection and exposes certain methods to control it. The constructor
|
104
|
+
# takes named parameters as Symbols. Note: if the :events option is given, this library will establish a separate
|
105
|
+
# socket for just events. Two sockets are used because some actions actually respond with events, making it very
|
106
|
+
# complicated to differentiate between response-type events and normal events.
|
107
|
+
#
|
108
|
+
# @param [Hash] options Available options are :host, :port, :username, :password, and :events
|
109
|
+
#
|
110
|
+
def initialize(options={})
|
111
|
+
options = parse_options options
|
112
|
+
|
113
|
+
@host = options[:host]
|
114
|
+
@username = options[:username]
|
115
|
+
@password = options[:password]
|
116
|
+
@port = options[:port]
|
117
|
+
@events = options[:events]
|
118
|
+
|
119
|
+
@sent_messages = {}
|
120
|
+
@sent_messages_lock = Mutex.new
|
121
|
+
|
122
|
+
@actions_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
|
123
|
+
:message_received => :action_message_received,
|
124
|
+
:error_received => :action_error_received
|
125
|
+
|
126
|
+
@write_queue = Queue.new
|
127
|
+
|
128
|
+
if @events
|
129
|
+
@events_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
|
130
|
+
:message_received => :event_message_received,
|
131
|
+
:error_received => :event_error_received
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def action_message_received(message)
|
136
|
+
if message.kind_of? Manager::ManagerInterfaceEvent
|
137
|
+
# Trigger the return value of the waiting action id...
|
138
|
+
corresponding_action = @current_action_with_causal_events
|
139
|
+
event_collection = @event_collection_for_current_action
|
140
|
+
|
141
|
+
if corresponding_action
|
142
|
+
|
143
|
+
# If this is the meta-event which signals no more events will follow and the response is complete.
|
144
|
+
if message.name.downcase == corresponding_action.causal_event_terminator_name
|
145
|
+
|
146
|
+
# Result found! Wake up any Threads waiting
|
147
|
+
corresponding_action.future_resource.resource = event_collection.freeze
|
148
|
+
|
149
|
+
@current_action_with_causal_events = nil
|
150
|
+
@event_collection_for_current_action = nil
|
151
|
+
|
152
|
+
else
|
153
|
+
event_collection << message
|
154
|
+
# We have more causal events coming.
|
155
|
+
end
|
156
|
+
else
|
157
|
+
ahn_log.ami.error "Got an unexpected event on actions socket! This may be a bug! #{message.inspect}"
|
158
|
+
end
|
159
|
+
|
160
|
+
elsif message["ActionID"].nil?
|
161
|
+
# No ActionID! Release the write lock and wake up the waiter
|
162
|
+
else
|
163
|
+
action_id = message["ActionID"]
|
164
|
+
corresponding_action = data_for_message_received_with_action_id action_id
|
165
|
+
if corresponding_action
|
166
|
+
message.action = corresponding_action
|
167
|
+
|
168
|
+
if corresponding_action.has_causal_events?
|
169
|
+
# By this point the write loop will already have started blocking by calling the response() method on the
|
170
|
+
# action. Because we must collect more events before we wake the write loop up again, let's create these
|
171
|
+
# instance variable which will needed when the subsequent causal events come in.
|
172
|
+
@current_action_with_causal_events = corresponding_action
|
173
|
+
@event_collection_for_current_action = []
|
174
|
+
else
|
175
|
+
# Wake any Threads waiting on the response.
|
176
|
+
corresponding_action.future_resource.resource = message
|
177
|
+
end
|
178
|
+
else
|
179
|
+
ahn_log.ami.error "Received an AMI message with an unrecognized ActionID!! This may be an bug! #{message.inspect}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def action_error_received(ami_error)
|
185
|
+
action_id = ami_error["ActionID"]
|
186
|
+
|
187
|
+
corresponding_action = data_for_message_received_with_action_id action_id
|
188
|
+
|
189
|
+
if corresponding_action
|
190
|
+
corresponding_action.future_resource.resource = ami_error
|
191
|
+
else
|
192
|
+
ahn_log.ami.error "Received an AMI error with an unrecognized ActionID!! This may be an bug! #{ami_error.inspect}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# Called only when this ManagerInterface is instantiated with events enabled.
|
198
|
+
#
|
199
|
+
def event_message_received(event)
|
200
|
+
return if event.kind_of?(ManagerInterfaceResponse) && event["Message"] == "Authentication accepted"
|
201
|
+
# TODO: convert the event name to a certain namespace.
|
202
|
+
Events.trigger %w[asterisk manager_interface], event
|
203
|
+
end
|
204
|
+
|
205
|
+
def event_error_received(message)
|
206
|
+
# Does this ever even occur?
|
207
|
+
ahn_log.ami.error "Hmmm, got an error on the AMI events-only socket! This must be a bug! #{message.inspect}"
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Called when our Ragel parser encounters some unexpected syntax from Asterisk. Anytime this is called, it should
|
212
|
+
# be considered a bug in Adhearsion. Note: this same method is called regardless of whether the syntax error
|
213
|
+
# happened on the actions socket or on the events socket.
|
214
|
+
#
|
215
|
+
def syntax_error_encountered(ignored_chunk)
|
216
|
+
ahn_log.ami.error "ADHEARSION'S AMI PARSER ENCOUNTERED A SYNTAX ERROR! " +
|
217
|
+
"PLEASE REPORT THIS ON http://bugs.adhearsion.com! OFFENDING TEXT:\n#{ignored_chunk.inspect}"
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Must be called after instantiation. Also see ManagerInterface::connect().
|
222
|
+
#
|
223
|
+
# @raise [AuthenticationFailedException] if username or password are rejected
|
224
|
+
#
|
225
|
+
def connect!
|
226
|
+
establish_actions_connection
|
227
|
+
establish_events_connection if @events
|
228
|
+
self
|
229
|
+
end
|
230
|
+
|
231
|
+
def actions_connection_established
|
232
|
+
@actions_state = :connected
|
233
|
+
@actions_writer_thread = Thread.new(&method(:write_loop))
|
234
|
+
end
|
235
|
+
|
236
|
+
def actions_connection_disconnected
|
237
|
+
@actions_state = :disconnected
|
238
|
+
end
|
239
|
+
|
240
|
+
def events_connection_established
|
241
|
+
@events_state = :connected
|
242
|
+
end
|
243
|
+
|
244
|
+
def actions_connection_disconnected
|
245
|
+
@events_state = :disconnected
|
246
|
+
end
|
247
|
+
|
248
|
+
def disconnect!
|
249
|
+
# PSEUDO CODE
|
250
|
+
# TODO: Go through all the waiting condition variables and raise an exception
|
251
|
+
#@write_queue << :STOP!
|
252
|
+
raise NotImplementedError
|
253
|
+
end
|
254
|
+
|
255
|
+
def dynamic
|
256
|
+
# TODO: Return an object which responds to method_missing
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Used to directly send a new action to Asterisk. Note: NEVER supply an ActionID; these are handled internally.
|
261
|
+
#
|
262
|
+
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
|
263
|
+
# @param [Hash] headers Other key/value pairs to send in this action. Note: don't provide an ActionID
|
264
|
+
# @return [FutureResource] Call resource() on this object if you wish to access the response (optional). Note: if the response has not come in yet, your Thread will wait until it does.
|
265
|
+
#
|
266
|
+
def send_action_asynchronously(action_name, headers={})
|
267
|
+
check_action_name action_name
|
268
|
+
action = ManagerInterfaceAction.new(action_name, headers)
|
269
|
+
if action.replies_with_action_id?
|
270
|
+
@write_queue << action
|
271
|
+
action
|
272
|
+
else
|
273
|
+
raise NotImplementedError
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
##
|
278
|
+
# Sends an action over the AMI connection and blocks your Thread until the response comes in. If there was an error
|
279
|
+
# for some reason, the error will be raised as an ManagerInterfaceError.
|
280
|
+
#
|
281
|
+
# @param [String, Symbol] action_name The name of the action (e.g. Originate)
|
282
|
+
# @param [Hash] headers Other key/value pairs to send in this action. Note: don't provide an ActionID
|
283
|
+
# @raise [ManagerInterfaceError] When Asterisk can't execute this action, it sends back an Error which is converted into an ManagerInterfaceError object and raised. Access ManagerInterfaceError#message for the reported message from Asterisk.
|
284
|
+
# @return [ManagerInterfaceResponse, ImmediateResponse] Contains the response from Asterisk and all headers
|
285
|
+
#
|
286
|
+
def send_action_synchronously(*args)
|
287
|
+
returning send_action_asynchronously(*args).response do |response|
|
288
|
+
raise response if response.kind_of?(ManagerInterfaceError)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
alias send_action send_action_synchronously
|
293
|
+
|
294
|
+
|
295
|
+
####### #######
|
296
|
+
########### ###########
|
297
|
+
################# SOON-DEPRECATED COMMANDS #################
|
298
|
+
########### ###########
|
299
|
+
####### #######
|
300
|
+
|
301
|
+
# ping sends an action to the Asterisk Manager Interface that returns a pong
|
302
|
+
# more details here: http://www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Ping
|
303
|
+
def ping
|
304
|
+
deprecation_warning
|
305
|
+
send_action "Ping"
|
306
|
+
true
|
307
|
+
end
|
308
|
+
|
309
|
+
def deprecation_warning
|
310
|
+
ahn_log.ami.deprecation.warn "The implementation of the ping, originate, introduce, hangup, call_into_context " +
|
311
|
+
"and call_and_exec methods will soon be moved from this class to SuperManager. At the moment, the " +
|
312
|
+
"SuperManager abstractions are not completed. Don't worry. The migration to SuperManager will be very easy."+
|
313
|
+
" See http://docs.adhearsion.com/AMI for more information."
|
314
|
+
end
|
315
|
+
|
316
|
+
# The originate method launches a call to Asterisk, full details here:
|
317
|
+
# http://www.voip-info.org/tiki-index.php?page=Asterisk+Manager+API+Action+Originate
|
318
|
+
# Takes these arguments as a hash:
|
319
|
+
#
|
320
|
+
# Channel: Channel on which to originate the call (The same as you specify in the Dial application command)
|
321
|
+
# Context: Context to use on connect (must use Exten & Priority with it)
|
322
|
+
# Exten: Extension to use on connect (must use Context & Priority with it)
|
323
|
+
# Priority: Priority to use on connect (must use Context & Exten with it)
|
324
|
+
# Timeout: Timeout (in milliseconds) for the originating connection to happen(defaults to 30000 milliseconds)
|
325
|
+
# CallerID: CallerID to use for the call
|
326
|
+
# Variable: Channels variables to set (max 32). Variables will be set for both channels (local and connected).
|
327
|
+
# Account: Account code for the call
|
328
|
+
# Application: Application to use on connect (use Data for parameters)
|
329
|
+
# Data : Data if Application parameter is used
|
330
|
+
# Async: For the origination to be asynchronous (allows multiple calls to be generated without waiting for a response)
|
331
|
+
# ActionID: The request identifier. It allows you to identify the response to this request.
|
332
|
+
# You may use a number or a string. Useful when you make several simultaneous requests.
|
333
|
+
#
|
334
|
+
# For example:
|
335
|
+
# originate { :channel => 'SIP/1000@sipnetworks.com',
|
336
|
+
# :context => 'my_context',
|
337
|
+
# :exten => 's',
|
338
|
+
# :priority => '1' }
|
339
|
+
def originate(options={})
|
340
|
+
deprecation_warning
|
341
|
+
options = options.clone
|
342
|
+
options[:callerid] = options.delete :caller_id if options.has_key? :caller_id
|
343
|
+
options[:exten] = options.delete :extension if options.has_key? :extension
|
344
|
+
send_action "Originate", options
|
345
|
+
end
|
346
|
+
|
347
|
+
# An introduction connects two endpoints together. The first argument is
|
348
|
+
# the first person the PBX will call. When she's picked up, Asterisk will
|
349
|
+
# play ringing while the second person is being dialed.
|
350
|
+
#
|
351
|
+
# The first argument is the person called first. Pass this as a canonical
|
352
|
+
# IAX2/server/user type argument. Destination takes the same format, but
|
353
|
+
# comma-separated Dial() arguments can be optionally passed after the
|
354
|
+
# technology.
|
355
|
+
#
|
356
|
+
# TODO: Provide an example when this works.
|
357
|
+
#
|
358
|
+
def introduce(caller, callee, opts={})
|
359
|
+
deprecation_warning
|
360
|
+
dial_args = callee
|
361
|
+
dial_args += "|#{opts[:options]}" if opts[:options]
|
362
|
+
call_and_exec caller, "Dial", :args => dial_args, :caller_id => opts[:caller_id]
|
363
|
+
end
|
364
|
+
|
365
|
+
# hangup terminates a call accepts a channel as the argument
|
366
|
+
# full details here: http://www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Hangup
|
367
|
+
def hangup(channel)
|
368
|
+
deprecation_warning
|
369
|
+
send_action "Hangup", :channel => channel
|
370
|
+
end
|
371
|
+
|
372
|
+
# call_and_exec allows you to make a call to a channel and then execute an Astersik application
|
373
|
+
# on that call
|
374
|
+
def call_and_exec(channel, app, opts={})
|
375
|
+
deprecation_warning
|
376
|
+
args = { :channel => channel, :application => app }
|
377
|
+
args[:caller_id] = opts[:caller_id] if opts[:caller_id]
|
378
|
+
args[:data] = opts[:args] if opts[:args]
|
379
|
+
originate args
|
380
|
+
end
|
381
|
+
|
382
|
+
# call_into_context is syntactic sugar for the Asterisk originate command that allows you to
|
383
|
+
# lanuch a call into a particular context. For example:
|
384
|
+
#
|
385
|
+
# call_into_context('SIP/1000@sipnetworks.com', 'my_context', { :variables => { :session_guid => new_guid }})
|
386
|
+
def call_into_context(channel, context, options={})
|
387
|
+
deprecation_warning
|
388
|
+
args = {:channel => channel, :context => context}
|
389
|
+
args[:priority] = options[:priority] || 1
|
390
|
+
args[:exten] = options[:extension] if options[:extension]
|
391
|
+
args[:caller_id] = options[:caller_id] if options[:caller_id]
|
392
|
+
if options[:variables] && options[:variables].kind_of?(Hash)
|
393
|
+
args[:variable] = options[:variables].map {|pair| pair.join('=')}.join('|')
|
394
|
+
end
|
395
|
+
originate args
|
396
|
+
end
|
397
|
+
|
398
|
+
####### #######
|
399
|
+
########### ###########
|
400
|
+
################# END SOON-DEPRECATED COMMANDS #################
|
401
|
+
########### ###########
|
402
|
+
####### #######
|
403
|
+
|
404
|
+
|
405
|
+
protected
|
406
|
+
|
407
|
+
##
|
408
|
+
# This class will be removed once this AMI library fully supports all known protocol anomalies.
|
409
|
+
#
|
410
|
+
class UnsupportedActionName < ArgumentError
|
411
|
+
UNSUPPORTED_ACTION_NAMES = %w[
|
412
|
+
queues
|
413
|
+
iaxpeers
|
414
|
+
] unless defined? UNSUPPORTED_ACTION_NAMES
|
415
|
+
def initialize(name)
|
416
|
+
super "At the moment this AMI library doesn't support the #{name.inspect} action because it causes a protocol anomaly. Support for it will be coming shortly."
|
417
|
+
end
|
418
|
+
|
419
|
+
end
|
420
|
+
|
421
|
+
def check_action_name(name)
|
422
|
+
name = name.to_s.downcase
|
423
|
+
raise UnsupportedActionName.new(name) if UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
|
424
|
+
true
|
425
|
+
end
|
426
|
+
|
427
|
+
def write_loop
|
428
|
+
loop do
|
429
|
+
next_action = @write_queue.shift
|
430
|
+
return :stopped if next_action.equal? :STOP!
|
431
|
+
register_action_with_metadata next_action
|
432
|
+
|
433
|
+
ahn_log.ami.debug "Sending AMI action: #{"\n>>> " + next_action.to_s.gsub(/(\r\n)+/, "\n>>> ")}"
|
434
|
+
@actions_connection.send_data next_action.to_s
|
435
|
+
# If it's "causal event" action, we must wait here until it's fully responded
|
436
|
+
next_action.response if next_action.has_causal_events?
|
437
|
+
end
|
438
|
+
rescue => e
|
439
|
+
p e
|
440
|
+
end
|
441
|
+
|
442
|
+
##
|
443
|
+
# When we send out an AMI action, we need to track the ActionID and have the other Thread handling the socket IO
|
444
|
+
# notify the sending Thread that a response has been received. This method instantiates a new FutureResource and
|
445
|
+
# keeps it around in a synchronized Hash for the IO-handling Thread to notify when a response with a matching
|
446
|
+
# ActionID is seen again. See also data_for_message_received_with_action_id() which is how the IO-handling Thread
|
447
|
+
# gets the metadata registered in the method back later.
|
448
|
+
#
|
449
|
+
# @param [ManagerInterfaceAction] action The ManagerInterfaceAction to send
|
450
|
+
# @param [Hash] headers The other key/value pairs being sent with this message
|
451
|
+
#
|
452
|
+
def register_action_with_metadata(action)
|
453
|
+
raise ArgumentError, "Must supply an action!" if action.nil?
|
454
|
+
@sent_messages_lock.synchronize do
|
455
|
+
@sent_messages[action.action_id] = action
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def data_for_message_received_with_action_id(action_id)
|
460
|
+
@sent_messages_lock.synchronize do
|
461
|
+
@sent_messages.delete action_id
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
##
|
466
|
+
# Instantiates a new ManagerInterfaceActionsConnection and assigns it to @actions_connection.
|
467
|
+
#
|
468
|
+
# @return [EventSocket]
|
469
|
+
#
|
470
|
+
def establish_actions_connection
|
471
|
+
@actions_connection = EventSocket.connect(@host, @port) do |handler|
|
472
|
+
handler.receive_data { |data| @actions_lexer << data }
|
473
|
+
handler.connected { actions_connection_established }
|
474
|
+
handler.disconnected { actions_connection_disconnected }
|
475
|
+
end
|
476
|
+
login_actions
|
477
|
+
end
|
478
|
+
|
479
|
+
##
|
480
|
+
# Instantiates a new ManagerInterfaceEventsConnection and assigns it to @events_connection.
|
481
|
+
#
|
482
|
+
# @return [EventSocket]
|
483
|
+
#
|
484
|
+
def establish_events_connection
|
485
|
+
|
486
|
+
# Note: the @events_connection instance variable is set in login()
|
487
|
+
@events_connection = EventSocket.connect(@host, @port) do |handler|
|
488
|
+
handler.receive_data { |data| @events_lexer << data }
|
489
|
+
handler.connected { events_connection_established }
|
490
|
+
handler.disconnected { events_connection_disconnected }
|
491
|
+
end
|
492
|
+
login_events
|
493
|
+
ahn_log.ami "Successful AMI events-only connection into #{@username}@#{@host}"
|
494
|
+
end
|
495
|
+
|
496
|
+
def login_actions
|
497
|
+
action = send_action_asynchronously "Login", "Username" => @username, "Secret" => @password, "Events" => "Off"
|
498
|
+
response = action.response
|
499
|
+
if response.kind_of? ManagerInterfaceError
|
500
|
+
raise AuthenticationFailedException, "Incorrect username and password! #{response.message}"
|
501
|
+
else
|
502
|
+
ahn_log.ami "Successful AMI actions-only connection into #{@username}@#{@host}"
|
503
|
+
response
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
##
|
508
|
+
# Since this method is always called after the login_actions method, an AuthenticationFailedException would have already
|
509
|
+
# been raised if the username/password were off. Because this is the only action we ever need to send on this socket,
|
510
|
+
# it goes straight to the EventSocket connection (bypassing the @write_queue).
|
511
|
+
#
|
512
|
+
def login_events
|
513
|
+
login_action = ManagerInterfaceAction.new "Login", "Username" => @username, "Secret" => @password, "Events" => "On"
|
514
|
+
@events_connection.send_data login_action.to_s
|
515
|
+
end
|
516
|
+
|
517
|
+
def parse_options(options)
|
518
|
+
unrecognized_keys = options.keys.map { |key| key.to_sym } - DEFAULT_SETTINGS.keys
|
519
|
+
if unrecognized_keys.any?
|
520
|
+
raise ArgumentError, "Unrecognized named argument(s): #{unrecognized_keys.to_sentence}"
|
521
|
+
end
|
522
|
+
DEFAULT_SETTINGS.merge options
|
523
|
+
end
|
524
|
+
|
525
|
+
##
|
526
|
+
# Raised when calling ManagerInterface#connect!() and the server responds with an error after logging in.
|
527
|
+
#
|
528
|
+
class AuthenticationFailedException < Exception; end
|
529
|
+
|
530
|
+
class NotConnectedError < Exception; end
|
531
|
+
|
532
|
+
##
|
533
|
+
# Each time ManagerInterface#send_action is invoked, a new ManagerInterfaceAction is instantiated.
|
534
|
+
#
|
535
|
+
class ManagerInterfaceAction
|
536
|
+
|
537
|
+
attr_reader :name, :headers, :future_resource, :action_id, :causal_event_terminator_name
|
538
|
+
def initialize(name, headers={})
|
539
|
+
@name = name.to_s.downcase.freeze
|
540
|
+
@headers = headers.stringify_keys.freeze
|
541
|
+
@action_id = new_action_id.freeze
|
542
|
+
@future_resource = FutureResource.new
|
543
|
+
@causal_event_terminator_name = ManagerInterface.causal_event_terminator_name_for name
|
544
|
+
end
|
545
|
+
|
546
|
+
##
|
547
|
+
# Used internally by ManagerInterface for the actions in AMI which break the protocol's definition and do not
|
548
|
+
# reply with an ActionID.
|
549
|
+
#
|
550
|
+
def replies_with_action_id?
|
551
|
+
ManagerInterface.replies_with_action_id?(@name, @headers)
|
552
|
+
end
|
553
|
+
|
554
|
+
##
|
555
|
+
# Some AMI actions effectively respond with many events which collectively constitute the actual response. These
|
556
|
+
# Must be handled specially by the protocol parser, so this method helps inform the parser.
|
557
|
+
#
|
558
|
+
def has_causal_events?
|
559
|
+
ManagerInterface.has_causal_events?(@name, @headers)
|
560
|
+
end
|
561
|
+
|
562
|
+
##
|
563
|
+
# Abstracts the generation of new ActionIDs. This could be implemented virutally any way, provided each
|
564
|
+
# invocation returns something unique, so this will generate a GUID and return it.
|
565
|
+
#
|
566
|
+
# @return [String] characters in GUID format (e.g. "4C5F4E1C-A0F1-4D13-8751-C62F2F783062")
|
567
|
+
#
|
568
|
+
def new_action_id
|
569
|
+
new_guid # Implemented in lib/adhearsion/foundation/pseudo_guid.rb
|
570
|
+
end
|
571
|
+
|
572
|
+
##
|
573
|
+
# Converts this action into a protocol-valid String, ready to be sent over a socket.
|
574
|
+
#
|
575
|
+
def to_s
|
576
|
+
@textual_representation ||= (
|
577
|
+
"Action: #{@name}\r\nActionID: #{@action_id}\r\n" +
|
578
|
+
@headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") +
|
579
|
+
(@headers.any? ? "\r\n\r\n" : "\r\n")
|
580
|
+
)
|
581
|
+
end
|
582
|
+
|
583
|
+
##
|
584
|
+
# If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes
|
585
|
+
# in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse
|
586
|
+
# object.
|
587
|
+
#
|
588
|
+
def response
|
589
|
+
future_resource.resource
|
590
|
+
end
|
591
|
+
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Adhearsion
|
2
|
+
class DialPlan
|
3
|
+
class ConfirmationManager
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def encode_hash_for_dial_macro_argument(options)
|
8
|
+
options = options.clone
|
9
|
+
macro_name = options.delete :macro
|
10
|
+
options[:play] &&= options[:play].kind_of?(Array) ? options[:play].join('++') : options[:play]
|
11
|
+
encoded_options = URI.escape options.map { |key,value| "#{key}:#{value}" }.join('!')
|
12
|
+
returning "M(#{macro_name}^#{encoded_options})" do |str|
|
13
|
+
if str.rindex('^') != str.index('^')
|
14
|
+
raise ArgumentError, "You seem to have supplied a :confirm option with a caret (^) in it!" +
|
15
|
+
" Please remove it. This will blow Asterisk up."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle(call)
|
21
|
+
new(call).handle
|
22
|
+
end
|
23
|
+
|
24
|
+
def confirmation_call?(call)
|
25
|
+
call.variables.has_key?(:network_script) && call.variables[:network_script].starts_with?('confirm!')
|
26
|
+
end
|
27
|
+
|
28
|
+
def decode_hash(encoded_hash)
|
29
|
+
encoded_hash = encoded_hash =~ /^M\((.+)\)$/ ? $1 : encoded_hash
|
30
|
+
encoded_hash = encoded_hash =~ /^([^:]+\^)?(.+)$/ ? $2 : encoded_hash # Remove the macro name if it's there
|
31
|
+
unencoded = URI.unescape(encoded_hash).split('!')
|
32
|
+
unencoded.shift unless unencoded.first.include?(':')
|
33
|
+
unencoded = unencoded.map { |pair| key, value = pair.split(':'); [key.to_sym ,value] }.flatten
|
34
|
+
returning Hash[*unencoded] do |hash|
|
35
|
+
hash[:timeout] &&= hash[:timeout].to_i
|
36
|
+
hash[:play] &&= hash[:play].split('++')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :call
|
43
|
+
def initialize(call)
|
44
|
+
@call = call
|
45
|
+
extend Adhearsion::VoIP::Commands.for(call.originating_voip_platform)
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle
|
49
|
+
variables = self.class.decode_hash call.variables[:network_script]
|
50
|
+
|
51
|
+
answer
|
52
|
+
loop do
|
53
|
+
response = interruptable_play(*variables[:play])
|
54
|
+
if response && response.to_s == variables[:key].to_s
|
55
|
+
# Don't set a variable to pass through to dial()
|
56
|
+
break
|
57
|
+
elsif response && response.to_s != variables[:key].to_s
|
58
|
+
next
|
59
|
+
else
|
60
|
+
response = wait_for_digit variables[:timeout]
|
61
|
+
if response
|
62
|
+
if response.to_s == variables[:key].to_s
|
63
|
+
# Don't set a variable to pass through to dial()
|
64
|
+
break
|
65
|
+
else
|
66
|
+
next
|
67
|
+
end
|
68
|
+
else
|
69
|
+
# By setting MACRO_RESULT to CONTINUE, we cancel the dial.
|
70
|
+
variable 'MACRO_RESULT' => "CONTINUE"
|
71
|
+
break
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|