From 13b7538785596f9686e2c7206ec6379d11dc9635 Mon Sep 17 00:00:00 2001 From: Ginger Date: Sun, 21 Sep 2025 17:03:40 +0000 Subject: [PATCH] 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 Reviewed-on: https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1013 Reviewed-by: nex Co-authored-by: Ginger Co-committed-by: Ginger --- Cargo.toml | 1 + src/api/client/membership/invite.rs | 65 ++++++++++++++---------- src/api/client/message.rs | 56 ++++++++++++++++----- src/api/client/room/create.rs | 67 ++++++++++++++++--------- src/api/client/sync/v3.rs | 12 ++++- src/api/client/sync/v4.rs | 10 +++- src/api/client/sync/v5.rs | 10 +++- src/api/server/invite.rs | 22 ++++---- src/core/error/response.rs | 1 + src/database/maps.rs | 4 ++ src/service/rooms/state_cache/mod.rs | 15 +++++- src/service/rooms/state_cache/update.rs | 29 ++++++++--- src/service/users/mod.rs | 24 ++++++++- 13 files changed, 234 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 54015676..f370e5dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -381,6 +381,7 @@ features = [ "unstable-msc4095", "unstable-msc4121", "unstable-msc4125", + "unstable-msc4155", "unstable-msc4186", "unstable-msc4203", # sending to-device events to appservices "unstable-msc4210", # remove legacy mentions diff --git a/src/api/client/membership/invite.rs b/src/api/client/membership/invite.rs index dd23e8b6..72e8c0d2 100644 --- a/src/api/client/membership/invite.rs +++ b/src/api/client/membership/invite.rs @@ -4,11 +4,14 @@ use conduwuit::{ Err, Result, debug_error, err, info, matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder}, }; -use futures::{FutureExt, join}; +use futures::FutureExt; use ruma::{ OwnedServerName, RoomId, UserId, 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; @@ -47,22 +50,21 @@ pub(crate) async fn invite_user_route( .await?; match &body.recipient { - | invite_user::v3::InvitationRecipient::UserId { user_id } => { - let sender_ignored_recipient = services.users.user_is_ignored(sender_user, user_id); - let recipient_ignored_by_sender = - services.users.user_is_ignored(user_id, sender_user); + | invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => { + let sender_filter_level = services + .users + .invite_filter_level(recipient_user, sender_user) + .await; - let (sender_ignored_recipient, recipient_ignored_by_sender) = - join!(sender_ignored_recipient, recipient_ignored_by_sender); - - if sender_ignored_recipient { + if !matches!(sender_filter_level, FilterLevel::Allow) { + // drop invites if the sender has the recipient filtered return Ok(invite_user::v3::Response {}); } if let Ok(target_user_membership) = services .rooms .state_accessor - .get_member(&body.room_id, user_id) + .get_member(&body.room_id, recipient_user) .await { if target_user_membership.membership == MembershipState::Ban { @@ -70,16 +72,27 @@ pub(crate) async fn invite_user_route( } } - if recipient_ignored_by_sender { - // silently drop the invite to the recipient if they've been ignored by the - // sender, pretend it worked - return Ok(invite_user::v3::Response {}); + // check for blocked invites if the recipient is a local user. + if services.globals.user_is_local(recipient_user) { + let recipient_filter_level = services + .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( &services, sender_user, - user_id, + recipient_user, &body.room_id, body.reason.clone(), false, @@ -98,7 +111,7 @@ pub(crate) async fn invite_user_route( pub(crate) async fn invite_helper( services: &Services, sender_user: &UserId, - user_id: &UserId, + recipient_user: &UserId, room_id: &RoomId, reason: Option, is_direct: bool, @@ -111,12 +124,12 @@ pub(crate) async fn invite_helper( 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 state_lock = services.rooms.state.mutex.lock(room_id).await; 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), reason, ..RoomMemberEventContent::new(MembershipState::Invite) @@ -126,7 +139,7 @@ pub(crate) async fn invite_helper( .rooms .timeline .create_hash_and_sign_event( - PduBuilder::state(user_id.to_string(), &content), + PduBuilder::state(recipient_user.to_string(), &content), sender_user, Some(room_id), &state_lock, @@ -144,7 +157,7 @@ pub(crate) async fn invite_helper( let response = services .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(), event_id: (*pdu.event_id).to_owned(), room_version: room_version_id.clone(), @@ -173,7 +186,7 @@ pub(crate) async fn invite_helper( return Err!(Request(BadJson(warn!( %pdu.event_id, %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 content = RoomMemberEventContent { - displayname: services.users.displayname(user_id).await.ok(), - avatar_url: services.users.avatar_url(user_id).await.ok(), - blurhash: services.users.blurhash(user_id).await.ok(), + displayname: services.users.displayname(recipient_user).await.ok(), + avatar_url: services.users.avatar_url(recipient_user).await.ok(), + blurhash: services.users.blurhash(recipient_user).await.ok(), is_direct: Some(is_direct), reason, ..RoomMemberEventContent::new(MembershipState::Invite) @@ -225,7 +238,7 @@ pub(crate) async fn invite_helper( .rooms .timeline .build_and_append_pdu( - PduBuilder::state(user_id.to_string(), &content), + PduBuilder::state(recipient_user.to_string(), &content), sender_user, Some(room_id), &state_lock, diff --git a/src/api/client/message.rs b/src/api/client/message.rs index bc7403c1..4b73c2af 100644 --- a/src/api/client/message.rs +++ b/src/api/client/message.rs @@ -30,6 +30,7 @@ use ruma::{ events::{ AnyStateEvent, StateEventType, TimelineEventType::{self, *}, + invite_permission_config::FilterLevel, }, serde::Raw, }; @@ -267,7 +268,7 @@ pub(crate) async fn ignored_filter( pub(crate) async fn is_ignored_pdu( services: &Services, event: &Pdu, - user_id: &UserId, + recipient_user: &UserId, ) -> bool where Pdu: Event + Send + Sync, @@ -278,20 +279,28 @@ where 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 - .is_remote_server_ignored(event.sender().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)) + .is_remote_server_ignored(sender_user.server_name()) { + // 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; } @@ -320,6 +329,29 @@ pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Opti 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)] fn _is_sorted() { debug_assert!( diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index efde40dd..0359e9c2 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use axum::extract::State; use conduwuit::{ @@ -13,6 +13,7 @@ use ruma::{ api::client::room::{self, create_room}, events::{ TimelineEventType, + invite_permission_config::FilterLevel, room::{ canonical_alias::RoomCanonicalAliasEventContent, 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"))); } + 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 = match body.room_alias_name.as_ref() { | Some(alias) => 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 }); - 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 { - for invite in &body.invite { - if services.users.user_is_ignored(sender_user, invite).await { - 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)); + for recipient_user in &invitees { + power_levels_to_grant.insert(recipient_user.clone(), int!(100)); } } @@ -289,7 +316,7 @@ pub(crate) async fn create_room_route( } } } 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 // 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( body.power_level_content_override.as_ref(), &body.visibility, - users, + power_levels_to_grant, creators, )?; @@ -459,17 +486,9 @@ pub(crate) async fn create_room_route( // 8. Events implied by invite (and TODO: invite_3pid) drop(state_lock); - for user_id in &body.invite { - 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; - } - + for recipient_user in &invitees { 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() .await { diff --git a/src/api/client/sync/v3.rs b/src/api/client/sync/v3.rs index bd63e278..e897a9e5 100644 --- a/src/api/client/sync/v3.rs +++ b/src/api/client/sync/v3.rs @@ -60,7 +60,10 @@ use ruma::{ use service::rooms::short::{ShortEventId, ShortStateKey}; 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)] struct StateChanges { @@ -238,6 +241,13 @@ pub(crate) async fn build_sync_events( .rooms .state_cache .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 { let invite_count = services .rooms diff --git a/src/api/client/sync/v4.rs b/src/api/client/sync/v4.rs index a16e4526..fcf317bd 100644 --- a/src/api/client/sync/v4.rs +++ b/src/api/client/sync/v4.rs @@ -11,6 +11,7 @@ use conduwuit::{ utils::{ BoolExt, IterStream, ReadyExt, TryFutureExtExt, math::{ruma_from_usize, usize_from_ruma, usize_from_u64_truncated}, + stream::WidebandExt, }, warn, }; @@ -39,7 +40,7 @@ use ruma::{ use super::{load_timeline, share_encrypted_room}; use crate::{ Ruma, - client::{DEFAULT_BUMP_TYPES, ignored_filter}, + client::{DEFAULT_BUMP_TYPES, ignored_filter, is_ignored_invite}, }; type TodoRooms = BTreeMap, usize, u64)>; @@ -102,6 +103,13 @@ pub(crate) async fn sync_events_v4_route( .rooms .state_cache .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) .collect() .await; diff --git a/src/api/client/sync/v5.rs b/src/api/client/sync/v5.rs index e4cefba0..8bc9ff29 100644 --- a/src/api/client/sync/v5.rs +++ b/src/api/client/sync/v5.rs @@ -14,6 +14,7 @@ use conduwuit::{ BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt, future::ReadyEqExt, math::{ruma_from_usize, usize_from_ruma}, + stream::WidebandExt, }, warn, }; @@ -38,7 +39,7 @@ use ruma::{ use super::share_encrypted_room; use crate::{ 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); @@ -106,6 +107,13 @@ pub(crate) async fn sync_events_v5_route( .rooms .state_cache .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) .collect::>(); diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 0a9b2e10..78a65fe8 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -61,13 +61,16 @@ pub(crate) async fn create_invite_route( let mut signed_event = utils::to_canonical_object(&body.event) .map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?; - let invited_user: OwnedUserId = signed_event + let recipient_user: OwnedUserId = signed_event .get("state_key") .try_into() .map(UserId::to_owned) .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."))); } @@ -75,7 +78,7 @@ pub(crate) async fn create_invite_route( services .rooms .event_handler - .acl_check(invited_user.server_name(), &body.room_id) + .acl_check(recipient_user.server_name(), &body.room_id) .await?; services @@ -89,18 +92,19 @@ pub(crate) async fn create_invite_route( // Add event_id back 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") .try_into() .map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?; 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."))); } - 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."))); } @@ -131,9 +135,9 @@ pub(crate) async fn create_invite_route( .state_cache .update_membership( &body.room_id, - &invited_user, + &recipient_user, RoomMemberEventContent::new(MembershipState::Invite), - sender, + sender_user, Some(invite_state), body.via.clone(), true, @@ -141,7 +145,7 @@ pub(crate) async fn create_invite_route( .await?; for appservice in services.appservice.read().await.values() { - if appservice.is_user_match(&invited_user) { + if appservice.is_user_match(&recipient_user) { services .sending .send_appservice_request( diff --git a/src/core/error/response.rs b/src/core/error/response.rs index ae6fce62..3f8704c1 100644 --- a/src/core/error/response.rs +++ b/src/core/error/response.rs @@ -73,6 +73,7 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode { | ThreepidAuthFailed | UserDeactivated | ThreepidDenied + | InviteBlocked | WrongRoomKeysVersion { .. } | Forbidden { .. } => StatusCode::FORBIDDEN, diff --git a/src/database/maps.rs b/src/database/maps.rs index da97ef45..eee5f5e4 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -434,4 +434,8 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userroomid_notificationcount", ..descriptor::RANDOM }, + Descriptor { + name: "userroomid_invitesender", + ..descriptor::RANDOM_SMALL + }, ]; diff --git a/src/service/rooms/state_cache/mod.rs b/src/service/rooms/state_cache/mod.rs index 2d8f5cc5..6874cc80 100644 --- a/src/service/rooms/state_cache/mod.rs +++ b/src/service/rooms/state_cache/mod.rs @@ -12,7 +12,7 @@ use conduwuit::{ use database::{Deserialized, Ignore, Interfix, Map}; use futures::{Stream, StreamExt, future::join5, pin_mut}; use ruma::{ - OwnedRoomId, RoomId, ServerName, UserId, + OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId, events::{AnyStrippedStateEvent, AnySyncStateEvent, room::member::MembershipState}, serde::Raw, }; @@ -49,6 +49,7 @@ struct Data { userroomid_joined: Arc, userroomid_leftstate: Arc, userroomid_knockedstate: Arc, + userroomid_invitesender: Arc, } type AppServiceInRoomCache = SyncRwLock>>; @@ -83,6 +84,7 @@ impl crate::Service for Service { userroomid_joined: args.db["userroomid_joined"].clone(), userroomid_leftstate: args.db["userroomid_leftstate"].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); 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 { + let key = (user_id, room_id); + self.db + .userroomid_invitesender + .qry(&key) + .await + .deserialized() +} diff --git a/src/service/rooms/state_cache/update.rs b/src/service/rooms/state_cache/update.rs index 86c1afe7..295bd9e7 100644 --- a/src/service/rooms/state_cache/update.rs +++ b/src/service/rooms/state_cache/update.rs @@ -1,6 +1,6 @@ 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 futures::StreamExt; use ruma::{ @@ -9,6 +9,7 @@ use ruma::{ AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, direct::DirectEvent, + invite_permission_config::FilterLevel, room::{ create::RoomCreateEventContent, member::{MembershipState, RoomMemberEventContent}, @@ -121,12 +122,21 @@ pub async fn update_membership( self.mark_as_joined(user_id, room_id); }, | MembershipState::Invite => { - // We want to know if the sender is ignored by the receiver - if self.services.users.user_is_ignored(sender, user_id).await { - return Ok(()); + // return an error for blocked invites. 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!( + 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, last_state, invite_via) + self.mark_as_invited(user_id, room_id, sender, last_state, invite_via) .await; }, | 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.roomuserid_invitecount.remove(&roomuser_id); + self.db.userroomid_invitesender.remove(&userroom_id); self.db.userroomid_leftstate.remove(&userroom_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.roomuserid_invitecount.remove(&roomuser_id); + self.db.userroomid_invitesender.remove(&userroom_id); self.db.userroomid_knockedstate.remove(&userroom_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.roomuserid_invitecount.remove(&roomuser_id); + self.db.userroomid_invitesender.remove(&userroom_id); self.db.userroomid_leftstate.remove(&userroom_id); self.db.roomuserid_leftcount.remove(&roomuser_id); @@ -335,6 +348,7 @@ pub async fn mark_as_invited( &self, user_id: &UserId, room_id: &RoomId, + sender_user: &UserId, last_state: Option>>, invite_via: Option>, ) { @@ -350,6 +364,9 @@ pub async fn mark_as_invited( self.db .roomuserid_invitecount .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.roomuserid_joined.remove(&roomuser_id); diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index b5022b63..ac999858 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -20,7 +20,9 @@ use ruma::{ api::client::{device::Device, error::ErrorKind, filter::FilterDefinition}, encryption::{CrossSigningKey, DeviceKeys, OneTimeKey}, events::{ - AnyToDeviceEvent, GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent, + AnyToDeviceEvent, GlobalAccountDataEventType, + ignored_user_list::IgnoredUserListEvent, + invite_permission_config::{FilterLevel, InvitePermissionConfigEvent}, }, 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 #[inline] pub async fn is_admin(&self, user_id: &UserId) -> bool {