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 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
@@ -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)
@@ -46,7 +46,7 @@ module Punchblock
46
46
  def response=(other)
47
47
  if other.is_a?(Ref)
48
48
  @component_id = other.id
49
- client.register_component self
49
+ client.register_component self if client
50
50
  end
51
51
  super
52
52
  end
@@ -7,13 +7,13 @@ module Punchblock
7
7
  attr_accessor :event_handler
8
8
 
9
9
  def initialize(options = {})
10
- options[:logger] = options[:wire_logger]
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 = options.delete(:wire_logger) if options.has_key?(:wire_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
- @logger.debug "Sending IQ ID #{iq.id} #{command.inspect} to #{jid}" if @logger
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
- @logger.info "Receiving event for call ID #{p.call_id}" if @logger
104
+ pb_logger.info "Receiving event for call ID #{p.call_id}"
106
105
  @callmap[p.call_id] = p.from.domain
107
- @logger.debug p.inspect if @logger
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
- @logger.debug "Command #{iq.id} completed successfully" if @logger
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
- @logger.info "Connected to XMPP as #{@username}" if @logger
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
- @logger.warn "XMPP disconnected. Tried to reconnect #{@reconnect_attempts} times. Reconnecting in #{timer}s." if @logger
139
+ pb_logger.warn "XMPP disconnected. Tried to reconnect #{@reconnect_attempts} times. Reconnecting in #{timer}s."
141
140
  sleep timer
142
- @logger.info "Trying to reconnect..." if @logger
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
@@ -30,5 +30,9 @@ module Punchblock
30
30
  [headers].flatten.each { |i| self << Header.new(i) }
31
31
  end
32
32
  end
33
+
34
+ def inspect_attributes # :nodoc:
35
+ [:headers_hash] + super
36
+ end
33
37
  end
34
38
  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
- connection.handle_event Connection::Connected.new
52
+ handle_pb_event Connection::Connected.new
46
53
  @fully_booted_count = 0
47
54
  end
48
- else
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, ami_client
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
- def initialize
8
- @components = {}
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
- class Component
5
- include Celluloid
4
+ module Component
5
+ extend ActiveSupport::Autoload
6
6
 
7
- attr_reader :id
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
@@ -0,0 +1,14 @@
1
+ module Punchblock
2
+ module Translator
3
+ class Asterisk
4
+ module Component
5
+ module Asterisk
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :AGICommand
9
+ autoload :AMIAction
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end