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.
Files changed (107) hide show
  1. data/CHANGELOG +6 -0
  2. data/EVENTS +11 -0
  3. data/LICENSE +456 -0
  4. data/README.txt +5 -0
  5. data/Rakefile +120 -0
  6. data/adhearsion.gemspec +146 -0
  7. data/app_generators/ahn/USAGE +5 -0
  8. data/app_generators/ahn/ahn_generator.rb +87 -0
  9. data/app_generators/ahn/templates/.ahnrc +34 -0
  10. data/app_generators/ahn/templates/README +8 -0
  11. data/app_generators/ahn/templates/Rakefile +23 -0
  12. data/app_generators/ahn/templates/components/ami_remote/ami_remote.rb +15 -0
  13. data/app_generators/ahn/templates/components/disabled/HOW_TO_ENABLE +7 -0
  14. data/app_generators/ahn/templates/components/disabled/stomp_gateway/README.markdown +47 -0
  15. data/app_generators/ahn/templates/components/disabled/stomp_gateway/config.yml +12 -0
  16. data/app_generators/ahn/templates/components/disabled/stomp_gateway/stomp_gateway.rb +34 -0
  17. data/app_generators/ahn/templates/components/restful_rpc/README.markdown +11 -0
  18. data/app_generators/ahn/templates/components/restful_rpc/config.yml +34 -0
  19. data/app_generators/ahn/templates/components/restful_rpc/example-client.rb +48 -0
  20. data/app_generators/ahn/templates/components/restful_rpc/restful_rpc.rb +87 -0
  21. data/app_generators/ahn/templates/components/simon_game/simon_game.rb +56 -0
  22. data/app_generators/ahn/templates/config/startup.rb +53 -0
  23. data/app_generators/ahn/templates/dialplan.rb +3 -0
  24. data/app_generators/ahn/templates/events.rb +32 -0
  25. data/bin/ahn +28 -0
  26. data/bin/ahnctl +68 -0
  27. data/bin/jahn +42 -0
  28. data/examples/asterisk_manager_interface/standalone.rb +51 -0
  29. data/lib/adhearsion/cli.rb +223 -0
  30. data/lib/adhearsion/component_manager/spec_framework.rb +24 -0
  31. data/lib/adhearsion/component_manager.rb +208 -0
  32. data/lib/adhearsion/events_support.rb +84 -0
  33. data/lib/adhearsion/foundation/all.rb +9 -0
  34. data/lib/adhearsion/foundation/blank_slate.rb +5 -0
  35. data/lib/adhearsion/foundation/custom_daemonizer.rb +45 -0
  36. data/lib/adhearsion/foundation/event_socket.rb +203 -0
  37. data/lib/adhearsion/foundation/future_resource.rb +36 -0
  38. data/lib/adhearsion/foundation/global.rb +1 -0
  39. data/lib/adhearsion/foundation/metaprogramming.rb +17 -0
  40. data/lib/adhearsion/foundation/numeric.rb +13 -0
  41. data/lib/adhearsion/foundation/pseudo_guid.rb +10 -0
  42. data/lib/adhearsion/foundation/relationship_properties.rb +42 -0
  43. data/lib/adhearsion/foundation/string.rb +26 -0
  44. data/lib/adhearsion/foundation/synchronized_hash.rb +96 -0
  45. data/lib/adhearsion/foundation/thread_safety.rb +7 -0
  46. data/lib/adhearsion/host_definitions.rb +67 -0
  47. data/lib/adhearsion/initializer/asterisk.rb +81 -0
  48. data/lib/adhearsion/initializer/configuration.rb +254 -0
  49. data/lib/adhearsion/initializer/database.rb +49 -0
  50. data/lib/adhearsion/initializer/drb.rb +31 -0
  51. data/lib/adhearsion/initializer/freeswitch.rb +22 -0
  52. data/lib/adhearsion/initializer/rails.rb +40 -0
  53. data/lib/adhearsion/initializer.rb +373 -0
  54. data/lib/adhearsion/logging.rb +92 -0
  55. data/lib/adhearsion/tasks/database.rb +5 -0
  56. data/lib/adhearsion/tasks/deprecations.rb +59 -0
  57. data/lib/adhearsion/tasks/generating.rb +20 -0
  58. data/lib/adhearsion/tasks/lint.rb +4 -0
  59. data/lib/adhearsion/tasks/testing.rb +37 -0
  60. data/lib/adhearsion/tasks.rb +16 -0
  61. data/lib/adhearsion/version.rb +9 -0
  62. data/lib/adhearsion/voip/asterisk/agi_server.rb +81 -0
  63. data/lib/adhearsion/voip/asterisk/commands.rb +1284 -0
  64. data/lib/adhearsion/voip/asterisk/config_generators/agents.conf.rb +140 -0
  65. data/lib/adhearsion/voip/asterisk/config_generators/config_generator.rb +101 -0
  66. data/lib/adhearsion/voip/asterisk/config_generators/queues.conf.rb +250 -0
  67. data/lib/adhearsion/voip/asterisk/config_generators/voicemail.conf.rb +240 -0
  68. data/lib/adhearsion/voip/asterisk/config_manager.rb +71 -0
  69. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rb +1754 -0
  70. data/lib/adhearsion/voip/asterisk/manager_interface/ami_lexer.rl.rb +286 -0
  71. data/lib/adhearsion/voip/asterisk/manager_interface/ami_messages.rb +78 -0
  72. data/lib/adhearsion/voip/asterisk/manager_interface/ami_protocol_lexer_machine.rl +87 -0
  73. data/lib/adhearsion/voip/asterisk/manager_interface.rb +562 -0
  74. data/lib/adhearsion/voip/asterisk/special_dial_plan_managers.rb +80 -0
  75. data/lib/adhearsion/voip/asterisk/super_manager.rb +19 -0
  76. data/lib/adhearsion/voip/asterisk.rb +4 -0
  77. data/lib/adhearsion/voip/call.rb +440 -0
  78. data/lib/adhearsion/voip/call_routing.rb +64 -0
  79. data/lib/adhearsion/voip/commands.rb +9 -0
  80. data/lib/adhearsion/voip/constants.rb +39 -0
  81. data/lib/adhearsion/voip/conveniences.rb +18 -0
  82. data/lib/adhearsion/voip/dial_plan.rb +218 -0
  83. data/lib/adhearsion/voip/dsl/dialing_dsl/dialing_dsl_monkey_patches.rb +37 -0
  84. data/lib/adhearsion/voip/dsl/dialing_dsl.rb +151 -0
  85. data/lib/adhearsion/voip/dsl/dialplan/control_passing_exception.rb +27 -0
  86. data/lib/adhearsion/voip/dsl/dialplan/dispatcher.rb +124 -0
  87. data/lib/adhearsion/voip/dsl/dialplan/parser.rb +71 -0
  88. data/lib/adhearsion/voip/dsl/dialplan/thread_mixin.rb +16 -0
  89. data/lib/adhearsion/voip/dsl/numerical_string.rb +117 -0
  90. data/lib/adhearsion/voip/freeswitch/basic_connection_manager.rb +48 -0
  91. data/lib/adhearsion/voip/freeswitch/event_handler.rb +58 -0
  92. data/lib/adhearsion/voip/freeswitch/freeswitch_dialplan_command_factory.rb +129 -0
  93. data/lib/adhearsion/voip/freeswitch/inbound_connection_manager.rb +38 -0
  94. data/lib/adhearsion/voip/freeswitch/oes_server.rb +195 -0
  95. data/lib/adhearsion/voip/menu_state_machine/calculated_match.rb +80 -0
  96. data/lib/adhearsion/voip/menu_state_machine/matchers.rb +123 -0
  97. data/lib/adhearsion/voip/menu_state_machine/menu_builder.rb +58 -0
  98. data/lib/adhearsion/voip/menu_state_machine/menu_class.rb +149 -0
  99. data/lib/adhearsion.rb +37 -0
  100. data/lib/theatre/README.markdown +64 -0
  101. data/lib/theatre/callback_definition_loader.rb +84 -0
  102. data/lib/theatre/guid.rb +23 -0
  103. data/lib/theatre/invocation.rb +121 -0
  104. data/lib/theatre/namespace_manager.rb +153 -0
  105. data/lib/theatre/version.rb +2 -0
  106. data/lib/theatre.rb +151 -0
  107. 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
@@ -0,0 +1,4 @@
1
+ require File.dirname(__FILE__) + "/dsl/numerical_string"
2
+ require File.dirname(__FILE__) + "/asterisk/agi_server"
3
+ require File.dirname(__FILE__) + "/asterisk/manager_interface"
4
+ require File.dirname(__FILE__) + "/asterisk/commands"