Skip to main content

tuwunel_service/rooms/state_accessor/
mod.rs

1//! Reads room-state snapshots and evaluates state-based access policy.
2//!
3//! The service resolves current and historical state into typed events. It also
4//! centralizes visibility, redaction, erasure, invite, and tombstone decisions.
5
6mod erased;
7mod room_state;
8mod server_can;
9mod state;
10mod user_can;
11
12use std::sync::Arc;
13
14use async_trait::async_trait;
15use futures::{FutureExt, TryFutureExt, future::try_join};
16use ruma::{
17	EventEncryptionAlgorithm, OwnedRoomAliasId, RoomId, UserId,
18	events::{
19		StateEventType,
20		room::{
21			avatar::RoomAvatarEventContent,
22			canonical_alias::RoomCanonicalAliasEventContent,
23			create::RoomCreateEventContent,
24			encryption::RoomEncryptionEventContent,
25			guest_access::{GuestAccess, RoomGuestAccessEventContent},
26			history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
27			join_rules::{JoinRule, RoomJoinRulesEventContent},
28			member::RoomMemberEventContent,
29			name::RoomNameEventContent,
30			power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
31			topic::RoomTopicEventContent,
32		},
33	},
34	room::RoomType,
35};
36use tuwunel_core::{
37	Result, err, implement,
38	matrix::{Pdu, room_version},
39	utils::BoolExt,
40};
41
42use crate::rooms::state_res::events::RoomCreateEvent;
43
44/// Resolves room state and answers state-based authorization questions.
45///
46/// Accessors share the state, timeline, short-ID, and membership services so
47/// callers use one interpretation of current and historical room state.
48pub struct Service {
49	services: Arc<crate::services::OnceServices>,
50}
51
52#[async_trait]
53impl crate::Service for Service {
54	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
55		Ok(Arc::new(Self { services: args.services.clone() }))
56	}
57
58	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
59}
60
61impl Service {
62	/// Returns the effective power levels for a room.
63	///
64	/// A missing or invalid `m.room.power_levels` event falls back to the room
65	/// version's defaults. The create event and its room-version rules are required.
66	pub async fn get_power_levels(&self, room_id: &RoomId) -> Result<RoomPowerLevels> {
67		let create = self.get_create(room_id);
68		let power_levels = self
69			.room_state_get_content(room_id, &StateEventType::RoomPowerLevels, "")
70			.map_ok(|c: RoomPowerLevelsEventContent| c)
71			.map(Result::ok)
72			.map(Ok);
73
74		let (create, power_levels) = try_join(create, power_levels).await?;
75
76		let room_version = create.room_version()?;
77		let rules = room_version::rules(&room_version)?;
78		let creators = create.creators(&rules.authorization)?;
79
80		Ok(RoomPowerLevels::new(power_levels.into(), &rules.authorization, creators))
81	}
82
83	/// Returns the room's current create event wrapper.
84	///
85	/// The lookup uses the empty state key and returns an error when the event or
86	/// its state snapshot cannot be resolved.
87	pub async fn get_create(&self, room_id: &RoomId) -> Result<RoomCreateEvent<Pdu>> {
88		self.room_state_get(room_id, &StateEventType::RoomCreate, "")
89			.await
90			.map(RoomCreateEvent::new)
91	}
92
93	/// Returns the room's current non-empty name.
94	///
95	/// Missing, invalid, and empty `m.room.name` content is reported as an error.
96	pub async fn get_name(&self, room_id: &RoomId) -> Result<String> {
97		self.room_state_get_content(room_id, &StateEventType::RoomName, "")
98			.await
99			.and_then(|c: RoomNameEventContent| {
100				c.name
101					.is_empty()
102					.is_false()
103					.then_some(c.name)
104					.ok_or_else(|| err!(Request(NotFound("Empty name found in event content."))))
105			})
106	}
107
108	/// Returns the room's current avatar content.
109	///
110	/// Missing or invalid `m.room.avatar` state is returned as an error.
111	pub async fn get_avatar(&self, room_id: &RoomId) -> Result<RoomAvatarEventContent> {
112		self.room_state_get_content(room_id, &StateEventType::RoomAvatar, "")
113			.await
114	}
115
116	/// Returns a user's current membership event content in a room.
117	///
118	/// The user ID is used as the membership state key. Missing or invalid state
119	/// is returned as an error.
120	pub async fn get_member(
121		&self,
122		room_id: &RoomId,
123		user_id: &UserId,
124	) -> Result<RoomMemberEventContent> {
125		self.room_state_get_content(room_id, &StateEventType::RoomMember, user_id.as_str())
126			.await
127	}
128
129	/// Reports whether the room is world-readable.
130	///
131	/// Missing, unreadable, or invalid history-visibility state is treated as not
132	/// world-readable.
133	pub async fn is_world_readable(&self, room_id: &RoomId) -> bool {
134		self.room_state_get_content(room_id, &StateEventType::RoomHistoryVisibility, "")
135			.await
136			.map(|c: RoomHistoryVisibilityEventContent| {
137				c.history_visibility == HistoryVisibility::WorldReadable
138			})
139			.unwrap_or(false)
140	}
141
142	/// Reports whether guest users may join the room.
143	///
144	/// Missing, unreadable, or invalid guest-access state is treated as denying
145	/// guest joins.
146	pub async fn guest_can_join(&self, room_id: &RoomId) -> bool {
147		self.room_state_get_content(room_id, &StateEventType::RoomGuestAccess, "")
148			.await
149			.map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin)
150			.unwrap_or(false)
151	}
152
153	/// Returns the room's current primary canonical alias.
154	///
155	/// Alternate aliases are not considered. Missing state, invalid content, or
156	/// an absent primary alias is returned as an error.
157	pub async fn get_canonical_alias(&self, room_id: &RoomId) -> Result<OwnedRoomAliasId> {
158		self.room_state_get_content(room_id, &StateEventType::RoomCanonicalAlias, "")
159			.await
160			.and_then(|c: RoomCanonicalAliasEventContent| {
161				c.alias
162					.ok_or_else(|| err!(Request(NotFound("No alias found in event content."))))
163			})
164	}
165
166	/// Returns the room's current plain-text topic.
167	///
168	/// Rich-topic plain text takes precedence over the legacy field. Missing,
169	/// invalid, or empty topic content is returned as an error.
170	pub async fn get_room_topic(&self, room_id: &RoomId) -> Result<String> {
171		self.room_state_get_content(room_id, &StateEventType::RoomTopic, "")
172			.await
173			.and_then(|content: RoomTopicEventContent| {
174				plain_text_topic(content)
175					.ok_or_else(|| err!(Request(NotFound("Empty topic found in event content."))))
176			})
177	}
178
179	/// Returns the room's current join rule.
180	///
181	/// Any missing, unreadable, or invalid join-rules state falls back to
182	/// [`JoinRule::Invite`].
183	pub async fn get_join_rules(&self, room_id: &RoomId) -> JoinRule {
184		self.room_state_get_content(room_id, &StateEventType::RoomJoinRules, "")
185			.await
186			.map_or(JoinRule::Invite, |c: RoomJoinRulesEventContent| c.join_rule)
187	}
188
189	/// Returns the room type declared by the current create event.
190	///
191	/// A missing create event, invalid content, or absent room type is returned as
192	/// an error; ordinary rooms therefore do not yield a synthetic type.
193	pub async fn get_room_type(&self, room_id: &RoomId) -> Result<RoomType> {
194		self.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
195			.await
196			.and_then(|content: RoomCreateEventContent| {
197				content
198					.room_type
199					.ok_or_else(|| err!(Request(NotFound("No type found in event content"))))
200			})
201	}
202
203	/// Returns the room's configured encryption algorithm.
204	///
205	/// Missing or invalid `m.room.encryption` state is returned as an error.
206	pub async fn get_room_encryption(
207		&self,
208		room_id: &RoomId,
209	) -> Result<EventEncryptionAlgorithm> {
210		self.room_state_get_content(room_id, &StateEventType::RoomEncryption, "")
211			.await
212			.map(|content: RoomEncryptionEventContent| content.algorithm)
213	}
214
215	/// Reports whether an encryption state event is present.
216	///
217	/// This checks that the event can be loaded, but does not deserialize its
218	/// content or validate an encryption algorithm.
219	pub async fn is_encrypted_room(&self, room_id: &RoomId) -> bool {
220		self.room_state_get(room_id, &StateEventType::RoomEncryption, "")
221			.await
222			.is_ok()
223	}
224}
225
226/// Checks whether the room federates, per `m.federate` in its create event.
227///
228/// An absent `m.federate` means the room federates, which is the spec
229/// default. A missing or unparsable create event reports the same, so a
230/// failed read never reports a room as non-federating.
231#[implement(Service)]
232pub async fn is_federating(&self, room_id: &RoomId) -> bool {
233	self.get_create(room_id)
234		.await
235		.and_then(|create| create.federate())
236		.unwrap_or(true)
237}
238
239/// Resolves room-topic content to a non-empty plain-text rendering.
240///
241/// The `m.topic` block's `text/plain` representation takes precedence under
242/// MSC3765, followed by the legacy `topic` field. Empty values yield `None`.
243pub(crate) fn plain_text_topic(content: RoomTopicEventContent) -> Option<String> {
244	let topic = content
245		.topic_block
246		.text
247		.find_plain()
248		.map(ToOwned::to_owned)
249		.unwrap_or(content.topic);
250
251	topic.is_empty().is_false().then_some(topic)
252}