Skip to main content

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