Skip to main content

tuwunel_service/rooms/state_accessor/
mod.rs

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