Skip to main content

tuwunel_api/server/
make_join.rs

1use axum::extract::State;
2use futures::{StreamExt, TryFutureExt, pin_mut};
3use ruma::{
4	OwnedUserId, RoomId, UserId,
5	api::{
6		error::{ErrorKind, IncompatibleRoomVersionErrorData},
7		federation::membership::prepare_join_event,
8	},
9	events::{
10		StateEventType,
11		room::{
12			join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
13			member::{MembershipState, RoomMemberEventContent},
14		},
15	},
16};
17use tuwunel_core::{
18	Err, Error, Result, at, debug_info,
19	matrix::{RoomVersionRules, pdu::PduBuilder, room_version},
20	utils::IterStream,
21};
22use tuwunel_service::Services;
23
24use super::utils::require_known_room;
25use crate::Ruma;
26
27/// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}`
28///
29/// Creates a join template.
30pub(crate) async fn create_join_event_template_route(
31	State(services): State<crate::State>,
32	body: Ruma<prepare_join_event::v1::Request>,
33) -> Result<prepare_join_event::v1::Response> {
34	require_known_room(&services, &body.room_id, body.origin()).await?;
35
36	if body.user_id.server_name() != body.origin() {
37		return Err!(Request(BadJson("Not allowed to join on behalf of another server/user.")));
38	}
39
40	if let Some(server) = body.room_id.server_name()
41		&& services
42			.config
43			.is_forbidden_remote_server_name(server)
44	{
45		return Err!(Request(Forbidden(warn!(
46			"Room ID server name {server} is banned on this homeserver."
47		))));
48	}
49
50	let room_version_id = services
51		.state
52		.get_room_version(&body.room_id)
53		.await?;
54
55	if !body.ver.contains(&room_version_id) {
56		return Err(Error::BadRequest(
57			ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData::new(
58				room_version_id,
59			)),
60			"Room version not supported.",
61		));
62	}
63
64	let room_version_rules = room_version::rules(&room_version_id)?;
65
66	let state_lock = services.state.mutex.lock(&body.room_id).await;
67
68	let join_authorized_via_users_server: Option<OwnedUserId> =
69		if user_can_perform_restricted_join(
70			&services,
71			&body.user_id,
72			&body.room_id,
73			&room_version_rules,
74		)
75		.await?
76		{
77			let users = services
78				.state_cache
79				.local_users_in_room(&body.room_id)
80				.filter(|user| {
81					services.state_accessor.user_can_invite(
82						&body.room_id,
83						user,
84						&body.user_id,
85						&state_lock,
86					)
87				})
88				.map(ToOwned::to_owned);
89
90			pin_mut!(users);
91			let Some(auth_user) = users.next().await else {
92				return Err!(Request(UnableToGrantJoin(
93					"No user on this server is able to assist in joining."
94				)));
95			};
96
97			Some(auth_user)
98		} else {
99			None
100		};
101
102	let pdu_json = services
103		.timeline
104		.create_hash_and_sign_event(
105			PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
106				join_authorized_via_users_server,
107				..RoomMemberEventContent::new(MembershipState::Join)
108			}),
109			&body.user_id,
110			&body.room_id,
111			&state_lock,
112		)
113		.map_ok(at!(1))
114		.await?;
115
116	drop(state_lock);
117
118	Ok(prepare_join_event::v1::Response {
119		room_version: Some(room_version_id.clone()),
120		event: services
121			.federation
122			.format_pdu_into(pdu_json, Some(&room_version_id))
123			.await,
124	})
125}
126
127/// Checks whether the given user can join the given room via a restricted join.
128pub(crate) async fn user_can_perform_restricted_join(
129	services: &Services,
130	user_id: &UserId,
131	room_id: &RoomId,
132	room_version_rules: &RoomVersionRules,
133) -> Result<bool> {
134	if !room_version_rules
135		.authorization
136		.restricted_join_rule
137	{
138		return Ok(false);
139	}
140
141	if services
142		.state_cache
143		.is_joined(user_id, room_id)
144		.await
145	{
146		// joining user is already joined, there is nothing we need to do
147		return Ok(false);
148	}
149
150	if services
151		.state_cache
152		.is_invited(user_id, room_id)
153		.await
154	{
155		return Ok(false);
156	}
157
158	let Ok(join_rules_event_content) = services
159		.state_accessor
160		.room_state_get_content::<RoomJoinRulesEventContent>(
161			room_id,
162			&StateEventType::RoomJoinRules,
163			"",
164		)
165		.await
166	else {
167		return Ok(false);
168	};
169
170	let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) =
171		join_rules_event_content.join_rule
172	else {
173		return Ok(false);
174	};
175
176	if r.allow.is_empty() {
177		debug_info!("{room_id} is restricted but the allow key is empty");
178		return Ok(false);
179	}
180
181	if r.allow
182		.iter()
183		.filter_map(|rule| {
184			if let AllowRule::RoomMembership(membership) = rule {
185				Some(membership)
186			} else {
187				None
188			}
189		})
190		.stream()
191		.any(|m| {
192			services
193				.state_cache
194				.is_joined(user_id, &m.room_id)
195		})
196		.await
197	{
198		Ok(true)
199	} else {
200		Err!(Request(UnableToAuthorizeJoin(
201			"Joining user is not known to be in any required room."
202		)))
203	}
204}