1
0
Fork 0
mirror of https://forgejo.ellis.link/continuwuation/continuwuity.git synced 2025-09-30 18:42:05 +00:00

Add support for MSC4155 (#1013)

[rendered msc here](https://github.com/Johennes/matrix-spec-proposals/blob/johannes/invite-filtering/proposals/4155-invite-filtering.md). Closes #836.

Co-authored-by: nexy7574 <git@nexy7574.co.uk>
Reviewed-on: https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1013
Reviewed-by: nex <nex@noreply.forgejo.ellis.link>
Co-authored-by: Ginger <ginger@gingershaped.computer>
Co-committed-by: Ginger <ginger@gingershaped.computer>
This commit is contained in:
Ginger 2025-09-21 17:03:40 +00:00 committed by nex
parent 9745bcba1c
commit 13b7538785
13 changed files with 234 additions and 82 deletions

View file

@ -381,6 +381,7 @@ features = [
"unstable-msc4095", "unstable-msc4095",
"unstable-msc4121", "unstable-msc4121",
"unstable-msc4125", "unstable-msc4125",
"unstable-msc4155",
"unstable-msc4186", "unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices "unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions "unstable-msc4210", # remove legacy mentions

View file

@ -4,11 +4,14 @@ use conduwuit::{
Err, Result, debug_error, err, info, Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder}, matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
}; };
use futures::{FutureExt, join}; use futures::FutureExt;
use ruma::{ use ruma::{
OwnedServerName, RoomId, UserId, OwnedServerName, RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite}, api::{client::membership::invite_user, federation::membership::create_invite},
events::room::member::{MembershipState, RoomMemberEventContent}, events::{
invite_permission_config::FilterLevel,
room::member::{MembershipState, RoomMemberEventContent},
},
}; };
use service::Services; use service::Services;
@ -47,22 +50,21 @@ pub(crate) async fn invite_user_route(
.await?; .await?;
match &body.recipient { match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId { user_id } => { | invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => {
let sender_ignored_recipient = services.users.user_is_ignored(sender_user, user_id); let sender_filter_level = services
let recipient_ignored_by_sender = .users
services.users.user_is_ignored(user_id, sender_user); .invite_filter_level(recipient_user, sender_user)
.await;
let (sender_ignored_recipient, recipient_ignored_by_sender) = if !matches!(sender_filter_level, FilterLevel::Allow) {
join!(sender_ignored_recipient, recipient_ignored_by_sender); // drop invites if the sender has the recipient filtered
if sender_ignored_recipient {
return Ok(invite_user::v3::Response {}); return Ok(invite_user::v3::Response {});
} }
if let Ok(target_user_membership) = services if let Ok(target_user_membership) = services
.rooms .rooms
.state_accessor .state_accessor
.get_member(&body.room_id, user_id) .get_member(&body.room_id, recipient_user)
.await .await
{ {
if target_user_membership.membership == MembershipState::Ban { if target_user_membership.membership == MembershipState::Ban {
@ -70,16 +72,27 @@ pub(crate) async fn invite_user_route(
} }
} }
if recipient_ignored_by_sender { // check for blocked invites if the recipient is a local user.
// silently drop the invite to the recipient if they've been ignored by the if services.globals.user_is_local(recipient_user) {
// sender, pretend it worked let recipient_filter_level = services
return Ok(invite_user::v3::Response {}); .users
.invite_filter_level(sender_user, recipient_user)
.await;
// ignored invites aren't handled here
// since the recipient's membership should still be changed to `invite`.
// they're filtered out in the individual /sync handlers.
if matches!(recipient_filter_level, FilterLevel::Block) {
return Err!(Request(InviteBlocked(
"{recipient_user} has blocked invites from you."
)));
}
} }
invite_helper( invite_helper(
&services, &services,
sender_user, sender_user,
user_id, recipient_user,
&body.room_id, &body.room_id,
body.reason.clone(), body.reason.clone(),
false, false,
@ -98,7 +111,7 @@ pub(crate) async fn invite_user_route(
pub(crate) async fn invite_helper( pub(crate) async fn invite_helper(
services: &Services, services: &Services,
sender_user: &UserId, sender_user: &UserId,
user_id: &UserId, recipient_user: &UserId,
room_id: &RoomId, room_id: &RoomId,
reason: Option<String>, reason: Option<String>,
is_direct: bool, is_direct: bool,
@ -111,12 +124,12 @@ 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.")));
} }
if !services.globals.user_is_local(user_id) { if !services.globals.user_is_local(recipient_user) {
let (pdu, pdu_json, invite_room_state) = { let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await; let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent { let content = RoomMemberEventContent {
avatar_url: services.users.avatar_url(user_id).await.ok(), avatar_url: services.users.avatar_url(recipient_user).await.ok(),
is_direct: Some(is_direct), is_direct: Some(is_direct),
reason, reason,
..RoomMemberEventContent::new(MembershipState::Invite) ..RoomMemberEventContent::new(MembershipState::Invite)
@ -126,7 +139,7 @@ pub(crate) async fn invite_helper(
.rooms .rooms
.timeline .timeline
.create_hash_and_sign_event( .create_hash_and_sign_event(
PduBuilder::state(user_id.to_string(), &content), PduBuilder::state(recipient_user.to_string(), &content),
sender_user, sender_user,
Some(room_id), Some(room_id),
&state_lock, &state_lock,
@ -144,7 +157,7 @@ pub(crate) async fn invite_helper(
let response = services let response = services
.sending .sending
.send_federation_request(user_id.server_name(), create_invite::v2::Request { .send_federation_request(recipient_user.server_name(), create_invite::v2::Request {
room_id: room_id.to_owned(), room_id: room_id.to_owned(),
event_id: (*pdu.event_id).to_owned(), event_id: (*pdu.event_id).to_owned(),
room_version: room_version_id.clone(), room_version: room_version_id.clone(),
@ -173,7 +186,7 @@ pub(crate) async fn invite_helper(
return Err!(Request(BadJson(warn!( return Err!(Request(BadJson(warn!(
%pdu.event_id, %event_id, %pdu.event_id, %event_id,
"Server {} sent event with wrong event ID", "Server {} sent event with wrong event ID",
user_id.server_name() recipient_user.server_name()
)))); ))));
} }
@ -213,9 +226,9 @@ pub(crate) async fn invite_helper(
let state_lock = services.rooms.state.mutex.lock(room_id).await; let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent { let content = RoomMemberEventContent {
displayname: services.users.displayname(user_id).await.ok(), displayname: services.users.displayname(recipient_user).await.ok(),
avatar_url: services.users.avatar_url(user_id).await.ok(), avatar_url: services.users.avatar_url(recipient_user).await.ok(),
blurhash: services.users.blurhash(user_id).await.ok(), blurhash: services.users.blurhash(recipient_user).await.ok(),
is_direct: Some(is_direct), is_direct: Some(is_direct),
reason, reason,
..RoomMemberEventContent::new(MembershipState::Invite) ..RoomMemberEventContent::new(MembershipState::Invite)
@ -225,7 +238,7 @@ pub(crate) async fn invite_helper(
.rooms .rooms
.timeline .timeline
.build_and_append_pdu( .build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &content), PduBuilder::state(recipient_user.to_string(), &content),
sender_user, sender_user,
Some(room_id), Some(room_id),
&state_lock, &state_lock,

View file

@ -30,6 +30,7 @@ use ruma::{
events::{ events::{
AnyStateEvent, StateEventType, AnyStateEvent, StateEventType,
TimelineEventType::{self, *}, TimelineEventType::{self, *},
invite_permission_config::FilterLevel,
}, },
serde::Raw, serde::Raw,
}; };
@ -267,7 +268,7 @@ pub(crate) async fn ignored_filter(
pub(crate) async fn is_ignored_pdu<Pdu>( pub(crate) async fn is_ignored_pdu<Pdu>(
services: &Services, services: &Services,
event: &Pdu, event: &Pdu,
user_id: &UserId, recipient_user: &UserId,
) -> bool ) -> bool
where where
Pdu: Event + Send + Sync, Pdu: Event + Send + Sync,
@ -278,20 +279,28 @@ where
return true; return true;
} }
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok(); if IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok() {
// this PDU is a non-state event which it is safe to ignore
return true;
}
let ignored_server = services let sender_user = event.sender();
if services
.moderation .moderation
.is_remote_server_ignored(event.sender().server_name()); .is_remote_server_ignored(sender_user.server_name())
if ignored_type
&& (ignored_server
|| (!services.config.send_messages_from_ignored_users_to_client
&& services
.users
.user_is_ignored(event.sender(), user_id)
.await))
{ {
// this PDU was sent by a remote server which we are ignoring
return true;
}
if services
.users
.user_is_ignored(sender_user, recipient_user)
.await && !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 true;
} }
@ -320,6 +329,29 @@ pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Opti
filter.matches(pdu).then_some(item) filter.matches(pdu).then_some(item)
} }
#[inline]
pub(crate) async fn is_ignored_invite(
services: &Services,
recipient_user: &UserId,
room_id: &RoomId,
) -> bool {
let Ok(sender_user) = services
.rooms
.state_cache
.invite_sender(recipient_user, room_id)
.await
else {
// the invite may have been sent before the invite_sender table existed.
// assume it's not ignored
return false;
};
services
.users
.invite_filter_level(&sender_user, recipient_user)
.await == FilterLevel::Ignore
}
#[cfg_attr(debug_assertions, ctor::ctor)] #[cfg_attr(debug_assertions, ctor::ctor)]
fn _is_sorted() { fn _is_sorted() {
debug_assert!( debug_assert!(

View file

@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
@ -13,6 +13,7 @@ use ruma::{
api::client::room::{self, create_room}, api::client::room::{self, create_room},
events::{ events::{
TimelineEventType, TimelineEventType,
invite_permission_config::FilterLevel,
room::{ room::{
canonical_alias::RoomCanonicalAliasEventContent, canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent, create::RoomCreateEventContent,
@ -121,6 +122,40 @@ pub(crate) async fn create_room_route(
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed"))); return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
} }
let mut invitees = BTreeSet::new();
for recipient_user in &body.invite {
if !matches!(
services
.users
.invite_filter_level(recipient_user, sender_user)
.await,
FilterLevel::Allow
) {
// drop invites if the creator has them blocked
continue;
}
// if the recipient of the invite is local and has the sender blocked, error
// out. if the recipient is remote we can't tell yet, and if they're local and
// have the sender _ignored_ their invite will be filtered out in
// the handlers for the individual /sync endpoints
if services.globals.user_is_local(recipient_user)
&& matches!(
services
.users
.invite_filter_level(sender_user, recipient_user)
.await,
FilterLevel::Block
) {
return Err!(Request(InviteBlocked(
"{recipient_user} has blocked invites from you."
)));
}
invitees.insert(recipient_user.clone());
}
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() { let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) => | Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?), Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
@ -252,19 +287,11 @@ pub(crate) async fn create_room_route(
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom | _ => RoomPreset::PrivateChat, // Room visibility should not be custom
}); });
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]); let mut power_levels_to_grant = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
if preset == RoomPreset::TrustedPrivateChat { if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite { for recipient_user in &invitees {
if services.users.user_is_ignored(sender_user, invite).await { power_levels_to_grant.insert(recipient_user.clone(), int!(100));
continue;
} else if services.users.user_is_ignored(invite, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
users.insert(invite.clone(), int!(100));
} }
} }
@ -289,7 +316,7 @@ pub(crate) async fn create_room_route(
} }
} }
} else { } else {
users.insert(sender_user.to_owned(), int!(100)); power_levels_to_grant.insert(sender_user.to_owned(), int!(100));
creators.clear(); // If this vec is not empty, default_power_levels_content will creators.clear(); // If this vec is not empty, default_power_levels_content will
// treat this as a v12 room // treat this as a v12 room
} }
@ -297,7 +324,7 @@ pub(crate) async fn create_room_route(
let power_levels_content = default_power_levels_content( let power_levels_content = default_power_levels_content(
body.power_level_content_override.as_ref(), body.power_level_content_override.as_ref(),
&body.visibility, &body.visibility,
users, power_levels_to_grant,
creators, creators,
)?; )?;
@ -459,17 +486,9 @@ pub(crate) async fn create_room_route(
// 8. Events implied by invite (and TODO: invite_3pid) // 8. Events implied by invite (and TODO: invite_3pid)
drop(state_lock); drop(state_lock);
for user_id in &body.invite { for recipient_user in &invitees {
if services.users.user_is_ignored(sender_user, user_id).await {
continue;
} else if services.users.user_is_ignored(user_id, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
if let Err(e) = if let Err(e) =
invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct) invite_helper(&services, sender_user, recipient_user, &room_id, None, body.is_direct)
.boxed() .boxed()
.await .await
{ {

View file

@ -60,7 +60,10 @@ use ruma::{
use service::rooms::short::{ShortEventId, ShortStateKey}; use service::rooms::short::{ShortEventId, ShortStateKey};
use super::{load_timeline, share_encrypted_room}; use super::{load_timeline, share_encrypted_room};
use crate::{Ruma, RumaResponse, client::ignored_filter}; use crate::{
Ruma, RumaResponse,
client::{ignored_filter, is_ignored_invite},
};
#[derive(Default)] #[derive(Default)]
struct StateChanges { struct StateChanges {
@ -238,6 +241,13 @@ pub(crate) async fn build_sync_events(
.rooms .rooms
.state_cache .state_cache
.rooms_invited(sender_user) .rooms_invited(sender_user)
.wide_filter_map(async |(room_id, invite_state)| {
if is_ignored_invite(services, sender_user, &room_id).await {
None
} else {
Some((room_id, invite_state))
}
})
.fold_default(|mut invited_rooms: BTreeMap<_, _>, (room_id, invite_state)| async move { .fold_default(|mut invited_rooms: BTreeMap<_, _>, (room_id, invite_state)| async move {
let invite_count = services let invite_count = services
.rooms .rooms

View file

@ -11,6 +11,7 @@ use conduwuit::{
utils::{ utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt, BoolExt, IterStream, ReadyExt, TryFutureExtExt,
math::{ruma_from_usize, usize_from_ruma, usize_from_u64_truncated}, math::{ruma_from_usize, usize_from_ruma, usize_from_u64_truncated},
stream::WidebandExt,
}, },
warn, warn,
}; };
@ -39,7 +40,7 @@ use ruma::{
use super::{load_timeline, share_encrypted_room}; use super::{load_timeline, share_encrypted_room};
use crate::{ use crate::{
Ruma, Ruma,
client::{DEFAULT_BUMP_TYPES, ignored_filter}, client::{DEFAULT_BUMP_TYPES, ignored_filter, is_ignored_invite},
}; };
type TodoRooms = BTreeMap<OwnedRoomId, (BTreeSet<TypeStateKey>, usize, u64)>; type TodoRooms = BTreeMap<OwnedRoomId, (BTreeSet<TypeStateKey>, usize, u64)>;
@ -102,6 +103,13 @@ pub(crate) async fn sync_events_v4_route(
.rooms .rooms
.state_cache .state_cache
.rooms_invited(sender_user) .rooms_invited(sender_user)
.wide_filter_map(async |(room_id, invite_state)| {
if is_ignored_invite(&services, sender_user, &room_id).await {
None
} else {
Some((room_id, invite_state))
}
})
.map(|r| r.0) .map(|r| r.0)
.collect() .collect()
.await; .await;

View file

@ -14,6 +14,7 @@ use conduwuit::{
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt, BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
future::ReadyEqExt, future::ReadyEqExt,
math::{ruma_from_usize, usize_from_ruma}, math::{ruma_from_usize, usize_from_ruma},
stream::WidebandExt,
}, },
warn, warn,
}; };
@ -38,7 +39,7 @@ use ruma::{
use super::share_encrypted_room; use super::share_encrypted_room;
use crate::{ use crate::{
Ruma, Ruma,
client::{DEFAULT_BUMP_TYPES, ignored_filter, sync::load_timeline}, client::{DEFAULT_BUMP_TYPES, ignored_filter, is_ignored_invite, sync::load_timeline},
}; };
type SyncInfo<'a> = (&'a UserId, &'a DeviceId, u64, &'a sync_events::v5::Request); type SyncInfo<'a> = (&'a UserId, &'a DeviceId, u64, &'a sync_events::v5::Request);
@ -106,6 +107,13 @@ pub(crate) async fn sync_events_v5_route(
.rooms .rooms
.state_cache .state_cache
.rooms_invited(sender_user) .rooms_invited(sender_user)
.wide_filter_map(async |(room_id, invite_state)| {
if is_ignored_invite(services, sender_user, &room_id).await {
None
} else {
Some((room_id, invite_state))
}
})
.map(|r| r.0) .map(|r| r.0)
.collect::<Vec<OwnedRoomId>>(); .collect::<Vec<OwnedRoomId>>();

View file

@ -61,13 +61,16 @@ pub(crate) async fn create_invite_route(
let mut signed_event = utils::to_canonical_object(&body.event) let mut signed_event = utils::to_canonical_object(&body.event)
.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?; .map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
let invited_user: OwnedUserId = signed_event let recipient_user: OwnedUserId = signed_event
.get("state_key") .get("state_key")
.try_into() .try_into()
.map(UserId::to_owned) .map(UserId::to_owned)
.map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?; .map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
if !services.globals.server_is_ours(invited_user.server_name()) { if !services
.globals
.server_is_ours(recipient_user.server_name())
{
return Err!(Request(InvalidParam("User does not belong to this homeserver."))); return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
} }
@ -75,7 +78,7 @@ pub(crate) async fn create_invite_route(
services services
.rooms .rooms
.event_handler .event_handler
.acl_check(invited_user.server_name(), &body.room_id) .acl_check(recipient_user.server_name(), &body.room_id)
.await?; .await?;
services services
@ -89,18 +92,19 @@ pub(crate) async fn create_invite_route(
// Add event_id back // Add event_id back
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string())); signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
let sender: &UserId = signed_event let sender_user: &UserId = signed_event
.get("sender") .get("sender")
.try_into() .try_into()
.map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?; .map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
if services.rooms.metadata.is_banned(&body.room_id).await if services.rooms.metadata.is_banned(&body.room_id).await
&& !services.users.is_admin(&invited_user).await && !services.users.is_admin(&recipient_user).await
{ {
return Err!(Request(Forbidden("This room is banned on this homeserver."))); return Err!(Request(Forbidden("This room is banned on this homeserver.")));
} }
if services.config.block_non_admin_invites && !services.users.is_admin(&invited_user).await { if services.config.block_non_admin_invites && !services.users.is_admin(&recipient_user).await
{
return Err!(Request(Forbidden("This server does not allow room invites."))); return Err!(Request(Forbidden("This server does not allow room invites.")));
} }
@ -131,9 +135,9 @@ pub(crate) async fn create_invite_route(
.state_cache .state_cache
.update_membership( .update_membership(
&body.room_id, &body.room_id,
&invited_user, &recipient_user,
RoomMemberEventContent::new(MembershipState::Invite), RoomMemberEventContent::new(MembershipState::Invite),
sender, sender_user,
Some(invite_state), Some(invite_state),
body.via.clone(), body.via.clone(),
true, true,
@ -141,7 +145,7 @@ pub(crate) async fn create_invite_route(
.await?; .await?;
for appservice in services.appservice.read().await.values() { for appservice in services.appservice.read().await.values() {
if appservice.is_user_match(&invited_user) { if appservice.is_user_match(&recipient_user) {
services services
.sending .sending
.send_appservice_request( .send_appservice_request(

View file

@ -73,6 +73,7 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| ThreepidAuthFailed | ThreepidAuthFailed
| UserDeactivated | UserDeactivated
| ThreepidDenied | ThreepidDenied
| InviteBlocked
| WrongRoomKeysVersion { .. } | WrongRoomKeysVersion { .. }
| Forbidden { .. } => StatusCode::FORBIDDEN, | Forbidden { .. } => StatusCode::FORBIDDEN,

View file

@ -434,4 +434,8 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "userroomid_notificationcount", name: "userroomid_notificationcount",
..descriptor::RANDOM ..descriptor::RANDOM
}, },
Descriptor {
name: "userroomid_invitesender",
..descriptor::RANDOM_SMALL
},
]; ];

View file

@ -12,7 +12,7 @@ use conduwuit::{
use database::{Deserialized, Ignore, Interfix, Map}; use database::{Deserialized, Ignore, Interfix, Map};
use futures::{Stream, StreamExt, future::join5, pin_mut}; use futures::{Stream, StreamExt, future::join5, pin_mut};
use ruma::{ use ruma::{
OwnedRoomId, RoomId, ServerName, UserId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
events::{AnyStrippedStateEvent, AnySyncStateEvent, room::member::MembershipState}, events::{AnyStrippedStateEvent, AnySyncStateEvent, room::member::MembershipState},
serde::Raw, serde::Raw,
}; };
@ -49,6 +49,7 @@ struct Data {
userroomid_joined: Arc<Map>, userroomid_joined: Arc<Map>,
userroomid_leftstate: Arc<Map>, userroomid_leftstate: Arc<Map>,
userroomid_knockedstate: Arc<Map>, userroomid_knockedstate: Arc<Map>,
userroomid_invitesender: Arc<Map>,
} }
type AppServiceInRoomCache = SyncRwLock<HashMap<OwnedRoomId, HashMap<String, bool>>>; type AppServiceInRoomCache = SyncRwLock<HashMap<OwnedRoomId, HashMap<String, bool>>>;
@ -83,6 +84,7 @@ impl crate::Service for Service {
userroomid_joined: args.db["userroomid_joined"].clone(), userroomid_joined: args.db["userroomid_joined"].clone(),
userroomid_leftstate: args.db["userroomid_leftstate"].clone(), userroomid_leftstate: args.db["userroomid_leftstate"].clone(),
userroomid_knockedstate: args.db["userroomid_knockedstate"].clone(), userroomid_knockedstate: args.db["userroomid_knockedstate"].clone(),
userroomid_invitesender: args.db["userroomid_invitesender"].clone(),
}, },
})) }))
} }
@ -523,3 +525,14 @@ pub async fn is_left(&self, user_id: &UserId, room_id: &RoomId) -> bool {
let key = (user_id, room_id); let key = (user_id, room_id);
self.db.userroomid_leftstate.qry(&key).await.is_ok() self.db.userroomid_leftstate.qry(&key).await.is_ok()
} }
#[implement(Service)]
#[tracing::instrument(skip(self), level = "trace")]
pub async fn invite_sender(&self, user_id: &UserId, room_id: &RoomId) -> Result<OwnedUserId> {
let key = (user_id, room_id);
self.db
.userroomid_invitesender
.qry(&key)
.await
.deserialized()
}

View file

@ -1,6 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use conduwuit::{Result, implement, is_not_empty, utils::ReadyExt, warn}; use conduwuit::{Err, Result, implement, is_not_empty, utils::ReadyExt, warn};
use database::{Json, serialize_key}; use database::{Json, serialize_key};
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{
@ -9,6 +9,7 @@ use ruma::{
AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType,
RoomAccountDataEventType, StateEventType, RoomAccountDataEventType, StateEventType,
direct::DirectEvent, direct::DirectEvent,
invite_permission_config::FilterLevel,
room::{ room::{
create::RoomCreateEventContent, create::RoomCreateEventContent,
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
@ -121,12 +122,21 @@ pub async fn update_membership(
self.mark_as_joined(user_id, room_id); self.mark_as_joined(user_id, room_id);
}, },
| MembershipState::Invite => { | MembershipState::Invite => {
// We want to know if the sender is ignored by the receiver // return an error for blocked invites. ignored invites aren't handled here
if self.services.users.user_is_ignored(sender, user_id).await { // since the recipient's membership should still be changed to `invite`.
return Ok(()); // they're filtered out in the individual /sync handlers
if matches!(
self.services
.users
.invite_filter_level(sender, user_id)
.await,
FilterLevel::Block
) {
return Err!(Request(InviteBlocked(
"{user_id} has blocked invites from {sender}."
)));
} }
self.mark_as_invited(user_id, room_id, sender, last_state, invite_via)
self.mark_as_invited(user_id, room_id, last_state, invite_via)
.await; .await;
}, },
| MembershipState::Leave | MembershipState::Ban => { | MembershipState::Leave | MembershipState::Ban => {
@ -231,6 +241,7 @@ pub fn mark_as_joined(&self, user_id: &UserId, room_id: &RoomId) {
self.db.userroomid_invitestate.remove(&userroom_id); self.db.userroomid_invitestate.remove(&userroom_id);
self.db.roomuserid_invitecount.remove(&roomuser_id); self.db.roomuserid_invitecount.remove(&roomuser_id);
self.db.userroomid_invitesender.remove(&userroom_id);
self.db.userroomid_leftstate.remove(&userroom_id); self.db.userroomid_leftstate.remove(&userroom_id);
self.db.roomuserid_leftcount.remove(&roomuser_id); self.db.roomuserid_leftcount.remove(&roomuser_id);
@ -268,6 +279,7 @@ pub fn mark_as_left(&self, user_id: &UserId, room_id: &RoomId) {
self.db.userroomid_invitestate.remove(&userroom_id); self.db.userroomid_invitestate.remove(&userroom_id);
self.db.roomuserid_invitecount.remove(&roomuser_id); self.db.roomuserid_invitecount.remove(&roomuser_id);
self.db.userroomid_invitesender.remove(&userroom_id);
self.db.userroomid_knockedstate.remove(&userroom_id); self.db.userroomid_knockedstate.remove(&userroom_id);
self.db.roomuserid_knockedcount.remove(&roomuser_id); self.db.roomuserid_knockedcount.remove(&roomuser_id);
@ -304,6 +316,7 @@ pub fn mark_as_knocked(
self.db.userroomid_invitestate.remove(&userroom_id); self.db.userroomid_invitestate.remove(&userroom_id);
self.db.roomuserid_invitecount.remove(&roomuser_id); self.db.roomuserid_invitecount.remove(&roomuser_id);
self.db.userroomid_invitesender.remove(&userroom_id);
self.db.userroomid_leftstate.remove(&userroom_id); self.db.userroomid_leftstate.remove(&userroom_id);
self.db.roomuserid_leftcount.remove(&roomuser_id); self.db.roomuserid_leftcount.remove(&roomuser_id);
@ -335,6 +348,7 @@ pub async fn mark_as_invited(
&self, &self,
user_id: &UserId, user_id: &UserId,
room_id: &RoomId, room_id: &RoomId,
sender_user: &UserId,
last_state: Option<Vec<Raw<AnyStrippedStateEvent>>>, last_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
invite_via: Option<Vec<OwnedServerName>>, invite_via: Option<Vec<OwnedServerName>>,
) { ) {
@ -350,6 +364,9 @@ pub async fn mark_as_invited(
self.db self.db
.roomuserid_invitecount .roomuserid_invitecount
.raw_aput::<8, _, _>(&roomuser_id, self.services.globals.next_count().unwrap()); .raw_aput::<8, _, _>(&roomuser_id, self.services.globals.next_count().unwrap());
self.db
.userroomid_invitesender
.raw_put(&userroom_id, sender_user);
self.db.userroomid_joined.remove(&userroom_id); self.db.userroomid_joined.remove(&userroom_id);
self.db.roomuserid_joined.remove(&roomuser_id); self.db.roomuserid_joined.remove(&roomuser_id);

View file

@ -20,7 +20,9 @@ use ruma::{
api::client::{device::Device, error::ErrorKind, filter::FilterDefinition}, api::client::{device::Device, error::ErrorKind, filter::FilterDefinition},
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey}, encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
events::{ events::{
AnyToDeviceEvent, GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent, AnyToDeviceEvent, GlobalAccountDataEventType,
ignored_user_list::IgnoredUserListEvent,
invite_permission_config::{FilterLevel, InvitePermissionConfigEvent},
}, },
serde::Raw, serde::Raw,
}; };
@ -139,6 +141,26 @@ impl Service {
}) })
} }
/// Returns the recipient's filter level for an invite from the sender.
pub async fn invite_filter_level(
&self,
sender_user: &UserId,
recipient_user: &UserId,
) -> FilterLevel {
if self.user_is_ignored(sender_user, recipient_user).await {
FilterLevel::Ignore
} else {
self.services
.account_data
.get_global(recipient_user, GlobalAccountDataEventType::InvitePermissionConfig)
.await
.map(|config: InvitePermissionConfigEvent| {
config.content.user_filter_level(sender_user)
})
.unwrap_or(FilterLevel::Allow)
}
}
/// Check if a user is an admin /// Check if a user is an admin
#[inline] #[inline]
pub async fn is_admin(&self, user_id: &UserId) -> bool { pub async fn is_admin(&self, user_id: &UserId) -> bool {