feat: Consolidate antispam checks into a service

Also adds support for the spam checker join rule, and Draupnir callbacks
This commit is contained in:
timedout
2026-01-05 03:36:44 +00:00
committed by Jade Ellis
parent c249dd992e
commit 5ac82f36f3
13 changed files with 355 additions and 136 deletions
+12 -23
View File
@@ -1,11 +1,9 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result,
config::Antispam,
debug_error, err, info,
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
trace,
warn,
};
use futures::FutureExt;
use ruma::{
@@ -15,7 +13,6 @@ use ruma::{
invite_permission_config::FilterLevel,
room::member::{MembershipState, RoomMemberEventContent},
},
meowlnir_antispam::user_may_invite,
};
use service::Services;
@@ -128,24 +125,16 @@ pub(crate) async fn invite_helper(
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
trace!("maybe ask meowlnir");
if let Some(Antispam { meowlnir: Some(cfg) }) = &services.config.antispam {
trace!("asking meowlnir");
services
.sending
.send_meowlnir_antispam_request(
cfg,
user_may_invite::v1::Request::new(
cfg.management_room.clone(),
sender_user.to_owned(),
recipient_user.to_owned(),
),
)
.await
.inspect(|_| trace!("meowlnir :D"))
.inspect_err(|e| debug_error!("meowlnir sad: {e}"))?;
} else {
trace!("no meowlnir configured");
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.to_owned(), room_id.to_owned())
.await
{
warn!(
"Invite from {} to {} in room {} blocked by antispam: {e:?}",
sender_user, recipient_user, room_id
);
return Err!(Request(Forbidden("Invite blocked by antispam service.")));
}
if !services.globals.user_is_local(recipient_user) {
+21 -18
View File
@@ -3,9 +3,7 @@ use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result,
config::Antispam,
debug, debug_info, debug_warn, err, error, info,
Err, Result, debug, debug_info, debug_warn, err, error, info,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
@@ -39,7 +37,6 @@ use ruma::{
member::{MembershipState, RoomMemberEventContent},
},
},
meowlnir_antispam::user_may_join_room,
};
use service::{
Services,
@@ -82,6 +79,26 @@ pub(crate) async fn join_room_by_id_route(
)
.await?;
if let Err(e) = services
.antispam
.user_may_join_room(
sender_user.to_owned(),
body.room_id.clone(),
services
.rooms
.state_cache
.is_invited(sender_user, &body.room_id)
.await,
)
.await
{
warn!(
"Antispam prevented user {} from joining room {}: {}",
sender_user, body.room_id, e
);
return Err!(Request(Forbidden("You are not allowed to join this room.")));
}
// There is no body.server_name for /roomId/join
let mut servers: Vec<_> = services
.rooms
@@ -350,20 +367,6 @@ pub async fn join_room_by_id_helper(
.boxed()
.await?;
}
if let Some(Antispam { meowlnir: Some(cfg) }) = &services.config.antispam {
services
.sending
.send_meowlnir_antispam_request(
cfg,
user_may_join_room::v1::Request::new(
cfg.management_room.clone(),
sender_user.to_owned(),
room_id.to_owned(),
),
)
.await?;
}
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
+8 -16
View File
@@ -2,9 +2,7 @@ use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, PduEvent, Result,
config::Antispam,
err,
Err, Error, PduEvent, Result, err,
matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256},
warn,
@@ -13,7 +11,6 @@ use ruma::{
CanonicalJsonValue, OwnedUserId, UserId,
api::{client::error::ErrorKind, federation::membership::create_invite},
events::room::member::{MembershipState, RoomMemberEventContent},
meowlnir_antispam::user_may_invite,
serde::JsonObject,
};
@@ -151,18 +148,13 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("This server does not allow room invites.")));
}
if let Some(Antispam { meowlnir: Some(cfg) }) = &services.config.antispam {
services
.sending
.send_meowlnir_antispam_request(
cfg,
user_may_invite::v1::Request::new(
cfg.management_room.clone(),
sender_user.to_owned(),
recipient_user.clone(),
),
)
.await?;
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.clone(), body.room_id.clone())
.await
{
warn!("Antispam rejected invite: {e:?}");
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
}
let mut invite_state = body.invite_room_state.clone();
+48 -22
View File
@@ -1,7 +1,7 @@
use std::borrow::ToOwned;
use axum::extract::State;
use conduwuit::{
Err, Error, Result, debug_info, info, matrix::pdu::PduBuilder, utils::IterStream, warn,
};
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
use conduwuit_service::Services;
use futures::StreamExt;
use ruma::{
@@ -136,7 +136,6 @@ pub(crate) async fn create_join_event_template_route(
&state_lock,
)
.await?;
drop(state_lock);
// room v3 and above removed the "event_id" field from remote PDU format
@@ -192,25 +191,52 @@ pub(crate) async fn user_can_perform_restricted_join(
return Ok(false);
}
if r.allow
.iter()
.filter_map(|rule| {
if let AllowRule::RoomMembership(membership) = rule {
Some(membership)
} else {
None
}
})
.stream()
.any(|m| services.rooms.state_cache.is_joined(user_id, &m.room_id))
.await
{
Ok(true)
} else {
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
for allow_rule in &r.allow {
match allow_rule {
| AllowRule::RoomMembership(membership) => {
if services
.rooms
.state_cache
.is_joined(user_id, &membership.room_id)
.await
{
debug!(
"User {} is allowed to join room {} via membership in room {}",
user_id, room_id, membership.room_id
);
return Ok(true);
}
},
| AllowRule::UnstableSpamChecker => {
match services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned())
.await
{
| Ok(()) => {
return Ok(true);
},
| Err(e) => {
info!(
"meowlnir rejected restricted join for user {} into room {}: {e:?}",
user_id, room_id
);
},
}
},
| _ => {
debug_info!(
"Unsupported allow rule in restricted join for room {}: {:?}",
room_id,
allow_rule
);
},
}
}
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
}
pub(crate) fn maybe_strip_event_id(