Refactor join, alias services
Split knock, user register from api into services Fix autojoin not working with v12 rooms Fix 'm.login.registration_token/validity' for reloaded registration tokens Change join servers order Move autojoin for ldap
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use std::{collections::BTreeMap, fmt::Write as _};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
@@ -13,10 +13,9 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, debug, debug_warn, error, info, is_equal_to,
|
Err, Result, debug_warn, info,
|
||||||
matrix::{Event, pdu::PduBuilder},
|
matrix::{Event, pdu::PduBuilder},
|
||||||
utils::{self, ReadyExt},
|
utils::{self, ReadyExt},
|
||||||
warn,
|
|
||||||
};
|
};
|
||||||
use tuwunel_service::Services;
|
use tuwunel_service::Services;
|
||||||
|
|
||||||
@@ -62,148 +61,11 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
|||||||
|
|
||||||
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||||
|
|
||||||
// Create user
|
|
||||||
self.services
|
self.services
|
||||||
.users
|
.users
|
||||||
.create(&user_id, Some(password.as_str()), None)
|
.full_register(&user_id, Some(&password), None, None, false, true)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Default to pretty displayname
|
|
||||||
let mut displayname = user_id.localpart().to_owned();
|
|
||||||
|
|
||||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
|
||||||
// content is set to the user's display name with a space before it
|
|
||||||
if !self
|
|
||||||
.services
|
|
||||||
.server
|
|
||||||
.config
|
|
||||||
.new_user_displayname_suffix
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
write!(
|
|
||||||
displayname,
|
|
||||||
" {}",
|
|
||||||
self.services
|
|
||||||
.server
|
|
||||||
.config
|
|
||||||
.new_user_displayname_suffix
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.services
|
|
||||||
.users
|
|
||||||
.set_displayname(&user_id, Some(displayname));
|
|
||||||
|
|
||||||
// Initial account data
|
|
||||||
self.services
|
|
||||||
.account_data
|
|
||||||
.update(
|
|
||||||
None,
|
|
||||||
&user_id,
|
|
||||||
ruma::events::GlobalAccountDataEventType::PushRules
|
|
||||||
.to_string()
|
|
||||||
.into(),
|
|
||||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
|
||||||
content: ruma::events::push_rules::PushRulesEventContent {
|
|
||||||
global: ruma::push::Ruleset::server_default(&user_id),
|
|
||||||
},
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !self
|
|
||||||
.services
|
|
||||||
.server
|
|
||||||
.config
|
|
||||||
.auto_join_rooms
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
for room in &self.services.server.config.auto_join_rooms {
|
|
||||||
let Ok(room_id) = self.services.alias.maybe_resolve(room).await else {
|
|
||||||
error!(
|
|
||||||
%user_id,
|
|
||||||
"Failed to resolve room alias to room ID when attempting to auto join {room}, skipping"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self
|
|
||||||
.services
|
|
||||||
.state_cache
|
|
||||||
.server_in_room(self.services.globals.server_name(), &room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Skipping room {room} to automatically join as we have never joined before."
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let state_lock = self.services.state.mutex.lock(&room_id).await;
|
|
||||||
|
|
||||||
if let Some(room_server_name) = room.server_name() {
|
|
||||||
match self
|
|
||||||
.services
|
|
||||||
.membership
|
|
||||||
.join(
|
|
||||||
&user_id,
|
|
||||||
&room_id,
|
|
||||||
Some("Automatically joining this room upon registration".to_owned()),
|
|
||||||
&[
|
|
||||||
self.services.globals.server_name().to_owned(),
|
|
||||||
room_server_name.to_owned(),
|
|
||||||
],
|
|
||||||
&None,
|
|
||||||
&state_lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok(_response) => {
|
|
||||||
info!("Automatically joined room {room} for user {user_id}");
|
|
||||||
},
|
|
||||||
| Err(e) => {
|
|
||||||
// don't return this error so we don't fail registrations
|
|
||||||
error!(
|
|
||||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
|
||||||
);
|
|
||||||
self.services
|
|
||||||
.admin
|
|
||||||
.send_text(&format!(
|
|
||||||
"Failed to automatically join room {room} for user {user_id}: \
|
|
||||||
{e}"
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(state_lock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we dont add a device since we're not the user, just the creator
|
|
||||||
|
|
||||||
// if this account creation is from the CLI / --execute, invite the first user
|
|
||||||
// to admin room
|
|
||||||
if let Ok(admin_room) = self.services.admin.get_admin_room().await {
|
|
||||||
if self
|
|
||||||
.services
|
|
||||||
.state_cache
|
|
||||||
.room_joined_count(&admin_room)
|
|
||||||
.await
|
|
||||||
.is_ok_and(is_equal_to!(1))
|
|
||||||
{
|
|
||||||
self.services
|
|
||||||
.admin
|
|
||||||
.make_user_admin(&user_id)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
warn!("Granting {user_id} admin privileges as the first user");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!("create_user admin command called without an admin room being available");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
|
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -403,7 +265,7 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
|
|||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn force_join_list_of_local_users(
|
pub(super) async fn force_join_list_of_local_users(
|
||||||
&self,
|
&self,
|
||||||
room_id: OwnedRoomOrAliasId,
|
room: OwnedRoomOrAliasId,
|
||||||
yes_i_want_to_do_this: bool,
|
yes_i_want_to_do_this: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
if self.body.len() < 2
|
if self.body.len() < 2
|
||||||
@@ -415,7 +277,7 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
|
|
||||||
if !yes_i_want_to_do_this {
|
if !yes_i_want_to_do_this {
|
||||||
return Err!(
|
return Err!(
|
||||||
"You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force \
|
"You must pass the --yes-i-want-to-do-this flag to ensure you really want to force \
|
||||||
bulk join all specified local users.",
|
bulk join all specified local users.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -427,7 +289,7 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
let (room_id, servers) = self
|
let (room_id, servers) = self
|
||||||
.services
|
.services
|
||||||
.alias
|
.alias
|
||||||
.maybe_resolve_with_servers(&room_id, None)
|
.maybe_resolve_with_servers(&room, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
@@ -505,9 +367,10 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
.join(
|
.join(
|
||||||
&user_id,
|
&user_id,
|
||||||
&room_id,
|
&room_id,
|
||||||
|
Some(&room),
|
||||||
Some(String::from(BULK_JOIN_REASON)),
|
Some(String::from(BULK_JOIN_REASON)),
|
||||||
&servers,
|
&servers,
|
||||||
&None,
|
false,
|
||||||
&state_lock,
|
&state_lock,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -534,7 +397,7 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn force_join_all_local_users(
|
pub(super) async fn force_join_all_local_users(
|
||||||
&self,
|
&self,
|
||||||
room_id: OwnedRoomOrAliasId,
|
room: OwnedRoomOrAliasId,
|
||||||
yes_i_want_to_do_this: bool,
|
yes_i_want_to_do_this: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
if !yes_i_want_to_do_this {
|
if !yes_i_want_to_do_this {
|
||||||
@@ -551,7 +414,7 @@ pub(super) async fn force_join_all_local_users(
|
|||||||
let (room_id, servers) = self
|
let (room_id, servers) = self
|
||||||
.services
|
.services
|
||||||
.alias
|
.alias
|
||||||
.maybe_resolve_with_servers(&room_id, None)
|
.maybe_resolve_with_servers(&room, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
@@ -600,9 +463,10 @@ pub(super) async fn force_join_all_local_users(
|
|||||||
.join(
|
.join(
|
||||||
user_id,
|
user_id,
|
||||||
&room_id,
|
&room_id,
|
||||||
|
Some(&room),
|
||||||
Some(String::from(BULK_JOIN_REASON)),
|
Some(String::from(BULK_JOIN_REASON)),
|
||||||
&servers,
|
&servers,
|
||||||
&None,
|
false,
|
||||||
&state_lock,
|
&state_lock,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -627,16 +491,12 @@ pub(super) async fn force_join_all_local_users(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn force_join_room(
|
pub(super) async fn force_join_room(&self, user_id: String, room: OwnedRoomOrAliasId) -> Result {
|
||||||
&self,
|
|
||||||
user_id: String,
|
|
||||||
room_id: OwnedRoomOrAliasId,
|
|
||||||
) -> Result {
|
|
||||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
let (room_id, servers) = self
|
let (room_id, servers) = self
|
||||||
.services
|
.services
|
||||||
.alias
|
.alias
|
||||||
.maybe_resolve_with_servers(&room_id, None)
|
.maybe_resolve_with_servers(&room, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -648,7 +508,7 @@ pub(super) async fn force_join_room(
|
|||||||
|
|
||||||
self.services
|
self.services
|
||||||
.membership
|
.membership
|
||||||
.join(&user_id, &room_id, None, &servers, &None, &state_lock)
|
.join(&user_id, &room_id, Some(&room), None, &servers, false, &state_lock)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub(super) enum UserCommand {
|
|||||||
/// - Manually join a local user to a room.
|
/// - Manually join a local user to a room.
|
||||||
ForceJoinRoom {
|
ForceJoinRoom {
|
||||||
user_id: String,
|
user_id: String,
|
||||||
room_id: OwnedRoomOrAliasId,
|
room: OwnedRoomOrAliasId,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// - Manually leave a local user from a room.
|
/// - Manually leave a local user from a room.
|
||||||
@@ -148,7 +148,7 @@ pub(super) enum UserCommand {
|
|||||||
///
|
///
|
||||||
/// Requires the `--yes-i-want-to-do-this` flag.
|
/// Requires the `--yes-i-want-to-do-this` flag.
|
||||||
ForceJoinListOfLocalUsers {
|
ForceJoinListOfLocalUsers {
|
||||||
room_id: OwnedRoomOrAliasId,
|
room: OwnedRoomOrAliasId,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
yes_i_want_to_do_this: bool,
|
yes_i_want_to_do_this: bool,
|
||||||
@@ -160,7 +160,7 @@ pub(super) enum UserCommand {
|
|||||||
///
|
///
|
||||||
/// Requires the `--yes-i-want-to-do-this` flag.
|
/// Requires the `--yes-i-want-to-do-this` flag.
|
||||||
ForceJoinAllLocalUsers {
|
ForceJoinAllLocalUsers {
|
||||||
room_id: OwnedRoomOrAliasId,
|
room: OwnedRoomOrAliasId,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
yes_i_want_to_do_this: bool,
|
yes_i_want_to_do_this: bool,
|
||||||
|
|||||||
@@ -121,26 +121,16 @@ async fn room_available_servers(
|
|||||||
|
|
||||||
// insert our server as the very first choice if in list, else check if we can
|
// insert our server as the very first choice if in list, else check if we can
|
||||||
// prefer the room alias server first
|
// prefer the room alias server first
|
||||||
match servers
|
if let Some(server_index) = servers
|
||||||
.iter()
|
.iter()
|
||||||
.position(|server_name| services.globals.server_is_ours(server_name))
|
.position(|server_name| services.globals.server_is_ours(server_name))
|
||||||
{
|
{
|
||||||
| Some(server_index) => {
|
servers.swap(0, server_index);
|
||||||
servers.swap_remove(server_index);
|
} else if let Some(alias_server_index) = servers
|
||||||
servers.insert(0, services.globals.server_name().to_owned());
|
.iter()
|
||||||
},
|
.position(|server| server == room_alias.server_name())
|
||||||
| _ => {
|
{
|
||||||
match servers
|
servers.swap(0, alias_server_index);
|
||||||
.iter()
|
|
||||||
.position(|server| server == room_alias.server_name())
|
|
||||||
{
|
|
||||||
| Some(alias_server_index) => {
|
|
||||||
servers.swap_remove(alias_server_index);
|
|
||||||
servers.insert(0, room_alias.server_name().into());
|
|
||||||
},
|
|
||||||
| _ => {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
servers
|
servers
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ use axum::extract::State;
|
|||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
|
MilliSecondsSinceUnixEpoch,
|
||||||
api::client::device::{
|
api::client::device::{
|
||||||
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{Err, Result, debug, err, utils, utils::string::to_small_string};
|
use tuwunel_core::{Err, Result, debug, err, utils::string::to_small_string};
|
||||||
|
|
||||||
use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa};
|
use crate::{Ruma, router::auth_uiaa};
|
||||||
|
|
||||||
/// # `GET /_matrix/client/r0/devices`
|
/// # `GET /_matrix/client/r0/devices`
|
||||||
///
|
///
|
||||||
@@ -94,13 +94,11 @@ pub(crate) async fn update_device_route(
|
|||||||
appservice.registration.id
|
appservice.registration.id
|
||||||
);
|
);
|
||||||
|
|
||||||
let device_id = OwnedDeviceId::from(utils::random_string(DEVICE_ID_LENGTH));
|
|
||||||
|
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
.create_device(
|
.create_device(
|
||||||
sender_user,
|
sender_user,
|
||||||
&device_id,
|
None,
|
||||||
(Some(&appservice.registration.as_token), None),
|
(Some(&appservice.registration.as_token), None),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ pub(crate) async fn invite_user_route(
|
|||||||
|
|
||||||
invite_check(&services, sender_user, room_id).await?;
|
invite_check(&services, sender_user, room_id).await?;
|
||||||
|
|
||||||
banned_room_check(&services, sender_user, Some(room_id), room_id.server_name(), client)
|
banned_room_check(&services, sender_user, room_id, None, client).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let invite_user::v3::InvitationRecipient::UserId { user_id } = &body.recipient else {
|
let invite_user::v3::InvitationRecipient::UserId { user_id } = &body.recipient else {
|
||||||
return Err!(Request(ThreepidDenied("Third party identifiers are not implemented")));
|
return Err!(Request(ThreepidDenied("Third party identifiers are not implemented")));
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ use axum::extract::State;
|
|||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
RoomId, RoomOrAliasId,
|
RoomId,
|
||||||
api::client::membership::{join_room_by_id, join_room_by_id_or_alias},
|
api::client::membership::{join_room_by_id, join_room_by_id_or_alias},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{Result, warn};
|
use tuwunel_core::{Result, warn};
|
||||||
|
|
||||||
use super::banned_room_check;
|
use super::banned_room_check;
|
||||||
use crate::{Ruma, client::membership::get_join_params};
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
|
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
|
||||||
///
|
///
|
||||||
@@ -28,23 +28,20 @@ pub(crate) async fn join_room_by_id_route(
|
|||||||
|
|
||||||
let room_id: &RoomId = &body.room_id;
|
let room_id: &RoomId = &body.room_id;
|
||||||
|
|
||||||
banned_room_check(&services, sender_user, Some(room_id), room_id.server_name(), client)
|
banned_room_check(&services, sender_user, room_id, None, client).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let (room_id, servers) =
|
let state_lock = services.state.mutex.lock(room_id).await;
|
||||||
get_join_params(&services, sender_user, <&RoomOrAliasId>::from(room_id), &[]).await?;
|
|
||||||
|
|
||||||
let state_lock = services.state.mutex.lock(&room_id).await;
|
|
||||||
|
|
||||||
let mut errors = 0_usize;
|
let mut errors = 0_usize;
|
||||||
while let Err(e) = services
|
while let Err(e) = services
|
||||||
.membership
|
.membership
|
||||||
.join(
|
.join(
|
||||||
sender_user,
|
sender_user,
|
||||||
&room_id,
|
room_id,
|
||||||
|
None,
|
||||||
body.reason.clone(),
|
body.reason.clone(),
|
||||||
&servers,
|
&[],
|
||||||
&body.appservice_info,
|
body.appservice_info.is_some(),
|
||||||
&state_lock,
|
&state_lock,
|
||||||
)
|
)
|
||||||
.boxed()
|
.boxed()
|
||||||
@@ -62,7 +59,7 @@ pub(crate) async fn join_room_by_id_route(
|
|||||||
|
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
|
|
||||||
Ok(join_room_by_id::v3::Response { room_id })
|
Ok(join_room_by_id::v3::Response { room_id: room_id.to_owned() })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # `POST /_matrix/client/r0/join/{roomIdOrAlias}`
|
/// # `POST /_matrix/client/r0/join/{roomIdOrAlias}`
|
||||||
@@ -83,10 +80,12 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
|||||||
let sender_user = body.sender_user();
|
let sender_user = body.sender_user();
|
||||||
let appservice_info = &body.appservice_info;
|
let appservice_info = &body.appservice_info;
|
||||||
|
|
||||||
let (room_id, servers) =
|
let (room_id, servers) = services
|
||||||
get_join_params(&services, sender_user, &body.room_id_or_alias, &body.via).await?;
|
.alias
|
||||||
|
.maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via))
|
||||||
|
.await?;
|
||||||
|
|
||||||
banned_room_check(&services, sender_user, Some(&room_id), room_id.server_name(), client)
|
banned_room_check(&services, sender_user, &room_id, Some(&body.room_id_or_alias), client)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let state_lock = services.state.mutex.lock(&room_id).await;
|
let state_lock = services.state.mutex.lock(&room_id).await;
|
||||||
@@ -97,9 +96,10 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
|||||||
.join(
|
.join(
|
||||||
sender_user,
|
sender_user,
|
||||||
&room_id,
|
&room_id,
|
||||||
|
Some(&body.room_id_or_alias),
|
||||||
body.reason.clone(),
|
body.reason.clone(),
|
||||||
&servers,
|
&servers,
|
||||||
appservice_info,
|
appservice_info.is_some(),
|
||||||
&state_lock,
|
&state_lock,
|
||||||
)
|
)
|
||||||
.boxed()
|
.boxed()
|
||||||
@@ -117,5 +117,5 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
|||||||
|
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
|
|
||||||
Ok(join_room_by_id_or_alias::v3::Response { room_id })
|
Ok(join_room_by_id_or_alias::v3::Response { room_id: room_id.clone() })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,10 @@
|
|||||||
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
|
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use futures::{FutureExt, StreamExt};
|
use ruma::api::client::knock::knock_room;
|
||||||
use ruma::{
|
use tuwunel_core::Result;
|
||||||
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, RoomId,
|
|
||||||
RoomVersionId, UserId,
|
|
||||||
api::{
|
|
||||||
client::knock::knock_room,
|
|
||||||
federation::{
|
|
||||||
membership::RawStrippedState,
|
|
||||||
{self},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
canonical_json::to_canonical_value,
|
|
||||||
events::{
|
|
||||||
StateEventType,
|
|
||||||
room::member::{MembershipState, RoomMemberEventContent},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use tuwunel_core::{
|
|
||||||
Err, Result, debug, debug_info, debug_warn, err, extract_variant, info,
|
|
||||||
matrix::{
|
|
||||||
PduCount,
|
|
||||||
event::{Event, gen_event_id},
|
|
||||||
pdu::{PduBuilder, PduEvent},
|
|
||||||
},
|
|
||||||
trace,
|
|
||||||
utils::{self},
|
|
||||||
warn,
|
|
||||||
};
|
|
||||||
use tuwunel_service::{
|
|
||||||
Services,
|
|
||||||
rooms::{
|
|
||||||
state::RoomMutexGuard,
|
|
||||||
state_compressor::{CompressedState, HashSetCompressStateEvent},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::banned_room_check;
|
use super::banned_room_check;
|
||||||
use crate::{Ruma, client::membership::get_join_params};
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
|
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
|
||||||
///
|
///
|
||||||
@@ -51,542 +16,30 @@ pub(crate) async fn knock_room_route(
|
|||||||
body: Ruma<knock_room::v3::Request>,
|
body: Ruma<knock_room::v3::Request>,
|
||||||
) -> Result<knock_room::v3::Response> {
|
) -> Result<knock_room::v3::Response> {
|
||||||
let sender_user = body.sender_user();
|
let sender_user = body.sender_user();
|
||||||
let body = &body.body;
|
|
||||||
|
|
||||||
let (room_id, servers) =
|
let (room_id, servers) = services
|
||||||
get_join_params(&services, sender_user, &body.room_id_or_alias, &body.via).await?;
|
.alias
|
||||||
|
.maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via))
|
||||||
banned_room_check(&services, sender_user, Some(&room_id), room_id.server_name(), client)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
knock_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers).await
|
banned_room_check(&services, sender_user, &room_id, Some(&body.room_id_or_alias), client)
|
||||||
}
|
|
||||||
|
|
||||||
async fn knock_room_by_id_helper(
|
|
||||||
services: &Services,
|
|
||||||
sender_user: &UserId,
|
|
||||||
room_id: &RoomId,
|
|
||||||
reason: Option<String>,
|
|
||||||
servers: &[OwnedServerName],
|
|
||||||
) -> Result<knock_room::v3::Response> {
|
|
||||||
let state_lock = services.state.mutex.lock(room_id).await;
|
|
||||||
|
|
||||||
if services
|
|
||||||
.state_cache
|
|
||||||
.is_invited(sender_user, room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
debug_warn!("{sender_user} is already invited in {room_id} but attempted to knock");
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"You cannot knock on a room you are already invited/accepted to."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if services
|
|
||||||
.state_cache
|
|
||||||
.is_joined(sender_user, room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
debug_warn!("{sender_user} is already joined in {room_id} but attempted to knock");
|
|
||||||
return Err!(Request(Forbidden("You cannot knock on a room you are already joined in.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if services
|
|
||||||
.state_cache
|
|
||||||
.is_knocked(sender_user, room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
debug_warn!("{sender_user} is already knocked in {room_id}");
|
|
||||||
return Ok(knock_room::v3::Response { room_id: room_id.into() });
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(membership) = services
|
|
||||||
.state_accessor
|
|
||||||
.get_member(room_id, sender_user)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if membership.membership == MembershipState::Ban {
|
|
||||||
debug_warn!("{sender_user} is banned from {room_id} but attempted to knock");
|
|
||||||
return Err!(Request(Forbidden("You cannot knock on a room you are banned from.")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let server_in_room = services
|
|
||||||
.state_cache
|
|
||||||
.server_in_room(services.globals.server_name(), room_id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let local_knock = server_in_room
|
|
||||||
|| servers.is_empty()
|
|
||||||
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
|
|
||||||
|
|
||||||
if local_knock {
|
|
||||||
knock_room_helper_local(services, sender_user, room_id, reason, servers, state_lock)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
knock_room_helper_remote(services, sender_user, room_id, reason, servers, state_lock)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(knock_room::v3::Response::new(room_id.to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn knock_room_helper_local(
|
|
||||||
services: &Services,
|
|
||||||
sender_user: &UserId,
|
|
||||||
room_id: &RoomId,
|
|
||||||
reason: Option<String>,
|
|
||||||
servers: &[OwnedServerName],
|
|
||||||
state_lock: RoomMutexGuard,
|
|
||||||
) -> Result {
|
|
||||||
debug_info!("We can knock locally");
|
|
||||||
|
|
||||||
let room_version_id = services.state.get_room_version(room_id).await?;
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
room_version_id,
|
|
||||||
RoomVersionId::V1
|
|
||||||
| RoomVersionId::V2
|
|
||||||
| RoomVersionId::V3
|
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V5
|
|
||||||
| RoomVersionId::V6
|
|
||||||
) {
|
|
||||||
return Err!(Request(Forbidden("This room does not support knocking.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = RoomMemberEventContent {
|
|
||||||
displayname: services.users.displayname(sender_user).await.ok(),
|
|
||||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
|
||||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
|
||||||
reason: reason.clone(),
|
|
||||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try normal knock first
|
|
||||||
let Err(error) = services
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
PduBuilder::state(sender_user.to_string(), &content),
|
|
||||||
sender_user,
|
|
||||||
room_id,
|
|
||||||
&state_lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if servers.is_empty() || (servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
|
|
||||||
{
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock");
|
|
||||||
|
|
||||||
let (make_knock_response, remote_server) =
|
|
||||||
make_knock_request(services, sender_user, room_id, servers).await?;
|
|
||||||
|
|
||||||
info!("make_knock finished");
|
|
||||||
|
|
||||||
let room_version_id = make_knock_response.room_version;
|
|
||||||
|
|
||||||
if !services
|
|
||||||
.server
|
|
||||||
.supported_room_version(&room_version_id)
|
|
||||||
{
|
|
||||||
return Err!(BadServerResponse(
|
|
||||||
"Remote room version {room_version_id} is not supported by tuwunel"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut knock_event_stub = serde_json::from_str::<CanonicalJsonObject>(
|
|
||||||
make_knock_response.event.get(),
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
knock_event_stub.insert(
|
|
||||||
"origin".into(),
|
|
||||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
|
||||||
);
|
|
||||||
knock_event_stub.insert(
|
|
||||||
"origin_server_ts".into(),
|
|
||||||
CanonicalJsonValue::Integer(
|
|
||||||
utils::millis_since_unix_epoch()
|
|
||||||
.try_into()
|
|
||||||
.expect("Timestamp is valid js_int value"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
knock_event_stub.insert(
|
|
||||||
"content".into(),
|
|
||||||
to_canonical_value(RoomMemberEventContent {
|
|
||||||
displayname: services.users.displayname(sender_user).await.ok(),
|
|
||||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
|
||||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
|
||||||
reason,
|
|
||||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
|
||||||
})
|
|
||||||
.expect("event is valid, we just created it"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
|
||||||
// to be present
|
|
||||||
services
|
|
||||||
.server_keys
|
|
||||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
|
||||||
|
|
||||||
// Generate event id
|
|
||||||
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
|
||||||
|
|
||||||
// Add event_id
|
|
||||||
knock_event_stub
|
|
||||||
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
|
|
||||||
|
|
||||||
// It has enough fields to be called a proper event now
|
|
||||||
let knock_event = knock_event_stub;
|
|
||||||
|
|
||||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
|
||||||
let send_knock_request = federation::membership::create_knock_event::v1::Request {
|
|
||||||
room_id: room_id.to_owned(),
|
|
||||||
event_id: event_id.clone(),
|
|
||||||
pdu: services
|
|
||||||
.federation
|
|
||||||
.format_pdu_into(knock_event.clone(), Some(&room_version_id))
|
|
||||||
.await,
|
|
||||||
};
|
|
||||||
|
|
||||||
let send_knock_response = services
|
|
||||||
.federation
|
|
||||||
.execute(&remote_server, send_knock_request)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
info!("send_knock finished");
|
let state_lock = services.state.mutex.lock(&room_id).await;
|
||||||
|
|
||||||
services
|
services
|
||||||
.short
|
.membership
|
||||||
.get_or_create_shortroomid(room_id)
|
.knock(
|
||||||
.await;
|
|
||||||
|
|
||||||
info!("Parsing knock event");
|
|
||||||
|
|
||||||
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
|
|
||||||
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
|
|
||||||
|
|
||||||
info!("Updating membership locally to knock state with provided stripped state events");
|
|
||||||
let count = services.globals.next_count();
|
|
||||||
services
|
|
||||||
.state_cache
|
|
||||||
.update_membership(
|
|
||||||
room_id,
|
|
||||||
sender_user,
|
sender_user,
|
||||||
parsed_knock_pdu
|
&room_id,
|
||||||
.get_content::<RoomMemberEventContent>()
|
Some(&body.room_id_or_alias),
|
||||||
.expect("we just created this"),
|
body.reason.clone(),
|
||||||
sender_user,
|
&servers,
|
||||||
Some(
|
|
||||||
send_knock_response
|
|
||||||
.knock_room_state
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
PduCount::Normal(*count),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("Appending room knock event locally");
|
|
||||||
services
|
|
||||||
.timeline
|
|
||||||
.append_pdu(
|
|
||||||
&parsed_knock_pdu,
|
|
||||||
knock_event,
|
|
||||||
once(parsed_knock_pdu.event_id.borrow()),
|
|
||||||
&state_lock,
|
&state_lock,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
drop(state_lock);
|
||||||
}
|
|
||||||
|
Ok(knock_room::v3::Response::new(room_id.clone()))
|
||||||
async fn knock_room_helper_remote(
|
|
||||||
services: &Services,
|
|
||||||
sender_user: &UserId,
|
|
||||||
room_id: &RoomId,
|
|
||||||
reason: Option<String>,
|
|
||||||
servers: &[OwnedServerName],
|
|
||||||
state_lock: RoomMutexGuard,
|
|
||||||
) -> Result {
|
|
||||||
info!("Knocking {room_id} over federation.");
|
|
||||||
|
|
||||||
let (make_knock_response, remote_server) =
|
|
||||||
make_knock_request(services, sender_user, room_id, servers).await?;
|
|
||||||
|
|
||||||
info!("make_knock finished");
|
|
||||||
|
|
||||||
let room_version_id = make_knock_response.room_version;
|
|
||||||
|
|
||||||
if !services
|
|
||||||
.server
|
|
||||||
.supported_room_version(&room_version_id)
|
|
||||||
{
|
|
||||||
return Err!(BadServerResponse(
|
|
||||||
"Remote room version {room_version_id} is not supported by tuwunel"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut knock_event_stub: CanonicalJsonObject =
|
|
||||||
serde_json::from_str(make_knock_response.event.get()).map_err(|e| {
|
|
||||||
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
knock_event_stub.insert(
|
|
||||||
"origin".into(),
|
|
||||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
|
||||||
);
|
|
||||||
knock_event_stub.insert(
|
|
||||||
"origin_server_ts".into(),
|
|
||||||
CanonicalJsonValue::Integer(
|
|
||||||
utils::millis_since_unix_epoch()
|
|
||||||
.try_into()
|
|
||||||
.expect("Timestamp is valid js_int value"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
knock_event_stub.insert(
|
|
||||||
"content".into(),
|
|
||||||
to_canonical_value(RoomMemberEventContent {
|
|
||||||
displayname: services.users.displayname(sender_user).await.ok(),
|
|
||||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
|
||||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
|
||||||
reason,
|
|
||||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
|
||||||
})
|
|
||||||
.expect("event is valid, we just created it"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
|
||||||
// to be present
|
|
||||||
services
|
|
||||||
.server_keys
|
|
||||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
|
||||||
|
|
||||||
// Generate event id
|
|
||||||
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
|
||||||
|
|
||||||
// Add event_id
|
|
||||||
knock_event_stub
|
|
||||||
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
|
|
||||||
|
|
||||||
// It has enough fields to be called a proper event now
|
|
||||||
let knock_event = knock_event_stub;
|
|
||||||
|
|
||||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
|
||||||
let send_knock_request = federation::membership::create_knock_event::v1::Request {
|
|
||||||
room_id: room_id.to_owned(),
|
|
||||||
event_id: event_id.clone(),
|
|
||||||
pdu: services
|
|
||||||
.federation
|
|
||||||
.format_pdu_into(knock_event.clone(), Some(&room_version_id))
|
|
||||||
.await,
|
|
||||||
};
|
|
||||||
|
|
||||||
let send_knock_response = services
|
|
||||||
.federation
|
|
||||||
.execute(&remote_server, send_knock_request)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("send_knock finished");
|
|
||||||
|
|
||||||
services
|
|
||||||
.short
|
|
||||||
.get_or_create_shortroomid(room_id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
info!("Parsing knock event");
|
|
||||||
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
|
|
||||||
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
|
|
||||||
|
|
||||||
info!("Going through send_knock response knock state events");
|
|
||||||
let state = send_knock_response
|
|
||||||
.knock_room_state
|
|
||||||
.iter()
|
|
||||||
.map(|event| {
|
|
||||||
serde_json::from_str::<CanonicalJsonObject>(
|
|
||||||
extract_variant!(event.clone(), RawStrippedState::Stripped)
|
|
||||||
.expect("Raw<AnyStrippedStateEvent>")
|
|
||||||
.json()
|
|
||||||
.get(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.filter_map(Result::ok);
|
|
||||||
|
|
||||||
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
|
|
||||||
|
|
||||||
for event in state {
|
|
||||||
let Some(state_key) = event.get("state_key") else {
|
|
||||||
debug_warn!("send_knock stripped state event missing state_key: {event:?}");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(event_type) = event.get("type") else {
|
|
||||||
debug_warn!("send_knock stripped state event missing event type: {event:?}");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(state_key) = serde_json::from_value::<String>(state_key.clone().into()) else {
|
|
||||||
debug_warn!("send_knock stripped state event has invalid state_key: {event:?}");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok(event_type) = serde_json::from_value::<StateEventType>(event_type.clone().into())
|
|
||||||
else {
|
|
||||||
debug_warn!("send_knock stripped state event has invalid event type: {event:?}");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let event_id = gen_event_id(&event, &room_version_id)?;
|
|
||||||
let shortstatekey = services
|
|
||||||
.short
|
|
||||||
.get_or_create_shortstatekey(&event_type, &state_key)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
services
|
|
||||||
.timeline
|
|
||||||
.add_pdu_outlier(&event_id, &event);
|
|
||||||
|
|
||||||
state_map.insert(shortstatekey, event_id.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Compressing state from send_knock");
|
|
||||||
let compressed: CompressedState = services
|
|
||||||
.state_compressor
|
|
||||||
.compress_state_events(
|
|
||||||
state_map
|
|
||||||
.iter()
|
|
||||||
.map(|(ssk, eid)| (ssk, eid.borrow())),
|
|
||||||
)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
debug!("Saving compressed state");
|
|
||||||
let HashSetCompressStateEvent {
|
|
||||||
shortstatehash: statehash_before_knock,
|
|
||||||
added,
|
|
||||||
removed,
|
|
||||||
} = services
|
|
||||||
.state_compressor
|
|
||||||
.save_state(room_id, Arc::new(compressed))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("Forcing state for new room");
|
|
||||||
services
|
|
||||||
.state
|
|
||||||
.force_state(room_id, statehash_before_knock, added, removed, &state_lock)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let statehash_after_knock = services
|
|
||||||
.state
|
|
||||||
.append_to_state(&parsed_knock_pdu)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("Updating membership locally to knock state with provided stripped state events");
|
|
||||||
let count = services.globals.next_count();
|
|
||||||
services
|
|
||||||
.state_cache
|
|
||||||
.update_membership(
|
|
||||||
room_id,
|
|
||||||
sender_user,
|
|
||||||
parsed_knock_pdu
|
|
||||||
.get_content::<RoomMemberEventContent>()
|
|
||||||
.expect("we just created this"),
|
|
||||||
sender_user,
|
|
||||||
Some(
|
|
||||||
send_knock_response
|
|
||||||
.knock_room_state
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
PduCount::Normal(*count),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("Appending room knock event locally");
|
|
||||||
services
|
|
||||||
.timeline
|
|
||||||
.append_pdu(
|
|
||||||
&parsed_knock_pdu,
|
|
||||||
knock_event,
|
|
||||||
once(parsed_knock_pdu.event_id.borrow()),
|
|
||||||
&state_lock,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("Setting final room state for new room");
|
|
||||||
// We set the room state after inserting the pdu, so that we never have a moment
|
|
||||||
// in time where events in the current room state do not exist
|
|
||||||
services
|
|
||||||
.state
|
|
||||||
.set_room_state(room_id, statehash_after_knock, &state_lock);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn make_knock_request(
|
|
||||||
services: &Services,
|
|
||||||
sender_user: &UserId,
|
|
||||||
room_id: &RoomId,
|
|
||||||
servers: &[OwnedServerName],
|
|
||||||
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
|
|
||||||
let mut make_knock_response_and_server =
|
|
||||||
Err!(BadServerResponse("No server available to assist in knocking."));
|
|
||||||
|
|
||||||
let mut make_knock_counter: usize = 0;
|
|
||||||
|
|
||||||
for remote_server in servers {
|
|
||||||
if services.globals.server_is_ours(remote_server) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Asking {remote_server} for make_knock ({make_knock_counter})");
|
|
||||||
|
|
||||||
let make_knock_response = services
|
|
||||||
.federation
|
|
||||||
.execute(remote_server, federation::membership::prepare_knock_event::v1::Request {
|
|
||||||
room_id: room_id.to_owned(),
|
|
||||||
user_id: sender_user.to_owned(),
|
|
||||||
ver: services
|
|
||||||
.server
|
|
||||||
.supported_room_versions()
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
trace!("make_knock response: {make_knock_response:?}");
|
|
||||||
make_knock_counter = make_knock_counter.saturating_add(1);
|
|
||||||
|
|
||||||
make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone()));
|
|
||||||
|
|
||||||
if make_knock_response_and_server.is_ok() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if make_knock_counter > 40 {
|
|
||||||
warn!(
|
|
||||||
"50 servers failed to provide valid make_knock response, assuming no server can \
|
|
||||||
assist in knocking."
|
|
||||||
);
|
|
||||||
make_knock_response_and_server =
|
|
||||||
Err!(BadServerResponse("No server available to assist in knocking."));
|
|
||||||
|
|
||||||
return make_knock_response_and_server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
make_knock_response_and_server
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,12 @@ mod leave;
|
|||||||
mod members;
|
mod members;
|
||||||
mod unban;
|
mod unban;
|
||||||
|
|
||||||
use std::{cmp::Ordering, net::IpAddr};
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use ruma::{
|
use ruma::{RoomId, RoomOrAliasId, UserId, api::client::membership::joined_rooms};
|
||||||
OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName, UserId,
|
use tuwunel_core::{Err, Result, result::LogErr, warn};
|
||||||
api::client::membership::joined_rooms,
|
|
||||||
};
|
|
||||||
use tuwunel_core::{Err, Result, result::LogErr, utils::shuffle, warn};
|
|
||||||
use tuwunel_service::Services;
|
use tuwunel_service::Services;
|
||||||
|
|
||||||
pub(crate) use self::{
|
pub(crate) use self::{
|
||||||
@@ -58,57 +55,42 @@ pub(crate) async fn joined_rooms_route(
|
|||||||
pub(crate) async fn banned_room_check(
|
pub(crate) async fn banned_room_check(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
room_id: Option<&RoomId>,
|
room_id: &RoomId,
|
||||||
server_name: Option<&ServerName>,
|
orig_room_id: Option<&RoomOrAliasId>,
|
||||||
client_ip: IpAddr,
|
client_ip: IpAddr,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
if services.users.is_admin(user_id).await {
|
if services.users.is_admin(user_id).await {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: weird condition
|
// room id is banned ...
|
||||||
if let Some(room_id) = room_id {
|
if services.metadata.is_banned(room_id).await
|
||||||
if services.metadata.is_banned(room_id).await
|
// ... or legacy room id server is banned ...
|
||||||
|| (room_id.server_name().is_some()
|
|| room_id.server_name().is_some_and(|server_name| {
|
||||||
&& services
|
services
|
||||||
.config
|
.config
|
||||||
.forbidden_remote_server_names
|
.forbidden_remote_server_names
|
||||||
.is_match(
|
.is_match(server_name.host())
|
||||||
room_id
|
})
|
||||||
.server_name()
|
// ... or alias server is banned
|
||||||
.expect("legacy room mxid")
|
|| orig_room_id.is_some_and(|orig_room_id| {
|
||||||
.host(),
|
orig_room_id.server_name().is_some_and(|orig_server_name|
|
||||||
)) {
|
services
|
||||||
warn!(
|
.config
|
||||||
"User {user_id} who is not an admin attempted to send an invite for or \
|
.forbidden_remote_server_names
|
||||||
attempted to join a banned room or banned room server name: {room_id}"
|
.is_match(orig_server_name.host()))
|
||||||
);
|
}) {
|
||||||
|
warn!(
|
||||||
|
"User {user_id} who is not an admin attempted to send an invite for or attempted to \
|
||||||
|
join a banned room or banned room server name: {room_id}"
|
||||||
|
);
|
||||||
|
|
||||||
maybe_deactivate(services, user_id, client_ip)
|
maybe_deactivate(services, user_id, client_ip)
|
||||||
.await
|
.await
|
||||||
.log_err()
|
.log_err()
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
||||||
}
|
|
||||||
} else if let Some(server_name) = server_name {
|
|
||||||
if services
|
|
||||||
.config
|
|
||||||
.forbidden_remote_server_names
|
|
||||||
.is_match(server_name.host())
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"User {user_id} who is not an admin tried joining a room which has the server \
|
|
||||||
name {server_name} that is globally forbidden. Rejecting.",
|
|
||||||
);
|
|
||||||
|
|
||||||
maybe_deactivate(services, user_id, client_ip)
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
return Err!(Request(Forbidden("This remote server is banned on this homeserver.")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -120,16 +102,15 @@ async fn maybe_deactivate(services: &Services, user_id: &UserId, client_ip: IpAd
|
|||||||
.config
|
.config
|
||||||
.auto_deactivate_banned_room_attempts
|
.auto_deactivate_banned_room_attempts
|
||||||
{
|
{
|
||||||
warn!("Automatically deactivating user {user_id} due to attempted banned room join");
|
let notice = format!(
|
||||||
|
"Automatically deactivating user {user_id} due to attempted banned room join from \
|
||||||
|
IP {client_ip}"
|
||||||
|
);
|
||||||
|
|
||||||
|
warn!("{notice}");
|
||||||
|
|
||||||
if services.server.config.admin_room_notices {
|
if services.server.config.admin_room_notices {
|
||||||
services
|
services.admin.send_text(¬ice).await;
|
||||||
.admin
|
|
||||||
.send_text(&format!(
|
|
||||||
"Automatically deactivating user {user_id} due to attempted banned room \
|
|
||||||
join from IP {client_ip}"
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
services
|
services
|
||||||
@@ -141,99 +122,3 @@ async fn maybe_deactivate(services: &Services, user_id: &UserId, client_ip: IpAd
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: should this be in services? banned check would have to resolve again if
|
|
||||||
// room_id is not available at callsite
|
|
||||||
async fn get_join_params(
|
|
||||||
services: &Services,
|
|
||||||
user_id: &UserId,
|
|
||||||
room_id_or_alias: &RoomOrAliasId,
|
|
||||||
via: &[OwnedServerName],
|
|
||||||
) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
|
|
||||||
// servers tried first, additional_servers shuffled then tried after
|
|
||||||
let (room_id, mut primary_servers, mut additional_servers) =
|
|
||||||
match OwnedRoomId::try_from(room_id_or_alias.to_owned()) {
|
|
||||||
// if room id, shuffle via + room_id server_name ...
|
|
||||||
| Ok(room_id) => {
|
|
||||||
let mut additional_servers = via.to_vec();
|
|
||||||
|
|
||||||
if let Some(server) = room_id.server_name() {
|
|
||||||
additional_servers.push(server.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
(room_id, Vec::new(), additional_servers)
|
|
||||||
},
|
|
||||||
// ... if room alias, resolve and don't shuffle ...
|
|
||||||
| Err(room_alias) => {
|
|
||||||
let (room_id, servers) = services.alias.resolve_alias(&room_alias).await?;
|
|
||||||
|
|
||||||
(room_id, servers, Vec::new())
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// either way, add invited vias
|
|
||||||
additional_servers.extend(
|
|
||||||
services
|
|
||||||
.state_cache
|
|
||||||
.servers_invite_via(&room_id)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.await,
|
|
||||||
);
|
|
||||||
|
|
||||||
// either way, add invite senders' servers
|
|
||||||
additional_servers.extend(
|
|
||||||
services
|
|
||||||
.state_cache
|
|
||||||
.invite_state(user_id, &room_id)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|event| event.get_field("sender").ok().flatten())
|
|
||||||
.filter_map(|sender: &str| UserId::parse(sender).ok())
|
|
||||||
.map(|user| user.server_name().to_owned()),
|
|
||||||
);
|
|
||||||
|
|
||||||
primary_servers.sort_unstable();
|
|
||||||
primary_servers.dedup();
|
|
||||||
shuffle(&mut primary_servers);
|
|
||||||
|
|
||||||
// shuffle additionals, append to base servers
|
|
||||||
additional_servers.sort_unstable();
|
|
||||||
additional_servers.dedup();
|
|
||||||
shuffle(&mut additional_servers);
|
|
||||||
|
|
||||||
let mut servers: Vec<_> = room_id_or_alias
|
|
||||||
.server_name()
|
|
||||||
.filter(|_| room_id_or_alias.is_room_alias_id())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.into_iter()
|
|
||||||
.chain(primary_servers.into_iter())
|
|
||||||
.chain(additional_servers.into_iter())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// sort deprioritized servers last
|
|
||||||
servers.sort_by(|a, b| {
|
|
||||||
let a_matches = services
|
|
||||||
.server
|
|
||||||
.config
|
|
||||||
.deprioritize_joins_through_servers
|
|
||||||
.is_match(a.host());
|
|
||||||
|
|
||||||
let b_matches = services
|
|
||||||
.server
|
|
||||||
.config
|
|
||||||
.deprioritize_joins_through_servers
|
|
||||||
.is_match(b.host());
|
|
||||||
|
|
||||||
if a_matches && !b_matches {
|
|
||||||
Ordering::Greater
|
|
||||||
} else if !a_matches && b_matches {
|
|
||||||
Ordering::Less
|
|
||||||
} else {
|
|
||||||
Ordering::Equal
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok((room_id, servers))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -88,9 +88,6 @@ pub(super) use user_directory::*;
|
|||||||
pub(super) use voip::*;
|
pub(super) use voip::*;
|
||||||
pub(super) use well_known::*;
|
pub(super) use well_known::*;
|
||||||
|
|
||||||
/// generated device ID length
|
|
||||||
const DEVICE_ID_LENGTH: usize = 10;
|
|
||||||
|
|
||||||
/// generated user access token length
|
/// generated user access token length
|
||||||
const TOKEN_LENGTH: usize = tuwunel_service::users::device::TOKEN_LENGTH;
|
const TOKEN_LENGTH: usize = tuwunel_service::users::device::TOKEN_LENGTH;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::fmt::Write;
|
|||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use futures::FutureExt;
|
|
||||||
use register::RegistrationKind;
|
use register::RegistrationKind;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
UserId,
|
UserId,
|
||||||
@@ -13,15 +12,11 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||||
},
|
},
|
||||||
events::GlobalAccountDataEventType,
|
|
||||||
push,
|
|
||||||
};
|
|
||||||
use tuwunel_core::{
|
|
||||||
Err, Error, Result, debug_info, debug_warn, error, info, is_equal_to, utils, warn,
|
|
||||||
};
|
};
|
||||||
|
use tuwunel_core::{Err, Error, Result, debug_info, debug_warn, info, utils};
|
||||||
use tuwunel_service::users::device::generate_refresh_token;
|
use tuwunel_service::users::device::generate_refresh_token;
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH};
|
use super::SESSION_ID_LENGTH;
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||||
@@ -48,15 +43,10 @@ pub(crate) async fn get_register_available_route(
|
|||||||
.appservice_info
|
.appservice_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|appservice| {
|
.is_some_and(|appservice| {
|
||||||
appservice.registration.id == "irc"
|
let id = &appservice.registration.id;
|
||||||
|| appservice
|
id == "irc"
|
||||||
.registration
|
|| id.contains("matrix-appservice-irc")
|
||||||
.id
|
|| id.contains("matrix_appservice_irc")
|
||||||
.contains("matrix-appservice-irc")
|
|
||||||
|| appservice
|
|
||||||
.registration
|
|
||||||
.id
|
|
||||||
.contains("matrix_appservice_irc")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if services
|
if services
|
||||||
@@ -148,71 +138,32 @@ pub(crate) async fn register_route(
|
|||||||
let is_guest = body.kind == RegistrationKind::Guest;
|
let is_guest = body.kind == RegistrationKind::Guest;
|
||||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||||
|
|
||||||
|
let user = body.username.as_deref().unwrap_or("");
|
||||||
|
let device_name = body
|
||||||
|
.initial_device_display_name
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
if !services.config.allow_registration && body.appservice_info.is_none() {
|
if !services.config.allow_registration && body.appservice_info.is_none() {
|
||||||
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
info!(
|
||||||
| (Some(username), Some(device_display_name)) => {
|
%is_guest,
|
||||||
info!(
|
%user,
|
||||||
%is_guest,
|
%device_name,
|
||||||
user = %username,
|
"Rejecting registration attempt as registration is disabled"
|
||||||
device_name = %device_display_name,
|
);
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (Some(username), _) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
user = %username,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (_, Some(device_display_name)) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
device_name = %device_display_name,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (None, _) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err!(Request(Forbidden("Registration has been disabled.")));
|
return Err!(Request(Forbidden("Registration has been disabled.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_guest && !services.config.allow_guest_registration {
|
if is_guest && !services.config.allow_guest_registration {
|
||||||
let display_name = body
|
|
||||||
.initial_device_display_name
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
debug_warn!(
|
debug_warn!(
|
||||||
"Guest registration disabled / registration enabled with token configured, \
|
%device_name,
|
||||||
rejecting guest registration attempt, initial device name: \"{display_name}\""
|
"Guest registration disabled, rejecting guest registration attempt"
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// forbid guests from registering if there is not a real admin user yet. give
|
|
||||||
// generic user error.
|
|
||||||
if is_guest && services.users.count().await < 2 {
|
|
||||||
let display_name = body
|
|
||||||
.initial_device_display_name
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
warn!(
|
|
||||||
"Guest account attempted to register before a real admin user has been registered, \
|
|
||||||
rejecting registration. Guest's initial device name: \"{display_name}\""
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_id = match (body.username.as_ref(), is_guest) {
|
let user_id = match (body.username.as_ref(), is_guest) {
|
||||||
| (Some(username), false) => {
|
| (Some(username), false) => {
|
||||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
||||||
@@ -313,7 +264,13 @@ pub(crate) async fn register_route(
|
|||||||
|
|
||||||
// UIAA
|
// UIAA
|
||||||
let mut uiaainfo;
|
let mut uiaainfo;
|
||||||
let skip_auth = if services.globals.registration_token.is_some() && !is_guest {
|
let skip_auth = if !services
|
||||||
|
.globals
|
||||||
|
.get_registration_tokens()
|
||||||
|
.await
|
||||||
|
.is_empty()
|
||||||
|
&& !is_guest
|
||||||
|
{
|
||||||
// Registration token required
|
// Registration token required
|
||||||
uiaainfo = UiaaInfo {
|
uiaainfo = UiaaInfo {
|
||||||
flows: vec![AuthFlow {
|
flows: vec![AuthFlow {
|
||||||
@@ -378,45 +335,9 @@ pub(crate) async fn register_route(
|
|||||||
|
|
||||||
let password = if is_guest { None } else { body.password.as_deref() };
|
let password = if is_guest { None } else { body.password.as_deref() };
|
||||||
|
|
||||||
// Create user
|
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
.create(&user_id, password, None)
|
.full_register(&user_id, password, None, body.appservice_info.as_ref(), is_guest, true)
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Default to pretty displayname
|
|
||||||
let mut displayname = user_id.localpart().to_owned();
|
|
||||||
|
|
||||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
|
||||||
// content is set to the user's display name with a space before it
|
|
||||||
if !services
|
|
||||||
.config
|
|
||||||
.new_user_displayname_suffix
|
|
||||||
.is_empty()
|
|
||||||
&& body.appservice_info.is_none()
|
|
||||||
{
|
|
||||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.set_displayname(&user_id, Some(displayname.clone()));
|
|
||||||
|
|
||||||
// Initial account data
|
|
||||||
services
|
|
||||||
.account_data
|
|
||||||
.update(
|
|
||||||
None,
|
|
||||||
&user_id,
|
|
||||||
GlobalAccountDataEventType::PushRules
|
|
||||||
.to_string()
|
|
||||||
.into(),
|
|
||||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
|
||||||
content: ruma::events::push_rules::PushRulesEventContent {
|
|
||||||
global: push::Ruleset::server_default(&user_id),
|
|
||||||
},
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if (!is_guest && body.inhibit_login)
|
if (!is_guest && body.inhibit_login)
|
||||||
@@ -426,169 +347,56 @@ pub(crate) async fn register_route(
|
|||||||
.is_some_and(|appservice| appservice.registration.device_management)
|
.is_some_and(|appservice| appservice.registration.device_management)
|
||||||
{
|
{
|
||||||
return Ok(register::v3::Response {
|
return Ok(register::v3::Response {
|
||||||
access_token: None,
|
|
||||||
user_id,
|
user_id,
|
||||||
device_id: None,
|
device_id: None,
|
||||||
|
access_token: None,
|
||||||
refresh_token: None,
|
refresh_token: None,
|
||||||
expires_in: None,
|
expires_in: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new device id if the user didn't specify one
|
let device_id = if is_guest { None } else { body.device_id.as_deref() };
|
||||||
let device_id = if is_guest { None } else { body.device_id.clone() }
|
|
||||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
|
||||||
|
|
||||||
// Generate new token for the device
|
// Generate new token for the device
|
||||||
let (access_token, expires_in) = services
|
let (access_token, expires_in) = services
|
||||||
.users
|
.users
|
||||||
.generate_access_token(body.body.refresh_token);
|
.generate_access_token(body.refresh_token);
|
||||||
|
|
||||||
// Generate a new refresh_token if requested by client
|
// Generate a new refresh_token if requested by client
|
||||||
let refresh_token = expires_in.is_some().then(generate_refresh_token);
|
let refresh_token = expires_in.is_some().then(generate_refresh_token);
|
||||||
|
|
||||||
// Create device for this account
|
// Create device for this account
|
||||||
services
|
let device_id = services
|
||||||
.users
|
.users
|
||||||
.create_device(
|
.create_device(
|
||||||
&user_id,
|
&user_id,
|
||||||
&device_id,
|
device_id,
|
||||||
(Some(&access_token), expires_in),
|
(Some(&access_token), expires_in),
|
||||||
refresh_token.as_deref(),
|
refresh_token.as_deref(),
|
||||||
body.initial_device_display_name.clone(),
|
body.initial_device_display_name.as_deref(),
|
||||||
Some(client.to_string()),
|
Some(client.to_string()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
debug_info!(%user_id, %device_id, "User account was created");
|
debug_info!(%user_id, %device_id, "User account was created");
|
||||||
|
|
||||||
let device_display_name = body
|
if body.appservice_info.is_none() && (!is_guest || services.config.log_guest_registrations) {
|
||||||
.initial_device_display_name
|
let mut notice = String::from(if is_guest { "New guest user" } else { "New user" });
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// log in conduit admin channel if a non-guest user registered
|
write!(notice, " registered on this server from IP {client}")?;
|
||||||
if body.appservice_info.is_none() && !is_guest {
|
|
||||||
if !device_display_name.is_empty() {
|
|
||||||
let notice = format!(
|
|
||||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
|
||||||
display name \"{device_display_name}\""
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("{notice}");
|
if let Some(device_name) = body.initial_device_display_name.as_deref() {
|
||||||
if services.server.config.admin_room_notices {
|
write!(notice, " with device name {device_name}")?;
|
||||||
services.admin.notice(¬ice).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
|
||||||
|
|
||||||
info!("{notice}");
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services.admin.notice(¬ice).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// log in conduit admin channel if a guest registered
|
if !is_guest {
|
||||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
info!("{notice}");
|
||||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
|
||||||
|
|
||||||
if !device_display_name.is_empty() {
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!(
|
|
||||||
"Guest user \"{user_id}\" with device display name \
|
|
||||||
\"{device_display_name}\" registered on this server from IP {client}"
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
#[allow(clippy::collapsible_else_if)]
|
debug_info!("{notice}");
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!(
|
|
||||||
"Guest user \"{user_id}\" with no device display name registered on \
|
|
||||||
this server from IP {client}",
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the first real user, grant them admin privileges except for guest
|
if services.server.config.admin_room_notices {
|
||||||
// users
|
services.admin.notice(¬ice).await;
|
||||||
// Note: the server user is generated first
|
|
||||||
if !is_guest
|
|
||||||
&& services.config.grant_admin_to_first_user
|
|
||||||
&& let Ok(admin_room) = services.admin.get_admin_room().await
|
|
||||||
&& services
|
|
||||||
.state_cache
|
|
||||||
.room_joined_count(&admin_room)
|
|
||||||
.await
|
|
||||||
.is_ok_and(is_equal_to!(1))
|
|
||||||
{
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.make_user_admin(&user_id)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
warn!("Granting {user_id} admin privileges as the first user");
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.appservice_info.is_none()
|
|
||||||
&& !services.server.config.auto_join_rooms.is_empty()
|
|
||||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
|
||||||
{
|
|
||||||
for room in &services.server.config.auto_join_rooms {
|
|
||||||
let Ok(room_id) = services.alias.maybe_resolve(room).await else {
|
|
||||||
error!(
|
|
||||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
|
||||||
{room}, skipping"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !services
|
|
||||||
.state_cache
|
|
||||||
.server_in_room(services.globals.server_name(), &room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Skipping room {room} to automatically join as we have never joined before."
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room_server_name) = room.server_name() {
|
|
||||||
let state_lock = services.state.mutex.lock(&room_id).await;
|
|
||||||
|
|
||||||
match services
|
|
||||||
.membership
|
|
||||||
.join(
|
|
||||||
&user_id,
|
|
||||||
&room_id,
|
|
||||||
Some("Automatically joining this room upon registration".to_owned()),
|
|
||||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
|
||||||
&body.appservice_info,
|
|
||||||
&state_lock,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Err(e) => {
|
|
||||||
// don't return this error so we don't fail registrations
|
|
||||||
error!(
|
|
||||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
info!("Automatically joined room {room} for user {user_id}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(state_lock);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,9 +419,13 @@ pub(crate) async fn check_registration_token_validity(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||||
) -> Result<check_registration_token_validity::v1::Response> {
|
) -> Result<check_registration_token_validity::v1::Response> {
|
||||||
let Some(reg_token) = services.globals.registration_token.clone() else {
|
let tokens = services.globals.get_registration_tokens().await;
|
||||||
return Err!(Request(Forbidden("Server does not allow token registration")));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
|
if tokens.is_empty() {
|
||||||
|
return Err!(Request(Forbidden("Server does not allow token registration")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid = tokens.contains(&body.token);
|
||||||
|
|
||||||
|
Ok(check_registration_token_validity::v1::Response { valid })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub(crate) async fn get_room_summary(
|
|||||||
) -> Result<get_summary::v1::Response> {
|
) -> Result<get_summary::v1::Response> {
|
||||||
let (room_id, servers) = services
|
let (room_id, servers) = services
|
||||||
.alias
|
.alias
|
||||||
.maybe_resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone()))
|
.maybe_resolve_with_servers(&body.room_id_or_alias, Some(&body.via))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if services.metadata.is_banned(&room_id).await {
|
if services.metadata.is_banned(&room_id).await {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use ruma::{OwnedUserId, UserId};
|
use ruma::{OwnedUserId, UserId};
|
||||||
use tuwunel_core::{Err, Result, debug, error, info, warn};
|
use tuwunel_core::{Err, Result, debug};
|
||||||
use tuwunel_service::Services;
|
use tuwunel_service::Services;
|
||||||
|
|
||||||
use super::password_login;
|
use super::password_login;
|
||||||
@@ -51,69 +51,8 @@ pub(super) async fn ldap_login(
|
|||||||
if !services.users.exists(lowercased_user_id).await {
|
if !services.users.exists(lowercased_user_id).await {
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
.create(lowercased_user_id, Some("*"), Some("ldap"))
|
.full_register(lowercased_user_id, Some("*"), Some("ldap"), None, false, false)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Auto-join rooms for newly created LDAP users
|
|
||||||
if !services.server.config.auto_join_rooms.is_empty() {
|
|
||||||
for room in &services.server.config.auto_join_rooms {
|
|
||||||
let Ok(room_id) = services.alias.maybe_resolve(room).await else {
|
|
||||||
error!(
|
|
||||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
|
||||||
{room}, skipping"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !services
|
|
||||||
.state_cache
|
|
||||||
.server_in_room(services.globals.server_name(), &room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Skipping room {room} to automatically join as we have never joined \
|
|
||||||
before."
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room_server_name) = room.server_name() {
|
|
||||||
let state_lock = services.state.mutex.lock(&room_id).await;
|
|
||||||
|
|
||||||
match services
|
|
||||||
.membership
|
|
||||||
.join(
|
|
||||||
lowercased_user_id,
|
|
||||||
&room_id,
|
|
||||||
Some("Automatically joining this room upon first login".to_owned()),
|
|
||||||
&[
|
|
||||||
services.globals.server_name().to_owned(),
|
|
||||||
room_server_name.to_owned(),
|
|
||||||
],
|
|
||||||
&None,
|
|
||||||
&state_lock,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Err(e) => {
|
|
||||||
// don't return this error so we don't fail logins
|
|
||||||
error!(
|
|
||||||
"Failed to automatically join room {room} for user \
|
|
||||||
{lowercased_user_id}: {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
info!(
|
|
||||||
"Automatically joined room {room} for user {lowercased_user_id}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(state_lock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_tuwunel_admin = services
|
let is_tuwunel_admin = services
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use ruma::api::client::session::{
|
|||||||
v3::{DiscoveryInfo, HomeserverInfo, LoginInfo},
|
v3::{DiscoveryInfo, HomeserverInfo, LoginInfo},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tuwunel_core::{Err, Result, info, utils, utils::stream::ReadyExt};
|
use tuwunel_core::{Err, Result, info, utils::stream::ReadyExt};
|
||||||
use tuwunel_service::users::device::generate_refresh_token;
|
use tuwunel_service::users::device::generate_refresh_token;
|
||||||
|
|
||||||
use self::{ldap::ldap_login, password::password_login};
|
use self::{ldap::ldap_login, password::password_login};
|
||||||
@@ -30,7 +30,7 @@ pub(crate) use self::{
|
|||||||
refresh::refresh_token_route,
|
refresh::refresh_token_route,
|
||||||
token::login_token_route,
|
token::login_token_route,
|
||||||
};
|
};
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::TOKEN_LENGTH;
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/login`
|
/// # `GET /_matrix/client/v3/login`
|
||||||
@@ -97,43 +97,39 @@ pub(crate) async fn login_route(
|
|||||||
// Generate a new refresh_token if requested by client
|
// Generate a new refresh_token if requested by client
|
||||||
let refresh_token = expires_in.is_some().then(generate_refresh_token);
|
let refresh_token = expires_in.is_some().then(generate_refresh_token);
|
||||||
|
|
||||||
// Generate new device id if the user didn't specify one
|
|
||||||
let device_id = body
|
|
||||||
.device_id
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
|
||||||
|
|
||||||
// Determine if device_id was provided and exists in the db for this user
|
// Determine if device_id was provided and exists in the db for this user
|
||||||
let device_exists = services
|
let device_id = if let Some(device_id) = &body.device_id
|
||||||
.users
|
&& services
|
||||||
.all_device_ids(&user_id)
|
|
||||||
.ready_any(|v| v == device_id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !device_exists {
|
|
||||||
services
|
|
||||||
.users
|
.users
|
||||||
.create_device(
|
.all_device_ids(&user_id)
|
||||||
&user_id,
|
.ready_any(|v| v == device_id)
|
||||||
&device_id,
|
.await
|
||||||
(Some(&access_token), expires_in),
|
{
|
||||||
refresh_token.as_deref(),
|
|
||||||
body.initial_device_display_name.clone(),
|
|
||||||
Some(client.to_string()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
.set_access_token(
|
.set_access_token(
|
||||||
&user_id,
|
&user_id,
|
||||||
&device_id,
|
device_id,
|
||||||
&access_token,
|
&access_token,
|
||||||
expires_in,
|
expires_in,
|
||||||
refresh_token.as_deref(),
|
refresh_token.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
|
||||||
|
device_id.clone()
|
||||||
|
} else {
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.create_device(
|
||||||
|
&user_id,
|
||||||
|
body.device_id.as_deref(),
|
||||||
|
(Some(&access_token), expires_in),
|
||||||
|
refresh_token.as_deref(),
|
||||||
|
body.initial_device_display_name.as_deref(),
|
||||||
|
Some(client.to_string()),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
info!("{user_id} logged in");
|
info!("{user_id} logged in");
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub(crate) async fn turn_server_route(
|
|||||||
|
|
||||||
let turn_secret = &services.globals.turn_secret;
|
let turn_secret = &services.globals.turn_secret;
|
||||||
|
|
||||||
let (username, password) = if !turn_secret.is_empty() {
|
let (username, password) = if let Some(turn_secret) = turn_secret {
|
||||||
let expiry = SecondsSinceUnixEpoch::from_system_time(
|
let expiry = SecondsSinceUnixEpoch::from_system_time(
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.checked_add(Duration::from_secs(services.config.turn_ttl))
|
.checked_add(Duration::from_secs(services.config.turn_ttl))
|
||||||
|
|||||||
@@ -1006,7 +1006,7 @@ pub struct Config {
|
|||||||
///
|
///
|
||||||
/// display: sensitive
|
/// display: sensitive
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub turn_secret: String,
|
pub turn_secret: Option<String>,
|
||||||
|
|
||||||
/// TURN secret to use that's read from the file path specified.
|
/// TURN secret to use that's read from the file path specified.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mod data;
|
mod data;
|
||||||
|
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{collections::HashSet, ops::Range, sync::Arc};
|
||||||
|
|
||||||
use data::Data;
|
use data::Data;
|
||||||
use ruma::{OwnedUserId, RoomAliasId, ServerName, UserId};
|
use ruma::{OwnedUserId, RoomAliasId, ServerName, UserId};
|
||||||
@@ -13,8 +13,7 @@ pub struct Service {
|
|||||||
server: Arc<Server>,
|
server: Arc<Server>,
|
||||||
|
|
||||||
pub server_user: OwnedUserId,
|
pub server_user: OwnedUserId,
|
||||||
pub turn_secret: String,
|
pub turn_secret: Option<String>,
|
||||||
pub registration_token: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::Service for Service {
|
impl crate::Service for Service {
|
||||||
@@ -22,32 +21,17 @@ impl crate::Service for Service {
|
|||||||
let db = Data::new(args);
|
let db = Data::new(args);
|
||||||
let config = &args.server.config;
|
let config = &args.server.config;
|
||||||
|
|
||||||
let turn_secret = config.turn_secret_file.as_ref().map_or_else(
|
let turn_secret = config
|
||||||
|| config.turn_secret.clone(),
|
.turn_secret_file
|
||||||
|path| {
|
|
||||||
std::fs::read_to_string(path).unwrap_or_else(|e| {
|
|
||||||
error!("Failed to read the TURN secret file: {e}");
|
|
||||||
|
|
||||||
config.turn_secret.clone()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let registration_token = config
|
|
||||||
.registration_token_file
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or_else(
|
.and_then(|path| {
|
||||||
|| config.registration_token.clone(),
|
std::fs::read_to_string(path)
|
||||||
|path| {
|
.inspect_err(|e| {
|
||||||
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
|
error!("Failed to read the TURN secret file: {e}");
|
||||||
error!("Failed to read the registration token file: {e}");
|
})
|
||||||
}) else {
|
.ok()
|
||||||
return config.registration_token.clone();
|
})
|
||||||
};
|
.or_else(|| config.turn_secret.clone());
|
||||||
|
|
||||||
Some(token)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
db,
|
db,
|
||||||
@@ -58,7 +42,6 @@ impl crate::Service for Service {
|
|||||||
)
|
)
|
||||||
.expect("@conduit:server_name is valid"),
|
.expect("@conduit:server_name is valid"),
|
||||||
turn_secret,
|
turn_secret,
|
||||||
registration_token,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,4 +105,29 @@ impl Service {
|
|||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_read_only(&self) -> bool { self.db.db.is_read_only() }
|
pub fn is_read_only(&self) -> bool { self.db.db.is_read_only() }
|
||||||
|
|
||||||
|
pub async fn get_registration_tokens(&self) -> HashSet<String> {
|
||||||
|
let mut tokens = HashSet::new();
|
||||||
|
if let Some(file) = &self
|
||||||
|
.server
|
||||||
|
.config
|
||||||
|
.registration_token_file
|
||||||
|
.as_ref()
|
||||||
|
{
|
||||||
|
match std::fs::read_to_string(file) {
|
||||||
|
| Err(e) => error!("Failed to read the registration token file: {e}"),
|
||||||
|
| Ok(text) => {
|
||||||
|
text.split_ascii_whitespace().for_each(|token| {
|
||||||
|
tokens.insert(token.to_owned());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = &self.server.config.registration_token {
|
||||||
|
tokens.insert(token.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
iter::once,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, pin_mut};
|
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, pin_mut};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, OwnedUserId, RoomId, RoomVersionId,
|
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, OwnedUserId, RoomId, RoomOrAliasId,
|
||||||
UserId,
|
RoomVersionId, UserId,
|
||||||
api::{client::error::ErrorKind, federation},
|
api::{client::error::ErrorKind, federation},
|
||||||
canonical_json::to_canonical_value,
|
canonical_json::to_canonical_value,
|
||||||
events::{
|
events::{
|
||||||
@@ -20,13 +25,13 @@ use tuwunel_core::{
|
|||||||
matrix::{event::gen_event_id_canonical_json, room_version},
|
matrix::{event::gen_event_id_canonical_json, room_version},
|
||||||
pdu::{PduBuilder, format::from_incoming_federation},
|
pdu::{PduBuilder, format::from_incoming_federation},
|
||||||
state_res, trace,
|
state_res, trace,
|
||||||
utils::{self, IterStream, ReadyExt},
|
utils::{self, IterStream, ReadyExt, shuffle},
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Service;
|
use super::Service;
|
||||||
use crate::{
|
use crate::{
|
||||||
appservice::RegistrationInfo,
|
Services,
|
||||||
rooms::{
|
rooms::{
|
||||||
state::RoomMutexGuard,
|
state::RoomMutexGuard,
|
||||||
state_compressor::{CompressedState, HashSetCompressStateEvent},
|
state_compressor::{CompressedState, HashSetCompressStateEvent},
|
||||||
@@ -39,22 +44,27 @@ use crate::{
|
|||||||
skip_all,
|
skip_all,
|
||||||
fields(%sender_user, %room_id)
|
fields(%sender_user, %room_id)
|
||||||
)]
|
)]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn join(
|
pub async fn join(
|
||||||
&self,
|
&self,
|
||||||
sender_user: &UserId,
|
sender_user: &UserId,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
|
orig_room_id: Option<&RoomOrAliasId>,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
servers: &[OwnedServerName],
|
servers: &[OwnedServerName],
|
||||||
appservice_info: &Option<RegistrationInfo>,
|
is_appservice: bool,
|
||||||
state_lock: &RoomMutexGuard,
|
state_lock: &RoomMutexGuard,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
let servers =
|
||||||
|
get_servers_for_room(&self.services, sender_user, room_id, orig_room_id, servers).await?;
|
||||||
|
|
||||||
let user_is_guest = self
|
let user_is_guest = self
|
||||||
.services
|
.services
|
||||||
.users
|
.users
|
||||||
.is_deactivated(sender_user)
|
.is_deactivated(sender_user)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
&& appservice_info.is_none();
|
&& !is_appservice;
|
||||||
|
|
||||||
if user_is_guest
|
if user_is_guest
|
||||||
&& !self
|
&& !self
|
||||||
@@ -99,12 +109,12 @@ pub async fn join(
|
|||||||
|| (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0]));
|
|| (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0]));
|
||||||
|
|
||||||
if local_join {
|
if local_join {
|
||||||
self.join_local(sender_user, room_id, reason, servers, state_lock)
|
self.join_local(sender_user, room_id, reason, &servers, state_lock)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
// Ask a remote server if we are not participating in this room
|
// Ask a remote server if we are not participating in this room
|
||||||
self.join_remote(sender_user, room_id, reason, servers, state_lock)
|
self.join_remote(sender_user, room_id, reason, &servers, state_lock)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -838,3 +848,79 @@ async fn make_join_request(
|
|||||||
|
|
||||||
make_join_response_and_server
|
make_join_response_and_server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn get_servers_for_room(
|
||||||
|
services: &Services,
|
||||||
|
user_id: &UserId,
|
||||||
|
room_id: &RoomId,
|
||||||
|
orig_room_id: Option<&RoomOrAliasId>,
|
||||||
|
via: &[OwnedServerName],
|
||||||
|
) -> Result<Vec<OwnedServerName>> {
|
||||||
|
// add invited vias
|
||||||
|
let mut additional_servers = services
|
||||||
|
.state_cache
|
||||||
|
.servers_invite_via(room_id)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// add invite senders' servers
|
||||||
|
additional_servers.extend(
|
||||||
|
services
|
||||||
|
.state_cache
|
||||||
|
.invite_state(user_id, room_id)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|event| event.get_field("sender").ok().flatten())
|
||||||
|
.filter_map(|sender: &str| UserId::parse(sender).ok())
|
||||||
|
.map(|user| user.server_name().to_owned()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut servers = Vec::from(via);
|
||||||
|
shuffle(&mut servers);
|
||||||
|
|
||||||
|
if let Some(server_name) = room_id.server_name() {
|
||||||
|
servers.insert(0, server_name.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(orig_room_id) = orig_room_id
|
||||||
|
&& let Some(orig_server_name) = orig_room_id.server_name()
|
||||||
|
{
|
||||||
|
servers.insert(0, orig_server_name.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffle(&mut additional_servers);
|
||||||
|
|
||||||
|
servers.extend_from_slice(&additional_servers);
|
||||||
|
|
||||||
|
// 1. (room alias server)?
|
||||||
|
// 2. (room id server)?
|
||||||
|
// 3. shuffle [via query + resolve servers]?
|
||||||
|
// 4. shuffle [invited via, inviters servers]?
|
||||||
|
|
||||||
|
info!("{servers:?}");
|
||||||
|
|
||||||
|
// dedup preserving order
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
servers.retain(|x| set.insert(x.clone()));
|
||||||
|
|
||||||
|
info!("{servers:?}");
|
||||||
|
|
||||||
|
// sort deprioritized servers last
|
||||||
|
if !servers.is_empty() {
|
||||||
|
for i in 0..servers.len() {
|
||||||
|
if services
|
||||||
|
.server
|
||||||
|
.config
|
||||||
|
.deprioritize_joins_through_servers
|
||||||
|
.is_match(servers[i].host())
|
||||||
|
{
|
||||||
|
let server = servers.remove(i);
|
||||||
|
servers.push(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(servers)
|
||||||
|
}
|
||||||
|
|||||||
655
src/service/membership/knock.rs
Normal file
655
src/service/membership/knock.rs
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
|
||||||
|
|
||||||
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use ruma::{
|
||||||
|
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, RoomId,
|
||||||
|
RoomOrAliasId, RoomVersionId, UserId,
|
||||||
|
api::federation::{self, membership::RawStrippedState},
|
||||||
|
canonical_json::to_canonical_value,
|
||||||
|
events::{
|
||||||
|
StateEventType,
|
||||||
|
room::member::{MembershipState, RoomMemberEventContent},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tuwunel_core::{
|
||||||
|
Err, Event, PduCount, Result, debug, debug_info, debug_warn, err, extract_variant, implement,
|
||||||
|
info,
|
||||||
|
matrix::event::gen_event_id,
|
||||||
|
pdu::{PduBuilder, PduEvent},
|
||||||
|
trace, utils, warn,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Service;
|
||||||
|
use crate::{
|
||||||
|
membership::join::get_servers_for_room,
|
||||||
|
rooms::{
|
||||||
|
state::RoomMutexGuard,
|
||||||
|
state_compressor::{CompressedState, HashSetCompressStateEvent},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
#[tracing::instrument(
|
||||||
|
level = "debug",
|
||||||
|
skip_all,
|
||||||
|
fields(%sender_user, %room_id)
|
||||||
|
)]
|
||||||
|
pub async fn knock(
|
||||||
|
&self,
|
||||||
|
sender_user: &UserId,
|
||||||
|
room_id: &RoomId,
|
||||||
|
orig_server_name: Option<&RoomOrAliasId>,
|
||||||
|
reason: Option<String>,
|
||||||
|
servers: &[OwnedServerName],
|
||||||
|
state_lock: &RoomMutexGuard,
|
||||||
|
) -> Result {
|
||||||
|
let servers =
|
||||||
|
get_servers_for_room(&self.services, sender_user, room_id, orig_server_name, servers)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if self
|
||||||
|
.services
|
||||||
|
.state_cache
|
||||||
|
.is_invited(sender_user, room_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
debug_warn!("{sender_user} is already invited in {room_id} but attempted to knock");
|
||||||
|
return Err!(Request(Forbidden(
|
||||||
|
"You cannot knock on a room you are already invited/accepted to."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.services
|
||||||
|
.state_cache
|
||||||
|
.is_joined(sender_user, room_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
debug_warn!("{sender_user} is already joined in {room_id} but attempted to knock");
|
||||||
|
return Err!(Request(Forbidden("You cannot knock on a room you are already joined in.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.services
|
||||||
|
.state_cache
|
||||||
|
.is_knocked(sender_user, room_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
debug_warn!("{sender_user} is already knocked in {room_id}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(membership) = self
|
||||||
|
.services
|
||||||
|
.state_accessor
|
||||||
|
.get_member(room_id, sender_user)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if membership.membership == MembershipState::Ban {
|
||||||
|
debug_warn!("{sender_user} is banned from {room_id} but attempted to knock");
|
||||||
|
return Err!(Request(Forbidden("You cannot knock on a room you are banned from.")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_in_room = self
|
||||||
|
.services
|
||||||
|
.state_cache
|
||||||
|
.server_in_room(self.services.globals.server_name(), room_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let local_knock = server_in_room
|
||||||
|
|| servers.is_empty()
|
||||||
|
|| (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0]));
|
||||||
|
|
||||||
|
if local_knock {
|
||||||
|
self.knock_room_helper_local(sender_user, room_id, reason, &servers, state_lock)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
self.knock_room_helper_remote(sender_user, room_id, reason, &servers, state_lock)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
async fn knock_room_helper_local(
|
||||||
|
&self,
|
||||||
|
sender_user: &UserId,
|
||||||
|
room_id: &RoomId,
|
||||||
|
reason: Option<String>,
|
||||||
|
servers: &[OwnedServerName],
|
||||||
|
state_lock: &RoomMutexGuard,
|
||||||
|
) -> Result {
|
||||||
|
debug_info!("We can knock locally");
|
||||||
|
|
||||||
|
let room_version_id = self
|
||||||
|
.services
|
||||||
|
.state
|
||||||
|
.get_room_version(room_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
room_version_id,
|
||||||
|
RoomVersionId::V1
|
||||||
|
| RoomVersionId::V2
|
||||||
|
| RoomVersionId::V3
|
||||||
|
| RoomVersionId::V4
|
||||||
|
| RoomVersionId::V5
|
||||||
|
| RoomVersionId::V6
|
||||||
|
) {
|
||||||
|
return Err!(Request(Forbidden("This room does not support knocking.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = RoomMemberEventContent {
|
||||||
|
displayname: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.displayname(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
avatar_url: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.avatar_url(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
blurhash: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.blurhash(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
reason: reason.clone(),
|
||||||
|
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try normal knock first
|
||||||
|
let Err(error) = self
|
||||||
|
.services
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
PduBuilder::state(sender_user.to_string(), &content),
|
||||||
|
sender_user,
|
||||||
|
room_id,
|
||||||
|
state_lock,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if servers.is_empty()
|
||||||
|
|| (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0]))
|
||||||
|
{
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock");
|
||||||
|
|
||||||
|
let (make_knock_response, remote_server) = self
|
||||||
|
.make_knock_request(sender_user, room_id, servers)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("make_knock finished");
|
||||||
|
|
||||||
|
let room_version_id = make_knock_response.room_version;
|
||||||
|
|
||||||
|
if !self
|
||||||
|
.services
|
||||||
|
.server
|
||||||
|
.supported_room_version(&room_version_id)
|
||||||
|
{
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote room version {room_version_id} is not supported by tuwunel"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut knock_event_stub = serde_json::from_str::<CanonicalJsonObject>(
|
||||||
|
make_knock_response.event.get(),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
knock_event_stub.insert(
|
||||||
|
"origin".into(),
|
||||||
|
CanonicalJsonValue::String(
|
||||||
|
self.services
|
||||||
|
.globals
|
||||||
|
.server_name()
|
||||||
|
.as_str()
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
knock_event_stub.insert(
|
||||||
|
"origin_server_ts".into(),
|
||||||
|
CanonicalJsonValue::Integer(
|
||||||
|
utils::millis_since_unix_epoch()
|
||||||
|
.try_into()
|
||||||
|
.expect("Timestamp is valid js_int value"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
knock_event_stub.insert(
|
||||||
|
"content".into(),
|
||||||
|
to_canonical_value(RoomMemberEventContent {
|
||||||
|
displayname: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.displayname(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
avatar_url: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.avatar_url(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
blurhash: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.blurhash(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
reason,
|
||||||
|
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||||
|
})
|
||||||
|
.expect("event is valid, we just created it"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||||
|
// to be present
|
||||||
|
self.services
|
||||||
|
.server_keys
|
||||||
|
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
||||||
|
|
||||||
|
// Generate event id
|
||||||
|
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
||||||
|
|
||||||
|
// Add event_id
|
||||||
|
knock_event_stub
|
||||||
|
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
|
||||||
|
|
||||||
|
// It has enough fields to be called a proper event now
|
||||||
|
let knock_event = knock_event_stub;
|
||||||
|
|
||||||
|
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||||
|
let send_knock_request = federation::membership::create_knock_event::v1::Request {
|
||||||
|
room_id: room_id.to_owned(),
|
||||||
|
event_id: event_id.clone(),
|
||||||
|
pdu: self
|
||||||
|
.services
|
||||||
|
.federation
|
||||||
|
.format_pdu_into(knock_event.clone(), Some(&room_version_id))
|
||||||
|
.await,
|
||||||
|
};
|
||||||
|
|
||||||
|
let send_knock_response = self
|
||||||
|
.services
|
||||||
|
.federation
|
||||||
|
.execute(&remote_server, send_knock_request)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("send_knock finished");
|
||||||
|
|
||||||
|
self.services
|
||||||
|
.short
|
||||||
|
.get_or_create_shortroomid(room_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
info!("Parsing knock event");
|
||||||
|
|
||||||
|
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
|
||||||
|
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
|
||||||
|
|
||||||
|
info!("Updating membership locally to knock state with provided stripped state events");
|
||||||
|
let count = self.services.globals.next_count();
|
||||||
|
self.services
|
||||||
|
.state_cache
|
||||||
|
.update_membership(
|
||||||
|
room_id,
|
||||||
|
sender_user,
|
||||||
|
parsed_knock_pdu
|
||||||
|
.get_content::<RoomMemberEventContent>()
|
||||||
|
.expect("we just created this"),
|
||||||
|
sender_user,
|
||||||
|
Some(
|
||||||
|
send_knock_response
|
||||||
|
.knock_room_state
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
PduCount::Normal(*count),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Appending room knock event locally");
|
||||||
|
self.services
|
||||||
|
.timeline
|
||||||
|
.append_pdu(
|
||||||
|
&parsed_knock_pdu,
|
||||||
|
knock_event,
|
||||||
|
once(parsed_knock_pdu.event_id.borrow()),
|
||||||
|
state_lock,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
async fn knock_room_helper_remote(
|
||||||
|
&self,
|
||||||
|
sender_user: &UserId,
|
||||||
|
room_id: &RoomId,
|
||||||
|
reason: Option<String>,
|
||||||
|
servers: &[OwnedServerName],
|
||||||
|
state_lock: &RoomMutexGuard,
|
||||||
|
) -> Result {
|
||||||
|
info!("Knocking {room_id} over federation.");
|
||||||
|
|
||||||
|
let (make_knock_response, remote_server) = self
|
||||||
|
.make_knock_request(sender_user, room_id, servers)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("make_knock finished");
|
||||||
|
|
||||||
|
let room_version_id = make_knock_response.room_version;
|
||||||
|
|
||||||
|
if !self
|
||||||
|
.services
|
||||||
|
.server
|
||||||
|
.supported_room_version(&room_version_id)
|
||||||
|
{
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote room version {room_version_id} is not supported by tuwunel"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut knock_event_stub: CanonicalJsonObject =
|
||||||
|
serde_json::from_str(make_knock_response.event.get()).map_err(|e| {
|
||||||
|
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
knock_event_stub.insert(
|
||||||
|
"origin".into(),
|
||||||
|
CanonicalJsonValue::String(
|
||||||
|
self.services
|
||||||
|
.globals
|
||||||
|
.server_name()
|
||||||
|
.as_str()
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
knock_event_stub.insert(
|
||||||
|
"origin_server_ts".into(),
|
||||||
|
CanonicalJsonValue::Integer(
|
||||||
|
utils::millis_since_unix_epoch()
|
||||||
|
.try_into()
|
||||||
|
.expect("Timestamp is valid js_int value"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
knock_event_stub.insert(
|
||||||
|
"content".into(),
|
||||||
|
to_canonical_value(RoomMemberEventContent {
|
||||||
|
displayname: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.displayname(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
avatar_url: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.avatar_url(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
blurhash: self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.blurhash(sender_user)
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
|
reason,
|
||||||
|
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||||
|
})
|
||||||
|
.expect("event is valid, we just created it"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||||
|
// to be present
|
||||||
|
self.services
|
||||||
|
.server_keys
|
||||||
|
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
||||||
|
|
||||||
|
// Generate event id
|
||||||
|
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
||||||
|
|
||||||
|
// Add event_id
|
||||||
|
knock_event_stub
|
||||||
|
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
|
||||||
|
|
||||||
|
// It has enough fields to be called a proper event now
|
||||||
|
let knock_event = knock_event_stub;
|
||||||
|
|
||||||
|
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||||
|
let send_knock_request = federation::membership::create_knock_event::v1::Request {
|
||||||
|
room_id: room_id.to_owned(),
|
||||||
|
event_id: event_id.clone(),
|
||||||
|
pdu: self
|
||||||
|
.services
|
||||||
|
.federation
|
||||||
|
.format_pdu_into(knock_event.clone(), Some(&room_version_id))
|
||||||
|
.await,
|
||||||
|
};
|
||||||
|
|
||||||
|
let send_knock_response = self
|
||||||
|
.services
|
||||||
|
.federation
|
||||||
|
.execute(&remote_server, send_knock_request)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("send_knock finished");
|
||||||
|
|
||||||
|
self.services
|
||||||
|
.short
|
||||||
|
.get_or_create_shortroomid(room_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
info!("Parsing knock event");
|
||||||
|
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
|
||||||
|
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
|
||||||
|
|
||||||
|
info!("Going through send_knock response knock state events");
|
||||||
|
let state = send_knock_response
|
||||||
|
.knock_room_state
|
||||||
|
.iter()
|
||||||
|
.map(|event| {
|
||||||
|
serde_json::from_str::<CanonicalJsonObject>(
|
||||||
|
extract_variant!(event.clone(), RawStrippedState::Stripped)
|
||||||
|
.expect("Raw<AnyStrippedStateEvent>")
|
||||||
|
.json()
|
||||||
|
.get(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter_map(Result::ok);
|
||||||
|
|
||||||
|
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
|
||||||
|
|
||||||
|
for event in state {
|
||||||
|
let Some(state_key) = event.get("state_key") else {
|
||||||
|
debug_warn!("send_knock stripped state event missing state_key: {event:?}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(event_type) = event.get("type") else {
|
||||||
|
debug_warn!("send_knock stripped state event missing event type: {event:?}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(state_key) = serde_json::from_value::<String>(state_key.clone().into()) else {
|
||||||
|
debug_warn!("send_knock stripped state event has invalid state_key: {event:?}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(event_type) = serde_json::from_value::<StateEventType>(event_type.clone().into())
|
||||||
|
else {
|
||||||
|
debug_warn!("send_knock stripped state event has invalid event type: {event:?}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let event_id = gen_event_id(&event, &room_version_id)?;
|
||||||
|
let shortstatekey = self
|
||||||
|
.services
|
||||||
|
.short
|
||||||
|
.get_or_create_shortstatekey(&event_type, &state_key)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self.services
|
||||||
|
.timeline
|
||||||
|
.add_pdu_outlier(&event_id, &event);
|
||||||
|
|
||||||
|
state_map.insert(shortstatekey, event_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Compressing state from send_knock");
|
||||||
|
let compressed: CompressedState = self
|
||||||
|
.services
|
||||||
|
.state_compressor
|
||||||
|
.compress_state_events(
|
||||||
|
state_map
|
||||||
|
.iter()
|
||||||
|
.map(|(ssk, eid)| (ssk, eid.borrow())),
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
debug!("Saving compressed state");
|
||||||
|
let HashSetCompressStateEvent {
|
||||||
|
shortstatehash: statehash_before_knock,
|
||||||
|
added,
|
||||||
|
removed,
|
||||||
|
} = self
|
||||||
|
.services
|
||||||
|
.state_compressor
|
||||||
|
.save_state(room_id, Arc::new(compressed))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("Forcing state for new room");
|
||||||
|
self.services
|
||||||
|
.state
|
||||||
|
.force_state(room_id, statehash_before_knock, added, removed, state_lock)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let statehash_after_knock = self
|
||||||
|
.services
|
||||||
|
.state
|
||||||
|
.append_to_state(&parsed_knock_pdu)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Updating membership locally to knock state with provided stripped state events");
|
||||||
|
let count = self.services.globals.next_count();
|
||||||
|
self.services
|
||||||
|
.state_cache
|
||||||
|
.update_membership(
|
||||||
|
room_id,
|
||||||
|
sender_user,
|
||||||
|
parsed_knock_pdu
|
||||||
|
.get_content::<RoomMemberEventContent>()
|
||||||
|
.expect("we just created this"),
|
||||||
|
sender_user,
|
||||||
|
Some(
|
||||||
|
send_knock_response
|
||||||
|
.knock_room_state
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
PduCount::Normal(*count),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Appending room knock event locally");
|
||||||
|
self.services
|
||||||
|
.timeline
|
||||||
|
.append_pdu(
|
||||||
|
&parsed_knock_pdu,
|
||||||
|
knock_event,
|
||||||
|
once(parsed_knock_pdu.event_id.borrow()),
|
||||||
|
state_lock,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Setting final room state for new room");
|
||||||
|
// We set the room state after inserting the pdu, so that we never have a moment
|
||||||
|
// in time where events in the current room state do not exist
|
||||||
|
self.services
|
||||||
|
.state
|
||||||
|
.set_room_state(room_id, statehash_after_knock, state_lock);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
async fn make_knock_request(
|
||||||
|
&self,
|
||||||
|
sender_user: &UserId,
|
||||||
|
room_id: &RoomId,
|
||||||
|
servers: &[OwnedServerName],
|
||||||
|
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
|
||||||
|
let mut make_knock_response_and_server =
|
||||||
|
Err!(BadServerResponse("No server available to assist in knocking."));
|
||||||
|
|
||||||
|
let mut make_knock_counter: usize = 0;
|
||||||
|
|
||||||
|
for remote_server in servers {
|
||||||
|
if self
|
||||||
|
.services
|
||||||
|
.globals
|
||||||
|
.server_is_ours(remote_server)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Asking {remote_server} for make_knock ({make_knock_counter})");
|
||||||
|
|
||||||
|
let make_knock_response = self
|
||||||
|
.services
|
||||||
|
.federation
|
||||||
|
.execute(remote_server, federation::membership::prepare_knock_event::v1::Request {
|
||||||
|
room_id: room_id.to_owned(),
|
||||||
|
user_id: sender_user.to_owned(),
|
||||||
|
ver: self
|
||||||
|
.services
|
||||||
|
.server
|
||||||
|
.supported_room_versions()
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
trace!("make_knock response: {make_knock_response:?}");
|
||||||
|
make_knock_counter = make_knock_counter.saturating_add(1);
|
||||||
|
|
||||||
|
make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone()));
|
||||||
|
|
||||||
|
if make_knock_response_and_server.is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if make_knock_counter > 40 {
|
||||||
|
warn!(
|
||||||
|
"50 servers failed to provide valid make_knock response, assuming no server can \
|
||||||
|
assist in knocking."
|
||||||
|
);
|
||||||
|
make_knock_response_and_server =
|
||||||
|
Err!(BadServerResponse("No server available to assist in knocking."));
|
||||||
|
|
||||||
|
return make_knock_response_and_server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_knock_response_and_server
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ mod ban;
|
|||||||
mod invite;
|
mod invite;
|
||||||
mod join;
|
mod join;
|
||||||
mod kick;
|
mod kick;
|
||||||
|
mod knock;
|
||||||
mod leave;
|
mod leave;
|
||||||
mod unban;
|
mod unban;
|
||||||
|
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ impl Service {
|
|||||||
pub async fn maybe_resolve_with_servers(
|
pub async fn maybe_resolve_with_servers(
|
||||||
&self,
|
&self,
|
||||||
room: &RoomOrAliasId,
|
room: &RoomOrAliasId,
|
||||||
servers: Option<Vec<OwnedServerName>>,
|
servers: Option<&[OwnedServerName]>,
|
||||||
) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
|
) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
|
||||||
match <&RoomId>::try_from(room) {
|
match <&RoomId>::try_from(room) {
|
||||||
| Ok(room_id) => Ok((room_id.to_owned(), servers.unwrap_or_default())),
|
| Ok(room_id) => Ok((room_id.to_owned(), Vec::from(servers.unwrap_or_default()))),
|
||||||
| Err(alias) => self.resolve_alias(alias).await,
|
| Err(alias) => self.resolve_alias(alias).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashSet},
|
collections::BTreeMap,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,32 +45,6 @@ impl crate::Service for Service {
|
|||||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[implement(Service)]
|
|
||||||
pub async fn read_tokens(&self) -> Result<HashSet<String>> {
|
|
||||||
let mut tokens = HashSet::new();
|
|
||||||
if let Some(file) = &self
|
|
||||||
.services
|
|
||||||
.config
|
|
||||||
.registration_token_file
|
|
||||||
.as_ref()
|
|
||||||
{
|
|
||||||
match std::fs::read_to_string(file) {
|
|
||||||
| Err(e) => error!("Failed to read the registration token file: {e}"),
|
|
||||||
| Ok(text) => {
|
|
||||||
text.split_ascii_whitespace().for_each(|token| {
|
|
||||||
tokens.insert(token.to_owned());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(token) = &self.services.config.registration_token {
|
|
||||||
tokens.insert(token.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new Uiaa session. Make sure the session token is unique.
|
/// Creates a new Uiaa session. Make sure the session token is unique.
|
||||||
#[implement(Service)]
|
#[implement(Service)]
|
||||||
pub fn create(
|
pub fn create(
|
||||||
@@ -175,7 +149,11 @@ pub async fn try_auth(
|
|||||||
uiaainfo.completed.push(AuthType::Password);
|
uiaainfo.completed.push(AuthType::Password);
|
||||||
},
|
},
|
||||||
| AuthData::RegistrationToken(t) => {
|
| AuthData::RegistrationToken(t) => {
|
||||||
let tokens = self.read_tokens().await?;
|
let tokens = self
|
||||||
|
.services
|
||||||
|
.globals
|
||||||
|
.get_registration_tokens()
|
||||||
|
.await;
|
||||||
if tokens.contains(t.token.trim()) {
|
if tokens.contains(t.token.trim()) {
|
||||||
uiaainfo
|
uiaainfo
|
||||||
.completed
|
.completed
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) ->
|
|||||||
|
|
||||||
self.create_device(
|
self.create_device(
|
||||||
user_id,
|
user_id,
|
||||||
&request.device_id,
|
Some(&request.device_id),
|
||||||
(None, None),
|
(None, None),
|
||||||
None,
|
None,
|
||||||
request.initial_device_display_name.clone(),
|
request.initial_device_display_name.as_deref(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ use tuwunel_core::{
|
|||||||
};
|
};
|
||||||
use tuwunel_database::{Deserialized, Ignore, Interfix, Json, Map};
|
use tuwunel_database::{Deserialized, Ignore, Interfix, Json, Map};
|
||||||
|
|
||||||
|
/// generated device ID length
|
||||||
|
const DEVICE_ID_LENGTH: usize = 10;
|
||||||
|
|
||||||
/// generated user access token length
|
/// generated user access token length
|
||||||
pub const TOKEN_LENGTH: usize = 32;
|
pub const TOKEN_LENGTH: usize = 32;
|
||||||
|
|
||||||
@@ -28,12 +31,16 @@ pub const TOKEN_LENGTH: usize = 32;
|
|||||||
pub async fn create_device(
|
pub async fn create_device(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
device_id: &DeviceId,
|
device_id: Option<&DeviceId>,
|
||||||
(access_token, expires_in): (Option<&str>, Option<Duration>),
|
(access_token, expires_in): (Option<&str>, Option<Duration>),
|
||||||
refresh_token: Option<&str>,
|
refresh_token: Option<&str>,
|
||||||
initial_device_display_name: Option<String>,
|
initial_device_display_name: Option<&str>,
|
||||||
client_ip: Option<String>,
|
client_ip: Option<String>,
|
||||||
) -> Result {
|
) -> Result<OwnedDeviceId> {
|
||||||
|
let device_id = device_id
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.unwrap_or_else(|| OwnedDeviceId::from(utils::random_string(DEVICE_ID_LENGTH)));
|
||||||
|
|
||||||
if !self.exists(user_id).await {
|
if !self.exists(user_id).await {
|
||||||
return Err!(Request(InvalidParam(error!(
|
return Err!(Request(InvalidParam(error!(
|
||||||
"Called create_device for non-existent user {user_id}"
|
"Called create_device for non-existent user {user_id}"
|
||||||
@@ -42,18 +49,18 @@ pub async fn create_device(
|
|||||||
|
|
||||||
let notify = true;
|
let notify = true;
|
||||||
self.put_device_metadata(user_id, notify, &Device {
|
self.put_device_metadata(user_id, notify, &Device {
|
||||||
device_id: device_id.into(),
|
device_id: device_id.clone(),
|
||||||
display_name: initial_device_display_name.map(Into::into),
|
display_name: initial_device_display_name.map(Into::into),
|
||||||
last_seen_ip: client_ip.map(Into::into),
|
last_seen_ip: client_ip.map(Into::into),
|
||||||
last_seen_ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
last_seen_ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(access_token) = access_token {
|
if let Some(access_token) = access_token {
|
||||||
self.set_access_token(user_id, device_id, access_token, expires_in, refresh_token)
|
self.set_access_token(user_id, &device_id, access_token, expires_in, refresh_token)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(device_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a device from a user.
|
/// Removes a device from a user.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod device;
|
|||||||
mod keys;
|
mod keys;
|
||||||
mod ldap;
|
mod ldap;
|
||||||
mod profile;
|
mod profile;
|
||||||
|
mod register;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -124,10 +125,8 @@ impl Service {
|
|||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
origin: Option<&str>,
|
origin: Option<&str>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
origin.map_or_else(
|
let origin = origin.unwrap_or("password");
|
||||||
|| self.db.userid_origin.insert(user_id, "password"),
|
self.db.userid_origin.insert(user_id, origin);
|
||||||
|origin| self.db.userid_origin.insert(user_id, origin),
|
|
||||||
);
|
|
||||||
self.set_password(user_id, password).await
|
self.set_password(user_id, password).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
156
src/service/users/register.rs
Normal file
156
src/service/users/register.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use futures::FutureExt;
|
||||||
|
use ruma::{UserId, events::GlobalAccountDataEventType, push};
|
||||||
|
use tuwunel_core::{Err, Result, error, implement, info, is_equal_to, warn};
|
||||||
|
|
||||||
|
use crate::appservice::RegistrationInfo;
|
||||||
|
|
||||||
|
/// Fully register a local user
|
||||||
|
///
|
||||||
|
/// Returns a device id and access token for the registered user
|
||||||
|
#[implement(super::Service)]
|
||||||
|
pub async fn full_register(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
password: Option<&str>,
|
||||||
|
origin: Option<&str>,
|
||||||
|
appservice_info: Option<&RegistrationInfo>,
|
||||||
|
is_guest: bool,
|
||||||
|
grant_admin: bool,
|
||||||
|
) -> Result {
|
||||||
|
if !self.services.globals.user_is_local(user_id) {
|
||||||
|
return Err!("Cannot register remote user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.services.users.exists(user_id).await {
|
||||||
|
return Err!(Request(UserInUse("User ID is not available.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
self.services
|
||||||
|
.users
|
||||||
|
.create(user_id, password, origin)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Default to pretty displayname
|
||||||
|
let mut displayname = user_id.localpart().to_owned();
|
||||||
|
|
||||||
|
// If `new_user_displayname_suffix` is set, registration will push whatever
|
||||||
|
// content is set to the user's display name with a space before it
|
||||||
|
if !self
|
||||||
|
.services
|
||||||
|
.config
|
||||||
|
.new_user_displayname_suffix
|
||||||
|
.is_empty()
|
||||||
|
&& appservice_info.is_none()
|
||||||
|
{
|
||||||
|
write!(
|
||||||
|
displayname,
|
||||||
|
" {}",
|
||||||
|
self.services
|
||||||
|
.server
|
||||||
|
.config
|
||||||
|
.new_user_displayname_suffix
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.services
|
||||||
|
.users
|
||||||
|
.set_displayname(user_id, Some(displayname.clone()));
|
||||||
|
|
||||||
|
// Initial account data
|
||||||
|
self.services
|
||||||
|
.account_data
|
||||||
|
.update(
|
||||||
|
None,
|
||||||
|
user_id,
|
||||||
|
GlobalAccountDataEventType::PushRules
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||||
|
content: ruma::events::push_rules::PushRulesEventContent {
|
||||||
|
global: push::Ruleset::server_default(user_id),
|
||||||
|
},
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If this is the first real user, grant them admin privileges except for guest
|
||||||
|
// users
|
||||||
|
// Note: the server user is generated first
|
||||||
|
if !is_guest
|
||||||
|
&& grant_admin
|
||||||
|
&& self.services.config.grant_admin_to_first_user
|
||||||
|
&& let Ok(admin_room) = self.services.admin.get_admin_room().await
|
||||||
|
&& self
|
||||||
|
.services
|
||||||
|
.state_cache
|
||||||
|
.room_joined_count(&admin_room)
|
||||||
|
.await
|
||||||
|
.is_ok_and(is_equal_to!(1))
|
||||||
|
{
|
||||||
|
self.services
|
||||||
|
.admin
|
||||||
|
.make_user_admin(user_id)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
|
warn!("Granting {user_id} admin privileges as the first user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if appservice_info.is_none()
|
||||||
|
&& (self.services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||||
|
{
|
||||||
|
for room in &self.services.server.config.auto_join_rooms {
|
||||||
|
let Ok(room_id) = self.services.alias.maybe_resolve(room).await else {
|
||||||
|
error!(
|
||||||
|
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||||
|
{room}, skipping"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self
|
||||||
|
.services
|
||||||
|
.state_cache
|
||||||
|
.server_in_room(self.services.globals.server_name(), &room_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"Skipping room {room} to automatically join as we have never joined before."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state_lock = self.services.state.mutex.lock(&room_id).await;
|
||||||
|
|
||||||
|
match self
|
||||||
|
.services
|
||||||
|
.membership
|
||||||
|
.join(
|
||||||
|
user_id,
|
||||||
|
&room_id,
|
||||||
|
Some(room),
|
||||||
|
Some("Automatically joining this room upon registration".to_owned()),
|
||||||
|
&[],
|
||||||
|
false,
|
||||||
|
&state_lock,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Err(e) => {
|
||||||
|
// don't return this error so we don't fail registrations
|
||||||
|
error!("Failed to automatically join room {room} for user {user_id}: {e}");
|
||||||
|
},
|
||||||
|
| _ => {
|
||||||
|
info!("Automatically joined room {room} for user {user_id}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(state_lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user