matrix_ui_serializable/models/async_requests.rs
1use matrix_sdk::{
2 OwnedServerName, RoomMemberships,
3 media::MediaRequestParameters,
4 room::{
5 RoomMember,
6 edit::EditedContent,
7 reply::{EnforceThread, Reply},
8 },
9 ruma::{
10 OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
11 events::room::message::{ReplyWithinThread, RoomMessageEventContent},
12 matrix_uri::MatrixId,
13 },
14};
15use matrix_sdk_ui::timeline::TimelineEventItemId;
16use serde::{Deserialize, Deserializer};
17use serde_json::Value;
18use tokio::sync::oneshot;
19
20use crate::{events::timeline::PaginationDirection, init::singletons::REQUEST_SENDER};
21
22/// Submits a request to the worker thread to be executed asynchronously.
23pub(crate) fn submit_async_request(req: MatrixRequest) {
24 REQUEST_SENDER
25 .get()
26 .unwrap() // this is initialized
27 .send(req)
28 .expect("BUG: async worker task receiver has died!");
29}
30
31/// The set of requests for async work that can be made to the worker thread.
32#[allow(clippy::large_enum_variant)]
33pub enum MatrixRequest {
34 /// Request to paginate the older (or newer) events of a room's timeline.
35 PaginateRoomTimeline {
36 room_id: OwnedRoomId,
37 /// The maximum number of timeline events to fetch in each pagination batch.
38 num_events: u16,
39 direction: PaginationDirection,
40 },
41 /// Request to edit the content of an event in the given room's timeline.
42 EditMessage {
43 room_id: OwnedRoomId,
44 timeline_event_item_id: TimelineEventItemId,
45 edited_content: EditedContent,
46 },
47 /// Request to fetch the full details of the given event in the given room's timeline.
48 FetchDetailsForEvent {
49 room_id: OwnedRoomId,
50 event_id: OwnedEventId,
51 },
52 /// Request to fetch profile information for all members of a room.
53 /// This can be *very* slow depending on the number of members in the room.
54 SyncRoomMemberList {
55 room_id: OwnedRoomId,
56 },
57 /// Request to join the given room.
58 JoinRoom {
59 room_id: OwnedRoomId,
60 },
61 /// Request to leave the given room.
62 LeaveRoom {
63 room_id: OwnedRoomId,
64 },
65 /// Request to get the actual list of members in a room.
66 /// This returns the list of members that can be displayed in the UI.
67 GetRoomMembers {
68 room_id: OwnedRoomId,
69 memberships: RoomMemberships,
70 /// * If `true` (not recommended), only the local cache will be accessed.
71 /// * If `false` (recommended), details will be fetched from the server.
72 local_only: bool,
73 },
74 /// Request to fetch profile information for the given user ID.
75 GetUserProfile {
76 user_id: OwnedUserId,
77 /// * If `Some`, the user is known to be a member of a room, so this will
78 /// fetch the user's profile from that room's membership info.
79 /// * If `None`, the user's profile info will be fetched from the server
80 /// in a room-agnostic manner, and no room membership info will be returned.
81 room_id: Option<OwnedRoomId>,
82 /// * If `true` (not recommended), only the local cache will be accessed.
83 /// * If `false` (recommended), details will be fetched from the server.
84 local_only: bool,
85 },
86 /// Request to fetch the number of unread messages in the given room.
87 GetNumberUnreadMessages {
88 room_id: OwnedRoomId,
89 },
90 /// Request to ignore/block or unignore/unblock a user.
91 IgnoreUser {
92 /// Whether to ignore (`true`) or unignore (`false`) the user.
93 ignore: bool,
94 /// The room membership info of the user to (un)ignore.
95 room_member: RoomMember,
96 /// The room ID of the room where the user is a member,
97 /// which is only needed because it isn't present in the `RoomMember` object.
98 room_id: OwnedRoomId,
99 },
100 /// Request to resolve a room alias into a room ID and the servers that know about that room.
101 ResolveRoomAlias(OwnedRoomAliasId),
102 /// Request to fetch media from the server.
103 /// Upon completion of the async media request, the `on_fetched` function
104 /// will be invoked with four arguments: the `destination`, the `media_request`,
105 /// the result of the media fetch, and the `update_sender`.
106 FetchMedia {
107 media_request: MediaRequestParameters,
108 content_sender: oneshot::Sender<Result<Vec<u8>, matrix_sdk::Error>>,
109 },
110 /// Request to send a message to the given room.
111 SendMessage {
112 room_id: OwnedRoomId,
113 message: RoomMessageEventContent,
114 replied_to: Option<Reply>,
115 },
116 /// Sends a notice to the given room that the current user is or is not typing.
117 ///
118 /// This request does not return a response or notify the UI thread, and
119 /// furthermore, there is no need to send a follow-up request to stop typing
120 /// (though you certainly can do so).
121 SendTypingNotice {
122 room_id: OwnedRoomId,
123 typing: bool,
124 },
125 /// Subscribe to typing notices for the given room.
126 ///
127 /// This request does not return a response or notify the UI thread.
128 SubscribeToTypingNotices {
129 room_id: OwnedRoomId,
130 /// Whether to subscribe or unsubscribe from typing notices for this room.
131 subscribe: bool,
132 },
133 /// Subscribe to changes in the read receipts of our own user.
134 ///
135 /// This request does not return a response or notify the UI thread.
136 SubscribeToOwnUserReadReceiptsChanged {
137 room_id: OwnedRoomId,
138 /// Whether to subscribe or unsubscribe to changes in the read receipts of our own user for this room
139 subscribe: bool,
140 },
141 /// Sends a read receipt for the given event in the given room.
142 ReadReceipt {
143 room_id: OwnedRoomId,
144 event_id: OwnedEventId,
145 },
146 /// Sends a fully-read receipt for the given event in the given room.
147 FullyReadReceipt {
148 room_id: OwnedRoomId,
149 event_id: OwnedEventId,
150 },
151 /// Sends a request to obtain the power levels for this room.
152 ///
153 /// The response is delivered back to the main UI thread via [`TimelineUpdate::UserPowerLevels`].
154 GetRoomPowerLevels {
155 room_id: OwnedRoomId,
156 },
157 /// Toggles the given reaction to the given event in the given room.
158 ToggleReaction {
159 room_id: OwnedRoomId,
160 timeline_event_id: TimelineEventItemId,
161 reaction: String,
162 },
163 /// Redacts (deletes) the given event in the given room.
164 #[doc(alias("delete"))]
165 RedactMessage {
166 room_id: OwnedRoomId,
167 timeline_event_id: TimelineEventItemId,
168 reason: Option<String>,
169 },
170 /// Sends a request to obtain the room's pill link info for the given Matrix ID.
171 ///
172 /// The MatrixLinkPillInfo::Loaded variant is sent back to the main UI thread via.
173 GetMatrixRoomLinkPillInfo {
174 matrix_id: MatrixId,
175 via: Vec<OwnedServerName>,
176 },
177 CreateDMRoom {
178 user_id: OwnedUserId,
179 },
180}
181// Deserialize trait is implemented in models/async_requests.rs
182
183impl<'de> Deserialize<'de> for MatrixRequest {
184 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185 where
186 D: Deserializer<'de>,
187 {
188 // First deserialize into a generic Value to inspect the structure
189 let value = Value::deserialize(deserializer)?;
190
191 // Extract the "event" field to determine the variant
192 let event = value
193 .get("event")
194 .and_then(|v| v.as_str())
195 .ok_or_else(|| serde::de::Error::missing_field("event"))?;
196
197 // Extract the "payload" field containing the variant data
198 let payload = value
199 .get("payload")
200 .ok_or_else(|| serde::de::Error::missing_field("payload"))?;
201
202 // Match on the event type and deserialize the appropriate variant
203 match event {
204 "paginateRoomTimeline" => {
205 let data: PaginateRoomTimelinePayload =
206 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
207 Ok(MatrixRequest::PaginateRoomTimeline {
208 room_id: data.room_id,
209 num_events: data.num_events,
210 direction: data.direction,
211 })
212 }
213 // "editMessage" => {
214 // let data: EditMessagePayload =
215 // serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
216 // Ok(MatrixRequest::EditMessage {
217 // room_id: data.room_id,
218 // timeline_event_item_id: data.timeline_event_item_id,
219 // edited_content: data.edited_content,
220 // })
221 // }
222 "fetchDetailsForEvent" => {
223 let data: FetchDetailsForEventPayload =
224 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
225 Ok(MatrixRequest::FetchDetailsForEvent {
226 room_id: data.room_id,
227 event_id: data.event_id,
228 })
229 }
230 // "syncRoomMemberList" => {
231 // let data: SyncRoomMemberListPayload =
232 // serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
233 // Ok(MatrixRequest::SyncRoomMemberList {
234 // room_id: data.room_id,
235 // })
236 // }
237 "joinRoom" => {
238 let data: JoinRoomPayload =
239 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
240 Ok(MatrixRequest::JoinRoom {
241 room_id: data.room_id,
242 })
243 }
244 "leaveRoom" => {
245 let data: LeaveRoomPayload =
246 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
247 Ok(MatrixRequest::LeaveRoom {
248 room_id: data.room_id,
249 })
250 }
251 // "getRoomMembers" => {
252 // let data: GetRoomMembersPayload =
253 // serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
254 // Ok(MatrixRequest::GetRoomMembers {
255 // room_id: data.room_id,
256 // memberships: data.memberships,
257 // local_only: data.local_only,
258 // })
259 // }
260 "getUserProfile" => {
261 let data: GetUserProfilePayload =
262 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
263 Ok(MatrixRequest::GetUserProfile {
264 user_id: data.user_id,
265 room_id: data.room_id,
266 local_only: data.local_only,
267 })
268 }
269 "getNumberUnreadMessages" => {
270 let data: GetNumberUnreadMessagesPayload =
271 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
272 Ok(MatrixRequest::GetNumberUnreadMessages {
273 room_id: data.room_id,
274 })
275 }
276 // "ignoreUser" => {
277 // let data: IgnoreUserPayload =
278 // serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
279 // Ok(MatrixRequest::IgnoreUser {
280 // ignore: data.ignore,
281 // room_member: data.room_member,
282 // room_id: data.room_id,
283 // })
284 // }
285 "resolveRoomAlias" => {
286 let alias =
287 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
288 Ok(MatrixRequest::ResolveRoomAlias(alias))
289 }
290 "sendMessage" => {
291 let data: SendMessagePayload =
292 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
293 let reply_option = match data.reply_to_event_id {
294 Some(id) => Some(Reply {
295 event_id: id,
296 enforce_thread: EnforceThread::Threaded(ReplyWithinThread::Yes),
297 }),
298 None => None,
299 };
300 Ok(MatrixRequest::SendMessage {
301 room_id: data.room_id,
302 message: data.message,
303 replied_to: reply_option,
304 })
305 }
306 "sendTypingNotice" => {
307 let data: SendTypingNoticePayload =
308 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
309 Ok(MatrixRequest::SendTypingNotice {
310 room_id: data.room_id,
311 typing: data.typing,
312 })
313 }
314 "subscribeToTypingNotices" => {
315 let data: SubscribeToTypingNoticesPayload =
316 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
317 Ok(MatrixRequest::SubscribeToTypingNotices {
318 room_id: data.room_id,
319 subscribe: data.subscribe,
320 })
321 }
322 "subscribeToOwnUserReadReceiptsChanged" => {
323 let data: SubscribeToOwnUserReadReceiptsChangedPayload =
324 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
325 Ok(MatrixRequest::SubscribeToOwnUserReadReceiptsChanged {
326 room_id: data.room_id,
327 subscribe: data.subscribe,
328 })
329 }
330 "readReceipt" => {
331 let data: ReadReceiptPayload =
332 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
333 Ok(MatrixRequest::ReadReceipt {
334 room_id: data.room_id,
335 event_id: data.event_id,
336 })
337 }
338 "fullyReadReceipt" => {
339 let data: FullyReadReceiptPayload =
340 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
341 Ok(MatrixRequest::FullyReadReceipt {
342 room_id: data.room_id,
343 event_id: data.event_id,
344 })
345 }
346 "getRoomPowerLevels" => {
347 let data: GetRoomPowerLevelsPayload =
348 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
349 Ok(MatrixRequest::GetRoomPowerLevels {
350 room_id: data.room_id,
351 })
352 }
353 "toggleReaction" => {
354 let data: ToggleReactionPayload =
355 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
356 Ok(MatrixRequest::ToggleReaction {
357 room_id: data.room_id,
358 timeline_event_id: TimelineEventItemId::EventId(
359 OwnedEventId::try_from(data.timeline_event_id)
360 .expect("Frontend sent incorrect event id"),
361 ), // We only use eventId, not transactions.
362 reaction: data.reaction,
363 })
364 }
365 // "redactMessage" => {
366 // let data: RedactMessagePayload =
367 // serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
368 // Ok(MatrixRequest::RedactMessage {
369 // room_id: data.room_id,
370 // timeline_event_id: data.timeline_event_id,
371 // reason: data.reason,
372 // })
373 // }
374 // "getMatrixRoomLinkPillInfo" => {
375 // let data: GetMatrixRoomLinkPillInfoPayload =
376 // serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
377 // Ok(MatrixRequest::GetMatrixRoomLinkPillInfo {
378 // matrix_id: data.matrix_id,
379 // via: data.via,
380 // })
381 // }
382 "createDMRoom" => {
383 let data: CreateDMRoomPayload =
384 serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?;
385 Ok(MatrixRequest::CreateDMRoom {
386 user_id: data.user_id,
387 })
388 }
389 _ => Err(serde::de::Error::unknown_variant(
390 event,
391 &[
392 "paginateRoomTimeline",
393 // "editMessage",
394 "fetchDetailsForEvent",
395 // "syncRoomMemberList",
396 "joinRoom",
397 "leaveRoom",
398 // "getRoomMembers",
399 "getUserProfile",
400 "getNumberUnreadMessages",
401 // "ignoreUser",
402 "resolveRoomAlias",
403 "sendMessage",
404 "sendTypingNotice",
405 "subscribeToTypingNotices",
406 "subscribeToOwnUserReadReceiptsChanged",
407 "readReceipt",
408 "fullyReadReceipt",
409 "getRoomPowerLevels",
410 "toggleReaction",
411 // "redactMessage",
412 // "getMatrixRoomLinkPillInfo",
413 "createDMRoom",
414 ],
415 )),
416 }
417 }
418}
419
420// Helper structs for deserializing payloads
421#[derive(Deserialize)]
422#[serde(rename_all = "camelCase")]
423struct PaginateRoomTimelinePayload {
424 room_id: OwnedRoomId,
425 num_events: u16,
426 direction: PaginationDirection,
427}
428
429// #[derive(Deserialize)]
430// #[serde(rename_all = "camelCase")]
431// struct EditMessagePayload {
432// room_id: OwnedRoomId,
433// timeline_event_item_id: TimelineEventItemId,
434// edited_content: EditedContent,
435// }
436
437#[derive(Deserialize)]
438#[serde(rename_all = "camelCase")]
439struct FetchDetailsForEventPayload {
440 room_id: OwnedRoomId,
441 event_id: OwnedEventId,
442}
443
444// #[derive(Deserialize)]
445// #[serde(rename_all = "camelCase")]
446// struct SyncRoomMemberListPayload {
447// room_id: OwnedRoomId,
448// }
449
450#[derive(Deserialize)]
451#[serde(rename_all = "camelCase")]
452struct JoinRoomPayload {
453 room_id: OwnedRoomId,
454}
455
456#[derive(Deserialize)]
457#[serde(rename_all = "camelCase")]
458struct LeaveRoomPayload {
459 room_id: OwnedRoomId,
460}
461
462// #[derive(Deserialize)]
463// #[serde(rename_all = "camelCase")]
464// struct GetRoomMembersPayload {
465// room_id: OwnedRoomId,
466// memberships: RoomMemberships,
467// local_only: bool,
468// }
469
470#[derive(Deserialize)]
471#[serde(rename_all = "camelCase")]
472struct GetUserProfilePayload {
473 user_id: OwnedUserId,
474 room_id: Option<OwnedRoomId>,
475 local_only: bool,
476}
477
478#[derive(Deserialize)]
479#[serde(rename_all = "camelCase")]
480struct GetNumberUnreadMessagesPayload {
481 room_id: OwnedRoomId,
482}
483
484// #[derive(Deserialize)]
485// #[serde(rename_all = "camelCase")]
486// struct IgnoreUserPayload {
487// ignore: bool,
488// room_member: RoomMember,
489// room_id: OwnedRoomId,
490// }
491
492#[derive(Deserialize)]
493#[serde(rename_all = "camelCase")]
494struct SendMessagePayload {
495 room_id: OwnedRoomId,
496 message: RoomMessageEventContent,
497 reply_to_event_id: Option<OwnedEventId>,
498}
499
500#[derive(Deserialize)]
501#[serde(rename_all = "camelCase")]
502struct SendTypingNoticePayload {
503 room_id: OwnedRoomId,
504 typing: bool,
505}
506
507#[derive(Deserialize)]
508#[serde(rename_all = "camelCase")]
509struct SubscribeToTypingNoticesPayload {
510 room_id: OwnedRoomId,
511 subscribe: bool,
512}
513
514#[derive(Deserialize)]
515#[serde(rename_all = "camelCase")]
516struct SubscribeToOwnUserReadReceiptsChangedPayload {
517 room_id: OwnedRoomId,
518 subscribe: bool,
519}
520
521#[derive(Deserialize)]
522#[serde(rename_all = "camelCase")]
523struct ReadReceiptPayload {
524 room_id: OwnedRoomId,
525 event_id: OwnedEventId,
526}
527
528#[derive(Deserialize)]
529#[serde(rename_all = "camelCase")]
530struct FullyReadReceiptPayload {
531 room_id: OwnedRoomId,
532 event_id: OwnedEventId,
533}
534
535#[derive(Deserialize)]
536#[serde(rename_all = "camelCase")]
537struct GetRoomPowerLevelsPayload {
538 room_id: OwnedRoomId,
539}
540
541#[derive(Deserialize)]
542#[serde(rename_all = "camelCase")]
543struct ToggleReactionPayload {
544 room_id: OwnedRoomId,
545 timeline_event_id: String,
546 reaction: String,
547}
548
549// #[derive(Deserialize)]
550// #[serde(rename_all = "camelCase")]
551// struct RedactMessagePayload {
552// room_id: OwnedRoomId,
553// timeline_event_id: TimelineEventItemId,
554// reason: Option<String>,
555// }
556
557// #[derive(Deserialize)]
558// #[serde(rename_all = "camelCase")]
559// struct GetMatrixRoomLinkPillInfoPayload {
560// matrix_id: MatrixId,
561// via: Vec<OwnedServerName>,
562// }
563
564#[derive(Deserialize)]
565#[serde(rename_all = "camelCase")]
566struct CreateDMRoomPayload {
567 user_id: OwnedUserId,
568}