use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId,
events::{
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
relation::BundledThread,
},
serde::Raw,
};
use serde::Deserialize;
use crate::deserialized_responses::{ThreadSummary, ThreadSummaryStatus};
#[derive(Deserialize)]
enum RelationsType {
#[serde(rename = "m.thread")]
Thread,
#[serde(rename = "m.replace")]
Edit,
}
#[derive(Deserialize)]
struct RelatesTo {
#[serde(rename = "rel_type")]
rel_type: RelationsType,
#[serde(rename = "event_id")]
event_id: Option<OwnedEventId>,
}
#[allow(missing_debug_implementations)]
#[derive(Deserialize)]
struct SimplifiedContent {
#[serde(rename = "m.relates_to")]
relates_to: Option<RelatesTo>,
}
pub fn extract_thread_root_from_content(
content: Raw<AnyMessageLikeEventContent>,
) -> Option<OwnedEventId> {
let relates_to = content.deserialize_as_unchecked::<SimplifiedContent>().ok()?.relates_to?;
match relates_to.rel_type {
RelationsType::Thread => relates_to.event_id,
RelationsType::Edit => None,
}
}
pub fn extract_thread_root(event: &Raw<AnySyncTimelineEvent>) -> Option<OwnedEventId> {
extract_thread_root_from_content(event.get_field("content").ok().flatten()?)
}
pub fn extract_edit_target(event: &Raw<AnySyncTimelineEvent>) -> Option<OwnedEventId> {
let relates_to = event.get_field::<SimplifiedContent>("content").ok().flatten()?.relates_to?;
match relates_to.rel_type {
RelationsType::Edit => relates_to.event_id,
RelationsType::Thread => None,
}
}
#[allow(missing_debug_implementations)]
#[derive(Deserialize)]
struct Relations {
#[serde(rename = "m.thread")]
thread: Option<Box<BundledThread>>,
}
#[allow(missing_debug_implementations)]
#[derive(Deserialize)]
struct Unsigned {
#[serde(rename = "m.relations")]
relations: Option<Relations>,
}
pub fn extract_bundled_thread_summary(
event: &Raw<AnySyncTimelineEvent>,
) -> (ThreadSummaryStatus, Option<Raw<AnySyncMessageLikeEvent>>) {
match event.get_field::<Unsigned>("unsigned") {
Ok(Some(Unsigned { relations: Some(Relations { thread: Some(bundled_thread) }) })) => {
let count = bundled_thread.count.try_into().unwrap_or(u32::MAX);
let latest_reply =
bundled_thread.latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
(
ThreadSummaryStatus::Some(ThreadSummary { num_replies: count, latest_reply }),
Some(bundled_thread.latest_event),
)
}
Ok(_) => (ThreadSummaryStatus::None, None),
Err(_) => (ThreadSummaryStatus::Unknown, None),
}
}
pub fn extract_timestamp(
event: &Raw<AnySyncTimelineEvent>,
max_value: MilliSecondsSinceUnixEpoch,
) -> Option<MilliSecondsSinceUnixEpoch> {
let mut origin_server_ts = event.get_field("origin_server_ts").ok().flatten()?;
if origin_server_ts > max_value {
origin_server_ts = max_value;
}
Some(origin_server_ts)
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use ruma::{UInt, event_id};
use serde_json::json;
use super::{
MilliSecondsSinceUnixEpoch, Raw, extract_bundled_thread_summary, extract_thread_root,
extract_timestamp,
};
use crate::deserialized_responses::{ThreadSummary, ThreadSummaryStatus};
#[test]
fn test_extract_thread_root() {
let thread_root = event_id!("$thread_root_event_id:example.com");
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
"content": {
"body": "Hello, world!",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": thread_root,
}
}
}))
.unwrap()
.cast_unchecked();
let observed_thread_root = extract_thread_root(&event);
assert_eq!(observed_thread_root.as_deref(), Some(thread_root));
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
}))
.unwrap()
.cast_unchecked();
let observed_thread_root = extract_thread_root(&event);
assert_matches!(observed_thread_root, None);
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
"content": {
"body": "Hello, world!",
}
}))
.unwrap()
.cast_unchecked();
let observed_thread_root = extract_thread_root(&event);
assert_matches!(observed_thread_root, None);
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
"content": {
"body": "Hello, world!",
"m.relates_to": {
"rel_type": "m.reference",
"event_id": "$referenced_event_id:example.com",
}
}
}))
.unwrap()
.cast_unchecked();
let observed_thread_root = extract_thread_root(&event);
assert_matches!(observed_thread_root, None);
}
#[test]
fn test_extract_bundled_thread_summary() {
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
"content": {
"body": "Hello, world!",
},
"unsigned": {
"m.relations": {
"m.thread": {
"latest_event": {
"event_id": "$latest_event:example.com",
"type": "m.room.message",
"sender": "@bob:example.com",
"origin_server_ts": 42,
"content": {
"body": "Hello to you too!",
}
},
"count": 2,
"current_user_participated": true,
}
}
}
}))
.unwrap()
.cast_unchecked();
assert_matches!(
extract_bundled_thread_summary(&event),
(ThreadSummaryStatus::Some(ThreadSummary { .. }), Some(..))
);
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
}))
.unwrap()
.cast_unchecked();
assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
"content": {
"body": "Bonjour, monde!",
},
"unsigned": {
"m.relations": {
"m.replace":
{
"event_id": "$update:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 43,
"content": {
"body": "* Hello, world!",
}
},
}
}
}))
.unwrap()
.cast_unchecked();
assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
let event = Raw::new(&json!({
"event_id": "$eid:example.com",
"type": "m.room.message",
"sender": "@alice:example.com",
"origin_server_ts": 42,
"unsigned": {
"m.relations": {
"m.thread": {
}
}
}
}))
.unwrap()
.cast_unchecked();
assert_matches!(
extract_bundled_thread_summary(&event),
(ThreadSummaryStatus::Unknown, None)
);
}
#[test]
fn test_extract_timestamp() {
let event = Raw::new(&json!({
"event_id": "$ev0",
"type": "m.room.message",
"sender": "@mnt_io:matrix.org",
"origin_server_ts": 42,
"content": {
"body": "Le gras, c'est la vie",
}
}))
.unwrap()
.cast_unchecked();
let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(42u32))));
}
#[test]
fn test_extract_timestamp_no_origin_server_ts() {
let event = Raw::new(&json!({
"event_id": "$ev0",
"type": "m.room.message",
"sender": "@mnt_io:matrix.org",
"content": {
"body": "Le gras, c'est la vie",
}
}))
.unwrap()
.cast_unchecked();
let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
assert!(timestamp.is_none());
}
#[test]
fn test_extract_timestamp_invalid_origin_server_ts() {
let event = Raw::new(&json!({
"event_id": "$ev0",
"type": "m.room.message",
"sender": "@mnt_io:matrix.org",
"origin_server_ts": "saucisse",
"content": {
"body": "Le gras, c'est la vie",
}
}))
.unwrap()
.cast_unchecked();
let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
assert!(timestamp.is_none());
}
#[test]
fn test_extract_timestamp_malicious_origin_server_ts() {
let event = Raw::new(&json!({
"event_id": "$ev0",
"type": "m.room.message",
"sender": "@mnt_io:matrix.org",
"origin_server_ts": 101,
"content": {
"body": "Le gras, c'est la vie",
}
}))
.unwrap()
.cast_unchecked();
let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(100u32))));
}
}