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