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::{PersistedMlsPendingGroup, StoredBufferedCommit},
11    traits::FetchFromDatabase as _,
12};
13use openmls::group::MlsGroup;
14
15use super::{Error, Result, TransactionContext};
16use crate::{
17    ConversationConfiguration, CredentialRef, KeystoreError, LeafError, 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::root("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::root("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    /// The check is not redundant. One conversation id can name a group in `mls_groups` and a
66    /// pending group in `mls_pending_groups` at the same time — that is what rejoining a
67    /// conversation by external commit looks like — so removing one of the two does not on its own
68    /// make the buffers garbage. Clearing unconditionally would discard messages the surviving
69    /// conversation is still going to replay.
70    ///
71    /// Callers must have staged their own deletion before calling this, since that deletion is
72    /// exactly what this reads back. It is also the reason this consults the keystore rather than
73    /// [`Self::conversation_exists`]: the question is which rows will exist once the transaction
74    /// commits, which the in-memory conversation cache does not answer.
75    pub(crate) async fn clear_orphaned_conversation_buffers(&self, id: &ConversationIdRef) -> Result<()> {
76        let database = self.database().await?;
77
78        let conversation_remains = database.mls_group_exists(id).await
79            || database
80                .get_borrowed::<PersistedMlsPendingGroup>(id.as_ref())
81                .await
82                .map_err(KeystoreError::wrap(
83                    "looking for a pending group of a removed conversation",
84                ))?
85                .is_some();
86        if conversation_remains {
87            return Ok(());
88        }
89
90        database
91            .remove_pending_messages_by_conversation_id(id)
92            .await
93            .map_err(KeystoreError::wrap(
94                "clearing buffered messages of a removed conversation",
95            ))?;
96
97        self.inner()
98            .await?
99            .transaction()
100            .remove_borrowed::<StoredBufferedCommit>(id.as_ref())
101            .await
102            .map_err(KeystoreError::wrap(
103                "clearing the buffered commit of a removed conversation",
104            ))?;
105
106        Ok(())
107    }
108
109    pub(crate) async fn pending_conversation(&self, id: &ConversationIdRef) -> Result<PendingConversation> {
110        let inner = self.inner().await?;
111        let Some(pending_group) = inner
112            .transaction
113            .get_borrowed::<PersistedMlsPendingGroup>(id.as_ref())
114            .await
115            .map_err(KeystoreError::wrap("finding persisted mls pending group"))?
116        else {
117            return Err(LeafError::ConversationNotFound(id.to_owned()).into());
118        };
119        let pending_group = Arc::unwrap_or_clone(pending_group);
120        Ok(PendingConversation::new(pending_group, self.clone()))
121    }
122
123    /// Create a new empty conversation
124    ///
125    /// # Arguments
126    /// * `id` - identifier of the group/conversation (must be unique otherwise the existing group will be overridden)
127    /// * `creator_credential_type` - kind of credential the creator wants to create the group with
128    /// * `config` - configuration of the group/conversation
129    ///
130    /// # Errors
131    /// Errors can happen from the KeyStore or from OpenMls for ex if no [openmls::key_packages::KeyPackage] can
132    /// be found in the KeyStore
133    #[cfg_attr(test, crate::dispotent)]
134    pub async fn new_conversation(
135        &self,
136        id: &ConversationIdRef,
137        credential_ref: &CredentialRef,
138        configuration: ConversationConfiguration,
139    ) -> Result<()> {
140        let database = self.database().await?;
141        let provider = self.crypto_provider().await?;
142        if self.conversation_exists(id).await? || self.pending_conversation_exists(id).await? {
143            return Err(LeafError::ConversationAlreadyExists(id.to_owned()).into());
144        }
145
146        let credential = credential_ref
147            .load(&*database)
148            .await
149            .map_err(RecursiveError::mls_credential_ref(
150                "loading credential from database to create new conversation",
151            ))?;
152
153        let config = configuration
154            .as_openmls_default_configuration()
155            .map_err(RecursiveError::mls_conversation("converting config to openmls default"))?;
156
157        let group = MlsGroup::new_with_group_id(
158            &provider,
159            &credential.signature_key_pair,
160            &config,
161            openmls::prelude::GroupId::from_slice(id.as_ref()),
162            credential.to_mls_credential_with_key(),
163        )
164        .await
165        .map_err(OpenMlsError::wrap("creating group with id"))?;
166
167        self.persist_conversation_from_mls_group(group, configuration, Default::default())
168            .await?;
169
170        Ok(())
171    }
172}