freeswitch-esl-client 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +40 -0
- data/LICENSE +21 -0
- data/README.md +305 -0
- data/freeswitch-esl-client.gemspec +40 -0
- data/lib/freeswitch-esl/client.rb +382 -0
- data/lib/freeswitch-esl/connection.rb +35 -0
- data/lib/freeswitch-esl/errors.rb +7 -0
- data/lib/freeswitch-esl/outbound/connection.rb +155 -0
- data/lib/freeswitch-esl/outbound/server.rb +65 -0
- data/lib/freeswitch-esl/outbound/session.rb +218 -0
- data/lib/freeswitch-esl/parser.rb +106 -0
- data/lib/freeswitch-esl/response.rb +61 -0
- data/lib/freeswitch-esl/version.rb +3 -0
- data/lib/freeswitch-esl-client.rb +4 -0
- data/lib/freeswitch-esl.rb +25 -0
- metadata +128 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
module FreeswitchESL
|
|
2
|
+
module Outbound
|
|
3
|
+
# A live outbound call handed to us by FreeSWITCH.
|
|
4
|
+
#
|
|
5
|
+
# All the app methods are fire-and-forget: FreeSWITCH queues the commands
|
|
6
|
+
# on this channel and executes them in order, so
|
|
7
|
+
#
|
|
8
|
+
# session.answer
|
|
9
|
+
# session.play('ivr/ivr-welcome.wav')
|
|
10
|
+
# session.hangup
|
|
11
|
+
#
|
|
12
|
+
# behaves as expected. Register event handlers with #on if you need to
|
|
13
|
+
# react to the call (DTMF, bridge, hangup...).
|
|
14
|
+
class Session
|
|
15
|
+
# Events an outbound socket has to subscribe to explicitly; without the
|
|
16
|
+
# `event` command FreeSWITCH delivers nothing.
|
|
17
|
+
DEFAULT_EVENTS = %w[
|
|
18
|
+
CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_BRIDGE CHANNEL_UNBRIDGE
|
|
19
|
+
CHANNEL_HANGUP CHANNEL_HANGUP_COMPLETE CHANNEL_DESTROY CHANNEL_STATE
|
|
20
|
+
CHANNEL_EXECUTE CHANNEL_EXECUTE_COMPLETE DTMF RECORD_STOP PLAYBACK_STOP
|
|
21
|
+
BACKGROUND_JOB CUSTOM
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :uuid, :caller_id_number, :caller_id_name, :destination_number,
|
|
25
|
+
:headers, :connection, :state, :hangup_cause
|
|
26
|
+
|
|
27
|
+
def initialize(conn, headers)
|
|
28
|
+
@connection = conn
|
|
29
|
+
@headers = headers || {}
|
|
30
|
+
@uuid = @headers['Unique-ID']
|
|
31
|
+
@caller_id_number = @headers['Caller-Caller-ID-Number'] || @headers['variable_caller_id_number']
|
|
32
|
+
@caller_id_name = @headers['Caller-Caller-ID-Name'] || @headers['variable_caller_id_name']
|
|
33
|
+
@destination_number = @headers['Caller-Destination-Number'] ||
|
|
34
|
+
@headers['variable_destination_number'] ||
|
|
35
|
+
@headers['Caller-ANI']
|
|
36
|
+
@state = @headers['Channel-State']
|
|
37
|
+
@context = @headers['Caller-Context']
|
|
38
|
+
@answered = false
|
|
39
|
+
@ended = false
|
|
40
|
+
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------- events
|
|
44
|
+
|
|
45
|
+
# session.on('CHANNEL_ANSWER') { |event| ... }
|
|
46
|
+
# session.on('DTMF') { |event| ... }
|
|
47
|
+
# session.on('*') { |event| ... }
|
|
48
|
+
def on(event_name, &block)
|
|
49
|
+
@handlers[event_name.to_s.upcase] << block
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_event(event, _raw_body = nil)
|
|
54
|
+
name = event['Event-Name'].to_s.upcase
|
|
55
|
+
@state = event['Channel-State'] if event['Channel-State']
|
|
56
|
+
|
|
57
|
+
case name
|
|
58
|
+
when 'CHANNEL_ANSWER'
|
|
59
|
+
@answered = true
|
|
60
|
+
when 'CHANNEL_HANGUP_COMPLETE'
|
|
61
|
+
@hangup_cause = event['Hangup-Cause']
|
|
62
|
+
@ended = true
|
|
63
|
+
when 'CHANNEL_DESTROY'
|
|
64
|
+
@ended = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Array(@handlers[name]).each { |handler| safe_call(handler, event) }
|
|
68
|
+
Array(@handlers['*']).each { |handler| safe_call(handler, event) }
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def safe_call(handler, event)
|
|
73
|
+
handler.call(event)
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
connection&.logger&.error "[FS-ESL OUTBOUND] handler error: #{e.class}: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def answered?
|
|
79
|
+
@answered
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ended?
|
|
83
|
+
@ended
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------------- commands
|
|
87
|
+
|
|
88
|
+
def answer
|
|
89
|
+
execute('answer')
|
|
90
|
+
@answered = true
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pre_answer
|
|
95
|
+
execute('pre_answer')
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ring_ready
|
|
100
|
+
execute('ring_ready')
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def hangup(cause = 'NORMAL_CLEARING')
|
|
105
|
+
execute('hangup', cause)
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def play(file)
|
|
110
|
+
execute('playback', file)
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def play_and_get_digits(min = 1, max = 10, tries = 3, timeout = 3000, terminators = '#',
|
|
115
|
+
file = 'silence_stream://10', invalid_file = nil, var_name = 'digits',
|
|
116
|
+
digit_timeout = 3000)
|
|
117
|
+
args = [
|
|
118
|
+
min, max, tries, timeout, terminators, file,
|
|
119
|
+
invalid_file || 'silence_stream://150', var_name, digit_timeout
|
|
120
|
+
].join(' ')
|
|
121
|
+
execute('play_and_get_digits', args)
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def read(min = 1, max = 10, file = 'silence_stream://10', timeout = 3000,
|
|
126
|
+
terminators = '#', var_name = 'digits')
|
|
127
|
+
execute('read', [min, max, file, var_name, timeout, terminators].join(' '))
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def record(file, limit = 3600, silence_thresh = 200, silence_hits = 5)
|
|
132
|
+
execute('record', [file, limit, silence_thresh, silence_hits].join(' '))
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
alias record_file record
|
|
136
|
+
|
|
137
|
+
def speak(text, engine: nil, voice: nil)
|
|
138
|
+
if engine && voice
|
|
139
|
+
execute('speak', "#{engine}|#{voice}|#{text}")
|
|
140
|
+
else
|
|
141
|
+
execute('speak', text)
|
|
142
|
+
end
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def bridge(dialstring)
|
|
147
|
+
execute('bridge', dialstring)
|
|
148
|
+
self
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def transfer(extension, dialplan: 'XML', context: 'default')
|
|
152
|
+
execute('transfer', "#{extension} #{dialplan} #{context}")
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def park
|
|
157
|
+
execute('park')
|
|
158
|
+
self
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def set(name, value)
|
|
162
|
+
execute('set', "#{name}=#{value}")
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def export(name, value)
|
|
167
|
+
execute('export', "#{name}=#{value}")
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def unset(name)
|
|
172
|
+
execute('unset', name)
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def sleep(ms)
|
|
177
|
+
execute('sleep', ms.to_s)
|
|
178
|
+
self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def flush_dtmf
|
|
182
|
+
execute('flush_dtmf')
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def send_dtmf(dtmf)
|
|
187
|
+
execute('send_dtmf', dtmf)
|
|
188
|
+
self
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def execute(app, args = nil, &block)
|
|
192
|
+
connection.execute(app, args, &block)
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def api(command, &block)
|
|
197
|
+
connection.api(command, &block)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def bgapi(command, &block)
|
|
201
|
+
connection.bgapi(command, &block)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def send_command(command)
|
|
205
|
+
connection.send_command(command)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------- helpers
|
|
209
|
+
|
|
210
|
+
def variable(name)
|
|
211
|
+
@headers["variable_#{name}"] || @headers[name]
|
|
212
|
+
end
|
|
213
|
+
alias [] variable
|
|
214
|
+
|
|
215
|
+
def close; end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module FreeswitchESL
|
|
2
|
+
# Parses ESL header blocks ("Key: value" lines) and reassembles complete
|
|
3
|
+
# frames from a byte stream.
|
|
4
|
+
#
|
|
5
|
+
# ESL frames are always:
|
|
6
|
+
#
|
|
7
|
+
# Key: value\n
|
|
8
|
+
# ...
|
|
9
|
+
# \n
|
|
10
|
+
# <body of Content-Length bytes>
|
|
11
|
+
#
|
|
12
|
+
# The body can itself contain blank lines (api/response output, event bodies),
|
|
13
|
+
# so splitting the stream on "\n\n" is not correct — Content-Length must be
|
|
14
|
+
# honoured.
|
|
15
|
+
class Parser
|
|
16
|
+
class << self
|
|
17
|
+
def parse_headers(block)
|
|
18
|
+
headers = {}
|
|
19
|
+
return headers if block.nil?
|
|
20
|
+
|
|
21
|
+
block.split(/\r?\n/).each do |line|
|
|
22
|
+
next if line.empty?
|
|
23
|
+
next unless line.include?(':')
|
|
24
|
+
|
|
25
|
+
key, value = line.split(':', 2)
|
|
26
|
+
headers[key.strip] = value.to_s.strip
|
|
27
|
+
end
|
|
28
|
+
headers
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Splits an event body into [headers, body]. FreeSWITCH events embed their
|
|
32
|
+
# own header block and, for BACKGROUND_JOB, the command output follows a
|
|
33
|
+
# blank line.
|
|
34
|
+
def parse_event(raw)
|
|
35
|
+
raw = raw.to_s
|
|
36
|
+
idx = raw.index("\n\n")
|
|
37
|
+
return [parse_headers(raw), ''] unless idx
|
|
38
|
+
|
|
39
|
+
[parse_headers(raw[0...idx]), raw[(idx + 2)..] || '']
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Backwards compatible helper: parse a single complete frame string.
|
|
43
|
+
def parse_frame(frame)
|
|
44
|
+
frame = frame.to_s
|
|
45
|
+
idx = frame.index("\n\n")
|
|
46
|
+
if idx
|
|
47
|
+
headers = parse_headers(frame[0...idx])
|
|
48
|
+
body = frame[(idx + 2)..] || ''
|
|
49
|
+
else
|
|
50
|
+
headers = parse_headers(frame)
|
|
51
|
+
body = ''
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
headers: headers,
|
|
56
|
+
body: body,
|
|
57
|
+
content_type: headers['Content-Type'] || headers['content-type']
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Incremental ESL frame reassembler.
|
|
64
|
+
#
|
|
65
|
+
# framer = FreeswitchESL::Framer.new
|
|
66
|
+
# framer << data
|
|
67
|
+
# framer.each_frame { |frame| ... }
|
|
68
|
+
class Framer
|
|
69
|
+
def initialize
|
|
70
|
+
@buffer = +''.b
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def <<(data)
|
|
74
|
+
@buffer << data.to_s.b
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def each_frame
|
|
79
|
+
loop do
|
|
80
|
+
header_end = @buffer.index("\n\n")
|
|
81
|
+
break unless header_end
|
|
82
|
+
|
|
83
|
+
headers = Parser.parse_headers(@buffer.byteslice(0, header_end))
|
|
84
|
+
body_start = header_end + 2
|
|
85
|
+
length = headers['Content-Length'].to_i
|
|
86
|
+
length = 0 if length.negative?
|
|
87
|
+
|
|
88
|
+
if length.zero?
|
|
89
|
+
@buffer = @buffer.byteslice(body_start..) || +''.b
|
|
90
|
+
yield({ headers: headers, body: +'', content_type: headers['Content-Type'] })
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
break if @buffer.bytesize < body_start + length
|
|
95
|
+
|
|
96
|
+
body = @buffer.byteslice(body_start, length)
|
|
97
|
+
@buffer = @buffer.byteslice((body_start + length)..) || +''.b
|
|
98
|
+
yield({ headers: headers, body: body, content_type: headers['Content-Type'] })
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def empty?
|
|
103
|
+
@buffer.empty?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module FreeswitchESL
|
|
2
|
+
class Response
|
|
3
|
+
attr_reader :headers, :body, :raw
|
|
4
|
+
|
|
5
|
+
def initialize(headers = {}, body = '', raw = '')
|
|
6
|
+
@headers = headers || {}
|
|
7
|
+
@body = body || ''
|
|
8
|
+
@raw = raw
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# bgapi responses carry the command output in the body and have no
|
|
12
|
+
# Reply-Text header, so both are taken into account.
|
|
13
|
+
def success?
|
|
14
|
+
reply = reply_text.to_s
|
|
15
|
+
return false if reply.start_with?('-')
|
|
16
|
+
return true if reply.start_with?('+OK')
|
|
17
|
+
|
|
18
|
+
!body.to_s.lstrip.start_with?('-')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def error?
|
|
22
|
+
!success?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reply_text
|
|
26
|
+
@headers['Reply-Text'] || @headers['reply-text'] || first_body_line
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def job_uuid
|
|
30
|
+
@headers['Job-UUID'] || @headers['job-uuid']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def event_name
|
|
34
|
+
@headers['Event-Name']
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# UUID returned by `originate` (`+OK <uuid>`) — nil for other commands.
|
|
38
|
+
def uuid
|
|
39
|
+
reply_text.to_s[/\+OK\s+([0-9a-f-]{36})/, 1]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def [](key)
|
|
43
|
+
@headers[key]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The command output (alias of #body, reads better for api/bgapi).
|
|
47
|
+
def output
|
|
48
|
+
body
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_s
|
|
52
|
+
body.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def first_body_line
|
|
58
|
+
body.to_s.lines.first.to_s.strip
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'freeswitch-esl/version'
|
|
2
|
+
require 'freeswitch-esl/errors'
|
|
3
|
+
require 'freeswitch-esl/parser'
|
|
4
|
+
require 'freeswitch-esl/response'
|
|
5
|
+
require 'freeswitch-esl/connection'
|
|
6
|
+
require 'freeswitch-esl/client'
|
|
7
|
+
require 'freeswitch-esl/outbound/connection'
|
|
8
|
+
require 'freeswitch-esl/outbound/session'
|
|
9
|
+
require 'freeswitch-esl/outbound/server'
|
|
10
|
+
|
|
11
|
+
module FreeswitchESL
|
|
12
|
+
class << self
|
|
13
|
+
def new(**options)
|
|
14
|
+
Client.new(**options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def outbound_server(**options, &block)
|
|
18
|
+
Outbound::Server.new(**options, &block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start_outbound_server(**options, &block)
|
|
22
|
+
Outbound::Server.new(**options, &block).start
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: freeswitch-esl-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: eventmachine
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.2'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 1.2.7
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.2'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 1.2.7
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: logger
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.6'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.6'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rake
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '13.0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '13.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rspec
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.0'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3.0'
|
|
74
|
+
description: |
|
|
75
|
+
EventMachine-based client for the FreeSWITCH Event Socket Layer (mod_event_socket).
|
|
76
|
+
Supports inbound mode (connect to port 8021, subscribe to events, run api/bgapi,
|
|
77
|
+
auto-reconnect) and outbound mode (an embedded TCP server that FreeSWITCH dials into
|
|
78
|
+
with the socket application, so call control lives in Ruby instead of dialplan XML).
|
|
79
|
+
Includes a binary-safe ESL frame parser, an originate helper covering FreeSWITCH
|
|
80
|
+
variable scopes and positional arguments, and per-channel outbound sessions.
|
|
81
|
+
email:
|
|
82
|
+
- michael@example.com
|
|
83
|
+
executables: []
|
|
84
|
+
extensions: []
|
|
85
|
+
extra_rdoc_files: []
|
|
86
|
+
files:
|
|
87
|
+
- CHANGELOG.md
|
|
88
|
+
- LICENSE
|
|
89
|
+
- README.md
|
|
90
|
+
- freeswitch-esl-client.gemspec
|
|
91
|
+
- lib/freeswitch-esl-client.rb
|
|
92
|
+
- lib/freeswitch-esl.rb
|
|
93
|
+
- lib/freeswitch-esl/client.rb
|
|
94
|
+
- lib/freeswitch-esl/connection.rb
|
|
95
|
+
- lib/freeswitch-esl/errors.rb
|
|
96
|
+
- lib/freeswitch-esl/outbound/connection.rb
|
|
97
|
+
- lib/freeswitch-esl/outbound/server.rb
|
|
98
|
+
- lib/freeswitch-esl/outbound/session.rb
|
|
99
|
+
- lib/freeswitch-esl/parser.rb
|
|
100
|
+
- lib/freeswitch-esl/response.rb
|
|
101
|
+
- lib/freeswitch-esl/version.rb
|
|
102
|
+
homepage: https://github.com/miclast/freeswitch-esl-client
|
|
103
|
+
licenses:
|
|
104
|
+
- MIT
|
|
105
|
+
metadata:
|
|
106
|
+
homepage_uri: https://github.com/miclast/freeswitch-esl-client
|
|
107
|
+
changelog_uri: https://github.com/miclast/freeswitch-esl-client/blob/main/CHANGELOG.md
|
|
108
|
+
bug_tracker_uri: https://github.com/miclast/freeswitch-esl-client/issues
|
|
109
|
+
documentation_uri: https://www.rubydoc.info/gems/freeswitch-esl-client
|
|
110
|
+
rubygems_mfa_required: 'true'
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '3.2'
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 4.0.23
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: Ruby client for the FreeSWITCH Event Socket Layer (inbound and outbound).
|
|
128
|
+
test_files: []
|