punchblock 0.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.
- data/.document +5 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +31 -0
- data/Rakefile +23 -0
- data/assets/ozone/ask-1.0.xsd +56 -0
- data/assets/ozone/conference-1.0.xsd +17 -0
- data/assets/ozone/ozone-1.0.xsd +127 -0
- data/assets/ozone/say-1.0.xsd +24 -0
- data/assets/ozone/transfer-1.0.xsd +32 -0
- data/bin/punchblock-console +125 -0
- data/lib/punchblock/command/accept.rb +30 -0
- data/lib/punchblock/command/answer.rb +30 -0
- data/lib/punchblock/command/dial.rb +88 -0
- data/lib/punchblock/command/hangup.rb +25 -0
- data/lib/punchblock/command/join.rb +81 -0
- data/lib/punchblock/command/mute.rb +7 -0
- data/lib/punchblock/command/redirect.rb +49 -0
- data/lib/punchblock/command/reject.rb +61 -0
- data/lib/punchblock/command/unjoin.rb +50 -0
- data/lib/punchblock/command/unmute.rb +7 -0
- data/lib/punchblock/command.rb +16 -0
- data/lib/punchblock/command_node.rb +46 -0
- data/lib/punchblock/component/input.rb +320 -0
- data/lib/punchblock/component/output.rb +449 -0
- data/lib/punchblock/component/record.rb +216 -0
- data/lib/punchblock/component/tropo/ask.rb +197 -0
- data/lib/punchblock/component/tropo/conference.rb +328 -0
- data/lib/punchblock/component/tropo/say.rb +113 -0
- data/lib/punchblock/component/tropo/transfer.rb +178 -0
- data/lib/punchblock/component/tropo.rb +12 -0
- data/lib/punchblock/component.rb +73 -0
- data/lib/punchblock/connection.rb +209 -0
- data/lib/punchblock/core_ext/blather/stanza/presence.rb +11 -0
- data/lib/punchblock/core_ext/blather/stanza.rb +26 -0
- data/lib/punchblock/dsl.rb +46 -0
- data/lib/punchblock/event/answered.rb +7 -0
- data/lib/punchblock/event/complete.rb +65 -0
- data/lib/punchblock/event/dtmf.rb +19 -0
- data/lib/punchblock/event/end.rb +15 -0
- data/lib/punchblock/event/info.rb +15 -0
- data/lib/punchblock/event/joined.rb +50 -0
- data/lib/punchblock/event/offer.rb +29 -0
- data/lib/punchblock/event/ringing.rb +7 -0
- data/lib/punchblock/event/unjoined.rb +50 -0
- data/lib/punchblock/event.rb +16 -0
- data/lib/punchblock/generic_connection.rb +18 -0
- data/lib/punchblock/has_headers.rb +34 -0
- data/lib/punchblock/header.rb +47 -0
- data/lib/punchblock/media_container.rb +39 -0
- data/lib/punchblock/media_node.rb +17 -0
- data/lib/punchblock/protocol_error.rb +16 -0
- data/lib/punchblock/rayo_node.rb +88 -0
- data/lib/punchblock/ref.rb +26 -0
- data/lib/punchblock/version.rb +3 -0
- data/lib/punchblock.rb +42 -0
- data/log/.gitkeep +0 -0
- data/punchblock.gemspec +42 -0
- data/spec/punchblock/command/accept_spec.rb +13 -0
- data/spec/punchblock/command/answer_spec.rb +13 -0
- data/spec/punchblock/command/dial_spec.rb +54 -0
- data/spec/punchblock/command/hangup_spec.rb +13 -0
- data/spec/punchblock/command/join_spec.rb +21 -0
- data/spec/punchblock/command/mute_spec.rb +11 -0
- data/spec/punchblock/command/redirect_spec.rb +19 -0
- data/spec/punchblock/command/reject_spec.rb +43 -0
- data/spec/punchblock/command/unjoin_spec.rb +19 -0
- data/spec/punchblock/command/unmute_spec.rb +11 -0
- data/spec/punchblock/command_node_spec.rb +80 -0
- data/spec/punchblock/component/input_spec.rb +188 -0
- data/spec/punchblock/component/output_spec.rb +531 -0
- data/spec/punchblock/component/record_spec.rb +235 -0
- data/spec/punchblock/component/tropo/ask_spec.rb +183 -0
- data/spec/punchblock/component/tropo/conference_spec.rb +360 -0
- data/spec/punchblock/component/tropo/say_spec.rb +171 -0
- data/spec/punchblock/component/tropo/transfer_spec.rb +153 -0
- data/spec/punchblock/component_spec.rb +126 -0
- data/spec/punchblock/connection_spec.rb +194 -0
- data/spec/punchblock/event/answered_spec.rb +23 -0
- data/spec/punchblock/event/complete_spec.rb +80 -0
- data/spec/punchblock/event/dtmf_spec.rb +24 -0
- data/spec/punchblock/event/end_spec.rb +30 -0
- data/spec/punchblock/event/info_spec.rb +30 -0
- data/spec/punchblock/event/joined_spec.rb +32 -0
- data/spec/punchblock/event/offer_spec.rb +35 -0
- data/spec/punchblock/event/ringing_spec.rb +23 -0
- data/spec/punchblock/event/unjoined_spec.rb +32 -0
- data/spec/punchblock/header_spec.rb +44 -0
- data/spec/punchblock/protocol_error_spec.rb +9 -0
- data/spec/punchblock/ref_spec.rb +21 -0
- data/spec/spec_helper.rb +43 -0
- metadata +353 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module Punchblock
|
|
2
|
+
module Component
|
|
3
|
+
module Tropo
|
|
4
|
+
class Transfer < ComponentNode
|
|
5
|
+
register :transfer, :transfer
|
|
6
|
+
|
|
7
|
+
include HasHeaders
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# Creates an Rayo transfer command
|
|
11
|
+
#
|
|
12
|
+
# @param [Hash] options for transferring a call
|
|
13
|
+
# @option options [String, Array[String]] :to The destination(s) for the call transfer (ie - tel:+14155551212 or sip:you@sip.tropo.com). Can be an array to hunt.
|
|
14
|
+
# @option options [String] :from The caller ID for the call transfer (ie - tel:+14155551212 or sip:you@sip.tropo.com)
|
|
15
|
+
# @option options [String, Optional] :terminator The string key press required to abort the transfer.
|
|
16
|
+
# @option options [Integer, Optional] :timeout How long to wait - in seconds - for an answer, busy signal, or other event to occur.
|
|
17
|
+
# @option options [Boolean, Optional] :answer_on_media If set to true, the call will be considered "answered" and audio will begin playing as soon as media is received from the far end (ringing / busy signal / etc)
|
|
18
|
+
# @option options [Symbol, Optional] :media Rules for handling media. Can be :direct, where parties negotiate media directly, or :bridge where the media server will bridge audio, allowing media features like recording and ASR.
|
|
19
|
+
# @option options [Ring, Hash, Optional] :ring to play to the caller untill connected
|
|
20
|
+
#
|
|
21
|
+
# @return [Message::Transfer] an Rayo "transfer" message
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# Transfer.new(:to => 'sip:you@yourdomain.com', :from => 'sip:myapp@mydomain.com', :terminator => '#').to_xml
|
|
25
|
+
#
|
|
26
|
+
# returns:
|
|
27
|
+
# <transfer xmlns="urn:xmpp:tropo:transfer:1" from="sip:myapp@mydomain.com" terminator="#">
|
|
28
|
+
# <to>sip:you@yourdomain.com</to>
|
|
29
|
+
# </transfer>
|
|
30
|
+
#
|
|
31
|
+
def self.new(options = {})
|
|
32
|
+
super().tap do |new_node|
|
|
33
|
+
options.each_pair { |k,v| new_node.send :"#{k}=", v }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# @return [Array[String]] The destination(s) for the call transfer
|
|
39
|
+
#
|
|
40
|
+
def to
|
|
41
|
+
find('ns:to', :ns => self.class.registered_ns).map &:text
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# @param [String, Array[String]] :to The destination(s) for the call transfer (ie - tel:+14155551212 or sip:you@sip.tropo.com). Can be an array to hunt.
|
|
46
|
+
#
|
|
47
|
+
def to=(transfer_to)
|
|
48
|
+
find('//ns:to', :ns => self.class.registered_ns).each &:remove
|
|
49
|
+
if transfer_to
|
|
50
|
+
[transfer_to].flatten.each do |i|
|
|
51
|
+
to = RayoNode.new :to
|
|
52
|
+
to << i
|
|
53
|
+
self << to
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# @return [String] The caller ID for the call transfer
|
|
60
|
+
#
|
|
61
|
+
def from
|
|
62
|
+
read_attr :from
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# @param [String, Array[String]] :to The destination(s) for the call transfer (ie - tel:+14155551212 or sip:you@sip.tropo.com). Can be an array to hunt.
|
|
67
|
+
#
|
|
68
|
+
def from=(transfer_from)
|
|
69
|
+
write_attr :from, transfer_from
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# @return [String] The string key press required to abort the transfer.
|
|
74
|
+
#
|
|
75
|
+
def terminator
|
|
76
|
+
read_attr :terminator
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# @param [String] terminator The string key press required to abort the transfer.
|
|
81
|
+
#
|
|
82
|
+
def terminator=(terminator)
|
|
83
|
+
write_attr :terminator, terminator
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# @return [Integer] How long to wait - in seconds - for an answer, busy signal, or other event to occur.
|
|
88
|
+
#
|
|
89
|
+
def timeout
|
|
90
|
+
read_attr :timeout, :to_i
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# @param [Integer] timeout How long to wait - in seconds - for an answer, busy signal, or other event to occur.
|
|
95
|
+
#
|
|
96
|
+
def timeout=(timeout)
|
|
97
|
+
write_attr :timeout, timeout
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# @return [Boolean] If true, the call will be considered "answered" and audio will begin playing as soon as media is received from the far end (ringing / busy signal / etc)
|
|
102
|
+
#
|
|
103
|
+
def answer_on_media
|
|
104
|
+
read_attr('answer-on-media') == 'true'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# @param [Boolean] aom If set to true, the call will be considered "answered" and audio will begin playing as soon as media is received from the far end (ringing / busy signal / etc)
|
|
109
|
+
#
|
|
110
|
+
def answer_on_media=(aom)
|
|
111
|
+
write_attr 'answer-on-media', aom.to_s
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# @return [Symbol] Rules for handling media. Can be :direct, where parties negotiate media directly, or :bridge where the media server will bridge audio, allowing media features like recording and ASR.
|
|
116
|
+
#
|
|
117
|
+
def media
|
|
118
|
+
read_attr 'media', :to_sym
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# @param [Symbol] media Rules for handling media. Can be :direct, where parties negotiate media directly, or :bridge where the media server will bridge audio, allowing media features like recording and ASR.
|
|
123
|
+
#
|
|
124
|
+
def media=(media)
|
|
125
|
+
write_attr 'media', media
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# @return [Ring] the ringer to play to the caller while transferring
|
|
130
|
+
#
|
|
131
|
+
def ring
|
|
132
|
+
node = find_first '//ns:ring', :ns => self.registered_ns
|
|
133
|
+
Ring.new node if node
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# @param [Hash] ring
|
|
138
|
+
# @option ring [String] :text Text to speak to the caller as an announcement
|
|
139
|
+
# @option ring [String] :url URL to play to the caller as an announcement
|
|
140
|
+
#
|
|
141
|
+
def ring=(ring)
|
|
142
|
+
ring = Ring.new(ring) unless ring.is_a?(Ring)
|
|
143
|
+
self << ring
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class Ring < Say
|
|
147
|
+
register :ring, :transfer
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def inspect_attributes # :nodoc:
|
|
151
|
+
[:to, :from, :terminator, :timeout, :answer_on_media] + super
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
class Complete
|
|
155
|
+
class Success < Event::Complete::Reason
|
|
156
|
+
register :success, :transfer_complete
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class Timeout < Event::Complete::Reason
|
|
160
|
+
register :timeout, :transfer_complete
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class Terminator < Event::Complete::Reason
|
|
164
|
+
register :terminator, :transfer_complete
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class Busy < Event::Complete::Reason
|
|
168
|
+
register :busy, :transfer_complete
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
class Reject < Event::Complete::Reason
|
|
172
|
+
register :reject, :transfer_complete
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end # Transfer
|
|
176
|
+
end # Tropo
|
|
177
|
+
end # Command
|
|
178
|
+
end # Punchblock
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Punchblock
|
|
2
|
+
module Component
|
|
3
|
+
extend ActiveSupport::Autoload
|
|
4
|
+
|
|
5
|
+
autoload :Input
|
|
6
|
+
autoload :Output
|
|
7
|
+
autoload :Record
|
|
8
|
+
autoload :Tropo
|
|
9
|
+
|
|
10
|
+
InvalidActionError = Class.new StandardError
|
|
11
|
+
|
|
12
|
+
class ComponentNode < CommandNode
|
|
13
|
+
attr_accessor :event_queue, :complete_event, :event_callback
|
|
14
|
+
|
|
15
|
+
def initialize(*args)
|
|
16
|
+
super
|
|
17
|
+
@event_queue = Queue.new
|
|
18
|
+
@complete_event = FutureResource.new
|
|
19
|
+
@event_callback = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_event(event)
|
|
23
|
+
event.original_component = self
|
|
24
|
+
transition_state! event
|
|
25
|
+
complete_event.resource = event if event.is_a? Event::Complete
|
|
26
|
+
if event_callback.respond_to?(:call)
|
|
27
|
+
add_event_to_queue = event_callback.call event
|
|
28
|
+
end
|
|
29
|
+
@event_queue << event unless add_event_to_queue
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def transition_state!(event)
|
|
33
|
+
complete! if event.is_a? Event::Complete
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def write_action(action)
|
|
37
|
+
connection.write call_id, action, component_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def response=(other)
|
|
41
|
+
super
|
|
42
|
+
if other.is_a?(Blather::Stanza::Iq)
|
|
43
|
+
ref = other.rayo_node
|
|
44
|
+
if ref.is_a?(Ref)
|
|
45
|
+
@component_id = ref.id
|
|
46
|
+
@connection.record_command_id_for_iq_id @component_id, other.id
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Create an Rayo stop message
|
|
53
|
+
#
|
|
54
|
+
# @return [Stop] an Rayo stop message
|
|
55
|
+
#
|
|
56
|
+
def stop_action
|
|
57
|
+
Stop.new :component_id => component_id, :call_id => call_id
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# Sends an Rayo stop message for the current component
|
|
62
|
+
#
|
|
63
|
+
def stop!(options = {})
|
|
64
|
+
raise InvalidActionError, "Cannot stop a #{self.class.name.split("::").last} that is not executing" unless executing?
|
|
65
|
+
stop_action.tap { |action| write_action action }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class Stop < CommandNode # :nodoc:
|
|
70
|
+
register :stop, :core
|
|
71
|
+
end
|
|
72
|
+
end # Component
|
|
73
|
+
end # Punchblock
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
module Punchblock
|
|
9
|
+
class Connection < GenericConnection
|
|
10
|
+
include Blather::DSL
|
|
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
|
+
raise ArgumentError unless @username = options.delete(:username)
|
|
27
|
+
raise ArgumentError unless options.has_key? :password
|
|
28
|
+
@rayo_domain = options[:rayo_domain] || Blather::JID.new(@username).domain
|
|
29
|
+
|
|
30
|
+
setup @username, options.delete(:password)
|
|
31
|
+
|
|
32
|
+
@callmap = {} # This hash maps call IDs to their XMPP domain.
|
|
33
|
+
|
|
34
|
+
@component_id_to_iq_id = {}
|
|
35
|
+
@iq_id_to_command = {}
|
|
36
|
+
|
|
37
|
+
@auto_reconnect = !!options[:auto_reconnect]
|
|
38
|
+
@reconnect_attempts = 0
|
|
39
|
+
|
|
40
|
+
@write_timeout = options[:write_timeout] || 3
|
|
41
|
+
|
|
42
|
+
@ping_period = options.has_key?(:ping_period) ? options[:ping_period] : 60
|
|
43
|
+
|
|
44
|
+
Blather.logger = options.delete(:wire_logger) if options.has_key?(:wire_logger)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# Write a command to the Rayo server for a particular call
|
|
49
|
+
#
|
|
50
|
+
# @param [String] call the call ID on which to act
|
|
51
|
+
# @param [CommandNode] cmd the command to execute on the call
|
|
52
|
+
# @param [String, Optional] component_id the component_id on which to execute
|
|
53
|
+
#
|
|
54
|
+
# @raise Exception if there is a server-side error
|
|
55
|
+
#
|
|
56
|
+
# @return true
|
|
57
|
+
#
|
|
58
|
+
def write(call_id, cmd, component_id = nil)
|
|
59
|
+
async_write call_id, cmd, component_id
|
|
60
|
+
cmd.response(@write_timeout).tap { |result| raise result if result.is_a? Exception }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @return [Queue] Pop this queue to determine result of command execution. Will be true or an exception
|
|
65
|
+
def async_write(call_id, cmd, component_id = nil)
|
|
66
|
+
iq = prep_command_for_execution call_id, cmd, component_id
|
|
67
|
+
write_to_stream iq
|
|
68
|
+
cmd.request!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def prep_command_for_execution(call_id, cmd, component_id = nil)
|
|
72
|
+
cmd.connection = self
|
|
73
|
+
cmd.call_id = call_id
|
|
74
|
+
jid = cmd.is_a?(Command::Dial) ? @rayo_domain : "#{call_id}@#{@callmap[call_id]}"
|
|
75
|
+
jid << "/#{component_id}" if component_id
|
|
76
|
+
create_iq(jid).tap do |iq|
|
|
77
|
+
@logger.debug "Sending IQ ID #{iq.id} #{cmd.inspect} to #{jid}" if @logger
|
|
78
|
+
iq << cmd
|
|
79
|
+
@iq_id_to_command[iq.id] = cmd
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Fire up the connection
|
|
85
|
+
#
|
|
86
|
+
def run
|
|
87
|
+
register_handlers
|
|
88
|
+
connect
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def connect
|
|
92
|
+
begin
|
|
93
|
+
EM.run { client.run }
|
|
94
|
+
rescue Blather::SASLError, Blather::StreamError => e
|
|
95
|
+
raise ProtocolError.new(e.class.to_s, e.message)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def stop
|
|
100
|
+
@reconnect_attempts = nil
|
|
101
|
+
client.close
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def connected?
|
|
105
|
+
client.connected?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
#
|
|
110
|
+
# Get the original command issued by command ID
|
|
111
|
+
#
|
|
112
|
+
# @param [String] component_id
|
|
113
|
+
#
|
|
114
|
+
# @return [RayoNode]
|
|
115
|
+
#
|
|
116
|
+
def original_component_from_id(component_id)
|
|
117
|
+
@iq_id_to_command[@component_id_to_iq_id[component_id]]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def record_command_id_for_iq_id(command_id, iq_id)
|
|
121
|
+
@component_id_to_iq_id[command_id] = iq_id
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def handle_presence(p)
|
|
127
|
+
throw :pass unless p.rayo_event?
|
|
128
|
+
@logger.info "Receiving event for call ID #{p.call_id}" if @logger
|
|
129
|
+
@callmap[p.call_id] = p.from.domain
|
|
130
|
+
@logger.debug p.inspect if @logger
|
|
131
|
+
event = p.event
|
|
132
|
+
event.connection = self
|
|
133
|
+
if event.source
|
|
134
|
+
event.source.add_event event
|
|
135
|
+
else
|
|
136
|
+
@event_queue.push event
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def handle_iq_result(iq)
|
|
141
|
+
# FIXME: Do we need to raise a warning if the domain changes?
|
|
142
|
+
throw :pass unless command = @iq_id_to_command[iq.id]
|
|
143
|
+
@callmap[iq.from.node] = iq.from.domain
|
|
144
|
+
@logger.debug "Command #{iq.id} completed successfully" if @logger
|
|
145
|
+
command.response = iq
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def handle_error(iq)
|
|
149
|
+
e = Blather::StanzaError.import iq
|
|
150
|
+
|
|
151
|
+
protocol_error = ProtocolError.new e.name, e.text, iq.call_id, iq.component_id
|
|
152
|
+
|
|
153
|
+
throw :pass unless command = @iq_id_to_command[iq.id]
|
|
154
|
+
|
|
155
|
+
command.response = protocol_error
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def register_handlers
|
|
159
|
+
# Push a message to the queue and the log that we connected
|
|
160
|
+
when_ready do
|
|
161
|
+
@event_queue.push connected
|
|
162
|
+
@logger.info "Connected to XMPP as #{@username}" if @logger
|
|
163
|
+
@reconnect_attempts = 0
|
|
164
|
+
@rayo_ping = EM::PeriodicTimer.new(@ping_period) { ping_rayo } if @ping_period
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
disconnected do
|
|
168
|
+
@rayo_ping.cancel if @rayo_ping
|
|
169
|
+
if @auto_reconnect && @reconnect_attempts
|
|
170
|
+
timer = 30 * 2 ** @reconnect_attempts
|
|
171
|
+
@logger.warn "XMPP disconnected. Tried to reconnect #{@reconnect_attempts} times. Reconnecting in #{timer}s." if @logger
|
|
172
|
+
sleep timer
|
|
173
|
+
@logger.info "Trying to reconnect..." if @logger
|
|
174
|
+
@reconnect_attempts += 1
|
|
175
|
+
connect
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Read/handle call control messages. These are mostly just acknowledgement of commands
|
|
180
|
+
iq :result? do |msg|
|
|
181
|
+
handle_iq_result msg
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Read/handle error IQs
|
|
185
|
+
iq :error? do |e|
|
|
186
|
+
handle_error e
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Read/handle presence requests. This is how we get events.
|
|
190
|
+
presence do |msg|
|
|
191
|
+
handle_presence msg
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def ping_rayo
|
|
196
|
+
client.write_with_handler Blather::Stanza::Iq::Ping.new(:set, @rayo_domain) do |response|
|
|
197
|
+
begin
|
|
198
|
+
handle_error response if response.is_a? Blather::BlatherError
|
|
199
|
+
rescue ProtocolError => e
|
|
200
|
+
raise e unless e.name == :feature_not_implemented
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def create_iq(jid = nil)
|
|
206
|
+
Blather::Stanza::Iq.new :set, jid || @call_id
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Blather
|
|
2
|
+
class Stanza
|
|
3
|
+
##
|
|
4
|
+
# @return [Punchblock::RayoNode] a child of RayoNode
|
|
5
|
+
# representing the Rayo command/event contained within the stanza
|
|
6
|
+
#
|
|
7
|
+
def rayo_node
|
|
8
|
+
first_child = children.first
|
|
9
|
+
Punchblock::RayoNode.import first_child, call_id, component_id if first_child
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @return [String] the call ID this stanza applies to
|
|
14
|
+
#
|
|
15
|
+
def call_id
|
|
16
|
+
from.node
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @return [String] the command ID this stanza applies to
|
|
21
|
+
#
|
|
22
|
+
def component_id
|
|
23
|
+
from.resource
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
##
|
|
2
|
+
# DO NOT USE THIS API!
|
|
3
|
+
# This file is temporary, to help make testing Punchblock easier.
|
|
4
|
+
# THIS IS IMPERMANENT AND WILL DISAPPEAR
|
|
5
|
+
module Punchblock
|
|
6
|
+
class DSL
|
|
7
|
+
def initialize(protocol, call, queue) # :nodoc:
|
|
8
|
+
@protocol, @call, @queue = protocol, call, queue
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def accept # :nodoc:
|
|
12
|
+
write @protocol.class::Command::Accept.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def answer # :nodoc:
|
|
16
|
+
write @protocol.class::Command::Answer.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def hangup # :nodoc:
|
|
20
|
+
write @protocol.class::Command::Hangup.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reject(reason = nil) # :nodoc:
|
|
24
|
+
write @protocol.class::Command::Reject.new(:reason => reason)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def redirect(dest) # :nodoc:
|
|
28
|
+
write @protocol.class::Command::Redirect.new(:to => dest)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def record(options = {})
|
|
32
|
+
write @protocol.class::Component::Record.new(options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def say(string, type = :text) # :nodoc:
|
|
36
|
+
write @protocol.class::Component::Tropo::Say.new(type => string)
|
|
37
|
+
puts "Waiting on the queue..."
|
|
38
|
+
response = @queue.pop
|
|
39
|
+
# TODO: Error handling
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def write(msg) # :nodoc:
|
|
43
|
+
@protocol.write @call, msg
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Punchblock
|
|
2
|
+
class Event
|
|
3
|
+
class Complete < Event
|
|
4
|
+
# TODO: Validate response and return response type.
|
|
5
|
+
# -----
|
|
6
|
+
# <complete xmlns="urn:xmpp:rayo:ext:1"/>
|
|
7
|
+
|
|
8
|
+
register :complete, :ext
|
|
9
|
+
|
|
10
|
+
def reason
|
|
11
|
+
element = find_first('*')
|
|
12
|
+
if element
|
|
13
|
+
RayoNode.import(element).tap do |reason|
|
|
14
|
+
reason.call_id = call_id
|
|
15
|
+
reason.component_id = component_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def recording
|
|
21
|
+
element = find_first('//ns:recording', :ns => RAYO_NAMESPACES[:record_complete])
|
|
22
|
+
if element
|
|
23
|
+
RayoNode.import(element).tap do |recording|
|
|
24
|
+
recording.call_id = call_id
|
|
25
|
+
recording.component_id = component_id
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def inspect_attributes # :nodoc:
|
|
31
|
+
[:reason, :recording] + super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Reason < RayoNode
|
|
35
|
+
def name
|
|
36
|
+
super.to_sym
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inspect_attributes # :nodoc:
|
|
40
|
+
[:name] + super
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Stop < Reason
|
|
45
|
+
register :stop, :ext_complete
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Hangup < Reason
|
|
49
|
+
register :hangup, :ext_complete
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Error < Reason
|
|
53
|
+
register :error, :ext_complete
|
|
54
|
+
|
|
55
|
+
def details
|
|
56
|
+
text.strip
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def inspect_attributes # :nodoc:
|
|
60
|
+
[:details] + super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end # Complete
|
|
64
|
+
end
|
|
65
|
+
end # Punchblock
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Punchblock
|
|
2
|
+
class Event
|
|
3
|
+
class DTMF < Event
|
|
4
|
+
register :dtmf, :core
|
|
5
|
+
|
|
6
|
+
def signal
|
|
7
|
+
read_attr :signal
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def signal=(other)
|
|
11
|
+
write_attr :signal, other
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def inspect_attributes # :nodoc:
|
|
15
|
+
[:signal] + super
|
|
16
|
+
end
|
|
17
|
+
end # End
|
|
18
|
+
end
|
|
19
|
+
end # Punchblock
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Punchblock
|
|
2
|
+
class Event
|
|
3
|
+
class End < Event
|
|
4
|
+
register :end, :core
|
|
5
|
+
|
|
6
|
+
def reason
|
|
7
|
+
children.select { |c| c.is_a? Nokogiri::XML::Element }.first.name.to_sym
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def inspect_attributes # :nodoc:
|
|
11
|
+
[:reason] + super
|
|
12
|
+
end
|
|
13
|
+
end # End
|
|
14
|
+
end
|
|
15
|
+
end # Punchblock
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Punchblock
|
|
2
|
+
class Event
|
|
3
|
+
class Info < Event
|
|
4
|
+
register :info, :core
|
|
5
|
+
|
|
6
|
+
def event_name
|
|
7
|
+
children.select { |c| c.is_a? Nokogiri::XML::Element }.first.name.to_sym
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def inspect_attributes # :nodoc:
|
|
11
|
+
[:event_name] + super
|
|
12
|
+
end
|
|
13
|
+
end # Info
|
|
14
|
+
end
|
|
15
|
+
end # Punchblock
|