Files
continuwuity/src/api/server/send_join.rs
T
2026-07-12 02:11:50 +00:00

291 lines
7.8 KiB
Rust

use std::{borrow::Borrow, time::Instant, vec};
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at, debug, err, info, trace,
utils::stream::{BroadbandExt, IterStream, TryBroadbandExt},
warn,
};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName, UserId,
api::federation::membership::create_join_event,
assign,
events::{
TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
room_version_rules::RoomVersionRules,
};
use serde_json::value::to_raw_value;
use crate::{Ruma, server::utils::validate_any_membership_event};
/// Creates a join membership event for the target, returning a computed
/// response with room state, auth chains, etc.
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()), level = "info")]
async fn create_join_event(
services: &Services,
origin: &ServerName,
room_id: &RoomId,
event_id: &EventId,
room_version_rules: &RoomVersionRules,
mut pdu: CanonicalJsonObject,
omit_members: bool,
) -> Result<create_join_event::v2::RoomState> {
if !services.rooms.metadata.exists(room_id).await {
return Err!(Request(NotFound("Room is unknown to this server.")));
}
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await
{
info!(
origin = origin.as_str(),
room_id = %room_id,
"Refusing to serve send_join for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
// ACL check origin server
services
.rooms
.event_handler
.acl_check(origin, room_id)
.await?;
// We need to return the state prior to joining, let's keep a reference to that
// here
let shortstatehash = services
.rooms
.state
.get_room_shortstatehash(room_id)
.await
.map_err(|e| err!(Request(NotFound(error!("Room has no state: {e}")))))?;
let sender = pdu
.get("sender")
.and_then(|v| v.as_str())
.map(UserId::parse)
.and_then(Result::ok)
.expect("sender was already validated");
let content: RoomMemberEventContent = serde_json::from_value(
pdu.get("content")
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
if let Some(authorising_user) = content.join_authorized_via_users_server {
if !room_version_rules.authorization.restricted_join_rule {
return Err!(Request(InvalidParam(
"Room version does not support restricted rooms but \
join_authorised_via_users_server ({authorising_user}) was found in the event."
)));
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Cannot authorise membership event through {authorising_user} as they do not \
belong to this homeserver"
)));
}
if !services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.await
{
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room you are trying to join, \
they cannot authorise your join."
)));
}
if !super::user_can_perform_restricted_join(services, &sender, room_id).await? {
return Err!(Request(UnableToAuthorizeJoin(
"Joining user did not pass restricted room's rules."
)));
}
services
.server_keys
.hash_and_sign_event(&mut pdu, room_version_rules)
.map_err(|e| {
err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}"))))
})?;
}
let mutex_lock = services
.rooms
.event_handler
.mutex_federation
.lock(room_id)
.await;
trace!("Acquired send_join mutex, persisting join event");
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(sender.server_name(), room_id, event_id, pdu.clone(), false)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
drop(mutex_lock);
trace!("Fetching current state IDs");
let state_ids: Vec<OwnedEventId> = services
.rooms
.state_accessor
.state_full_ids(shortstatehash)
.map(at!(1))
.collect()
.await;
trace!(%omit_members, "Constructing current state");
let state = state_ids
.iter()
.try_stream()
.broad_filter_map(|event_id| async move {
if omit_members && let Ok(e) = event_id.as_ref() {
let pdu = services.rooms.timeline.get_pdu(e).await;
if pdu.is_ok_and(|p| *p.kind() == TimelineEventType::RoomMember) {
trace!("omitting member event {e:?} from returned state");
// skip members
return None;
}
}
Some(event_id)
})
.broad_and_then(|event_id| services.rooms.timeline.get_pdu_json(event_id))
.broad_and_then(|pdu| {
services
.sending
.convert_to_outgoing_federation_event(pdu)
.map(Ok)
})
.try_collect()
.boxed()
.await?;
let starting_events = state_ids.iter().map(Borrow::borrow);
trace!("Constructing auth chain");
let auth_chain = services
.rooms
.auth_chain
.event_ids_iter(room_id, starting_events)
.broad_and_then(|event_id| async move {
services.rooms.timeline.get_pdu_json(&event_id).await
})
.broad_and_then(|pdu| {
services
.sending
.convert_to_outgoing_federation_event(pdu)
.map(Ok)
})
.try_collect()
.boxed()
.await?;
info!(fast_join = %omit_members, "Sending join event to other servers");
services.sending.send_pdu_room(room_id, &pdu_id).await?;
debug!("Finished sending join event");
let servers_in_room: Option<Vec<_>> = if !omit_members {
None
} else {
trace!("Fetching list of servers in room");
let servers: Vec<String> = services
.rooms
.state_cache
.room_servers(room_id)
.map(|sn| sn.as_str().to_owned())
.collect()
.await;
// If there's no servers, just add us
let servers = if servers.is_empty() {
warn!("Failed to find any servers, adding our own server name as a last resort");
vec![services.globals.server_name().to_string()]
} else {
trace!("Found {} servers in room", servers.len());
servers
};
Some(servers)
};
Ok(assign!(create_join_event::v2::RoomState::new(), {
auth_chain,
state,
event: to_raw_value(&CanonicalJsonValue::Object(pdu)).ok(),
members_omitted: omit_members,
servers_in_room,
}))
}
/// # `PUT /_matrix/federation/v2/send_join/{roomId}/{eventId}`
///
/// Submits a signed join event.
pub(crate) async fn create_join_event_v2_route(
State(services): State<crate::State>,
body: Ruma<create_join_event::v2::Request>,
) -> Result<create_join_event::v2::Response> {
if services
.moderation
.is_remote_server_forbidden(&body.identity)
{
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
let create_event = services
.rooms
.state_accessor
.get_room_create_event(&body.room_id)
.await;
let room_version_rules = room_version.rules().unwrap();
let (value, membership, sender, target) = validate_any_membership_event(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id.clone(),
body.room_id.clone(),
body.event_id.clone(),
)
.await?;
if membership != MembershipState::Join {
return Err!(Request(InvalidParam("Invalid membership (expected `join`)")));
}
if sender.server_name() != body.identity {
return Err!(Request(InvalidParam("Sender belongs to a different server")));
}
if sender != target {
return Err!(Request(InvalidParam("Sender does not match state key")));
}
let now = Instant::now();
let room_state = create_join_event(
&services,
&body.identity,
&body.room_id,
&body.event_id,
&room_version_rules,
value,
body.omit_members,
)
.boxed()
.await?;
info!(
"Finished sending a join for {} in {} in {:?}",
body.identity,
&body.room_id,
now.elapsed()
);
Ok(create_join_event::v2::Response::new(room_state))
}