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