From e0db68241d17be89619abd0b318274add40707c1 Mon Sep 17 00:00:00 2001
From: Doug <6060466+pixlwave@users.noreply.github.com>
Date: Mon, 7 Oct 2024 10:39:45 +0100
Subject: [PATCH] events: Add helpers for media captions to audio, file, image
and video messages
---
crates/ruma-events/CHANGELOG.md | 1 +
crates/ruma-events/src/room/message.rs | 1 +
crates/ruma-events/src/room/message/audio.rs | 20 ++-
crates/ruma-events/src/room/message/file.rs | 20 ++-
crates/ruma-events/src/room/message/image.rs | 20 ++-
.../src/room/message/media_caption.rs | 22 ++++
crates/ruma-events/src/room/message/video.rs | 20 ++-
crates/ruma-events/tests/it/room_message.rs | 117 +++++++++++++++++-
8 files changed, 212 insertions(+), 9 deletions(-)
create mode 100644 crates/ruma-events/src/room/message/media_caption.rs
diff --git a/crates/ruma-events/CHANGELOG.md b/crates/ruma-events/CHANGELOG.md
index 683bbc6d..48879d4a 100644
--- a/crates/ruma-events/CHANGELOG.md
+++ b/crates/ruma-events/CHANGELOG.md
@@ -23,6 +23,7 @@ The new format (Session) is required to reliably display the call member count (
`CallMemberEventContent` is now an enum to model the two different formats.
- `CallMemberStateKey` (instead of `OwnedUserId`) is now used as the state key type for `CallMemberEventContent`.
This guarantees correct formatting of the event key.
+- Add helpers for captions on audio, file, image and video messages.
Breaking changes:
diff --git a/crates/ruma-events/src/room/message.rs b/crates/ruma-events/src/room/message.rs
index 39f7475a..2f1d016e 100644
--- a/crates/ruma-events/src/room/message.rs
+++ b/crates/ruma-events/src/room/message.rs
@@ -30,6 +30,7 @@ mod file;
mod image;
mod key_verification_request;
mod location;
+mod media_caption;
mod notice;
mod relation;
pub(crate) mod relation_serde;
diff --git a/crates/ruma-events/src/room/message/audio.rs b/crates/ruma-events/src/room/message/audio.rs
index edd59f7e..438e3f5d 100644
--- a/crates/ruma-events/src/room/message/audio.rs
+++ b/crates/ruma-events/src/room/message/audio.rs
@@ -5,7 +5,10 @@ use ruma_common::OwnedMxcUri;
use serde::{Deserialize, Serialize};
use super::FormattedBody;
-use crate::room::{EncryptedFile, MediaSource};
+use crate::room::{
+ message::media_caption::{caption, formatted_caption},
+ EncryptedFile, MediaSource,
+};
/// The payload for an audio message.
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -88,6 +91,21 @@ impl AudioMessageEventContent {
pub fn info(self, info: impl Into>>) -> Self {
Self { info: info.into(), ..self }
}
+
+ /// Returns the caption for the audio as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// In short, this is the `body` field if the `filename` field exists and has a different value,
+ /// otherwise the media file does not have a caption.
+ pub fn caption(&self) -> Option<&str> {
+ caption(&self.body, self.filename.as_deref())
+ }
+
+ /// Returns the formatted caption for the audio as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// This is the same as `caption`, but returns the formatted body instead of the plain body.
+ pub fn formatted_caption(&self) -> Option<&FormattedBody> {
+ formatted_caption(&self.body, self.formatted.as_ref(), self.filename.as_deref())
+ }
}
/// Metadata about an audio clip.
diff --git a/crates/ruma-events/src/room/message/file.rs b/crates/ruma-events/src/room/message/file.rs
index 3f7abb51..0edbc634 100644
--- a/crates/ruma-events/src/room/message/file.rs
+++ b/crates/ruma-events/src/room/message/file.rs
@@ -3,7 +3,10 @@ use ruma_common::OwnedMxcUri;
use serde::{Deserialize, Serialize};
use super::FormattedBody;
-use crate::room::{EncryptedFile, MediaSource, ThumbnailInfo};
+use crate::room::{
+ message::media_caption::{caption, formatted_caption},
+ EncryptedFile, MediaSource, ThumbnailInfo,
+};
/// The payload for a file message.
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -60,6 +63,21 @@ impl FileMessageEventContent {
pub fn info(self, info: impl Into >>) -> Self {
Self { info: info.into(), ..self }
}
+
+ /// Returns the caption of the media file as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// In short, this is the `body` field if the `filename` field exists and has a different value,
+ /// otherwise the media file does not have a caption.
+ pub fn caption(&self) -> Option<&str> {
+ caption(&self.body, self.filename.as_deref())
+ }
+
+ /// Returns the formatted caption of the media file as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// This is the same as `caption`, but returns the formatted body instead of the plain body.
+ pub fn formatted_caption(&self) -> Option<&FormattedBody> {
+ formatted_caption(&self.body, self.formatted.as_ref(), self.filename.as_deref())
+ }
}
/// Metadata about a file.
diff --git a/crates/ruma-events/src/room/message/image.rs b/crates/ruma-events/src/room/message/image.rs
index 674be973..29e1c60c 100644
--- a/crates/ruma-events/src/room/message/image.rs
+++ b/crates/ruma-events/src/room/message/image.rs
@@ -2,7 +2,10 @@ use ruma_common::OwnedMxcUri;
use serde::{Deserialize, Serialize};
use super::FormattedBody;
-use crate::room::{EncryptedFile, ImageInfo, MediaSource};
+use crate::room::{
+ message::media_caption::{caption, formatted_caption},
+ EncryptedFile, ImageInfo, MediaSource,
+};
/// The payload for an image message.
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -59,4 +62,19 @@ impl ImageMessageEventContent {
pub fn info(self, info: impl Into >>) -> Self {
Self { info: info.into(), ..self }
}
+
+ /// Returns the caption for the image as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// In short, this is the `body` field if the `filename` field exists and has a different value,
+ /// otherwise the media file does not have a caption.
+ pub fn caption(&self) -> Option<&str> {
+ caption(&self.body, self.filename.as_deref())
+ }
+
+ /// Returns the formatted caption for the image as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// This is the same as `caption`, but returns the formatted body instead of the plain body.
+ pub fn formatted_caption(&self) -> Option<&FormattedBody> {
+ formatted_caption(&self.body, self.formatted.as_ref(), self.filename.as_deref())
+ }
}
diff --git a/crates/ruma-events/src/room/message/media_caption.rs b/crates/ruma-events/src/room/message/media_caption.rs
new file mode 100644
index 00000000..a24a688e
--- /dev/null
+++ b/crates/ruma-events/src/room/message/media_caption.rs
@@ -0,0 +1,22 @@
+//! Reusable methods for captioning media files.
+
+use crate::room::message::FormattedBody;
+
+/// Computes the caption of a media file as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+///
+/// In short, this is the `body` field if the `filename` field exists and has a different value,
+/// otherwise the media file does not have a caption.
+pub(crate) fn caption<'a>(body: &'a str, filename: Option<&str>) -> Option<&'a str> {
+ filename.is_some_and(|filename| body != filename).then_some(body)
+}
+
+/// Computes the formatted caption of a media file as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+///
+/// This is the same as `caption`, but returns the formatted body instead of the plain body.
+pub(crate) fn formatted_caption<'a>(
+ body: &str,
+ formatted: Option<&'a FormattedBody>,
+ filename: Option<&str>,
+) -> Option<&'a FormattedBody> {
+ filename.is_some_and(|filename| body != filename).then_some(formatted).flatten()
+}
diff --git a/crates/ruma-events/src/room/message/video.rs b/crates/ruma-events/src/room/message/video.rs
index 00d9dd00..df8ac0c1 100644
--- a/crates/ruma-events/src/room/message/video.rs
+++ b/crates/ruma-events/src/room/message/video.rs
@@ -5,7 +5,10 @@ use ruma_common::OwnedMxcUri;
use serde::{Deserialize, Serialize};
use super::FormattedBody;
-use crate::room::{EncryptedFile, MediaSource, ThumbnailInfo};
+use crate::room::{
+ message::media_caption::{caption, formatted_caption},
+ EncryptedFile, MediaSource, ThumbnailInfo,
+};
/// The payload for a video message.
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -62,6 +65,21 @@ impl VideoMessageEventContent {
pub fn info(self, info: impl Into >>) -> Self {
Self { info: info.into(), ..self }
}
+
+ /// Returns the caption of the video as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// In short, this is the `body` field if the `filename` field exists and has a different value,
+ /// otherwise the media file does not have a caption.
+ pub fn caption(&self) -> Option<&str> {
+ caption(&self.body, self.filename.as_deref())
+ }
+
+ /// Returns the formatted caption of the video as defined by the [spec](https://spec.matrix.org/latest/client-server-api/#media-captions).
+ ///
+ /// This is the same as `caption`, but returns the formatted body instead of the plain body.
+ pub fn formatted_caption(&self) -> Option<&FormattedBody> {
+ formatted_caption(&self.body, self.formatted.as_ref(), self.filename.as_deref())
+ }
}
/// Metadata about a video.
diff --git a/crates/ruma-events/tests/it/room_message.rs b/crates/ruma-events/tests/it/room_message.rs
index acc2422b..717f5685 100644
--- a/crates/ruma-events/tests/it/room_message.rs
+++ b/crates/ruma-events/tests/it/room_message.rs
@@ -12,7 +12,7 @@ use ruma_events::{
room::{
message::{
AddMentions, AudioMessageEventContent, EmoteMessageEventContent,
- FileMessageEventContent, ForwardThread, ImageMessageEventContent,
+ FileMessageEventContent, FormattedBody, ForwardThread, ImageMessageEventContent,
KeyVerificationRequestEventContent, MessageType, OriginalRoomMessageEvent,
OriginalSyncRoomMessageEvent, Relation, ReplyWithinThread, RoomMessageEventContent,
TextMessageEventContent, VideoMessageEventContent,
@@ -900,8 +900,9 @@ fn audio_msgtype_deserialization() {
let event_content = from_json_value::(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Audio(content));
assert_eq!(content.body, "Upload: my_song.mp3");
- assert_matches!(content.source, MediaSource::Plain(url));
+ assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
+ assert!(content.caption().is_none());
}
#[test]
@@ -983,8 +984,9 @@ fn file_msgtype_plain_content_deserialization() {
let event_content = from_json_value::(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(content));
assert_eq!(content.body, "Upload: my_file.txt");
- assert_matches!(content.source, MediaSource::Plain(url));
+ assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
+ assert!(content.caption().is_none());
}
#[test]
@@ -1045,8 +1047,9 @@ fn image_msgtype_deserialization() {
let event_content = from_json_value::(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Image(content));
assert_eq!(content.body, "Upload: my_image.jpg");
- assert_matches!(content.source, MediaSource::Plain(url));
+ assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
+ assert!(content.caption().is_none());
}
#[cfg(not(feature = "unstable-msc3488"))]
@@ -1202,8 +1205,9 @@ fn video_msgtype_deserialization() {
let event_content = from_json_value::(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Video(content));
assert_eq!(content.body, "Upload: my_video.mp4");
- assert_matches!(content.source, MediaSource::Plain(url));
+ assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
+ assert!(content.caption().is_none());
}
#[test]
@@ -1330,3 +1334,106 @@ fn invalid_replacement() {
assert_matches!(&data, Cow::Borrowed(_)); // data is stored in JSON form because it's invalid
assert_eq!(JsonValue::Object(data.into_owned()), relation);
}
+
+#[test]
+fn test_audio_caption() {
+ let mut content = AudioMessageEventContent::plain(
+ "my_sound.ogg".to_owned(),
+ mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
+ );
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.filename = Some("my_sound.ogg".to_owned());
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.body = "This was a great podcast episode".to_owned();
+ assert_eq!(content.caption(), Some("This was a great podcast episode"));
+ assert!(content.formatted_caption().is_none());
+
+ content.formatted =
+ Some(FormattedBody::html("This was a great podcast episode".to_owned()));
+ assert_eq!(content.caption(), Some("This was a great podcast episode"));
+ assert_eq!(
+ content.formatted_caption().map(|f| f.body.clone()),
+ Some("This was a great podcast episode".to_owned())
+ );
+}
+
+#[test]
+fn test_file_caption() {
+ let mut content = FileMessageEventContent::plain(
+ "my_file.txt".to_owned(),
+ mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
+ );
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.filename = Some("my_file.txt".to_owned());
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.body = "Please check these notes".to_owned();
+ assert_eq!(content.caption(), Some("Please check these notes"));
+ assert!(content.formatted_caption().is_none());
+
+ content.formatted =
+ Some(FormattedBody::html("Please check these notes ".to_owned()));
+ assert_eq!(content.caption(), Some("Please check these notes"));
+ assert_eq!(
+ content.formatted_caption().map(|f| f.body.clone()),
+ Some("Please check these notes ".to_owned())
+ );
+}
+
+#[test]
+fn test_image_caption() {
+ let mut content = ImageMessageEventContent::plain(
+ "my_image.jpg".to_owned(),
+ mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
+ );
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.filename = Some("my_image.jpg".to_owned());
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.body = "Check it out 😎".to_owned();
+ assert_eq!(content.caption(), Some("Check it out 😎"));
+ assert!(content.formatted_caption().is_none());
+
+ content.formatted = Some(FormattedBody::html("Check it out 😎 ".to_owned()));
+ assert_eq!(content.caption(), Some("Check it out 😎"));
+ assert_eq!(
+ content.formatted_caption().map(|f| f.body.clone()),
+ Some("Check it out 😎 ".to_owned())
+ );
+}
+
+#[test]
+fn test_video_caption() {
+ let mut content = VideoMessageEventContent::plain(
+ "my_video.mp4".to_owned(),
+ mxc_uri!("mxc://notareal.hs/abcdef").to_owned(),
+ );
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.filename = Some("my_video.mp4".to_owned());
+ assert!(content.caption().is_none());
+ assert!(content.formatted_caption().is_none());
+
+ content.body = "You missed a great evening".to_owned();
+ assert_eq!(content.caption(), Some("You missed a great evening"));
+ assert!(content.formatted_caption().is_none());
+
+ content.formatted =
+ Some(FormattedBody::html("You missed a great evening".to_owned()));
+ assert_eq!(content.caption(), Some("You missed a great evening"));
+ assert_eq!(
+ content.formatted_caption().map(|f| f.body.clone()),
+ Some("You missed a great evening".to_owned())
+ );
+}