tuwunel_api_federation/
make_join.rs1use 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
24pub(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
124pub(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 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}