jicksta-adhearsion 0.7.999
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +6 -0
- data/EVENTS +11 -0
- data/LICENSE +456 -0
- data/README.txt +5 -0
- data/Rakefile +120 -0
- data/adhearsion.gemspec +146 -0
- data/app_generators/ahn/USAGE +5 -0
- data/app_generators/ahn/ahn_generator.rb +87 -0
- data/app_generators/ahn/templates/.ahnrc +34 -0
- data/app_generators/ahn/templates/README +8 -0
- data/app_generators/ahn/templates/Rakefile +23 -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/stomp_gateway/README.markdown +47 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/config.yml +12 -0
- data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
- data/app_generators/ahn/templates/components/restful_rpc/README.markdown +11 -0
- data/app_generators/ahn/templates/components/restful_rpc/config.yml +34 -0
- data/app_generators/ahn/templates/components/restful_rpc/example-client.rb +48 -0
- data/app_generators/ahn/templates/components/restful_rpc/restful_rpc.rb +87 -0
- data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
- data/app_generators/ahn/templates/config/startup.rb +53 -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/spec_framework.rb +24 -0
- data/lib/adhearsion/component_manager.rb +208 -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 +49 -0
- data/lib/adhearsion/initializer/drb.rb +31 -0
- data/lib/adhearsion/initializer/freeswitch.rb +22 -0
- data/lib/adhearsion/initializer/rails.rb +40 -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 +81 -0
- data/lib/adhearsion/voip/asterisk/commands.rb +1284 -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 +1754 -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 +562 -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 +440 -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 +177 -0
@@ -0,0 +1,562 @@
|
|
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
|
+
def ping
|
302
|
+
deprecation_warning
|
303
|
+
send_action "Ping"
|
304
|
+
end
|
305
|
+
|
306
|
+
def deprecation_warning
|
307
|
+
ahn_log.ami.deprecation.warn "The implementation of the ping, originate, introduce, hangup, call_into_context " +
|
308
|
+
"and call_and_exec methods will soon be moved from this class to SuperManager. At the moment, the " +
|
309
|
+
"SuperManager abstractions are not completed. Don't worry. The migration to SuperManager will be very easy."+
|
310
|
+
" See http://docs.adhearsion.com/AMI for more information."
|
311
|
+
end
|
312
|
+
|
313
|
+
def originate(options={})
|
314
|
+
deprecation_warning
|
315
|
+
options = options.clone
|
316
|
+
options[:callerid] = options.delete :caller_id if options.has_key? :caller_id
|
317
|
+
send_action "Originate", options
|
318
|
+
end
|
319
|
+
|
320
|
+
# An introduction connects two endpoints together. The first argument is
|
321
|
+
# the first person the PBX will call. When she's picked up, Asterisk will
|
322
|
+
# play ringing while the second person is being dialed.
|
323
|
+
#
|
324
|
+
# The first argument is the person called first. Pass this as a canonical
|
325
|
+
# IAX2/server/user type argument. Destination takes the same format, but
|
326
|
+
# comma-separated Dial() arguments can be optionally passed after the
|
327
|
+
# technology.
|
328
|
+
#
|
329
|
+
# TODO: Provide an example when this works.
|
330
|
+
#
|
331
|
+
def introduce(caller, callee, opts={})
|
332
|
+
deprecation_warning
|
333
|
+
dial_args = callee
|
334
|
+
dial_args += "|#{opts[:options]}" if opts[:options]
|
335
|
+
call_and_exec caller, "Dial", :args => dial_args, :caller_id => opts[:caller_id]
|
336
|
+
end
|
337
|
+
|
338
|
+
def hangup(channel)
|
339
|
+
deprecation_warning
|
340
|
+
send_action "Hangup", :channel => channel
|
341
|
+
end
|
342
|
+
|
343
|
+
def call_and_exec(channel, app, opts={})
|
344
|
+
deprecation_warning
|
345
|
+
args = { :channel => channel, :application => app }
|
346
|
+
args[:caller_id] = opts[:caller_id] if opts[:caller_id]
|
347
|
+
args[:data] = opts[:args] if opts[:args]
|
348
|
+
originate args
|
349
|
+
end
|
350
|
+
|
351
|
+
def call_into_context(channel, context, options={})
|
352
|
+
deprecation_warning
|
353
|
+
args = {:channel => channel, :context => context}
|
354
|
+
args[:priority] = options[:priority] || 1
|
355
|
+
args[:extension] = options[:extension] if options[:extension]
|
356
|
+
args[:caller_id] = options[:caller_id] if options[:caller_id]
|
357
|
+
if options[:variables] && options[:variables].kind_of?(Hash)
|
358
|
+
args[:variable] = options[:variables].map {|pair| pair.join('=')}.join('|')
|
359
|
+
end
|
360
|
+
originate args
|
361
|
+
end
|
362
|
+
|
363
|
+
####### #######
|
364
|
+
########### ###########
|
365
|
+
################# END SOON-DEPRECATED COMMANDS #################
|
366
|
+
########### ###########
|
367
|
+
####### #######
|
368
|
+
|
369
|
+
|
370
|
+
protected
|
371
|
+
|
372
|
+
##
|
373
|
+
# This class will be removed once this AMI library fully supports all known protocol anomalies.
|
374
|
+
#
|
375
|
+
class UnsupportedActionName < ArgumentError
|
376
|
+
UNSUPPORTED_ACTION_NAMES = %w[
|
377
|
+
queues
|
378
|
+
iaxpeers
|
379
|
+
] unless defined? UNSUPPORTED_ACTION_NAMES
|
380
|
+
def initialize(name)
|
381
|
+
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."
|
382
|
+
end
|
383
|
+
|
384
|
+
end
|
385
|
+
|
386
|
+
def check_action_name(name)
|
387
|
+
name = name.to_s.downcase
|
388
|
+
raise UnsupportedActionName.new(name) if UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
|
389
|
+
true
|
390
|
+
end
|
391
|
+
|
392
|
+
def write_loop
|
393
|
+
loop do
|
394
|
+
next_action = @write_queue.shift
|
395
|
+
return :stopped if next_action.equal? :STOP!
|
396
|
+
register_action_with_metadata next_action
|
397
|
+
|
398
|
+
ahn_log.ami.debug "Sending AMI action: #{"\n>>> " + next_action.to_s.gsub(/(\r\n)+/, "\n>>> ")}"
|
399
|
+
@actions_connection.send_data next_action.to_s
|
400
|
+
# If it's "causal event" action, we must wait here until it's fully responded
|
401
|
+
next_action.response if next_action.has_causal_events?
|
402
|
+
end
|
403
|
+
rescue => e
|
404
|
+
p e
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# When we send out an AMI action, we need to track the ActionID and have the other Thread handling the socket IO
|
409
|
+
# notify the sending Thread that a response has been received. This method instantiates a new FutureResource and
|
410
|
+
# keeps it around in a synchronized Hash for the IO-handling Thread to notify when a response with a matching
|
411
|
+
# ActionID is seen again. See also data_for_message_received_with_action_id() which is how the IO-handling Thread
|
412
|
+
# gets the metadata registered in the method back later.
|
413
|
+
#
|
414
|
+
# @param [ManagerInterfaceAction] action The ManagerInterfaceAction to send
|
415
|
+
# @param [Hash] headers The other key/value pairs being sent with this message
|
416
|
+
#
|
417
|
+
def register_action_with_metadata(action)
|
418
|
+
raise ArgumentError, "Must supply an action!" if action.nil?
|
419
|
+
@sent_messages_lock.synchronize do
|
420
|
+
@sent_messages[action.action_id] = action
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def data_for_message_received_with_action_id(action_id)
|
425
|
+
@sent_messages_lock.synchronize do
|
426
|
+
@sent_messages.delete action_id
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
##
|
431
|
+
# Instantiates a new ManagerInterfaceActionsConnection and assigns it to @actions_connection.
|
432
|
+
#
|
433
|
+
# @return [EventSocket]
|
434
|
+
#
|
435
|
+
def establish_actions_connection
|
436
|
+
@actions_connection = EventSocket.connect(@host, @port) do |handler|
|
437
|
+
handler.receive_data { |data| @actions_lexer << data }
|
438
|
+
handler.connected { actions_connection_established }
|
439
|
+
handler.disconnected { actions_connection_disconnected }
|
440
|
+
end
|
441
|
+
login_actions
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
# Instantiates a new ManagerInterfaceEventsConnection and assigns it to @events_connection.
|
446
|
+
#
|
447
|
+
# @return [EventSocket]
|
448
|
+
#
|
449
|
+
def establish_events_connection
|
450
|
+
|
451
|
+
# Note: the @events_connection instance variable is set in login()
|
452
|
+
@events_connection = EventSocket.connect(@host, @port) do |handler|
|
453
|
+
handler.receive_data { |data| @events_lexer << data }
|
454
|
+
handler.connected { events_connection_established }
|
455
|
+
handler.disconnected { events_connection_disconnected }
|
456
|
+
end
|
457
|
+
login_events
|
458
|
+
ahn_log.ami "Successful AMI events-only connection into #{@username}@#{@host}"
|
459
|
+
end
|
460
|
+
|
461
|
+
def login_actions
|
462
|
+
action = send_action_asynchronously "Login", "Username" => @username, "Secret" => @password, "Events" => "Off"
|
463
|
+
response = action.response
|
464
|
+
if response.kind_of? ManagerInterfaceError
|
465
|
+
raise AuthenticationFailedException, "Incorrect username and password! #{response.message}"
|
466
|
+
else
|
467
|
+
ahn_log.ami "Successful AMI actions-only connection into #{@username}@#{@host}"
|
468
|
+
response
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
##
|
473
|
+
# Since this method is always called after the login_actions method, an AuthenticationFailedException would have already
|
474
|
+
# been raised if the username/password were off. Because this is the only action we ever need to send on this socket,
|
475
|
+
# it goes straight to the EventSocket connection (bypassing the @write_queue).
|
476
|
+
#
|
477
|
+
def login_events
|
478
|
+
login_action = ManagerInterfaceAction.new "Login", "Username" => @username, "Secret" => @password, "Events" => "On"
|
479
|
+
@events_connection.send_data login_action.to_s
|
480
|
+
end
|
481
|
+
|
482
|
+
def parse_options(options)
|
483
|
+
unrecognized_keys = options.keys.map { |key| key.to_sym } - DEFAULT_SETTINGS.keys
|
484
|
+
if unrecognized_keys.any?
|
485
|
+
raise ArgumentError, "Unrecognized named argument(s): #{unrecognized_keys.to_sentence}"
|
486
|
+
end
|
487
|
+
DEFAULT_SETTINGS.merge options
|
488
|
+
end
|
489
|
+
|
490
|
+
##
|
491
|
+
# Raised when calling ManagerInterface#connect!() and the server responds with an error after logging in.
|
492
|
+
#
|
493
|
+
class AuthenticationFailedException < Exception; end
|
494
|
+
|
495
|
+
class NotConnectedError < Exception; end
|
496
|
+
|
497
|
+
##
|
498
|
+
# Each time ManagerInterface#send_action is invoked, a new ManagerInterfaceAction is instantiated.
|
499
|
+
#
|
500
|
+
class ManagerInterfaceAction
|
501
|
+
|
502
|
+
attr_reader :name, :headers, :future_resource, :action_id, :causal_event_terminator_name
|
503
|
+
def initialize(name, headers={})
|
504
|
+
@name = name.to_s.downcase.freeze
|
505
|
+
@headers = headers.stringify_keys.freeze
|
506
|
+
@action_id = new_action_id.freeze
|
507
|
+
@future_resource = FutureResource.new
|
508
|
+
@causal_event_terminator_name = ManagerInterface.causal_event_terminator_name_for name
|
509
|
+
end
|
510
|
+
|
511
|
+
##
|
512
|
+
# Used internally by ManagerInterface for the actions in AMI which break the protocol's definition and do not
|
513
|
+
# reply with an ActionID.
|
514
|
+
#
|
515
|
+
def replies_with_action_id?
|
516
|
+
ManagerInterface.replies_with_action_id?(@name, @headers)
|
517
|
+
end
|
518
|
+
|
519
|
+
##
|
520
|
+
# Some AMI actions effectively respond with many events which collectively constitute the actual response. These
|
521
|
+
# Must be handled specially by the protocol parser, so this method helps inform the parser.
|
522
|
+
#
|
523
|
+
def has_causal_events?
|
524
|
+
ManagerInterface.has_causal_events?(@name, @headers)
|
525
|
+
end
|
526
|
+
|
527
|
+
##
|
528
|
+
# Abstracts the generation of new ActionIDs. This could be implemented virutally any way, provided each
|
529
|
+
# invocation returns something unique, so this will generate a GUID and return it.
|
530
|
+
#
|
531
|
+
# @return [String] characters in GUID format (e.g. "4C5F4E1C-A0F1-4D13-8751-C62F2F783062")
|
532
|
+
#
|
533
|
+
def new_action_id
|
534
|
+
new_guid # Implemented in lib/adhearsion/foundation/pseudo_guid.rb
|
535
|
+
end
|
536
|
+
|
537
|
+
##
|
538
|
+
# Converts this action into a protocol-valid String, ready to be sent over a socket.
|
539
|
+
#
|
540
|
+
def to_s
|
541
|
+
@textual_representation ||= (
|
542
|
+
"Action: #{@name}\r\nActionID: #{@action_id}\r\n" +
|
543
|
+
@headers.map { |(key,value)| "#{key}: #{value}" }.join("\r\n") +
|
544
|
+
(@headers.any? ? "\r\n\r\n" : "\r\n")
|
545
|
+
)
|
546
|
+
end
|
547
|
+
|
548
|
+
##
|
549
|
+
# If the response has simply not been received yet from Asterisk, the calling Thread will block until it comes
|
550
|
+
# in. Once the response comes in, subsequent calls immediately return a reference to the ManagerInterfaceResponse
|
551
|
+
# object.
|
552
|
+
#
|
553
|
+
def response
|
554
|
+
future_resource.resource
|
555
|
+
end
|
556
|
+
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
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
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Adhearsion
|
2
|
+
module VoIP
|
3
|
+
module Asterisk
|
4
|
+
module Manager
|
5
|
+
|
6
|
+
##
|
7
|
+
# Higher level abstraction of the Asterisk Manager Interface.
|
8
|
+
#
|
9
|
+
class SuperManager
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|