diff --git a/matrix_sdk/src/models/message.rs b/matrix_sdk/src/models/message.rs new file mode 100644 index 00000000..6cb2482f --- /dev/null +++ b/matrix_sdk/src/models/message.rs @@ -0,0 +1,233 @@ +use std::collections::{vec_deque::IntoIter, VecDeque}; + +use crate::events::collections::all::RoomEvent; +use crate::events::room::message::MessageEvent; +use crate::events::EventJson; + +use serde::{de, ser}; + +pub(crate) mod ser_deser { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let messages: VecDeque> = de::Deserialize::deserialize(deserializer)?; + + // TODO this should probably bail out if deserialization fails not skip + let msgs: VecDeque = messages + .into_iter() + .flat_map(|json| json.deserialize()) + .collect(); + + Ok(MessageQueue { msgs }) + } + + pub fn serialize(msgs: &MessageQueue, serializer: S) -> Result + where + S: ser::Serializer, + { + use ser::Serialize; + + msgs.msgs.serialize(serializer) + } +} + +/// A queue that holds at most 10 messages received from the server. +#[derive(Clone, Debug, Default)] +pub struct MessageQueue { + msgs: VecDeque, +} + +impl PartialEq for MessageQueue { + fn eq(&self, other: &MessageQueue) -> bool { + self.msgs.len() == other.msgs.len() + && self + .msgs + .iter() + .zip(other.msgs.iter()) + .all(|(a, b)| match (a, b) { + (RoomEvent::RoomMessage(msg_a), RoomEvent::RoomMessage(msg_b)) => { + msg_a.event_id == msg_b.event_id + } + _ => false, + }) + } +} + +impl MessageQueue { + /// Create a new empty `MessageQueue`. + pub fn new() -> Self { + Self { + msgs: VecDeque::with_capacity(20), + } + } + + /// Appends a `MessageEvent` to the end of the `MessageQueue`. + /// + /// Removes the oldest element in the queue if there are more than 10 elements. + pub fn push(&mut self, msg: MessageEvent) -> bool { + self.msgs.push_back(RoomEvent::RoomMessage(msg)); + if self.msgs.len() > 10 { + self.msgs.pop_front(); + } + true + } + + pub fn iter(&self) -> impl Iterator { + self.msgs.iter() + } +} + +impl IntoIterator for MessageQueue { + type Item = RoomEvent; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.msgs.into_iter() + } +} + +#[cfg(test)] +mod test { + use super::*; + + use std::collections::HashMap; + use std::convert::TryFrom; + + use crate::events::{collections::all::RoomEvent, EventJson}; + use crate::identifiers::{RoomId, UserId}; + use crate::{state::ClientState, Room}; + + #[test] + fn serialize() { + let id = RoomId::try_from("!roomid:example.com").unwrap(); + let user = UserId::try_from("@example:example.com").unwrap(); + + let mut room = Room::new(&id, &user); + + let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap(); + let event = serde_json::from_str::>(&json).unwrap(); + + let mut msgs = MessageQueue::new(); + if let Ok(ev) = event.deserialize() { + if let RoomEvent::RoomMessage(msg) = ev { + msgs.push(msg); + } + } + room.messages = msgs; + + let mut joined_rooms = HashMap::new(); + joined_rooms.insert(id, room); + + assert_eq!( + r#"{ + "!roomid:example.com": { + "room_id": "!roomid:example.com", + "room_name": { + "name": null, + "canonical_alias": null, + "aliases": [], + "heroes": [], + "joined_member_count": null, + "invited_member_count": null + }, + "own_user_id": "@example:example.com", + "creator": null, + "members": {}, + "messages": [ + { + "type": "m.room.message", + "content": { + "body": "is dancing", + "format": "org.matrix.custom.html", + "formatted_body": "is dancing", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1520372800469, + "sender": "@example:localhost", + "unsigned": { + "age": 598971425 + } + } + ], + "typing_users": [], + "power_levels": null, + "encrypted": false, + "unread_highlight": null, + "unread_notifications": null, + "tombstone": null + } +}"#, + serde_json::to_string_pretty(&joined_rooms).unwrap() + ); + } + + #[test] + fn deserialize() { + let id = RoomId::try_from("!roomid:example.com").unwrap(); + let user = UserId::try_from("@example:example.com").unwrap(); + + let mut room = Room::new(&id, &user); + + let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap(); + let event = serde_json::from_str::>(&json).unwrap(); + + let mut msgs = MessageQueue::new(); + if let Ok(ev) = event.deserialize() { + if let RoomEvent::RoomMessage(msg) = ev { + msgs.push(msg); + } + } + room.messages = msgs; + + let mut joined_rooms = HashMap::new(); + joined_rooms.insert(id, room.clone()); + + let json = r#"{ + "!roomid:example.com": { + "room_id": "!roomid:example.com", + "room_name": { + "name": null, + "canonical_alias": null, + "aliases": [], + "heroes": [], + "joined_member_count": null, + "invited_member_count": null + }, + "own_user_id": "@example:example.com", + "creator": null, + "members": {}, + "messages": [ + { + "type": "m.room.message", + "content": { + "body": "is dancing", + "format": "org.matrix.custom.html", + "formatted_body": "is dancing", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1520372800469, + "sender": "@example:localhost", + "unsigned": { + "age": 598971425 + } + } + ], + "typing_users": [], + "power_levels": null, + "encrypted": false, + "unread_highlight": null, + "unread_notifications": null, + "tombstone": null + } +}"#; + assert_eq!( + joined_rooms, + serde_json::from_str::>(json).unwrap() + ); + } +} diff --git a/matrix_sdk/src/models/mod.rs b/matrix_sdk/src/models/mod.rs index 5e461a32..41f4f461 100644 --- a/matrix_sdk/src/models/mod.rs +++ b/matrix_sdk/src/models/mod.rs @@ -1,4 +1,5 @@ mod event_deser; +mod message; mod room; mod room_member; diff --git a/matrix_sdk/src/models/room.rs b/matrix_sdk/src/models/room.rs index 92c808a7..f73d9409 100644 --- a/matrix_sdk/src/models/room.rs +++ b/matrix_sdk/src/models/room.rs @@ -16,6 +16,7 @@ use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; +use super::message::MessageQueue; use super::RoomMember; use crate::api::r0::sync::sync_events::RoomSummary; @@ -26,6 +27,7 @@ use crate::events::room::{ canonical_alias::CanonicalAliasEvent, encryption::EncryptionEvent, member::{MemberEvent, MembershipChange}, + message::MessageEvent, name::NameEvent, power_levels::{NotificationPowerLevels, PowerLevelsEvent, PowerLevelsEventContent}, tombstone::TombstoneEvent, @@ -36,6 +38,7 @@ use crate::identifiers::{RoomAliasId, RoomId, UserId}; use crate::js_int::{Int, UInt}; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] +#[cfg_attr(test, derive(Clone))] /// `RoomName` allows the calculation of a text room name. pub struct RoomName { /// The displayed name of the room. @@ -58,6 +61,7 @@ pub struct RoomName { } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(test, derive(Clone))] pub struct PowerLevels { /// The level required to ban a user. pub ban: Int, @@ -84,6 +88,7 @@ pub struct PowerLevels { } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(test, derive(Clone))] pub struct Tombstone { /// A server-defined message. body: String, @@ -92,6 +97,7 @@ pub struct Tombstone { } #[derive(Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(test, derive(Clone))] /// A Matrix room. pub struct Room { /// The unique id of the room. @@ -104,6 +110,12 @@ pub struct Room { pub creator: Option, /// The map of room members. pub members: HashMap, + /// A queue of messages no longer than MAX_MSGS + /// + /// This is helpful when using a `StateStore` to avoid multiple requests + /// to the server for messages. + #[serde(with = "super::message::ser_deser")] + pub messages: MessageQueue, /// A list of users that are currently typing. pub typing_users: Vec, /// The power level requirements for specific actions in this room @@ -207,6 +219,7 @@ impl Room { own_user_id: own_user_id.clone(), creator: None, members: HashMap::new(), + messages: MessageQueue::new(), typing_users: Vec::new(), power_levels: None, encrypted: false, @@ -320,6 +333,13 @@ impl Room { } } + /// Handle a room.message event and update the `MessageQueue` if necessary. + /// + /// Returns true if `MessageQueue` was added to. + pub fn handle_message(&mut self, event: &MessageEvent) -> bool { + self.messages.push(event.clone()) + } + /// Handle a room.aliases event, updating the room state if necessary. /// /// Returns true if the room name changed, false otherwise. @@ -396,15 +416,16 @@ impl Room { pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool { match event { // update to the current members of the room - RoomEvent::RoomMember(m) => self.handle_membership(m), + RoomEvent::RoomMember(member) => self.handle_membership(member), // finds all events related to the name of the room for later use - RoomEvent::RoomName(n) => self.handle_room_name(n), - RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), - RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), + RoomEvent::RoomName(name) => self.handle_room_name(name), + RoomEvent::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias), + RoomEvent::RoomAliases(alias) => self.handle_room_aliases(alias), // power levels of the room members - RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), - RoomEvent::RoomTombstone(t) => self.handle_tombstone(t), - RoomEvent::RoomEncryption(e) => self.handle_encryption_event(e), + RoomEvent::RoomPowerLevels(power) => self.handle_power_level(power), + RoomEvent::RoomTombstone(tomb) => self.handle_tombstone(tomb), + RoomEvent::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt), + RoomEvent::RoomMessage(msg) => self.handle_message(msg), _ => false, } } @@ -418,13 +439,16 @@ impl Room { /// * `event` - The event of the room. pub fn receive_state_event(&mut self, event: &StateEvent) -> bool { match event { - StateEvent::RoomMember(m) => self.handle_membership(m), - StateEvent::RoomName(n) => self.handle_room_name(n), - StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), - StateEvent::RoomAliases(a) => self.handle_room_aliases(a), - StateEvent::RoomPowerLevels(p) => self.handle_power_level(p), - StateEvent::RoomTombstone(t) => self.handle_tombstone(t), - StateEvent::RoomEncryption(e) => self.handle_encryption_event(e), + // update to the current members of the room + StateEvent::RoomMember(member) => self.handle_membership(member), + // finds all events related to the name of the room for later use + StateEvent::RoomName(name) => self.handle_room_name(name), + StateEvent::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias), + StateEvent::RoomAliases(alias) => self.handle_room_aliases(alias), + // power levels of the room members + StateEvent::RoomPowerLevels(power) => self.handle_power_level(power), + StateEvent::RoomTombstone(tomb) => self.handle_tombstone(tomb), + StateEvent::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt), _ => false, } } diff --git a/matrix_sdk/src/models/room_member.rs b/matrix_sdk/src/models/room_member.rs index 882d8751..e0270b00 100644 --- a/matrix_sdk/src/models/room_member.rs +++ b/matrix_sdk/src/models/room_member.rs @@ -28,6 +28,7 @@ use serde::{Deserialize, Serialize}; // Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob. #[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(Clone))] /// A Matrix room member. /// pub struct RoomMember { diff --git a/matrix_sdk/src/state/mod.rs b/matrix_sdk/src/state/mod.rs index 3ca6ea7e..78a35807 100644 --- a/matrix_sdk/src/state/mod.rs +++ b/matrix_sdk/src/state/mod.rs @@ -125,6 +125,7 @@ mod test { "own_user_id": "@example:example.com", "creator": null, "members": {}, + "messages": [], "typing_users": [], "power_levels": null, "encrypted": false,