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, PersistedMlsPendingGroup, StoredBufferedCommit},
11    traits::{DeletableBySearchKey, EntityDeleteBorrowed, 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 inner = self.inner().await?;
77        let tx = inner.transaction();
78
79        let group_exists = tx
80            .get_borrowed::<PersistedMlsGroup>(id.as_ref())
81            .await
82            .map_err(KeystoreError::wrap("looking for a group of a removed conversation"))?
83            .is_some();
84        let pending_group_exists = tx
85            .get_borrowed::<PersistedMlsPendingGroup>(id.as_ref())
86            .await
87            .map_err(KeystoreError::wrap(
88                "looking for a pending group of a removed conversation",
89            ))?
90            .is_some();
91        if group_exists || pending_group_exists {
92            return Ok(());
93        }
94
95        MlsPendingMessage::delete_all_matching(tx, id.keystore()).map_err(KeystoreError::wrap(
96            "clearing the pending messages of a removed conversation",
97        ))?;
98        StoredBufferedCommit::delete_borrowed(tx, id.as_ref()).map_err(KeystoreError::wrap(
99            "clearing the buffered commit of a removed conversation",
100        ))?;
101
102        Ok(())
103    }
104
105    pub(crate) async fn pending_conversation(&self, id: &ConversationIdRef) -> Result<PendingConversation> {
106        let inner = self.inner().await?;
107        let Some(pending_group) = inner
108            .transaction
109            .get_borrowed::<PersistedMlsPendingGroup>(id.as_ref())
110            .await
111            .map_err(KeystoreError::wrap("finding persisted mls pending group"))?
112        else {
113            return Err(LeafError::ConversationNotFound(id.to_owned()).into());
114        };
115        let pending_group = Arc::unwrap_or_clone(pending_group);
116        Ok(PendingConversation::new(pending_group, self.clone()))
117    }
118
119    /// Create a new empty conversation
120    ///
121    /// # Arguments
122    /// * `id` - identifier of the group/conversation (must be unique otherwise the existing group will be overridden)
123    /// * `creator_credential_type` - kind of credential the creator wants to create the group with
124    /// * `config` - configuration of the group/conversation
125    ///
126    /// # Errors
127    /// Errors can happen from the KeyStore or from OpenMls for ex if no [openmls::key_packages::KeyPackage] can
128    /// be found in the KeyStore
129    #[cfg_attr(test, crate::dispotent)]
130    pub async fn new_conversation(
131        &self,
132        id: &ConversationIdRef,
133        credential_ref: &CredentialRef,
134        configuration: ConversationConfiguration,
135    ) -> Result<()> {
136        let database = self.database().await?;
137        let provider = self.crypto_provider().await?;
138        if self.conversation_exists(id).await? || self.pending_conversation_exists(id).await? {
139            return Err(LeafError::ConversationAlreadyExists(id.to_owned()).into());
140        }
141
142        let credential = credential_ref
143            .load(&*database)
144            .await
145            .map_err(RecursiveError::mls_credential_ref(
146                "loading credential from database to create new conversation",
147            ))?;
148
149        let config = configuration
150            .as_openmls_default_configuration()
151            .map_err(RecursiveError::mls_conversation("converting config to openmls default"))?;
152
153        let group = MlsGroup::new_with_group_id(
154            &provider,
155            &credential.signature_key_pair,
156            &config,
157            openmls::prelude::GroupId::from_slice(id.as_ref()),
158            credential.to_mls_credential_with_key(),
159        )
160        .await
161        .map_err(OpenMlsError::wrap("creating group with id"))?;
162
163        self.persist_conversation_from_mls_group(group, configuration, Default::default())
164            .await?;
165
166        Ok(())
167    }
168}