punchblock 0.4.3 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +3 -0
- data/bin/punchblock-console +11 -10
- data/lib/punchblock.rb +2 -1
- data/lib/punchblock/client.rb +64 -0
- data/lib/punchblock/client/component_registry.rb +22 -0
- data/lib/punchblock/command/dial.rb +1 -4
- data/lib/punchblock/component.rb +6 -8
- data/lib/punchblock/connection.rb +4 -206
- data/lib/punchblock/connection/connected.rb +15 -0
- data/lib/punchblock/connection/xmpp.rb +161 -0
- data/lib/punchblock/dsl.rb +4 -4
- data/lib/punchblock/rayo_node.rb +2 -2
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +1 -1
- data/spec/punchblock/client/component_registry_spec.rb +15 -0
- data/spec/punchblock/client_spec.rb +125 -0
- data/spec/punchblock/command/dial_spec.rb +1 -8
- data/spec/punchblock/component/input_spec.rb +3 -2
- data/spec/punchblock/component/output_spec.rb +10 -9
- data/spec/punchblock/component/record_spec.rb +5 -4
- data/spec/punchblock/component/tropo/ask_spec.rb +3 -2
- data/spec/punchblock/component/tropo/conference_spec.rb +6 -5
- data/spec/punchblock/component/tropo/say_spec.rb +5 -4
- data/spec/punchblock/component/tropo/transfer_spec.rb +3 -2
- data/spec/punchblock/component_spec.rb +3 -9
- data/spec/punchblock/connection/xmpp_spec.rb +201 -0
- metadata +45 -38
- data/lib/punchblock/generic_connection.rb +0 -18
- data/spec/punchblock/connection_spec.rb +0 -216
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# develop
|
2
2
|
|
3
|
+
# v0.5.0
|
4
|
+
* Refactoring/API change: Client and connection level concerns are now separated, and one must create a Connection to be passed to a Client on creation. Client now has the choice between an event queue and guarded event handlers.
|
5
|
+
|
3
6
|
# v0.4.3
|
4
7
|
* Feature: Support for meta-data on recordings (size & duration)
|
5
8
|
* Feature: Allow specifying all of Blather's setup options (required to use PB as an XMPP component)
|
data/bin/punchblock-console
CHANGED
@@ -54,18 +54,19 @@ options[:transport_logger] = Logger.new options.delete(:transport_log_file)
|
|
54
54
|
options[:transport_logger].level = Logger::DEBUG
|
55
55
|
options[:transport_logger].debug "Starting up..."
|
56
56
|
|
57
|
-
connection = Connection.new options
|
57
|
+
connection = Connection::XMPP.new options
|
58
|
+
client = Client.new :connection => connection
|
58
59
|
|
59
60
|
[:INT, :TERM].each do |signal|
|
60
61
|
trap signal do
|
61
62
|
puts "Shutting down!"
|
62
|
-
|
63
|
+
client.stop
|
63
64
|
end
|
64
65
|
end
|
65
66
|
|
66
|
-
|
67
|
+
client_thread = Thread.new do
|
67
68
|
begin
|
68
|
-
|
69
|
+
client.run
|
69
70
|
rescue => e
|
70
71
|
puts "Exception in XMPP thread! #{e.message}"
|
71
72
|
puts e.backtrace.join("\t\n")
|
@@ -80,8 +81,8 @@ CALL_QUEUES = {}
|
|
80
81
|
# the queue.
|
81
82
|
Thread.new do
|
82
83
|
loop do
|
83
|
-
event =
|
84
|
-
if event ==
|
84
|
+
event = client.event_queue.pop
|
85
|
+
if event == Connection::Connected
|
85
86
|
puts event
|
86
87
|
puts "Waiting for a call..."
|
87
88
|
next
|
@@ -92,7 +93,7 @@ Thread.new do
|
|
92
93
|
raise "Duplicate call ID for #{event.call_id}" if CALL_QUEUES.has_key?(event.call_id)
|
93
94
|
CALL_QUEUES[event.call_id] = Queue.new
|
94
95
|
CALL_QUEUES[event.call_id].push event
|
95
|
-
run_call
|
96
|
+
run_call client, event
|
96
97
|
when Event
|
97
98
|
CALL_QUEUES[event.call_id].push event
|
98
99
|
else
|
@@ -101,14 +102,14 @@ Thread.new do
|
|
101
102
|
end
|
102
103
|
end
|
103
104
|
|
104
|
-
def run_call(
|
105
|
+
def run_call(client, offer)
|
105
106
|
### CALL THREAD
|
106
107
|
# One thread is spun up to handle each call.
|
107
108
|
Thread.new do
|
108
109
|
raise "Unknown call #{offer.call_id}" unless CALL_QUEUES.has_key?(offer.call_id)
|
109
110
|
queue = CALL_QUEUES[offer.call_id]
|
110
111
|
call = queue.pop
|
111
|
-
dsl = DSL.new
|
112
|
+
dsl = DSL.new client, offer.call_id, queue
|
112
113
|
|
113
114
|
puts "Incoming offer to #{offer.to} from #{offer.headers_hash[:from]} #{offer}"
|
114
115
|
dsl.pry
|
@@ -118,4 +119,4 @@ def run_call(connection, offer)
|
|
118
119
|
end
|
119
120
|
end
|
120
121
|
|
121
|
-
|
122
|
+
client_thread.join
|
data/lib/punchblock.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
%w{
|
2
2
|
active_support/dependencies/autoload
|
3
3
|
active_support/core_ext/object/blank
|
4
|
+
active_support/core_ext/module/delegation
|
4
5
|
future-resource
|
5
6
|
has_guarded_handlers
|
6
7
|
}.each { |l| require l }
|
@@ -21,12 +22,12 @@ end
|
|
21
22
|
module Punchblock
|
22
23
|
extend ActiveSupport::Autoload
|
23
24
|
|
25
|
+
autoload :Client
|
24
26
|
autoload :Command
|
25
27
|
autoload :CommandNode
|
26
28
|
autoload :Component
|
27
29
|
autoload :Connection
|
28
30
|
autoload :DSL
|
29
|
-
autoload :GenericConnection
|
30
31
|
autoload :HasHeaders
|
31
32
|
autoload :Header
|
32
33
|
autoload :MediaContainer
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Punchblock
|
2
|
+
class Client
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
|
5
|
+
autoload :ComponentRegistry
|
6
|
+
|
7
|
+
include HasGuardedHandlers
|
8
|
+
|
9
|
+
attr_reader :connection, :event_queue, :component_registry
|
10
|
+
|
11
|
+
delegate :run, :stop, :to => :connection
|
12
|
+
|
13
|
+
# @param [Hash] options
|
14
|
+
# @option options [Connection::XMPP] :connection The Punchblock connection to use for this session
|
15
|
+
#
|
16
|
+
def initialize(options = {})
|
17
|
+
@event_queue = Queue.new
|
18
|
+
@connection = options[:connection]
|
19
|
+
@connection.event_handler = lambda { |event| self.handle_event event } if @connection
|
20
|
+
register_initial_handlers
|
21
|
+
@component_registry = ComponentRegistry.new
|
22
|
+
@write_timeout = options[:write_timeout] || 3
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_event(event)
|
26
|
+
event.client = self
|
27
|
+
if event.source
|
28
|
+
event.source.add_event event
|
29
|
+
else
|
30
|
+
trigger_handler :event, event
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_event_handler(*guards, &block)
|
35
|
+
register_handler :event, *guards, &block
|
36
|
+
end
|
37
|
+
|
38
|
+
def register_initial_handlers
|
39
|
+
register_handler_with_priority :event, -10 do |event|
|
40
|
+
event_queue.push event
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def register_component(component)
|
45
|
+
component_registry << component
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_component_by_id(component_id)
|
49
|
+
component_registry.find_by_id component_id
|
50
|
+
end
|
51
|
+
|
52
|
+
def execute_command(command, options = {})
|
53
|
+
async = options.has_key?(:async) ? options.delete(:async) : true
|
54
|
+
command.client = self
|
55
|
+
if command.respond_to?(:register_event_handler)
|
56
|
+
command.register_event_handler do |event|
|
57
|
+
trigger_handler :event, event
|
58
|
+
end
|
59
|
+
end
|
60
|
+
connection.write command, options
|
61
|
+
command.response(@write_timeout).tap { |result| raise result if result.is_a? Exception } unless async
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Punchblock
|
2
|
+
class Client
|
3
|
+
class ComponentRegistry
|
4
|
+
def initialize
|
5
|
+
@mutex = Mutex.new
|
6
|
+
@components = Hash.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(component)
|
10
|
+
@mutex.synchronize do
|
11
|
+
@components[component.component_id] = component
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def find_by_id(component_id)
|
16
|
+
@mutex.synchronize do
|
17
|
+
@components[component_id]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -73,11 +73,8 @@ module Punchblock
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def response=(other)
|
76
|
+
@call_id = other.id if other.is_a?(Ref)
|
76
77
|
super
|
77
|
-
if other.is_a?(Blather::Stanza::Iq)
|
78
|
-
ref = other.rayo_node
|
79
|
-
@call_id = ref.id if ref.is_a?(Ref)
|
80
|
-
end
|
81
78
|
end
|
82
79
|
|
83
80
|
def inspect_attributes # :nodoc:
|
data/lib/punchblock/component.rb
CHANGED
@@ -38,18 +38,16 @@ module Punchblock
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def write_action(action)
|
41
|
-
|
41
|
+
client.execute_command action, :call_id => call_id, :component_id => component_id
|
42
|
+
action
|
42
43
|
end
|
43
44
|
|
44
45
|
def response=(other)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
if ref.is_a?(Ref)
|
49
|
-
@component_id = ref.id
|
50
|
-
@connection.record_command_id_for_iq_id @component_id, other.id
|
51
|
-
end
|
46
|
+
if other.is_a?(Ref)
|
47
|
+
@component_id = other.id
|
48
|
+
client.register_component self
|
52
49
|
end
|
50
|
+
super
|
53
51
|
end
|
54
52
|
|
55
53
|
##
|
@@ -1,210 +1,8 @@
|
|
1
|
-
%w{
|
2
|
-
timeout
|
3
|
-
blather/client/dsl
|
4
|
-
punchblock/core_ext/blather/stanza
|
5
|
-
punchblock/core_ext/blather/stanza/presence
|
6
|
-
}.each { |f| require f }
|
7
|
-
|
8
1
|
module Punchblock
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
##
|
13
|
-
# Initialize the required connection attributes
|
14
|
-
#
|
15
|
-
# @param [Hash] options
|
16
|
-
# @option options [String] :username client JID
|
17
|
-
# @option options [String] :password XMPP password
|
18
|
-
# @option options [String] :rayo_domain the domain on which Rayo is running
|
19
|
-
# @option options [Logger] :wire_logger to which all XMPP transactions will be logged
|
20
|
-
# @option options [Boolean, Optional] :auto_reconnect whether or not to auto reconnect
|
21
|
-
# @option options [Numeric, Optional] :write_timeout for which to wait on a command response
|
22
|
-
# @option options [Numeric, nil, Optional] :ping_period interval in seconds on which to ping the server. Nil or false to disable
|
23
|
-
#
|
24
|
-
def initialize(options = {})
|
25
|
-
super
|
26
|
-
|
27
|
-
raise ArgumentError unless (@username = options[:username]) && options[:password]
|
28
|
-
|
29
|
-
setup *[:username, :password, :host, :port, :certs].map { |key| options.delete key }
|
30
|
-
|
31
|
-
@rayo_domain = options[:rayo_domain] || Blather::JID.new(@username).domain
|
32
|
-
|
33
|
-
@callmap = {} # This hash maps call IDs to their XMPP domain.
|
34
|
-
|
35
|
-
@component_id_to_iq_id = {}
|
36
|
-
@iq_id_to_command = {}
|
37
|
-
|
38
|
-
@auto_reconnect = !!options[:auto_reconnect]
|
39
|
-
@reconnect_attempts = 0
|
40
|
-
|
41
|
-
@write_timeout = options[:write_timeout] || 3
|
42
|
-
|
43
|
-
@ping_period = options.has_key?(:ping_period) ? options[:ping_period] : 60
|
44
|
-
|
45
|
-
Blather.logger = options.delete(:wire_logger) if options.has_key?(:wire_logger)
|
46
|
-
end
|
47
|
-
|
48
|
-
##
|
49
|
-
# Write a command to the Rayo server for a particular call
|
50
|
-
#
|
51
|
-
# @param [String] call the call ID on which to act
|
52
|
-
# @param [CommandNode] cmd the command to execute on the call
|
53
|
-
# @param [String, Optional] component_id the component_id on which to execute
|
54
|
-
#
|
55
|
-
# @raise Exception if there is a server-side error
|
56
|
-
#
|
57
|
-
# @return true
|
58
|
-
#
|
59
|
-
def write(call_id, cmd, component_id = nil)
|
60
|
-
async_write call_id, cmd, component_id
|
61
|
-
cmd.response(@write_timeout).tap { |result| raise result if result.is_a? Exception }
|
62
|
-
end
|
63
|
-
|
64
|
-
##
|
65
|
-
# @return [Queue] Pop this queue to determine result of command execution. Will be true or an exception
|
66
|
-
def async_write(call_id, cmd, component_id = nil)
|
67
|
-
iq = prep_command_for_execution call_id, cmd, component_id
|
68
|
-
write_to_stream iq
|
69
|
-
cmd.request!
|
70
|
-
end
|
71
|
-
|
72
|
-
def prep_command_for_execution(call_id, cmd, component_id = nil)
|
73
|
-
cmd.connection = self
|
74
|
-
cmd.call_id = call_id
|
75
|
-
jid = cmd.is_a?(Command::Dial) ? @rayo_domain : "#{call_id}@#{@callmap[call_id]}"
|
76
|
-
jid << "/#{component_id}" if component_id
|
77
|
-
create_iq(jid).tap do |iq|
|
78
|
-
@logger.debug "Sending IQ ID #{iq.id} #{cmd.inspect} to #{jid}" if @logger
|
79
|
-
iq << cmd
|
80
|
-
@iq_id_to_command[iq.id] = cmd
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
##
|
85
|
-
# Fire up the connection
|
86
|
-
#
|
87
|
-
def run
|
88
|
-
register_handlers
|
89
|
-
connect
|
90
|
-
end
|
91
|
-
|
92
|
-
def connect
|
93
|
-
begin
|
94
|
-
EM.run { client.run }
|
95
|
-
rescue Blather::SASLError, Blather::StreamError => e
|
96
|
-
raise ProtocolError.new(e.class.to_s, e.message)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def stop
|
101
|
-
@reconnect_attempts = nil
|
102
|
-
client.close
|
103
|
-
end
|
104
|
-
|
105
|
-
def connected?
|
106
|
-
client.connected?
|
107
|
-
end
|
108
|
-
|
109
|
-
##
|
110
|
-
#
|
111
|
-
# Get the original command issued by command ID
|
112
|
-
#
|
113
|
-
# @param [String] component_id
|
114
|
-
#
|
115
|
-
# @return [RayoNode]
|
116
|
-
#
|
117
|
-
def original_component_from_id(component_id)
|
118
|
-
@iq_id_to_command[@component_id_to_iq_id[component_id]]
|
119
|
-
end
|
120
|
-
|
121
|
-
def record_command_id_for_iq_id(command_id, iq_id)
|
122
|
-
@component_id_to_iq_id[command_id] = iq_id
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
def handle_presence(p)
|
128
|
-
throw :pass unless p.rayo_event? && p.from.domain == @rayo_domain
|
129
|
-
@logger.info "Receiving event for call ID #{p.call_id}" if @logger
|
130
|
-
@callmap[p.call_id] = p.from.domain
|
131
|
-
@logger.debug p.inspect if @logger
|
132
|
-
event = p.event
|
133
|
-
event.connection = self
|
134
|
-
if event.source
|
135
|
-
event.source.add_event event
|
136
|
-
else
|
137
|
-
@event_queue.push event
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def handle_iq_result(iq)
|
142
|
-
# FIXME: Do we need to raise a warning if the domain changes?
|
143
|
-
throw :pass unless command = @iq_id_to_command[iq.id]
|
144
|
-
@callmap[iq.from.node] = iq.from.domain
|
145
|
-
@logger.debug "Command #{iq.id} completed successfully" if @logger
|
146
|
-
command.response = iq
|
147
|
-
end
|
148
|
-
|
149
|
-
def handle_error(iq)
|
150
|
-
e = Blather::StanzaError.import iq
|
151
|
-
|
152
|
-
protocol_error = ProtocolError.new e.name, e.text, iq.call_id, iq.component_id
|
153
|
-
|
154
|
-
throw :pass unless command = @iq_id_to_command[iq.id]
|
155
|
-
|
156
|
-
command.response = protocol_error
|
157
|
-
end
|
158
|
-
|
159
|
-
def register_handlers
|
160
|
-
# Push a message to the queue and the log that we connected
|
161
|
-
when_ready do
|
162
|
-
@event_queue.push connected
|
163
|
-
@logger.info "Connected to XMPP as #{@username}" if @logger
|
164
|
-
@reconnect_attempts = 0
|
165
|
-
@rayo_ping = EM::PeriodicTimer.new(@ping_period) { ping_rayo } if @ping_period
|
166
|
-
end
|
167
|
-
|
168
|
-
disconnected do
|
169
|
-
@rayo_ping.cancel if @rayo_ping
|
170
|
-
if @auto_reconnect && @reconnect_attempts
|
171
|
-
timer = 30 * 2 ** @reconnect_attempts
|
172
|
-
@logger.warn "XMPP disconnected. Tried to reconnect #{@reconnect_attempts} times. Reconnecting in #{timer}s." if @logger
|
173
|
-
sleep timer
|
174
|
-
@logger.info "Trying to reconnect..." if @logger
|
175
|
-
@reconnect_attempts += 1
|
176
|
-
connect
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# Read/handle call control messages. These are mostly just acknowledgement of commands
|
181
|
-
iq :result? do |msg|
|
182
|
-
handle_iq_result msg
|
183
|
-
end
|
184
|
-
|
185
|
-
# Read/handle error IQs
|
186
|
-
iq :error? do |e|
|
187
|
-
handle_error e
|
188
|
-
end
|
189
|
-
|
190
|
-
# Read/handle presence requests. This is how we get events.
|
191
|
-
presence do |msg|
|
192
|
-
handle_presence msg
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
def ping_rayo
|
197
|
-
client.write_with_handler Blather::Stanza::Iq::Ping.new(:set, @rayo_domain) do |response|
|
198
|
-
begin
|
199
|
-
handle_error response if response.is_a? Blather::BlatherError
|
200
|
-
rescue ProtocolError => e
|
201
|
-
raise e unless e.name == :feature_not_implemented
|
202
|
-
end
|
203
|
-
end
|
204
|
-
end
|
2
|
+
module Connection
|
3
|
+
extend ActiveSupport::Autoload
|
205
4
|
|
206
|
-
|
207
|
-
|
208
|
-
end
|
5
|
+
autoload :Connected
|
6
|
+
autoload :XMPP
|
209
7
|
end
|
210
8
|
end
|