rvoip-core 0.3.4

Transport-agnostic spine for RVoIP — types, ConnectionAdapter trait, BridgeManager, Orchestrator entry point
Documentation
use crate::ids::{ConversationId, IdentityId, ParticipantId};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum ParticipantKind {
    Human,
    Ai,
    System,
    External,
}

#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum ParticipantRole {
    Customer,
    Agent,
    Supervisor,
    Observer,
    Custom(String),
}

impl fmt::Debug for ParticipantRole {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Customer => "Customer",
            Self::Agent => "Agent",
            Self::Supervisor => "Supervisor",
            Self::Observer => "Observer",
            Self::Custom(_) => "Custom",
        })
    }
}

#[derive(Clone)]
pub struct Participant {
    pub id: ParticipantId,
    pub conversation_id: ConversationId,
    pub identity_ref: Option<IdentityId>,
    pub kind: ParticipantKind,
    pub role: ParticipantRole,
    pub display_name: Option<String>,
    pub joined_at: DateTime<Utc>,
    pub left_at: Option<DateTime<Utc>>,
}

impl fmt::Debug for Participant {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Participant")
            .field("identity_ref_present", &self.identity_ref.is_some())
            .field("kind", &self.kind)
            .field("role", &self.role)
            .field("display_name_present", &self.display_name.is_some())
            .field("joined_at", &self.joined_at)
            .field("left_at", &self.left_at)
            .finish()
    }
}

#[cfg(test)]
mod diagnostic_tests {
    use super::*;

    #[test]
    fn participant_debug_redacts_identity_and_custom_values() {
        let secret = "credential-canary-value";
        let participant = Participant {
            id: ParticipantId::new(),
            conversation_id: ConversationId::new(),
            identity_ref: Some(IdentityId::new()),
            kind: ParticipantKind::External,
            role: ParticipantRole::Custom(secret.into()),
            display_name: Some(secret.into()),
            joined_at: Utc::now(),
            left_at: None,
        };
        assert!(!format!("{participant:?}").contains(secret));
        assert!(!format!("{:?}", participant.role).contains(secret));
    }
}