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.
Files changed (94) hide show
  1. data/.document +5 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.markdown +31 -0
  7. data/Rakefile +23 -0
  8. data/assets/ozone/ask-1.0.xsd +56 -0
  9. data/assets/ozone/conference-1.0.xsd +17 -0
  10. data/assets/ozone/ozone-1.0.xsd +127 -0
  11. data/assets/ozone/say-1.0.xsd +24 -0
  12. data/assets/ozone/transfer-1.0.xsd +32 -0
  13. data/bin/punchblock-console +125 -0
  14. data/lib/punchblock/command/accept.rb +30 -0
  15. data/lib/punchblock/command/answer.rb +30 -0
  16. data/lib/punchblock/command/dial.rb +88 -0
  17. data/lib/punchblock/command/hangup.rb +25 -0
  18. data/lib/punchblock/command/join.rb +81 -0
  19. data/lib/punchblock/command/mute.rb +7 -0
  20. data/lib/punchblock/command/redirect.rb +49 -0
  21. data/lib/punchblock/command/reject.rb +61 -0
  22. data/lib/punchblock/command/unjoin.rb +50 -0
  23. data/lib/punchblock/command/unmute.rb +7 -0
  24. data/lib/punchblock/command.rb +16 -0
  25. data/lib/punchblock/command_node.rb +46 -0
  26. data/lib/punchblock/component/input.rb +320 -0
  27. data/lib/punchblock/component/output.rb +449 -0
  28. data/lib/punchblock/component/record.rb +216 -0
  29. data/lib/punchblock/component/tropo/ask.rb +197 -0
  30. data/lib/punchblock/component/tropo/conference.rb +328 -0
  31. data/lib/punchblock/component/tropo/say.rb +113 -0
  32. data/lib/punchblock/component/tropo/transfer.rb +178 -0
  33. data/lib/punchblock/component/tropo.rb +12 -0
  34. data/lib/punchblock/component.rb +73 -0
  35. data/lib/punchblock/connection.rb +209 -0
  36. data/lib/punchblock/core_ext/blather/stanza/presence.rb +11 -0
  37. data/lib/punchblock/core_ext/blather/stanza.rb +26 -0
  38. data/lib/punchblock/dsl.rb +46 -0
  39. data/lib/punchblock/event/answered.rb +7 -0
  40. data/lib/punchblock/event/complete.rb +65 -0
  41. data/lib/punchblock/event/dtmf.rb +19 -0
  42. data/lib/punchblock/event/end.rb +15 -0
  43. data/lib/punchblock/event/info.rb +15 -0
  44. data/lib/punchblock/event/joined.rb +50 -0
  45. data/lib/punchblock/event/offer.rb +29 -0
  46. data/lib/punchblock/event/ringing.rb +7 -0
  47. data/lib/punchblock/event/unjoined.rb +50 -0
  48. data/lib/punchblock/event.rb +16 -0
  49. data/lib/punchblock/generic_connection.rb +18 -0
  50. data/lib/punchblock/has_headers.rb +34 -0
  51. data/lib/punchblock/header.rb +47 -0
  52. data/lib/punchblock/media_container.rb +39 -0
  53. data/lib/punchblock/media_node.rb +17 -0
  54. data/lib/punchblock/protocol_error.rb +16 -0
  55. data/lib/punchblock/rayo_node.rb +88 -0
  56. data/lib/punchblock/ref.rb +26 -0
  57. data/lib/punchblock/version.rb +3 -0
  58. data/lib/punchblock.rb +42 -0
  59. data/log/.gitkeep +0 -0
  60. data/punchblock.gemspec +42 -0
  61. data/spec/punchblock/command/accept_spec.rb +13 -0
  62. data/spec/punchblock/command/answer_spec.rb +13 -0
  63. data/spec/punchblock/command/dial_spec.rb +54 -0
  64. data/spec/punchblock/command/hangup_spec.rb +13 -0
  65. data/spec/punchblock/command/join_spec.rb +21 -0
  66. data/spec/punchblock/command/mute_spec.rb +11 -0
  67. data/spec/punchblock/command/redirect_spec.rb +19 -0
  68. data/spec/punchblock/command/reject_spec.rb +43 -0
  69. data/spec/punchblock/command/unjoin_spec.rb +19 -0
  70. data/spec/punchblock/command/unmute_spec.rb +11 -0
  71. data/spec/punchblock/command_node_spec.rb +80 -0
  72. data/spec/punchblock/component/input_spec.rb +188 -0
  73. data/spec/punchblock/component/output_spec.rb +531 -0
  74. data/spec/punchblock/component/record_spec.rb +235 -0
  75. data/spec/punchblock/component/tropo/ask_spec.rb +183 -0
  76. data/spec/punchblock/component/tropo/conference_spec.rb +360 -0
  77. data/spec/punchblock/component/tropo/say_spec.rb +171 -0
  78. data/spec/punchblock/component/tropo/transfer_spec.rb +153 -0
  79. data/spec/punchblock/component_spec.rb +126 -0
  80. data/spec/punchblock/connection_spec.rb +194 -0
  81. data/spec/punchblock/event/answered_spec.rb +23 -0
  82. data/spec/punchblock/event/complete_spec.rb +80 -0
  83. data/spec/punchblock/event/dtmf_spec.rb +24 -0
  84. data/spec/punchblock/event/end_spec.rb +30 -0
  85. data/spec/punchblock/event/info_spec.rb +30 -0
  86. data/spec/punchblock/event/joined_spec.rb +32 -0
  87. data/spec/punchblock/event/offer_spec.rb +35 -0
  88. data/spec/punchblock/event/ringing_spec.rb +23 -0
  89. data/spec/punchblock/event/unjoined_spec.rb +32 -0
  90. data/spec/punchblock/header_spec.rb +44 -0
  91. data/spec/punchblock/protocol_error_spec.rb +9 -0
  92. data/spec/punchblock/ref_spec.rb +21 -0
  93. data/spec/spec_helper.rb +43 -0
  94. 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,12 @@
1
+ module Punchblock
2
+ module Component
3
+ module Tropo
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Ask
7
+ autoload :Conference
8
+ autoload :Say
9
+ autoload :Transfer
10
+ end
11
+ end # Command
12
+ 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,11 @@
1
+ module Blather
2
+ class Stanza
3
+ class Presence
4
+ alias :event :rayo_node
5
+
6
+ def rayo_event?
7
+ event.is_a? Punchblock::Event
8
+ end
9
+ end
10
+ end
11
+ 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,7 @@
1
+ module Punchblock
2
+ class Event
3
+ class Answered < Event
4
+ register :answered, :core
5
+ end # End
6
+ end # Event
7
+ end # Punchblock
@@ -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