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:
parent
9745bcba1c
commit
13b7538785
13 changed files with 234 additions and 82 deletions
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>>();
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue