punchblock 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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