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.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VoiceML
4
+ # @api private
5
+ # Mixin holding a `Transport` reference and helpers for AccountSid-scoped pathing.
6
+ class BaseResource
7
+ def initialize(transport)
8
+ @transport = transport
9
+ end
10
+
11
+ private
12
+
13
+ # Build a URL under `/2010-04-01/Accounts/{AccountSid}/...`. Caller passes path segments
14
+ # (e.g. `"Calls"`, sid, `"Recordings"`). Empty segments are skipped; nothing is
15
+ # URL-encoded — sids and slugs never need escaping.
16
+ #
17
+ # As of v0.5.0 every REST endpoint resolves under its `.json` form (Twilio drop-in
18
+ # compatibility). Pass `suffix: ''` to opt out — used by `.wav` audio fetches and any
19
+ # caller that needs to append a different extension.
20
+ def path(*parts, suffix: '.json')
21
+ tail = parts.compact.reject { |p| p.to_s.empty? }.join('/')
22
+ "/2010-04-01/Accounts/#{@transport.account_sid}/#{tail}#{suffix}"
23
+ end
24
+
25
+ # Translate snake_case Ruby kwargs to the form/query field names the server expects.
26
+ # `nil` values are dropped; booleans become "true"/"false" inside Transport.
27
+ def form_params(map, kwargs)
28
+ out = {}
29
+ map.each do |wire_name, ruby_key|
30
+ value = kwargs[ruby_key]
31
+ next if value.nil?
32
+
33
+ out[wire_name] = value
34
+ end
35
+ out
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,414 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../models/calls'
5
+ require_relative '../models/recordings'
6
+ require_relative '../models/streams'
7
+ require_relative '../models/siprec'
8
+ require_relative '../models/transcriptions'
9
+ require_relative '../models/diagnostics'
10
+ require_relative '../models/payments'
11
+
12
+ module VoiceML
13
+ # Operations on `/Calls` and call-scoped sub-resources (Recordings, Streams, Siprec,
14
+ # Transcriptions, Notifications, Events, UserDefinedMessages).
15
+ #
16
+ # All methods accept idiomatic snake_case keyword arguments — they're translated to
17
+ # Twilio's PascalCase wire names internally.
18
+ class CallsResource < BaseResource
19
+ # Create-call form-field map (snake_case kwarg -> Twilio wire name).
20
+ CREATE_FIELDS = {
21
+ 'To' => :to,
22
+ 'From' => :from,
23
+ 'Url' => :url,
24
+ 'Method' => :method,
25
+ 'Twiml' => :twiml,
26
+ 'ApplicationSid' => :application_sid,
27
+ 'FallbackUrl' => :fallback_url,
28
+ 'FallbackMethod' => :fallback_method,
29
+ 'StatusCallback' => :status_callback,
30
+ 'StatusCallbackMethod' => :status_callback_method,
31
+ 'StatusCallbackEvent' => :status_callback_event,
32
+ 'MachineDetection' => :machine_detection,
33
+ 'MachineDetectionTimeout' => :machine_detection_timeout,
34
+ 'MachineDetectionSpeechThreshold' => :machine_detection_speech_threshold,
35
+ 'MachineDetectionSpeechEndThreshold' => :machine_detection_speech_end_threshold,
36
+ 'MachineDetectionSilenceTimeout' => :machine_detection_silence_timeout,
37
+ 'AsyncAmdStatusCallback' => :async_amd_status_callback,
38
+ 'AsyncAmdStatusCallbackMethod' => :async_amd_status_callback_method,
39
+ 'Record' => :record,
40
+ 'RecordingStatusCallback' => :recording_status_callback,
41
+ 'RecordingStatusCallbackMethod' => :recording_status_callback_method,
42
+ 'RecordingStatusCallbackEvent' => :recording_status_callback_event,
43
+ 'RecordingChannels' => :recording_channels,
44
+ 'RecordingTrack' => :recording_track,
45
+ 'Trim' => :trim,
46
+ 'Timeout' => :timeout,
47
+ 'SendDigits' => :send_digits,
48
+ 'CallerId' => :caller_id,
49
+ 'CallReason' => :call_reason,
50
+ 'SipAuthUsername' => :sip_auth_username,
51
+ 'SipAuthPassword' => :sip_auth_password,
52
+ 'Byoc' => :byoc,
53
+ 'AsyncAmd' => :async_amd,
54
+ 'CallToken' => :call_token
55
+ }.freeze
56
+
57
+ UPDATE_FIELDS = {
58
+ 'Status' => :status,
59
+ 'Twiml' => :twiml,
60
+ 'Url' => :url,
61
+ 'Method' => :method,
62
+ 'FallbackUrl' => :fallback_url,
63
+ 'FallbackMethod' => :fallback_method,
64
+ 'StatusCallback' => :status_callback,
65
+ 'StatusCallbackMethod' => :status_callback_method,
66
+ 'StatusCallbackEvent' => :status_callback_event
67
+ }.freeze
68
+
69
+ LIST_FIELDS = {
70
+ 'To' => :to,
71
+ 'From' => :from,
72
+ 'Status' => :status,
73
+ 'ParentCallSid' => :parent_call_sid,
74
+ 'StartTime' => :start_time,
75
+ 'StartTime<' => :start_time_lt,
76
+ 'StartTime>' => :start_time_gt,
77
+ # Note: spec defines `StartTime>=` and `StartTime<=` as the literal query names.
78
+ 'StartTime>=' => :start_time_gte,
79
+ 'StartTime<=' => :start_time_lte,
80
+ 'EndTime' => :end_time,
81
+ 'EndTime<' => :end_time_lt,
82
+ 'EndTime>' => :end_time_gt,
83
+ 'Page' => :page,
84
+ 'PageSize' => :page_size,
85
+ 'PageToken' => :page_token
86
+ }.freeze
87
+
88
+ LIST_RECORDINGS_FIELDS = {
89
+ 'DateCreated' => :date_created,
90
+ 'DateCreated<' => :date_created_lt,
91
+ 'DateCreated>' => :date_created_gt,
92
+ 'Page' => :page,
93
+ 'PageSize' => :page_size,
94
+ 'PageToken' => :page_token
95
+ }.freeze
96
+
97
+ LIST_STUB_PAGE_FIELDS = {
98
+ 'Page' => :page,
99
+ 'PageSize' => :page_size,
100
+ 'PageToken' => :page_token
101
+ }.freeze
102
+
103
+ LIST_NOTIFICATIONS_FIELDS = {
104
+ 'Page' => :page,
105
+ 'PageSize' => :page_size,
106
+ 'PageToken' => :page_token,
107
+ 'Log' => :log,
108
+ 'MessageDate' => :message_date,
109
+ 'MessageDate<' => :message_date_lt,
110
+ 'MessageDate>' => :message_date_gt
111
+ }.freeze
112
+
113
+ START_RECORDING_FIELDS = {
114
+ 'RecordingMaxDuration' => :recording_max_duration,
115
+ 'RecordingChannels' => :recording_channels,
116
+ 'PlayBeep' => :play_beep,
117
+ 'RecordingStatusCallback' => :recording_status_callback,
118
+ 'RecordingStatusCallbackMethod' => :recording_status_callback_method,
119
+ 'RecordingStatusCallbackEvent' => :recording_status_callback_event
120
+ }.freeze
121
+
122
+ START_STREAM_FIELDS = {
123
+ 'Url' => :url,
124
+ 'Track' => :track,
125
+ 'Name' => :name,
126
+ 'StatusCallback' => :status_callback,
127
+ 'StatusCallbackMethod' => :status_callback_method
128
+ }.freeze
129
+
130
+ START_SIPREC_FIELDS = {
131
+ 'Name' => :name,
132
+ 'ConnectorName' => :connector_name,
133
+ 'Track' => :track,
134
+ 'StatusCallback' => :status_callback,
135
+ 'StatusCallbackMethod' => :status_callback_method
136
+ }.freeze
137
+
138
+ START_TRANSCRIPTION_FIELDS = {
139
+ 'Name' => :name,
140
+ 'Track' => :track,
141
+ 'LanguageCode' => :language_code,
142
+ 'TranscriptionEngine' => :transcription_engine,
143
+ 'ProfanityFilter' => :profanity_filter,
144
+ 'PartialResults' => :partial_results,
145
+ 'Hints' => :hints,
146
+ 'StatusCallback' => :status_callback,
147
+ 'StatusCallbackMethod' => :status_callback_method,
148
+ 'StatusCallbackEvents' => :status_callback_events
149
+ }.freeze
150
+
151
+ START_PAYMENT_FIELDS = {
152
+ 'IdempotencyKey' => :idempotency_key,
153
+ 'StatusCallback' => :status_callback,
154
+ 'BankAccountType' => :bank_account_type,
155
+ 'ChargeAmount' => :charge_amount,
156
+ 'Currency' => :currency,
157
+ 'Description' => :description,
158
+ 'Input' => :input,
159
+ 'MinPostalCodeLength' => :min_postal_code_length,
160
+ 'Parameter' => :parameter,
161
+ 'PaymentConnector' => :payment_connector,
162
+ 'PaymentMethod' => :payment_method,
163
+ 'PostalCode' => :postal_code,
164
+ 'SecurityCode' => :security_code,
165
+ 'Timeout' => :timeout,
166
+ 'TokenType' => :token_type,
167
+ 'ValidCardTypes' => :valid_card_types,
168
+ 'RequireMatchingInputs' => :require_matching_inputs,
169
+ 'Confirmation' => :confirmation
170
+ }.freeze
171
+
172
+ UPDATE_PAYMENT_FIELDS = {
173
+ 'IdempotencyKey' => :idempotency_key,
174
+ 'StatusCallback' => :status_callback,
175
+ 'Capture' => :capture,
176
+ 'Status' => :status
177
+ }.freeze
178
+
179
+ # @return [VoiceML::CallList]
180
+ def list(**kwargs)
181
+ data = @transport.request(:get, path('Calls'), params: form_params(LIST_FIELDS, kwargs))
182
+ CallList.from_hash(data)
183
+ end
184
+
185
+ # Walk every page of /Calls and yield each Call. Returns an Enumerator when
186
+ # called without a block.
187
+ #
188
+ # @yield [VoiceML::Call]
189
+ # @return [Enumerator<VoiceML::Call>] when no block given
190
+ def each(**kwargs, &block)
191
+ return enum_for(:each, **kwargs) unless block
192
+
193
+ page_num = kwargs.delete(:page) || 0
194
+ loop do
195
+ chunk = list(**kwargs, page: page_num)
196
+ chunk.calls.each(&block)
197
+ break if chunk.next_page_uri.nil? || chunk.next_page_uri.empty? || chunk.calls.empty?
198
+ page_num += 1
199
+ end
200
+ end
201
+
202
+ # Create a new outbound call.
203
+ #
204
+ # Pass at most one of `url:` / `twiml:` / `application_sid:` (Twiml wins if multiple are set
205
+ # — Twilio's documented precedence).
206
+ #
207
+ # @return [VoiceML::Call]
208
+ def create(**kwargs)
209
+ data = @transport.request(:post, path('Calls'), form: form_params(CREATE_FIELDS, kwargs))
210
+ Call.from_hash(data)
211
+ end
212
+
213
+ # @return [VoiceML::Call]
214
+ def get(call_sid)
215
+ data = @transport.request(:get, path('Calls', call_sid))
216
+ Call.from_hash(data)
217
+ end
218
+
219
+ # @return [VoiceML::Call]
220
+ def update(call_sid, **kwargs)
221
+ data = @transport.request(:post, path('Calls', call_sid),
222
+ form: form_params(UPDATE_FIELDS, kwargs))
223
+ Call.from_hash(data)
224
+ end
225
+
226
+ # @return [nil]
227
+ def delete(call_sid)
228
+ @transport.request(:delete, path('Calls', call_sid))
229
+ nil
230
+ end
231
+
232
+ # --- Call-scoped Recordings ---
233
+
234
+ # @return [VoiceML::RecordingList]
235
+ def list_recordings(call_sid, **kwargs)
236
+ RecordingList.from_hash(
237
+ @transport.request(:get, path('Calls', call_sid, 'Recordings'),
238
+ params: form_params(LIST_RECORDINGS_FIELDS, kwargs))
239
+ )
240
+ end
241
+
242
+ # @return [VoiceML::Recording]
243
+ def start_recording(call_sid, **kwargs)
244
+ data = @transport.request(:post, path('Calls', call_sid, 'Recordings'),
245
+ form: form_params(START_RECORDING_FIELDS, kwargs))
246
+ Recording.from_hash(data)
247
+ end
248
+
249
+ # @return [VoiceML::Recording]
250
+ def get_recording(call_sid, recording_sid)
251
+ Recording.from_hash(
252
+ @transport.request(:get, path('Calls', call_sid, 'Recordings', recording_sid))
253
+ )
254
+ end
255
+
256
+ # @param status [String] one of "in-progress", "paused", "stopped".
257
+ # @return [VoiceML::Recording]
258
+ def update_recording(call_sid, recording_sid, status:)
259
+ data = @transport.request(:post, path('Calls', call_sid, 'Recordings', recording_sid),
260
+ form: { 'Status' => status })
261
+ Recording.from_hash(data)
262
+ end
263
+
264
+ # @return [nil]
265
+ def delete_recording(call_sid, recording_sid)
266
+ @transport.request(:delete, path('Calls', call_sid, 'Recordings', recording_sid))
267
+ nil
268
+ end
269
+
270
+ # --- Streams ---
271
+
272
+ # @return [VoiceML::StreamList]
273
+ def list_streams(call_sid)
274
+ StreamList.from_hash(@transport.request(:get, path('Calls', call_sid, 'Streams')))
275
+ end
276
+
277
+ # @return [VoiceML::Stream]
278
+ def start_stream(call_sid, **kwargs)
279
+ data = @transport.request(:post, path('Calls', call_sid, 'Streams'),
280
+ form: form_params(START_STREAM_FIELDS, kwargs))
281
+ Stream.from_hash(data)
282
+ end
283
+
284
+ # @return [VoiceML::Stream]
285
+ def get_stream(call_sid, stream_sid)
286
+ Stream.from_hash(@transport.request(:get, path('Calls', call_sid, 'Streams', stream_sid)))
287
+ end
288
+
289
+ # @return [VoiceML::Stream]
290
+ def stop_stream(call_sid, stream_sid)
291
+ data = @transport.request(:post, path('Calls', call_sid, 'Streams', stream_sid),
292
+ form: { 'Status' => 'stopped' })
293
+ Stream.from_hash(data)
294
+ end
295
+
296
+ # --- SIPREC ---
297
+
298
+ # @return [VoiceML::SiprecList]
299
+ def list_siprec(call_sid)
300
+ SiprecList.from_hash(@transport.request(:get, path('Calls', call_sid, 'Siprec')))
301
+ end
302
+
303
+ # @return [VoiceML::SiprecSession]
304
+ def start_siprec(call_sid, **kwargs)
305
+ data = @transport.request(:post, path('Calls', call_sid, 'Siprec'),
306
+ form: form_params(START_SIPREC_FIELDS, kwargs))
307
+ SiprecSession.from_hash(data)
308
+ end
309
+
310
+ # @return [VoiceML::SiprecSession]
311
+ def get_siprec(call_sid, siprec_sid)
312
+ SiprecSession.from_hash(
313
+ @transport.request(:get, path('Calls', call_sid, 'Siprec', siprec_sid))
314
+ )
315
+ end
316
+
317
+ # @return [VoiceML::SiprecSession]
318
+ def stop_siprec(call_sid, siprec_sid)
319
+ data = @transport.request(:post, path('Calls', call_sid, 'Siprec', siprec_sid),
320
+ form: { 'Status' => 'stopped' })
321
+ SiprecSession.from_hash(data)
322
+ end
323
+
324
+ # --- Transcriptions ---
325
+
326
+ # @return [VoiceML::TranscriptionList]
327
+ def list_transcriptions(call_sid)
328
+ TranscriptionList.from_hash(
329
+ @transport.request(:get, path('Calls', call_sid, 'Transcriptions'))
330
+ )
331
+ end
332
+
333
+ # @return [VoiceML::CallTranscription]
334
+ def start_transcription(call_sid, **kwargs)
335
+ data = @transport.request(:post, path('Calls', call_sid, 'Transcriptions'),
336
+ form: form_params(START_TRANSCRIPTION_FIELDS, kwargs))
337
+ CallTranscription.from_hash(data)
338
+ end
339
+
340
+ # @return [VoiceML::CallTranscription]
341
+ def get_transcription(call_sid, transcription_sid)
342
+ CallTranscription.from_hash(
343
+ @transport.request(:get, path('Calls', call_sid, 'Transcriptions', transcription_sid))
344
+ )
345
+ end
346
+
347
+ # @return [VoiceML::CallTranscription]
348
+ def stop_transcription(call_sid, transcription_sid)
349
+ data = @transport.request(:post,
350
+ path('Calls', call_sid, 'Transcriptions', transcription_sid),
351
+ form: { 'Status' => 'stopped' })
352
+ CallTranscription.from_hash(data)
353
+ end
354
+
355
+ # --- Notifications / Events (compat stubs) ---
356
+
357
+ # @return [VoiceML::NotificationsList]
358
+ def list_notifications(call_sid, **kwargs)
359
+ NotificationsList.from_hash(
360
+ @transport.request(:get, path('Calls', call_sid, 'Notifications'),
361
+ params: form_params(LIST_NOTIFICATIONS_FIELDS, kwargs))
362
+ )
363
+ end
364
+
365
+ # @return [Hash]
366
+ def get_notification(call_sid, notification_sid)
367
+ @transport.request(:get, path('Calls', call_sid, 'Notifications', notification_sid))
368
+ end
369
+
370
+ # @return [VoiceML::EventsList]
371
+ def list_events(call_sid, **kwargs)
372
+ EventsList.from_hash(
373
+ @transport.request(:get, path('Calls', call_sid, 'Events'),
374
+ params: form_params(LIST_STUB_PAGE_FIELDS, kwargs))
375
+ )
376
+ end
377
+
378
+ # `POST /Calls/{sid}/UserDefinedMessages` — always raises `NotImplementedAPIError`.
379
+ # Mounted on the server only as a 501 stub. The SDK forwards the call so callers get a
380
+ # clean exception rather than discovering at runtime that the endpoint doesn't exist.
381
+ def send_user_defined_message(call_sid, payload = nil)
382
+ @transport.request(:post, path('Calls', call_sid, 'UserDefinedMessages'),
383
+ form: payload)
384
+ end
385
+
386
+ # --- Payments (the REST companion to the `<Pay>` TwiML verb) ---
387
+
388
+ # Begin a `<Pay>` session on the live call. Returns the freshly-minted
389
+ # CallPayment. Returns 403 when the tenant is not `pay_enabled` or has no
390
+ # `stripe_secret_key` configured.
391
+ #
392
+ # `idempotency_key:` is accepted and persisted for diagnostic visibility but
393
+ # replay-dedup is NOT enforced today.
394
+ #
395
+ # @return [VoiceML::CallPayment]
396
+ def start_payment(call_sid, **kwargs)
397
+ data = @transport.request(:post, path('Calls', call_sid, 'Payments'),
398
+ form: form_params(START_PAYMENT_FIELDS, kwargs))
399
+ CallPayment.from_hash(data)
400
+ end
401
+
402
+ # Advance or terminate an existing Pay session. `status: "complete"`
403
+ # captures the collected fields; `status: "cancel"` aborts the session.
404
+ # `capture: ...` tells the runtime which input the user is about to type
405
+ # next.
406
+ #
407
+ # @return [VoiceML::CallPayment]
408
+ def update_payment(call_sid, payment_sid, **kwargs)
409
+ data = @transport.request(:post, path('Calls', call_sid, 'Payments', payment_sid),
410
+ form: form_params(UPDATE_PAYMENT_FIELDS, kwargs))
411
+ CallPayment.from_hash(data)
412
+ end
413
+ end
414
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../models/conferences'
5
+ require_relative '../models/recordings'
6
+
7
+ module VoiceML
8
+ # Operations on `/Conferences` and their participants/recordings.
9
+ class ConferencesResource < BaseResource
10
+ UPDATE_PARTICIPANT_FIELDS = {
11
+ 'Muted' => :muted,
12
+ 'Hold' => :hold
13
+ }.freeze
14
+
15
+ LIST_FIELDS = {
16
+ 'FriendlyName' => :friendly_name,
17
+ 'Status' => :status,
18
+ 'DateCreated' => :date_created,
19
+ 'DateCreated<' => :date_created_lt,
20
+ 'DateCreated>' => :date_created_gt,
21
+ 'DateUpdated' => :date_updated,
22
+ 'DateUpdated<' => :date_updated_lt,
23
+ 'DateUpdated>' => :date_updated_gt,
24
+ 'Page' => :page,
25
+ 'PageSize' => :page_size,
26
+ 'PageToken' => :page_token
27
+ }.freeze
28
+
29
+ CREATE_PARTICIPANT_FIELDS = {
30
+ 'From' => :from,
31
+ 'To' => :to,
32
+ 'Label' => :label,
33
+ 'Muted' => :muted,
34
+ 'StartConferenceOnEnter' => :start_conference_on_enter,
35
+ 'EndConferenceOnExit' => :end_conference_on_exit,
36
+ 'Timeout' => :timeout,
37
+ 'StatusCallback' => :status_callback,
38
+ 'StatusCallbackMethod' => :status_callback_method,
39
+ 'StatusCallbackEvent' => :status_callback_event
40
+ }.freeze
41
+
42
+ UPDATE_RECORDING_FIELDS = {
43
+ 'Status' => :status
44
+ }.freeze
45
+
46
+ LIST_PARTICIPANTS_FIELDS = {
47
+ 'Muted' => :muted,
48
+ 'Hold' => :hold,
49
+ 'Coaching' => :coaching,
50
+ 'Page' => :page,
51
+ 'PageSize' => :page_size,
52
+ 'PageToken' => :page_token
53
+ }.freeze
54
+
55
+ LIST_CALL_RECORDINGS_FIELDS = {
56
+ 'DateCreated' => :date_created,
57
+ 'DateCreated<' => :date_created_lt,
58
+ 'DateCreated>' => :date_created_gt,
59
+ 'Page' => :page,
60
+ 'PageSize' => :page_size,
61
+ 'PageToken' => :page_token
62
+ }.freeze
63
+
64
+ # @return [VoiceML::ConferenceList]
65
+ def list(**kwargs)
66
+ ConferenceList.from_hash(
67
+ @transport.request(:get, path('Conferences'), params: form_params(LIST_FIELDS, kwargs))
68
+ )
69
+ end
70
+
71
+ # @yield [VoiceML::Conference]
72
+ # @return [Enumerator<VoiceML::Conference>] when no block given
73
+ def each(**kwargs, &block)
74
+ return enum_for(:each, **kwargs) unless block
75
+
76
+ page_num = kwargs.delete(:page) || 0
77
+ loop do
78
+ chunk = list(**kwargs, page: page_num)
79
+ chunk.conferences.each(&block)
80
+ break if chunk.next_page_uri.nil? || chunk.next_page_uri.empty? || chunk.conferences.empty?
81
+ page_num += 1
82
+ end
83
+ end
84
+
85
+ # @return [VoiceML::Conference]
86
+ def get(conference_sid)
87
+ Conference.from_hash(@transport.request(:get, path('Conferences', conference_sid)))
88
+ end
89
+
90
+ # End a live conference. v1 supports only `status: "completed"`.
91
+ # @return [VoiceML::Conference]
92
+ def end_conference(conference_sid, status: 'completed')
93
+ data = @transport.request(:post, path('Conferences', conference_sid),
94
+ form: { 'Status' => status })
95
+ Conference.from_hash(data)
96
+ end
97
+
98
+ # --- Participants ---
99
+
100
+ # @return [VoiceML::ParticipantList]
101
+ def list_participants(conference_sid, **kwargs)
102
+ ParticipantList.from_hash(
103
+ @transport.request(:get, path('Conferences', conference_sid, 'Participants'),
104
+ params: form_params(LIST_PARTICIPANTS_FIELDS, kwargs))
105
+ )
106
+ end
107
+
108
+ # @return [VoiceML::Participant]
109
+ def get_participant(conference_sid, call_sid)
110
+ Participant.from_hash(
111
+ @transport.request(:get, path('Conferences', conference_sid, 'Participants', call_sid))
112
+ )
113
+ end
114
+
115
+ # Mute/unmute or hold/unhold a participant. At least one of `muted:` / `hold:` must be set.
116
+ # @return [VoiceML::Participant]
117
+ def update_participant(conference_sid, call_sid, **kwargs)
118
+ data = @transport.request(
119
+ :post,
120
+ path('Conferences', conference_sid, 'Participants', call_sid),
121
+ form: form_params(UPDATE_PARTICIPANT_FIELDS, kwargs)
122
+ )
123
+ Participant.from_hash(data)
124
+ end
125
+
126
+ # @return [nil]
127
+ def kick_participant(conference_sid, call_sid)
128
+ @transport.request(:delete,
129
+ path('Conferences', conference_sid, 'Participants', call_sid))
130
+ nil
131
+ end
132
+
133
+ # Dial a leg into a conference.
134
+ # @return [VoiceML::Participant]
135
+ def create_participant(conference_sid, from:, to:, **kwargs)
136
+ kwargs = kwargs.merge(from: from, to: to)
137
+ data = @transport.request(
138
+ :post,
139
+ path('Conferences', conference_sid, 'Participants'),
140
+ form: form_params(CREATE_PARTICIPANT_FIELDS, kwargs)
141
+ )
142
+ Participant.from_hash(data)
143
+ end
144
+
145
+ # --- Recordings ---
146
+
147
+ # @return [VoiceML::RecordingList]
148
+ def list_recordings(conference_sid, **kwargs)
149
+ RecordingList.from_hash(
150
+ @transport.request(:get, path('Conferences', conference_sid, 'Recordings'),
151
+ params: form_params(LIST_CALL_RECORDINGS_FIELDS, kwargs))
152
+ )
153
+ end
154
+
155
+ # @return [VoiceML::Recording]
156
+ def get_recording(conference_sid, recording_sid)
157
+ Recording.from_hash(
158
+ @transport.request(:get, path('Conferences', conference_sid, 'Recordings', recording_sid))
159
+ )
160
+ end
161
+
162
+ # @return [VoiceML::Recording]
163
+ def update_recording(conference_sid, recording_sid, **kwargs)
164
+ data = @transport.request(
165
+ :post,
166
+ path('Conferences', conference_sid, 'Recordings', recording_sid),
167
+ form: form_params(UPDATE_RECORDING_FIELDS, kwargs)
168
+ )
169
+ Recording.from_hash(data)
170
+ end
171
+
172
+ # @return [nil]
173
+ def delete_recording(conference_sid, recording_sid)
174
+ @transport.request(:delete, path('Conferences', conference_sid, 'Recordings', recording_sid))
175
+ nil
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ require_relative '../models/diagnostics'
8
+
9
+ module VoiceML
10
+ # Diagnostic surfaces — `/health` and the OpenAPI doc endpoints.
11
+ #
12
+ # These don't sit under `/2010-04-01/Accounts/{AccountSid}/...`; they're mounted at the
13
+ # server root and don't require auth (the spec marks them `security: []`).
14
+ class DiagnosticsResource
15
+ def initialize(transport)
16
+ @transport = transport
17
+ end
18
+
19
+ # Hit `GET /health`. 200 = all hard checks pass; 503 raises `VoiceML::ServerError`
20
+ # with the failure list on `error.body`.
21
+ #
22
+ # @return [VoiceML::HealthStatus]
23
+ def health
24
+ HealthStatus.from_hash(unauth_request('/health'))
25
+ end
26
+
27
+ # Fetch the OpenAPI spec as parsed JSON.
28
+ #
29
+ # @return [Hash]
30
+ def openapi_json
31
+ unauth_request('/openapi.json')
32
+ end
33
+
34
+ private
35
+
36
+ def unauth_request(path)
37
+ uri = URI.parse("#{@transport.base_url}#{path}")
38
+ req = Net::HTTP::Get.new(uri)
39
+ req['Accept'] = 'application/json'
40
+ req['User-Agent'] = @transport.user_agent
41
+ http = Net::HTTP.new(uri.host, uri.port)
42
+ http.use_ssl = uri.scheme == 'https'
43
+ http.open_timeout = 10
44
+ http.read_timeout = 10
45
+ response = http.start { |h| h.request(req) }
46
+
47
+ status = response.code.to_i
48
+ unless status >= 200 && status < 300
49
+ body = begin
50
+ response.body && !response.body.empty? ? JSON.parse(response.body) : response.body
51
+ rescue JSON::ParserError
52
+ response.body
53
+ end
54
+ message = body.is_a?(Hash) && body['message'].is_a?(String) ? body['message'] : "HTTP #{status}"
55
+ more_info = body.is_a?(Hash) && body['more_info'].is_a?(String) ? body['more_info'] : nil
56
+ raise VoiceML.error_from_response(status, message, body: body, more_info: more_info)
57
+ end
58
+
59
+ return nil if response.body.nil? || response.body.empty?
60
+
61
+ JSON.parse(response.body)
62
+ end
63
+ end
64
+ end