voiceml 0.7.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +265 -0
- data/lib/voiceml/client.rb +83 -0
- data/lib/voiceml/errors.rb +75 -0
- data/lib/voiceml/models/applications.rb +46 -0
- data/lib/voiceml/models/calls.rb +48 -0
- data/lib/voiceml/models/common.rb +64 -0
- data/lib/voiceml/models/conferences.rb +85 -0
- data/lib/voiceml/models/diagnostics.rb +56 -0
- data/lib/voiceml/models/incoming_phone_numbers.rb +59 -0
- data/lib/voiceml/models/messages.rb +56 -0
- data/lib/voiceml/models/payments.rb +96 -0
- data/lib/voiceml/models/queues.rb +82 -0
- data/lib/voiceml/models/recordings.rb +59 -0
- data/lib/voiceml/models/siprec.rb +44 -0
- data/lib/voiceml/models/streams.rb +44 -0
- data/lib/voiceml/models/transcriptions.rb +44 -0
- data/lib/voiceml/resources/applications.rb +62 -0
- data/lib/voiceml/resources/base.rb +38 -0
- data/lib/voiceml/resources/calls.rb +414 -0
- data/lib/voiceml/resources/conferences.rb +178 -0
- data/lib/voiceml/resources/diagnostics.rb +64 -0
- data/lib/voiceml/resources/incoming_phone_numbers.rb +162 -0
- data/lib/voiceml/resources/messages.rb +101 -0
- data/lib/voiceml/resources/notifications.rb +32 -0
- data/lib/voiceml/resources/queues.rb +120 -0
- data/lib/voiceml/resources/recordings.rb +89 -0
- data/lib/voiceml/transport.rb +296 -0
- data/lib/voiceml/version.rb +5 -0
- data/lib/voiceml.rb +27 -0
- metadata +118 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
require_relative 'errors'
|
|
8
|
+
require_relative 'version'
|
|
9
|
+
|
|
10
|
+
module VoiceML
|
|
11
|
+
# HTTP transport for the VoiceML REST API.
|
|
12
|
+
#
|
|
13
|
+
# - Auth: HTTP Basic with `account_sid` (Twilio-format `AC` + 32 hex) as the username and the
|
|
14
|
+
# per-tenant API key as the password. Drop-in compatible with the Twilio SDK constructor.
|
|
15
|
+
# - Wire format: requests are form-urlencoded by default (Twilio convention). The server also
|
|
16
|
+
# accepts JSON; pass `json: <hash>` to send JSON instead. Responses are always JSON.
|
|
17
|
+
# - Retries: 429 + 5xx are retried up to `max_retries` times with exponential backoff,
|
|
18
|
+
# honoring the `Retry-After` header when the server emits one.
|
|
19
|
+
# - Binary fetch: `fetch_bytes` follows the 302 -> S3 redirect that
|
|
20
|
+
# `GET /Recordings/{sid}.wav` issues when audio has been archived. Callers usually only
|
|
21
|
+
# care about the final bytes.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
class Transport
|
|
25
|
+
DEFAULT_BASE_URL = 'https://voiceml.voicetel.com'
|
|
26
|
+
DEFAULT_TIMEOUT = 30
|
|
27
|
+
DEFAULT_MAX_RETRIES = 2
|
|
28
|
+
RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
29
|
+
|
|
30
|
+
# Linear-time trailing-slash strip. Equivalent to `base_url.sub(%r{/+\z}, '')` but
|
|
31
|
+
# without the polynomial-backtracking shape CodeQL flags (`rb/polynomial-redos`).
|
|
32
|
+
def self.strip_trailing_slashes(s)
|
|
33
|
+
i = s.length
|
|
34
|
+
i -= 1 while i.positive? && s.getbyte(i - 1) == 47 # '/'
|
|
35
|
+
i == s.length ? s : s[0, i]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
attr_reader :account_sid, :base_url, :max_retries, :timeout, :user_agent
|
|
39
|
+
|
|
40
|
+
def initialize(account_sid:, api_key:, base_url: DEFAULT_BASE_URL,
|
|
41
|
+
timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES,
|
|
42
|
+
user_agent: nil, http_client: nil)
|
|
43
|
+
raise ConfigurationError, 'account_sid is required' if account_sid.nil? || account_sid.empty?
|
|
44
|
+
raise ConfigurationError, 'api_key is required' if api_key.nil? || api_key.empty?
|
|
45
|
+
raise ConfigurationError, 'max_retries must be >= 0' if max_retries.negative?
|
|
46
|
+
|
|
47
|
+
@account_sid = account_sid
|
|
48
|
+
@api_key = api_key
|
|
49
|
+
@base_url = self.class.strip_trailing_slashes(base_url)
|
|
50
|
+
@timeout = timeout
|
|
51
|
+
@max_retries = max_retries
|
|
52
|
+
@user_agent = user_agent || "voiceml-ruby/#{VoiceML::VERSION}"
|
|
53
|
+
@uri_base = URI.parse(@base_url)
|
|
54
|
+
@conn_mutex = Mutex.new
|
|
55
|
+
@owns_client = http_client.nil?
|
|
56
|
+
@persistent = http_client
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Close the persistent connection, releasing the underlying socket.
|
|
60
|
+
# Safe to call multiple times. The transport remains usable — the next
|
|
61
|
+
# request will transparently open a fresh connection.
|
|
62
|
+
def close
|
|
63
|
+
@conn_mutex.synchronize { finish_connection } if @owns_client
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Perform a request. Pass `form:` for a form-urlencoded POST body, `json:` for a JSON body,
|
|
67
|
+
# or neither for a plain GET/DELETE. `params:` are query-string params.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash, Array, nil] the parsed JSON body (or `nil` for empty 2xx).
|
|
70
|
+
# @raise [VoiceML::ApiError] for non-2xx responses (subclasses by status family).
|
|
71
|
+
def request(method, path, params: nil, form: nil, json: nil)
|
|
72
|
+
uri = build_uri(path, params)
|
|
73
|
+
|
|
74
|
+
attempt = 0
|
|
75
|
+
loop do
|
|
76
|
+
response = perform_request(method, uri, form: form, json: json)
|
|
77
|
+
|
|
78
|
+
if RETRYABLE_STATUSES.include?(response.code.to_i) && attempt < @max_retries
|
|
79
|
+
sleep(backoff_delay(attempt, response))
|
|
80
|
+
attempt += 1
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
return parse_response(response)
|
|
85
|
+
rescue *transport_errors => e
|
|
86
|
+
raise ApiError.new("transport error after #{attempt + 1} attempts: #{e.message}",
|
|
87
|
+
status_code: 0) if attempt >= @max_retries
|
|
88
|
+
|
|
89
|
+
sleep(backoff_delay(attempt))
|
|
90
|
+
attempt += 1
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Fetch a binary payload (audio/wav recordings). Follows the single 302 -> presigned S3
|
|
95
|
+
# redirect that `GET /Recordings/{sid}.wav` issues when audio has been archived.
|
|
96
|
+
#
|
|
97
|
+
# @return [Array(Integer, String, Hash)] status code, response body bytes, header hash.
|
|
98
|
+
def fetch_bytes(path)
|
|
99
|
+
uri = build_uri(path, nil)
|
|
100
|
+
visited = []
|
|
101
|
+
loop do
|
|
102
|
+
raise ApiError.new('too many redirects', status_code: 0) if visited.length > 5
|
|
103
|
+
|
|
104
|
+
req = Net::HTTP::Get.new(uri)
|
|
105
|
+
apply_common_headers(req, auth: !visited.any? { |u| u.host != uri.host })
|
|
106
|
+
response = send_on_persistent(uri, req)
|
|
107
|
+
|
|
108
|
+
case response
|
|
109
|
+
when Net::HTTPRedirection
|
|
110
|
+
visited << uri
|
|
111
|
+
uri = URI.parse(response['location'])
|
|
112
|
+
next
|
|
113
|
+
when Net::HTTPSuccess
|
|
114
|
+
headers = response.each_header.to_h
|
|
115
|
+
return [response.code.to_i, response.body || '', headers]
|
|
116
|
+
else
|
|
117
|
+
raise_for_response(response)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def transport_errors
|
|
125
|
+
[Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET,
|
|
126
|
+
Errno::EHOSTUNREACH, SocketError, EOFError]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_uri(path, params)
|
|
130
|
+
uri = if path.start_with?('http://', 'https://')
|
|
131
|
+
URI.parse(path)
|
|
132
|
+
else
|
|
133
|
+
URI.parse("#{@base_url}#{path}")
|
|
134
|
+
end
|
|
135
|
+
if params && !params.empty?
|
|
136
|
+
encoded = encode_query(params)
|
|
137
|
+
uri.query = encoded unless encoded.empty?
|
|
138
|
+
end
|
|
139
|
+
uri
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def encode_query(params)
|
|
143
|
+
pairs = []
|
|
144
|
+
params.each do |k, v|
|
|
145
|
+
next if v.nil?
|
|
146
|
+
|
|
147
|
+
if v.is_a?(Array)
|
|
148
|
+
v.each { |entry| pairs << [k.to_s, format_scalar(entry)] }
|
|
149
|
+
else
|
|
150
|
+
pairs << [k.to_s, format_scalar(v)]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
URI.encode_www_form(pairs)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def encode_form(form)
|
|
157
|
+
pairs = []
|
|
158
|
+
form.each do |k, v|
|
|
159
|
+
next if v.nil?
|
|
160
|
+
|
|
161
|
+
if v.is_a?(Array)
|
|
162
|
+
v.each { |entry| pairs << [k.to_s, format_scalar(entry)] }
|
|
163
|
+
else
|
|
164
|
+
pairs << [k.to_s, format_scalar(v)]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
URI.encode_www_form(pairs)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def format_scalar(value)
|
|
171
|
+
case value
|
|
172
|
+
when true then 'true'
|
|
173
|
+
when false then 'false'
|
|
174
|
+
else value.to_s
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def perform_request(method, uri, form:, json:)
|
|
179
|
+
req = build_net_request(method, uri)
|
|
180
|
+
apply_common_headers(req, auth: true)
|
|
181
|
+
|
|
182
|
+
if !json.nil?
|
|
183
|
+
req['Content-Type'] = 'application/json'
|
|
184
|
+
req.body = JSON.generate(json)
|
|
185
|
+
elsif !form.nil? && method.to_s.upcase != 'GET' && method.to_s.upcase != 'DELETE'
|
|
186
|
+
req['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
187
|
+
req.body = encode_form(form)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
send_on_persistent(uri, req)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def build_net_request(method, uri)
|
|
194
|
+
case method.to_s.upcase
|
|
195
|
+
when 'GET' then Net::HTTP::Get.new(uri)
|
|
196
|
+
when 'POST' then Net::HTTP::Post.new(uri)
|
|
197
|
+
when 'PUT' then Net::HTTP::Put.new(uri)
|
|
198
|
+
when 'DELETE' then Net::HTTP::Delete.new(uri)
|
|
199
|
+
when 'PATCH' then Net::HTTP::Patch.new(uri)
|
|
200
|
+
else
|
|
201
|
+
raise ArgumentError, "unsupported HTTP method: #{method}"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def apply_common_headers(req, auth:)
|
|
206
|
+
req['Accept'] = 'application/json'
|
|
207
|
+
req['User-Agent'] = @user_agent
|
|
208
|
+
req.basic_auth(@account_sid, @api_key) if auth
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def send_on_persistent(uri, req)
|
|
212
|
+
conn = persistent_connection(uri)
|
|
213
|
+
conn.request(req)
|
|
214
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
215
|
+
@conn_mutex.synchronize { finish_connection }
|
|
216
|
+
persistent_connection(uri).request(req)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def persistent_connection(uri)
|
|
220
|
+
@conn_mutex.synchronize do
|
|
221
|
+
if @persistent&.started? &&
|
|
222
|
+
@persistent.address == uri.host &&
|
|
223
|
+
@persistent.port == uri.port
|
|
224
|
+
return @persistent
|
|
225
|
+
end
|
|
226
|
+
finish_connection
|
|
227
|
+
h = Net::HTTP.new(uri.host, uri.port)
|
|
228
|
+
h.use_ssl = uri.scheme == 'https'
|
|
229
|
+
h.open_timeout = @timeout
|
|
230
|
+
h.read_timeout = @timeout
|
|
231
|
+
h.keep_alive_timeout = 30
|
|
232
|
+
h.start
|
|
233
|
+
@persistent = h
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def finish_connection
|
|
238
|
+
@persistent&.finish rescue nil
|
|
239
|
+
@persistent = nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def parse_response(response)
|
|
243
|
+
status = response.code.to_i
|
|
244
|
+
return nil if status >= 200 && status < 300 && (response.body.nil? || response.body.empty?)
|
|
245
|
+
|
|
246
|
+
if status >= 200 && status < 300
|
|
247
|
+
begin
|
|
248
|
+
return JSON.parse(response.body)
|
|
249
|
+
rescue JSON::ParserError => e
|
|
250
|
+
raise ApiError.new("non-JSON success response (#{e.message}): #{response.body.to_s[0, 200]}",
|
|
251
|
+
status_code: status, body: response.body)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
raise_for_response(response)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def raise_for_response(response)
|
|
259
|
+
status = response.code.to_i
|
|
260
|
+
raw_body = response.body
|
|
261
|
+
body = begin
|
|
262
|
+
raw_body && !raw_body.empty? ? JSON.parse(raw_body) : raw_body
|
|
263
|
+
rescue JSON::ParserError
|
|
264
|
+
raw_body
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
code = nil
|
|
268
|
+
message = "HTTP #{status}"
|
|
269
|
+
more_info = nil
|
|
270
|
+
if body.is_a?(Hash)
|
|
271
|
+
rc = body['code']
|
|
272
|
+
code = rc if rc.is_a?(Integer) || rc.is_a?(String)
|
|
273
|
+
m = body['message']
|
|
274
|
+
message = m if m.is_a?(String) && !m.empty?
|
|
275
|
+
mi = body['more_info']
|
|
276
|
+
more_info = mi if mi.is_a?(String) && !mi.empty?
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
raise VoiceML.error_from_response(status, message, code: code, body: body, more_info: more_info)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def backoff_delay(attempt, response = nil)
|
|
283
|
+
if response
|
|
284
|
+
ra = response['retry-after']
|
|
285
|
+
if ra
|
|
286
|
+
begin
|
|
287
|
+
return [Float(ra), 0.0].max
|
|
288
|
+
rescue ArgumentError, TypeError
|
|
289
|
+
# fall through
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
[8.0, 0.5 * (2**attempt)].min
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
data/lib/voiceml.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Official Ruby SDK for the VoiceML REST API.
|
|
4
|
+
#
|
|
5
|
+
# VoiceML is VoiceTel's outbound voice + AMD service with a Twilio-compatible REST surface
|
|
6
|
+
# (`https://voiceml.voicetel.com`). The wire shape, auth model, error codes, and pagination
|
|
7
|
+
# envelope all match Twilio's documented behaviour — so existing Twilio client patterns
|
|
8
|
+
# map across.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# require 'voiceml'
|
|
12
|
+
#
|
|
13
|
+
# client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
14
|
+
# call = client.calls.create(
|
|
15
|
+
# to: '+18005551234',
|
|
16
|
+
# from: '+18005550000',
|
|
17
|
+
# url: 'https://example.com/twiml',
|
|
18
|
+
# machine_detection: 'DetectMessageEnd'
|
|
19
|
+
# )
|
|
20
|
+
# puts call.sid, call.status
|
|
21
|
+
module VoiceML
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
require_relative 'voiceml/version'
|
|
25
|
+
require_relative 'voiceml/errors'
|
|
26
|
+
require_relative 'voiceml/transport'
|
|
27
|
+
require_relative 'voiceml/client'
|
metadata
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: voiceml
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.7.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- VoiceTel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '13.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '13.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.13'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.13'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: webmock
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.23'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.23'
|
|
55
|
+
description: Twilio-compatible voice + AMD service from VoiceTel
|
|
56
|
+
email:
|
|
57
|
+
- support@voicetel.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- LICENSE
|
|
63
|
+
- README.md
|
|
64
|
+
- lib/voiceml.rb
|
|
65
|
+
- lib/voiceml/client.rb
|
|
66
|
+
- lib/voiceml/errors.rb
|
|
67
|
+
- lib/voiceml/models/applications.rb
|
|
68
|
+
- lib/voiceml/models/calls.rb
|
|
69
|
+
- lib/voiceml/models/common.rb
|
|
70
|
+
- lib/voiceml/models/conferences.rb
|
|
71
|
+
- lib/voiceml/models/diagnostics.rb
|
|
72
|
+
- lib/voiceml/models/incoming_phone_numbers.rb
|
|
73
|
+
- lib/voiceml/models/messages.rb
|
|
74
|
+
- lib/voiceml/models/payments.rb
|
|
75
|
+
- lib/voiceml/models/queues.rb
|
|
76
|
+
- lib/voiceml/models/recordings.rb
|
|
77
|
+
- lib/voiceml/models/siprec.rb
|
|
78
|
+
- lib/voiceml/models/streams.rb
|
|
79
|
+
- lib/voiceml/models/transcriptions.rb
|
|
80
|
+
- lib/voiceml/resources/applications.rb
|
|
81
|
+
- lib/voiceml/resources/base.rb
|
|
82
|
+
- lib/voiceml/resources/calls.rb
|
|
83
|
+
- lib/voiceml/resources/conferences.rb
|
|
84
|
+
- lib/voiceml/resources/diagnostics.rb
|
|
85
|
+
- lib/voiceml/resources/incoming_phone_numbers.rb
|
|
86
|
+
- lib/voiceml/resources/messages.rb
|
|
87
|
+
- lib/voiceml/resources/notifications.rb
|
|
88
|
+
- lib/voiceml/resources/queues.rb
|
|
89
|
+
- lib/voiceml/resources/recordings.rb
|
|
90
|
+
- lib/voiceml/transport.rb
|
|
91
|
+
- lib/voiceml/version.rb
|
|
92
|
+
homepage: https://voiceml.voicetel.com
|
|
93
|
+
licenses:
|
|
94
|
+
- MIT
|
|
95
|
+
metadata:
|
|
96
|
+
documentation_uri: https://voiceml.voicetel.com
|
|
97
|
+
source_code_uri: https://voiceml.voicetel.com
|
|
98
|
+
rubygems_mfa_required: 'true'
|
|
99
|
+
post_install_message:
|
|
100
|
+
rdoc_options: []
|
|
101
|
+
require_paths:
|
|
102
|
+
- lib
|
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '3.0'
|
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '0'
|
|
113
|
+
requirements: []
|
|
114
|
+
rubygems_version: 3.4.19
|
|
115
|
+
signing_key:
|
|
116
|
+
specification_version: 4
|
|
117
|
+
summary: Official Ruby SDK for the VoiceML REST API
|
|
118
|
+
test_files: []
|