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