use std::collections::BTreeSet;
use std::str;
use nostr::prelude::*;
use nrc_mls_storage::groups::types as group_types;
use nrc_mls_storage::messages::types as message_types;
use nrc_mls_storage::NostrMlsStorageProvider;
use openmls::group::GroupId;
use openmls::prelude::*;
use openmls_basic_credential::SignatureKeyPair;
use tls_codec::Serialize as TlsSerialize;
use super::extension::NostrGroupDataExtension;
use super::NostrMls;
use crate::error::Error;
#[derive(Debug)]
pub struct GroupResult {
pub group: group_types::Group,
pub welcome_rumors: Vec<UnsignedEvent>,
}
#[derive(Debug)]
pub struct UpdateGroupResult {
pub evolution_event: Event,
pub welcome_rumors: Option<Vec<UnsignedEvent>>,
}
#[derive(Debug, Clone)]
pub struct NostrGroupConfigData {
pub name: String,
pub description: String,
pub image_url: Option<String>,
pub image_key: Option<Vec<u8>>,
pub image_nonce: Option<Vec<u8>>,
pub relays: Vec<RelayUrl>,
pub admins: Vec<PublicKey>,
}
#[derive(Debug, Clone, Default)]
pub struct NostrGroupDataUpdate {
pub name: Option<String>,
pub description: Option<String>,
pub image_url: Option<Option<String>>,
pub image_key: Option<Option<Vec<u8>>>,
pub image_nonce: Option<Option<Vec<u8>>>,
pub relays: Option<Vec<RelayUrl>>,
pub admins: Option<Vec<PublicKey>>,
}
impl NostrGroupConfigData {
pub fn new(
name: String,
description: String,
image_url: Option<String>,
image_key: Option<Vec<u8>>,
image_nonce: Option<Vec<u8>>,
relays: Vec<RelayUrl>,
admins: Vec<PublicKey>,
) -> Self {
Self {
name,
description,
image_url,
image_key,
image_nonce,
relays,
admins,
}
}
}
impl NostrGroupDataUpdate {
pub fn new() -> Self {
Self::default()
}
pub fn name<T>(mut self, name: T) -> Self
where
T: Into<String>,
{
self.name = Some(name.into());
self
}
pub fn description<T>(mut self, description: T) -> Self
where
T: Into<String>,
{
self.description = Some(description.into());
self
}
pub fn image_url<T>(mut self, image_url: Option<T>) -> Self
where
T: Into<String>,
{
self.image_url = Some(image_url.map(Into::into));
self
}
pub fn image_key(mut self, image_key: Option<Vec<u8>>) -> Self {
self.image_key = Some(image_key);
self
}
pub fn image_nonce(mut self, image_nonce: Option<Vec<u8>>) -> Self {
self.image_nonce = Some(image_nonce);
self
}
pub fn relays(mut self, relays: Vec<RelayUrl>) -> Self {
self.relays = Some(relays);
self
}
pub fn admins(mut self, admins: Vec<PublicKey>) -> Self {
self.admins = Some(admins);
self
}
}
impl<Storage> NostrMls<Storage>
where
Storage: NostrMlsStorageProvider,
{
pub(crate) fn get_own_pubkey(&self, group: &MlsGroup) -> Result<PublicKey, Error> {
let own_leaf = group.own_leaf().ok_or(Error::OwnLeafNotFound)?;
let credentials: BasicCredential =
BasicCredential::try_from(own_leaf.credential().clone())?;
let hex_bytes: &[u8] = credentials.identity();
let hex_str: &str = str::from_utf8(hex_bytes)?;
let public_key = PublicKey::from_hex(hex_str)?;
Ok(public_key)
}
pub(crate) fn is_leaf_node_admin(
&self,
group_id: &GroupId,
leaf_node: &LeafNode,
) -> Result<bool, Error> {
let pubkey = self.pubkey_for_leaf_node(leaf_node)?;
let stored_group = self.get_group(group_id)?.ok_or(Error::GroupNotFound)?;
Ok(stored_group.admin_pubkeys.contains(&pubkey))
}
pub(crate) fn is_member_admin(
&self,
group_id: &GroupId,
member: &Member,
) -> Result<bool, Error> {
let pubkey = self.pubkey_for_member(member)?;
let stored_group = self.get_group(group_id)?.ok_or(Error::GroupNotFound)?;
Ok(stored_group.admin_pubkeys.contains(&pubkey))
}
pub(crate) fn pubkey_for_leaf_node(&self, leaf_node: &LeafNode) -> Result<PublicKey, Error> {
let credentials: BasicCredential =
BasicCredential::try_from(leaf_node.credential().clone())?;
let hex_bytes: &[u8] = credentials.identity();
let hex_str: &str = str::from_utf8(hex_bytes)?;
let public_key = PublicKey::from_hex(hex_str)?;
Ok(public_key)
}
pub(crate) fn pubkey_for_member(&self, member: &Member) -> Result<PublicKey, Error> {
let credentials: BasicCredential = BasicCredential::try_from(member.credential.clone())?;
let hex_bytes: &[u8] = credentials.identity();
let hex_str: &str = str::from_utf8(hex_bytes)?;
let public_key = PublicKey::from_hex(hex_str)?;
Ok(public_key)
}
pub(crate) fn load_mls_signer(&self, group: &MlsGroup) -> Result<SignatureKeyPair, Error> {
let own_leaf: &LeafNode = group.own_leaf().ok_or(Error::OwnLeafNotFound)?;
let public_key: &[u8] = own_leaf.signature_key().as_slice();
SignatureKeyPair::read(
self.provider.storage(),
public_key,
group.ciphersuite().signature_algorithm(),
)
.ok_or(Error::CantLoadSigner)
}
pub(crate) fn load_mls_group(&self, group_id: &GroupId) -> Result<Option<MlsGroup>, Error> {
MlsGroup::load(self.provider.storage(), group_id)
.map_err(|e| Error::Provider(e.to_string()))
}
pub(crate) fn exporter_secret(
&self,
group_id: &GroupId,
) -> Result<group_types::GroupExporterSecret, Error> {
let group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
match self
.storage()
.get_group_exporter_secret(group_id, group.epoch().as_u64())
.map_err(|e| Error::Group(e.to_string()))?
{
Some(group_exporter_secret) => Ok(group_exporter_secret),
None => {
let export_secret: [u8; 32] = group
.export_secret(self.provider.crypto(), "nostr", b"nostr", 32)?
.try_into()
.map_err(|_| {
Error::Group("Failed to convert export secret to [u8; 32]".to_string())
})?;
let group_exporter_secret = group_types::GroupExporterSecret {
mls_group_id: group_id.clone(),
epoch: group.epoch().as_u64(),
secret: export_secret,
};
self.storage()
.save_group_exporter_secret(group_exporter_secret.clone())
.map_err(|e| Error::Group(e.to_string()))?;
Ok(group_exporter_secret)
}
}
}
pub fn get_group(&self, group_id: &GroupId) -> Result<Option<group_types::Group>, Error> {
self.storage()
.find_group_by_mls_group_id(group_id)
.map_err(|e| Error::Group(e.to_string()))
}
pub fn get_groups(&self) -> Result<Vec<group_types::Group>, Error> {
self.storage()
.all_groups()
.map_err(|e| Error::Group(e.to_string()))
}
pub fn get_members(&self, group_id: &GroupId) -> Result<BTreeSet<PublicKey>, Error> {
let group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let mut members = group.members();
members.try_fold(BTreeSet::new(), |mut acc, m| {
let credentials: BasicCredential = BasicCredential::try_from(m.credential)?;
let hex_bytes: &[u8] = credentials.identity();
let hex_str: &str = str::from_utf8(hex_bytes)?;
let public_key = PublicKey::from_hex(hex_str)?;
acc.insert(public_key);
Ok(acc)
})
}
pub(crate) fn pending_added_members_pubkeys(
&self,
group_id: &GroupId,
) -> Result<Vec<PublicKey>, Error> {
let mls_group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let mut added_pubkeys = Vec::new();
let pending_proposals = mls_group.pending_proposals();
for proposal in pending_proposals {
if let Proposal::Add(add_proposal) = proposal.proposal() {
let leaf_node = add_proposal.key_package().leaf_node();
let pubkey = self.pubkey_for_leaf_node(leaf_node)?;
added_pubkeys.push(pubkey);
}
}
Ok(added_pubkeys)
}
pub fn add_members(
&self,
group_id: &GroupId,
key_package_events: &[Event],
) -> Result<UpdateGroupResult, Error> {
let mut mls_group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let mls_signer: SignatureKeyPair = self.load_mls_signer(&mls_group)?;
let own_leaf = mls_group.own_leaf().ok_or(Error::OwnLeafNotFound)?;
if !self.is_leaf_node_admin(mls_group.group_id(), own_leaf)? {
return Err(Error::Group(
"Only group admins can add members".to_string(),
));
}
let mut key_packages_vec: Vec<KeyPackage> = Vec::new();
for event in key_package_events {
let key_package: KeyPackage = self.parse_key_package(event)?;
key_packages_vec.push(key_package);
}
let (commit_message, welcome_message, _group_info) = mls_group
.add_members(&self.provider, &mls_signer, &key_packages_vec)
.map_err(|e| Error::Group(e.to_string()))?;
let serialized_commit_message = commit_message
.tls_serialize_detached()
.map_err(|e| Error::Group(e.to_string()))?;
let commit_event =
self.build_encrypted_message_event(mls_group.group_id(), serialized_commit_message)?;
let processed_message: message_types::ProcessedMessage = message_types::ProcessedMessage {
wrapper_event_id: commit_event.id,
message_event_id: None,
processed_at: Timestamp::now(),
state: message_types::ProcessedMessageState::ProcessedCommit,
failure_reason: None,
};
self.storage()
.save_processed_message(processed_message)
.map_err(|e| Error::Message(e.to_string()))?;
let serialized_welcome_message = welcome_message
.tls_serialize_detached()
.map_err(|e| Error::Group(e.to_string()))?;
let group_relays = self
.get_relays(mls_group.group_id())?
.into_iter()
.collect::<Vec<_>>();
let welcome_rumors = self.build_welcome_rumors_for_key_packages(
&mls_group,
serialized_welcome_message,
key_package_events.to_vec(),
&group_relays,
)?;
Ok(UpdateGroupResult {
evolution_event: commit_event,
welcome_rumors, })
}
pub fn remove_members(
&self,
group_id: &GroupId,
pubkeys: &[PublicKey],
) -> Result<UpdateGroupResult, Error> {
let mut mls_group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let signer: SignatureKeyPair = self.load_mls_signer(&mls_group)?;
let own_leaf = mls_group.own_leaf().ok_or(Error::OwnLeafNotFound)?;
if !self.is_leaf_node_admin(group_id, own_leaf)? {
return Err(Error::Group(
"Only group admins can remove members".to_string(),
));
}
let mut leaf_indices = Vec::new();
let members = mls_group.members();
for (index, member) in members.enumerate() {
let pubkey = self.pubkey_for_member(&member)?;
if pubkeys.contains(&pubkey) {
leaf_indices.push(LeafNodeIndex::new(index as u32));
}
}
if leaf_indices.is_empty() {
return Err(Error::Group(
"No matching members found to remove".to_string(),
));
}
let (commit_message, welcome_option, _group_info) = mls_group
.remove_members(&self.provider, &signer, &leaf_indices)
.map_err(|e| Error::Group(e.to_string()))?;
let serialized_commit_message = commit_message
.tls_serialize_detached()
.map_err(|e| Error::Group(e.to_string()))?;
let commit_event =
self.build_encrypted_message_event(mls_group.group_id(), serialized_commit_message)?;
let processed_message: message_types::ProcessedMessage = message_types::ProcessedMessage {
wrapper_event_id: commit_event.id,
message_event_id: None,
processed_at: Timestamp::now(),
state: message_types::ProcessedMessageState::ProcessedCommit,
failure_reason: None,
};
self.storage()
.save_processed_message(processed_message)
.map_err(|e| Error::Message(e.to_string()))?;
if welcome_option.is_some() {
return Err(Error::Group(
"Found welcomes when removing users".to_string(),
));
}
Ok(UpdateGroupResult {
evolution_event: commit_event,
welcome_rumors: None, })
}
fn update_group_data_extension(
&self,
mls_group: &mut MlsGroup,
group_id: &GroupId,
group_data: &NostrGroupDataExtension,
) -> Result<UpdateGroupResult, Error> {
let own_leaf = mls_group.own_leaf().ok_or(Error::OwnLeafNotFound)?;
if !self.is_leaf_node_admin(group_id, own_leaf)? {
return Err(Error::Group(
"Only group admins can update group context extensions".to_string(),
));
}
let extension = Self::get_unknown_extension_from_group_data(group_data)?;
let mut extensions = mls_group.extensions().clone();
extensions.add_or_replace(extension);
let signature_keypair = self.load_mls_signer(mls_group)?;
let (message_out, _, _) = mls_group.update_group_context_extensions(
&self.provider,
extensions,
&signature_keypair,
)?;
let commit_event = self.build_encrypted_message_event(
mls_group.group_id(),
message_out.tls_serialize_detached()?,
)?;
let processed_message: message_types::ProcessedMessage = message_types::ProcessedMessage {
wrapper_event_id: commit_event.id,
message_event_id: None,
processed_at: Timestamp::now(),
state: message_types::ProcessedMessageState::ProcessedCommit,
failure_reason: None,
};
self.storage()
.save_processed_message(processed_message)
.map_err(|e| Error::Message(e.to_string()))?;
Ok(UpdateGroupResult {
evolution_event: commit_event,
welcome_rumors: None,
})
}
pub fn update_group_data(
&self,
group_id: &GroupId,
update: NostrGroupDataUpdate,
) -> Result<UpdateGroupResult, Error> {
let mut mls_group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let mut group_data = NostrGroupDataExtension::from_group(&mls_group)?;
if let Some(name) = update.name {
group_data.name = name;
}
if let Some(description) = update.description {
group_data.description = description;
}
if let Some(image_url) = update.image_url {
group_data.image_url = image_url;
}
if let Some(image_key) = update.image_key {
group_data.image_key = image_key;
}
if let Some(image_nonce) = update.image_nonce {
group_data.image_nonce = image_nonce;
}
if let Some(relays) = update.relays {
group_data.relays = relays.into_iter().collect();
}
if let Some(admins) = update.admins {
group_data.admins = admins.into_iter().collect();
}
self.update_group_data_extension(&mut mls_group, group_id, &group_data)
}
pub fn get_relays(&self, group_id: &GroupId) -> Result<BTreeSet<RelayUrl>, Error> {
let relays = self
.storage()
.group_relays(group_id)
.map_err(|e| Error::Group(e.to_string()))?;
Ok(relays.into_iter().map(|r| r.relay_url).collect())
}
fn get_unknown_extension_from_group_data(
group_data: &NostrGroupDataExtension,
) -> Result<Extension, Error> {
let serialized_group_data = group_data.as_raw().tls_serialize_detached()?;
Ok(Extension::Unknown(
group_data.extension_type(),
UnknownExtension(serialized_group_data),
))
}
pub fn create_group(
&self,
creator_public_key: &PublicKey,
member_key_package_events: Vec<Event>,
config: NostrGroupConfigData,
) -> Result<GroupResult, Error> {
let member_pubkeys = member_key_package_events
.clone()
.into_iter()
.map(|e| e.pubkey)
.collect::<Vec<PublicKey>>();
let admins = config.admins.clone();
self.validate_group_members(creator_public_key, &member_pubkeys, &admins)?;
let (credential, signer) = self.generate_credential_with_key(creator_public_key)?;
tracing::debug!(
target: "nostr_mls::groups::create_mls_group",
"Credential and signer created, {:?}",
credential
);
let group_data = NostrGroupDataExtension::new(
config.name,
config.description,
admins,
config.relays.clone(),
config.image_url.clone(),
config.image_key.clone(),
config.image_nonce.clone(),
);
tracing::debug!(
target: "nostr_mls::groups::create_mls_group",
"Group data created, {:?}",
group_data
);
let extension = Self::get_unknown_extension_from_group_data(&group_data)?;
let required_capabilities_extension = self.required_capabilities_extension();
let extensions = Extensions::from_vec(vec![extension, required_capabilities_extension])?;
tracing::debug!(
target: "nostr_mls::groups::create_mls_group",
"Group config extensions created, {:?}",
extensions
);
let capabilities = self.capabilities();
let group_config = MlsGroupCreateConfig::builder()
.ciphersuite(self.ciphersuite)
.use_ratchet_tree_extension(true)
.capabilities(capabilities)
.with_group_context_extensions(extensions)?
.build();
tracing::debug!(
target: "nostr_mls::groups::create_mls_group",
"Group config built, {:?}",
group_config
);
let mut mls_group =
MlsGroup::new(&self.provider, &signer, &group_config, credential.clone())?;
let mut key_packages_vec: Vec<KeyPackage> = Vec::new();
for event in &member_key_package_events {
let key_package: KeyPackage = self.parse_key_package(event)?;
key_packages_vec.push(key_package);
}
let (_, welcome_out, _group_info) =
mls_group.add_members(&self.provider, &signer, &key_packages_vec)?;
mls_group.merge_pending_commit(&self.provider)?;
let serialized_welcome_message = welcome_out.tls_serialize_detached()?;
let welcome_rumors = self
.build_welcome_rumors_for_key_packages(
&mls_group,
serialized_welcome_message,
member_key_package_events,
&config.relays,
)?
.ok_or(Error::Welcome("Error creating welcome rumors".to_string()))?;
let group = group_types::Group {
mls_group_id: mls_group.group_id().clone(),
nostr_group_id: group_data.clone().nostr_group_id,
name: group_data.clone().name,
description: group_data.clone().description,
admin_pubkeys: group_data.clone().admins,
last_message_id: None,
last_message_at: None,
epoch: mls_group.epoch().as_u64(),
state: group_types::GroupState::Active,
image_url: config.image_url,
image_key: config.image_key,
image_nonce: config.image_nonce,
};
self.storage().save_group(group.clone()).map_err(
|e: nrc_mls_storage::groups::error::GroupError| Error::Group(e.to_string()),
)?;
self.storage()
.replace_group_relays(&group.mls_group_id, config.relays.into_iter().collect())
.map_err(|e| Error::Group(e.to_string()))?;
Ok(GroupResult {
group,
welcome_rumors,
})
}
pub fn self_update(&self, group_id: &GroupId) -> Result<UpdateGroupResult, Error> {
let mut mls_group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let current_secret: group_types::GroupExporterSecret = self
.storage()
.get_group_exporter_secret(group_id, mls_group.epoch().as_u64())
.map_err(|e| Error::Group(e.to_string()))?
.ok_or(Error::GroupExporterSecretNotFound)?;
tracing::debug!(target: "nostr_openmls::groups::self_update", "Current epoch: {:?}", current_secret.epoch);
let current_signer: SignatureKeyPair = self.load_mls_signer(&mls_group)?;
let own_leaf = mls_group.own_leaf().ok_or(Error::OwnLeafNotFound)?;
let new_signature_keypair = SignatureKeyPair::new(self.ciphersuite.signature_algorithm())?;
new_signature_keypair
.store(self.provider.storage())
.map_err(|e| Error::Provider(e.to_string()))?;
let pubkey = BasicCredential::try_from(own_leaf.credential().clone())?
.identity()
.to_vec();
let new_credential: BasicCredential = BasicCredential::new(pubkey);
let new_credential_with_key = CredentialWithKey {
credential: new_credential.into(),
signature_key: new_signature_keypair.public().into(),
};
let new_signer_bundle = NewSignerBundle {
signer: &new_signature_keypair,
credential_with_key: new_credential_with_key.clone(),
};
let leaf_node_params = LeafNodeParameters::builder()
.with_credential_with_key(new_credential_with_key)
.with_capabilities(own_leaf.capabilities().clone())
.with_extensions(own_leaf.extensions().clone())
.build();
let commit_message_bundle = mls_group.self_update_with_new_signer(
&self.provider,
¤t_signer,
new_signer_bundle,
leaf_node_params,
)?;
let serialized_commit_message = commit_message_bundle.commit().tls_serialize_detached()?;
let commit_event =
self.build_encrypted_message_event(mls_group.group_id(), serialized_commit_message)?;
let processed_message: message_types::ProcessedMessage = message_types::ProcessedMessage {
wrapper_event_id: commit_event.id,
message_event_id: None,
processed_at: Timestamp::now(),
state: message_types::ProcessedMessageState::ProcessedCommit,
failure_reason: None,
};
self.storage()
.save_processed_message(processed_message)
.map_err(|e| Error::Message(e.to_string()))?;
let serialized_welcome_message = commit_message_bundle
.welcome()
.map(|w| {
w.tls_serialize_detached()
.map_err(|e| Error::Group(e.to_string()))
})
.transpose()?;
if serialized_welcome_message.is_some() {
return Err(Error::Group(
"Found welcomes when performing a self update".to_string(),
));
}
Ok(UpdateGroupResult {
evolution_event: commit_event,
welcome_rumors: None, })
}
pub fn leave_group(&self, group_id: &GroupId) -> Result<UpdateGroupResult, Error> {
let mut group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let signer: SignatureKeyPair = self.load_mls_signer(&group)?;
let leave_message = group
.leave_group(&self.provider, &signer)
.map_err(|e| Error::Group(e.to_string()))?;
let serialized_message_out = leave_message
.tls_serialize_detached()
.map_err(|e| Error::Group(e.to_string()))?;
let evolution_event =
self.build_encrypted_message_event(group.group_id(), serialized_message_out)?;
let processed_message: message_types::ProcessedMessage = message_types::ProcessedMessage {
wrapper_event_id: evolution_event.id,
message_event_id: None,
processed_at: Timestamp::now(),
state: message_types::ProcessedMessageState::ProcessedCommit,
failure_reason: None,
};
self.storage()
.save_processed_message(processed_message)
.map_err(|e| Error::Message(e.to_string()))?;
Ok(UpdateGroupResult {
evolution_event,
welcome_rumors: None,
})
}
pub fn merge_pending_commit(&self, group_id: &GroupId) -> Result<(), Error> {
let mut mls_group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
mls_group.merge_pending_commit(&self.provider)?;
self.sync_group_metadata_from_mls(group_id)?;
Ok(())
}
pub fn sync_group_metadata_from_mls(&self, group_id: &GroupId) -> Result<(), Error> {
let mls_group = self.load_mls_group(group_id)?.ok_or(Error::GroupNotFound)?;
let mut stored_group = self.get_group(group_id)?.ok_or(Error::GroupNotFound)?;
stored_group.epoch = mls_group.epoch().as_u64();
if let Ok(group_data) = NostrGroupDataExtension::from_group(&mls_group) {
stored_group.name = group_data.name;
stored_group.description = group_data.description;
stored_group.image_url = group_data.image_url;
stored_group.image_key = group_data.image_key;
stored_group.admin_pubkeys = group_data.admins;
stored_group.nostr_group_id = group_data.nostr_group_id;
self.storage()
.replace_group_relays(group_id, group_data.relays)
.map_err(|e| Error::Group(e.to_string()))?;
}
self.storage()
.save_group(stored_group)
.map_err(|e| Error::Group(e.to_string()))?;
Ok(())
}
fn validate_group_members(
&self,
creator_pubkey: &PublicKey,
member_pubkeys: &[PublicKey],
admin_pubkeys: &[PublicKey],
) -> Result<bool, Error> {
if !admin_pubkeys.contains(creator_pubkey) {
return Err(Error::Group("Creator must be an admin".to_string()));
}
if member_pubkeys.contains(creator_pubkey) {
return Err(Error::Group(
"Creator must not be included as a member".to_string(),
));
}
for pubkey in admin_pubkeys.iter() {
if !member_pubkeys.contains(pubkey) && creator_pubkey != pubkey {
return Err(Error::Group("Admin must be a member".to_string()));
}
}
Ok(true)
}
pub(crate) fn build_encrypted_message_event(
&self,
group_id: &GroupId,
serialized_content: Vec<u8>,
) -> Result<Event, Error> {
let group = self.get_group(group_id)?.ok_or(Error::GroupNotFound)?;
let secret: group_types::GroupExporterSecret = self.exporter_secret(group_id)?;
let secret_key: SecretKey = SecretKey::from_slice(&secret.secret)?;
let export_nostr_keys: Keys = Keys::new(secret_key);
let encrypted_content: String = nip44::encrypt(
export_nostr_keys.secret_key(),
&export_nostr_keys.public_key,
&serialized_content,
nip44::Version::default(),
)?;
let ephemeral_nostr_keys: Keys = Keys::generate();
let tag: Tag = Tag::custom(TagKind::h(), [hex::encode(group.nostr_group_id)]);
let event = EventBuilder::new(Kind::MlsGroupMessage, encrypted_content)
.tag(tag)
.sign_with_keys(&ephemeral_nostr_keys)?;
Ok(event)
}
pub(crate) fn build_welcome_rumors_for_key_packages(
&self,
group: &MlsGroup,
serialized_welcome: Vec<u8>,
key_package_events: Vec<Event>,
group_relays: &[RelayUrl],
) -> Result<Option<Vec<UnsignedEvent>>, Error> {
let committer_pubkey = self.get_own_pubkey(group)?;
let mut welcome_rumors_vec = Vec::new();
for event in key_package_events {
let welcome_rumor =
EventBuilder::new(Kind::MlsWelcome, hex::encode(&serialized_welcome))
.tags(vec![
Tag::from_standardized(TagStandard::Relays(group_relays.to_vec())),
Tag::event(event.id),
])
.build(committer_pubkey);
welcome_rumors_vec.push(welcome_rumor);
}
let welcome_rumors = if !welcome_rumors_vec.is_empty() {
Some(welcome_rumors_vec)
} else {
None
};
Ok(welcome_rumors)
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use nostr::{Keys, PublicKey};
use nostr_mls_memory_storage::NostrMlsMemoryStorage;
use nrc_mls_storage::messages::{types as message_types, MessageStorage};
use openmls::group::GroupId;
use openmls::prelude::BasicCredential;
use super::NostrGroupDataExtension;
use crate::groups::NostrGroupDataUpdate;
use crate::test_util::*;
use crate::tests::create_test_nostr_mls;
#[test]
fn test_validate_group_members() {
let nostr_mls = create_test_nostr_mls();
let (creator, members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let member_pks: Vec<PublicKey> = members.iter().map(|k| k.public_key()).collect();
assert!(nostr_mls
.validate_group_members(&creator_pk, &member_pks, &admins)
.is_ok());
let bad_admins = vec![member_pks[0]];
assert!(nostr_mls
.validate_group_members(&creator_pk, &member_pks, &bad_admins)
.is_err());
let bad_members = vec![creator_pk, member_pks[0]];
assert!(nostr_mls
.validate_group_members(&creator_pk, &bad_members, &admins)
.is_err());
let non_member = Keys::generate().public_key();
let bad_admins = vec![creator_pk, non_member];
assert!(nostr_mls
.validate_group_members(&creator_pk, &member_pks, &bad_admins)
.is_err());
}
#[test]
fn test_create_group_basic() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let members = creator_nostr_mls
.get_members(group_id)
.expect("Failed to get members");
assert_eq!(members.len(), 3); assert!(members.contains(&creator_pk));
for member_keys in &initial_members {
assert!(members.contains(&member_keys.public_key()));
}
}
#[test]
fn test_get_members() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let members = creator_nostr_mls
.get_members(group_id)
.expect("Failed to get members");
assert_eq!(members.len(), 3); assert!(members.contains(&creator_pk));
for member_keys in &initial_members {
assert!(members.contains(&member_keys.public_key()));
}
}
#[test]
fn test_add_members_epoch_advancement() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let initial_group = creator_nostr_mls
.get_group(group_id)
.expect("Failed to get group")
.expect("Group should exist");
let initial_epoch = initial_group.epoch;
let new_member = Keys::generate();
let new_key_package_event = create_key_package_event(&creator_nostr_mls, &new_member);
let _add_result = creator_nostr_mls
.add_members(group_id, &[new_key_package_event])
.expect("Failed to add member");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for member addition");
let mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let final_mls_epoch = mls_group.epoch().as_u64();
assert!(
final_mls_epoch > initial_epoch,
"MLS group epoch should advance after adding members (initial: {}, final: {})",
initial_epoch,
final_mls_epoch
);
let final_members = creator_nostr_mls
.get_members(group_id)
.expect("Failed to get members");
assert!(
final_members.contains(&new_member.public_key()),
"New member should be in the group"
);
assert_eq!(
final_members.len(),
4, "Should have 4 total members"
);
}
#[test]
fn test_get_own_pubkey() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let own_pubkey = creator_nostr_mls
.get_own_pubkey(&mls_group)
.expect("Failed to get own pubkey");
assert_eq!(
own_pubkey, creator_pk,
"Own pubkey should match creator pubkey"
);
}
#[test]
fn test_admin_check() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let stored_group = creator_nostr_mls
.get_group(group_id)
.expect("Failed to get group")
.expect("Group should exist");
assert!(
stored_group.admin_pubkeys.contains(&creator_pk),
"Creator should be admin"
);
}
#[test]
fn test_admin_permission_checks() {
let admin_nostr_mls = create_test_nostr_mls();
let non_admin_nostr_mls = create_test_nostr_mls();
let admin_keys = Keys::generate();
let non_admin_keys = Keys::generate();
let member1_keys = Keys::generate();
let admin_pk = admin_keys.public_key();
let _non_admin_pk = non_admin_keys.public_key();
let member1_pk = member1_keys.public_key();
let non_admin_event = create_key_package_event(&admin_nostr_mls, &non_admin_keys);
let member1_event = create_key_package_event(&admin_nostr_mls, &member1_keys);
let create_result = admin_nostr_mls
.create_group(
&admin_pk,
vec![non_admin_event.clone(), member1_event.clone()],
create_nostr_group_config_data(vec![admin_pk]), )
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
admin_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let new_member_keys = Keys::generate();
let _new_member_pk = new_member_keys.public_key();
let new_member_event = create_key_package_event(&non_admin_nostr_mls, &new_member_keys);
let add_result = admin_nostr_mls.add_members(group_id, &[new_member_event]);
assert!(add_result.is_ok(), "Admin should be able to add members");
admin_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for member addition");
let remove_result = admin_nostr_mls.remove_members(group_id, &[member1_pk]);
assert!(
remove_result.is_ok(),
"Admin should be able to remove members"
);
}
#[test]
fn test_pubkey_for_member() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let members: Vec<_> = mls_group.members().collect();
let mut found_pubkeys = Vec::new();
for member in &members {
let pubkey = creator_nostr_mls
.pubkey_for_member(member)
.expect("Failed to get pubkey for member");
found_pubkeys.push(pubkey);
}
assert!(
found_pubkeys.contains(&creator_pk),
"Should find creator pubkey"
);
for member_keys in &initial_members {
assert!(
found_pubkeys.contains(&member_keys.public_key()),
"Should find member pubkey: {:?}",
member_keys.public_key()
);
}
assert_eq!(found_pubkeys.len(), 3, "Should have 3 members total");
}
#[test]
fn test_remove_members_group_not_found() {
use openmls::group::GroupId;
let nostr_mls = create_test_nostr_mls();
let non_existent_group_id = GroupId::from_slice(&[1, 2, 3, 4, 5]);
let dummy_pubkey = Keys::generate().public_key();
let result = nostr_mls.remove_members(&non_existent_group_id, &[dummy_pubkey]);
assert!(
matches!(result, Err(crate::Error::GroupNotFound)),
"Should return GroupNotFound error for non-existent group"
);
}
#[test]
fn test_remove_members_no_matching_members() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let non_member = Keys::generate().public_key();
let result = creator_nostr_mls.remove_members(group_id, &[non_member]);
assert!(
matches!(
result,
Err(crate::Error::Group(ref msg)) if msg.contains("No matching members found")
),
"Should return error when no matching members found"
);
}
#[test]
fn test_remove_members_epoch_advancement() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let initial_group = creator_nostr_mls
.get_group(group_id)
.expect("Failed to get group")
.expect("Group should exist");
let initial_epoch = initial_group.epoch;
let member_to_remove = initial_members[0].public_key();
let _remove_result = creator_nostr_mls
.remove_members(group_id, &[member_to_remove])
.expect("Failed to remove member");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for member removal");
let mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let final_mls_epoch = mls_group.epoch().as_u64();
assert!(
final_mls_epoch > initial_epoch,
"MLS group epoch should advance after removing members (initial: {}, final: {})",
initial_epoch,
final_mls_epoch
);
let final_members = creator_nostr_mls
.get_members(group_id)
.expect("Failed to get members");
assert!(
!final_members.contains(&member_to_remove),
"Removed member should not be in the group"
);
assert_eq!(
final_members.len(),
2, "Should have 2 total members after removal"
);
}
#[test]
fn test_self_update_success() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let initial_members_set = creator_nostr_mls
.get_members(group_id)
.expect("Failed to get initial members");
assert_eq!(initial_members_set.len(), 3);
let initial_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let initial_epoch = initial_mls_group.epoch().as_u64();
let _initial_secret = creator_nostr_mls
.exporter_secret(group_id)
.expect("Failed to get initial exporter secret");
let update_result = creator_nostr_mls
.self_update(group_id)
.expect("Failed to perform self update");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for self update");
assert!(
!update_result.evolution_event.content.is_empty(),
"Evolution event should not be empty"
);
let final_members = creator_nostr_mls
.get_members(group_id)
.expect("Failed to get final members");
assert_eq!(
final_members.len(),
3,
"Member count should remain the same after self update"
);
assert!(
final_members.contains(&creator_pk),
"Creator should still be in group"
);
for initial_member_keys in &initial_members {
assert!(
final_members.contains(&initial_member_keys.public_key()),
"Initial member should still be in group"
);
}
let final_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let final_epoch = final_mls_group.epoch().as_u64();
assert!(
final_epoch > initial_epoch,
"Epoch should advance after self update (initial: {}, final: {})",
initial_epoch,
final_epoch
);
}
#[test]
fn test_self_update_group_not_found() {
use openmls::group::GroupId;
let nostr_mls = create_test_nostr_mls();
let non_existent_group_id = GroupId::from_slice(&[1, 2, 3, 4, 5]);
let result = nostr_mls.self_update(&non_existent_group_id);
assert!(
matches!(result, Err(crate::Error::GroupNotFound)),
"Should return GroupNotFound error for non-existent group"
);
}
#[test]
fn test_self_update_key_rotation() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let initial_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let initial_own_leaf = initial_mls_group
.own_leaf()
.expect("Failed to get initial own leaf");
let initial_signature_key = initial_own_leaf.signature_key().as_slice().to_vec();
let _initial_secret = creator_nostr_mls
.exporter_secret(group_id)
.expect("Failed to get initial exporter secret");
let _update_result = creator_nostr_mls
.self_update(group_id)
.expect("Failed to perform self update");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for self update");
let final_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let final_own_leaf = final_mls_group
.own_leaf()
.expect("Failed to get final own leaf");
let final_signature_key = final_own_leaf.signature_key().as_slice().to_vec();
assert_ne!(
initial_signature_key, final_signature_key,
"Signature key should be different after self update"
);
let initial_credential = BasicCredential::try_from(initial_own_leaf.credential().clone())
.expect("Failed to extract initial credential");
let final_credential = BasicCredential::try_from(final_own_leaf.credential().clone())
.expect("Failed to extract final credential");
assert_eq!(
initial_credential.identity(),
final_credential.identity(),
"Public key identity should remain the same after self update"
);
}
#[test]
fn test_self_update_exporter_secret_rotation() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let initial_secret = creator_nostr_mls
.exporter_secret(group_id)
.expect("Failed to get initial exporter secret");
let _update_result = creator_nostr_mls
.self_update(group_id)
.expect("Failed to perform self update");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for self update");
let final_secret = creator_nostr_mls
.exporter_secret(group_id)
.expect("Failed to get final exporter secret");
assert_ne!(
initial_secret.secret, final_secret.secret,
"Exporter secret should be different after self update"
);
assert!(
final_secret.epoch > initial_secret.epoch,
"Epoch should advance after self update (initial: {}, final: {})",
initial_secret.epoch,
final_secret.epoch
);
assert_eq!(
initial_secret.mls_group_id, final_secret.mls_group_id,
"Group ID should remain the same"
);
}
#[test]
fn test_update_group_data() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let initial_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let initial_group_data = NostrGroupDataExtension::from_group(&initial_mls_group).unwrap();
let new_name = "Updated Name".to_string();
let update = NostrGroupDataUpdate::new().name(new_name.clone());
let update_result = creator_nostr_mls
.update_group_data(group_id, update)
.expect("Failed to update group name");
assert!(!update_result.evolution_event.content.is_empty());
assert!(update_result.welcome_rumors.is_none());
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let updated_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let updated_group_data = NostrGroupDataExtension::from_group(&updated_mls_group).unwrap();
assert_eq!(updated_group_data.name, new_name);
assert_eq!(
updated_group_data.description,
initial_group_data.description
);
assert_eq!(updated_group_data.image_url, initial_group_data.image_url);
let new_description = "Updated Description".to_string();
let new_image_url = "https://example.com/new-image.png".to_string();
let new_image_key = vec![1, 2, 3, 4, 5];
let update = NostrGroupDataUpdate::new()
.description(new_description.clone())
.image_url(Some(new_image_url.clone()))
.image_key(Some(new_image_key.clone()));
let update_result = creator_nostr_mls
.update_group_data(group_id, update)
.expect("Failed to update multiple fields");
assert!(!update_result.evolution_event.content.is_empty());
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let final_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let final_group_data = NostrGroupDataExtension::from_group(&final_mls_group).unwrap();
assert_eq!(final_group_data.name, new_name); assert_eq!(final_group_data.description, new_description);
assert_eq!(final_group_data.image_url, Some(new_image_url));
assert_eq!(final_group_data.image_key, Some(new_image_key));
let update = NostrGroupDataUpdate::new()
.image_url::<String>(None)
.image_key(None);
let update_result = creator_nostr_mls
.update_group_data(group_id, update)
.expect("Failed to clear optional fields");
assert!(!update_result.evolution_event.content.is_empty());
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let cleared_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let cleared_group_data = NostrGroupDataExtension::from_group(&cleared_mls_group).unwrap();
assert_eq!(cleared_group_data.name, new_name);
assert_eq!(cleared_group_data.description, new_description);
assert_eq!(cleared_group_data.image_url, None);
assert_eq!(cleared_group_data.image_key, None);
let empty_update = NostrGroupDataUpdate::new();
let update_result = creator_nostr_mls
.update_group_data(group_id, empty_update)
.expect("Failed to apply empty update");
assert!(!update_result.evolution_event.content.is_empty());
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let unchanged_mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let unchanged_group_data =
NostrGroupDataExtension::from_group(&unchanged_mls_group).unwrap();
assert_eq!(unchanged_group_data.name, cleared_group_data.name);
assert_eq!(
unchanged_group_data.description,
cleared_group_data.description
);
assert_eq!(unchanged_group_data.image_url, cleared_group_data.image_url);
assert_eq!(unchanged_group_data.image_key, cleared_group_data.image_key);
}
#[test]
fn test_sync_group_metadata_from_mls() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins.clone()),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let initial_stored_group = creator_nostr_mls
.get_group(group_id)
.expect("Failed to get initial stored group")
.expect("Stored group should exist");
let mut mls_group = creator_nostr_mls
.load_mls_group(group_id)
.expect("Failed to load MLS group")
.expect("MLS group should exist");
let mut new_group_data = NostrGroupDataExtension::from_group(&mls_group).unwrap();
new_group_data.name = "Synchronized Name".to_string();
new_group_data.description = "Synchronized Description".to_string();
let extension =
super::NostrMls::<NostrMlsMemoryStorage>::get_unknown_extension_from_group_data(
&new_group_data,
)
.unwrap();
let mut extensions = mls_group.extensions().clone();
extensions.add_or_replace(extension);
let signature_keypair = creator_nostr_mls.load_mls_signer(&mls_group).unwrap();
let (_message_out, _, _) = mls_group
.update_group_context_extensions(
&creator_nostr_mls.provider,
extensions,
&signature_keypair,
)
.unwrap();
mls_group
.merge_pending_commit(&creator_nostr_mls.provider)
.unwrap();
let stale_stored_group = creator_nostr_mls
.get_group(group_id)
.expect("Failed to get stale stored group")
.expect("Stored group should exist");
assert_eq!(stale_stored_group.name, initial_stored_group.name);
assert_eq!(
stale_stored_group.description,
initial_stored_group.description
);
assert_eq!(stale_stored_group.epoch, initial_stored_group.epoch);
creator_nostr_mls
.sync_group_metadata_from_mls(group_id)
.expect("Failed to sync group metadata");
let synced_stored_group = creator_nostr_mls
.get_group(group_id)
.expect("Failed to get synced stored group")
.expect("Stored group should exist");
assert_eq!(synced_stored_group.name, "Synchronized Name");
assert_eq!(synced_stored_group.description, "Synchronized Description");
assert!(synced_stored_group.epoch > initial_stored_group.epoch);
assert_eq!(
synced_stored_group.admin_pubkeys,
admins.into_iter().collect::<BTreeSet<_>>()
);
assert_eq!(
synced_stored_group.mls_group_id,
initial_stored_group.mls_group_id
);
assert_eq!(
synced_stored_group.last_message_id,
initial_stored_group.last_message_id
);
assert_eq!(
synced_stored_group.last_message_at,
initial_stored_group.last_message_at
);
assert_eq!(synced_stored_group.state, initial_stored_group.state);
}
#[test]
fn test_extension_updates_create_processed_messages() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit");
let test_cases = vec![
("update_group_name", "New Name"),
("update_group_description", "New Description"),
];
for (operation, _value) in test_cases {
let update_result = match operation {
"update_group_name" => {
let update = NostrGroupDataUpdate::new().name("New Name".to_string());
creator_nostr_mls.update_group_data(group_id, update)
}
"update_group_description" => {
let update =
NostrGroupDataUpdate::new().description("New Description".to_string());
creator_nostr_mls.update_group_data(group_id, update)
}
_ => panic!("Unknown operation"),
};
let update_result = update_result.unwrap_or_else(|_| panic!("Failed to {}", operation));
let commit_event_id = update_result.evolution_event.id;
let processed_message = creator_nostr_mls
.storage()
.find_processed_message_by_event_id(&commit_event_id)
.expect("Failed to query processed message")
.expect("ProcessedMessage should exist");
assert_eq!(processed_message.wrapper_event_id, commit_event_id);
assert_eq!(processed_message.message_event_id, None);
assert_eq!(
processed_message.state,
message_types::ProcessedMessageState::ProcessedCommit
);
assert_eq!(processed_message.failure_reason, None);
creator_nostr_mls
.merge_pending_commit(group_id)
.unwrap_or_else(|_| panic!("Failed to merge pending commit for {}", operation));
}
}
#[test]
fn test_stored_group_sync_after_all_operations() {
let creator_nostr_mls = create_test_nostr_mls();
let (creator, initial_members, admins) = create_test_group_members();
let creator_pk = creator.public_key();
let mut initial_key_package_events = Vec::new();
for member_keys in &initial_members {
let key_package_event = create_key_package_event(&creator_nostr_mls, member_keys);
initial_key_package_events.push(key_package_event);
}
let create_result = creator_nostr_mls
.create_group(
&creator_pk,
initial_key_package_events,
create_nostr_group_config_data(admins),
)
.expect("Failed to create group");
let group_id = &create_result.group.mls_group_id;
let verify_epoch_sync = || {
let mls_group = creator_nostr_mls.load_mls_group(group_id).unwrap().unwrap();
let stored_group = creator_nostr_mls.get_group(group_id).unwrap().unwrap();
assert_eq!(
stored_group.epoch,
mls_group.epoch().as_u64(),
"Stored group epoch should match MLS group epoch"
);
};
verify_epoch_sync();
let new_member = Keys::generate();
let new_key_package_event = create_key_package_event(&creator_nostr_mls, &new_member);
let _add_result = creator_nostr_mls
.add_members(group_id, &[new_key_package_event])
.expect("Failed to add member");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for add member");
verify_epoch_sync();
let _initial_secret = creator_nostr_mls
.exporter_secret(group_id)
.expect("Failed to get initial exporter secret");
let _self_update_result = creator_nostr_mls
.self_update(group_id)
.expect("Failed to perform self update");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for self update");
verify_epoch_sync();
let update = NostrGroupDataUpdate::new().name("Final Name".to_string());
let _name_result = creator_nostr_mls
.update_group_data(group_id, update)
.expect("Failed to update group name");
creator_nostr_mls
.merge_pending_commit(group_id)
.expect("Failed to merge pending commit for name update");
verify_epoch_sync();
let final_mls_group = creator_nostr_mls.load_mls_group(group_id).unwrap().unwrap();
let final_stored_group = creator_nostr_mls.get_group(group_id).unwrap().unwrap();
let final_group_data = NostrGroupDataExtension::from_group(&final_mls_group).unwrap();
assert_eq!(final_stored_group.name, final_group_data.name);
assert_eq!(final_stored_group.description, final_group_data.description);
assert_eq!(final_stored_group.admin_pubkeys, final_group_data.admins);
assert_eq!(
final_stored_group.nostr_group_id,
final_group_data.nostr_group_id
);
}
#[test]
fn test_sync_group_metadata_error_cases() {
let creator_nostr_mls = create_test_nostr_mls();
let non_existent_group_id = GroupId::from_slice(&[1, 2, 3, 4, 5]);
let result = creator_nostr_mls.sync_group_metadata_from_mls(&non_existent_group_id);
assert!(matches!(result, Err(crate::Error::GroupNotFound)));
}
}