Compare commits

...

13 Commits

Author SHA1 Message Date
timedout d15ac1d3c1 fix: Use 404 instead of 400 (and include sender) 2026-02-15 15:55:36 +00:00
timedout a9ebdf58e2 feat: Filter ignored PDUs in relations 2026-02-15 15:55:35 +00:00
timedout f1ab27d344 feat: Return SENDER_IGNORED error for context and relations 2026-02-15 15:55:35 +00:00
timedout 8bc6e6ccca feat: Return SENDER_IGNORED error in is_ignored_pdu 2026-02-15 15:55:32 +00:00
Jade Ellis 60a3abe752 refactor: Use HashSet 2026-02-15 15:35:29 +00:00
Ellie e3b874d336 fix(sync): handle wildcard state keys in sliding sync required_state 2026-02-15 15:35:29 +00:00
Jade Ellis f3f82831b4 docs: Changelog 2026-02-15 15:23:15 +00:00
Jade Ellis 26aac1408e fix: Correct user agent changes
Correct the domain
Remove "embed" in the UA because the
global UA was modified, rather than
just the one for preview requests
2026-02-15 15:21:06 +00:00
Trash Panda be8f62396a feat(core): Change default user agent 2026-02-15 15:21:06 +00:00
Trash Panda 40996a6602 feat(core): Add config option for the url preview user agent 2026-02-15 15:21:05 +00:00
Jade Ellis 9cae531f90 doc: Changelog 2026-02-15 15:19:03 +00:00
Jade Ellis 56eea935b6 feat: Deadlock detector thread 2026-02-15 15:19:02 +00:00
Renovate Bot fcb646f8c4 chore(deps): update rust-patch-updates 2026-02-15 05:02:30 +00:00
18 changed files with 172 additions and 59 deletions
Generated
+38 -43
View File
@@ -841,7 +841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
dependencies = [
"serde",
"toml 0.9.11+spec-1.1.0",
"toml 0.9.12+spec-1.1.0",
]
[[package]]
@@ -917,9 +917,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.57"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
dependencies = [
"clap_builder",
"clap_derive",
@@ -927,9 +927,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.57"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
dependencies = [
"anstyle",
"clap_lex",
@@ -949,9 +949,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "cmake"
@@ -1030,6 +1030,7 @@ dependencies = [
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry_sdk",
"parking_lot",
"sentry",
"sentry-tower",
"sentry-tracing",
@@ -1149,14 +1150,14 @@ dependencies = [
"serde_json",
"serde_regex",
"smallstr",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"tikv-jemalloc-ctl",
"tikv-jemalloc-sys",
"tikv-jemallocator",
"tokio",
"tokio-metrics",
"toml 0.9.11+spec-1.1.0",
"toml 0.9.12+spec-1.1.0",
"tracing",
"tracing-core",
"tracing-subscriber",
@@ -1917,7 +1918,7 @@ dependencies = [
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec 1.15.1",
"smallvec",
"zune-inflate",
]
@@ -2152,7 +2153,7 @@ checksum = "3a74b56a4039a46e8c91cc9d84e8a7df4e1f8b24239ca57d1304b3263cb599b9"
dependencies = [
"compact_str",
"garde_derive",
"smallvec 1.15.1",
"smallvec",
]
[[package]]
@@ -2364,7 +2365,7 @@ dependencies = [
"rand 0.9.2",
"resolv-conf",
"serde",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -2482,7 +2483,7 @@ dependencies = [
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec 1.15.1",
"smallvec",
"tokio",
"want",
]
@@ -2577,7 +2578,7 @@ dependencies = [
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec 1.15.1",
"smallvec",
"zerovec",
]
@@ -2635,7 +2636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec 1.15.1",
"smallvec",
"utf8_iter",
]
@@ -2920,9 +2921,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.180"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libfuzzer-sys"
@@ -3224,7 +3225,7 @@ dependencies = [
"equivalent",
"parking_lot",
"portable-atomic",
"smallvec 1.15.1",
"smallvec",
"tagptr",
"uuid",
]
@@ -3721,7 +3722,7 @@ dependencies = [
"libc",
"petgraph",
"redox_syscall",
"smallvec 1.15.1",
"smallvec",
"windows-link",
]
@@ -4466,7 +4467,7 @@ dependencies = [
"serde",
"serde_html_form",
"serde_json",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"time",
"tracing",
@@ -4493,7 +4494,7 @@ dependencies = [
"ruma-macros",
"serde",
"serde_json",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"tracing",
"url",
@@ -4751,12 +4752,12 @@ dependencies = [
[[package]]
name = "saphyr-parser-bw"
version = "0.0.607"
version = "0.0.608"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f9bae8d059bf1ca32753cf3cdafbf5d391502de2fc2ca54510811fe9c100d90"
checksum = "d55ae5ea09894b6d5382621db78f586df37ef18ab581bf32c754e75076b124b1"
dependencies = [
"arraydeque",
"smallvec 2.0.0-alpha.12",
"smallvec",
"thiserror 2.0.18",
]
@@ -4963,13 +4964,13 @@ dependencies = [
[[package]]
name = "serde-saphyr"
version = "0.0.17"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc14a55107113a16346915d7e3d78acc539a923458385db89670e22cac106d7a"
checksum = "191a4f997fef5e095212c5790898516e9567d2d8502c4159317603ff0321e394"
dependencies = [
"ahash",
"annotate-snippets",
"base64 0.21.7",
"base64 0.22.1",
"encoding_rs_io",
"figment",
"garde",
@@ -4981,7 +4982,7 @@ dependencies = [
"saphyr-parser-bw",
"serde",
"serde_json",
"smallvec 2.0.0-alpha.12",
"smallvec",
"validator",
"zmij",
]
@@ -5195,7 +5196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862077b1e764f04c251fe82a2ef562fd78d7cadaeb072ca7c2bcaf7217b1ff3b"
dependencies = [
"serde",
"smallvec 1.15.1",
"smallvec",
]
[[package]]
@@ -5207,12 +5208,6 @@ dependencies = [
"serde",
]
[[package]]
name = "smallvec"
version = "2.0.0-alpha.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857"
[[package]]
name = "socket2"
version = "0.5.10"
@@ -5330,9 +5325,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.114"
version = "2.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
dependencies = [
"proc-macro2",
"quote",
@@ -5657,9 +5652,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.11+spec-1.1.0"
version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"indexmap",
"serde_core",
@@ -5716,9 +5711,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
version = "1.0.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
dependencies = [
"winnow",
]
@@ -5903,7 +5898,7 @@ checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
dependencies = [
"js-sys",
"opentelemetry",
"smallvec 1.15.1",
"smallvec",
"tracing",
"tracing-core",
"tracing-log",
@@ -5922,7 +5917,7 @@ dependencies = [
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec 1.15.1",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
+1 -1
View File
@@ -158,7 +158,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.17"
version = "0.0.18"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
+2
View File
@@ -0,0 +1,2 @@
Added unstable support for [MSC4406: `M_SENDER_IGNORED`](https://github.com/matrix-org/matrix-spec-proposals/pull/4406).
Contributed by @nex
+1
View File
@@ -0,0 +1 @@
Continuwuity will now print information to the console when it detects a deadlock
+1
View File
@@ -0,0 +1 @@
You can now set a custom User Agent for URL previews; the default one has been modified to be less likely to be rejected. Contributed by @trashpanda
+4
View File
@@ -1474,6 +1474,10 @@
#
#url_preview_check_root_domain = false
# User agent that is used specifically when fetching url previews.
#
#url_preview_user_agent = "continuwuity/<version> (bot; +https://continuwuity.org)"
# List of forbidden room aliases and room IDs as strings of regex
# patterns.
#
+7 -1
View File
@@ -16,7 +16,10 @@ use ruma::{OwnedEventId, UserId, api::client::context::get_context, events::Stat
use crate::{
Ruma,
client::message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
client::{
is_ignored_pdu,
message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
},
};
const LIMIT_MAX: usize = 100;
@@ -78,6 +81,9 @@ pub(crate) async fn get_context_route(
return Err!(Request(NotFound("Event not found.")));
}
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
is_ignored_pdu(&services, &base_pdu, sender_user).await?;
let base_count = base_id.pdu_count();
let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user);
+21 -8
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, at, debug_warn,
Err, Error, Result, at, debug_warn,
matrix::{
event::{Event, Matches},
pdu::PduCount,
@@ -26,7 +26,7 @@ use ruma::{
DeviceId, RoomId, UserId,
api::{
Direction,
client::{filter::RoomEventFilter, message::get_message_events},
client::{error::ErrorKind, filter::RoomEventFilter, message::get_message_events},
},
events::{
AnyStateEvent, StateEventType,
@@ -279,23 +279,30 @@ pub(crate) async fn ignored_filter(
is_ignored_pdu(services, pdu, user_id)
.await
.unwrap_or(true)
.eq(&false)
.then_some(item)
}
/// Determine whether a PDU should be ignored for a given recipient user.
/// Returns True if this PDU should be ignored, returns False otherwise.
///
/// The error SenderIgnored is returned if the sender or the sender's server is
/// ignored by the relevant user. If the error cannot be returned to the user,
/// it should equate to a true value (i.e. ignored).
#[inline]
pub(crate) async fn is_ignored_pdu<Pdu>(
services: &Services,
event: &Pdu,
recipient_user: &UserId,
) -> bool
) -> Result<bool>
where
Pdu: Event + Send + Sync,
{
// exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this.
if event.kind().to_cow_str() == "org.matrix.dummy_event" {
return true;
return Ok(true);
}
let sender_user = event.sender();
@@ -310,21 +317,27 @@ where
if !type_ignored {
// We cannot safely ignore this type
return false;
return Ok(false);
}
if server_ignored {
// the sender's server is ignored, so ignore this event
return true;
return Err(Error::BadRequest(
ErrorKind::SenderIgnored { sender: None },
"The sender's server is ignored by this server.",
));
}
if user_ignored && !services.config.send_messages_from_ignored_users_to_client {
// the recipient of this PDU has the sender ignored, and we're not
// configured to send ignored messages to clients
return true;
return Err(Error::BadRequest(
ErrorKind::SenderIgnored { sender: Some(event.sender().to_owned()) },
"You have ignored this sender.",
));
}
false
Ok(false)
}
#[inline]
+25 -2
View File
@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Err, Result, at, debug_warn,
Err, Result, at, debug_warn, err,
matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
};
@@ -18,7 +18,7 @@ use ruma::{
events::{TimelineEventType, relation::RelationType},
};
use crate::Ruma;
use crate::{Ruma, client::is_ignored_pdu};
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
@@ -118,6 +118,14 @@ async fn paginate_relations_with_filter(
debug_warn!(req_evt = %target, %room_id, "Event relations requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}
let target_pdu = services
.rooms
.timeline
.get_pdu(target)
.await
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
is_ignored_pdu(services, &target_pdu, sender_user).await?;
let start: PduCount = from
.map(str::parse)
@@ -159,6 +167,7 @@ async fn paginate_relations_with_filter(
.ready_take_while(|(count, _)| Some(*count) != to)
.take(limit)
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
.wide_filter_map(|item| ignored_filter(services, item, sender_user))
.then(async |mut pdu| {
if let Err(e) = services
.rooms
@@ -214,3 +223,17 @@ async fn visibility_filter<Pdu: Event + Send + Sync>(
.await
.then_some(item)
}
async fn ignored_filter<Pdu: Event + Send + Sync>(
services: &Services,
item: (PduCount, Pdu),
sender_user: &UserId,
) -> Option<(PduCount, Pdu)> {
let (_, pdu) = &item;
if is_ignored_pdu(services, pdu, sender_user).await.ok()? {
None
} else {
Some(item)
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ pub(crate) async fn get_room_event_route(
let (mut event, visible) = try_join(event, visible).await?;
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await {
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await? {
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
+7 -1
View File
@@ -685,8 +685,15 @@ async fn collect_required_state(
required_state_request: &BTreeSet<TypeStateKey>,
) -> Vec<Raw<AnySyncStateEvent>> {
let mut required_state = Vec::new();
let mut wildcard_types: HashSet<&StateEventType> = HashSet::new();
for (event_type, state_key) in required_state_request {
if wildcard_types.contains(event_type) {
continue;
}
if state_key.as_str() == "*" {
wildcard_types.insert(event_type);
if let Ok(keys) = services
.rooms
.state_accessor
@@ -703,7 +710,6 @@ async fn collect_required_state(
required_state.push(Event::into_format(event));
}
}
break;
}
} else if let Ok(event) = services
.rooms
+5
View File
@@ -1696,6 +1696,11 @@ pub struct Config {
#[serde(default)]
pub url_preview_check_root_domain: bool,
/// User agent that is used specifically when fetching url previews.
///
/// default: "continuwuity/<version> (bot; +https://continuwuity.org)"
pub url_preview_user_agent: Option<String>,
/// List of forbidden room aliases and room IDs as strings of regex
/// patterns.
///
+2 -1
View File
@@ -85,7 +85,8 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
// 404
| NotFound | NotImplemented | FeatureDisabled => StatusCode::NOT_FOUND,
| NotFound | NotImplemented | FeatureDisabled | SenderIgnored { .. } =>
StatusCode::NOT_FOUND,
// 403
| GuestAccessForbidden
+10 -1
View File
@@ -8,9 +8,11 @@
use std::sync::OnceLock;
static BRANDING: &str = "continuwuity";
static WEBSITE: &str = "https://continuwuity.org";
static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
static VERSION: OnceLock<String> = OnceLock::new();
static VERSION_UA: OnceLock<String> = OnceLock::new();
static USER_AGENT: OnceLock<String> = OnceLock::new();
#[inline]
@@ -19,11 +21,18 @@ pub fn name() -> &'static str { BRANDING }
#[inline]
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
#[inline]
pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) }
#[inline]
pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
fn init_user_agent() -> String { format!("{}/{}", name(), version()) }
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
fn init_version_ua() -> String {
conduwuit_build_metadata::version_tag()
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}"))
}
fn init_version() -> String {
conduwuit_build_metadata::version_tag()
+1
View File
@@ -230,6 +230,7 @@ tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracing-journald = { workspace = true, optional = true }
parking_lot.workspace = true
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
+36
View File
@@ -0,0 +1,36 @@
use std::{thread, time::Duration};
/// Runs a loop that checks for deadlocks every 10 seconds.
///
/// Note that this requires the `deadlock_detection` parking_lot feature to be
/// enabled.
pub(crate) fn deadlock_detection_thread() {
loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = parking_lot::deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
eprintln!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
eprintln!("Deadlock #{i}");
for t in threads {
eprintln!("Thread Id {:#?}", t.thread_id());
eprintln!("{:#?}", t.backtrace());
}
}
}
}
/// Spawns the deadlock detection thread.
///
/// This thread will run in the background and check for deadlocks every 10
/// seconds. When a deadlock is detected, it will print detailed information to
/// stderr.
pub(crate) fn spawn() {
thread::Builder::new()
.name("deadlock_detector".to_owned())
.spawn(deadlock_detection_thread)
.expect("failed to spawn deadlock detection thread");
}
+4
View File
@@ -5,6 +5,7 @@ use std::sync::{Arc, atomic::Ordering};
use conduwuit_core::{debug_info, error};
mod clap;
mod deadlock;
mod logging;
mod mods;
mod panic;
@@ -27,6 +28,9 @@ pub fn run() -> Result<()> {
}
pub fn run_with_args(args: &Args) -> Result<()> {
// Spawn deadlock detection thread
deadlock::spawn();
let runtime = runtime::new(args)?;
let server = Server::new(args, Some(runtime.handle()))?;
+6
View File
@@ -36,6 +36,11 @@ impl crate::Service for Service {
.clone()
.and_then(Either::right);
let url_preview_user_agent = config
.url_preview_user_agent
.clone()
.unwrap_or_else(|| conduwuit::version::user_agent().to_owned());
Ok(Arc::new(Self {
default: base(config)?
.dns_resolver(resolver.resolver.clone())
@@ -49,6 +54,7 @@ impl crate::Service for Service {
.dns_resolver(resolver.resolver.clone())
.timeout(Duration::from_secs(config.url_preview_timeout))
.redirect(redirect::Policy::limited(3))
.user_agent(url_preview_user_agent)
.build()?,
extern_media: base(config)?