Skip to main content

tuwunel_service/account_data/
direct.rs

1//! Direct-room account data helpers.
2//!
3//! These operations read and update a user's `m.direct` mapping. Callers can list direct rooms
4//! without selecting a counterparty.
5
6use std::collections::BTreeSet;
7
8use ruma::{
9	OwnedRoomId, RoomId, UserId,
10	events::{
11		GlobalAccountDataEventType,
12		direct::{DirectEvent, DirectEventContent, DirectUserIdentifier},
13	},
14};
15use tuwunel_core::{Result, at, implement, is_equal_to};
16
17/// The rooms the user's `m.direct` names, whoever the counterparty is.
18///
19/// The map is keyed by counterparty rather than by room, so answering for one
20/// room means flattening all of it. A user with no `m.direct` at all has no
21/// direct rooms.
22#[implement(super::Service)]
23pub async fn direct_rooms(&self, user_id: &UserId) -> BTreeSet<OwnedRoomId> {
24	self.direct_content(user_id)
25		.await
26		.into_iter()
27		.flat_map(DirectEventContent::into_iter)
28		.map(at!(1))
29		.flat_map(Vec::into_iter)
30		.collect()
31}
32
33/// Record a room as a direct chat with `target` in the user's `m.direct`.
34///
35/// The room joins the counterparty's list, which is created when this is
36/// their first direct room. A list already naming the room is left alone,
37/// so repeated calls settle on one entry.
38///
39/// # Panics
40///
41/// Panics when storing a changed mapping requires a global sequence number that cannot be allocated
42/// or persisted.
43#[implement(super::Service)]
44pub async fn mark_direct(&self, user_id: &UserId, target: &UserId, room_id: &RoomId) -> Result {
45	let mut content = self
46		.direct_content(user_id)
47		.await
48		.unwrap_or_default();
49
50	let target: &DirectUserIdentifier = target.into();
51	let listed = content
52		.get(target)
53		.is_some_and(|rooms| rooms.iter().any(is_equal_to!(room_id)));
54
55	if listed {
56		return Ok(());
57	}
58
59	content
60		.entry(target.to_owned())
61		.or_default()
62		.push(room_id.to_owned());
63
64	let event = serde_json::to_value(DirectEvent { content })?;
65
66	let event_type = GlobalAccountDataEventType::Direct
67		.to_string()
68		.into();
69
70	self.services
71		.account_data
72		.update(None, user_id, event_type, &event)
73		.await
74}
75
76/// The user's `m.direct` content, absent when they have never had one.
77///
78/// Account data is stored as the whole event rather than as its content, so
79/// the record deserializes as an event and the content comes off it.
80#[implement(super::Service)]
81async fn direct_content(&self, user_id: &UserId) -> Result<DirectEventContent> {
82	self.services
83		.account_data
84		.get_global(user_id, GlobalAccountDataEventType::Direct)
85		.await
86		.map(|event: DirectEvent| event.content)
87}