punchblock 0.5.1 → 0.6.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 (64) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/LICENSE.txt +3 -1
  3. data/bin/punchblock-console +19 -1
  4. data/lib/punchblock.rb +11 -4
  5. data/lib/punchblock/command/reject.rb +6 -2
  6. data/lib/punchblock/component.rb +1 -0
  7. data/lib/punchblock/component/asterisk.rb +10 -0
  8. data/lib/punchblock/component/asterisk/agi.rb +11 -0
  9. data/lib/punchblock/component/asterisk/agi/command.rb +157 -0
  10. data/lib/punchblock/component/asterisk/ami.rb +12 -0
  11. data/lib/punchblock/component/asterisk/ami/action.rb +144 -0
  12. data/lib/punchblock/component/input.rb +2 -2
  13. data/lib/punchblock/connection.rb +1 -0
  14. data/lib/punchblock/connection/asterisk.rb +31 -0
  15. data/lib/punchblock/core_ext/celluloid.rb +11 -0
  16. data/lib/punchblock/event.rb +1 -1
  17. data/lib/punchblock/event/asterisk.rb +9 -0
  18. data/lib/punchblock/event/asterisk/ami.rb +11 -0
  19. data/lib/punchblock/event/asterisk/ami/event.rb +66 -0
  20. data/lib/punchblock/event/complete.rb +20 -0
  21. data/lib/punchblock/event/dtmf.rb +19 -0
  22. data/lib/punchblock/event/end.rb +23 -0
  23. data/lib/punchblock/event/offer.rb +23 -0
  24. data/lib/punchblock/header.rb +4 -44
  25. data/lib/punchblock/key_value_pair_node.rb +50 -0
  26. data/lib/punchblock/rayo_node.rb +1 -1
  27. data/lib/punchblock/ref.rb +6 -0
  28. data/lib/punchblock/translator.rb +7 -0
  29. data/lib/punchblock/translator/asterisk.rb +74 -0
  30. data/lib/punchblock/translator/asterisk/ami_action.rb +86 -0
  31. data/lib/punchblock/translator/asterisk/call.rb +25 -0
  32. data/lib/punchblock/translator/asterisk/component.rb +11 -0
  33. data/lib/punchblock/version.rb +1 -1
  34. data/punchblock.gemspec +3 -1
  35. data/spec/punchblock/command/accept_spec.rb +8 -0
  36. data/spec/punchblock/command/answer_spec.rb +8 -0
  37. data/spec/punchblock/command/dial_spec.rb +22 -2
  38. data/spec/punchblock/command/hangup_spec.rb +8 -0
  39. data/spec/punchblock/command/join_spec.rb +21 -0
  40. data/spec/punchblock/command/mute_spec.rb +8 -0
  41. data/spec/punchblock/command/redirect_spec.rb +21 -0
  42. data/spec/punchblock/command/reject_spec.rb +19 -8
  43. data/spec/punchblock/command/unjoin_spec.rb +17 -0
  44. data/spec/punchblock/command/unmute_spec.rb +8 -0
  45. data/spec/punchblock/component/asterisk/agi/command_spec.rb +102 -0
  46. data/spec/punchblock/component/asterisk/ami/action_spec.rb +118 -0
  47. data/spec/punchblock/component/input_spec.rb +40 -0
  48. data/spec/punchblock/component/output_spec.rb +28 -0
  49. data/spec/punchblock/component/record_spec.rb +27 -0
  50. data/spec/punchblock/connection/asterisk_spec.rb +69 -0
  51. data/spec/punchblock/event/asterisk/ami/event_spec.rb +60 -0
  52. data/spec/punchblock/event/complete_spec.rb +8 -0
  53. data/spec/punchblock/event/dtmf_spec.rb +8 -0
  54. data/spec/punchblock/event/end_spec.rb +8 -0
  55. data/spec/punchblock/event/offer_spec.rb +15 -2
  56. data/spec/punchblock/ref_spec.rb +6 -0
  57. data/spec/punchblock/translator/asterisk/ami_action_spec.rb +149 -0
  58. data/spec/punchblock/translator/asterisk/call_spec.rb +18 -0
  59. data/spec/punchblock/translator/asterisk/component_spec.rb +11 -0
  60. data/spec/punchblock/translator/asterisk_spec.rb +150 -0
  61. data/spec/spec_helper.rb +42 -0
  62. metadata +92 -42
  63. data/lib/punchblock/event/info.rb +0 -15
  64. data/spec/punchblock/event/info_spec.rb +0 -30
@@ -75,6 +75,14 @@ module Punchblock
75
75
 
76
76
  its(:name) { should == :error }
77
77
  its(:details) { should == "Something really bad happened" }
78
+
79
+ describe "when setting options in initializer" do
80
+ subject do
81
+ Complete::Error.new :details => 'Ooops'
82
+ end
83
+
84
+ its(:details) { should == 'Ooops' }
85
+ end
78
86
  end
79
87
  end
80
88
  end # Punchblock
@@ -19,6 +19,14 @@ module Punchblock
19
19
  its(:signal) { should == '#' }
20
20
  its(:xmlns) { should == 'urn:xmpp:rayo:1' }
21
21
  end
22
+
23
+ describe "when setting options in initializer" do
24
+ subject do
25
+ DTMF.new :signal => '#'
26
+ end
27
+
28
+ its(:signal) { should == '#' }
29
+ end
22
30
  end
23
31
  end
24
32
  end # Punchblock
@@ -25,6 +25,14 @@ module Punchblock
25
25
  its(:reason) { should == :timeout }
26
26
  its(:xmlns) { should == 'urn:xmpp:rayo:1' }
27
27
  end
28
+
29
+ describe "when setting options in initializer" do
30
+ subject do
31
+ End.new :reason => :hangup
32
+ end
33
+
34
+ its(:reason) { should == :hangup }
35
+ end
28
36
  end
29
37
  end
30
38
  end # Punchblock
@@ -27,8 +27,21 @@ module Punchblock
27
27
  it_should_behave_like 'event'
28
28
  it_should_behave_like 'event_headers'
29
29
 
30
- its(:to) { should == 'tel:+18003211212' }
31
- its(:from) { should == 'tel:+13058881212' }
30
+ its(:to) { should == 'tel:+18003211212' }
31
+ its(:from) { should == 'tel:+13058881212' }
32
+ end
33
+
34
+ describe "when setting options in initializer" do
35
+ subject do
36
+ Offer.new :to => 'tel:+18003211212',
37
+ :from => 'tel:+13058881212',
38
+ :headers => { :x_skill => "agent", :x_customer_id => "8877" }
39
+ end
40
+
41
+ it_should_behave_like 'event_headers'
42
+
43
+ its(:to) { should == 'tel:+18003211212' }
44
+ its(:from) { should == 'tel:+13058881212' }
32
45
  end
33
46
  end
34
47
  end
@@ -17,5 +17,11 @@ module Punchblock
17
17
 
18
18
  its(:id) { should == 'fgh4590' }
19
19
  end
20
+
21
+ describe "when setting options in initializer" do
22
+ subject { Ref.new :id => 'foo' }
23
+
24
+ its(:id) { should == 'foo' }
25
+ end
20
26
  end
21
27
  end # Punchblock
@@ -0,0 +1,149 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ describe AMIAction do
7
+ let(:mock_ami_client) { mock 'RubyAMI::Client' }
8
+
9
+ let :command do
10
+ Punchblock::Component::Asterisk::AMI::Action.new :name => 'ExtensionStatus', :params => { :context => 'default', :exten => 'idonno' }
11
+ end
12
+
13
+ subject { AMIAction.new command, mock_ami_client }
14
+
15
+ let :expected_action do
16
+ RubyAMI::Action.new 'ExtensionStatus', 'Context' => 'default', 'Exten' => 'idonno'
17
+ end
18
+
19
+ context 'initial execution' do
20
+ let(:component_id) { UUIDTools::UUID.random_create }
21
+
22
+ let :expected_response do
23
+ Ref.new :id => component_id
24
+ end
25
+
26
+ before { UUIDTools::UUID.stubs :random_create => component_id }
27
+
28
+ it 'should send the appropriate RubyAMI::Action and send the component node a ref with the action ID' do
29
+ mock_ami_client.expects(:send_action).once.with(expected_action).returns(expected_action)
30
+ command.expects(:response=).once.with(expected_response)
31
+ subject.execute
32
+ end
33
+ end
34
+
35
+ context 'when the AMI action completes' do
36
+ before do
37
+ command.request!
38
+ command.execute!
39
+ end
40
+
41
+ let :response do
42
+ RubyAMI::Response.new.tap do |r|
43
+ r['ActionID'] = "552a9d9f-46d7-45d8-a257-06fe95f48d99"
44
+ r['Message'] = 'Channel status will follow'
45
+ r["Exten"] = "idonno"
46
+ r["Context"] = "default"
47
+ r["Hint"] = ""
48
+ r["Status"] = "-1"
49
+ end
50
+ end
51
+
52
+ let :expected_complete_reason do
53
+ Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new :message => 'Channel status will follow', :attributes => {:exten => "idonno", :context => "default", :hint => "", :status => "-1"}
54
+ end
55
+
56
+ context 'for a non-causal action' do
57
+ it 'should send a complete event to the component node' do
58
+ subject.action.response = response
59
+
60
+ command.should be_complete
61
+
62
+ command.complete_event.resource(0.5).reason.should == expected_complete_reason
63
+ end
64
+ end
65
+
66
+ context 'for a causal action' do
67
+ let :command do
68
+ Punchblock::Component::Asterisk::AMI::Action.new :name => 'CoreShowChannels'
69
+ end
70
+
71
+ let :expected_action do
72
+ RubyAMI::Action.new 'CoreShowChannels'
73
+ end
74
+
75
+ let :event do
76
+ RubyAMI::Event.new('CoreShowChannel').tap do |e|
77
+ e['ActionID'] = "552a9d9f-46d7-45d8-a257-06fe95f48d99"
78
+ e['Channel'] = 'SIP/127.0.0.1-00000013'
79
+ e['UniqueID'] = '1287686437.19'
80
+ e['Context'] = 'adhearsion'
81
+ e['Extension'] = '23432'
82
+ e['Priority'] = '2'
83
+ e['ChannelState'] = '6'
84
+ e['ChannelStateDesc'] = 'Up'
85
+ end
86
+ end
87
+
88
+ let :terminating_event do
89
+ RubyAMI::Event.new('CoreShowChannelsComplete').tap do |e|
90
+ e['EventList'] = 'Complete'
91
+ e['ListItems'] = '3'
92
+ e['ActionID'] = 'umtLtvSg-RN5n-GEay-Z786-YdiaSLNXkcYN'
93
+ end
94
+ end
95
+
96
+ let :event_node do
97
+ Punchblock::Event::Asterisk::AMI::Event.new :name => 'CoreShowChannel', :attributes => {
98
+ :channel => 'SIP/127.0.0.1-00000013',
99
+ :uniqueid => '1287686437.19',
100
+ :context => 'adhearsion',
101
+ :extension => '23432',
102
+ :priority => '2',
103
+ :channelstate => '6',
104
+ :channelstatedesc => 'Up'
105
+ }
106
+ end
107
+
108
+ let :expected_complete_reason do
109
+ Punchblock::Component::Asterisk::AMI::Action::Complete::Success.new :message => 'Channel status will follow', :attributes => {:exten => "idonno", :context => "default", :hint => "", :status => "-1", :eventlist => 'Complete', :listitems => '3'}
110
+ end
111
+
112
+ it 'should send events to the component node' do
113
+ command.register_event_handler Punchblock::Event::Asterisk::AMI::Event do |event|
114
+ @event = event
115
+ end
116
+ subject.action << event
117
+ subject.action << response
118
+ subject.action << terminating_event
119
+ @event.should == event_node
120
+ end
121
+
122
+ it 'should send a complete event to the component node' do
123
+ subject.action << response
124
+ subject.action << terminating_event
125
+
126
+ command.complete_event.resource(0.5).reason.should == expected_complete_reason
127
+ end
128
+ end
129
+
130
+ context 'with an error' do
131
+ let :error do
132
+ RubyAMI::Error.new.tap { |e| e.message = 'Action failed' }
133
+ end
134
+
135
+ let :expected_complete_reason do
136
+ Punchblock::Event::Complete::Error.new :details => 'Action failed'
137
+ end
138
+
139
+ it 'should send a complete event to the component node' do
140
+ subject.action << error
141
+
142
+ command.complete_event.resource(0.5).reason.should == expected_complete_reason
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ describe Call do
7
+ describe '#register_component' do
8
+ it 'should make the component accessible by ID' do
9
+ component_id = 'abc123'
10
+ component = mock 'Translator::Asterisk::Component', :id => component_id
11
+ subject.register_component component
12
+ subject.component_with_id(component_id).should be component
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ class Asterisk
6
+ describe Component do
7
+
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,150 @@
1
+ require 'spec_helper'
2
+
3
+ module Punchblock
4
+ module Translator
5
+ describe Asterisk do
6
+ let(:ami_client) { mock 'RubyAMI::Client' }
7
+ let(:connection) { mock 'Connection::Asterisk' }
8
+
9
+ subject { Asterisk.new ami_client, connection }
10
+
11
+ its(:ami_client) { should be ami_client }
12
+ its(:connection) { should be connection }
13
+
14
+ describe '#execute_command' do
15
+ describe 'with a call command' do
16
+ let(:command) { Command::Answer.new }
17
+ let(:call_id) { 'abc123' }
18
+
19
+ it 'executes the call command' do
20
+ subject.actor_subject.expects(:execute_call_command).with do |c|
21
+ c.should be command
22
+ c.call_id.should == call_id
23
+ end
24
+ subject.execute_command command, :call_id => call_id
25
+ end
26
+ end
27
+
28
+ describe 'with a component command' do
29
+ let(:command) { Component::Stop.new }
30
+ let(:call_id) { 'abc123' }
31
+ let(:component_id) { '123abc' }
32
+
33
+ it 'executes the component command' do
34
+ subject.actor_subject.expects(:execute_component_command).with do |c|
35
+ c.should be command
36
+ c.call_id.should == call_id
37
+ c.component_id.should == component_id
38
+ end
39
+ subject.execute_command command, :call_id => call_id, :component_id => component_id
40
+ end
41
+ end
42
+
43
+ describe 'with a global command' do
44
+ let(:command) { Command::Dial.new }
45
+
46
+ it 'executes the command directly' do
47
+ subject.actor_subject.expects(:execute_global_command).with command
48
+ subject.execute_command command
49
+ end
50
+ end
51
+ end
52
+
53
+ describe '#register_call' do
54
+ it 'should make the call accessible by ID' do
55
+ call_id = 'abc123'
56
+ call = mock 'Translator::Asterisk::Call', :id => call_id
57
+ subject.register_call call
58
+ subject.call_with_id(call_id).should be call
59
+ end
60
+ end
61
+
62
+ describe '#execute_call_command' do
63
+ let(:call_id) { 'abc123' }
64
+ let(:call) { mock 'Translator::Asterisk::Call', :id => call_id }
65
+ let(:command) { mock 'Command::Answer', :call_id => call_id }
66
+
67
+ before do
68
+ subject.register_call call
69
+ end
70
+
71
+ it 'sends the command to the call for execution' do
72
+ call.expects(:execute_command).once.with command
73
+ subject.execute_call_command command
74
+ end
75
+ end
76
+
77
+ describe '#execute_component_command' do
78
+ let(:call_id) { 'abc123' }
79
+ let(:call) { Translator::Asterisk::Call.new }
80
+
81
+ let(:component_id) { '123abc' }
82
+ let(:component) { mock 'Translator::Asterisk::Component', :id => component_id }
83
+
84
+ let(:command) { mock 'Component::Stop', :call_id => call_id, :component_id => component_id }
85
+
86
+ before do
87
+ call.stubs(:id).returns call_id
88
+ call.register_component component
89
+ subject.register_call call
90
+ end
91
+
92
+ it 'sends the command to the component for execution' do
93
+ component.expects(:execute_command).once.with command
94
+ subject.execute_component_command command
95
+ end
96
+ end
97
+
98
+ describe '#execute_global_command' do
99
+ context 'with a Dial' do
100
+ pending
101
+ end
102
+
103
+ context 'with an AMI action' do
104
+ let :command do
105
+ Component::Asterisk::AMI::Action.new :name => 'Status', :params => { :channel => 'foo' }
106
+ end
107
+
108
+ let(:mock_action) { mock 'Asterisk::AMIAction' }
109
+
110
+ it 'should create a component actor and execute it asynchronously' do
111
+ Asterisk::AMIAction.expects(:new).once.with(command, subject.ami_client).returns mock_action
112
+ mock_action.expects(:execute!).once
113
+ subject.execute_global_command command
114
+ end
115
+ end
116
+ end
117
+
118
+ describe '#handle_ami_event' do
119
+ let :ami_event do
120
+ RubyAMI::Event.new('Newchannel').tap do |e|
121
+ e['Channel'] = "SIP/101-3f3f"
122
+ e['State'] = "Ring"
123
+ e['Callerid'] = "101"
124
+ e['Uniqueid'] = "1094154427.10"
125
+ end
126
+ end
127
+
128
+ let :expected_pb_event do
129
+ Event::Asterisk::AMI::Event.new :name => 'Newchannel',
130
+ :attributes => { :channel => "SIP/101-3f3f",
131
+ :state => "Ring",
132
+ :callerid => "101",
133
+ :uniqueid => "1094154427.10"}
134
+ end
135
+
136
+ it 'should create a Punchblock AMI event object and pass it to the connection' do
137
+ subject.connection.expects(:handle_event).once.with expected_pb_event
138
+ subject.handle_ami_event ami_event
139
+ end
140
+
141
+ context 'with something that is not a RubyAMI::Event' do
142
+ it 'does not send anything to the connection' do
143
+ subject.connection.expects(:handle_event).never
144
+ subject.handle_ami_event :foo
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
data/spec/spec_helper.rb CHANGED
@@ -20,6 +20,7 @@ def import_stanza(xml)
20
20
  Blather::Stanza.import parse_stanza(xml).root
21
21
  end
22
22
 
23
+ # FIXME: change this to rayo_event? It can be ambigous
23
24
  shared_examples_for 'event' do
24
25
  its(:call_id) { should == '9f00061' }
25
26
  its(:component_id) { should == '1' }
@@ -41,3 +42,44 @@ shared_examples_for 'event_headers' do
41
42
  its(:headers) { should == [Punchblock::Header.new(:x_skill, 'agent'), Punchblock::Header.new(:x_customer_id, '8877')]}
42
43
  its(:headers_hash) { should == {:x_skill => 'agent', :x_customer_id => '8877'} }
43
44
  end
45
+
46
+ shared_examples_for 'key_value_pairs' do
47
+ it 'will auto-inherit nodes' do
48
+ n = parse_stanza "<#{element_name} name='boo' value='bah' />"
49
+ h = class_name.new n.root
50
+ h.name.should == :boo
51
+ h.value.should == 'bah'
52
+ end
53
+
54
+ it 'has a name attribute' do
55
+ n = class_name.new :boo, 'bah'
56
+ n.name.should == :boo
57
+ n.name = :foo
58
+ n.name.should == :foo
59
+ end
60
+
61
+ it "substitutes - for _ on the name attribute when reading" do
62
+ n = parse_stanza "<#{element_name} name='boo-bah' value='foo' />"
63
+ h = class_name.new n.root
64
+ h.name.should == :boo_bah
65
+ end
66
+
67
+ it "substitutes _ for - on the name attribute when writing" do
68
+ h = class_name.new :boo_bah, 'foo'
69
+ h.to_xml.should == "<#{element_name} name=\"boo-bah\" value=\"foo\"/>"
70
+ end
71
+
72
+ it 'has a value param' do
73
+ n = class_name.new :boo, 'en'
74
+ n.value.should == 'en'
75
+ n.value = 'de'
76
+ n.value.should == 'de'
77
+ end
78
+
79
+ it 'can determine equality' do
80
+ a = class_name.new :boo, 'bah'
81
+ a.should == class_name.new(:boo, 'bah')
82
+ a.should_not == class_name.new(:bah, 'bah')
83
+ a.should_not == class_name.new(:boo, 'boo')
84
+ end
85
+ end