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