rename api::client_server to api::client
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
8428f43c78
commit
f32380772f
42 changed files with 149 additions and 154 deletions
621
src/api/client/account.rs
Normal file
621
src/api/client/account.rs
Normal file
|
@ -0,0 +1,621 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use conduit::debug_info;
|
||||
use register::RegistrationKind;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
account::{
|
||||
change_password, check_registration_token_validity, deactivate, get_3pids, get_username_availability,
|
||||
register::{self, LoginType},
|
||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
|
||||
ThirdPartyIdRemovalStatus,
|
||||
},
|
||||
error::ErrorKind,
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
|
||||
push, UserId,
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use super::{join_room_by_id_helper, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::{
|
||||
service::user_is_local,
|
||||
services,
|
||||
utils::{self},
|
||||
Error, Result, Ruma,
|
||||
};
|
||||
|
||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||
|
||||
/// # `GET /_matrix/client/v3/register/available`
|
||||
///
|
||||
/// Checks if a username is valid and available on this server.
|
||||
///
|
||||
/// Conditions for returning true:
|
||||
/// - The user id is not historical
|
||||
/// - The server name of the user id matches this server
|
||||
/// - No user or appservice on this server already claimed this username
|
||||
///
|
||||
/// Note: This will not reserve the username, so the username might become
|
||||
/// invalid when trying to register
|
||||
pub(crate) async fn get_register_available_route(
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
// Validate user id
|
||||
let user_id = UserId::parse_with_server_name(body.username.to_lowercase(), services().globals.server_name())
|
||||
.ok()
|
||||
.filter(|user_id| !user_id.is_historical() && user_is_local(user_id))
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
|
||||
// Check if username is creative enough
|
||||
if services().users.exists(&user_id)? {
|
||||
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
|
||||
}
|
||||
|
||||
if services()
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(user_id.localpart())
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Username is forbidden."));
|
||||
}
|
||||
|
||||
// TODO add check for appservice namespaces
|
||||
|
||||
// If no if check is true we have an username that's available to be used.
|
||||
Ok(get_username_availability::v3::Response {
|
||||
available: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/register`
|
||||
///
|
||||
/// Register an account on this homeserver.
|
||||
///
|
||||
/// You can use [`GET
|
||||
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
||||
/// html) to check if the user id is valid and available.
|
||||
///
|
||||
/// - Only works if registration is enabled
|
||||
/// - If type is guest: ignores all parameters except
|
||||
/// initial_device_display_name
|
||||
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
|
||||
/// - If type is not guest and no username is given: Always fails after UIAA
|
||||
/// check
|
||||
/// - Creates a new account and populates it with default account data
|
||||
/// - If `inhibit_login` is false: Creates a device and returns device id and
|
||||
/// access_token
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> {
|
||||
if !services().globals.allow_registration() && body.appservice_info.is_none() {
|
||||
info!(
|
||||
"Registration disabled and request not from known appservice, rejecting registration attempt for username \
|
||||
{:?}",
|
||||
body.username
|
||||
);
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration has been disabled."));
|
||||
}
|
||||
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
|
||||
if is_guest
|
||||
&& (!services().globals.allow_guest_registration()
|
||||
|| (services().globals.allow_registration() && services().globals.config.registration_token.is_some()))
|
||||
{
|
||||
info!(
|
||||
"Guest registration disabled / registration enabled with token configured, rejecting guest registration, \
|
||||
initial device name: {:?}",
|
||||
body.initial_device_display_name
|
||||
);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::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()? < 2 {
|
||||
warn!(
|
||||
"Guest account attempted to register before a real admin user has been registered, rejecting \
|
||||
registration. Guest's initial device name: {:?}",
|
||||
body.initial_device_display_name
|
||||
);
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration temporarily disabled."));
|
||||
}
|
||||
|
||||
let user_id = match (&body.username, is_guest) {
|
||||
(Some(username), false) => {
|
||||
let proposed_user_id =
|
||||
UserId::parse_with_server_name(username.to_lowercase(), services().globals.server_name())
|
||||
.ok()
|
||||
.filter(|user_id| !user_id.is_historical() && user_is_local(user_id))
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
|
||||
if services().users.exists(&proposed_user_id)? {
|
||||
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
|
||||
}
|
||||
|
||||
if services()
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(proposed_user_id.localpart())
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Username is forbidden."));
|
||||
}
|
||||
|
||||
proposed_user_id
|
||||
},
|
||||
_ => loop {
|
||||
let proposed_user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services().globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
if !services().users.exists(&proposed_user_id)? {
|
||||
break proposed_user_id;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
if let Some(ref info) = body.appservice_info {
|
||||
if !info.is_user_match(&user_id) {
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing appservice token."));
|
||||
}
|
||||
} else if services().appservice.is_exclusive_user_id(&user_id).await {
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
|
||||
}
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo;
|
||||
let skip_auth;
|
||||
if services().globals.config.registration_token.is_some() {
|
||||
// Registration token required
|
||||
uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
skip_auth = body.appservice_info.is_some();
|
||||
} else {
|
||||
// No registration token necessary, but clients must still go through the flow
|
||||
uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::Dummy],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
}
|
||||
|
||||
if !skip_auth {
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services().uiaa.try_auth(
|
||||
&UserId::parse_with_server_name("", services().globals.server_name()).expect("we know this is valid"),
|
||||
"".into(),
|
||||
auth,
|
||||
&uiaainfo,
|
||||
)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services().uiaa.create(
|
||||
&UserId::parse_with_server_name("", services().globals.server_name()).expect("we know this is valid"),
|
||||
"".into(),
|
||||
&uiaainfo,
|
||||
&json,
|
||||
)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
}
|
||||
|
||||
let password = if is_guest {
|
||||
None
|
||||
} else {
|
||||
body.password.as_deref()
|
||||
};
|
||||
|
||||
// Create user
|
||||
services().users.create(&user_id, password)?;
|
||||
|
||||
// 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().globals.new_user_displayname_suffix().is_empty() {
|
||||
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix)
|
||||
.expect("should be able to write to string buffer");
|
||||
}
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()))
|
||||
.await?;
|
||||
|
||||
// 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),
|
||||
},
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)?;
|
||||
|
||||
// Inhibit login does not work for guests
|
||||
if !is_guest && body.inhibit_login {
|
||||
return Ok(register::v3::Response {
|
||||
access_token: None,
|
||||
user_id,
|
||||
device_id: None,
|
||||
refresh_token: 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.clone()
|
||||
}
|
||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||
|
||||
// Generate new token for the device
|
||||
let token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// Create device for this account
|
||||
services()
|
||||
.users
|
||||
.create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone())?;
|
||||
|
||||
debug_info!(%user_id, %device_id, "User account was created");
|
||||
|
||||
// log in conduit admin channel if a non-guest user registered
|
||||
if body.appservice_info.is_none() && !is_guest {
|
||||
info!("New user \"{user_id}\" registered on this server.");
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"New user \"{user_id}\" registered on this server."
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() {
|
||||
info!("New guest user \"{user_id}\" registered on this server.");
|
||||
|
||||
if let Some(device_display_name) = &body.initial_device_display_name {
|
||||
if body
|
||||
.initial_device_display_name
|
||||
.as_ref()
|
||||
.is_some_and(|device_display_name| !device_display_name.is_empty())
|
||||
{
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"Guest user \"{user_id}\" with device display name `{device_display_name}` registered on this \
|
||||
server."
|
||||
)))
|
||||
.await;
|
||||
} else {
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on this server.",
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on this server.",
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the first real user, grant them admin privileges except for guest
|
||||
// users Note: the server user, @conduit:servername, is generated first
|
||||
if !is_guest {
|
||||
if let Some(admin_room) = service::admin::Service::get_admin_room().await? {
|
||||
if services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&admin_room)?
|
||||
== Some(1)
|
||||
{
|
||||
services()
|
||||
.admin
|
||||
.make_user_admin(&user_id, displayname)
|
||||
.await?;
|
||||
|
||||
warn!("Granting {} admin privileges as the first user", user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.appservice_info.is_none()
|
||||
&& !services().globals.config.auto_join_rooms.is_empty()
|
||||
&& (services().globals.allow_guests_auto_join_rooms() || !is_guest)
|
||||
{
|
||||
for room in &services().globals.config.auto_join_rooms {
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services().globals.server_name(), room)?
|
||||
{
|
||||
warn!("Skipping room {room} to automatically join as we have never joined before.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_id_server_name) = room.server_name() {
|
||||
if let Err(e) = join_room_by_id_helper(
|
||||
Some(&user_id),
|
||||
room,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[room_id_server_name.to_owned(), services().globals.server_name().to_owned()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// don't return this error so we don't fail registrations
|
||||
error!("Failed to automatically join room {room} for user {user_id}: {e}");
|
||||
} else {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(register::v3::Response {
|
||||
access_token: Some(token),
|
||||
user_id,
|
||||
device_id: Some(device_id),
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/password`
|
||||
///
|
||||
/// Changes the password of this account.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
/// - Changes the password of the sender user
|
||||
/// - The password hash is calculated using argon2 with 32 character salt, the
|
||||
/// plain password is
|
||||
/// not saved
|
||||
///
|
||||
/// If logout_devices is true it does the following for each device except the
|
||||
/// sender device:
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
pub(crate) async fn change_password_route(
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::Password],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_password(sender_user, Some(&body.new_password))?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
for id in services()
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
.filter(|id| id != sender_device)
|
||||
{
|
||||
services().users.remove_device(sender_user, &id)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("User {} changed their password.", sender_user);
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"User {sender_user} changed their password."
|
||||
)))
|
||||
.await;
|
||||
|
||||
Ok(change_password::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET _matrix/client/r0/account/whoami`
|
||||
///
|
||||
/// Get `user_id` of the sender user.
|
||||
///
|
||||
/// Note: Also works for Application Services
|
||||
pub(crate) async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoami::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let device_id = body.sender_device.clone();
|
||||
|
||||
Ok(whoami::v3::Response {
|
||||
user_id: sender_user.clone(),
|
||||
device_id,
|
||||
is_guest: services().users.is_deactivated(sender_user)? && body.appservice_info.is_none(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||
///
|
||||
/// Deactivate sender user account.
|
||||
///
|
||||
/// - Leaves all rooms and rejects all invitations
|
||||
/// - Invalidates all access tokens
|
||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
/// - Removes ability to log in again
|
||||
pub(crate) async fn deactivate_route(body: Ruma<deactivate::v3::Request>) -> Result<deactivate::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::Password],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
// Make the user leave all rooms before deactivation
|
||||
super::leave_all_rooms(sender_user).await;
|
||||
|
||||
// Remove devices and mark account as deactivated
|
||||
services().users.deactivate_account(sender_user)?;
|
||||
|
||||
info!("User {} deactivated their account.", sender_user);
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"User {sender_user} deactivated their account."
|
||||
)))
|
||||
.await;
|
||||
|
||||
Ok(deactivate::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET _matrix/client/v3/account/3pid`
|
||||
///
|
||||
/// Get a list of third party identifiers associated with this account.
|
||||
///
|
||||
/// - Currently always returns empty list
|
||||
pub(crate) async fn third_party_route(body: Ruma<get_3pids::v3::Request>) -> Result<get_3pids::v3::Response> {
|
||||
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_3pids::v3::Response::new(Vec::new()))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an email
|
||||
/// address to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::ThreepidDenied,
|
||||
"Third party identifier is not allowed",
|
||||
))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an phone
|
||||
/// number to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::ThreepidDenied,
|
||||
"Third party identifier is not allowed",
|
||||
))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// Checks if the provided registration token is valid at the time of checking
|
||||
///
|
||||
/// Currently does not have any ratelimiting, and this isn't very practical as
|
||||
/// there is only one registration token allowed.
|
||||
pub(crate) async fn check_registration_token_validity(
|
||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||
) -> Result<check_registration_token_validity::v1::Response> {
|
||||
let Some(reg_token) = services().globals.config.registration_token.clone() else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Server does not allow token registration.",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response {
|
||||
valid: reg_token == body.token,
|
||||
})
|
||||
}
|
277
src/api/client/alias.rs
Normal file
277
src/api/client/alias.rs
Normal file
|
@ -0,0 +1,277 @@
|
|||
use rand::seq::SliceRandom;
|
||||
use ruma::{
|
||||
api::{
|
||||
appservice,
|
||||
client::{
|
||||
alias::{create_alias, delete_alias, get_alias},
|
||||
error::ErrorKind,
|
||||
},
|
||||
federation,
|
||||
},
|
||||
OwnedRoomAliasId, OwnedServerName, RoomAliasId, RoomId,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
debug_info, debug_warn,
|
||||
service::{appservice::RegistrationInfo, server_is_ours},
|
||||
services, Error, Result, Ruma,
|
||||
};
|
||||
|
||||
/// # `PUT /_matrix/client/v3/directory/room/{roomAlias}`
|
||||
///
|
||||
/// Creates a new room alias on this server.
|
||||
pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) -> Result<create_alias::v3::Response> {
|
||||
alias_checks(&body.room_alias, &body.appservice_info).await?;
|
||||
|
||||
// this isn't apart of alias_checks or delete alias route because we should
|
||||
// allow removing forbidden room aliases
|
||||
if services()
|
||||
.globals
|
||||
.forbidden_alias_names()
|
||||
.is_match(body.room_alias.alias())
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Room alias is forbidden."));
|
||||
}
|
||||
|
||||
if services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&body.room_alias)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::Conflict("Alias already exists."));
|
||||
}
|
||||
|
||||
if services()
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(&body.room_alias, &body.room_id)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Invalid room alias. Alias must be in the form of '#localpart:server_name'",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(create_alias::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/v3/directory/room/{roomAlias}`
|
||||
///
|
||||
/// Deletes a room alias from this server.
|
||||
///
|
||||
/// - TODO: additional access control checks
|
||||
/// - TODO: Update canonical alias event
|
||||
pub(crate) async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) -> Result<delete_alias::v3::Response> {
|
||||
alias_checks(&body.room_alias, &body.appservice_info).await?;
|
||||
if services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&body.room_alias)?
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist."));
|
||||
}
|
||||
|
||||
if services()
|
||||
.rooms
|
||||
.alias
|
||||
.remove_alias(&body.room_alias)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Invalid room alias. Alias must be in the form of '#localpart:server_name'",
|
||||
));
|
||||
};
|
||||
|
||||
// TODO: update alt_aliases?
|
||||
|
||||
Ok(delete_alias::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/directory/room/{roomAlias}`
|
||||
///
|
||||
/// Resolve an alias locally or over federation.
|
||||
pub(crate) async fn get_alias_route(body: Ruma<get_alias::v3::Request>) -> Result<get_alias::v3::Response> {
|
||||
get_alias_helper(body.body.room_alias, None).await
|
||||
}
|
||||
|
||||
pub async fn get_alias_helper(
|
||||
room_alias: OwnedRoomAliasId, servers: Option<Vec<OwnedServerName>>,
|
||||
) -> Result<get_alias::v3::Response> {
|
||||
debug!("get_alias_helper servers: {servers:?}");
|
||||
if !server_is_ours(room_alias.server_name())
|
||||
&& (!servers
|
||||
.as_ref()
|
||||
.is_some_and(|servers| servers.contains(&services().globals.server_name().to_owned()))
|
||||
|| servers.as_ref().is_none())
|
||||
{
|
||||
let mut response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
room_alias.server_name(),
|
||||
federation::query::get_room_information::v1::Request {
|
||||
room_alias: room_alias.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
debug!("room alias server_name get_alias_helper response: {response:?}");
|
||||
|
||||
if let Err(ref e) = response {
|
||||
debug_info!(
|
||||
"Server {} of the original room alias failed to assist in resolving room alias: {e}",
|
||||
room_alias.server_name()
|
||||
);
|
||||
}
|
||||
|
||||
if response.as_ref().is_ok_and(|resp| resp.servers.is_empty()) || response.as_ref().is_err() {
|
||||
if let Some(servers) = servers {
|
||||
for server in servers {
|
||||
response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&server,
|
||||
federation::query::get_room_information::v1::Request {
|
||||
room_alias: room_alias.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
debug!("Got response from server {server} for room aliases: {response:?}");
|
||||
|
||||
if let Ok(ref response) = response {
|
||||
if !response.servers.is_empty() {
|
||||
break;
|
||||
}
|
||||
debug_warn!(
|
||||
"Server {server} responded with room aliases, but was empty? Response: {response:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(response) = response {
|
||||
let room_id = response.room_id;
|
||||
|
||||
let mut pre_servers = response.servers;
|
||||
// since the room alis server responded, insert it into the list
|
||||
pre_servers.push(room_alias.server_name().into());
|
||||
|
||||
let servers = room_available_servers(&room_id, &room_alias, &Some(pre_servers));
|
||||
debug!(
|
||||
"room alias servers from federation response for room ID {room_id} and room alias {room_alias}: \
|
||||
{servers:?}"
|
||||
);
|
||||
|
||||
return Ok(get_alias::v3::Response::new(room_id, servers));
|
||||
}
|
||||
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"No servers could assist in resolving the room alias",
|
||||
));
|
||||
}
|
||||
|
||||
let mut room_id = None;
|
||||
match services().rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
Some(r) => room_id = Some(r),
|
||||
None => {
|
||||
for appservice in services().appservice.read().await.values() {
|
||||
if appservice.aliases.is_match(room_alias.as_str())
|
||||
&& matches!(
|
||||
services()
|
||||
.sending
|
||||
.send_appservice_request(
|
||||
appservice.registration.clone(),
|
||||
appservice::query::query_room_alias::v1::Request {
|
||||
room_alias: room_alias.clone(),
|
||||
},
|
||||
)
|
||||
.await,
|
||||
Ok(Some(_opt_result))
|
||||
) {
|
||||
room_id = Some(
|
||||
services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&room_alias)?
|
||||
.ok_or_else(|| Error::bad_config("Room does not exist."))?,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let Some(room_id) = room_id else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room with alias not found."));
|
||||
};
|
||||
|
||||
let servers = room_available_servers(&room_id, &room_alias, &None);
|
||||
|
||||
debug!("room alias servers for room ID {room_id} and room alias {room_alias}");
|
||||
|
||||
Ok(get_alias::v3::Response::new(room_id, servers))
|
||||
}
|
||||
|
||||
fn room_available_servers(
|
||||
room_id: &RoomId, room_alias: &RoomAliasId, pre_servers: &Option<Vec<OwnedServerName>>,
|
||||
) -> Vec<OwnedServerName> {
|
||||
// find active servers in room state cache to suggest
|
||||
let mut servers: Vec<OwnedServerName> = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_servers(room_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
// push any servers we want in the list already (e.g. responded remote alias
|
||||
// servers, room alias server itself)
|
||||
if let Some(pre_servers) = pre_servers {
|
||||
servers.extend(pre_servers.clone());
|
||||
};
|
||||
|
||||
servers.sort_unstable();
|
||||
servers.dedup();
|
||||
|
||||
// shuffle list of servers randomly after sort and dedupe
|
||||
servers.shuffle(&mut rand::thread_rng());
|
||||
|
||||
// insert our server as the very first choice if in list, else check if we can
|
||||
// prefer the room alias server first
|
||||
if let Some(server_index) = servers
|
||||
.iter()
|
||||
.position(|server_name| server_is_ours(server_name))
|
||||
{
|
||||
servers.swap_remove(server_index);
|
||||
servers.insert(0, services().globals.server_name().to_owned());
|
||||
} else if let Some(alias_server_index) = servers
|
||||
.iter()
|
||||
.position(|server| server == room_alias.server_name())
|
||||
{
|
||||
servers.swap_remove(alias_server_index);
|
||||
servers.insert(0, room_alias.server_name().into());
|
||||
}
|
||||
|
||||
servers
|
||||
}
|
||||
|
||||
async fn alias_checks(room_alias: &RoomAliasId, appservice_info: &Option<RegistrationInfo>) -> Result<()> {
|
||||
if !server_is_ours(room_alias.server_name()) {
|
||||
return Err(Error::BadRequest(ErrorKind::InvalidParam, "Alias is from another server."));
|
||||
}
|
||||
|
||||
if let Some(ref info) = appservice_info {
|
||||
if !info.aliases.is_match(room_alias.as_str()) {
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
|
||||
}
|
||||
} else if services().appservice.is_exclusive_alias(room_alias).await {
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice."));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
369
src/api/client/backup.rs
Normal file
369
src/api/client/backup.rs
Normal file
|
@ -0,0 +1,369 @@
|
|||
use ruma::{
|
||||
api::client::{
|
||||
backup::{
|
||||
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session, create_backup_version,
|
||||
delete_backup_keys, delete_backup_keys_for_room, delete_backup_keys_for_session, delete_backup_version,
|
||||
get_backup_info, get_backup_keys, get_backup_keys_for_room, get_backup_keys_for_session,
|
||||
get_latest_backup_info, update_backup_version,
|
||||
},
|
||||
error::ErrorKind,
|
||||
},
|
||||
UInt,
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Creates a new backup.
|
||||
pub(crate) async fn create_backup_version_route(
|
||||
body: Ruma<create_backup_version::v3::Request>,
|
||||
) -> Result<create_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let version = services()
|
||||
.key_backups
|
||||
.create_backup(sender_user, &body.algorithm)?;
|
||||
|
||||
Ok(create_backup_version::v3::Response {
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
|
||||
///
|
||||
/// Update information about an existing backup. Only `auth_data` can be
|
||||
/// modified.
|
||||
pub(crate) async fn update_backup_version_route(
|
||||
body: Ruma<update_backup_version::v3::Request>,
|
||||
) -> Result<update_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
services()
|
||||
.key_backups
|
||||
.update_backup(sender_user, &body.version, &body.algorithm)?;
|
||||
|
||||
Ok(update_backup_version::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Get information about the latest backup version.
|
||||
pub(crate) async fn get_latest_backup_info_route(
|
||||
body: Ruma<get_latest_backup_info::v3::Request>,
|
||||
) -> Result<get_latest_backup_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let (version, algorithm) = services()
|
||||
.key_backups
|
||||
.get_latest_backup(sender_user)?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Key backup does not exist."))?;
|
||||
|
||||
Ok(get_latest_backup_info::v3::Response {
|
||||
algorithm,
|
||||
count: (UInt::try_from(services().key_backups.count_keys(sender_user, &version)?)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services().key_backups.get_etag(sender_user, &version)?,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/room_keys/version/{version}`
|
||||
///
|
||||
/// Get information about an existing backup.
|
||||
pub(crate) async fn get_backup_info_route(
|
||||
body: Ruma<get_backup_info::v3::Request>,
|
||||
) -> Result<get_backup_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let algorithm = services()
|
||||
.key_backups
|
||||
.get_backup(sender_user, &body.version)?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Key backup does not exist."))?;
|
||||
|
||||
Ok(get_backup_info::v3::Response {
|
||||
algorithm,
|
||||
count: (UInt::try_from(
|
||||
services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
version: body.version.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/version/{version}`
|
||||
///
|
||||
/// Delete an existing key backup.
|
||||
///
|
||||
/// - Deletes both information about the backup, as well as all key data related
|
||||
/// to the backup
|
||||
pub(crate) async fn delete_backup_version_route(
|
||||
body: Ruma<delete_backup_version::v3::Request>,
|
||||
) -> Result<delete_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.delete_backup(sender_user, &body.version)?;
|
||||
|
||||
Ok(delete_backup_version::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Add the received backup keys to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is
|
||||
/// allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
pub(crate) async fn add_backup_keys_route(
|
||||
body: Ruma<add_backup_keys::v3::Request>,
|
||||
) -> Result<add_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services()
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
for (room_id, room) in &body.rooms {
|
||||
for (session_id, key_data) in &room.sessions {
|
||||
services()
|
||||
.key_backups
|
||||
.add_key(sender_user, &body.version, room_id, session_id, key_data)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(add_backup_keys::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Add the received backup keys to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is
|
||||
/// allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
pub(crate) async fn add_backup_keys_for_room_route(
|
||||
body: Ruma<add_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<add_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services()
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
for (session_id, key_data) in &body.sessions {
|
||||
services()
|
||||
.key_backups
|
||||
.add_key(sender_user, &body.version, &body.room_id, session_id, key_data)?;
|
||||
}
|
||||
|
||||
Ok(add_backup_keys_for_room::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Add the received backup key to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is
|
||||
/// allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
pub(crate) async fn add_backup_keys_for_session_route(
|
||||
body: Ruma<add_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<add_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services()
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.add_key(sender_user, &body.version, &body.room_id, &body.session_id, &body.session_data)?;
|
||||
|
||||
Ok(add_backup_keys_for_session::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Retrieves all keys from the backup.
|
||||
pub(crate) async fn get_backup_keys_route(
|
||||
body: Ruma<get_backup_keys::v3::Request>,
|
||||
) -> Result<get_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let rooms = services().key_backups.get_all(sender_user, &body.version)?;
|
||||
|
||||
Ok(get_backup_keys::v3::Response {
|
||||
rooms,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Retrieves all keys from the backup for a given room.
|
||||
pub(crate) async fn get_backup_keys_for_room_route(
|
||||
body: Ruma<get_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<get_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let sessions = services()
|
||||
.key_backups
|
||||
.get_room(sender_user, &body.version, &body.room_id)?;
|
||||
|
||||
Ok(get_backup_keys_for_room::v3::Response {
|
||||
sessions,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Retrieves a key from the backup.
|
||||
pub(crate) async fn get_backup_keys_for_session_route(
|
||||
body: Ruma<get_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<get_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let key_data = services()
|
||||
.key_backups
|
||||
.get_session(sender_user, &body.version, &body.room_id, &body.session_id)?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Backup key not found for this user's session."))?;
|
||||
|
||||
Ok(get_backup_keys_for_session::v3::Response {
|
||||
key_data,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Delete the keys from the backup.
|
||||
pub(crate) async fn delete_backup_keys_route(
|
||||
body: Ruma<delete_backup_keys::v3::Request>,
|
||||
) -> Result<delete_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.delete_all_keys(sender_user, &body.version)?;
|
||||
|
||||
Ok(delete_backup_keys::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Delete the keys from the backup for a given room.
|
||||
pub(crate) async fn delete_backup_keys_for_room_route(
|
||||
body: Ruma<delete_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<delete_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.delete_room_keys(sender_user, &body.version, &body.room_id)?;
|
||||
|
||||
Ok(delete_backup_keys_for_room::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Delete a key from the backup.
|
||||
pub(crate) async fn delete_backup_keys_for_session_route(
|
||||
body: Ruma<delete_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<delete_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.delete_room_key(sender_user, &body.version, &body.room_id, &body.session_id)?;
|
||||
|
||||
Ok(delete_backup_keys_for_session::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
38
src/api/client/capabilities.rs
Normal file
38
src/api/client/capabilities.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::api::client::discovery::get_capabilities::{
|
||||
self, Capabilities, RoomVersionStability, RoomVersionsCapability, ThirdPartyIdChangesCapability,
|
||||
};
|
||||
|
||||
use crate::{services, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/v3/capabilities`
|
||||
///
|
||||
/// Get information on the supported feature set and other relevent capabilities
|
||||
/// of this server.
|
||||
pub(crate) async fn get_capabilities_route(
|
||||
_body: Ruma<get_capabilities::v3::Request>,
|
||||
) -> Result<get_capabilities::v3::Response> {
|
||||
let mut available = BTreeMap::new();
|
||||
for room_version in &services().globals.unstable_room_versions {
|
||||
available.insert(room_version.clone(), RoomVersionStability::Unstable);
|
||||
}
|
||||
for room_version in &services().globals.stable_room_versions {
|
||||
available.insert(room_version.clone(), RoomVersionStability::Stable);
|
||||
}
|
||||
|
||||
let mut capabilities = Capabilities::default();
|
||||
capabilities.room_versions = RoomVersionsCapability {
|
||||
default: services().globals.default_room_version(),
|
||||
available,
|
||||
};
|
||||
|
||||
// conduit does not implement 3PID stuff
|
||||
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
Ok(get_capabilities::v3::Response {
|
||||
capabilities,
|
||||
})
|
||||
}
|
115
src/api/client/config.rs
Normal file
115
src/api/client/config.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use ruma::{
|
||||
api::client::{
|
||||
config::{get_global_account_data, get_room_account_data, set_global_account_data, set_room_account_data},
|
||||
error::ErrorKind,
|
||||
},
|
||||
events::{AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent},
|
||||
serde::Raw,
|
||||
OwnedUserId, RoomId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, value::RawValue as RawJsonValue};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/account_data/{type}`
|
||||
///
|
||||
/// Sets some account data for the sender user.
|
||||
pub(crate) async fn set_global_account_data_route(
|
||||
body: Ruma<set_global_account_data::v3::Request>,
|
||||
) -> Result<set_global_account_data::v3::Response> {
|
||||
set_account_data(None, &body.sender_user, &body.event_type.to_string(), body.data.json())?;
|
||||
|
||||
Ok(set_global_account_data::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
|
||||
///
|
||||
/// Sets some room account data for the sender user.
|
||||
pub(crate) async fn set_room_account_data_route(
|
||||
body: Ruma<set_room_account_data::v3::Request>,
|
||||
) -> Result<set_room_account_data::v3::Response> {
|
||||
set_account_data(
|
||||
Some(&body.room_id),
|
||||
&body.sender_user,
|
||||
&body.event_type.to_string(),
|
||||
body.data.json(),
|
||||
)?;
|
||||
|
||||
Ok(set_room_account_data::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/account_data/{type}`
|
||||
///
|
||||
/// Gets some account data for the sender user.
|
||||
pub(crate) async fn get_global_account_data_route(
|
||||
body: Ruma<get_global_account_data::v3::Request>,
|
||||
) -> Result<get_global_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: Box<RawJsonValue> = services()
|
||||
.account_data
|
||||
.get(None, sender_user, body.event_type.to_string().into())?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<ExtractGlobalEventContent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
Ok(get_global_account_data::v3::Response {
|
||||
account_data,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
|
||||
///
|
||||
/// Gets some room account data for the sender user.
|
||||
pub(crate) async fn get_room_account_data_route(
|
||||
body: Ruma<get_room_account_data::v3::Request>,
|
||||
) -> Result<get_room_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: Box<RawJsonValue> = services()
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, body.event_type.clone())?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<ExtractRoomEventContent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
Ok(get_room_account_data::v3::Response {
|
||||
account_data,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_account_data(
|
||||
room_id: Option<&RoomId>, sender_user: &Option<OwnedUserId>, event_type: &str, data: &RawJsonValue,
|
||||
) -> Result<()> {
|
||||
let sender_user = sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let data: serde_json::Value =
|
||||
serde_json::from_str(data.get()).map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?;
|
||||
|
||||
services().account_data.update(
|
||||
room_id,
|
||||
sender_user,
|
||||
event_type.into(),
|
||||
&json!({
|
||||
"type": event_type,
|
||||
"content": data,
|
||||
}),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExtractRoomEventContent {
|
||||
content: Raw<AnyRoomAccountDataEventContent>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExtractGlobalEventContent {
|
||||
content: Raw<AnyGlobalAccountDataEventContent>,
|
||||
}
|
199
src/api/client/context.rs
Normal file
199
src/api/client/context.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use ruma::{
|
||||
api::client::{context::get_context, error::ErrorKind, filter::LazyLoadOptions},
|
||||
events::StateEventType,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/context`
|
||||
///
|
||||
/// Allows loading room history around an event.
|
||||
///
|
||||
/// - Only works if the user is joined (TODO: always allow, but only show events
|
||||
/// if the user was
|
||||
/// joined, depending on history_visibility)
|
||||
pub(crate) async fn get_context_route(body: Ruma<get_context::v3::Request>) -> Result<get_context::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
let (lazy_load_enabled, lazy_load_send_redundant) = match &body.filter.lazy_load_options {
|
||||
LazyLoadOptions::Enabled {
|
||||
include_redundant_members,
|
||||
} => (true, *include_redundant_members),
|
||||
LazyLoadOptions::Disabled => (false, false),
|
||||
};
|
||||
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
|
||||
let base_token = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Base event id not found."))?;
|
||||
|
||||
let base_event = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Base event not found."))?;
|
||||
|
||||
let room_id = base_event.room_id.clone();
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &body.event_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this event.",
|
||||
));
|
||||
}
|
||||
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
&base_event.sender,
|
||||
)? || lazy_load_send_redundant
|
||||
{
|
||||
lazy_loaded.insert(base_event.sender.as_str().to_owned());
|
||||
}
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = usize::try_from(body.limit).unwrap_or(10).min(100);
|
||||
|
||||
let base_event = base_event.to_room_event();
|
||||
|
||||
let events_before: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_until(sender_user, &room_id, base_token)?
|
||||
.take(limit / 2)
|
||||
.filter_map(Result::ok) // Remove buggy events
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_before {
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
&event.sender,
|
||||
)? || lazy_load_send_redundant
|
||||
{
|
||||
lazy_loaded.insert(event.sender.as_str().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
let start_token = events_before
|
||||
.last()
|
||||
.map_or_else(|| base_token.stringify(), |(count, _)| count.stringify());
|
||||
|
||||
let events_before: Vec<_> = events_before
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
let events_after: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_after(sender_user, &room_id, base_token)?
|
||||
.take(limit / 2)
|
||||
.filter_map(Result::ok) // Remove buggy events
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_after {
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
&event.sender,
|
||||
)? || lazy_load_send_redundant
|
||||
{
|
||||
lazy_loaded.insert(event.sender.as_str().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
let shortstatehash = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.pdu_shortstatehash(
|
||||
events_after
|
||||
.last()
|
||||
.map_or(&*body.event_id, |(_, e)| &*e.event_id),
|
||||
)?
|
||||
.map_or(
|
||||
services()
|
||||
.rooms
|
||||
.state
|
||||
.get_room_shortstatehash(&room_id)?
|
||||
.expect("All rooms have state"),
|
||||
|hash| hash,
|
||||
);
|
||||
|
||||
let state_ids = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_full_ids(shortstatehash)
|
||||
.await?;
|
||||
|
||||
let end_token = events_after
|
||||
.last()
|
||||
.map_or_else(|| base_token.stringify(), |(count, _)| count.stringify());
|
||||
|
||||
let events_after: Vec<_> = events_after
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
let mut state = Vec::with_capacity(state_ids.len());
|
||||
|
||||
for (shortstatekey, id) in state_ids {
|
||||
let (event_type, state_key) = services()
|
||||
.rooms
|
||||
.short
|
||||
.get_statekey_from_short(shortstatekey)?;
|
||||
|
||||
if event_type != StateEventType::RoomMember {
|
||||
let Some(pdu) = services().rooms.timeline.get_pdu(&id)? else {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
continue;
|
||||
};
|
||||
|
||||
state.push(pdu.to_state_event());
|
||||
} else if !lazy_load_enabled || lazy_loaded.contains(&state_key) {
|
||||
let Some(pdu) = services().rooms.timeline.get_pdu(&id)? else {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
continue;
|
||||
};
|
||||
|
||||
state.push(pdu.to_state_event());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(get_context::v3::Response {
|
||||
start: Some(start_token),
|
||||
end: Some(end_token),
|
||||
events_before,
|
||||
event: Some(base_event),
|
||||
events_after,
|
||||
state,
|
||||
})
|
||||
}
|
165
src/api/client/device.rs
Normal file
165
src/api/client/device.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use ruma::api::client::{
|
||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
||||
error::ErrorKind,
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
};
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
use crate::{services, utils, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices`
|
||||
///
|
||||
/// Get metadata on all devices of the sender user.
|
||||
pub(crate) async fn get_devices_route(body: Ruma<get_devices::v3::Request>) -> Result<get_devices::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let devices: Vec<device::Device> = services()
|
||||
.users
|
||||
.all_devices_metadata(sender_user)
|
||||
.filter_map(Result::ok) // Filter out buggy devices
|
||||
.collect();
|
||||
|
||||
Ok(get_devices::v3::Response {
|
||||
devices,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices/{deviceId}`
|
||||
///
|
||||
/// Get metadata on a single device of the sender user.
|
||||
pub(crate) async fn get_device_route(body: Ruma<get_device::v3::Request>) -> Result<get_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let device = services()
|
||||
.users
|
||||
.get_device_metadata(sender_user, &body.body.device_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
|
||||
|
||||
Ok(get_device::v3::Response {
|
||||
device,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
|
||||
///
|
||||
/// Updates the metadata on a given device of the sender user.
|
||||
pub(crate) async fn update_device_route(body: Ruma<update_device::v3::Request>) -> Result<update_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut device = services()
|
||||
.users
|
||||
.get_device_metadata(sender_user, &body.device_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
|
||||
|
||||
device.display_name.clone_from(&body.display_name);
|
||||
|
||||
services()
|
||||
.users
|
||||
.update_device_metadata(sender_user, &body.device_id, &device)?;
|
||||
|
||||
Ok(update_device::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/devices/{deviceId}`
|
||||
///
|
||||
/// Deletes the given device.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
pub(crate) async fn delete_device_route(body: Ruma<delete_device::v3::Request>) -> Result<delete_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::Password],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
services()
|
||||
.users
|
||||
.remove_device(sender_user, &body.device_id)?;
|
||||
|
||||
Ok(delete_device::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
|
||||
///
|
||||
/// Deletes the given device.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
///
|
||||
/// For each device:
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
pub(crate) async fn delete_devices_route(
|
||||
body: Ruma<delete_devices::v3::Request>,
|
||||
) -> Result<delete_devices::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::Password],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
for device_id in &body.devices {
|
||||
services().users.remove_device(sender_user, device_id)?;
|
||||
}
|
||||
|
||||
Ok(delete_devices::v3::Response {})
|
||||
}
|
402
src/api/client/directory.rs
Normal file
402
src/api/client/directory.rs
Normal file
|
@ -0,0 +1,402 @@
|
|||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
directory::{get_public_rooms, get_public_rooms_filtered, get_room_visibility, set_room_visibility},
|
||||
error::ErrorKind,
|
||||
room,
|
||||
},
|
||||
federation,
|
||||
},
|
||||
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork},
|
||||
events::{
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
guest_access::{GuestAccess, RoomGuestAccessEventContent},
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
StateEventType,
|
||||
},
|
||||
uint, ServerName, UInt,
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{service::server_is_ours, services, Error, Result, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/v3/publicRooms`
|
||||
///
|
||||
/// Lists the public rooms on this server.
|
||||
///
|
||||
/// - Rooms are ordered by the number of joined members
|
||||
pub(crate) async fn get_public_rooms_filtered_route(
|
||||
body: Ruma<get_public_rooms_filtered::v3::Request>,
|
||||
) -> Result<get_public_rooms_filtered::v3::Response> {
|
||||
if let Some(server) = &body.server {
|
||||
if services()
|
||||
.globals
|
||||
.forbidden_remote_room_directory_server_names()
|
||||
.contains(server)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Server is banned on this homeserver.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let response = get_public_rooms_filtered_helper(
|
||||
body.server.as_deref(),
|
||||
body.limit,
|
||||
body.since.as_deref(),
|
||||
&body.filter,
|
||||
&body.room_network,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to return our /publicRooms: {e}");
|
||||
Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/publicRooms`
|
||||
///
|
||||
/// Lists the public rooms on this server.
|
||||
///
|
||||
/// - Rooms are ordered by the number of joined members
|
||||
pub(crate) async fn get_public_rooms_route(
|
||||
body: Ruma<get_public_rooms::v3::Request>,
|
||||
) -> Result<get_public_rooms::v3::Response> {
|
||||
if let Some(server) = &body.server {
|
||||
if services()
|
||||
.globals
|
||||
.forbidden_remote_room_directory_server_names()
|
||||
.contains(server)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Server is banned on this homeserver.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let response = get_public_rooms_filtered_helper(
|
||||
body.server.as_deref(),
|
||||
body.limit,
|
||||
body.since.as_deref(),
|
||||
&Filter::default(),
|
||||
&RoomNetwork::Matrix,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to return our /publicRooms: {e}");
|
||||
Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
|
||||
})?;
|
||||
|
||||
Ok(get_public_rooms::v3::Response {
|
||||
chunk: response.chunk,
|
||||
prev_batch: response.prev_batch,
|
||||
next_batch: response.next_batch,
|
||||
total_room_count_estimate: response.total_room_count_estimate,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
|
||||
///
|
||||
/// Sets the visibility of a given room in the room directory.
|
||||
///
|
||||
/// - TODO: Access control checks
|
||||
pub(crate) async fn set_room_visibility_route(
|
||||
body: Ruma<set_room_visibility::v3::Request>,
|
||||
) -> Result<set_room_visibility::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services().rooms.metadata.exists(&body.room_id)? {
|
||||
// Return 404 if the room doesn't exist
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
|
||||
}
|
||||
|
||||
match &body.visibility {
|
||||
room::Visibility::Public => {
|
||||
if services().globals.config.lockdown_public_room_directory && !services().users.is_admin(sender_user)? {
|
||||
info!(
|
||||
"Non-admin user {sender_user} tried to publish {0} to the room directory while \
|
||||
\"lockdown_public_room_directory\" is enabled",
|
||||
body.room_id
|
||||
);
|
||||
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Publishing rooms to the room directory is not allowed",
|
||||
));
|
||||
}
|
||||
|
||||
services().rooms.directory.set_public(&body.room_id)?;
|
||||
info!("{sender_user} made {0} public", body.room_id);
|
||||
},
|
||||
room::Visibility::Private => services().rooms.directory.set_not_public(&body.room_id)?,
|
||||
_ => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Room visibility type is not supported.",
|
||||
));
|
||||
},
|
||||
}
|
||||
|
||||
Ok(set_room_visibility::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/directory/list/room/{roomId}`
|
||||
///
|
||||
/// Gets the visibility of a given room in the room directory.
|
||||
pub(crate) async fn get_room_visibility_route(
|
||||
body: Ruma<get_room_visibility::v3::Request>,
|
||||
) -> Result<get_room_visibility::v3::Response> {
|
||||
if !services().rooms.metadata.exists(&body.room_id)? {
|
||||
// Return 404 if the room doesn't exist
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
|
||||
}
|
||||
|
||||
Ok(get_room_visibility::v3::Response {
|
||||
visibility: if services().rooms.directory.is_public_room(&body.room_id)? {
|
||||
room::Visibility::Public
|
||||
} else {
|
||||
room::Visibility::Private
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_public_rooms_filtered_helper(
|
||||
server: Option<&ServerName>, limit: Option<UInt>, since: Option<&str>, filter: &Filter, _network: &RoomNetwork,
|
||||
) -> Result<get_public_rooms_filtered::v3::Response> {
|
||||
if let Some(other_server) = server.filter(|server_name| !server_is_ours(server_name)) {
|
||||
let response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
other_server,
|
||||
federation::directory::get_public_rooms_filtered::v1::Request {
|
||||
limit,
|
||||
since: since.map(ToOwned::to_owned),
|
||||
filter: Filter {
|
||||
generic_search_term: filter.generic_search_term.clone(),
|
||||
room_types: filter.room_types.clone(),
|
||||
},
|
||||
room_network: RoomNetwork::Matrix,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(get_public_rooms_filtered::v3::Response {
|
||||
chunk: response.chunk,
|
||||
prev_batch: response.prev_batch,
|
||||
next_batch: response.next_batch,
|
||||
total_room_count_estimate: response.total_room_count_estimate,
|
||||
});
|
||||
}
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = limit.map_or(10, u64::from);
|
||||
let mut num_since: u64 = 0;
|
||||
|
||||
if let Some(s) = &since {
|
||||
let mut characters = s.chars();
|
||||
let backwards = match characters.next() {
|
||||
Some('n') => false,
|
||||
Some('p') => true,
|
||||
_ => return Err(Error::BadRequest(ErrorKind::InvalidParam, "Invalid `since` token")),
|
||||
};
|
||||
|
||||
num_since = characters
|
||||
.collect::<String>()
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `since` token."))?;
|
||||
|
||||
if backwards {
|
||||
num_since = num_since.saturating_sub(limit);
|
||||
}
|
||||
}
|
||||
|
||||
let mut all_rooms: Vec<_> = services()
|
||||
.rooms
|
||||
.directory
|
||||
.public_rooms()
|
||||
.map(|room_id| {
|
||||
let room_id = room_id?;
|
||||
|
||||
let chunk = PublicRoomsChunk {
|
||||
canonical_alias: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")?
|
||||
.map_or(Ok(None), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomCanonicalAliasEventContent| c.alias)
|
||||
.map_err(|_| Error::bad_database("Invalid canonical alias event in database."))
|
||||
})?,
|
||||
name: services().rooms.state_accessor.get_name(&room_id)?,
|
||||
num_joined_members: services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&room_id)?
|
||||
.unwrap_or_else(|| {
|
||||
warn!("Room {} has no member count", room_id);
|
||||
0
|
||||
})
|
||||
.try_into()
|
||||
.expect("user count should not be that big"),
|
||||
topic: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomTopic, "")?
|
||||
.map_or(Ok(None), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomTopicEventContent| Some(c.topic))
|
||||
.map_err(|e| {
|
||||
error!("Invalid room topic event in database for room {room_id}: {e}");
|
||||
Error::bad_database("Invalid room topic event in database.")
|
||||
})
|
||||
})
|
||||
.unwrap_or(None),
|
||||
world_readable: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
|
||||
.map_or(Ok(false), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomHistoryVisibilityEventContent| {
|
||||
c.history_visibility == HistoryVisibility::WorldReadable
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Invalid room history visibility event in database for room {room_id}, assuming is \"shared\": {e}",
|
||||
);
|
||||
Error::bad_database("Invalid room history visibility event in database.")
|
||||
})}).unwrap_or(false),
|
||||
guest_can_join: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")?
|
||||
.map_or(Ok(false), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin)
|
||||
.map_err(|_| Error::bad_database("Invalid room guest access event in database."))
|
||||
})?,
|
||||
avatar_url: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomAvatar, "")?
|
||||
.map(|s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomAvatarEventContent| c.url)
|
||||
.map_err(|_| Error::bad_database("Invalid room avatar event in database."))
|
||||
})
|
||||
.transpose()?
|
||||
// url is now an Option<String> so we must flatten
|
||||
.flatten(),
|
||||
join_rule: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomJoinRules, "")?
|
||||
.map(|s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||
JoinRule::Public => Some(PublicRoomJoinRule::Public),
|
||||
JoinRule::Knock => Some(PublicRoomJoinRule::Knock),
|
||||
_ => None,
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("Invalid room join rule event in database: {}", e);
|
||||
Error::BadDatabase("Invalid room join rule event in database.")
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.flatten()
|
||||
.ok_or_else(|| Error::bad_database("Missing room join rule event for room."))?,
|
||||
room_type: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")?
|
||||
.map(|s| {
|
||||
serde_json::from_str::<RoomCreateEventContent>(s.content.get()).map_err(|e| {
|
||||
error!("Invalid room create event in database: {}", e);
|
||||
Error::BadDatabase("Invalid room create event in database.")
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.and_then(|e| e.room_type),
|
||||
room_id,
|
||||
};
|
||||
Ok(chunk)
|
||||
})
|
||||
.filter_map(|r: Result<_>| r.ok()) // Filter out buggy rooms
|
||||
.filter(|chunk| {
|
||||
if let Some(query) = filter.generic_search_term.as_ref().map(|q| q.to_lowercase()) {
|
||||
if let Some(name) = &chunk.name {
|
||||
if name.as_str().to_lowercase().contains(&query) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(topic) = &chunk.topic {
|
||||
if topic.to_lowercase().contains(&query) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(canonical_alias) = &chunk.canonical_alias {
|
||||
if canonical_alias.as_str().to_lowercase().contains(&query) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
} else {
|
||||
// No search term
|
||||
true
|
||||
}
|
||||
})
|
||||
// We need to collect all, so we can sort by member count
|
||||
.collect();
|
||||
|
||||
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));
|
||||
|
||||
let total_room_count_estimate = UInt::try_from(all_rooms.len()).unwrap_or_else(|_| uint!(0));
|
||||
|
||||
let chunk: Vec<_> = all_rooms
|
||||
.into_iter()
|
||||
.skip(
|
||||
num_since
|
||||
.try_into()
|
||||
.expect("num_since should not be this high"),
|
||||
)
|
||||
.take(limit.try_into().expect("limit should not be this high"))
|
||||
.collect();
|
||||
|
||||
let prev_batch = if num_since == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(format!("p{num_since}"))
|
||||
};
|
||||
|
||||
let next_batch = if chunk.len() < limit.try_into().unwrap() {
|
||||
None
|
||||
} else {
|
||||
Some(format!(
|
||||
"n{}",
|
||||
num_since
|
||||
.checked_add(limit)
|
||||
.expect("num_since and limit should not be that large")
|
||||
))
|
||||
};
|
||||
|
||||
Ok(get_public_rooms_filtered::v3::Response {
|
||||
chunk,
|
||||
prev_batch,
|
||||
next_batch,
|
||||
total_room_count_estimate: Some(total_room_count_estimate),
|
||||
})
|
||||
}
|
30
src/api/client/filter.rs
Normal file
30
src/api/client/filter.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
filter::{create_filter, get_filter},
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/filter/{filterId}`
|
||||
///
|
||||
/// Loads a filter that was previously created.
|
||||
///
|
||||
/// - A user can only access their own filters
|
||||
pub(crate) async fn get_filter_route(body: Ruma<get_filter::v3::Request>) -> Result<get_filter::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let Some(filter) = services().users.get_filter(sender_user, &body.filter_id)? else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Filter not found."));
|
||||
};
|
||||
|
||||
Ok(get_filter::v3::Response::new(filter))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/filter`
|
||||
///
|
||||
/// Creates a new filter to be used by other endpoints.
|
||||
pub(crate) async fn create_filter_route(body: Ruma<create_filter::v3::Request>) -> Result<create_filter::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
Ok(create_filter::v3::Response::new(
|
||||
services().users.create_filter(sender_user, &body.filter)?,
|
||||
))
|
||||
}
|
519
src/api/client/keys.rs
Normal file
519
src/api/client/keys.rs
Normal file
|
@ -0,0 +1,519 @@
|
|||
use std::{
|
||||
cmp,
|
||||
collections::{hash_map, BTreeMap, HashMap, HashSet},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
keys::{claim_keys, get_key_changes, get_keys, upload_keys, upload_signatures, upload_signing_keys},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
federation,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tracing::debug;
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
use crate::{
|
||||
service::user_is_local,
|
||||
services,
|
||||
utils::{self},
|
||||
Error, Result, Ruma,
|
||||
};
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/upload`
|
||||
///
|
||||
/// Publish end-to-end encryption keys for the sender device.
|
||||
///
|
||||
/// - Adds one time keys
|
||||
/// - If there are no device keys yet: Adds device keys (TODO: merge with
|
||||
/// existing keys?)
|
||||
pub(crate) async fn upload_keys_route(body: Ruma<upload_keys::v3::Request>) -> Result<upload_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
for (key_key, key_value) in &body.one_time_keys {
|
||||
services()
|
||||
.users
|
||||
.add_one_time_key(sender_user, sender_device, key_key, key_value)?;
|
||||
}
|
||||
|
||||
if let Some(device_keys) = &body.device_keys {
|
||||
// TODO: merge this and the existing event?
|
||||
// This check is needed to assure that signatures are kept
|
||||
if services()
|
||||
.users
|
||||
.get_device_keys(sender_user, sender_device)?
|
||||
.is_none()
|
||||
{
|
||||
services()
|
||||
.users
|
||||
.add_device_keys(sender_user, sender_device, device_keys)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(upload_keys::v3::Response {
|
||||
one_time_key_counts: services()
|
||||
.users
|
||||
.count_one_time_keys(sender_user, sender_device)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/query`
|
||||
///
|
||||
/// Get end-to-end encryption keys for the given users.
|
||||
///
|
||||
/// - Always fetches users from other servers over federation
|
||||
/// - Gets master keys, self-signing keys, user signing keys and device keys.
|
||||
/// - The master and self-signing keys contain signatures that the user is
|
||||
/// allowed to see
|
||||
pub(crate) async fn get_keys_route(body: Ruma<get_keys::v3::Request>) -> Result<get_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
get_keys_helper(
|
||||
Some(sender_user),
|
||||
&body.device_keys,
|
||||
|u| u == sender_user,
|
||||
true, // Always allow local users to see device names of other local users
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/claim`
|
||||
///
|
||||
/// Claims one-time keys
|
||||
pub(crate) async fn claim_keys_route(body: Ruma<claim_keys::v3::Request>) -> Result<claim_keys::v3::Response> {
|
||||
claim_keys_helper(&body.one_time_keys).await
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/device_signing/upload`
|
||||
///
|
||||
/// Uploads end-to-end key information for the sender user.
|
||||
///
|
||||
/// - Requires UIAA to verify password
|
||||
pub(crate) async fn upload_signing_keys_route(
|
||||
body: Ruma<upload_signing_keys::v3::Request>,
|
||||
) -> Result<upload_signing_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::Password],
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
if let Some(master_key) = &body.master_key {
|
||||
services().users.add_cross_signing_keys(
|
||||
sender_user,
|
||||
master_key,
|
||||
&body.self_signing_key,
|
||||
&body.user_signing_key,
|
||||
true, // notify so that other users see the new keys
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(upload_signing_keys::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/signatures/upload`
|
||||
///
|
||||
/// Uploads end-to-end key signatures from the sender user.
|
||||
pub(crate) async fn upload_signatures_route(
|
||||
body: Ruma<upload_signatures::v3::Request>,
|
||||
) -> Result<upload_signatures::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
for (user_id, keys) in &body.signed_keys {
|
||||
for (key_id, key) in keys {
|
||||
let key = serde_json::to_value(key)
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid key JSON"))?;
|
||||
|
||||
for signature in key
|
||||
.get("signatures")
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Missing signatures field."))?
|
||||
.get(sender_user.to_string())
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid user in signatures field."))?
|
||||
.as_object()
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid signature."))?
|
||||
.clone()
|
||||
{
|
||||
// Signature validation?
|
||||
let signature = (
|
||||
signature.0,
|
||||
signature
|
||||
.1
|
||||
.as_str()
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid signature value."))?
|
||||
.to_owned(),
|
||||
);
|
||||
services()
|
||||
.users
|
||||
.sign_key(user_id, key_id, signature, sender_user)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(upload_signatures::v3::Response {
|
||||
failures: BTreeMap::new(), // TODO: integrate
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/changes`
|
||||
///
|
||||
/// Gets a list of users who have updated their device identity keys since the
|
||||
/// previous sync token.
|
||||
///
|
||||
/// - TODO: left users
|
||||
pub(crate) async fn get_key_changes_route(
|
||||
body: Ruma<get_key_changes::v3::Request>,
|
||||
) -> Result<get_key_changes::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut device_list_updates = HashSet::new();
|
||||
|
||||
device_list_updates.extend(
|
||||
services()
|
||||
.users
|
||||
.keys_changed(
|
||||
sender_user.as_str(),
|
||||
body.from
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?,
|
||||
Some(
|
||||
body.to
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?,
|
||||
),
|
||||
)
|
||||
.filter_map(Result::ok),
|
||||
);
|
||||
|
||||
for room_id in services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
device_list_updates.extend(
|
||||
services()
|
||||
.users
|
||||
.keys_changed(
|
||||
room_id.as_ref(),
|
||||
body.from
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?,
|
||||
Some(
|
||||
body.to
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?,
|
||||
),
|
||||
)
|
||||
.filter_map(Result::ok),
|
||||
);
|
||||
}
|
||||
Ok(get_key_changes::v3::Response {
|
||||
changed: device_list_updates.into_iter().collect(),
|
||||
left: Vec::new(), // TODO
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
||||
sender_user: Option<&UserId>, device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>, allowed_signatures: F,
|
||||
include_display_names: bool,
|
||||
) -> Result<get_keys::v3::Response> {
|
||||
let mut master_keys = BTreeMap::new();
|
||||
let mut self_signing_keys = BTreeMap::new();
|
||||
let mut user_signing_keys = BTreeMap::new();
|
||||
let mut device_keys = BTreeMap::new();
|
||||
|
||||
let mut get_over_federation = HashMap::new();
|
||||
|
||||
for (user_id, device_ids) in device_keys_input {
|
||||
let user_id: &UserId = user_id;
|
||||
|
||||
if !user_is_local(user_id) {
|
||||
get_over_federation
|
||||
.entry(user_id.server_name())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((user_id, device_ids));
|
||||
continue;
|
||||
}
|
||||
|
||||
if device_ids.is_empty() {
|
||||
let mut container = BTreeMap::new();
|
||||
for device_id in services().users.all_device_ids(user_id) {
|
||||
let device_id = device_id?;
|
||||
if let Some(mut keys) = services().users.get_device_keys(user_id, &device_id)? {
|
||||
let metadata = services()
|
||||
.users
|
||||
.get_device_metadata(user_id, &device_id)?
|
||||
.ok_or_else(|| Error::bad_database("all_device_keys contained nonexistent device."))?;
|
||||
|
||||
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
|
||||
.map_err(|_| Error::bad_database("invalid device keys in database"))?;
|
||||
|
||||
container.insert(device_id, keys);
|
||||
}
|
||||
}
|
||||
device_keys.insert(user_id.to_owned(), container);
|
||||
} else {
|
||||
for device_id in device_ids {
|
||||
let mut container = BTreeMap::new();
|
||||
if let Some(mut keys) = services().users.get_device_keys(user_id, device_id)? {
|
||||
let metadata = services()
|
||||
.users
|
||||
.get_device_metadata(user_id, device_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Tried to get keys for nonexistent device.",
|
||||
))?;
|
||||
|
||||
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
|
||||
.map_err(|_| Error::bad_database("invalid device keys in database"))?;
|
||||
container.insert(device_id.to_owned(), keys);
|
||||
}
|
||||
device_keys.insert(user_id.to_owned(), container);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(master_key) = services()
|
||||
.users
|
||||
.get_master_key(sender_user, user_id, &allowed_signatures)?
|
||||
{
|
||||
master_keys.insert(user_id.to_owned(), master_key);
|
||||
}
|
||||
if let Some(self_signing_key) =
|
||||
services()
|
||||
.users
|
||||
.get_self_signing_key(sender_user, user_id, &allowed_signatures)?
|
||||
{
|
||||
self_signing_keys.insert(user_id.to_owned(), self_signing_key);
|
||||
}
|
||||
if Some(user_id) == sender_user {
|
||||
if let Some(user_signing_key) = services().users.get_user_signing_key(user_id)? {
|
||||
user_signing_keys.insert(user_id.to_owned(), user_signing_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut failures = BTreeMap::new();
|
||||
|
||||
let back_off = |id| async {
|
||||
match services()
|
||||
.globals
|
||||
.bad_query_ratelimiter
|
||||
.write()
|
||||
.await
|
||||
.entry(id)
|
||||
{
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
e.insert((Instant::now(), 1));
|
||||
},
|
||||
hash_map::Entry::Occupied(mut e) => {
|
||||
*e.get_mut() = (Instant::now(), e.get().1.saturating_add(1));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let mut futures: FuturesUnordered<_> = get_over_federation
|
||||
.into_iter()
|
||||
.map(|(server, vec)| async move {
|
||||
if let Some((time, tries)) = services()
|
||||
.globals
|
||||
.bad_query_ratelimiter
|
||||
.read()
|
||||
.await
|
||||
.get(server)
|
||||
{
|
||||
// Exponential backoff
|
||||
const MAX_DURATION: Duration = Duration::from_secs(60 * 60 * 24);
|
||||
let min_elapsed_duration = cmp::min(MAX_DURATION, Duration::from_secs(5 * 60) * (*tries) * (*tries));
|
||||
|
||||
if time.elapsed() < min_elapsed_duration {
|
||||
debug!("Backing off query from {:?}", server);
|
||||
return (server, Err(Error::BadServerResponse("bad query, still backing off")));
|
||||
}
|
||||
}
|
||||
|
||||
let mut device_keys_input_fed = BTreeMap::new();
|
||||
for (user_id, keys) in vec {
|
||||
device_keys_input_fed.insert(user_id.to_owned(), keys.clone());
|
||||
}
|
||||
|
||||
let request = federation::keys::get_keys::v1::Request {
|
||||
device_keys: device_keys_input_fed,
|
||||
};
|
||||
let response = services()
|
||||
.sending
|
||||
.send_federation_request(server, request)
|
||||
.await;
|
||||
|
||||
(server, Ok(response))
|
||||
})
|
||||
.collect();
|
||||
|
||||
while let Some((server, response)) = futures.next().await {
|
||||
if let Ok(Ok(response)) = response {
|
||||
for (user, masterkey) in response.master_keys {
|
||||
let (master_key_id, mut master_key) = services().users.parse_master_key(&user, &masterkey)?;
|
||||
|
||||
if let Some(our_master_key) =
|
||||
services()
|
||||
.users
|
||||
.get_key(&master_key_id, sender_user, &user, &allowed_signatures)?
|
||||
{
|
||||
let (_, our_master_key) = services().users.parse_master_key(&user, &our_master_key)?;
|
||||
master_key.signatures.extend(our_master_key.signatures);
|
||||
}
|
||||
let json = serde_json::to_value(master_key).expect("to_value always works");
|
||||
let raw = serde_json::from_value(json).expect("Raw::from_value always works");
|
||||
services().users.add_cross_signing_keys(
|
||||
&user, &raw, &None, &None,
|
||||
false, /* Dont notify. A notification would trigger another key request resulting in an
|
||||
* endless loop */
|
||||
)?;
|
||||
master_keys.insert(user.clone(), raw);
|
||||
}
|
||||
|
||||
self_signing_keys.extend(response.self_signing_keys);
|
||||
device_keys.extend(response.device_keys);
|
||||
} else {
|
||||
back_off(server.to_owned()).await;
|
||||
failures.insert(server.to_string(), json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(get_keys::v3::Response {
|
||||
failures,
|
||||
device_keys,
|
||||
master_keys,
|
||||
self_signing_keys,
|
||||
user_signing_keys,
|
||||
})
|
||||
}
|
||||
|
||||
fn add_unsigned_device_display_name(
|
||||
keys: &mut Raw<ruma::encryption::DeviceKeys>, metadata: ruma::api::client::device::Device,
|
||||
include_display_names: bool,
|
||||
) -> serde_json::Result<()> {
|
||||
if let Some(display_name) = metadata.display_name {
|
||||
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
|
||||
|
||||
let unsigned = object.entry("unsigned").or_insert_with(|| json!({}));
|
||||
if let serde_json::Value::Object(unsigned_object) = unsigned {
|
||||
if include_display_names {
|
||||
unsigned_object.insert("device_display_name".to_owned(), display_name.into());
|
||||
} else {
|
||||
unsigned_object.insert(
|
||||
"device_display_name".to_owned(),
|
||||
Some(metadata.device_id.as_str().to_owned()).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
*keys = Raw::from_json(serde_json::value::to_raw_value(&object)?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn claim_keys_helper(
|
||||
one_time_keys_input: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, DeviceKeyAlgorithm>>,
|
||||
) -> Result<claim_keys::v3::Response> {
|
||||
let mut one_time_keys = BTreeMap::new();
|
||||
|
||||
let mut get_over_federation = BTreeMap::new();
|
||||
|
||||
for (user_id, map) in one_time_keys_input {
|
||||
if !user_is_local(user_id) {
|
||||
get_over_federation
|
||||
.entry(user_id.server_name())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((user_id, map));
|
||||
}
|
||||
|
||||
let mut container = BTreeMap::new();
|
||||
for (device_id, key_algorithm) in map {
|
||||
if let Some(one_time_keys) = services()
|
||||
.users
|
||||
.take_one_time_key(user_id, device_id, key_algorithm)?
|
||||
{
|
||||
let mut c = BTreeMap::new();
|
||||
c.insert(one_time_keys.0, one_time_keys.1);
|
||||
container.insert(device_id.clone(), c);
|
||||
}
|
||||
}
|
||||
one_time_keys.insert(user_id.clone(), container);
|
||||
}
|
||||
|
||||
let mut failures = BTreeMap::new();
|
||||
|
||||
let mut futures: FuturesUnordered<_> = get_over_federation
|
||||
.into_iter()
|
||||
.map(|(server, vec)| async move {
|
||||
let mut one_time_keys_input_fed = BTreeMap::new();
|
||||
for (user_id, keys) in vec {
|
||||
one_time_keys_input_fed.insert(user_id.clone(), keys.clone());
|
||||
}
|
||||
(
|
||||
server,
|
||||
services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
server,
|
||||
federation::keys::claim_keys::v1::Request {
|
||||
one_time_keys: one_time_keys_input_fed,
|
||||
},
|
||||
)
|
||||
.await,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
while let Some((server, response)) = futures.next().await {
|
||||
match response {
|
||||
Ok(keys) => {
|
||||
one_time_keys.extend(keys.one_time_keys);
|
||||
},
|
||||
Err(_e) => {
|
||||
failures.insert(server.to_string(), json!({}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(claim_keys::v3::Response {
|
||||
failures,
|
||||
one_time_keys,
|
||||
})
|
||||
}
|
743
src/api/client/media.rs
Normal file
743
src/api/client/media.rs
Normal file
|
@ -0,0 +1,743 @@
|
|||
use std::{io::Cursor, sync::Arc, time::Duration};
|
||||
|
||||
use image::io::Reader as ImgReader;
|
||||
use ipaddress::IPAddress;
|
||||
use reqwest::Url;
|
||||
use ruma::api::client::{
|
||||
error::{ErrorKind, RetryAfter},
|
||||
media::{
|
||||
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
|
||||
get_media_preview,
|
||||
},
|
||||
};
|
||||
use tracing::{debug, error, warn};
|
||||
use webpage::HTML;
|
||||
|
||||
use crate::{
|
||||
debug_warn,
|
||||
service::{
|
||||
media::{FileMeta, UrlPreviewData},
|
||||
server_is_ours,
|
||||
},
|
||||
services,
|
||||
utils::{
|
||||
self,
|
||||
content_disposition::{content_disposition_type, make_content_disposition, sanitise_filename},
|
||||
},
|
||||
Error, Result, Ruma, RumaResponse,
|
||||
};
|
||||
|
||||
/// generated MXC ID (`media-id`) length
|
||||
const MXC_LENGTH: usize = 32;
|
||||
|
||||
/// Cache control for immutable objects
|
||||
const CACHE_CONTROL_IMMUTABLE: &str = "public,max-age=31536000,immutable";
|
||||
|
||||
const CORP_CROSS_ORIGIN: &str = "cross-origin";
|
||||
|
||||
/// # `GET /_matrix/media/v3/config`
|
||||
///
|
||||
/// Returns max upload size.
|
||||
pub(crate) async fn get_media_config_route(
|
||||
_body: Ruma<get_media_config::v3::Request>,
|
||||
) -> Result<get_media_config::v3::Response> {
|
||||
Ok(get_media_config::v3::Response {
|
||||
upload_size: services().globals.max_request_size().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v1/config`
|
||||
///
|
||||
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
|
||||
/// clients may call. conduwuit adds these for compatibility purposes.
|
||||
/// See <https://spec.matrix.org/legacy/legacy/#id27>
|
||||
///
|
||||
/// Returns max upload size.
|
||||
pub(crate) async fn get_media_config_v1_route(
|
||||
body: Ruma<get_media_config::v3::Request>,
|
||||
) -> Result<RumaResponse<get_media_config::v3::Response>> {
|
||||
get_media_config_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v3/preview_url`
|
||||
///
|
||||
/// Returns URL preview.
|
||||
pub(crate) async fn get_media_preview_route(
|
||||
body: Ruma<get_media_preview::v3::Request>,
|
||||
) -> Result<get_media_preview::v3::Response> {
|
||||
let url = &body.url;
|
||||
if !url_preview_allowed(url) {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "URL is not allowed to be previewed"));
|
||||
}
|
||||
|
||||
match get_url_preview(url).await {
|
||||
Ok(preview) => {
|
||||
let res = serde_json::value::to_raw_value(&preview).map_err(|e| {
|
||||
error!("Failed to convert UrlPreviewData into a serde json value: {}", e);
|
||||
Error::BadRequest(
|
||||
ErrorKind::LimitExceeded {
|
||||
retry_after: Some(RetryAfter::Delay(Duration::from_secs(5))),
|
||||
},
|
||||
"Failed to generate a URL preview, try again later.",
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(get_media_preview::v3::Response::from_raw_value(res))
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Failed to generate a URL preview: {e}");
|
||||
|
||||
// there doesn't seem to be an agreed-upon error code in the spec.
|
||||
// the only response codes in the preview_url spec page are 200 and 429.
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::LimitExceeded {
|
||||
retry_after: Some(RetryAfter::Delay(Duration::from_secs(5))),
|
||||
},
|
||||
"Failed to generate a URL preview, try again later.",
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v1/preview_url`
|
||||
///
|
||||
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
|
||||
/// clients may call. conduwuit adds these for compatibility purposes.
|
||||
/// See <https://spec.matrix.org/legacy/legacy/#id27>
|
||||
///
|
||||
/// Returns URL preview.
|
||||
pub(crate) async fn get_media_preview_v1_route(
|
||||
body: Ruma<get_media_preview::v3::Request>,
|
||||
) -> Result<RumaResponse<get_media_preview::v3::Response>> {
|
||||
get_media_preview_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/media/v3/upload`
|
||||
///
|
||||
/// Permanently save media in the server.
|
||||
///
|
||||
/// - Some metadata will be saved in the database
|
||||
/// - Media will be saved in the media/ directory
|
||||
pub(crate) async fn create_content_route(
|
||||
body: Ruma<create_content::v3::Request>,
|
||||
) -> Result<create_content::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mxc = format!(
|
||||
"mxc://{}/{}",
|
||||
services().globals.server_name(),
|
||||
utils::random_string(MXC_LENGTH)
|
||||
);
|
||||
|
||||
services()
|
||||
.media
|
||||
.create(
|
||||
Some(sender_user.clone()),
|
||||
mxc.clone(),
|
||||
body.filename
|
||||
.as_ref()
|
||||
.map(|filename| {
|
||||
format!(
|
||||
"{}; filename={}",
|
||||
content_disposition_type(&body.content_type),
|
||||
sanitise_filename(filename.to_owned())
|
||||
)
|
||||
})
|
||||
.as_deref(),
|
||||
body.content_type.as_deref(),
|
||||
&body.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(create_content::v3::Response {
|
||||
content_uri: mxc.into(),
|
||||
blurhash: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/media/v1/upload`
|
||||
///
|
||||
/// Permanently save media in the server.
|
||||
///
|
||||
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
|
||||
/// clients may call. conduwuit adds these for compatibility purposes.
|
||||
/// See <https://spec.matrix.org/legacy/legacy/#id27>
|
||||
///
|
||||
/// - Some metadata will be saved in the database
|
||||
/// - Media will be saved in the media/ directory
|
||||
pub(crate) async fn create_content_v1_route(
|
||||
body: Ruma<create_content::v3::Request>,
|
||||
) -> Result<RumaResponse<create_content::v3::Response>> {
|
||||
create_content_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media from our server or over federation.
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
/// - Only redirects if `allow_redirect` is true
|
||||
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
|
||||
/// seconds
|
||||
pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> Result<get_content::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_type,
|
||||
file,
|
||||
content_disposition,
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
{
|
||||
let content_disposition = Some(make_content_disposition(&content_type, content_disposition, None));
|
||||
|
||||
Ok(get_content::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
|
||||
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
|
||||
})
|
||||
} else if !server_is_ours(&body.server_name) && body.allow_remote {
|
||||
let response = get_remote_content(
|
||||
&mxc,
|
||||
&body.server_name,
|
||||
body.media_id.clone(),
|
||||
body.allow_redirect,
|
||||
body.timeout_ms,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
|
||||
Error::BadRequest(ErrorKind::NotFound, "Remote media error.")
|
||||
})?;
|
||||
|
||||
let content_disposition = Some(make_content_disposition(
|
||||
&response.content_type,
|
||||
response.content_disposition,
|
||||
None,
|
||||
));
|
||||
|
||||
Ok(get_content::v3::Response {
|
||||
file: response.file,
|
||||
content_type: response.content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
|
||||
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v1/download/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media from our server or over federation.
|
||||
///
|
||||
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
|
||||
/// clients may call. conduwuit adds these for compatibility purposes.
|
||||
/// See <https://spec.matrix.org/legacy/legacy/#id27>
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
/// - Only redirects if `allow_redirect` is true
|
||||
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
|
||||
/// seconds
|
||||
pub(crate) async fn get_content_v1_route(
|
||||
body: Ruma<get_content::v3::Request>,
|
||||
) -> Result<RumaResponse<get_content::v3::Response>> {
|
||||
get_content_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}/{fileName}`
|
||||
///
|
||||
/// Load media from our server or over federation, permitting desired filename.
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
/// - Only redirects if `allow_redirect` is true
|
||||
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
|
||||
/// seconds
|
||||
pub(crate) async fn get_content_as_filename_route(
|
||||
body: Ruma<get_content_as_filename::v3::Request>,
|
||||
) -> Result<get_content_as_filename::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_type,
|
||||
file,
|
||||
content_disposition,
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
{
|
||||
let content_disposition = Some(make_content_disposition(
|
||||
&content_type,
|
||||
content_disposition,
|
||||
Some(body.filename.clone()),
|
||||
));
|
||||
|
||||
Ok(get_content_as_filename::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
|
||||
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
|
||||
})
|
||||
} else if !server_is_ours(&body.server_name) && body.allow_remote {
|
||||
match get_remote_content(
|
||||
&mxc,
|
||||
&body.server_name,
|
||||
body.media_id.clone(),
|
||||
body.allow_redirect,
|
||||
body.timeout_ms,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(remote_content_response) => {
|
||||
let content_disposition = Some(make_content_disposition(
|
||||
&remote_content_response.content_type,
|
||||
remote_content_response.content_disposition,
|
||||
None,
|
||||
));
|
||||
|
||||
Ok(get_content_as_filename::v3::Response {
|
||||
content_disposition,
|
||||
content_type: remote_content_response.content_type,
|
||||
file: remote_content_response.file,
|
||||
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
|
||||
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
|
||||
})
|
||||
},
|
||||
Err(e) => {
|
||||
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Remote media error."))
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v1/download/{serverName}/{mediaId}/{fileName}`
|
||||
///
|
||||
/// Load media from our server or over federation, permitting desired filename.
|
||||
///
|
||||
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
|
||||
/// clients may call. conduwuit adds these for compatibility purposes.
|
||||
/// See <https://spec.matrix.org/legacy/legacy/#id27>
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
/// - Only redirects if `allow_redirect` is true
|
||||
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
|
||||
/// seconds
|
||||
pub(crate) async fn get_content_as_filename_v1_route(
|
||||
body: Ruma<get_content_as_filename::v3::Request>,
|
||||
) -> Result<RumaResponse<get_content_as_filename::v3::Response>> {
|
||||
get_content_as_filename_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v3/thumbnail/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media thumbnail from our server or over federation.
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
/// - Only redirects if `allow_redirect` is true
|
||||
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
|
||||
/// seconds
|
||||
pub(crate) async fn get_content_thumbnail_route(
|
||||
body: Ruma<get_content_thumbnail::v3::Request>,
|
||||
) -> Result<get_content_thumbnail::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_type,
|
||||
file,
|
||||
content_disposition,
|
||||
}) = services()
|
||||
.media
|
||||
.get_thumbnail(
|
||||
mxc.clone(),
|
||||
body.width
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
|
||||
body.height
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid."))?,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let content_disposition = Some(make_content_disposition(&content_type, content_disposition, None));
|
||||
|
||||
Ok(get_content_thumbnail::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
|
||||
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
|
||||
content_disposition,
|
||||
})
|
||||
} else if !server_is_ours(&body.server_name) && body.allow_remote {
|
||||
if services()
|
||||
.globals
|
||||
.prevent_media_downloads_from()
|
||||
.contains(&body.server_name)
|
||||
{
|
||||
// we'll lie to the client and say the blocked server's media was not found and
|
||||
// log. the client has no way of telling anyways so this is a security bonus.
|
||||
debug_warn!("Received request for media `{}` on blocklisted server", mxc);
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
|
||||
}
|
||||
|
||||
match services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&body.server_name,
|
||||
get_content_thumbnail::v3::Request {
|
||||
allow_remote: body.allow_remote,
|
||||
height: body.height,
|
||||
width: body.width,
|
||||
method: body.method.clone(),
|
||||
server_name: body.server_name.clone(),
|
||||
media_id: body.media_id.clone(),
|
||||
timeout_ms: body.timeout_ms,
|
||||
allow_redirect: body.allow_redirect,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(get_thumbnail_response) => {
|
||||
services()
|
||||
.media
|
||||
.upload_thumbnail(
|
||||
None,
|
||||
mxc,
|
||||
None,
|
||||
get_thumbnail_response.content_type.as_deref(),
|
||||
body.width.try_into().expect("all UInts are valid u32s"),
|
||||
body.height.try_into().expect("all UInts are valid u32s"),
|
||||
&get_thumbnail_response.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let content_disposition = Some(make_content_disposition(
|
||||
&get_thumbnail_response.content_type,
|
||||
get_thumbnail_response.content_disposition,
|
||||
None,
|
||||
));
|
||||
|
||||
Ok(get_content_thumbnail::v3::Response {
|
||||
file: get_thumbnail_response.file,
|
||||
content_type: get_thumbnail_response.content_type,
|
||||
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
|
||||
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
|
||||
content_disposition,
|
||||
})
|
||||
},
|
||||
Err(e) => {
|
||||
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Remote media error."))
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v1/thumbnail/{serverName}/{mediaId}`
|
||||
///
|
||||
/// Load media thumbnail from our server or over federation.
|
||||
///
|
||||
/// This is a legacy endpoint ("/v1/") that some very old homeservers and/or
|
||||
/// clients may call. conduwuit adds these for compatibility purposes.
|
||||
/// See <https://spec.matrix.org/legacy/legacy/#id27>
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
/// - Only redirects if `allow_redirect` is true
|
||||
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
|
||||
/// seconds
|
||||
pub(crate) async fn get_content_thumbnail_v1_route(
|
||||
body: Ruma<get_content_thumbnail::v3::Request>,
|
||||
) -> Result<RumaResponse<get_content_thumbnail::v3::Response>> {
|
||||
get_content_thumbnail_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
async fn get_remote_content(
|
||||
mxc: &str, server_name: &ruma::ServerName, media_id: String, allow_redirect: bool, timeout_ms: Duration,
|
||||
) -> Result<get_content::v3::Response, Error> {
|
||||
if services()
|
||||
.globals
|
||||
.prevent_media_downloads_from()
|
||||
.contains(&server_name.to_owned())
|
||||
{
|
||||
// we'll lie to the client and say the blocked server's media was not found and
|
||||
// log. the client has no way of telling anyways so this is a security bonus.
|
||||
debug_warn!("Received request for media `{mxc}` on blocklisted server");
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
|
||||
}
|
||||
|
||||
let content_response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
server_name,
|
||||
get_content::v3::Request {
|
||||
allow_remote: true,
|
||||
server_name: server_name.to_owned(),
|
||||
media_id,
|
||||
timeout_ms,
|
||||
allow_redirect,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let content_disposition = Some(make_content_disposition(
|
||||
&content_response.content_type,
|
||||
content_response.content_disposition,
|
||||
None,
|
||||
));
|
||||
|
||||
services()
|
||||
.media
|
||||
.create(
|
||||
None,
|
||||
mxc.to_owned(),
|
||||
content_disposition.as_deref(),
|
||||
content_response.content_type.as_deref(),
|
||||
&content_response.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(get_content::v3::Response {
|
||||
file: content_response.file,
|
||||
content_type: content_response.content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
|
||||
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn download_image(client: &reqwest::Client, url: &str) -> Result<UrlPreviewData> {
|
||||
let image = client.get(url).send().await?.bytes().await?;
|
||||
let mxc = format!(
|
||||
"mxc://{}/{}",
|
||||
services().globals.server_name(),
|
||||
utils::random_string(MXC_LENGTH)
|
||||
);
|
||||
|
||||
services()
|
||||
.media
|
||||
.create(None, mxc.clone(), None, None, &image)
|
||||
.await?;
|
||||
|
||||
let (width, height) = match ImgReader::new(Cursor::new(&image)).with_guessed_format() {
|
||||
Err(_) => (None, None),
|
||||
Ok(reader) => match reader.into_dimensions() {
|
||||
Err(_) => (None, None),
|
||||
Ok((width, height)) => (Some(width), Some(height)),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(UrlPreviewData {
|
||||
image: Some(mxc),
|
||||
image_size: Some(image.len()),
|
||||
image_width: width,
|
||||
image_height: height,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn download_html(client: &reqwest::Client, url: &str) -> Result<UrlPreviewData> {
|
||||
let mut response = client.get(url).send().await?;
|
||||
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
while let Some(chunk) = response.chunk().await? {
|
||||
bytes.extend_from_slice(&chunk);
|
||||
if bytes.len() > services().globals.url_preview_max_spider_size() {
|
||||
debug!(
|
||||
"Response body from URL {} exceeds url_preview_max_spider_size ({}), not processing the rest of the \
|
||||
response body and assuming our necessary data is in this range.",
|
||||
url,
|
||||
services().globals.url_preview_max_spider_size()
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let body = String::from_utf8_lossy(&bytes);
|
||||
let Ok(html) = HTML::from_string(body.to_string(), Some(url.to_owned())) else {
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Failed to parse HTML"));
|
||||
};
|
||||
|
||||
let mut data = match html.opengraph.images.first() {
|
||||
None => UrlPreviewData::default(),
|
||||
Some(obj) => download_image(client, &obj.url).await?,
|
||||
};
|
||||
|
||||
let props = html.opengraph.properties;
|
||||
|
||||
/* use OpenGraph title/description, but fall back to HTML if not available */
|
||||
data.title = props.get("title").cloned().or(html.title);
|
||||
data.description = props.get("description").cloned().or(html.description);
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn request_url_preview(url: &str) -> Result<UrlPreviewData> {
|
||||
if let Ok(ip) = IPAddress::parse(url) {
|
||||
if !services().globals.valid_cidr_range(&ip) {
|
||||
return Err(Error::BadServerResponse("Requesting from this address is forbidden"));
|
||||
}
|
||||
}
|
||||
|
||||
let client = &services().globals.client.url_preview;
|
||||
let response = client.head(url).send().await?;
|
||||
|
||||
if let Some(remote_addr) = response.remote_addr() {
|
||||
if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) {
|
||||
if !services().globals.valid_cidr_range(&ip) {
|
||||
return Err(Error::BadServerResponse("Requesting from this address is forbidden"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(content_type) = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|x| x.to_str().ok())
|
||||
else {
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Unknown Content-Type"));
|
||||
};
|
||||
let data = match content_type {
|
||||
html if html.starts_with("text/html") => download_html(client, url).await?,
|
||||
img if img.starts_with("image/") => download_image(client, url).await?,
|
||||
_ => return Err(Error::BadRequest(ErrorKind::Unknown, "Unsupported Content-Type")),
|
||||
};
|
||||
|
||||
services().media.set_url_preview(url, &data).await?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn get_url_preview(url: &str) -> Result<UrlPreviewData> {
|
||||
if let Some(preview) = services().media.get_url_preview(url).await {
|
||||
return Ok(preview);
|
||||
}
|
||||
|
||||
// ensure that only one request is made per URL
|
||||
let mutex_request = Arc::clone(
|
||||
services()
|
||||
.media
|
||||
.url_preview_mutex
|
||||
.write()
|
||||
.await
|
||||
.entry(url.to_owned())
|
||||
.or_default(),
|
||||
);
|
||||
let _request_lock = mutex_request.lock().await;
|
||||
|
||||
match services().media.get_url_preview(url).await {
|
||||
Some(preview) => Ok(preview),
|
||||
None => request_url_preview(url).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn url_preview_allowed(url_str: &str) -> bool {
|
||||
let url: Url = match Url::parse(url_str) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse URL from a str: {}", e);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
if ["http", "https"]
|
||||
.iter()
|
||||
.all(|&scheme| scheme != url.scheme().to_lowercase())
|
||||
{
|
||||
debug!("Ignoring non-HTTP/HTTPS URL to preview: {}", url);
|
||||
return false;
|
||||
}
|
||||
|
||||
let host = match url.host_str() {
|
||||
None => {
|
||||
debug!("Ignoring URL preview for a URL that does not have a host (?): {}", url);
|
||||
return false;
|
||||
},
|
||||
Some(h) => h.to_owned(),
|
||||
};
|
||||
|
||||
let allowlist_domain_contains = services().globals.url_preview_domain_contains_allowlist();
|
||||
let allowlist_domain_explicit = services().globals.url_preview_domain_explicit_allowlist();
|
||||
let denylist_domain_explicit = services().globals.url_preview_domain_explicit_denylist();
|
||||
let allowlist_url_contains = services().globals.url_preview_url_contains_allowlist();
|
||||
|
||||
if allowlist_domain_contains.contains(&"*".to_owned())
|
||||
|| allowlist_domain_explicit.contains(&"*".to_owned())
|
||||
|| allowlist_url_contains.contains(&"*".to_owned())
|
||||
{
|
||||
debug!("Config key contains * which is allowing all URL previews. Allowing URL {}", url);
|
||||
return true;
|
||||
}
|
||||
|
||||
if !host.is_empty() {
|
||||
if denylist_domain_explicit.contains(&host) {
|
||||
debug!(
|
||||
"Host {} is not allowed by url_preview_domain_explicit_denylist (check 1/4)",
|
||||
&host
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if allowlist_domain_explicit.contains(&host) {
|
||||
debug!("Host {} is allowed by url_preview_domain_explicit_allowlist (check 2/4)", &host);
|
||||
return true;
|
||||
}
|
||||
|
||||
if allowlist_domain_contains
|
||||
.iter()
|
||||
.any(|domain_s| domain_s.contains(&host.clone()))
|
||||
{
|
||||
debug!("Host {} is allowed by url_preview_domain_contains_allowlist (check 3/4)", &host);
|
||||
return true;
|
||||
}
|
||||
|
||||
if allowlist_url_contains
|
||||
.iter()
|
||||
.any(|url_s| url.to_string().contains(&url_s.to_string()))
|
||||
{
|
||||
debug!("URL {} is allowed by url_preview_url_contains_allowlist (check 4/4)", &host);
|
||||
return true;
|
||||
}
|
||||
|
||||
// check root domain if available and if user has root domain checks
|
||||
if services().globals.url_preview_check_root_domain() {
|
||||
debug!("Checking root domain");
|
||||
match host.split_once('.') {
|
||||
None => return false,
|
||||
Some((_, root_domain)) => {
|
||||
if denylist_domain_explicit.contains(&root_domain.to_owned()) {
|
||||
debug!(
|
||||
"Root domain {} is not allowed by url_preview_domain_explicit_denylist (check 1/3)",
|
||||
&root_domain
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if allowlist_domain_explicit.contains(&root_domain.to_owned()) {
|
||||
debug!(
|
||||
"Root domain {} is allowed by url_preview_domain_explicit_allowlist (check 2/3)",
|
||||
&root_domain
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if allowlist_domain_contains
|
||||
.iter()
|
||||
.any(|domain_s| domain_s.contains(&root_domain.to_owned()))
|
||||
{
|
||||
debug!(
|
||||
"Root domain {} is allowed by url_preview_domain_contains_allowlist (check 3/3)",
|
||||
&root_domain
|
||||
);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
1788
src/api/client/membership.rs
Normal file
1788
src/api/client/membership.rs
Normal file
File diff suppressed because it is too large
Load diff
287
src/api/client/message.rs
Normal file
287
src/api/client/message.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use conduit::PduCount;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
filter::{RoomEventFilter, UrlFilter},
|
||||
message::{get_message_events, send_message_event},
|
||||
},
|
||||
events::{MessageLikeEventType, StateEventType},
|
||||
RoomId, UserId,
|
||||
};
|
||||
use serde_json::{from_str, Value};
|
||||
|
||||
use crate::{service::pdu::PduBuilder, services, utils, Error, PduEvent, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
|
||||
///
|
||||
/// Send a message event into the room.
|
||||
///
|
||||
/// - Is a NOOP if the txn id was already used before and returns the same event
|
||||
/// id again
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is
|
||||
/// allowed
|
||||
pub(crate) async fn send_message_event_route(
|
||||
body: Ruma<send_message_event::v3::Request>,
|
||||
) -> Result<send_message_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_deref();
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(body.room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
// Forbid m.room.encrypted if encryption is disabled
|
||||
if MessageLikeEventType::RoomEncrypted == body.event_type && !services().globals.allow_encryption() {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Encryption has been disabled"));
|
||||
}
|
||||
|
||||
if body.event_type == MessageLikeEventType::CallInvite
|
||||
&& services().rooms.directory.is_public_room(&body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Room call invites are not allowed in public rooms",
|
||||
));
|
||||
}
|
||||
|
||||
// Check if this is a new transaction id
|
||||
if let Some(response) = services()
|
||||
.transaction_ids
|
||||
.existing_txnid(sender_user, sender_device, &body.txn_id)?
|
||||
{
|
||||
// The client might have sent a txnid of the /sendToDevice endpoint
|
||||
// This txnid has no response associated with it
|
||||
if response.is_empty() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Tried to use txn id already used for an incompatible endpoint.",
|
||||
));
|
||||
}
|
||||
|
||||
let event_id = utils::string_from_bytes(&response)
|
||||
.map_err(|_| Error::bad_database("Invalid txnid bytes in database."))?
|
||||
.try_into()
|
||||
.map_err(|_| Error::bad_database("Invalid event id in txnid data."))?;
|
||||
return Ok(send_message_event::v3::Response {
|
||||
event_id,
|
||||
});
|
||||
}
|
||||
|
||||
let mut unsigned = BTreeMap::new();
|
||||
unsigned.insert("transaction_id".to_owned(), body.txn_id.to_string().into());
|
||||
|
||||
let event_id = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: body.event_type.to_string().into(),
|
||||
content: from_str(body.body.body.json().get())
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
|
||||
unsigned: Some(unsigned),
|
||||
state_key: None,
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
services()
|
||||
.transaction_ids
|
||||
.add_txnid(sender_user, sender_device, &body.txn_id, event_id.as_bytes())?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
Ok(send_message_event::v3::Response::new((*event_id).to_owned()))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/messages`
|
||||
///
|
||||
/// Allows paginating through room history.
|
||||
///
|
||||
/// - Only works if the user is joined (TODO: always allow, but only show events
|
||||
/// where the user was
|
||||
/// joined, depending on `history_visibility`)
|
||||
pub(crate) async fn get_message_events_route(
|
||||
body: Ruma<get_message_events::v3::Request>,
|
||||
) -> Result<get_message_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
let from = match body.from.clone() {
|
||||
Some(from) => PduCount::try_from_string(&from)?,
|
||||
None => match body.dir {
|
||||
ruma::api::Direction::Forward => PduCount::min(),
|
||||
ruma::api::Direction::Backward => PduCount::max(),
|
||||
},
|
||||
};
|
||||
|
||||
let to = body
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|t| PduCount::try_from_string(t).ok());
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.lazy_loading
|
||||
.lazy_load_confirm_delivery(sender_user, sender_device, &body.room_id, from)
|
||||
.await?;
|
||||
|
||||
let limit = usize::try_from(body.limit).unwrap_or(10).min(100);
|
||||
|
||||
let next_token;
|
||||
|
||||
let mut resp = get_message_events::v3::Response::new();
|
||||
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
|
||||
match body.dir {
|
||||
ruma::api::Direction::Forward => {
|
||||
let events_after: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_after(sender_user, &body.room_id, from)?
|
||||
.filter_map(Result::ok) // Filter out buggy events
|
||||
.filter(|(_, pdu)| { contains_url_filter(pdu, &body.filter) && visibility_filter(pdu, sender_user, &body.room_id)
|
||||
|
||||
})
|
||||
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
|
||||
.take(limit)
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_after {
|
||||
/* TODO: Remove the not "element_hacks" check when these are resolved:
|
||||
* https://github.com/vector-im/element-android/issues/3417
|
||||
* https://github.com/vector-im/element-web/issues/21034
|
||||
*/
|
||||
if !cfg!(feature = "element_hacks")
|
||||
&& !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
&event.sender,
|
||||
)? {
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
next_token = events_after.last().map(|(count, _)| count).copied();
|
||||
|
||||
let events_after: Vec<_> = events_after
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
resp.start = from.stringify();
|
||||
resp.end = next_token.map(|count| count.stringify());
|
||||
resp.chunk = events_after;
|
||||
},
|
||||
ruma::api::Direction::Backward => {
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.backfill_if_required(&body.room_id, from)
|
||||
.await?;
|
||||
let events_before: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_until(sender_user, &body.room_id, from)?
|
||||
.filter_map(Result::ok) // Filter out buggy events
|
||||
.filter(|(_, pdu)| {contains_url_filter(pdu, &body.filter) && visibility_filter(pdu, sender_user, &body.room_id)})
|
||||
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
|
||||
.take(limit)
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_before {
|
||||
/* TODO: Remove the not "element_hacks" check when these are resolved:
|
||||
* https://github.com/vector-im/element-android/issues/3417
|
||||
* https://github.com/vector-im/element-web/issues/21034
|
||||
*/
|
||||
if !cfg!(feature = "element_hacks")
|
||||
&& !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
&event.sender,
|
||||
)? {
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
next_token = events_before.last().map(|(count, _)| count).copied();
|
||||
|
||||
let events_before: Vec<_> = events_before
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
resp.start = from.stringify();
|
||||
resp.end = next_token.map(|count| count.stringify());
|
||||
resp.chunk = events_before;
|
||||
},
|
||||
}
|
||||
|
||||
resp.state = Vec::new();
|
||||
for ll_id in &lazy_loaded {
|
||||
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
|
||||
&body.room_id,
|
||||
&StateEventType::RoomMember,
|
||||
ll_id.as_str(),
|
||||
)? {
|
||||
resp.state.push(member_event.to_state_event());
|
||||
}
|
||||
}
|
||||
|
||||
// remove the feature check when we are sure clients like element can handle it
|
||||
if !cfg!(feature = "element_hacks") {
|
||||
if let Some(next_token) = next_token {
|
||||
services()
|
||||
.rooms
|
||||
.lazy_loading
|
||||
.lazy_load_mark_sent(sender_user, sender_device, &body.room_id, lazy_loaded, next_token)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
fn visibility_filter(pdu: &PduEvent, user_id: &UserId, room_id: &RoomId) -> bool {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(user_id, room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn contains_url_filter(pdu: &PduEvent, filter: &RoomEventFilter) -> bool {
|
||||
if filter.url_filter.is_none() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let content: Value = from_str(pdu.content.get()).unwrap();
|
||||
match filter.url_filter {
|
||||
Some(UrlFilter::EventsWithoutUrl) => !content["url"].is_string(),
|
||||
Some(UrlFilter::EventsWithUrl) => content["url"].is_string(),
|
||||
None => true,
|
||||
}
|
||||
}
|
82
src/api/client/mod.rs
Normal file
82
src/api/client/mod.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
pub(super) mod account;
|
||||
pub(super) mod alias;
|
||||
pub(super) mod backup;
|
||||
pub(super) mod capabilities;
|
||||
pub(super) mod config;
|
||||
pub(super) mod context;
|
||||
pub(super) mod device;
|
||||
pub(super) mod directory;
|
||||
pub(super) mod filter;
|
||||
pub(super) mod keys;
|
||||
pub(super) mod media;
|
||||
pub(super) mod membership;
|
||||
pub(super) mod message;
|
||||
pub(super) mod presence;
|
||||
pub(super) mod profile;
|
||||
pub(super) mod push;
|
||||
pub(super) mod read_marker;
|
||||
pub(super) mod redact;
|
||||
pub(super) mod relations;
|
||||
pub(super) mod report;
|
||||
pub(super) mod room;
|
||||
pub(super) mod search;
|
||||
pub(super) mod session;
|
||||
pub(super) mod space;
|
||||
pub(super) mod state;
|
||||
pub(super) mod sync;
|
||||
pub(super) mod tag;
|
||||
pub(super) mod thirdparty;
|
||||
pub(super) mod threads;
|
||||
pub(super) mod to_device;
|
||||
pub(super) mod typing;
|
||||
pub(super) mod unstable;
|
||||
pub(super) mod unversioned;
|
||||
pub(super) mod user_directory;
|
||||
pub(super) mod voip;
|
||||
|
||||
pub(super) use account::*;
|
||||
pub use alias::get_alias_helper;
|
||||
pub(super) use alias::*;
|
||||
pub(super) use backup::*;
|
||||
pub(super) use capabilities::*;
|
||||
pub(super) use config::*;
|
||||
pub(super) use context::*;
|
||||
pub(super) use device::*;
|
||||
pub(super) use directory::*;
|
||||
pub(super) use filter::*;
|
||||
pub(super) use keys::*;
|
||||
pub(super) use media::*;
|
||||
pub(super) use membership::*;
|
||||
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room};
|
||||
pub(super) use message::*;
|
||||
pub(super) use presence::*;
|
||||
pub(super) use profile::*;
|
||||
pub(super) use push::*;
|
||||
pub(super) use read_marker::*;
|
||||
pub(super) use redact::*;
|
||||
pub(super) use relations::*;
|
||||
pub(super) use report::*;
|
||||
pub(super) use room::*;
|
||||
pub(super) use search::*;
|
||||
pub(super) use session::*;
|
||||
pub(super) use space::*;
|
||||
pub(super) use state::*;
|
||||
pub(super) use sync::*;
|
||||
pub(super) use tag::*;
|
||||
pub(super) use thirdparty::*;
|
||||
pub(super) use threads::*;
|
||||
pub(super) use to_device::*;
|
||||
pub(super) use typing::*;
|
||||
pub(super) use unstable::*;
|
||||
pub(super) use unversioned::*;
|
||||
pub(super) use user_directory::*;
|
||||
pub(super) use voip::*;
|
||||
|
||||
/// generated device ID length
|
||||
const DEVICE_ID_LENGTH: usize = 10;
|
||||
|
||||
/// generated user access token length
|
||||
const TOKEN_LENGTH: usize = 32;
|
||||
|
||||
/// generated user session ID length
|
||||
const SESSION_ID_LENGTH: usize = service::uiaa::SESSION_ID_LENGTH;
|
79
src/api/client/presence.rs
Normal file
79
src/api/client/presence.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
presence::{get_presence, set_presence},
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/presence/{userId}/status`
|
||||
///
|
||||
/// Sets the presence state of the sender user.
|
||||
pub(crate) async fn set_presence_route(body: Ruma<set_presence::v3::Request>) -> Result<set_presence::v3::Response> {
|
||||
if !services().globals.allow_local_presence() {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Presence is disabled on this server"));
|
||||
}
|
||||
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
services()
|
||||
.presence
|
||||
.set_presence(sender_user, &body.presence, None, None, body.status_msg.clone())?;
|
||||
|
||||
Ok(set_presence::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/presence/{userId}/status`
|
||||
///
|
||||
/// Gets the presence state of the given user.
|
||||
///
|
||||
/// - Only works if you share a room with the user
|
||||
pub(crate) async fn get_presence_route(body: Ruma<get_presence::v3::Request>) -> Result<get_presence::v3::Response> {
|
||||
if !services().globals.allow_local_presence() {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Presence is disabled on this server"));
|
||||
}
|
||||
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut presence_event = None;
|
||||
|
||||
for _room_id in services()
|
||||
.rooms
|
||||
.user
|
||||
.get_shared_rooms(vec![sender_user.clone(), body.user_id.clone()])?
|
||||
{
|
||||
if let Some(presence) = services().presence.get_presence(&body.user_id)? {
|
||||
presence_event = Some(presence);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(presence) = presence_event {
|
||||
let status_msg = if presence
|
||||
.content
|
||||
.status_msg
|
||||
.as_ref()
|
||||
.is_some_and(String::is_empty)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
presence.content.status_msg
|
||||
};
|
||||
|
||||
Ok(get_presence::v3::Response {
|
||||
// TODO: Should ruma just use the presenceeventcontent type here?
|
||||
status_msg,
|
||||
currently_active: presence.content.currently_active,
|
||||
last_active_ago: presence
|
||||
.content
|
||||
.last_active_ago
|
||||
.map(|millis| Duration::from_millis(millis.into())),
|
||||
presence: presence.content.presence,
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Presence state for this user was not found",
|
||||
))
|
||||
}
|
||||
}
|
365
src/api/client/profile.rs
Normal file
365
src/api/client/profile.rs
Normal file
|
@ -0,0 +1,365 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
profile::{get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name},
|
||||
},
|
||||
federation,
|
||||
},
|
||||
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
|
||||
presence::PresenceState,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
service::{pdu::PduBuilder, user_is_local},
|
||||
services, Error, Result, Ruma,
|
||||
};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/profile/{userId}/displayname`
|
||||
///
|
||||
/// Updates the displayname.
|
||||
///
|
||||
/// - Also makes sure other users receive the update using presence EDUs
|
||||
pub(crate) async fn set_displayname_route(
|
||||
body: Ruma<set_display_name::v3::Request>,
|
||||
) -> Result<set_display_name::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_displayname(sender_user, body.displayname.clone())
|
||||
.await?;
|
||||
|
||||
// Send a new membership event and presence update into all joined rooms
|
||||
let all_rooms_joined: Vec<_> = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
.map(|room_id| {
|
||||
Ok::<_, Error>((
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
displayname: body.displayname.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
..serde_json::from_str(
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
|
||||
.ok_or_else(|| {
|
||||
Error::bad_database("Tried to send displayname update for user not in the room.")
|
||||
})?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
},
|
||||
room_id,
|
||||
))
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
for (pdu_builder, room_id) in all_rooms_joined {
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
if let Err(e) = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
|
||||
.await
|
||||
{
|
||||
warn!(%e, "Failed to update/send new display name in room");
|
||||
}
|
||||
}
|
||||
|
||||
if services().globals.allow_local_presence() {
|
||||
// Presence update
|
||||
services()
|
||||
.presence
|
||||
.ping_presence(sender_user, &PresenceState::Online)?;
|
||||
}
|
||||
|
||||
Ok(set_display_name::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/profile/{userId}/displayname`
|
||||
///
|
||||
/// Returns the displayname of the user.
|
||||
///
|
||||
/// - If user is on another server and we do not have a local copy already
|
||||
/// fetch displayname over federation
|
||||
pub(crate) async fn get_displayname_route(
|
||||
body: Ruma<get_display_name::v3::Request>,
|
||||
) -> Result<get_display_name::v3::Response> {
|
||||
if !user_is_local(&body.user_id) {
|
||||
// Create and update our local copy of the user
|
||||
if let Ok(response) = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
body.user_id.server_name(),
|
||||
federation::query::get_profile_information::v1::Request {
|
||||
user_id: body.user_id.clone(),
|
||||
field: None, // we want the full user's profile to update locally too
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
services().users.create(&body.user_id, None)?;
|
||||
}
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_displayname(&body.user_id, response.displayname.clone())
|
||||
.await?;
|
||||
services()
|
||||
.users
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone())
|
||||
.await?;
|
||||
services()
|
||||
.users
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone())
|
||||
.await?;
|
||||
|
||||
return Ok(get_display_name::v3::Response {
|
||||
displayname: response.displayname,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
// Return 404 if this user doesn't exist and we couldn't fetch it over
|
||||
// federation
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
|
||||
}
|
||||
|
||||
Ok(get_display_name::v3::Response {
|
||||
displayname: services().users.displayname(&body.user_id)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/v3/profile/{userId}/avatar_url`
|
||||
///
|
||||
/// Updates the `avatar_url` and `blurhash`.
|
||||
///
|
||||
/// - Also makes sure other users receive the update using presence EDUs
|
||||
pub(crate) async fn set_avatar_url_route(
|
||||
body: Ruma<set_avatar_url::v3::Request>,
|
||||
) -> Result<set_avatar_url::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_avatar_url(sender_user, body.avatar_url.clone())
|
||||
.await?;
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_blurhash(sender_user, body.blurhash.clone())
|
||||
.await?;
|
||||
|
||||
// Send a new membership event and presence update into all joined rooms
|
||||
let all_joined_rooms: Vec<_> = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
.map(|room_id| {
|
||||
Ok::<_, Error>((
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
avatar_url: body.avatar_url.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
..serde_json::from_str(
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
|
||||
.ok_or_else(|| {
|
||||
Error::bad_database("Tried to send displayname update for user not in the room.")
|
||||
})?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
},
|
||||
room_id,
|
||||
))
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
for (pdu_builder, room_id) in all_joined_rooms {
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
if let Err(e) = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
|
||||
.await
|
||||
{
|
||||
warn!(%e, "Failed to set/update room with new avatar URL / pfp");
|
||||
}
|
||||
}
|
||||
|
||||
if services().globals.allow_local_presence() {
|
||||
// Presence update
|
||||
services()
|
||||
.presence
|
||||
.ping_presence(sender_user, &PresenceState::Online)?;
|
||||
}
|
||||
|
||||
Ok(set_avatar_url::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/profile/{userId}/avatar_url`
|
||||
///
|
||||
/// Returns the `avatar_url` and `blurhash` of the user.
|
||||
///
|
||||
/// - If user is on another server and we do not have a local copy already
|
||||
/// fetch `avatar_url` and blurhash over federation
|
||||
pub(crate) async fn get_avatar_url_route(
|
||||
body: Ruma<get_avatar_url::v3::Request>,
|
||||
) -> Result<get_avatar_url::v3::Response> {
|
||||
if !user_is_local(&body.user_id) {
|
||||
// Create and update our local copy of the user
|
||||
if let Ok(response) = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
body.user_id.server_name(),
|
||||
federation::query::get_profile_information::v1::Request {
|
||||
user_id: body.user_id.clone(),
|
||||
field: None, // we want the full user's profile to update locally as well
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
services().users.create(&body.user_id, None)?;
|
||||
}
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_displayname(&body.user_id, response.displayname.clone())
|
||||
.await?;
|
||||
services()
|
||||
.users
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone())
|
||||
.await?;
|
||||
services()
|
||||
.users
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone())
|
||||
.await?;
|
||||
|
||||
return Ok(get_avatar_url::v3::Response {
|
||||
avatar_url: response.avatar_url,
|
||||
blurhash: response.blurhash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
// Return 404 if this user doesn't exist and we couldn't fetch it over
|
||||
// federation
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
|
||||
}
|
||||
|
||||
Ok(get_avatar_url::v3::Response {
|
||||
avatar_url: services().users.avatar_url(&body.user_id)?,
|
||||
blurhash: services().users.blurhash(&body.user_id)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/profile/{userId}`
|
||||
///
|
||||
/// Returns the displayname, avatar_url and blurhash of the user.
|
||||
///
|
||||
/// - If user is on another server and we do not have a local copy already,
|
||||
/// fetch profile over federation.
|
||||
pub(crate) async fn get_profile_route(body: Ruma<get_profile::v3::Request>) -> Result<get_profile::v3::Response> {
|
||||
if !user_is_local(&body.user_id) {
|
||||
// Create and update our local copy of the user
|
||||
if let Ok(response) = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
body.user_id.server_name(),
|
||||
federation::query::get_profile_information::v1::Request {
|
||||
user_id: body.user_id.clone(),
|
||||
field: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
services().users.create(&body.user_id, None)?;
|
||||
}
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_displayname(&body.user_id, response.displayname.clone())
|
||||
.await?;
|
||||
services()
|
||||
.users
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone())
|
||||
.await?;
|
||||
services()
|
||||
.users
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone())
|
||||
.await?;
|
||||
|
||||
return Ok(get_profile::v3::Response {
|
||||
displayname: response.displayname,
|
||||
avatar_url: response.avatar_url,
|
||||
blurhash: response.blurhash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
// Return 404 if this user doesn't exist and we couldn't fetch it over
|
||||
// federation
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
|
||||
}
|
||||
|
||||
Ok(get_profile::v3::Response {
|
||||
avatar_url: services().users.avatar_url(&body.user_id)?,
|
||||
blurhash: services().users.blurhash(&body.user_id)?,
|
||||
displayname: services().users.displayname(&body.user_id)?,
|
||||
})
|
||||
}
|
373
src/api/client/push.rs
Normal file
373
src/api/client/push.rs
Normal file
|
@ -0,0 +1,373 @@
|
|||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
push::{
|
||||
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions, get_pushrule_enabled, get_pushrules_all,
|
||||
set_pusher, set_pushrule, set_pushrule_actions, set_pushrule_enabled, RuleScope,
|
||||
},
|
||||
},
|
||||
events::{push_rules::PushRulesEvent, GlobalAccountDataEventType},
|
||||
push::{InsertPushRuleError, RemovePushRuleError, Ruleset},
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/`
|
||||
///
|
||||
/// Retrieves the push rules event for this user.
|
||||
pub(crate) async fn get_pushrules_all_route(
|
||||
body: Ruma<get_pushrules_all::v3::Request>,
|
||||
) -> Result<get_pushrules_all::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event =
|
||||
services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?;
|
||||
|
||||
if let Some(event) = event {
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
Ok(get_pushrules_all::v3::Response {
|
||||
global: account_data.global,
|
||||
})
|
||||
} else {
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: Ruleset::server_default(sender_user),
|
||||
},
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)?;
|
||||
|
||||
Ok(get_pushrules_all::v3::Response {
|
||||
global: Ruleset::server_default(sender_user),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Retrieves a single specified push rule for this user.
|
||||
pub(crate) async fn get_pushrule_route(body: Ruma<get_pushrule::v3::Request>) -> Result<get_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
let rule = account_data
|
||||
.global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(Into::into);
|
||||
|
||||
if let Some(rule) = rule {
|
||||
Ok(get_pushrule::v3::Response {
|
||||
rule,
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Creates a single specified push rule for this user.
|
||||
pub(crate) async fn set_pushrule_route(body: Ruma<set_pushrule::v3::Request>) -> Result<set_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let body = body.body;
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if let Err(error) =
|
||||
account_data
|
||||
.content
|
||||
.global
|
||||
.insert(body.rule.clone(), body.after.as_deref(), body.before.as_deref())
|
||||
{
|
||||
let err = match error {
|
||||
InsertPushRuleError::ServerDefaultRuleId => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Rule IDs starting with a dot are reserved for server-default rules.",
|
||||
),
|
||||
InsertPushRuleError::InvalidRuleId => {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Rule ID containing invalid characters.")
|
||||
},
|
||||
InsertPushRuleError::RelativeToServerDefaultRule => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Can't place a push rule relatively to a server-default rule.",
|
||||
),
|
||||
InsertPushRuleError::UnknownRuleId => {
|
||||
Error::BadRequest(ErrorKind::NotFound, "The before or after rule could not be found.")
|
||||
},
|
||||
InsertPushRuleError::BeforeHigherThanAfter => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"The before rule has a higher priority than the after rule.",
|
||||
),
|
||||
_ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(set_pushrule::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Gets the actions of a single specified push rule for this user.
|
||||
pub(crate) async fn get_pushrule_actions_route(
|
||||
body: Ruma<get_pushrule_actions::v3::Request>,
|
||||
) -> Result<get_pushrule_actions::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
let global = account_data.global;
|
||||
let actions = global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(|rule| rule.actions().to_owned())
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))?;
|
||||
|
||||
Ok(get_pushrule_actions::v3::Response {
|
||||
actions,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Sets the actions of a single specified push rule for this user.
|
||||
pub(crate) async fn set_pushrule_actions_route(
|
||||
body: Ruma<set_pushrule_actions::v3::Request>,
|
||||
) -> Result<set_pushrule_actions::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if account_data
|
||||
.content
|
||||
.global
|
||||
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(set_pushrule_actions::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Gets the enabled status of a single specified push rule for this user.
|
||||
pub(crate) async fn get_pushrule_enabled_route(
|
||||
body: Ruma<get_pushrule_enabled::v3::Request>,
|
||||
) -> Result<get_pushrule_enabled::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
let global = account_data.content.global;
|
||||
let enabled = global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(ruma::push::AnyPushRuleRef::enabled)
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))?;
|
||||
|
||||
Ok(get_pushrule_enabled::v3::Response {
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Sets the enabled status of a single specified push rule for this user.
|
||||
pub(crate) async fn set_pushrule_enabled_route(
|
||||
body: Ruma<set_pushrule_enabled::v3::Request>,
|
||||
) -> Result<set_pushrule_enabled::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if account_data
|
||||
.content
|
||||
.global
|
||||
.set_enabled(body.kind.clone(), &body.rule_id, body.enabled)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(set_pushrule_enabled::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Deletes a single specified push rule for this user.
|
||||
pub(crate) async fn delete_pushrule_route(
|
||||
body: Ruma<delete_pushrule::v3::Request>,
|
||||
) -> Result<delete_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if let Err(error) = account_data
|
||||
.content
|
||||
.global
|
||||
.remove(body.kind.clone(), &body.rule_id)
|
||||
{
|
||||
let err = match error {
|
||||
RemovePushRuleError::ServerDefault => {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Cannot delete a server-default pushrule.")
|
||||
},
|
||||
RemovePushRuleError::NotFound => Error::BadRequest(ErrorKind::NotFound, "Push rule not found."),
|
||||
_ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(delete_pushrule::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushers`
|
||||
///
|
||||
/// Gets all currently active pushers for the sender user.
|
||||
pub(crate) async fn get_pushers_route(body: Ruma<get_pushers::v3::Request>) -> Result<get_pushers::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_pushers::v3::Response {
|
||||
pushers: services().pusher.get_pushers(sender_user)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/pushers/set`
|
||||
///
|
||||
/// Adds a pusher for the sender user.
|
||||
///
|
||||
/// - TODO: Handle `append`
|
||||
pub(crate) async fn set_pushers_route(body: Ruma<set_pusher::v3::Request>) -> Result<set_pusher::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.pusher
|
||||
.set_pusher(sender_user, body.action.clone())?;
|
||||
|
||||
Ok(set_pusher::v3::Response::default())
|
||||
}
|
178
src/api/client/read_marker.rs
Normal file
178
src/api/client/read_marker.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use conduit::PduCount;
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
|
||||
events::{
|
||||
receipt::{ReceiptThread, ReceiptType},
|
||||
RoomAccountDataEventType,
|
||||
},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
|
||||
///
|
||||
/// Sets different types of read markers.
|
||||
///
|
||||
/// - Updates fully-read account data event to `fully_read`
|
||||
/// - If `read_receipt` is set: Update private marker and public read receipt
|
||||
/// EDU
|
||||
pub(crate) async fn set_read_marker_route(
|
||||
body: Ruma<set_read_marker::v3::Request>,
|
||||
) -> Result<set_read_marker::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if let Some(fully_read) = &body.fully_read {
|
||||
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
|
||||
content: ruma::events::fully_read::FullyReadEventContent {
|
||||
event_id: fully_read.clone(),
|
||||
},
|
||||
};
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)?;
|
||||
}
|
||||
|
||||
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
|
||||
services()
|
||||
.rooms
|
||||
.user
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
}
|
||||
|
||||
if let Some(event) = &body.private_read_receipt {
|
||||
let count = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(event)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
|
||||
let count = match count {
|
||||
PduCount::Backfilled(_) => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Read receipt is in backfilled timeline",
|
||||
))
|
||||
},
|
||||
PduCount::Normal(c) => c,
|
||||
};
|
||||
services()
|
||||
.rooms
|
||||
.read_receipt
|
||||
.private_read_set(&body.room_id, sender_user, count)?;
|
||||
}
|
||||
|
||||
if let Some(event) = &body.read_receipt {
|
||||
let mut user_receipts = BTreeMap::new();
|
||||
user_receipts.insert(
|
||||
sender_user.clone(),
|
||||
ruma::events::receipt::Receipt {
|
||||
ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
thread: ReceiptThread::Unthreaded,
|
||||
},
|
||||
);
|
||||
|
||||
let mut receipts = BTreeMap::new();
|
||||
receipts.insert(ReceiptType::Read, user_receipts);
|
||||
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(event.to_owned(), receipts);
|
||||
|
||||
services().rooms.read_receipt.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(set_read_marker::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}`
|
||||
///
|
||||
/// Sets private read marker and public read receipt EDU.
|
||||
pub(crate) async fn create_receipt_route(
|
||||
body: Ruma<create_receipt::v3::Request>,
|
||||
) -> Result<create_receipt::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if matches!(
|
||||
&body.receipt_type,
|
||||
create_receipt::v3::ReceiptType::Read | create_receipt::v3::ReceiptType::ReadPrivate
|
||||
) {
|
||||
services()
|
||||
.rooms
|
||||
.user
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
}
|
||||
|
||||
match body.receipt_type {
|
||||
create_receipt::v3::ReceiptType::FullyRead => {
|
||||
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
|
||||
content: ruma::events::fully_read::FullyReadEventContent {
|
||||
event_id: body.event_id.clone(),
|
||||
},
|
||||
};
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)?;
|
||||
},
|
||||
create_receipt::v3::ReceiptType::Read => {
|
||||
let mut user_receipts = BTreeMap::new();
|
||||
user_receipts.insert(
|
||||
sender_user.clone(),
|
||||
ruma::events::receipt::Receipt {
|
||||
ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
thread: ReceiptThread::Unthreaded,
|
||||
},
|
||||
);
|
||||
let mut receipts = BTreeMap::new();
|
||||
receipts.insert(ReceiptType::Read, user_receipts);
|
||||
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(body.event_id.clone(), receipts);
|
||||
|
||||
services().rooms.read_receipt.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)?;
|
||||
},
|
||||
create_receipt::v3::ReceiptType::ReadPrivate => {
|
||||
let count = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
|
||||
let count = match count {
|
||||
PduCount::Backfilled(_) => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Read receipt is in backfilled timeline",
|
||||
))
|
||||
},
|
||||
PduCount::Normal(c) => c,
|
||||
};
|
||||
services()
|
||||
.rooms
|
||||
.read_receipt
|
||||
.private_read_set(&body.room_id, sender_user, count)?;
|
||||
},
|
||||
_ => return Err(Error::bad_database("Unsupported receipt type")),
|
||||
}
|
||||
|
||||
Ok(create_receipt::v3::Response {})
|
||||
}
|
58
src/api/client/redact.rs
Normal file
58
src/api/client/redact.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ruma::{
|
||||
api::client::redact::redact_event,
|
||||
events::{room::redaction::RoomRedactionEventContent, TimelineEventType},
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
|
||||
use crate::{service::pdu::PduBuilder, services, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}`
|
||||
///
|
||||
/// Tries to send a redaction event into the room.
|
||||
///
|
||||
/// - TODO: Handle txn id
|
||||
pub(crate) async fn redact_event_route(body: Ruma<redact_event::v3::Request>) -> Result<redact_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let body = body.body;
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(body.room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let event_id = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomRedaction,
|
||||
content: to_raw_value(&RoomRedactionEventContent {
|
||||
redacts: Some(body.event_id.clone()),
|
||||
reason: body.reason.clone(),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: None,
|
||||
redacts: Some(body.event_id.into()),
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(redact_event::v3::Response {
|
||||
event_id,
|
||||
})
|
||||
}
|
88
src/api/client/relations.rs
Normal file
88
src/api/client/relations.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use ruma::api::client::relations::{
|
||||
get_relating_events, get_relating_events_with_rel_type, get_relating_events_with_rel_type_and_event_type,
|
||||
};
|
||||
|
||||
use crate::{services, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
|
||||
pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
|
||||
body: Ruma<get_relating_events_with_rel_type_and_event_type::v1::Request>,
|
||||
) -> Result<get_relating_events_with_rel_type_and_event_type::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let res = services()
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
&Some(body.event_type.clone()),
|
||||
&Some(body.rel_type.clone()),
|
||||
&body.from,
|
||||
&body.to,
|
||||
&body.limit,
|
||||
body.recurse,
|
||||
body.dir,
|
||||
)?;
|
||||
|
||||
Ok(get_relating_events_with_rel_type_and_event_type::v1::Response {
|
||||
chunk: res.chunk,
|
||||
next_batch: res.next_batch,
|
||||
prev_batch: res.prev_batch,
|
||||
recursion_depth: res.recursion_depth,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}`
|
||||
pub(crate) async fn get_relating_events_with_rel_type_route(
|
||||
body: Ruma<get_relating_events_with_rel_type::v1::Request>,
|
||||
) -> Result<get_relating_events_with_rel_type::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let res = services()
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
&None,
|
||||
&Some(body.rel_type.clone()),
|
||||
&body.from,
|
||||
&body.to,
|
||||
&body.limit,
|
||||
body.recurse,
|
||||
body.dir,
|
||||
)?;
|
||||
|
||||
Ok(get_relating_events_with_rel_type::v1::Response {
|
||||
chunk: res.chunk,
|
||||
next_batch: res.next_batch,
|
||||
prev_batch: res.prev_batch,
|
||||
recursion_depth: res.recursion_depth,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}`
|
||||
pub(crate) async fn get_relating_events_route(
|
||||
body: Ruma<get_relating_events::v1::Request>,
|
||||
) -> Result<get_relating_events::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
&None,
|
||||
&None,
|
||||
&body.from,
|
||||
&body.to,
|
||||
&body.limit,
|
||||
body.recurse,
|
||||
body.dir,
|
||||
)
|
||||
}
|
133
src/api/client/report.rs
Normal file
133
src/api/client/report.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use rand::Rng;
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, room::report_content},
|
||||
events::room::message,
|
||||
int, EventId, RoomId, UserId,
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{debug_info, service::pdu::PduEvent, services, utils::HtmlEscape, Error, Result, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
|
||||
///
|
||||
/// Reports an inappropriate event to homeserver admins
|
||||
pub(crate) async fn report_event_route(
|
||||
body: Ruma<report_content::v3::Request>,
|
||||
) -> Result<report_content::v3::Response> {
|
||||
// user authentication
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
info!(
|
||||
"Received /report request by user {sender_user} for room {} and event ID {}",
|
||||
body.room_id, body.event_id
|
||||
);
|
||||
|
||||
// check if we know about the reported event ID or if it's invalid
|
||||
let Some(pdu) = services().rooms.timeline.get_pdu(&body.event_id)? else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Event ID is not known to us or Event ID is invalid",
|
||||
));
|
||||
};
|
||||
|
||||
is_report_valid(&pdu.event_id, &body.room_id, sender_user, &body.reason, body.score, &pdu)?;
|
||||
|
||||
// send admin room message that we received the report with an @room ping for
|
||||
// urgency
|
||||
services()
|
||||
.admin
|
||||
.send_message(message::RoomMessageEventContent::text_html(
|
||||
format!(
|
||||
"@room Report received from: {}\n\nEvent ID: {}\nRoom ID: {}\nSent By: {}\n\nReport Score: {}\nReport \
|
||||
Reason: {}",
|
||||
sender_user.to_owned(),
|
||||
pdu.event_id,
|
||||
pdu.room_id,
|
||||
pdu.sender.clone(),
|
||||
body.score.unwrap_or_else(|| ruma::Int::from(0)),
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
),
|
||||
format!(
|
||||
"<details><summary>@room Report received from: <a href=\"https://matrix.to/#/{0}\">{0}\
|
||||
</a></summary><ul><li>Event Info<ul><li>Event ID: <code>{1}</code>\
|
||||
<a href=\"https://matrix.to/#/{2}/{1}\">🔗</a></li><li>Room ID: <code>{2}</code>\
|
||||
</li><li>Sent By: <a href=\"https://matrix.to/#/{3}\">{3}</a></li></ul></li><li>\
|
||||
Report Info<ul><li>Report Score: {4}</li><li>Report Reason: {5}</li></ul></li>\
|
||||
</ul></details>",
|
||||
sender_user.to_owned(),
|
||||
pdu.event_id.clone(),
|
||||
pdu.room_id.clone(),
|
||||
pdu.sender.clone(),
|
||||
body.score.unwrap_or_else(|| ruma::Int::from(0)),
|
||||
HtmlEscape(body.reason.as_deref().unwrap_or(""))
|
||||
),
|
||||
))
|
||||
.await;
|
||||
|
||||
delay_response().await?;
|
||||
|
||||
Ok(report_content::v3::Response {})
|
||||
}
|
||||
|
||||
/// in the following order:
|
||||
///
|
||||
/// check if the room ID from the URI matches the PDU's room ID
|
||||
/// check if reporting user is in the reporting room
|
||||
/// check if score is in valid range
|
||||
/// check if report reasoning is less than or equal to 750 characters
|
||||
fn is_report_valid(
|
||||
event_id: &EventId, room_id: &RoomId, sender_user: &UserId, reason: &Option<String>, score: Option<ruma::Int>,
|
||||
pdu: &std::sync::Arc<PduEvent>,
|
||||
) -> Result<bool> {
|
||||
debug_info!("Checking if report from user {sender_user} for event {event_id} in room {room_id} is valid");
|
||||
|
||||
if room_id != pdu.room_id {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Event ID does not belong to the reported room",
|
||||
));
|
||||
}
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&pdu.room_id)
|
||||
.filter_map(Result::ok)
|
||||
.any(|user_id| user_id == *sender_user)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"You are not in the room you are reporting.",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(true) = score.map(|s| s > int!(0) || s < int!(-100)) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Invalid score, must be within 0 to -100",
|
||||
));
|
||||
};
|
||||
|
||||
if let Some(true) = reason.clone().map(|s| s.len() >= 750) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Reason too long, should be 750 characters or fewer",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// even though this is kinda security by obscurity, let's still make a small
|
||||
/// random delay sending a successful response per spec suggestion regarding
|
||||
/// enumerating for potential events existing in our server.
|
||||
async fn delay_response() -> Result<()> {
|
||||
let time_to_wait = rand::thread_rng().gen_range(8..21);
|
||||
debug_info!("Got successful /report request, waiting {time_to_wait} seconds before sending successful response.");
|
||||
sleep(Duration::from_secs(time_to_wait)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
978
src/api/client/room.rs
Normal file
978
src/api/client/room.rs
Normal file
|
@ -0,0 +1,978 @@
|
|||
use std::{cmp::max, collections::BTreeMap, sync::Arc};
|
||||
|
||||
use conduit::{debug_info, debug_warn};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
room::{self, aliases, create_room, get_room_event, upgrade_room},
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
guest_access::{GuestAccess, RoomGuestAccessEventContent},
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
name::RoomNameEventContent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
StateEventType, TimelineEventType,
|
||||
},
|
||||
int,
|
||||
serde::{JsonObject, Raw},
|
||||
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use super::invite_helper;
|
||||
use crate::{
|
||||
service::{appservice::RegistrationInfo, pdu::PduBuilder},
|
||||
services, Error, Result, Ruma,
|
||||
};
|
||||
|
||||
/// Recommended transferable state events list from the spec
|
||||
const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[
|
||||
StateEventType::RoomServerAcl,
|
||||
StateEventType::RoomEncryption,
|
||||
StateEventType::RoomName,
|
||||
StateEventType::RoomAvatar,
|
||||
StateEventType::RoomTopic,
|
||||
StateEventType::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules,
|
||||
StateEventType::RoomPowerLevels,
|
||||
];
|
||||
|
||||
/// # `POST /_matrix/client/v3/createRoom`
|
||||
///
|
||||
/// Creates a new room.
|
||||
///
|
||||
/// - Room ID is randomly generated
|
||||
/// - Create alias if `room_alias_name` is set
|
||||
/// - Send create event
|
||||
/// - Join sender user
|
||||
/// - Send power levels event
|
||||
/// - Send canonical room alias
|
||||
/// - Send join rules
|
||||
/// - Send history visibility
|
||||
/// - Send guest access
|
||||
/// - Send events listed in initial state
|
||||
/// - Send events implied by `name` and `topic`
|
||||
/// - Send invite events
|
||||
pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> Result<create_room::v3::Response> {
|
||||
use create_room::v3::RoomPreset;
|
||||
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services().globals.allow_room_creation()
|
||||
&& body.appservice_info.is_none()
|
||||
&& !services().users.is_admin(sender_user)?
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Room creation has been disabled."));
|
||||
}
|
||||
|
||||
let room_id: OwnedRoomId = if let Some(custom_room_id) = &body.room_id {
|
||||
custom_room_id_check(custom_room_id)?
|
||||
} else {
|
||||
RoomId::new(&services().globals.config.server_name)
|
||||
};
|
||||
|
||||
// check if room ID doesn't already exist instead of erroring on auth check
|
||||
if services().rooms.short.get_shortroomid(&room_id)?.is_some() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::RoomInUse,
|
||||
"Room with that custom room ID already exists",
|
||||
));
|
||||
}
|
||||
|
||||
services().rooms.short.get_or_create_shortroomid(&room_id)?;
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let alias: Option<OwnedRoomAliasId> = if let Some(alias) = &body.room_alias_name {
|
||||
Some(room_alias_check(alias, &body.appservice_info).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let room_version = match body.room_version.clone() {
|
||||
Some(room_version) => {
|
||||
if services()
|
||||
.globals
|
||||
.supported_room_versions()
|
||||
.contains(&room_version)
|
||||
{
|
||||
room_version
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UnsupportedRoomVersion,
|
||||
"This server does not support that room version.",
|
||||
));
|
||||
}
|
||||
},
|
||||
None => services().globals.default_room_version(),
|
||||
};
|
||||
|
||||
let content = match &body.creation_content {
|
||||
Some(content) => {
|
||||
let mut content = content
|
||||
.deserialize_as::<CanonicalJsonObject>()
|
||||
.map_err(|e| {
|
||||
error!("Failed to deserialise content as canonical JSON: {}", e);
|
||||
Error::bad_database("Failed to deserialise content as canonical JSON.")
|
||||
})?;
|
||||
match room_version {
|
||||
RoomVersionId::V1
|
||||
| RoomVersionId::V2
|
||||
| RoomVersionId::V3
|
||||
| RoomVersionId::V4
|
||||
| RoomVersionId::V5
|
||||
| RoomVersionId::V6
|
||||
| RoomVersionId::V7
|
||||
| RoomVersionId::V8
|
||||
| RoomVersionId::V9
|
||||
| RoomVersionId::V10 => {
|
||||
content.insert(
|
||||
"creator".into(),
|
||||
json!(&sender_user).try_into().map_err(|e| {
|
||||
info!("Invalid creation content: {e}");
|
||||
Error::BadRequest(ErrorKind::BadJson, "Invalid creation content")
|
||||
})?,
|
||||
);
|
||||
},
|
||||
RoomVersionId::V11 => {}, // V11 removed the "creator" key
|
||||
_ => {
|
||||
warn!("Unexpected or unsupported room version {room_version}");
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::BadJson,
|
||||
"Unexpected or unsupported room version found",
|
||||
));
|
||||
},
|
||||
}
|
||||
|
||||
content.insert(
|
||||
"room_version".into(),
|
||||
json!(room_version.as_str())
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid creation content"))?,
|
||||
);
|
||||
content
|
||||
},
|
||||
None => {
|
||||
let content = match room_version {
|
||||
RoomVersionId::V1
|
||||
| RoomVersionId::V2
|
||||
| RoomVersionId::V3
|
||||
| RoomVersionId::V4
|
||||
| RoomVersionId::V5
|
||||
| RoomVersionId::V6
|
||||
| RoomVersionId::V7
|
||||
| RoomVersionId::V8
|
||||
| RoomVersionId::V9
|
||||
| RoomVersionId::V10 => RoomCreateEventContent::new_v1(sender_user.clone()),
|
||||
RoomVersionId::V11 => RoomCreateEventContent::new_v11(),
|
||||
_ => {
|
||||
warn!("Unexpected or unsupported room version {room_version}");
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::BadJson,
|
||||
"Unexpected or unsupported room version found",
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut content = serde_json::from_str::<CanonicalJsonObject>(
|
||||
to_raw_value(&content)
|
||||
.expect("we just created this as content was None")
|
||||
.get(),
|
||||
)
|
||||
.unwrap();
|
||||
content.insert(
|
||||
"room_version".into(),
|
||||
json!(room_version.as_str())
|
||||
.try_into()
|
||||
.expect("we just created this as content was None"),
|
||||
);
|
||||
content
|
||||
},
|
||||
};
|
||||
|
||||
// 1. The room create event
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. Let the room creator join
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
membership: MembershipState::Join,
|
||||
displayname: services().users.displayname(sender_user)?,
|
||||
avatar_url: services().users.avatar_url(sender_user)?,
|
||||
is_direct: Some(body.is_direct),
|
||||
third_party_invite: None,
|
||||
blurhash: services().users.blurhash(sender_user)?,
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 3. Power levels
|
||||
|
||||
// Figure out preset. We need it for preset specific events
|
||||
let preset = body.preset.clone().unwrap_or(match &body.visibility {
|
||||
room::Visibility::Public => RoomPreset::PublicChat,
|
||||
_ => RoomPreset::PrivateChat, // Room visibility should not be custom
|
||||
});
|
||||
|
||||
let mut users = BTreeMap::new();
|
||||
users.insert(sender_user.clone(), int!(100));
|
||||
|
||||
if preset == RoomPreset::TrustedPrivateChat {
|
||||
for invite_ in &body.invite {
|
||||
users.insert(invite_.clone(), int!(100));
|
||||
}
|
||||
}
|
||||
|
||||
let power_levels_content =
|
||||
default_power_levels_content(&body.power_level_content_override, &body.visibility, users)?;
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_content).expect("to_raw_value always works on serde_json::Value"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 4. Canonical room alias
|
||||
if let Some(room_alias_id) = &alias {
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCanonicalAlias,
|
||||
content: to_raw_value(&RoomCanonicalAliasEventContent {
|
||||
alias: Some(room_alias_id.to_owned()),
|
||||
alt_aliases: vec![],
|
||||
})
|
||||
.expect("We checked that alias earlier, it must be fine"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 5. Events set by preset
|
||||
|
||||
// 5.1 Join Rules
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomJoinRules,
|
||||
content: to_raw_value(&RoomJoinRulesEventContent::new(match preset {
|
||||
RoomPreset::PublicChat => JoinRule::Public,
|
||||
// according to spec "invite" is the default
|
||||
_ => JoinRule::Invite,
|
||||
}))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 5.2 History Visibility
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomHistoryVisibility,
|
||||
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 5.3 Guest Access
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomGuestAccess,
|
||||
content: to_raw_value(&RoomGuestAccessEventContent::new(match preset {
|
||||
RoomPreset::PublicChat => GuestAccess::Forbidden,
|
||||
_ => GuestAccess::CanJoin,
|
||||
}))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 6. Events listed in initial_state
|
||||
for event in &body.initial_state {
|
||||
let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| {
|
||||
warn!("Invalid initial state event: {:?}", e);
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.")
|
||||
})?;
|
||||
|
||||
debug_info!("Room creation initial state event: {event:?}");
|
||||
|
||||
// client/appservice workaround: if a user sends an initial_state event with a
|
||||
// state event in there with the content of literally `{}` (not null or empty
|
||||
// string), let's just skip it over and warn.
|
||||
if pdu_builder.content.get().eq("{}") {
|
||||
info!("skipping empty initial state event with content of `{{}}`: {event:?}");
|
||||
debug_warn!("content: {}", pdu_builder.content.get());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Implicit state key defaults to ""
|
||||
pdu_builder.state_key.get_or_insert_with(String::new);
|
||||
|
||||
// Silently skip encryption events if they are not allowed
|
||||
if pdu_builder.event_type == TimelineEventType::RoomEncryption && !services().globals.allow_encryption() {
|
||||
continue;
|
||||
}
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 7. Events implied by name and topic
|
||||
if let Some(name) = &body.name {
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomName,
|
||||
content: to_raw_value(&RoomNameEventContent::new(name.clone()))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(topic) = &body.topic {
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomTopic,
|
||||
content: to_raw_value(&RoomTopicEventContent {
|
||||
topic: topic.clone(),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 8. Events implied by invite (and TODO: invite_3pid)
|
||||
drop(state_lock);
|
||||
for user_id in &body.invite {
|
||||
if let Err(e) = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await {
|
||||
warn!(%e, "Failed to send invite");
|
||||
}
|
||||
}
|
||||
|
||||
// Homeserver specific stuff
|
||||
if let Some(alias) = alias {
|
||||
services().rooms.alias.set_alias(&alias, &room_id)?;
|
||||
}
|
||||
|
||||
if body.visibility == room::Visibility::Public {
|
||||
services().rooms.directory.set_public(&room_id)?;
|
||||
}
|
||||
|
||||
info!("{sender_user} created a room with room ID {room_id}");
|
||||
|
||||
Ok(create_room::v3::Response::new(room_id))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
|
||||
///
|
||||
/// Gets a single event.
|
||||
///
|
||||
/// - You have to currently be joined to the room (TODO: Respect history
|
||||
/// visibility)
|
||||
pub(crate) async fn get_room_event_route(
|
||||
body: Ruma<get_room_event::v3::Request>,
|
||||
) -> Result<get_room_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(&body.event_id)?
|
||||
.ok_or_else(|| {
|
||||
warn!("Event not found, event ID: {:?}", &body.event_id);
|
||||
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
|
||||
})?;
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &event.room_id, &body.event_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this event.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event = (*event).clone();
|
||||
event.add_age()?;
|
||||
|
||||
Ok(get_room_event::v3::Response {
|
||||
event: event.to_room_event(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
|
||||
///
|
||||
/// Lists all aliases of the room.
|
||||
///
|
||||
/// - Only users joined to the room are allowed to call this, or if
|
||||
/// `history_visibility` is world readable in the room
|
||||
pub(crate) async fn get_room_aliases_route(body: Ruma<aliases::v3::Request>) -> Result<aliases::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(aliases::v3::Response {
|
||||
aliases: services()
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
|
||||
///
|
||||
/// Upgrades the room.
|
||||
///
|
||||
/// - Creates a replacement room
|
||||
/// - Sends a tombstone event into the current room
|
||||
/// - Sender user joins the room
|
||||
/// - Transfers some state events
|
||||
/// - Moves local aliases
|
||||
/// - Modifies old room power levels to prevent users from speaking
|
||||
pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) -> Result<upgrade_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.globals
|
||||
.supported_room_versions()
|
||||
.contains(&body.new_version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UnsupportedRoomVersion,
|
||||
"This server does not support that room version.",
|
||||
));
|
||||
}
|
||||
|
||||
// Create a replacement room
|
||||
let replacement_room = RoomId::new(services().globals.server_name());
|
||||
services()
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&replacement_room)?;
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(body.room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
// Send a m.room.tombstone event to the old room to indicate that it is not
|
||||
// intended to be used any further Fail if the sender does not have the required
|
||||
// permissions
|
||||
let tombstone_event_id = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomTombstone,
|
||||
content: to_raw_value(&RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.clone(),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change lock to replacement room
|
||||
drop(state_lock);
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(replacement_room.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
// Get the old room creation event
|
||||
let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>(
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &StateEventType::RoomCreate, "")?
|
||||
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
|
||||
|
||||
// Use the m.room.tombstone event as the predecessor
|
||||
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
|
||||
body.room_id.clone(),
|
||||
(*tombstone_event_id).to_owned(),
|
||||
));
|
||||
|
||||
// Send a m.room.create event containing a predecessor field and the applicable
|
||||
// room_version
|
||||
match body.new_version {
|
||||
RoomVersionId::V1
|
||||
| RoomVersionId::V2
|
||||
| RoomVersionId::V3
|
||||
| RoomVersionId::V4
|
||||
| RoomVersionId::V5
|
||||
| RoomVersionId::V6
|
||||
| RoomVersionId::V7
|
||||
| RoomVersionId::V8
|
||||
| RoomVersionId::V9
|
||||
| RoomVersionId::V10 => {
|
||||
create_event_content.insert(
|
||||
"creator".into(),
|
||||
json!(&sender_user).try_into().map_err(|e| {
|
||||
info!("Error forming creation event: {e}");
|
||||
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")
|
||||
})?,
|
||||
);
|
||||
},
|
||||
RoomVersionId::V11 => {
|
||||
// "creator" key no longer exists in V11 rooms
|
||||
create_event_content.remove("creator");
|
||||
},
|
||||
_ => {
|
||||
warn!("Unexpected or unsupported room version {}", body.new_version);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::BadJson,
|
||||
"Unexpected or unsupported room version found",
|
||||
));
|
||||
},
|
||||
}
|
||||
|
||||
create_event_content.insert(
|
||||
"room_version".into(),
|
||||
json!(&body.new_version)
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
|
||||
);
|
||||
create_event_content.insert(
|
||||
"predecessor".into(),
|
||||
json!(predecessor)
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
|
||||
);
|
||||
|
||||
// Validate creation event content
|
||||
if serde_json::from_str::<CanonicalJsonObject>(
|
||||
to_raw_value(&create_event_content)
|
||||
.expect("Error forming creation event")
|
||||
.get(),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
|
||||
}
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_event_content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Join the new room
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
membership: MembershipState::Join,
|
||||
displayname: services().users.displayname(sender_user)?,
|
||||
avatar_url: services().users.avatar_url(sender_user)?,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
blurhash: services().users.blurhash(sender_user)?,
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Replicate transferable state events to the new room
|
||||
for event_type in TRANSFERABLE_STATE_EVENTS {
|
||||
let event_content = match services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, event_type, "")?
|
||||
{
|
||||
Some(v) => v.content.clone(),
|
||||
None => continue, // Skipping missing events.
|
||||
};
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: event_content,
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Moves any local aliases to the new room
|
||||
for alias in services()
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
services()
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(&alias, &replacement_room)?;
|
||||
}
|
||||
|
||||
// Get the old room power levels
|
||||
let mut power_levels_event_content: RoomPowerLevelsEventContent = serde_json::from_str(
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &StateEventType::RoomPowerLevels, "")?
|
||||
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
|
||||
|
||||
// Setting events_default and invite to the greater of 50 and users_default + 1
|
||||
let new_level = max(
|
||||
int!(50),
|
||||
power_levels_event_content
|
||||
.users_default
|
||||
.checked_add(int!(1))
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(ErrorKind::BadJson, "users_default power levels event content is not valid")
|
||||
})?,
|
||||
);
|
||||
power_levels_event_content.events_default = new_level;
|
||||
power_levels_event_content.invite = new_level;
|
||||
|
||||
// Modify the power levels in the old room to prevent sending of events and
|
||||
// inviting new users
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_event_content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
// Return the replacement room id
|
||||
Ok(upgrade_room::v3::Response {
|
||||
replacement_room,
|
||||
})
|
||||
}
|
||||
|
||||
/// creates the power_levels_content for the PDU builder
|
||||
fn default_power_levels_content(
|
||||
power_level_content_override: &Option<Raw<RoomPowerLevelsEventContent>>, visibility: &room::Visibility,
|
||||
users: BTreeMap<OwnedUserId, Int>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut power_levels_content = serde_json::to_value(RoomPowerLevelsEventContent {
|
||||
users,
|
||||
..Default::default()
|
||||
})
|
||||
.expect("event is valid, we just created it");
|
||||
|
||||
// secure proper defaults of sensitive/dangerous permissions that moderators
|
||||
// (power level 50) should not have easy access to
|
||||
power_levels_content["events"]["m.room.power_levels"] = serde_json::to_value(100).expect("100 is valid Value");
|
||||
power_levels_content["events"]["m.room.server_acl"] = serde_json::to_value(100).expect("100 is valid Value");
|
||||
power_levels_content["events"]["m.room.tombstone"] = serde_json::to_value(100).expect("100 is valid Value");
|
||||
power_levels_content["events"]["m.room.encryption"] = serde_json::to_value(100).expect("100 is valid Value");
|
||||
power_levels_content["events"]["m.room.history_visibility"] =
|
||||
serde_json::to_value(100).expect("100 is valid Value");
|
||||
|
||||
// synapse does this too. clients do not expose these permissions. it prevents
|
||||
// default users from calling public rooms, for obvious reasons.
|
||||
if *visibility == room::Visibility::Public {
|
||||
power_levels_content["events"]["m.call.invite"] = serde_json::to_value(50).expect("50 is valid Value");
|
||||
power_levels_content["events"]["org.matrix.msc3401.call"] =
|
||||
serde_json::to_value(50).expect("50 is valid Value");
|
||||
power_levels_content["events"]["org.matrix.msc3401.call.member"] =
|
||||
serde_json::to_value(50).expect("50 is valid Value");
|
||||
}
|
||||
|
||||
if let Some(power_level_content_override) = power_level_content_override {
|
||||
let json: JsonObject = serde_json::from_str(power_level_content_override.json().get())
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid power_level_content_override."))?;
|
||||
|
||||
for (key, value) in json {
|
||||
power_levels_content[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(power_levels_content)
|
||||
}
|
||||
|
||||
/// if a room is being created with a room alias, run our checks
|
||||
async fn room_alias_check(
|
||||
room_alias_name: &str, appservice_info: &Option<RegistrationInfo>,
|
||||
) -> Result<OwnedRoomAliasId> {
|
||||
// Basic checks on the room alias validity
|
||||
if room_alias_name.contains(':') {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Room alias contained `:` which is not allowed. Please note that this expects a localpart, not the full \
|
||||
room alias.",
|
||||
));
|
||||
} else if room_alias_name.contains(char::is_whitespace) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Room alias contained spaces which is not a valid room alias.",
|
||||
));
|
||||
}
|
||||
|
||||
// check if room alias is forbidden
|
||||
if services()
|
||||
.globals
|
||||
.forbidden_alias_names()
|
||||
.is_match(room_alias_name)
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Room alias name is forbidden."));
|
||||
}
|
||||
|
||||
let full_room_alias = RoomAliasId::parse(format!("#{}:{}", room_alias_name, services().globals.config.server_name))
|
||||
.map_err(|e| {
|
||||
info!("Failed to parse room alias {room_alias_name}: {e}");
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Invalid room alias specified.")
|
||||
})?;
|
||||
|
||||
if services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&full_room_alias)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::RoomInUse, "Room alias already exists."));
|
||||
}
|
||||
|
||||
if let Some(ref info) = appservice_info {
|
||||
if !info.aliases.is_match(full_room_alias.as_str()) {
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
|
||||
}
|
||||
} else if services()
|
||||
.appservice
|
||||
.is_exclusive_alias(&full_room_alias)
|
||||
.await
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice."));
|
||||
}
|
||||
|
||||
debug_info!("Full room alias: {full_room_alias}");
|
||||
|
||||
Ok(full_room_alias)
|
||||
}
|
||||
|
||||
/// if a room is being created with a custom room ID, run our checks against it
|
||||
fn custom_room_id_check(custom_room_id: &str) -> Result<OwnedRoomId> {
|
||||
// apply forbidden room alias checks to custom room IDs too
|
||||
if services()
|
||||
.globals
|
||||
.forbidden_alias_names()
|
||||
.is_match(custom_room_id)
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Custom room ID is forbidden."));
|
||||
}
|
||||
|
||||
if custom_room_id.contains(':') {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Custom room ID contained `:` which is not allowed. Please note that this expects a localpart, not the \
|
||||
full room ID.",
|
||||
));
|
||||
} else if custom_room_id.contains(char::is_whitespace) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Custom room ID contained spaces which is not valid.",
|
||||
));
|
||||
}
|
||||
|
||||
let full_room_id = format!("!{}:{}", custom_room_id, services().globals.config.server_name);
|
||||
|
||||
debug_info!("Full custom room ID: {full_room_id}");
|
||||
|
||||
RoomId::parse(full_room_id).map_err(|e| {
|
||||
info!("User attempted to create room with custom room ID {custom_room_id} but failed parsing: {e}");
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Custom room ID could not be parsed")
|
||||
})
|
||||
}
|
189
src/api/client/search.rs
Normal file
189
src/api/client/search.rs
Normal file
|
@ -0,0 +1,189 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
search::search_events::{
|
||||
self,
|
||||
v3::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
|
||||
},
|
||||
},
|
||||
events::AnyStateEvent,
|
||||
serde::Raw,
|
||||
uint, OwnedRoomId,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/r0/search`
|
||||
///
|
||||
/// Searches rooms for messages.
|
||||
///
|
||||
/// - Only works if the user is currently joined to the room (TODO: Respect
|
||||
/// history visibility)
|
||||
pub(crate) async fn search_events_route(body: Ruma<search_events::v3::Request>) -> Result<search_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let search_criteria = body.search_categories.room_events.as_ref().unwrap();
|
||||
let filter = &search_criteria.filter;
|
||||
let include_state = &search_criteria.include_state;
|
||||
|
||||
let room_ids = filter.rooms.clone().unwrap_or_else(|| {
|
||||
services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
.collect()
|
||||
});
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit: usize = filter
|
||||
.limit
|
||||
.unwrap_or_else(|| uint!(10))
|
||||
.try_into()
|
||||
.unwrap_or(10)
|
||||
.min(100);
|
||||
|
||||
let mut room_states: BTreeMap<OwnedRoomId, Vec<Raw<AnyStateEvent>>> = BTreeMap::new();
|
||||
|
||||
if include_state.is_some_and(|include_state| include_state) {
|
||||
for room_id in &room_ids {
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
// check if sender_user can see state events
|
||||
if services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, room_id)?
|
||||
{
|
||||
let room_state = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(room_id)
|
||||
.await?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
debug!("Room state: {:?}", room_state);
|
||||
|
||||
room_states.insert(room_id.clone(), room_state);
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut searches = Vec::new();
|
||||
|
||||
for room_id in &room_ids {
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(search) = services()
|
||||
.rooms
|
||||
.search
|
||||
.search_pdus(room_id, &search_criteria.search_term)?
|
||||
{
|
||||
searches.push(search.0.peekable());
|
||||
}
|
||||
}
|
||||
|
||||
let skip: usize = match body.next_batch.as_ref().map(|s| s.parse()) {
|
||||
Some(Ok(s)) => s,
|
||||
Some(Err(_)) => return Err(Error::BadRequest(ErrorKind::InvalidParam, "Invalid next_batch token.")),
|
||||
None => 0, // Default to the start
|
||||
};
|
||||
|
||||
let mut results = Vec::new();
|
||||
let next_batch: usize = skip.saturating_add(limit);
|
||||
|
||||
for _ in 0..next_batch {
|
||||
if let Some(s) = searches
|
||||
.iter_mut()
|
||||
.map(|s| (s.peek().cloned(), s))
|
||||
.max_by_key(|(peek, _)| peek.clone())
|
||||
.and_then(|(_, i)| i.next())
|
||||
{
|
||||
results.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
let results: Vec<_> = results
|
||||
.iter()
|
||||
.filter_map(|result| {
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_from_id(result)
|
||||
.ok()?
|
||||
.filter(|pdu| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
})
|
||||
.map(|result| {
|
||||
Ok::<_, Error>(SearchResult {
|
||||
context: EventContextResult {
|
||||
end: None,
|
||||
events_after: Vec::new(),
|
||||
events_before: Vec::new(),
|
||||
profile_info: BTreeMap::new(),
|
||||
start: None,
|
||||
},
|
||||
rank: None,
|
||||
result: Some(result),
|
||||
})
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.collect();
|
||||
|
||||
let next_batch = if results.len() < limit {
|
||||
None
|
||||
} else {
|
||||
Some(next_batch.to_string())
|
||||
};
|
||||
|
||||
Ok(search_events::v3::Response::new(ResultCategories {
|
||||
room_events: ResultRoomEvents {
|
||||
count: Some(results.len().try_into().unwrap_or_else(|_| uint!(0))),
|
||||
groups: BTreeMap::new(), // TODO
|
||||
next_batch,
|
||||
results,
|
||||
state: room_states,
|
||||
highlights: search_criteria
|
||||
.search_term
|
||||
.split_terminator(|c: char| !c.is_alphanumeric())
|
||||
.map(str::to_lowercase)
|
||||
.collect(),
|
||||
},
|
||||
}))
|
||||
}
|
250
src/api/client/session.rs
Normal file
250
src/api/client/session.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
session::{
|
||||
get_login_types::{
|
||||
self,
|
||||
v3::{ApplicationServiceLoginType, PasswordLoginType},
|
||||
},
|
||||
login::{
|
||||
self,
|
||||
v3::{DiscoveryInfo, HomeserverInfo},
|
||||
},
|
||||
logout, logout_all,
|
||||
},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
UserId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::{services, utils, utils::hash, Error, Result, Ruma};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
//exp: usize,
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/login`
|
||||
///
|
||||
/// Get the supported login types of this server. One of these should be used as
|
||||
/// the `type` field when logging in.
|
||||
pub(crate) async fn get_login_types_route(
|
||||
_body: Ruma<get_login_types::v3::Request>,
|
||||
) -> Result<get_login_types::v3::Response> {
|
||||
Ok(get_login_types::v3::Response::new(vec![
|
||||
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
||||
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
||||
]))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/login`
|
||||
///
|
||||
/// Authenticates the user and returns an access token it can use in subsequent
|
||||
/// requests.
|
||||
///
|
||||
/// - The user needs to authenticate using their password (or if enabled using a
|
||||
/// json web token)
|
||||
/// - If `device_id` is known: invalidates old access token of that device
|
||||
/// - If `device_id` is unknown: creates a new device
|
||||
/// - Returns access token that is associated with the user and device
|
||||
///
|
||||
/// Note: You can use [`GET
|
||||
/// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
|
||||
/// supported login types.
|
||||
pub(crate) async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Response> {
|
||||
// Validate login method
|
||||
// TODO: Other login methods
|
||||
let user_id = match &body.login_info {
|
||||
#[allow(deprecated)]
|
||||
login::v3::LoginInfo::Password(login::v3::Password {
|
||||
identifier,
|
||||
password,
|
||||
user,
|
||||
..
|
||||
}) => {
|
||||
debug!("Got password login type");
|
||||
let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name())
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse(user)
|
||||
} else {
|
||||
warn!("Bad login type: {:?}", &body.login_info);
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
|
||||
}
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
|
||||
let hash = services()
|
||||
.users
|
||||
.password_hash(&user_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."))?;
|
||||
|
||||
if hash.is_empty() {
|
||||
return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated"));
|
||||
}
|
||||
|
||||
if hash::verify_password(password, &hash).is_err() {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
login::v3::LoginInfo::Token(login::v3::Token {
|
||||
token,
|
||||
}) => {
|
||||
debug!("Got token login type");
|
||||
if let Some(jwt_decoding_key) = services().globals.jwt_decoding_key() {
|
||||
let token =
|
||||
jsonwebtoken::decode::<Claims>(token, jwt_decoding_key, &jsonwebtoken::Validation::default())
|
||||
.map_err(|e| {
|
||||
warn!("Failed to parse JWT token from user logging in: {e}");
|
||||
Error::BadRequest(ErrorKind::InvalidUsername, "Token is invalid.")
|
||||
})?;
|
||||
|
||||
let username = token.claims.sub.to_lowercase();
|
||||
|
||||
UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| {
|
||||
warn!("Failed to parse username from user logging in: {e}");
|
||||
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
|
||||
})?
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Unknown,
|
||||
"Token login is not supported (server has no jwt decoding key).",
|
||||
));
|
||||
}
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
|
||||
identifier,
|
||||
user,
|
||||
}) => {
|
||||
debug!("Got appservice login type");
|
||||
let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name())
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse(user)
|
||||
} else {
|
||||
warn!("Bad login type: {:?}", &body.login_info);
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
|
||||
}
|
||||
.map_err(|e| {
|
||||
warn!("Failed to parse username from appservice logging in: {e}");
|
||||
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
|
||||
})?;
|
||||
|
||||
if let Some(ref info) = body.appservice_info {
|
||||
if !info.is_user_match(&user_id) {
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing appservice token."));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
_ => {
|
||||
warn!("Unsupported or unknown login type: {:?}", &body.login_info);
|
||||
debug!("JSON body: {:?}", &body.json_body);
|
||||
return Err(Error::BadRequest(ErrorKind::Unknown, "Unsupported or unknown login type."));
|
||||
},
|
||||
};
|
||||
|
||||
// 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());
|
||||
|
||||
// Generate a new token for the device
|
||||
let token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// Determine if device_id was provided and exists in the db for this user
|
||||
let device_exists = body.device_id.as_ref().map_or(false, |device_id| {
|
||||
services()
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
.any(|x| x.as_ref().map_or(false, |v| v == device_id))
|
||||
});
|
||||
|
||||
if device_exists {
|
||||
services().users.set_token(&user_id, &device_id, &token)?;
|
||||
} else {
|
||||
services()
|
||||
.users
|
||||
.create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone())?;
|
||||
}
|
||||
|
||||
// send client well-known if specified so the client knows to reconfigure itself
|
||||
let client_discovery_info: Option<DiscoveryInfo> = services()
|
||||
.globals
|
||||
.well_known_client()
|
||||
.as_ref()
|
||||
.map(|server| DiscoveryInfo::new(HomeserverInfo::new(server.to_string())));
|
||||
|
||||
info!("{user_id} logged in");
|
||||
|
||||
// home_server is deprecated but apparently must still be sent despite it being
|
||||
// deprecated over 6 years ago. initially i thought this macro was unnecessary,
|
||||
// but ruma uses this same macro for the same reason so...
|
||||
#[allow(deprecated)]
|
||||
Ok(login::v3::Response {
|
||||
user_id,
|
||||
access_token: token,
|
||||
device_id,
|
||||
well_known: client_discovery_info,
|
||||
expires_in: None,
|
||||
home_server: Some(services().globals.server_name().to_owned()),
|
||||
refresh_token: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/logout`
|
||||
///
|
||||
/// Log out the current device.
|
||||
///
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
pub(crate) async fn logout_route(body: Ruma<logout::v3::Request>) -> Result<logout::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
services().users.remove_device(sender_user, sender_device)?;
|
||||
|
||||
// send device list update for user after logout
|
||||
services().users.mark_device_key_update(sender_user)?;
|
||||
|
||||
Ok(logout::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/logout/all`
|
||||
///
|
||||
/// Log out all devices of this user.
|
||||
///
|
||||
/// - Invalidates all access tokens
|
||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
///
|
||||
/// Note: This is equivalent to calling [`GET
|
||||
/// /_matrix/client/r0/logout`](fn.logout_route.html) from each device of this
|
||||
/// user.
|
||||
pub(crate) async fn logout_all_route(body: Ruma<logout_all::v3::Request>) -> Result<logout_all::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
for device_id in services().users.all_device_ids(sender_user).flatten() {
|
||||
services().users.remove_device(sender_user, &device_id)?;
|
||||
}
|
||||
|
||||
// send device list update for user after logout
|
||||
services().users.mark_device_key_update(sender_user)?;
|
||||
|
||||
Ok(logout_all::v3::Response::new())
|
||||
}
|
56
src/api/client/space.rs
Normal file
56
src/api/client/space.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, space::get_hierarchy},
|
||||
uint, UInt,
|
||||
};
|
||||
|
||||
use crate::{service::rooms::spaces::PagnationToken, services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy`
|
||||
///
|
||||
/// Paginates over the space tree in a depth-first manner to locate child rooms
|
||||
/// of a given space.
|
||||
pub(crate) async fn get_hierarchy_route(body: Ruma<get_hierarchy::v1::Request>) -> Result<get_hierarchy::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let limit: usize = body
|
||||
.limit
|
||||
.unwrap_or_else(|| uint!(10))
|
||||
.try_into()
|
||||
.unwrap_or(10)
|
||||
.min(100);
|
||||
|
||||
let max_depth = body
|
||||
.max_depth
|
||||
.unwrap_or_else(|| UInt::from(3_u32))
|
||||
.min(UInt::from(10_u32));
|
||||
|
||||
let key = body
|
||||
.from
|
||||
.as_ref()
|
||||
.and_then(|s| PagnationToken::from_str(s).ok());
|
||||
|
||||
// Should prevent unexpeded behaviour in (bad) clients
|
||||
if let Some(ref token) = key {
|
||||
if token.suggested_only != body.suggested_only || token.max_depth != max_depth {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"suggested_only and max_depth cannot change on paginated requests",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.spaces
|
||||
.get_client_hierarchy(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
limit,
|
||||
key.map_or(0, |token| token.skip.try_into().unwrap_or(0)),
|
||||
max_depth.try_into().unwrap_or(3),
|
||||
body.suggested_only,
|
||||
)
|
||||
.await
|
||||
}
|
278
src/api/client/state.rs
Normal file
278
src/api/client/state.rs
Normal file
|
@ -0,0 +1,278 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
state::{get_state_events, get_state_events_for_key, send_state_event},
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
},
|
||||
AnyStateEventContent, StateEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, RoomId, UserId,
|
||||
};
|
||||
use tracing::{error, log::warn};
|
||||
|
||||
use crate::{
|
||||
service::{pdu::PduBuilder, server_is_ours},
|
||||
services, Error, Result, Ruma, RumaResponse,
|
||||
};
|
||||
|
||||
/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
|
||||
///
|
||||
/// Sends a state event into the room.
|
||||
///
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is
|
||||
/// allowed
|
||||
/// - If event is new `canonical_alias`: Rejects if alias is incorrect
|
||||
pub(crate) async fn send_state_event_for_key_route(
|
||||
body: Ruma<send_state_event::v3::Request>,
|
||||
) -> Result<send_state_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event_id = send_state_event_for_key_helper(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_type,
|
||||
&body.body.body,
|
||||
body.state_key.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(send_state_event::v3::Response {
|
||||
event_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}`
|
||||
///
|
||||
/// Sends a state event into the room.
|
||||
///
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is
|
||||
/// allowed
|
||||
/// - If event is new `canonical_alias`: Rejects if alias is incorrect
|
||||
pub(crate) async fn send_state_event_for_empty_key_route(
|
||||
body: Ruma<send_state_event::v3::Request>,
|
||||
) -> Result<RumaResponse<send_state_event::v3::Response>> {
|
||||
send_state_event_for_key_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/rooms/{roomid}/state`
|
||||
///
|
||||
/// Get all state events for a room.
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world
|
||||
/// readable
|
||||
pub(crate) async fn get_state_events_route(
|
||||
body: Ruma<get_state_events::v3::Request>,
|
||||
) -> Result<get_state_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(get_state_events::v3::Response {
|
||||
room_state: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&body.room_id)
|
||||
.await?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}/{stateKey}`
|
||||
///
|
||||
/// Get single state event of a room with the specified state key.
|
||||
/// The optional query parameter `?format=event|content` allows returning the
|
||||
/// full room state event or just the state event's content (default behaviour)
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world
|
||||
/// readable
|
||||
pub(crate) async fn get_state_events_for_key_route(
|
||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
||||
) -> Result<get_state_events_for_key::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &body.event_type, &body.state_key)?
|
||||
.ok_or_else(|| {
|
||||
warn!("State event {:?} not found in room {:?}", &body.event_type, &body.room_id);
|
||||
Error::BadRequest(ErrorKind::NotFound, "State event not found.")
|
||||
})?;
|
||||
if body
|
||||
.format
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.to_lowercase().eq("event"))
|
||||
{
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
content: None,
|
||||
event: serde_json::from_str(event.to_state_event().json().get()).map_err(|e| {
|
||||
error!("Invalid room state event in database: {}", e);
|
||||
Error::bad_database("Invalid room state event in database")
|
||||
})?,
|
||||
})
|
||||
} else {
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
content: Some(serde_json::from_str(event.content.get()).map_err(|e| {
|
||||
error!("Invalid room state event content in database: {}", e);
|
||||
Error::bad_database("Invalid room state event content in database")
|
||||
})?),
|
||||
event: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}`
|
||||
///
|
||||
/// Get single state event of a room.
|
||||
/// The optional query parameter `?format=event|content` allows returning the
|
||||
/// full room state event or just the state event's content (default behaviour)
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world
|
||||
/// readable
|
||||
pub(crate) async fn get_state_events_for_empty_key_route(
|
||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
||||
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
|
||||
get_state_events_for_key_route(body).await.map(RumaResponse)
|
||||
}
|
||||
|
||||
async fn send_state_event_for_key_helper(
|
||||
sender: &UserId, room_id: &RoomId, event_type: &StateEventType, json: &Raw<AnyStateEventContent>, state_key: String,
|
||||
) -> Result<Arc<EventId>> {
|
||||
allowed_to_send_state_event(room_id, event_type, json).await?;
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.await
|
||||
.entry(room_id.to_owned())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let event_id = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: serde_json::from_str(json.json().get()).expect("content is valid json"),
|
||||
unsigned: None,
|
||||
state_key: Some(state_key),
|
||||
redacts: None,
|
||||
},
|
||||
sender,
|
||||
room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(event_id)
|
||||
}
|
||||
|
||||
async fn allowed_to_send_state_event(
|
||||
room_id: &RoomId, event_type: &StateEventType, json: &Raw<AnyStateEventContent>,
|
||||
) -> Result<()> {
|
||||
match event_type {
|
||||
// Forbid m.room.encryption if encryption is disabled
|
||||
StateEventType::RoomEncryption => {
|
||||
if !services().globals.allow_encryption() {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Encryption has been disabled"));
|
||||
}
|
||||
},
|
||||
// admin room is a sensitive room, it should not ever be made public
|
||||
StateEventType::RoomJoinRules => {
|
||||
if let Some(admin_room_id) = service::admin::Service::get_admin_room().await? {
|
||||
if admin_room_id == room_id {
|
||||
if let Ok(join_rule) = serde_json::from_str::<RoomJoinRulesEventContent>(json.json().get()) {
|
||||
if join_rule.join_rule == JoinRule::Public {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Admin room is not allowed to be public.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// admin room is a sensitive room, it should not ever be made world readable
|
||||
StateEventType::RoomHistoryVisibility => {
|
||||
if let Some(admin_room_id) = service::admin::Service::get_admin_room().await? {
|
||||
if admin_room_id == room_id {
|
||||
if let Ok(visibility_content) =
|
||||
serde_json::from_str::<RoomHistoryVisibilityEventContent>(json.json().get())
|
||||
{
|
||||
if visibility_content.history_visibility == HistoryVisibility::WorldReadable {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Admin room is not allowed to be made world readable (public room history).",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// TODO: allow alias if it previously existed
|
||||
StateEventType::RoomCanonicalAlias => {
|
||||
if let Ok(canonical_alias) = serde_json::from_str::<RoomCanonicalAliasEventContent>(json.json().get()) {
|
||||
let mut aliases = canonical_alias.alt_aliases.clone();
|
||||
|
||||
if let Some(alias) = canonical_alias.alias {
|
||||
aliases.push(alias);
|
||||
}
|
||||
|
||||
for alias in aliases {
|
||||
if !server_is_ours(alias.server_name())
|
||||
|| services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&alias)?
|
||||
.filter(|room| room == room_id) // Make sure it's the right room
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You are only allowed to send canonical_alias events when its aliases already exist",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
1662
src/api/client/sync.rs
Normal file
1662
src/api/client/sync.rs
Normal file
File diff suppressed because it is too large
Load diff
112
src/api/client/tag.rs
Normal file
112
src/api/client/tag.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
api::client::tag::{create_tag, delete_tag, get_tags},
|
||||
events::{
|
||||
tag::{TagEvent, TagEventContent},
|
||||
RoomAccountDataEventType,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
|
||||
///
|
||||
/// Adds a tag to the room.
|
||||
///
|
||||
/// - Inserts the tag into the tag event of the room account data.
|
||||
pub(crate) async fn update_tag_route(body: Ruma<create_tag::v3::Request>) -> Result<create_tag::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let mut tags_event = event.map_or_else(
|
||||
|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
|
||||
)?;
|
||||
|
||||
tags_event
|
||||
.content
|
||||
.tags
|
||||
.insert(body.tag.clone().into(), body.tag_info.clone());
|
||||
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(create_tag::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
|
||||
///
|
||||
/// Deletes a tag from the room.
|
||||
///
|
||||
/// - Removes the tag from the tag event of the room account data.
|
||||
pub(crate) async fn delete_tag_route(body: Ruma<delete_tag::v3::Request>) -> Result<delete_tag::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let mut tags_event = event.map_or_else(
|
||||
|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
|
||||
)?;
|
||||
|
||||
tags_event.content.tags.remove(&body.tag.clone().into());
|
||||
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(delete_tag::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags`
|
||||
///
|
||||
/// Returns tags on the room.
|
||||
///
|
||||
/// - Gets the tag event of the room account data.
|
||||
pub(crate) async fn get_tags_route(body: Ruma<get_tags::v3::Request>) -> Result<get_tags::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let tags_event = event.map_or_else(
|
||||
|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
|
||||
)?;
|
||||
|
||||
Ok(get_tags::v3::Response {
|
||||
tags: tags_event.content.tags,
|
||||
})
|
||||
}
|
17
src/api/client/thirdparty.rs
Normal file
17
src/api/client/thirdparty.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::api::client::thirdparty::get_protocols;
|
||||
|
||||
use crate::{Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/thirdparty/protocols`
|
||||
///
|
||||
/// TODO: Fetches all metadata about protocols supported by the homeserver.
|
||||
pub(crate) async fn get_protocols_route(
|
||||
_body: Ruma<get_protocols::v3::Request>,
|
||||
) -> Result<get_protocols::v3::Response> {
|
||||
// TODO
|
||||
Ok(get_protocols::v3::Response {
|
||||
protocols: BTreeMap::new(),
|
||||
})
|
||||
}
|
51
src/api/client/threads.rs
Normal file
51
src/api/client/threads.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use ruma::{
|
||||
api::client::{error::ErrorKind, threads::get_threads},
|
||||
uint,
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/threads`
|
||||
pub(crate) async fn get_threads_route(body: Ruma<get_threads::v1::Request>) -> Result<get_threads::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = body
|
||||
.limit
|
||||
.unwrap_or_else(|| uint!(10))
|
||||
.try_into()
|
||||
.unwrap_or(10)
|
||||
.min(100);
|
||||
|
||||
let from = if let Some(from) = &body.from {
|
||||
from.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, ""))?
|
||||
} else {
|
||||
u64::MAX
|
||||
};
|
||||
|
||||
let threads = services()
|
||||
.rooms
|
||||
.threads
|
||||
.threads_until(sender_user, &body.room_id, from, &body.include)?
|
||||
.take(limit)
|
||||
.filter_map(Result::ok)
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &body.room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_batch = threads.last().map(|(count, _)| count.to_string());
|
||||
|
||||
Ok(get_threads::v1::Response {
|
||||
chunk: threads
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect(),
|
||||
next_batch,
|
||||
})
|
||||
}
|
90
src/api/client/to_device.rs
Normal file
90
src/api/client/to_device.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{error::ErrorKind, to_device::send_event_to_device},
|
||||
federation::{self, transactions::edu::DirectDeviceContent},
|
||||
},
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
};
|
||||
|
||||
use crate::{services, user_is_local, Error, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId}`
|
||||
///
|
||||
/// Send a to-device event to a set of client devices.
|
||||
pub(crate) async fn send_event_to_device_route(
|
||||
body: Ruma<send_event_to_device::v3::Request>,
|
||||
) -> Result<send_event_to_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_deref();
|
||||
|
||||
// Check if this is a new transaction id
|
||||
if services()
|
||||
.transaction_ids
|
||||
.existing_txnid(sender_user, sender_device, &body.txn_id)?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(send_event_to_device::v3::Response {});
|
||||
}
|
||||
|
||||
for (target_user_id, map) in &body.messages {
|
||||
for (target_device_id_maybe, event) in map {
|
||||
if !user_is_local(target_user_id) {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(target_device_id_maybe.clone(), event.clone());
|
||||
let mut messages = BTreeMap::new();
|
||||
messages.insert(target_user_id.clone(), map);
|
||||
let count = services().globals.next_count()?;
|
||||
|
||||
services().sending.send_edu_server(
|
||||
target_user_id.server_name(),
|
||||
serde_json::to_vec(&federation::transactions::edu::Edu::DirectToDevice(DirectDeviceContent {
|
||||
sender: sender_user.clone(),
|
||||
ev_type: body.event_type.clone(),
|
||||
message_id: count.to_string().into(),
|
||||
messages,
|
||||
}))
|
||||
.expect("DirectToDevice EDU can be serialized"),
|
||||
)?;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
match target_device_id_maybe {
|
||||
DeviceIdOrAllDevices::DeviceId(target_device_id) => {
|
||||
services().users.add_to_device_event(
|
||||
sender_user,
|
||||
target_user_id,
|
||||
target_device_id,
|
||||
&body.event_type.to_string(),
|
||||
event
|
||||
.deserialize_as()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid"))?,
|
||||
)?;
|
||||
},
|
||||
|
||||
DeviceIdOrAllDevices::AllDevices => {
|
||||
for target_device_id in services().users.all_device_ids(target_user_id) {
|
||||
services().users.add_to_device_event(
|
||||
sender_user,
|
||||
target_user_id,
|
||||
&target_device_id?,
|
||||
&body.event_type.to_string(),
|
||||
event
|
||||
.deserialize_as()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid"))?,
|
||||
)?;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save transaction id with empty data
|
||||
services()
|
||||
.transaction_ids
|
||||
.add_txnid(sender_user, sender_device, &body.txn_id, &[])?;
|
||||
|
||||
Ok(send_event_to_device::v3::Response {})
|
||||
}
|
59
src/api/client/typing.rs
Normal file
59
src/api/client/typing.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use ruma::api::client::{error::ErrorKind, typing::create_typing_event};
|
||||
|
||||
use crate::{services, utils, Error, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}`
|
||||
///
|
||||
/// Sets the typing state of the sender user.
|
||||
pub(crate) async fn create_typing_event_route(
|
||||
body: Ruma<create_typing_event::v3::Request>,
|
||||
) -> Result<create_typing_event::v3::Response> {
|
||||
use create_typing_event::v3::Typing;
|
||||
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "You are not in this room."));
|
||||
}
|
||||
|
||||
if let Typing::Yes(duration) = body.state {
|
||||
let duration = utils::clamp(
|
||||
duration.as_millis().try_into().unwrap_or(u64::MAX),
|
||||
services()
|
||||
.globals
|
||||
.config
|
||||
.typing_client_timeout_min_s
|
||||
.checked_mul(1000)
|
||||
.unwrap(),
|
||||
services()
|
||||
.globals
|
||||
.config
|
||||
.typing_client_timeout_max_s
|
||||
.checked_mul(1000)
|
||||
.unwrap(),
|
||||
);
|
||||
services()
|
||||
.rooms
|
||||
.typing
|
||||
.typing_add(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
utils::millis_since_unix_epoch()
|
||||
.checked_add(duration)
|
||||
.expect("user typing timeout should not get this high"),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
services()
|
||||
.rooms
|
||||
.typing
|
||||
.typing_remove(sender_user, &body.room_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(create_typing_event::v3::Response {})
|
||||
}
|
45
src/api/client/unstable.rs
Normal file
45
src/api/client/unstable.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use ruma::{
|
||||
api::client::{error::ErrorKind, membership::mutual_rooms},
|
||||
OwnedRoomId,
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms`
|
||||
///
|
||||
/// Gets all the rooms the sender shares with the specified user.
|
||||
///
|
||||
/// TODO: Implement pagination, currently this just returns everything
|
||||
///
|
||||
/// An implementation of [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
|
||||
pub(crate) async fn get_mutual_rooms_route(
|
||||
body: Ruma<mutual_rooms::unstable::Request>,
|
||||
) -> Result<mutual_rooms::unstable::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if sender_user == &body.user_id {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Unknown,
|
||||
"You cannot request rooms in common with yourself.",
|
||||
));
|
||||
}
|
||||
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
return Ok(mutual_rooms::unstable::Response {
|
||||
joined: vec![],
|
||||
next_batch_token: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mutual_rooms: Vec<OwnedRoomId> = services()
|
||||
.rooms
|
||||
.user
|
||||
.get_shared_rooms(vec![sender_user.clone(), body.user_id.clone()])?
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
Ok(mutual_rooms::unstable::Response {
|
||||
joined: mutual_rooms,
|
||||
next_batch_token: None,
|
||||
})
|
||||
}
|
174
src/api/client/unversioned.rs
Normal file
174
src/api/client/unversioned.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{response::IntoResponse, Json};
|
||||
use ruma::api::client::{
|
||||
discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
||||
discover_support::{self, Contact},
|
||||
get_supported_versions,
|
||||
},
|
||||
error::ErrorKind,
|
||||
};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/versions`
|
||||
///
|
||||
/// Get the versions of the specification and unstable features supported by
|
||||
/// this server.
|
||||
///
|
||||
/// - Versions take the form MAJOR.MINOR.PATCH
|
||||
/// - Only the latest PATCH release will be reported for each MAJOR.MINOR value
|
||||
/// - Unstable features are namespaced and may include version information in
|
||||
/// their name
|
||||
///
|
||||
/// Note: Unstable features are used while developing new features. Clients
|
||||
/// should avoid using unstable features in their stable releases
|
||||
pub(crate) async fn get_supported_versions_route(
|
||||
_body: Ruma<get_supported_versions::Request>,
|
||||
) -> Result<get_supported_versions::Response> {
|
||||
let resp = get_supported_versions::Response {
|
||||
versions: vec![
|
||||
"r0.0.1".to_owned(),
|
||||
"r0.1.0".to_owned(),
|
||||
"r0.2.0".to_owned(),
|
||||
"r0.3.0".to_owned(),
|
||||
"r0.4.0".to_owned(),
|
||||
"r0.5.0".to_owned(),
|
||||
"r0.6.0".to_owned(),
|
||||
"r0.6.1".to_owned(),
|
||||
"v1.1".to_owned(),
|
||||
"v1.2".to_owned(),
|
||||
"v1.3".to_owned(),
|
||||
"v1.4".to_owned(),
|
||||
"v1.5".to_owned(),
|
||||
],
|
||||
unstable_features: BTreeMap::from_iter([
|
||||
("org.matrix.e2e_cross_signing".to_owned(), true),
|
||||
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
|
||||
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
|
||||
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
||||
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
||||
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
||||
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
||||
("org.matrix.msc3575".to_owned(), true), /* sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1588877046) */
|
||||
]),
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// # `GET /.well-known/matrix/client`
|
||||
///
|
||||
/// Returns the .well-known URL if it is configured, otherwise returns 404.
|
||||
pub(crate) async fn well_known_client(
|
||||
_body: Ruma<discover_homeserver::Request>,
|
||||
) -> Result<discover_homeserver::Response> {
|
||||
let client_url = match services().globals.well_known_client() {
|
||||
Some(url) => url.to_string(),
|
||||
None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
|
||||
};
|
||||
|
||||
Ok(discover_homeserver::Response {
|
||||
homeserver: HomeserverInfo {
|
||||
base_url: client_url.clone(),
|
||||
},
|
||||
identity_server: None,
|
||||
sliding_sync_proxy: Some(SlidingSyncProxyInfo {
|
||||
url: client_url,
|
||||
}),
|
||||
tile_server: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /.well-known/matrix/support`
|
||||
///
|
||||
/// Server support contact and support page of a homeserver's domain.
|
||||
pub(crate) async fn well_known_support(_body: Ruma<discover_support::Request>) -> Result<discover_support::Response> {
|
||||
let support_page = services()
|
||||
.globals
|
||||
.well_known_support_page()
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
let role = services().globals.well_known_support_role().clone();
|
||||
|
||||
// support page or role must be either defined for this to be valid
|
||||
if support_page.is_none() && role.is_none() {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
|
||||
}
|
||||
|
||||
let email_address = services().globals.well_known_support_email().clone();
|
||||
let matrix_id = services().globals.well_known_support_mxid().clone();
|
||||
|
||||
// if a role is specified, an email address or matrix id is required
|
||||
if role.is_some() && (email_address.is_none() && matrix_id.is_none()) {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
|
||||
}
|
||||
|
||||
// TOOD: support defining multiple contacts in the config
|
||||
let mut contacts: Vec<Contact> = vec![];
|
||||
|
||||
if let Some(role) = role {
|
||||
let contact = Contact {
|
||||
role,
|
||||
email_address,
|
||||
matrix_id,
|
||||
};
|
||||
|
||||
contacts.push(contact);
|
||||
}
|
||||
|
||||
// support page or role+contacts must be either defined for this to be valid
|
||||
if contacts.is_empty() && support_page.is_none() {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
|
||||
}
|
||||
|
||||
Ok(discover_support::Response {
|
||||
contacts,
|
||||
support_page,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /client/server.json`
|
||||
///
|
||||
/// Endpoint provided by sliding sync proxy used by some clients such as Element
|
||||
/// Web as a non-standard health check.
|
||||
pub(crate) async fn syncv3_client_server_json() -> Result<impl IntoResponse> {
|
||||
let server_url = match services().globals.well_known_client() {
|
||||
Some(url) => url.to_string(),
|
||||
None => match services().globals.well_known_server() {
|
||||
Some(url) => url.to_string(),
|
||||
None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"server": server_url,
|
||||
"version": conduit::version::conduwuit(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// # `GET /_conduwuit/server_version`
|
||||
///
|
||||
/// Conduwuit-specific API to get the server version, results akin to
|
||||
/// `/_matrix/federation/v1/version`
|
||||
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
|
||||
Ok(Json(serde_json::json!({
|
||||
"name": "conduwuit",
|
||||
"version": conduit::version::conduwuit(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// # `GET /_conduwuit/local_user_count`
|
||||
///
|
||||
/// conduwuit-specific API to return the amount of users registered on this
|
||||
/// homeserver. Endpoint is disabled if federation is disabled for privacy. This
|
||||
/// only includes active users (not deactivated, no guests, etc)
|
||||
pub(crate) async fn conduwuit_local_user_count() -> Result<impl IntoResponse> {
|
||||
let user_count = services().users.list_local_users()?.len();
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"count": user_count
|
||||
})))
|
||||
}
|
102
src/api/client/user_directory.rs
Normal file
102
src/api/client/user_directory.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use ruma::{
|
||||
api::client::user_directory::search_users,
|
||||
events::{
|
||||
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
StateEventType,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{services, Result, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/r0/user_directory/search`
|
||||
///
|
||||
/// Searches all known users for a match.
|
||||
///
|
||||
/// - Hides any local users that aren't in any public rooms (i.e. those that
|
||||
/// have the join rule set to public)
|
||||
/// and don't share a room with the sender
|
||||
pub(crate) async fn search_users_route(body: Ruma<search_users::v3::Request>) -> Result<search_users::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let limit = usize::try_from(body.limit).unwrap_or(10); // default limit is 10
|
||||
|
||||
let mut users = services().users.iter().filter_map(|user_id| {
|
||||
// Filter out buggy users (they should not exist, but you never know...)
|
||||
let user_id = user_id.ok()?;
|
||||
|
||||
let user = search_users::v3::User {
|
||||
user_id: user_id.clone(),
|
||||
display_name: services().users.displayname(&user_id).ok()?,
|
||||
avatar_url: services().users.avatar_url(&user_id).ok()?,
|
||||
};
|
||||
|
||||
let user_id_matches = user
|
||||
.user_id
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.contains(&body.search_term.to_lowercase());
|
||||
|
||||
let user_displayname_matches = user
|
||||
.display_name
|
||||
.as_ref()
|
||||
.filter(|name| {
|
||||
name.to_lowercase()
|
||||
.contains(&body.search_term.to_lowercase())
|
||||
})
|
||||
.is_some();
|
||||
|
||||
if !user_id_matches && !user_displayname_matches {
|
||||
return None;
|
||||
}
|
||||
|
||||
// It's a matching user, but is the sender allowed to see them?
|
||||
let mut user_visible = false;
|
||||
|
||||
let user_is_in_public_rooms = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(Result::ok)
|
||||
.any(|room| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room, &StateEventType::RoomJoinRules, "")
|
||||
.map_or(false, |event| {
|
||||
event.map_or(false, |event| {
|
||||
serde_json::from_str(event.content.get())
|
||||
.map_or(false, |r: RoomJoinRulesEventContent| r.join_rule == JoinRule::Public)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if user_is_in_public_rooms {
|
||||
user_visible = true;
|
||||
} else {
|
||||
let user_is_in_shared_rooms = services()
|
||||
.rooms
|
||||
.user
|
||||
.get_shared_rooms(vec![sender_user.clone(), user_id])
|
||||
.ok()?
|
||||
.next()
|
||||
.is_some();
|
||||
|
||||
if user_is_in_shared_rooms {
|
||||
user_visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !user_visible {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(user)
|
||||
});
|
||||
|
||||
let results = users.by_ref().take(limit).collect();
|
||||
let limited = users.next().is_some();
|
||||
|
||||
Ok(search_users::v3::Response {
|
||||
results,
|
||||
limited,
|
||||
})
|
||||
}
|
51
src/api/client/voip.rs
Normal file
51
src/api/client/voip.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use hmac::{Hmac, Mac};
|
||||
use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch};
|
||||
use sha1::Sha1;
|
||||
|
||||
use crate::{services, Result, Ruma};
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
|
||||
/// # `GET /_matrix/client/r0/voip/turnServer`
|
||||
///
|
||||
/// TODO: Returns information about the recommended turn server.
|
||||
pub(crate) async fn turn_server_route(
|
||||
body: Ruma<get_turn_server_info::v3::Request>,
|
||||
) -> Result<get_turn_server_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let turn_secret = services().globals.turn_secret().clone();
|
||||
|
||||
let (username, password) = if !turn_secret.is_empty() {
|
||||
let expiry = SecondsSinceUnixEpoch::from_system_time(
|
||||
SystemTime::now()
|
||||
.checked_add(Duration::from_secs(services().globals.turn_ttl()))
|
||||
.expect("TURN TTL should not get this high"),
|
||||
)
|
||||
.expect("time is valid");
|
||||
|
||||
let username: String = format!("{}:{}", expiry.get(), sender_user);
|
||||
|
||||
let mut mac = HmacSha1::new_from_slice(turn_secret.as_bytes()).expect("HMAC can take key of any size");
|
||||
mac.update(username.as_bytes());
|
||||
|
||||
let password: String = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
|
||||
|
||||
(username, password)
|
||||
} else {
|
||||
(
|
||||
services().globals.turn_username().clone(),
|
||||
services().globals.turn_password().clone(),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(get_turn_server_info::v3::Response {
|
||||
username,
|
||||
password,
|
||||
uris: services().globals.turn_uris().to_vec(),
|
||||
ttl: Duration::from_secs(services().globals.turn_ttl()),
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue