use axum::extract::State; use ruma::{ api::{client::error::ErrorKind, federation::membership::prepare_join_event}, events::{ room::{ join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, }, StateEventType, TimelineEventType, }, CanonicalJsonObject, RoomId, RoomVersionId, UserId, }; use serde_json::value::to_raw_value; use tracing::warn; use crate::{ service::{pdu::PduBuilder, Services}, Error, Result, Ruma, }; /// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}` /// /// Creates a join template. pub(crate) async fn create_join_event_template_route( State(services): State, body: Ruma, ) -> Result { if !services.rooms.metadata.exists(&body.room_id)? { return Err(Error::BadRequest(ErrorKind::NotFound, "Room is unknown to this server.")); } let origin = body.origin.as_ref().expect("server is authenticated"); if body.user_id.server_name() != origin { return Err(Error::BadRequest( ErrorKind::InvalidParam, "Not allowed to join on behalf of another server/user", )); } // ACL check origin server services .rooms .event_handler .acl_check(origin, &body.room_id)?; if services .globals .config .forbidden_remote_server_names .contains(origin) { warn!( "Server {origin} for remote user {} tried joining room ID {} which has a server name that is globally \ forbidden. Rejecting.", &body.user_id, &body.room_id, ); return Err(Error::BadRequest( ErrorKind::forbidden(), "Server is banned on this homeserver.", )); } if let Some(server) = body.room_id.server_name() { if services .globals .config .forbidden_remote_server_names .contains(&server.to_owned()) { return Err(Error::BadRequest( ErrorKind::forbidden(), "Server is banned on this homeserver.", )); } } let room_version_id = services.rooms.state.get_room_version(&body.room_id)?; let state_lock = services.rooms.state.mutex.lock(&body.room_id).await; let join_authorized_via_users_server = if (services .rooms .state_cache .is_left(&body.user_id, &body.room_id) .unwrap_or(true)) && user_can_perform_restricted_join(services, &body.user_id, &body.room_id, &room_version_id)? { let auth_user = services .rooms .state_cache .room_members(&body.room_id) .filter_map(Result::ok) .filter(|user| user.server_name() == services.globals.server_name()) .find(|user| { services .rooms .state_accessor .user_can_invite(&body.room_id, user, &body.user_id, &state_lock) .unwrap_or(false) }); if auth_user.is_some() { auth_user } else { return Err(Error::BadRequest( ErrorKind::UnableToGrantJoin, "No user on this server is able to assist in joining.", )); } } else { None }; let room_version_id = services.rooms.state.get_room_version(&body.room_id)?; if !body.ver.contains(&room_version_id) { return Err(Error::BadRequest( ErrorKind::IncompatibleRoomVersion { room_version: room_version_id, }, "Room version not supported.", )); } let content = to_raw_value(&RoomMemberEventContent { avatar_url: None, blurhash: None, displayname: None, is_direct: None, membership: MembershipState::Join, third_party_invite: None, reason: None, join_authorized_via_users_server, }) .expect("member event is valid value"); let (_pdu, mut pdu_json) = services.rooms.timeline.create_hash_and_sign_event( PduBuilder { event_type: TimelineEventType::RoomMember, content, unsigned: None, state_key: Some(body.user_id.to_string()), redacts: None, }, &body.user_id, &body.room_id, &state_lock, )?; drop(state_lock); // room v3 and above removed the "event_id" field from remote PDU format maybe_strip_event_id(&mut pdu_json, &room_version_id)?; Ok(prepare_join_event::v1::Response { room_version: Some(room_version_id), event: to_raw_value(&pdu_json).expect("CanonicalJson can be serialized to JSON"), }) } /// Checks whether the given user can join the given room via a restricted join. /// This doesn't check the current user's membership. This should be done /// externally, either by using the state cache or attempting to authorize the /// event. pub(crate) fn user_can_perform_restricted_join( services: &Services, user_id: &UserId, room_id: &RoomId, room_version_id: &RoomVersionId, ) -> Result { use RoomVersionId::*; let join_rules_event = services .rooms .state_accessor .room_state_get(room_id, &StateEventType::RoomJoinRules, "")?; let Some(join_rules_event_content) = join_rules_event .as_ref() .map(|join_rules_event| { serde_json::from_str::(join_rules_event.content.get()).map_err(|e| { warn!("Invalid join rules event in database: {e}"); Error::bad_database("Invalid join rules event in database") }) }) .transpose()? else { return Ok(false); }; if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) { return Ok(false); } let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) = join_rules_event_content.join_rule else { return Ok(false); }; if r.allow .iter() .filter_map(|rule| { if let AllowRule::RoomMembership(membership) = rule { Some(membership) } else { None } }) .any(|m| { services .rooms .state_cache .is_joined(user_id, &m.room_id) .unwrap_or(false) }) { Ok(true) } else { Err(Error::BadRequest( ErrorKind::UnableToAuthorizeJoin, "User is not known to be in any required room.", )) } } pub(crate) fn maybe_strip_event_id(pdu_json: &mut CanonicalJsonObject, room_version_id: &RoomVersionId) -> Result<()> { use RoomVersionId::*; match room_version_id { V1 | V2 => {}, V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 => { pdu_json.remove("event_id"); }, _ => { warn!("Unexpected or unsupported room version {room_version_id}"); return Err(Error::BadRequest( ErrorKind::BadJson, "Unexpected or unsupported room version found", )); }, }; Ok(()) }