voiceml 0.7.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +265 -0
- data/lib/voiceml/client.rb +83 -0
- data/lib/voiceml/errors.rb +75 -0
- data/lib/voiceml/models/applications.rb +46 -0
- data/lib/voiceml/models/calls.rb +48 -0
- data/lib/voiceml/models/common.rb +64 -0
- data/lib/voiceml/models/conferences.rb +85 -0
- data/lib/voiceml/models/diagnostics.rb +56 -0
- data/lib/voiceml/models/incoming_phone_numbers.rb +59 -0
- data/lib/voiceml/models/messages.rb +56 -0
- data/lib/voiceml/models/payments.rb +96 -0
- data/lib/voiceml/models/queues.rb +82 -0
- data/lib/voiceml/models/recordings.rb +59 -0
- data/lib/voiceml/models/siprec.rb +44 -0
- data/lib/voiceml/models/streams.rb +44 -0
- data/lib/voiceml/models/transcriptions.rb +44 -0
- data/lib/voiceml/resources/applications.rb +62 -0
- data/lib/voiceml/resources/base.rb +38 -0
- data/lib/voiceml/resources/calls.rb +414 -0
- data/lib/voiceml/resources/conferences.rb +178 -0
- data/lib/voiceml/resources/diagnostics.rb +64 -0
- data/lib/voiceml/resources/incoming_phone_numbers.rb +162 -0
- data/lib/voiceml/resources/messages.rb +101 -0
- data/lib/voiceml/resources/notifications.rb +32 -0
- data/lib/voiceml/resources/queues.rb +120 -0
- data/lib/voiceml/resources/recordings.rb +89 -0
- data/lib/voiceml/transport.rb +296 -0
- data/lib/voiceml/version.rb +5 -0
- data/lib/voiceml.rb +27 -0
- metadata +118 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7644aa34cf82b78bbc8744567b54be9a00115a4715bb490da3974c28cf40f990
|
|
4
|
+
data.tar.gz: 5133a70ebe35ac5ad190fcb3f2ffb61dfe7197e697ea98bbc78dd672f6c02bfe
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3a5017e8cd3263cb6b32c8c28c8291d5e6f94b71472350fefec20362dc54dfb825195f1693344dca599bf8313cc7a1a16c7ace2d85ee9a733093ec71d1a37b33
|
|
7
|
+
data.tar.gz: cba24d62dbda0ff451f0cd1c23173bf45be879675f6afa86cb9dc3faa98a34583ef144955f180674f60d667a4fb67eab1ec189deee558df655e8d6a152ec12cd
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VoiceTel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# ๐ VoiceML Ruby SDK
|
|
2
|
+
|
|
3
|
+
The official Ruby client for the [VoiceML REST API](https://voicetel.com/docs/api/v0.7/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
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## ๐ Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Features](#-features)
|
|
14
|
+
- [Installation](#-installation)
|
|
15
|
+
- [Quickstart](#-quickstart)
|
|
16
|
+
- [Authentication](#-authentication)
|
|
17
|
+
- [Resource Reference](#-resource-reference)
|
|
18
|
+
- [Error Handling](#-error-handling)
|
|
19
|
+
- [Pagination](#-pagination)
|
|
20
|
+
- [Migration from twilio-ruby](#-migration-from-twilio-ruby)
|
|
21
|
+
- [Rate Limits](#-rate-limits)
|
|
22
|
+
- [Development](#-development)
|
|
23
|
+
- [API Documentation](#-api-documentation)
|
|
24
|
+
- [Contributors](#-contributors)
|
|
25
|
+
- [Sponsors](#-sponsors)
|
|
26
|
+
- [License](#-license)
|
|
27
|
+
|
|
28
|
+
## โจ Features
|
|
29
|
+
|
|
30
|
+
### ๐งฑ Idiomatic Ruby End-to-End
|
|
31
|
+
- **snake_case kwargs everywhere** โ `to:`, `from:`, `machine_detection:`, `status_callback:` โ automatically translated to Twilio's PascalCase wire field names internally.
|
|
32
|
+
- **Resource-style API** โ `client.calls.create(...)`, `client.queues.list`, `client.messages.each` โ flat, predictable, no `account(sid).calls.create(...)` chain.
|
|
33
|
+
- **Twilio-compatible wire shapes** โ `AccountSid`, `From`, `To`, status callbacks, pagination envelopes โ match what Twilio's Programmable Voice API documents, so existing Twilio client patterns map directly.
|
|
34
|
+
|
|
35
|
+
### ๐ Production-Grade Transport
|
|
36
|
+
- **Automatic retry** with exponential backoff on 429 / 5xx and transport errors โ honors `Retry-After` headers.
|
|
37
|
+
- **Configurable timeouts** per client.
|
|
38
|
+
- **HTTP Basic auth** with `AccountSid:ApiKey` โ exactly what the Twilio Ruby SDK uses, so existing credentials work unchanged. Accepts the `auth_token:` keyword as an alias for migration-compatible drop-in.
|
|
39
|
+
- **Structured exception hierarchy** โ `RateLimitError`, `AuthenticationError`, `NotFoundError`, etc. all subclasses of `VoiceML::ApiError` you can rescue broadly or narrowly.
|
|
40
|
+
|
|
41
|
+
### ๐ Complete API Coverage
|
|
42
|
+
- **Calls** โ originate, fetch, terminate, update + per-call recordings, streams, siprec, transcriptions, notifications, events, and the `/Calls/{sid}/Payments` lifecycle (Pay TwiML companion).
|
|
43
|
+
- **Conferences** โ list, fetch, end conferences, plus participants (mute / hold / kick) and conference-scoped recordings.
|
|
44
|
+
- **Queues** โ create, list, update, delete, peek, dequeue (front or specific member).
|
|
45
|
+
- **Applications** โ CRUD on stored TwiML + callback bundles.
|
|
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.
|
|
48
|
+
- **IncomingPhoneNumbers** โ list, fetch, update.
|
|
49
|
+
- **Notifications** โ fetch, list.
|
|
50
|
+
- **Diagnostics** โ `/health` deep probe, OpenAPI spec.
|
|
51
|
+
|
|
52
|
+
### ๐งช Tested
|
|
53
|
+
- **65 specs** with mocked HTTP layer (`webmock`) covering every resource and pagination edge cases โ spec drift gets caught at parse time.
|
|
54
|
+
- Integration smoke spec gated by env vars โ safe for CI.
|
|
55
|
+
|
|
56
|
+
### ๐ฆ Clean Distribution
|
|
57
|
+
- Zero codegen footprint โ every byte hand-written.
|
|
58
|
+
- Pure-Ruby, no native extensions.
|
|
59
|
+
- Ships via [RubyGems](https://rubygems.org/gems/voiceml).
|
|
60
|
+
|
|
61
|
+
## ๐ Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
gem install voiceml
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or in your `Gemfile`:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
gem 'voiceml', '~> 0.7'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Ruby 3.0 or later.
|
|
74
|
+
|
|
75
|
+
## ๐ Quickstart
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
require 'voiceml'
|
|
79
|
+
|
|
80
|
+
client = VoiceML::Client.new(
|
|
81
|
+
account_sid: 'AC00000000000000000000000000000001',
|
|
82
|
+
api_key: ENV.fetch('VOICEML_API_KEY')
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
call = client.calls.create(
|
|
86
|
+
to: '+18005551234',
|
|
87
|
+
from: '+18005550000',
|
|
88
|
+
url: 'https://example.com/twiml',
|
|
89
|
+
machine_detection: 'DetectMessageEnd'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
puts call.sid, call.status
|
|
93
|
+
|
|
94
|
+
client.queues.list.queues.each do |q|
|
|
95
|
+
puts "#{q.friendly_name}: #{q.current_size}"
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## ๐ Authentication
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
- **Username** = your `AccountSid` (Twilio-format `AC` + 32 hex chars).
|
|
104
|
+
- **Password** = your per-tenant API key (pass as `api_key:` or, for migration-compatible drop-in, `auth_token:`).
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
108
|
+
client.diagnostics.health # uses your AccountSid + key on every call
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> Don't have credentials yet? See **[voicetel.com/docs/api/v0.7/voiceml/](https://voicetel.com/docs/api/v0.7/voiceml/)** for issuance and rotation.
|
|
112
|
+
|
|
113
|
+
## ๐บ๏ธ Resource Reference
|
|
114
|
+
|
|
115
|
+
| Resource | Methods | Covers |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `client.calls` | originate, fetch, list, terminate, update | + per-call recordings, streams, siprec, transcriptions, notifications, events, payments |
|
|
118
|
+
| `client.conferences` | list, fetch, end | participants (mute / hold / kick), conference-scoped recordings |
|
|
119
|
+
| `client.queues` | create, list, update, delete | peek, dequeue (front or specific member) |
|
|
120
|
+
| `client.applications` | CRUD on TwiML + callback bundles | |
|
|
121
|
+
| `client.recordings` | account-wide list, metadata, audio fetch, delete | follows S3 redirect for audio |
|
|
122
|
+
| `client.messages` | create, fetch, list, update, delete | To/From/DateSent filters; Body redaction; Status=canceled |
|
|
123
|
+
| `client.incoming_phone_numbers` | list, fetch, update | |
|
|
124
|
+
| `client.notifications` | fetch, list | |
|
|
125
|
+
| `client.diagnostics` | `/health`, OpenAPI spec | |
|
|
126
|
+
|
|
127
|
+
Methods accept idiomatic snake_case keyword arguments โ they're translated to Twilio's PascalCase wire field names internally:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
131
|
+
|
|
132
|
+
call = client.calls.create(
|
|
133
|
+
to: '+18005551234',
|
|
134
|
+
from: '+18005550000',
|
|
135
|
+
url: 'https://example.com/twiml'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# On a live call, open a Pay session:
|
|
139
|
+
session = client.calls.start_payment(
|
|
140
|
+
call.sid,
|
|
141
|
+
idempotency_key: 'order-482917',
|
|
142
|
+
status_callback: 'https://example.com/pay-status'
|
|
143
|
+
)
|
|
144
|
+
puts session.sid, session.status
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## ๐จ Error Handling
|
|
148
|
+
|
|
149
|
+
All errors inherit from `VoiceML::Error`. The `VoiceML::ApiError` family carries the HTTP status, the Twilio-compatible error code, and the parsed response body. Rescue broadly or narrowly:
|
|
150
|
+
|
|
151
|
+
| Status | Exception |
|
|
152
|
+
|--------|-----------|
|
|
153
|
+
| 400 | `BadRequestError` |
|
|
154
|
+
| 401 | `AuthenticationError` |
|
|
155
|
+
| 403 | `PermissionDeniedError` |
|
|
156
|
+
| 404 | `NotFoundError` |
|
|
157
|
+
| 409 | `ConflictError` |
|
|
158
|
+
| 410 | `GoneError` |
|
|
159
|
+
| 429 | `RateLimitError` |
|
|
160
|
+
| 501 | `NotImplementedAPIError` |
|
|
161
|
+
| 5xx | `ServerError` |
|
|
162
|
+
| other | `ApiError` |
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
begin
|
|
166
|
+
client.calls.get('CA00000000000000000000000000000aaa')
|
|
167
|
+
rescue VoiceML::NotFoundError => e
|
|
168
|
+
warn "That call isn't on your account: #{e.message}"
|
|
169
|
+
rescue VoiceML::RateLimitError => e
|
|
170
|
+
warn "Slow down โ status #{e.status_code}, code #{e.code}"
|
|
171
|
+
rescue VoiceML::ApiError => e
|
|
172
|
+
warn "API error #{e.status_code}: #{e.message}"
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The Twilio-compatible error body (`code`, `message`, `more_info`, `status`) is parsed onto `error.code` / `error.message` with the raw payload on `error.body`.
|
|
177
|
+
|
|
178
|
+
## ๐ Pagination
|
|
179
|
+
|
|
180
|
+
List operations return a typed response with a Twilio-compatible pagination envelope (`page`, `page_size`, `next_page_uri`, `previous_page_uri`, โฆ). For `/Calls`, `/Conferences`, `/Queues`, `/Recordings`, and `/Messages`, use the `each` helper to walk every page transparently. Block form yields each item; without a block, returns an `Enumerator`:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# Block form โ yields each Call across all pages
|
|
184
|
+
client.calls.each(status: 'completed', page_size: 200) do |call|
|
|
185
|
+
process(call)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Enumerator form โ chain with Enumerable methods
|
|
189
|
+
recent = client.messages.each(from: '+18005550000', page_size: 200).first(50)
|
|
190
|
+
|
|
191
|
+
client.queues.each do |q|
|
|
192
|
+
archive(q) if q.current_size.zero?
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
For other resources, page manually with `client.<resource>.list(page: n, page_size: 100)`.
|
|
197
|
+
|
|
198
|
+
## ๐ Migration from twilio-ruby
|
|
199
|
+
|
|
200
|
+
The `account_sid` + auth-token pair `Twilio::REST::Client.new` validates in its constructor works unchanged here. The SDK accepts `auth_token:` as a Twilio-compatible alias for `api_key:`:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# Before โ twilio-ruby
|
|
204
|
+
require 'twilio-ruby'
|
|
205
|
+
client = Twilio::REST::Client.new('AC...', '<token>')
|
|
206
|
+
|
|
207
|
+
# After โ voiceml (Twilio-compatible)
|
|
208
|
+
require 'voiceml'
|
|
209
|
+
client = VoiceML::Client.new(account_sid: 'AC...', auth_token: '<api-key>')
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Method names follow the resource map above (`client.calls.create(...)`, `client.queues.list`, โฆ) rather than twilio-ruby's `client.api.v2010.accounts(sid).calls.create(...)` chain โ flatter, fewer keystrokes, same wire format on the way out.
|
|
213
|
+
|
|
214
|
+
## โฑ๏ธ Rate Limits
|
|
215
|
+
|
|
216
|
+
VoiceML applies per-tenant rate limits at the edge. The SDK automatically retries 429 responses with `Retry-After` honored, up to `max_retries` (default `2`). To bump it:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
client = VoiceML::Client.new(
|
|
220
|
+
account_sid: 'AC...',
|
|
221
|
+
api_key: '...',
|
|
222
|
+
max_retries: 4,
|
|
223
|
+
timeout: 60
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## ๐ ๏ธ Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
git clone https://github.com/voicetel/voiceml-ruby-sdk
|
|
231
|
+
cd voiceml-ruby-sdk
|
|
232
|
+
bundle install
|
|
233
|
+
|
|
234
|
+
# Run the full spec suite (fast, no network)
|
|
235
|
+
bundle exec rspec
|
|
236
|
+
|
|
237
|
+
# Run a single file
|
|
238
|
+
bundle exec rspec spec/pagination_spec.rb
|
|
239
|
+
|
|
240
|
+
# Build the gem
|
|
241
|
+
gem build voiceml.gemspec
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## ๐ API Documentation
|
|
245
|
+
|
|
246
|
+
- **Reference docs:** [voicetel.com/docs/api/v0.7/voiceml/](https://voicetel.com/docs/api/v0.7/voiceml/)
|
|
247
|
+
- **Validator:** [voicetel.com/voiceml/validator/](https://voicetel.com/voiceml/validator/)
|
|
248
|
+
- **SDK catalogue:** [voicetel.com/docs/voiceml-sdks/](https://voicetel.com/docs/voiceml-sdks/)
|
|
249
|
+
- **YARD comments:** every resource method carries `@param` / `@return` docs โ `yard doc lib` builds them locally.
|
|
250
|
+
|
|
251
|
+
## ๐ Contributors
|
|
252
|
+
|
|
253
|
+
- [Michael Mavroudis](https://github.com/mavroudis) โ Lead Developer
|
|
254
|
+
|
|
255
|
+
Contributions welcome. Open an issue describing the change you want to make, or send a pull request against `main`.
|
|
256
|
+
|
|
257
|
+
## ๐ Sponsors
|
|
258
|
+
|
|
259
|
+
| Sponsor | Contribution |
|
|
260
|
+
|---------|--------------|
|
|
261
|
+
| [VoiceTel Communications](https://voicetel.com) | Primary development and production hosting |
|
|
262
|
+
|
|
263
|
+
## ๐ License
|
|
264
|
+
|
|
265
|
+
MIT. See [LICENSE](LICENSE) and [voicetel.com/legal/](https://voicetel.com/legal/).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'transport'
|
|
4
|
+
require_relative 'resources/calls'
|
|
5
|
+
require_relative 'resources/conferences'
|
|
6
|
+
require_relative 'resources/queues'
|
|
7
|
+
require_relative 'resources/applications'
|
|
8
|
+
require_relative 'resources/recordings'
|
|
9
|
+
require_relative 'resources/incoming_phone_numbers'
|
|
10
|
+
require_relative 'resources/notifications'
|
|
11
|
+
require_relative 'resources/diagnostics'
|
|
12
|
+
require_relative 'resources/messages'
|
|
13
|
+
|
|
14
|
+
module VoiceML
|
|
15
|
+
# Synchronous client for the VoiceML REST API.
|
|
16
|
+
#
|
|
17
|
+
# VoiceML uses HTTP Basic auth: the `account_sid` (Twilio-format `AC` + 32 hex) is the
|
|
18
|
+
# username and `api_key` is the password. Drop-in compatible with the Twilio Ruby SDK
|
|
19
|
+
# constructor signature.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# client = VoiceML::Client.new(account_sid: 'AC...', api_key: '...')
|
|
23
|
+
# call = client.calls.create(
|
|
24
|
+
# to: '+18005551234',
|
|
25
|
+
# from: '+18005550000',
|
|
26
|
+
# url: 'https://example.com/twiml'
|
|
27
|
+
# )
|
|
28
|
+
# puts call.sid, call.status
|
|
29
|
+
class Client
|
|
30
|
+
attr_reader :calls, :conferences, :queues, :applications, :recordings,
|
|
31
|
+
:incoming_phone_numbers, :notifications, :diagnostics, :messages
|
|
32
|
+
|
|
33
|
+
# @param account_sid [String] Twilio-format AccountSid (`AC` + 32 hex).
|
|
34
|
+
# @param api_key [String, nil] per-tenant API key. Pass either `api_key:` or the
|
|
35
|
+
# Twilio-compatible alias `auth_token:` (not both โ `ArgumentError` if you do).
|
|
36
|
+
# @param auth_token [String, nil] Twilio-compatible alias for `api_key`. Lets twilio-ruby
|
|
37
|
+
# code (`VoiceML::Client.new(account_sid: sid, auth_token: token)`) work unchanged.
|
|
38
|
+
# @param base_url [String] server base URL. Defaults to `https://voiceml.voicetel.com`.
|
|
39
|
+
# @param timeout [Numeric] per-request timeout in seconds. Defaults to 30.
|
|
40
|
+
# @param max_retries [Integer] retry attempts for 429/5xx and transport errors. Defaults to 2.
|
|
41
|
+
# @param user_agent [String, nil] override the `User-Agent` header. Defaults to
|
|
42
|
+
# `"voiceml-ruby/#{VERSION}"`.
|
|
43
|
+
def initialize(account_sid:, api_key: nil, auth_token: nil,
|
|
44
|
+
base_url: Transport::DEFAULT_BASE_URL,
|
|
45
|
+
timeout: Transport::DEFAULT_TIMEOUT,
|
|
46
|
+
max_retries: Transport::DEFAULT_MAX_RETRIES,
|
|
47
|
+
user_agent: nil, http_client: nil)
|
|
48
|
+
if !api_key.nil? && !auth_token.nil?
|
|
49
|
+
raise ArgumentError, 'pass either api_key: or auth_token:, not both'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
resolved_key = api_key || auth_token
|
|
53
|
+
|
|
54
|
+
@transport = Transport.new(
|
|
55
|
+
account_sid: account_sid,
|
|
56
|
+
api_key: resolved_key,
|
|
57
|
+
base_url: base_url,
|
|
58
|
+
timeout: timeout,
|
|
59
|
+
max_retries: max_retries,
|
|
60
|
+
user_agent: user_agent,
|
|
61
|
+
http_client: http_client
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@calls = CallsResource.new(@transport)
|
|
65
|
+
@conferences = ConferencesResource.new(@transport)
|
|
66
|
+
@queues = QueuesResource.new(@transport)
|
|
67
|
+
@applications = ApplicationsResource.new(@transport)
|
|
68
|
+
@recordings = RecordingsResource.new(@transport)
|
|
69
|
+
@incoming_phone_numbers = IncomingPhoneNumbersResource.new(@transport)
|
|
70
|
+
@notifications = NotificationsResource.new(@transport)
|
|
71
|
+
@diagnostics = DiagnosticsResource.new(@transport)
|
|
72
|
+
@messages = MessagesResource.new(@transport)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def account_sid
|
|
76
|
+
@transport.account_sid
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def base_url
|
|
80
|
+
@transport.base_url
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VoiceML
|
|
4
|
+
# Base class for every error raised by this SDK. Catch `VoiceML::Error` to handle them all.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the client is constructed with conflicting or missing config.
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when the API returns a non-2xx response.
|
|
11
|
+
#
|
|
12
|
+
# The Twilio-compatible error body (`{ code, message, more_info, status }`) is parsed into
|
|
13
|
+
# `#code`, `#message`, and `#more_info` when present, with the raw payload exposed on
|
|
14
|
+
# `#body`. `#more_info` is the Twilio documentation URL for the error code (e.g.
|
|
15
|
+
# `https://www.twilio.com/docs/errors/20404`).
|
|
16
|
+
class ApiError < Error
|
|
17
|
+
attr_reader :status_code, :code, :body, :more_info
|
|
18
|
+
|
|
19
|
+
def initialize(message, status_code:, code: nil, body: nil, more_info: nil)
|
|
20
|
+
super(message)
|
|
21
|
+
@status_code = status_code
|
|
22
|
+
@code = code
|
|
23
|
+
@body = body
|
|
24
|
+
@more_info = more_info
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# HTTP 400 โ the request was malformed or failed server-side validation.
|
|
29
|
+
class BadRequestError < ApiError; end
|
|
30
|
+
|
|
31
|
+
# HTTP 401 โ Basic auth missing, account unknown, key wrong, or source IP not allowed.
|
|
32
|
+
class AuthenticationError < ApiError; end
|
|
33
|
+
|
|
34
|
+
# HTTP 403 โ authenticated, but not allowed to perform this action.
|
|
35
|
+
class PermissionDeniedError < ApiError; end
|
|
36
|
+
|
|
37
|
+
# HTTP 404 โ the resource does not exist (or belongs to a different tenant).
|
|
38
|
+
class NotFoundError < ApiError; end
|
|
39
|
+
|
|
40
|
+
# HTTP 409 โ request conflicts with current resource state.
|
|
41
|
+
class ConflictError < ApiError; end
|
|
42
|
+
|
|
43
|
+
# HTTP 410 โ recording audio is no longer available (no local file, no S3 key).
|
|
44
|
+
class GoneError < ApiError; end
|
|
45
|
+
|
|
46
|
+
# HTTP 429 โ per-account rate limit exceeded.
|
|
47
|
+
class RateLimitError < ApiError; end
|
|
48
|
+
|
|
49
|
+
# HTTP 501 โ endpoint is mounted as a stub (e.g. UserDefinedMessages).
|
|
50
|
+
class NotImplementedAPIError < ApiError; end
|
|
51
|
+
|
|
52
|
+
# HTTP 5xx โ the server hit an error processing the request.
|
|
53
|
+
class ServerError < ApiError; end
|
|
54
|
+
|
|
55
|
+
# @api private
|
|
56
|
+
ERROR_CLASSES = {
|
|
57
|
+
400 => BadRequestError,
|
|
58
|
+
401 => AuthenticationError,
|
|
59
|
+
403 => PermissionDeniedError,
|
|
60
|
+
404 => NotFoundError,
|
|
61
|
+
409 => ConflictError,
|
|
62
|
+
410 => GoneError,
|
|
63
|
+
429 => RateLimitError,
|
|
64
|
+
501 => NotImplementedAPIError
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# Map an HTTP status to the most specific `ApiError` subclass.
|
|
68
|
+
# @api private
|
|
69
|
+
def self.error_from_response(status_code, message, code: nil, body: nil, more_info: nil)
|
|
70
|
+
klass = ERROR_CLASSES[status_code]
|
|
71
|
+
klass ||= ServerError if status_code >= 500 && status_code < 600
|
|
72
|
+
klass ||= ApiError
|
|
73
|
+
klass.new(message, status_code: status_code, code: code, body: body, more_info: more_info)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'common'
|
|
4
|
+
|
|
5
|
+
module VoiceML
|
|
6
|
+
# Persistent TwiML+callback bundle dispatched by `<Dial><Application>`.
|
|
7
|
+
class Application
|
|
8
|
+
ATTRIBUTES = %w[
|
|
9
|
+
sid account_sid friendly_name api_version voice_url voice_method
|
|
10
|
+
voice_fallback_url voice_fallback_method voice_caller_id_lookup
|
|
11
|
+
status_callback status_callback_method status_callback_event
|
|
12
|
+
date_created date_updated uri
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
16
|
+
|
|
17
|
+
def initialize(attrs = {})
|
|
18
|
+
ATTRIBUTES.each do |field|
|
|
19
|
+
value = attrs.key?(field) ? attrs[field] : attrs[field.to_sym]
|
|
20
|
+
instance_variable_set("@#{field}", value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.from_hash(hash)
|
|
25
|
+
return nil if hash.nil?
|
|
26
|
+
|
|
27
|
+
new(hash)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Paginated `GET /Applications` response.
|
|
32
|
+
class ApplicationList
|
|
33
|
+
include Pageable
|
|
34
|
+
|
|
35
|
+
attr_reader :applications
|
|
36
|
+
|
|
37
|
+
def initialize(hash = {})
|
|
38
|
+
assign_page_fields(hash)
|
|
39
|
+
@applications = (hash['applications'] || []).map { |a| Application.from_hash(a) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.from_hash(hash)
|
|
43
|
+
new(hash || {})
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'common'
|
|
4
|
+
|
|
5
|
+
module VoiceML
|
|
6
|
+
# A Twilio-compatible Call resource. Returned by `client.calls.create`, `client.calls.get`,
|
|
7
|
+
# `client.calls.update`, and listed in `CallList#calls`.
|
|
8
|
+
class Call
|
|
9
|
+
ATTRIBUTES = %w[
|
|
10
|
+
sid account_sid api_version to to_formatted from from_formatted
|
|
11
|
+
parent_call_sid caller_name forwarded_from status direction
|
|
12
|
+
answered_by start_time end_time duration price price_unit
|
|
13
|
+
phone_number_sid annotation group_sid queue_time trunk_sid
|
|
14
|
+
date_created date_updated uri subresource_uris
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader(*ATTRIBUTES.map(&:to_sym))
|
|
18
|
+
|
|
19
|
+
def initialize(attrs = {})
|
|
20
|
+
ATTRIBUTES.each do |field|
|
|
21
|
+
value = attrs.key?(field) ? attrs[field] : attrs[field.to_sym]
|
|
22
|
+
instance_variable_set("@#{field}", value)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.from_hash(hash)
|
|
27
|
+
return nil if hash.nil?
|
|
28
|
+
|
|
29
|
+
new(hash)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Paginated `GET /Calls` response.
|
|
34
|
+
class CallList
|
|
35
|
+
include Pageable
|
|
36
|
+
|
|
37
|
+
attr_reader :calls
|
|
38
|
+
|
|
39
|
+
def initialize(hash = {})
|
|
40
|
+
assign_page_fields(hash)
|
|
41
|
+
@calls = (hash['calls'] || []).map { |c| Call.from_hash(c) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.from_hash(hash)
|
|
45
|
+
new(hash || {})
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VoiceML
|
|
4
|
+
# Twilio-compatible pagination envelope fields. Mix into list-response classes to expose
|
|
5
|
+
# `page`, `page_size`, `total`, `next_page_uri`, `previous_page_uri`, `first_page_uri`,
|
|
6
|
+
# `uri`, `num_pages`, `start`, `end`. The list items themselves are declared on each
|
|
7
|
+
# concrete subclass (`calls`, `conferences`, etc).
|
|
8
|
+
module Pageable
|
|
9
|
+
PAGE_FIELDS = %w[
|
|
10
|
+
page page_size num_pages total start end
|
|
11
|
+
first_page_uri next_page_uri previous_page_uri uri
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
PAGE_FIELDS.each do |field|
|
|
15
|
+
attr_reader field.to_sym
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def assign_page_fields(hash)
|
|
19
|
+
PAGE_FIELDS.each do |field|
|
|
20
|
+
instance_variable_set("@#{field}", hash[field])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Twilio-compatible error body. Surface only โ the transport raises a VoiceML::ApiError
|
|
26
|
+
# subclass with this payload attached as `error.body`.
|
|
27
|
+
class ErrorBody
|
|
28
|
+
attr_reader :code, :message, :more_info, :status
|
|
29
|
+
|
|
30
|
+
def initialize(code: nil, message: nil, more_info: nil, status: nil)
|
|
31
|
+
@code = code
|
|
32
|
+
@message = message
|
|
33
|
+
@more_info = more_info
|
|
34
|
+
@status = status
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.from_hash(hash)
|
|
38
|
+
return nil if hash.nil?
|
|
39
|
+
|
|
40
|
+
new(
|
|
41
|
+
code: hash['code'],
|
|
42
|
+
message: hash['message'],
|
|
43
|
+
more_info: hash['more_info'],
|
|
44
|
+
status: hash['status']
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# One tripped check from the `/health` deep probe.
|
|
50
|
+
class HealthFailure
|
|
51
|
+
attr_reader :check, :detail
|
|
52
|
+
|
|
53
|
+
def initialize(check:, detail:)
|
|
54
|
+
@check = check
|
|
55
|
+
@detail = detail
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.from_hash(hash)
|
|
59
|
+
return nil if hash.nil?
|
|
60
|
+
|
|
61
|
+
new(check: hash['check'], detail: hash['detail'])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|