use crate::room_state::member::MemberId;
use crate::room_state::privacy::{PrivacyMode, SecretVersion};
use crate::room_state::ChatRoomParametersV1;
use crate::util::sign_struct;
use crate::util::{truncated_base64, verify_struct};
use crate::ChatRoomStateV1;
use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
use freenet_scaffold::util::{fast_hash, FastHash};
use freenet_scaffold::ComposableState;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::time::SystemTime;
#[derive(Clone, PartialEq, Debug, Default)]
pub struct MessageActionsState {
pub edited_content: HashMap<MessageId, String>,
pub deleted: std::collections::HashSet<MessageId>,
pub reactions: HashMap<MessageId, HashMap<String, Vec<MemberId>>>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Default)]
pub struct MessagesV1 {
pub messages: Vec<AuthorizedMessageV1>,
#[serde(skip)]
pub actions_state: MessageActionsState,
}
impl ComposableState for MessagesV1 {
type ParentState = ChatRoomStateV1;
type Summary = Vec<MessageId>;
type Delta = Vec<AuthorizedMessageV1>;
type Parameters = ChatRoomParametersV1;
fn verify(
&self,
parent_state: &Self::ParentState,
parameters: &Self::Parameters,
) -> Result<(), String> {
let members_by_id = parent_state.members.members_by_member_id();
let owner_id = parameters.owner_id();
for message in &self.messages {
let verifying_key = if message.message.author == owner_id {
¶meters.owner
} else if let Some(member) = members_by_id.get(&message.message.author) {
&member.member.member_vk
} else {
return Err(format!(
"Message author not found: {:?}",
message.message.author
));
};
if message.validate(verifying_key).is_err() {
return Err(format!("Invalid message signature: id:{:?}", message.id()));
}
}
Ok(())
}
fn summarize(
&self,
_parent_state: &Self::ParentState,
_parameters: &Self::Parameters,
) -> Self::Summary {
self.messages.iter().map(|m| m.id()).collect()
}
fn delta(
&self,
_parent_state: &Self::ParentState,
_parameters: &Self::Parameters,
old_state_summary: &Self::Summary,
) -> Option<Self::Delta> {
let delta: Vec<AuthorizedMessageV1> = self
.messages
.iter()
.filter(|m| !old_state_summary.contains(&m.id()))
.cloned()
.collect();
if delta.is_empty() {
None
} else {
Some(delta)
}
}
fn apply_delta(
&mut self,
parent_state: &Self::ParentState,
parameters: &Self::Parameters,
delta: &Option<Self::Delta>,
) -> Result<(), String> {
let max_recent_messages = parent_state.configuration.configuration.max_recent_messages;
let max_message_size = parent_state.configuration.configuration.max_message_size;
let privacy_mode = &parent_state.configuration.configuration.privacy_mode;
if let Some(delta) = delta {
for msg in delta {
let content = &msg.message.content;
match content {
RoomMessageBody::Private { secret_version, .. } => {
if *privacy_mode == PrivacyMode::Private
&& !parent_state
.secrets
.versions
.iter()
.any(|v| v.record.version == *secret_version)
{
return Err(format!(
"Private message references unknown secret version {}",
secret_version
));
}
}
RoomMessageBody::Public { .. } => {
if *privacy_mode == PrivacyMode::Private && !content.is_event() {
return Err("Cannot send public messages in private room".to_string());
}
}
}
}
let existing_ids: std::collections::HashSet<_> =
self.messages.iter().map(|m| m.id()).collect();
self.messages.extend(
delta
.iter()
.filter(|msg| !existing_ids.contains(&msg.id()))
.cloned(),
);
}
self.messages
.retain(|m| m.message.content.content_len() <= max_message_size);
let members_by_id = parent_state.members.members_by_member_id();
let owner_id = MemberId::from(¶meters.owner);
self.messages.retain(|m| {
members_by_id.contains_key(&m.message.author) || m.message.author == owner_id
});
self.messages.sort_by(|a, b| {
a.message
.time
.cmp(&b.message.time)
.then_with(|| a.id().cmp(&b.id()))
});
if self.messages.len() > max_recent_messages {
self.messages
.drain(0..self.messages.len() - max_recent_messages);
}
self.rebuild_actions_state();
Ok(())
}
}
impl MessagesV1 {
pub fn rebuild_actions_state(&mut self) {
self.rebuild_actions_state_with_decrypted(&HashMap::new());
}
pub fn rebuild_actions_state_with_decrypted(
&mut self,
decrypted_content: &HashMap<MessageId, Vec<u8>>,
) {
use crate::room_state::content::{
ActionContentV1, DecodedContent, ACTION_TYPE_DELETE, ACTION_TYPE_EDIT,
ACTION_TYPE_REACTION, ACTION_TYPE_REMOVE_REACTION,
};
self.actions_state = MessageActionsState::default();
let message_authors: HashMap<MessageId, MemberId> = self
.messages
.iter()
.filter(|m| !m.message.content.is_action())
.map(|m| (m.id(), m.message.author))
.collect();
for msg in &self.messages {
let actor = msg.message.author;
if !msg.message.content.is_action() {
continue;
}
let action = match &msg.message.content {
RoomMessageBody::Public { .. } => {
match msg.message.content.decode_content() {
Some(DecodedContent::Action(action)) => action,
_ => continue,
}
}
RoomMessageBody::Private { .. } => {
let msg_id = msg.id();
if let Some(plaintext) = decrypted_content.get(&msg_id) {
match ActionContentV1::decode(plaintext) {
Ok(action) => action,
Err(_) => continue,
}
} else {
continue;
}
}
};
let target = &action.target;
match action.action_type {
ACTION_TYPE_EDIT => {
if let Some(&original_author) = message_authors.get(target) {
if actor == original_author {
if !self.actions_state.deleted.contains(target) {
if let Some(payload) = action.edit_payload() {
self.actions_state
.edited_content
.insert(target.clone(), payload.new_text);
}
}
}
}
}
ACTION_TYPE_DELETE => {
if let Some(&original_author) = message_authors.get(target) {
if actor == original_author {
self.actions_state.deleted.insert(target.clone());
self.actions_state.edited_content.remove(target);
}
}
}
ACTION_TYPE_REACTION => {
if message_authors.contains_key(target)
&& !self.actions_state.deleted.contains(target)
{
if let Some(payload) = action.reaction_payload() {
let reactions = self
.actions_state
.reactions
.entry(target.clone())
.or_default();
let reactors = reactions.entry(payload.emoji).or_default();
if !reactors.contains(&actor) {
reactors.push(actor);
}
}
}
}
ACTION_TYPE_REMOVE_REACTION => {
if let Some(payload) = action.reaction_payload() {
if let Some(reactions) = self.actions_state.reactions.get_mut(target) {
if let Some(reactors) = reactions.get_mut(&payload.emoji) {
reactors.retain(|r| r != &actor);
if reactors.is_empty() {
reactions.remove(&payload.emoji);
}
}
if reactions.is_empty() {
self.actions_state.reactions.remove(target);
}
}
}
}
_ => {
}
}
}
}
pub fn is_edited(&self, message_id: &MessageId) -> bool {
self.actions_state.edited_content.contains_key(message_id)
}
pub fn is_deleted(&self, message_id: &MessageId) -> bool {
self.actions_state.deleted.contains(message_id)
}
pub fn effective_text(&self, message: &AuthorizedMessageV1) -> Option<String> {
let id = message.id();
if let Some(edited_text) = self.actions_state.edited_content.get(&id) {
return Some(edited_text.clone());
}
message.message.content.as_public_string()
}
pub fn reactions(&self, message_id: &MessageId) -> Option<&HashMap<String, Vec<MemberId>>> {
self.actions_state.reactions.get(message_id)
}
pub fn display_messages(&self) -> impl Iterator<Item = &AuthorizedMessageV1> {
self.messages.iter().filter(|m| {
!m.message.content.is_action() && !self.actions_state.deleted.contains(&m.id())
})
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub enum RoomMessageBody {
Public {
content_type: u32,
content_version: u32,
data: Vec<u8>,
},
Private {
content_type: u32,
content_version: u32,
ciphertext: Vec<u8>,
nonce: [u8; 12],
secret_version: SecretVersion,
},
}
impl RoomMessageBody {
pub fn public(text: String) -> Self {
use crate::room_state::content::{TextContentV1, CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
let content = TextContentV1::new(text);
Self::Public {
content_type: CONTENT_TYPE_TEXT,
content_version: TEXT_CONTENT_VERSION,
data: content.encode(),
}
}
pub fn join_event() -> Self {
use crate::room_state::content::{
EventContentV1, CONTENT_TYPE_EVENT, EVENT_CONTENT_VERSION,
};
let content = EventContentV1::join();
Self::Public {
content_type: CONTENT_TYPE_EVENT,
content_version: EVENT_CONTENT_VERSION,
data: content.encode(),
}
}
pub fn public_raw(content_type: u32, content_version: u32, data: Vec<u8>) -> Self {
Self::Public {
content_type,
content_version,
data,
}
}
pub fn private(
content_type: u32,
content_version: u32,
ciphertext: Vec<u8>,
nonce: [u8; 12],
secret_version: SecretVersion,
) -> Self {
Self::Private {
content_type,
content_version,
ciphertext,
nonce,
secret_version,
}
}
pub fn private_text(
ciphertext: Vec<u8>,
nonce: [u8; 12],
secret_version: SecretVersion,
) -> Self {
use crate::room_state::content::{CONTENT_TYPE_TEXT, TEXT_CONTENT_VERSION};
Self::Private {
content_type: CONTENT_TYPE_TEXT,
content_version: TEXT_CONTENT_VERSION,
ciphertext,
nonce,
secret_version,
}
}
pub fn edit(target: MessageId, new_text: String) -> Self {
use crate::room_state::content::{
ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
};
let action = ActionContentV1::edit(target, new_text);
Self::Public {
content_type: CONTENT_TYPE_ACTION,
content_version: ACTION_CONTENT_VERSION,
data: action.encode(),
}
}
pub fn delete(target: MessageId) -> Self {
use crate::room_state::content::{
ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
};
let action = ActionContentV1::delete(target);
Self::Public {
content_type: CONTENT_TYPE_ACTION,
content_version: ACTION_CONTENT_VERSION,
data: action.encode(),
}
}
pub fn reaction(target: MessageId, emoji: String) -> Self {
use crate::room_state::content::{
ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
};
let action = ActionContentV1::reaction(target, emoji);
Self::Public {
content_type: CONTENT_TYPE_ACTION,
content_version: ACTION_CONTENT_VERSION,
data: action.encode(),
}
}
pub fn remove_reaction(target: MessageId, emoji: String) -> Self {
use crate::room_state::content::{
ActionContentV1, ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION,
};
let action = ActionContentV1::remove_reaction(target, emoji);
Self::Public {
content_type: CONTENT_TYPE_ACTION,
content_version: ACTION_CONTENT_VERSION,
data: action.encode(),
}
}
pub fn reply(
text: String,
target_message_id: MessageId,
target_author_name: String,
target_content_preview: String,
) -> Self {
use crate::room_state::content::{
ReplyContentV1, CONTENT_TYPE_REPLY, REPLY_CONTENT_VERSION,
};
let reply = ReplyContentV1::new(
text,
target_message_id,
target_author_name,
target_content_preview,
);
Self::Public {
content_type: CONTENT_TYPE_REPLY,
content_version: REPLY_CONTENT_VERSION,
data: reply.encode(),
}
}
pub fn private_action(
ciphertext: Vec<u8>,
nonce: [u8; 12],
secret_version: SecretVersion,
) -> Self {
use crate::room_state::content::{ACTION_CONTENT_VERSION, CONTENT_TYPE_ACTION};
Self::Private {
content_type: CONTENT_TYPE_ACTION,
content_version: ACTION_CONTENT_VERSION,
ciphertext,
nonce,
secret_version,
}
}
pub fn is_public(&self) -> bool {
matches!(self, Self::Public { .. })
}
pub fn is_private(&self) -> bool {
matches!(self, Self::Private { .. })
}
pub fn content_type(&self) -> u32 {
match self {
Self::Public { content_type, .. } | Self::Private { content_type, .. } => *content_type,
}
}
pub fn content_version(&self) -> u32 {
match self {
Self::Public {
content_version, ..
}
| Self::Private {
content_version, ..
} => *content_version,
}
}
pub fn is_action(&self) -> bool {
use crate::room_state::content::CONTENT_TYPE_ACTION;
self.content_type() == CONTENT_TYPE_ACTION
}
pub fn is_event(&self) -> bool {
use crate::room_state::content::CONTENT_TYPE_EVENT;
self.content_type() == CONTENT_TYPE_EVENT
}
pub fn decode_content(&self) -> Option<crate::room_state::content::DecodedContent> {
use crate::room_state::content::{
ActionContentV1, DecodedContent, EventContentV1, ReplyContentV1, TextContentV1,
CONTENT_TYPE_ACTION, CONTENT_TYPE_EVENT, CONTENT_TYPE_REPLY, CONTENT_TYPE_TEXT,
};
match self {
Self::Public {
content_type,
content_version,
data,
} => match *content_type {
CONTENT_TYPE_TEXT => TextContentV1::decode(data).ok().map(DecodedContent::Text),
CONTENT_TYPE_ACTION => ActionContentV1::decode(data)
.ok()
.map(DecodedContent::Action),
CONTENT_TYPE_REPLY => ReplyContentV1::decode(data).ok().map(DecodedContent::Reply),
CONTENT_TYPE_EVENT => EventContentV1::decode(data).ok().map(DecodedContent::Event),
_ => Some(DecodedContent::Unknown {
content_type: *content_type,
content_version: *content_version,
}),
},
Self::Private { .. } => None,
}
}
pub fn target_id(&self) -> Option<MessageId> {
use crate::room_state::content::{ActionContentV1, CONTENT_TYPE_ACTION};
match self {
Self::Public {
content_type, data, ..
} if *content_type == CONTENT_TYPE_ACTION => {
ActionContentV1::decode(data).ok().map(|a| a.target)
}
_ => None,
}
}
pub fn content_len(&self) -> usize {
match self {
Self::Public { data, .. } => data.len(),
Self::Private { ciphertext, .. } => ciphertext.len(),
}
}
pub fn secret_version(&self) -> Option<SecretVersion> {
match self {
Self::Public { .. } => None,
Self::Private { secret_version, .. } => Some(*secret_version),
}
}
pub fn to_string_lossy(&self) -> String {
match self {
Self::Public { .. } => {
if let Some(decoded) = self.decode_content() {
decoded.to_display_string()
} else {
"[Failed to decode message]".to_string()
}
}
Self::Private {
ciphertext,
secret_version,
..
} => {
format!(
"[Encrypted message: {} bytes, v{}]",
ciphertext.len(),
secret_version
)
}
}
}
pub fn as_public_string(&self) -> Option<String> {
self.decode_content()
.and_then(|c| c.as_text().map(|s| s.to_string()))
}
}
impl fmt::Display for RoomMessageBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string_lossy())
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct MessageV1 {
pub room_owner: MemberId,
pub author: MemberId,
pub time: SystemTime,
pub content: RoomMessageBody,
}
impl Default for MessageV1 {
fn default() -> Self {
Self {
room_owner: MemberId(FastHash(0)),
author: MemberId(FastHash(0)),
time: SystemTime::UNIX_EPOCH,
content: RoomMessageBody::public(String::new()),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthorizedMessageV1 {
pub message: MessageV1,
pub signature: Signature,
}
impl fmt::Debug for AuthorizedMessageV1 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthorizedMessage")
.field("message", &self.message)
.field(
"signature",
&format_args!("{}", truncated_base64(self.signature.to_bytes())),
)
.finish()
}
}
#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Clone, Debug, Ord, PartialOrd)]
pub struct MessageId(pub FastHash);
impl fmt::Display for MessageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl AuthorizedMessageV1 {
pub fn new(message: MessageV1, signing_key: &SigningKey) -> Self {
Self {
message: message.clone(),
signature: sign_struct(&message, signing_key),
}
}
pub fn with_signature(message: MessageV1, signature: Signature) -> Self {
Self { message, signature }
}
pub fn validate(
&self,
verifying_key: &VerifyingKey,
) -> Result<(), ed25519_dalek::SignatureError> {
verify_struct(&self.message, &self.signature, verifying_key)
}
pub fn id(&self) -> MessageId {
MessageId(fast_hash(&self.signature.to_bytes()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
use rand::rngs::OsRng;
use std::time::Duration;
fn create_test_message(owner_id: MemberId, author_id: MemberId) -> MessageV1 {
MessageV1 {
room_owner: owner_id,
author: author_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Test message".to_string()),
}
}
#[test]
fn test_messages_v1_default() {
let default_messages = MessagesV1::default();
assert!(default_messages.messages.is_empty());
}
#[test]
fn test_authorized_message_v1_debug() {
let signing_key = SigningKey::generate(&mut OsRng);
let owner_id = MemberId(FastHash(0));
let author_id = MemberId(FastHash(1));
let message = create_test_message(owner_id, author_id);
let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
let debug_output = format!("{:?}", authorized_message);
assert!(debug_output.contains("AuthorizedMessage"));
assert!(debug_output.contains("message"));
assert!(debug_output.contains("signature"));
}
#[test]
fn test_authorized_message_new_and_validate() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let owner_id = MemberId(FastHash(0));
let author_id = MemberId(FastHash(1));
let message = create_test_message(owner_id, author_id);
let authorized_message = AuthorizedMessageV1::new(message.clone(), &signing_key);
assert_eq!(authorized_message.message, message);
assert!(authorized_message.validate(&verifying_key).is_ok());
let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
assert!(authorized_message.validate(&wrong_key).is_err());
let mut tampered_message = authorized_message.clone();
tampered_message.message.content = RoomMessageBody::public("Tampered content".to_string());
assert!(tampered_message.validate(&verifying_key).is_err());
}
#[test]
fn test_message_id() {
let signing_key = SigningKey::generate(&mut OsRng);
let owner_id = MemberId(FastHash(0));
let author_id = MemberId(FastHash(1));
let message = create_test_message(owner_id, author_id);
let authorized_message = AuthorizedMessageV1::new(message, &signing_key);
let id1 = authorized_message.id();
let id2 = authorized_message.id();
assert_eq!(id1, id2);
let message2 = create_test_message(owner_id, author_id);
let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
assert_ne!(authorized_message.id(), authorized_message2.id());
}
#[test]
fn test_messages_verify() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id = MemberId::from(&owner_verifying_key);
let author_signing_key = SigningKey::generate(&mut OsRng);
let author_verifying_key = author_signing_key.verifying_key();
let author_id = MemberId::from(&author_verifying_key);
let message = create_test_message(owner_id, author_id);
let authorized_message = AuthorizedMessageV1::new(message, &author_signing_key);
let messages = MessagesV1 {
messages: vec![authorized_message],
..Default::default()
};
let mut parent_state = ChatRoomStateV1::default();
let author_member = crate::room_state::member::Member {
owner_member_id: owner_id,
invited_by: owner_id,
member_vk: author_verifying_key,
};
let authorized_author =
crate::room_state::member::AuthorizedMember::new(author_member, &owner_signing_key);
parent_state.members.members = vec![authorized_author];
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
assert!(
messages.verify(&parent_state, ¶meters).is_ok(),
"Valid messages should pass verification: {:?}",
messages.verify(&parent_state, ¶meters)
);
let mut invalid_messages = messages.clone();
invalid_messages.messages[0].signature = Signature::from_bytes(&[0; 64]); assert!(
invalid_messages.verify(&parent_state, ¶meters).is_err(),
"Messages with invalid signature should fail verification"
);
let non_existent_author_id =
MemberId::from(&SigningKey::generate(&mut OsRng).verifying_key());
let invalid_message = create_test_message(owner_id, non_existent_author_id);
let invalid_authorized_message =
AuthorizedMessageV1::new(invalid_message, &author_signing_key);
let invalid_messages = MessagesV1 {
messages: vec![invalid_authorized_message],
..Default::default()
};
assert!(
invalid_messages.verify(&parent_state, ¶meters).is_err(),
"Messages with non-existent author should fail verification"
);
}
#[test]
fn test_messages_summarize() {
let signing_key = SigningKey::generate(&mut OsRng);
let owner_id = MemberId(FastHash(0));
let author_id = MemberId(FastHash(1));
let message1 = create_test_message(owner_id, author_id);
let message2 = create_test_message(owner_id, author_id);
let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
let messages = MessagesV1 {
messages: vec![authorized_message1.clone(), authorized_message2.clone()],
..Default::default()
};
let parent_state = ChatRoomStateV1::default();
let parameters = ChatRoomParametersV1 {
owner: signing_key.verifying_key(),
};
let summary = messages.summarize(&parent_state, ¶meters);
assert_eq!(summary.len(), 2);
assert_eq!(summary[0], authorized_message1.id());
assert_eq!(summary[1], authorized_message2.id());
let empty_messages = MessagesV1::default();
let empty_summary = empty_messages.summarize(&parent_state, ¶meters);
assert!(empty_summary.is_empty());
}
#[test]
fn test_messages_delta() {
let signing_key = SigningKey::generate(&mut OsRng);
let owner_id = MemberId(FastHash(0));
let author_id = MemberId(FastHash(1));
let base = SystemTime::now();
let message1 = MessageV1 {
room_owner: owner_id,
author: author_id,
time: base,
content: RoomMessageBody::public("Message 1".to_string()),
};
let message2 = MessageV1 {
room_owner: owner_id,
author: author_id,
time: base + Duration::from_millis(1),
content: RoomMessageBody::public("Message 2".to_string()),
};
let message3 = MessageV1 {
room_owner: owner_id,
author: author_id,
time: base + Duration::from_millis(2),
content: RoomMessageBody::public("Message 3".to_string()),
};
let authorized_message1 = AuthorizedMessageV1::new(message1, &signing_key);
let authorized_message2 = AuthorizedMessageV1::new(message2, &signing_key);
let authorized_message3 = AuthorizedMessageV1::new(message3, &signing_key);
let messages = MessagesV1 {
messages: vec![
authorized_message1.clone(),
authorized_message2.clone(),
authorized_message3.clone(),
],
..Default::default()
};
let parent_state = ChatRoomStateV1::default();
let parameters = ChatRoomParametersV1 {
owner: signing_key.verifying_key(),
};
let old_summary = vec![authorized_message1.id(), authorized_message2.id()];
let delta = messages
.delta(&parent_state, ¶meters, &old_summary)
.unwrap();
assert_eq!(delta.len(), 1);
assert_eq!(delta[0], authorized_message3);
let empty_summary: Vec<MessageId> = vec![];
let full_delta = messages
.delta(&parent_state, ¶meters, &empty_summary)
.unwrap();
assert_eq!(full_delta.len(), 3);
assert_eq!(full_delta, messages.messages);
let full_summary = vec![
authorized_message1.id(),
authorized_message2.id(),
authorized_message3.id(),
];
let no_delta = messages.delta(&parent_state, ¶meters, &full_summary);
assert!(no_delta.is_none());
}
#[test]
fn test_messages_apply_delta() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id = MemberId::from(&owner_verifying_key);
let author_signing_key = SigningKey::generate(&mut OsRng);
let author_verifying_key = author_signing_key.verifying_key();
let author_id = MemberId::from(&author_verifying_key);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_recent_messages = 3;
parent_state.configuration.configuration.max_message_size = 100;
parent_state.members.members = vec![crate::room_state::member::AuthorizedMember {
member: crate::room_state::member::Member {
owner_member_id: owner_id,
invited_by: owner_id,
member_vk: author_verifying_key,
},
signature: owner_signing_key.try_sign(&[0; 32]).unwrap(),
}];
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let create_message = |time: SystemTime| {
let message = MessageV1 {
room_owner: owner_id,
author: author_id,
time,
content: RoomMessageBody::public("Test message".to_string()),
};
AuthorizedMessageV1::new(message, &author_signing_key)
};
let now = SystemTime::now();
let message1 = create_message(now - Duration::from_secs(3));
let message2 = create_message(now - Duration::from_secs(2));
let message3 = create_message(now - Duration::from_secs(1));
let message4 = create_message(now);
let mut messages = MessagesV1 {
messages: vec![message1.clone(), message2.clone()],
..Default::default()
};
let delta = vec![message3.clone(), message4.clone()];
assert!(messages
.apply_delta(&parent_state, ¶meters, &Some(delta))
.is_ok());
assert_eq!(
messages.messages.len(),
3,
"Should have 3 messages after applying delta"
);
assert!(
!messages.messages.contains(&message1),
"Oldest message should be removed"
);
assert!(
messages.messages.contains(&message2),
"Second oldest message should be retained"
);
assert!(
messages.messages.contains(&message3),
"New message should be added"
);
assert!(
messages.messages.contains(&message4),
"Newest message should be added"
);
let old_message = create_message(now - Duration::from_secs(4));
let delta = vec![old_message.clone()];
assert!(messages
.apply_delta(&parent_state, ¶meters, &Some(delta))
.is_ok());
assert_eq!(messages.messages.len(), 3, "Should still have 3 messages");
assert!(
!messages.messages.contains(&old_message),
"Older message should not be added"
);
assert!(
messages.messages.contains(&message2),
"Message2 should be retained"
);
assert!(
messages.messages.contains(&message3),
"Message3 should be retained"
);
assert!(
messages.messages.contains(&message4),
"Newest message should be retained"
);
}
#[test]
fn test_oversized_message_filtered_by_apply_delta() {
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id = MemberId::from(&owner_vk);
let author_sk = SigningKey::generate(&mut OsRng);
let author_vk = author_sk.verifying_key();
let author_id = MemberId::from(&author_vk);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_message_size = 50;
parent_state.configuration.configuration.max_recent_messages = 10;
parent_state.members.members = vec![crate::room_state::member::AuthorizedMember {
member: crate::room_state::member::Member {
owner_member_id: owner_id,
invited_by: owner_id,
member_vk: author_vk,
},
signature: owner_sk.try_sign(&[0; 32]).unwrap(),
}];
let parameters = ChatRoomParametersV1 { owner: owner_vk };
let small_msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: author_id,
time: SystemTime::now(),
content: RoomMessageBody::public("short".to_string()),
},
&author_sk,
);
let big_msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: author_id,
time: SystemTime::now(),
content: RoomMessageBody::public("x".repeat(100)),
},
&author_sk,
);
assert!(small_msg.message.content.content_len() <= 50);
assert!(big_msg.message.content.content_len() > 50);
let mut messages = MessagesV1::default();
let delta = vec![small_msg.clone(), big_msg.clone()];
assert!(messages
.apply_delta(&parent_state, ¶meters, &Some(delta))
.is_ok());
assert_eq!(
messages.messages.len(),
1,
"Only small message should survive"
);
assert!(messages.messages.contains(&small_msg));
assert!(
!messages.messages.contains(&big_msg),
"Oversized message should be filtered"
);
}
#[test]
fn test_message_author_preservation_across_users() {
let user1_sk = SigningKey::generate(&mut OsRng);
let user1_vk = user1_sk.verifying_key();
let user1_id = MemberId::from(&user1_vk);
let user2_sk = SigningKey::generate(&mut OsRng);
let user2_vk = user2_sk.verifying_key();
let user2_id = MemberId::from(&user2_vk);
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id = MemberId::from(&owner_vk);
println!("User1 ID: {}", user1_id);
println!("User2 ID: {}", user2_id);
println!("Owner ID: {}", owner_id);
let msg1 = MessageV1 {
room_owner: owner_id,
author: user1_id,
content: RoomMessageBody::public("Message from user1".to_string()),
time: SystemTime::now(),
};
let msg2 = MessageV1 {
room_owner: owner_id,
author: user2_id,
content: RoomMessageBody::public("Message from user2".to_string()),
time: SystemTime::now() + Duration::from_secs(1),
};
let auth_msg1 = AuthorizedMessageV1::new(msg1.clone(), &user1_sk);
let auth_msg2 = AuthorizedMessageV1::new(msg2.clone(), &user2_sk);
let messages = MessagesV1 {
messages: vec![auth_msg1.clone(), auth_msg2.clone()],
..Default::default()
};
assert_eq!(messages.messages.len(), 2);
let stored_msg1 = &messages.messages[0];
let stored_msg2 = &messages.messages[1];
assert_eq!(
stored_msg1.message.author, user1_id,
"Message 1 author should be user1, but got {}",
stored_msg1.message.author
);
assert_eq!(
stored_msg2.message.author, user2_id,
"Message 2 author should be user2, but got {}",
stored_msg2.message.author
);
assert_ne!(user1_id, user2_id, "User IDs should be different");
let user1_id_str = user1_id.to_string();
let user2_id_str = user2_id.to_string();
println!("User1 ID string: {}", user1_id_str);
println!("User2 ID string: {}", user2_id_str);
assert_ne!(
user1_id_str, user2_id_str,
"User ID strings should be different"
);
}
#[test]
fn test_edit_action() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let owner_id = MemberId::from(&verifying_key);
let author_id = owner_id;
let original_msg = MessageV1 {
room_owner: owner_id,
author: author_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Original content".to_string()),
};
let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
let original_id = auth_original.id();
let edit_msg = MessageV1 {
room_owner: owner_id,
author: author_id,
time: SystemTime::now() + Duration::from_secs(1),
content: RoomMessageBody::edit(original_id.clone(), "Edited content".to_string()),
};
let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
let mut messages = MessagesV1 {
messages: vec![auth_original.clone(), auth_edit],
..Default::default()
};
messages.rebuild_actions_state();
assert!(messages.is_edited(&original_id));
let effective = messages.effective_text(&auth_original);
assert_eq!(effective, Some("Edited content".to_string()));
let display: Vec<_> = messages.display_messages().collect();
assert_eq!(display.len(), 1);
}
#[test]
fn test_edit_by_non_author_ignored() {
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id = MemberId::from(&owner_vk);
let other_sk = SigningKey::generate(&mut OsRng);
let other_id = MemberId::from(&other_sk.verifying_key());
let original_msg = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Original content".to_string()),
};
let auth_original = AuthorizedMessageV1::new(original_msg, &owner_sk);
let original_id = auth_original.id();
let edit_msg = MessageV1 {
room_owner: owner_id,
author: other_id,
time: SystemTime::now() + Duration::from_secs(1),
content: RoomMessageBody::edit(original_id.clone(), "Hacked content".to_string()),
};
let auth_edit = AuthorizedMessageV1::new(edit_msg, &other_sk);
let mut messages = MessagesV1 {
messages: vec![auth_original.clone(), auth_edit],
..Default::default()
};
messages.rebuild_actions_state();
assert!(!messages.is_edited(&original_id));
let effective = messages.effective_text(&auth_original);
assert_eq!(effective, Some("Original content".to_string()));
}
#[test]
fn test_delete_action() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let owner_id = MemberId::from(&verifying_key);
let original_msg = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Will be deleted".to_string()),
};
let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
let original_id = auth_original.id();
let delete_msg = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now() + Duration::from_secs(1),
content: RoomMessageBody::delete(original_id.clone()),
};
let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
let mut messages = MessagesV1 {
messages: vec![auth_original, auth_delete],
..Default::default()
};
messages.rebuild_actions_state();
assert!(messages.is_deleted(&original_id));
let display: Vec<_> = messages.display_messages().collect();
assert_eq!(display.len(), 0);
}
#[test]
fn test_reaction_action() {
let user1_sk = SigningKey::generate(&mut OsRng);
let user1_id = MemberId::from(&user1_sk.verifying_key());
let user2_sk = SigningKey::generate(&mut OsRng);
let user2_id = MemberId::from(&user2_sk.verifying_key());
let owner_id = user1_id;
let original_msg = MessageV1 {
room_owner: owner_id,
author: user1_id,
time: SystemTime::now(),
content: RoomMessageBody::public("React to me!".to_string()),
};
let auth_original = AuthorizedMessageV1::new(original_msg, &user1_sk);
let original_id = auth_original.id();
let reaction_msg = MessageV1 {
room_owner: owner_id,
author: user2_id,
time: SystemTime::now() + Duration::from_secs(1),
content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
};
let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user2_sk);
let reaction_msg2 = MessageV1 {
room_owner: owner_id,
author: user1_id,
time: SystemTime::now() + Duration::from_secs(2),
content: RoomMessageBody::reaction(original_id.clone(), "👍".to_string()),
};
let auth_reaction2 = AuthorizedMessageV1::new(reaction_msg2, &user1_sk);
let mut messages = MessagesV1 {
messages: vec![auth_original, auth_reaction, auth_reaction2],
..Default::default()
};
messages.rebuild_actions_state();
let reactions = messages.reactions(&original_id).unwrap();
let thumbs_up = reactions.get("👍").unwrap();
assert_eq!(thumbs_up.len(), 2);
assert!(thumbs_up.contains(&user1_id));
assert!(thumbs_up.contains(&user2_id));
}
#[test]
fn test_remove_reaction_action() {
let user_sk = SigningKey::generate(&mut OsRng);
let user_id = MemberId::from(&user_sk.verifying_key());
let owner_id = user_id;
let original_msg = MessageV1 {
room_owner: owner_id,
author: user_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Test message".to_string()),
};
let auth_original = AuthorizedMessageV1::new(original_msg, &user_sk);
let original_id = auth_original.id();
let reaction_msg = MessageV1 {
room_owner: owner_id,
author: user_id,
time: SystemTime::now() + Duration::from_secs(1),
content: RoomMessageBody::reaction(original_id.clone(), "❤️".to_string()),
};
let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &user_sk);
let remove_msg = MessageV1 {
room_owner: owner_id,
author: user_id,
time: SystemTime::now() + Duration::from_secs(2),
content: RoomMessageBody::remove_reaction(original_id.clone(), "❤️".to_string()),
};
let auth_remove = AuthorizedMessageV1::new(remove_msg, &user_sk);
let mut messages = MessagesV1 {
messages: vec![auth_original, auth_reaction, auth_remove],
..Default::default()
};
messages.rebuild_actions_state();
assert!(messages.reactions(&original_id).is_none());
}
#[test]
fn test_action_on_deleted_message_ignored() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let owner_id = MemberId::from(&verifying_key);
let original_msg = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Will be deleted".to_string()),
};
let auth_original = AuthorizedMessageV1::new(original_msg, &signing_key);
let original_id = auth_original.id();
let delete_msg = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now() + Duration::from_secs(1),
content: RoomMessageBody::delete(original_id.clone()),
};
let auth_delete = AuthorizedMessageV1::new(delete_msg, &signing_key);
let edit_msg = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now() + Duration::from_secs(2),
content: RoomMessageBody::edit(original_id.clone(), "Too late!".to_string()),
};
let auth_edit = AuthorizedMessageV1::new(edit_msg, &signing_key);
let mut messages = MessagesV1 {
messages: vec![auth_original, auth_delete, auth_edit],
..Default::default()
};
messages.rebuild_actions_state();
assert!(messages.is_deleted(&original_id));
assert!(!messages.is_edited(&original_id));
}
#[test]
fn test_display_messages_filters_actions() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let owner_id = MemberId::from(&verifying_key);
let msg1 = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Hello".to_string()),
};
let auth_msg1 = AuthorizedMessageV1::new(msg1, &signing_key);
let msg1_id = auth_msg1.id();
let reaction_msg = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now() + Duration::from_secs(1),
content: RoomMessageBody::reaction(msg1_id, "👍".to_string()),
};
let auth_reaction = AuthorizedMessageV1::new(reaction_msg, &signing_key);
let msg2 = MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now() + Duration::from_secs(2),
content: RoomMessageBody::public("World".to_string()),
};
let auth_msg2 = AuthorizedMessageV1::new(msg2, &signing_key);
let mut messages = MessagesV1 {
messages: vec![auth_msg1, auth_reaction, auth_msg2],
..Default::default()
};
messages.rebuild_actions_state();
let display: Vec<_> = messages.display_messages().collect();
assert_eq!(display.len(), 2);
assert_eq!(
display[0].message.content.as_public_string(),
Some("Hello".to_string())
);
assert_eq!(
display[1].message.content.as_public_string(),
Some("World".to_string())
);
}
}