concord 2.5.0

A terminal user interface client for Discord
use super::{
    DISCORD_OPUS_SILENCE_FRAME, DISCORD_OPUS_TIMESTAMP_INCREMENT, RTP_AEAD_NONCE_SUFFIX_BYTES,
    dave::{VoiceDaveOutboundPayload, VoiceDaveState},
    rtp::{VoiceOutboundRtpState, VoiceRtpEncryptor, build_voice_rtp_packet_with_marker},
};

#[allow(dead_code)]
pub(super) struct VoiceOutboundSendState {
    pub(super) rtp: VoiceOutboundRtpState,
    pub(super) encryptor: VoiceRtpEncryptor,
    pub(super) nonce_suffix: u32,
    pub(super) allow_microphone_transmit: bool,
    pub(super) self_mute: bool,
    pub(super) dave_active: bool,
    pub(super) speaking: bool,
    pub(super) logged_block_reason: Option<VoiceOutboundSendBlockReason>,
    pub(super) events: Vec<VoiceOutboundSendEvent>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[allow(dead_code)]
pub(super) enum VoiceOutboundSendEvent {
    Speaking { speaking: bool, ssrc: u32 },
    Packet { bytes: Vec<u8> },
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(dead_code)]
pub(super) enum VoiceOutboundSendOutcome {
    Noop,
    Sent,
    Blocked(VoiceOutboundSendBlockReason),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(dead_code)]
#[allow(clippy::enum_variant_names)]
pub(super) enum VoiceOutboundSendBlockReason {
    DaveOutboundUnsupported,
    DaveOutboundMissingSession,
    DaveOutboundNotReady,
    DaveOutboundEncryptFailed,
}

#[allow(dead_code)]
impl VoiceOutboundSendState {
    pub(super) fn new(
        mode: &str,
        secret_key: &[u8],
        rtp: VoiceOutboundRtpState,
        nonce_suffix: u32,
    ) -> Result<Self, String> {
        Ok(Self {
            rtp,
            encryptor: VoiceRtpEncryptor::new(mode, secret_key)?,
            nonce_suffix,
            allow_microphone_transmit: false,
            self_mute: true,
            dave_active: false,
            speaking: false,
            logged_block_reason: None,
            events: Vec::new(),
        })
    }

    pub(super) fn set_capture_gate(&mut self, allow_microphone_transmit: bool, self_mute: bool) {
        self.allow_microphone_transmit = allow_microphone_transmit;
        self.self_mute = self_mute;
    }

    #[allow(dead_code)]
    pub(super) fn set_dave_active(&mut self, active: bool) {
        self.dave_active = active;
    }

    #[allow(dead_code)]
    pub(super) fn events(&self) -> &[VoiceOutboundSendEvent] {
        &self.events
    }

    #[allow(dead_code)]
    pub(super) fn take_events(&mut self) -> Vec<VoiceOutboundSendEvent> {
        std::mem::take(&mut self.events)
    }

    #[allow(dead_code)]
    pub(super) fn record_blocked_transmit(&mut self, reason: VoiceOutboundSendBlockReason) -> bool {
        if self.logged_block_reason == Some(reason) {
            return false;
        }
        self.logged_block_reason = Some(reason);
        true
    }

    #[allow(dead_code)]
    pub(super) fn take_logged_block_reason(&mut self) -> Option<VoiceOutboundSendBlockReason> {
        self.logged_block_reason.take()
    }

    #[allow(dead_code)]
    pub(super) fn send_opus_frame(
        &mut self,
        opus_payload: &[u8],
    ) -> Result<VoiceOutboundSendOutcome, String> {
        self.send_opus_frame_with_dave_payload(VoiceDaveOutboundPayload::Plain(
            opus_payload.to_vec(),
        ))
    }

    pub(super) fn send_opus_frame_with_dave(
        &mut self,
        opus_payload: &[u8],
        dave: &mut VoiceDaveState,
    ) -> Result<VoiceOutboundSendOutcome, String> {
        let dave_payload = dave.prepare_outbound_opus(opus_payload);
        self.send_opus_frame_with_dave_payload(dave_payload)
    }

    pub(super) fn send_opus_frame_with_dave_payload(
        &mut self,
        dave_payload: VoiceDaveOutboundPayload,
    ) -> Result<VoiceOutboundSendOutcome, String> {
        if !self.capture_gate_enabled() {
            return Ok(VoiceOutboundSendOutcome::Noop);
        }
        if self.dave_active {
            return Ok(VoiceOutboundSendOutcome::Blocked(
                VoiceOutboundSendBlockReason::DaveOutboundUnsupported,
            ));
        }
        let opus_payload = match dave_payload {
            VoiceDaveOutboundPayload::Plain(opus) | VoiceDaveOutboundPayload::Encrypted(opus) => {
                opus
            }
            VoiceDaveOutboundPayload::Blocked(reason) => {
                return Ok(VoiceOutboundSendOutcome::Blocked(reason));
            }
        };

        let starts_talkspurt = !self.speaking;
        let encrypted = self.encrypt_current_packet(&opus_payload, starts_talkspurt)?;
        if starts_talkspurt {
            self.events.push(VoiceOutboundSendEvent::Speaking {
                speaking: true,
                ssrc: self.rtp.ssrc,
            });
            self.speaking = true;
        }
        self.events
            .push(VoiceOutboundSendEvent::Packet { bytes: encrypted });
        self.advance_packet_state();
        Ok(VoiceOutboundSendOutcome::Sent)
    }

    #[allow(dead_code)]
    pub(super) fn stop_speaking(&mut self) -> Result<VoiceOutboundSendOutcome, String> {
        Ok(self.stop_speaking_immediately())
    }

    pub(super) fn stop_speaking_with_dave(
        &mut self,
        _dave: &mut VoiceDaveState,
    ) -> Result<VoiceOutboundSendOutcome, String> {
        Ok(self.stop_speaking_immediately())
    }

    pub(super) fn send_trailing_silence_frame_with_dave(
        &mut self,
        dave: &mut VoiceDaveState,
        finish_talkspurt: bool,
    ) -> Result<VoiceOutboundSendOutcome, String> {
        let dave_payload = dave.prepare_outbound_opus(&DISCORD_OPUS_SILENCE_FRAME);
        self.send_trailing_silence_frame_with_dave_payload(dave_payload, finish_talkspurt)
    }

    pub(super) fn send_trailing_silence_frame_with_dave_payload(
        &mut self,
        dave_payload: VoiceDaveOutboundPayload,
        finish_talkspurt: bool,
    ) -> Result<VoiceOutboundSendOutcome, String> {
        if !self.speaking {
            return Ok(VoiceOutboundSendOutcome::Noop);
        }
        if self.dave_active {
            return Ok(self.queue_speaking_off());
        }
        if self.ensure_nonce_capacity(1).is_err() {
            return Ok(self.queue_speaking_off());
        }

        let opus_payload = match dave_payload {
            VoiceDaveOutboundPayload::Plain(opus) | VoiceDaveOutboundPayload::Encrypted(opus) => {
                opus
            }
            VoiceDaveOutboundPayload::Blocked(_) => return Ok(self.queue_speaking_off()),
        };
        let encrypted = self.encrypt_current_packet(&opus_payload, false)?;
        self.events
            .push(VoiceOutboundSendEvent::Packet { bytes: encrypted });
        self.advance_packet_state();

        if finish_talkspurt {
            return Ok(self.queue_speaking_off());
        }
        Ok(VoiceOutboundSendOutcome::Sent)
    }

    pub(super) fn stop_speaking_immediately(&mut self) -> VoiceOutboundSendOutcome {
        if !self.speaking {
            return VoiceOutboundSendOutcome::Noop;
        }
        self.queue_speaking_off()
    }

    pub(super) fn queue_speaking_off(&mut self) -> VoiceOutboundSendOutcome {
        self.events.push(VoiceOutboundSendEvent::Speaking {
            speaking: false,
            ssrc: self.rtp.ssrc,
        });
        self.speaking = false;
        VoiceOutboundSendOutcome::Sent
    }

    pub(super) fn capture_gate_enabled(&self) -> bool {
        self.allow_microphone_transmit && !self.self_mute
    }

    pub(super) fn encrypt_current_packet(
        &self,
        opus_payload: &[u8],
        marker: bool,
    ) -> Result<Vec<u8>, String> {
        let nonce_suffix = self.current_nonce_suffix()?;
        let packet = build_voice_rtp_packet_with_marker(
            self.rtp.sequence,
            self.rtp.timestamp,
            self.rtp.ssrc,
            marker,
            opus_payload,
        )?;
        self.encryptor.encrypt_packet(&packet, nonce_suffix)
    }

    pub(super) fn current_nonce_suffix(&self) -> Result<[u8; RTP_AEAD_NONCE_SUFFIX_BYTES], String> {
        if self.nonce_suffix == u32::MAX {
            return Err("voice RTP nonce suffix exhausted".to_owned());
        }
        Ok(self.nonce_suffix.to_be_bytes())
    }

    pub(super) fn ensure_nonce_capacity(&self, packets: usize) -> Result<(), String> {
        let remaining = u32::MAX - self.nonce_suffix;
        if remaining < packets as u32 {
            return Err("voice RTP nonce suffix exhausted".to_owned());
        }
        Ok(())
    }

    pub(super) fn advance_packet_state(&mut self) {
        self.rtp.sequence = self.rtp.sequence.wrapping_add(1);
        self.nonce_suffix = self.nonce_suffix.saturating_add(1);
    }

    pub(super) fn advance_media_clock_frames(&mut self, frames: u32) {
        self.rtp.timestamp = self
            .rtp
            .timestamp
            .wrapping_add(DISCORD_OPUS_TIMESTAMP_INCREMENT.wrapping_mul(frames));
    }
}