adhearsion 0.8.4 → 0.8.5

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.
Files changed (36) hide show
  1. data/CHANGELOG +12 -0
  2. data/Rakefile +6 -5
  3. data/adhearsion.gemspec +9 -5
  4. data/app_generators/ahn/ahn_generator.rb +5 -0
  5. data/app_generators/ahn/templates/components/disabled/restful_rpc/restful_rpc.rb +1 -1
  6. data/app_generators/ahn/templates/components/disabled/sandbox/sandbox.rb +1 -1
  7. data/app_generators/ahn/templates/components/disabled/xmpp_gateway/README.markdown +3 -0
  8. data/app_generators/ahn/templates/components/disabled/xmpp_gateway/xmpp_gateway.rb +11 -0
  9. data/app_generators/ahn/templates/components/disabled/xmpp_gateway/xmpp_gateway.yml +0 -0
  10. data/app_generators/ahn/templates/config/startup.rb +10 -1
  11. data/lib/adhearsion/cli.rb +7 -2
  12. data/lib/adhearsion/foundation/event_socket.rb +1 -1
  13. data/lib/adhearsion/foundation/future_resource.rb +1 -1
  14. data/lib/adhearsion/host_definitions.rb +1 -1
  15. data/lib/adhearsion/initializer.rb +21 -8
  16. data/lib/adhearsion/initializer/configuration.rb +44 -3
  17. data/lib/adhearsion/initializer/database.rb +11 -1
  18. data/lib/adhearsion/initializer/ldap.rb +7 -1
  19. data/lib/adhearsion/initializer/xmpp.rb +42 -0
  20. data/lib/adhearsion/version.rb +26 -2
  21. data/lib/adhearsion/voip/asterisk/agi_server.rb +2 -1
  22. data/lib/adhearsion/voip/asterisk/commands.rb +186 -85
  23. data/lib/adhearsion/voip/asterisk/manager_interface.rb +147 -50
  24. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1 -1
  25. data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +1 -1
  26. data/lib/adhearsion/voip/call.rb +15 -8
  27. data/lib/adhearsion/voip/dial_plan.rb +21 -4
  28. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +1 -1
  29. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +2 -2
  30. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +2 -2
  31. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +2 -2
  32. data/lib/adhearsion/xmpp/connection.rb +61 -0
  33. data/lib/theatre/invocation.rb +1 -1
  34. data/lib/theatre/namespace_manager.rb +1 -1
  35. metadata +12 -6
  36. data/lib/adhearsion/foundation/global.rb +0 -1
@@ -29,7 +29,9 @@ module Adhearsion
29
29
  #
30
30
  class ManagerInterface
31
31
 
32
- CAUSAL_EVENT_NAMES = ["queuestatus", "sippeers", "parkedcalls", "status", "dahdishowchannels"] unless defined? CAUSAL_EVENT_NAMES
32
+ CAUSAL_EVENT_NAMES = %w[queuestatus sippeers iaxpeers parkedcalls
33
+ dahdishowchannels coreshowchannels dbget
34
+ status konferencelist] unless defined? CAUSAL_EVENT_NAMES
33
35
 
34
36
  class << self
35
37
 
@@ -41,13 +43,7 @@ module Adhearsion
41
43
 
42
44
  def replies_with_action_id?(name, headers={})
43
45
  name = name.to_s.downcase
44
- # TODO: Expand this case statement
45
- case name
46
- when "queues", "iaxpeers"
47
- false
48
- else
49
- true
50
- end
46
+ !UnsupportedActionName::UNSUPPORTED_ACTION_NAMES.include? name
51
47
  end
52
48
 
53
49
  ##
@@ -76,21 +72,27 @@ module Adhearsion
76
72
  return nil unless has_causal_events?(action_name)
77
73
  action_name = action_name.to_s.downcase
78
74
  case action_name
79
- when "queuestatus", 'parkedcalls', "status"
75
+ when "sippeers", "iaxpeers"
76
+ "peerlistcomplete"
77
+ when "dbget"
78
+ "dbgetresponse"
79
+ when "konferencelist"
80
+ "conferencelistcomplete"
81
+ else
80
82
  action_name + "complete"
81
- when "sippeers"
82
- "peerlistcomplete"
83
83
  end
84
84
  end
85
85
 
86
86
  end
87
87
 
88
88
  DEFAULT_SETTINGS = {
89
- :host => "localhost",
90
- :port => 5038,
91
- :username => "admin",
92
- :password => "secret",
93
- :events => true
89
+ :host => "localhost",
90
+ :port => 5038,
91
+ :username => "admin",
92
+ :password => "secret",
93
+ :events => true,
94
+ :auto_reconnect => true,
95
+ :event_callback => proc { |event| Events.trigger(%w[asterisk manager_interface], event) }
94
96
  }.freeze unless defined? DEFAULT_SETTINGS
95
97
 
96
98
  attr_reader *DEFAULT_SETTINGS.keys
@@ -106,11 +108,13 @@ module Adhearsion
106
108
  def initialize(options={})
107
109
  options = parse_options options
108
110
 
109
- @host = options[:host]
110
- @username = options[:username]
111
- @password = options[:password]
112
- @port = options[:port]
113
- @events = options[:events]
111
+ @host = options[:host]
112
+ @username = options[:username]
113
+ @password = options[:password]
114
+ @port = options[:port]
115
+ @events = options[:events]
116
+ @auto_reconnect = options[:auto_reconnect]
117
+ @event_callback = options[:event_callback]
114
118
 
115
119
  @sent_messages = {}
116
120
  @sent_messages_lock = Mutex.new
@@ -136,21 +140,29 @@ module Adhearsion
136
140
 
137
141
  if corresponding_action
138
142
 
143
+ # The "DBGet" command is causal, meaning it has an separate
144
+ # event that contains the data for command's response. However,
145
+ # unlike other causal commands, AMI does not send a
146
+ # "DBGetComplete" action indicating the causal event is
147
+ # finished. This is fixed starting in Asterisk 1.8.
148
+ if message.name.downcase == "dbgetresponse"
149
+ event_collection << message
150
+ end
151
+
139
152
  # If this is the meta-event which signals no more events will follow and the response is complete.
140
153
  if message.name.downcase == corresponding_action.causal_event_terminator_name
141
-
142
- # Result found! Wake up any Threads waiting
154
+ # Wake up the waiting Thread
143
155
  corresponding_action.future_resource.resource = event_collection.freeze
144
156
 
157
+ # Clear the stored action and event collection
145
158
  @current_action_with_causal_events = nil
146
159
  @event_collection_for_current_action = nil
147
-
148
160
  else
149
161
  event_collection << message
150
162
  # We have more causal events coming.
151
163
  end
152
164
  else
153
- ahn_log.ami.error "Got an unexpected event on actions socket! This may be a bug! #{message.inspect}"
165
+ 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}"
154
166
  end
155
167
 
156
168
  elsif message["ActionID"].nil?
@@ -195,7 +207,7 @@ module Adhearsion
195
207
  def event_message_received(event)
196
208
  return if event.kind_of?(ManagerInterfaceResponse) && event["Message"] == "Authentication accepted"
197
209
  # TODO: convert the event name to a certain namespace.
198
- Events.trigger %w[asterisk manager_interface], event
210
+ @event_callback.call(event)
199
211
  end
200
212
 
201
213
  def event_error_received(message)
@@ -226,13 +238,14 @@ module Adhearsion
226
238
 
227
239
  def actions_connection_established
228
240
  @actions_state = :connected
229
- @actions_writer_thread = Thread.new(&method(:write_loop))
241
+ start_actions_writer_loop
230
242
  end
231
243
 
232
244
  def actions_connection_disconnected
233
245
  @actions_state = :disconnected
234
246
  ahn_log.ami.error "AMI connection for ACTION disconnected !!!"
235
- establish_actions_connection
247
+ clear_actions_connection
248
+ establish_actions_connection if @auto_reconnect
236
249
  end
237
250
 
238
251
  def events_connection_established
@@ -242,14 +255,23 @@ module Adhearsion
242
255
  def events_connection_disconnected
243
256
  @events_state = :disconnected
244
257
  ahn_log.ami.error "AMI connection for EVENT disconnected !!!"
245
- establish_events_connection
258
+ clear_events_connection
259
+ establish_events_connection if @auto_reconnect
260
+ end
261
+
262
+ def clear_actions_connection
263
+ stop_actions_writer_loop
264
+ clear_actions_connection_resources
265
+ disconnect_actions_connection
266
+ end
267
+
268
+ def clear_events_connection
269
+ disconnect_events_connection
246
270
  end
247
271
 
248
272
  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
273
+ clear_actions_connection
274
+ clear_events_connection
253
275
  end
254
276
 
255
277
  def dynamic
@@ -390,7 +412,7 @@ module Adhearsion
390
412
  args[:exten] = options[:extension] if options[:extension]
391
413
  args[:caller_id] = options[:caller_id] if options[:caller_id]
392
414
  if options[:variables] && options[:variables].kind_of?(Hash)
393
- args[:variable] = options[:variables].map {|pair| pair.join('=')}.join(AHN_CONFIG.asterisk.argument_delimiter)
415
+ args[:variable] = options[:variables].map {|pair| pair.join('=')}.join(@coreSettings["ArgumentDelimiter"])
394
416
  end
395
417
  originate args
396
418
  end
@@ -410,8 +432,25 @@ module Adhearsion
410
432
  class UnsupportedActionName < ArgumentError
411
433
  UNSUPPORTED_ACTION_NAMES = %w[
412
434
  queues
413
- iaxpeers
414
435
  ] unless defined? UNSUPPORTED_ACTION_NAMES
436
+
437
+ # Blacklist some actions depends on the Asterisk version
438
+ def self.preinitialize(version)
439
+ if version < 1.8
440
+ %w[iaxpeers muteaudio mixmonitormute aocmessage].each do |action|
441
+ UNSUPPORTED_ACTION_NAMES << action
442
+ end
443
+ end
444
+
445
+ if version < 1.6
446
+ %w[skinnydevices skinnyshowdevice skinnylines skinnyshowline coreshowchannels
447
+ sipshowregistry getconfigjson bridge listallvoicemailusers dbdel dbdeltree
448
+ insert jitterbufstats atxfer iaxregistry queuereload queuereset].each do |action|
449
+ UNSUPPORTED_ACTION_NAMES << action
450
+ end
451
+ end
452
+ end
453
+
415
454
  def initialize(name)
416
455
  super "At the moment this AMI library doesn't support the #{name.inspect} action because it causes a protocol anomaly. Support for it will be coming shortly."
417
456
  end
@@ -424,19 +463,33 @@ module Adhearsion
424
463
  true
425
464
  end
426
465
 
427
- def write_loop
466
+ def start_actions_writer_loop
467
+ @actions_writer_thread = Thread.new(&method(:actions_writer_loop))
468
+ end
469
+
470
+ def stop_actions_writer_loop
471
+ if @actions_writer_thread
472
+ @write_queue << :STOP!
473
+ @actions_writer_thread.join
474
+ @actions_writer_thread = nil
475
+ end
476
+ end
477
+
478
+ def actions_writer_loop
428
479
  loop do
429
- next_action = @write_queue.shift
430
- return :stopped if next_action.equal? :STOP!
431
- register_action_with_metadata next_action
432
-
433
- ahn_log.ami.debug "Sending AMI action: #{"\n>>> " + next_action.to_s.gsub(/(\r\n)+/, "\n>>> ")}"
434
- @actions_connection.send_data next_action.to_s
435
- # If it's "causal event" action, we must wait here until it's fully responded
436
- next_action.response if next_action.has_causal_events?
480
+ begin
481
+ next_action = @write_queue.shift
482
+ return :stopped if next_action.equal? :STOP!
483
+ register_action_with_metadata next_action
484
+
485
+ ahn_log.ami.debug "Sending AMI action: #{"\n>>> " + next_action.to_s.gsub(/(\r\n)+/, "\n>>> ")}"
486
+ @actions_connection.send_data next_action.to_s
487
+ # If it's "causal event" action, we must wait here until it's fully responded
488
+ next_action.response if next_action.has_causal_events?
489
+ rescue Object => e
490
+ ahn_log.ami.debug "Error in AMI writer loop: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
491
+ end
437
492
  end
438
- rescue => e
439
- p e
440
493
  end
441
494
 
442
495
  ##
@@ -462,6 +515,22 @@ module Adhearsion
462
515
  end
463
516
  end
464
517
 
518
+ # Give an error response to any outstanding messages -- they
519
+ # won't be completed now
520
+ def clear_actions_connection_resources
521
+ # Fail all outstanding messages and reset the message list
522
+ @sent_messages_lock.synchronize do
523
+ @sent_messages.each do |action_id, action|
524
+ error = ManagerInterfaceError.new
525
+ error.message = "Connection terminated to AMI server"
526
+
527
+ action.future_resource.resource = error
528
+ end
529
+
530
+ @sent_messages = {}
531
+ end
532
+ end
533
+
465
534
  ##
466
535
  # Instantiates a new ManagerInterfaceActionsConnection and assigns it to @actions_connection.
467
536
  #
@@ -469,13 +538,22 @@ module Adhearsion
469
538
  #
470
539
  def establish_actions_connection
471
540
  @actions_connection = EventSocket.connect(@host, @port) do |handler|
472
- handler.receive_data { |data| @actions_lexer << data }
541
+ handler.receive_data { |data| @actions_lexer << data }
473
542
  handler.connected { actions_connection_established }
474
543
  handler.disconnected { actions_connection_disconnected }
475
544
  end
476
545
  login_actions
477
546
  end
478
547
 
548
+ def disconnect_actions_connection
549
+ # Clean up the EventSocket we may have
550
+ if @actions_connection
551
+ @actions_connection.disconnect!
552
+ @actions_connection.join
553
+ @actions_connection = nil
554
+ end
555
+ end
556
+
479
557
  ##
480
558
  # Instantiates a new ManagerInterfaceEventsConnection and assigns it to @events_connection.
481
559
  #
@@ -485,7 +563,7 @@ module Adhearsion
485
563
 
486
564
  # Note: the @events_connection instance variable is set in login()
487
565
  @events_connection = EventSocket.connect(@host, @port) do |handler|
488
- handler.receive_data { |data| @events_lexer << data }
566
+ handler.receive_data { |data| @events_lexer << data }
489
567
  handler.connected { events_connection_established }
490
568
  handler.disconnected { events_connection_disconnected }
491
569
  end
@@ -500,10 +578,29 @@ module Adhearsion
500
578
  raise AuthenticationFailedException, "Incorrect username and password! #{response.message}"
501
579
  else
502
580
  ahn_log.ami "Successful AMI actions-only connection into #{@username}@#{@host}"
581
+ if @actions_lexer.ami_version < 1.1
582
+ @coreSettings = Hash.new
583
+ @coreSettings["AsteriskVersion"] = "1.4.0"
584
+ @coreSettings["AMIversion"] = "1.0"
585
+ @coreSettings["ArgumentDelimiter"] = "|"
586
+ else
587
+ @coreSettings = send_action_synchronously("CoreSettings").headers
588
+ @coreSettings["ArgumentDelimiter"] = ","
589
+ end
590
+ UnsupportedActionName::preinitialize(@coreSettings["AsteriskVersion"].to_f)
503
591
  response
504
592
  end
505
593
  end
506
594
 
595
+ def disconnect_events_connection
596
+ # Clean up the EventSocket we may have
597
+ if @events_connection
598
+ @events_connection.disconnect!
599
+ @events_connection.join
600
+ @events_connection = nil
601
+ end
602
+ end
603
+
507
604
  ##
508
605
  # Since this method is always called after the login_actions method, an AuthenticationFailedException would have already
509
606
  # been raised if the username/password were off. Because this is the only action we ever need to send on this socket,
@@ -525,9 +622,9 @@ module Adhearsion
525
622
  ##
526
623
  # Raised when calling ManagerInterface#connect!() and the server responds with an error after logging in.
527
624
  #
528
- class AuthenticationFailedException < Exception; end
625
+ class AuthenticationFailedException < StandardError; end
529
626
 
530
- class NotConnectedError < Exception; end
627
+ class NotConnectedError < StandardError; end
531
628
 
532
629
  ##
533
630
  # Each time ManagerInterface#send_action is invoked, a new ManagerInterfaceAction is instantiated.
@@ -1533,7 +1533,7 @@ end
1533
1533
  end
1534
1534
 
1535
1535
  def init_error
1536
- @current_message = ManagerInterfaceError.new()
1536
+ @current_message = ManagerInterfaceError.new
1537
1537
  end
1538
1538
 
1539
1539
  def version_starts
@@ -46,7 +46,7 @@ module Adhearsion
46
46
 
47
47
  end
48
48
 
49
- class ManagerInterfaceError < Exception
49
+ class ManagerInterfaceError < StandardError
50
50
 
51
51
  attr_accessor :message
52
52
  def initialize
@@ -1,4 +1,5 @@
1
1
  require 'uri'
2
+ require 'thread'
2
3
  #TODO Some of this is asterisk-specific
3
4
  module Adhearsion
4
5
  class << self
@@ -16,7 +17,7 @@ module Adhearsion
16
17
  end
17
18
  end
18
19
 
19
- class Hangup < Exception
20
+ class Hangup < StandardError
20
21
  # At the moment, we'll just use this to end a call-handling Thread.
21
22
  end
22
23
 
@@ -52,7 +53,8 @@ module Adhearsion
52
53
  end
53
54
  end
54
55
 
55
- # Searches all active calls by their unique_identifier. See Call#unique_identifier.
56
+ # Searches all active calls by their unique_identifier. See Call#unique_identifier.
57
+ # Is this actually by channel?
56
58
  def find(id)
57
59
  atomically do
58
60
  return calls[id]
@@ -86,9 +88,9 @@ module Adhearsion
86
88
 
87
89
  end
88
90
 
89
- class UselessCallException < Exception; end
91
+ class UselessCallException < StandardError; end
90
92
 
91
- class MetaAgiCallException < Exception
93
+ class MetaAgiCallException < StandardError
92
94
  attr_reader :call
93
95
  def initialize(call)
94
96
  super()
@@ -113,7 +115,7 @@ module Adhearsion
113
115
  # instance variable.
114
116
  class CallMessageQueue < Queue
115
117
 
116
- class InboxClosedException < Exception
118
+ class InboxClosedException < StandardError
117
119
  # this gets raised when the :cancel message is delivered to the queue and with_next_message (or similar auto-message-iteration)
118
120
  # features are called.
119
121
  end
@@ -200,10 +202,15 @@ module Adhearsion
200
202
  end
201
203
  end
202
204
 
203
- def tag(symbol)
204
- raise ArgumentError, "tag must be a Symbol" unless symbol.is_a? Symbol
205
+ # This may still be a symbol, but no longer requires the tag to be a symbol although beware
206
+ # that using a symbol would create a memory leak if used improperly
207
+ # @param [String, Symbol] label String or Symbol with which to tag this call
208
+ def tag(label)
209
+ if ![String, Symbol].include?(label.class)
210
+ raise ArgumentError, "Tag must be a String or Symbol"
211
+ end
205
212
  @tag_mutex.synchronize do
206
- @tags << symbol
213
+ @tags << label
207
214
  end
208
215
  end
209
216
 
@@ -1,6 +1,23 @@
1
1
  # Hardcoding require for now since for some reason it's not being loaded
2
2
  require 'adhearsion/voip/dsl/dialplan/control_passing_exception'
3
3
 
4
+ require 'adhearsion/version'
5
+ # JRuby contains a bug that breaks some of the menu functionality
6
+ # See: https://adhearsion.lighthouseapp.com/projects/5871/tickets/92-menu-method-under-jruby-does-not-appear-to-work
7
+ begin
8
+ curver = Adhearsion::PkgVersion.new(JRUBY_VERSION)
9
+ minver = Adhearsion::PkgVersion.new("1.6.0")
10
+ if curver < minver
11
+ puts "****************************************************************************"
12
+ puts "Versions of JRuby prior to 1.6.0 contain a bug that limits the prevents"
13
+ puts "using the \"+\" operator to jump from one context to another."
14
+ puts "Adhearsion has detected JRuby version #{JRUBY_VERSION}. For more information see:"
15
+ puts "https://adhearsion.lighthouseapp.com/projects/5871/tickets/92-menu-method-under-jruby-does-not-appear-to-work"
16
+ puts "****************************************************************************"
17
+ end
18
+ rescue NameError # In case JRUBY_VERSION is not defined.
19
+ end
20
+
4
21
  module Adhearsion
5
22
  class DialPlan
6
23
  attr_accessor :loader, :entry_points
@@ -76,7 +93,7 @@ module Adhearsion
76
93
 
77
94
  class Manager
78
95
 
79
- class NoContextError < Exception; end
96
+ class NoContextError < StandardError; end
80
97
 
81
98
  class << self
82
99
  def handle(call)
@@ -110,10 +127,10 @@ module Adhearsion
110
127
  # Find the dialplan by the context name from the call or from the
111
128
  # first path entry in the AGI URL
112
129
  def entry_point_for(call)
113
- if entry_point = dial_plan.lookup(call.context.to_sym)
114
- entry_point
115
- elsif call.respond_to?(:request) && m = call.request.path.match(%r{/([^/]+)})
130
+ if call.respond_to?(:request) && m = call.request.path.match(%r{/([^/]+)})
116
131
  dial_plan.lookup(m[1].to_sym)
132
+ elsif entry_point = dial_plan.lookup(call.context.to_sym)
133
+ entry_point
117
134
  end
118
135
  end
119
136