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::{
10    entities::{MlsPendingMessage, PersistedMlsGroup, StoredBufferedCommit},
11    traits::{DeletableBySearchKey, EntityDeleteBorrowed, FetchFromDatabase as _},
12};
13use openmls::group::MlsGroup;
14
15use super::{Error, Result, TransactionContext};
16use crate::{
17    ConversationConfiguration, CredentialRef, KeystoreError, OpenMlsError, RecursiveError,
18    mls::conversation::{ConversationIdRef, ConversationMut, PendingConversation},
19};
20
21impl TransactionContext {
22    /// Checks if a given conversation id exists locally.
23    ///
24    /// Somewhat cheaper than `self.conversation(id).is_ok()`.
25    pub async fn conversation_exists(&self, id: &ConversationIdRef) -> Result<bool> {
26        let database = self.database().await?.into();
27        self.mls_groups()
28            .await?
29            .exists(id, &database)
30            .await
31            .map_err(RecursiveError::context("checking for conversation existence"))
32            .map_err(Into::into)
33    }
34
35    /// Acquire a conversation guard.
36    ///
37    /// This helper struct permits mutations on a conversation.
38    pub async fn conversation(&self, id: &ConversationIdRef) -> Result<ConversationMut> {
39        let inner = self.inner().await?;
40        let session = self.session().await?;
41        let conversation = self
42            .mls_groups()
43            .await?
44            .get_or_fetch(id, &inner.transaction, session)
45            .await
46            .map_err(RecursiveError::context("fetching conversation from mls groups by id"))?;
47
48        if let Some(conversation) = conversation {
49            return Ok(ConversationMut::new(conversation, self.clone()));
50        }
51        // Check if there is a pending conversation with
52        // the same id
53        let pending = self.pending_conversation(id).await.map(Error::PendingConversation)?;
54        Err(pending)
55    }
56
57    /// Discard everything buffered for a conversation which no longer exists in any form.
58    ///
59    /// Buffered messages and buffered commits are keyed by conversation id, and both are only ever
60    /// read on behalf of a conversation. Once no conversation holds that id, they are unreachable:
61    /// nothing can restore them and nothing else will ever delete them. So whichever operation
62    /// removes the last trace of a conversation has to take its buffers along, and this is that
63    /// step.
64    ///
65    /// Callers must have staged their own deletion before calling this, since that deletion is
66    /// exactly what this reads back. It is also the reason this consults the keystore rather than
67    /// [`Self::conversation_exists`]: the question is which rows will exist once the transaction
68    /// commits, which the in-memory conversation cache does not answer.
69    pub(crate) async fn clear_orphaned_conversation_buffers(&self, id: &ConversationIdRef) -> Result<()> {
70        let inner = self.inner().await?;
71        let tx = inner.transaction();
72
73        let group_exists = tx
74            .get_borrowed::<PersistedMlsGroup>(id.keystore())
75            .await
76            .map_err(KeystoreError::wrap("looking for a group of a removed conversation"))?
77            .is_some();
78        if group_exists {
79            return Ok(());
80        }
81
82        MlsPendingMessage::delete_all_matching(tx, id.keystore()).map_err(KeystoreError::wrap(
83            "clearing the pending messages of a removed conversation",
84        ))?;
85        StoredBufferedCommit::delete_borrowed(tx, id.as_ref()).map_err(KeystoreError::wrap(
86            "clearing the buffered commit of a removed conversation",
87        ))?;
88
89        Ok(())
90    }
91
92    pub(crate) async fn pending_conversation(&self, id: &ConversationIdRef) -> Result<PendingConversation> {
93        let inner = self.inner().await?;
94        let group = inner
95            .transaction
96            .get_borrowed::<PersistedMlsGroup>(id.keystore())
97            .await
98            .map_err(KeystoreError::wrap("finding persisted mls group"))?
99            .filter(|group| group.is_pending);
100        let Some(group) = group else {
101            return Err(Error::ConversationNotFound(id.to_owned()));
102        };
103        let group = Arc::unwrap_or_clone(group);
104        Ok(PendingConversation::new(group, self.clone()))
105    }
106
107    /// Create a new empty conversation
108    ///
109    /// # Arguments
110    /// * `id` - identifier of the group/conversation (must be unique otherwise the existing group will be overridden)
111    /// * `creator_credential_type` - kind of credential the creator wants to create the group with
112    /// * `config` - configuration of the group/conversation
113    ///
114    /// # Errors
115    /// Errors can happen from the KeyStore or from OpenMls for ex if no [openmls::key_packages::KeyPackage] can
116    /// be found in the KeyStore
117    #[cfg_attr(test, crate::dispotent)]
118    pub async fn new_conversation(
119        &self,
120        id: &ConversationIdRef,
121        credential_ref: &CredentialRef,
122        configuration: ConversationConfiguration,
123    ) -> Result<()> {
124        let database = self.database().await?;
125        let provider = self.crypto_provider().await?;
126        if self.conversation_exists(id).await? || self.pending_conversation_exists(id).await? {
127            return Err(Error::ConversationAlreadyExists(id.to_owned()));
128        }
129
130        let credential = credential_ref.load(&*database).await.map_err(RecursiveError::context(
131            "loading credential from database to create new conversation",
132        ))?;
133
134        let config = configuration
135            .as_openmls_default_configuration()
136            .map_err(RecursiveError::context("converting config to openmls default"))?;
137
138        let group = MlsGroup::new_with_group_id(
139            &provider,
140            &credential.signature_key_pair,
141            &config,
142            openmls::prelude::GroupId::from_slice(id.as_ref()),
143            credential.to_mls_credential_with_key(),
144        )
145        .await
146        .map_err(OpenMlsError::wrap("creating group with id"))?;
147
148        self.persist_conversation_from_mls_group(group, configuration).await?;
149
150        Ok(())
151    }
152}