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