feat: Consolidate antispam checks into a service
Also adds support for the spam checker join rule, and Draupnir callbacks
This commit is contained in:
parent
c249dd992e
commit
5ac82f36f3
13 changed files with 355 additions and 136 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
|
@ -1632,6 +1632,16 @@ dependencies = [
|
||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "draupnir-antispam"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
|
dependencies = [
|
||||||
|
"ruma-common",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtor"
|
name = "dtor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -2985,7 +2995,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meowlnir-antispam"
|
name = "meowlnir-antispam"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -4075,9 +4085,10 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma"
|
name = "ruma"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assign",
|
"assign",
|
||||||
|
"draupnir-antispam",
|
||||||
"js_int",
|
"js_int",
|
||||||
"js_option",
|
"js_option",
|
||||||
"meowlnir-antispam",
|
"meowlnir-antispam",
|
||||||
|
|
@ -4096,7 +4107,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-appservice-api"
|
name = "ruma-appservice-api"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
|
@ -4108,7 +4119,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-client-api"
|
name = "ruma-client-api"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"assign",
|
"assign",
|
||||||
|
|
@ -4131,7 +4142,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-common"
|
name = "ruma-common"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|
@ -4163,7 +4174,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-events"
|
name = "ruma-events"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
|
@ -4188,7 +4199,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-federation-api"
|
name = "ruma-federation-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers",
|
"headers",
|
||||||
|
|
@ -4210,7 +4221,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identifiers-validation"
|
name = "ruma-identifiers-validation"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
|
@ -4219,7 +4230,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identity-service-api"
|
name = "ruma-identity-service-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
|
@ -4229,7 +4240,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-macros"
|
name = "ruma-macros"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
|
|
@ -4244,7 +4255,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-push-gateway-api"
|
name = "ruma-push-gateway-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
|
|
@ -4256,7 +4267,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-signatures"
|
name = "ruma-signatures"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=46e31bd6439eccbd3a1762f710c17fc15168c15e#46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,7 @@ version = "0.1.2"
|
||||||
# Used for matrix spec type definitions and helpers
|
# Used for matrix spec type definitions and helpers
|
||||||
[workspace.dependencies.ruma]
|
[workspace.dependencies.ruma]
|
||||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||||
rev = "377d801fa035480b772c640b430097c1ec0ddb16"
|
rev = "46e31bd6439eccbd3a1762f710c17fc15168c15e"
|
||||||
features = [
|
features = [
|
||||||
"compat",
|
"compat",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
|
||||||
|
|
@ -1647,7 +1647,7 @@
|
||||||
|
|
||||||
# Enable the tokio-console. This option is only relevant to developers.
|
# Enable the tokio-console. This option is only relevant to developers.
|
||||||
#
|
#
|
||||||
# For more information, see:
|
# For more information, see:
|
||||||
# https://continuwuity.org/development.html#debugging-with-tokio-console
|
# https://continuwuity.org/development.html#debugging-with-tokio-console
|
||||||
#
|
#
|
||||||
#tokio_console = false
|
#tokio_console = false
|
||||||
|
|
@ -1757,10 +1757,6 @@
|
||||||
#
|
#
|
||||||
#ldap = false
|
#ldap = false
|
||||||
|
|
||||||
# Configuration for antispam support
|
|
||||||
#
|
|
||||||
#antispam = false
|
|
||||||
|
|
||||||
[global.tls]
|
[global.tls]
|
||||||
|
|
||||||
# Path to a valid TLS certificate file.
|
# Path to a valid TLS certificate file.
|
||||||
|
|
@ -1930,7 +1926,7 @@
|
||||||
|
|
||||||
[global.antispam.meowlnir]
|
[global.antispam.meowlnir]
|
||||||
|
|
||||||
# The base URL on which to contact meowlnir (before /_meowlnir/antispam).
|
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
|
||||||
#
|
#
|
||||||
# Example: "http://127.0.0.1:29339"
|
# Example: "http://127.0.0.1:29339"
|
||||||
#
|
#
|
||||||
|
|
@ -1944,3 +1940,24 @@
|
||||||
# The management room for which to send requests
|
# The management room for which to send requests
|
||||||
#
|
#
|
||||||
#management_room =
|
#management_room =
|
||||||
|
|
||||||
|
# If enabled run all federated join attempts (both federated and local)
|
||||||
|
# through the Meowlnir anti-spam checks.
|
||||||
|
#
|
||||||
|
# By default, only join attempts for rooms with the `fi.mau.spam_checker`
|
||||||
|
# restricted join rule are checked.
|
||||||
|
#
|
||||||
|
#check_all_joins = false
|
||||||
|
|
||||||
|
[global.antispam.draupnir]
|
||||||
|
|
||||||
|
# The base URL on which to contact Draupnir (before /api/).
|
||||||
|
#
|
||||||
|
# Example: "http://127.0.0.1:29339"
|
||||||
|
#
|
||||||
|
#base_url =
|
||||||
|
|
||||||
|
# The authentication secret defined in
|
||||||
|
# web->synapseHTTPAntispam->authorization
|
||||||
|
#
|
||||||
|
#secret =
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Result,
|
Err, Result, debug_error, err, info,
|
||||||
config::Antispam,
|
|
||||||
debug_error, err, info,
|
|
||||||
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
||||||
trace,
|
warn,
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
|
|
@ -15,7 +13,6 @@ use ruma::{
|
||||||
invite_permission_config::FilterLevel,
|
invite_permission_config::FilterLevel,
|
||||||
room::member::{MembershipState, RoomMemberEventContent},
|
room::member::{MembershipState, RoomMemberEventContent},
|
||||||
},
|
},
|
||||||
meowlnir_antispam::user_may_invite,
|
|
||||||
};
|
};
|
||||||
use service::Services;
|
use service::Services;
|
||||||
|
|
||||||
|
|
@ -128,24 +125,16 @@ pub(crate) async fn invite_helper(
|
||||||
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("maybe ask meowlnir");
|
if let Err(e) = services
|
||||||
if let Some(Antispam { meowlnir: Some(cfg) }) = &services.config.antispam {
|
.antispam
|
||||||
trace!("asking meowlnir");
|
.user_may_invite(sender_user.to_owned(), recipient_user.to_owned(), room_id.to_owned())
|
||||||
services
|
.await
|
||||||
.sending
|
{
|
||||||
.send_meowlnir_antispam_request(
|
warn!(
|
||||||
cfg,
|
"Invite from {} to {} in room {} blocked by antispam: {e:?}",
|
||||||
user_may_invite::v1::Request::new(
|
sender_user, recipient_user, room_id
|
||||||
cfg.management_room.clone(),
|
);
|
||||||
sender_user.to_owned(),
|
return Err!(Request(Forbidden("Invite blocked by antispam service.")));
|
||||||
recipient_user.to_owned(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.inspect(|_| trace!("meowlnir :D"))
|
|
||||||
.inspect_err(|e| debug_error!("meowlnir sad: {e}"))?;
|
|
||||||
} else {
|
|
||||||
trace!("no meowlnir configured");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !services.globals.user_is_local(recipient_user) {
|
if !services.globals.user_is_local(recipient_user) {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Result,
|
Err, Result, debug, debug_info, debug_warn, err, error, info,
|
||||||
config::Antispam,
|
|
||||||
debug, debug_info, debug_warn, err, error, info,
|
|
||||||
matrix::{
|
matrix::{
|
||||||
StateKey,
|
StateKey,
|
||||||
event::{gen_event_id, gen_event_id_canonical_json},
|
event::{gen_event_id, gen_event_id_canonical_json},
|
||||||
|
|
@ -39,7 +37,6 @@ use ruma::{
|
||||||
member::{MembershipState, RoomMemberEventContent},
|
member::{MembershipState, RoomMemberEventContent},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
meowlnir_antispam::user_may_join_room,
|
|
||||||
};
|
};
|
||||||
use service::{
|
use service::{
|
||||||
Services,
|
Services,
|
||||||
|
|
@ -82,6 +79,26 @@ pub(crate) async fn join_room_by_id_route(
|
||||||
)
|
)
|
||||||
.await?;
|
.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
|
// There is no body.server_name for /roomId/join
|
||||||
let mut servers: Vec<_> = services
|
let mut servers: Vec<_> = services
|
||||||
.rooms
|
.rooms
|
||||||
|
|
@ -350,20 +367,6 @@ pub async fn join_room_by_id_helper(
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?;
|
.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()))
|
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@ use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, PduEvent, Result,
|
Err, Error, PduEvent, Result, err,
|
||||||
config::Antispam,
|
|
||||||
err,
|
|
||||||
matrix::{Event, event::gen_event_id},
|
matrix::{Event, event::gen_event_id},
|
||||||
utils::{self, hash::sha256},
|
utils::{self, hash::sha256},
|
||||||
warn,
|
warn,
|
||||||
|
|
@ -13,7 +11,6 @@ use ruma::{
|
||||||
CanonicalJsonValue, OwnedUserId, UserId,
|
CanonicalJsonValue, OwnedUserId, UserId,
|
||||||
api::{client::error::ErrorKind, federation::membership::create_invite},
|
api::{client::error::ErrorKind, federation::membership::create_invite},
|
||||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||||
meowlnir_antispam::user_may_invite,
|
|
||||||
serde::JsonObject,
|
serde::JsonObject,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -151,18 +148,13 @@ pub(crate) async fn create_invite_route(
|
||||||
return Err!(Request(Forbidden("This server does not allow room invites.")));
|
return Err!(Request(Forbidden("This server does not allow room invites.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Antispam { meowlnir: Some(cfg) }) = &services.config.antispam {
|
if let Err(e) = services
|
||||||
services
|
.antispam
|
||||||
.sending
|
.user_may_invite(sender_user.to_owned(), recipient_user.clone(), body.room_id.clone())
|
||||||
.send_meowlnir_antispam_request(
|
.await
|
||||||
cfg,
|
{
|
||||||
user_may_invite::v1::Request::new(
|
warn!("Antispam rejected invite: {e:?}");
|
||||||
cfg.management_room.clone(),
|
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
|
||||||
sender_user.to_owned(),
|
|
||||||
recipient_user.clone(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut invite_state = body.invite_room_state.clone();
|
let mut invite_state = body.invite_room_state.clone();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
use std::borrow::ToOwned;
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::{
|
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
|
||||||
Err, Error, Result, debug_info, info, matrix::pdu::PduBuilder, utils::IterStream, warn,
|
|
||||||
};
|
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
|
|
@ -136,7 +136,6 @@ pub(crate) async fn create_join_event_template_route(
|
||||||
&state_lock,
|
&state_lock,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
|
|
||||||
// room v3 and above removed the "event_id" field from remote PDU format
|
// 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);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.allow
|
for allow_rule in &r.allow {
|
||||||
.iter()
|
match allow_rule {
|
||||||
.filter_map(|rule| {
|
| AllowRule::RoomMembership(membership) => {
|
||||||
if let AllowRule::RoomMembership(membership) = rule {
|
if services
|
||||||
Some(membership)
|
.rooms
|
||||||
} else {
|
.state_cache
|
||||||
None
|
.is_joined(user_id, &membership.room_id)
|
||||||
}
|
.await
|
||||||
})
|
{
|
||||||
.stream()
|
debug!(
|
||||||
.any(|m| services.rooms.state_cache.is_joined(user_id, &m.room_id))
|
"User {} is allowed to join room {} via membership in room {}",
|
||||||
.await
|
user_id, room_id, membership.room_id
|
||||||
{
|
);
|
||||||
Ok(true)
|
return Ok(true);
|
||||||
} else {
|
}
|
||||||
Err!(Request(UnableToAuthorizeJoin(
|
},
|
||||||
"Joining user is not known to be in any required room."
|
| 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(
|
pub(crate) fn maybe_strip_event_id(
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ use crate::{Result, err, error::Error, utils::sys};
|
||||||
### For more information, see:
|
### For more information, see:
|
||||||
### https://continuwuity.org/configuration.html
|
### https://continuwuity.org/configuration.html
|
||||||
"#,
|
"#,
|
||||||
ignore = "config_paths catchall well_known tls blurhashing allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure"
|
ignore = "config_paths catchall well_known tls blurhashing \
|
||||||
|
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
|
||||||
)]
|
)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
// Paths to config file(s). Not supposed to be set manually in the config file,
|
// Paths to config file(s). Not supposed to be set manually in the config file,
|
||||||
|
|
@ -1887,7 +1888,7 @@ pub struct Config {
|
||||||
|
|
||||||
/// Enable the tokio-console. This option is only relevant to developers.
|
/// Enable the tokio-console. This option is only relevant to developers.
|
||||||
///
|
///
|
||||||
/// For more information, see:
|
/// For more information, see:
|
||||||
/// https://continuwuity.org/development.html#debugging-with-tokio-console
|
/// https://continuwuity.org/development.html#debugging-with-tokio-console
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tokio_console: bool,
|
pub tokio_console: bool,
|
||||||
|
|
@ -2247,6 +2248,7 @@ struct ListeningAddr {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct Antispam {
|
pub struct Antispam {
|
||||||
pub meowlnir: Option<MeowlnirConfig>,
|
pub meowlnir: Option<MeowlnirConfig>,
|
||||||
|
pub draupnir: Option<DraupnirConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
|
@ -2255,7 +2257,7 @@ pub struct Antispam {
|
||||||
section = "global.antispam.meowlnir"
|
section = "global.antispam.meowlnir"
|
||||||
)]
|
)]
|
||||||
pub struct MeowlnirConfig {
|
pub struct MeowlnirConfig {
|
||||||
/// The base URL on which to contact meowlnir (before /_meowlnir/antispam).
|
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
|
||||||
///
|
///
|
||||||
/// Example: "http://127.0.0.1:29339"
|
/// Example: "http://127.0.0.1:29339"
|
||||||
pub base_url: Url,
|
pub base_url: Url,
|
||||||
|
|
@ -2266,6 +2268,32 @@ pub struct MeowlnirConfig {
|
||||||
|
|
||||||
/// The management room for which to send requests
|
/// The management room for which to send requests
|
||||||
pub management_room: OwnedRoomId,
|
pub management_room: OwnedRoomId,
|
||||||
|
|
||||||
|
/// If enabled run all federated join attempts (both federated and local)
|
||||||
|
/// through the Meowlnir anti-spam checks.
|
||||||
|
///
|
||||||
|
/// By default, only join attempts for rooms with the `fi.mau.spam_checker`
|
||||||
|
/// restricted join rule are checked.
|
||||||
|
#[serde(default)]
|
||||||
|
pub check_all_joins: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: the DraupnirConfig and MeowlnirConfig are basically identical.
|
||||||
|
// Maybe management_room could just become an Option<> and these structs merged?
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[config_example_generator(
|
||||||
|
filename = "conduwuit-example.toml",
|
||||||
|
section = "global.antispam.draupnir"
|
||||||
|
)]
|
||||||
|
pub struct DraupnirConfig {
|
||||||
|
/// The base URL on which to contact Draupnir (before /api/).
|
||||||
|
///
|
||||||
|
/// Example: "http://127.0.0.1:29339"
|
||||||
|
pub base_url: Url,
|
||||||
|
|
||||||
|
/// The authentication secret defined in
|
||||||
|
/// web->synapseHTTPAntispam->authorization
|
||||||
|
pub secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEPRECATED_KEYS: &[&str; 9] = &[
|
const DEPRECATED_KEYS: &[&str; 9] = &[
|
||||||
|
|
|
||||||
172
src/service/antispam/mod.rs
Normal file
172
src/service/antispam/mod.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use conduwuit::{Result, config::Antispam, debug};
|
||||||
|
use ruma::{OwnedRoomId, OwnedUserId, draupnir_antispam, meowlnir_antispam};
|
||||||
|
|
||||||
|
use crate::{client, config, sending, service::Dep};
|
||||||
|
|
||||||
|
struct Services {
|
||||||
|
config: Dep<config::Service>,
|
||||||
|
client: Dep<client::Service>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Service {
|
||||||
|
services: Services,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl crate::Service for Service {
|
||||||
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
services: Services {
|
||||||
|
client: args.depend::<client::Service>("client"),
|
||||||
|
config: args.depend::<config::Service>("config"),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
async fn send_antispam_request<T>(
|
||||||
|
&self,
|
||||||
|
base_url: &str,
|
||||||
|
secret: &str,
|
||||||
|
request: T,
|
||||||
|
) -> Result<T::IncomingResponse>
|
||||||
|
where
|
||||||
|
T: ruma::api::OutgoingRequest + std::fmt::Debug + Send,
|
||||||
|
{
|
||||||
|
sending::antispam::send_antispam_request(
|
||||||
|
&self.services.client.appservice,
|
||||||
|
base_url,
|
||||||
|
secret,
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks with the antispam service whether `inviter` may invite `invitee`
|
||||||
|
/// to `room_id`.
|
||||||
|
///
|
||||||
|
/// If no antispam service is configured, this always returns `Ok(())`.
|
||||||
|
/// If an error is returned, the invite should be blocked - the antispam
|
||||||
|
/// service was unreachable, or refused the invite.
|
||||||
|
pub async fn user_may_invite(
|
||||||
|
&self,
|
||||||
|
inviter: OwnedUserId,
|
||||||
|
invitee: OwnedUserId,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(config) = &self.services.config.antispam {
|
||||||
|
let result = if let Some(meowlnir) = &config.meowlnir {
|
||||||
|
debug!("Asking meowlnir for user_may_invite");
|
||||||
|
self.send_antispam_request(
|
||||||
|
meowlnir.base_url.as_str(),
|
||||||
|
&meowlnir.secret,
|
||||||
|
meowlnir_antispam::user_may_invite::v1::Request::new(
|
||||||
|
meowlnir.management_room.clone(),
|
||||||
|
inviter,
|
||||||
|
invitee,
|
||||||
|
room_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect(|_| debug!("meowlnir allowed the invite"))
|
||||||
|
.inspect_err(|e| debug!("meowlnir denied the invite: {e:?}"))
|
||||||
|
.map(|_| ())
|
||||||
|
} else if let Some(draupnir) = &config.draupnir {
|
||||||
|
debug!("Asking draupnir for user_may_invite");
|
||||||
|
self.send_antispam_request(
|
||||||
|
draupnir.base_url.as_str(),
|
||||||
|
&draupnir.secret,
|
||||||
|
draupnir_antispam::user_may_invite::v1::Request::new(
|
||||||
|
room_id, inviter, invitee,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect(|_| debug!("draupnir allowed the invite"))
|
||||||
|
.inspect_err(|e| debug!("draupnir denied the invite: {e:?}"))
|
||||||
|
.map(|_| ())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks with the antispam service whether `user_id` may join `room_id`.
|
||||||
|
pub async fn user_may_join_room(
|
||||||
|
&self,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
is_invited: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(config) = &self.services.config.antispam {
|
||||||
|
let result = if let Some(meowlnir) = &config.meowlnir {
|
||||||
|
debug!("Asking meowlnir for user_may_join_room");
|
||||||
|
self.send_antispam_request(
|
||||||
|
meowlnir.base_url.as_str(),
|
||||||
|
&meowlnir.secret,
|
||||||
|
meowlnir_antispam::user_may_join_room::v1::Request::new(
|
||||||
|
meowlnir.management_room.clone(),
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
is_invited,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect(|_| debug!("meowlnir allowed the join"))
|
||||||
|
.inspect_err(|e| debug!("meowlnir denied the join: {e:?}"))
|
||||||
|
.map(|_| ())
|
||||||
|
} else if let Some(draupnir) = &config.draupnir {
|
||||||
|
debug!("Asking draupnir for user_may_join_room");
|
||||||
|
self.send_antispam_request(
|
||||||
|
draupnir.base_url.as_str(),
|
||||||
|
&draupnir.secret,
|
||||||
|
draupnir_antispam::user_may_join_room::v1::Request::new(
|
||||||
|
user_id, room_id, is_invited,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect(|_| debug!("draupnir allowed the join"))
|
||||||
|
.inspect_err(|e| debug!("draupnir denied the join: {e:?}"))
|
||||||
|
.map(|_| ())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks with Meowlnir whether the incoming federated `make_join` request
|
||||||
|
/// should be allowed. Applies the `fi.mau.spam_checker` join rule.
|
||||||
|
pub async fn meowlnir_accept_make_join(
|
||||||
|
&self,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(Antispam { meowlnir: Some(meowlnir), .. }) = &self.services.config.antispam {
|
||||||
|
debug!("Asking meowlnir for meowlnir_accept_make_join");
|
||||||
|
self.send_antispam_request(
|
||||||
|
meowlnir.base_url.as_str(),
|
||||||
|
&meowlnir.secret,
|
||||||
|
meowlnir_antispam::accept_make_join::v1::Request::new(
|
||||||
|
meowlnir.management_room.clone(),
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.inspect(|_| debug!("meowlnir allowed the make_join"))
|
||||||
|
.inspect_err(|e| debug!("meowlnir denied the make_join: {e:?}"))
|
||||||
|
.map(|_| ())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#![type_length_limit = "8192"]
|
#![type_length_limit = "8192"]
|
||||||
#![allow(refining_impl_trait)]
|
#![allow(refining_impl_trait)]
|
||||||
|
|
||||||
|
extern crate conduwuit_core as conduwuit;
|
||||||
|
extern crate conduwuit_database as database;
|
||||||
mod manager;
|
mod manager;
|
||||||
mod migrations;
|
mod migrations;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
@ -10,6 +12,7 @@ pub mod state;
|
||||||
pub mod account_data;
|
pub mod account_data;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod announcements;
|
pub mod announcements;
|
||||||
|
pub mod antispam;
|
||||||
pub mod appservice;
|
pub mod appservice;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
@ -30,9 +33,6 @@ pub mod transaction_ids;
|
||||||
pub mod uiaa;
|
pub mod uiaa;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
||||||
extern crate conduwuit_core as conduwuit;
|
|
||||||
extern crate conduwuit_database as database;
|
|
||||||
|
|
||||||
use ctor::{ctor, dtor};
|
use ctor::{ctor, dtor};
|
||||||
pub(crate) use service::{Args, Dep, Service};
|
pub(crate) use service::{Args, Dep, Service};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,23 @@
|
||||||
use std::{fmt::Debug, mem};
|
use std::{fmt::Debug, mem};
|
||||||
|
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use conduwuit::{Err, Result, config::MeowlnirConfig, debug_error, err, utils, warn};
|
use conduwuit::{Err, Result, debug_error, err, utils, warn};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
|
use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
|
||||||
|
|
||||||
/// Sends a request to an antispam service
|
/// Sends a request to an antispam service
|
||||||
pub(crate) async fn send_meowlnir_request<T>(
|
pub(crate) async fn send_antispam_request<T>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
config: &MeowlnirConfig,
|
base_url: &str,
|
||||||
|
secret: &str,
|
||||||
request: T,
|
request: T,
|
||||||
) -> Result<Option<T::IncomingResponse>>
|
) -> Result<T::IncomingResponse>
|
||||||
where
|
where
|
||||||
T: OutgoingRequest + Debug + Send,
|
T: OutgoingRequest + Debug + Send,
|
||||||
{
|
{
|
||||||
const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_15];
|
const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_15];
|
||||||
if config.secret.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let secret = config.secret.as_str();
|
|
||||||
let http_request = request
|
let http_request = request
|
||||||
.try_into_http_request::<BytesMut>(
|
.try_into_http_request::<BytesMut>(base_url, SendAccessToken::Always(secret), &VERSIONS)?
|
||||||
config.base_url.as_str(),
|
|
||||||
SendAccessToken::Always(secret),
|
|
||||||
&VERSIONS,
|
|
||||||
)?
|
|
||||||
.map(BytesMut::freeze);
|
.map(BytesMut::freeze);
|
||||||
let reqwest_request = reqwest::Request::try_from(http_request)?;
|
let reqwest_request = reqwest::Request::try_from(http_request)?;
|
||||||
|
|
||||||
|
|
@ -64,7 +57,7 @@ where
|
||||||
.expect("reqwest body is valid http body"),
|
.expect("reqwest body is valid http body"),
|
||||||
);
|
);
|
||||||
|
|
||||||
response.map(Some).map_err(|e| {
|
response.map_err(|e| {
|
||||||
err!(BadServerResponse(warn!(
|
err!(BadServerResponse(warn!(
|
||||||
"Antispam returned invalid/malformed response bytes: {e}",
|
"Antispam returned invalid/malformed response bytes: {e}",
|
||||||
)))
|
)))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
mod antispam;
|
pub mod antispam;
|
||||||
mod appservice;
|
mod appservice;
|
||||||
mod data;
|
mod data;
|
||||||
mod dest;
|
mod dest;
|
||||||
|
|
@ -13,9 +13,7 @@ use std::{
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Result, Server,
|
Result, Server, debug, debug_warn, err, error,
|
||||||
config::MeowlnirConfig,
|
|
||||||
debug, debug_warn, err, error,
|
|
||||||
smallvec::SmallVec,
|
smallvec::SmallVec,
|
||||||
utils::{ReadyExt, TryReadyExt, available_parallelism, math::usize_from_u64_truncated},
|
utils::{ReadyExt, TryReadyExt, available_parallelism, math::usize_from_u64_truncated},
|
||||||
warn,
|
warn,
|
||||||
|
|
@ -337,18 +335,6 @@ impl Service {
|
||||||
appservice::send_request(client, registration, request).await
|
appservice::send_request(client, registration, request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a request to the chosen antispam configuration
|
|
||||||
pub async fn send_meowlnir_antispam_request<T>(
|
|
||||||
&self,
|
|
||||||
config: &MeowlnirConfig,
|
|
||||||
request: T,
|
|
||||||
) -> Result<Option<T::IncomingResponse>>
|
|
||||||
where
|
|
||||||
T: OutgoingRequest + Debug + Send,
|
|
||||||
{
|
|
||||||
antispam::send_meowlnir_request(&self.services.client.appservice, config, request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clean up queued sending event data
|
/// Clean up queued sending event data
|
||||||
///
|
///
|
||||||
/// Used after we remove an appservice registration or a user deletes a push
|
/// Used after we remove an appservice registration or a user deletes a push
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ use futures::{Stream, StreamExt, TryStreamExt};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
account_data, admin, announcements, appservice, client, config, emergency, federation,
|
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
||||||
globals, key_backups,
|
federation, globals, key_backups,
|
||||||
manager::Manager,
|
manager::Manager,
|
||||||
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
|
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
|
||||||
service::{Args, Map, Service},
|
service::{Args, Map, Service},
|
||||||
|
|
@ -39,6 +39,7 @@ pub struct Services {
|
||||||
pub users: Arc<users::Service>,
|
pub users: Arc<users::Service>,
|
||||||
pub moderation: Arc<moderation::Service>,
|
pub moderation: Arc<moderation::Service>,
|
||||||
pub announcements: Arc<announcements::Service>,
|
pub announcements: Arc<announcements::Service>,
|
||||||
|
pub antispam: Arc<antispam::Service>,
|
||||||
|
|
||||||
manager: Mutex<Option<Arc<Manager>>>,
|
manager: Mutex<Option<Arc<Manager>>>,
|
||||||
pub(crate) service: Arc<Map>,
|
pub(crate) service: Arc<Map>,
|
||||||
|
|
@ -107,6 +108,7 @@ impl Services {
|
||||||
users: build!(users::Service),
|
users: build!(users::Service),
|
||||||
moderation: build!(moderation::Service),
|
moderation: build!(moderation::Service),
|
||||||
announcements: build!(announcements::Service),
|
announcements: build!(announcements::Service),
|
||||||
|
antispam: build!(antispam::Service),
|
||||||
|
|
||||||
manager: Mutex::new(None),
|
manager: Mutex::new(None),
|
||||||
service,
|
service,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue