Skip to main content

tuwunel_service/rooms/spaces/
local.rs

1use futures::{FutureExt, StreamExt, TryFutureExt};
2use ruma::{
3	RoomId, api::federation::space::SpaceHierarchyParentSummary as ParentSummary,
4	events::space::child::HierarchySpaceChildEvent, room::RoomSummary, serde::Raw,
5};
6use tuwunel_core::{
7	Err, Error, Event, Result, debug, error, implement,
8	utils::{future::TryExtExt, timepoint_has_passed},
9};
10
11use super::{Accessibility, Cached, Identifier};
12
13/// Gets the summary of a space using solely local information.
14#[implement(super::Service)]
15#[tracing::instrument(name = "local", level = "debug", skip_all)]
16pub(super) async fn get_summary_and_children_local(
17	&self,
18	current_room: &RoomId,
19	sender: &Identifier<'_>,
20) -> Result<Accessibility> {
21	use Accessibility::{Accessible, Inaccessible};
22
23	match self.cache_get(current_room).await {
24		| Err(e) if !e.is_not_found() => {
25			error!(?current_room, "cache error: {e}");
26			return Err(e);
27		},
28		| Ok(Cached { expires, summary: Some(cached) }) if !timepoint_has_passed(expires) => {
29			debug!(?current_room, ?expires, "cache hit");
30			return self
31				.is_accessible_child(current_room, &cached.summary.join_rule, sender)
32				.await
33				.then(|| Ok(Accessible(cached)))
34				.unwrap_or(Ok(Inaccessible));
35		},
36		| Ok(Cached { expires, summary: None }) if !timepoint_has_passed(expires) => {
37			// Cache negative: try local computation below.
38			debug!(?current_room, ?expires, "negative cache hit");
39		},
40		| _ => {
41			// Cache miss, expired, or negative: try local computation below.
42			debug!(?current_room, "no usable cache entry");
43		},
44	}
45
46	if !self
47		.services
48		.state_cache
49		.server_in_room(self.services.server.name.as_ref(), current_room)
50		.await
51	{
52		debug!(?current_room, "no local membership; defer to federation");
53		return Err!(Request(NotFound("Space room not found locally.")));
54	}
55
56	let children_state: Vec<_> = self
57		.get_space_child_events(current_room)
58		.map(Event::into_format)
59		.collect()
60		.await;
61
62	let summary = self
63		.get_room_summary(current_room, children_state, sender)
64		.boxed()
65		.await;
66
67	match summary {
68		| Ok(Inaccessible) => self.cache_put(current_room, None),
69		| Ok(Accessible(ref summary)) => self.cache_put(current_room, Some(summary)),
70		| _ => (),
71	}
72
73	summary
74}
75
76#[implement(super::Service)]
77pub(super) async fn get_room_summary(
78	&self,
79	room_id: &RoomId,
80	children_state: Vec<Raw<HierarchySpaceChildEvent>>,
81	sender: &Identifier<'_>,
82) -> Result<Accessibility, Error> {
83	let join_rule = self
84		.services
85		.state_accessor
86		.get_join_rules(room_id)
87		.await;
88
89	let is_accessible_child = self
90		.is_accessible_child(room_id, &join_rule.clone().into(), sender)
91		.await;
92
93	if !is_accessible_child {
94		return Ok(Accessibility::Inaccessible);
95	}
96
97	let name = self
98		.services
99		.state_accessor
100		.get_name(room_id)
101		.ok();
102
103	let topic = self
104		.services
105		.state_accessor
106		.get_room_topic(room_id)
107		.ok();
108
109	let room_type = self
110		.services
111		.state_accessor
112		.get_room_type(room_id)
113		.ok();
114
115	let world_readable = self
116		.services
117		.state_accessor
118		.is_world_readable(room_id);
119
120	let guest_can_join = self
121		.services
122		.state_accessor
123		.guest_can_join(room_id);
124
125	let num_joined_members = self
126		.services
127		.state_cache
128		.room_joined_count(room_id)
129		.unwrap_or(0);
130
131	let canonical_alias = self
132		.services
133		.state_accessor
134		.get_canonical_alias(room_id)
135		.ok();
136
137	let avatar_url = self
138		.services
139		.state_accessor
140		.get_avatar(room_id)
141		.map_ok(|content| content.url)
142		.ok();
143
144	let room_version = self.services.state.get_room_version(room_id).ok();
145
146	let encryption = self
147		.services
148		.state_accessor
149		.get_room_encryption(room_id)
150		.ok();
151
152	let (
153		canonical_alias,
154		name,
155		num_joined_members,
156		topic,
157		world_readable,
158		guest_can_join,
159		avatar_url,
160		room_type,
161		room_version,
162		encryption,
163	) = futures::join!(
164		canonical_alias,
165		name,
166		num_joined_members,
167		topic,
168		world_readable,
169		guest_can_join,
170		avatar_url,
171		room_type,
172		room_version,
173		encryption,
174	);
175
176	let summary = ParentSummary {
177		children_state,
178		summary: RoomSummary {
179			avatar_url: avatar_url.flatten(),
180			canonical_alias,
181			name,
182			topic,
183			world_readable,
184			guest_can_join,
185			room_type,
186			encryption,
187			room_version,
188			room_id: room_id.to_owned(),
189			num_joined_members: num_joined_members.try_into().unwrap_or_default(),
190			join_rule: join_rule.clone().into(),
191		},
192	};
193
194	Ok(Accessibility::Accessible(summary))
195}