Skip to main content

core_crypto/transaction_context/conversation/
mod.rs

1//! This module contains all [super::TransactionContext] methods related to a conversation.
2
3pub mod external_commit;
4mod persistence;
5pub mod welcome;
6
7use std::sync::Arc;
8
9use core_crypto_keystore::{entities::PersistedMlsGroup, traits::FetchFromDatabase as _};
10use openmls::group::MlsGroup;
11
12use super::{Error, Result, TransactionContext};
13use crate::{
14    ConversationConfiguration, CredentialRef, KeystoreError, OpenMlsError, RecursiveError,
15    mls::conversation::{ConversationIdRef, ConversationMut, PendingConversation},
16};
17
18impl TransactionContext {
19    /// Checks if a given conversation id exists locally.
20    ///
21    /// Somewhat cheaper than `self.conversation(id).is_ok()`.
22    pub async fn conversation_exists(&self, id: &ConversationIdRef) -> Result<bool> {
23        let database = self.database()?.into();
24        self.mls_groups()
25            .await?
26            .exists(id, &database)
27            .await
28            .map_err(RecursiveError::context("checking for conversation existence"))
29            .map_err(Into::into)
30    }
31
32    /// Acquire a conversation guard.
33    ///
34    /// This helper struct permits mutations on a conversation.
35    pub async fn conversation(&self, id: &ConversationIdRef) -> Result<ConversationMut> {
36        let inner = self.inner()?;
37        let session = self.session().await?;
38        let conversation = self
39            .mls_groups()
40            .await?
41            .get_or_fetch(id, &inner.transaction, session)
42            .await
43            .map_err(RecursiveError::context("fetching conversation from mls groups by id"))?;
44
45        if let Some(conversation) = conversation {
46            return Ok(ConversationMut::new(conversation, self.clone()));
47        }
48        // Check if there is a pending conversation with
49        // the same id
50        let pending = self.pending_conversation(id).await.map(Error::PendingConversation)?;
51        Err(pending)
52    }
53
54    pub(crate) async fn pending_conversation(&self, id: &ConversationIdRef) -> Result<PendingConversation> {
55        let inner = self.inner()?;
56        let group = inner
57            .transaction
58            .get_borrowed::<PersistedMlsGroup>(id.keystore())
59            .await
60            .map_err(KeystoreError::wrap("finding persisted mls group"))?
61            .filter(|group| group.is_pending);
62        let Some(group) = group else {
63            return Err(Error::ConversationNotFound(id.to_owned()));
64        };
65        let group = Arc::unwrap_or_clone(group);
66        Ok(PendingConversation::new(group, self.clone()))
67    }
68
69    /// Create a new empty conversation
70    ///
71    /// # Arguments
72    /// * `id` - identifier of the group/conversation (must be unique otherwise the existing group will be overridden)
73    /// * `creator_credential_type` - kind of credential the creator wants to create the group with
74    /// * `config` - configuration of the group/conversation
75    ///
76    /// # Errors
77    /// Errors can happen from the KeyStore or from OpenMls for ex if no [openmls::key_packages::KeyPackage] can
78    /// be found in the KeyStore
79    #[cfg_attr(test, crate::dispotent)]
80    pub async fn new_conversation(
81        &self,
82        id: &ConversationIdRef,
83        credential_ref: &CredentialRef,
84        configuration: ConversationConfiguration,
85    ) -> Result<()> {
86        let database = self.database()?;
87        let provider = self.crypto_provider().await?;
88        if self.conversation_exists(id).await? || self.pending_conversation_exists(id).await? {
89            return Err(Error::ConversationAlreadyExists(id.to_owned()));
90        }
91
92        let credential = credential_ref.load(&*database).await.map_err(RecursiveError::context(
93            "loading credential from database to create new conversation",
94        ))?;
95
96        let config = configuration
97            .as_openmls_default_configuration()
98            .map_err(RecursiveError::context("converting config to openmls default"))?;
99
100        // there's a deferred constraint on epoch encryption keypairs.
101        // openmls creates the keypairs before it hands us the `group` which we can use to
102        // persist the conversation, so the constraint _can't_ be fulfilled immediately;
103        // it gets checked at the outermost commit.
104        // wrapping these two operations in an explicit savepoint means that these two operations
105        // are bundled together; failing to persist the group un-persists the keypairs.
106        self.inner()?
107            .transaction()
108            .with_savepoint(
109                "new_conversation",
110                async || {
111                    let group = MlsGroup::new_with_group_id(
112                        &provider,
113                        &credential.signature_key_pair,
114                        &config,
115                        openmls::prelude::GroupId::from_slice(id.as_ref()),
116                        credential.to_mls_credential_with_key(),
117                    )
118                    .await
119                    .map_err(OpenMlsError::wrap("creating group with id"))?;
120
121                    self.persist_conversation_from_mls_group(group, configuration).await?;
122
123                    Ok(())
124                },
125                Error::savepoint,
126            )
127            .await
128    }
129}