Skip to main content

tuwunel_api/client/alias/
directory_room.rs

1use axum::extract::State;
2use futures::StreamExt;
3use rand::seq::SliceRandom;
4use ruma::{
5	OwnedServerName, RoomAliasId, RoomId, UserId,
6	api::client::alias::{create_alias, delete_alias, get_alias},
7	events::{StateEventType, room::canonical_alias::RoomCanonicalAliasEventContent},
8};
9use tuwunel_core::{Err, Result, debug, err, matrix::pdu::PduBuilder};
10use tuwunel_service::Services;
11
12use crate::Ruma;
13
14/// # `PUT /_matrix/client/v3/directory/room/{roomAlias}`
15///
16/// Creates a new room alias on this server.
17pub(crate) async fn create_alias_route(
18	State(services): State<crate::State>,
19	body: Ruma<create_alias::v3::Request>,
20) -> Result<create_alias::v3::Response> {
21	let sender_user = body.sender_user();
22
23	services
24		.alias
25		.creation_check(sender_user, body.appservice_info.as_ref())
26		.await?;
27
28	services
29		.alias
30		.appservice_checks(&body.room_alias, &body.appservice_info)
31		.await?;
32
33	// this isn't apart of alias_checks or delete alias route because we should
34	// allow removing forbidden room aliases
35	if services
36		.config
37		.forbidden_alias_names
38		.is_match(body.room_alias.alias())
39	{
40		return Err!(Request(Forbidden("Room alias is forbidden.")));
41	}
42
43	if services
44		.alias
45		.resolve_local_alias(&body.room_alias)
46		.await
47		.is_ok()
48	{
49		return Err!(Conflict("Alias already exists."));
50	}
51
52	services
53		.alias
54		.set_alias_by(&body.room_alias, &body.room_id, sender_user)?;
55
56	Ok(create_alias::v3::Response::new())
57}
58
59/// # `DELETE /_matrix/client/v3/directory/room/{roomAlias}`
60///
61/// Deletes a room alias from this server.
62///
63/// The deleted alias is also stripped from the room's canonical alias event on
64/// a best-effort basis. A sender without permission to send that state event
65/// still deletes the alias.
66pub(crate) async fn delete_alias_route(
67	State(services): State<crate::State>,
68	body: Ruma<delete_alias::v3::Request>,
69) -> Result<delete_alias::v3::Response> {
70	let sender_user = body.sender_user();
71	services
72		.alias
73		.appservice_checks(&body.room_alias, &body.appservice_info)
74		.await?;
75
76	let room_id = services
77		.alias
78		.remove_alias_by(&body.room_alias, sender_user)
79		.await?;
80
81	retire_canonical_alias(&services, &room_id, &body.room_alias, sender_user)
82		.await
83		.inspect_err(|e| debug!(%room_id, "Not updating canonical alias: {e}"))
84		.ok();
85
86	Ok(delete_alias::v3::Response::new())
87}
88
89/// # `GET /_matrix/client/v3/directory/room/{roomAlias}`
90///
91/// Resolve an alias locally or over federation.
92pub(crate) async fn get_alias_route(
93	State(services): State<crate::State>,
94	body: Ruma<get_alias::v3::Request>,
95) -> Result<get_alias::v3::Response> {
96	let room_alias = body.body.room_alias;
97
98	let (room_id, servers) = services
99		.alias
100		.resolve_alias(&room_alias)
101		.await
102		.map_err(|_| err!(Request(NotFound("Room with alias not found."))))?;
103
104	let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
105	debug!(?room_alias, ?room_id, "available servers: {servers:?}");
106
107	Ok(get_alias::v3::Response::new(room_id, servers))
108}
109
110/// Removes a deleted alias from the room's canonical alias state.
111///
112/// The directory entry is authoritative and has already been removed, so this
113/// runs on a best-effort basis. A sender lacking permission to send
114/// `m.room.canonical_alias` leaves the stale state event in place.
115async fn retire_canonical_alias(
116	services: &Services,
117	room_id: &RoomId,
118	deleted: &RoomAliasId,
119	sender_user: &UserId,
120) -> Result {
121	let state_lock = services.state.mutex.lock(room_id).await;
122
123	let Ok(content) = services
124		.state_accessor
125		.room_state_get_content::<RoomCanonicalAliasEventContent>(
126			room_id,
127			&StateEventType::RoomCanonicalAlias,
128			"",
129		)
130		.await
131	else {
132		return Ok(());
133	};
134
135	if !content.aliases().any(|alias| alias == deleted) {
136		return Ok(());
137	}
138
139	let content = RoomCanonicalAliasEventContent {
140		alias: content.alias.filter(|alias| alias != deleted),
141		alt_aliases: content
142			.alt_aliases
143			.into_iter()
144			.filter(|alt| alt != deleted)
145			.collect(),
146	};
147
148	services
149		.timeline
150		.build_and_append_pdu(PduBuilder::state("", &content), sender_user, room_id, &state_lock)
151		.await
152		.map(|_| ())
153}
154
155async fn room_available_servers(
156	services: &Services,
157	room_id: &RoomId,
158	room_alias: &RoomAliasId,
159	pre_servers: Vec<OwnedServerName>,
160) -> Vec<OwnedServerName> {
161	// find active servers in room state cache to suggest
162	let mut servers: Vec<OwnedServerName> = services
163		.state_cache
164		.room_servers(room_id)
165		.map(ToOwned::to_owned)
166		.collect()
167		.await;
168
169	// push any servers we want in the list already (e.g. responded remote alias
170	// servers, room alias server itself)
171	servers.extend(pre_servers);
172
173	servers.sort_unstable();
174	servers.dedup();
175
176	// shuffle list of servers randomly after sort and dedupe
177	servers.shuffle(&mut rand::rng());
178
179	// insert our server as the very first choice if in list, else check if we can
180	// prefer the room alias server first
181	if let Some(server_index) = servers
182		.iter()
183		.position(|server_name| services.globals.server_is_ours(server_name))
184	{
185		servers.swap(0, server_index);
186	} else if let Some(alias_server_index) = servers
187		.iter()
188		.position(|server| server == room_alias.server_name())
189	{
190		servers.swap(0, alias_server_index);
191	}
192
193	servers
194}