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

DEFAULT_SETTINGS =
{
  :host     => "localhost",
  :port     => 5038,
  :username => "admin",
  :password => "secret",
  :events   => true
}.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
# 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]
  
  @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.



79
80
81
82
83
84
85
86
87
88
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 79

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 "queuestatus", 'parkedcalls', "status"
       action_name + "complete"
     when "sippeers"
       "peerlistcomplete"
   end
end

.connect(*args) ⇒ Object



34
35
36
37
38
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 34

def connect(*args)
  returning new(*args) 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
66
67
68
69
70
71
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 63

def has_causal_events?(name, headers={})
  name = name.to_s.downcase
  case name
    when "queuestatus", "sippeers", "parkedcalls", "status"
      true
    else
      false
  end
end

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

Returns:

  • (Boolean)


40
41
42
43
44
45
46
47
48
49
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 40

def replies_with_action_id?(name, headers={})
  name = name.to_s.downcase
  # TODO: Expand this case statement
  case name
    when "queues", "iaxpeers"
      false
    else
      true
  end                
end

Instance Method Details

#action_error_received(ami_error) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 184

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



135
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
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 135

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
      
      # 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
        
        # Result found! Wake up any Threads waiting
        corresponding_action.future_resource.resource = event_collection.freeze
        
        @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 may be a bug! #{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



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

def actions_connection_disconnected
  @actions_state = :disconnected
end

#actions_connection_establishedObject



231
232
233
234
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 231

def actions_connection_established
  @actions_state = :connected
  @actions_writer_thread = Thread.new(&method(:write_loop))
end

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



343
344
345
346
347
348
349
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 343

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]
  originate args
end

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



351
352
353
354
355
356
357
358
359
360
361
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 351

def call_into_context(channel, context, options={})
  deprecation_warning
  args = {:channel => channel, :context => context}
  args[:priority] = options[:priority] || 1
  args[:extension] = options[:extension] if options[:extension]
  args[:caller_id] = options[:caller_id] if options[:caller_id]
  if options[:variables] && options[:variables].kind_of?(Hash)
    args[:variable] = options[:variables].map {|pair| pair.join('=')}.join('|')
  end
  originate args
end

#connect!Object

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

Raises:



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

def connect!
  establish_actions_connection
  establish_events_connection if @events
  self
end

#deprecation_warningObject



306
307
308
309
310
311
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 306

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

Raises:

  • (NotImplementedError)


248
249
250
251
252
253
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 248

def disconnect!
  # PSEUDO CODE
  # TODO: Go through all the waiting condition variables and raise an exception
  #@write_queue << :STOP!
  raise NotImplementedError
end

#dynamicObject



255
256
257
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 255

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

#event_error_received(message) ⇒ Object



205
206
207
208
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 205

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.



199
200
201
202
203
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 199

def event_message_received(event)
  return if event.kind_of?(ManagerInterfaceResponse) && event["Message"] == "Authentication accepted"
  # TODO: convert the event name to a certain namespace.
  Events.trigger %w[asterisk manager_interface], event
end

#events_connection_establishedObject



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

def events_connection_established
  @events_state = :connected
end

#hangup(channel) ⇒ Object



338
339
340
341
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 338

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.



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

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



313
314
315
316
317
318
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 313

def originate(options={})
  deprecation_warning
  options = options.clone
  options[:callerid] = options.delete :caller_id if options.has_key? :caller_id
  send_action "Originate", options
end

#pingObject

#######

###########

SOON-DEPRECATED COMMANDS #################

###########
        #######


301
302
303
304
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 301

def ping
  deprecation_warning
  send_action "Ping"
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.



266
267
268
269
270
271
272
273
274
275
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 266

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.



286
287
288
289
290
# File 'lib/adhearsion/voip/asterisk/manager_interface.rb', line 286

def send_action_synchronously(*args)
  returning send_action_asynchronously(*args).response 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.



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

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