Class: Adhearsion::VoIP::Asterisk::Manager::ManagerInterface

Inherits:
Object
  • Object
show all
Defined in:
lib/adhearsion/voip/asterisk/manager_interface.rb

Overview

This class abstracts a connection to the Asterisk Manager Interface. Its purpose is, first and foremost, to make the protocol consistent. Though the classes employed to assist this class (ManagerInterfaceAction, ManagerInterfaceResponse, ManagerInterfaceError, etc.) are relatively user-friendly, they’re designed to be a building block on which to build higher-level abstractions of the Asterisk Manager Interface.

For a higher-level abstraction of the Asterisk Manager Interface, see the SuperManager class.

Defined Under Namespace

Classes: AuthenticationFailedException, ManagerInterfaceAction, NotConnectedError, UnsupportedActionName

Constant Summary collapse

CAUSAL_EVENT_NAMES =
%w[queuestatus sippeers iaxpeers parkedcalls
dahdishowchannels coreshowchannels dbget
status konferencelist]
RETRY_SLEEP =
5
DEFAULT_SETTINGS =
{
  :host           => "localhost",
  :port           => 5038,
  :username       => "admin",
  :password       => "secret",
  :events         => true,
  :auto_reconnect => true,
  :event_callback => proc { |event| Events.trigger(%w[asterisk manager_interface], event) }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ ManagerInterface

Creates a new Asterisk Manager Interface connection and exposes certain methods to control it. The constructor takes named parameters as Symbols. Note: if the :events option is given, this library will establish a separate socket for just events. Two sockets are used because some actions actually respond with events, making it very complicated to differentiate between response-type events and normal events.

Parameters:

  • options (Hash) (defaults to: {})

    Available options are :host, :port, :username, :password, and :events



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 110

def initialize(options={})
  options = parse_options options
  @host           = options[:host]
  @username       = options[:username]
  @password       = options[:password]
  @port           = options[:port]
  @events         = options[:events]
  @auto_reconnect = options[:auto_reconnect]
  @event_callback = options[:event_callback]

  @sent_messages = {}
  @sent_messages_lock = Mutex.new

  @actions_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
      :message_received => :action_message_received,
      :error_received   => :action_error_received

  @write_queue = Queue.new

  if @events
    @events_lexer = DelegatingAsteriskManagerInterfaceLexer.new self, \
        :message_received => :event_message_received,
        :error_received   => :event_error_received
  end
end

Class Method Details

.causal_event_terminator_name_for(action_name) ⇒ String

Used to determine the event name for an action which has causal events.

Parameters:

Returns:

  • (String)

    The corresponding event name which signals the completion of the causal event sequence.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 73

def causal_event_terminator_name_for(action_name)
  return nil unless has_causal_events?(action_name)
  action_name = action_name.to_s.downcase
   case action_name
     when "sippeers", "iaxpeers"
     "peerlistcomplete"
   when "dbget"
     "dbgetresponse"
   when "konferencelist"
     "conferencelistcomplete"
   else
       action_name + "complete"
   end
end

.connect(*args) ⇒ Object



40
41
42
43
44
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 40

def connect(*args)
  new(*args).tap do |connection|
    connection.connect!
  end
end

.has_causal_events?(name, headers = {}) ⇒ String

When sending an action with “causal events” (i.e. events which must be collected to form a proper response), AMI should send a particular event which instructs us that no more events will be sent. This event is called the “causal event terminator”.

Note: you must supply both the name of the event and any headers because it’s possible that some uses of an action (i.e. same name, different headers) have causal events while other uses don’t.

Parameters:

  • name (String)

    the name of the event

  • the (Hash)

    headers associated with this event

Returns:

  • (String)

    the downcase()‘d name of the event name for which to wait



63
64
65
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 63

def has_causal_events?(name, headers={})
  CAUSAL_EVENT_NAMES.include? name.to_s.downcase
end

.replies_with_action_id?(name, headers = {}) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
49
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 46

def replies_with_action_id?(name, headers={})
  name = name.to_s.downcase
  !UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
end

Instance Method Details

#action_error_received(ami_error) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 193

def action_error_received(ami_error)
  action_id = ami_error["ActionID"]

  corresponding_action = data_for_message_received_with_action_id action_id

  if corresponding_action
    corresponding_action.future_resource.resource = ami_error
  else
    ahn_log.ami.error "Received an AMI error with an unrecognized ActionID!! This may be an bug! #{ami_error.inspect}"
  end
end

#action_message_received(message) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 136

def action_message_received(message)
  if message.kind_of? Manager::ManagerInterfaceEvent
    # Trigger the return value of the waiting action id...
    corresponding_action   = @current_action_with_causal_events
    event_collection       = @event_collection_for_current_action

    if corresponding_action

      # The "DBGet" command is causal, meaning it has an separate
      # event that contains the data for command's response.  However,
      # unlike other causal commands, AMI does not send a
      # "DBGetComplete" action indicating the causal event is
      # finished.  This is fixed starting in Asterisk 1.8.
      if message.name.downcase == "dbgetresponse"
        event_collection << message
      end

      # If this is the meta-event which signals no more events will follow and the response is complete.
      if message.name.downcase == corresponding_action.causal_event_terminator_name
        # Wake up the waiting Thread
        corresponding_action.future_resource.resource = event_collection.freeze

        # Clear the stored action and event collection
        @current_action_with_causal_events   = nil
        @event_collection_for_current_action = nil
      else
        event_collection << message
        # We have more causal events coming.
      end
    else
      ahn_log.ami.error "Got an unexpected event on actions socket! This AMI command may have a multi-message response. Try making Adhearsion treat it as CAUSAL_EVENT #{message.inspect}"
    end

  elsif message["ActionID"].nil?
    # No ActionID! Release the write lock and wake up the waiter
  else
    action_id = message["ActionID"]
    corresponding_action = data_for_message_received_with_action_id action_id
    if corresponding_action
      message.action = corresponding_action

      if corresponding_action.has_causal_events?
        # By this point the write loop will already have started blocking by calling the response() method on the
        # action. Because we must collect more events before we wake the write loop up again, let's create these
        # instance variable which will needed when the subsequent causal events come in.
        @current_action_with_causal_events   = corresponding_action
        @event_collection_for_current_action = []
      else
        # Wake any Threads waiting on the response.
        corresponding_action.future_resource.resource = message
      end
    else
      ahn_log.ami.error "Received an AMI message with an unrecognized ActionID!! This may be an bug! #{message.inspect}"
    end
  end
end

#actions_connection_disconnectedObject



245
246
247
248
249
250
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 245

def actions_connection_disconnected
  @actions_state = :disconnected
  ahn_log.ami.error "AMI connection for ACTION disconnected !!!"
  clear_actions_connection
  establish_actions_connection if @auto_reconnect
end

#actions_connection_establishedObject



240
241
242
243
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 240

def actions_connection_established
  @actions_state = :connected
  start_actions_writer_loop
end

#call_and_exec(channel, app, opts = {}) ⇒ Object

call_and_exec allows you to make a call to a channel and then execute an Astersik application on that call



400
401
402
403
404
405
406
407
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 400

def call_and_exec(channel, app, opts={})
  #deprecation_warning
  args = { :channel => channel, :application => app }
  args[:caller_id] = opts[:caller_id] if opts[:caller_id]
  args[:data] = opts[:args] if opts[:args]
  args[:variables] = opts[:variables] if opts[:variables]
  originate args
end

#call_into_context(channel, context, options = {}) ⇒ Object

call_into_context is syntactic sugar for the Asterisk originate command that allows you to launch a call into a particular context. For example:

call_into_context(‘SIP/[email protected]’, ‘my_context’, { :variables => { :session_guid => new_guid }})



413
414
415
416
417
418
419
420
421
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 413

def call_into_context(channel, context, options={})
  #deprecation_warning
  args = {:channel => channel, :context => context}
  args[:priority] = options[:priority] || 1
  args[:exten] = options[:extension] if options[:extension]
  args[:caller_id] = options[:caller_id] if options[:caller_id]
  args[:variables] = options[:variables] if options[:variables]
  originate args
end

#clear_actions_connectionObject



263
264
265
266
267
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 263

def clear_actions_connection
  stop_actions_writer_loop
  clear_actions_connection_resources
  disconnect_actions_connection if @actions_state.equal? :connected
end

#clear_events_connectionObject



269
270
271
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 269

def clear_events_connection
  disconnect_events_connection if @events_state.equal? :connected
end

#connect!Object

Must be called after instantiation. Also see ManagerInterface::connect().

Raises:



234
235
236
237
238
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 234

def connect!
  establish_actions_connection
  establish_events_connection if @events
  self
end

#deprecation_warningObject



332
333
334
335
336
337
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 332

def deprecation_warning
  ahn_log.ami.deprecation.warn "The implementation of the ping, originate, introduce, hangup, call_into_context " +
      "and call_and_exec methods will soon be moved from this class to SuperManager. At the moment, the " +
      "SuperManager abstractions are not completed. Don't worry. The migration to SuperManager will be very easy."+
      " See http://docs.adhearsion.com/AMI for more information."
end

#disconnect!Object



273
274
275
276
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 273

def disconnect!
  clear_actions_connection
  clear_events_connection
end

#dynamicObject



278
279
280
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 278

def dynamic
  # TODO: Return an object which responds to method_missing
end

#event_error_received(message) ⇒ Object



214
215
216
217
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 214

def event_error_received(message)
  # Does this ever even occur?
  ahn_log.ami.error "Hmmm, got an error on the AMI events-only socket! This must be a bug! #{message.inspect}"
end

#event_message_received(event) ⇒ Object

Called only when this ManagerInterface is instantiated with events enabled.



208
209
210
211
212
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 208

def event_message_received(event)
  return if event.kind_of?(ManagerInterfaceResponse) && event["Message"] == "Authentication accepted"
  # TODO: convert the event name to a certain namespace.
  @event_callback.call(event)
end

#events_connection_disconnectedObject



256
257
258
259
260
261
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 256

def events_connection_disconnected
  @events_state = :disconnected
  ahn_log.ami.error "AMI connection for EVENT disconnected !!!"
  clear_events_connection
  establish_events_connection if @auto_reconnect
end

#events_connection_establishedObject



252
253
254
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 252

def events_connection_established
  @events_state = :connected
end

#hangup(channel) ⇒ Object

hangup terminates a call accepts a channel as the argument full details here: www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Hangup



393
394
395
396
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 393

def hangup(channel)
  #deprecation_warning
  send_action "Hangup", :channel => channel
end

#introduce(caller, callee, opts = {}) ⇒ Object

An introduction connects two endpoints together. The first argument is the first person the PBX will call. When she’s picked up, Asterisk will play ringing while the second person is being dialed.

The first argument is the person called first. Pass this as a canonical IAX2/server/user type argument. Destination takes the same format, but comma-separated Dial() arguments can be optionally passed after the technology.

TODO: Provide an example when this works.



384
385
386
387
388
389
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 384

def introduce(caller, callee, opts={})
  #deprecation_warning
  dial_args  = callee
  dial_args += "|#{opts[:options]}" if opts[:options]
  call_and_exec caller, "Dial", :args => dial_args, :caller_id => opts[:caller_id]
end

#originate(options = {}) ⇒ Object

The originate method launches a call to Asterisk, full details here: www.voip-info.org/tiki-index.php?page=Asterisk+Manager+API+Action+Originate Takes these arguments as a hash:

Channel: Channel on which to originate the call (The same as you specify in the Dial application command)
Context: Context to use on connect (must use Exten & Priority with it)
Exten: Extension to use on connect (must use Context & Priority with it)
Priority: Priority to use on connect (must use Context & Exten with it)
Timeout: Timeout (in milliseconds) for the originating connection to happen(defaults to 30000 milliseconds)
CallerID: CallerID to use for the call
Variable: Channels variables to set (max 32). Variables will be set for both channels (local and connected).
Account: Account code for the call
Application: Application to use on connect (use Data for parameters)
Data : Data if Application parameter is used
Async: For the origination to be asynchronous (allows multiple calls to be generated without waiting for a response)
ActionID: The request identifier. It allows you to identify the response to this request.
You may use a number or a string. Useful when you make several simultaneous requests.

For example: originate { :channel => ‘SIP/[email protected]’,

:context  => 'my_context',
:exten    => 's',
:priority => '1' }


362
363
364
365
366
367
368
369
370
371
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 362

def originate(options={})
  #deprecation_warning
  options = options.clone
  options[:callerid] = options.delete :caller_id if options.has_key? :caller_id
  options[:exten] = options.delete :extension if options.has_key? :extension
  if options[:variables] && options[:variables].kind_of?(Hash)
    options[:variable] = options[:variables].map {|pair| pair.join('=')}.join(@coreSettings["ArgumentDelimiter"])
  end
  send_action "Originate", options
end

#pingObject

ping sends an action to the Asterisk Manager Interface that returns a pong more details here: www.voip-info.org/wiki/index.php?page=Asterisk+Manager+API+Action+Ping



326
327
328
329
330
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 326

def ping
  #deprecation_warning
  send_action "Ping"
  true
end

#send_action_asynchronously(action_name, headers = {}) ⇒ FutureResource

Used to directly send a new action to Asterisk. Note: NEVER supply an ActionID; these are handled internally.

Parameters:

  • action_name (String, Symbol)

    The name of the action (e.g. Originate)

  • headers (Hash) (defaults to: {})

    Other key/value pairs to send in this action. Note: don’t provide an ActionID

Returns:

  • (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.



289
290
291
292
293
294
295
296
297
298
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 289

def send_action_asynchronously(action_name, headers={})
  check_action_name action_name
  action = ManagerInterfaceAction.new(action_name, headers)
  if action.replies_with_action_id?
    @write_queue << action
    action
  else
    raise NotImplementedError
  end
end

#send_action_synchronously(*args) ⇒ ManagerInterfaceResponse, ImmediateResponse Also known as: send_action

Sends an action over the AMI connection and blocks your Thread until the response comes in. If there was an error for some reason, the error will be raised as an ManagerInterfaceError.

Parameters:

  • action_name (String, Symbol)

    The name of the action (e.g. Originate)

  • headers (Hash)

    Other key/value pairs to send in this action. Note: don’t provide an ActionID

Returns:

Raises:

  • (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.



309
310
311
312
313
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 309

def send_action_synchronously(*args)
  send_action_asynchronously(*args).response.tap do |response|
    raise response if response.kind_of?(ManagerInterfaceError)
  end
end

#syntax_error_encountered(ignored_chunk) ⇒ Object

Called when our Ragel parser encounters some unexpected syntax from Asterisk. Anytime this is called, it should be considered a bug in Adhearsion. Note: this same method is called regardless of whether the syntax error happened on the actions socket or on the events socket.



224
225
226
227
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 224

def syntax_error_encountered(ignored_chunk)
  ahn_log.ami.error "ADHEARSION'S AMI PARSER ENCOUNTERED A SYNTAX ERROR! " +
      "PLEASE REPORT THIS ON http://bugs.adhearsion.com! OFFENDING TEXT:\n#{ignored_chunk.inspect}"
end