librevox 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ require 'eventmachine'
2
+ require 'librevox/response'
3
+ require 'librevox/commands'
4
+
5
+ module Librevox
6
+ module Listener
7
+ class Base < EventMachine::Protocols::HeaderAndContentProtocol
8
+ class << self
9
+ def hooks
10
+ @hooks ||= []
11
+ end
12
+
13
+ def event(event, &block)
14
+ hooks << [event, block]
15
+ end
16
+ end
17
+
18
+ # In some cases there are both applications and commands with the same
19
+ # name, e.g. fifo. But we can't have two `fifo`-methods, so we include
20
+ # commands in CommandDelegate, and wrap all commands in the `api` call,
21
+ # which forwards the call to the CommandDelegate instance, which in turn
22
+ # forwards the #run_cmd-call from the command back to the listener. Yay.
23
+ class CommandDelegate
24
+ include Librevox::Commands
25
+
26
+ def initialize(listener)
27
+ @listener = listener
28
+ end
29
+
30
+ def run_cmd(*args, &block)
31
+ @listener.run_cmd *args, &block
32
+ end
33
+ end
34
+
35
+ def api(cmd, *args, &block)
36
+ @command_delegate.send(cmd, *args, &block)
37
+ end
38
+
39
+ def run_cmd(cmd, &block)
40
+ send_data "#{cmd}\n\n"
41
+ @api_queue << (block || lambda {})
42
+ end
43
+
44
+ attr_accessor :response
45
+ alias :event :response
46
+
47
+ def post_init
48
+ @command_delegate = CommandDelegate.new(self)
49
+ @api_queue = []
50
+ end
51
+
52
+ def receive_request(header, content)
53
+ @response = Librevox::Response.new(header, content)
54
+
55
+ if response.event?
56
+ on_event
57
+ invoke_event response.event
58
+ elsif response.api_response? && @api_queue.any?
59
+ invoke_api_queue
60
+ end
61
+ end
62
+
63
+ # override
64
+ def on_event
65
+ end
66
+
67
+ private
68
+ def invoke_event(event_name)
69
+ self.class.hooks.each do |name,block|
70
+ instance_eval(&block) if name == event_name.downcase.to_sym
71
+ end
72
+ end
73
+
74
+ def invoke_api_queue
75
+ block = @api_queue.shift
76
+ block.call(response)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ require 'librevox/listener/base'
2
+
3
+ module Librevox
4
+ module Listener
5
+ class Inbound < Base
6
+ def initialize(args={})
7
+ super
8
+
9
+ @auth = args[:auth] || "ClueCon"
10
+ end
11
+
12
+ def post_init
13
+ super
14
+ send_data "auth #{@auth}\n\n"
15
+ send_data "event plain ALL\n\n"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ require 'librevox/listener/base'
2
+ require 'librevox/applications'
3
+
4
+ module Librevox
5
+ module Listener
6
+ class Outbound < Base
7
+ class << self
8
+ attr_reader :session_callback
9
+
10
+ def session(&block)
11
+ @session_callback = block
12
+ end
13
+ end
14
+
15
+ include Librevox::Applications
16
+
17
+ def execute_app(app, args="", params={}, &block)
18
+ msg = "sendmsg\n"
19
+ msg << "call-command: execute\n"
20
+ msg << "execute-app-name: #{app}\n"
21
+ msg << "execute-app-arg: #{args}\n" unless args.empty?
22
+
23
+ send_data "#{msg}\n"
24
+
25
+ @read_channel_var = params[:read_var]
26
+
27
+ if @read_channel_var
28
+ @command_queue << lambda {update_session}
29
+ end
30
+
31
+ @command_queue << (block || lambda {})
32
+ end
33
+
34
+ # This should probably be in Application#sendmsg instead.
35
+ def sendmsg(msg)
36
+ send_data "sendmsg\n%s" % msg
37
+ end
38
+
39
+ attr_accessor :session
40
+
41
+ def post_init
42
+ super
43
+ @session = nil
44
+ @command_queue = []
45
+
46
+ send_data "connect\n\n"
47
+ send_data "myevents\n\n"
48
+ @command_queue << lambda {}
49
+ send_data "linger\n\n"
50
+ @command_queue << lambda {}
51
+ end
52
+
53
+ def receive_request(*args)
54
+ super(*args)
55
+
56
+ if session.nil?
57
+ @session = response
58
+ instance_eval &self.class.session_callback
59
+ elsif response.event? && response.event == "CHANNEL_DATA"
60
+ @session = response
61
+ resume_with_channel_var
62
+ elsif response.command_reply? && !response.event?
63
+ @command_queue.shift.call if @command_queue.any?
64
+ end
65
+ end
66
+
67
+ def resume_with_channel_var
68
+ if @read_channel_var
69
+ variable = "variable_#{@read_channel_var}".to_sym
70
+ value = @session.content[variable]
71
+ @command_queue.shift.call(value) if @command_queue.any?
72
+ end
73
+ end
74
+
75
+ def update_session
76
+ send_data("api uuid_dump #{session.headers[:unique_id]}\n\n")
77
+ end
78
+
79
+ def session_initiated
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,48 @@
1
+ require 'eventmachine'
2
+ require 'em/protocols/header_and_content'
3
+
4
+ class String
5
+ alias :each :each_line
6
+ end
7
+
8
+ module Librevox
9
+ class Response
10
+ attr_accessor :headers, :content
11
+
12
+ def initialize(headers="", content="")
13
+ self.headers = headers
14
+ self.content = content
15
+ end
16
+
17
+ def headers=(headers)
18
+ @headers = headers_2_hash(headers)
19
+ @headers.each {|k,v| v.chomp! if v.is_a?(String)}
20
+ end
21
+
22
+ def content=(content)
23
+ @content = content.match(/:/) ? headers_2_hash(content) : content
24
+ @content.each {|k,v| v.chomp! if v.is_a?(String)}
25
+ end
26
+
27
+ def event?
28
+ @content.is_a?(Hash) && @content.include?(:event_name)
29
+ end
30
+
31
+ def event
32
+ @content[:event_name] if event?
33
+ end
34
+
35
+ def api_response?
36
+ @headers[:content_type] == "api/response"
37
+ end
38
+
39
+ def command_reply?
40
+ @headers[:content_type] == "command/reply"
41
+ end
42
+
43
+ private
44
+ def headers_2_hash(*args)
45
+ EM::Protocols::HeaderAndContentProtocol.headers_2_hash *args
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "librevox"
3
+ s.version = "0.1"
4
+ s.date = "2009-12-14"
5
+ s.summary = "Ruby library for interacting with FreeSWITCH."
6
+ s.email = "harry@vangberg.name"
7
+ s.homepage = "http://github.com/ichverstehe/librevox"
8
+ s.description = "EventMachine-based Ruby library for interacting with the
9
+ open source telephony platform FreeSwitch."
10
+ s.authors = ["Harry Vangberg"]
11
+ s.files = [
12
+ "README.md",
13
+ "LICENSE",
14
+ "TODO",
15
+ "Rakefile",
16
+ "librevox.gemspec",
17
+ "lib/librevox.rb",
18
+ "lib/librevox/applications.rb",
19
+ "lib/librevox/command_socket.rb",
20
+ "lib/librevox/commands.rb",
21
+ "lib/librevox/response.rb",
22
+ "lib/librevox/listener/base.rb",
23
+ "lib/librevox/listener/inbound.rb",
24
+ "lib/librevox/listener/outbound.rb"
25
+ ]
26
+ s.test_files = [
27
+ "spec/helper.rb",
28
+ "spec/librevox/listener.rb",
29
+ "spec/librevox/spec_applications.rb",
30
+ "spec/librevox/spec_command_socket.rb",
31
+ "spec/librevox/spec_commands.rb",
32
+ "spec/librevox/spec_response.rb",
33
+ "spec/librevox/listener/spec_inbound.rb",
34
+ "spec/librevox/listener/spec_outbound.rb"
35
+ ]
36
+ end
37
+
@@ -0,0 +1,6 @@
1
+ $:.unshift 'lib'
2
+
3
+ require 'bacon'
4
+ require 'librevox'
5
+
6
+ Bacon.summary_on_exit
@@ -0,0 +1,130 @@
1
+ require 'spec/helper'
2
+
3
+ require 'librevox/listener/base'
4
+
5
+ class Librevox::Listener::Base
6
+ attr_accessor :outgoing_data
7
+
8
+ def initialize(*args)
9
+ @outgoing_data = []
10
+ super *args
11
+ end
12
+
13
+ def send_data(data)
14
+ @outgoing_data << data
15
+ end
16
+
17
+ def read_data
18
+ @outgoing_data.pop
19
+ end
20
+ end
21
+
22
+ shared "events" do
23
+ before do
24
+ @class = @listener.class
25
+
26
+ @class.event(:some_event) {send_data "something"}
27
+ @class.event(:other_event) {send_data "something else"}
28
+
29
+ # Establish session
30
+ @listener.receive_data("Content-Length: 0\nTest: Testing\n\n")
31
+ end
32
+
33
+ should "add event hook" do
34
+ @class.hooks.size.should == 2
35
+ end
36
+
37
+ should "execute callback for event" do
38
+ @listener.receive_data("Content-Length: 23\n\nEvent-Name: OTHER_EVENT\n\n")
39
+ @listener.read_data.should == "something else"
40
+
41
+ @listener.receive_data("Content-Length: 22\n\nEvent-Name: SOME_EVENT\n\n")
42
+ @listener.read_data.should == "something"
43
+ end
44
+
45
+ should "expose response as event" do
46
+ @listener.receive_data("Content-Length: 23\n\nEvent-Name: OTHER_EVENT\n\n")
47
+ @listener.event.class.should == Librevox::Response
48
+ @listener.event.content[:event_name].should == "OTHER_EVENT"
49
+ end
50
+ end
51
+
52
+ module Librevox::Commands
53
+ def sample_cmd(cmd, args="", &b)
54
+ execute_cmd cmd, args, &b
55
+ end
56
+ end
57
+
58
+ shared "api commands" do
59
+ before do
60
+ @class = @listener.class
61
+
62
+ # Establish session
63
+ @listener.receive_data("Content-Type: command/reply\nTest: Testing\n\n")
64
+ end
65
+
66
+ describe "multiple api commands" do
67
+ before do
68
+ @listener.outgoing_data.clear
69
+ @class.event(:api_test) {
70
+ api :sample_cmd, "foo" do
71
+ api :sample_cmd, "foo", "bar baz"
72
+ end
73
+ }
74
+ end
75
+
76
+ should "only send one command at a time" do
77
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 22\n\nEvent-Name: API_TEST\n\n")
78
+ @listener.read_data.should == "api foo\n\n"
79
+ @listener.read_data.should == nil
80
+
81
+ @listener.receive_data("Content-Type: api/response\nReply-Text: +OK\n\n")
82
+ @listener.read_data.should == "api foo bar baz\n\n"
83
+ @listener.read_data.should == nil
84
+ end
85
+ end
86
+
87
+ describe "flat api commands" do
88
+ before do
89
+ @listener.outgoing_data.clear
90
+ @class.event(:api_flat_test) {
91
+ api :sample_cmd, "foo"
92
+ api :sample_cmd, "bar" do
93
+ api :sample_cmd, "baz"
94
+ end
95
+ }
96
+ end
97
+
98
+ should "wait for response before calling next proc" do
99
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 27\n\nEvent-Name: API_FLAT_TEST\n\n")
100
+
101
+ @listener.read_data.should.not == "api baz\n\n"
102
+
103
+ # response to "foo"
104
+ @listener.receive_data("Content-Type: api/response\nContent-Length: 3\n\n+OK\n\n")
105
+ @listener.read_data.should.not == "api baz\n\n"
106
+
107
+ # response to "bar"
108
+ @listener.receive_data("Content-Type: api/response\nContent-Length: 3\n\n+OK\n\n")
109
+ @listener.read_data.should == "api baz\n\n"
110
+ end
111
+ end
112
+
113
+ describe "api command with block argument" do
114
+ before do
115
+ @listener.outgoing_data.clear
116
+ @class.event(:api_arg_test) {
117
+ api :sample_cmd, "foo" do |r|
118
+ send_data "response: #{r.content}"
119
+ end
120
+ }
121
+ end
122
+
123
+ should "pass response" do
124
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 26\n\nEvent-Name: API_ARG_TEST\n\n")
125
+ @listener.receive_data("Content-Type: api/response\nContent-Length: 3\n\n+OK\n\n")
126
+
127
+ @listener.read_data.should == "response: +OK"
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec/helper'
2
+ require 'spec/librevox/listener'
3
+
4
+ require 'librevox/listener/inbound'
5
+
6
+ class InboundTestListener < Librevox::Listener::Inbound
7
+ end
8
+
9
+ describe "Inbound listener" do
10
+ before do
11
+ @listener = InboundTestListener.new(nil)
12
+ end
13
+
14
+ behaves_like "events"
15
+ behaves_like "api commands"
16
+
17
+ should "authorize and subscribe to events" do
18
+ @listener.outgoing_data.shift.should == "auth ClueCon\n\n"
19
+ @listener.outgoing_data.shift.should == "event plain ALL\n\n"
20
+ end
21
+ end
@@ -0,0 +1,221 @@
1
+ require 'spec/helper'
2
+ require 'spec/librevox/listener'
3
+
4
+ require 'librevox/listener/outbound'
5
+
6
+ module Librevox::Applications
7
+ def sample_app(name, *args, &b)
8
+ execute_app name, args.join(" "), &b
9
+ end
10
+ end
11
+
12
+ class OutboundTestListener < Librevox::Listener::Outbound
13
+ session do
14
+ send_data "session was initiated"
15
+ end
16
+ end
17
+
18
+ def receive_event_and_linger_replies
19
+ @listener.receive_data("Content-Type: command/reply\nReply-Text: +OK Events Enabled\n\n")
20
+ @listener.receive_data("Content-Type: command/reply\nReply-Text: +OK will linger\n\n")
21
+ end
22
+
23
+ describe "Outbound listener" do
24
+ before do
25
+ @listener = OutboundTestListener.new(nil)
26
+ @listener.receive_data("Content-Type: command/reply\nCaller-Caller-ID-Number: 8675309\n\n")
27
+ receive_event_and_linger_replies
28
+ end
29
+
30
+ should "connect to freeswitch and subscribe to events" do
31
+ @listener.outgoing_data.shift.should.equal "connect\n\n"
32
+ @listener.outgoing_data.shift.should.equal "myevents\n\n"
33
+ @listener.outgoing_data.shift.should.equal "linger\n\n"
34
+ end
35
+
36
+ should "establish a session" do
37
+ @listener.session.class.should.equal Librevox::Response
38
+ end
39
+
40
+ should "call session callback after establishing new session" do
41
+ @listener.read_data.should.equal "session was initiated"
42
+ end
43
+
44
+ should "make channel variables available through session" do
45
+ @listener.session.headers[:caller_caller_id_number].should.equal "8675309"
46
+ end
47
+
48
+ behaves_like "events"
49
+ behaves_like "api commands"
50
+
51
+ should "register app" do
52
+ @listener.respond_to?(:sample_app).should.be.true?
53
+ end
54
+ end
55
+
56
+ class OutboundListenerWithNestedApps < Librevox::Listener::Outbound
57
+ session do
58
+ sample_app "foo" do
59
+ sample_app "bar"
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "Outbound listener with apps" do
65
+ before do
66
+ @listener = OutboundListenerWithNestedApps.new(nil)
67
+
68
+ # Establish session and get rid of connect-string
69
+ @listener.receive_data("Content-Type: command/reply\nEstablish-Session: OK\n\n")
70
+ receive_event_and_linger_replies
71
+ 3.times {@listener.outgoing_data.shift}
72
+ end
73
+
74
+ should "only send one app at a time" do
75
+ @listener.read_data.should == "sendmsg\ncall-command: execute\nexecute-app-name: foo\n\n"
76
+ @listener.read_data.should == nil
77
+
78
+ @listener.receive_data("Content-Type: command/reply\nReply-Text: +OK\n\n")
79
+ @listener.read_data.should == "sendmsg\ncall-command: execute\nexecute-app-name: bar\n\n"
80
+ @listener.read_data.should == nil
81
+ end
82
+
83
+ should "not be driven forward by events" do
84
+ @listener.read_data # sample_app "foo"
85
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 45\n\nEvent-Name: CHANNEL_EXECUTE\nSession-Var: Some\n\n")
86
+ @listener.read_data.should == nil
87
+ end
88
+
89
+ should "not be driven forward by api responses" do
90
+ @listener.read_data # sample_app "foo"
91
+ @listener.receive_data("Content-Type: api/response\nContent-Length: 3\n\nFoo")
92
+ @listener.read_data.should == nil
93
+ end
94
+
95
+ should "not be driven forward by disconnect notifications" do
96
+ @listener.read_data # sample_app "foo"
97
+ @listener.receive_data("Content-Type: text/disconnect-notice\nContent-Length: 9\n\nLingering")
98
+ @listener.read_data.should == nil
99
+ end
100
+ end
101
+
102
+ module Librevox::Applications
103
+ def reader_app(&b)
104
+ execute_app 'reader_app', [], {:read_var => 'a_reader_var'}, &b
105
+ end
106
+ end
107
+
108
+ class OutboundListenerWithReader < Librevox::Listener::Outbound
109
+ session do
110
+ reader_app do |data|
111
+ send_data "read this: #{data}"
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "Outbound listener with app reading data" do
117
+ before do
118
+ @listener = OutboundListenerWithReader.new(nil)
119
+
120
+ # Establish session and get rid of connect-string
121
+ @listener.receive_data("Content-Type: command/reply\nSession-Var: First\nUnique-ID: 1234\n\n")
122
+ receive_event_and_linger_replies
123
+ 3.times {@listener.outgoing_data.shift}
124
+ end
125
+
126
+ should "not send anything while missing response" do
127
+ @listener.read_data # the command executing reader_app
128
+ @listener.read_data.should == nil
129
+ end
130
+
131
+ should "send uuid_dump to get channel var, after getting response" do
132
+ @listener.receive_data("Content-Type: command/reply\nReply-Text: +OK\n\n")
133
+ @listener.read_data.should == "api uuid_dump 1234\n\n"
134
+ end
135
+
136
+ should "update session with new data" do
137
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 3\n\n+OK\n\n")
138
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 44\n\nEvent-Name: CHANNEL_DATA\nSession-Var: Second\n\n")
139
+ @listener.session.content[:session_var].should == "Second"
140
+ end
141
+
142
+ should "pass value of channel variable to block" do
143
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 3\n\n+OK\n\n")
144
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 59\n\nEvent-Name: CHANNEL_DATA\nvariable_a-reader-var: some value\n\n")
145
+ @listener.read_data.should == "read this: some value"
146
+ end
147
+ end
148
+
149
+ class OutboundListenerWithNonNestedApps < Librevox::Listener::Outbound
150
+ attr_reader :queue
151
+ session do
152
+ sample_app "foo"
153
+ reader_app do |data|
154
+ send_data "the end: #{data}"
155
+ end
156
+ end
157
+ end
158
+
159
+ describe "Outbound listener with non-nested apps" do
160
+ before do
161
+ @listener = OutboundListenerWithNonNestedApps.new(nil)
162
+
163
+ # Establish session and get rid of connect-string
164
+ @listener.receive_data("Content-Type: command/reply\nSession-Var: First\nUnique-ID: 1234\n\n")
165
+ receive_event_and_linger_replies
166
+ 3.times {@listener.outgoing_data.shift}
167
+ end
168
+
169
+ should "wait for response before calling next proc" do
170
+ # response to sample_app
171
+ @listener.read_data.should.not.match /the end/
172
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 3\n\n+OK\n\n")
173
+
174
+ # response to reader_app
175
+ @listener.read_data.should.not.match /the end/
176
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 3\n\n+OK\n\n")
177
+
178
+ # response to uuid_dump caused by reader_app
179
+ @listener.read_data.should.not.match /the end/
180
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 59\n\nEvent-Name: CHANNEL_DATA\nvariable_a-reader-var: some value\n\n")
181
+
182
+ @listener.read_data.should == "the end: some value"
183
+ end
184
+ end
185
+
186
+ module Librevox::Commands
187
+ def sample_cmd(cmd, *args, &b)
188
+ execute_cmd cmd, *args, &b
189
+ end
190
+ end
191
+
192
+ class OutboundListenerWithAppsAndApi < Librevox::Listener::Outbound
193
+ session do
194
+ sample_app "foo" do
195
+ api :sample_cmd, "bar" do
196
+ sample_app "baz"
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ describe "Outbound listener with both apps and api calls" do
203
+ before do
204
+ @listener = OutboundListenerWithAppsAndApi.new(nil)
205
+
206
+ # Establish session and get rid of connect-string
207
+ @listener.receive_data("Content-Type: command/reply\nSession-Var: First\nUnique-ID: 1234\n\n")
208
+ receive_event_and_linger_replies
209
+ 3.times {@listener.outgoing_data.shift}
210
+ end
211
+
212
+ should "wait for response before calling next proc" do
213
+ @listener.read_data.should == "sendmsg\ncall-command: execute\nexecute-app-name: foo\n\n"
214
+ @listener.receive_data("Content-Type: command/reply\nContent-Length: 3\n\n+OK\n\n")
215
+
216
+ @listener.read_data.should == "api bar\n\n"
217
+ @listener.receive_data("Content-Type: api/response\nContent-Length: 3\n\n+OK\n\n")
218
+
219
+ @listener.read_data.should == "sendmsg\ncall-command: execute\nexecute-app-name: baz\n\n"
220
+ end
221
+ end