Skip to main content

tuwunel_api/client/membership/
join.rs

1use axum::extract::State;
2use ruma::{
3	CanonicalJsonObject, CanonicalJsonValue, RoomId,
4	api::client::membership::{join_room_by_id, join_room_by_id_or_alias},
5};
6use tuwunel_core::{Result, warn};
7use tuwunel_service::membership::Join;
8
9use super::banned_room_check;
10use crate::{ClientIp, Ruma};
11
12/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
13///
14/// Tries to join the sender user into a room.
15///
16/// - If the server knowns about this room: creates the join event and does auth
17///   rules locally
18/// - If the server does not know about the room: asks other servers over
19///   federation
20#[tracing::instrument(skip_all, fields(%client), name = "join")]
21pub(crate) async fn join_room_by_id_route(
22	State(services): State<crate::State>,
23	ClientIp(client): ClientIp,
24	body: Ruma<join_room_by_id::v3::Request>,
25) -> Result<join_room_by_id::v3::Response> {
26	let sender_user = body.sender_user();
27
28	let room_id: &RoomId = &body.room_id;
29
30	banned_room_check(&services, sender_user, room_id, None, client).await?;
31
32	let extra_content = extra_member_content(body.json_body.as_ref());
33
34	let mut errors = 0_usize;
35	while let Err(e) = services
36		.membership
37		.join(Join {
38			sender_user,
39			room_id,
40			orig_room_id: None,
41			reason: body.reason.clone(),
42			servers: &[],
43			is_appservice: body.appservice_info.is_some(),
44			extra_content: extra_content.clone(),
45		})
46		.await
47	{
48		errors = errors.saturating_add(1);
49		if errors >= services.config.max_join_attempts_per_join_request {
50			warn!(
51				"Several servers failed. Giving up for this request. Try again for different \
52				 server selection."
53			);
54			return Err(e);
55		}
56	}
57
58	Ok(join_room_by_id::v3::Response { room_id: room_id.to_owned() })
59}
60
61/// # `POST /_matrix/client/r0/join/{roomIdOrAlias}`
62///
63/// Tries to join the sender user into a room.
64///
65/// - If the server knowns about this room: creates the join event and does auth
66///   rules locally
67/// - If the server does not know about the room: use the server name query
68///   param if specified. if not specified, asks other servers over federation
69///   via room alias server name and room ID server name
70#[tracing::instrument(skip_all, fields(%client), name = "join")]
71pub(crate) async fn join_room_by_id_or_alias_route(
72	State(services): State<crate::State>,
73	ClientIp(client): ClientIp,
74	body: Ruma<join_room_by_id_or_alias::v3::Request>,
75) -> Result<join_room_by_id_or_alias::v3::Response> {
76	let sender_user = body.sender_user();
77	let appservice_info = &body.appservice_info;
78
79	let (room_id, servers) = services
80		.alias
81		.maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via))
82		.await?;
83
84	banned_room_check(&services, sender_user, &room_id, Some(&body.room_id_or_alias), client)
85		.await?;
86
87	let extra_content = extra_member_content(body.json_body.as_ref());
88
89	let mut errors = 0_usize;
90	while let Err(e) = services
91		.membership
92		.join(Join {
93			sender_user,
94			room_id: &room_id,
95			orig_room_id: Some(&body.room_id_or_alias),
96			reason: body.reason.clone(),
97			servers: &servers,
98			is_appservice: appservice_info.is_some(),
99			extra_content: extra_content.clone(),
100		})
101		.await
102	{
103		errors = errors.saturating_add(1);
104		if errors >= services.config.max_join_attempts_per_join_request {
105			warn!(
106				"Several servers failed. Giving up for this request. Try again for different \
107				 server selection."
108			);
109			return Err(e);
110		}
111	}
112
113	Ok(join_room_by_id_or_alias::v3::Response { room_id: room_id.clone() })
114}
115
116const RESERVED_JOIN_KEYS: [&str; 3] =
117	["reason", "third_party_signed", "join_authorised_via_users_server"];
118
119// Drop recognized and server-owned keys the client must not set.
120fn extra_member_content(json_body: Option<&CanonicalJsonValue>) -> Option<CanonicalJsonObject> {
121	let CanonicalJsonValue::Object(object) = json_body? else {
122		return None;
123	};
124
125	let extra: CanonicalJsonObject = object
126		.iter()
127		.filter(|(key, _)| !RESERVED_JOIN_KEYS.contains(&key.as_str()))
128		.map(|(key, value)| (key.clone(), value.clone()))
129		.collect();
130
131	(!extra.is_empty()).then_some(extra)
132}