use mdk_storage_traits::groups::types as group_types;
use mdk_storage_traits::messages::types as message_types;
use mdk_storage_traits::{GroupId, MdkStorageProvider};
use nostr::{Event, EventId, JsonUtil, Tag, Timestamp, UnsignedEvent};
use openmls::prelude::MlsGroup;
use openmls_basic_credential::SignatureKeyPair;
use tls_codec::Serialize as TlsSerialize;
use crate::MDK;
use crate::error::Error;
use super::Result;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventTag(Tag);
impl EventTag {
pub fn expiration(timestamp: Timestamp) -> Self {
Self(Tag::expiration(timestamp))
}
pub(crate) fn into_tag(self) -> Tag {
self.0
}
}
impl<Storage> MDK<Storage>
where
Storage: MdkStorageProvider,
{
pub(crate) fn create_mls_message_payload(
&self,
group: &mut MlsGroup,
rumor: &mut UnsignedEvent,
) -> Result<Vec<u8>> {
let signer: SignatureKeyPair = self.load_mls_signer(group)?;
rumor.ensure_id();
let json: String = rumor.as_json();
let message_out = group.create_message(&self.provider, &signer, json.as_bytes())?;
let serialized_message = message_out.tls_serialize_detached()?;
Ok(serialized_message)
}
pub fn create_message(
&self,
mls_group_id: &GroupId,
mut rumor: UnsignedEvent,
tags: Option<Vec<EventTag>>,
) -> Result<Event> {
let mut mls_group = self
.load_mls_group(mls_group_id)?
.ok_or(Error::GroupNotFound)?;
let mut group: group_types::Group = self
.get_group(mls_group_id)
.map_err(|_e| Error::Group("Storage error while getting group".to_string()))?
.ok_or(Error::GroupNotFound)?;
let message: Vec<u8> = self.create_mls_message_payload(&mut mls_group, &mut rumor)?;
let rumor_id: EventId = rumor.id();
let event = self.build_message_event(mls_group_id, message, tags)?;
let now = Timestamp::now();
let message: message_types::Message = message_types::Message {
id: rumor_id,
pubkey: rumor.pubkey,
kind: rumor.kind,
mls_group_id: mls_group_id.clone(),
created_at: rumor.created_at,
processed_at: now,
content: rumor.content.clone(),
tags: rumor.tags.clone(),
event: rumor.clone(),
wrapper_event_id: event.id,
state: message_types::MessageState::Created,
epoch: Some(mls_group.epoch().as_u64()),
};
let processed_message = super::create_processed_message_record(
event.id,
Some(rumor_id),
Some(mls_group.epoch().as_u64()),
Some(mls_group_id.clone()),
message_types::ProcessedMessageState::Created,
None,
);
self.save_message_record(message.clone())?;
self.save_processed_message_record(processed_message)?;
group.update_last_message_if_newer(&message);
self.save_group_record(group)?;
Ok(event)
}
}
#[cfg(test)]
mod tests {
use mdk_storage_traits::GroupId;
use mdk_storage_traits::messages::types as message_types;
use nostr::{Keys, Kind, TagKind, Timestamp};
use crate::error::Error;
use crate::messages::EventTag;
use crate::test_util::*;
use crate::tests::create_test_mdk;
#[test]
fn test_create_message_success() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let mut rumor = create_test_rumor(&creator, "Hello, world!");
let rumor_id = rumor.id();
let result = mdk.create_message(&group_id, rumor, None);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.kind, Kind::MlsGroupMessage);
let stored_message = mdk
.get_message(&group_id, &rumor_id)
.expect("Failed to get message")
.expect("Message should exist");
assert_eq!(stored_message.id, rumor_id);
assert_eq!(stored_message.content, "Hello, world!");
assert_eq!(stored_message.state, message_types::MessageState::Created);
assert_eq!(stored_message.wrapper_event_id, event.id);
}
#[test]
fn test_create_message_group_not_found() {
let mdk = create_test_mdk();
let creator = Keys::generate();
let rumor = create_test_rumor(&creator, "Hello, world!");
let non_existent_group_id = GroupId::from_slice(&[1, 2, 3, 4]);
let result = mdk.create_message(&non_existent_group_id, rumor, None);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::GroupNotFound));
}
#[test]
fn test_create_message_updates_group_metadata() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let initial_group = mdk
.get_group(&group_id)
.expect("Failed to get group")
.expect("Group should exist");
assert!(initial_group.last_message_at.is_none());
assert!(initial_group.last_message_id.is_none());
let mut rumor = create_test_rumor(&creator, "Hello, world!");
let rumor_id = rumor.id();
let rumor_timestamp = rumor.created_at;
let _event = mdk
.create_message(&group_id, rumor, None)
.expect("Failed to create message");
let updated_group = mdk
.get_group(&group_id)
.expect("Failed to get group")
.expect("Group should exist");
assert_eq!(updated_group.last_message_at, Some(rumor_timestamp));
assert_eq!(updated_group.last_message_id, Some(rumor_id));
}
#[test]
fn test_message_content_preservation() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let test_cases = vec![
"Simple text message",
"Message with emojis 🚀 🎉 ✨",
"Message with\nmultiple\nlines",
"Message with special chars: !@#$%^&*()",
"Minimal content",
];
for content in test_cases {
let mut rumor = create_test_rumor(&creator, content);
let rumor_id = rumor.id();
let _event = mdk
.create_message(&group_id, rumor, None)
.expect("Failed to create message");
let stored_message = mdk
.get_message(&group_id, &rumor_id)
.expect("Failed to get message")
.expect("Message should exist");
assert_eq!(stored_message.content, content);
assert_eq!(stored_message.pubkey, creator.public_key());
}
}
#[test]
fn test_create_message_ensures_rumor_id() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let rumor = create_test_rumor(&creator, "Test message");
let result = mdk.create_message(&group_id, rumor, None);
assert!(result.is_ok());
let event = result.unwrap();
let messages = mdk
.get_messages(&group_id, None)
.expect("Failed to get messages");
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].wrapper_event_id, event.id);
}
#[test]
fn test_group_message_event_structure_mip03_compliance() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let rumor = create_test_rumor(&creator, "Test message for MIP-03 compliance");
let message_event = mdk
.create_message(&group_id, rumor, None)
.expect("Failed to create message");
assert_eq!(
message_event.kind,
Kind::MlsGroupMessage,
"Message event must have kind 445 (MlsGroupMessage)"
);
assert!(
message_event.content.len() > 50,
"Encrypted content should be substantial (> 50 chars), got {}",
message_event.content.len()
);
assert_ne!(
message_event.content, "Test message for MIP-03 compliance",
"Content should be encrypted, not plaintext"
);
assert_eq!(
message_event.tags.len(),
2,
"Message event must have exactly 2 tags: h and encoding"
);
let group_id_tag = message_event
.tags
.iter()
.find(|t| t.kind() == TagKind::h())
.expect("Message event should have h tag");
assert_eq!(
group_id_tag.kind(),
TagKind::h(),
"Tag must be 'h' (group ID) tag"
);
let encoding_tag = message_event
.tags
.iter()
.find(|t| t.kind() == TagKind::Custom("encoding".into()))
.expect("Message event should have encoding tag");
assert_eq!(
encoding_tag
.content()
.expect("encoding tag should have content"),
"base64"
);
let group_id_hex = group_id_tag.content().expect("h tag should have content");
assert_eq!(
group_id_hex.len(),
64,
"Group ID should be 32 bytes (64 hex chars), got {}",
group_id_hex.len()
);
let group_id_bytes = hex::decode(group_id_hex).expect("Group ID should be valid hex");
assert_eq!(
group_id_bytes.len(),
32,
"Group ID should decode to 32 bytes"
);
assert!(
message_event.verify().is_ok(),
"Message event must be properly signed"
);
assert_ne!(
message_event.pubkey,
creator.public_key(),
"Message should use ephemeral pubkey, not sender's real pubkey"
);
}
#[test]
fn test_group_message_ephemeral_keys_mip03_compliance() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let rumor1 = create_test_rumor(&creator, "First message");
let rumor2 = create_test_rumor(&creator, "Second message");
let rumor3 = create_test_rumor(&creator, "Third message");
let event1 = mdk
.create_message(&group_id, rumor1, None)
.expect("Failed to create first message");
let event2 = mdk
.create_message(&group_id, rumor2, None)
.expect("Failed to create second message");
let event3 = mdk
.create_message(&group_id, rumor3, None)
.expect("Failed to create third message");
let pubkeys = [event1.pubkey, event2.pubkey, event3.pubkey];
assert_ne!(
pubkeys[0], pubkeys[1],
"First and second messages should use different ephemeral keys"
);
assert_ne!(
pubkeys[1], pubkeys[2],
"Second and third messages should use different ephemeral keys"
);
assert_ne!(
pubkeys[0], pubkeys[2],
"First and third messages should use different ephemeral keys"
);
let real_pubkey = creator.public_key();
for (i, pubkey) in pubkeys.iter().enumerate() {
assert_ne!(
*pubkey,
real_pubkey,
"Message {} should not use sender's real pubkey",
i + 1
);
}
}
#[test]
fn test_commit_event_structure_mip03_compliance() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let new_member = Keys::generate();
let add_result = mdk
.add_members(&group_id, &[create_key_package_event(&mdk, &new_member)])
.expect("Failed to add member");
let commit_event = &add_result.evolution_event;
assert_eq!(
commit_event.kind,
Kind::MlsGroupMessage,
"Commit event should have kind 445"
);
assert_eq!(
commit_event.tags.len(),
2,
"Commit event should have exactly 2 tags: h and encoding"
);
let commit_tags: Vec<&nostr::Tag> = commit_event.tags.iter().collect();
assert_eq!(
commit_tags
.iter()
.find(|t| t.kind() == TagKind::h())
.expect("Commit event should have h tag")
.kind(),
TagKind::h(),
"Commit event should have h tag"
);
let commit_encoding_tag = commit_tags
.iter()
.find(|t| t.kind() == TagKind::Custom("encoding".into()))
.expect("Commit event should have encoding tag");
assert_eq!(
commit_encoding_tag
.content()
.expect("encoding tag should have content"),
"base64"
);
assert_ne!(
commit_event.pubkey,
creator.public_key(),
"Commit should use ephemeral pubkey, not creator's real pubkey"
);
assert!(
commit_event.verify().is_ok(),
"Commit event must be properly signed"
);
assert!(
commit_event.content.len() > 50,
"Commit content should be encrypted and substantial"
);
}
#[test]
fn test_group_id_consistency_mip03() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let stored_group = mdk
.get_group(&group_id)
.expect("Failed to get group")
.expect("Group should exist");
let expected_nostr_group_id = hex::encode(stored_group.nostr_group_id);
let rumor = create_test_rumor(&creator, "Test message");
let message_event = mdk
.create_message(&group_id, rumor, None)
.expect("Failed to create message");
let h_tag = message_event
.tags
.iter()
.find(|t| t.kind() == TagKind::h())
.expect("Message should have h tag");
let message_group_id = h_tag.content().expect("h tag should have content");
assert_eq!(
message_group_id, expected_nostr_group_id,
"h tag group ID should match NostrGroupDataExtension"
);
}
#[test]
fn test_group_id_consistency_across_messages() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let event1 = mdk
.create_message(&group_id, create_test_rumor(&creator, "Message 1"), None)
.expect("Failed to create message 1");
let event2 = mdk
.create_message(&group_id, create_test_rumor(&creator, "Message 2"), None)
.expect("Failed to create message 2");
let event3 = mdk
.create_message(&group_id, create_test_rumor(&creator, "Message 3"), None)
.expect("Failed to create message 3");
let group_id1 = event1
.tags
.iter()
.find(|t| t.kind() == TagKind::h())
.expect("Message 1 should have h tag")
.content()
.expect("h tag should have content");
let group_id2 = event2
.tags
.iter()
.find(|t| t.kind() == TagKind::h())
.expect("Message 2 should have h tag")
.content()
.expect("h tag should have content");
let group_id3 = event3
.tags
.iter()
.find(|t| t.kind() == TagKind::h())
.expect("Message 3 should have h tag")
.content()
.expect("h tag should have content");
assert_eq!(
group_id1, group_id2,
"All messages should reference the same group"
);
assert_eq!(
group_id2, group_id3,
"All messages should reference the same group"
);
}
#[test]
fn test_message_content_encryption_mip03() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let plaintext = "Secret message content that should be encrypted";
let rumor = create_test_rumor(&creator, plaintext);
let message_event = mdk
.create_message(&group_id, rumor, None)
.expect("Failed to create message");
assert!(
!message_event.content.contains(plaintext),
"Encrypted content should not contain plaintext"
);
assert!(
message_event.content.len() > plaintext.len(),
"Encrypted content should be longer than plaintext due to encryption overhead"
);
assert!(
message_event.content.len() > 40,
"ChaCha20-Poly1305 encrypted content should be substantial (base64-encoded)"
);
}
#[test]
fn test_message_encryption_uniqueness() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let plaintext = "Identical message content";
let rumor1 = create_test_rumor(&creator, plaintext);
let rumor2 = create_test_rumor(&creator, plaintext);
let event1 = mdk
.create_message(&group_id, rumor1, None)
.expect("Failed to create first message");
let event2 = mdk
.create_message(&group_id, rumor2, None)
.expect("Failed to create second message");
assert_ne!(
event1.content, event2.content,
"Two messages with same plaintext should have different encrypted content"
);
}
#[test]
fn test_complete_message_lifecycle_spec_compliance() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let create_result = mdk
.create_group(
&creator.public_key(),
vec![
create_key_package_event(&mdk, &members[0]),
create_key_package_event(&mdk, &members[1]),
],
create_nostr_group_config_data(admins.clone()),
)
.expect("Failed to create group");
let group_id = create_result.group.mls_group_id.clone();
mdk.merge_pending_commit(&group_id)
.expect("Failed to merge pending commit");
let rumor1 = create_test_rumor(&creator, "First message");
let msg_event1 = mdk
.create_message(&group_id, rumor1, None)
.expect("Failed to send first message");
assert_eq!(msg_event1.kind, Kind::MlsGroupMessage);
assert_eq!(msg_event1.tags.len(), 2);
assert!(msg_event1.tags.iter().any(|t| t.kind() == TagKind::h()));
assert!(
msg_event1
.tags
.iter()
.any(|t| t.kind() == TagKind::Custom("encoding".into()))
);
let pubkey1 = msg_event1.pubkey;
let new_member = Keys::generate();
let add_result = mdk
.add_members(&group_id, &[create_key_package_event(&mdk, &new_member)])
.expect("Failed to add member");
let commit_event = &add_result.evolution_event;
assert_eq!(commit_event.kind, Kind::MlsGroupMessage);
assert_eq!(commit_event.tags.len(), 2);
assert!(commit_event.tags.iter().any(|t| t.kind() == TagKind::h()));
assert!(
commit_event
.tags
.iter()
.any(|t| t.kind() == TagKind::Custom("encoding".into()))
);
assert_ne!(
commit_event.pubkey,
creator.public_key(),
"Commit should use ephemeral key"
);
mdk.merge_pending_commit(&group_id)
.expect("Failed to merge commit");
let rumor2 = create_test_rumor(&creator, "Second message after member add");
let msg_event2 = mdk
.create_message(&group_id, rumor2, None)
.expect("Failed to send second message");
let pubkey2 = msg_event2.pubkey;
assert_ne!(
pubkey1, pubkey2,
"Different messages should use different ephemeral keys"
);
assert_ne!(
pubkey1, commit_event.pubkey,
"Message and commit should use different ephemeral keys"
);
assert_ne!(
pubkey2, commit_event.pubkey,
"Message and commit should use different ephemeral keys"
);
let msg1_tags: Vec<&nostr::Tag> = msg_event1.tags.iter().collect();
let commit_tags: Vec<&nostr::Tag> = commit_event.tags.iter().collect();
let msg2_tags: Vec<&nostr::Tag> = msg_event2.tags.iter().collect();
let group_id_hex1 = msg1_tags[0].content().unwrap();
let group_id_hex2 = commit_tags[0].content().unwrap();
let group_id_hex3 = msg2_tags[0].content().unwrap();
assert_eq!(
group_id_hex1, group_id_hex2,
"All events should reference same group"
);
assert_eq!(
group_id_hex2, group_id_hex3,
"All events should reference same group"
);
}
#[test]
fn test_message_event_validation() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let rumor = create_test_rumor(&creator, "Validation test message");
let message_event = mdk
.create_message(&group_id, rumor, None)
.expect("Failed to create message");
assert!(
message_event.verify().is_ok(),
"Message event should have valid signature"
);
let recomputed_id = message_event.id;
assert_eq!(
message_event.id, recomputed_id,
"Event ID should be correctly computed"
);
let now = Timestamp::now();
assert!(
message_event.created_at <= now,
"Message timestamp should not be in the future"
);
let one_day_ago = now.as_secs().saturating_sub(86400);
assert!(
message_event.created_at.as_secs() > one_day_ago,
"Message timestamp should be recent"
);
}
#[test]
fn test_create_message_for_nonexistent_group() {
let mdk = create_test_mdk();
let creator = Keys::generate();
let rumor = create_test_rumor(&creator, "Hello");
let non_existent_group_id = GroupId::from_slice(&[1, 2, 3, 4, 5]);
let result = mdk.create_message(&non_existent_group_id, rumor, None);
assert!(
matches!(result, Err(Error::GroupNotFound)),
"Should return GroupNotFound error"
);
}
#[test]
fn test_message_with_empty_content() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let rumor = create_test_rumor(&creator, "");
let result = mdk.create_message(&group_id, rumor, None);
assert!(result.is_ok(), "Empty message should be valid");
}
#[test]
fn test_message_with_long_content() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let long_content = "a".repeat(10000);
let rumor = create_test_rumor(&creator, &long_content);
let result = mdk.create_message(&group_id, rumor, None);
assert!(result.is_ok(), "Long message should be valid");
let event = result.unwrap();
assert_eq!(event.kind, Kind::MlsGroupMessage);
}
#[test]
fn test_create_message_with_tags() {
let mdk = create_test_mdk();
let (creator, members, admins) = create_test_group_members();
let group_id = create_test_group(&mdk, &creator, &members, &admins);
let expiration = EventTag::expiration(Timestamp::from(1_231_006_505));
let rumor = create_test_rumor(&creator, "Ephemeral location update");
let event = mdk
.create_message(&group_id, rumor, Some(vec![expiration]))
.expect("Failed to create message with extra tags");
assert_eq!(event.tags.len(), 3, "Should have h + encoding + expiration");
assert!(event.tags.iter().any(|t| t.kind() == TagKind::h()));
assert!(
event
.tags
.iter()
.any(|t| t.kind() == TagKind::Custom("encoding".into()))
);
assert!(
event.tags.iter().any(|t| t.kind() == TagKind::Expiration),
"Expiration tag should be present on the wrapper event"
);
}
}