Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/libthreema/lib/src/csp_e2e/message/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 35 kB image not shown  

Quelle  mod.rs   Sprache: Rust

 

//! Message-related structures and tasks.
use core::fmt;

use duplicate::duplicate_item;
use educe::Educe;
use libthreema_macros::Name;
use tracing::{debug, warn};

use crate::{
    common::{
        ConversationId, DistributionListId, GroupIdentity, MessageFlags, MessageId, MessageOverrides,
        MessageOverridesDeliveryReceiptPolicy, ThreemaId,
    },
    model::message::{
        ContactDeliveryUpdate, IncomingMessageDirection, LocationMessageState, MessageDirection,
        MessageReactionUpdate, MessageReactionUpdateIntent, MessageStarMarker, MessageState,
        MessageStateBody, MessageStateBodyUpdate, MessageStateMetadata, MessageStateUpdate, MessageText,
        TextMessageState,
    },
    protobuf::common::CspE2eMessageType,
    utils::{
        debug::{Name as _, debug_slice_length},
        time::utc_now_ms,
        update::Apply,
    },
};

pub mod codec;
pub(super) mod payload;
pub mod task;

/// Lifetime of the message on the server.
#[derive(Clone)]
pub(super) enum MessageLifetime {
    /// The message is kept indefinitely until received.
    Indefinite,

    /// The message is kept for a short amount of time until it is silently dropped (usually 30s).
    #[expect(unused, reason = "Not used yet")]
    Brief,

    /// The message is only transmitted if the receiver is currently online.
    ///
    /// This is a combination of the _no server queuing_ and _no server acknowledgement_ flags
    /// which are only used in conjunction.
    Ephemeral,
}

/// Message properties associated to a message type.
#[expect(
    clippy::struct_excessive_bools,
    reason = "The one place where it's reasonable"
)]
#[derive(Clone)]
pub(super) struct MessageProperties {
    /// Whether the message should be/has been pushed.
    pub(super) push_notification: bool,

    /// Lifetime of the message on the server.
    pub(super) lifetime: MessageLifetime,

    /// Whether the message requires user profile distribution.
    pub(super) user_profile_distribution: bool,

    /// Whether the message requires to create a _direct_ contact upon reception.
    pub(super) requires_direct_contact: bool,

    /// Whether the message requires replay protection (by storing the nonce).
    pub(super) replay_protection: bool,

    /// Whether the message should be reflected in case it is incoming.
    pub(super) reflect_incoming: bool,

    /// Whether the message should be reflected in case it is outgoing.
    pub(super) reflect_outgoing: bool,

    /// Whether an update that the message has been _sent_ should be reflected (in case it is
    /// outgoing).
    pub(super) reflect_sent_update: bool,

    /// Whether delivery receipts may be sent in response to this message.
    pub(super) delivery_receipts: bool,
}
impl Apply<MessageProperties> for MessageFlags {
    fn apply(&mut self, properties: MessageProperties) {
        let MessageProperties {
            push_notification,
            lifetime,
            user_profile_distribution: _user_profile_distribution,
            requires_direct_contact: _requires_direct_contact,
            reflect_incoming: _reflect_incoming,
            replay_protection: _replay_protection,
            reflect_outgoing: _reflect_outgoing,
            reflect_sent_update: _reflect_sent_update,
            delivery_receipts: _delivery_receipts,
        } = properties;

        if push_notification {
            self.0 |= Self::SEND_PUSH_NOTIFICATION;
        }
        match lifetime {
            MessageLifetime::Indefinite => {},
            MessageLifetime::Brief => {
                self.0 |= Self::SHORT_LIVED_SERVER_QUEUING;
            },
            MessageLifetime::Ephemeral => {
                self.0 |= Self::NO_SERVER_QUEUING | Self::NO_SERVER_ACKNOWLEDGEMENT;
            },
        }
    }
}
impl Apply<MessageOverrides> for MessageProperties {
    fn apply(&mut self, overrides: MessageOverrides) {
        if matches!(
            overrides.delivery_receipt_policy,
            MessageOverridesDeliveryReceiptPolicy::DontSend
        ) {
            self.delivery_receipts = false;
        }
    }
}

/// Message properties associated to a group message type.
///
/// This is a reduced version of [`MessageProperties`].
#[expect(
    clippy::struct_excessive_bools,
    reason = "The one place where it's reasonable"
)]
struct GroupMessageProperties {
    push_notification: bool,
    lifetime: MessageLifetime,
    user_profile_distribution: bool,
    replay_protection: bool,
    reflect_incoming: bool,
    reflect_outgoing: bool,
    reflect_sent_update: bool,
}
impl GroupMessageProperties {
    const fn into_message_properties(self) -> MessageProperties {
        MessageProperties {
            push_notification: self.push_notification,
            lifetime: self.lifetime,
            user_profile_distribution: self.user_profile_distribution,
            requires_direct_contact: false,
            replay_protection: self.replay_protection,
            reflect_incoming: self.reflect_incoming,
            reflect_outgoing: self.reflect_outgoing,
            reflect_sent_update: self.reflect_sent_update,
            delivery_receipts: false,
        }
    }
}

#[derive(Clone, Educe)]
#[educe(Debug)]
pub(super) struct TextMessage {
    /// Text of the message.
    #[educe(Debug(method(debug_slice_length)))]
    pub(super) text: String,
}
impl TextMessage {
    const CONTACT_EXEMPT_FROM_BLOCKING: bool = false;
    const CONTACT_PROPERTIES: MessageProperties = MessageProperties {
        push_notification: true,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: true,
        requires_direct_contact: true,
        replay_protection: true,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: true,
        delivery_receipts: true,
    };
    const CONTACT_TYPE: CspE2eMessageType = CspE2eMessageType::Text;
    const GROUP_EXEMPT_FROM_BLOCKING: bool = false;
    const GROUP_PROPERTIES: GroupMessageProperties = GroupMessageProperties {
        push_notification: true,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: true,
        replay_protection: true,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: true,
    };
    const GROUP_TYPE: CspE2eMessageType = CspE2eMessageType::GroupText;
}
impl From<&TextMessage> for MessageStateBody {
    fn from(message: &TextMessage) -> Self {
        Self::Text(TextMessageState {
            original: MessageText {
                text: message.text.clone(),
            },
            revisions: vec![],
        })
    }
}

#[derive(Clone, Name)]
pub(super) struct LocationMessage {
    /// Latitude.
    pub(super) latitude: f64,

    /// Longitude.
    pub(super) longitude: f64,

    /// Accuracy of the original sender's position in meters.
    pub(super) accuracy_m: Option<f64>,

    /// Closest name of the location or a point of interest.
    ///
    /// Empty string means that no name was provided.
    pub(super) name: String,

    /// An arbitrary address, not following any standardized pattern. May contain line breaks.
    ///
    /// Empty string means that no address was provided.
    pub(super) address: String,
}
impl LocationMessage {
    const CONTACT_EXEMPT_FROM_BLOCKING: bool = false;
    const CONTACT_PROPERTIES: MessageProperties = MessageProperties {
        push_notification: true,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: true,
        requires_direct_contact: true,
        replay_protection: true,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: true,
        delivery_receipts: true,
    };
    const CONTACT_TYPE: CspE2eMessageType = CspE2eMessageType::Location;
    const GROUP_EXEMPT_FROM_BLOCKING: bool = false;
    const GROUP_PROPERTIES: GroupMessageProperties = GroupMessageProperties {
        push_notification: true,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: true,
        replay_protection: true,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: true,
    };
    const GROUP_TYPE: CspE2eMessageType = CspE2eMessageType::Location;
}
impl fmt::Debug for LocationMessage {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(Self::NAME)
    }
}
impl From<&LocationMessage> for MessageStateBody {
    fn from(message: &LocationMessage) -> Self {
        Self::Location(LocationMessageState {
            latitude: message.latitude,
            longitude: message.longitude,
            accuracy_m: message.accuracy_m,
            address: message.address.clone(),
            name: message.name.clone(),
        })
    }
}

/// The type of delivery receipt.
#[derive(Debug, Clone, Copy, PartialEq, strum::FromRepr)]
#[repr(u8)]
pub(super) enum DeliveryReceiptType {
    /// The referred messages have been received (i.e. it was delivered to the recipient).
    Received = 0x01,

    /// The referred messages have been read.
    Read = 0x02,
}

/// The type of legacy reaction.
#[derive(Debug, Clone, Copy, strum::FromRepr)]
#[repr(u8)]
pub(crate) enum LegacyReactionType {
    /// The user reacted with _acknowledge_ (i.e. _thumbs up_) to the messages.
    Acknowledge = 0x03,

    /// The user reacted with _decline_ (i.e. _thumbs down_) to the messages.
    Decline = 0x04,
}

/// A delivery receipt.
///
/// Note: This only covers real delivery receipts, not legacy reactions. These are always mapped to and from
/// [`LegacyReactionMessage`].
#[derive(Clone)]
pub(super) struct DeliveryReceiptMessage {
    /// Type of delivery receipt.
    pub(super) receipt_type: DeliveryReceiptType,

    /// Message ids of referred messages the receipt type should be applied to.
    pub(super) message_ids: Vec<MessageId>,
}
impl DeliveryReceiptMessage {
    const CONTACT_EXEMPT_FROM_BLOCKING: bool = false;
    const CONTACT_PROPERTIES: MessageProperties = MessageProperties {
        push_notification: false,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: false,
        requires_direct_contact: false,
        replay_protection: false,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: false,
        delivery_receipts: false,
    };
    const CONTACT_TYPE: CspE2eMessageType = CspE2eMessageType::DeliveryReceipt;

    pub(super) fn to_incoming_state_updates(
        &self,
        user_identity: ThreemaId,
        sender_identity: ThreemaId,
        created_at: u64,
        conversation_id: ConversationId,
        referred_messages_metadata: &[MessageStateMetadata],
    ) -> Vec<MessageStateUpdate> {
        let update = MessageStateBodyUpdate::ContactDeliveryReceipt(match &self.receipt_type {
            DeliveryReceiptType::Received => ContactDeliveryUpdate::Delivered(created_at),
            DeliveryReceiptType::Read => ContactDeliveryUpdate::Read(created_at),
        });

        // Create a state update for each referred message, if found and applicable.
        self.message_ids
            .iter()
            .filter_map(|message_id| {
                // Filter unknown.
                let Some(metadata) = referred_messages_metadata
                    .iter()
                    .find(|metadata| metadata.message_id == *message_id)
                else {
                    debug!(
                        referred_message_id = ?message_id,
                        "Discarding incoming delivery receipt referring to an unknown message",
                    );
                    return None;
                };

                // Ensure an outgoing message is referred.
                if metadata.creator_identity != user_identity {
                    warn!(
                        referred_message_id = ?message_id,
                        "Discarding incoming delivery receipt for an incoming message",
                    );
                    return None;
                }

                // Map to state update.
                Some(MessageStateUpdate {
                    conversation_id,
                    creator_identity: sender_identity,
                    message_id: *message_id,
                    update: update.clone(),
                })
            })
            .collect()
    }
}

/// A legacy reaction message.
///
/// Note: Delivery receipts containing legacy reactions are always mapped to and from this message internally.
#[derive(Clone)]
pub(super) struct LegacyReactionMessage {
    /// Type of legacy reaction.
    pub(super) reaction_type: LegacyReactionType,

    /// Message ids of referred messages the reaction should be applied to.
    pub(super) message_ids: Vec<MessageId>,
}
impl LegacyReactionMessage {
    const CONTACT_EXEMPT_FROM_BLOCKING: bool = false;
    const CONTACT_PROPERTIES: MessageProperties = MessageProperties {
        push_notification: false,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: true,
        requires_direct_contact: false,
        replay_protection: true,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: false,
        delivery_receipts: false,
    };
    const CONTACT_TYPE: CspE2eMessageType = CspE2eMessageType::DeliveryReceipt;
    const GROUP_EXEMPT_FROM_BLOCKING: bool = false;
    const GROUP_PROPERTIES: GroupMessageProperties = GroupMessageProperties {
        push_notification: false,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: true,
        replay_protection: true,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: false,
    };
    const GROUP_TYPE: CspE2eMessageType = CspE2eMessageType::GroupDeliveryReceipt;

    pub(super) fn to_incoming_state_updates(
        &self,
        user_identity: ThreemaId,
        sender_identity: ThreemaId,
        created_at: u64,
        conversation_id: ConversationId,
        referred_messages_metadata: &[MessageStateMetadata],
    ) -> Vec<MessageStateUpdate> {
        // Create a state update for each referred message, if found and applicable.
        self.message_ids
            .iter()
            .filter_map(|message_id| {
                // Filter unknown.
                let Some(metadata) = referred_messages_metadata
                    .iter()
                    .find(|metadata| metadata.message_id == *message_id)
                else {
                    debug!(
                        referred_message_id = ?message_id,
                        "Discarding incoming legacy reaction referring to an unknown message",
                    );
                    return None;
                };

                // Ensure an outgoing message is referred.
                if metadata.creator_identity != user_identity {
                    warn!(
                        referred_message_id = ?message_id,
                        "Discarding incoming legacy reaction for an incoming message",
                    );
                    return None;
                }

                // Ensure the message is eligible for reactions.
                if !metadata.message_type.is_reactable() {
                    debug!(
                        referred_message_id = ?message_id,
                        "Discarding incoming legacy reaction for {:?} not eligible for reactions",
                        metadata.message_type
                    );
                    return None;
                }

                // Map to state update.
                Some(MessageStateUpdate {
                    conversation_id,
                    creator_identity: sender_identity,
                    message_id: *message_id,
                    update: MessageStateBodyUpdate::Reaction(MessageReactionUpdate {
                        created_at,
                        intent: MessageReactionUpdateIntent::Replace,
                        emoji: self.reaction_type.into(),
                    }),
                })
            })
            .collect()
    }
}

/// A group setup message.
#[derive(Clone)]
pub(super) struct GroupSetupMessage {
    /// Members of the group.
    #[expect(unused, reason = "Will be used in LIB-143")]
    pub(super) members: Vec<ThreemaId>,
}
impl GroupSetupMessage {
    const GROUP_PROPERTIES: GroupMessageProperties = GroupMessageProperties {
        push_notification: false,
        lifetime: MessageLifetime::Indefinite,
        user_profile_distribution: true,
        replay_protection: true,
        reflect_incoming: true,
        reflect_outgoing: true,
        reflect_sent_update: false,
    };
    const TYPE: CspE2eMessageType = CspE2eMessageType::GroupSetup;
}

/// A control message from Threema Web, requesting a session to be resumed.
#[derive(Clone, Educe)]
#[educe(Debug)]
pub(super) struct WebSessionResumeMessage {
    /// Raw `push-payload` of a `web-session-resume` message.
    #[educe(Debug(method(debug_slice_length)))]
    pub(super) push_payload: Vec<u8>,
}
impl WebSessionResumeMessage {
    const CONTACT_EXEMPT_FROM_BLOCKING: bool = true;
    const CONTACT_PROPERTIES: MessageProperties = MessageProperties {
        push_notification: false,
        lifetime: MessageLifetime::Ephemeral,
        user_profile_distribution: false,
        requires_direct_contact: false,
        replay_protection: true,
        reflect_incoming: false,
        reflect_outgoing: false,
        reflect_sent_update: false,
        delivery_receipts: false,
    };
    const CONTACT_TYPE: CspE2eMessageType = CspE2eMessageType::WebSessionResume;
}

/// The body of a contact message.
#[derive(Clone)]
pub(super) enum ContactMessageBody {
    Text(TextMessage),
    Location(LocationMessage),
    DeliveryReceipt(DeliveryReceiptMessage),
    LegacyReaction(LegacyReactionMessage),
    WebSessionResume(WebSessionResumeMessage),
    // TODO(LIB-16): Complete
}
impl ContactMessageBody {
    /// Get the 1:1 message's type.
    #[inline]
    const fn message_type(&self) -> CspE2eMessageType {
        match &self {
            Self::Text(_) => TextMessage::CONTACT_TYPE,
            Self::Location(_) => LocationMessage::CONTACT_TYPE,
            Self::DeliveryReceipt(_) => DeliveryReceiptMessage::CONTACT_TYPE,
            Self::LegacyReaction(_) => LegacyReactionMessage::CONTACT_TYPE,
            Self::WebSessionResume(_) => WebSessionResumeMessage::CONTACT_TYPE,
        }
    }

    /// Get the 1:1 message's properties.
    #[inline]
    const fn properties(&self) -> MessageProperties {
        match &self {
            Self::Text(_) => TextMessage::CONTACT_PROPERTIES,
            Self::Location(_) => LocationMessage::CONTACT_PROPERTIES,
            Self::DeliveryReceipt(_) => DeliveryReceiptMessage::CONTACT_PROPERTIES,
            Self::LegacyReaction(_) => LegacyReactionMessage::CONTACT_PROPERTIES,
            Self::WebSessionResume(_) => WebSessionResumeMessage::CONTACT_PROPERTIES,
        }
    }

    /// Determine whether the 1:1 message is exempt from blocking.
    #[inline]
    const fn is_exempt_from_blocking(&self) -> bool {
        match &self {
            Self::Text(_) => TextMessage::CONTACT_EXEMPT_FROM_BLOCKING,
            Self::Location(_) => LocationMessage::CONTACT_EXEMPT_FROM_BLOCKING,
            Self::DeliveryReceipt(_) => DeliveryReceiptMessage::CONTACT_EXEMPT_FROM_BLOCKING,
            Self::LegacyReaction(_) => LegacyReactionMessage::CONTACT_EXEMPT_FROM_BLOCKING,
            Self::WebSessionResume(_) => WebSessionResumeMessage::CONTACT_EXEMPT_FROM_BLOCKING,
        }
    }
}

/// The message body of a distribution list message.
#[derive(Clone)]
#[expect(unused, reason = "LIB-57")]
pub(super) enum DistributionListMessageBody {
    Text(TextMessage),
    Location(LocationMessage),
    // TODO(LIB-57): Complete
}
impl DistributionListMessageBody {
    /// Get the distribution list message's type.
    ///
    /// Note: These are always 1:1 message types, so we're reusing them.
    #[inline]
    const fn message_type(&self) -> CspE2eMessageType {
        match &self {
            Self::Text(_) => TextMessage::CONTACT_TYPE,
            Self::Location(_) => LocationMessage::CONTACT_TYPE,
        }
    }

    /// Get the distribution list message's properties.
    ///
    /// Note: The properties are identical to the 1:1 message's properties, so we're reusing them.
    #[inline]
    const fn properties(&self) -> MessageProperties {
        match &self {
            Self::Text(_) => TextMessage::CONTACT_PROPERTIES,
            Self::Location(_) => LocationMessage::CONTACT_PROPERTIES,
        }
    }
}

/// The message body of a group message.
#[derive(Clone)]
pub(super) enum GroupMessageBody {
    Text(TextMessage),
    Location(LocationMessage),
    LegacyReaction(LegacyReactionMessage),
    #[expect(unused, reason = "LIB-143")]
    GroupSetup(GroupSetupMessage),
    // TODO(LIB-53): Complete
}
impl GroupMessageBody {
    /// Get the group message's type.
    #[inline]
    const fn message_type(&self) -> CspE2eMessageType {
        match &self {
            Self::Text(_) => TextMessage::GROUP_TYPE,
            Self::Location(_) => LocationMessage::GROUP_TYPE,
            Self::LegacyReaction(_) => LegacyReactionMessage::GROUP_TYPE,
            Self::GroupSetup(_) => GroupSetupMessage::TYPE,
        }
    }

    /// Get the group message's properties.
    #[inline]
    const fn properties(&self) -> GroupMessageProperties {
        match &self {
            Self::Text(_) => TextMessage::GROUP_PROPERTIES,
            Self::Location(_) => LocationMessage::GROUP_PROPERTIES,
            Self::LegacyReaction(_) => LegacyReactionMessage::GROUP_PROPERTIES,
            Self::GroupSetup(_) => GroupSetupMessage::GROUP_PROPERTIES,
        }
    }

    /// Determine whether the group message is exempt from blocking.
    #[inline]
    const fn is_exempt_from_blocking(&self) -> bool {
        match &self {
            Self::Text(_) => TextMessage::GROUP_EXEMPT_FROM_BLOCKING,
            Self::Location(_) => LocationMessage::GROUP_EXEMPT_FROM_BLOCKING,
            Self::LegacyReaction(_) => LegacyReactionMessage::GROUP_EXEMPT_FROM_BLOCKING,
            // TODO(LIB-143): Handle exemption logic properly.
            Self::GroupSetup(_) => false,
        }
    }
}

/// A distribution list message (always outgoing).
#[expect(unused, reason = "LIB-57")]
#[derive(Clone)]
pub(super) struct DistributionListMessage {
    /// The distribution list ID.
    pub(super) distribution_list_id: DistributionListId,

    /// The message body.
    pub(super) body: DistributionListMessageBody,
}

/// A group message.
#[derive(Clone)]
pub(super) struct GroupMessage {
    /// The identity of the group.
    pub(super) group_identity: GroupIdentity,

    /// The message body.
    pub(super) body: GroupMessageBody,
}

/// The message body of an incoming message.
#[derive(Clone)]
pub(super) enum IncomingMessageBody {
    Contact(ContactMessageBody),
    Group(GroupMessage),
}
impl IncomingMessageBody {
    /// Get the message's type.
    #[inline]
    const fn message_type(&self) -> CspE2eMessageType {
        match &self {
            Self::Contact(body) => body.message_type(),
            Self::Group(body) => body.body.message_type(),
        }
    }

    /// Get the message's properties.
    #[inline]
    const fn properties(&self) -> MessageProperties {
        match &self {
            Self::Contact(body) => body.properties(),
            Self::Group(body) => body.body.properties().into_message_properties(),
        }
    }

    /// Determine whether the message is exempt from blocking.
    #[inline]
    const fn is_exempt_from_blocking(&self) -> bool {
        match &self {
            Self::Contact(body) => body.is_exempt_from_blocking(),
            Self::Group(body) => body.body.is_exempt_from_blocking(),
        }
    }
}

/// An outgoing contact message.
#[expect(unused, reason = "LIB-51")]
#[derive(Clone)]
pub(super) struct OutgoingContactMessage {
    /// The Threema ID of the sender.
    pub(super) receiver_identity: ThreemaId,

    /// The message body.
    pub(super) body: ContactMessageBody,
}

/// The message body of an outgoing message.
#[expect(unused, reason = "LIB-51")]
#[derive(Clone)]
pub(super) enum OutgoingMessageBody {
    Contact(OutgoingContactMessage),
    DistributionList(DistributionListMessage),
    Group(GroupMessage),
}
impl OutgoingMessageBody {
    /// Get the message's type.
    #[inline]
    const fn message_type(&self) -> CspE2eMessageType {
        match &self {
            Self::Contact(body) => body.body.message_type(),
            Self::DistributionList(body) => body.body.message_type(),
            Self::Group(body) => body.body.message_type(),
        }
    }

    /// Get the message's properties.
    #[inline]
    const fn properties(&self) -> MessageProperties {
        match &self {
            Self::Contact(body) => body.body.properties(),
            Self::DistributionList(body) => body.body.properties(),
            Self::Group(body) => body.body.properties().into_message_properties(),
        }
    }
}

/// An incoming message.
#[derive(Clone)]
pub(super) struct IncomingMessage {
    /// Identity of the sender.
    pub(super) sender_identity: ThreemaId,

    /// The ID of the message.
    pub(super) message_id: MessageId,

    /// Behaviour overrides.
    pub(super) overrides: MessageOverrides,

    /// Unix-ish timestamp in milliseconds for when the message has been created.
    pub(super) created_at: u64,

    /// The message body.
    pub(super) body: IncomingMessageBody,
}
impl IncomingMessage {
    /// Get the associated [`ConversationId`].
    #[inline]
    pub(super) fn conversation_id(&self) -> ConversationId {
        match &self.body {
            IncomingMessageBody::Contact(_) => ConversationId::Contact(self.sender_identity),
            IncomingMessageBody::Group(group_message) => ConversationId::Group(group_message.group_identity),
        }
    }

    /// Determine whether the message is exempt from blocking.
    #[inline]
    pub(super) fn is_exempt_from_blocking(&self) -> bool {
        self.body.is_exempt_from_blocking()
    }

    /// Convert to an initial [`MessageState`].
    ///
    /// IMPORTANT: The caller must ensure that the body is the reference to the unpacked body of this message!
    pub(super) fn to_state<TBody>(&self, body: TBody) -> MessageState
    where
        TBody: Into<MessageStateBody>,
    {
        MessageState {
            conversation_id: self.conversation_id(),
            creator_identity: self.sender_identity,
            message_id: self.message_id,
            created_at: self.created_at,
            direction: MessageDirection::Incoming(IncomingMessageDirection {
                received_at: utc_now_ms(),
                overrides: self.overrides,
            }),
            read_at: None,
            star_marker: MessageStarMarker::None,
            reactions: vec![],
            body: body.into(),
        }
    }
}

/// An outgoing message.
#[derive(Clone)]
pub(super) struct OutgoingMessage {
    /// The ID of the message.
    pub(super) message_id: MessageId,

    /// Behaviour overrides.
    pub(super) overrides: MessageOverrides,

    /// Unix-ish timestamp in milliseconds for when the message has been created.
    pub(super) created_at: u64,

    /// The message's body for the respective conversation.
    pub(super) body: OutgoingMessageBody,
}
impl OutgoingMessage {
    /// Computes [`MessageFlags`] from the [`MessageProperties`] and then the [`MessageOverrides`].
    pub(super) fn flags(&self) -> MessageFlags {
        MessageFlags::default()
            .chain_apply(self.properties())
            .chain_apply(self.overrides)
    }
}

#[expect(unused, reason = "LIB-51")]
#[duplicate_item(
    struct_name;
    [ IncomingMessage ];
    [ OutgoingMessage ];
)]
impl struct_name {
    /// Get the message's type.
    #[inline]
    #[must_use]
    pub(super) fn message_type(&self) -> CspE2eMessageType {
        self.body.message_type()
    }

    /// Get the message's properties.
    ///
    /// IMPORTANT: This does **NOT** include the overrides from [`MessageOverrides`]!
    #[inline]
    const fn properties(&self) -> MessageProperties {
        self.body.properties()
    }

    /// Get the effective message's properties, i.e. those with the [`MessageOverrides`] applied.
    #[inline]
    pub(super) fn effective_properties(&self) -> MessageProperties {
        self.body.properties().chain_apply(self.overrides)
    }
}

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

    use super::*;
    use crate::model::message::{EmojiReaction, MessageStateBodyType};

    const ORIGINAL_MESSAGE_ID: MessageId = MessageId(1);
    const SECOND_ORIGINAL_MESSAGE_ID: MessageId = MessageId(2);

    fn user_identity() -> ThreemaId {
        ThreemaId::try_from("0THEUSER").unwrap()
    }

    fn sender_identity() -> ThreemaId {
        ThreemaId::try_from("0PORRITO").unwrap()
    }

    fn conversation_id() -> ConversationId {
        ConversationId::Contact(sender_identity())
    }

    #[rstest]
    fn valid_delivery_receipt(
        #[values(DeliveryReceiptType::Received, DeliveryReceiptType::Read)]
        delivery_receipt_type: DeliveryReceiptType,
    ) {
        let delivery_receipt = DeliveryReceiptMessage {
            receipt_type: delivery_receipt_type,
            message_ids: vec![ORIGINAL_MESSAGE_ID, SECOND_ORIGINAL_MESSAGE_ID],
        };

        let mut updates = delivery_receipt.to_incoming_state_updates(
            user_identity(),
            sender_identity(),
            utc_now_ms(),
            conversation_id(),
            &[
                MessageStateMetadata {
                    conversation_id: conversation_id(),
                    creator_identity: user_identity(),
                    message_id: ORIGINAL_MESSAGE_ID,
                    message_type: MessageStateBodyType::Text,
                },
                MessageStateMetadata {
                    conversation_id: conversation_id(),
                    creator_identity: user_identity(),
                    message_id: SECOND_ORIGINAL_MESSAGE_ID,
                    message_type: MessageStateBodyType::PollOpen,
                },
            ],
        );

        // Check that the two are accepted, correct and in the same order.
        assert_eq!(updates.len(), 2);
        assert_matches!(updates.pop(), Some(update) => {
            assert_eq!(update.conversation_id, conversation_id());
            assert_eq!(update.creator_identity, sender_identity());
            assert_eq!(update.message_id, SECOND_ORIGINAL_MESSAGE_ID);
            assert_matches!(update.update, MessageStateBodyUpdate::ContactDeliveryReceipt(receipt) => {
                assert_eq!(match receipt {
                    ContactDeliveryUpdate::Delivered(_) => DeliveryReceiptType::Received,
                    ContactDeliveryUpdate::Read(_) => DeliveryReceiptType::Read
                }, delivery_receipt_type);
            });
        });
        assert_matches!(updates.pop(), Some(update) => {
            assert_eq!(update.conversation_id, conversation_id());
            assert_eq!(update.creator_identity, sender_identity());
            assert_eq!(update.message_id, ORIGINAL_MESSAGE_ID);
            assert_matches!(update.update, MessageStateBodyUpdate::ContactDeliveryReceipt(receipt) => {
                assert_eq!(match receipt {
                    ContactDeliveryUpdate::Delivered(_) => DeliveryReceiptType::Received,
                    ContactDeliveryUpdate::Read(_) => DeliveryReceiptType::Read
                }, delivery_receipt_type);
            });
        });
    }

    #[rstest]
    #[case::message_id_mismatch(
        MessageStateMetadata {
            conversation_id: conversation_id(),
            creator_identity: sender_identity(),
            message_id: MessageId(42),
            message_type: MessageStateBodyType::Text
        }
    )]
    #[case::incoming_contact_message(
        MessageStateMetadata {
            conversation_id: conversation_id(),
            creator_identity: sender_identity(),
            message_id: ORIGINAL_MESSAGE_ID,
            message_type: MessageStateBodyType::Text
        }
    )]
    fn invalid_delivery_receipt(#[case] invalid_metadata: MessageStateMetadata) {
        let delivery_receipt = DeliveryReceiptMessage {
            receipt_type: DeliveryReceiptType::Read,
            message_ids: vec![ORIGINAL_MESSAGE_ID],
        };

        let updates = delivery_receipt.to_incoming_state_updates(
            user_identity(),
            sender_identity(),
            utc_now_ms(),
            conversation_id(),
            &[invalid_metadata],
        );

        // No updates.
        assert_eq!(updates.len(), 0);
    }

    #[rstest]
    fn valid_legacy_reaction(
        #[values(LegacyReactionType::Acknowledge, LegacyReactionType::Decline)]
        reaction_type: LegacyReactionType,
    ) {
        let legacy_reaction = LegacyReactionMessage {
            reaction_type,
            message_ids: vec![ORIGINAL_MESSAGE_ID, SECOND_ORIGINAL_MESSAGE_ID],
        };

        let mut updates = legacy_reaction.to_incoming_state_updates(
            user_identity(),
            sender_identity(),
            utc_now_ms(),
            conversation_id(),
            &[
                MessageStateMetadata {
                    conversation_id: conversation_id(),
                    creator_identity: user_identity(),
                    message_id: ORIGINAL_MESSAGE_ID,
                    message_type: MessageStateBodyType::Text,
                },
                MessageStateMetadata {
                    conversation_id: conversation_id(),
                    creator_identity: user_identity(),
                    message_id: SECOND_ORIGINAL_MESSAGE_ID,
                    message_type: MessageStateBodyType::Location,
                },
            ],
        );

        // Check that the two are accepted, correct and in the same order.
        assert_eq!(updates.len(), 2);
        assert_matches!(updates.pop(), Some(update) => {
            assert_eq!(update.conversation_id, conversation_id());
            assert_eq!(update.creator_identity, sender_identity());
            assert_eq!(update.message_id, SECOND_ORIGINAL_MESSAGE_ID);
            assert_matches!(update.update, MessageStateBodyUpdate::Reaction(reaction) => {
                assert_matches!(reaction.intent, MessageReactionUpdateIntent::Replace);
                assert_eq!(reaction.emoji.codepoint, EmojiReaction::from(reaction_type).codepoint);
            });
        });
        assert_matches!(updates.pop(), Some(update) => {
            assert_eq!(update.conversation_id, conversation_id());
            assert_eq!(update.creator_identity, sender_identity());
            assert_eq!(update.message_id, ORIGINAL_MESSAGE_ID);
            assert_matches!(update.update, MessageStateBodyUpdate::Reaction(reaction) => {
                assert_matches!(reaction.intent, MessageReactionUpdateIntent::Replace);
                assert_eq!(reaction.emoji.codepoint, EmojiReaction::from(reaction_type).codepoint);
            });
        });
    }

    #[rstest]
    #[case::message_id_mismatch(
        MessageStateMetadata {
            conversation_id: conversation_id(),
            creator_identity: user_identity(),
            message_id: MessageId(42), message_type:
            MessageStateBodyType::Text
        }
    )]
    #[case::incoming_contact_message(
        MessageStateMetadata {
            conversation_id: conversation_id(),
            creator_identity: sender_identity(),
            message_id: ORIGINAL_MESSAGE_ID,
            message_type: MessageStateBodyType::Text
        }
    )]
    #[case::not_reactable(
        MessageStateMetadata {
            conversation_id: conversation_id(),
            creator_identity: user_identity(),
            message_id: ORIGINAL_MESSAGE_ID,
            message_type: MessageStateBodyType::PollOpen
        }
    )]
    fn invalid_legacy_reaction(#[case] invalid_metadata: MessageStateMetadata) {
        let legacy_reaction = LegacyReactionMessage {
            reaction_type: LegacyReactionType::Acknowledge,
            message_ids: vec![ORIGINAL_MESSAGE_ID],
        };

        let updates = legacy_reaction.to_incoming_state_updates(
            user_identity(),
            sender_identity(),
            utc_now_ms(),
            conversation_id(),
            &[invalid_metadata],
        );

        // No updates.
        assert_eq!(updates.len(), 0);
    }
}

Messung
CHZG
869810094

¤ Dauer der Verarbeitung: 0.11 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.