Compare commits

...

10 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
12 changed files with 91 additions and 15 deletions
+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 @@
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()
+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)?