//! "Stripped-down" versions of the core state events. //! //! Each "stripped" event includes only the `content`, `type`, and `state_key` fields of its full //! version. These stripped types are useful for APIs where the user is providing the content of a //! state event to be created, when the other fields can be inferred from a larger context, or where //! the other fields are otherwise inapplicable. use ruma_identifiers::UserId; use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use crate::{ room::{ aliases::AliasesEventContent, avatar::AvatarEventContent, canonical_alias::CanonicalAliasEventContent, create::CreateEventContent, guest_access::GuestAccessEventContent, history_visibility::HistoryVisibilityEventContent, join_rules::JoinRulesEventContent, member::MemberEventContent, name::NameEventContent, power_levels::PowerLevelsEventContent, third_party_invite::ThirdPartyInviteEventContent, topic::TopicEventContent, }, EventType, TryFromRaw, }; /// A stripped-down version of a state event that is included along with some other events. #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum StrippedState { /// A stripped-down version of the *m.room.aliases* event. RoomAliases(StrippedRoomAliases), /// A stripped-down version of the *m.room.avatar* event. RoomAvatar(StrippedRoomAvatar), /// A stripped-down version of the *m.room.canonical_alias* event. RoomCanonicalAlias(StrippedRoomCanonicalAlias), /// A striped-down version of the *m.room.create* event. RoomCreate(StrippedRoomCreate), /// A stripped-down version of the *m.room.guest_access* event. RoomGuestAccess(StrippedRoomGuestAccess), /// A stripped-down version of the *m.room.history_visibility* event. RoomHistoryVisibility(StrippedRoomHistoryVisibility), /// A stripped-down version of the *m.room.join_rules* event. RoomJoinRules(StrippedRoomJoinRules), /// A stripped-down version of the *m.room.member* event. RoomMember(StrippedRoomMember), /// A stripped-down version of the *m.room.name* event. RoomName(StrippedRoomName), /// A stripped-down version of the *m.room.power_levels* event. RoomPowerLevels(StrippedRoomPowerLevels), /// A stripped-down version of the *m.room.third_party_invite* event. RoomThirdPartyInvite(StrippedRoomThirdPartyInvite), /// A stripped-down version of the *m.room.topic* event. RoomTopic(StrippedRoomTopic), } /// A "stripped-down" version of a core state event. #[derive(Clone, Debug, PartialEq, Serialize)] pub struct StrippedStateContent { /// Data specific to the event type. pub content: C, // FIXME(jplatte): It's unclear to me why this is stored /// The type of the event. #[serde(rename = "type")] pub event_type: EventType, /// A key that determines which piece of room state the event represents. pub state_key: String, /// The unique identifier for the user who sent this event. pub sender: UserId, } /// A stripped-down version of the *m.room.aliases* event. pub type StrippedRoomAliases = StrippedStateContent; /// A stripped-down version of the *m.room.avatar* event. pub type StrippedRoomAvatar = StrippedStateContent; /// A stripped-down version of the *m.room.canonical_alias* event. pub type StrippedRoomCanonicalAlias = StrippedStateContent; /// A stripped-down version of the *m.room.create* event. pub type StrippedRoomCreate = StrippedStateContent; /// A stripped-down version of the *m.room.guest_access* event. pub type StrippedRoomGuestAccess = StrippedStateContent; /// A stripped-down version of the *m.room.history_visibility* event. pub type StrippedRoomHistoryVisibility = StrippedStateContent; /// A stripped-down version of the *m.room.join_rules* event. pub type StrippedRoomJoinRules = StrippedStateContent; /// A stripped-down version of the *m.room.member* event. pub type StrippedRoomMember = StrippedStateContent; /// A stripped-down version of the *m.room.name* event. pub type StrippedRoomName = StrippedStateContent; /// A stripped-down version of the *m.room.power_levels* event. pub type StrippedRoomPowerLevels = StrippedStateContent; /// A stripped-down version of the *m.room.third_party_invite* event. pub type StrippedRoomThirdPartyInvite = StrippedStateContent; /// A stripped-down version of the *m.room.topic* event. pub type StrippedRoomTopic = StrippedStateContent; impl TryFromRaw for StrippedState { type Raw = raw::StrippedState; type Err = String; fn try_from_raw(raw: raw::StrippedState) -> Result { use raw::StrippedState::*; fn convert( raw_variant: fn(T::Raw) -> raw::StrippedState, variant: fn(T) -> StrippedState, raw: T::Raw, ) -> Result { T::try_from_raw(raw) .map(variant) .map_err(|(msg, raw)| (msg.into(), raw_variant(raw))) } match raw { RoomAliases(c) => convert(RoomAliases, Self::RoomAliases, c), RoomAvatar(c) => convert(RoomAvatar, Self::RoomAvatar, c), RoomCanonicalAlias(c) => convert(RoomCanonicalAlias, Self::RoomCanonicalAlias, c), RoomCreate(c) => convert(RoomCreate, Self::RoomCreate, c), RoomGuestAccess(c) => convert(RoomGuestAccess, Self::RoomGuestAccess, c), RoomHistoryVisibility(c) => { convert(RoomHistoryVisibility, Self::RoomHistoryVisibility, c) } RoomJoinRules(c) => convert(RoomJoinRules, Self::RoomJoinRules, c), RoomMember(c) => convert(RoomMember, Self::RoomMember, c), RoomName(c) => convert(RoomName, Self::RoomName, c), RoomPowerLevels(c) => convert(RoomPowerLevels, Self::RoomPowerLevels, c), RoomThirdPartyInvite(c) => convert(RoomThirdPartyInvite, Self::RoomThirdPartyInvite, c), RoomTopic(c) => convert(RoomTopic, Self::RoomTopic, c), } } } impl TryFromRaw for StrippedStateContent where C: TryFromRaw, { type Raw = StrippedStateContent; type Err = C::Err; fn try_from_raw(mut raw: StrippedStateContent) -> Result { Ok(Self { content: match C::try_from_raw(raw.content) { Ok(c) => c, Err((msg, raw_content)) => { // we moved raw.content, so we need to put it back before returning raw raw.content = raw_content; return Err((msg, raw)); } }, event_type: raw.event_type, state_key: raw.state_key, sender: raw.sender, }) } } impl Serialize for StrippedState { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match *self { StrippedState::RoomAliases(ref event) => event.serialize(serializer), StrippedState::RoomAvatar(ref event) => event.serialize(serializer), StrippedState::RoomCanonicalAlias(ref event) => event.serialize(serializer), StrippedState::RoomCreate(ref event) => event.serialize(serializer), StrippedState::RoomGuestAccess(ref event) => event.serialize(serializer), StrippedState::RoomHistoryVisibility(ref event) => event.serialize(serializer), StrippedState::RoomJoinRules(ref event) => event.serialize(serializer), StrippedState::RoomMember(ref event) => event.serialize(serializer), StrippedState::RoomName(ref event) => event.serialize(serializer), StrippedState::RoomPowerLevels(ref event) => event.serialize(serializer), StrippedState::RoomThirdPartyInvite(ref event) => event.serialize(serializer), StrippedState::RoomTopic(ref event) => event.serialize(serializer), } } } impl<'de, C> Deserialize<'de> for StrippedStateContent where C: DeserializeOwned, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::Error as _; use serde_json::from_value; let conv_err = |error: serde_json::Error| D::Error::custom(error.to_string()); // TODO: Optimize let value = Value::deserialize(deserializer)?; let event_type = from_value( value .get("type") .map(Clone::clone) .ok_or_else(|| D::Error::missing_field("type"))?, ) .map_err(conv_err)?; let content = from_value( value .get("content") .map(Clone::clone) .ok_or_else(|| D::Error::missing_field("content"))?, ) .map_err(conv_err)?; let sender = from_value( value .get("sender") .map(Clone::clone) .ok_or_else(|| D::Error::missing_field("sender"))?, ) .map_err(conv_err)?; let state_key = from_value( value .get("state_key") .map(Clone::clone) .ok_or_else(|| D::Error::missing_field("state_key"))?, ) .map_err(conv_err)?; Ok(Self { content, event_type, state_key, sender, }) } } mod raw { use serde::{Deserialize, Deserializer}; use super::StrippedStateContent; use crate::room::{ aliases::raw::AliasesEventContent, avatar::raw::AvatarEventContent, canonical_alias::raw::CanonicalAliasEventContent, create::raw::CreateEventContent, guest_access::raw::GuestAccessEventContent, history_visibility::raw::HistoryVisibilityEventContent, join_rules::raw::JoinRulesEventContent, member::raw::MemberEventContent, name::raw::NameEventContent, power_levels::raw::PowerLevelsEventContent, third_party_invite::raw::ThirdPartyInviteEventContent, topic::raw::TopicEventContent, }; /// A stripped-down version of the *m.room.aliases* event. pub type StrippedRoomAliases = StrippedStateContent; /// A stripped-down version of the *m.room.avatar* event. pub type StrippedRoomAvatar = StrippedStateContent; /// A stripped-down version of the *m.room.canonical_alias* event. pub type StrippedRoomCanonicalAlias = StrippedStateContent; /// A stripped-down version of the *m.room.create* event. pub type StrippedRoomCreate = StrippedStateContent; /// A stripped-down version of the *m.room.guest_access* event. pub type StrippedRoomGuestAccess = StrippedStateContent; /// A stripped-down version of the *m.room.history_visibility* event. pub type StrippedRoomHistoryVisibility = StrippedStateContent; /// A stripped-down version of the *m.room.join_rules* event. pub type StrippedRoomJoinRules = StrippedStateContent; /// A stripped-down version of the *m.room.member* event. pub type StrippedRoomMember = StrippedStateContent; /// A stripped-down version of the *m.room.name* event. pub type StrippedRoomName = StrippedStateContent; /// A stripped-down version of the *m.room.power_levels* event. pub type StrippedRoomPowerLevels = StrippedStateContent; /// A stripped-down version of the *m.room.third_party_invite* event. pub type StrippedRoomThirdPartyInvite = StrippedStateContent; /// A stripped-down version of the *m.room.topic* event. pub type StrippedRoomTopic = StrippedStateContent; /// A stripped-down version of a state event that is included along with some other events. #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum StrippedState { /// A stripped-down version of the *m.room.aliases* event. RoomAliases(StrippedRoomAliases), /// A stripped-down version of the *m.room.avatar* event. RoomAvatar(StrippedRoomAvatar), /// A stripped-down version of the *m.room.canonical_alias* event. RoomCanonicalAlias(StrippedRoomCanonicalAlias), /// A striped-down version of the *m.room.create* event. RoomCreate(StrippedRoomCreate), /// A stripped-down version of the *m.room.guest_access* event. RoomGuestAccess(StrippedRoomGuestAccess), /// A stripped-down version of the *m.room.history_visibility* event. RoomHistoryVisibility(StrippedRoomHistoryVisibility), /// A stripped-down version of the *m.room.join_rules* event. RoomJoinRules(StrippedRoomJoinRules), /// A stripped-down version of the *m.room.member* event. RoomMember(StrippedRoomMember), /// A stripped-down version of the *m.room.name* event. RoomName(StrippedRoomName), /// A stripped-down version of the *m.room.power_levels* event. RoomPowerLevels(StrippedRoomPowerLevels), /// A stripped-down version of the *m.room.third_party_invite* event. RoomThirdPartyInvite(StrippedRoomThirdPartyInvite), /// A stripped-down version of the *m.room.topic* event. RoomTopic(StrippedRoomTopic), } impl<'de> Deserialize<'de> for StrippedState { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { use crate::EventType::*; use serde::de::Error as _; use serde_json::{from_value, Value}; let conv_err = |error: serde_json::Error| D::Error::custom(error.to_string()); // TODO: Optimize let value = Value::deserialize(deserializer)?; let event_type = from_value( value .get("type") .map(Clone::clone) .ok_or_else(|| D::Error::missing_field("type"))?, ) .map_err(conv_err)?; Ok(match event_type { RoomAliases => StrippedState::RoomAliases(from_value(value).map_err(conv_err)?), RoomAvatar => Self::RoomAvatar(from_value(value).map_err(conv_err)?), RoomCanonicalAlias => { Self::RoomCanonicalAlias(from_value(value).map_err(conv_err)?) } RoomCreate => Self::RoomCreate(from_value(value).map_err(conv_err)?), RoomGuestAccess => Self::RoomGuestAccess(from_value(value).map_err(conv_err)?), RoomHistoryVisibility => { Self::RoomHistoryVisibility(from_value(value).map_err(conv_err)?) } RoomJoinRules => Self::RoomJoinRules(from_value(value).map_err(conv_err)?), RoomMember => Self::RoomMember(from_value(value).map_err(conv_err)?), RoomName => Self::RoomName(from_value(value).map_err(conv_err)?), RoomPowerLevels => Self::RoomPowerLevels(from_value(value).map_err(conv_err)?), RoomThirdPartyInvite => { Self::RoomThirdPartyInvite(from_value(value).map_err(conv_err)?) } RoomTopic => Self::RoomTopic(from_value(value).map_err(conv_err)?), _ => return Err(D::Error::custom("not a state event")), }) } } } #[cfg(test)] mod tests { use std::convert::TryFrom; use js_int::UInt; use ruma_identifiers::UserId; use serde_json::to_string; use super::{StrippedRoomName, StrippedRoomTopic, StrippedState}; use crate::{ room::{join_rules::JoinRule, topic::TopicEventContent}, EventResult, EventType, }; #[test] fn serialize_stripped_state_event() { let content = StrippedRoomTopic { content: TopicEventContent { topic: "Testing room".to_string(), }, state_key: "".to_string(), event_type: EventType::RoomTopic, sender: UserId::try_from("@example:localhost").unwrap(), }; let event = StrippedState::RoomTopic(content); assert_eq!( to_string(&event).unwrap(), r#"{"content":{"topic":"Testing room"},"type":"m.room.topic","state_key":"","sender":"@example:localhost"}"# ); } #[test] fn deserialize_stripped_state_events() { let name_event = r#"{ "type": "m.room.name", "state_key": "", "sender": "@example:localhost", "content": {"name": "Ruma"} }"#; let join_rules_event = r#"{ "type": "m.room.join_rules", "state_key": "", "sender": "@example:localhost", "content": { "join_rule": "public" } }"#; let avatar_event = r#"{ "type": "m.room.avatar", "state_key": "", "sender": "@example:localhost", "content": { "info": { "h": 128, "w": 128, "mimetype": "image/jpeg", "size": 1024, "thumbnail_info": { "h": 16, "w": 16, "mimetype": "image/jpeg", "size": 32 }, "thumbnail_url": "https://domain.com/image-thumbnail.jpg" }, "thumbnail_info": { "h": 16, "w": 16, "mimetype": "image/jpeg", "size": 32 }, "thumbnail_url": "https://domain.com/image-thumbnail.jpg", "url": "https://domain.com/image.jpg" } }"#; match serde_json::from_str::>(name_event) .unwrap() .into_result() .unwrap() { StrippedState::RoomName(event) => { assert_eq!(event.content.name, Some("Ruma".to_string())); assert_eq!(event.event_type, EventType::RoomName); assert_eq!(event.state_key, ""); assert_eq!(event.sender.to_string(), "@example:localhost"); } _ => unreachable!(), }; // Ensure `StrippedStateContent` can be parsed, not just `StrippedState`. assert!( serde_json::from_str::>(name_event) .unwrap() .into_result() .is_ok() ); match serde_json::from_str::>(join_rules_event) .unwrap() .into_result() .unwrap() { StrippedState::RoomJoinRules(event) => { assert_eq!(event.content.join_rule, JoinRule::Public); assert_eq!(event.event_type, EventType::RoomJoinRules); assert_eq!(event.state_key, ""); assert_eq!(event.sender.to_string(), "@example:localhost"); } _ => unreachable!(), }; match serde_json::from_str::>(avatar_event) .unwrap() .into_result() .unwrap() { StrippedState::RoomAvatar(event) => { let image_info = event.content.info.unwrap(); assert_eq!(image_info.height.unwrap(), UInt::try_from(128).unwrap()); assert_eq!(image_info.width.unwrap(), UInt::try_from(128).unwrap()); assert_eq!(image_info.mimetype.unwrap(), "image/jpeg"); assert_eq!(image_info.size.unwrap(), UInt::try_from(1024).unwrap()); assert_eq!( image_info.thumbnail_info.unwrap().size.unwrap(), UInt::try_from(32).unwrap() ); assert_eq!(event.content.url, "https://domain.com/image.jpg"); assert_eq!(event.event_type, EventType::RoomAvatar); assert_eq!(event.state_key, ""); assert_eq!(event.sender.to_string(), "@example:localhost"); } _ => unreachable!(), }; } }