Skip to main content

tuwunel_api/client/membership/
join.rs

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