adhearsion 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -3
  3. data/CHANGELOG.md +29 -0
  4. data/Gemfile +0 -2
  5. data/Guardfile +2 -2
  6. data/README.markdown +3 -6
  7. data/Rakefile +1 -1
  8. data/adhearsion.gemspec +7 -2
  9. data/features/cli_create.feature +85 -7
  10. data/features/plugin_generator.feature +4 -0
  11. data/features/step_definitions/app_generator_steps.rb +8 -1
  12. data/lib/adhearsion.rb +6 -3
  13. data/lib/adhearsion/call.rb +101 -30
  14. data/lib/adhearsion/call_controller.rb +40 -12
  15. data/lib/adhearsion/call_controller/dial.rb +119 -36
  16. data/lib/adhearsion/call_controller/input.rb +11 -5
  17. data/lib/adhearsion/call_controller/output.rb +47 -33
  18. data/lib/adhearsion/call_controller/output/async_player.rb +3 -2
  19. data/lib/adhearsion/call_controller/output/formatter.rb +7 -2
  20. data/lib/adhearsion/call_controller/output/player.rb +2 -2
  21. data/lib/adhearsion/call_controller/record.rb +16 -13
  22. data/lib/adhearsion/call_controller/utility.rb +3 -3
  23. data/lib/adhearsion/calls.rb +21 -8
  24. data/lib/adhearsion/cli_commands/ahn_command.rb +1 -0
  25. data/lib/adhearsion/configuration.rb +2 -2
  26. data/lib/adhearsion/console.rb +3 -2
  27. data/lib/adhearsion/generators.rb +7 -9
  28. data/lib/adhearsion/generators/app/app_generator.rb +12 -2
  29. data/lib/adhearsion/generators/app/templates/Gemfile.erb +7 -9
  30. data/lib/adhearsion/generators/app/templates/README.md +0 -19
  31. data/lib/adhearsion/generators/app/templates/adhearsion.erb +37 -0
  32. data/lib/adhearsion/generators/app/templates/config/environment.rb +6 -1
  33. data/lib/adhearsion/generators/app/templates/events.erb +18 -0
  34. data/lib/adhearsion/generators/app/templates/routes.erb +19 -0
  35. data/lib/adhearsion/generators/app/templates/{lib/simon_game.rb → simon_game.rb} +0 -0
  36. data/lib/adhearsion/generators/app/templates/{spec/call_controllers/simon_game_spec.rb → simon_game_spec.rb} +0 -0
  37. data/lib/adhearsion/generators/controller/controller_generator.rb +2 -2
  38. data/lib/adhearsion/generators/controller/templates/lib/{controller.rb → controller.rb.erb} +0 -0
  39. data/lib/adhearsion/generators/controller/templates/spec/{controller_spec.rb → controller_spec.rb.erb} +0 -0
  40. data/lib/adhearsion/generators/plugin/plugin_generator.rb +16 -15
  41. data/lib/adhearsion/generators/plugin/templates/gitignore +17 -0
  42. data/lib/adhearsion/generators/plugin/templates/spec/plugin-template/controller_methods_spec.rb.tt +1 -1
  43. data/lib/adhearsion/initializer.rb +14 -2
  44. data/lib/adhearsion/logging.rb +1 -0
  45. data/lib/adhearsion/outbound_call.rb +3 -7
  46. data/lib/adhearsion/punchblock_plugin/initializer.rb +3 -2
  47. data/lib/adhearsion/router/openended_route.rb +1 -1
  48. data/lib/adhearsion/router/route.rb +4 -3
  49. data/lib/adhearsion/version.rb +1 -1
  50. data/spec/adhearsion/call_controller/dial_spec.rb +811 -79
  51. data/spec/adhearsion/call_controller/output/formatter_spec.rb +13 -1
  52. data/spec/adhearsion/call_controller/output_spec.rb +35 -1
  53. data/spec/adhearsion/call_controller_spec.rb +174 -18
  54. data/spec/adhearsion/call_spec.rb +423 -39
  55. data/spec/adhearsion/calls_spec.rb +19 -3
  56. data/spec/adhearsion/outbound_call_spec.rb +88 -45
  57. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +3 -3
  58. data/spec/adhearsion/router/route_spec.rb +2 -2
  59. data/spec/spec_helper.rb +2 -0
  60. metadata +92 -77
  61. data/features/app_generator.feature +0 -49
  62. data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +0 -71
  63. data/lib/adhearsion/generators/plugin/templates/.gitignore +0 -9
@@ -64,7 +64,7 @@ module Adhearsion
64
64
  attr_reader :block
65
65
 
66
66
  delegate :[], :[]=, :to => :@metadata
67
- delegate :variables, :to => :call
67
+ delegate :variables, :send_message, :to => :call
68
68
 
69
69
  #
70
70
  # Create a new instance
@@ -75,12 +75,13 @@ module Adhearsion
75
75
  #
76
76
  def initialize(call, metadata = nil, &block)
77
77
  @call, @metadata, @block = call, metadata || {}, block
78
+ @block_context = eval "self", @block.binding if @block
79
+ @active_components = []
78
80
  end
79
81
 
80
82
  def method_missing(method_name, *args, &block)
81
- if @block
82
- block_context = eval "self", @block.binding
83
- block_context.send method_name, *args, &block
83
+ if @block_context
84
+ @block_context.send method_name, *args, &block
84
85
  else
85
86
  super
86
87
  end
@@ -117,7 +118,7 @@ module Adhearsion
117
118
  call.async.register_controller self
118
119
  execute_callbacks :before_call
119
120
  run
120
- rescue Call::Hangup
121
+ rescue Call::Hangup, Call::ExpiredError
121
122
  logger.info "Call was hung up while executing a controller"
122
123
  rescue SyntaxError, StandardError => e
123
124
  Events.trigger :exception, [e, logger]
@@ -155,6 +156,31 @@ module Adhearsion
155
156
  throw :pass_controller, controller_class.new(call, metadata)
156
157
  end
157
158
 
159
+ #
160
+ # Stop execution of all the components currently running in the controller.
161
+ #
162
+ def stop_all_components
163
+ logger.info "Stopping all controller components"
164
+ @active_components.each do |component|
165
+ begin
166
+ component.stop!
167
+ rescue Punchblock::Component::InvalidActionError
168
+ end
169
+ end
170
+ end
171
+
172
+ #
173
+ # Cease execution of this controller, including any components it is executing, and pass to another.
174
+ #
175
+ # @param [Class] controller_class The class of controller to pass to
176
+ # @param [Hash] metadata generic key-value storage applicable to the controller
177
+ #
178
+ def hard_pass(controller_class, metadata = nil)
179
+ logger.info "Hard passing with active components #{@active_components.inspect}"
180
+ stop_all_components
181
+ pass controller_class, metadata
182
+ end
183
+
158
184
  # @private
159
185
  def execute_callbacks(type)
160
186
  self.class.callbacks[type].each do |callback|
@@ -173,6 +199,12 @@ module Adhearsion
173
199
  def write_and_await_response(command)
174
200
  block_until_resumed
175
201
  call.write_and_await_response command
202
+ if command.is_a?(Punchblock::Component::ComponentNode)
203
+ command.register_event_handler Punchblock::Event::Complete do |event|
204
+ @active_components.delete command
205
+ end
206
+ @active_components << command
207
+ end
176
208
  end
177
209
 
178
210
  # @private
@@ -250,13 +282,9 @@ module Adhearsion
250
282
  def join(target, options = {})
251
283
  block_until_resumed
252
284
  async = (target.is_a?(Hash) ? target : options).delete :async
253
- join_command = call.join target, options
254
- waiter = join_command.call_uri || join_command.mixer_name
255
- if async
256
- call.wait_for_joined waiter
257
- else
258
- call.wait_for_unjoined waiter
259
- end
285
+ join = call.join target, options
286
+ waiter = async ? join[:joined_condition] : join[:unjoined_condition]
287
+ waiter.wait
260
288
  end
261
289
 
262
290
  alias :safely :catching_standard_errors
@@ -36,6 +36,13 @@ module Adhearsion
36
36
  # @option options [CallController] :confirm the controller to execute on the first outbound call to be answered, to give an opportunity to screen the call. The calls will be joined if the outbound call is still active after this controller completes.
37
37
  # @option options [Hash] :confirm_metadata Metadata to set on the confirmation controller before executing it. This is shared between all calls if dialing multiple endpoints; if you care about it being mutated, you should provide an immutable value (using eg https://github.com/harukizaemon/hamster).
38
38
  #
39
+ # @option options [Hash] :join_options Options to specify the kind of join operation to perform. See `Call#join` for details.
40
+ # @option options [Call, String, Hash] :join_target the target to join to. May be a Call object, a call ID (String, Hash) or a mixer name (Hash). See `Call#join` for details.
41
+ #
42
+ # @option options [#call] :pre_join A callback to be executed immediately prior to answering and joining a successful call. Is called with a single parameter which is the outbound call being joined.
43
+ #
44
+ # @option options [Array, #call] :ringback A collection of audio (see #play for acceptable values) to render as a replacement for ringback. If a callback is passed, it will be used to start ringback, and must return something that responds to #stop! to stop it.
45
+ #
39
46
  # @example Make a call to the PSTN using my SIP provider for VoIP termination
40
47
  # dial "SIP/19095551001@my.sip.voip.terminator.us"
41
48
  #
@@ -49,10 +56,13 @@ module Adhearsion
49
56
  #
50
57
  def dial(to, options = {})
51
58
  dial = Dial.new to, options, call
52
- dial.run
59
+ dial.run(self)
53
60
  dial.await_completion
61
+ dial.terminate_ringback
54
62
  dial.cleanup_calls
55
63
  dial.status
64
+ ensure
65
+ catching_standard_errors { dial.delete_logger if dial }
56
66
  end
57
67
 
58
68
  # Dial one or more third parties and join one to this call after execution of a confirmation controller.
@@ -63,10 +73,13 @@ module Adhearsion
63
73
  # @see #dial
64
74
  def dial_and_confirm(to, options = {})
65
75
  dial = ParallelConfirmationDial.new to, options, call
66
- dial.run
76
+ dial.run(self)
67
77
  dial.await_completion
78
+ dial.terminate_ringback
68
79
  dial.cleanup_calls
69
80
  dial.status
81
+ ensure
82
+ catching_standard_errors { dial.delete_logger if dial }
70
83
  end
71
84
 
72
85
  class Dial
@@ -74,6 +87,7 @@ module Adhearsion
74
87
 
75
88
  def initialize(to, options, call)
76
89
  raise Call::Hangup unless call.alive? && call.active?
90
+ @id = SecureRandom.uuid
77
91
  @options, @call = options, call
78
92
  @targets = to.respond_to?(:has_key?) ? to : Array(to)
79
93
  @call_targets = {}
@@ -81,12 +95,13 @@ module Adhearsion
81
95
  end
82
96
 
83
97
  def inspect
84
- "#<#{self.class} to=#{@to.inspect} options=#{@options.inspect}>"
98
+ "#<#{self.class}[#{@id}] to=#{@to.inspect} options=#{@options.inspect}>"
85
99
  end
86
100
 
87
101
  # Prep outbound calls, link call lifecycles and place outbound calls
88
- def run
102
+ def run(controller)
89
103
  track_originating_call
104
+ start_ringback controller
90
105
  prep_calls
91
106
  place_calls
92
107
  end
@@ -102,19 +117,42 @@ module Adhearsion
102
117
  end
103
118
  end
104
119
 
120
+ #
121
+ # Starts ringback on the specified controller
122
+ #
123
+ # @param [Adhearsion::CallController] controller the controller on which to play ringback
124
+ def start_ringback(controller)
125
+ return unless @ringback
126
+ @ringback_component = if @ringback.respond_to?(:call)
127
+ @ringback.call
128
+ else
129
+ controller.play! @ringback, repeat_times: 0
130
+ end
131
+ end
132
+
133
+ #
134
+ # Terminates any ringback that might be playing
135
+ #
136
+ def terminate_ringback
137
+ return unless @ringback_component
138
+ return unless @ringback_component.executing?
139
+ @ringback_component.stop!
140
+ end
141
+
105
142
  #
106
143
  # Prepares a set of OutboundCall actors to be dialed and links their lifecycles to the Dial operation
107
144
  #
108
145
  # @yield Each call to the passed block for further setup operations
109
146
  def prep_calls
110
- @calls = @targets.map do |target, specific_options|
147
+ @calls = Set.new
148
+ @targets.map do |target, specific_options|
111
149
  new_call = OutboundCall.new
112
150
 
113
151
  join_status = JoinStatus.new
114
152
  status.joins[new_call] = join_status
115
153
 
116
154
  new_call.on_end do |event|
117
- @latch.countdown! unless new_call["dial_countdown_#{@call.id}"]
155
+ @latch.countdown! unless new_call["dial_countdown_#{@id}"]
118
156
  if event.reason == :error
119
157
  status.error!
120
158
  join_status.errored!
@@ -127,7 +165,7 @@ module Adhearsion
127
165
  new_call.on_unjoined @call do |unjoined|
128
166
  join_status.ended
129
167
  unless @splitting
130
- new_call["dial_countdown_#{@call.id}"] = true
168
+ new_call["dial_countdown_#{@id}"] = true
131
169
  @latch.countdown!
132
170
  end
133
171
  end
@@ -145,7 +183,10 @@ module Adhearsion
145
183
  pre_join_tasks new_call
146
184
  @call.answer
147
185
  join_status.started
148
- new_call.join @call
186
+ new_call.join @join_target, @join_options
187
+ unless @join_target == @call
188
+ @call.join @join_target, @join_options
189
+ end
149
190
  status.answer!
150
191
  elsif status.result == :answer
151
192
  join_status.lost_confirmation!
@@ -156,7 +197,7 @@ module Adhearsion
156
197
 
157
198
  yield new_call if block_given?
158
199
 
159
- new_call
200
+ @calls << new_call
160
201
  end
161
202
 
162
203
  status.calls = @calls
@@ -183,29 +224,49 @@ module Adhearsion
183
224
  def split(targets = {})
184
225
  logger.info "Splitting calls apart"
185
226
  @splitting = true
186
- @calls.each do |call|
187
- logger.info "Unjoining peer #{call.id}"
188
- ignoring_missing_joins { @call.unjoin call.id }
189
- if split_controller = targets[:others]
190
- logger.info "Executing split controller #{split_controller} on #{call.id}"
191
- call.execute_controller split_controller.new(call, 'current_dial' => self), targets[:others_callback]
227
+ calls_to_split = @calls.map do |call|
228
+ ignoring_ended_calls do
229
+ [call.id, call] if call.active?
230
+ end
231
+ end.compact
232
+ logger.info "Splitting peer calls #{calls_to_split.map(&:first).join ", "}"
233
+ calls_to_split.each do |id, call|
234
+ ignoring_ended_calls do
235
+ logger.info "Unjoining peer #{call.id} from #{join_target}"
236
+ ignoring_missing_joins { call.unjoin join_target }
237
+ if split_controller = targets[:others]
238
+ logger.info "Executing split controller #{split_controller} on #{call.id}"
239
+ call.execute_controller split_controller.new(call, 'current_dial' => self), targets[:others_callback]
240
+ end
192
241
  end
193
242
  end
194
- if split_controller = targets[:main]
195
- logger.info "Executing split controller #{split_controller} on main call"
196
- @call.execute_controller split_controller.new(@call, 'current_dial' => self), targets[:main_callback]
243
+ ignoring_ended_calls do
244
+ if join_target != @call
245
+ logger.info "Unjoining main call #{@call.id} from #{join_target}"
246
+ @call.unjoin join_target
247
+ end
248
+ if split_controller = targets[:main]
249
+ logger.info "Executing split controller #{split_controller} on main call"
250
+ @call.execute_controller split_controller.new(@call, 'current_dial' => self), targets[:main_callback]
251
+ end
197
252
  end
198
253
  end
199
254
 
200
255
  # Rejoin parties that were previously split
201
256
  # @param [Call, String, Hash] target The target to join calls to. See Call#join for details.
202
- def rejoin(target = @call)
257
+ # @param [Hash] join_options Options to specify the kind of join operation to perform. See `Call#join` for details.
258
+ def rejoin(target = nil, join_options = nil)
259
+ target ||= join_target
260
+ join_options ||= @join_options
203
261
  logger.info "Rejoining to #{target}"
204
- unless target == @call
205
- @call.join target
262
+ ignoring_ended_calls do
263
+ unless target == @call
264
+ @join_target = target
265
+ @call.join target, join_options
266
+ end
206
267
  end
207
268
  @calls.each do |call|
208
- call.join target
269
+ ignoring_ended_calls { call.join target, join_options }
209
270
  end
210
271
  end
211
272
 
@@ -213,16 +274,15 @@ module Adhearsion
213
274
  # @param [Dial] other the other dial operation to merge calls from
214
275
  def merge(other)
215
276
  logger.info "Merging with #{other.inspect}"
216
- mixer_name = SecureRandom.uuid
217
277
 
218
278
  split
219
279
  other.split
220
280
 
221
- rejoin mixer_name: mixer_name
222
- other.rejoin mixer_name: mixer_name
281
+ rejoin({mixer_name: @id}, {})
282
+ other.rejoin({mixer_name: @id}, {})
223
283
 
224
284
  calls_to_merge = other.status.calls + [other.root_call]
225
- @calls.concat calls_to_merge
285
+ @calls.merge calls_to_merge
226
286
 
227
287
  latch = CountDownLatch.new calls_to_merge.size
228
288
  calls_to_merge.each do |call|
@@ -236,7 +296,9 @@ module Adhearsion
236
296
  def await_completion
237
297
  @latch.wait(@options[:timeout]) || status.timeout!
238
298
  return unless status.result == :answer
299
+ logger.debug "Main calls were completed, waiting for any added calls: #{@waiters.inspect}"
239
300
  @waiters.each(&:wait)
301
+ logger.debug "All calls were completed, unblocking."
240
302
  end
241
303
 
242
304
  #
@@ -249,9 +311,8 @@ module Adhearsion
249
311
  # Hangup any remaining calls
250
312
  def cleanup_calls
251
313
  calls_to_hangup = @calls.map do |call|
252
- begin
314
+ ignoring_ended_calls do
253
315
  [call.id, call] if call.active?
254
- rescue Celluloid::DeadActorError
255
316
  end
256
317
  end.compact
257
318
  if calls_to_hangup.size.zero?
@@ -263,15 +324,15 @@ module Adhearsion
263
324
  else
264
325
  logger.info "#dial finished. Hanging up #{calls_to_hangup.size} outbound calls which are still active: #{calls_to_hangup.map(&:first).join ", "}."
265
326
  calls_to_hangup.each do |id, outbound_call|
266
- begin
267
- outbound_call.hangup
268
- rescue Celluloid::DeadActorError
269
- # This actor may previously have been shut down due to the call ending
270
- end
327
+ ignoring_ended_calls { outbound_call.hangup }
271
328
  end
272
329
  end
273
330
  end
274
331
 
332
+ def delete_logger
333
+ ::Logging::Repository.instance.delete logger_id
334
+ end
335
+
275
336
  protected
276
337
 
277
338
  def root_call
@@ -280,6 +341,15 @@ module Adhearsion
280
341
 
281
342
  private
282
343
 
344
+ # @private
345
+ def logger_id
346
+ "#{self.class}: #{@id}"
347
+ end
348
+
349
+ def join_target
350
+ @join_target || @call
351
+ end
352
+
283
353
  def set_defaults
284
354
  @status = DialStatus.new
285
355
 
@@ -294,6 +364,12 @@ module Adhearsion
294
364
  @confirmation_controller = @options.delete :confirm
295
365
  @confirmation_metadata = @options.delete :confirm_metadata
296
366
 
367
+ @pre_join = @options.delete :pre_join
368
+ @ringback = @options.delete :ringback
369
+
370
+ @join_options = @options.delete(:join_options) || {}
371
+ @join_target = @options.delete(:join_target) || @call
372
+
297
373
  @skip_cleanup = false
298
374
  end
299
375
 
@@ -305,15 +381,15 @@ module Adhearsion
305
381
  end
306
382
 
307
383
  def pre_join_tasks(call)
384
+ @pre_join[call] if @pre_join
385
+ terminate_ringback
308
386
  end
309
387
 
310
388
  def on_all_except(call)
311
389
  @calls.each do |target_call|
312
- begin
390
+ ignoring_ended_calls do
313
391
  next if target_call.id == call.id
314
392
  yield target_call
315
- rescue Celluloid::DeadActorError
316
- # This actor may previously have been shut down due to the call ending
317
393
  end
318
394
  end
319
395
  end
@@ -323,6 +399,12 @@ module Adhearsion
323
399
  rescue Punchblock::ProtocolError => e
324
400
  raise unless e.name == :service_unavailable
325
401
  end
402
+
403
+ def ignoring_ended_calls
404
+ yield
405
+ rescue Celluloid::DeadActorError, Adhearsion::Call::Hangup, Adhearsion::Call::ExpiredError
406
+ # This actor may previously have been shut down due to the call ending
407
+ end
326
408
  end
327
409
 
328
410
  class ParallelConfirmationDial < Dial
@@ -337,6 +419,7 @@ module Adhearsion
337
419
  end
338
420
 
339
421
  def pre_join_tasks(call)
422
+ super
340
423
  on_all_except call do |target_call|
341
424
  if @apology_controller
342
425
  logger.info "#dial apologising to call #{target_call.id} because this call has been confirmed by another channel"
@@ -40,10 +40,13 @@ module Adhearsion
40
40
  # @see Output#play
41
41
  # @see CallController#pass
42
42
  #
43
- def ask(*args, &block)
43
+ def ask(*args, options, &block)
44
44
  logger.warn "This implementation of #ask is deprecated due to issues with dropped DTMF. For a solution, see http://adhearsion.com/docs/common_problems#toc_3"
45
45
 
46
- options = args.last.kind_of?(Hash) ? args.pop : {}
46
+ unless options.is_a?(Hash)
47
+ args << options
48
+ options = {}
49
+ end
47
50
  sound_files = args.flatten
48
51
 
49
52
  menu_instance = MenuDSL::Menu.new options do
@@ -130,10 +133,13 @@ module Adhearsion
130
133
  # @see Output#play
131
134
  # @see CallController#pass
132
135
  #
133
- def menu(*args, &block)
136
+ def menu(*args, options, &block)
134
137
  logger.warn "This implementation of #menu is deprecated due to issues with dropped DTMF. For a solution, see http://adhearsion.com/docs/common_problems#toc_3"
135
138
 
136
- options = args.last.kind_of?(Hash) ? args.pop : {}
139
+ unless options.is_a?(Hash)
140
+ args << options
141
+ options = {}
142
+ end
137
143
  sound_files = args.flatten
138
144
 
139
145
  menu_instance = MenuDSL::Menu.new options, &block
@@ -203,7 +209,7 @@ module Adhearsion
203
209
  #
204
210
  # Waits for a single digit and returns it, or returns nil if nothing was pressed
205
211
  #
206
- # @param [Integer] the timeout to wait before returning, in seconds. nil or -1 mean no timeout.
212
+ # @param [Integer] timeout the timeout to wait before returning, in seconds. nil or -1 mean no timeout.
207
213
  # @return [String, nil] the pressed key, or nil if timeout was reached.
208
214
  #
209
215
  # @private