voiceml 0.9.1 β 0.9.2
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 +13 -6
- data/lib/voiceml/client.rb +24 -3
- data/lib/voiceml/hosts.rb +62 -0
- data/lib/voiceml/models/messaging_v1.rb +40 -0
- data/lib/voiceml/models/pricing.rb +231 -0
- data/lib/voiceml/resources/messaging_v1.rb +87 -0
- data/lib/voiceml/resources/pricing.rb +140 -0
- data/lib/voiceml/transport.rb +44 -4
- data/lib/voiceml/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce6f40c38ff553e748881a1b57f940e977cd24983da928988a7dcc77640cc32d
|
|
4
|
+
data.tar.gz: d6d26a2ab6ee10d9df572f24c6b667cb87cb03bb68ef40234fb05308db3d3789
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28fe625cbe02ccd47894507e8c28d8eecd06f0caae4aea040f5e21af82980072e39a74aa7cb788fc885f9b24e58239099a87dac639a79b9f3a3c248769e0a652
|
|
7
|
+
data.tar.gz: 85fd838a671b848768a791d590fbcdefa40c49e89d80b8f2c2c98f475af7035489a727ca3d1ecd7e5988296a5a62f32e3784344fc9fcc11f24beec16d5d9619e
|
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
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
## π Table of Contents
|
|
@@ -49,10 +49,15 @@ The official Ruby client for the [VoiceML REST API](https://voicetel.com/docs/ap
|
|
|
49
49
|
- **Notifications** β fetch, list.
|
|
50
50
|
- **SIP** β SIP Trunking: Domains (CRUD), CredentialLists + Credentials (CRUD), IpAccessControlLists + IpAddresses (CRUD), DomainβACL/CredentialList mappings (historical, Auth/Calls, Auth/Registrations namespaces).
|
|
51
51
|
- **Routes V2** β Twilio Inbound Processing Region API: `client.routes_v2.sip_domains.fetch(name)` / `update(name, voice_region:, friendly_name:)`.
|
|
52
|
+
- **Messaging Service** β `client.messaging_v1.services` CRUD (`MG...` services), routed automatically at the `messaging.voicetel.com` product host.
|
|
53
|
+
- **Pricing** β read-only `client.pricing.v1` / `client.pricing.v2` for Voice, Messaging, PhoneNumbers, and Trunking country + number lookups.
|
|
52
54
|
- **Diagnostics** β `/health` deep probe, OpenAPI spec.
|
|
53
55
|
|
|
56
|
+
### π Per-product hosts
|
|
57
|
+
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.
|
|
58
|
+
|
|
54
59
|
### π§ͺ Tested
|
|
55
|
-
- **
|
|
60
|
+
- **188 examples** with mocked HTTP layer (`webmock`) covering every resource and pagination edge cases β spec drift gets caught at parse time.
|
|
56
61
|
- Integration smoke spec gated by env vars β safe for CI.
|
|
57
62
|
|
|
58
63
|
### π¦ Clean Distribution
|
|
@@ -110,7 +115,7 @@ client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
|
110
115
|
client.diagnostics.health # uses your AccountSid + key on every call
|
|
111
116
|
```
|
|
112
117
|
|
|
113
|
-
> Don't have credentials yet? See **[voicetel.com/docs/api/v0.
|
|
118
|
+
> 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
119
|
|
|
115
120
|
## πΊοΈ Resource Reference
|
|
116
121
|
|
|
@@ -124,6 +129,8 @@ client.diagnostics.health # uses your AccountSid + key on every call
|
|
|
124
129
|
| `client.messages` | create, fetch, list, update, delete | To/From/DateSent filters; Body redaction; Status=canceled |
|
|
125
130
|
| `client.incoming_phone_numbers` | list, fetch, update | |
|
|
126
131
|
| `client.notifications` | fetch, list | |
|
|
132
|
+
| `client.messaging_v1.services` | create, list, fetch, update, delete | Messaging Service (`MG...`); routed at `messaging.voicetel.com` |
|
|
133
|
+
| `client.pricing.v1` / `.v2` | countries.list / fetch, numbers.fetch | Voice, Messaging, PhoneNumbers, Trunking (read-only, default host) |
|
|
127
134
|
| `client.diagnostics` | `/health`, OpenAPI spec | |
|
|
128
135
|
|
|
129
136
|
Methods accept idiomatic snake_case keyword arguments β they're translated to Twilio's PascalCase wire field names internally:
|
|
@@ -245,7 +252,7 @@ gem build voiceml.gemspec
|
|
|
245
252
|
|
|
246
253
|
## π API Documentation
|
|
247
254
|
|
|
248
|
-
- **Reference docs:** [voicetel.com/docs/api/v0.
|
|
255
|
+
- **Reference docs:** [voicetel.com/docs/api/v0.9/voiceml/](https://voicetel.com/docs/api/v0.9/voiceml/)
|
|
249
256
|
- **Validator:** [voicetel.com/voiceml/validator/](https://voicetel.com/voiceml/validator/)
|
|
250
257
|
- **SDK catalogue:** [voicetel.com/docs/voiceml-sdks/](https://voicetel.com/docs/voiceml-sdks/)
|
|
251
258
|
- **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,6 +16,8 @@ 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'
|
|
18
21
|
|
|
19
22
|
module VoiceML
|
|
20
23
|
# Synchronous client for the VoiceML REST API.
|
|
@@ -34,7 +37,8 @@ module VoiceML
|
|
|
34
37
|
class Client
|
|
35
38
|
attr_reader :calls, :conferences, :queues, :applications, :recordings,
|
|
36
39
|
:incoming_phone_numbers, :notifications, :diagnostics, :messages,
|
|
37
|
-
:sip, :routes_v2, :voice_v1, :conversations_v1, :assistants_v1
|
|
40
|
+
:sip, :routes_v2, :voice_v1, :conversations_v1, :assistants_v1,
|
|
41
|
+
:messaging_v1, :pricing
|
|
38
42
|
|
|
39
43
|
# @param account_sid [String] Twilio-format AccountSid (`AC` + 32 hex).
|
|
40
44
|
# @param api_key [String, nil] per-tenant API key. Pass either `api_key:` or the
|
|
@@ -46,11 +50,16 @@ module VoiceML
|
|
|
46
50
|
# @param max_retries [Integer] retry attempts for 429/5xx and transport errors. Defaults to 2.
|
|
47
51
|
# @param user_agent [String, nil] override the `User-Agent` header. Defaults to
|
|
48
52
|
# `"voiceml-ruby/#{VERSION}"`.
|
|
53
|
+
# @param messaging_base_url [String, nil] override the host for `messaging_v1`. Defaults to
|
|
54
|
+
# the `messaging.*.voicetel.com` subdomain derived from `base_url` (else `base_url` itself).
|
|
55
|
+
# @param conversations_base_url [String, nil] override the host for `conversations_v1`. Defaults
|
|
56
|
+
# to the `conversations.*.voicetel.com` subdomain derived from `base_url` (else `base_url`).
|
|
49
57
|
def initialize(account_sid:, api_key: nil, auth_token: nil,
|
|
50
58
|
base_url: Transport::DEFAULT_BASE_URL,
|
|
51
59
|
timeout: Transport::DEFAULT_TIMEOUT,
|
|
52
60
|
max_retries: Transport::DEFAULT_MAX_RETRIES,
|
|
53
|
-
user_agent: nil, http_client: nil
|
|
61
|
+
user_agent: nil, http_client: nil,
|
|
62
|
+
messaging_base_url: nil, conversations_base_url: nil)
|
|
54
63
|
if !api_key.nil? && !auth_token.nil?
|
|
55
64
|
raise ArgumentError, 'pass either api_key: or auth_token:, not both'
|
|
56
65
|
end
|
|
@@ -67,6 +76,16 @@ module VoiceML
|
|
|
67
76
|
http_client: http_client
|
|
68
77
|
)
|
|
69
78
|
|
|
79
|
+
# VoiceML mirrors Twilio's product-per-subdomain model: the whole Conversations group
|
|
80
|
+
# rides `conversations.voicetel.com` and Messaging Service rides `messaging.voicetel.com`,
|
|
81
|
+
# while everything else stays on the default host. See VoiceML::Hosts.
|
|
82
|
+
_default, messaging_host, conversations_host =
|
|
83
|
+
Hosts.resolve_product_base_urls(base_url,
|
|
84
|
+
messaging_base_url: messaging_base_url,
|
|
85
|
+
conversations_base_url: conversations_base_url)
|
|
86
|
+
messaging_transport = ScopedTransport.new(@transport, messaging_host)
|
|
87
|
+
conversations_transport = ScopedTransport.new(@transport, conversations_host)
|
|
88
|
+
|
|
70
89
|
@calls = CallsResource.new(@transport)
|
|
71
90
|
@conferences = ConferencesResource.new(@transport)
|
|
72
91
|
@queues = QueuesResource.new(@transport)
|
|
@@ -79,8 +98,10 @@ module VoiceML
|
|
|
79
98
|
@sip = SipResource.new(@transport)
|
|
80
99
|
@routes_v2 = RoutesV2Resource.new(@transport)
|
|
81
100
|
@voice_v1 = VoiceV1Resource.new(@transport)
|
|
82
|
-
@conversations_v1 = ConversationsV1Resource.new(
|
|
101
|
+
@conversations_v1 = ConversationsV1Resource.new(conversations_transport)
|
|
83
102
|
@assistants_v1 = AssistantsV1Resource.new(@transport)
|
|
103
|
+
@messaging_v1 = MessagingV1Resource.new(messaging_transport)
|
|
104
|
+
@pricing = PricingResource.new(@transport)
|
|
84
105
|
end
|
|
85
106
|
|
|
86
107
|
def account_sid
|
|
@@ -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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'common'
|
|
4
|
+
require_relative 'voice_v1' # V1Pageable lives here; Messaging v1 reuses the same `meta` envelope
|
|
5
|
+
|
|
6
|
+
module VoiceML
|
|
7
|
+
# Twilio Messaging v1 (messaging.twilio.com/v1) resources.
|
|
8
|
+
#
|
|
9
|
+
# A Messaging Service (`MG...`) shares the `/v1/Services` path shape with the
|
|
10
|
+
# Conversations Service (`IS...`); the two are disambiguated on the wire by host
|
|
11
|
+
# (`messaging.voicetel.com` vs `conversations.voicetel.com`). This SDK routes
|
|
12
|
+
# `client.messaging_v1.*` at the messaging host automatically β see VoiceML::Hosts.
|
|
13
|
+
|
|
14
|
+
# MessagingService β `MG...`. The feature-toggle fields are accept-and-echo on
|
|
15
|
+
# VoiceML; the service's operative role is gating scheduled sends.
|
|
16
|
+
class MessagingService
|
|
17
|
+
ATTRIBUTES = %w[
|
|
18
|
+
sid account_sid friendly_name date_created date_updated
|
|
19
|
+
inbound_request_url inbound_method fallback_url fallback_method status_callback
|
|
20
|
+
sticky_sender mms_converter smart_encoding scan_message_content
|
|
21
|
+
fallback_to_long_code area_code_geomatch synchronous_validation validity_period
|
|
22
|
+
url usecase use_inbound_webhook_on_number
|
|
23
|
+
].freeze
|
|
24
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
25
|
+
def initialize(attrs = {})
|
|
26
|
+
ATTRIBUTES.each { |f| instance_variable_set("@#{f}", attrs[f] || attrs[f.to_sym]) }
|
|
27
|
+
end
|
|
28
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# List envelope for `GET /v1/Services` on the messaging host.
|
|
32
|
+
class MessagingServiceList
|
|
33
|
+
include V1Pageable
|
|
34
|
+
attr_reader :services
|
|
35
|
+
def initialize(hash = {})
|
|
36
|
+
assign_meta_fields(hash)
|
|
37
|
+
@services = (hash['services'] || []).map { |h| MessagingService.from_hash(h) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'common'
|
|
4
|
+
require_relative 'voice_v1' # V1Pageable lives here; PricingCountriesList reuses the `meta` envelope
|
|
5
|
+
|
|
6
|
+
module VoiceML
|
|
7
|
+
# Twilio Pricing (pricing.twilio.com) v1/v2 response models.
|
|
8
|
+
#
|
|
9
|
+
# VoiceML has no dedicated pricing subdomain, so these ride the default host
|
|
10
|
+
# (`voiceml.voicetel.com`) under `/v1` and `/v2`. All operations are read-only GETs.
|
|
11
|
+
# Every field is permissive/nullable β VoiceML is NANP-only, so a `Countries` list
|
|
12
|
+
# carries exactly one entry and a `Numbers` fetch 404s for non-NANP destinations.
|
|
13
|
+
|
|
14
|
+
# --- Price leaves ---------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
class PricingInboundCallPrice
|
|
17
|
+
ATTRIBUTES = %w[base_price current_price number_type].freeze
|
|
18
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
19
|
+
def initialize(attrs = {})
|
|
20
|
+
ATTRIBUTES.each { |f| instance_variable_set("@#{f}", attrs[f] || attrs[f.to_sym]) }
|
|
21
|
+
end
|
|
22
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class PricingOutboundCallPrice
|
|
26
|
+
ATTRIBUTES = %w[base_price current_price].freeze
|
|
27
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
28
|
+
def initialize(attrs = {})
|
|
29
|
+
ATTRIBUTES.each { |f| instance_variable_set("@#{f}", attrs[f] || attrs[f.to_sym]) }
|
|
30
|
+
end
|
|
31
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class PricingOutboundCallPriceWithOrigin
|
|
35
|
+
attr_reader :origination_prefixes, :base_price, :current_price
|
|
36
|
+
def initialize(attrs = {})
|
|
37
|
+
@origination_prefixes = attrs['origination_prefixes'] || attrs[:origination_prefixes] || []
|
|
38
|
+
@base_price = attrs['base_price'] || attrs[:base_price]
|
|
39
|
+
@current_price = attrs['current_price'] || attrs[:current_price]
|
|
40
|
+
end
|
|
41
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class PricingOutboundPrefixPrice
|
|
45
|
+
attr_reader :prefixes, :base_price, :current_price, :friendly_name
|
|
46
|
+
def initialize(attrs = {})
|
|
47
|
+
@prefixes = attrs['prefixes'] || attrs[:prefixes] || []
|
|
48
|
+
@base_price = attrs['base_price'] || attrs[:base_price]
|
|
49
|
+
@current_price = attrs['current_price'] || attrs[:current_price]
|
|
50
|
+
@friendly_name = attrs['friendly_name'] || attrs[:friendly_name]
|
|
51
|
+
end
|
|
52
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class PricingOutboundPrefixPriceWithOrigin
|
|
56
|
+
attr_reader :origination_prefixes, :destination_prefixes, :base_price, :current_price, :friendly_name
|
|
57
|
+
def initialize(attrs = {})
|
|
58
|
+
@origination_prefixes = attrs['origination_prefixes'] || attrs[:origination_prefixes] || []
|
|
59
|
+
@destination_prefixes = attrs['destination_prefixes'] || attrs[:destination_prefixes] || []
|
|
60
|
+
@base_price = attrs['base_price'] || attrs[:base_price]
|
|
61
|
+
@current_price = attrs['current_price'] || attrs[:current_price]
|
|
62
|
+
@friendly_name = attrs['friendly_name'] || attrs[:friendly_name]
|
|
63
|
+
end
|
|
64
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class PricingOutboundSMSPrice
|
|
68
|
+
attr_reader :carrier, :mcc, :mnc, :prices
|
|
69
|
+
def initialize(attrs = {})
|
|
70
|
+
@carrier = attrs['carrier'] || attrs[:carrier]
|
|
71
|
+
@mcc = attrs['mcc'] || attrs[:mcc]
|
|
72
|
+
@mnc = attrs['mnc'] || attrs[:mnc]
|
|
73
|
+
@prices = (attrs['prices'] || attrs[:prices] || []).map { |h| PricingInboundCallPrice.from_hash(h) }
|
|
74
|
+
end
|
|
75
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class PricingPhoneNumberPrice
|
|
79
|
+
ATTRIBUTES = %w[number_type base_price current_price].freeze
|
|
80
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
81
|
+
def initialize(attrs = {})
|
|
82
|
+
ATTRIBUTES.each { |f| instance_variable_set("@#{f}", attrs[f] || attrs[f.to_sym]) }
|
|
83
|
+
end
|
|
84
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# --- Countries list envelope ----------------------------------------------
|
|
88
|
+
|
|
89
|
+
class PricingCountryRef
|
|
90
|
+
ATTRIBUTES = %w[country iso_country url].freeze
|
|
91
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
92
|
+
def initialize(attrs = {})
|
|
93
|
+
ATTRIBUTES.each { |f| instance_variable_set("@#{f}", attrs[f] || attrs[f.to_sym]) }
|
|
94
|
+
end
|
|
95
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class PricingCountriesList
|
|
99
|
+
include V1Pageable
|
|
100
|
+
attr_reader :countries
|
|
101
|
+
def initialize(hash = {})
|
|
102
|
+
assign_meta_fields(hash)
|
|
103
|
+
@countries = (hash['countries'] || []).map { |h| PricingCountryRef.from_hash(h) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# --- Pricing v1 country / number bodies -----------------------------------
|
|
108
|
+
|
|
109
|
+
class PricingVoiceCountry
|
|
110
|
+
attr_reader :country, :iso_country, :outbound_prefix_prices, :inbound_call_prices, :price_unit, :url
|
|
111
|
+
def initialize(attrs = {})
|
|
112
|
+
@country = attrs['country'] || attrs[:country]
|
|
113
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
114
|
+
@outbound_prefix_prices =
|
|
115
|
+
(attrs['outbound_prefix_prices'] || []).map { |h| PricingOutboundPrefixPrice.from_hash(h) }
|
|
116
|
+
@inbound_call_prices =
|
|
117
|
+
(attrs['inbound_call_prices'] || []).map { |h| PricingInboundCallPrice.from_hash(h) }
|
|
118
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
119
|
+
@url = attrs['url'] || attrs[:url]
|
|
120
|
+
end
|
|
121
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class PricingVoiceNumber
|
|
125
|
+
attr_reader :number, :country, :iso_country, :outbound_call_price, :inbound_call_price, :price_unit, :url
|
|
126
|
+
def initialize(attrs = {})
|
|
127
|
+
@number = attrs['number'] || attrs[:number]
|
|
128
|
+
@country = attrs['country'] || attrs[:country]
|
|
129
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
130
|
+
@outbound_call_price = PricingOutboundCallPrice.from_hash(attrs['outbound_call_price'])
|
|
131
|
+
@inbound_call_price = PricingInboundCallPrice.from_hash(attrs['inbound_call_price'])
|
|
132
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
133
|
+
@url = attrs['url'] || attrs[:url]
|
|
134
|
+
end
|
|
135
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class PricingMessagingCountry
|
|
139
|
+
attr_reader :country, :iso_country, :outbound_sms_prices, :inbound_sms_prices, :price_unit, :url
|
|
140
|
+
def initialize(attrs = {})
|
|
141
|
+
@country = attrs['country'] || attrs[:country]
|
|
142
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
143
|
+
@outbound_sms_prices =
|
|
144
|
+
(attrs['outbound_sms_prices'] || []).map { |h| PricingOutboundSMSPrice.from_hash(h) }
|
|
145
|
+
@inbound_sms_prices =
|
|
146
|
+
(attrs['inbound_sms_prices'] || []).map { |h| PricingInboundCallPrice.from_hash(h) }
|
|
147
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
148
|
+
@url = attrs['url'] || attrs[:url]
|
|
149
|
+
end
|
|
150
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class PricingPhoneNumberCountry
|
|
154
|
+
attr_reader :country, :iso_country, :phone_number_prices, :price_unit, :url
|
|
155
|
+
def initialize(attrs = {})
|
|
156
|
+
@country = attrs['country'] || attrs[:country]
|
|
157
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
158
|
+
@phone_number_prices =
|
|
159
|
+
(attrs['phone_number_prices'] || []).map { |h| PricingPhoneNumberPrice.from_hash(h) }
|
|
160
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
161
|
+
@url = attrs['url'] || attrs[:url]
|
|
162
|
+
end
|
|
163
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# --- Pricing v2 country / number bodies -----------------------------------
|
|
167
|
+
|
|
168
|
+
class PricingVoiceCountryV2
|
|
169
|
+
attr_reader :country, :iso_country, :outbound_prefix_prices, :inbound_call_prices, :price_unit, :url
|
|
170
|
+
def initialize(attrs = {})
|
|
171
|
+
@country = attrs['country'] || attrs[:country]
|
|
172
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
173
|
+
@outbound_prefix_prices =
|
|
174
|
+
(attrs['outbound_prefix_prices'] || []).map { |h| PricingOutboundPrefixPriceWithOrigin.from_hash(h) }
|
|
175
|
+
@inbound_call_prices =
|
|
176
|
+
(attrs['inbound_call_prices'] || []).map { |h| PricingInboundCallPrice.from_hash(h) }
|
|
177
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
178
|
+
@url = attrs['url'] || attrs[:url]
|
|
179
|
+
end
|
|
180
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class PricingVoiceNumberV2
|
|
184
|
+
attr_reader :destination_number, :origination_number, :country, :iso_country,
|
|
185
|
+
:outbound_call_prices, :inbound_call_price, :price_unit, :url
|
|
186
|
+
def initialize(attrs = {})
|
|
187
|
+
@destination_number = attrs['destination_number'] || attrs[:destination_number]
|
|
188
|
+
@origination_number = attrs['origination_number'] || attrs[:origination_number]
|
|
189
|
+
@country = attrs['country'] || attrs[:country]
|
|
190
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
191
|
+
@outbound_call_prices =
|
|
192
|
+
(attrs['outbound_call_prices'] || []).map { |h| PricingOutboundCallPriceWithOrigin.from_hash(h) }
|
|
193
|
+
@inbound_call_price = PricingInboundCallPrice.from_hash(attrs['inbound_call_price'])
|
|
194
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
195
|
+
@url = attrs['url'] || attrs[:url]
|
|
196
|
+
end
|
|
197
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
class PricingTrunkingCountry
|
|
201
|
+
attr_reader :country, :iso_country, :terminating_prefix_prices, :originating_call_prices, :price_unit, :url
|
|
202
|
+
def initialize(attrs = {})
|
|
203
|
+
@country = attrs['country'] || attrs[:country]
|
|
204
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
205
|
+
@terminating_prefix_prices =
|
|
206
|
+
(attrs['terminating_prefix_prices'] || []).map { |h| PricingOutboundPrefixPriceWithOrigin.from_hash(h) }
|
|
207
|
+
@originating_call_prices =
|
|
208
|
+
(attrs['originating_call_prices'] || []).map { |h| PricingInboundCallPrice.from_hash(h) }
|
|
209
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
210
|
+
@url = attrs['url'] || attrs[:url]
|
|
211
|
+
end
|
|
212
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
class PricingTrunkingNumber
|
|
216
|
+
attr_reader :destination_number, :origination_number, :country, :iso_country,
|
|
217
|
+
:terminating_prefix_prices, :originating_call_price, :price_unit, :url
|
|
218
|
+
def initialize(attrs = {})
|
|
219
|
+
@destination_number = attrs['destination_number'] || attrs[:destination_number]
|
|
220
|
+
@origination_number = attrs['origination_number'] || attrs[:origination_number]
|
|
221
|
+
@country = attrs['country'] || attrs[:country]
|
|
222
|
+
@iso_country = attrs['iso_country'] || attrs[:iso_country]
|
|
223
|
+
@terminating_prefix_prices =
|
|
224
|
+
(attrs['terminating_prefix_prices'] || []).map { |h| PricingOutboundPrefixPriceWithOrigin.from_hash(h) }
|
|
225
|
+
@originating_call_price = PricingInboundCallPrice.from_hash(attrs['originating_call_price'])
|
|
226
|
+
@price_unit = attrs['price_unit'] || attrs[:price_unit]
|
|
227
|
+
@url = attrs['url'] || attrs[:url]
|
|
228
|
+
end
|
|
229
|
+
def self.from_hash(h); h.nil? ? nil : new(h); end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../models/messaging_v1'
|
|
4
|
+
|
|
5
|
+
module VoiceML
|
|
6
|
+
# `client.messaging_v1` β Twilio Messaging v1 (messaging.twilio.com/v1).
|
|
7
|
+
# The whole group is routed at the messaging host (`messaging.voicetel.com`) by the
|
|
8
|
+
# client, which is what disambiguates a Messaging Service (`MG...`) from a
|
|
9
|
+
# Conversation Service (`IS...`) β they share the `/v1/Services` path shape.
|
|
10
|
+
class MessagingV1Resource
|
|
11
|
+
attr_reader :services
|
|
12
|
+
|
|
13
|
+
def initialize(transport)
|
|
14
|
+
@services = MessagingV1ServicesResource.new(transport)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Operations on `/v1/Services` at the messaging host.
|
|
19
|
+
#
|
|
20
|
+
# `create` / `list` / `fetch` / `delete` reuse the shared path; `update`
|
|
21
|
+
# (`POST /v1/Services/{sid}`) is unique to Messaging Service.
|
|
22
|
+
class MessagingV1ServicesResource
|
|
23
|
+
SERVICE_FIELDS = {
|
|
24
|
+
'FriendlyName' => :friendly_name,
|
|
25
|
+
'InboundRequestUrl' => :inbound_request_url,
|
|
26
|
+
'InboundMethod' => :inbound_method,
|
|
27
|
+
'FallbackUrl' => :fallback_url,
|
|
28
|
+
'FallbackMethod' => :fallback_method,
|
|
29
|
+
'StatusCallback' => :status_callback,
|
|
30
|
+
'StickySender' => :sticky_sender,
|
|
31
|
+
'MmsConverter' => :mms_converter,
|
|
32
|
+
'SmartEncoding' => :smart_encoding,
|
|
33
|
+
'ScanMessageContent' => :scan_message_content,
|
|
34
|
+
'FallbackToLongCode' => :fallback_to_long_code,
|
|
35
|
+
'AreaCodeGeomatch' => :area_code_geomatch,
|
|
36
|
+
'SynchronousValidation' => :synchronous_validation,
|
|
37
|
+
'ValidityPeriod' => :validity_period,
|
|
38
|
+
'Usecase' => :usecase,
|
|
39
|
+
'UseInboundWebhookOnNumber' => :use_inbound_webhook_on_number
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
def initialize(transport)
|
|
43
|
+
@transport = transport
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create(friendly_name:, **kwargs)
|
|
47
|
+
kwargs[:friendly_name] = friendly_name
|
|
48
|
+
MessagingService.from_hash(
|
|
49
|
+
@transport.request(:post, '/v1/Services', form: build_form(kwargs))
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def list(page_size: nil)
|
|
54
|
+
params = {}
|
|
55
|
+
params['PageSize'] = page_size unless page_size.nil?
|
|
56
|
+
MessagingServiceList.new(@transport.request(:get, '/v1/Services', params: params))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def fetch(sid)
|
|
60
|
+
MessagingService.from_hash(@transport.request(:get, "/v1/Services/#{sid}"))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def update(sid, **kwargs)
|
|
64
|
+
MessagingService.from_hash(
|
|
65
|
+
@transport.request(:post, "/v1/Services/#{sid}", form: build_form(kwargs))
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def delete(sid)
|
|
70
|
+
@transport.request(:delete, "/v1/Services/#{sid}")
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def build_form(kwargs)
|
|
77
|
+
out = {}
|
|
78
|
+
SERVICE_FIELDS.each do |wire, k|
|
|
79
|
+
value = kwargs[k]
|
|
80
|
+
next if value.nil?
|
|
81
|
+
|
|
82
|
+
out[wire] = value
|
|
83
|
+
end
|
|
84
|
+
out
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
require_relative '../models/pricing'
|
|
6
|
+
|
|
7
|
+
module VoiceML
|
|
8
|
+
# `client.pricing` β Twilio Pricing v1/v2 (pricing.twilio.com) surface.
|
|
9
|
+
#
|
|
10
|
+
# Read-only. Served on the default host (VoiceML has no pricing subdomain). Layout:
|
|
11
|
+
#
|
|
12
|
+
# client.pricing.v1.voice.countries.list / fetch
|
|
13
|
+
# client.pricing.v1.voice.numbers.fetch
|
|
14
|
+
# client.pricing.v1.messaging.countries.list / fetch
|
|
15
|
+
# client.pricing.v1.phone_numbers.countries.list / fetch
|
|
16
|
+
# client.pricing.v2.voice.countries.list / fetch
|
|
17
|
+
# client.pricing.v2.voice.numbers.fetch
|
|
18
|
+
# client.pricing.v2.trunking.countries.list / fetch
|
|
19
|
+
# client.pricing.v2.trunking.numbers.fetch
|
|
20
|
+
#
|
|
21
|
+
# Every `countries.list` returns the shared PricingCountriesList envelope; `fetch`
|
|
22
|
+
# returns the product-specific country/number body. Number path segments are
|
|
23
|
+
# URL-encoded (E.164 `+` -> `%2B`).
|
|
24
|
+
class PricingResource
|
|
25
|
+
attr_reader :v1, :v2
|
|
26
|
+
|
|
27
|
+
def initialize(transport)
|
|
28
|
+
@v1 = PricingV1Resource.new(transport)
|
|
29
|
+
@v2 = PricingV2Resource.new(transport)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A pricing product group exposing `.countries` and optionally `.numbers`.
|
|
34
|
+
class PricingProduct
|
|
35
|
+
attr_reader :countries, :numbers
|
|
36
|
+
|
|
37
|
+
def initialize(countries, numbers = nil)
|
|
38
|
+
@countries = countries
|
|
39
|
+
@numbers = numbers
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `.../Countries` list + per-country fetch. `model` is the fetch body class.
|
|
44
|
+
class PricingCountriesResource
|
|
45
|
+
def initialize(transport, base_path, model)
|
|
46
|
+
@transport = transport
|
|
47
|
+
@base = base_path
|
|
48
|
+
@model = model
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def list(page_size: nil)
|
|
52
|
+
params = {}
|
|
53
|
+
params['PageSize'] = page_size unless page_size.nil?
|
|
54
|
+
PricingCountriesList.new(@transport.request(:get, @base, params: params))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fetch(iso_country)
|
|
58
|
+
@model.from_hash(@transport.request(:get, "#{@base}/#{iso_country}"))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# `/v1/Voice/Numbers/{Number}` β single-number fetch.
|
|
63
|
+
class PricingV1VoiceNumbersResource
|
|
64
|
+
def initialize(transport)
|
|
65
|
+
@transport = transport
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fetch(number)
|
|
69
|
+
PricingVoiceNumber.from_hash(
|
|
70
|
+
@transport.request(:get, "/v1/Voice/Numbers/#{URI.encode_www_form_component(number)}")
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `/v2/Voice/Numbers/{Destination}` β origin-aware single-number fetch.
|
|
76
|
+
class PricingV2VoiceNumbersResource
|
|
77
|
+
def initialize(transport)
|
|
78
|
+
@transport = transport
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def fetch(destination_number, origination_number: nil)
|
|
82
|
+
params = {}
|
|
83
|
+
params['OriginationNumber'] = origination_number unless origination_number.nil?
|
|
84
|
+
PricingVoiceNumberV2.from_hash(
|
|
85
|
+
@transport.request(:get, "/v2/Voice/Numbers/#{URI.encode_www_form_component(destination_number)}",
|
|
86
|
+
params: params)
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `/v2/Trunking/Numbers/{Destination}` β origin-aware single-number fetch.
|
|
92
|
+
class PricingV2TrunkingNumbersResource
|
|
93
|
+
def initialize(transport)
|
|
94
|
+
@transport = transport
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def fetch(destination_number, origination_number: nil)
|
|
98
|
+
params = {}
|
|
99
|
+
params['OriginationNumber'] = origination_number unless origination_number.nil?
|
|
100
|
+
PricingTrunkingNumber.from_hash(
|
|
101
|
+
@transport.request(:get, "/v2/Trunking/Numbers/#{URI.encode_www_form_component(destination_number)}",
|
|
102
|
+
params: params)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `client.pricing.v1.*` β Voice, Messaging, PhoneNumbers.
|
|
108
|
+
class PricingV1Resource
|
|
109
|
+
attr_reader :voice, :messaging, :phone_numbers
|
|
110
|
+
|
|
111
|
+
def initialize(transport)
|
|
112
|
+
@voice = PricingProduct.new(
|
|
113
|
+
PricingCountriesResource.new(transport, '/v1/Voice/Countries', PricingVoiceCountry),
|
|
114
|
+
PricingV1VoiceNumbersResource.new(transport)
|
|
115
|
+
)
|
|
116
|
+
@messaging = PricingProduct.new(
|
|
117
|
+
PricingCountriesResource.new(transport, '/v1/Messaging/Countries', PricingMessagingCountry)
|
|
118
|
+
)
|
|
119
|
+
@phone_numbers = PricingProduct.new(
|
|
120
|
+
PricingCountriesResource.new(transport, '/v1/PhoneNumbers/Countries', PricingPhoneNumberCountry)
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# `client.pricing.v2.*` β Voice, Trunking.
|
|
126
|
+
class PricingV2Resource
|
|
127
|
+
attr_reader :voice, :trunking
|
|
128
|
+
|
|
129
|
+
def initialize(transport)
|
|
130
|
+
@voice = PricingProduct.new(
|
|
131
|
+
PricingCountriesResource.new(transport, '/v2/Voice/Countries', PricingVoiceCountryV2),
|
|
132
|
+
PricingV2VoiceNumbersResource.new(transport)
|
|
133
|
+
)
|
|
134
|
+
@trunking = PricingProduct.new(
|
|
135
|
+
PricingCountriesResource.new(transport, '/v2/Trunking/Countries', PricingTrunkingCountry),
|
|
136
|
+
PricingV2TrunkingNumbersResource.new(transport)
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/voiceml/transport.rb
CHANGED
|
@@ -66,10 +66,14 @@ module VoiceML
|
|
|
66
66
|
# Perform a request. Pass `form:` for a form-urlencoded POST body, `json:` for a JSON body,
|
|
67
67
|
# or neither for a plain GET/DELETE. `params:` are query-string params.
|
|
68
68
|
#
|
|
69
|
+
# `base_url:` overrides the client's default host for this one call (used to route product
|
|
70
|
+
# resources at their subdomain β Conversations, Messaging Service). When set, the request
|
|
71
|
+
# goes to an absolute URL so the correct host/SNI is used per request; see `ScopedTransport`.
|
|
72
|
+
#
|
|
69
73
|
# @return [Hash, Array, nil] the parsed JSON body (or `nil` for empty 2xx).
|
|
70
74
|
# @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)
|
|
75
|
+
def request(method, path, params: nil, form: nil, json: nil, base_url: nil)
|
|
76
|
+
uri = build_uri(request_path(path, base_url), params)
|
|
73
77
|
|
|
74
78
|
attempt = 0
|
|
75
79
|
loop do
|
|
@@ -95,8 +99,8 @@ module VoiceML
|
|
|
95
99
|
# redirect that `GET /Recordings/{sid}.wav` issues when audio has been archived.
|
|
96
100
|
#
|
|
97
101
|
# @return [Array(Integer, String, Hash)] status code, response body bytes, header hash.
|
|
98
|
-
def fetch_bytes(path)
|
|
99
|
-
uri = build_uri(path, nil)
|
|
102
|
+
def fetch_bytes(path, base_url: nil)
|
|
103
|
+
uri = build_uri(request_path(path, base_url), nil)
|
|
100
104
|
visited = []
|
|
101
105
|
loop do
|
|
102
106
|
raise ApiError.new('too many redirects', status_code: 0) if visited.length > 5
|
|
@@ -126,6 +130,14 @@ module VoiceML
|
|
|
126
130
|
Errno::EHOSTUNREACH, SocketError, EOFError]
|
|
127
131
|
end
|
|
128
132
|
|
|
133
|
+
# Prefix `path` with a per-request `base_url` override, if any. Leaves absolute paths and
|
|
134
|
+
# the default-host case untouched so `build_uri` handles them as before.
|
|
135
|
+
def request_path(path, base_url)
|
|
136
|
+
return path if base_url.nil? || base_url.empty?
|
|
137
|
+
|
|
138
|
+
"#{base_url}#{path}"
|
|
139
|
+
end
|
|
140
|
+
|
|
129
141
|
def build_uri(path, params)
|
|
130
142
|
uri = if path.start_with?('http://', 'https://')
|
|
131
143
|
URI.parse(path)
|
|
@@ -293,4 +305,32 @@ module VoiceML
|
|
|
293
305
|
[8.0, 0.5 * (2**attempt)].min
|
|
294
306
|
end
|
|
295
307
|
end
|
|
308
|
+
|
|
309
|
+
# A transport view pinned to a specific product base URL.
|
|
310
|
+
#
|
|
311
|
+
# Wraps a real `Transport` and forwards every call, injecting a `base_url` override so an
|
|
312
|
+
# entire resource group lands on its product subdomain (`conversations.voicetel.com` /
|
|
313
|
+
# `messaging.voicetel.com`) without each resource needing to know its own host.
|
|
314
|
+
#
|
|
315
|
+
# @api private
|
|
316
|
+
class ScopedTransport
|
|
317
|
+
attr_reader :base_url
|
|
318
|
+
|
|
319
|
+
def initialize(inner, base_url)
|
|
320
|
+
@inner = inner
|
|
321
|
+
@base_url = Transport.strip_trailing_slashes(base_url)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def account_sid
|
|
325
|
+
@inner.account_sid
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def request(method, path, params: nil, form: nil, json: nil)
|
|
329
|
+
@inner.request(method, path, params: params, form: form, json: json, base_url: @base_url)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def fetch_bytes(path)
|
|
333
|
+
@inner.fetch_bytes(path, base_url: @base_url)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
296
336
|
end
|
data/lib/voiceml/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: voiceml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- VoiceTel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- lib/voiceml.rb
|
|
65
65
|
- lib/voiceml/client.rb
|
|
66
66
|
- lib/voiceml/errors.rb
|
|
67
|
+
- lib/voiceml/hosts.rb
|
|
67
68
|
- lib/voiceml/models/accounts.rb
|
|
68
69
|
- lib/voiceml/models/applications.rb
|
|
69
70
|
- lib/voiceml/models/assistants_v1.rb
|
|
@@ -75,9 +76,11 @@ files:
|
|
|
75
76
|
- lib/voiceml/models/incoming_phone_numbers.rb
|
|
76
77
|
- lib/voiceml/models/media.rb
|
|
77
78
|
- lib/voiceml/models/messages.rb
|
|
79
|
+
- lib/voiceml/models/messaging_v1.rb
|
|
78
80
|
- lib/voiceml/models/notifications.rb
|
|
79
81
|
- lib/voiceml/models/outgoing_caller_ids.rb
|
|
80
82
|
- lib/voiceml/models/payments.rb
|
|
83
|
+
- lib/voiceml/models/pricing.rb
|
|
81
84
|
- lib/voiceml/models/queues.rb
|
|
82
85
|
- lib/voiceml/models/recordings.rb
|
|
83
86
|
- lib/voiceml/models/routes_v2.rb
|
|
@@ -96,7 +99,9 @@ files:
|
|
|
96
99
|
- lib/voiceml/resources/diagnostics.rb
|
|
97
100
|
- lib/voiceml/resources/incoming_phone_numbers.rb
|
|
98
101
|
- lib/voiceml/resources/messages.rb
|
|
102
|
+
- lib/voiceml/resources/messaging_v1.rb
|
|
99
103
|
- lib/voiceml/resources/notifications.rb
|
|
104
|
+
- lib/voiceml/resources/pricing.rb
|
|
100
105
|
- lib/voiceml/resources/queues.rb
|
|
101
106
|
- lib/voiceml/resources/recordings.rb
|
|
102
107
|
- lib/voiceml/resources/routes_v2.rb
|