Skip to main content

tuwunel_service/rooms/alias/
mod.rs

1use std::sync::Arc;
2
3use futures::{Stream, StreamExt};
4use ruma::{
5	OwnedRoomId, OwnedServerName, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, UserId,
6	api::federation::query::get_room_information::v1::Request, events::StateEventType,
7};
8use tuwunel_core::{
9	Err, Result, err,
10	matrix::Event,
11	utils::{ReadyExt, stream::TryIgnore},
12};
13use tuwunel_database::{Deserialized, Ignore, Interfix, Map};
14
15use crate::appservice::RegistrationInfo;
16
17pub struct Service {
18	db: Data,
19	services: Arc<crate::services::OnceServices>,
20}
21
22struct Data {
23	alias_userid: Arc<Map>,
24	alias_roomid: Arc<Map>,
25	aliasid_alias: Arc<Map>,
26}
27
28impl crate::Service for Service {
29	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
30		Ok(Arc::new(Self {
31			db: Data {
32				alias_userid: args.db["alias_userid"].clone(),
33				alias_roomid: args.db["alias_roomid"].clone(),
34				aliasid_alias: args.db["aliasid_alias"].clone(),
35			},
36			services: args.services.clone(),
37		}))
38	}
39
40	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
41}
42
43impl Service {
44	pub fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> Result {
45		self.check_alias_local(alias)?;
46
47		self.set_alias_by(alias, room_id, &self.services.globals.server_user)
48	}
49
50	#[tracing::instrument(skip(self))]
51	pub fn set_alias_by(
52		&self,
53		alias: &RoomAliasId,
54		room_id: &RoomId,
55		user_id: &UserId,
56	) -> Result {
57		self.check_alias_local(alias)?;
58
59		if alias == self.services.admin.admin_alias
60			&& user_id != self.services.globals.server_user
61		{
62			return Err!(Request(Forbidden("Only the server user can set this alias")));
63		}
64
65		let count = self.services.globals.next_count();
66
67		let localpart = alias.alias();
68
69		// Comes first as we don't want a stuck alias
70		self.db.alias_userid.insert(localpart, user_id);
71
72		self.db.alias_roomid.insert(localpart, room_id);
73
74		self.db
75			.aliasid_alias
76			.put_raw((room_id, *count), alias);
77
78		Ok(())
79	}
80
81	pub async fn remove_alias_by(&self, alias: &RoomAliasId, user_id: &UserId) -> Result {
82		if !self.user_can_remove_alias(alias, user_id).await? {
83			return Err!(Request(Forbidden("User is not permitted to remove this alias.")));
84		}
85
86		self.remove_alias(alias).await
87	}
88
89	#[tracing::instrument(skip(self))]
90	pub async fn remove_alias(&self, alias: &RoomAliasId) -> Result {
91		let alias = alias.alias();
92		let Ok(room_id) = self.db.alias_roomid.get(&alias).await else {
93			return Err!(Request(NotFound("Alias does not exist or is invalid.")));
94		};
95
96		let prefix = (&room_id, Interfix);
97		self.db
98			.aliasid_alias
99			.keys_prefix_raw(&prefix)
100			.ignore_err()
101			.ready_for_each(|key| self.db.aliasid_alias.remove(key))
102			.await;
103
104		self.db.alias_roomid.remove(alias.as_bytes());
105		self.db.alias_userid.remove(alias.as_bytes());
106
107		Ok(())
108	}
109
110	#[inline]
111	pub async fn maybe_resolve(&self, room: &RoomOrAliasId) -> Result<OwnedRoomId> {
112		match <&RoomId>::try_from(room) {
113			| Ok(room_id) => Ok(room_id.to_owned()),
114			| Err(alias) => Ok(self.resolve_alias(alias).await?.0),
115		}
116	}
117
118	pub async fn maybe_resolve_with_servers(
119		&self,
120		room: &RoomOrAliasId,
121		servers: Option<&[OwnedServerName]>,
122	) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
123		match <&RoomId>::try_from(room) {
124			| Ok(room_id) => Ok((room_id.to_owned(), Vec::from(servers.unwrap_or_default()))),
125			| Err(alias) => self.resolve_alias(alias).await,
126		}
127	}
128
129	#[tracing::instrument(skip(self), name = "resolve")]
130	pub async fn resolve_alias(
131		&self,
132		room_alias: &RoomAliasId,
133	) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
134		if self.services.globals.alias_is_local(room_alias) {
135			if let Ok(room_id) = self.resolve_local_alias(room_alias).await {
136				return Ok((room_id, Vec::new()));
137			}
138
139			if let Ok(room_id) = self.resolve_appservice_alias(room_alias).await {
140				return Ok((room_id, Vec::new()));
141			}
142
143			return Err!(Request(NotFound("Room with alias not found.")));
144		}
145
146		return self.remote_resolve(room_alias).await;
147	}
148
149	async fn remote_resolve(
150		&self,
151		room_alias: &RoomAliasId,
152	) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
153		let server = room_alias.server_name();
154
155		let request = Request { room_alias: room_alias.to_owned() };
156
157		let response = self
158			.services
159			.federation
160			.execute(server, request)
161			.await?;
162
163		Ok((response.room_id, response.servers))
164	}
165
166	#[tracing::instrument(skip(self), level = "trace")]
167	pub async fn resolve_local_alias(&self, alias: &RoomAliasId) -> Result<OwnedRoomId> {
168		self.check_alias_local(alias)?;
169		self.db
170			.alias_roomid
171			.get(alias.alias())
172			.await
173			.deserialized()
174	}
175
176	#[tracing::instrument(skip(self), level = "debug")]
177	pub fn local_aliases_for_room<'a>(
178		&'a self,
179		room_id: &'a RoomId,
180	) -> impl Stream<Item = &RoomAliasId> + Send + 'a {
181		let prefix = (room_id, Interfix);
182		self.db
183			.aliasid_alias
184			.stream_prefix(&prefix)
185			.ignore_err()
186			.map(|(_, alias): (Ignore, &RoomAliasId)| alias)
187	}
188
189	#[tracing::instrument(skip(self), level = "debug")]
190	pub fn all_local_aliases(&self) -> impl Stream<Item = (&RoomId, &str)> + Send + '_ {
191		self.db
192			.alias_roomid
193			.stream()
194			.ignore_err()
195			.map(|(alias_localpart, room_id): (&str, &RoomId)| (room_id, alias_localpart))
196	}
197
198	async fn user_can_remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result<bool> {
199		self.check_alias_local(alias)?;
200
201		let room_id = self
202			.resolve_local_alias(alias)
203			.await
204			.map_err(|_| err!(Request(NotFound("Alias not found."))))?;
205
206		// The creator of an alias can remove it
207		if self
208            .who_created_alias(alias).await
209            .is_ok_and(|user| user == user_id)
210            // Server admins can remove any local alias
211            || self.services.admin.user_is_admin(user_id).await
212		{
213			return Ok(true);
214		}
215
216		// Checking whether the user is able to change canonical aliases of the room
217		if let Ok(power_levels) = self
218			.services
219			.state_accessor
220			.get_power_levels(&room_id)
221			.await
222		{
223			return Ok(
224				power_levels.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias)
225			);
226		}
227
228		// If there is no power levels event, only the room creator can change
229		// canonical aliases
230		if let Ok(event) = self
231			.services
232			.state_accessor
233			.room_state_get(&room_id, &StateEventType::RoomCreate, "")
234			.await
235		{
236			return Ok(event.sender() == user_id);
237		}
238
239		Err!(Database("Room has no m.room.create event"))
240	}
241
242	async fn who_created_alias(&self, alias: &RoomAliasId) -> Result<OwnedUserId> {
243		self.check_alias_local(alias)?;
244
245		self.db
246			.alias_userid
247			.get(alias.alias())
248			.await
249			.deserialized()
250	}
251
252	async fn resolve_appservice_alias(&self, room_alias: &RoomAliasId) -> Result<OwnedRoomId> {
253		use ruma::api::appservice::query::query_room_alias;
254
255		self.check_alias_local(room_alias)?;
256
257		for appservice in self.services.appservice.read().await.values() {
258			if appservice.aliases.is_match(room_alias.as_str())
259				&& matches!(
260					self.services
261						.appservice
262						.send_request(
263							appservice.registration.clone(),
264							query_room_alias::v1::Request { room_alias: room_alias.to_owned() },
265						)
266						.await,
267					Ok(Some(_opt_result))
268				) {
269				return self
270					.resolve_local_alias(room_alias)
271					.await
272					.map_err(|_| err!(Request(NotFound("Room does not exist."))));
273			}
274		}
275
276		Err!(Request(NotFound("Room does not exist.")))
277	}
278
279	fn check_alias_local(&self, alias: &RoomAliasId) -> Result {
280		if !self.services.globals.alias_is_local(alias) {
281			return Err!(Request(InvalidParam("Alias is from another server.")));
282		}
283
284		Ok(())
285	}
286
287	#[tracing::instrument(skip(self, appservice_info), level = "trace")]
288	pub async fn appservice_checks(
289		&self,
290		room_alias: &RoomAliasId,
291		appservice_info: &Option<RegistrationInfo>,
292	) -> Result {
293		self.check_alias_local(room_alias)?;
294		if let Some(info) = appservice_info {
295			if !info.aliases.is_match(room_alias.as_str()) {
296				return Err!(Request(Exclusive("Room alias is not in namespace.")));
297			}
298		} else if self
299			.services
300			.appservice
301			.is_exclusive_alias(room_alias)
302			.await
303		{
304			return Err!(Request(Exclusive("Room alias reserved by appservice.")));
305		}
306
307		Ok(())
308	}
309}