punchblock 0.6.1 → 0.6.2
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.
- data/CHANGELOG.md +4 -0
- data/lib/punchblock.rb +15 -13
- data/lib/punchblock/client.rb +1 -0
- data/lib/punchblock/component.rb +1 -1
- data/lib/punchblock/connection/asterisk.rb +2 -2
- data/lib/punchblock/connection/xmpp.rb +8 -9
- data/lib/punchblock/core_ext/ruby.rb +24 -0
- data/lib/punchblock/has_headers.rb +4 -0
- data/lib/punchblock/translator/asterisk.rb +40 -9
- data/lib/punchblock/translator/asterisk/call.rb +107 -3
- data/lib/punchblock/translator/asterisk/component.rb +9 -3
- data/lib/punchblock/translator/asterisk/component/asterisk.rb +14 -0
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +84 -0
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +96 -0
- data/lib/punchblock/version.rb +1 -1
- data/spec/punchblock/connection/xmpp_spec.rb +3 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +206 -0
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +143 -0
- data/spec/punchblock/translator/asterisk/component/asterisk/ami_action_spec.rb +159 -0
- data/spec/punchblock/translator/asterisk_spec.rb +90 -10
- metadata +46 -41
- data/lib/punchblock/translator/asterisk/ami_action.rb +0 -86
- data/spec/punchblock/translator/asterisk/ami_action_spec.rb +0 -149
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# develop
|
|
2
2
|
|
|
3
|
+
# v0.6.2
|
|
4
|
+
# Feature: Added basic support for running Punchblock apps on Asterisk. Calls coming in to AsyncAGI result in the client receiving an Offer, hangup events are sent, and accept/answer/hangup commands work.
|
|
5
|
+
# API change: The logger is now set using Punchblock.logger= rather than as a hash key to Connection.new
|
|
6
|
+
|
|
3
7
|
# v0.6.1
|
|
4
8
|
* Feature: Allow instructing the connection we are ready. An XMPP connection will send initial presence with a status of 'chat' to the rayo domain
|
|
5
9
|
* Bugfix: When running on Asterisk, two FullyBooted events will now trigger a connected event
|
data/lib/punchblock.rb
CHANGED
|
@@ -4,21 +4,9 @@
|
|
|
4
4
|
active_support/core_ext/module/delegation
|
|
5
5
|
future-resource
|
|
6
6
|
has_guarded_handlers
|
|
7
|
+
punchblock/core_ext/ruby
|
|
7
8
|
}.each { |l| require l }
|
|
8
9
|
|
|
9
|
-
class Hash
|
|
10
|
-
def select(&block)
|
|
11
|
-
val = super(&block)
|
|
12
|
-
if val.is_a?(Array)
|
|
13
|
-
val = val.inject({}) do |accumulator, element|
|
|
14
|
-
accumulator[element[0]] = element[1]
|
|
15
|
-
accumulator
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
val
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
10
|
module Punchblock
|
|
23
11
|
extend ActiveSupport::Autoload
|
|
24
12
|
|
|
@@ -36,6 +24,20 @@ module Punchblock
|
|
|
36
24
|
autoload :RayoNode
|
|
37
25
|
autoload :Translator
|
|
38
26
|
|
|
27
|
+
class << self
|
|
28
|
+
def logger
|
|
29
|
+
@logger || reset_logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def logger=(other)
|
|
33
|
+
@logger = other
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_logger
|
|
37
|
+
@logger = NullObject.new
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
39
41
|
##
|
|
40
42
|
# This exception may be raised if a transport error is detected.
|
|
41
43
|
TransportError = Class.new StandardError
|
data/lib/punchblock/client.rb
CHANGED
|
@@ -50,6 +50,7 @@ module Punchblock
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def execute_command(command, options = {})
|
|
53
|
+
pb_logger.debug "Executing command: #{command.inspect} with options #{options.inspect}"
|
|
53
54
|
async = options.has_key?(:async) ? options.delete(:async) : true
|
|
54
55
|
command.client = self
|
|
55
56
|
if command.respond_to?(:register_event_handler)
|
data/lib/punchblock/component.rb
CHANGED
|
@@ -7,13 +7,13 @@ module Punchblock
|
|
|
7
7
|
attr_accessor :event_handler
|
|
8
8
|
|
|
9
9
|
def initialize(options = {})
|
|
10
|
-
options
|
|
11
|
-
@ami_client = RubyAMI::Client.new options.merge(:event_handler => lambda { |event| translator.handle_ami_event! event })
|
|
10
|
+
@ami_client = RubyAMI::Client.new options.merge(:event_handler => lambda { |event| translator.handle_ami_event! event }, :logger => pb_logger)
|
|
12
11
|
@translator = Translator::Asterisk.new @ami_client, self
|
|
13
12
|
super()
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def run
|
|
16
|
+
logger.debug "Starting the RubyAMI client"
|
|
17
17
|
ami_client.start
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -18,7 +18,6 @@ module Punchblock
|
|
|
18
18
|
# @option options [String] :username client JID
|
|
19
19
|
# @option options [String] :password XMPP password
|
|
20
20
|
# @option options [String] :rayo_domain the domain on which Rayo is running
|
|
21
|
-
# @option options [Logger] :wire_logger to which all XMPP transactions will be logged
|
|
22
21
|
# @option options [Boolean, Optional] :auto_reconnect whether or not to auto reconnect
|
|
23
22
|
# @option options [Numeric, Optional] :write_timeout for which to wait on a command response
|
|
24
23
|
# @option options [Numeric, nil, Optional] :ping_period interval in seconds on which to ping the server. Nil or false to disable
|
|
@@ -37,7 +36,7 @@ module Punchblock
|
|
|
37
36
|
|
|
38
37
|
@ping_period = options.has_key?(:ping_period) ? options[:ping_period] : 60
|
|
39
38
|
|
|
40
|
-
Blather.logger =
|
|
39
|
+
Blather.logger = pb_logger
|
|
41
40
|
|
|
42
41
|
super()
|
|
43
42
|
end
|
|
@@ -61,7 +60,7 @@ module Punchblock
|
|
|
61
60
|
jid = command.is_a?(Command::Dial) ? @rayo_domain : "#{call_id}@#{@callmap[call_id]}"
|
|
62
61
|
jid << "/#{component_id}" if component_id
|
|
63
62
|
create_iq(jid).tap do |iq|
|
|
64
|
-
|
|
63
|
+
pb_logger.debug "Sending IQ ID #{iq.id} #{command.inspect} to #{jid}"
|
|
65
64
|
iq << command
|
|
66
65
|
end
|
|
67
66
|
end
|
|
@@ -102,9 +101,9 @@ module Punchblock
|
|
|
102
101
|
|
|
103
102
|
def handle_presence(p)
|
|
104
103
|
throw :pass unless p.rayo_event?
|
|
105
|
-
|
|
104
|
+
pb_logger.info "Receiving event for call ID #{p.call_id}"
|
|
106
105
|
@callmap[p.call_id] = p.from.domain
|
|
107
|
-
|
|
106
|
+
pb_logger.debug p.inspect
|
|
108
107
|
event = p.event
|
|
109
108
|
event.connection = self
|
|
110
109
|
event.domain = p.from.domain
|
|
@@ -114,7 +113,7 @@ module Punchblock
|
|
|
114
113
|
def handle_iq_result(iq, command)
|
|
115
114
|
# FIXME: Do we need to raise a warning if the domain changes?
|
|
116
115
|
@callmap[iq.from.node] = iq.from.domain
|
|
117
|
-
|
|
116
|
+
pb_logger.debug "Command #{iq.id} completed successfully"
|
|
118
117
|
command.response = iq.rayo_node.is_a?(Ref) ? iq.rayo_node : true
|
|
119
118
|
end
|
|
120
119
|
|
|
@@ -128,7 +127,7 @@ module Punchblock
|
|
|
128
127
|
# Push a message to the queue and the log that we connected
|
|
129
128
|
when_ready do
|
|
130
129
|
event_handler.call Connected.new
|
|
131
|
-
|
|
130
|
+
pb_logger.info "Connected to XMPP as #{@username}"
|
|
132
131
|
@reconnect_attempts = 0
|
|
133
132
|
@rayo_ping = EM::PeriodicTimer.new(@ping_period) { ping_rayo } if @ping_period
|
|
134
133
|
end
|
|
@@ -137,9 +136,9 @@ module Punchblock
|
|
|
137
136
|
@rayo_ping.cancel if @rayo_ping
|
|
138
137
|
if @auto_reconnect && @reconnect_attempts
|
|
139
138
|
timer = 30 * 2 ** @reconnect_attempts
|
|
140
|
-
|
|
139
|
+
pb_logger.warn "XMPP disconnected. Tried to reconnect #{@reconnect_attempts} times. Reconnecting in #{timer}s."
|
|
141
140
|
sleep timer
|
|
142
|
-
|
|
141
|
+
pb_logger.info "Trying to reconnect..."
|
|
143
142
|
@reconnect_attempts += 1
|
|
144
143
|
connect
|
|
145
144
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Hash
|
|
2
|
+
def select(&block)
|
|
3
|
+
val = super(&block)
|
|
4
|
+
if val.is_a?(Array)
|
|
5
|
+
val = val.inject({}) do |accumulator, element|
|
|
6
|
+
accumulator[element[0]] = element[1]
|
|
7
|
+
accumulator
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
val
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class NullObject
|
|
15
|
+
def method_missing(*args)
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Object
|
|
21
|
+
def pb_logger
|
|
22
|
+
Punchblock.logger
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -9,19 +9,20 @@ module Punchblock
|
|
|
9
9
|
|
|
10
10
|
extend ActiveSupport::Autoload
|
|
11
11
|
|
|
12
|
-
autoload :AMIAction
|
|
13
12
|
autoload :Call
|
|
14
13
|
autoload :Component
|
|
15
14
|
|
|
16
|
-
attr_reader :ami_client, :connection
|
|
15
|
+
attr_reader :ami_client, :connection, :calls
|
|
17
16
|
|
|
18
17
|
def initialize(ami_client, connection)
|
|
18
|
+
pb_logger.debug "Starting up..."
|
|
19
19
|
@ami_client, @connection = ami_client, connection
|
|
20
|
-
@calls, @components = {}, {}
|
|
20
|
+
@calls, @components, @channel_to_call_id = {}, {}, {}
|
|
21
21
|
@fully_booted_count = 0
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def register_call(call)
|
|
25
|
+
@channel_to_call_id[call.channel] = call.id
|
|
25
26
|
@calls[call.id] ||= call
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -29,6 +30,10 @@ module Punchblock
|
|
|
29
30
|
@calls[call_id]
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
def call_for_channel(channel)
|
|
34
|
+
call_with_id @channel_to_call_id[channel]
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
def register_component(component)
|
|
33
38
|
@components[component.id] ||= component
|
|
34
39
|
end
|
|
@@ -39,18 +44,32 @@ module Punchblock
|
|
|
39
44
|
|
|
40
45
|
def handle_ami_event(event)
|
|
41
46
|
return unless event.is_a? RubyAMI::Event
|
|
47
|
+
pb_logger.trace "Handling AMI event #{event.inspect}"
|
|
42
48
|
if event.name.downcase == "fullybooted"
|
|
49
|
+
pb_logger.trace "Counting FullyBooted event"
|
|
43
50
|
@fully_booted_count += 1
|
|
44
51
|
if @fully_booted_count >= 2
|
|
45
|
-
|
|
52
|
+
handle_pb_event Connection::Connected.new
|
|
46
53
|
@fully_booted_count = 0
|
|
47
54
|
end
|
|
48
|
-
|
|
49
|
-
connection.handle_event Event::Asterisk::AMI::Event.new(:name => event.name, :attributes => event.headers)
|
|
55
|
+
return
|
|
50
56
|
end
|
|
57
|
+
if event.name.downcase == "asyncagi" && event['SubEvent'] == "Start"
|
|
58
|
+
handle_async_agi_start_event event
|
|
59
|
+
end
|
|
60
|
+
if call = call_for_channel(event['Channel'])
|
|
61
|
+
pb_logger.trace "Found call by channel matching this event. Sending to call #{call.id}"
|
|
62
|
+
call.process_ami_event! event
|
|
63
|
+
end
|
|
64
|
+
handle_pb_event Event::Asterisk::AMI::Event.new(:name => event.name, :attributes => event.headers)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_pb_event(event)
|
|
68
|
+
connection.handle_event event
|
|
51
69
|
end
|
|
52
70
|
|
|
53
71
|
def execute_command(command, options = {})
|
|
72
|
+
pb_logger.debug "Executing command #{command.inspect}"
|
|
54
73
|
command.request!
|
|
55
74
|
if command.call_id || options[:call_id]
|
|
56
75
|
command.call_id ||= options[:call_id]
|
|
@@ -66,18 +85,30 @@ module Punchblock
|
|
|
66
85
|
end
|
|
67
86
|
|
|
68
87
|
def execute_call_command(command)
|
|
69
|
-
call_with_id(command.call_id).execute_command command
|
|
88
|
+
call_with_id(command.call_id).execute_command! command
|
|
70
89
|
end
|
|
71
90
|
|
|
72
91
|
def execute_component_command(command)
|
|
73
|
-
call_with_id(command.call_id).execute_component_command command
|
|
92
|
+
call_with_id(command.call_id).execute_component_command! command
|
|
74
93
|
end
|
|
75
94
|
|
|
76
95
|
def execute_global_command(command)
|
|
77
|
-
component = AMIAction.new command,
|
|
96
|
+
component = Component::Asterisk::AMIAction.new command, current_actor
|
|
78
97
|
# register_component component
|
|
79
98
|
component.execute!
|
|
80
99
|
end
|
|
100
|
+
|
|
101
|
+
def send_ami_action(name, headers = {}, &block)
|
|
102
|
+
ami_client.send_action name, headers, &block
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def handle_async_agi_start_event(event)
|
|
108
|
+
call = Call.new event['Channel'], current_actor, event['Env']
|
|
109
|
+
register_call call
|
|
110
|
+
call.send_offer!
|
|
111
|
+
end
|
|
81
112
|
end
|
|
82
113
|
end
|
|
83
114
|
end
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
1
3
|
module Punchblock
|
|
2
4
|
module Translator
|
|
3
5
|
class Asterisk
|
|
4
6
|
class Call
|
|
5
7
|
include Celluloid
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
attr_reader :id, :channel, :translator, :agi_env
|
|
10
|
+
|
|
11
|
+
def initialize(channel, translator, agi_env = '')
|
|
12
|
+
@channel, @translator = channel, translator
|
|
13
|
+
@agi_env = parse_environment agi_env
|
|
14
|
+
@id, @components = UUIDTools::UUID.random_create.to_s, {}
|
|
15
|
+
pb_logger.debug "Starting up call with channel #{channel}, id #{@id}"
|
|
9
16
|
end
|
|
10
17
|
|
|
11
18
|
def register_component(component)
|
|
@@ -17,7 +24,104 @@ module Punchblock
|
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
def execute_component_command(command)
|
|
20
|
-
component_with_id(command.component_id).execute_command command
|
|
27
|
+
component_with_id(command.component_id).execute_command! command
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def send_offer
|
|
31
|
+
send_pb_event offer_event
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def process_ami_event(ami_event)
|
|
35
|
+
pb_logger.trace "Processing AMI event #{ami_event.inspect}"
|
|
36
|
+
case ami_event.name
|
|
37
|
+
when 'Hangup'
|
|
38
|
+
pb_logger.debug "Received a Hangup AMI event. Sending End event."
|
|
39
|
+
send_pb_event Event::End.new(:reason => :hangup)
|
|
40
|
+
when 'AsyncAGI'
|
|
41
|
+
pb_logger.debug "Received an AsyncAGI event. Looking for matching AGICommand component."
|
|
42
|
+
if component = component_with_id(ami_event['CommandID'])
|
|
43
|
+
pb_logger.debug "Found component #{component.id} for event. Forwarding event..."
|
|
44
|
+
component.handle_ami_event! ami_event
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def execute_command(command)
|
|
50
|
+
pb_logger.debug "Executing command: #{command.inspect}"
|
|
51
|
+
case command
|
|
52
|
+
when Command::Accept
|
|
53
|
+
send_agi_action 'EXEC RINGING' do |response|
|
|
54
|
+
command.response = true
|
|
55
|
+
end
|
|
56
|
+
when Command::Answer
|
|
57
|
+
send_agi_action 'EXEC ANSWER' do |response|
|
|
58
|
+
command.response = true
|
|
59
|
+
end
|
|
60
|
+
when Command::Hangup
|
|
61
|
+
send_ami_action 'Hangup', 'Channel' => channel do |response|
|
|
62
|
+
command.response = true
|
|
63
|
+
end
|
|
64
|
+
when Punchblock::Component::Asterisk::AGI::Command
|
|
65
|
+
execute_agi_command command
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def send_agi_action(command, &block)
|
|
70
|
+
pb_logger.debug "Sending AGI action #{command}"
|
|
71
|
+
@current_agi_command = Punchblock::Component::Asterisk::AGI::Command.new :name => command, :call_id => id
|
|
72
|
+
@current_agi_command.request!
|
|
73
|
+
@current_agi_command.register_event_handler Punchblock::Event::Complete do |e|
|
|
74
|
+
pb_logger.debug "AGI action received complete event #{e.inspect}"
|
|
75
|
+
block.call e
|
|
76
|
+
end
|
|
77
|
+
execute_agi_command @current_agi_command
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def send_ami_action(name, headers = {}, &block)
|
|
81
|
+
(name.is_a?(RubyAMI::Action) ? name : RubyAMI::Action.new(name, headers, &block)).tap do |action|
|
|
82
|
+
@current_ami_action = action
|
|
83
|
+
pb_logger.debug "Sending AMI action #{action.inspect}"
|
|
84
|
+
translator.send_ami_action! action
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def execute_agi_command(command)
|
|
91
|
+
Component::Asterisk::AGICommand.new(command, current_actor).tap do |component|
|
|
92
|
+
register_component component
|
|
93
|
+
component.execute!
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def send_pb_event(event)
|
|
98
|
+
event.call_id = id
|
|
99
|
+
pb_logger.debug "Sending Punchblock event: #{event.inspect}"
|
|
100
|
+
translator.handle_pb_event! event
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def offer_event
|
|
104
|
+
Event::Offer.new :to => agi_env[:agi_dnid],
|
|
105
|
+
:from => [agi_env[:agi_type].downcase, agi_env[:agi_callerid]].join(':'),
|
|
106
|
+
:headers => sip_headers
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def sip_headers
|
|
110
|
+
agi_env.to_a.inject({}) do |accumulator, element|
|
|
111
|
+
accumulator[('x_' + element[0].to_s).to_sym] = element[1] || ''
|
|
112
|
+
accumulator
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_environment(agi_env)
|
|
117
|
+
agi_env_as_array(agi_env).inject({}) do |accumulator, element|
|
|
118
|
+
accumulator[element[0].to_sym] = element[1] || ''
|
|
119
|
+
accumulator
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def agi_env_as_array(agi_env)
|
|
124
|
+
URI.unescape(agi_env).encode.split("\n").map { |p| p.split ': ' }
|
|
21
125
|
end
|
|
22
126
|
end
|
|
23
127
|
end
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
module Punchblock
|
|
2
2
|
module Translator
|
|
3
3
|
class Asterisk
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
module Component
|
|
5
|
+
extend ActiveSupport::Autoload
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
autoload :Asterisk
|
|
8
|
+
|
|
9
|
+
class Component
|
|
10
|
+
include Celluloid
|
|
11
|
+
|
|
12
|
+
attr_reader :id
|
|
13
|
+
end
|
|
8
14
|
end
|
|
9
15
|
end
|
|
10
16
|
end
|