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