tuwunel_api/server/
make_join.rs1use 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
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 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 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 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
135pub(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 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 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}