freeswitcher 0.1.4 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +4 -3
- data/CHANGELOG +123 -0
- data/MANIFEST +3 -9
- data/README +57 -60
- data/Rakefile +4 -4
- data/examples/outbound_event_socket.rb +2 -5
- data/freeswitcher.gemspec +62 -65
- data/lib/fsr/app.rb +1 -3
- data/lib/fsr/app/answer.rb +2 -0
- data/lib/fsr/app/play_and_get_digits.rb +11 -1
- data/lib/fsr/app/read.rb +12 -2
- data/lib/fsr/app/uuid_getvar.rb +13 -2
- data/lib/fsr/app/uuid_setvar.rb +11 -1
- data/lib/fsr/cmd.rb +22 -4
- data/lib/fsr/cmd/originate.rb +23 -6
- data/lib/fsr/command_socket.rb +1 -1
- data/lib/fsr/listener/inbound.rb +19 -10
- data/lib/fsr/listener/outbound.rb +75 -18
- data/lib/fsr/version.rb +1 -1
- data/spec/fsr/app/answer.rb +12 -0
- data/spec/fsr/app/fs_break.rb +12 -0
- data/spec/fsr/app/fs_sleep.rb +12 -0
- data/spec/fsr/cmd/originate.rb +32 -0
- metadata +63 -84
- data/lib/fsr/listener/outbound.rb.orig +0 -131
data/lib/fsr/app.rb
CHANGED
@@ -25,9 +25,7 @@ module FSR
|
|
25
25
|
|
26
26
|
def self.register(application, obj)
|
27
27
|
APPLICATIONS[application.to_sym] = obj
|
28
|
-
|
29
|
-
code = REGISTER_CODE % [application, application]
|
30
|
-
App.module_eval(code)
|
28
|
+
App.module_eval(REGISTER_CODE % [application, application])
|
31
29
|
end
|
32
30
|
|
33
31
|
def self.list
|
data/lib/fsr/app/answer.rb
CHANGED
@@ -3,6 +3,8 @@ module FSR
|
|
3
3
|
#http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_play_and_get_digits
|
4
4
|
module App
|
5
5
|
class PlayAndGetDigits < Application
|
6
|
+
attr_reader :chan_var
|
7
|
+
|
6
8
|
def initialize(sound_file, invalid_file, min = 0, max = 10, tries = 3, timeout = 7000, terminators = ["#"], chan_var = "fsr_read_dtmf", regexp = "\d")
|
7
9
|
@sound_file = sound_file
|
8
10
|
@invalid_file = invalid_file
|
@@ -22,7 +24,15 @@ module FSR
|
|
22
24
|
def sendmsg
|
23
25
|
"call-command: execute\nexecute-app-name: %s\nexecute-app-arg: %s\nevent-lock:true\n\n" % ["play_and_get_digits", arguments.join(" ")]
|
24
26
|
end
|
25
|
-
|
27
|
+
SENDMSG_METHOD = %q|
|
28
|
+
def play_and_get_digits(*args, &block)
|
29
|
+
me = super(*args)
|
30
|
+
@read_var = "variable_#{me.chan_var}"
|
31
|
+
sendmsg me
|
32
|
+
@queue.unshift Proc.new { update_session }
|
33
|
+
@queue.unshift block if block_given?
|
34
|
+
end
|
35
|
+
|
|
26
36
|
end
|
27
37
|
|
28
38
|
register(:play_and_get_digits, PlayAndGetDigits)
|
data/lib/fsr/app/read.rb
CHANGED
@@ -3,6 +3,7 @@ module FSR
|
|
3
3
|
# http://wiki.freeswitch.org/wiki/Misc._Dialplan_Tools_read
|
4
4
|
module App
|
5
5
|
class Read < Application
|
6
|
+
attr_reader :chan_var
|
6
7
|
def initialize(sound_file, min = 0, max = 10, chan_var = "fsr_read_dtmf", timeout = 10000, terminators = ["#"])
|
7
8
|
@sound_file, @min, @max, @chan_var, @timeout, @terminators = sound_file, min, max, chan_var, timeout, terminators
|
8
9
|
end
|
@@ -14,9 +15,18 @@ module FSR
|
|
14
15
|
def sendmsg
|
15
16
|
"call-command: execute\nexecute-app-name: %s\nexecute-app-arg: %s\nevent-lock:true\n\n" % [app_name, arguments.join(" ")]
|
16
17
|
end
|
17
|
-
|
18
|
+
SENDMSG_METHOD = %q|
|
19
|
+
def read(*args, &block)
|
20
|
+
me = super(*args)
|
21
|
+
@read_var = "variable_#{me.chan_var}"
|
22
|
+
sendmsg me
|
23
|
+
@queue.unshift Proc.new { update_session }
|
24
|
+
@queue.unshift block if block_given?
|
25
|
+
end
|
26
|
+
|
|
18
27
|
end
|
19
|
-
|
20
28
|
register(:read, Read)
|
29
|
+
|
30
|
+
|
21
31
|
end
|
22
32
|
end
|
data/lib/fsr/app/uuid_getvar.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require "fsr/app"
|
2
2
|
module FSR
|
3
|
-
#http://wiki.freeswitch.org/wiki/Mod_commands#
|
3
|
+
#http://wiki.freeswitch.org/wiki/Mod_commands#uuid_setvar
|
4
4
|
module App
|
5
5
|
class UuidGetVar < Application
|
6
|
+
attr_reader :var, :uuid
|
7
|
+
|
6
8
|
def initialize(uuid, var)
|
7
9
|
@uuid = uuid # Unique channel ID
|
8
|
-
@var = var # Channel variable you wish to '
|
10
|
+
@var = var # Channel variable you wish to 'set'
|
9
11
|
end
|
10
12
|
|
11
13
|
def arguments
|
@@ -16,6 +18,15 @@ module FSR
|
|
16
18
|
"call-command: execute\nexecute-app-name: %s\nexecute-app-arg: %s\nevent-lock:true\n\n" % [app_name, arguments.join(" ")]
|
17
19
|
end
|
18
20
|
|
21
|
+
SENDMSG_METHOD = %q|
|
22
|
+
def uuid_getvar(*args, &block)
|
23
|
+
me = super(*args)
|
24
|
+
@uuid_var = me.var
|
25
|
+
api_call = "api uuid_getvar #{me.uuid} #{me.var}\n\n"
|
26
|
+
send_data(api_call)
|
27
|
+
@queue.unshift block if block_given?
|
28
|
+
end
|
29
|
+
|
|
19
30
|
end
|
20
31
|
|
21
32
|
register(:uuid_getvar, UuidGetVar)
|
data/lib/fsr/app/uuid_setvar.rb
CHANGED
@@ -3,10 +3,12 @@ module FSR
|
|
3
3
|
#http://wiki.freeswitch.org/wiki/Mod_commands#uuid_setvar
|
4
4
|
module App
|
5
5
|
class UuidSetVar < Application
|
6
|
+
attr_reader :var, :uuid, :assignment
|
7
|
+
|
6
8
|
def initialize(uuid, var, assignment)
|
7
9
|
@uuid = uuid # Unique channel ID
|
8
10
|
@var = var # Channel variable you wish to 'set'
|
9
|
-
@assignment
|
11
|
+
@assignment = assignment
|
10
12
|
end
|
11
13
|
|
12
14
|
def arguments
|
@@ -17,6 +19,14 @@ module FSR
|
|
17
19
|
"call-command: execute\nexecute-app-name: %s\nexecute-app-arg: %s\nevent-lock:true\n\n" % [app_name, arguments.join(" ")]
|
18
20
|
end
|
19
21
|
|
22
|
+
SENDMSG_METHOD = %q|
|
23
|
+
def uuid_setvar(*args, &block)
|
24
|
+
me = super(*args)
|
25
|
+
api_call = "api uuid_setvar #{me.uuid} #{me.var} #{me.assignment}\n\n"
|
26
|
+
send_data(api_call)
|
27
|
+
@queue.unshift block if block_given?
|
28
|
+
end
|
29
|
+
|
|
20
30
|
end
|
21
31
|
|
22
32
|
register(:uuid_setvar, UuidSetVar)
|
data/lib/fsr/cmd.rb
CHANGED
@@ -1,10 +1,28 @@
|
|
1
1
|
module FSR
|
2
2
|
module Cmd
|
3
3
|
class Command
|
4
|
+
DEFAULT_OPTIONS = {
|
5
|
+
:origination_caller_id_name => FSR::DEFAULT_CALLER_ID_NAME,
|
6
|
+
:origination_caller_id_number => FSR::DEFAULT_CALLER_ID_NUMBER,
|
7
|
+
:originate_timeout => 30,
|
8
|
+
:ignore_early_media => true
|
9
|
+
}
|
10
|
+
|
11
|
+
protected
|
12
|
+
def default_options(args = {}, defaults = nil, &block)
|
13
|
+
opts = if defaults.nil?
|
14
|
+
DEFAULT_OPTIONS.merge(args)
|
15
|
+
else
|
16
|
+
raise(ArgumentError, "defaults argument must ba a hash") unless defaults.kind_of?(Hash)
|
17
|
+
defaults.merge(args)
|
18
|
+
end
|
19
|
+
yield opts if block_given?
|
20
|
+
end
|
21
|
+
|
4
22
|
end
|
5
23
|
|
6
24
|
COMMANDS = {}
|
7
|
-
LOAD_PATH = [
|
25
|
+
LOAD_PATH = [Pathname(FSR::ROOT).join( "fsr", "cmd")]
|
8
26
|
|
9
27
|
def self.register(command, obj)
|
10
28
|
COMMANDS[command.to_sym] = obj
|
@@ -28,8 +46,8 @@ module FSR
|
|
28
46
|
end
|
29
47
|
Log.debug "Trying load paths"
|
30
48
|
# If we find a file named the same as the command passed in LOAD_PATH, load it
|
31
|
-
if found_command_path = LOAD_PATH.detect { |command_path|
|
32
|
-
command_file =
|
49
|
+
if found_command_path = LOAD_PATH.detect { |command_path| command_path.join("#{command}.rb").file? }
|
50
|
+
command_file = found_command_path.join(command)
|
33
51
|
Log.debug "Trying to load #{command_file}"
|
34
52
|
if force_reload
|
35
53
|
load command_file.to_s + ".rb"
|
@@ -44,7 +62,7 @@ module FSR
|
|
44
62
|
# Load all of the commands we find in Cmd::LOAD_PATH
|
45
63
|
def self.load_all(force_reload = false)
|
46
64
|
LOAD_PATH.each do |load_path|
|
47
|
-
Dir[
|
65
|
+
Dir[load_path.join("*.rb")].each { |command_file| load_command(command_file, force_reload) }
|
48
66
|
end
|
49
67
|
list
|
50
68
|
end
|
data/lib/fsr/cmd/originate.rb
CHANGED
@@ -6,19 +6,35 @@ module FSR
|
|
6
6
|
attr_reader :fs_socket, :target_options
|
7
7
|
|
8
8
|
def initialize(fs_socket = nil, args = {})
|
9
|
+
# Using an argument hash may not be the the best way to go here, but as long as we're doing
|
10
|
+
# so we'll type check it
|
11
|
+
raise(ArgumentError, "args (Passed: <<<#{args}>>>) must be a hash") unless args.kind_of?(Hash)
|
12
|
+
|
9
13
|
# These are options that will precede the target address
|
10
|
-
|
11
|
-
|
14
|
+
if args[:target_options]
|
15
|
+
raise(ArgumentError, "args[:target_options] (Passed: <<<#{args[:target_options]}>>>) must be a hash") unless args[:target_options].kind_of?(Hash)
|
16
|
+
else
|
17
|
+
args[:target_options] = {}
|
18
|
+
end
|
19
|
+
@target_options = default_options(args[:target_options]) do |o|
|
20
|
+
o[:origination_caller_id_number] = args[:caller_id_number] if args[:caller_id_number]
|
21
|
+
o[:origination_caller_id_name] = args[:caller_id_name] if args[:caller_id_name]
|
22
|
+
if o[:timeout]
|
23
|
+
o[:originate_timeout] = o.delete(:timeout)
|
24
|
+
elsif args[:timeout]
|
25
|
+
o[:originate_timeout] = args[:timeout]
|
26
|
+
end
|
27
|
+
o
|
28
|
+
end
|
29
|
+
raise(ArgumentError, "Origination timeout (#{@target_options[:originate_timeout]}) must be a positive integer") unless @target_options[:originate_timeout].to_i > 0
|
12
30
|
|
13
31
|
@fs_socket = fs_socket # This socket must support say and <<
|
14
32
|
@target = args[:target] # The target address to call
|
33
|
+
raise(ArgumentError, "Cannot originate without a :target set") unless @target.to_s.size > 0
|
15
34
|
# The origination endpoint (can be an extension (use a string) or application)
|
16
35
|
@endpoint = args[:endpoint] || args[:application]
|
36
|
+
raise(ArgumentError, "Cannot originate without an :endpoint set") unless @endpoint.to_s.size > 0
|
17
37
|
|
18
|
-
@target_options[:origination_caller_id_number] ||= args[:caller_id_number] || FSR::DEFAULT_CALLER_ID_NUMBER
|
19
|
-
@target_options[:origination_caller_id_name] ||= args[:caller_id_name] || FSR::DEFAULT_CALLER_ID_NAME
|
20
|
-
@target_options[:originate_timeout] ||= args[:timeout] || @target_options[:timeout] || 30
|
21
|
-
@target_options[:ignore_early_media] ||= true
|
22
38
|
end
|
23
39
|
|
24
40
|
# Send the command to the event socket, using bgapi by default.
|
@@ -39,6 +55,7 @@ module FSR
|
|
39
55
|
raise "Invalid endpoint"
|
40
56
|
end
|
41
57
|
end
|
58
|
+
|
42
59
|
end
|
43
60
|
|
44
61
|
register(:originate, Originate)
|
data/lib/fsr/command_socket.rb
CHANGED
data/lib/fsr/listener/inbound.rb
CHANGED
@@ -13,11 +13,16 @@ module FSR
|
|
13
13
|
@auth = args[:auth] || "ClueCon"
|
14
14
|
end
|
15
15
|
|
16
|
+
# post_init is called upon each "new" socket connection
|
16
17
|
def post_init
|
17
18
|
say("auth #{@auth}")
|
18
19
|
say('event plain ALL')
|
19
20
|
end
|
20
21
|
|
22
|
+
# receive_request is the callback method when data is recieved from the socket
|
23
|
+
#
|
24
|
+
# param header headers from standard Header and Content protocol
|
25
|
+
# param content content from standard Header and Content protocol
|
21
26
|
def receive_request(header, content)
|
22
27
|
hash_header = headers_2_hash(header)
|
23
28
|
hash_content = headers_2_hash(content)
|
@@ -29,28 +34,32 @@ module FSR
|
|
29
34
|
on_event(event)
|
30
35
|
end
|
31
36
|
|
37
|
+
# say encapsulates #send_data for the user
|
38
|
+
#
|
39
|
+
# param line Line of text to send to the socket
|
32
40
|
def say(line)
|
33
41
|
send_data("#{line}\r\n\r\n")
|
34
42
|
end
|
35
|
-
|
43
|
+
|
44
|
+
# on_event is the callback method when an event is triggered
|
45
|
+
#
|
46
|
+
# param event The triggered event object
|
47
|
+
# return event The triggered event object
|
36
48
|
def on_event(event)
|
37
49
|
event
|
38
50
|
end
|
39
51
|
|
40
|
-
#
|
41
|
-
#
|
42
|
-
# <b>Parameters</b>
|
43
|
-
# - event : What event to trigger the block on. May be
|
44
|
-
# :CHANNEL_CREATE, :CHANNEL_DESTROY etc
|
45
|
-
# - block : Block to execute
|
52
|
+
# add_event_hook adds an Event to listen for. When that Event is triggered, it will call the defined block
|
46
53
|
#
|
47
|
-
#
|
48
|
-
#
|
54
|
+
# @param event The event to trigger the block on. Examples, :CHANNEL_CREATE, :CHANNEL_DESTROY, etc
|
55
|
+
# @param block The block to execute when the event is triggered
|
49
56
|
def self.add_event_hook(event, &block)
|
50
57
|
HOOKS[event] = block
|
51
58
|
end
|
52
59
|
|
53
|
-
#
|
60
|
+
# del_event_hook removes an Event.
|
61
|
+
#
|
62
|
+
# @param event The event to remove. Examples, :CHANNEL_CREATE, :CHANNEL_DESTROY, etc
|
54
63
|
def self.del_event_hook(event)
|
55
64
|
HOOKS.delete(event)
|
56
65
|
end
|
@@ -20,7 +20,13 @@ module FSR
|
|
20
20
|
"end"
|
21
21
|
].join("\n")
|
22
22
|
|
23
|
-
APPLICATIONS.each
|
23
|
+
APPLICATIONS.each do |app, obj|
|
24
|
+
if obj.const_defined?("SENDMSG_METHOD")
|
25
|
+
module_eval(obj::SENDMSG_METHOD)
|
26
|
+
else
|
27
|
+
module_eval(SENDMSG_METHOD_DEFINITION % app.to_s)
|
28
|
+
end
|
29
|
+
end
|
24
30
|
|
25
31
|
# session_initiated is called when a @session is first created.
|
26
32
|
# Overwrite this in your worker class with the call/channel
|
@@ -66,6 +72,7 @@ module FSR
|
|
66
72
|
|
67
73
|
protected
|
68
74
|
def post_init
|
75
|
+
@read_var = nil
|
69
76
|
@session = nil # holds the session object
|
70
77
|
@queue = [] # Keep track of queue for state machine
|
71
78
|
send_data("connect\n\n")
|
@@ -77,36 +84,86 @@ module FSR
|
|
77
84
|
# to be picked up by #session_initiated or #receive_reply.
|
78
85
|
# If your listener is listening for events, this will also renew your @session
|
79
86
|
# each time you receive a CHANNEL_DATA event.
|
87
|
+
#
|
80
88
|
# @param header The header of the request, as passed by HeaderAndContentProtocol
|
81
89
|
# @param content The content of the request, as passed by HeaderAndContentProtocol
|
82
90
|
#
|
83
91
|
# @return [HeaderAndContentResponse] An EventMachine HeaderAndContentResponse
|
84
92
|
def receive_request(header, content)
|
85
93
|
hash_header = headers_2_hash(header)
|
86
|
-
|
94
|
+
if content.to_s.match(/:/)
|
95
|
+
hash_content = headers_2_hash(content)
|
96
|
+
else
|
97
|
+
hash_content = content
|
98
|
+
end
|
87
99
|
session_header_and_content = HeaderAndContentResponse.new({:headers => hash_header, :content => hash_content})
|
88
100
|
# If we're a new session, call session initiate
|
89
|
-
if
|
90
|
-
|
91
|
-
|
92
|
-
@
|
93
|
-
|
94
|
-
@
|
101
|
+
if session.nil?
|
102
|
+
establish_new_session(session_header_and_content)
|
103
|
+
elsif @uuid_var and session_header_and_content.headers[:content_type] == "api/response"
|
104
|
+
FSR::Log.info("@uuid_var is set => #{session_header_and_content.inspect} : #{content}")
|
105
|
+
r, @uuid_var = session_header_and_content.content.strip, nil
|
106
|
+
@queue.pop.call(r) if @queue.size > 0
|
95
107
|
elsif session_header_and_content.content[:event_name] # If content includes an event_name, it must be a response from an api command
|
96
|
-
|
97
|
-
session_header_and_content = HeaderAndContentResponse.new({:headers => hash_header.merge(hash_content.strip_value_newlines), :content => {}})
|
98
|
-
@session = session_header_and_content
|
99
|
-
@step += 1 if @state.include?(:initiated)
|
100
|
-
@queue.pop.call unless @queue.empty?
|
101
|
-
receive_reply(hash_header)
|
102
|
-
end
|
108
|
+
check_for_updated_session(session_header_and_content, hash_content, hash_header)
|
103
109
|
else
|
104
|
-
|
105
|
-
|
106
|
-
|
110
|
+
update_state_machine(session_header_and_content)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
def queue_pop
|
116
|
+
if @queue.size > 0
|
117
|
+
if @read_var and session.headers[@read_var.to_sym]
|
118
|
+
r, @read_var = @read_var.to_sym, nil
|
119
|
+
@queue.pop.call(session.headers[r])
|
120
|
+
else
|
121
|
+
@queue.pop.call
|
122
|
+
end
|
107
123
|
end
|
108
124
|
end
|
109
125
|
|
126
|
+
private
|
127
|
+
# establish_new_session is called once during a new session.
|
128
|
+
# This establishes and sets up the variables used to hold state.
|
129
|
+
# Called from #receive_request
|
130
|
+
#
|
131
|
+
# @param header_and_content_hash HeaderAndContentProtocol object representing the session
|
132
|
+
#
|
133
|
+
# @return header_and_content_hash HeaderAndContentProtocol object representing the session
|
134
|
+
def establish_new_session(header_and_content_hash)
|
135
|
+
@session = header_and_content_hash
|
136
|
+
@step = 0
|
137
|
+
@state = [:uninitiated]
|
138
|
+
session_initiated
|
139
|
+
@state << :initiated
|
140
|
+
header_and_content_hash
|
141
|
+
end
|
142
|
+
|
143
|
+
# check_for_updated_session is called from #receive_request if a session has already been established
|
144
|
+
# This will overwrite @session with the new variables representing state.
|
145
|
+
#
|
146
|
+
# @param header_and_content_hash HeaderAndContentProtocol object
|
147
|
+
# @param hash_content hash of content from standard Header and Content Protocol
|
148
|
+
# @param hash_header hash of headers from standard Header and Content Protocol
|
149
|
+
def check_for_updated_session(header_and_content_hash, hash_content, hash_header)
|
150
|
+
if header_and_content_hash.content[:event_name].to_s.match(/CHANNEL_DATA/i) # Anytime we see CHANNEL_DATA event, we want to update our @session
|
151
|
+
header_and_content_hash = HeaderAndContentResponse.new({:headers => hash_header.merge(hash_content.strip_value_newlines), :content => {}})
|
152
|
+
@session = header_and_content_hash
|
153
|
+
update_state_machine(header_and_content_hash.headers)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# update_state_machine when called will increment @step,
|
158
|
+
# call the next block from @queue and dispatch the callback method
|
159
|
+
# #receive_reply
|
160
|
+
# @param response_header Response sent to #recieve_reply callback method
|
161
|
+
def update_state_machine(response_header)
|
162
|
+
@step += 1 if @state.include?(:initiated)
|
163
|
+
queue_pop
|
164
|
+
receive_reply(response_header)
|
165
|
+
end
|
166
|
+
|
110
167
|
end
|
111
168
|
end
|
112
169
|
end
|
data/lib/fsr/version.rb
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec/helper'
|
2
|
+
require "fsr/app"
|
3
|
+
FSR::App.load_application("answer")
|
4
|
+
|
5
|
+
describe "Testing FSR::App::Answer" do
|
6
|
+
|
7
|
+
it "answers the incoming call" do
|
8
|
+
ans = FSR::App::Answer.new
|
9
|
+
ans.sendmsg.should == "call-command: execute\nexecute-app-name: answer\n\n"
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|