eric-adhearsion 0.7.999 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +8 -2
- data/EVENTS +11 -0
- data/Rakefile +96 -24
- data/adhearsion.gemspec +148 -0
- data/app_generators/ahn/ahn_generator.rb +24 -9
- data/app_generators/ahn/templates/.ahnrc +25 -3
- data/app_generators/ahn/templates/Rakefile +22 -2
- 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/simon_game/{lib/simon_game.rb → simon_game.rb} +14 -19
- data/app_generators/ahn/templates/config/startup.rb +3 -6
- data/app_generators/ahn/templates/dialplan.rb +2 -3
- data/app_generators/ahn/templates/events.rb +32 -0
- data/bin/jahn +10 -0
- data/examples/asterisk_manager_interface/standalone.rb +51 -0
- data/lib/adhearsion.rb +17 -11
- data/lib/adhearsion/cli.rb +141 -24
- data/lib/adhearsion/component_manager.rb +169 -238
- data/lib/adhearsion/component_manager/component_tester.rb +55 -0
- data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
- data/lib/adhearsion/events_support.rb +84 -0
- data/lib/adhearsion/{core_extensions → foundation}/all.rb +0 -0
- data/lib/adhearsion/{blank_slate.rb → foundation/blank_slate.rb} +0 -0
- data/lib/adhearsion/{core_extensions → foundation}/custom_daemonizer.rb +0 -0
- data/lib/adhearsion/foundation/event_socket.rb +203 -0
- data/lib/adhearsion/foundation/future_resource.rb +36 -0
- data/lib/adhearsion/{core_extensions → foundation}/global.rb +0 -0
- data/lib/adhearsion/{core_extensions → foundation}/metaprogramming.rb +0 -0
- data/lib/adhearsion/foundation/numeric.rb +13 -0
- data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
- data/lib/adhearsion/{core_extensions → foundation}/relationship_properties.rb +2 -0
- data/lib/adhearsion/foundation/string.rb +26 -0
- data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
- data/lib/adhearsion/{core_extensions → foundation}/thread_safety.rb +0 -0
- data/lib/adhearsion/host_definitions.rb +5 -1
- data/lib/adhearsion/initializer.rb +229 -73
- data/lib/adhearsion/initializer/asterisk.rb +33 -11
- data/lib/adhearsion/initializer/configuration.rb +58 -6
- data/lib/adhearsion/initializer/database.rb +3 -46
- data/lib/adhearsion/initializer/drb.rb +9 -3
- data/lib/adhearsion/initializer/freeswitch.rb +3 -3
- data/lib/adhearsion/initializer/rails.rb +1 -1
- data/lib/adhearsion/tasks.rb +2 -1
- data/lib/adhearsion/tasks/deprecations.rb +59 -0
- data/lib/adhearsion/version.rb +3 -3
- data/lib/adhearsion/voip/asterisk.rb +2 -2
- data/lib/adhearsion/voip/asterisk/agi_server.rb +9 -6
- data/lib/adhearsion/voip/asterisk/commands.rb +106 -4
- data/lib/adhearsion/voip/asterisk/manager_interface.rb +562 -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/super_manager.rb +19 -0
- data/lib/adhearsion/voip/call.rb +51 -2
- data/lib/adhearsion/voip/dial_plan.rb +74 -61
- data/lib/adhearsion/voip/dsl/dialing_dsl.rb +1 -1
- data/lib/adhearsion/voip/dsl/dialplan/parser.rb +2 -6
- data/lib/adhearsion/voip/dsl/numerical_string.rb +2 -2
- data/lib/adhearsion/voip/freeswitch/oes_server.rb +2 -2
- data/lib/theatre.rb +151 -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
- metadata +63 -138
- data/Manifest.txt +0 -149
- data/README.txt +0 -6
- data/ahn_generators/component/USAGE +0 -5
- data/ahn_generators/component/component_generator.rb +0 -57
- data/ahn_generators/component/templates/configuration.rb +0 -0
- data/ahn_generators/component/templates/lib/lib.rb.erb +0 -3
- data/ahn_generators/component/templates/test/test.rb.erb +0 -12
- data/ahn_generators/component/templates/test/test_helper.rb +0 -14
- data/app_generators/ahn/templates/components/simon_game/configuration.rb +0 -0
- data/app_generators/ahn/templates/components/simon_game/test/test_helper.rb +0 -14
- data/app_generators/ahn/templates/components/simon_game/test/test_simon_game.rb +0 -31
- data/lib/adhearsion/core_extensions/array.rb +0 -0
- data/lib/adhearsion/core_extensions/guid.rb +0 -5
- data/lib/adhearsion/core_extensions/hash.rb +0 -0
- data/lib/adhearsion/core_extensions/numeric.rb +0 -4
- data/lib/adhearsion/core_extensions/proc.rb +0 -0
- data/lib/adhearsion/core_extensions/pseudo_uuid.rb +0 -11
- data/lib/adhearsion/core_extensions/publishable.rb +0 -73
- data/lib/adhearsion/core_extensions/string.rb +0 -26
- data/lib/adhearsion/core_extensions/thread.rb +0 -13
- data/lib/adhearsion/core_extensions/time.rb +0 -0
- data/lib/adhearsion/distributed/gateways/dbus_gateway.rb +0 -0
- data/lib/adhearsion/distributed/gateways/osa_gateway.rb +0 -0
- data/lib/adhearsion/distributed/gateways/rest_gateway.rb +0 -9
- data/lib/adhearsion/distributed/gateways/soap_gateway.rb +0 -9
- data/lib/adhearsion/distributed/gateways/xmlrpc_gateway.rb +0 -9
- data/lib/adhearsion/distributed/peer_finder.rb +0 -0
- data/lib/adhearsion/distributed/remote_cli.rb +0 -0
- data/lib/adhearsion/hooks.rb +0 -57
- data/lib/adhearsion/initializer/paths.rb +0 -55
- data/lib/adhearsion/services/scheduler.rb +0 -5
- data/lib/adhearsion/voip/asterisk/ami.rb +0 -147
- data/lib/adhearsion/voip/asterisk/ami/actions.rb +0 -238
- data/lib/adhearsion/voip/asterisk/ami/machine.rb +0 -871
- data/lib/adhearsion/voip/asterisk/ami/machine.rl +0 -109
- data/lib/adhearsion/voip/asterisk/ami/parser.rb +0 -262
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/spec/fixtures/dialplan.rb +0 -3
- data/spec/initializer/test_configuration.rb +0 -267
- data/spec/initializer/test_loading.rb +0 -162
- data/spec/initializer/test_paths.rb +0 -43
- data/spec/silence.rb +0 -10
- data/spec/test_ahn_command.rb +0 -149
- data/spec/test_code_quality.rb +0 -87
- data/spec/test_component_manager.rb +0 -97
- data/spec/test_constants.rb +0 -8
- data/spec/test_drb.rb +0 -104
- data/spec/test_helper.rb +0 -94
- data/spec/test_hooks.rb +0 -37
- data/spec/test_host_definitions.rb +0 -79
- data/spec/test_initialization.rb +0 -105
- data/spec/test_logging.rb +0 -80
- data/spec/test_relationship_properties.rb +0 -54
- data/spec/voip/asterisk/ami_response_definitions.rb +0 -23
- data/spec/voip/asterisk/config_file_generators/test_agents.rb +0 -253
- data/spec/voip/asterisk/config_file_generators/test_queues.rb +0 -325
- data/spec/voip/asterisk/config_file_generators/test_voicemail.rb +0 -306
- data/spec/voip/asterisk/menu_command/test_calculated_match.rb +0 -111
- data/spec/voip/asterisk/menu_command/test_matchers.rb +0 -98
- data/spec/voip/asterisk/mock_ami_server.rb +0 -176
- data/spec/voip/asterisk/test_agi_server.rb +0 -451
- data/spec/voip/asterisk/test_ami.rb +0 -227
- data/spec/voip/asterisk/test_commands.rb +0 -2006
- data/spec/voip/asterisk/test_config_manager.rb +0 -129
- data/spec/voip/dsl/dispatcher_spec_helper.rb +0 -45
- data/spec/voip/dsl/test_dialing_dsl.rb +0 -268
- data/spec/voip/dsl/test_dispatcher.rb +0 -82
- data/spec/voip/dsl/test_parser.rb +0 -87
- data/spec/voip/freeswitch/test_basic_connection_manager.rb +0 -39
- data/spec/voip/freeswitch/test_inbound_connection_manager.rb +0 -39
- data/spec/voip/freeswitch/test_oes_server.rb +0 -9
- data/spec/voip/test_call_routing.rb +0 -127
- data/spec/voip/test_dialplan_manager.rb +0 -372
- data/spec/voip/test_numerical_string.rb +0 -48
- data/spec/voip/test_phone_number.rb +0 -36
- data/test/test_ahn_generator.rb +0 -59
- data/test/test_component_generator.rb +0 -52
- data/test/test_generator_helper.rb +0 -20
@@ -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
|