1use crate::adapter::{EndReason, TransferStatus, TransferTarget};
2use crate::identity::{AuthenticatedPrincipal, IdentityAssurance};
3use crate::ids::{
4 AiAttachmentId, BridgeId, ConnectionId, ConversationId, IdentityId, ListenerId, MessageId,
5 ParticipantId, RecordingId, SessionId, StreamId, TenantId,
6};
7use crate::store::VconHandle;
8use crate::stream::QualitySnapshot;
9use crate::vcon::VconRef;
10use crate::DataMessage;
11use chrono::{DateTime, Utc};
12use rvoip_infra_common::events::cross_crate::{RvoipCoreCrossCrateEvent, RvoipCrossCrateEvent};
13use std::fmt;
14
15#[derive(Clone)]
27#[non_exhaustive]
28pub enum Event {
29 ConversationOpened {
31 conversation_id: ConversationId,
32 at: DateTime<Utc>,
33 },
34 ConversationClosed {
35 conversation_id: ConversationId,
36 at: DateTime<Utc>,
37 },
38
39 SessionStarted {
41 session_id: SessionId,
42 conversation_id: ConversationId,
43 at: DateTime<Utc>,
44 },
45 SessionEnded {
46 session_id: SessionId,
47 report: Option<SessionQualityReport>,
51 at: DateTime<Utc>,
52 },
53 SessionFailed {
54 session_id: SessionId,
55 detail: String,
56 at: DateTime<Utc>,
57 },
58
59 ConnectionInbound {
61 connection_id: ConnectionId,
62 at: DateTime<Utc>,
63 },
64 ConnectionOutbound {
65 connection_id: ConnectionId,
66 at: DateTime<Utc>,
67 },
68 ConnectionConnected {
69 connection_id: ConnectionId,
70 at: DateTime<Utc>,
71 },
72 ConnectionAuthenticated {
79 connection_id: ConnectionId,
80 identity_id: String,
81 participant_id: String,
82 assurance: IdentityAssurance,
83 at: DateTime<Utc>,
84 },
85 ConnectionPrincipalAuthenticated {
88 connection_id: ConnectionId,
89 participant_id: String,
90 principal: AuthenticatedPrincipal,
91 at: DateTime<Utc>,
92 },
93 ConnectionProgress {
95 connection_id: ConnectionId,
96 kind: ConnectionProgressKind,
97 at: DateTime<Utc>,
98 },
99 ConnectionEnded {
100 connection_id: ConnectionId,
101 reason: EndReason,
102 at: DateTime<Utc>,
103 },
104 ConnectionFailed {
105 connection_id: ConnectionId,
106 detail: String,
107 at: DateTime<Utc>,
108 },
109
110 ConnectionsBridged {
112 bridge_id: BridgeId,
113 a: ConnectionId,
114 b: ConnectionId,
115 at: DateTime<Utc>,
116 },
117 ConnectionsUnbridged {
118 bridge_id: BridgeId,
119 at: DateTime<Utc>,
120 },
121
122 ConnectionTransferred {
127 connection_id: ConnectionId,
128 target: TransferTarget,
129 at: DateTime<Utc>,
130 },
131 ConnectionTransferStatus {
133 connection_id: ConnectionId,
134 status: TransferStatus,
135 at: DateTime<Utc>,
136 },
137
138 ParticipantJoined {
140 session_id: SessionId,
141 participant_id: ParticipantId,
142 at: DateTime<Utc>,
143 },
144 ParticipantLeft {
145 session_id: SessionId,
146 participant_id: ParticipantId,
147 at: DateTime<Utc>,
148 },
149
150 AiAttached {
152 connection_id: ConnectionId,
153 attachment_id: AiAttachmentId,
154 provider_ref: String,
155 at: DateTime<Utc>,
156 },
157 AiDetached {
158 attachment_id: AiAttachmentId,
159 at: DateTime<Utc>,
160 },
161 ListenerAttached {
162 listener_id: ListenerId,
163 at: DateTime<Utc>,
164 },
165 ListenerDetached {
166 listener_id: ListenerId,
167 at: DateTime<Utc>,
168 },
169
170 MessageReceived {
172 message_id: MessageId,
173 conversation_id: ConversationId,
174 at: DateTime<Utc>,
175 },
176 DataMessageReceived {
177 connection_id: ConnectionId,
178 message: DataMessage,
179 at: DateTime<Utc>,
180 },
181 MessageSent {
182 message_id: MessageId,
183 conversation_id: ConversationId,
184 at: DateTime<Utc>,
185 },
186 MessageDelivered {
187 message_id: MessageId,
188 at: DateTime<Utc>,
189 },
190 MessageRead {
191 message_id: MessageId,
192 at: DateTime<Utc>,
193 },
194
195 DtmfReceived {
197 connection_id: ConnectionId,
198 digits: String,
199 at: DateTime<Utc>,
200 },
201
202 TranscriptTurn {
204 stream_id: StreamId,
205 speaker: Option<ParticipantId>,
206 text: String,
207 confidence: f32,
208 is_final: bool,
209 assigned_provider: Option<String>,
210 at: DateTime<Utc>,
211 },
212 RecordingStarted {
213 recording_id: RecordingId,
214 at: DateTime<Utc>,
215 },
216 RecordingStopped {
217 recording_id: RecordingId,
218 at: DateTime<Utc>,
219 },
220 RecordingComplete {
221 recording_id: RecordingId,
222 sink: String,
223 vcon_ref: Option<VconRef>,
228 at: DateTime<Utc>,
229 },
230
231 VconReady {
233 session_id: SessionId,
234 handle: VconHandle,
235 at: DateTime<Utc>,
236 },
237 VconRedacted {
238 session_id: SessionId,
239 old: VconHandle,
240 new: VconHandle,
241 at: DateTime<Utc>,
242 },
243
244 IdentityAssuranceChanged {
246 connection_id: ConnectionId,
247 identity_id: Option<IdentityId>,
248 at: DateTime<Utc>,
249 },
250
251 IdentityStepUpRequested {
256 connection_id: ConnectionId,
257 required: crate::capability::IdentityAssuranceRequirement,
258 at: DateTime<Utc>,
259 },
260
261 IdentityStepUpResponseReceived {
267 connection_id: ConnectionId,
268 method: String,
269 credential: String,
270 at: DateTime<Utc>,
271 },
272
273 RegistrationChanged {
275 aor: String,
276 at: DateTime<Utc>,
277 },
278 RegistrationHeartbeat {
279 aor: String,
280 at: DateTime<Utc>,
281 },
282
283 CapacityReport {
285 tenant_id: Option<TenantId>,
286 active_connections: u64,
287 active_bridges: u64,
288 admission_in_use: u64,
289 at: DateTime<Utc>,
290 },
291 UsageRecord {
292 tenant_id: TenantId,
293 kind: UsageKind,
294 units: u64,
295 at: DateTime<Utc>,
296 },
297 Anomaly {
298 kind: AnomalyKind,
299 connection_id: Option<ConnectionId>,
300 detail: String,
301 at: DateTime<Utc>,
302 },
303 MediaQuality {
304 connection_id: ConnectionId,
305 snapshot: QualitySnapshot,
306 at: DateTime<Utc>,
307 },
308
309 BargeInDetected {
315 connection_id: ConnectionId,
316 ai_attachment_id: AiAttachmentId,
317 at: DateTime<Utc>,
318 },
319
320 ActiveSpeakerChanged {
326 session_id: SessionId,
327 connection_id: ConnectionId,
328 audio_level_dbov: i8,
329 at: DateTime<Utc>,
330 },
331}
332
333impl fmt::Debug for Event {
334 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
335 match self {
336 Self::ConversationOpened { .. } => formatter.write_str("ConversationOpened"),
337 Self::ConversationClosed { .. } => formatter.write_str("ConversationClosed"),
338 Self::SessionStarted { .. } => formatter.write_str("SessionStarted"),
339 Self::SessionEnded { report, .. } => formatter
340 .debug_struct("SessionEnded")
341 .field("quality_report_present", &report.is_some())
342 .finish(),
343 Self::SessionFailed { detail, .. } => formatter
344 .debug_struct("SessionFailed")
345 .field("detail_present", &!detail.is_empty())
346 .field("detail_bytes", &detail.len())
347 .finish(),
348 Self::ConnectionInbound { .. } => formatter.write_str("ConnectionInbound"),
349 Self::ConnectionOutbound { .. } => formatter.write_str("ConnectionOutbound"),
350 Self::ConnectionConnected { .. } => formatter.write_str("ConnectionConnected"),
351 Self::ConnectionAuthenticated { .. } => formatter.write_str("ConnectionAuthenticated"),
352 Self::ConnectionPrincipalAuthenticated { .. } => {
353 formatter.write_str("ConnectionPrincipalAuthenticated")
354 }
355 Self::ConnectionProgress { kind, .. } => formatter
356 .debug_struct("ConnectionProgress")
357 .field("kind", kind)
358 .finish(),
359 Self::ConnectionEnded { .. } => formatter.write_str("ConnectionEnded"),
360 Self::ConnectionFailed { detail, .. } => formatter
361 .debug_struct("ConnectionFailed")
362 .field("detail_present", &!detail.is_empty())
363 .field("detail_bytes", &detail.len())
364 .finish(),
365 Self::ConnectionsBridged { .. } => formatter.write_str("ConnectionsBridged"),
366 Self::ConnectionsUnbridged { .. } => formatter.write_str("ConnectionsUnbridged"),
367 Self::ConnectionTransferred { .. } => formatter.write_str("ConnectionTransferred"),
368 Self::ConnectionTransferStatus { status, .. } => formatter
369 .debug_struct("ConnectionTransferStatus")
370 .field("status", status)
371 .finish(),
372 Self::ParticipantJoined { .. } => formatter.write_str("ParticipantJoined"),
373 Self::ParticipantLeft { .. } => formatter.write_str("ParticipantLeft"),
374 Self::AiAttached { provider_ref, .. } => formatter
375 .debug_struct("AiAttached")
376 .field("provider_ref_present", &!provider_ref.is_empty())
377 .field("provider_ref_bytes", &provider_ref.len())
378 .finish(),
379 Self::AiDetached { .. } => formatter.write_str("AiDetached"),
380 Self::ListenerAttached { .. } => formatter.write_str("ListenerAttached"),
381 Self::ListenerDetached { .. } => formatter.write_str("ListenerDetached"),
382 Self::MessageReceived { .. } => formatter.write_str("MessageReceived"),
383 Self::DataMessageReceived { message, .. } => formatter
384 .debug_struct("DataMessageReceived")
385 .field("body_bytes", &message.bytes.len())
386 .finish(),
387 Self::MessageSent { .. } => formatter.write_str("MessageSent"),
388 Self::MessageDelivered { .. } => formatter.write_str("MessageDelivered"),
389 Self::MessageRead { .. } => formatter.write_str("MessageRead"),
390 Self::DtmfReceived { digits, .. } => formatter
391 .debug_struct("DtmfReceived")
392 .field("digit_count", &digits.chars().count())
393 .finish(),
394 Self::TranscriptTurn {
395 text,
396 speaker,
397 confidence,
398 is_final,
399 assigned_provider,
400 ..
401 } => formatter
402 .debug_struct("TranscriptTurn")
403 .field("speaker_present", &speaker.is_some())
404 .field("text_bytes", &text.len())
405 .field("confidence", confidence)
406 .field("is_final", is_final)
407 .field("assigned_provider_present", &assigned_provider.is_some())
408 .finish(),
409 Self::RecordingStarted { .. } => formatter.write_str("RecordingStarted"),
410 Self::RecordingStopped { .. } => formatter.write_str("RecordingStopped"),
411 Self::RecordingComplete { sink, vcon_ref, .. } => formatter
412 .debug_struct("RecordingComplete")
413 .field("sink_present", &!sink.is_empty())
414 .field("sink_bytes", &sink.len())
415 .field("vcon_ref_present", &vcon_ref.is_some())
416 .finish(),
417 Self::VconReady { .. } => formatter.write_str("VconReady"),
418 Self::VconRedacted { .. } => formatter.write_str("VconRedacted"),
419 Self::IdentityAssuranceChanged { identity_id, .. } => formatter
420 .debug_struct("IdentityAssuranceChanged")
421 .field("identity_present", &identity_id.is_some())
422 .finish(),
423 Self::IdentityStepUpRequested { .. } => formatter.write_str("IdentityStepUpRequested"),
424 Self::IdentityStepUpResponseReceived {
425 method, credential, ..
426 } => formatter
427 .debug_struct("IdentityStepUpResponseReceived")
428 .field("method_present", &!method.is_empty())
429 .field("method_bytes", &method.len())
430 .field("credential_present", &!credential.is_empty())
431 .field("credential_bytes", &credential.len())
432 .finish(),
433 Self::RegistrationChanged { .. } => formatter.write_str("RegistrationChanged"),
434 Self::RegistrationHeartbeat { .. } => formatter.write_str("RegistrationHeartbeat"),
435 Self::CapacityReport {
436 tenant_id,
437 active_connections,
438 active_bridges,
439 admission_in_use,
440 ..
441 } => formatter
442 .debug_struct("CapacityReport")
443 .field("tenant_present", &tenant_id.is_some())
444 .field("active_connections", active_connections)
445 .field("active_bridges", active_bridges)
446 .field("admission_in_use", admission_in_use)
447 .finish(),
448 Self::UsageRecord { kind, units, .. } => formatter
449 .debug_struct("UsageRecord")
450 .field("kind", kind)
451 .field("units", units)
452 .finish(),
453 Self::Anomaly {
454 kind,
455 connection_id,
456 detail,
457 ..
458 } => formatter
459 .debug_struct("Anomaly")
460 .field("kind", kind)
461 .field("connection_present", &connection_id.is_some())
462 .field("detail_present", &!detail.is_empty())
463 .field("detail_bytes", &detail.len())
464 .finish(),
465 Self::MediaQuality { .. } => formatter.write_str("MediaQuality"),
466 Self::BargeInDetected { .. } => formatter.write_str("BargeInDetected"),
467 Self::ActiveSpeakerChanged {
468 audio_level_dbov, ..
469 } => formatter
470 .debug_struct("ActiveSpeakerChanged")
471 .field("audio_level_dbov", audio_level_dbov)
472 .finish(),
473 }
474 }
475}
476
477#[derive(Clone, Default)]
480pub struct SessionQualityReport {
481 pub mos: Option<f32>,
482 pub packet_loss_pct: f32,
483 pub jitter_ms: f32,
484 pub rtt_ms: Option<f32>,
485 pub codec: Option<String>,
486 pub bitrate_bps: Option<u32>,
487 pub talk_pct: Option<f32>,
488 pub silence_pct: Option<f32>,
489 pub pdd_ms: Option<u32>,
490 pub ring_time_ms: Option<u32>,
491 pub setup_time_ms: Option<u32>,
492 pub hangup_reason: Option<String>,
493}
494
495impl fmt::Debug for SessionQualityReport {
496 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
497 formatter
498 .debug_struct("SessionQualityReport")
499 .field("mos", &self.mos)
500 .field("packet_loss_pct", &self.packet_loss_pct)
501 .field("jitter_ms", &self.jitter_ms)
502 .field("rtt_ms", &self.rtt_ms)
503 .field("codec_present", &self.codec.is_some())
504 .field("codec_bytes", &self.codec.as_ref().map_or(0, String::len))
505 .field("bitrate_bps", &self.bitrate_bps)
506 .field("talk_pct", &self.talk_pct)
507 .field("silence_pct", &self.silence_pct)
508 .field("pdd_ms", &self.pdd_ms)
509 .field("ring_time_ms", &self.ring_time_ms)
510 .field("setup_time_ms", &self.setup_time_ms)
511 .field("hangup_reason_present", &self.hangup_reason.is_some())
512 .field(
513 "hangup_reason_bytes",
514 &self.hangup_reason.as_ref().map_or(0, String::len),
515 )
516 .finish()
517 }
518}
519
520#[derive(Clone, Copy, Debug, Eq, PartialEq)]
521pub enum ConnectionProgressKind {
522 Trying,
523 Ringing,
524 EarlyMedia,
525 Busy,
526 NoAnswer,
527 AnsweringMachine,
528 HumanAnswered,
529}
530
531#[derive(Clone, Copy, Debug, Eq, PartialEq)]
532pub enum UsageKind {
533 SessionSeconds,
534 RecordingSeconds,
535 TranscriptionSeconds,
536 BridgedMinutes,
537 MessagesSent,
538}
539
540#[derive(Clone, Copy, Debug, Eq, PartialEq)]
541pub enum AnomalyKind {
542 QualityDrop,
543 PossibleFraud,
544 UnexpectedTeardown,
545 AssuranceMismatch,
546}
547
548impl Event {
549 pub fn to_cross_crate(&self) -> RvoipCrossCrateEvent {
554 use Event::*;
555 let inner = match self {
556 ConversationOpened {
557 conversation_id, ..
558 } => RvoipCoreCrossCrateEvent::ConversationOpened {
559 conversation_id: conversation_id.to_string(),
560 },
561 ConversationClosed {
562 conversation_id, ..
563 } => RvoipCoreCrossCrateEvent::ConversationClosed {
564 conversation_id: conversation_id.to_string(),
565 },
566 SessionStarted {
567 session_id,
568 conversation_id,
569 ..
570 } => RvoipCoreCrossCrateEvent::SessionStarted {
571 session_id: session_id.to_string(),
572 conversation_id: conversation_id.to_string(),
573 },
574 SessionEnded { session_id, .. } => RvoipCoreCrossCrateEvent::SessionEnded {
575 session_id: session_id.to_string(),
576 },
577 SessionFailed {
578 session_id, detail, ..
579 } => RvoipCoreCrossCrateEvent::SessionFailed {
580 session_id: session_id.to_string(),
581 detail: detail.clone(),
582 },
583 ConnectionInbound { connection_id, .. } => {
584 RvoipCoreCrossCrateEvent::ConnectionInbound {
585 connection_id: connection_id.to_string(),
586 }
587 }
588 ConnectionOutbound { connection_id, .. } => {
589 RvoipCoreCrossCrateEvent::ConnectionOutbound {
590 connection_id: connection_id.to_string(),
591 }
592 }
593 ConnectionConnected { connection_id, .. } => {
594 RvoipCoreCrossCrateEvent::ConnectionConnected {
595 connection_id: connection_id.to_string(),
596 }
597 }
598 ConnectionAuthenticated { connection_id, .. } => {
599 RvoipCoreCrossCrateEvent::IdentityAssuranceChanged {
604 connection_id: connection_id.to_string(),
605 identity_id: None,
606 }
607 }
608 ConnectionPrincipalAuthenticated { connection_id, .. } => {
609 RvoipCoreCrossCrateEvent::IdentityAssuranceChanged {
610 connection_id: connection_id.to_string(),
611 identity_id: None,
612 }
613 }
614 ConnectionProgress {
615 connection_id,
616 kind,
617 ..
618 } => RvoipCoreCrossCrateEvent::ConnectionProgress {
619 connection_id: connection_id.to_string(),
620 kind: format!("{:?}", kind),
621 },
622 ConnectionEnded {
623 connection_id,
624 reason,
625 ..
626 } => RvoipCoreCrossCrateEvent::ConnectionEnded {
627 connection_id: connection_id.to_string(),
628 reason: format!("{:?}", reason),
629 },
630 ConnectionFailed {
631 connection_id,
632 detail,
633 ..
634 } => RvoipCoreCrossCrateEvent::ConnectionFailed {
635 connection_id: connection_id.to_string(),
636 detail: detail.clone(),
637 },
638 ConnectionsBridged {
639 bridge_id, a, b, ..
640 } => RvoipCoreCrossCrateEvent::ConnectionsBridged {
641 bridge_id: bridge_id.to_string(),
642 a: a.to_string(),
643 b: b.to_string(),
644 },
645 ConnectionsUnbridged { bridge_id, .. } => {
646 RvoipCoreCrossCrateEvent::ConnectionsUnbridged {
647 bridge_id: bridge_id.to_string(),
648 }
649 }
650 ConnectionTransferred {
651 connection_id,
652 target,
653 ..
654 } => RvoipCoreCrossCrateEvent::ConnectionTransferred {
655 connection_id: connection_id.to_string(),
656 target: format!("{:?}", target),
657 },
658 ConnectionTransferStatus {
659 connection_id,
660 status,
661 ..
662 } => RvoipCoreCrossCrateEvent::ConnectionProgress {
663 connection_id: connection_id.to_string(),
664 kind: format!("transfer:{status:?}"),
665 },
666 ParticipantJoined {
667 session_id,
668 participant_id,
669 ..
670 } => RvoipCoreCrossCrateEvent::ParticipantJoined {
671 session_id: session_id.to_string(),
672 participant_id: participant_id.to_string(),
673 },
674 ParticipantLeft {
675 session_id,
676 participant_id,
677 ..
678 } => RvoipCoreCrossCrateEvent::ParticipantLeft {
679 session_id: session_id.to_string(),
680 participant_id: participant_id.to_string(),
681 },
682 AiAttached {
683 connection_id,
684 attachment_id,
685 provider_ref,
686 ..
687 } => RvoipCoreCrossCrateEvent::AiAttached {
688 connection_id: connection_id.to_string(),
689 attachment_id: attachment_id.to_string(),
690 provider_ref: provider_ref.clone(),
691 },
692 AiDetached { attachment_id, .. } => RvoipCoreCrossCrateEvent::AiDetached {
693 attachment_id: attachment_id.to_string(),
694 },
695 ListenerAttached { listener_id, .. } => RvoipCoreCrossCrateEvent::ListenerAttached {
696 listener_id: listener_id.to_string(),
697 },
698 ListenerDetached { listener_id, .. } => RvoipCoreCrossCrateEvent::ListenerDetached {
699 listener_id: listener_id.to_string(),
700 },
701 MessageReceived {
702 message_id,
703 conversation_id,
704 ..
705 } => RvoipCoreCrossCrateEvent::MessageReceived {
706 message_id: message_id.to_string(),
707 conversation_id: conversation_id.to_string(),
708 },
709 DataMessageReceived {
710 connection_id,
711 message,
712 ..
713 } => RvoipCoreCrossCrateEvent::DataMessageReceived {
714 connection_id: connection_id.to_string(),
715 body_size: message.bytes.len(),
716 reliability: format!("{:?}", message.reliability),
717 },
718 MessageSent {
719 message_id,
720 conversation_id,
721 ..
722 } => RvoipCoreCrossCrateEvent::MessageSent {
723 message_id: message_id.to_string(),
724 conversation_id: conversation_id.to_string(),
725 },
726 MessageDelivered { message_id, .. } => RvoipCoreCrossCrateEvent::MessageDelivered {
727 message_id: message_id.to_string(),
728 },
729 MessageRead { message_id, .. } => RvoipCoreCrossCrateEvent::MessageRead {
730 message_id: message_id.to_string(),
731 },
732 DtmfReceived {
733 connection_id,
734 digits,
735 ..
736 } => RvoipCoreCrossCrateEvent::DtmfReceived {
737 connection_id: connection_id.to_string(),
738 digits: digits.clone(),
739 },
740 TranscriptTurn {
741 stream_id,
742 speaker,
743 text,
744 confidence,
745 is_final,
746 assigned_provider,
747 ..
748 } => RvoipCoreCrossCrateEvent::TranscriptTurn {
749 stream_id: stream_id.to_string(),
750 speaker: speaker.as_ref().map(|p| p.to_string()),
751 text: text.clone(),
752 confidence: *confidence,
753 is_final: *is_final,
754 assigned_provider: assigned_provider.clone(),
755 },
756 RecordingStarted { recording_id, .. } => RvoipCoreCrossCrateEvent::RecordingStarted {
757 recording_id: recording_id.to_string(),
758 },
759 RecordingStopped { recording_id, .. } => RvoipCoreCrossCrateEvent::RecordingStopped {
760 recording_id: recording_id.to_string(),
761 },
762 RecordingComplete {
763 recording_id,
764 sink,
765 vcon_ref: _,
766 ..
767 } => RvoipCoreCrossCrateEvent::RecordingComplete {
768 recording_id: recording_id.to_string(),
769 sink: sink.clone(),
770 },
771 VconReady {
772 session_id, handle, ..
773 } => RvoipCoreCrossCrateEvent::VconReady {
774 session_id: session_id.to_string(),
775 handle_url: handle.url.clone(),
776 content_hash: handle.content_hash.clone(),
777 },
778 VconRedacted {
779 session_id,
780 old,
781 new,
782 ..
783 } => RvoipCoreCrossCrateEvent::VconRedacted {
784 session_id: session_id.to_string(),
785 old_url: old.url.clone(),
786 new_url: new.url.clone(),
787 },
788 IdentityAssuranceChanged {
789 connection_id,
790 identity_id,
791 ..
792 } => RvoipCoreCrossCrateEvent::IdentityAssuranceChanged {
793 connection_id: connection_id.to_string(),
794 identity_id: identity_id.as_ref().map(|i| i.to_string()),
795 },
796 RegistrationChanged { aor, .. } => {
797 RvoipCoreCrossCrateEvent::RegistrationChanged { aor: aor.clone() }
798 }
799 RegistrationHeartbeat { aor, .. } => {
800 RvoipCoreCrossCrateEvent::RegistrationHeartbeat { aor: aor.clone() }
801 }
802 CapacityReport {
803 tenant_id,
804 active_connections,
805 active_bridges,
806 admission_in_use,
807 ..
808 } => RvoipCoreCrossCrateEvent::CapacityReport {
809 tenant_id: tenant_id.as_ref().map(|t| t.to_string()),
810 active_connections: *active_connections,
811 active_bridges: *active_bridges,
812 admission_in_use: *admission_in_use,
813 },
814 UsageRecord {
815 tenant_id,
816 kind,
817 units,
818 ..
819 } => RvoipCoreCrossCrateEvent::UsageRecord {
820 tenant_id: tenant_id.to_string(),
821 kind: format!("{:?}", kind),
822 units: *units,
823 },
824 Anomaly {
825 kind,
826 connection_id,
827 detail,
828 ..
829 } => RvoipCoreCrossCrateEvent::Anomaly {
830 kind: format!("{:?}", kind),
831 connection_id: connection_id.as_ref().map(|c| c.to_string()),
832 detail: detail.clone(),
833 },
834 MediaQuality {
835 connection_id,
836 snapshot,
837 ..
838 } => RvoipCoreCrossCrateEvent::MediaQuality {
839 connection_id: connection_id.to_string(),
840 jitter_ms: snapshot.jitter_ms,
841 packet_loss_pct: snapshot.packet_loss_pct,
842 mos: snapshot.mos,
843 },
844 BargeInDetected { connection_id, .. } => {
845 RvoipCoreCrossCrateEvent::IdentityAssuranceChanged {
849 connection_id: connection_id.to_string(),
850 identity_id: None,
851 }
852 }
853 ActiveSpeakerChanged { connection_id, .. } => {
854 RvoipCoreCrossCrateEvent::MediaQuality {
858 connection_id: connection_id.to_string(),
859 jitter_ms: 0.0,
860 packet_loss_pct: 0.0,
861 mos: None,
862 }
863 }
864 IdentityStepUpRequested { connection_id, .. }
865 | IdentityStepUpResponseReceived { connection_id, .. } => {
866 RvoipCoreCrossCrateEvent::IdentityAssuranceChanged {
873 connection_id: connection_id.to_string(),
874 identity_id: None,
875 }
876 }
877 };
878 RvoipCrossCrateEvent::Core(inner)
879 }
880}
881
882#[cfg(test)]
883mod credential_diagnostic_tests {
884 use super::*;
885
886 #[test]
887 fn enclosing_step_up_event_redacts_credential_and_keeps_live_value() {
888 const CANARY: &str = "core-event-credential-canary\r\nAuthorization: exposed";
889 let event = Event::IdentityStepUpResponseReceived {
890 connection_id: ConnectionId::new(),
891 method: "bearer".into(),
892 credential: CANARY.into(),
893 at: Utc::now(),
894 };
895 let rendered = format!("{event:?}");
896 assert!(!rendered.contains(CANARY), "credential leaked: {rendered}");
897 match event {
898 Event::IdentityStepUpResponseReceived { credential, .. } => {
899 assert_eq!(credential, CANARY)
900 }
901 other => panic!("unexpected event: {other:?}"),
902 }
903 }
904
905 #[test]
906 fn quality_report_debug_redacts_codec_and_hangup_text() {
907 const CANARY: &str = "quality-report-canary\r\nAuthorization: exposed";
908 let report = SessionQualityReport {
909 codec: Some(CANARY.into()),
910 hangup_reason: Some(CANARY.into()),
911 ..SessionQualityReport::default()
912 };
913 let debug = format!("{report:?}");
914 assert!(!debug.contains(CANARY));
915 assert!(debug.contains("codec_present: true"));
916 assert!(debug.contains("hangup_reason_present: true"));
917 }
918}