1use ruma::{
19 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
119#[cfg(test)]
120mod tests {
121 use assert_matches::assert_matches;
122 use ruma::{event_id, serde::Raw};
123 use serde_json::json;
124
125 use super::extract_thread_root;
126 use crate::{
127 deserialized_responses::{ThreadSummary, ThreadSummaryStatus},
128 serde_helpers::extract_bundled_thread_summary,
129 };
130
131 #[test]
132 fn test_extract_thread_root() {
133 let thread_root = event_id!("$thread_root_event_id:example.com");
138 let event = Raw::new(&json!({
139 "event_id": "$eid:example.com",
140 "type": "m.room.message",
141 "sender": "@alice:example.com",
142 "origin_server_ts": 42,
143 "content": {
144 "body": "Hello, world!",
145 "m.relates_to": {
146 "rel_type": "m.thread",
147 "event_id": thread_root,
148 }
149 }
150 }))
151 .unwrap()
152 .cast_unchecked();
153
154 let observed_thread_root = extract_thread_root(&event);
155 assert_eq!(observed_thread_root.as_deref(), Some(thread_root));
156
157 let event = Raw::new(&json!({
160 "event_id": "$eid:example.com",
161 "type": "m.room.message",
162 "sender": "@alice:example.com",
163 "origin_server_ts": 42,
164 }))
165 .unwrap()
166 .cast_unchecked();
167
168 let observed_thread_root = extract_thread_root(&event);
169 assert_matches!(observed_thread_root, None);
170
171 let event = Raw::new(&json!({
173 "event_id": "$eid:example.com",
174 "type": "m.room.message",
175 "sender": "@alice:example.com",
176 "origin_server_ts": 42,
177 "content": {
178 "body": "Hello, world!",
179 }
180 }))
181 .unwrap()
182 .cast_unchecked();
183
184 let observed_thread_root = extract_thread_root(&event);
185 assert_matches!(observed_thread_root, None);
186
187 let event = Raw::new(&json!({
189 "event_id": "$eid:example.com",
190 "type": "m.room.message",
191 "sender": "@alice:example.com",
192 "origin_server_ts": 42,
193 "content": {
194 "body": "Hello, world!",
195 "m.relates_to": {
196 "rel_type": "m.reference",
197 "event_id": "$referenced_event_id:example.com",
198 }
199 }
200 }))
201 .unwrap()
202 .cast_unchecked();
203
204 let observed_thread_root = extract_thread_root(&event);
205 assert_matches!(observed_thread_root, None);
206 }
207
208 #[test]
209 fn test_extract_bundled_thread_summary() {
210 let event = Raw::new(&json!({
212 "event_id": "$eid:example.com",
213 "type": "m.room.message",
214 "sender": "@alice:example.com",
215 "origin_server_ts": 42,
216 "content": {
217 "body": "Hello, world!",
218 },
219 "unsigned": {
220 "m.relations": {
221 "m.thread": {
222 "latest_event": {
223 "event_id": "$latest_event:example.com",
224 "type": "m.room.message",
225 "sender": "@bob:example.com",
226 "origin_server_ts": 42,
227 "content": {
228 "body": "Hello to you too!",
229 }
230 },
231 "count": 2,
232 "current_user_participated": true,
233 }
234 }
235 }
236 }))
237 .unwrap()
238 .cast_unchecked();
239
240 assert_matches!(
241 extract_bundled_thread_summary(&event),
242 (ThreadSummaryStatus::Some(ThreadSummary { .. }), Some(..))
243 );
244
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 }))
252 .unwrap()
253 .cast_unchecked();
254
255 assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
256
257 let event = Raw::new(&json!({
259 "event_id": "$eid:example.com",
260 "type": "m.room.message",
261 "sender": "@alice:example.com",
262 "origin_server_ts": 42,
263 "content": {
264 "body": "Bonjour, monde!",
265 },
266 "unsigned": {
267 "m.relations": {
268 "m.replace":
269 {
270 "event_id": "$update:example.com",
271 "type": "m.room.message",
272 "sender": "@alice:example.com",
273 "origin_server_ts": 43,
274 "content": {
275 "body": "* Hello, world!",
276 }
277 },
278 }
279 }
280 }))
281 .unwrap()
282 .cast_unchecked();
283
284 assert_matches!(extract_bundled_thread_summary(&event), (ThreadSummaryStatus::None, None));
285
286 let event = Raw::new(&json!({
289 "event_id": "$eid:example.com",
290 "type": "m.room.message",
291 "sender": "@alice:example.com",
292 "origin_server_ts": 42,
293 "unsigned": {
294 "m.relations": {
295 "m.thread": {
296 }
298 }
299 }
300 }))
301 .unwrap()
302 .cast_unchecked();
303
304 assert_matches!(
305 extract_bundled_thread_summary(&event),
306 (ThreadSummaryStatus::Unknown, None)
307 );
308 }
309}