Skip to main content

tuwunel_service/rooms/state_accessor/
mod.rs

1mod erased;
2mod room_state;
3mod server_can;
4mod state;
5mod user_can;
6
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use futures::{FutureExt, TryFutureExt, future::try_join};
11use ruma::{
12	EventEncryptionAlgorithm, OwnedRoomAliasId, RoomId, UserId,
13	events::{
14		StateEventType,
15		room::{
16			avatar::RoomAvatarEventContent,
17			canonical_alias::RoomCanonicalAliasEventContent,
18			create::RoomCreateEventContent,
19			encryption::RoomEncryptionEventContent,
20			guest_access::{GuestAccess, RoomGuestAccessEventContent},
21			history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
22			join_rules::{JoinRule, RoomJoinRulesEventContent},
23			member::RoomMemberEventContent,
24			name::RoomNameEventContent,
25			power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
26			topic::RoomTopicEventContent,
27		},
28	},
29	room::RoomType,
30};
31use tuwunel_core::{
32	Result, err,
33	matrix::{Pdu, room_version},
34	utils::BoolExt,
35};
36
37use crate::rooms::state_res::events::RoomCreateEvent;
38
39pub struct Service {
40	services: Arc<crate::services::OnceServices>,
41}
42
43#[async_trait]
44impl crate::Service for Service {
45	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
46		Ok(Arc::new(Self { services: args.services.clone() }))
47	}
48
49	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
50}
51
52impl Service {
53	/// Gets the effective power levels of a room, regardless of if there is an
54	/// `m.room.power_levels` state.
55	pub async fn get_power_levels(&self, room_id: &RoomId) -> Result<RoomPowerLevels> {
56		let create = self.get_create(room_id);
57		let power_levels = self
58			.room_state_get_content(room_id, &StateEventType::RoomPowerLevels, "")
59			.map_ok(|c: RoomPowerLevelsEventContent| c)
60			.map(Result::ok)
61			.map(Ok);
62
63		let (create, power_levels) = try_join(create, power_levels).await?;
64
65		let room_version = create.room_version()?;
66		let rules = room_version::rules(&room_version)?;
67		let creators = create.creators(&rules.authorization)?;
68
69		Ok(RoomPowerLevels::new(power_levels.into(), &rules.authorization, creators))
70	}
71
72	pub async fn get_create(&self, room_id: &RoomId) -> Result<RoomCreateEvent<Pdu>> {
73		self.room_state_get(room_id, &StateEventType::RoomCreate, "")
74			.await
75			.map(RoomCreateEvent::new)
76	}
77
78	pub async fn get_name(&self, room_id: &RoomId) -> Result<String> {
79		self.room_state_get_content(room_id, &StateEventType::RoomName, "")
80			.await
81			.and_then(|c: RoomNameEventContent| {
82				c.name
83					.is_empty()
84					.is_false()
85					.then_some(c.name)
86					.ok_or_else(|| err!(Request(NotFound("Empty name found in event content."))))
87			})
88	}
89
90	pub async fn get_avatar(&self, room_id: &RoomId) -> Result<RoomAvatarEventContent> {
91		self.room_state_get_content(room_id, &StateEventType::RoomAvatar, "")
92			.await
93	}
94
95	pub async fn is_direct(&self, room_id: &RoomId, user_id: &UserId) -> bool {
96		self.get_member(room_id, user_id)
97			.await
98			.is_ok_and(|content| content.is_direct)
99	}
100
101	pub async fn get_member(
102		&self,
103		room_id: &RoomId,
104		user_id: &UserId,
105	) -> Result<RoomMemberEventContent> {
106		self.room_state_get_content(room_id, &StateEventType::RoomMember, user_id.as_str())
107			.await
108	}
109
110	/// Checks if guests are able to view room content without joining
111	pub async fn is_world_readable(&self, room_id: &RoomId) -> bool {
112		self.room_state_get_content(room_id, &StateEventType::RoomHistoryVisibility, "")
113			.await
114			.map(|c: RoomHistoryVisibilityEventContent| {
115				c.history_visibility == HistoryVisibility::WorldReadable
116			})
117			.unwrap_or(false)
118	}
119
120	/// Checks if guests are able to join a given room
121	pub async fn guest_can_join(&self, room_id: &RoomId) -> bool {
122		self.room_state_get_content(room_id, &StateEventType::RoomGuestAccess, "")
123			.await
124			.map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin)
125			.unwrap_or(false)
126	}
127
128	/// Gets the primary alias from canonical alias event
129	pub async fn get_canonical_alias(&self, room_id: &RoomId) -> Result<OwnedRoomAliasId> {
130		self.room_state_get_content(room_id, &StateEventType::RoomCanonicalAlias, "")
131			.await
132			.and_then(|c: RoomCanonicalAliasEventContent| {
133				c.alias
134					.ok_or_else(|| err!(Request(NotFound("No alias found in event content."))))
135			})
136	}
137
138	/// Gets the room topic
139	pub async fn get_room_topic(&self, room_id: &RoomId) -> Result<String> {
140		self.room_state_get_content(room_id, &StateEventType::RoomTopic, "")
141			.await
142			.and_then(|content: RoomTopicEventContent| {
143				plain_text_topic(content)
144					.ok_or_else(|| err!(Request(NotFound("Empty topic found in event content."))))
145			})
146	}
147
148	/// Returns the join rules for a given room (`JoinRule` type). Will default
149	/// to Invite if doesnt exist or invalid
150	pub async fn get_join_rules(&self, room_id: &RoomId) -> JoinRule {
151		self.room_state_get_content(room_id, &StateEventType::RoomJoinRules, "")
152			.await
153			.map_or(JoinRule::Invite, |c: RoomJoinRulesEventContent| c.join_rule)
154	}
155
156	pub async fn get_room_type(&self, room_id: &RoomId) -> Result<RoomType> {
157		self.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
158			.await
159			.and_then(|content: RoomCreateEventContent| {
160				content
161					.room_type
162					.ok_or_else(|| err!(Request(NotFound("No type found in event content"))))
163			})
164	}
165
166	/// Gets the room's encryption algorithm if `m.room.encryption` state event
167	/// is found
168	pub async fn get_room_encryption(
169		&self,
170		room_id: &RoomId,
171	) -> Result<EventEncryptionAlgorithm> {
172		self.room_state_get_content(room_id, &StateEventType::RoomEncryption, "")
173			.await
174			.map(|content: RoomEncryptionEventContent| content.algorithm)
175	}
176
177	pub async fn is_encrypted_room(&self, room_id: &RoomId) -> bool {
178		self.room_state_get(room_id, &StateEventType::RoomEncryption, "")
179			.await
180			.is_ok()
181	}
182}
183
184/// Resolves an `m.room.topic` to its plain-text rendering: the `m.topic`
185/// block's `text/plain` representation when present (MSC3765), else the legacy
186/// `topic` field; `None` when neither yields a non-empty string.
187pub(crate) fn plain_text_topic(content: RoomTopicEventContent) -> Option<String> {
188	let topic = content
189		.topic_block
190		.text
191		.find_plain()
192		.map(ToOwned::to_owned)
193		.unwrap_or(content.topic);
194
195	topic.is_empty().is_false().then_some(topic)
196}