librevox 0.1

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