Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  mod.rs   Sprache: unbekannt

 
Spracherkennung für: .rs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

//! Common items needed in many places.
use core::{
    array::TryFromSliceError,
    fmt,
    num::ParseIntError,
    ops::Deref,
    str::{self, FromStr},
};
use std::env;

#[cfg(test)]
use anyhow;
use data_encoding::HEXLOWER;
use libthreema_macros::Name;
use rand::{self, Rng as _};
use serde::{Deserialize, Serialize};
use tracing::warn;

use crate::{
    crypto::{consts::U24, generic_array::GenericArray},
    protobuf,
    utils::{apply::Apply, debug::Name as _, protobuf::PaddedMessage as _, time::utc_now_ms},
};

pub mod config;
pub mod keys;
pub mod task;

/// Client info
#[derive(Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(
    feature = "wasm",
    derive(tsify::Tsify, serde::Deserialize),
    serde(tag = "platform", rename_all = "kebab-case", rename_all_fields = "camelCase"),
    tsify(from_wasm_abi)
)]
pub enum ClientInfo {
    /// Android
    Android {
        /// Version string.
        version: String,

        /// Locale, i.e. `<language>/<country-code>` where:
        ///
        /// - `<language>` is an ISO 639-1:2002-ish language code
        /// - `<country-code>` is an ISO 3166-1-ish country code
        locale: String,

        /// Device model.
        device_model: String,

        /// OS version.
        os_version: String,
    },

    /// iOS
    Ios {
        /// Version string.
        version: String,

        /// Locale, i.e. `<language>/<country-code>` where:
        ///
        /// - `<language>` is an ISO 639-1:2002-ish language code
        /// - `<country-code>` is an ISO 3166-1-ish country code
        locale: String,

        /// Device model.
        device_model: String,

        /// OS version.
        os_version: String,
    },

    /// Desktop 2.x
    Desktop {
        /// Version string.
        version: String,

        /// Locale, i.e. `<language>/<country-code>` where:
        ///
        /// - `<language>` is an ISO 639-1:2002-ish language code
        /// - `<country-code>` is an ISO 3166-1-ish country code
        locale: String,

        /// Renderer name (e.g. `electron`).
        renderer_name: String,

        /// Renderer version.
        renderer_version: String,

        /// OS name (e.g. `linux`).
        os_name: String,

        /// OS architecture (e.g. `x64`).
        os_architecture: String,
    },

    /// libthreema standalone (CLI, testing, ...)
    Libthreema,
}
impl ClientInfo {
    /// Encode to HTTPS user agent name.
    #[must_use]
    pub fn to_user_agent(&self) -> String {
        match self {
            ClientInfo::Android { version, .. } => format!("Threema Android/{version}"),
            ClientInfo::Ios { version, .. } => format!("Threema iOS/{version}"),
            ClientInfo::Desktop { version, .. } => format!("Threema Desktop/{version}"),
            ClientInfo::Libthreema => format!("libthreema/{version}", version = env!("CARGO_PKG_VERSION")),
        }
    }

    /// Encode to colon-separated as used by the CSP `client-info` (hopefully not for long).
    #[must_use]
    pub fn to_semicolon_separated(&self) -> String {
        match self {
            ClientInfo::Android {
                version,
                locale,
                device_model,
                os_version,
            } => {
                format!(
                    "{version};A;{locale};{device_model};{os_version}",
                    version = version.replace(';', "_"),
                    locale = locale.replace(';', "_"),
                    device_model = device_model.replace(';', "_"),
                    os_version = os_version.replace(';', "_")
                )
            },

            ClientInfo::Ios {
                version,
                locale,
                device_model,
                os_version,
            } => {
                format!(
                    "{version};I;{locale};{device_model};{os_version}",
                    version = version.replace(';', "_"),
                    locale = locale.replace(';', "_"),
                    device_model = device_model.replace(';', "_"),
                    os_version = os_version.replace(';', "_")
                )
            },

            ClientInfo::Desktop {
                version,
                locale,
                renderer_name,
                renderer_version,
                os_name,
                os_architecture,
            } => {
                format!(
                    "{version};Q;{locale};{renderer_name};{renderer_version};{os_name};{os_architecture}",
                    version = version.replace(';', "_"),
                    locale = locale.replace(';', "_"),
                    renderer_name = renderer_name.replace(';', "_"),
                    renderer_version = renderer_version.replace(';', "_"),
                    os_name = os_name.replace(';', "_"),
                    os_architecture = os_architecture.replace(';', "_")
                )
            },

            ClientInfo::Libthreema => {
                format!(
                    "{version};L;en/CH;{os_name};{os_architecture}",
                    version = env!("CARGO_PKG_VERSION"),
                    os_name = env::consts::OS,
                    os_architecture = env::consts::ARCH
                )
            },
        }
    }

    /// Construct a [`protobuf::d2d::DeviceInfo`] from a device label and the [`ClientInfo`].
    ///
    /// The device label (e.g. "PC at Work") is recommended to not exceed 64 grapheme clusters.
    pub(crate) fn to_device_info(&self, label: Option<String>) -> protobuf::d2d::DeviceInfo {
        let (platform, platform_details, app_version) = match self {
            ClientInfo::Android {
                version,
                device_model,
                ..
            } => (
                protobuf::d2d::device_info::Platform::Android,
                device_model.clone(),
                version.clone(),
            ),

            ClientInfo::Ios {
                version,
                device_model,
                ..
            } => (
                protobuf::d2d::device_info::Platform::Ios,
                device_model.clone(),
                version.clone(),
            ),

            ClientInfo::Desktop {
                version,
                renderer_name,
                renderer_version,
                os_name,
                ..
            } => (
                protobuf::d2d::device_info::Platform::Desktop,
                format!("{renderer_name} {renderer_version} ({os_name})"),
                version.clone(),
            ),

            ClientInfo::Libthreema => (
                protobuf::d2d::device_info::Platform::Unspecified,
                format!("libthreema ({os_name})", os_name = env::consts::OS),
                env!("CARGO_PKG_VERSION").to_owned(),
            ),
        };
        protobuf::d2d::DeviceInfo {
            #[expect(deprecated, reason = "Will be filled by encode_to_vec_padded")]
            padding: vec![],
            platform: platform as i32,
            platform_details,
            app_version,
            label: label.unwrap_or_default(),
        }
    }
}

/// Invalid [`ThreemaId`].
#[derive(Debug, thiserror::Error)]
pub enum ThreemaIdError {
    /// Invalid length (must be exactly 8 bytes).
    #[error("Threema ID must be exactly 8 bytes")]
    InvalidLength,

    /// Invalid symbols provided.
    #[error("Threema ID contains invalid symbols")]
    InvalidSymbols,
}

/// A valid Threema ID.
#[expect(
    clippy::unsafe_derive_deserialize,
    reason = "False positive triggered by the unsafe block in as_str, \
    see https://github.com/rust-lang/rust-clippy/issues/10349"
)]
#[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, Name)]
#[serde(try_from = "&str", into = "String")]
pub struct ThreemaId([u8; Self::LENGTH]);
impl ThreemaId {
    /// Byte length of a Threema ID.
    pub const LENGTH: usize = 8;

    /// Construct a predefined Threema ID.
    ///
    /// IMPORTANT: This skips the validation, so `identity` must be known to be valid!
    #[must_use]
    pub const fn predefined(identity: [u8; Self::LENGTH]) -> Self {
        ThreemaId(identity)
    }

    /// Byte representation of the Threema ID.
    #[inline]
    #[must_use]
    pub fn to_bytes(self) -> [u8; Self::LENGTH] {
        self.0
    }

    /// String representation of the Threema ID.
    #[inline]
    #[must_use]
    pub fn as_str(&self) -> &str {
        // SAFETY: This is safe because the creation of a `ThreemaId` requires that it is a valid
        // UTF-8 sequence.
        unsafe { str::from_utf8_unchecked(&self.0) }
    }

    /// Return whether this is a Gateway ID
    #[inline]
    #[must_use]
    pub fn is_gateway_id(self) -> bool {
        self.0[0] == b'*'
    }
}
impl fmt::Display for ThreemaId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}
impl fmt::Debug for ThreemaId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}
impl From<ThreemaId> for String {
    fn from(id: ThreemaId) -> Self {
        id.as_str().to_owned()
    }
}
impl TryFrom<&[u8]> for ThreemaId {
    type Error = ThreemaIdError;

    fn try_from(id: &[u8]) -> Result<Self, Self::Error> {
        let id = <[u8; Self::LENGTH]>::try_from(id).map_err(|_| ThreemaIdError::InvalidLength)?;
        if ![b'*'..=b'*', b'0'..=b'9', b'A'..=b'Z']
            .iter()
            .any(|range| range.contains(id.first().expect("id must be >= 8 bytes")))
        {
            return Err(ThreemaIdError::InvalidSymbols);
        }
        if !id.get(1..8).expect("id must be >= 8 bytes").iter().all(|byte| {
            [b'0'..=b'9', b'A'..=b'Z']
                .iter()
                .any(|range| range.contains(byte))
        }) {
            return Err(ThreemaIdError::InvalidSymbols);
        }
        Ok(ThreemaId(id))
    }
}
impl TryFrom<&str> for ThreemaId {
    type Error = ThreemaIdError;

    fn try_from(id: &str) -> Result<Self, Self::Error> {
        Self::try_from(id.as_bytes())
    }
}
impl FromStr for ThreemaId {
    type Err = ThreemaIdError;

    fn from_str(id: &str) -> Result<Self, Self::Err> {
        Self::try_from(id)
    }
}

#[cfg(test)]
mod threema_id_tests {
    use assert_matches::assert_matches;

    use super::{ThreemaId, ThreemaIdError};

    #[test]
    fn valid() {
        assert!(ThreemaId::try_from("ECHOECHO").is_ok());
        assert!(ThreemaId::try_from([0x45, 0x43, 0x48, 0x4f, 0x45, 0x43, 0x48, 0x4f].as_slice()).is_ok());
        assert!(ThreemaId::try_from("*RICHTIG").is_ok());
        assert!(ThreemaId::try_from([0x2a, 0x52, 0x49, 0x43, 0x48, 0x54, 0x49, 0x47].as_slice()).is_ok());
    }

    #[test]
    fn invalid() {
        assert_matches!(ThreemaId::try_from(""), Err(ThreemaIdError::InvalidLength));
        assert_matches!(ThreemaId::try_from("ZUWENIG"), Err(ThreemaIdError::InvalidLength));
        assert_matches!(
            ThreemaId::try_from("*NEINNEIN"),
            Err(ThreemaIdError::InvalidLength)
        );
        assert_matches!(
            ThreemaId::try_from("ECHÜECHÜ"),
            Err(ThreemaIdError::InvalidLength)
        );
        assert_matches!(
            ThreemaId::try_from([0x00, 0x9f, 0x92, 0x96, 0x00, 0x00, 0x00, 0x00].as_slice()),
            Err(ThreemaIdError::InvalidSymbols)
        );
        assert_matches!(
            ThreemaId::try_from([0_u8; 8].as_slice()),
            Err(ThreemaIdError::InvalidSymbols)
        );
        assert_matches!(
            ThreemaId::try_from("ECH_ECH_"),
            Err(ThreemaIdError::InvalidSymbols)
        );
        assert_matches!(
            ThreemaId::try_from("********"),
            Err(ThreemaIdError::InvalidSymbols)
        );
    }
}

/// A CSP server group.
#[derive(Clone, Copy, Eq, Hash, PartialEq, Deserialize, Name)]
#[serde(try_from = "&str")]
pub struct ChatServerGroup(pub u8);
impl TryFrom<&str> for ChatServerGroup {
    type Error = ParseIntError;

    fn try_from(string: &str) -> Result<Self, Self::Error> {
        Ok(Self(u8::from_str_radix(string, 16)?))
    }
}
impl FromStr for ChatServerGroup {
    type Err = ParseIntError;

    fn from_str(id: &str) -> Result<Self, Self::Err> {
        Self::try_from(id)
    }
}
impl fmt::Display for ChatServerGroup {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{:02x}", self.0)
    }
}
impl fmt::Debug for ChatServerGroup {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}

/// A CSP device cookie.
#[derive(Clone, Copy, Eq, Hash, PartialEq, Name)]
pub struct DeviceCookie(pub [u8; Self::LENGTH]);
impl DeviceCookie {
    /// Byte length of a CSP device cookie.
    pub(crate) const LENGTH: usize = 16;

    #[cfg(any(test, feature = "cli"))]
    pub(crate) fn from_hex(string: &str) -> anyhow::Result<Self> {
        let bytes = HEXLOWER.decode(string.as_bytes())?;
        let bytes: [u8; Self::LENGTH] = bytes.as_slice().try_into()?;
        Ok(Self(bytes))
    }
}
#[cfg(any(test, feature = "cli"))]
impl FromStr for DeviceCookie {
    type Err = anyhow::Error;

    fn from_str(device_cookie: &str) -> Result<Self, Self::Err> {
        Self::from_hex(device_cookie)
    }
}
impl fmt::Display for DeviceCookie {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&HEXLOWER.encode(&self.0))
    }
}
impl fmt::Debug for DeviceCookie {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}

/// A D2X device ID.
#[derive(Clone, Copy, Eq, Hash, PartialEq, Name)]
pub struct D2xDeviceId(pub u64);
#[cfg(any(test, feature = "cli"))]
impl TryFrom<&str> for D2xDeviceId {
    type Error = ParseIntError;

    fn try_from(id: &str) -> Result<Self, Self::Error> {
        Ok(Self(u64::from_str_radix(id, 16)?.to_be()))
    }
}
#[cfg(any(test, feature = "cli"))]
impl FromStr for D2xDeviceId {
    type Err = ParseIntError;

    fn from_str(id: &str) -> Result<Self, Self::Err> {
        Self::try_from(id)
    }
}
impl fmt::Display for D2xDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{:02x}", self.0)
    }
}
impl fmt::Debug for D2xDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}({:016x})", Self::NAME, self.0.to_be())
    }
}

/// A CSP device ID.
///
/// WARNING: This should never be equal to the [`D2xDeviceId`].
#[derive(Clone, Copy, Eq, Hash, PartialEq, Name)]
pub struct CspDeviceId(pub u64);
impl CspDeviceId {
    /// Byte length of a CSP device ID.
    pub(crate) const LENGTH: usize = 8;
}
#[cfg(any(test, feature = "cli"))]
impl TryFrom<&str> for CspDeviceId {
    type Error = ParseIntError;

    fn try_from(id: &str) -> Result<Self, Self::Error> {
        Ok(Self(u64::from_str_radix(id, 16)?.to_be()))
    }
}
#[cfg(any(test, feature = "cli"))]
impl FromStr for CspDeviceId {
    type Err = ParseIntError;

    fn from_str(id: &str) -> Result<Self, Self::Err> {
        Self::try_from(id)
    }
}
impl fmt::Display for CspDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{:02x}", self.0)
    }
}
impl fmt::Debug for CspDeviceId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}({:016x})", Self::NAME, self.0.to_be())
    }
}

/// A unique group identity.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct GroupIdentity {
    /// Group ID as chosen by the group's creator
    pub group_id: u64,
    /// Threema ID of the group's creator
    pub creator_identity: ThreemaId,
}
impl TryFrom<&protobuf::common::GroupIdentity> for GroupIdentity {
    type Error = ThreemaIdError;

    fn try_from(group_identity: &protobuf::common::GroupIdentity) -> Result<Self, Self::Error> {
        Ok(GroupIdentity {
            group_id: group_identity.group_id,
            creator_identity: ThreemaId::try_from(group_identity.creator_identity.as_str())?,
        })
    }
}
impl From<&GroupIdentity> for protobuf::common::GroupIdentity {
    fn from(group_identity: &GroupIdentity) -> Self {
        protobuf::common::GroupIdentity {
            group_id: group_identity.group_id,
            creator_identity: group_identity.creator_identity.into(),
        }
    }
}

/// A specific conversation (aka _receiver_).
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum ConversationId {
    /// A specific 1:1 contact.
    Contact(ThreemaId),
    /// A specific distribution list.
    DistributionList(u64),
    /// A specific group.
    Group(GroupIdentity),
}
impl TryFrom<&protobuf::d2d::conversation_id::Id> for ConversationId {
    type Error = ThreemaIdError;

    fn try_from(conversation: &protobuf::d2d::conversation_id::Id) -> Result<Self, Self::Error> {
        Ok(match conversation {
            protobuf::d2d::conversation_id::Id::Contact(contact_identity) => {
                Self::Contact(ThreemaId::try_from(contact_identity.as_str())?)
            },
            protobuf::d2d::conversation_id::Id::DistributionList(distribution_list_id) => {
                Self::DistributionList(*distribution_list_id)
            },
            protobuf::d2d::conversation_id::Id::Group(group_identity) => {
                Self::Group(GroupIdentity::try_from(group_identity)?)
            },
        })
    }
}
impl From<&ConversationId> for protobuf::d2d::ConversationId {
    fn from(conversation: &ConversationId) -> Self {
        let id = match conversation {
            ConversationId::Contact(contact_identity) => {
                protobuf::d2d::conversation_id::Id::Contact(contact_identity.as_str().to_owned())
            },
            ConversationId::DistributionList(distribution_list_id) => {
                protobuf::d2d::conversation_id::Id::DistributionList(*distribution_list_id)
            },
            ConversationId::Group(group_identity) => {
                protobuf::d2d::conversation_id::Id::Group(group_identity.into())
            },
        };
        protobuf::d2d::ConversationId { id: Some(id) }
    }
}

/// CSP features supported by a device or available for a contact.
///
/// IMPORTANT: The flags determine what a device/contact is capable of, not
/// whether the settings allow for it. For example, group calls may be supported
/// but ignored if disabled in the settings.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FeatureMask(pub u64);
#[rustfmt::skip]
impl FeatureMask {
    /// No features available
    pub const NONE: u64 =                     0b_0000_0000_0000_0000;
    /// Supports voice messages.
    pub const VOICE_MESSAGE_SUPPORT: u64 =    0b_0000_0000_0000_0001;
    /// Supports groups.
    pub const GROUP_SUPPORT: u64 =            0b_0000_0000_0000_0010;
    /// Supports polls.
    pub const POLL_SUPPORT: u64 =             0b_0000_0000_0000_0100;
    /// Supports file messages.
    pub const FILE_MESSAGE_SUPPORT: u64 =     0b_0000_0000_0000_1000;
    /// Supports 1:1 audio calls.
    pub const O2O_AUDIO_CALL_SUPPORT: u64 =   0b_0000_0000_0001_0000;
    /// Supports 1:1 video calls.
    pub const O2O_VIDEO_CALL_SUPPORT: u64 =   0b_0000_0000_0010_0000;
    /// Supports forward security.
    pub const FORWARD_SECURITY_SUPPORT: u64 = 0b_0000_0000_0100_0000;
    /// Supports group calls.
    pub const GROUP_CALL_SUPPORT: u64 =       0b_0000_0000_1000_0000;
    /// Supports editing messages.
    pub const EDIT_MESSAGE_SUPPORT: u64 =     0b_0000_0001_0000_0000;
    /// Supports deleting messages.
    pub const DELETE_MESSAGE_SUPPORT: u64 =   0b_0000_0010_0000_0000;
}

/// A 24-byte nonce for use with XSalsa20Poly1305 or XChaCha20Poly1305.
#[derive(Clone, Eq, PartialEq, Hash, Name)]
pub struct Nonce(pub [u8; Self::LENGTH]);
impl Nonce {
    /// Byte length of a nonce.
    pub const LENGTH: usize = 24;

    /// Generate a random nonce
    #[must_use]
    pub fn random() -> Self {
        let mut nonce = Self([0_u8; Self::LENGTH]);
        rand::thread_rng().fill(&mut nonce.0);
        nonce
    }

    #[cfg(test)]
    pub(crate) fn from_hex(string: &str) -> anyhow::Result<Self> {
        let bytes = HEXLOWER.decode(string.as_bytes())?;
        let bytes: [u8; Self::LENGTH] = bytes.as_slice().try_into()?;
        Ok(Self(bytes))
    }
}
impl From<[u8; Self::LENGTH]> for Nonce {
    fn from(nonce: [u8; Self::LENGTH]) -> Self {
        Self(nonce)
    }
}
impl<'array, 'nonce: 'array> From<&'nonce Nonce> for &'array GenericArray<u8, U24> {
    fn from(nonce: &'nonce Nonce) -> Self {
        Self::from(&nonce.0)
    }
}
impl From<GenericArray<u8, U24>> for Nonce {
    fn from(nonce: GenericArray<u8, U24>) -> Self {
        Self(nonce.into())
    }
}
impl TryFrom<&[u8]> for Nonce {
    type Error = TryFromSliceError;

    fn try_from(nonce: &[u8]) -> Result<Self, Self::Error> {
        Ok(Nonce::from(<[u8; Self::LENGTH]>::try_from(nonce)?))
    }
}
impl fmt::Display for Nonce {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&HEXLOWER.encode(&self.0))
    }
}
impl fmt::Debug for Nonce {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}

/// A message ID.
///
/// May or may not be unique, depending on the context it is used for.
#[derive(Clone, Copy, Eq, Hash, PartialEq, Name)]
pub struct MessageId(pub u64);
impl MessageId {
    /// Byte length of a message ID.
    pub const LENGTH: usize = 8;

    /// Generate a random message ID.
    #[must_use]
    pub fn random() -> Self {
        Self(rand::thread_rng().r#gen())
    }

    #[cfg(test)]
    pub(crate) fn from_hex(string: &str) -> Result<Self, ParseIntError> {
        Ok(Self(u64::from_str_radix(string, 16)?.to_be()))
    }

    /// Byte representation of the Message ID.
    #[inline]
    #[must_use]
    pub fn to_bytes(self) -> [u8; Self::LENGTH] {
        u64::to_le_bytes(self.0)
    }
}
impl fmt::Display for MessageId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{:016x}", self.0.to_be())
    }
}
impl fmt::Debug for MessageId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_tuple(Self::NAME)
            .field(&self.to_string())
            .finish()
    }
}
impl From<[u8; Self::LENGTH]> for MessageId {
    fn from(message_id: [u8; Self::LENGTH]) -> Self {
        Self(u64::from_le_bytes(message_id))
    }
}

/// Message flags which were/are transmitted to the server.
#[derive(Clone, Copy, Default, Name, PartialEq, Eq)]
pub struct MessageFlags(pub u8);
#[rustfmt::skip]
impl MessageFlags {
    pub(crate) const LENGTH: usize = 1;

    /// Whether a push should be sent by the chat server. Only meaningful for an outgoing message.
    pub const SEND_PUSH_NOTIFICATION:u8 =          0b_0000_0001;

    /// Whether the chat server should discard the message in case the receiver is not currently connected to
    /// the chat server. Only meaningful for an outgoing message.
    pub const NO_SERVER_QUEUING: u8 =              0b_0000_0010;

    /// Whether the message should not be acknowledged by the chat server (outgoing) or by the client
    /// (incoming).
    pub const NO_SERVER_ACKNOWLEDGEMENT: u8 =      0b_0000_0100;

    // Reserved:                                   0b_0000_1000
    // Reserved (formerly _group message marker_): 0b_0001_0000

    /// Instructs the chat server to only queue the message for a short period of time (currently 60 seconds).
    /// Only meaningful for an outgoing message.
    pub const SHORT_LIVED_SERVER_QUEUING: u8 =     0b_0010_0000;

    // Reserved:                                   0b_0100_0000

    /// If present, overrides behaviour of messages that would normally trigger a delivery receipt of type
    /// _received_ or _read_.
    pub const NO_DELIVERY_RECEIPTS: u8 =           0b_1000_0000;
}
impl fmt::Debug for MessageFlags {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let check_flag = |flag: u8, name: &'static str| -> Option<&'static str> {
            if self.0 & flag != 0 { Some(name) } else { None }
        };

        // Keep this format in sync with [`MessageOverrides`]!
        write!(
            formatter,
            "{}({})",
            Self::NAME,
            itertools::join(
                [
                    check_flag(Self::SEND_PUSH_NOTIFICATION, "push"),
                    check_flag(Self::NO_SERVER_QUEUING, "no-queue"),
                    check_flag(Self::NO_SERVER_ACKNOWLEDGEMENT, "no-ack"),
                    check_flag(Self::SHORT_LIVED_SERVER_QUEUING, "short-lived"),
                    check_flag(Self::NO_DELIVERY_RECEIPTS, "no-receipts"),
                ]
                .into_iter()
                .flatten(),
                ", ",
            ),
        )
    }
}

/// Metadata associated to a message.
#[derive(Debug, Clone)]
pub struct MessageMetadata {
    /// Unique message ID. Must match the message ID of the outer struct.
    pub message_id: MessageId,
    /// Unix-ish timestamp in milliseconds for when the message has been created.
    pub created_at: u64,
    /// Nickname of the sender at the time the message had been created.
    pub nickname: Delta<String>,
}
impl MessageMetadata {
    /// Create for a new outgoing message.
    #[must_use]
    pub fn new_outgoing(nickname: Delta<String>, message_id: MessageId) -> Self {
        Self {
            message_id,
            created_at: utc_now_ms(),
            nickname,
        }
    }
}
impl From<protobuf::csp_e2e::MessageMetadata> for MessageMetadata {
    fn from(metadata: protobuf::csp_e2e::MessageMetadata) -> Self {
        MessageMetadata {
            message_id: MessageId(metadata.message_id),
            created_at: metadata.created_at,
            nickname: Delta::from_non_empty(metadata.nickname.map(|nickname| nickname.trim().to_owned())),
        }
    }
}
impl From<MessageMetadata> for Vec<u8> {
    fn from(metadata: MessageMetadata) -> Self {
        let metadata = protobuf::csp_e2e::MessageMetadata {
            #[expect(deprecated, reason = "Will be filled by encode_to_vec_padded")]
            padding: vec![],
            message_id: metadata.message_id.0,
            created_at: metadata.created_at,
            nickname: metadata.nickname.into_non_empty(),
        };
        metadata.encode_to_vec_padded()
    }
}

/// A blob ID.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct BlobId(pub [u8; Self::LENGTH]);
impl BlobId {
    /// Byte length of a blob ID.
    pub const LENGTH: usize = 16;
}

/// A delta update, where some value can be unchanged, updated or removed.
#[derive(Debug, Clone, PartialEq)]
pub enum Delta<T> {
    /// Value remains unchanged.
    Unchanged,
    /// Value updated to inner `T`.
    Update(T),
    /// Value removed.
    Remove,
}
impl<T> Delta<T> {
    /// Maps a `Delta<T>` to `Delta<U>` by applying a function to a contained value (if `Update`) or returns
    /// `Unchanged` or `Remove` respectively.
    pub(crate) fn map<U, F: FnOnce(T) -> U>(self, transform_fn: F) -> Delta<U> {
        match self {
            Delta::Unchanged => Delta::Unchanged,
            Delta::Update(value) => Delta::Update(transform_fn(value)),
            Delta::Remove => Delta::Remove,
        }
    }

    /// Converts from [`&Delta<T>`](`Delta<T>`) to [`Delta<&T>`].
    #[inline]
    pub(crate) const fn as_ref(&self) -> Delta<&T> {
        match &self {
            Self::Unchanged => Delta::Unchanged,
            Self::Update(value) => Delta::Update(value),
            Self::Remove => Delta::Remove,
        }
    }

    /// Converts from [`Delta<T>`] (or [`&Delta<T>`](`Delta<T>`)) to `Delta<&T::Target>`.
    #[expect(dead_code, reason = "May use later")]
    #[inline]
    pub(crate) fn as_deref(&self) -> Delta<&T::Target>
    where
        T: Deref,
    {
        self.as_ref().map(Deref::deref)
    }
}
impl<T> Apply<Delta<T>> for Option<T> {
    /// Apply the delta update to self.
    ///
    /// - [`Delta::Unchanged`] does nothing,
    /// - [`Delta::Update`] replaces any value in self with `Some(T)`,
    /// - [`Delta::Remove`] replaces any value in self with `None`.
    #[inline]
    fn apply(&mut self, value: Delta<T>) {
        match value {
            Delta::Unchanged => {},
            Delta::Update(value) => {
                let _ = self.insert(value);
            },
            Delta::Remove => {
                let _ = self.take();
            },
        }
    }
}
impl<T: Eq> Delta<T> {
    /// Re-evaluate the delta update against a `current` value, ensuring that it reflects actual
    /// changes and otherwise reports [`Delta::Unchanged`] if there is no change.
    pub(crate) fn changes(self, current: Option<&T>) -> Delta<T> {
        match (self, current) {
            (Self::Update(update), None) => Delta::Update(update),
            (Self::Update(update), Some(current)) => {
                if current == &update {
                    Delta::Unchanged
                } else {
                    Delta::Update(update)
                }
            },
            (Self::Remove, None) | (Self::Unchanged, _) => Delta::Unchanged,
            (Self::Remove, Some(_)) => Delta::Remove,
        }
    }
}
impl<T: Default + Eq> Delta<T> {
    /// Creates a [`Delta<T>`] from a source where the `T::Default` is semantically equivalent to
    /// [`Delta::Remove`].
    pub(crate) fn from_non_empty(update: Option<T>) -> Self {
        match update {
            Some(update) => {
                if update == T::default() {
                    Self::Remove
                } else {
                    Self::Update(update)
                }
            },
            None => Self::Unchanged,
        }
    }

    /// Creates a [`Option<T>`] from the delta update where [`Delta::Remove`] is converted into `T::Default`.
    ///
    /// WARNING: A [`Delta::Update`] should never contain a `T::Default`, since the resulting [`Option<T>`] is
    /// recognized as a [`Delta::Remove`] when converted back via [`Delta<T>::from_non_empty`].
    pub(crate) fn into_non_empty(self) -> Option<T> {
        match self {
            Self::Unchanged => None,
            Self::Update(value) => {
                if value == T::default() {
                    warn!("Delta::Update contained T::Default");
                }
                Some(value)
            },
            Self::Remove => Some(T::default()),
        }
    }
}

[Dauer der Verarbeitung: 0.41 Sekunden, vorverarbeitet 2026-04-27]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge