Skip to main content

tuwunel_service/account_data/
mod.rs

1//! User and room account data storage.
2//!
3//! The service stores raw account data events behind monotonic update counters and maintains a
4//! secondary index by room, user, and event type. Its streams expose changes for incremental sync.
5
6mod direct;
7mod push_rules;
8mod room_tags;
9
10use std::sync::Arc;
11
12use futures::{Stream, StreamExt, TryFutureExt, pin_mut};
13use ruma::{
14	RoomId, UserId,
15	events::{
16		AnyGlobalAccountDataEvent, AnyRawAccountDataEvent, AnyRoomAccountDataEvent,
17		GlobalAccountDataEventType, RoomAccountDataEventType,
18	},
19	push::{RuleKind, Ruleset},
20	serde::Raw,
21};
22use serde::Deserialize;
23use serde_json::json;
24use tuwunel_core::{
25	Err, Result, at, err, implement,
26	utils::{ReadyExt, TryReadyExt, result::LogErr, stream::TryIgnore},
27};
28use tuwunel_database::{Deserialized, Handle, Ignore, Interfix, Json, Map};
29
30/// Maximum number of push rules one account may hold.
31///
32/// The ruleset is a single account-data blob rewritten in full on every
33/// mutation and matched against every event for every local recipient, so the
34/// count bounds the write cost and the per-event matching work alike.
35pub const MAX_RULES: usize = 10_000;
36
37/// Longest rule ID stored, in bytes.
38///
39/// Room IDs reach 255 bytes and are routinely used as rule IDs, so the ceiling
40/// sits above that rather than at it.
41pub const MAX_RULE_ID_BYTES: usize = 300;
42
43/// Largest match and action data stored for one rule, in bytes.
44///
45/// Rule IDs are excluded and bounded separately by [`MAX_RULE_ID_BYTES`].
46pub const MAX_RULE_BYTES: usize = 1024;
47
48/// Stores and queries global and room-scoped account data.
49///
50/// Updates commit the secondary pointer and new event blob in one transaction while retaining
51/// monotonic counters for sync. Typed accessors deserialize the complete account data event
52/// envelope.
53pub struct Service {
54	services: Arc<crate::services::OnceServices>,
55	db: Data,
56}
57
58struct Data {
59	roomuserdataid_accountdata: Arc<Map>,
60	roomusertype_roomuserdataid: Arc<Map>,
61}
62
63impl crate::Service for Service {
64	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
65		Ok(Arc::new(Self {
66			services: args.services.clone(),
67			db: Data {
68				roomuserdataid_accountdata: args.db["roomuserdataid_accountdata"].clone(),
69				roomusertype_roomuserdataid: args.db["roomusertype_roomuserdataid"].clone(),
70			},
71		}))
72	}
73
74	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
75}
76
77/// Whether a ruleset has room for the rule with the given kind and ID.
78///
79/// A rule replacing one already present adds nothing, so only an unseen ID is
80/// held against [`MAX_RULES`]. The ID is bounded here rather than at each call
81/// site because a room ID serves as the rule ID for room rules and carries no
82/// length of its own once arbitrary-length identifiers are accepted.
83#[must_use]
84pub fn admits_rule(ruleset: &Ruleset, kind: RuleKind, rule_id: &str) -> bool {
85	rule_id.len() <= MAX_RULE_ID_BYTES
86		&& (ruleset.get(kind, rule_id).is_some()
87			|| ruleset.iter().take(MAX_RULES).count() < MAX_RULES)
88}
89
90/// Stores an account data event and replaces its previous revision.
91///
92/// The input must contain complete `type` and `content` fields. A new global counter value keys the
93/// event blob, secondary pointer, and removal of the superseded blob in one transaction.
94///
95/// # Panics
96///
97/// Panics when dispatching the global sequence number fails.
98#[implement(Service)]
99pub async fn update(
100	&self,
101	room_id: Option<&RoomId>,
102	user_id: &UserId,
103	event_type: RoomAccountDataEventType,
104	data: &serde_json::Value,
105) -> Result {
106	if data.get("type").is_none() || data.get("content").is_none() {
107		return Err!(Request(InvalidParam("Account data doesn't have all required fields.")));
108	}
109
110	let count = self.services.globals.next_count();
111	let roomuserdataid = (room_id, user_id, *count, &event_type);
112	let key = (room_id, user_id, &event_type);
113	let prev = self
114		.db
115		.roomusertype_roomuserdataid
116		.qry(&key)
117		.await;
118
119	let mut txn = self.services.db.txn();
120
121	txn.put(&self.db.roomuserdataid_accountdata, roomuserdataid, Json(data));
122	txn.put(&self.db.roomusertype_roomuserdataid, key, roomuserdataid);
123
124	if let Ok(prev) = prev {
125		txn.del_raw(&self.db.roomuserdataid_accountdata, prev);
126	}
127
128	txn.execute();
129
130	Ok(())
131}
132
133/// Replaces an account data event with an MSC3391 tombstone.
134///
135/// Delta sync surfaces the empty content so clients can apply the deletion. Zero-token V3 and V5
136/// sync responses and client account-data GET routes treat the tombstone as absent.
137///
138/// # Panics
139///
140/// Panics when dispatching the global sequence number fails.
141#[implement(Service)]
142pub async fn delete(
143	&self,
144	room_id: Option<&RoomId>,
145	user_id: &UserId,
146	event_type: RoomAccountDataEventType,
147) -> Result {
148	let tombstone = json!({
149		"type": event_type.to_string(),
150		"content": {},
151	});
152
153	self.update(room_id, user_id, event_type, &tombstone)
154		.await
155}
156
157/// Searches the global account data for a specific kind.
158///
159/// Global data is stored under no room, so one kind can be held once globally
160/// and once in every room without collision. The record is the whole
161/// `{type, content}` event, so `T` names the event rather than its content.
162#[implement(Service)]
163pub async fn get_global<T>(&self, user_id: &UserId, kind: GlobalAccountDataEventType) -> Result<T>
164where
165	T: for<'de> Deserialize<'de>,
166{
167	self.get_raw(None, user_id, &kind.to_string())
168		.await
169		.deserialized()
170}
171
172/// Searches the room account data for a specific kind.
173///
174/// The room scopes the lookup, so one kind may hold different data in each of
175/// them. The record is the whole `{type, content}` event, so `T` names the
176/// event rather than its content.
177#[implement(Service)]
178pub async fn get_room<T>(
179	&self,
180	room_id: &RoomId,
181	user_id: &UserId,
182	kind: RoomAccountDataEventType,
183) -> Result<T>
184where
185	T: for<'de> Deserialize<'de>,
186{
187	self.get_raw(Some(room_id), user_id, &kind.to_string())
188		.await
189		.deserialized()
190}
191
192/// Loads a stored account data event without deserializing it.
193///
194/// The optional room ID selects room-scoped or global account data. The lookup resolves the
195/// current secondary index entry before borrowing the raw event from storage.
196#[implement(Service)]
197pub async fn get_raw(
198	&self,
199	room_id: Option<&RoomId>,
200	user_id: &UserId,
201	kind: &str,
202) -> Result<Handle<'_>> {
203	let key = (room_id, user_id, kind.to_owned());
204	self.db
205		.roomusertype_roomuserdataid
206		.qry(&key)
207		.and_then(|roomuserdataid| {
208			self.db
209				.roomuserdataid_accountdata
210				.get(&roomuserdataid)
211		})
212		.await
213}
214
215/// Streams account data changes after a counter value.
216///
217/// The optional upper bound is inclusive. Cursor and decoding failures are logged and omitted from
218/// this convenience stream.
219#[implement(Service)]
220pub fn changes_since<'a>(
221	&'a self,
222	room_id: Option<&'a RoomId>,
223	user_id: &'a UserId,
224	since: u64,
225	to: Option<u64>,
226) -> impl Stream<Item = AnyRawAccountDataEvent> + Send + 'a {
227	self.changes_since_fallible(room_id, user_id, since, to)
228		.map(LogErr::log_err)
229		.ignore_err()
230}
231
232/// Returns bounded account-data changes without suppressing failures.
233///
234/// The lower bound is exclusive and the optional upper bound is inclusive.
235/// Cursor, decode, and deserialization failures remain in the stream for an
236/// atomic caller to handle.
237#[implement(Service)]
238pub fn changes_since_fallible<'a>(
239	&'a self,
240	room_id: Option<&'a RoomId>,
241	user_id: &'a UserId,
242	since: u64,
243	to: Option<u64>,
244) -> impl Stream<Item = Result<AnyRawAccountDataEvent>> + Send + 'a {
245	type Key<'a> = (Option<&'a RoomId>, &'a UserId, u64, Ignore);
246
247	// Skip the data that's exactly at since, because we sent that last time
248	let first_possible = (room_id, user_id, since.saturating_add(1));
249
250	self.db
251		.roomuserdataid_accountdata
252		.stream_from(&first_possible)
253		.ready_try_take_while(move |((room_id_, user_id_, count, _), _): &(Key<'_>, _)| {
254			Ok(room_id == *room_id_ && user_id == *user_id_ && to.is_none_or(|to| *count <= to))
255		})
256		.ready_and_then(move |(_, v)| {
257			match room_id {
258				| Some(_) => serde_json::from_slice::<Raw<AnyRoomAccountDataEvent>>(v)
259					.map(AnyRawAccountDataEvent::Room),
260				| None => serde_json::from_slice::<Raw<AnyGlobalAccountDataEvent>>(v)
261					.map(AnyRawAccountDataEvent::Global),
262			}
263			.map_err(|e| err!(Database("Database contains invalid account data: {e}")))
264		})
265}
266
267/// Erases a user's account data within one MSC4025 namespace.
268///
269/// An absent room selects global data, while a room selects only that room's data. Rows yielded
270/// successfully from both prefix scans are deleted together; scan errors are skipped.
271#[implement(Service)]
272pub async fn erase_user(&self, user_id: &UserId, room_id: Option<&RoomId>) {
273	let prefix = (room_id, user_id, Interfix);
274	let mut txn = self.services.db.txn();
275
276	self.db
277		.roomuserdataid_accountdata
278		.keys_prefix_raw(&prefix)
279		.ignore_err()
280		.ready_for_each(|key| txn.del_raw(&self.db.roomuserdataid_accountdata, key))
281		.await;
282
283	self.db
284		.roomusertype_roomuserdataid
285		.keys_prefix_raw(&prefix)
286		.ignore_err()
287		.ready_for_each(|key| txn.del_raw(&self.db.roomusertype_roomuserdataid, key))
288		.await;
289
290	txn.execute();
291}
292
293/// Returns the latest account data counter at or below an optional bound.
294///
295/// The lookup is scoped to one user and either global data or a single room. An empty scope produces
296/// a not-found request error.
297#[implement(Service)]
298pub async fn last_count<'a>(
299	&'a self,
300	room_id: Option<&'a RoomId>,
301	user_id: &'a UserId,
302	upper: Option<u64>,
303) -> Result<u64> {
304	type Key<'a> = (Option<&'a RoomId>, &'a UserId, u64, Ignore);
305
306	let upper = upper.unwrap_or(u64::MAX);
307	let key = (room_id, user_id, upper, Interfix);
308	let keys = self
309		.db
310		.roomuserdataid_accountdata
311		.rev_keys_from(&key)
312		.ignore_err()
313		.ready_take_while(move |(room_id_, user_id_, ..): &Key<'_>| {
314			room_id == *room_id_ && user_id == *user_id_
315		})
316		.map(at!(2));
317
318	pin_mut!(keys);
319	keys.next()
320		.await
321		.ok_or_else(|| err!(Request(NotFound("No account data found."))))
322}