Skip to main content

tuwunel_service/rooms/state_cache/
mod.rs

1//! Persistent indexes derived from room membership state.
2//!
3//! The service maintains paired user-to-room and room-to-user membership rows,
4//! aggregate counts, participating-server indexes, and cached stripped state.
5//! Read helpers expose point queries and cursor-backed streams over those
6//! derived indexes.
7
8mod update;
9mod via;
10
11use std::{
12	collections::HashMap,
13	convert::identity,
14	sync::{Arc, RwLock},
15};
16
17use futures::{
18	Stream, StreamExt, TryStreamExt,
19	future::join5,
20	pin_mut,
21	stream::{empty, select},
22};
23use ruma::{
24	OwnedRoomId, OwnedServerName, RoomId, ServerName, UserId,
25	events::{AnyStrippedStateEvent, AnySyncStateEvent, room::member::MembershipState},
26	serde::Raw,
27};
28use serde::de::DeserializeOwned;
29use tuwunel_core::{
30	Result, debug_warn, implement,
31	matrix::{Event, Pdu, event::Owned},
32	trace,
33	utils::{
34		self, BoolExt,
35		result::NotFound,
36		stream::{BroadbandExt, IterStream, ReadyExt, TryIgnore},
37	},
38	warn,
39};
40use tuwunel_database::{Deserialized, Ignore, Interfix, Map};
41use update::EMPTY_INVITE_STATE;
42/// Input types for applying membership-cache transitions.
43///
44/// The update descriptor owns event data while borrowing the affected user and
45/// room identifiers. Optional stripped state accompanies invite and knock
46/// transitions.
47pub use update::{MembershipUpdate, StrippedRoomState};
48
49use crate::appservice::RegistrationInfo;
50
51/// Persistent room-membership cache and derived-index service.
52///
53/// Paired indexes answer membership queries in either direction, while room
54/// aggregates track counts and participating servers. A process-local cache
55/// memoizes appservice membership decisions.
56pub struct Service {
57	appservice_in_room_cache: AppServiceInRoomCache,
58	services: Arc<crate::services::OnceServices>,
59	db: Data,
60}
61
62struct Data {
63	roomid_knockedcount: Arc<Map>,
64	roomid_invitedcount: Arc<Map>,
65	roomid_inviteviaservers: Arc<Map>,
66	roomid_joinedcount: Arc<Map>,
67	roomserverids: Arc<Map>,
68	roomuserid_invitecount: Arc<Map>,
69	roomuserid_joinedcount: Arc<Map>,
70	roomuserid_leftcount: Arc<Map>,
71	roomuserid_knockedcount: Arc<Map>,
72	roomuseroncejoinedids: Arc<Map>,
73	serverroomids: Arc<Map>,
74	userroomid_invitestate: Arc<Map>,
75	userroomid_joinedcount: Arc<Map>,
76	userroomid_leftstate: Arc<Map>,
77	userroomid_knockedstate: Arc<Map>,
78}
79
80type AppServiceInRoomCache = RwLock<HashMap<OwnedRoomId, HashMap<String, bool>>>;
81type StrippedStateEventItem = (OwnedRoomId, Vec<Raw<AnyStrippedStateEvent>>);
82type SyncStateEventItem = (OwnedRoomId, Vec<Raw<AnySyncStateEvent>>);
83
84impl crate::Service for Service {
85	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
86		Ok(Arc::new(Self {
87			appservice_in_room_cache: RwLock::new(HashMap::new()),
88			services: args.services.clone(),
89			db: Data {
90				roomid_knockedcount: args.db["roomid_knockedcount"].clone(),
91				roomid_invitedcount: args.db["roomid_invitedcount"].clone(),
92				roomid_inviteviaservers: args.db["roomid_inviteviaservers"].clone(),
93				roomid_joinedcount: args.db["roomid_joinedcount"].clone(),
94				roomserverids: args.db["roomserverids"].clone(),
95				roomuserid_invitecount: args.db["roomuserid_invitecount"].clone(),
96				roomuserid_joinedcount: args.db["roomuserid_joined"].clone(),
97				roomuserid_leftcount: args.db["roomuserid_leftcount"].clone(),
98				roomuserid_knockedcount: args.db["roomuserid_knockedcount"].clone(),
99				roomuseroncejoinedids: args.db["roomuseroncejoinedids"].clone(),
100				serverroomids: args.db["serverroomids"].clone(),
101				userroomid_invitestate: args.db["userroomid_invitestate"].clone(),
102				userroomid_joinedcount: args.db["userroomid_joined"].clone(),
103				userroomid_leftstate: args.db["userroomid_leftstate"].clone(),
104				userroomid_knockedstate: args.db["userroomid_knockedstate"].clone(),
105			},
106		}))
107	}
108
109	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
110}
111
112/// Tests whether an appservice participates in a room.
113///
114/// A registration participates when its sender or a namespace-matching user is
115/// joined. Results are memoized by room and registration identifier. Membership
116/// rebuilds and explicit clears remove cached entries, but do not fence an
117/// in-flight lookup from republishing an older result. Deleting a room's
118/// membership indexes does not invalidate this cache.
119#[implement(Service)]
120#[tracing::instrument(level = "trace", skip_all)]
121pub async fn appservice_in_room(&self, room_id: &RoomId, appservice: &RegistrationInfo) -> bool {
122	let cached = self
123		.appservice_in_room_cache
124		.read()
125		.expect("locked")
126		.get(room_id)
127		.and_then(|map| map.get(&appservice.registration.id))
128		.copied();
129
130	if let Some(cached) = cached {
131		return cached;
132	}
133
134	let in_room = self.is_joined(&appservice.sender, room_id).await
135		|| self
136			.room_members(room_id)
137			.ready_any(|user_id| appservice.is_user_match(user_id))
138			.await;
139
140	self.appservice_in_room_cache
141		.write()
142		.expect("locked")
143		.entry(room_id.into())
144		.or_default()
145		.insert(appservice.registration.id.clone(), in_room);
146
147	in_room
148}
149
150/// Returns the appservice membership cache's room count and capacity.
151///
152/// The first value counts room-level map entries rather than individual
153/// registrations. The second reports the backing map's current allocation
154/// capacity.
155#[implement(Service)]
156pub fn get_appservice_in_room_cache_usage(&self) -> (usize, usize) {
157	let cache = self
158		.appservice_in_room_cache
159		.read()
160		.expect("locked");
161
162	(cache.len(), cache.capacity())
163}
164
165/// Clears every memoized appservice membership decision.
166///
167/// Persistent membership indexes are not changed. Later uncached lookups
168/// recompute and cache their answers from current joined membership, but a
169/// lookup already in flight can republish an older result after the clear.
170#[implement(Service)]
171#[tracing::instrument(level = "debug", skip_all)]
172pub fn clear_appservice_in_room_cache(&self) {
173	self.appservice_in_room_cache
174		.write()
175		.expect("locked")
176		.clear();
177}
178
179/// Returns a stream of the remote servers participating in this room.
180///
181/// Our own server is filtered out, so the result is the federation fan-out
182/// set for the room. Items borrow the database cursor and are invalid after the
183/// next poll; consume or own each item before advancing. Storage errors are
184/// skipped as in [`Self::room_servers`].
185#[implement(Service)]
186#[tracing::instrument(skip(self), level = "trace")]
187pub fn remote_room_servers<'a>(
188	&'a self,
189	room_id: &'a RoomId,
190) -> impl Stream<Item = &ServerName> + Send + 'a {
191	self.room_servers(room_id)
192		.ready_filter(|server| !self.services.globals.server_is_ours(server))
193}
194
195/// Streams all servers recorded as participating in a room.
196///
197/// Storage and key-decoding failures are skipped. Each server name borrows the
198/// cursor and must be consumed or owned before the stream advances.
199#[implement(Service)]
200#[tracing::instrument(skip(self), level = "debug")]
201pub fn room_servers<'a>(
202	&'a self,
203	room_id: &'a RoomId,
204) -> impl Stream<Item = &ServerName> + Send + 'a {
205	let prefix = (room_id, Interfix);
206	self.db
207		.roomserverids
208		.keys_prefix(&prefix)
209		.ignore_err()
210		.map(|(_, server): (Ignore, &ServerName)| server)
211}
212
213/// Tests whether a server is recorded as participating in a room.
214///
215/// The reverse server-to-room index supplies the answer. Missing rows and
216/// storage failures both return `false`.
217#[implement(Service)]
218#[tracing::instrument(skip(self), level = "trace")]
219pub async fn server_in_room<'a>(&'a self, server: &'a ServerName, room_id: &'a RoomId) -> bool {
220	let key = (server, room_id);
221	self.db.serverroomids.qry(&key).await.is_ok()
222}
223
224/// Streams all rooms recorded for a participating server.
225///
226/// Storage and key-decoding failures are skipped. Each room identifier borrows
227/// the cursor and must be consumed or owned before the stream advances.
228#[implement(Service)]
229#[tracing::instrument(skip(self), level = "debug")]
230pub fn server_rooms<'a>(
231	&'a self,
232	server: &'a ServerName,
233) -> impl Stream<Item = &RoomId> + Send + 'a {
234	let prefix = (server, Interfix);
235	self.db
236		.serverroomids
237		.keys_prefix(&prefix)
238		.ignore_err()
239		.map(|(_, room_id): (Ignore, &RoomId)| room_id)
240}
241
242/// Streams each server participating in at least one known room.
243///
244/// Adjacent duplicate prefixes are collapsed, yielding server names in
245/// ascending key order. Storage and decoding failures are skipped, and each
246/// item borrows the cursor until its next poll.
247#[implement(Service)]
248#[tracing::instrument(skip(self), level = "debug")]
249pub fn servers(&self) -> impl Stream<Item = &ServerName> + Send + '_ {
250	self.db
251		.serverroomids
252		.keys()
253		.ignore_err()
254		.ready_scan(
255			None,
256			|last: &mut Option<OwnedServerName>, (server, _): (&ServerName, Ignore)| {
257				let fresh = last.as_deref() != Some(server);
258
259				if fresh {
260					*last = Some(server.to_owned());
261				}
262
263				Some(fresh.then_some(server))
264			},
265		)
266		.ready_filter_map(identity)
267}
268
269/// Tests whether a server participates in any known room.
270///
271/// The check stops at the first reverse-index row. Errors skipped by
272/// [`Self::server_rooms`] are indistinguishable from absence.
273#[implement(Service)]
274#[tracing::instrument(skip(self), level = "trace")]
275pub async fn server_shares_room(&self, server: &ServerName) -> bool {
276	self.server_rooms(server)
277		.ready_any(|_| true)
278		.await
279}
280
281/// Tests whether a server shares a joined room with a user.
282///
283/// The check searches rooms recorded for the server for any current user
284/// join. Index errors are treated as absent rows by the underlying helpers.
285#[implement(Service)]
286#[tracing::instrument(skip(self), level = "trace")]
287pub async fn server_sees_user(&self, server: &ServerName, user_id: &UserId) -> bool {
288	self.server_rooms(server)
289		.map(ToOwned::to_owned)
290		.broad_any(async |room_id| self.is_joined(user_id, &room_id).await)
291		.await
292}
293
294/// Tests whether two users share a currently joined room.
295///
296/// The check consumes only the first intersection result. Storage or decoding
297/// failures skipped by the joined-room streams can produce `false`.
298#[implement(Service)]
299#[tracing::instrument(skip(self), level = "trace")]
300pub async fn user_sees_user(&self, user_a: &UserId, user_b: &UserId) -> bool {
301	let get_shared_rooms = self.get_shared_rooms(user_a, user_b);
302
303	pin_mut!(get_shared_rooms);
304	get_shared_rooms.next().await.is_some()
305}
306
307/// Streams rooms in which both users are currently joined.
308///
309/// The two key-ordered joined-room streams are intersected without
310/// materializing either set. Items borrow their source database cursor and are
311/// invalid after the next poll; consume or own each item before advancing. Read
312/// failures are skipped by the source streams.
313#[implement(Service)]
314#[tracing::instrument(skip(self), level = "debug")]
315pub fn get_shared_rooms<'a>(
316	&'a self,
317	user_a: &'a UserId,
318	user_b: &'a UserId,
319) -> impl Stream<Item = &RoomId> + Send + 'a {
320	let a = self.rooms_joined(user_a);
321	let b = self.rooms_joined(user_b);
322
323	utils::set::intersection_sorted_stream2(a, b)
324}
325
326/// Streams all users currently indexed as joined to a room.
327///
328/// Storage and key-decoding failures are skipped. Each user identifier borrows
329/// the cursor and must be consumed or owned before the stream advances.
330#[implement(Service)]
331#[tracing::instrument(skip(self), level = "debug")]
332pub fn room_members<'a>(
333	&'a self,
334	room_id: &'a RoomId,
335) -> impl Stream<Item = &UserId> + Send + 'a {
336	self.room_members_checked(room_id).ignore_err()
337}
338
339/// Streams joined users within the room's exact encoded prefix.
340///
341/// Storage and user-key decoding failures are surfaced as error items rather
342/// than dropped. User IDs borrow the cursor and must be consumed or owned
343/// before advancing it.
344#[implement(Service)]
345#[tracing::instrument(skip(self), level = "debug")]
346pub fn room_members_checked<'a>(
347	&'a self,
348	room_id: &'a RoomId,
349) -> impl Stream<Item = Result<&'a UserId>> + Send + 'a {
350	let prefix = (room_id, Interfix);
351
352	self.db
353		.roomuserid_joinedcount
354		.keys_prefix(&prefix)
355		.map_ok(|(_, user_id): (Ignore, &UserId)| user_id)
356}
357
358/// Returns the stored number of users currently joined to a room.
359///
360/// The aggregate is rebuilt from membership indexes by
361/// [`Self::update_joined_count`]. Missing or malformed count rows return an
362/// error.
363#[implement(Service)]
364#[tracing::instrument(skip(self), level = "trace")]
365pub async fn room_joined_count(&self, room_id: &RoomId) -> Result<u64> {
366	self.db
367		.roomid_joinedcount
368		.get(room_id)
369		.await
370		.deserialized()
371}
372
373/// Returns the stored number of users currently invited to a room.
374///
375/// The aggregate is rebuilt from membership indexes by
376/// [`Self::update_joined_count`]. Missing or malformed count rows return an
377/// error.
378#[implement(Service)]
379#[tracing::instrument(skip(self), level = "trace")]
380pub async fn room_invited_count(&self, room_id: &RoomId) -> Result<u64> {
381	self.db
382		.roomid_invitedcount
383		.get(room_id)
384		.await
385		.deserialized()
386}
387
388/// Returns the stored number of users currently knocking on a room.
389///
390/// The aggregate is rebuilt from membership indexes by
391/// [`Self::update_joined_count`]. Missing or malformed count rows return an
392/// error.
393#[implement(Service)]
394#[tracing::instrument(skip(self), level = "trace")]
395pub async fn room_knocked_count(&self, room_id: &RoomId) -> Result<u64> {
396	self.db
397		.roomid_knockedcount
398		.get(room_id)
399		.await
400		.deserialized()
401}
402
403/// Streams active local users currently joined to a room.
404///
405/// Local joined users are filtered through the user service, excluding guests
406/// and deactivated accounts. The stream otherwise inherits
407/// [`Self::room_members`]'s cursor lifetime and error policy.
408#[implement(Service)]
409#[tracing::instrument(skip(self), level = "debug")]
410pub fn active_local_users_in_room<'a>(
411	&'a self,
412	room_id: &'a RoomId,
413) -> impl Stream<Item = &UserId> + Send + 'a {
414	self.local_users_in_room(room_id)
415		.filter(|user| self.services.users.is_active(user))
416}
417
418/// Streams all local users currently joined to a room.
419///
420/// Guest and deactivated accounts remain included. The stream otherwise
421/// inherits [`Self::room_members`]'s cursor lifetime and error policy.
422#[implement(Service)]
423#[tracing::instrument(skip(self), level = "debug")]
424pub fn local_users_in_room<'a>(
425	&'a self,
426	room_id: &'a RoomId,
427) -> impl Stream<Item = &UserId> + Send + 'a {
428	self.room_members(room_id)
429		.ready_filter(|user| self.services.globals.user_is_local(user))
430}
431
432/// Streams local users currently invited to a room.
433///
434/// Remote invitees are filtered out by server name. The stream otherwise
435/// inherits [`Self::room_members_invited`]'s cursor lifetime and error policy.
436#[implement(Service)]
437#[tracing::instrument(skip(self), level = "debug")]
438pub fn local_users_invited_to_room<'a>(
439	&'a self,
440	room_id: &'a RoomId,
441) -> impl Stream<Item = &UserId> + Send + 'a {
442	self.room_members_invited(room_id)
443		.ready_filter(|user| self.services.globals.user_is_local(user))
444}
445
446/// Streams user identifiers from the once-joined index under a room prefix.
447///
448/// Once-joined rows are currently written with user-first keys, while this
449/// accessor probes a room-first prefix, so the stored layout can yield no
450/// matches. Yielded user identifiers borrow the database cursor and are invalid
451/// after the next poll; consume or own each item before advancing. Storage and
452/// decoding failures are skipped.
453#[implement(Service)]
454#[tracing::instrument(skip(self), level = "debug")]
455pub fn room_useroncejoined<'a>(
456	&'a self,
457	room_id: &'a RoomId,
458) -> impl Stream<Item = &UserId> + Send + 'a {
459	let prefix = (room_id, Interfix);
460	self.db
461		.roomuseroncejoinedids
462		.keys_prefix(&prefix)
463		.ignore_err()
464		.map(|(_, user_id): (Ignore, &UserId)| user_id)
465}
466
467/// Streams all users currently indexed as invited to a room.
468///
469/// Storage and key-decoding failures are skipped. Each user identifier borrows
470/// the cursor and must be consumed or owned before the stream advances.
471#[implement(Service)]
472#[tracing::instrument(skip(self), level = "debug")]
473pub fn room_members_invited<'a>(
474	&'a self,
475	room_id: &'a RoomId,
476) -> impl Stream<Item = &UserId> + Send + 'a {
477	let prefix = (room_id, Interfix);
478	self.db
479		.roomuserid_invitecount
480		.keys_prefix(&prefix)
481		.ignore_err()
482		.map(|(_, user_id): (Ignore, &UserId)| user_id)
483}
484
485/// Streams all users currently indexed as knocking on a room.
486///
487/// Storage and key-decoding failures are skipped. Each user identifier borrows
488/// the cursor and must be consumed or owned before the stream advances.
489#[implement(Service)]
490#[tracing::instrument(skip(self), level = "debug")]
491pub fn room_members_knocked<'a>(
492	&'a self,
493	room_id: &'a RoomId,
494) -> impl Stream<Item = &UserId> + Send + 'a {
495	let prefix = (room_id, Interfix);
496	self.db
497		.roomuserid_knockedcount
498		.keys_prefix(&prefix)
499		.ignore_err()
500		.map(|(_, user_id): (Ignore, &UserId)| user_id)
501}
502
503/// Returns the stream position associated with a user's current invite.
504///
505/// This value identifies the membership transition rather than counting
506/// invitations. Missing or malformed index rows return an error.
507#[implement(Service)]
508#[tracing::instrument(skip(self), level = "trace")]
509pub async fn get_invite_count(&self, room_id: &RoomId, user_id: &UserId) -> Result<u64> {
510	let key = (room_id, user_id);
511	self.db
512		.roomuserid_invitecount
513		.qry(&key)
514		.await
515		.deserialized()
516}
517
518/// Returns the stream position associated with a user's current knock.
519///
520/// This value identifies the membership transition rather than counting
521/// knocks. Missing or malformed index rows return an error.
522#[implement(Service)]
523#[tracing::instrument(skip(self), level = "trace")]
524pub async fn get_knock_count(&self, room_id: &RoomId, user_id: &UserId) -> Result<u64> {
525	let key = (room_id, user_id);
526	self.db
527		.roomuserid_knockedcount
528		.qry(&key)
529		.await
530		.deserialized()
531}
532
533/// Returns the stream position associated with a user's current leave row.
534///
535/// This value identifies the membership transition rather than counting
536/// leaves. Missing or malformed index rows return an error.
537#[implement(Service)]
538#[tracing::instrument(skip(self), level = "trace")]
539pub async fn get_left_count(&self, room_id: &RoomId, user_id: &UserId) -> Result<u64> {
540	let key = (room_id, user_id);
541	self.db
542		.roomuserid_leftcount
543		.qry(&key)
544		.await
545		.deserialized()
546}
547
548/// Returns the stream position associated with a user's current join.
549///
550/// This value identifies the membership transition rather than counting
551/// joins. A join recorded before positions were stored holds an empty value
552/// and reads as zero. Missing or malformed index rows return an error.
553#[implement(Service)]
554#[tracing::instrument(skip(self), level = "trace")]
555pub async fn get_joined_count(&self, room_id: &RoomId, user_id: &UserId) -> Result<u64> {
556	let key = (room_id, user_id);
557
558	self.db
559		.roomuserid_joinedcount
560		.qry(&key)
561		.await
562		.and_then(|value| {
563			value
564				.is_empty()
565				.map_or_else(|| value.deserialized(), || Ok(0))
566		})
567}
568
569/// Streams every cached membership category for a user.
570///
571/// Join, leave, invite, and knock indexes are combined without a global
572/// ordering guarantee. Yielded room identifiers borrow their source database
573/// cursor and are invalid after the next poll; consume or own each item before
574/// advancing. Source-stream errors are skipped.
575#[implement(Service)]
576#[inline]
577pub fn all_user_memberships<'a>(
578	&'a self,
579	user_id: &'a UserId,
580) -> impl Stream<Item = (MembershipState, &RoomId)> + Send + 'a {
581	self.user_memberships(user_id, None)
582}
583
584/// Streams selected cached membership categories for a user.
585///
586/// A missing mask selects join, leave, invite, and knock; an empty mask selects
587/// none. Category streams are interleaved without a global ordering guarantee,
588/// and their storage errors are skipped. Yielded room identifiers borrow their
589/// source database cursor and are invalid after the next poll; consume or own
590/// each item before advancing.
591#[implement(Service)]
592#[tracing::instrument(skip(self), level = "debug")]
593pub fn user_memberships<'a>(
594	&'a self,
595	user_id: &'a UserId,
596	mask: Option<&[MembershipState]>,
597) -> impl Stream<Item = (MembershipState, &RoomId)> + Send + 'a {
598	let joined = mask
599		.is_none_or(|mask| mask.contains(&MembershipState::Join))
600		.then(|| {
601			self.rooms_joined(user_id)
602				.map(|room_id| (MembershipState::Join, room_id))
603				.left_stream()
604		})
605		.unwrap_or_else(|| empty().right_stream());
606
607	let invited = mask
608		.is_none_or(|mask| mask.contains(&MembershipState::Invite))
609		.then(|| {
610			self.rooms_invited(user_id)
611				.map(|room_id| (MembershipState::Invite, room_id))
612				.left_stream()
613		})
614		.unwrap_or_else(|| empty().right_stream());
615
616	let knocked = mask
617		.is_none_or(|mask| mask.contains(&MembershipState::Knock))
618		.then(|| {
619			self.rooms_knocked(user_id)
620				.map(|room_id| (MembershipState::Knock, room_id))
621				.left_stream()
622		})
623		.unwrap_or_else(|| empty().right_stream());
624
625	let left = mask
626		.is_none_or(|mask| mask.contains(&MembershipState::Leave))
627		.then(|| {
628			self.rooms_left(user_id)
629				.map(|room_id| (MembershipState::Leave, room_id))
630				.left_stream()
631		})
632		.unwrap_or_else(|| empty().right_stream());
633
634	select(select(joined, left), select(invited, knocked))
635}
636
637/// Streams rooms in which a user is currently indexed as joined.
638///
639/// The raw user prefix preserves the established key scan. Storage and
640/// decoding failures are skipped, and each room identifier borrows the cursor
641/// until its next poll.
642#[implement(Service)]
643#[tracing::instrument(skip(self), level = "debug")]
644pub fn rooms_joined<'a>(
645	&'a self,
646	user_id: &'a UserId,
647) -> impl Stream<Item = &RoomId> + Send + 'a {
648	self.db
649		.userroomid_joinedcount
650		.keys_raw_prefix(user_id)
651		.ignore_err()
652		.map(|(_, room_id): (Ignore, &RoomId)| room_id)
653}
654
655/// Streams joined rooms within the user's exact encoded prefix.
656///
657/// Storage and room-key decoding failures are surfaced as error items rather
658/// than dropped. Room IDs borrow the cursor and must be consumed or owned
659/// before advancing it.
660#[implement(Service)]
661#[tracing::instrument(skip(self), level = "trace")]
662pub fn rooms_joined_checked<'a>(
663	&'a self,
664	user_id: &'a UserId,
665) -> impl Stream<Item = Result<&'a RoomId>> + Send + 'a {
666	let prefix = (user_id, Interfix);
667
668	self.db
669		.userroomid_joinedcount
670		.keys_prefix(&prefix)
671		.map_ok(|(_, room_id): (Ignore, &RoomId)| room_id)
672}
673
674/// Streams rooms in which a user is currently indexed as invited.
675///
676/// The raw user prefix preserves the established key scan. Storage and
677/// decoding failures are skipped, and each room identifier borrows the cursor
678/// until its next poll.
679#[implement(Service)]
680#[tracing::instrument(skip(self), level = "debug")]
681pub fn rooms_invited<'a>(
682	&'a self,
683	user_id: &'a UserId,
684) -> impl Stream<Item = &RoomId> + Send + 'a {
685	self.db
686		.userroomid_invitestate
687		.keys_raw_prefix(user_id)
688		.ignore_err()
689		.map(|(_, room_id): (Ignore, &RoomId)| room_id)
690}
691
692/// Streams rooms in which a user is currently indexed as knocking.
693///
694/// The raw user prefix preserves the established key scan. Storage and
695/// decoding failures are skipped, and each room identifier borrows the cursor
696/// until its next poll.
697#[implement(Service)]
698#[tracing::instrument(skip(self), level = "debug")]
699pub fn rooms_knocked<'a>(
700	&'a self,
701	user_id: &'a UserId,
702) -> impl Stream<Item = &RoomId> + Send + 'a {
703	self.db
704		.userroomid_knockedstate
705		.keys_raw_prefix(user_id)
706		.ignore_err()
707		.map(|(_, room_id): (Ignore, &RoomId)| room_id)
708}
709
710/// Streams rooms for which a user's leave state is retained.
711///
712/// Forgotten rooms have no leave row and therefore do not appear. Storage and
713/// decoding failures are skipped, and each room identifier borrows the cursor
714/// until its next poll.
715#[implement(Service)]
716#[tracing::instrument(skip(self), level = "debug")]
717pub fn rooms_left<'a>(&'a self, user_id: &'a UserId) -> impl Stream<Item = &RoomId> + Send + 'a {
718	self.db
719		.userroomid_leftstate
720		.keys_raw_prefix(user_id)
721		.ignore_err()
722		.map(|(_, room_id): (Ignore, &RoomId)| room_id)
723}
724
725/// Streams stored stripped state for a user's current invitations.
726///
727/// Each item owns its room identifier and decoded state vector. Storage,
728/// key-decoding, and state-deserialization failures are skipped.
729#[implement(Service)]
730#[tracing::instrument(skip(self), level = "debug")]
731pub fn rooms_invited_state<'a>(
732	&'a self,
733	user_id: &'a UserId,
734) -> impl Stream<Item = StrippedStateEventItem> + Send + 'a {
735	type KeyVal<'a> = (Key<'a>, Raw<Vec<AnyStrippedStateEvent>>);
736	type Key<'a> = (&'a UserId, &'a RoomId);
737
738	let prefix = (user_id, Interfix);
739	self.db
740		.userroomid_invitestate
741		.stream_prefix(&prefix)
742		.ignore_err()
743		.map(|((_, room_id), state): KeyVal<'_>| (room_id.to_owned(), state))
744		.map(|(room_id, state)| Ok((room_id, state.deserialize_as_unchecked()?)))
745		.ignore_err()
746}
747
748/// Streams stored stripped state for a user's current knocks.
749///
750/// Each item owns its room identifier and decoded state vector. Storage,
751/// key-decoding, and state-deserialization failures are skipped.
752#[implement(Service)]
753#[tracing::instrument(skip(self), level = "trace")]
754pub fn rooms_knocked_state<'a>(
755	&'a self,
756	user_id: &'a UserId,
757) -> impl Stream<Item = StrippedStateEventItem> + Send + 'a {
758	type KeyVal<'a> = (Key<'a>, Raw<Vec<AnyStrippedStateEvent>>);
759	type Key<'a> = (&'a UserId, &'a RoomId);
760
761	let prefix = (user_id, Interfix);
762	self.db
763		.userroomid_knockedstate
764		.stream_prefix(&prefix)
765		.ignore_err()
766		.map(|((_, room_id), state): KeyVal<'_>| (room_id.to_owned(), state))
767		.map(|(room_id, state)| Ok((room_id, state.deserialize_as_unchecked()?)))
768		.ignore_err()
769}
770
771/// Streams stored state for rooms a user has left but not forgotten.
772///
773/// Both native state arrays and compatible single-event rows are accepted.
774/// Storage and key-decoding failures are skipped, while unusable state values
775/// yield an empty event vector for their room.
776#[implement(Service)]
777#[tracing::instrument(skip(self), level = "debug")]
778pub fn rooms_left_state<'a>(
779	&'a self,
780	user_id: &'a UserId,
781) -> impl Stream<Item = SyncStateEventItem> + Send + 'a {
782	type KeyVal<'a> = (Key<'a>, Raw<Vec<Raw<AnySyncStateEvent>>>);
783	type Key<'a> = (&'a UserId, &'a RoomId);
784
785	let prefix = (user_id, Interfix);
786	self.db
787		.userroomid_leftstate
788		.stream_prefix(&prefix)
789		.ignore_err()
790		.map(|((_, room_id), state): KeyVal<'_>| (room_id.to_owned(), state))
791		.map(|(room_id, state)| {
792			let state = state_events(&room_id, &state);
793
794			(room_id, state)
795		})
796}
797
798/// Returns the stripped state stored for a user's current invitation.
799///
800/// The value is decoded as an array of stripped state events. Missing rows,
801/// storage failures, and malformed state return an error.
802#[implement(Service)]
803#[tracing::instrument(skip(self), level = "trace")]
804pub async fn invite_state(
805	&self,
806	user_id: &UserId,
807	room_id: &RoomId,
808) -> Result<Vec<Raw<AnyStrippedStateEvent>>> {
809	let key = (user_id, room_id);
810	self.db
811		.userroomid_invitestate
812		.qry(&key)
813		.await
814		.deserialized()
815		.and_then(|val: Raw<Vec<AnyStrippedStateEvent>>| {
816			val.deserialize_as_unchecked().map_err(Into::into)
817		})
818}
819
820/// Whether the user's stored invite carries any stripped state.
821///
822/// The row is probed raw rather than decoded through [`Self::invite_state`], so
823/// no stripped state is materialized to answer it. A failed read is an error
824/// rather than an absent row.
825#[implement(Service)]
826#[tracing::instrument(skip(self), level = "trace")]
827pub async fn has_invite_state(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
828	let key = (user_id, room_id);
829
830	self.db
831		.userroomid_invitestate
832		.qry(&key)
833		.await
834		.optional()
835		.map(|state| state.is_some_and(|state| state.len() > EMPTY_INVITE_STATE.len()))
836}
837
838/// Returns the stripped state stored for a user's current knock.
839///
840/// The value is decoded as an array of stripped state events. Missing rows,
841/// storage failures, and malformed state return an error.
842#[implement(Service)]
843#[tracing::instrument(skip(self), level = "trace")]
844pub async fn knock_state(
845	&self,
846	user_id: &UserId,
847	room_id: &RoomId,
848) -> Result<Vec<Raw<AnyStrippedStateEvent>>> {
849	let key = (user_id, room_id);
850	self.db
851		.userroomid_knockedstate
852		.qry(&key)
853		.await
854		.deserialized()
855		.and_then(|val: Raw<Vec<AnyStrippedStateEvent>>| {
856			val.deserialize_as_unchecked().map_err(Into::into)
857		})
858}
859
860/// Returns the cached state for a room a user has left.
861///
862/// Native state arrays and compatible single-event rows are normalized into
863/// one vector. Missing or unreadable rows return an error, while an unusable
864/// stored JSON shape is logged and becomes an empty vector.
865#[implement(Service)]
866#[tracing::instrument(skip(self), level = "trace")]
867pub async fn left_state(
868	&self,
869	user_id: &UserId,
870	room_id: &RoomId,
871) -> Result<Vec<Raw<AnyStrippedStateEvent>>> {
872	let key = (user_id, room_id);
873	self.db
874		.userroomid_leftstate
875		.qry(&key)
876		.await
877		.deserialized()
878		.map(|state: Raw<Vec<AnyStrippedStateEvent>>| state_events(room_id, &state))
879}
880
881/// Infers a user's cached membership state for one room.
882///
883/// Current indexes take precedence in join, leave, knock, then invite order.
884/// When none exists, a once-joined marker is reported as `Ban`; no marker
885/// returns `None`. Read failures from the boolean probes are treated as
886/// absence.
887#[implement(Service)]
888#[tracing::instrument(skip(self), level = "trace")]
889pub async fn user_membership(
890	&self,
891	user_id: &UserId,
892	room_id: &RoomId,
893) -> Option<MembershipState> {
894	let states = join5(
895		self.is_joined(user_id, room_id),
896		self.is_left(user_id, room_id),
897		self.is_knocked(user_id, room_id),
898		self.is_invited(user_id, room_id),
899		self.once_joined(user_id, room_id),
900	)
901	.await;
902
903	match states {
904		| (true, ..) => Some(MembershipState::Join),
905		| (_, true, ..) => Some(MembershipState::Leave),
906		| (_, _, true, ..) => Some(MembershipState::Knock),
907		| (_, _, _, true, ..) => Some(MembershipState::Invite),
908		| (false, false, false, false, true) => Some(MembershipState::Ban),
909		| _ => None,
910	}
911}
912
913/// Tests whether a user has ever been marked as joined to a room.
914///
915/// The durable marker survives later membership transitions and explicit
916/// forget operations. Missing rows and storage failures both return `false`.
917#[implement(Service)]
918#[tracing::instrument(skip(self), level = "debug")]
919pub async fn once_joined(&self, user_id: &UserId, room_id: &RoomId) -> bool {
920	let key = (user_id, room_id);
921	self.db.roomuseroncejoinedids.contains(&key).await
922}
923
924/// Tests whether a user is currently joined to any of the given rooms.
925///
926/// The rooms are probed concurrently and the first joined room decides the
927/// answer; an empty set of rooms answers `false`. A probe that fails counts
928/// as not joined, as it does for [`Self::is_joined`].
929#[implement(Service)]
930#[tracing::instrument(skip(self, room_ids), level = "trace")]
931pub async fn is_joined_any<'a, Rooms>(&self, user_id: &UserId, room_ids: Rooms) -> bool
932where
933	Rooms: IntoIterator<Item = &'a RoomId> + Send,
934	Rooms::IntoIter: Send,
935{
936	room_ids
937		.into_iter()
938		.stream()
939		.broad_any(|room_id| self.is_joined(user_id, room_id))
940		.await
941}
942
943/// Tests whether a user is currently indexed as joined to a room.
944///
945/// The user-to-room join index supplies the answer. Missing rows and storage
946/// failures both return `false`.
947#[implement(Service)]
948#[tracing::instrument(skip(self), level = "trace")]
949pub async fn is_joined<'a>(&'a self, user_id: &'a UserId, room_id: &'a RoomId) -> bool {
950	let key = (user_id, room_id);
951	self.db
952		.userroomid_joinedcount
953		.contains(&key)
954		.await
955}
956
957/// Tests whether a user is currently indexed as knocking on a room.
958///
959/// The stored knock-state row supplies the answer. Missing rows and storage
960/// failures both return `false`.
961#[implement(Service)]
962#[tracing::instrument(skip(self), level = "trace")]
963pub async fn is_knocked<'a>(&'a self, user_id: &'a UserId, room_id: &'a RoomId) -> bool {
964	let key = (user_id, room_id);
965	self.db
966		.userroomid_knockedstate
967		.contains(&key)
968		.await
969}
970
971/// Tests whether a user is currently indexed as invited to a room.
972///
973/// The stored invite-state row supplies the answer. Missing rows and storage
974/// failures both return `false`.
975#[implement(Service)]
976#[tracing::instrument(skip(self), level = "trace")]
977pub async fn is_invited(&self, user_id: &UserId, room_id: &RoomId) -> bool {
978	let key = (user_id, room_id);
979	self.db
980		.userroomid_invitestate
981		.contains(&key)
982		.await
983}
984
985/// Tests whether a user's leave state is currently retained for a room.
986///
987/// Explicitly forgotten rooms have no row and return `false`. Missing rows and
988/// storage failures are otherwise indistinguishable.
989#[implement(Service)]
990#[tracing::instrument(skip(self), level = "trace")]
991pub async fn is_left(&self, user_id: &UserId, room_id: &RoomId) -> bool {
992	let key = (user_id, room_id);
993	self.db.userroomid_leftstate.contains(&key).await
994}
995
996/// Deletes a room's aggregate and paired membership indexes.
997///
998/// Aggregate rows and every paired index row found during enumeration are
999/// deleted in one database transaction. Local users' leave rows survive unless
1000/// `force` is true, while remote leave rows are always removed. Once-joined
1001/// markers and appservice membership-cache entries are untouched. Storage
1002/// errors encountered during enumeration are skipped and can leave index rows
1003/// behind.
1004#[implement(Service)]
1005#[tracing::instrument(skip(self), level = "trace")]
1006pub async fn delete_room_join_counts(&self, room_id: &RoomId, force: bool) -> Result {
1007	let prefix = (room_id, Interfix);
1008	let mut txn = self.services.db.txn();
1009
1010	txn.del_raw(&self.db.roomid_knockedcount, room_id);
1011
1012	txn.del_raw(&self.db.roomid_invitedcount, room_id);
1013
1014	txn.del_raw(&self.db.roomid_inviteviaservers, room_id);
1015
1016	txn.del_raw(&self.db.roomid_joinedcount, room_id);
1017
1018	self.db
1019		.roomserverids
1020		.keys_prefix(&prefix)
1021		.ignore_err()
1022		.ready_for_each(|key: (&RoomId, &ServerName)| {
1023			trace!("Removing key: {key:?}");
1024			txn.del(&self.db.roomserverids, key);
1025
1026			let reverse_key = (key.1, key.0);
1027
1028			trace!("Removing reverse key: {reverse_key:?}");
1029			txn.del(&self.db.serverroomids, reverse_key);
1030		})
1031		.await;
1032
1033	self.db
1034		.roomuserid_invitecount
1035		.keys_prefix(&prefix)
1036		.ignore_err()
1037		.ready_for_each(|key: (&RoomId, &UserId)| {
1038			trace!("Removing key: {key:?}");
1039			txn.del(&self.db.roomuserid_invitecount, key);
1040
1041			let reverse_key = (key.1, key.0);
1042
1043			trace!("Removing reverse key: {reverse_key:?}");
1044			txn.del(&self.db.userroomid_invitestate, reverse_key);
1045		})
1046		.await;
1047
1048	self.db
1049		.roomuserid_joinedcount
1050		.keys_prefix(&prefix)
1051		.ignore_err()
1052		.ready_for_each(|key: (&RoomId, &UserId)| {
1053			trace!("Removing key: {key:?}");
1054			txn.del(&self.db.roomuserid_joinedcount, key);
1055
1056			let reverse_key = (key.1, key.0);
1057
1058			trace!("Removing reverse key: {reverse_key:?}");
1059			txn.del(&self.db.userroomid_joinedcount, reverse_key);
1060		})
1061		.await;
1062
1063	self.db
1064		.roomuserid_knockedcount
1065		.keys_prefix(&prefix)
1066		.ignore_err()
1067		.ready_for_each(|key: (&RoomId, &UserId)| {
1068			trace!("Removing key: {key:?}");
1069			txn.del(&self.db.roomuserid_knockedcount, key);
1070
1071			let reverse_key = (key.1, key.0);
1072
1073			trace!("Removing reverse key: {reverse_key:?}");
1074			txn.del(&self.db.userroomid_knockedstate, reverse_key);
1075		})
1076		.await;
1077
1078	self.db
1079		.roomuserid_leftcount
1080		.keys_prefix(&prefix)
1081		.ignore_err()
1082		.ready_filter(|(_, user_id): &(&RoomId, &UserId)| {
1083			force || !self.services.globals.user_is_local(user_id)
1084		})
1085		.ready_for_each(|key: (&RoomId, &UserId)| {
1086			trace!("Removing key: {key:?}");
1087			txn.del(&self.db.roomuserid_leftcount, key);
1088
1089			let reverse_key = (key.1, key.0);
1090
1091			trace!("Removing reverse key: {reverse_key:?}");
1092			txn.del(&self.db.userroomid_leftstate, reverse_key);
1093		})
1094		.await;
1095
1096	txn.execute();
1097
1098	Ok(())
1099}
1100
1101/// Normalizes either supported cached leave-state representation.
1102///
1103/// Imported databases can contain one leave event where this service writes an
1104/// array of state events. Both shapes are read without rewriting the row, and
1105/// malformed values are logged before producing an empty vector.
1106fn state_events<T, U>(room_id: &RoomId, state: &Raw<T>) -> Vec<U>
1107where
1108	U: DeserializeOwned + From<Owned<Pdu>>,
1109{
1110	match state.json().get().trim_start().as_bytes().first() {
1111		| Some(b'[') => state
1112			.deserialize_as_unchecked()
1113			.inspect_err(
1114				|e| debug_warn!(%room_id, error = %e, "Unusable cached membership state"),
1115			)
1116			.unwrap_or_default(),
1117
1118		// A foreign row holds the leave event alone; lift it into the array shape.
1119		| Some(b'{') => state
1120			.deserialize_as_unchecked()
1121			.map(|event: Pdu| [event.into_format()].into())
1122			.inspect_err(|e| debug_warn!(%room_id, error = %e, "Unusable cached leave event"))
1123			.unwrap_or_default(),
1124
1125		| _ => Vec::new(),
1126	}
1127}