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
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_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(RelationType, OwnedEventId)> {
76 let relates_to = event.get_field::<SimplifiedContent>("content").ok().flatten()?.relates_to?;
77 Some((relates_to.rel_type, relates_to.event_id?))
78}
79
80pub fn extract_redaction_target(
83 event: &Raw<AnySyncTimelineEvent>,
84 redaction_rules: &RedactionRules,
85) -> Option<OwnedEventId> {
86 let Ok(Some(MessageLikeEventType::RoomRedaction)) =
88 event.get_field::<MessageLikeEventType>("type")
89 else {
90 return None;
92 };
93
94 let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(redaction))) =
97 event.deserialize()
98 else {
99 return None;
101 };
102
103 redaction.redacts(redaction_rules).map(ToOwned::to_owned)
104}
105
106pub fn extract_bundled_thread(event: &Raw<AnySyncTimelineEvent>) -> Option<BundledThread> {
108 #[derive(Deserialize)]
109 struct Unsigned {
110 #[serde(rename = "m.relations")]
111 relations: Option<Relations>,
112 }
113
114 #[derive(Deserialize)]
115 struct Relations {
116 #[serde(rename = "m.thread")]
117 thread: Option<BundledThread>,
118 }
119
120 match event.get_field::<Unsigned>("unsigned") {
121 Ok(Some(Unsigned { relations: Some(Relations { thread: Some(bundled_thread) }) })) => {
122 Some(bundled_thread)
123 }
124 Ok(_) | Err(_) => None,
125 }
126}
127
128pub fn extract_timestamp(
134 event: &Raw<AnySyncTimelineEvent>,
135 max_value: MilliSecondsSinceUnixEpoch,
136) -> Option<MilliSecondsSinceUnixEpoch> {
137 let mut origin_server_ts = event.get_field("origin_server_ts").ok().flatten()?;
138
139 if origin_server_ts > max_value {
140 origin_server_ts = max_value;
141 }
142
143 Some(origin_server_ts)
144}
145
146#[cfg(test)]
147mod tests {
148 use assert_matches::assert_matches;
149 use ruma::{UInt, event_id, owned_event_id};
150 use serde_json::json;
151
152 use super::{
153 MilliSecondsSinceUnixEpoch, Raw, RelationType, extract_bundled_thread, extract_relation,
154 extract_thread_root, extract_timestamp,
155 };
156
157 #[test]
158 fn test_extract_thread_root() {
159 let thread_root = event_id!("$thread_root_event_id:example.com");
164 let event = Raw::new(&json!({
165 "event_id": "$eid:example.com",
166 "type": "m.room.message",
167 "sender": "@alice:example.com",
168 "origin_server_ts": 42,
169 "content": {
170 "body": "Hello, world!",
171 "m.relates_to": {
172 "rel_type": "m.thread",
173 "event_id": thread_root,
174 }
175 }
176 }))
177 .unwrap()
178 .cast_unchecked();
179
180 let observed_thread_root = extract_thread_root(&event);
181 assert_eq!(observed_thread_root.as_deref(), Some(thread_root));
182 let observed_relation = extract_relation(&event).unwrap();
183 assert_eq!(observed_relation, (RelationType::Thread, thread_root.to_owned()));
184
185 let event = Raw::new(&json!({
188 "event_id": "$eid:example.com",
189 "type": "m.room.message",
190 "sender": "@alice:example.com",
191 "origin_server_ts": 42,
192 }))
193 .unwrap()
194 .cast_unchecked();
195
196 let observed_thread_root = extract_thread_root(&event);
197 assert_matches!(observed_thread_root, None);
198 assert_matches!(extract_relation(&event), None);
199
200 let event = Raw::new(&json!({
202 "event_id": "$eid:example.com",
203 "type": "m.room.message",
204 "sender": "@alice:example.com",
205 "origin_server_ts": 42,
206 "content": {
207 "body": "Hello, world!",
208 }
209 }))
210 .unwrap()
211 .cast_unchecked();
212
213 let observed_thread_root = extract_thread_root(&event);
214 assert_matches!(observed_thread_root, None);
215 assert_matches!(extract_relation(&event), None);
216
217 let event = Raw::new(&json!({
219 "event_id": "$eid:example.com",
220 "type": "m.room.message",
221 "sender": "@alice:example.com",
222 "origin_server_ts": 42,
223 "content": {
224 "body": "Hello, world!",
225 "m.relates_to": {
226 "rel_type": "m.reference",
227 "event_id": "$referenced_event_id:example.com",
228 }
229 }
230 }))
231 .unwrap()
232 .cast_unchecked();
233
234 let observed_thread_root = extract_thread_root(&event);
235 assert_matches!(observed_thread_root, None);
236 let observed_relation = extract_relation(&event).unwrap();
237 assert_eq!(
238 observed_relation,
239 (RelationType::Reference, owned_event_id!("$referenced_event_id:example.com"))
240 );
241 }
242
243 #[test]
244 fn test_extract_bundled_thread() {
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!(extract_bundled_thread(&event).is_some());
276
277 let event = Raw::new(&json!({
279 "event_id": "$eid:example.com",
280 "type": "m.room.message",
281 "sender": "@alice:example.com",
282 "origin_server_ts": 42,
283 }))
284 .unwrap()
285 .cast_unchecked();
286
287 assert!(extract_bundled_thread(&event).is_none());
288
289 let event = Raw::new(&json!({
291 "event_id": "$eid:example.com",
292 "type": "m.room.message",
293 "sender": "@alice:example.com",
294 "origin_server_ts": 42,
295 "content": {
296 "body": "Bonjour, monde!",
297 },
298 "unsigned": {
299 "m.relations": {
300 "m.replace":
301 {
302 "event_id": "$update:example.com",
303 "type": "m.room.message",
304 "sender": "@alice:example.com",
305 "origin_server_ts": 43,
306 "content": {
307 "body": "* Hello, world!",
308 }
309 },
310 }
311 }
312 }))
313 .unwrap()
314 .cast_unchecked();
315
316 assert!(extract_bundled_thread(&event).is_none());
317
318 let event = Raw::new(&json!({
320 "event_id": "$eid:example.com",
321 "type": "m.room.message",
322 "sender": "@alice:example.com",
323 "origin_server_ts": 42,
324 "unsigned": {
325 "m.relations": {
326 "m.thread": {
327 }
329 }
330 }
331 }))
332 .unwrap()
333 .cast_unchecked();
334
335 assert!(extract_bundled_thread(&event).is_none());
336 }
337
338 #[test]
339 fn test_extract_timestamp() {
340 let event = Raw::new(&json!({
341 "event_id": "$ev0",
342 "type": "m.room.message",
343 "sender": "@mnt_io:matrix.org",
344 "origin_server_ts": 42,
345 "content": {
346 "body": "Le gras, c'est la vie",
347 }
348 }))
349 .unwrap()
350 .cast_unchecked();
351
352 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
353
354 assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(42u32))));
355 }
356
357 #[test]
358 fn test_extract_timestamp_no_origin_server_ts() {
359 let event = Raw::new(&json!({
360 "event_id": "$ev0",
361 "type": "m.room.message",
362 "sender": "@mnt_io:matrix.org",
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!(timestamp.is_none());
373 }
374
375 #[test]
376 fn test_extract_timestamp_invalid_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 "origin_server_ts": "saucisse",
382 "content": {
383 "body": "Le gras, c'est la vie",
384 }
385 }))
386 .unwrap()
387 .cast_unchecked();
388
389 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
390
391 assert!(timestamp.is_none());
392 }
393
394 #[test]
395 fn test_extract_timestamp_malicious_origin_server_ts() {
396 let event = Raw::new(&json!({
397 "event_id": "$ev0",
398 "type": "m.room.message",
399 "sender": "@mnt_io:matrix.org",
400 "origin_server_ts": 101,
401 "content": {
402 "body": "Le gras, c'est la vie",
403 }
404 }))
405 .unwrap()
406 .cast_unchecked();
407
408 let timestamp = extract_timestamp(&event, MilliSecondsSinceUnixEpoch(UInt::from(100u32)));
409
410 assert_eq!(timestamp, Some(MilliSecondsSinceUnixEpoch(UInt::from(100u32))));
411 }
412}