1use ruma::{
19 MilliSecondsSinceUnixEpoch, OwnedEventId,
20 events::{
21 AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
22 MessageLikeEventType,
23 relation::{BundledThread, RelationType},
24 },
25 room_version_rules::RedactionRules,
26 serde::Raw,
27};
28use serde::Deserialize;
29
30use crate::deserialized_responses::{ThreadSummary, ThreadSummaryStatus};
31
32#[derive(Deserialize)]
33struct RelatesTo {
34 #[serde(rename = "rel_type")]
35 rel_type: RelationType,
36 #[serde(rename = "event_id")]
37 event_id: Option<OwnedEventId>,
38}
39
40#[allow(missing_debug_implementations)]
41#[derive(Deserialize)]
42struct SimplifiedContent {
43 #[serde(rename = "m.relates_to")]
44 relates_to: Option<RelatesTo>,
45}
46
47pub fn extract_thread_root_from_content(
55 content: Raw<AnyMessageLikeEventContent>,
56) -> Option<OwnedEventId> {
57 let relates_to = content.deserialize_as_unchecked::<SimplifiedContent>().ok()?.relates_to?;
58 match relates_to.rel_type {
59 RelationType::Thread => relates_to.event_id,
60 _ => None,
61 }
62}
63
64pub fn extract_thread_root(event: &Raw<AnySyncTimelineEvent>) -> Option<OwnedEventId> {
72 extract_thread_root_from_content(event.get_field("content").ok().flatten()?)
73}
74
75pub fn extract_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(RelationType, OwnedEventId)> {
78 let relates_to = event.get_field::<SimplifiedContent>("content").ok().flatten()?.relates_to?;
79 Some((relates_to.rel_type, relates_to.event_id?))
80}
81
82#[allow(missing_debug_implementations)]
83#[derive(Deserialize)]
84struct Relations {
85 #[serde(rename = "m.thread")]
86 thread: Option<Box<BundledThread>>,
87}
88
89pub fn extract_redaction_target(
92 event: &Raw<AnySyncTimelineEvent>,
93 redaction_rules: &RedactionRules,
94) -> Option<OwnedEventId> {
95 let Ok(Some(MessageLikeEventType::RoomRedaction)) =
97 event.get_field::<MessageLikeEventType>("type")
98 else {
99 return None;
101 };
102
103 let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(redaction))) =
106 event.deserialize()
107 else {
108 return None;
110 };
111
112 redaction.redacts(redaction_rules).map(ToOwned::to_owned)
113}
114
115#[allow(missing_debug_implementations)]
116#[derive(Deserialize)]
117struct Unsigned {
118 #[serde(rename = "m.relations")]
119 relations: Option<Relations>,
120}
121
122pub fn extract_bundled_thread_summary(
124 event: &Raw<AnySyncTimelineEvent>,
125) -> (ThreadSummaryStatus, Option<Raw<AnySyncMessageLikeEvent>>) {
126 match event.get_field::<Unsigned>("unsigned") {
127 Ok(Some(Unsigned { relations: Some(Relations { thread: Some(bundled_thread) }) })) => {
128 let count = bundled_thread.count.try_into().unwrap_or(u32::MAX);
132
133 let latest_reply =
134 bundled_thread.latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
135
136 (
137 ThreadSummaryStatus::Some(ThreadSummary { num_replies: count, latest_reply }),
138 Some(bundled_thread.latest_event),
139 )
140 }
141 Ok(_) => (ThreadSummaryStatus::None, None),
142 Err(_) => (ThreadSummaryStatus::Unknown, None),
143 }
144}
145
146pub fn extract_timestamp(
152 event: &Raw<AnySyncTimelineEvent>,
153 max_value: MilliSecondsSinceUnixEpoch,
154) -> Option<MilliSecondsSinceUnixEpoch> {
155 let mut origin_server_ts = event.get_field("origin_server_ts").ok().flatten()?;
156
157 if origin_server_ts > max_value {
158 origin_server_ts = max_value;
159 }
160
161 Some(origin_server_ts)
162}
163
164#[cfg(test)]
165mod tests {
166 use assert_matches::assert_matches;
167 use ruma::{UInt, event_id, owned_event_id};
168 use serde_json::json;
169
170 use super::{
171 MilliSecondsSinceUnixEpoch, Raw, extract_bundled_thread_summary, extract_thread_root,
172 extract_timestamp,
173 };
174 use crate::{
175 deserialized_responses::{ThreadSummary, ThreadSummaryStatus},
176 serde_helpers::{RelationType, extract_relation},
177 };
178
179 #[test]
180 fn test_extract_thread_root() {
181 let thread_root = event_id!("$thread_root_event_id:example.com");
186 let event = Raw::new(&json!({
187 "event_id": "$eid:example.com",
188 "type": "m.room.message",
189 "sender": "@alice:example.com",
190 "origin_server_ts": 42,
191 "content": {
192 "body": "Hello, world!",
193 "m.relates_to": {
194 "rel_type": "m.thread",
195 "event_id": thread_root,
196 }
197 }
198 }))
199 .unwrap()
200 .cast_unchecked();
201
202 let observed_thread_root = extract_thread_root(&event);
203 assert_eq!(observed_thread_root.as_deref(), Some(thread_root));
204 let observed_relation = extract_relation(&event).unwrap();
205 assert_eq!(observed_relation, (RelationType::Thread, thread_root.to_owned()));
206
207 let event = Raw::new(&json!({
210 "event_id": "$eid:example.com",
211 "type": "m.room.message",
212 "sender": "@alice:example.com",
213 "origin_server_ts": 42,
214 }))
215 .unwrap()
216 .cast_unchecked();
217
218 let observed_thread_root = extract_thread_root(&event);
219 assert_matches!(observed_thread_root, None);
220 assert_matches!(extract_relation(&event), None);
221
222 let event = Raw::new(&json!({
224 "event_id": "$eid:example.com",
225 "type": "m.room.message",
226 "sender": "@alice:example.com",
227 "origin_server_ts": 42,
228 "content": {
229 "body": "Hello, world!",
230 }
231 }))
232 .unwrap()
233 .cast_unchecked();
234
235 let observed_thread_root = extract_thread_root(&event);
236 assert_matches!(observed_thread_root, None);
237 assert_matches!(extract_relation(&event), None);
238
239 let event = Raw::new(&json!({
241 "event_id": "$eid:example.com",
242 "type": "m.room.message",
243 "sender": "@alice:example.com",
244 "origin_server_ts": 42,
245 "content": {
246 "body": "Hello, world!",
247 "m.relates_to": {
248 "rel_type": "m.reference",
249 "event_id": "$referenced_event_id:example.com",
250 }
251 }
252 }))
253 .unwrap()
254 .cast_unchecked();
255
256 let observed_thread_root = extract_thread_root(&event);
257 assert_matches!(observed_thread_root, None);
258 let observed_relation = extract_relation(&event).unwrap();
259 assert_eq!(
260 observed_relation,
261 (RelationType::Reference, owned_event_id!("$referenced_event_id:example.com"))
262 );
263 }
264
265 #[test]
266 fn test_extract_bundled_thread_summary() {
267 let event = Raw::new(&json!({
269 "event_id": "$eid:example.com",
270 "type": "m.room.message",
271 "sender": "@alice:example.com",
272 "origin_server_ts": 42,
273 "content": {
274 "body": "Hello, world!",
275 },
276 "unsigned": {
277 "m.relations": {
278 "m.thread": {
279 "latest_event": {
280 "event_id": "$latest_event:example.com",
281 "type": "m.room.message",
282 "sender": "@bob:example.com",
283 "origin_server_ts": 42,
284 "content": {
285 "body": "Hello to you too!",
286 }
287 },
288 "count": 2,
289 "current_user_participated": true,
290 }
291 }
292 }
293 }))
294 .unwrap()
295 .cast_unchecked();
296
297 assert_matches!(
298 extract_bundled_thread_summary(&event),
299 (ThreadSummaryStatus::Some(ThreadSummary { .. }), Some(..))
300 );
301
302 let event = Raw::new(&json!({
304 "event_id": "$eid:example.com",
305 "type": "m.room.message",
306 "sender": "@alice:example.com",
307 "origin_server_ts": 42,
308 }))
309 .unwrap()
310 .cast_unchecked();
311
312 assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
313
314 let event = Raw::new(&json!({
316 "event_id": "$eid:example.com",
317 "type": "m.room.message",
318 "sender": "@alice:example.com",
319 "origin_server_ts": 42,
320 "content": {
321 "body": "Bonjour, monde!",
322 },
323 "unsigned": {
324 "m.relations": {
325 "m.replace":
326 {
327 "event_id": "$update:example.com",
328 "type": "m.room.message",
329 "sender": "@alice:example.com",
330 "origin_server_ts": 43,
331 "content": {
332 "body": "* Hello, world!",
333 }
334 },
335 }
336 }
337 }))
338 .unwrap()
339 .cast_unchecked();
340
341 assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
342
343 let event = Raw::new(&json!({
346 "event_id": "$eid:example.com",
347 "type": "m.room.message",
348 "sender": "@alice:example.com",
349 "origin_server_ts": 42,
350 "unsigned": {
351 "m.relations": {
352 "m.thread": {
353 }
355 }
356 }
357 }))
358 .unwrap()
359 .cast_unchecked();
360
361 assert_matches!(
362 extract_bundled_thread_summary(&event),
363 (ThreadSummaryStatus::Unknown, None)
364 );
365 }
366
367 #[test]
368 fn test_extract_timestamp() {
369 let event = Raw::new(&json!({
370 "event_id": "$ev0",
371 "type": "m.room.message",
372 "sender": "@mnt_io:matrix.org",
373 "origin_server_ts": 42,
374 "content": {
375 "body": "Le gras, c'est la vie",
376 }
377 }))
378 .unwrap()
379 .cast_unchecked();
380
381 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
382
383 assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(42u32))));
384 }
385
386 #[test]
387 fn test_extract_timestamp_no_origin_server_ts() {
388 let event = Raw::new(&json!({
389 "event_id": "$ev0",
390 "type": "m.room.message",
391 "sender": "@mnt_io:matrix.org",
392 "content": {
393 "body": "Le gras, c'est la vie",
394 }
395 }))
396 .unwrap()
397 .cast_unchecked();
398
399 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
400
401 assert!(timestamp.is_none());
402 }
403
404 #[test]
405 fn test_extract_timestamp_invalid_origin_server_ts() {
406 let event = Raw::new(&json!({
407 "event_id": "$ev0",
408 "type": "m.room.message",
409 "sender": "@mnt_io:matrix.org",
410 "origin_server_ts": "saucisse",
411 "content": {
412 "body": "Le gras, c'est la vie",
413 }
414 }))
415 .unwrap()
416 .cast_unchecked();
417
418 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
419
420 assert!(timestamp.is_none());
421 }
422
423 #[test]
424 fn test_extract_timestamp_malicious_origin_server_ts() {
425 let event = Raw::new(&json!({
426 "event_id": "$ev0",
427 "type": "m.room.message",
428 "sender": "@mnt_io:matrix.org",
429 "origin_server_ts": 101,
430 "content": {
431 "body": "Le gras, c'est la vie",
432 }
433 }))
434 .unwrap()
435 .cast_unchecked();
436
437 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
438
439 assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(100u32))));
440 }
441}