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 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