diff --git a/extensions/warp-ipfs/src/store/conversation.rs b/extensions/warp-ipfs/src/store/conversation.rs index 6b65bb05f..570ee1ff9 100644 --- a/extensions/warp-ipfs/src/store/conversation.rs +++ b/extensions/warp-ipfs/src/store/conversation.rs @@ -1,9 +1,13 @@ +pub mod document; pub mod message; pub mod reference; -use super::{keystore::Keystore, topics::ConversationTopic, verify_serde_sig, PeerIdExt}; +use super::{keystore::Keystore, topics::ConversationTopic, PeerIdExt}; use crate::store::DidExt; +use crate::store::conversation::document::{ + DirectConversationDocument, GroupConversationDocument, InnerDocument, +}; use crate::store::conversation::message::MessageDocument; use crate::store::conversation::reference::MessageReferenceList; use chrono::{DateTime, Utc}; @@ -13,6 +17,7 @@ use futures::{ stream::{self, BoxStream}, StreamExt, TryFutureExt, }; +use indexmap::IndexSet; use ipld_core::cid::Cid; use rust_ipfs::{Ipfs, Keypair}; use serde::{Deserialize, Serialize}; @@ -44,25 +49,15 @@ pub struct ConversationDocument { pub version: ConversationVersion, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub creator: Option, pub created: DateTime, pub modified: DateTime, - pub permissions: GroupPermissions, - pub conversation_type: ConversationType, - pub recipients: Vec, #[serde(default)] pub favorite: bool, #[serde(default)] pub archived: bool, - pub excluded: HashMap, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub restrict: Vec, #[serde(default)] pub deleted: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub messages: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, #[serde(skip_serializing_if = "Option::is_none")] pub banner: Option, @@ -70,6 +65,7 @@ pub struct ConversationDocument { pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub signature: Option, + pub inner: InnerDocument, } impl Hash for ConversationDocument { @@ -106,125 +102,55 @@ impl ConversationDocument { } pub fn recipients(&self) -> Vec { - let valid_keys = self - .excluded - .iter() - .filter_map(|(did, signature)| { - let context = format!("exclude {}", did); - let signature = bs58::decode(signature).into_vec().unwrap_or_default(); - verify_serde_sig(did.clone(), &context, &signature) - .map(|_| did) - .ok() - }) - .collect::>(); - - self.recipients - .iter() - .filter(|recipient| !valid_keys.contains(recipient)) - .cloned() - .collect() + self.inner.participants() } pub fn conversation_type(&self) -> ConversationType { - self.conversation_type + match self.inner { + InnerDocument::Direct { .. } => ConversationType::Direct, + InnerDocument::Group { .. } => ConversationType::Group, + } } } impl ConversationDocument { - #[allow(clippy::too_many_arguments)] - pub fn new( - keypair: &Keypair, - name: Option, - mut recipients: Vec, - restrict: Vec, - id: Option, - conversation_type: ConversationType, - permissions: GroupPermissions, - created: Option>, - modified: Option>, - creator: Option, - signature: Option, - ) -> Result { + pub fn new_direct(keypair: &Keypair, recipients: [DID; 2]) -> Result { let did = keypair.to_did()?; - let id = id.unwrap_or_else(Uuid::new_v4); - - if !recipients.contains(&did) { - recipients.push(did.clone()); - } - - if recipients.is_empty() { - return Err(Error::CannotCreateConversation); - } - - let messages = None; - let excluded = Default::default(); + let conversation_id = super::generate_shared_topic( + keypair, + recipients + .iter() + .filter(|peer| did.ne(peer)) + .collect::>() + .first() + .ok_or(Error::Other)?, + Some("direct-conversation"), + )?; - let created = created.unwrap_or(Utc::now()); - let modified = modified.unwrap_or(created); + let inner = InnerDocument::Direct(DirectConversationDocument { + participants: recipients, + messages: None, + }); - let mut document = Self { - id, + let document = Self { + id: conversation_id, version: ConversationVersion::default(), - name, - recipients, - creator, - created, - modified, + created: Utc::now(), + modified: Utc::now(), favorite: false, archived: false, - conversation_type, - permissions, - excluded, - messages, - signature, - restrict, deleted: false, icon: None, banner: None, description: None, + name: None, + inner, + signature: None, }; - if document.signature.is_some() { - document.verify()?; - } - - if let Some(creator) = document.creator.as_ref() { - if creator.eq(&did) { - document.sign(keypair)?; - } - } - Ok(document) } - pub fn new_direct(keypair: &Keypair, recipients: [DID; 2]) -> Result { - let did = keypair.to_did()?; - let conversation_id = Some(super::generate_shared_topic( - keypair, - recipients - .iter() - .filter(|peer| did.ne(peer)) - .collect::>() - .first() - .ok_or(Error::Other)?, - Some("direct-conversation"), - )?); - - Self::new( - keypair, - None, - recipients.to_vec(), - vec![], - conversation_id, - ConversationType::Direct, - GroupPermissions::new(), - None, - None, - None, - None, - ) - } - pub fn new_group( keypair: &Keypair, name: Option, @@ -232,104 +158,123 @@ impl ConversationDocument { restrict: &[DID], permissions: GroupPermissions, ) -> Result { - let conversation_id = Some(Uuid::new_v4()); - let creator = Some(keypair.to_did()?); - Self::new( - keypair, - name, - recipients.into_iter().collect(), - restrict.to_vec(), - conversation_id, - ConversationType::Group, - permissions, - None, - None, - creator, - None, - ) - } -} - -impl ConversationDocument { - pub fn sign(&mut self, keypair: &Keypair) -> Result<(), Error> { - if self.conversation_type() == ConversationType::Direct { - return Ok(()); - } + let conversation_id = Uuid::new_v4(); + let creator = keypair.to_did()?; - let Some(creator) = self.creator.as_ref() else { - return Err(Error::PublicKeyInvalid); - }; + let mut participants = recipients.into_iter().collect::>(); - if self.version != ConversationVersion::default() { - self.version = ConversationVersion::default(); + if !participants.contains(&creator) { + participants.insert(creator.clone()); } - let construct = warp::crypto::hash::sha256_iter( - [ - Some(self.id().into_bytes().to_vec()), - Some(creator.to_string().as_bytes().to_vec()), - Some(Vec::from_iter( - self.restrict - .iter() - .flat_map(|rec| rec.to_string().as_bytes().to_vec()), - )), - self.icon.map(|s| s.hash().digest().to_vec()), - self.banner.map(|s| s.hash().digest().to_vec()), - ] - .into_iter(), - None, - ); - - let signature = keypair.sign(&construct).expect("not RSA"); - self.signature = Some(bs58::encode(signature).into_string()); + let restrict = IndexSet::from_iter(restrict.iter().cloned()); - Ok(()) - } - - pub fn verify(&self) -> Result<(), Error> { - if self.conversation_type() == ConversationType::Direct { - return Ok(()); - } + let inner = InnerDocument::Group(GroupConversationDocument { + creator, + participants, + messages: None, + permissions, + excluded: HashMap::default(), + restrict, + }); - let Some(creator) = self.creator.as_ref() else { - return Err(Error::PublicKeyInvalid); + let mut document = Self { + id: conversation_id, + version: ConversationVersion::default(), + created: Utc::now(), + modified: Utc::now(), + favorite: false, + archived: false, + deleted: false, + icon: None, + banner: None, + description: None, + name, + inner, + signature: None, }; - let creator_pk = creator.to_public_key()?; - - let Some(signature) = self.signature.as_ref() else { - return Err(Error::InvalidSignature); - }; + document.sign(keypair)?; - let signature = bs58::decode(signature).into_vec()?; + Ok(document) + } +} - let construct = match self.version { - ConversationVersion::V0 => warp::crypto::hash::sha256_iter( - [ - Some(self.id().into_bytes().to_vec()), - Some(creator.to_string().as_bytes().to_vec()), - Some(Vec::from_iter( - self.restrict - .iter() - .flat_map(|rec| rec.to_string().as_bytes().to_vec()), - )), - self.icon.map(|s| s.hash().digest().to_vec()), - self.banner.map(|s| s.hash().digest().to_vec()), - ] - .into_iter(), - None, - ), - }; +impl ConversationDocument { + pub fn sign(&mut self, keypair: &Keypair) -> Result<(), Error> { + let conversation_id = self.id; + match self.inner { + InnerDocument::Direct(_) => {} + InnerDocument::Group(ref mut document) => { + if self.version != ConversationVersion::default() { + self.version = ConversationVersion::default(); + } - if !creator_pk.verify(&construct, &signature) { - return Err(Error::InvalidSignature); + let construct = warp::crypto::hash::sha256_iter( + [ + Some(conversation_id.into_bytes().to_vec()), + Some(document.creator.to_string().as_bytes().to_vec()), + Some(Vec::from_iter( + document + .restrict + .iter() + .flat_map(|rec| rec.to_string().as_bytes().to_vec()), + )), + self.icon.map(|s| s.hash().digest().to_vec()), + self.banner.map(|s| s.hash().digest().to_vec()), + ] + .into_iter(), + None, + ); + + let signature = keypair.sign(&construct).expect("not RSA"); + self.signature = Some(bs58::encode(signature).into_string()); + } } + Ok(()) + } + pub fn verify(&self) -> Result<(), Error> { + match self.inner { + InnerDocument::Direct(_) => {} + InnerDocument::Group(ref document) => { + let creator_pk = document.creator.to_public_key()?; + + let Some(signature) = &self.signature else { + return Err(Error::InvalidSignature); + }; + + let signature = bs58::decode(signature).into_vec()?; + + let construct = match self.version { + ConversationVersion::V0 => warp::crypto::hash::sha256_iter( + [ + Some(self.id().into_bytes().to_vec()), + Some(document.creator.to_string().as_bytes().to_vec()), + Some(Vec::from_iter( + document + .restrict + .iter() + .flat_map(|rec| rec.to_string().as_bytes().to_vec()), + )), + self.icon.map(|s| s.hash().digest().to_vec()), + self.banner.map(|s| s.hash().digest().to_vec()), + ] + .into_iter(), + None, + ), + }; + + if !creator_pk.verify(&construct, &signature) { + return Err(Error::InvalidSignature); + } + } + } Ok(()) } pub async fn message_reference_list(&self, ipfs: &Ipfs) -> Result { - let refs = match self.messages { + let refs = match self.inner.messages_cid() { Some(cid) => { ipfs.get_dag(cid) .timeout(Duration::from_secs(10)) @@ -354,7 +299,7 @@ impl ConversationDocument { ) -> Result<(), Error> { self.modified = Utc::now(); let next_cid = ipfs.put_dag(list).await?; - self.messages.replace(next_cid); + self.inner.set_messages_cid(next_cid); Ok(()) } @@ -658,15 +603,17 @@ impl From<&ConversationDocument> for Conversation { let mut conversation = Conversation::default(); conversation.set_id(document.id); conversation.set_name(document.name.clone()); - conversation.set_creator(document.creator.clone()); conversation.set_recipients(document.recipients()); conversation.set_created(document.created); - conversation.set_conversation_type(document.conversation_type); - conversation.set_permissions(document.permissions.clone()); + conversation.set_conversation_type(document.conversation_type()); conversation.set_modified(document.modified); conversation.set_favorite(document.favorite); conversation.set_description(document.description.clone()); conversation.set_archived(document.archived); + if let InnerDocument::Group(ref document) = document.inner { + conversation.set_creator(Some(document.creator.clone())); + conversation.set_permissions(document.permissions.clone()); + } conversation } } diff --git a/extensions/warp-ipfs/src/store/conversation/document.rs b/extensions/warp-ipfs/src/store/conversation/document.rs new file mode 100644 index 000000000..9fe6035aa --- /dev/null +++ b/extensions/warp-ipfs/src/store/conversation/document.rs @@ -0,0 +1,76 @@ +use crate::store::verify_serde_sig; +use indexmap::IndexSet; +use ipld_core::cid::Cid; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use warp::crypto::DID; +use warp::raygun::GroupPermissions; + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub struct DirectConversationDocument { + pub participants: [DID; 2], + #[serde(skip_serializing_if = "Option::is_none")] + pub messages: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub struct GroupConversationDocument { + pub creator: DID, + #[serde(default, skip_serializing_if = "IndexSet::is_empty")] + pub participants: IndexSet, + #[serde(skip_serializing_if = "Option::is_none")] + pub messages: Option, + pub permissions: GroupPermissions, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub excluded: HashMap, + #[serde(default, skip_serializing_if = "IndexSet::is_empty")] + pub restrict: IndexSet, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(tag = "conversation_type", rename_all = "lowercase")] +pub enum InnerDocument { + Direct(DirectConversationDocument), + Group(GroupConversationDocument), +} + +impl InnerDocument { + pub fn set_messages_cid(&mut self, cid: impl Into>) { + match self { + InnerDocument::Group(ref mut document) => document.messages = cid.into(), + InnerDocument::Direct(ref mut document) => document.messages = cid.into(), + } + } + + pub fn messages_cid(&self) -> Option { + match self { + InnerDocument::Group(ref document) => document.messages, + InnerDocument::Direct(ref document) => document.messages, + } + } + pub fn participants(&self) -> Vec { + match self { + InnerDocument::Direct(document) => document.participants.to_vec(), + InnerDocument::Group(document) => { + let valid_keys = document + .excluded + .iter() + .filter_map(|(did, signature)| { + let context = format!("exclude {}", did); + let signature = bs58::decode(signature).into_vec().unwrap_or_default(); + verify_serde_sig(did.clone(), &context, &signature) + .map(|_| did) + .ok() + }) + .collect::>(); + + document + .participants + .iter() + .filter(|recipient| !valid_keys.contains(recipient)) + .cloned() + .collect() + } + } + } +} diff --git a/extensions/warp-ipfs/src/store/message.rs b/extensions/warp-ipfs/src/store/message.rs index dd31d7c5f..0be4ba570 100644 --- a/extensions/warp-ipfs/src/store/message.rs +++ b/extensions/warp-ipfs/src/store/message.rs @@ -51,6 +51,7 @@ use crate::store::{ }; use crate::store::community::CommunityDocument; +use crate::store::conversation::document::{GroupConversationDocument, InnerDocument}; use chrono::{DateTime, Utc}; use warp::raygun::community::{ Community, CommunityChannel, CommunityChannelPermission, CommunityChannelType, CommunityInvite, @@ -2493,9 +2494,10 @@ impl ConversationInner { ) -> Result<(), Error> { let document = document.borrow_mut(); let keypair = self.root.keypair(); - if let Some(creator) = document.creator.as_ref() { + if let InnerDocument::Group(ref inner_document) = document.inner { + let creator = &inner_document.creator; let did = keypair.to_did()?; - if creator.eq(&did) && matches!(document.conversation_type(), ConversationType::Group) { + if creator.eq(&did) { document.sign(keypair)?; } } @@ -2702,11 +2704,8 @@ impl ConversationInner { let mut can_broadcast = true; - if matches!(document_type.conversation_type(), ConversationType::Group) { - let creator = document_type - .creator - .as_ref() - .ok_or(Error::InvalidConversation)?; + if let InnerDocument::Group(ref document) = document_type.inner { + let creator = &document.creator; if creator.ne(own_did) { can_broadcast = false; @@ -2727,11 +2726,9 @@ impl ConversationInner { if can_broadcast { let peer_id_list = recipients - .clone() - .iter() + .into_iter() .filter(|did| own_did.ne(did)) - .map(|did| (did.clone(), did)) - .filter_map(|(a, b)| b.to_peer_id().map(|pk| (a, pk)).ok()) + .filter_map(|did| did.to_peer_id().map(|pk| (did, pk)).ok()) .collect::>(); let event = ConversationEvents::DeleteConversation { @@ -3117,7 +3114,7 @@ async fn process_conversation( return Ok(()); } - if !conversation.recipients.contains(&did) { + if !conversation.recipients().contains(&did) { tracing::warn!(%conversation_id, "was added to conversation but never was apart of the conversation."); return Ok(()); } @@ -3129,7 +3126,7 @@ async fn process_conversation( conversation.verify()?; //TODO: Resolve message list - conversation.messages = None; + conversation.inner.set_messages_cid(None); conversation.archived = false; conversation.favorite = false; @@ -3141,7 +3138,7 @@ async fn process_conversation( tracing::info!(%conversation_id, "{} conversation created", conversation_type); - for recipient in conversation.recipients.iter().filter(|d| did.ne(d)) { + for recipient in conversation.recipients().iter().filter(|d| did.ne(d)) { if let Err(e) = this.request_key(conversation_id, recipient).await { tracing::warn!(%conversation_id, error = %e, %recipient, "Failed to send exchange request"); } @@ -3185,7 +3182,7 @@ async fn process_conversation( if conversation.recipients().contains(&sender) && matches!(conversation.conversation_type(), ConversationType::Direct) || matches!(conversation.conversation_type(), ConversationType::Group) - && matches!(&conversation.creator, Some(creator) if creator.eq(&sender)) => + && matches!(&conversation.inner, InnerDocument::Group(GroupConversationDocument { creator, .. }) if creator.eq(&sender)) => { conversation } @@ -3354,15 +3351,15 @@ async fn process_identity_events( for conversation in list.iter().filter(|c| c.recipients().contains(&did)) { let id = conversation.id(); - match conversation.conversation_type() { - ConversationType::Direct => { + match conversation.inner { + InnerDocument::Direct(_) => { if let Err(e) = this.delete_conversation(id, true).await { tracing::warn!(conversation_id = %id, error = %e, "Failed to delete conversation"); continue; } } - ConversationType::Group => { - if conversation.creator != Some(own_did.clone()) { + InnerDocument::Group(ref document) => { + if document.creator != own_did { continue; } @@ -3410,19 +3407,15 @@ async fn process_identity_events( MultiPassEventKind::Unblocked { did } => { let list = this.list().await; - for conversation in list + for id in list .iter() - .filter(|c| { - c.creator - .as_ref() - .map(|creator| own_did.eq(creator)) - .unwrap_or_default() + .filter_map(|c| match c.inner { + InnerDocument::Direct(_) => None, + InnerDocument::Group(ref document) => Some((c.id, document)), }) - .filter(|c| c.conversation_type() == ConversationType::Group) - .filter(|c| c.restrict.contains(&did)) + .filter(|(_, c)| c.restrict.contains(&did)) + .map(|(id, _)| id) { - let id = conversation.id(); - let conversation_meta = this .conversation_task .get(&id) @@ -3449,15 +3442,15 @@ async fn process_identity_events( for conversation in list.iter().filter(|c| c.recipients().contains(&did)) { let id = conversation.id(); - match conversation.conversation_type() { - ConversationType::Direct => { + match conversation.inner { + InnerDocument::Direct(_) => { if let Err(e) = this.delete_conversation(id, true).await { tracing::warn!(conversation_id = %id, error = %e, "Failed to delete conversation"); continue; } } - ConversationType::Group => { - if conversation.creator != Some(own_did.clone()) { + InnerDocument::Group(ref document) => { + if document.creator != own_did { continue; } diff --git a/extensions/warp-ipfs/src/store/message/task.rs b/extensions/warp-ipfs/src/store/message/task.rs index 584a89617..9ee8910c4 100644 --- a/extensions/warp-ipfs/src/store/message/task.rs +++ b/extensions/warp-ipfs/src/store/message/task.rs @@ -38,6 +38,7 @@ use web_time::Instant; // use crate::config; // use crate::shuttle::message::client::MessageCommand; +use crate::store::conversation::document::{GroupConversationDocument, InnerDocument}; use crate::store::conversation::message::{MessageDocument, MessageDocumentBuilder}; use crate::store::discovery::Discovery; use crate::store::document::files::FileDocument; @@ -370,9 +371,9 @@ impl ConversationTask { task.queue = data; } - for participant in task.document.recipients.iter() { - if !task.discovery.contains(participant).await { - let _ = task.discovery.insert(participant).await; + for participant in task.document.recipients() { + if !task.discovery.contains(&participant).await { + let _ = task.discovery.insert(&participant).await; } } @@ -844,7 +845,7 @@ impl ConversationTask { impl ConversationTask { pub async fn delete(&mut self) -> Result<(), Error> { // TODO: Maybe announce to network of the local node removal here - self.document.messages.take(); + self.document.inner.set_messages_cid(None); self.document.deleted = true; self.set_document().await?; if let Ok(mut ks_map) = self.root.get_keystore_map().await { @@ -874,13 +875,10 @@ impl ConversationTask { pub async fn set_document(&mut self) -> Result<(), Error> { let keypair = self.root.keypair(); - if let Some(creator) = self.document.creator.as_ref() { - let did = keypair.to_did()?; - if creator.eq(&did) - && matches!(self.document.conversation_type(), ConversationType::Group) - { - self.document.sign(keypair)?; - } + let did = keypair.to_did()?; + if matches!(&self.document.inner, InnerDocument::Group(GroupConversationDocument { creator, ..}) if creator.eq(&did) ) + { + self.document.sign(keypair)?; } self.document.verify()?; @@ -895,11 +893,10 @@ impl ConversationTask { mut document: ConversationDocument, ) -> Result<(), Error> { let keypair = self.root.keypair(); - if let Some(creator) = document.creator.as_ref() { - let did = keypair.to_did()?; - if creator.eq(&did) && matches!(document.conversation_type(), ConversationType::Group) { - document.sign(keypair)?; - } + let did = keypair.to_did()?; + if matches!(&document.inner, InnerDocument::Group(GroupConversationDocument { creator, ..}) if creator.eq(&did) ) + { + document.sign(keypair)?; } document.verify()?; @@ -987,36 +984,38 @@ impl ConversationTask { permissions: P, ) -> Result<(), Error> { let own_did = self.identity.did_key(); - let Some(creator) = self.document.creator.as_ref() else { - return Err(Error::InvalidConversation); - }; + let (permissions, added, removed) = match self.document.inner { + InnerDocument::Direct(_) => return Err(Error::InvalidConversation), + InnerDocument::Group(ref mut document) => { + if document.creator != own_did { + return Err(Error::PublicKeyInvalid); + } - if creator != &own_did { - return Err(Error::PublicKeyInvalid); - } + let permissions = match permissions.into() { + GroupPermissionOpt::Map(permissions) => permissions, + GroupPermissionOpt::Single((id, set)) => { + let permissions = document.permissions.clone(); + { + let permissions = document.permissions.entry(id).or_default(); + *permissions = set; + } + permissions + } + }; - let permissions = match permissions.into() { - GroupPermissionOpt::Map(permissions) => permissions, - GroupPermissionOpt::Single((id, set)) => { - let permissions = self.document.permissions.clone(); - { - let permissions = self.document.permissions.entry(id).or_default(); - *permissions = set; - } - permissions + let (added, removed) = document.permissions.compare_with_new(&permissions); + + document.permissions = permissions; + + (document.permissions.clone(), added, removed) } }; - let (added, removed) = self.document.permissions.compare_with_new(&permissions); - - self.document.permissions = permissions; self.set_document().await?; let event = MessagingEvents::UpdateConversation { conversation: self.document.clone(), - kind: ConversationUpdateKind::ChangePermissions { - permissions: self.document.permissions.clone(), - }, + kind: ConversationUpdateKind::ChangePermissions { permissions }, }; let _ = self @@ -1804,19 +1803,17 @@ impl ConversationTask { } pub async fn add_participant(&mut self, did_key: &DID) -> Result<(), Error> { - if let ConversationType::Direct = self.document.conversation_type() { - return Err(Error::InvalidConversation); - } - assert_eq!(self.document.conversation_type(), ConversationType::Group); + let this = &mut *self; - let Some(creator) = self.document.creator.clone() else { + let InnerDocument::Group(ref mut document) = this.document.inner else { return Err(Error::InvalidConversation); }; - let own_did = &self.identity.did_key(); + let creator = &document.creator; - if !self - .document + let own_did = &this.identity.did_key(); + + if !document .permissions .has_permission(own_did, GroupPermission::AddParticipants) && creator.ne(own_did) @@ -1828,48 +1825,48 @@ impl ConversationTask { return Err(Error::PublicKeyInvalid); } - if self.root.is_blocked(did_key).await? { + if this.root.is_blocked(did_key).await? { return Err(Error::PublicKeyIsBlocked); } - if self.document.restrict.contains(did_key) { + if document.restrict.contains(did_key) { return Err(Error::PublicKeyIsBlocked); } - if self.document.recipients.contains(did_key) { + if document.participants.contains(did_key) { return Err(Error::IdentityExist); } - self.document.recipients.push(did_key.clone()); + document.participants.insert(did_key.clone()); - self.set_document().await?; + this.set_document().await?; let event = MessagingEvents::UpdateConversation { - conversation: self.document.clone(), + conversation: this.document.clone(), kind: ConversationUpdateKind::AddParticipant { did: did_key.clone(), }, }; - let tx = self.event_broadcast.clone(); + let tx = this.event_broadcast.clone(); let _ = tx.send(MessageEventKind::RecipientAdded { - conversation_id: self.conversation_id, + conversation_id: this.conversation_id, recipient: did_key.clone(), }); - if !self.discovery.contains(did_key).await { - let _ = self.discovery.insert(did_key).await; + if !this.discovery.contains(did_key).await { + let _ = this.discovery.insert(did_key).await; } - self.publish(None, event, true).await?; + this.publish(None, event, true).await?; let new_event = ConversationEvents::NewGroupConversation { - conversation: self.document.clone(), + conversation: this.document.clone(), }; - self.send_single_conversation_event(did_key, new_event) + this.send_single_conversation_event(did_key, new_event) .await?; - if let Err(_e) = self.request_key(did_key).await {} + if let Err(_e) = this.request_key(did_key).await {} Ok(()) } @@ -1878,19 +1875,18 @@ impl ConversationTask { did_key: &DID, broadcast: bool, ) -> Result<(), Error> { - if matches!(self.document.conversation_type(), ConversationType::Direct) { - return Err(Error::InvalidConversation); - } + let this = &mut *self; - let Some(creator) = self.document.creator.as_ref() else { + let InnerDocument::Group(ref mut document) = this.document.inner else { return Err(Error::InvalidConversation); }; - let own_did = &self.identity.did_key(); + let creator = &document.creator; + + let own_did = &this.identity.did_key(); if creator.ne(own_did) - && !self - .document + && !document .permissions .has_permission(own_did, GroupPermission::RemoveParticipants) { @@ -1901,34 +1897,34 @@ impl ConversationTask { return Err(Error::PublicKeyInvalid); } - if !self.document.recipients.contains(did_key) { + if !document.participants.contains(did_key) { return Err(Error::IdentityDoesntExist); } - self.document.recipients.retain(|did| did.ne(did_key)); - self.set_document().await?; + document.participants.retain(|did| did.ne(did_key)); + this.set_document().await?; let event = MessagingEvents::UpdateConversation { - conversation: self.document.clone(), + conversation: this.document.clone(), kind: ConversationUpdateKind::RemoveParticipant { did: did_key.clone(), }, }; - let tx = self.event_broadcast.clone(); + let tx = this.event_broadcast.clone(); let _ = tx.send(MessageEventKind::RecipientRemoved { - conversation_id: self.conversation_id, + conversation_id: this.conversation_id, recipient: did_key.clone(), }); - self.publish(None, event, true).await?; + this.publish(None, event, true).await?; if broadcast { let new_event = ConversationEvents::DeleteConversation { - conversation_id: self.conversation_id, + conversation_id: this.conversation_id, }; - self.send_single_conversation_event(did_key, new_event) + this.send_single_conversation_event(did_key, new_event) .await?; } @@ -1936,15 +1932,15 @@ impl ConversationTask { } pub async fn add_restricted(&mut self, did_key: &DID) -> Result<(), Error> { - if matches!(self.document.conversation_type(), ConversationType::Direct) { - return Err(Error::InvalidConversation); - } + let this = &mut *self; - let Some(creator) = self.document.creator.clone() else { + let InnerDocument::Group(ref mut document) = this.document.inner else { return Err(Error::InvalidConversation); }; - let own_did = &self.identity.did_key(); + let creator = &document.creator; + + let own_did = &this.identity.did_key(); if creator.ne(own_did) { return Err(Error::PublicKeyInvalid); @@ -1954,37 +1950,37 @@ impl ConversationTask { return Err(Error::PublicKeyInvalid); } - if !self.root.is_blocked(did_key).await? { + if !this.root.is_blocked(did_key).await? { return Err(Error::PublicKeyIsntBlocked); } - debug_assert!(!self.document.recipients.contains(did_key)); - debug_assert!(!self.document.restrict.contains(did_key)); + debug_assert!(!document.participants.contains(did_key)); + debug_assert!(!document.restrict.contains(did_key)); - self.document.restrict.push(did_key.clone()); + document.restrict.insert(did_key.clone()); - self.set_document().await?; + this.set_document().await?; let event = MessagingEvents::UpdateConversation { - conversation: self.document.clone(), + conversation: this.document.clone(), kind: ConversationUpdateKind::AddRestricted { did: did_key.clone(), }, }; - self.publish(None, event, true).await + this.publish(None, event, true).await } pub async fn remove_restricted(&mut self, did_key: &DID) -> Result<(), Error> { - if matches!(self.document.conversation_type(), ConversationType::Direct) { - return Err(Error::InvalidConversation); - } + let this = &mut *self; - let Some(creator) = self.document.creator.clone() else { + let InnerDocument::Group(ref mut document) = this.document.inner else { return Err(Error::InvalidConversation); }; - let own_did = &self.identity.did_key(); + let creator = &document.creator; + + let own_did = &this.identity.did_key(); if creator.ne(own_did) { return Err(Error::PublicKeyInvalid); @@ -1994,26 +1990,24 @@ impl ConversationTask { return Err(Error::PublicKeyInvalid); } - if self.root.is_blocked(did_key).await? { + if this.root.is_blocked(did_key).await? { return Err(Error::PublicKeyIsBlocked); } - debug_assert!(self.document.restrict.contains(did_key)); + debug_assert!(document.restrict.contains(did_key)); - self.document - .restrict - .retain(|restricted| restricted != did_key); + document.restrict.retain(|restricted| restricted != did_key); - self.set_document().await?; + this.set_document().await?; let event = MessagingEvents::UpdateConversation { - conversation: self.document.clone(), + conversation: this.document.clone(), kind: ConversationUpdateKind::RemoveRestricted { did: did_key.clone(), }, }; - self.publish(None, event, true).await + this.publish(None, event, true).await } pub async fn update_conversation_name(&mut self, name: &str) -> Result<(), Error> { @@ -2029,19 +2023,17 @@ impl ConversationTask { }); } - if let ConversationType::Direct = self.document.conversation_type() { - return Err(Error::InvalidConversation); - } - assert_eq!(self.document.conversation_type(), ConversationType::Group); + let this = &mut *self; - let Some(creator) = self.document.creator.clone() else { + let InnerDocument::Group(ref mut document) = this.document.inner else { return Err(Error::InvalidConversation); }; - let own_did = &self.identity.did_key(); + let creator = &document.creator; - if !&self - .document + let own_did = &this.identity.did_key(); + + if !document .permissions .has_permission(own_did, GroupPermission::EditGroupInfo) && creator.ne(own_did) @@ -2049,25 +2041,25 @@ impl ConversationTask { return Err(Error::Unauthorized); } - self.document.name = (!name.is_empty()).then_some(name.to_string()); + this.document.name = (!name.is_empty()).then_some(name.to_string()); - self.set_document().await?; + this.set_document().await?; - let new_name = self.document.name(); + let new_name = this.document.name(); let event = MessagingEvents::UpdateConversation { - conversation: self.document.clone(), + conversation: this.document.clone(), kind: ConversationUpdateKind::ChangeName { name: new_name }, }; - let _ = self + let _ = this .event_broadcast .send(MessageEventKind::ConversationNameUpdated { - conversation_id: self.conversation_id, + conversation_id: this.conversation_id, name: name.to_string(), }); - self.publish(None, event, true).await + this.publish(None, event, true).await } pub async fn conversation_image( @@ -2118,13 +2110,10 @@ impl ConversationTask { ConversationImageType::Banner => MAX_CONVERSATION_BANNER_SIZE, ConversationImageType::Icon => MAX_CONVERSATION_ICON_SIZE, }; - if self.document.conversation_type() == ConversationType::Group { - let Some(creator) = self.document.creator.as_ref() else { - return Err(Error::InvalidConversation); - }; + if let InnerDocument::Group(ref mut document) = self.document.inner { + let creator = &document.creator; let own_did = self.identity.did_key(); - if !&self - .document + if !document .permissions .has_permission(&own_did, GroupPermission::EditGroupImages) && own_did.ne(creator) @@ -2260,13 +2249,10 @@ impl ConversationTask { &mut self, image_type: ConversationImageType, ) -> Result<(), Error> { - if self.document.conversation_type() == ConversationType::Group { - let Some(creator) = self.document.creator.as_ref() else { - return Err(Error::InvalidConversation); - }; + if let InnerDocument::Group(ref mut document) = self.document.inner { + let creator = &document.creator; let own_did = self.identity.did_key(); - if !&self - .document + if !document .permissions .has_permission(&own_did, GroupPermission::EditGroupImages) && own_did.ne(creator) @@ -2314,15 +2300,12 @@ impl ConversationTask { pub async fn set_description(&mut self, desc: Option<&str>) -> Result<(), Error> { let conversation_id = self.conversation_id; - if self.document.conversation_type() == ConversationType::Group { - let Some(creator) = self.document.creator.as_ref() else { - return Err(Error::InvalidConversation); - }; + if let InnerDocument::Group(ref mut document) = self.document.inner { + let creator = &document.creator; let own_did = self.identity.did_key(); - if !&self - .document + if !document .permissions .has_permission(&own_did, GroupPermission::EditGroupInfo) && own_did.ne(creator) @@ -2625,22 +2608,22 @@ impl ConversationTask { async fn add_exclusion(&mut self, member: DID, signature: String) -> Result<(), Error> { let conversation_id = self.conversation_id; - if !matches!(self.document.conversation_type(), ConversationType::Group) { - return Err(anyhow::anyhow!("Can only leave from a group conversation").into()); - } + let this = &mut *self; - let Some(creator) = self.document.creator.as_ref() else { - return Err(anyhow::anyhow!("Group conversation requires a creator").into()); + let InnerDocument::Group(ref mut document) = this.document.inner else { + return Err(Error::InvalidConversation); }; - let own_did = self.identity.did_key(); + let creator = &document.creator; + + let own_did = this.identity.did_key(); // Precaution if member.eq(creator) { return Err(anyhow::anyhow!("Cannot remove the creator of the group").into()); } - if !self.document.recipients.contains(&member) { + if !document.participants.contains(&member) { return Err(anyhow::anyhow!("{member} does not belong to {conversation_id}").into()); } @@ -2657,7 +2640,7 @@ impl ConversationTask { } //Validate again since we have a permit - if !self.document.recipients.contains(&member) { + if !document.participants.contains(&member) { return Err( anyhow::anyhow!("{member} does not belong to {conversation_id}").into(), ); @@ -2665,7 +2648,7 @@ impl ConversationTask { let mut can_emit = false; - if let Entry::Vacant(entry) = self.document.excluded.entry(member.clone()) { + if let Entry::Vacant(entry) = document.excluded.entry(member.clone()) { entry.insert(signature); can_emit = true; } @@ -2956,23 +2939,36 @@ async fn message_event( kind, } => { conversation.verify()?; - conversation.excluded = this.document.excluded.clone(); - conversation.messages = this.document.messages; + + match (&mut conversation.inner, &this.document.inner) { + (InnerDocument::Direct(external), InnerDocument::Direct(ref internal)) => { + external.messages = internal.messages; + } + (InnerDocument::Group(external), InnerDocument::Group(ref internal)) => { + external.excluded = internal.excluded.clone(); + external.messages = internal.messages; + } + _ => unreachable!(), + } + conversation.favorite = this.document.favorite; conversation.archived = this.document.archived; match kind { ConversationUpdateKind::AddParticipant { did } => { - if !this.document.creator.as_ref().is_some_and(|c| c == sender) - && !this - .document + let InnerDocument::Group(ref mut document) = this.document.inner else { + return Err(Error::InvalidConversation); + }; + let creator = &document.creator; + if creator != sender + && !document .permissions .has_permission(sender, GroupPermission::AddParticipants) { return Err(Error::Unauthorized); } - if this.document.recipients.contains(&did) { + if document.participants.contains(&did) { return Ok(()); } @@ -2994,25 +2990,30 @@ async fn message_event( } } ConversationUpdateKind::RemoveParticipant { did } => { - if !this.document.creator.as_ref().is_some_and(|c| c == sender) - && !this - .document + let InnerDocument::Group(ref mut document) = this.document.inner else { + return Err(Error::InvalidConversation); + }; + let creator = &document.creator; + + if creator != sender + && !document .permissions .has_permission(sender, GroupPermission::RemoveParticipants) { return Err(Error::Unauthorized); } - if !this.document.recipients.contains(&did) { + + if !document.participants.contains(&did) { return Err(Error::IdentityDoesntExist); } - this.document.permissions.shift_remove(&did); + document.permissions.shift_remove(&did); //Maybe remove participant from discovery? - let can_emit = !this.document.excluded.contains_key(&did); + let can_emit = !document.excluded.contains_key(&did); - this.document.excluded.remove(&did); + document.excluded.remove(&did); this.replace_document(conversation).await?; @@ -3029,14 +3030,16 @@ async fn message_event( } } ConversationUpdateKind::ChangeName { name: Some(name) } => { - if !this.document.creator.as_ref().is_some_and(|c| c == sender) - && !this - .document - .permissions - .has_permission(sender, GroupPermission::EditGroupInfo) - { - return Err(Error::Unauthorized); - } + if let InnerDocument::Group(ref mut document) = this.document.inner { + let creator = &document.creator; + if creator != sender + && !document + .permissions + .has_permission(sender, GroupPermission::EditGroupInfo) + { + return Err(Error::Unauthorized); + } + }; let name = name.trim(); let name_length = name.len(); @@ -3069,14 +3072,16 @@ async fn message_event( } ConversationUpdateKind::ChangeName { name: None } => { - if !this.document.creator.as_ref().is_some_and(|c| c == sender) - && !this - .document - .permissions - .has_permission(sender, GroupPermission::EditGroupInfo) - { - return Err(Error::Unauthorized); - } + if let InnerDocument::Group(ref mut document) = this.document.inner { + let creator = &document.creator; + if creator != sender + && !document + .permissions + .has_permission(sender, GroupPermission::EditGroupInfo) + { + return Err(Error::Unauthorized); + } + }; this.replace_document(conversation).await?; @@ -3092,20 +3097,32 @@ async fn message_event( } ConversationUpdateKind::AddRestricted { .. } | ConversationUpdateKind::RemoveRestricted { .. } => { - if !this.document.creator.as_ref().is_some_and(|c| c == sender) { - return Err(Error::Unauthorized); + if let InnerDocument::Group(ref mut document) = this.document.inner { + let creator = &document.creator; + if creator != sender + && !document + .permissions + .has_permission(sender, GroupPermission::EditGroupInfo) + { + return Err(Error::Unauthorized); + } + + this.replace_document(conversation).await?; } - this.replace_document(conversation).await?; //TODO: Maybe add a api event to emit for when blocked users are added/removed from the document // but for now, we can leave this as a silent update since the block list would be for internal handling for now } ConversationUpdateKind::ChangePermissions { permissions } => { - if !this.document.creator.as_ref().is_some_and(|c| c == sender) { + let InnerDocument::Group(ref mut document) = this.document.inner else { + return Err(Error::InvalidConversation); + }; + let creator = &document.creator; + if creator != sender { return Err(Error::Unauthorized); } - let (added, removed) = this.document.permissions.compare_with_new(&permissions); - this.document.permissions = permissions; + let (added, removed) = document.permissions.compare_with_new(&permissions); + document.permissions = permissions; this.replace_document(conversation).await?; if let Err(e) = this.event_broadcast.send( @@ -3119,15 +3136,17 @@ async fn message_event( } } ConversationUpdateKind::AddedIcon | ConversationUpdateKind::RemovedIcon => { - if this.document.conversation_type == ConversationType::Group - && !this.document.creator.as_ref().is_some_and(|c| c == sender) - && !this - .document - .permissions - .has_permission(sender, GroupPermission::EditGroupImages) - { - return Err(Error::Unauthorized); - } + if let InnerDocument::Group(ref mut document) = this.document.inner { + let creator = &document.creator; + if creator != sender + && !document + .permissions + .has_permission(sender, GroupPermission::EditGroupInfo) + { + return Err(Error::Unauthorized); + } + }; + this.replace_document(conversation).await?; if let Err(e) = this @@ -3139,15 +3158,16 @@ async fn message_event( } ConversationUpdateKind::AddedBanner | ConversationUpdateKind::RemovedBanner => { - if this.document.conversation_type == ConversationType::Group - && !this.document.creator.as_ref().is_some_and(|c| c == sender) - && !this - .document - .permissions - .has_permission(sender, GroupPermission::EditGroupImages) - { - return Err(Error::Unauthorized); - } + if let InnerDocument::Group(ref mut document) = this.document.inner { + let creator = &document.creator; + if creator != sender + && !document + .permissions + .has_permission(sender, GroupPermission::EditGroupInfo) + { + return Err(Error::Unauthorized); + } + }; this.replace_document(conversation).await?; if let Err(e) = this @@ -3158,15 +3178,16 @@ async fn message_event( } } ConversationUpdateKind::ChangeDescription { description } => { - if this.document.conversation_type == ConversationType::Group - && !this.document.creator.as_ref().is_some_and(|c| c == sender) - && !this - .document - .permissions - .has_permission(sender, GroupPermission::EditGroupInfo) - { - return Err(Error::Unauthorized); - } + if let InnerDocument::Group(ref mut document) = this.document.inner { + let creator = &document.creator; + if creator != sender + && !document + .permissions + .has_permission(sender, GroupPermission::EditGroupInfo) + { + return Err(Error::Unauthorized); + } + }; if let Some(desc) = description.as_ref() { if desc.is_empty() || desc.len() > MAX_CONVERSATION_DESCRIPTION { return Err(Error::InvalidLength {