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