Skip to content
Snippets Groups Projects
membership.rs 45 KiB
Newer Older
			} else {
				return Err(error);
			}
		} else {
			return Err(error);
		}
	}

	Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
async fn make_join_request(
	sender_user: &UserId, room_id: &RoomId, servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
	let mut make_join_response_and_server = Err(Error::BadServerResponse("No server available to assist in joining."));

	for remote_server in servers {
		if remote_server == services().globals.server_name() {
			continue;
		}
		info!("Asking {remote_server} for make_join");
		let make_join_response = services()
			.sending
			.send_federation_request(
				remote_server,
				federation::membership::prepare_join_event::v1::Request {
					room_id: room_id.to_owned(),
					user_id: sender_user.to_owned(),
					ver: services().globals.supported_room_versions(),
				},
			)
			.await;

		make_join_response_and_server = make_join_response.map(|r| (r, remote_server.clone()));

		if make_join_response_and_server.is_ok() {
			break;
		}
	}

	make_join_response_and_server
async fn validate_and_add_event_id(
	pdu: &RawJsonValue, room_version: &RoomVersionId, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
Timo Kösters's avatar
Timo Kösters committed
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
	let mut value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
		error!("Invalid PDU in server response: {:?}: {:?}", pdu, e);
		Error::BadServerResponse("Invalid PDU in server response")
	})?;
	let event_id = EventId::parse(format!(
		"${}",
		ruma::signatures::reference_hash(&value, room_version).expect("ruma can calculate reference hashes")
	))
	.expect("ruma's reference hashes are valid event ids");

	let back_off = |id| async {
		match services().globals.bad_event_ratelimiter.write().await.entry(id) {
			Entry::Vacant(e) => {
				e.insert((Instant::now(), 1));
			},
			Entry::Occupied(mut e) => *e.get_mut() = (Instant::now(), e.get().1 + 1),
		}
	if let Some((time, tries)) = services().globals.bad_event_ratelimiter.read().await.get(&event_id) {
		// Exponential backoff
		let mut min_elapsed_duration = Duration::from_secs(5 * 60) * (*tries) * (*tries);
		if min_elapsed_duration > Duration::from_secs(60 * 60 * 24) {
			min_elapsed_duration = Duration::from_secs(60 * 60 * 24);
		}

		if time.elapsed() < min_elapsed_duration {
			debug!("Backing off from {}", event_id);
			return Err(Error::BadServerResponse("bad event, still backing off"));
		}
	}

	if let Err(e) = ruma::signatures::verify_event(&*pub_key_map.read().await, &value, room_version) {
		warn!("Event {} failed verification {:?} {}", event_id, pdu, e);
		return Err(Error::BadServerResponse("Event failed verification."));
	}

	value.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned()));

	Ok((event_id, value))
🥺's avatar
🥺 committed
pub(crate) async fn invite_helper(
	sender_user: &UserId, user_id: &UserId, room_id: &RoomId, reason: Option<String>, is_direct: bool,
) -> Result<()> {
	if !services().users.is_admin(user_id)? && services().globals.block_non_admin_invites() {
		info!("User {sender_user} is not an admin and attempted to send an invite to room {room_id}");
		return Err(Error::BadRequest(
			ErrorKind::Forbidden,
			"Invites are not allowed on this server.",
		));
	}

	if user_id.server_name() != services().globals.server_name() {
		let (pdu, pdu_json, invite_room_state) = {
			let mutex_state =
				Arc::clone(services().globals.roomid_mutex_state.write().await.entry(room_id.to_owned()).or_default());
			let state_lock = mutex_state.lock().await;

			let content = to_raw_value(&RoomMemberEventContent {
				avatar_url: services().users.avatar_url(user_id)?,
				displayname: None,
				is_direct: Some(is_direct),
				membership: MembershipState::Invite,
				third_party_invite: None,
				blurhash: None,
				reason,
				join_authorized_via_users_server: None,
			})
			.expect("member event is valid value");

			let (pdu, pdu_json) = services().rooms.timeline.create_hash_and_sign_event(
				PduBuilder {
					event_type: TimelineEventType::RoomMember,
					content,
					unsigned: None,
					state_key: Some(user_id.to_string()),
					redacts: None,
				},
				sender_user,
				room_id,
				&state_lock,
			)?;

			let invite_room_state = services().rooms.state.calculate_invite_state(&pdu)?;

			drop(state_lock);

			(pdu, pdu_json, invite_room_state)
		};

		let room_version_id = services().rooms.state.get_room_version(room_id)?;

		let response = services()
			.sending
			.send_federation_request(
				user_id.server_name(),
				create_invite::v2::Request {
					room_id: room_id.to_owned(),
					event_id: (*pdu.event_id).to_owned(),
					room_version: room_version_id.clone(),
					event: PduEvent::convert_to_outgoing_federation_event(pdu_json.clone()),
					invite_room_state,
				},
			)
			.await?;

		let pub_key_map = RwLock::new(BTreeMap::new());

		// We do not add the event_id field to the pdu here because of signature and
		// hashes checks
		let (event_id, value) = match gen_event_id_canonical_json(&response.event, &room_version_id) {
			Ok(t) => t,
			Err(_) => {
				// Event could not be converted to canonical json
				return Err(Error::BadRequest(
					ErrorKind::InvalidParam,
					"Could not convert event to canonical json.",
				));
			},
		};

		if *pdu.event_id != *event_id {
			warn!(
				"Server {} changed invite event, that's not allowed in the spec: ours: {:?}, theirs: {:?}",
				user_id.server_name(),
				pdu_json,
				value
			);
		}

		let origin: OwnedServerName = serde_json::from_value(
			serde_json::to_value(
				value
					.get("origin")
					.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event needs an origin field."))?,
			)
			.expect("CanonicalJson is valid json value"),
		)
		.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Origin field is invalid."))?;

		services().rooms.event_handler.fetch_required_signing_keys([&value], &pub_key_map).await?;

		let pdu_id: Vec<u8> = services()
			.rooms
			.event_handler
			.handle_incoming_pdu(&origin, &event_id, room_id, value, true, &pub_key_map)
			.await?
			.ok_or(Error::BadRequest(
				ErrorKind::InvalidParam,
				"Could not accept incoming PDU as timeline event.",
			))?;

		// Bind to variable because of lifetimes
		let servers = services()
			.rooms
			.state_cache
			.room_servers(room_id)
			.filter(|server| &**server != services().globals.server_name());

		services().sending.send_pdu(servers, &pdu_id)?;

		return Ok(());
	}

	if !services().rooms.state_cache.is_joined(sender_user, room_id)? {
		return Err(Error::BadRequest(
			ErrorKind::Forbidden,
			"You don't have permission to view this room.",
		));
	}

	let mutex_state =
		Arc::clone(services().globals.roomid_mutex_state.write().await.entry(room_id.to_owned()).or_default());
	let state_lock = mutex_state.lock().await;

	services()
		.rooms
		.timeline
		.build_and_append_pdu(
			PduBuilder {
				event_type: TimelineEventType::RoomMember,
				content: to_raw_value(&RoomMemberEventContent {
					membership: MembershipState::Invite,
					displayname: services().users.displayname(user_id)?,
					avatar_url: services().users.avatar_url(user_id)?,
					is_direct: Some(is_direct),
					third_party_invite: None,
					blurhash: services().users.blurhash(user_id)?,
					reason,
					join_authorized_via_users_server: None,
				})
				.expect("event is valid, we just created it"),
				unsigned: None,
				state_key: Some(user_id.to_string()),
				redacts: None,
			},
			sender_user,
			room_id,
			&state_lock,
		)
		.await?;

	drop(state_lock);

	Ok(())
// Make a user leave all their joined rooms
pub async fn leave_all_rooms(user_id: &UserId) -> Result<()> {
	let all_rooms = services()
		.rooms
		.state_cache
		.rooms_joined(user_id)
		.chain(services().rooms.state_cache.rooms_invited(user_id).map(|t| t.map(|(r, _)| r)))
		.collect::<Vec<_>>();

	for room_id in all_rooms {
		let room_id = match room_id {
			Ok(room_id) => room_id,
			Err(_) => continue,
		};

		let _ = leave_room(user_id, &room_id, None).await;
	}

	Ok(())
pub async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> {
	// Ask a remote server if we don't have this room
	if !services().rooms.metadata.exists(room_id)? && room_id.server_name() != Some(services().globals.server_name()) {
		if let Err(e) = remote_leave_room(user_id, room_id).await {
			warn!("Failed to leave room {} remotely: {}", user_id, e);
			// Don't tell the client about this error
		}

		let last_state = services()
			.rooms
			.state_cache
			.invite_state(user_id, room_id)?
			.map_or_else(|| services().rooms.state_cache.left_state(user_id, room_id), |s| Ok(Some(s)))?;

		// We always drop the invite, we can't rely on other servers
		services()
			.rooms
			.state_cache
			.update_membership(
				room_id,
				user_id,
				RoomMemberEventContent::new(MembershipState::Leave),
				user_id,
				last_state,
				true,
			)
			.await?;
	} else {
		let mutex_state =
			Arc::clone(services().globals.roomid_mutex_state.write().await.entry(room_id.to_owned()).or_default());
		let state_lock = mutex_state.lock().await;

		let member_event =
			services().rooms.state_accessor.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?;

		// Fix for broken rooms
		let member_event = match member_event {
			None => {
				error!("Trying to leave a room you are not a member of.");

				services()
					.rooms
					.state_cache
					.update_membership(
						room_id,
						user_id,
						RoomMemberEventContent::new(MembershipState::Leave),
						user_id,
						None,
						true,
					)
					.await?;
				return Ok(());
			},
			Some(e) => e,
		};

		let mut event: RoomMemberEventContent = serde_json::from_str(member_event.content.get()).map_err(|e| {
			error!("Invalid room member event in database: {}", e);
			Error::bad_database("Invalid member event in database.")
		})?;

		event.membership = MembershipState::Leave;
		event.reason = reason;

		services()
			.rooms
			.timeline
			.build_and_append_pdu(
				PduBuilder {
					event_type: TimelineEventType::RoomMember,
					content: to_raw_value(&event).expect("event is valid, we just created it"),
					unsigned: None,
					state_key: Some(user_id.to_string()),
					redacts: None,
				},
				user_id,
				room_id,
				&state_lock,
			)
			.await?;
	}

	Ok(())
Timo Kösters's avatar
Timo Kösters committed
async fn remote_leave_room(user_id: &UserId, room_id: &RoomId) -> Result<()> {
	let mut make_leave_response_and_server = Err(Error::BadServerResponse("No server available to assist in leaving."));

	let invite_state = services()
		.rooms
		.state_cache
		.invite_state(user_id, room_id)?
		.ok_or(Error::BadRequest(ErrorKind::BadState, "User is not invited."))?;

	let servers: HashSet<_> = invite_state
		.iter()
		.filter_map(|event| serde_json::from_str(event.json().get()).ok())
		.filter_map(|event: serde_json::Value| event.get("sender").cloned())
		.filter_map(|sender| sender.as_str().map(ToOwned::to_owned))
		.filter_map(|sender| UserId::parse(sender).ok())
		.map(|user| user.server_name().to_owned())
		.collect();

	for remote_server in servers {
		let make_leave_response = services()
			.sending
			.send_federation_request(
				&remote_server,
				federation::membership::prepare_leave_event::v1::Request {
					room_id: room_id.to_owned(),
					user_id: user_id.to_owned(),
				},
			)
			.await;

		make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server));

		if make_leave_response_and_server.is_ok() {
			break;
		}
	}

	let (make_leave_response, remote_server) = make_leave_response_and_server?;

	let room_version_id = match make_leave_response.room_version {
		Some(version) if services().globals.supported_room_versions().contains(&version) => version,
		_ => return Err(Error::BadServerResponse("Room version is not supported")),
	};

	let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(make_leave_response.event.get())
		.map_err(|_| Error::BadServerResponse("Invalid make_leave event json received from server."))?;

	// TODO: Is origin needed?
	leave_event_stub.insert(
		"origin".to_owned(),
		CanonicalJsonValue::String(services().globals.server_name().as_str().to_owned()),
	);
	leave_event_stub.insert(
		"origin_server_ts".to_owned(),
		CanonicalJsonValue::Integer(
			utils::millis_since_unix_epoch().try_into().expect("Timestamp is valid js_int value"),
		),
	);
	// We don't leave the event id in the pdu because that's only allowed in v1 or
	// v2 rooms
	leave_event_stub.remove("event_id");

	// In order to create a compatible ref hash (EventID) the `hashes` field needs
	// to be present
	ruma::signatures::hash_and_sign_event(
		services().globals.server_name().as_str(),
		services().globals.keypair(),
		&mut leave_event_stub,
		&room_version_id,
	)
	.expect("event is valid, we just created it");

	// Generate event id
	let event_id = EventId::parse(format!(
		"${}",
		ruma::signatures::reference_hash(&leave_event_stub, &room_version_id)
			.expect("ruma can calculate reference hashes")
	))
	.expect("ruma's reference hashes are valid event ids");

	// Add event_id back
	leave_event_stub.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned()));

	// It has enough fields to be called a proper event now
	let leave_event = leave_event_stub;

	services()
		.sending
		.send_federation_request(
			&remote_server,
			federation::membership::create_leave_event::v2::Request {
				room_id: room_id.to_owned(),
				event_id,
				pdu: PduEvent::convert_to_outgoing_federation_event(leave_event.clone()),
			},
		)
		.await?;

	Ok(())