punchblock 1.3.0 → 1.4.0

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 (42) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/lib/punchblock.rb +1 -1
  3. data/lib/punchblock/connection.rb +1 -0
  4. data/lib/punchblock/connection/asterisk.rb +0 -1
  5. data/lib/punchblock/connection/freeswitch.rb +49 -0
  6. data/lib/punchblock/event/offer.rb +1 -1
  7. data/lib/punchblock/translator.rb +5 -0
  8. data/lib/punchblock/translator/asterisk.rb +16 -28
  9. data/lib/punchblock/translator/asterisk/call.rb +4 -21
  10. data/lib/punchblock/translator/asterisk/component.rb +0 -5
  11. data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +0 -3
  12. data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +0 -1
  13. data/lib/punchblock/translator/asterisk/component/input.rb +7 -97
  14. data/lib/punchblock/translator/asterisk/component/output.rb +0 -4
  15. data/lib/punchblock/translator/asterisk/component/record.rb +0 -2
  16. data/lib/punchblock/translator/freeswitch.rb +153 -0
  17. data/lib/punchblock/translator/freeswitch/call.rb +265 -0
  18. data/lib/punchblock/translator/freeswitch/component.rb +92 -0
  19. data/lib/punchblock/translator/freeswitch/component/abstract_output.rb +57 -0
  20. data/lib/punchblock/translator/freeswitch/component/flite_output.rb +17 -0
  21. data/lib/punchblock/translator/freeswitch/component/input.rb +29 -0
  22. data/lib/punchblock/translator/freeswitch/component/output.rb +56 -0
  23. data/lib/punchblock/translator/freeswitch/component/record.rb +79 -0
  24. data/lib/punchblock/translator/freeswitch/component/tts_output.rb +26 -0
  25. data/lib/punchblock/translator/input_component.rb +108 -0
  26. data/lib/punchblock/version.rb +1 -1
  27. data/punchblock.gemspec +3 -2
  28. data/spec/punchblock/connection/freeswitch_spec.rb +90 -0
  29. data/spec/punchblock/translator/asterisk/call_spec.rb +23 -2
  30. data/spec/punchblock/translator/asterisk/component/input_spec.rb +3 -3
  31. data/spec/punchblock/translator/asterisk_spec.rb +1 -1
  32. data/spec/punchblock/translator/freeswitch/call_spec.rb +922 -0
  33. data/spec/punchblock/translator/freeswitch/component/flite_output_spec.rb +279 -0
  34. data/spec/punchblock/translator/freeswitch/component/input_spec.rb +312 -0
  35. data/spec/punchblock/translator/freeswitch/component/output_spec.rb +369 -0
  36. data/spec/punchblock/translator/freeswitch/component/record_spec.rb +373 -0
  37. data/spec/punchblock/translator/freeswitch/component/tts_output_spec.rb +285 -0
  38. data/spec/punchblock/translator/freeswitch/component_spec.rb +118 -0
  39. data/spec/punchblock/translator/freeswitch_spec.rb +597 -0
  40. data/spec/punchblock_spec.rb +11 -0
  41. data/spec/spec_helper.rb +1 -0
  42. metadata +52 -7
@@ -54,13 +54,11 @@ module Punchblock
54
54
  when :unimrcp
55
55
  send_ref
56
56
  @call.send_agi_action! 'EXEC MRCPSynth', escaped_doc, mrcpsynth_options do |complete_event|
57
- pb_logger.debug "MRCPSynth completed with #{complete_event}."
58
57
  output_component.send_complete_event! success_reason
59
58
  end
60
59
  when :swift
61
60
  send_ref
62
61
  @call.send_agi_action! 'EXEC Swift', swift_doc do |complete_event|
63
- pb_logger.debug "Swift completed with #{complete_event}."
64
62
  output_component.send_complete_event! success_reason
65
63
  end
66
64
  end
@@ -89,10 +87,8 @@ module Punchblock
89
87
  end
90
88
 
91
89
  def playback(path)
92
- pb_logger.debug "Playing an audio file (#{path}) via Playback"
93
90
  op = current_actor
94
91
  @call.send_agi_action! 'EXEC Playback', path do |complete_event|
95
- pb_logger.debug "File playback completed with #{complete_event}. Sending complete event"
96
92
  op.send_complete_event! success_reason
97
93
  end
98
94
  end
@@ -31,7 +31,6 @@ module Punchblock
31
31
  send_ref
32
32
 
33
33
  if @component_node.start_beep
34
- pb_logger.debug "Playing a beep via STREAM FILE before recording"
35
34
  @call.send_agi_action! 'STREAM FILE', 'beep', '""' do
36
35
  component.signal! :beep_finished
37
36
  end
@@ -41,7 +40,6 @@ module Punchblock
41
40
  call.send_ami_action! 'Monitor', 'Channel' => call.channel, 'File' => filename, 'Format' => @format, 'Mix' => true
42
41
  unless max_duration == -1
43
42
  after max_duration/1000 do
44
- pb_logger.trace "Max duration encountered, stopping recording"
45
43
  call.send_ami_action! 'StopMonitor', 'Channel' => call.channel
46
44
  end
47
45
  end
@@ -0,0 +1,153 @@
1
+ # encoding: utf-8
2
+
3
+ require 'celluloid'
4
+ require 'ruby_fs'
5
+
6
+ module Punchblock
7
+ module Translator
8
+ class Freeswitch
9
+ include Celluloid
10
+ include HasGuardedHandlers
11
+
12
+ extend ActiveSupport::Autoload
13
+
14
+ autoload :Call
15
+ autoload :Component
16
+
17
+ attr_reader :connection, :media_engine, :default_voice, :calls
18
+
19
+ trap_exit :actor_died
20
+
21
+ def initialize(connection, media_engine = nil, default_voice = nil)
22
+ @connection, @media_engine, @default_voice = connection, media_engine, default_voice
23
+ @calls, @components = {}, {}
24
+ setup_handlers
25
+ end
26
+
27
+ def register_call(call)
28
+ @calls[call.id] ||= call
29
+ end
30
+
31
+ def deregister_call(call)
32
+ @calls.delete call.id
33
+ end
34
+
35
+ def call_with_id(call_id)
36
+ @calls[call_id]
37
+ end
38
+
39
+ def register_component(component)
40
+ @components[component.id] ||= component
41
+ end
42
+
43
+ def component_with_id(component_id)
44
+ @components[component_id]
45
+ end
46
+
47
+ def setup_handlers
48
+ register_handler :es, RubyFS::Stream::Connected do
49
+ handle_pb_event Connection::Connected.new
50
+ throw :halt
51
+ end
52
+
53
+ register_handler :es, RubyFS::Stream::Disconnected do
54
+ throw :halt
55
+ end
56
+
57
+ register_handler :es, :event_name => 'CHANNEL_PARK' do |event|
58
+ throw :pass if es_event_known_call? event
59
+ call = Call.new event[:unique_id], current_actor, event.content.select { |k,v| k.to_s =~ /variable/ }, stream, @media_engine, @default_voice
60
+ link call
61
+ register_call call
62
+ call.send_offer!
63
+ end
64
+
65
+ register_handler :es, [:has_key?, :other_leg_unique_id] => true do |event|
66
+ call = call_with_id event[:other_leg_unique_id]
67
+ call.handle_es_event! event if call
68
+ end
69
+
70
+ register_handler :es, lambda { |event| es_event_known_call? event } do |event|
71
+ call = call_with_id event[:unique_id]
72
+ call.handle_es_event! event
73
+ end
74
+ end
75
+
76
+ def stream
77
+ connection.stream
78
+ end
79
+
80
+ def finalize
81
+ @calls.values.each(&:terminate)
82
+ end
83
+
84
+ def handle_es_event(event)
85
+ trigger_handler :es, event
86
+ end
87
+ exclusive :handle_es_event
88
+
89
+ def handle_pb_event(event)
90
+ connection.handle_event event
91
+ end
92
+
93
+ def execute_command(command, options = {})
94
+ command.request!
95
+
96
+ command.target_call_id ||= options[:call_id]
97
+ command.component_id ||= options[:component_id]
98
+
99
+ if command.target_call_id
100
+ execute_call_command command
101
+ elsif command.component_id
102
+ execute_component_command command
103
+ else
104
+ execute_global_command command
105
+ end
106
+ end
107
+
108
+ def execute_call_command(command)
109
+ if call = call_with_id(command.target_call_id)
110
+ call.execute_command! command
111
+ else
112
+ command.response = ProtocolError.new.setup :item_not_found, "Could not find a call with ID #{command.target_call_id}", command.target_call_id
113
+ end
114
+ end
115
+
116
+ def execute_component_command(command)
117
+ if (component = component_with_id(command.component_id))
118
+ component.execute_command! command
119
+ else
120
+ command.response = ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id}", command.target_call_id, command.component_id
121
+ end
122
+ end
123
+
124
+ def execute_global_command(command)
125
+ case command
126
+ when Punchblock::Command::Dial
127
+ call = Call.new_link Punchblock.new_uuid, current_actor, nil, stream, @media_engine, @default_voice
128
+ register_call call
129
+ call.dial! command
130
+ else
131
+ command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command"
132
+ end
133
+ end
134
+
135
+ def actor_died(actor, reason)
136
+ return unless reason
137
+ pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}"
138
+ if id = @calls.key(actor)
139
+ @calls.delete id
140
+ end_event = Punchblock::Event::End.new :target_call_id => id,
141
+ :reason => :error
142
+ handle_pb_event end_event
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def es_event_known_call?(event)
149
+ event[:unique_id] && call_with_id(event[:unique_id])
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,265 @@
1
+ # encoding: utf-8
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Freeswitch
6
+ class Call
7
+ include HasGuardedHandlers
8
+ include Celluloid
9
+ include DeadActorSafety
10
+
11
+ HANGUP_CAUSE_TO_END_REASON = Hash.new :error
12
+
13
+ HANGUP_CAUSE_TO_END_REASON['USER_BUSY'] = :busy
14
+
15
+ %w{
16
+ NORMAL_CLEARING ORIGINATOR_CANCEL SYSTEM_SHUTDOWN MANAGER_REQUEST
17
+ BLIND_TRANSFER ATTENDED_TRANSFER PICKED_OFF NORMAL_UNSPECIFIED
18
+ }.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :hangup }
19
+
20
+ %w{
21
+ NO_USER_RESPONSE NO_ANSWER SUBSCRIBER_ABSENT ALLOTTED_TIMEOUT
22
+ MEDIA_TIMEOUT PROGRESS_TIMEOUT
23
+ }.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :timeout }
24
+
25
+ %w{CALL_REJECTED NUMBER_CHANGED
26
+ REDIRECTION_TO_NEW_DESTINATION FACILITY_REJECTED NORMAL_CIRCUIT_CONGESTION
27
+ SWITCH_CONGESTION USER_NOT_REGISTERED FACILITY_NOT_SUBSCRIBED
28
+ OUTGOING_CALL_BARRED INCOMING_CALL_BARRED BEARERCAPABILITY_NOTAUTH
29
+ BEARERCAPABILITY_NOTAVAIL SERVICE_UNAVAILABLE BEARERCAPABILITY_NOTIMPL
30
+ CHAN_NOT_IMPLEMENTED FACILITY_NOT_IMPLEMENTED SERVICE_NOT_IMPLEMENTED
31
+ }.each { |c| HANGUP_CAUSE_TO_END_REASON[c] = :reject }
32
+
33
+ REJECT_TO_HANGUP_REASON = Hash.new 'NORMAL_TEMPORARY_FAILURE'
34
+ REJECT_TO_HANGUP_REASON.merge! :busy => 'USER_BUSY', :decline => 'CALL_REJECTED'
35
+
36
+ attr_reader :id, :translator, :es_env, :direction, :stream, :media_engine, :default_voice
37
+
38
+ trap_exit :actor_died
39
+
40
+ def initialize(id, translator, es_env = nil, stream = nil, media_engine = nil, default_voice = nil)
41
+ @id, @translator, @stream, @media_engine, @default_voice = id, translator, stream, media_engine, default_voice
42
+ @es_env = es_env || {}
43
+ @components = {}
44
+ @pending_joins, @pending_unjoins = {}, {}
45
+ @answered = false
46
+ setup_handlers
47
+ end
48
+
49
+ def register_component(component)
50
+ @components[component.id] ||= component
51
+ end
52
+
53
+ def component_with_id(component_id)
54
+ @components[component_id]
55
+ end
56
+
57
+ def send_offer
58
+ @direction = :inbound
59
+ send_pb_event offer_event
60
+ end
61
+
62
+ def to_s
63
+ "#<#{self.class}:#{id}>"
64
+ end
65
+ alias :inspect :to_s
66
+
67
+ def setup_handlers
68
+ register_handler :es, :event_name => 'CHANNEL_ANSWER' do
69
+ @answered = true
70
+ send_pb_event Event::Answered.new
71
+ end
72
+
73
+ register_handler :es, :event_name => 'CHANNEL_STATE', [:[], :channel_call_state] => 'RINGING' do
74
+ send_pb_event Event::Ringing.new
75
+ end
76
+
77
+ register_handler :es, :event_name => 'CHANNEL_HANGUP' do |event|
78
+ @components.dup.each_pair do |id, component|
79
+ safe_from_dead_actors do
80
+ component.call_ended if component.alive?
81
+ end
82
+ end
83
+ send_end_event HANGUP_CAUSE_TO_END_REASON[event[:hangup_cause]]
84
+ end
85
+
86
+ register_handler :es, :event_name => 'CHANNEL_BRIDGE' do |event|
87
+ command = @pending_joins[event[:other_leg_unique_id]]
88
+ command.response = true if command
89
+
90
+ other_call_id = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
91
+ send_pb_event Event::Joined.new(:call_id => other_call_id)
92
+ end
93
+
94
+ register_handler :es, :event_name => 'CHANNEL_UNBRIDGE' do |event|
95
+ command = @pending_unjoins[event[:other_leg_unique_id]]
96
+ command.response = true if command
97
+
98
+ other_call_id = event[:unique_id] == id ? event[:other_leg_unique_id] : event[:unique_id]
99
+ send_pb_event Event::Unjoined.new(:call_id => other_call_id)
100
+ end
101
+
102
+
103
+ register_handler :es, [:has_key?, :scope_variable_punchblock_component_id] => true do |event|
104
+ if component = component_with_id(event[:scope_variable_punchblock_component_id])
105
+ safe_from_dead_actors { component.handle_es_event event }
106
+ end
107
+ end
108
+ end
109
+
110
+ def handle_es_event(event)
111
+ trigger_handler :es, event
112
+ end
113
+
114
+ def application(*args)
115
+ stream.application id, *args
116
+ end
117
+
118
+ def sendmsg(*args)
119
+ stream.sendmsg id, *args
120
+ end
121
+
122
+ def uuid_foo(app, args = '')
123
+ stream.bgapi "uuid_#{app} #{id} #{args}"
124
+ end
125
+
126
+ def dial(dial_command)
127
+ @direction = :outbound
128
+
129
+ cid_number, cid_name = dial_command.from, nil
130
+ dial_command.from.match(/(?<cid_name>.*) <(?<cid_number>.*)>/) do |m|
131
+ cid_name = m[:cid_name]
132
+ cid_number = m[:cid_number]
133
+ end
134
+
135
+ options = {
136
+ :return_ring_ready => true,
137
+ :origination_uuid => id,
138
+ :origination_caller_id_number => "'#{cid_number}'"
139
+ }
140
+ options[:origination_caller_id_name] = "'#{cid_name}'" if cid_name
141
+ options[:originate_timeout] = dial_command.timeout/1000 if dial_command.timeout
142
+ opts = options.inject([]) do |a, (k, v)|
143
+ a << "#{k}=#{v}"
144
+ end.join(',')
145
+
146
+ stream.bgapi "originate {#{opts}}#{dial_command.to} &park()"
147
+
148
+ dial_command.response = Ref.new :id => id
149
+ end
150
+
151
+ def outbound?
152
+ direction == :outbound
153
+ end
154
+
155
+ def inbound?
156
+ direction == :inbound
157
+ end
158
+
159
+ def answered?
160
+ @answered
161
+ end
162
+
163
+ def execute_command(command)
164
+ if command.component_id
165
+ if component = component_with_id(command.component_id)
166
+ component.execute_command command
167
+ else
168
+ command.response = ProtocolError.new.setup :item_not_found, "Could not find a component with ID #{command.component_id} for call #{id}", id, command.component_id
169
+ end
170
+ end
171
+ case command
172
+ when Command::Accept
173
+ application 'respond', '180 Ringing'
174
+ command.response = true
175
+ when Command::Answer
176
+ command_id = Punchblock.new_uuid
177
+ register_tmp_handler :es, :event_name => 'CHANNEL_ANSWER', [:[], :scope_variable_punchblock_command_id] => command_id do
178
+ @answered = true
179
+ command.response = true
180
+ end
181
+ application 'answer', "%[punchblock_command_id=#{command_id}]"
182
+ when Command::Hangup
183
+ hangup
184
+ command.response = true
185
+ when Command::Join
186
+ @pending_joins[command.call_id] = command
187
+ uuid_foo :bridge, command.call_id
188
+ when Command::Unjoin
189
+ @pending_unjoins[command.call_id] = command
190
+ uuid_foo :transfer, '-both park inline'
191
+ when Command::Reject
192
+ hangup REJECT_TO_HANGUP_REASON[command.reason]
193
+ command.response = true
194
+ when Punchblock::Component::Output
195
+ case media_engine
196
+ when :freeswitch, :native, nil
197
+ execute_component Component::Output, command
198
+ when :flite
199
+ execute_component Component::FliteOutput, command, media_engine, default_voice
200
+ else
201
+ execute_component Component::TTSOutput, command, media_engine, default_voice
202
+ end
203
+ when Punchblock::Component::Input
204
+ execute_component Component::Input, command
205
+ when Punchblock::Component::Record
206
+ execute_component Component::Record, command
207
+ else
208
+ command.response = ProtocolError.new.setup 'command-not-acceptable', "Did not understand command for call #{id}", id
209
+ end
210
+ end
211
+
212
+ def hangup(reason = 'NORMAL_CLEARING')
213
+ sendmsg :call_command => 'hangup', :hangup_cause => reason
214
+ end
215
+
216
+ def logger_id
217
+ "#{self.class}: #{id}"
218
+ end
219
+
220
+ def actor_died(actor, reason)
221
+ return unless reason
222
+ pb_logger.error "A linked actor (#{actor.inspect}) died due to #{reason.inspect}"
223
+ if id = @components.key(actor)
224
+ @components.delete id
225
+ complete_event = Punchblock::Event::Complete.new :component_id => id, :reason => Punchblock::Event::Complete::Error.new
226
+ send_pb_event complete_event
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ def send_end_event(reason)
233
+ send_pb_event Event::End.new(:reason => reason)
234
+ translator.deregister_call current_actor
235
+ after(5) { terminate }
236
+ end
237
+
238
+ def execute_component(type, command, *execute_args)
239
+ type.new_link(command, current_actor).tap do |component|
240
+ register_component component
241
+ component.execute!(*execute_args)
242
+ end
243
+ end
244
+
245
+ def send_pb_event(event)
246
+ event.target_call_id = id
247
+ translator.handle_pb_event event
248
+ end
249
+
250
+ def offer_event
251
+ Event::Offer.new :to => es_env[:variable_sip_to_uri],
252
+ :from => "#{es_env[:variable_effective_caller_id_name]} <#{es_env[:variable_sip_from_uri]}>",
253
+ :headers => headers
254
+ end
255
+
256
+ def headers
257
+ es_env.to_a.inject({}) do |accumulator, element|
258
+ accumulator[('x_' + element[0].to_s).to_sym] = element[1] || ''
259
+ accumulator
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end