use std::{collections::BTreeMap, time::SystemTime};
use js_int::UInt;
use ruma_identifiers::{EventId, RoomId, UserId};
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{from_value, Value};
use super::{EncryptedFile, ImageInfo, ThumbnailInfo};
use crate::{EventType, FromRaw};
pub mod feedback;
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename = "m.room.message", tag = "type")]
pub struct MessageEvent {
pub content: MessageEventContent,
pub event_id: EventId,
#[serde(with = "ruma_serde::time::ms_since_unix_epoch")]
pub origin_server_ts: SystemTime,
#[serde(skip_serializing_if = "Option::is_none")]
pub room_id: Option<RoomId>,
pub sender: UserId,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub unsigned: BTreeMap<String, Value>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq)]
pub enum MessageEventContent {
Audio(AudioMessageEventContent),
Emote(EmoteMessageEventContent),
File(FileMessageEventContent),
Image(ImageMessageEventContent),
Location(LocationMessageEventContent),
Notice(NoticeMessageEventContent),
ServerNotice(ServerNoticeMessageEventContent),
Text(TextMessageEventContent),
Video(VideoMessageEventContent),
#[doc(hidden)]
__Nonexhaustive,
}
impl FromRaw for MessageEvent {
type Raw = raw::MessageEvent;
fn from_raw(raw: raw::MessageEvent) -> Self {
Self {
content: FromRaw::from_raw(raw.content),
event_id: raw.event_id,
origin_server_ts: raw.origin_server_ts,
room_id: raw.room_id,
sender: raw.sender,
unsigned: raw.unsigned,
}
}
}
impl FromRaw for MessageEventContent {
type Raw = raw::MessageEventContent;
fn from_raw(raw: raw::MessageEventContent) -> Self {
use raw::MessageEventContent::*;
match raw {
Audio(content) => MessageEventContent::Audio(content),
Emote(content) => MessageEventContent::Emote(content),
File(content) => MessageEventContent::File(content),
Image(content) => MessageEventContent::Image(content),
Location(content) => MessageEventContent::Location(content),
Notice(content) => MessageEventContent::Notice(content),
ServerNotice(content) => MessageEventContent::ServerNotice(content),
Text(content) => MessageEventContent::Text(content),
Video(content) => MessageEventContent::Video(content),
__Nonexhaustive => {
unreachable!("It should be impossible to obtain a __Nonexhaustive variant.")
}
}
}
}
impl_room_event!(MessageEvent, MessageEventContent, EventType::RoomMessage);
impl Serialize for MessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error as _;
match *self {
MessageEventContent::Audio(ref content) => content.serialize(serializer),
MessageEventContent::Emote(ref content) => content.serialize(serializer),
MessageEventContent::File(ref content) => content.serialize(serializer),
MessageEventContent::Image(ref content) => content.serialize(serializer),
MessageEventContent::Location(ref content) => content.serialize(serializer),
MessageEventContent::Notice(ref content) => content.serialize(serializer),
MessageEventContent::ServerNotice(ref content) => content.serialize(serializer),
MessageEventContent::Text(ref content) => content.serialize(serializer),
MessageEventContent::Video(ref content) => content.serialize(serializer),
MessageEventContent::__Nonexhaustive => Err(S::Error::custom(
"Attempted to deserialize __Nonexhaustive variant.",
)),
}
}
}
pub(crate) mod raw {
use super::*;
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct MessageEvent {
pub content: MessageEventContent,
pub event_id: EventId,
#[serde(with = "ruma_serde::time::ms_since_unix_epoch")]
pub origin_server_ts: SystemTime,
pub room_id: Option<RoomId>,
pub sender: UserId,
#[serde(default)]
pub unsigned: BTreeMap<String, Value>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq)]
pub enum MessageEventContent {
Audio(AudioMessageEventContent),
Emote(EmoteMessageEventContent),
File(FileMessageEventContent),
Image(ImageMessageEventContent),
Location(LocationMessageEventContent),
Notice(NoticeMessageEventContent),
ServerNotice(ServerNoticeMessageEventContent),
Text(TextMessageEventContent),
Video(VideoMessageEventContent),
#[doc(hidden)]
__Nonexhaustive,
}
impl<'de> Deserialize<'de> for MessageEventContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error as _;
let value: Value = Deserialize::deserialize(deserializer)?;
let message_type_value = match value.get("msgtype") {
Some(value) => value.clone(),
None => return Err(D::Error::missing_field("msgtype")),
};
let message_type = match from_value::<MessageType>(message_type_value) {
Ok(message_type) => message_type,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
match message_type {
MessageType::Audio => {
let content = match from_value::<AudioMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::Audio(content))
}
MessageType::Emote => {
let content = match from_value::<EmoteMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::Emote(content))
}
MessageType::File => {
let content = match from_value::<FileMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::File(content))
}
MessageType::Image => {
let content = match from_value::<ImageMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::Image(content))
}
MessageType::Location => {
let content = match from_value::<LocationMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::Location(content))
}
MessageType::Notice => {
let content = match from_value::<NoticeMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::Notice(content))
}
MessageType::ServerNotice => {
let content = match from_value::<ServerNoticeMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::ServerNotice(content))
}
MessageType::Text => {
let content = match from_value::<TextMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::Text(content))
}
MessageType::Video => {
let content = match from_value::<VideoMessageEventContent>(value) {
Ok(content) => content,
Err(error) => return Err(D::Error::custom(error.to_string())),
};
Ok(MessageEventContent::Video(content))
}
MessageType::__Nonexhaustive => Err(D::Error::custom(
"Attempted to deserialize __Nonexhaustive variant.",
)),
}
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub enum MessageType {
#[serde(rename = "m.audio")]
Audio,
#[serde(rename = "m.emote")]
Emote,
#[serde(rename = "m.file")]
File,
#[serde(rename = "m.image")]
Image,
#[serde(rename = "m.location")]
Location,
#[serde(rename = "m.notice")]
Notice,
#[serde(rename = "m.server_notice")]
ServerNotice,
#[serde(rename = "m.text")]
Text,
#[serde(rename = "m.video")]
Video,
#[doc(hidden)]
#[serde(skip)]
__Nonexhaustive,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct AudioMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<AudioInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<EncryptedFile>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct AudioInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct EmoteMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub formatted_body: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct FileMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<FileInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<EncryptedFile>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct FileInfo {
pub mimetype: Option<String>,
pub size: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<ThumbnailInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<EncryptedFile>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ImageMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<ImageInfo>,
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<EncryptedFile>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct LocationMessageEventContent {
pub body: String,
pub geo_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<LocationInfo>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct LocationInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<ThumbnailInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<EncryptedFile>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct NoticeMessageEventContent {
pub body: String,
#[serde(rename = "m.relates_to")]
#[serde(skip_serializing_if = "Option::is_none")]
pub relates_to: Option<RelatesTo>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ServerNoticeMessageEventContent {
pub body: String,
pub server_notice_type: ServerNoticeType,
pub admin_contact: Option<String>,
pub limit_type: Option<LimitType>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub enum ServerNoticeType {
#[serde(rename = "m.server_notice.usage_limit_reached")]
UsageLimitReached,
#[doc(hidden)]
#[serde(skip)]
__Nonexhaustive,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub enum LimitType {
#[serde(rename = "monthly_active_user")]
MonthlyActiveUser,
#[doc(hidden)]
#[serde(skip)]
__Nonexhaustive,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct TextMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub formatted_body: Option<String>,
#[serde(rename = "m.relates_to")]
#[serde(skip_serializing_if = "Option::is_none")]
pub relates_to: Option<RelatesTo>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct VideoMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<VideoInfo>,
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<EncryptedFile>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct VideoInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<UInt>,
#[serde(rename = "h")]
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
#[serde(rename = "w")]
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<ThumbnailInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<EncryptedFile>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RelatesTo {
#[serde(rename = "m.in_reply_to")]
pub in_reply_to: InReplyTo,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct InReplyTo {
pub event_id: EventId,
}
impl_enum! {
MessageType {
Audio => "m.audio",
Emote => "m.emote",
File => "m.file",
Image => "m.image",
Location => "m.location",
Notice => "m.notice",
ServerNotice => "m.server_notice",
Text => "m.text",
Video => "m.video",
}
}
impl TextMessageEventContent {
pub fn new_plain(body: impl Into<String>) -> TextMessageEventContent {
TextMessageEventContent {
body: body.into(),
format: None,
formatted_body: None,
relates_to: None,
}
}
}
impl Serialize for AudioMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.info.is_some() {
len += 1;
}
if self.url.is_some() {
len += 1;
}
if self.file.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("AudioMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
if self.info.is_some() {
state.serialize_field("info", &self.info)?;
}
state.serialize_field("msgtype", "m.audio")?;
if self.url.is_some() {
state.serialize_field("url", &self.url)?;
}
if self.file.is_some() {
state.serialize_field("file", &self.file)?;
}
state.end()
}
}
impl Serialize for EmoteMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.format.is_some() {
len += 1;
}
if self.formatted_body.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("EmoteMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
if self.format.is_some() {
state.serialize_field("format", &self.format)?;
}
if self.formatted_body.is_some() {
state.serialize_field("formatted_body", &self.formatted_body)?;
}
state.serialize_field("msgtype", "m.emote")?;
state.end()
}
}
impl Serialize for FileMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.filename.is_some() {
len += 1;
}
if self.info.is_some() {
len += 1;
}
if self.url.is_some() {
len += 1;
}
if self.file.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("FileMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
if self.filename.is_some() {
state.serialize_field("filename", &self.filename)?;
}
state.serialize_field("msgtype", "m.file")?;
if self.info.is_some() {
state.serialize_field("info", &self.info)?;
}
if self.url.is_some() {
state.serialize_field("url", &self.url)?;
}
if self.file.is_some() {
state.serialize_field("file", &self.file)?;
}
state.end()
}
}
impl Serialize for ImageMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.info.is_some() {
len += 1;
}
if self.url.is_some() {
len += 1;
}
if self.file.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("ImageMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
state.serialize_field("msgtype", "m.image")?;
if self.info.is_some() {
state.serialize_field("info", &self.info)?;
}
if self.url.is_some() {
state.serialize_field("url", &self.url)?;
}
if self.file.is_some() {
state.serialize_field("file", &self.file)?;
}
state.end()
}
}
impl Serialize for LocationMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 3;
if self.info.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("LocationMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
state.serialize_field("geo_uri", &self.geo_uri)?;
state.serialize_field("msgtype", "m.location")?;
if self.info.is_some() {
state.serialize_field("info", &self.info)?;
}
state.end()
}
}
impl Serialize for NoticeMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.relates_to.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("NoticeMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
state.serialize_field("msgtype", "m.notice")?;
if self.relates_to.is_some() {
state.serialize_field("m.relates_to", &self.relates_to)?;
}
state.end()
}
}
impl Serialize for ServerNoticeMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 3;
if self.admin_contact.is_some() {
len += 1;
}
if self.limit_type.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("ServerNoticeMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
state.serialize_field("msgtype", "m.server_notice")?;
state.serialize_field("server_notice_type", &self.server_notice_type)?;
if self.admin_contact.is_some() {
state.serialize_field("admin_contact", &self.admin_contact)?;
}
if self.limit_type.is_some() {
state.serialize_field("limit_type", &self.limit_type)?;
}
state.end()
}
}
impl Serialize for TextMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.format.is_some() {
len += 1;
}
if self.formatted_body.is_some() {
len += 1;
}
if self.relates_to.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("TextMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
if self.format.is_some() {
state.serialize_field("format", &self.format)?;
}
if self.formatted_body.is_some() {
state.serialize_field("formatted_body", &self.formatted_body)?;
}
state.serialize_field("msgtype", "m.text")?;
if self.relates_to.is_some() {
state.serialize_field("m.relates_to", &self.relates_to)?;
}
state.end()
}
}
impl Serialize for VideoMessageEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.info.is_some() {
len += 1;
}
if self.url.is_some() {
len += 1;
}
if self.file.is_some() {
len += 1;
}
let mut state = serializer.serialize_struct("VideoMessageEventContent", len)?;
state.serialize_field("body", &self.body)?;
state.serialize_field("msgtype", "m.video")?;
if self.info.is_some() {
state.serialize_field("info", &self.info)?;
}
if self.url.is_some() {
state.serialize_field("url", &self.url)?;
}
if self.file.is_some() {
state.serialize_field("file", &self.file)?;
}
state.end()
}
}
#[cfg(test)]
mod tests {
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{AudioMessageEventContent, MessageEventContent};
use crate::room::message::{InReplyTo, RelatesTo, TextMessageEventContent};
use crate::EventResult;
use ruma_identifiers::EventId;
use std::convert::TryFrom;
#[test]
fn serialization() {
let message_event_content = MessageEventContent::Audio(AudioMessageEventContent {
body: "test".to_string(),
info: None,
url: Some("http://example.com/audio.mp3".to_string()),
file: None,
});
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "test",
"msgtype": "m.audio",
"url": "http://example.com/audio.mp3"
})
);
}
#[test]
fn plain_text() {
let message_event_content = MessageEventContent::Text(TextMessageEventContent::new_plain(
"> <@test:example.com> test\n\ntest reply",
));
assert_eq!(
to_json_value(&message_event_content).unwrap(),
json!({
"body": "> <@test:example.com> test\n\ntest reply",
"msgtype": "m.text"
})
);
}
#[test]
fn relates_to_serialization() {
let message_event_content = MessageEventContent::Text(TextMessageEventContent {
body: "> <@test:example.com> test\n\ntest reply".to_owned(),
format: None,
formatted_body: None,
relates_to: Some(RelatesTo {
in_reply_to: InReplyTo {
event_id: EventId::try_from("$15827405538098VGFWH:example.com").unwrap(),
},
}),
});
let json_data = json!({
"body": "> <@test:example.com> test\n\ntest reply",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$15827405538098VGFWH:example.com"
}
}
});
assert_eq!(to_json_value(&message_event_content).unwrap(), json_data);
}
#[test]
fn deserialization() {
let message_event_content = MessageEventContent::Audio(AudioMessageEventContent {
body: "test".to_string(),
info: None,
url: Some("http://example.com/audio.mp3".to_string()),
file: None,
});
let json_data = json!({
"body": "test",
"msgtype": "m.audio",
"url": "http://example.com/audio.mp3"
});
assert_eq!(
from_json_value::<EventResult<MessageEventContent>>(json_data)
.unwrap()
.into_result()
.unwrap(),
message_event_content
);
}
#[test]
fn deserialization_failure() {
let json_data = json!({
"body": "test","msgtype": "m.location",
"url": "http://example.com/audio.mp3"
});
assert!(
from_json_value::<EventResult<MessageEventContent>>(json_data)
.unwrap()
.into_result()
.is_err()
);
}
}