mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-15 05:06:53 +00:00
feat: Check msc4155 invite filters when processing invites
This commit is contained in:
1 parent
0743e18a45
commit
41fb65f379
5 files changed
+154
-77
No files matched your search
@@ -8,7 +8,10 @@ use futures::{FutureExt, join};
|
||||
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,26 @@ 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);
|
||||
let recipient_filter_level = services
|
||||
.users
|
||||
.invite_filter_level(sender_user, recipient_user);
|
||||
|
||||
let (sender_ignored_recipient, recipient_ignored_by_sender) =
|
||||
join!(sender_ignored_recipient, recipient_ignored_by_sender);
|
||||
let (sender_filter_level, recipient_filter_level) =
|
||||
join!(sender_filter_level, recipient_filter_level);
|
||||
|
||||
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,24 +77,25 @@ 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 {});
|
||||
match recipient_filter_level {
|
||||
| FilterLevel::Allow => {
|
||||
invite_helper(
|
||||
&services,
|
||||
sender_user,
|
||||
recipient_user,
|
||||
&body.room_id,
|
||||
body.reason.clone(),
|
||||
false,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok(invite_user::v3::Response {})
|
||||
},
|
||||
| FilterLevel::Ignore => Ok(invite_user::v3::Response {}),
|
||||
| FilterLevel::Block =>
|
||||
Err!(Request(InviteBlocked("{recipient_user} has blocked invites from you."))),
|
||||
}
|
||||
|
||||
invite_helper(
|
||||
&services,
|
||||
sender_user,
|
||||
user_id,
|
||||
&body.room_id,
|
||||
body.reason.clone(),
|
||||
false,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok(invite_user::v3::Response {})
|
||||
},
|
||||
| _ => {
|
||||
Err!(Request(NotFound("User not found.")))
|
||||
@@ -98,7 +106,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<String>,
|
||||
is_direct: bool,
|
||||
@@ -111,12 +119,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 +134,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,
|
||||
room_id,
|
||||
&state_lock,
|
||||
@@ -144,7 +152,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 +181,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 +221,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 +233,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,
|
||||
room_id,
|
||||
&state_lock,
|
||||
|
||||
@@ -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,
|
||||
@@ -100,6 +101,38 @@ 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;
|
||||
}
|
||||
match services
|
||||
.users
|
||||
.invite_filter_level(sender_user, recipient_user)
|
||||
.await
|
||||
{
|
||||
| FilterLevel::Allow => {
|
||||
invitees.insert(recipient_user.clone());
|
||||
},
|
||||
| FilterLevel::Ignore => {
|
||||
continue;
|
||||
},
|
||||
| FilterLevel::Block =>
|
||||
return Err!(Request(InviteBlocked(
|
||||
"{recipient_user} has blocked invites from you."
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
let _short_id = services
|
||||
.rooms
|
||||
.short
|
||||
@@ -219,26 +252,18 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
let power_levels_content = default_power_levels_content(
|
||||
body.power_level_content_override.as_ref(),
|
||||
&body.visibility,
|
||||
users,
|
||||
power_levels_to_grant,
|
||||
)?;
|
||||
|
||||
services
|
||||
@@ -399,17 +424,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
|
||||
{
|
||||
|
||||
+28
-10
@@ -10,7 +10,10 @@ use conduwuit::{
|
||||
use ruma::{
|
||||
CanonicalJsonValue, OwnedUserId, UserId,
|
||||
api::{client::error::ErrorKind, federation::membership::create_invite},
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
events::{
|
||||
invite_permission_config::FilterLevel,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
serde::JsonObject,
|
||||
};
|
||||
|
||||
@@ -61,13 +64,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 +81,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,21 +95,33 @@ 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.")));
|
||||
}
|
||||
|
||||
let recipient_invite_filter_level = services
|
||||
.users
|
||||
.invite_filter_level(sender_user, &recipient_user)
|
||||
.await;
|
||||
|
||||
if matches!(recipient_invite_filter_level, FilterLevel::Block) {
|
||||
return Err!(Request(InviteBlocked(
|
||||
"{recipient_user} has blocked invites from {sender_user}."
|
||||
)));
|
||||
}
|
||||
|
||||
let mut invite_state = body.invite_room_state.clone();
|
||||
|
||||
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
||||
@@ -131,9 +149,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 +159,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(
|
||||
|
||||
@@ -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,13 +122,24 @@ 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(());
|
||||
match self
|
||||
.services
|
||||
.users
|
||||
.invite_filter_level(sender, user_id)
|
||||
.await
|
||||
{
|
||||
| FilterLevel::Allow => {
|
||||
self.mark_as_invited(user_id, room_id, last_state, invite_via)
|
||||
.await;
|
||||
},
|
||||
| FilterLevel::Ignore => {
|
||||
// do nothing
|
||||
},
|
||||
| FilterLevel::Block =>
|
||||
return Err!(Request(InviteBlocked(
|
||||
"{user_id} has blocked invites from you."
|
||||
))),
|
||||
}
|
||||
|
||||
self.mark_as_invited(user_id, room_id, last_state, invite_via)
|
||||
.await;
|
||||
},
|
||||
| MembershipState::Leave | MembershipState::Ban => {
|
||||
self.mark_as_left(user_id, room_id);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user