mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-07-07 21:16:24 +02:00
Compare commits
9 commits
6eea30b77a
...
b4ca934a91
Author | SHA1 | Date | |
---|---|---|---|
|
b4ca934a91 | ||
|
70ade6ad32 | ||
|
bfc2e09b87 | ||
|
6ccd6c0e5e | ||
|
cb15ac3c01 | ||
|
bad37eaf0d | ||
|
3bb5710356 | ||
|
64519ac672 | ||
|
74cf5445cc |
12 changed files with 456 additions and 66 deletions
|
@ -6,6 +6,7 @@ use conduwuit::{
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use futures::FutureExt;
|
||||||
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
|
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
|
||||||
|
|
||||||
use crate::{admin_command, admin_command_dispatch, get_room_info};
|
use crate::{admin_command, admin_command_dispatch, get_room_info};
|
||||||
|
@ -155,7 +156,10 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
|
||||||
evicting admins too)",
|
evicting admins too)",
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
|
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
{
|
||||||
warn!("Failed to leave room: {e}");
|
warn!("Failed to leave room: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +327,10 @@ async fn ban_list_of_rooms(&self) -> Result {
|
||||||
evicting admins too)",
|
evicting admins too)",
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
|
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
{
|
||||||
warn!("Failed to leave room: {e}");
|
warn!("Failed to leave room: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ use conduwuit::{
|
||||||
};
|
};
|
||||||
use conduwuit_api::client::{leave_all_rooms, update_avatar_url, update_displayname};
|
use conduwuit_api::client::{leave_all_rooms, update_avatar_url, update_displayname};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use futures::FutureExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
|
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
|
||||||
events::{
|
events::{
|
||||||
|
@ -655,7 +656,9 @@ pub(super) async fn force_leave_room(
|
||||||
return Err!("{user_id} is not joined in the room");
|
return Err!("{user_id} is not joined in the room");
|
||||||
}
|
}
|
||||||
|
|
||||||
leave_room(self.services, &user_id, &room_id, None).await?;
|
leave_room(self.services, &user_id, &room_id, None)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
|
|
||||||
self.write_str(&format!("{user_id} has left {room_id}.",))
|
self.write_str(&format!("{user_id} has left {room_id}.",))
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -763,7 +763,9 @@ pub(crate) async fn deactivate_route(
|
||||||
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await;
|
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await;
|
||||||
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await;
|
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await;
|
||||||
|
|
||||||
full_user_deactivate(&services, sender_user, &all_joined_rooms).await?;
|
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
|
|
||||||
info!("User {sender_user} deactivated their account.");
|
info!("User {sender_user} deactivated their account.");
|
||||||
|
|
||||||
|
@ -915,7 +917,9 @@ pub async fn full_user_deactivate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super::leave_all_rooms(services, user_id).await;
|
super::leave_all_rooms(services, user_id)
|
||||||
|
.boxed()
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,9 @@ async fn banned_room_check(
|
||||||
.collect()
|
.collect()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
full_user_deactivate(services, user_id, &all_joined_rooms).await?;
|
full_user_deactivate(services, user_id, &all_joined_rooms)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
||||||
|
@ -153,7 +155,9 @@ async fn banned_room_check(
|
||||||
.collect()
|
.collect()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
full_user_deactivate(services, user_id, &all_joined_rooms).await?;
|
full_user_deactivate(services, user_id, &all_joined_rooms)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err!(Request(Forbidden("This remote server is banned on this homeserver.")));
|
return Err!(Request(Forbidden("This remote server is banned on this homeserver.")));
|
||||||
|
@ -259,6 +263,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||||
room_id.server_name(),
|
room_id.server_name(),
|
||||||
client,
|
client,
|
||||||
)
|
)
|
||||||
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut servers = body.via.clone();
|
let mut servers = body.via.clone();
|
||||||
|
@ -478,6 +483,7 @@ pub(crate) async fn leave_room_route(
|
||||||
body: Ruma<leave_room::v3::Request>,
|
body: Ruma<leave_room::v3::Request>,
|
||||||
) -> Result<leave_room::v3::Response> {
|
) -> Result<leave_room::v3::Response> {
|
||||||
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone())
|
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone())
|
||||||
|
.boxed()
|
||||||
.await
|
.await
|
||||||
.map(|()| leave_room::v3::Response::new())
|
.map(|()| leave_room::v3::Response::new())
|
||||||
}
|
}
|
||||||
|
@ -1792,7 +1798,10 @@ pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
|
||||||
|
|
||||||
for room_id in all_rooms {
|
for room_id in all_rooms {
|
||||||
// ignore errors
|
// ignore errors
|
||||||
if let Err(e) = leave_room(services, user_id, &room_id, None).await {
|
if let Err(e) = leave_room(services, user_id, &room_id, None)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
{
|
||||||
warn!(%user_id, "Failed to leave {room_id} remotely: {e}");
|
warn!(%user_id, "Failed to leave {room_id} remotely: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,9 @@ where
|
||||||
.map(|(key, val)| (key, val.collect()))
|
.map(|(key, val)| (key, val.collect()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !populate {
|
if populate {
|
||||||
|
rooms.push(summary_to_chunk(summary.clone()));
|
||||||
|
} else {
|
||||||
children = children
|
children = children
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
|
@ -144,10 +146,8 @@ where
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if populate {
|
if !populate && queue.is_empty() && children.is_empty() {
|
||||||
rooms.push(summary_to_chunk(summary.clone()));
|
break;
|
||||||
} else if queue.is_empty() && children.is_empty() {
|
|
||||||
return Err!(Request(InvalidParam("Room IDs in token were not found.")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parents.insert(current_room.clone());
|
parents.insert(current_room.clone());
|
||||||
|
|
|
@ -6,6 +6,7 @@ use conduwuit::{
|
||||||
};
|
};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use futures::FutureExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, RoomId, UserId,
|
OwnedEventId, RoomId, UserId,
|
||||||
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
|
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
|
||||||
|
@ -59,6 +60,7 @@ pub(crate) async fn send_state_event_for_empty_key_route(
|
||||||
body: Ruma<send_state_event::v3::Request>,
|
body: Ruma<send_state_event::v3::Request>,
|
||||||
) -> Result<RumaResponse<send_state_event::v3::Response>> {
|
) -> Result<RumaResponse<send_state_event::v3::Response>> {
|
||||||
send_state_event_for_key_route(State(services), body)
|
send_state_event_for_key_route(State(services), body)
|
||||||
|
.boxed()
|
||||||
.await
|
.await
|
||||||
.map(RumaResponse)
|
.map(RumaResponse)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1009,8 +1009,6 @@ async fn calculate_state_incremental<'a>(
|
||||||
) -> Result<StateChanges> {
|
) -> Result<StateChanges> {
|
||||||
let since_shortstatehash = since_shortstatehash.unwrap_or(current_shortstatehash);
|
let since_shortstatehash = since_shortstatehash.unwrap_or(current_shortstatehash);
|
||||||
|
|
||||||
let state_changed = since_shortstatehash != current_shortstatehash;
|
|
||||||
|
|
||||||
let encrypted_room = services
|
let encrypted_room = services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
|
@ -1042,7 +1040,7 @@ async fn calculate_state_incremental<'a>(
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let state_diff_ids: OptionFuture<_> = (!full_state && state_changed)
|
let state_diff_ids: OptionFuture<_> = (!full_state)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
StreamExt::into_future(
|
StreamExt::into_future(
|
||||||
services
|
services
|
||||||
|
|
|
@ -2059,45 +2059,45 @@ fn default_database_backups_to_keep() -> i16 { 1 }
|
||||||
|
|
||||||
fn default_db_write_buffer_capacity_mb() -> f64 { 48.0 + parallelism_scaled_f64(4.0) }
|
fn default_db_write_buffer_capacity_mb() -> f64 { 48.0 + parallelism_scaled_f64(4.0) }
|
||||||
|
|
||||||
fn default_db_cache_capacity_mb() -> f64 { 128.0 + parallelism_scaled_f64(64.0) }
|
fn default_db_cache_capacity_mb() -> f64 { 512.0 + parallelism_scaled_f64(512.0) }
|
||||||
|
|
||||||
fn default_pdu_cache_capacity() -> u32 { parallelism_scaled_u32(10_000).saturating_add(100_000) }
|
fn default_pdu_cache_capacity() -> u32 { parallelism_scaled_u32(50_000).saturating_add(500_000) }
|
||||||
|
|
||||||
fn default_cache_capacity_modifier() -> f64 { 1.0 }
|
fn default_cache_capacity_modifier() -> f64 { 1.0 }
|
||||||
|
|
||||||
fn default_auth_chain_cache_capacity() -> u32 {
|
fn default_auth_chain_cache_capacity() -> u32 {
|
||||||
parallelism_scaled_u32(10_000).saturating_add(100_000)
|
parallelism_scaled_u32(50_000).saturating_add(500_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_shorteventid_cache_capacity() -> u32 {
|
fn default_shorteventid_cache_capacity() -> u32 {
|
||||||
parallelism_scaled_u32(50_000).saturating_add(100_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_eventidshort_cache_capacity() -> u32 {
|
|
||||||
parallelism_scaled_u32(25_000).saturating_add(100_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_eventid_pdu_cache_capacity() -> u32 {
|
|
||||||
parallelism_scaled_u32(25_000).saturating_add(100_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_shortstatekey_cache_capacity() -> u32 {
|
|
||||||
parallelism_scaled_u32(10_000).saturating_add(100_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_statekeyshort_cache_capacity() -> u32 {
|
|
||||||
parallelism_scaled_u32(10_000).saturating_add(100_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_servernameevent_data_cache_capacity() -> u32 {
|
|
||||||
parallelism_scaled_u32(100_000).saturating_add(500_000)
|
parallelism_scaled_u32(100_000).saturating_add(500_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_stateinfo_cache_capacity() -> u32 { parallelism_scaled_u32(100) }
|
fn default_eventidshort_cache_capacity() -> u32 {
|
||||||
|
parallelism_scaled_u32(100_000).saturating_add(500_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_eventid_pdu_cache_capacity() -> u32 {
|
||||||
|
parallelism_scaled_u32(50_000).saturating_add(500_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_shortstatekey_cache_capacity() -> u32 {
|
||||||
|
parallelism_scaled_u32(50_000).saturating_add(500_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_statekeyshort_cache_capacity() -> u32 {
|
||||||
|
parallelism_scaled_u32(50_000).saturating_add(500_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_servernameevent_data_cache_capacity() -> u32 {
|
||||||
|
parallelism_scaled_u32(200_000).saturating_add(500_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_stateinfo_cache_capacity() -> u32 { parallelism_scaled_u32(5000) }
|
||||||
|
|
||||||
fn default_roomid_spacehierarchy_cache_capacity() -> u32 { parallelism_scaled_u32(1000) }
|
fn default_roomid_spacehierarchy_cache_capacity() -> u32 { parallelism_scaled_u32(1000) }
|
||||||
|
|
||||||
fn default_dns_cache_entries() -> u32 { 32768 }
|
fn default_dns_cache_entries() -> u32 { 327680 }
|
||||||
|
|
||||||
fn default_dns_min_ttl() -> u64 { 60 * 180 }
|
fn default_dns_min_ttl() -> u64 { 60 * 180 }
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ fn descriptor_cf_options(
|
||||||
set_table_options(&mut opts, &desc, cache)?;
|
set_table_options(&mut opts, &desc, cache)?;
|
||||||
|
|
||||||
opts.set_min_write_buffer_number(1);
|
opts.set_min_write_buffer_number(1);
|
||||||
opts.set_max_write_buffer_number(2);
|
opts.set_max_write_buffer_number(3);
|
||||||
opts.set_write_buffer_size(desc.write_size);
|
opts.set_write_buffer_size(desc.write_size);
|
||||||
|
|
||||||
opts.set_target_file_size_base(desc.file_size);
|
opts.set_target_file_size_base(desc.file_size);
|
||||||
|
|
|
@ -4,7 +4,6 @@ mod execute;
|
||||||
mod grant;
|
mod grant;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
future::Future,
|
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{Arc, RwLock as StdRwLock, Weak},
|
sync::{Arc, RwLock as StdRwLock, Weak},
|
||||||
};
|
};
|
||||||
|
@ -14,7 +13,7 @@ use conduwuit::{
|
||||||
Error, PduEvent, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
|
Error, PduEvent, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
|
||||||
};
|
};
|
||||||
pub use create::create_admin_room;
|
pub use create::create_admin_room;
|
||||||
use futures::{FutureExt, TryFutureExt};
|
use futures::{Future, FutureExt, TryFutureExt};
|
||||||
use loole::{Receiver, Sender};
|
use loole::{Receiver, Sender};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, OwnedRoomId, RoomId, UserId,
|
OwnedEventId, OwnedRoomId, RoomId, UserId,
|
||||||
|
|
|
@ -306,28 +306,25 @@ impl super::Service {
|
||||||
|
|
||||||
#[tracing::instrument(name = "srv", level = "debug", skip(self))]
|
#[tracing::instrument(name = "srv", level = "debug", skip(self))]
|
||||||
async fn query_srv_record(&self, hostname: &'_ str) -> Result<Option<FedDest>> {
|
async fn query_srv_record(&self, hostname: &'_ str) -> Result<Option<FedDest>> {
|
||||||
let hostnames =
|
self.services.server.check_running()?;
|
||||||
[format!("_matrix-fed._tcp.{hostname}."), format!("_matrix._tcp.{hostname}.")];
|
|
||||||
|
|
||||||
for hostname in hostnames {
|
debug!("querying SRV for {hostname:?}");
|
||||||
self.services.server.check_running()?;
|
|
||||||
|
|
||||||
debug!("querying SRV for {hostname:?}");
|
let hostname_suffix = format!("_matrix-fed._tcp.{hostname}.");
|
||||||
let hostname = hostname.trim_end_matches('.');
|
let hostname = hostname_suffix.trim_end_matches('.');
|
||||||
match self.resolver.resolver.srv_lookup(hostname).await {
|
match self.resolver.resolver.srv_lookup(hostname).await {
|
||||||
| Err(e) => Self::handle_resolve_error(&e, hostname)?,
|
| Err(e) => Self::handle_resolve_error(&e, hostname)?,
|
||||||
| Ok(result) => {
|
| Ok(result) => {
|
||||||
return Ok(result.iter().next().map(|result| {
|
return Ok(result.iter().next().map(|result| {
|
||||||
FedDest::Named(
|
FedDest::Named(
|
||||||
result.target().to_string().trim_end_matches('.').to_owned(),
|
result.target().to_string().trim_end_matches('.').to_owned(),
|
||||||
format!(":{}", result.port())
|
format!(":{}", result.port())
|
||||||
.as_str()
|
.as_str()
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap_or_else(|_| FedDest::default_port()),
|
.unwrap_or_else(|_| FedDest::default_port()),
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|
|
@ -43,7 +43,10 @@ impl super::Service {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut replace_events = Vec::with_capacity(relations.len().min(10)); // Most events have few replacements
|
// Get the original event for validation of replacement events
|
||||||
|
let original_event = self.services.timeline.get_pdu(event_id).await?;
|
||||||
|
|
||||||
|
let mut replace_events = Vec::with_capacity(relations.len());
|
||||||
let mut reference_events = Vec::with_capacity(relations.len());
|
let mut reference_events = Vec::with_capacity(relations.len());
|
||||||
|
|
||||||
for relation in &relations {
|
for relation in &relations {
|
||||||
|
@ -56,7 +59,10 @@ impl super::Service {
|
||||||
if let Some(rel_type) = relates_to.get("rel_type") {
|
if let Some(rel_type) = relates_to.get("rel_type") {
|
||||||
match rel_type.as_str() {
|
match rel_type.as_str() {
|
||||||
| Some("m.replace") => {
|
| Some("m.replace") => {
|
||||||
replace_events.push(relation);
|
// Only consider valid replacements
|
||||||
|
if Self::is_valid_replacement_event(&original_event, pdu).await? {
|
||||||
|
replace_events.push(relation);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
| Some("m.reference") => {
|
| Some("m.reference") => {
|
||||||
reference_events.push(relation);
|
reference_events.push(relation);
|
||||||
|
@ -228,6 +234,56 @@ impl super::Service {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates that an event is acceptable as a replacement for another event
|
||||||
|
/// See C/S spec "Validity of replacement events"
|
||||||
|
#[tracing::instrument(level = "debug")]
|
||||||
|
async fn is_valid_replacement_event(
|
||||||
|
original_event: &PduEvent,
|
||||||
|
replacement_event: &PduEvent,
|
||||||
|
) -> Result<bool> {
|
||||||
|
// 1. Same room_id
|
||||||
|
if original_event.room_id() != replacement_event.room_id() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Same sender
|
||||||
|
if original_event.sender() != replacement_event.sender() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Same type
|
||||||
|
if original_event.event_type() != replacement_event.event_type() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Neither event should have a state_key property
|
||||||
|
if original_event.state_key().is_some() || replacement_event.state_key().is_some() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Original event must not have rel_type of m.replace
|
||||||
|
let original_content = original_event.get_content_as_value();
|
||||||
|
if let Some(relates_to) = original_content.get("m.relates_to") {
|
||||||
|
if let Some(rel_type) = relates_to.get("rel_type") {
|
||||||
|
if rel_type.as_str() == Some("m.replace") {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Replacement event must have m.new_content property
|
||||||
|
// Skip this check for encrypted events, as m.new_content would be inside the
|
||||||
|
// encrypted payload
|
||||||
|
if replacement_event.event_type() != &ruma::events::TimelineEventType::RoomEncrypted {
|
||||||
|
let replacement_content = replacement_event.get_content_as_value();
|
||||||
|
if replacement_content.get("m.new_content").is_none() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -391,4 +447,319 @@ mod tests {
|
||||||
assert!(result.is_err(), "fails when existing unsigned is invalid");
|
assert!(result.is_err(), "fails when existing unsigned is invalid");
|
||||||
// Should we ignore the error and overwrite anyway?
|
// Should we ignore the error and overwrite anyway?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test helper function to create test PDU events
|
||||||
|
fn create_test_event(
|
||||||
|
event_id: &str,
|
||||||
|
room_id: &str,
|
||||||
|
sender: &str,
|
||||||
|
event_type: TimelineEventType,
|
||||||
|
content: &JsonValue,
|
||||||
|
state_key: Option<&str>,
|
||||||
|
) -> PduEvent {
|
||||||
|
PduEvent {
|
||||||
|
event_id: event_id.try_into().unwrap(),
|
||||||
|
room_id: room_id.try_into().unwrap(),
|
||||||
|
sender: sender.try_into().unwrap(),
|
||||||
|
origin_server_ts: UInt::try_from(1_234_567_890_u64).unwrap(),
|
||||||
|
kind: event_type,
|
||||||
|
content: to_raw_value(&content).unwrap(),
|
||||||
|
state_key: state_key.map(Into::into),
|
||||||
|
prev_events: vec![],
|
||||||
|
depth: UInt::from(1_u32),
|
||||||
|
auth_events: vec![],
|
||||||
|
redacts: None,
|
||||||
|
unsigned: None,
|
||||||
|
hashes: EventHash { sha256: "test_hash".to_owned() },
|
||||||
|
signatures: None,
|
||||||
|
origin: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that a valid replacement event passes validation
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_valid_replacement_event() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({"msgtype": "m.text", "body": "original message"}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "* edited message",
|
||||||
|
"m.new_content": {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "edited message"
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.replace",
|
||||||
|
"event_id": "$original:example.com"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(result.unwrap(), "Valid replacement event should be accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test replacement event with different room ID is rejected
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replacement_event_different_room() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room1:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({"msgtype": "m.text", "body": "original message"}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room2:example.com", // Different room
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "* edited message",
|
||||||
|
"m.new_content": {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "edited message"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(!result.unwrap(), "Different room ID should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test replacement event with different sender is rejected
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replacement_event_different_sender() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user1:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({"msgtype": "m.text", "body": "original message"}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user2:example.com", // Different sender
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "* edited message",
|
||||||
|
"m.new_content": {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "edited message"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(!result.unwrap(), "Different sender should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test replacement event with different type is rejected
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replacement_event_different_type() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({"msgtype": "m.text", "body": "original message"}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomTopic, // Different event type
|
||||||
|
&json!({
|
||||||
|
"topic": "new topic",
|
||||||
|
"m.new_content": {
|
||||||
|
"topic": "new topic"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(!result.unwrap(), "Different event type should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test replacement event with state key is rejected
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replacement_event_with_state_key() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomName,
|
||||||
|
&json!({"name": "room name"}),
|
||||||
|
Some(""), // Has state key
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomName,
|
||||||
|
&json!({
|
||||||
|
"name": "new room name",
|
||||||
|
"m.new_content": {
|
||||||
|
"name": "new room name"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(!result.unwrap(), "Event with state key should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test replacement of an event that is already a replacement is rejected
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replacement_event_original_is_replacement() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "* edited message",
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.replace", // Original is already a replacement
|
||||||
|
"event_id": "$some_other:example.com"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "* edited again",
|
||||||
|
"m.new_content": {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "edited again"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(!result.unwrap(), "Replacement of replacement should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test replacement event missing m.new_content is rejected
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replacement_event_missing_new_content() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({"msgtype": "m.text", "body": "original message"}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomMessage,
|
||||||
|
&json!({
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "* edited message"
|
||||||
|
// Missing m.new_content
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(!result.unwrap(), "Missing m.new_content should be rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test encrypted replacement event without m.new_content is accepted
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replacement_event_encrypted_missing_new_content_is_valid() {
|
||||||
|
let original = create_test_event(
|
||||||
|
"$original:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomEncrypted,
|
||||||
|
&json!({
|
||||||
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
|
"ciphertext": "encrypted_payload_base64",
|
||||||
|
"sender_key": "sender_key",
|
||||||
|
"session_id": "session_id"
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = create_test_event(
|
||||||
|
"$replacement:example.com",
|
||||||
|
"!room:example.com",
|
||||||
|
"@user:example.com",
|
||||||
|
TimelineEventType::RoomEncrypted,
|
||||||
|
&json!({
|
||||||
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
|
"ciphertext": "encrypted_replacement_payload_base64",
|
||||||
|
"sender_key": "sender_key",
|
||||||
|
"session_id": "session_id",
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.replace",
|
||||||
|
"event_id": "$original:example.com"
|
||||||
|
}
|
||||||
|
// No m.new_content in cleartext - this is valid for encrypted events
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::super::Service::is_valid_replacement_event(&original, &replacement).await;
|
||||||
|
assert!(result.is_ok(), "Validation should succeed");
|
||||||
|
assert!(
|
||||||
|
result.unwrap(),
|
||||||
|
"Encrypted replacement without cleartext m.new_content should be accepted"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue