Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Ginger
931b9735e9
chore: Formatting fixes 2026-02-09 21:28:00 -05:00
Ginger
8add53f8ab
feat: Implement a migration to fix busted local invites 2026-02-09 21:01:29 -05:00
Ginger
50d75bfc8f
fix: Properly set stripped state for local invites 2026-02-09 16:25:26 -05:00
5 changed files with 63 additions and 10 deletions

View file

@ -0,0 +1 @@
Fixed invites sent to other users in the same homeserver not being properly sent down sync. Users with missing or broken invites should clear their client caches after updating to make them appear.

View file

@ -1,7 +1,7 @@
use std::{cmp, collections::HashMap}; use std::{cmp, collections::HashMap, future::ready};
use conduwuit::{ use conduwuit::{
Err, Pdu, Result, debug, debug_info, debug_warn, error, info, Err, Event, Pdu, Result, debug, debug_info, debug_warn, error, info,
result::NotFound, result::NotFound,
utils::{ utils::{
IterStream, ReadyExt, IterStream, ReadyExt,
@ -15,8 +15,9 @@ use itertools::Itertools;
use ruma::{ use ruma::{
OwnedRoomId, OwnedUserId, RoomId, UserId, OwnedRoomId, OwnedUserId, RoomId, UserId,
events::{ events::{
GlobalAccountDataEventType, StateEventType, push_rules::PushRulesEvent, AnyStrippedStateEvent, GlobalAccountDataEventType, StateEventType,
room::member::MembershipState, push_rules::PushRulesEvent,
room::member::{MembershipState, RoomMemberEventContent},
}, },
push::Ruleset, push::Ruleset,
serde::Raw, serde::Raw,
@ -162,6 +163,14 @@ async fn migrate(services: &Services) -> Result<()> {
populate_userroomid_leftstate_table(services).await?; populate_userroomid_leftstate_table(services).await?;
} }
if db["global"]
.get(FIXED_LOCAL_INVITE_STATE_MARKER)
.await
.is_not_found()
{
fix_local_invite_state(services).await?;
}
assert_eq!( assert_eq!(
services.globals.db.database_version().await, services.globals.db.database_version().await,
DATABASE_VERSION, DATABASE_VERSION,
@ -721,3 +730,46 @@ async fn populate_userroomid_leftstate_table(services: &Services) -> Result {
db.db.sort()?; db.db.sort()?;
Ok(()) Ok(())
} }
const FIXED_LOCAL_INVITE_STATE_MARKER: &str = "fix_local_invite_state";
async fn fix_local_invite_state(services: &Services) -> Result {
// Clean up the effects of !1249 by caching stripped state for invites
type KeyVal<'a> = (Key<'a>, Raw<Vec<AnyStrippedStateEvent>>);
type Key<'a> = (&'a UserId, &'a RoomId);
let db = &services.db;
let cork = db.cork_and_sync();
let userroomid_invitestate = services.db["userroomid_invitestate"].clone();
// for each user invited to a room
let fixed = userroomid_invitestate.stream()
// if they're a local user on this homeserver
.try_filter(|((user_id, _), _): &KeyVal<'_>| ready(services.globals.user_is_local(user_id)))
.and_then(async |((user_id, room_id), stripped_state): KeyVal<'_>| Ok::<_, conduwuit::Error>((user_id.to_owned(), room_id.to_owned(), stripped_state.deserialize()?)))
.try_fold(0_usize, async |mut fixed, (user_id, room_id, stripped_state)| {
// and their invite state is None
if stripped_state.is_empty()
// and they are actually invited to the room
&& let Ok(membership_event) = services.rooms.state_accessor.room_state_get(&room_id, &StateEventType::RoomMember, user_id.as_str()).await
&& membership_event.get_content::<RoomMemberEventContent>().is_ok_and(|content| content.membership == MembershipState::Invite)
// and the invite was sent by a local user
&& services.globals.user_is_local(&membership_event.sender) {
// build and save stripped state for their invite in the database
let stripped_state = services.rooms.state.summary_stripped(&membership_event, &room_id).await;
userroomid_invitestate.put((&user_id, &room_id), Json(stripped_state));
fixed = fixed.saturating_add(1);
}
Ok(fixed)
})
.await?;
drop(cork);
info!(?fixed, "Fixed local invite state cache entries.");
db["global"].insert(FIXED_LOCAL_INVITE_STATE_MARKER, []);
db.db.sort()?;
Ok(())
}

View file

@ -1,7 +1,7 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use conduwuit::{ use conduwuit::{
Result, err, implement, Pdu, Result, err, implement,
matrix::{Event, StateKey}, matrix::{Event, StateKey},
}; };
use futures::{Stream, StreamExt, TryFutureExt}; use futures::{Stream, StreamExt, TryFutureExt};
@ -84,7 +84,7 @@ pub async fn room_state_get(
room_id: &RoomId, room_id: &RoomId,
event_type: &StateEventType, event_type: &StateEventType,
state_key: &str, state_key: &str,
) -> Result<impl Event> { ) -> Result<Pdu> {
self.services self.services
.state .state
.get_room_shortstatehash(room_id) .get_room_shortstatehash(room_id)

View file

@ -30,6 +30,7 @@ struct Services {
config: Dep<config::Service>, config: Dep<config::Service>,
globals: Dep<globals::Service>, globals: Dep<globals::Service>,
metadata: Dep<rooms::metadata::Service>, metadata: Dep<rooms::metadata::Service>,
state: Dep<rooms::state::Service>,
state_accessor: Dep<rooms::state_accessor::Service>, state_accessor: Dep<rooms::state_accessor::Service>,
users: Dep<users::Service>, users: Dep<users::Service>,
} }
@ -64,6 +65,7 @@ impl crate::Service for Service {
config: args.depend::<config::Service>("config"), config: args.depend::<config::Service>("config"),
globals: args.depend::<globals::Service>("globals"), globals: args.depend::<globals::Service>("globals"),
metadata: args.depend::<rooms::metadata::Service>("rooms::metadata"), metadata: args.depend::<rooms::metadata::Service>("rooms::metadata"),
state: args.depend::<rooms::state::Service>("rooms::state"),
state_accessor: args state_accessor: args
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"), .depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
users: args.depend::<users::Service>("users"), users: args.depend::<users::Service>("users"),

View file

@ -118,10 +118,8 @@ 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 => {
// TODO: make sure that passing None for `last_state` is correct behavior. let last_state = self.services.state.summary_stripped(pdu, room_id).await;
// the call from `append_pdu` used to use `services.state.summary_stripped` self.mark_as_invited(user_id, room_id, pdu.sender(), Some(last_state), None)
// to fill that parameter.
self.mark_as_invited(user_id, room_id, pdu.sender(), None, None)
.await?; .await?;
}, },
| MembershipState::Leave | MembershipState::Ban => { | MembershipState::Leave | MembershipState::Ban => {