1use crate::adapter::{EndReason, OriginateRequest, RejectReason, TransferTarget};
2use crate::capability::CapabilityDescriptor;
3use crate::conversation::ConversationPolicy;
4use crate::ids::{
5 AiAttachmentId, BridgeId, ConnectionId, ConversationId, ListenerId, ParticipantId, RecordingId,
6 SessionId, TenantId,
7};
8use crate::message::Message;
9use crate::participant::{ParticipantKind, ParticipantRole};
10use crate::session::SessionMedium;
11use std::collections::HashMap;
12use std::fmt;
13
14pub enum Command {
17 OpenConversation {
19 tenant_id: TenantId,
20 policy: ConversationPolicy,
21 metadata: HashMap<String, String>,
22 correlation_id: String,
23 },
24 CloseConversation {
25 conversation_id: ConversationId,
26 force: bool,
27 correlation_id: String,
28 },
29
30 StartSession {
32 conversation_id: ConversationId,
33 medium: SessionMedium,
34 invitees: Vec<ParticipantId>,
35 correlation_id: String,
36 },
37 EndSession {
38 session_id: SessionId,
39 reason: EndReason,
40 correlation_id: String,
41 },
42 JoinSession {
43 session_id: SessionId,
44 participant_id: ParticipantId,
45 kind: ParticipantKind,
46 role: ParticipantRole,
47 correlation_id: String,
48 },
49 LeaveSession {
50 session_id: SessionId,
51 participant_id: ParticipantId,
52 correlation_id: String,
53 },
54
55 RouteInboundConnection {
57 connection_id: ConnectionId,
58 action: InboundAction,
59 correlation_id: String,
60 },
61 OriginateConnection {
62 request: OriginateRequest,
63 correlation_id: String,
64 },
65 EndConnection {
66 connection_id: ConnectionId,
67 reason: EndReason,
68 correlation_id: String,
69 },
70
71 BridgeConnections {
73 a: ConnectionId,
74 b: ConnectionId,
75 correlation_id: String,
76 },
77 UnbridgeConnections {
78 bridge_id: BridgeId,
79 correlation_id: String,
80 },
81
82 TransferConnection {
84 connection_id: ConnectionId,
85 target: TransferTarget,
86 correlation_id: String,
87 },
88
89 Hold {
91 connection_id: ConnectionId,
92 correlation_id: String,
93 },
94 Resume {
95 connection_id: ConnectionId,
96 correlation_id: String,
97 },
98 Mute {
99 connection_id: ConnectionId,
100 direction: MuteDirection,
101 correlation_id: String,
102 },
103 Unmute {
104 connection_id: ConnectionId,
105 direction: MuteDirection,
106 correlation_id: String,
107 },
108 SendMessage {
109 connection_id: ConnectionId,
110 message: Message,
111 correlation_id: String,
112 },
113 SendDtmf {
114 connection_id: ConnectionId,
115 digits: String,
116 duration_ms: u32,
117 correlation_id: String,
118 },
119 PlayAudio {
120 connection_id: ConnectionId,
121 source: AudioSource,
122 correlation_id: String,
123 },
124 RenegotiateMedia {
125 connection_id: ConnectionId,
126 capabilities: CapabilityDescriptor,
127 correlation_id: String,
128 },
129
130 AttachAi {
133 connection_id: ConnectionId,
134 provider_ref: String,
135 config: HashMap<String, String>,
136 correlation_id: String,
137 },
138 AttachListener {
139 target: ListenerTarget,
140 sink: ListenerSink,
141 correlation_id: String,
142 },
143 Detach {
144 attachment: AttachmentRef,
145 correlation_id: String,
146 },
147
148 StartRecording {
150 target: RecordingTarget,
151 sink: RecordingSink,
152 correlation_id: String,
153 },
154 StopRecording {
155 recording_id: RecordingId,
156 correlation_id: String,
157 },
158 PauseRecording {
159 recording_id: RecordingId,
160 correlation_id: String,
161 },
162 ResumeRecording {
163 recording_id: RecordingId,
164 correlation_id: String,
165 },
166 StartTranscription {
167 target: RecordingTarget,
168 provider_ref: String,
169 correlation_id: String,
170 },
171 StopTranscription {
172 target: RecordingTarget,
173 correlation_id: String,
174 },
175}
176
177impl fmt::Debug for Command {
178 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179 let variant = match self {
180 Self::OpenConversation { .. } => "OpenConversation",
181 Self::CloseConversation { .. } => "CloseConversation",
182 Self::StartSession { .. } => "StartSession",
183 Self::EndSession { .. } => "EndSession",
184 Self::JoinSession { .. } => "JoinSession",
185 Self::LeaveSession { .. } => "LeaveSession",
186 Self::RouteInboundConnection { .. } => "RouteInboundConnection",
187 Self::OriginateConnection { .. } => "OriginateConnection",
188 Self::EndConnection { .. } => "EndConnection",
189 Self::BridgeConnections { .. } => "BridgeConnections",
190 Self::UnbridgeConnections { .. } => "UnbridgeConnections",
191 Self::TransferConnection { .. } => "TransferConnection",
192 Self::Hold { .. } => "Hold",
193 Self::Resume { .. } => "Resume",
194 Self::Mute { .. } => "Mute",
195 Self::Unmute { .. } => "Unmute",
196 Self::SendMessage { .. } => "SendMessage",
197 Self::SendDtmf { .. } => "SendDtmf",
198 Self::PlayAudio { .. } => "PlayAudio",
199 Self::RenegotiateMedia { .. } => "RenegotiateMedia",
200 Self::AttachAi { .. } => "AttachAi",
201 Self::AttachListener { .. } => "AttachListener",
202 Self::Detach { .. } => "Detach",
203 Self::StartRecording { .. } => "StartRecording",
204 Self::StopRecording { .. } => "StopRecording",
205 Self::PauseRecording { .. } => "PauseRecording",
206 Self::ResumeRecording { .. } => "ResumeRecording",
207 Self::StartTranscription { .. } => "StartTranscription",
208 Self::StopTranscription { .. } => "StopTranscription",
209 };
210 formatter
211 .debug_struct("Command")
212 .field("variant", &variant)
213 .finish()
214 }
215}
216
217#[derive(Clone)]
218pub enum InboundAction {
219 Accept {
220 session_id: SessionId,
221 participant_id: ParticipantId,
222 },
223 Reject {
224 reason: RejectReason,
225 },
226 BridgeTo {
228 session_id: SessionId,
229 outbound: OriginateRequest,
230 },
231}
232
233impl fmt::Debug for InboundAction {
234 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235 formatter.write_str(match self {
236 Self::Accept { .. } => "InboundAction::Accept",
237 Self::Reject { .. } => "InboundAction::Reject",
238 Self::BridgeTo { .. } => "InboundAction::BridgeTo",
239 })
240 }
241}
242
243#[derive(Clone, Copy, Debug, Eq, PartialEq)]
244pub enum MuteDirection {
245 Send,
246 Receive,
247 Both,
248}
249
250#[derive(Clone)]
251pub enum AudioSource {
252 Url(String),
253 TtsRequest {
254 provider_ref: String,
255 text: String,
256 voice: Option<String>,
257 },
258}
259
260impl fmt::Debug for AudioSource {
261 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262 match self {
263 Self::Url(url) => formatter
264 .debug_struct("AudioSource::Url")
265 .field("value_present", &!url.is_empty())
266 .finish(),
267 Self::TtsRequest {
268 provider_ref,
269 text,
270 voice,
271 } => formatter
272 .debug_struct("AudioSource::TtsRequest")
273 .field("provider_present", &!provider_ref.is_empty())
274 .field("text_bytes", &text.len())
275 .field("voice_present", &voice.is_some())
276 .finish(),
277 }
278 }
279}
280
281#[derive(Clone)]
282pub enum ListenerTarget {
283 Connection(ConnectionId),
284 Session(SessionId),
285}
286
287impl fmt::Debug for ListenerTarget {
288 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
289 formatter.write_str(match self {
290 Self::Connection(_) => "ListenerTarget::Connection",
291 Self::Session(_) => "ListenerTarget::Session",
292 })
293 }
294}
295
296#[derive(Clone)]
297pub enum ListenerSink {
298 File { path: String },
299 Url(String),
300 Channel,
301}
302
303impl fmt::Debug for ListenerSink {
304 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
305 formatter.write_str(match self {
306 Self::File { .. } => "ListenerSink::File",
307 Self::Url(_) => "ListenerSink::Url",
308 Self::Channel => "ListenerSink::Channel",
309 })
310 }
311}
312
313#[derive(Clone)]
314pub enum AttachmentRef {
315 Ai(AiAttachmentId),
316 Listener(ListenerId),
317 Recording(RecordingId),
318}
319
320impl fmt::Debug for AttachmentRef {
321 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
322 formatter.write_str(match self {
323 Self::Ai(_) => "AttachmentRef::Ai",
324 Self::Listener(_) => "AttachmentRef::Listener",
325 Self::Recording(_) => "AttachmentRef::Recording",
326 })
327 }
328}
329
330#[derive(Clone)]
331pub enum RecordingTarget {
332 Connection(ConnectionId),
333 Session(SessionId),
334}
335
336impl fmt::Debug for RecordingTarget {
337 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
338 formatter.write_str(match self {
339 Self::Connection(_) => "RecordingTarget::Connection",
340 Self::Session(_) => "RecordingTarget::Session",
341 })
342 }
343}
344
345#[derive(Clone)]
346pub enum RecordingSink {
347 File { path: String },
348 Url(String),
349}
350
351impl fmt::Debug for RecordingSink {
352 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
353 formatter.write_str(match self {
354 Self::File { .. } => "RecordingSink::File",
355 Self::Url(_) => "RecordingSink::Url",
356 })
357 }
358}
359
360#[cfg(test)]
361mod diagnostic_tests {
362 use super::*;
363
364 #[test]
365 fn command_and_nested_payload_debug_never_render_caller_values() {
366 let secret = "credential-canary-value";
367 let connection_id = ConnectionId::new();
368 let commands = [
369 Command::SendDtmf {
370 connection_id: connection_id.clone(),
371 digits: secret.into(),
372 duration_ms: 100,
373 correlation_id: secret.into(),
374 },
375 Command::PlayAudio {
376 connection_id,
377 source: AudioSource::TtsRequest {
378 provider_ref: secret.into(),
379 text: secret.into(),
380 voice: Some(secret.into()),
381 },
382 correlation_id: secret.into(),
383 },
384 ];
385
386 for command in commands {
387 let debug = format!("{command:?}");
388 assert!(!debug.contains(secret));
389 assert!(debug.contains("variant"));
390 }
391
392 for debug in [
393 format!("{:?}", AudioSource::Url(secret.into())),
394 format!(
395 "{:?}",
396 ListenerSink::File {
397 path: secret.into()
398 }
399 ),
400 format!("{:?}", ListenerSink::Url(secret.into())),
401 format!(
402 "{:?}",
403 RecordingSink::File {
404 path: secret.into()
405 }
406 ),
407 format!("{:?}", RecordingSink::Url(secret.into())),
408 ] {
409 assert!(!debug.contains(secret));
410 }
411 }
412}