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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -16
  3. data/lib/voiceml/client.rb +76 -15
  4. data/lib/voiceml/errors.rb +15 -2
  5. data/lib/voiceml/hosts.rb +62 -0
  6. data/lib/voiceml/models/accounts.rb +47 -3
  7. data/lib/voiceml/models/applications.rb +6 -0
  8. data/lib/voiceml/models/assistants_v1.rb +40 -0
  9. data/lib/voiceml/models/calls.rb +4 -0
  10. data/lib/voiceml/models/common.rb +35 -4
  11. data/lib/voiceml/models/conferences.rb +8 -0
  12. data/lib/voiceml/models/conversations_v1.rb +131 -29
  13. data/lib/voiceml/models/diagnostics.rb +33 -12
  14. data/lib/voiceml/models/exports.rb +58 -0
  15. data/lib/voiceml/models/incoming_phone_numbers.rb +4 -0
  16. data/lib/voiceml/models/media.rb +4 -0
  17. data/lib/voiceml/models/messages.rb +29 -0
  18. data/lib/voiceml/models/messaging_v1.rb +73 -0
  19. data/lib/voiceml/models/monitor_v1.rb +54 -0
  20. data/lib/voiceml/models/notifications.rb +2 -0
  21. data/lib/voiceml/models/outgoing_caller_ids.rb +6 -0
  22. data/lib/voiceml/models/payments.rb +2 -0
  23. data/lib/voiceml/models/pricing.rb +265 -0
  24. data/lib/voiceml/models/queues.rb +8 -0
  25. data/lib/voiceml/models/recordings.rb +5 -1
  26. data/lib/voiceml/models/routes_v2.rb +6 -2
  27. data/lib/voiceml/models/sip.rb +34 -6
  28. data/lib/voiceml/models/sip_registrations.rb +49 -0
  29. data/lib/voiceml/models/siprec.rb +5 -17
  30. data/lib/voiceml/models/streams.rb +4 -16
  31. data/lib/voiceml/models/transcriptions.rb +35 -4
  32. data/lib/voiceml/models/usage_records.rb +48 -0
  33. data/lib/voiceml/models/user_defined_messages.rb +2 -0
  34. data/lib/voiceml/models/voice_v1.rb +29 -19
  35. data/lib/voiceml/request_validator.rb +125 -0
  36. data/lib/voiceml/resources/accounts.rb +75 -0
  37. data/lib/voiceml/resources/applications.rb +3 -1
  38. data/lib/voiceml/resources/assistants_v1.rb +53 -77
  39. data/lib/voiceml/resources/base.rb +138 -16
  40. data/lib/voiceml/resources/calls.rb +34 -49
  41. data/lib/voiceml/resources/conferences.rb +26 -5
  42. data/lib/voiceml/resources/conversations_v1.rb +147 -172
  43. data/lib/voiceml/resources/diagnostics.rb +20 -35
  44. data/lib/voiceml/resources/exports.rb +36 -0
  45. data/lib/voiceml/resources/incoming_phone_numbers.rb +53 -32
  46. data/lib/voiceml/resources/messages.rb +79 -15
  47. data/lib/voiceml/resources/messaging_v1.rb +121 -0
  48. data/lib/voiceml/resources/monitor_v1.rb +68 -0
  49. data/lib/voiceml/resources/notifications.rb +1 -1
  50. data/lib/voiceml/resources/outgoing_caller_ids.rb +77 -0
  51. data/lib/voiceml/resources/pricing.rb +148 -0
  52. data/lib/voiceml/resources/queues.rb +6 -5
  53. data/lib/voiceml/resources/recordings.rb +39 -5
  54. data/lib/voiceml/resources/routes_v2.rb +10 -4
  55. data/lib/voiceml/resources/sip.rb +30 -11
  56. data/lib/voiceml/resources/transcriptions.rb +40 -0
  57. data/lib/voiceml/resources/usage_records.rb +51 -0
  58. data/lib/voiceml/resources/voice_v1.rb +31 -38
  59. data/lib/voiceml/transport.rb +293 -206
  60. data/lib/voiceml/version.rb +1 -1
  61. data/lib/voiceml.rb +14 -15
  62. metadata +23 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f65ec54117bee65194bbf24028d00c20d820cee04cc9200472a42b329cd942f8
4
- data.tar.gz: 3e2104d7267e31948f3160b37e527cec42acf4a68cb366b4963435f71e55f8ca
3
+ metadata.gz: 758b40e32d36539e838ab6418d8a978b089ebfebc7754847049e47fb281e4336
4
+ data.tar.gz: 057dc821a03507d3883edc839e9e5e2ead794e162cef2ff76ea6bf1d6f9787d6
5
5
  SHA512:
6
- metadata.gz: dbbe6c38b68861d43ed68d7d3ffa335fb9caf7582fa47b091ca37e29ed3fda725f3b492aab06501c7a61d686c2db1e123fa0fd75e5440ed407a4304646feb19a
7
- data.tar.gz: '09ced634a9e275031687a7d3647795a4c7a287928a8da4ffe5f61babd10c86eee840890e5233552e64ddf4aa68a50319ef42cd2a37afe68975d87cddb16dbe69'
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.8/voiceml/) — Twilio-compatible outbound voice and answering-machine-detection from VoiceTel, with idiomatic snake_case kwargs, structured errors, and a Twilio-compatible wire format.
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
- ![Version](https://img.shields.io/badge/version-0.9.1-blue)
6
- ![Ruby](https://img.shields.io/badge/ruby-3.0%2B-red)
5
+ ![Version](https://img.shields.io/badge/version-0.9.3-blue)
6
+ ![Ruby](https://img.shields.io/badge/ruby-3.1%2B-red)
7
7
  ![License](https://img.shields.io/badge/license-MIT-green)
8
- ![Tests](https://img.shields.io/badge/tests-65%20specs-brightgreen)
8
+ ![Tests](https://img.shields.io/badge/tests-227%20examples-brightgreen)
9
9
  ![Gem](https://img.shields.io/badge/gem-voiceml-blue)
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
- - **81 specs** with mocked HTTP layer (`webmock`) covering every resource and pagination edge cases — spec drift gets caught at parse time.
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.0 or later.
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: '+18005551234',
89
- from: '+18005550000',
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 — identical to Twilio's auth shape, so credentials issued for Twilio code work here unchanged.
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.8/voiceml/](https://voicetel.com/docs/api/v0.8/voiceml/)** for issuance and rotation.
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: '+18005551234',
136
- from: '+18005550000',
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: '+18005550000', page_size: 200).first(50)
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.8/voiceml/](https://voicetel.com/docs/api/v0.8/voiceml/)
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.
@@ -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. Drop-in compatible with the Twilio Ruby SDK
24
- # constructor signature.
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: '+18005551234',
30
- # from: '+18005550000',
31
- # url: 'https://example.com/twiml'
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 the
41
- # Twilio-compatible alias `auth_token:` (not both — `ArgumentError` if you do).
42
- # @param auth_token [String, nil] Twilio-compatible alias for `api_key`. Lets twilio-ruby
43
- # code (`VoiceML::Client.new(account_sid: sid, auth_token: token)`) work unchanged.
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 for 429/5xx and transport errors. Defaults to 2.
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
- def initialize(account_sid:, api_key: nil, auth_token: nil,
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(@transport)
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
@@ -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
- class RateLimitError < ApiError; end
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
- # round-trip with the Twilio shape — useful for migration tooling that
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 auth_token owner_account_sid
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