Compare commits

..

19 Commits

Author SHA1 Message Date
Jade Ellis e5e2db37d9 ci: Run image release workflow on tag 2025-09-22 17:03:26 +01:00
Jade Ellis e08ea3b9e5 ci: Trace commands to push docker manifests 2025-09-22 17:03:26 +01:00
Jade Ellis 4f1907abfa ci: Change tag generation to use suffix flavour 2025-09-22 17:03:26 +01:00
Ginger 92d74c293e feat: Advertise support for MSC4155 2025-09-22 11:33:45 -04:00
Renovate Bot 3fbdced0e1 chore(deps): update github-actions-non-major 2025-09-22 05:04:03 +00:00
nexy7574 b70470fa71 fix: Event filters all non-state events 2025-09-21 20:10:36 +01:00
nexy7574 703d6a2075 chore: Bump version to rc.8 2025-09-21 18:17:24 +01:00
Savyasachee Jha 5b75e21810 Update resolv-conf to upstream 0.7.5 2025-09-21 17:13:38 +00:00
Ginger 13b7538785 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>
2025-09-21 17:03:40 +00:00
Renovate Bot 9745bcba1c chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.121.4 2025-09-21 05:02:02 +00:00
nexy7574 c9c79fbea6 fix: Fix restricted join rules inconsistencies 2025-09-20 21:07:13 +00:00
nexy7574 92e9802340 style: Tidy up 1054 2025-09-20 21:07:00 +00:00
nexy7574 1d80b7ce0c fix: Don't perform local join when there's no remote servers 2025-09-20 21:07:00 +00:00
Jade 563b6d4b30 fix: Update debug assertion with new serde type location
Fixes !1052
2025-09-20 18:04:16 +00:00
Renovate Bot e86fc6d9f8 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.119.5 2025-09-20 05:03:27 +00:00
Renovate Bot 13adea6498 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.118.1 2025-09-19 10:31:58 +00:00
Renovate Bot 17d0bb6cf6 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.117.0 2025-09-18 21:06:35 +00:00
Renovate Bot 6dc5051fa6 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.116.10 2025-09-18 19:26:39 +00:00
Renovate Bot 3034c03ad1 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.116.8 2025-09-18 13:36:19 +00:00
22 changed files with 295 additions and 127 deletions
@@ -61,14 +61,16 @@ runs:
id: meta
uses: docker/metadata-action@v5
with:
flavour: |
suffix=${{ inputs.tag_suffix }}
tags: |
type=semver,pattern={{version}},prefix=v,suffix=${{ inputs.tag_suffix }}
type=semver,pattern={{major}}.{{minor}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.0.') }},prefix=v,suffix=${{ inputs.tag_suffix }}
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }},prefix=v,suffix=${{ inputs.tag_suffix }}
type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},suffix=${{ inputs.tag_suffix }}
type=ref,event=pr,suffix=${{ inputs.tag_suffix }}
type=sha,format=short,suffix=${{ inputs.tag_suffix }}
type=raw,value=latest${{ inputs.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}}.{{minor}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.0.') }},prefix=v
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }},prefix=v
type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},
type=ref,event=pr
type=sha,format=short
type=raw,value=latest${{ inputs.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') }},priority=1100
images: ${{ inputs.images }}
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
env:
@@ -81,6 +83,7 @@ runs:
env:
IMAGES: ${{ inputs.images }}
run: |
set -o xtrace
IFS=$'\n'
IMAGES_LIST=($IMAGES)
ANNOTATIONS_LIST=($DOCKER_METADATA_OUTPUT_ANNOTATIONS)
@@ -98,6 +101,7 @@ runs:
env:
IMAGES: ${{ inputs.images }}
run: |
set -o xtrace
IMAGES_LIST=($IMAGES)
for REPO in "${IMAGES_LIST[@]}"; do
docker buildx imagetools inspect $REPO:${{ steps.meta.outputs.version }}
+2
View File
@@ -23,6 +23,8 @@ on:
- "renovate.json"
- "pkg/**"
- "docs/**"
tags:
- "v*.*.*"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:41.116.6@sha256:9289c8a411975de5fff15fd75a1f81146f9ff465ea2d7f9f5fa2fbe6e0c117d6
image: ghcr.io/renovatebot/renovate:41.122.3@sha256:1ea1fb8fc3e3cc5cec1340c6b25ac0de53c41662c7d65f46aeb8ca42282c93e9
options: --tmpfs /tmp:exec
steps:
- name: Checkout
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
submodules: false
persist-credentials: false
- uses: https://github.com/cachix/install-nix-action@7be5dee1421f63d07e71ce6e0a9f8a4b07c2a487 # v31.6.1
- uses: https://github.com/cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2
with:
nix_path: nixpkgs=channel:nixos-unstable
Generated
+15 -14
View File
@@ -875,7 +875,7 @@ dependencies = [
[[package]]
name = "conduwuit"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"clap",
"conduwuit_admin",
@@ -907,7 +907,7 @@ dependencies = [
[[package]]
name = "conduwuit_admin"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"clap",
"conduwuit_api",
@@ -929,7 +929,7 @@ dependencies = [
[[package]]
name = "conduwuit_api"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"async-trait",
"axum",
@@ -962,14 +962,14 @@ dependencies = [
[[package]]
name = "conduwuit_build_metadata"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"built 0.8.0",
]
[[package]]
name = "conduwuit_core"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"argon2",
"arrayvec",
@@ -1030,7 +1030,7 @@ dependencies = [
[[package]]
name = "conduwuit_database"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"async-channel",
"conduwuit_core",
@@ -1049,7 +1049,7 @@ dependencies = [
[[package]]
name = "conduwuit_macros"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
@@ -1059,7 +1059,7 @@ dependencies = [
[[package]]
name = "conduwuit_router"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"axum",
"axum-client-ip",
@@ -1094,7 +1094,7 @@ dependencies = [
[[package]]
name = "conduwuit_service"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -1134,7 +1134,7 @@ dependencies = [
[[package]]
name = "conduwuit_web"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"askama",
"axum",
@@ -4061,8 +4061,9 @@ dependencies = [
[[package]]
name = "resolv-conf"
version = "0.7.4"
source = "git+https://forgejo.ellis.link/continuwuation/resolv-conf?rev=ebbbec1cb965b487a0150f5d007e96c05e3d72af#ebbbec1cb965b487a0150f5d007e96c05e3d72af"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799"
[[package]]
name = "rgb"
@@ -6564,7 +6565,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"clap",
"serde",
@@ -6573,7 +6574,7 @@ dependencies = [
[[package]]
name = "xtask-generate-commands"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
dependencies = [
"clap-markdown",
"clap_builder",
+5 -7
View File
@@ -21,7 +21,7 @@ license = "Apache-2.0"
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
rust-version = "1.86.0"
version = "0.5.0-rc.7"
version = "0.5.0-rc.8"
[workspace.metadata.crane]
name = "conduwuit"
@@ -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
@@ -554,6 +555,9 @@ version = "0.11.5"
default-features = false
features = ["sync", "tls-rustls"]
[workspace.dependencies.resolv-conf]
version = "0.7.5"
#
# Patches
#
@@ -598,12 +602,6 @@ rev = "9c8e51510c35077df888ee72a36b4b05637147da"
git = "https://forgejo.ellis.link/continuwuation/hyper-util"
rev = "e4ae7628fe4fcdacef9788c4c8415317a4489941"
# Allows no-aaaa option in resolv.conf
# Use 1-indexed line numbers when displaying parse error messages
[patch.crates-io.resolv-conf]
git = "https://forgejo.ellis.link/continuwuation/resolv-conf"
rev = "ebbbec1cb965b487a0150f5d007e96c05e3d72af"
#
# Our crates
#
+39 -26
View File
@@ -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<String>,
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,
+10 -4
View File
@@ -313,11 +313,14 @@ pub async fn join_room_by_id_helper(
}
}
let local_join = server_in_room
|| servers.is_empty()
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
if !server_in_room && servers.is_empty() {
return Err!(Request(NotFound(
"No servers were provided to assist in joining the room remotely, and we are not \
already participating in the room."
)));
}
if local_join {
if server_in_room {
join_room_by_id_helper_local(
services,
sender_user,
@@ -739,6 +742,7 @@ async fn join_room_by_id_helper_local(
.iter()
.stream()
.any(|restriction_room_id| {
trace!("Checking if {sender_user} is joined to {restriction_room_id}");
services
.rooms
.state_cache
@@ -751,6 +755,7 @@ async fn join_room_by_id_helper_local(
.state_cache
.local_users_in_room(room_id)
.filter(|user| {
trace!("Checking if {user} can invite {sender_user} to {room_id}");
services.rooms.state_accessor.user_can_invite(
room_id,
user,
@@ -763,6 +768,7 @@ async fn join_room_by_id_helper_local(
.await
.map(ToOwned::to_owned)
} else {
trace!("No restriction rooms are joined by {sender_user}");
None
}
};
+46 -13
View File
@@ -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<Pdu>(
services: &Services,
event: &Pdu,
user_id: &UserId,
recipient_user: &UserId,
) -> bool
where
Pdu: Event + Send + Sync,
@@ -278,20 +279,29 @@ where
return true;
}
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok();
let ignored_server = services
let sender_user = event.sender();
let type_ignored = IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok();
let server_ignored = services
.moderation
.is_remote_server_ignored(event.sender().server_name());
.is_remote_server_ignored(sender_user.server_name());
let user_ignored = services
.users
.user_is_ignored(sender_user, recipient_user)
.await;
if ignored_type
&& (ignored_server
|| (!services.config.send_messages_from_ignored_users_to_client
&& services
.users
.user_is_ignored(event.sender(), user_id)
.await))
{
if !type_ignored {
// We cannot safely ignore this type
return false;
}
if server_ignored {
// the sender's server is ignored, so ignore this event
return true;
}
if user_ignored && !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 +330,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!(
+43 -24
View File
@@ -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<OwnedRoomAliasId> = 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
{
+11 -1
View File
@@ -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
+9 -1
View File
@@ -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<OwnedRoomId, (BTreeSet<TypeStateKey>, 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;
+9 -1
View File
@@ -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::<Vec<OwnedRoomId>>();
+1
View File
@@ -59,6 +59,7 @@ pub(crate) async fn get_supported_versions_route(
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
]),
};
+13 -9
View File
@@ -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(
+1
View File
@@ -73,6 +73,7 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| ThreepidAuthFailed
| UserDeactivated
| ThreepidDenied
| InviteBlocked
| WrongRoomKeysVersion { .. }
| Forbidden { .. } => StatusCode::FORBIDDEN,
+13 -9
View File
@@ -727,12 +727,20 @@ where
let user_joined = user_for_join_auth_membership == &MembershipState::Join;
let okay_power = is_creator(room_version, &creators, create_room, user_for_join_auth)
|| auth_user_pl >= invite_level;
trace!(
auth_user_pl=?auth_user_pl,
invite_level=?invite_level,
user_joined=?user_joined,
okay_power=?okay_power,
passing=?(user_joined && okay_power),
"user for join auth is valid check details"
);
user_joined && okay_power
} else {
// No auth user was given
trace!("No auth user given for join auth");
false
};
let sender_creator = is_creator(room_version, &creators, create_room, sender);
let target_creator = is_creator(room_version, &creators, create_room, target_user);
@@ -811,9 +819,7 @@ where
false
},
| JoinRule::KnockRestricted(_) => {
let valid_join = user_for_join_auth_is_valid
|| sender_membership == MembershipState::Join;
if membership_allows_join || valid_join {
if membership_allows_join || user_for_join_auth_is_valid {
true
} else {
warn!(
@@ -826,16 +832,14 @@ where
}
},
| JoinRule::Restricted(_) =>
if !user_for_join_auth_is_valid
&& sender_membership != MembershipState::Join
{
if membership_allows_join || user_for_join_auth_is_valid {
true
} else {
warn!(
"Join rule is a restricted one but no valid authorising user \
was given"
);
false
} else {
true
},
| JoinRule::Public => true,
| _ => {
+1 -1
View File
@@ -417,7 +417,7 @@ impl<'a, 'de: 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
debug_assert_eq!(
conduwuit::debug::type_name::<V>(),
"serde_json::value::de::<impl serde::de::Deserialize for \
"serde_json::value::de::<impl serde_core::de::Deserialize for \
serde_json::value::Value>::deserialize::ValueVisitor",
"deserialize_any: type not expected"
);
+4
View File
@@ -434,4 +434,8 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "userroomid_notificationcount",
..descriptor::RANDOM
},
Descriptor {
name: "userroomid_invitesender",
..descriptor::RANDOM_SMALL
},
];
+14 -1
View File
@@ -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<Map>,
userroomid_leftstate: Arc<Map>,
userroomid_knockedstate: Arc<Map>,
userroomid_invitesender: Arc<Map>,
}
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_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<OwnedUserId> {
let key = (user_id, room_id);
self.db
.userroomid_invitesender
.qry(&key)
.await
.deserialized()
}
+23 -6
View File
@@ -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<Vec<Raw<AnyStrippedStateEvent>>>,
invite_via: Option<Vec<OwnedServerName>>,
) {
@@ -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);
+23 -1
View File
@@ -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 {