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}
35
36#[derive(Deserialize)]
37struct RelatesTo {
38 #[serde(rename = "rel_type")]
39 rel_type: RelationsType,
40 #[serde(rename = "event_id")]
41 event_id: Option<OwnedEventId>,
42}
43
44#[allow(missing_debug_implementations)]
45#[derive(Deserialize)]
46struct SimplifiedContent {
47 #[serde(rename = "m.relates_to")]
48 relates_to: Option<RelatesTo>,
49}
50
51pub fn extract_thread_root_from_content(
59 content: Raw<AnyMessageLikeEventContent>,
60) -> Option<OwnedEventId> {
61 let relates_to = content.deserialize_as_unchecked::<SimplifiedContent>().ok()?.relates_to?;
62 match relates_to.rel_type {
63 RelationsType::Thread => relates_to.event_id,
64 }
65}
66
67pub fn extract_thread_root(event: &Raw<AnySyncTimelineEvent>) -> Option<OwnedEventId> {
75 let relates_to = event.get_field::<SimplifiedContent>("content").ok().flatten()?.relates_to?;
76 match relates_to.rel_type {
77 RelationsType::Thread => relates_to.event_id,
78 }
79}
80
81#[allow(missing_debug_implementations)]
82#[derive(Deserialize)]
83struct Relations {
84 #[serde(rename = "m.thread")]
85 thread: Option<Box<BundledThread>>,
86}
87
88#[allow(missing_debug_implementations)]
89#[derive(Deserialize)]
90struct Unsigned {
91 #[serde(rename = "m.relations")]
92 relations: Option<Relations>,
93}
94
95pub fn extract_bundled_thread_summary(
97 event: &Raw<AnySyncTimelineEvent>,
98) -> (ThreadSummaryStatus, Option<Raw<AnySyncMessageLikeEvent>>) {
99 match event.get_field::<Unsigned>("unsigned") {
100 Ok(Some(Unsigned { relations: Some(Relations { thread: Some(bundled_thread) }) })) => {
101 let count = bundled_thread.count.try_into().unwrap_or(u32::MAX);
105
106 let latest_reply =
107 bundled_thread.latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
108
109 (
110 ThreadSummaryStatus::Some(ThreadSummary { num_replies: count, latest_reply }),
111 Some(bundled_thread.latest_event),
112 )
113 }
114 Ok(_) => (ThreadSummaryStatus::None, None),
115 Err(_) => (ThreadSummaryStatus::Unknown, None),
116 }
117}
118
119pub fn extract_timestamp(
125 event: &Raw<AnySyncTimelineEvent>,
126 max_value: MilliSecondsSinceUnixEpoch,
127) -> Option<MilliSecondsSinceUnixEpoch> {
128 let mut origin_server_ts = event.get_field("origin_server_ts").ok().flatten()?;
129
130 if origin_server_ts > max_value {
131 origin_server_ts = max_value;
132 }
133
134 Some(origin_server_ts)
135}
136
137#[cfg(test)]
138mod tests {
139 use assert_matches::assert_matches;
140 use ruma::{UInt, event_id};
141 use serde_json::json;
142
143 use super::{
144 MilliSecondsSinceUnixEpoch, Raw, extract_bundled_thread_summary, extract_thread_root,
145 extract_timestamp,
146 };
147 use crate::deserialized_responses::{ThreadSummary, ThreadSummaryStatus};
148
149 #[test]
150 fn test_extract_thread_root() {
151 let thread_root = event_id!("$thread_root_event_id:example.com");
156 let event = Raw::new(&json!({
157 "event_id": "$eid:example.com",
158 "type": "m.room.message",
159 "sender": "@alice:example.com",
160 "origin_server_ts": 42,
161 "content": {
162 "body": "Hello, world!",
163 "m.relates_to": {
164 "rel_type": "m.thread",
165 "event_id": thread_root,
166 }
167 }
168 }))
169 .unwrap()
170 .cast_unchecked();
171
172 let observed_thread_root = extract_thread_root(&event);
173 assert_eq!(observed_thread_root.as_deref(), Some(thread_root));
174
175 let event = Raw::new(&json!({
178 "event_id": "$eid:example.com",
179 "type": "m.room.message",
180 "sender": "@alice:example.com",
181 "origin_server_ts": 42,
182 }))
183 .unwrap()
184 .cast_unchecked();
185
186 let observed_thread_root = extract_thread_root(&event);
187 assert_matches!(observed_thread_root, None);
188
189 let event = Raw::new(&json!({
191 "event_id": "$eid:example.com",
192 "type": "m.room.message",
193 "sender": "@alice:example.com",
194 "origin_server_ts": 42,
195 "content": {
196 "body": "Hello, world!",
197 }
198 }))
199 .unwrap()
200 .cast_unchecked();
201
202 let observed_thread_root = extract_thread_root(&event);
203 assert_matches!(observed_thread_root, None);
204
205 let event = Raw::new(&json!({
207 "event_id": "$eid:example.com",
208 "type": "m.room.message",
209 "sender": "@alice:example.com",
210 "origin_server_ts": 42,
211 "content": {
212 "body": "Hello, world!",
213 "m.relates_to": {
214 "rel_type": "m.reference",
215 "event_id": "$referenced_event_id:example.com",
216 }
217 }
218 }))
219 .unwrap()
220 .cast_unchecked();
221
222 let observed_thread_root = extract_thread_root(&event);
223 assert_matches!(observed_thread_root, None);
224 }
225
226 #[test]
227 fn test_extract_bundled_thread_summary() {
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 },
237 "unsigned": {
238 "m.relations": {
239 "m.thread": {
240 "latest_event": {
241 "event_id": "$latest_event:example.com",
242 "type": "m.room.message",
243 "sender": "@bob:example.com",
244 "origin_server_ts": 42,
245 "content": {
246 "body": "Hello to you too!",
247 }
248 },
249 "count": 2,
250 "current_user_participated": true,
251 }
252 }
253 }
254 }))
255 .unwrap()
256 .cast_unchecked();
257
258 assert_matches!(
259 extract_bundled_thread_summary(&event),
260 (ThreadSummaryStatus::Some(ThreadSummary { .. }), Some(..))
261 );
262
263 let event = Raw::new(&json!({
265 "event_id": "$eid:example.com",
266 "type": "m.room.message",
267 "sender": "@alice:example.com",
268 "origin_server_ts": 42,
269 }))
270 .unwrap()
271 .cast_unchecked();
272
273 assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
274
275 let event = Raw::new(&json!({
277 "event_id": "$eid:example.com",
278 "type": "m.room.message",
279 "sender": "@alice:example.com",
280 "origin_server_ts": 42,
281 "content": {
282 "body": "Bonjour, monde!",
283 },
284 "unsigned": {
285 "m.relations": {
286 "m.replace":
287 {
288 "event_id": "$update:example.com",
289 "type": "m.room.message",
290 "sender": "@alice:example.com",
291 "origin_server_ts": 43,
292 "content": {
293 "body": "* Hello, world!",
294 }
295 },
296 }
297 }
298 }))
299 .unwrap()
300 .cast_unchecked();
301
302 assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
303
304 let event = Raw::new(&json!({
307 "event_id": "$eid:example.com",
308 "type": "m.room.message",
309 "sender": "@alice:example.com",
310 "origin_server_ts": 42,
311 "unsigned": {
312 "m.relations": {
313 "m.thread": {
314 }
316 }
317 }
318 }))
319 .unwrap()
320 .cast_unchecked();
321
322 assert_matches!(
323 extract_bundled_thread_summary(&event),
324 (ThreadSummaryStatus::Unknown, None)
325 );
326 }
327
328 #[test]
329 fn test_extract_timestamp() {
330 let event = Raw::new(&json!({
331 "event_id": "$ev0",
332 "type": "m.room.message",
333 "sender": "@mnt_io:matrix.org",
334 "origin_server_ts": 42,
335 "content": {
336 "body": "Le gras, c'est la vie",
337 }
338 }))
339 .unwrap()
340 .cast_unchecked();
341
342 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
343
344 assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(42u32))));
345 }
346
347 #[test]
348 fn test_extract_timestamp_no_origin_server_ts() {
349 let event = Raw::new(&json!({
350 "event_id": "$ev0",
351 "type": "m.room.message",
352 "sender": "@mnt_io:matrix.org",
353 "content": {
354 "body": "Le gras, c'est la vie",
355 }
356 }))
357 .unwrap()
358 .cast_unchecked();
359
360 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
361
362 assert!(timestamp.is_none());
363 }
364
365 #[test]
366 fn test_extract_timestamp_invalid_origin_server_ts() {
367 let event = Raw::new(&json!({
368 "event_id": "$ev0",
369 "type": "m.room.message",
370 "sender": "@mnt_io:matrix.org",
371 "origin_server_ts": "saucisse",
372 "content": {
373 "body": "Le gras, c'est la vie",
374 }
375 }))
376 .unwrap()
377 .cast_unchecked();
378
379 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
380
381 assert!(timestamp.is_none());
382 }
383
384 #[test]
385 fn test_extract_timestamp_malicious_origin_server_ts() {
386 let event = Raw::new(&json!({
387 "event_id": "$ev0",
388 "type": "m.room.message",
389 "sender": "@mnt_io:matrix.org",
390 "origin_server_ts": 101,
391 "content": {
392 "body": "Le gras, c'est la vie",
393 }
394 }))
395 .unwrap()
396 .cast_unchecked();
397
398 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
399
400 assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(100u32))));
401 }
402}