voiceml 0.9.1 → 0.9.3
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 +4 -4
- data/README.md +27 -16
- data/lib/voiceml/client.rb +76 -15
- data/lib/voiceml/errors.rb +15 -2
- data/lib/voiceml/hosts.rb +62 -0
- data/lib/voiceml/models/accounts.rb +47 -3
- data/lib/voiceml/models/applications.rb +6 -0
- data/lib/voiceml/models/assistants_v1.rb +40 -0
- data/lib/voiceml/models/calls.rb +4 -0
- data/lib/voiceml/models/common.rb +35 -4
- data/lib/voiceml/models/conferences.rb +8 -0
- data/lib/voiceml/models/conversations_v1.rb +131 -29
- data/lib/voiceml/models/diagnostics.rb +33 -12
- data/lib/voiceml/models/exports.rb +58 -0
- data/lib/voiceml/models/incoming_phone_numbers.rb +4 -0
- data/lib/voiceml/models/media.rb +4 -0
- data/lib/voiceml/models/messages.rb +29 -0
- data/lib/voiceml/models/messaging_v1.rb +73 -0
- data/lib/voiceml/models/monitor_v1.rb +54 -0
- data/lib/voiceml/models/notifications.rb +2 -0
- data/lib/voiceml/models/outgoing_caller_ids.rb +6 -0
- data/lib/voiceml/models/payments.rb +2 -0
- data/lib/voiceml/models/pricing.rb +265 -0
- data/lib/voiceml/models/queues.rb +8 -0
- data/lib/voiceml/models/recordings.rb +5 -1
- data/lib/voiceml/models/routes_v2.rb +6 -2
- data/lib/voiceml/models/sip.rb +34 -6
- data/lib/voiceml/models/sip_registrations.rb +49 -0
- data/lib/voiceml/models/siprec.rb +5 -17
- data/lib/voiceml/models/streams.rb +4 -16
- data/lib/voiceml/models/transcriptions.rb +35 -4
- data/lib/voiceml/models/usage_records.rb +48 -0
- data/lib/voiceml/models/user_defined_messages.rb +2 -0
- data/lib/voiceml/models/voice_v1.rb +29 -19
- data/lib/voiceml/request_validator.rb +125 -0
- data/lib/voiceml/resources/accounts.rb +75 -0
- data/lib/voiceml/resources/applications.rb +3 -1
- data/lib/voiceml/resources/assistants_v1.rb +53 -77
- data/lib/voiceml/resources/base.rb +138 -16
- data/lib/voiceml/resources/calls.rb +34 -49
- data/lib/voiceml/resources/conferences.rb +26 -5
- data/lib/voiceml/resources/conversations_v1.rb +147 -172
- data/lib/voiceml/resources/diagnostics.rb +20 -35
- data/lib/voiceml/resources/exports.rb +36 -0
- data/lib/voiceml/resources/incoming_phone_numbers.rb +53 -32
- data/lib/voiceml/resources/messages.rb +79 -15
- data/lib/voiceml/resources/messaging_v1.rb +121 -0
- data/lib/voiceml/resources/monitor_v1.rb +68 -0
- data/lib/voiceml/resources/notifications.rb +1 -1
- data/lib/voiceml/resources/outgoing_caller_ids.rb +77 -0
- data/lib/voiceml/resources/pricing.rb +148 -0
- data/lib/voiceml/resources/queues.rb +6 -5
- data/lib/voiceml/resources/recordings.rb +39 -5
- data/lib/voiceml/resources/routes_v2.rb +10 -4
- data/lib/voiceml/resources/sip.rb +30 -11
- data/lib/voiceml/resources/transcriptions.rb +40 -0
- data/lib/voiceml/resources/usage_records.rb +51 -0
- data/lib/voiceml/resources/voice_v1.rb +31 -38
- data/lib/voiceml/transport.rb +293 -206
- data/lib/voiceml/version.rb +1 -1
- data/lib/voiceml.rb +14 -15
- metadata +23 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 758b40e32d36539e838ab6418d8a978b089ebfebc7754847049e47fb281e4336
|
|
4
|
+
data.tar.gz: 057dc821a03507d3883edc839e9e5e2ead794e162cef2ff76ea6bf1d6f9787d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d88a3dc916f95b965f0899f910a0fee987d85b9d81ee33dc2dec8ce9b3d71178209e7c07999fb599d76cf172ad620b84d144459e2d3cb9ecde7e9a87e2a60dc1
|
|
7
|
+
data.tar.gz: 9aa62aaa635a1e2021ded627867cc6954eb389c16732c67ab9470fad749a6acbe64f448b7bd92dae412798196d024022d83c33ce0203f6718d0158330da8f5d5
|
data/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# 📞 VoiceML Ruby SDK
|
|
2
2
|
|
|
3
|
-
The official Ruby client for the [VoiceML REST API](https://voicetel.com/docs/api/v0.
|
|
3
|
+
The official Ruby client for the [VoiceML REST API](https://voicetel.com/docs/api/v0.9/voiceml/) — Twilio-compatible outbound voice and answering-machine-detection from VoiceTel, with idiomatic snake_case kwargs, structured errors, and a Twilio-compatible wire format.
|
|
4
4
|
|
|
5
|
-

|
|
6
|
+

|
|
7
7
|

|
|
8
|
-

|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
## 📚 Table of Contents
|
|
@@ -44,15 +44,22 @@ The official Ruby client for the [VoiceML REST API](https://voicetel.com/docs/ap
|
|
|
44
44
|
- **Queues** — create, list, update, delete, peek, dequeue (front or specific member).
|
|
45
45
|
- **Applications** — CRUD on stored TwiML + callback bundles.
|
|
46
46
|
- **Recordings** — account-wide list, metadata fetch, audio fetch (follows S3 redirect), delete.
|
|
47
|
-
- **Messages** — create, fetch, list (To/From/DateSent filters + pagination), update (Body redaction; Status=canceled), delete.
|
|
47
|
+
- **Messages** — create, fetch, list (To/From/DateSent filters + pagination), update (Body redaction; Status=canceled), delete, delivery feedback (`create_feedback`).
|
|
48
48
|
- **IncomingPhoneNumbers** — list, fetch, update.
|
|
49
49
|
- **Notifications** — fetch, list.
|
|
50
|
+
- **Monitor Alerts** — `client.monitor_alerts` list (severity + date-window filters) and fetch over the `/v1` `meta` envelope.
|
|
51
|
+
- **Accounts** — `client.accounts.fetch` / `update` / `fetch_balance` / `rotate_key` (key rotation returns the new key on `AccountWithKey#api_key`).
|
|
50
52
|
- **SIP** — SIP Trunking: Domains (CRUD), CredentialLists + Credentials (CRUD), IpAccessControlLists + IpAddresses (CRUD), Domain↔ACL/CredentialList mappings (historical, Auth/Calls, Auth/Registrations namespaces).
|
|
51
53
|
- **Routes V2** — Twilio Inbound Processing Region API: `client.routes_v2.sip_domains.fetch(name)` / `update(name, voice_region:, friendly_name:)`.
|
|
54
|
+
- **Messaging Service** — `client.messaging_v1.services` CRUD (`MG...` services), routed automatically at the `messaging.voicetel.com` product host.
|
|
55
|
+
- **Pricing** — read-only `client.pricing.v1` / `client.pricing.v2` for Voice, Messaging, PhoneNumbers, and Trunking country + number lookups.
|
|
52
56
|
- **Diagnostics** — `/health` deep probe, OpenAPI spec.
|
|
53
57
|
|
|
58
|
+
### 🌐 Per-product hosts
|
|
59
|
+
VoiceML mirrors Twilio's product-per-subdomain model. The whole `conversations_v1` group is routed to `conversations.voicetel.com` and `messaging_v1` to `messaging.voicetel.com`, while everything else stays on the default `voiceml.voicetel.com`. These hosts are derived automatically from `base_url`; a non-`voicetel.com` base (e.g. a self-hosted instance) keeps every product on that single host. Override per product with `messaging_base_url:` / `conversations_base_url:` on the client constructor.
|
|
60
|
+
|
|
54
61
|
### 🧪 Tested
|
|
55
|
-
- **
|
|
62
|
+
- **227 examples** with mocked HTTP layer (`webmock`) covering every resource and pagination edge cases — spec drift gets caught at parse time.
|
|
56
63
|
- Integration smoke spec gated by env vars — safe for CI.
|
|
57
64
|
|
|
58
65
|
### 📦 Clean Distribution
|
|
@@ -72,7 +79,7 @@ Or in your `Gemfile`:
|
|
|
72
79
|
gem 'voiceml', '~> 0.7'
|
|
73
80
|
```
|
|
74
81
|
|
|
75
|
-
Requires Ruby 3.
|
|
82
|
+
Requires Ruby 3.1 or later.
|
|
76
83
|
|
|
77
84
|
## 🏁 Quickstart
|
|
78
85
|
|
|
@@ -85,8 +92,8 @@ client = VoiceML::Client.new(
|
|
|
85
92
|
)
|
|
86
93
|
|
|
87
94
|
call = client.calls.create(
|
|
88
|
-
to: '+
|
|
89
|
-
from: '+
|
|
95
|
+
to: '+14155550100',
|
|
96
|
+
from: '+14155550101',
|
|
90
97
|
url: 'https://example.com/twiml',
|
|
91
98
|
machine_detection: 'DetectMessageEnd'
|
|
92
99
|
)
|
|
@@ -100,7 +107,7 @@ end
|
|
|
100
107
|
|
|
101
108
|
## 🔑 Authentication
|
|
102
109
|
|
|
103
|
-
Every endpoint uses **HTTP Basic** with your `AccountSid` as the username and your per-tenant API key as the password —
|
|
110
|
+
Every endpoint uses **HTTP Basic** with your `AccountSid` as the username and your per-tenant API key as the password — the same positions Twilio SDKs use, so existing credential-passing code migrates unchanged. VoiceML issues its own credentials; Twilio-issued credentials do not work here.
|
|
104
111
|
|
|
105
112
|
- **Username** = your `AccountSid` (Twilio-format `AC` + 32 hex chars).
|
|
106
113
|
- **Password** = your per-tenant API key (pass as `api_key:` or, for migration-compatible drop-in, `auth_token:`).
|
|
@@ -110,7 +117,7 @@ client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
|
110
117
|
client.diagnostics.health # uses your AccountSid + key on every call
|
|
111
118
|
```
|
|
112
119
|
|
|
113
|
-
> Don't have credentials yet? See **[voicetel.com/docs/api/v0.
|
|
120
|
+
> Don't have credentials yet? See **[voicetel.com/docs/api/v0.9/voiceml/](https://voicetel.com/docs/api/v0.9/voiceml/)** for issuance and rotation.
|
|
114
121
|
|
|
115
122
|
## 🗺️ Resource Reference
|
|
116
123
|
|
|
@@ -121,9 +128,13 @@ client.diagnostics.health # uses your AccountSid + key on every call
|
|
|
121
128
|
| `client.queues` | create, list, update, delete | peek, dequeue (front or specific member) |
|
|
122
129
|
| `client.applications` | CRUD on TwiML + callback bundles | |
|
|
123
130
|
| `client.recordings` | account-wide list, metadata, audio fetch, delete | follows S3 redirect for audio |
|
|
124
|
-
| `client.messages` | create, fetch, list, update, delete | To/From/DateSent filters; Body redaction; Status=canceled |
|
|
131
|
+
| `client.messages` | create, fetch, list, update, delete, create_feedback | To/From/DateSent filters; Body redaction; Status=canceled; delivery feedback |
|
|
125
132
|
| `client.incoming_phone_numbers` | list, fetch, update | |
|
|
126
133
|
| `client.notifications` | fetch, list | |
|
|
134
|
+
| `client.monitor_alerts` | list, fetch | Monitor v1 Alerts (`/v1` `meta` envelope); severity + date-window filters |
|
|
135
|
+
| `client.accounts` | fetch, update, fetch_balance, rotate_key | rotate_key returns the new API key on `AccountWithKey#api_key` |
|
|
136
|
+
| `client.messaging_v1.services` | create, list, fetch, update, delete | Messaging Service (`MG...`); routed at `messaging.voicetel.com` |
|
|
137
|
+
| `client.pricing.v1` / `.v2` | countries.list / fetch, numbers.fetch | Voice, Messaging, PhoneNumbers, Trunking (read-only, default host) |
|
|
127
138
|
| `client.diagnostics` | `/health`, OpenAPI spec | |
|
|
128
139
|
|
|
129
140
|
Methods accept idiomatic snake_case keyword arguments — they're translated to Twilio's PascalCase wire field names internally:
|
|
@@ -132,8 +143,8 @@ Methods accept idiomatic snake_case keyword arguments — they're translated to
|
|
|
132
143
|
client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
133
144
|
|
|
134
145
|
call = client.calls.create(
|
|
135
|
-
to: '+
|
|
136
|
-
from: '+
|
|
146
|
+
to: '+14155550100',
|
|
147
|
+
from: '+14155550101',
|
|
137
148
|
url: 'https://example.com/twiml'
|
|
138
149
|
)
|
|
139
150
|
|
|
@@ -188,7 +199,7 @@ client.calls.each(status: 'completed', page_size: 200) do |call|
|
|
|
188
199
|
end
|
|
189
200
|
|
|
190
201
|
# Enumerator form — chain with Enumerable methods
|
|
191
|
-
recent = client.messages.each(from: '+
|
|
202
|
+
recent = client.messages.each(from: '+14155550101', page_size: 200).first(50)
|
|
192
203
|
|
|
193
204
|
client.queues.each do |q|
|
|
194
205
|
archive(q) if q.current_size.zero?
|
|
@@ -245,7 +256,7 @@ gem build voiceml.gemspec
|
|
|
245
256
|
|
|
246
257
|
## 📖 API Documentation
|
|
247
258
|
|
|
248
|
-
- **Reference docs:** [voicetel.com/docs/api/v0.
|
|
259
|
+
- **Reference docs:** [voicetel.com/docs/api/v0.9/voiceml/](https://voicetel.com/docs/api/v0.9/voiceml/)
|
|
249
260
|
- **Validator:** [voicetel.com/voiceml/validator/](https://voicetel.com/voiceml/validator/)
|
|
250
261
|
- **SDK catalogue:** [voicetel.com/docs/voiceml-sdks/](https://voicetel.com/docs/voiceml-sdks/)
|
|
251
262
|
- **YARD comments:** every resource method carries `@param` / `@return` docs — `yard doc lib` builds them locally.
|
data/lib/voiceml/client.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'transport'
|
|
4
|
+
require_relative 'hosts'
|
|
4
5
|
require_relative 'resources/calls'
|
|
5
6
|
require_relative 'resources/conferences'
|
|
6
7
|
require_relative 'resources/queues'
|
|
@@ -15,42 +16,73 @@ require_relative 'resources/routes_v2'
|
|
|
15
16
|
require_relative 'resources/voice_v1'
|
|
16
17
|
require_relative 'resources/conversations_v1'
|
|
17
18
|
require_relative 'resources/assistants_v1'
|
|
19
|
+
require_relative 'resources/messaging_v1'
|
|
20
|
+
require_relative 'resources/pricing'
|
|
21
|
+
require_relative 'resources/accounts'
|
|
22
|
+
require_relative 'resources/outgoing_caller_ids'
|
|
23
|
+
require_relative 'resources/exports'
|
|
24
|
+
require_relative 'resources/usage_records'
|
|
25
|
+
require_relative 'resources/transcriptions'
|
|
26
|
+
require_relative 'resources/monitor_v1'
|
|
18
27
|
|
|
19
28
|
module VoiceML
|
|
20
29
|
# Synchronous client for the VoiceML REST API.
|
|
21
30
|
#
|
|
22
31
|
# VoiceML uses HTTP Basic auth: the `account_sid` (Twilio-format `AC` + 32 hex) is the
|
|
23
|
-
# username and `api_key` is the password
|
|
24
|
-
#
|
|
32
|
+
# username and `api_key` is the password, in the same HTTP Basic positions twilio-ruby
|
|
33
|
+
# uses. VoiceML issues its own credentials — the AccountSid plus a VoiceML API key;
|
|
34
|
+
# Twilio-issued credentials do not work here.
|
|
35
|
+
#
|
|
36
|
+
# A client without an `api_key:` is a credential-less diagnostics client: only
|
|
37
|
+
# {VoiceML::DiagnosticsResource} calls work (the spec marks them `security: []`);
|
|
38
|
+
# anything authenticated raises {VoiceML::ConfigurationError}.
|
|
39
|
+
#
|
|
40
|
+
# The client is thread-safe: requests may be issued from any number of threads,
|
|
41
|
+
# each holding its own connection per host.
|
|
25
42
|
#
|
|
26
43
|
# @example
|
|
27
44
|
# client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
28
45
|
# call = client.calls.create(
|
|
29
|
-
# to: '+
|
|
30
|
-
# from: '+
|
|
31
|
-
# url: 'https://example.com/
|
|
46
|
+
# to: '+14155550100',
|
|
47
|
+
# from: '+14155550101',
|
|
48
|
+
# url: 'https://example.com/handler'
|
|
32
49
|
# )
|
|
33
50
|
# puts call.sid, call.status
|
|
34
51
|
class Client
|
|
35
52
|
attr_reader :calls, :conferences, :queues, :applications, :recordings,
|
|
36
53
|
:incoming_phone_numbers, :notifications, :diagnostics, :messages,
|
|
37
|
-
:sip, :routes_v2, :voice_v1, :conversations_v1, :assistants_v1
|
|
54
|
+
:sip, :routes_v2, :voice_v1, :conversations_v1, :assistants_v1,
|
|
55
|
+
:messaging_v1, :pricing, :accounts, :outgoing_caller_ids, :exports,
|
|
56
|
+
:usage_records, :transcriptions, :monitor_alerts
|
|
38
57
|
|
|
39
58
|
# @param account_sid [String] Twilio-format AccountSid (`AC` + 32 hex).
|
|
40
|
-
# @param api_key [String, nil] per-tenant API key. Pass either `api_key:` or
|
|
41
|
-
# Twilio-compatible alias `auth_token:` (not both — `ArgumentError` if you do).
|
|
42
|
-
#
|
|
43
|
-
#
|
|
59
|
+
# @param api_key [String, nil] per-tenant VoiceML API key. Pass either `api_key:` or
|
|
60
|
+
# the Twilio-compatible alias `auth_token:` (not both — `ArgumentError` if you do).
|
|
61
|
+
# Omit both for a credential-less diagnostics-only client.
|
|
62
|
+
# @param auth_token [String, nil] Twilio-compatible alias for `api_key`. Lets
|
|
63
|
+
# credential-passing code written for twilio-ruby keep its call shape; the key
|
|
64
|
+
# itself must be a VoiceML API key.
|
|
44
65
|
# @param base_url [String] server base URL. Defaults to `https://voiceml.voicetel.com`.
|
|
45
|
-
# @param timeout [Numeric] per-request timeout in seconds. Defaults to 30.
|
|
46
|
-
# @param max_retries [Integer] retry attempts
|
|
66
|
+
# @param timeout [Numeric] per-request connect/read/write timeout in seconds. Defaults to 30.
|
|
67
|
+
# @param max_retries [Integer] retry attempts. Idempotent requests (GET/HEAD/PUT/DELETE)
|
|
68
|
+
# retry on 429/5xx and transport errors; POST/PATCH retry only on 429 and
|
|
69
|
+
# connect-phase failures. Defaults to 2.
|
|
47
70
|
# @param user_agent [String, nil] override the `User-Agent` header. Defaults to
|
|
48
71
|
# `"voiceml-ruby/#{VERSION}"`.
|
|
49
|
-
|
|
72
|
+
# @param http_client [Net::HTTP, #call, nil] a `Net::HTTP` used for its own host, or a
|
|
73
|
+
# factory `->(uri) { Net::HTTP.new(uri.host, uri.port) }` supplying every
|
|
74
|
+
# connection. Caller-supplied connections are never finished by the SDK; hosts the
|
|
75
|
+
# object/factory does not cover fall back to SDK-managed connections.
|
|
76
|
+
# @param messaging_base_url [String, nil] override the host for `messaging_v1`. Defaults to
|
|
77
|
+
# the `messaging.*.voicetel.com` subdomain derived from `base_url` (else `base_url` itself).
|
|
78
|
+
# @param conversations_base_url [String, nil] override the host for `conversations_v1`. Defaults
|
|
79
|
+
# to the `conversations.*.voicetel.com` subdomain derived from `base_url` (else `base_url`).
|
|
80
|
+
def initialize(account_sid: nil, api_key: nil, auth_token: nil,
|
|
50
81
|
base_url: Transport::DEFAULT_BASE_URL,
|
|
51
82
|
timeout: Transport::DEFAULT_TIMEOUT,
|
|
52
83
|
max_retries: Transport::DEFAULT_MAX_RETRIES,
|
|
53
|
-
user_agent: nil, http_client: nil
|
|
84
|
+
user_agent: nil, http_client: nil,
|
|
85
|
+
messaging_base_url: nil, conversations_base_url: nil)
|
|
54
86
|
if !api_key.nil? && !auth_token.nil?
|
|
55
87
|
raise ArgumentError, 'pass either api_key: or auth_token:, not both'
|
|
56
88
|
end
|
|
@@ -67,6 +99,16 @@ module VoiceML
|
|
|
67
99
|
http_client: http_client
|
|
68
100
|
)
|
|
69
101
|
|
|
102
|
+
# VoiceML mirrors Twilio's product-per-subdomain model: the whole Conversations group
|
|
103
|
+
# rides `conversations.voicetel.com` and Messaging Service rides `messaging.voicetel.com`,
|
|
104
|
+
# while everything else stays on the default host. See VoiceML::Hosts.
|
|
105
|
+
_default, messaging_host, conversations_host =
|
|
106
|
+
Hosts.resolve_product_base_urls(base_url,
|
|
107
|
+
messaging_base_url: messaging_base_url,
|
|
108
|
+
conversations_base_url: conversations_base_url)
|
|
109
|
+
messaging_transport = ScopedTransport.new(@transport, messaging_host)
|
|
110
|
+
conversations_transport = ScopedTransport.new(@transport, conversations_host)
|
|
111
|
+
|
|
70
112
|
@calls = CallsResource.new(@transport)
|
|
71
113
|
@conferences = ConferencesResource.new(@transport)
|
|
72
114
|
@queues = QueuesResource.new(@transport)
|
|
@@ -79,8 +121,16 @@ module VoiceML
|
|
|
79
121
|
@sip = SipResource.new(@transport)
|
|
80
122
|
@routes_v2 = RoutesV2Resource.new(@transport)
|
|
81
123
|
@voice_v1 = VoiceV1Resource.new(@transport)
|
|
82
|
-
@conversations_v1 = ConversationsV1Resource.new(
|
|
124
|
+
@conversations_v1 = ConversationsV1Resource.new(conversations_transport)
|
|
83
125
|
@assistants_v1 = AssistantsV1Resource.new(@transport)
|
|
126
|
+
@messaging_v1 = MessagingV1Resource.new(messaging_transport)
|
|
127
|
+
@pricing = PricingResource.new(@transport)
|
|
128
|
+
@accounts = AccountsResource.new(@transport)
|
|
129
|
+
@outgoing_caller_ids = OutgoingCallerIdsResource.new(@transport)
|
|
130
|
+
@exports = ExportsResource.new(@transport)
|
|
131
|
+
@usage_records = UsageRecordsResource.new(@transport)
|
|
132
|
+
@transcriptions = TranscriptionsResource.new(@transport)
|
|
133
|
+
@monitor_alerts = MonitorAlertsResource.new(@transport)
|
|
84
134
|
end
|
|
85
135
|
|
|
86
136
|
def account_sid
|
|
@@ -90,5 +140,16 @@ module VoiceML
|
|
|
90
140
|
def base_url
|
|
91
141
|
@transport.base_url
|
|
92
142
|
end
|
|
143
|
+
|
|
144
|
+
# Close SDK-managed idle connections. The client remains usable.
|
|
145
|
+
def close
|
|
146
|
+
@transport.close
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A redacted dump: never includes the API key.
|
|
150
|
+
def inspect
|
|
151
|
+
"#<#{self.class.name} account_sid=#{account_sid} base_url=#{base_url}>"
|
|
152
|
+
end
|
|
153
|
+
alias to_s inspect
|
|
93
154
|
end
|
|
94
155
|
end
|
data/lib/voiceml/errors.rb
CHANGED
|
@@ -43,12 +43,25 @@ module VoiceML
|
|
|
43
43
|
# HTTP 410 — recording audio is no longer available (no local file, no S3 key).
|
|
44
44
|
class GoneError < ApiError; end
|
|
45
45
|
|
|
46
|
-
# HTTP 429 — per-account rate limit exceeded.
|
|
47
|
-
|
|
46
|
+
# HTTP 429 — per-account rate limit exceeded. When the server sends a
|
|
47
|
+
# `Retry-After` header, the parsed delay in seconds is exposed on
|
|
48
|
+
# `#retry_after`.
|
|
49
|
+
class RateLimitError < ApiError
|
|
50
|
+
attr_reader :retry_after
|
|
51
|
+
|
|
52
|
+
def retry_after=(seconds)
|
|
53
|
+
@retry_after = seconds
|
|
54
|
+
end
|
|
55
|
+
end
|
|
48
56
|
|
|
49
57
|
# HTTP 501 — endpoint is mounted as a stub (e.g. UserDefinedMessages).
|
|
50
58
|
class NotImplementedAPIError < ApiError; end
|
|
51
59
|
|
|
60
|
+
# The request could not be delivered: connection failures, TLS errors,
|
|
61
|
+
# timeouts, malformed or refused redirects. The original exception is
|
|
62
|
+
# preserved as `#cause`.
|
|
63
|
+
class ConnectionError < ApiError; end
|
|
64
|
+
|
|
52
65
|
# HTTP 5xx — the server hit an error processing the request.
|
|
53
66
|
class ServerError < ApiError; end
|
|
54
67
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
require_relative 'transport'
|
|
6
|
+
|
|
7
|
+
module VoiceML
|
|
8
|
+
# Per-product host resolution for the VoiceML API.
|
|
9
|
+
#
|
|
10
|
+
# Twilio splits its products across dedicated subdomains (`api.twilio.com`,
|
|
11
|
+
# `conversations.twilio.com`, `messaging.twilio.com`, ...). VoiceML mirrors that
|
|
12
|
+
# shape on `voicetel.com`: the Conversations product answers on
|
|
13
|
+
# `conversations.voicetel.com` and the Messaging Service product on
|
|
14
|
+
# `messaging.voicetel.com`, while everything else stays on the default
|
|
15
|
+
# `voiceml.voicetel.com` host. Conversation Service and Messaging Service share
|
|
16
|
+
# the identical `/v1/Services` path shape, so the *host* is what disambiguates
|
|
17
|
+
# them on the wire.
|
|
18
|
+
#
|
|
19
|
+
# Given the configured `base_url` this module derives the two product hosts by
|
|
20
|
+
# swapping the leftmost `voiceml` label — but only for recognised
|
|
21
|
+
# `*.voicetel.com` hosts. For any other base URL (a self-hosted callBroadcast
|
|
22
|
+
# instance, a test stub, a regional override) the product hosts fall back to the
|
|
23
|
+
# configured host unchanged, so a single-host deployment keeps working. A caller
|
|
24
|
+
# who needs Messaging Service against a custom host points `messaging_base_url`
|
|
25
|
+
# (or `conversations_base_url`) at their own subdomain explicitly.
|
|
26
|
+
module Hosts
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Swap the `voiceml` label of a `*.voicetel.com` host for `product`.
|
|
30
|
+
#
|
|
31
|
+
# Returns `base_url` unchanged when the host is not a `voiceml.*.voicetel.com`
|
|
32
|
+
# style host (e.g. a self-hosted instance), so single-host deployments keep
|
|
33
|
+
# working without special-casing.
|
|
34
|
+
def derive_product_host(base_url, product)
|
|
35
|
+
uri = URI.parse(base_url)
|
|
36
|
+
host = uri.host
|
|
37
|
+
return base_url if host.nil? || !host.end_with?('.voicetel.com')
|
|
38
|
+
|
|
39
|
+
labels = host.split('.')
|
|
40
|
+
idx = labels.index('voiceml')
|
|
41
|
+
return base_url if idx.nil?
|
|
42
|
+
|
|
43
|
+
labels[idx] = product
|
|
44
|
+
uri.host = labels.join('.')
|
|
45
|
+
uri.query = nil
|
|
46
|
+
uri.fragment = nil
|
|
47
|
+
uri.to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Return `[default, messaging, conversations]` base URLs.
|
|
51
|
+
#
|
|
52
|
+
# Explicit overrides win; otherwise each product host is derived from
|
|
53
|
+
# `base_url` (see module docstring). All three are returned without a
|
|
54
|
+
# trailing slash.
|
|
55
|
+
def resolve_product_base_urls(base_url, messaging_base_url: nil, conversations_base_url: nil)
|
|
56
|
+
default = Transport.strip_trailing_slashes(base_url)
|
|
57
|
+
messaging = Transport.strip_trailing_slashes(messaging_base_url || derive_product_host(default, 'messaging'))
|
|
58
|
+
conversations = Transport.strip_trailing_slashes(conversations_base_url || derive_product_host(default, 'conversations'))
|
|
59
|
+
[default, messaging, conversations]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -6,17 +6,18 @@ module VoiceML
|
|
|
6
6
|
# Twilio-compatible Account resource. The voiceml SDK is single-account
|
|
7
7
|
# (the credential's account is implicit on every call), so this model
|
|
8
8
|
# exists primarily to decode `GET/POST /Accounts/{Sid}.json` responses
|
|
9
|
-
#
|
|
10
|
-
# echoes the account record back to the caller.
|
|
9
|
+
# in the Twilio-compatible wire format — useful for migration tooling
|
|
10
|
+
# that echoes the account record back to the caller.
|
|
11
11
|
class Account
|
|
12
12
|
ATTRIBUTES = %w[
|
|
13
|
-
sid friendly_name status type
|
|
13
|
+
sid friendly_name status type owner_account_sid
|
|
14
14
|
date_created date_updated subresource_uris uri
|
|
15
15
|
].freeze
|
|
16
16
|
|
|
17
17
|
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
18
18
|
|
|
19
19
|
def initialize(attrs = {})
|
|
20
|
+
@raw_attributes = attrs
|
|
20
21
|
ATTRIBUTES.each do |field|
|
|
21
22
|
value = attrs.key?(field) ? attrs[field] : attrs[field.to_sym]
|
|
22
23
|
instance_variable_set("@#{field}", value)
|
|
@@ -34,11 +35,13 @@ module VoiceML
|
|
|
34
35
|
# the account's settlement currency. No `sid` field on the wire (the
|
|
35
36
|
# balance is account-scoped, not its own resource).
|
|
36
37
|
class Balance
|
|
38
|
+
include RawAttributes
|
|
37
39
|
ATTRIBUTES = %w[account_sid balance currency].freeze
|
|
38
40
|
|
|
39
41
|
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
40
42
|
|
|
41
43
|
def initialize(attrs = {})
|
|
44
|
+
@raw_attributes = attrs
|
|
42
45
|
ATTRIBUTES.each do |field|
|
|
43
46
|
value = attrs.key?(field) ? attrs[field] : attrs[field.to_sym]
|
|
44
47
|
instance_variable_set("@#{field}", value)
|
|
@@ -51,4 +54,45 @@ module VoiceML
|
|
|
51
54
|
new(hash)
|
|
52
55
|
end
|
|
53
56
|
end
|
|
57
|
+
|
|
58
|
+
# `POST /Accounts/{Sid}/RotateKey` response — the account configuration
|
|
59
|
+
# view plus the freshly generated `api_key`. No read endpoint returns the
|
|
60
|
+
# key, so store it right away; requests signed with the previous key fail
|
|
61
|
+
# with 401 from that point on. The key is secret material and never
|
|
62
|
+
# appears in `inspect` / `to_s` output.
|
|
63
|
+
class AccountWithKey
|
|
64
|
+
include RawAttributes
|
|
65
|
+
ATTRIBUTES = %w[
|
|
66
|
+
sid api_key sip_username name phone_regions allow_cidr disabled
|
|
67
|
+
stt_fs_vendor stt_http_url stt_http_token_set tts_http_url
|
|
68
|
+
tts_http_token_set tts_engine_name tts_default_voice tts_default_language
|
|
69
|
+
stripe_secret_key_set pay_enabled sms_provider sms_allowed_providers
|
|
70
|
+
stt_allowed_providers stt_provider voice_url voice_method
|
|
71
|
+
voice_fallback_url voice_fallback_method aws_bucket aws_region
|
|
72
|
+
aws_prefix aws_access_key_id aws_secret_access_key_set aws_kms_key_id
|
|
73
|
+
created_at updated_at
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
77
|
+
|
|
78
|
+
def initialize(attrs = {})
|
|
79
|
+
@raw_attributes = attrs
|
|
80
|
+
ATTRIBUTES.each do |field|
|
|
81
|
+
value = attrs.key?(field) ? attrs[field] : attrs[field.to_sym]
|
|
82
|
+
instance_variable_set("@#{field}", value)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.from_hash(hash)
|
|
87
|
+
return nil if hash.nil?
|
|
88
|
+
|
|
89
|
+
new(hash)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A redacted dump: never includes the api_key.
|
|
93
|
+
def inspect
|
|
94
|
+
"#<#{self.class.name} sid=#{@sid}>"
|
|
95
|
+
end
|
|
96
|
+
alias to_s inspect
|
|
97
|
+
end
|
|
54
98
|
end
|
|
@@ -5,16 +5,20 @@ require_relative 'common'
|
|
|
5
5
|
module VoiceML
|
|
6
6
|
# Persistent TwiML+callback bundle dispatched by `<Dial><Application>`.
|
|
7
7
|
class Application
|
|
8
|
+
include RawAttributes
|
|
8
9
|
ATTRIBUTES = %w[
|
|
9
10
|
sid account_sid friendly_name api_version voice_url voice_method
|
|
10
11
|
voice_fallback_url voice_fallback_method voice_caller_id_lookup
|
|
11
12
|
status_callback status_callback_method status_callback_event
|
|
13
|
+
message_status_callback sms_status_callback
|
|
14
|
+
sms_url sms_method sms_fallback_url sms_fallback_method
|
|
12
15
|
date_created date_updated uri
|
|
13
16
|
].freeze
|
|
14
17
|
|
|
15
18
|
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
16
19
|
|
|
17
20
|
def initialize(attrs = {})
|
|
21
|
+
@raw_attributes = attrs
|
|
18
22
|
ATTRIBUTES.each do |field|
|
|
19
23
|
value = attrs.key?(field) ? attrs[field] : attrs[field.to_sym]
|
|
20
24
|
instance_variable_set("@#{field}", value)
|
|
@@ -30,11 +34,13 @@ module VoiceML
|
|
|
30
34
|
|
|
31
35
|
# Paginated `GET /Applications` response.
|
|
32
36
|
class ApplicationList
|
|
37
|
+
include RawAttributes
|
|
33
38
|
include Pageable
|
|
34
39
|
|
|
35
40
|
attr_reader :applications
|
|
36
41
|
|
|
37
42
|
def initialize(hash = {})
|
|
43
|
+
@raw_attributes = hash
|
|
38
44
|
assign_page_fields(hash)
|
|
39
45
|
@applications = (hash['applications'] || []).map { |a| Application.from_hash(a) }
|
|
40
46
|
end
|