fix(rooms): upgrading a room keeps its Lotus settings (#327)
CI / Build & Quality Checks (pull_request) Successful in 4m0s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m50s
CI / Build & Quality Checks (pull_request) Successful in 4m0s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m50s
POST /rooms/{id}/upgrade copies only Synapse's fixed list of state, so an
upgrade silently dropped everything Lotus keeps in room state: an
audio-only voice channel became camera/screenshare-allowed, user limits
and bitrate caps vanished, the soundboard pack was lost, and a room set
to delete messages after 7 days kept new ones forever.
RoomUpgrade now reads the client-owned state first (utils/roomUpgradeState,
unit-tested: io.lotus.*, m.room.retention, role tags, room emoji packs)
and re-creates it in the replacement room; a partial copy is reported in
the dialog instead of closing silently.
Verified headless through the UI (v10 room with voice limit, quality
policy, a soundboard pack and retention → upgrade): before, none
survived; now all four are in the new room.
Follow-up (not here): for spaces, the children's m.space.parent still
point at the old space.
Fixes #327
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
400ee76b63
commit
57036bb6a4
@@ -0,0 +1,35 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isCarriedStateType, stateToCarry } from './roomUpgradeState';
|
||||
|
||||
const ev = (type: string, content: Record<string, unknown>, stateKey = '') => ({
|
||||
getType: () => type,
|
||||
getStateKey: () => stateKey,
|
||||
getContent: () => content,
|
||||
});
|
||||
|
||||
test('Lotus room state is carried; state Synapse already copies is not (#327)', () => {
|
||||
assert.equal(isCarriedStateType('io.lotus.voice_limit'), true);
|
||||
assert.equal(isCarriedStateType('io.lotus.room_quality'), true);
|
||||
assert.equal(isCarriedStateType('io.lotus.soundboard'), true);
|
||||
assert.equal(isCarriedStateType('m.room.retention'), true);
|
||||
assert.equal(isCarriedStateType('in.cinny.room.power_level_tags'), true);
|
||||
assert.equal(isCarriedStateType('im.ponies.room_emotes'), true);
|
||||
assert.equal(isCarriedStateType('m.room.power_levels'), false);
|
||||
assert.equal(isCarriedStateType('m.room.name'), false);
|
||||
assert.equal(isCarriedStateType('m.room.member'), false);
|
||||
});
|
||||
|
||||
test('stateToCarry keeps state keys and skips emptied events', () => {
|
||||
const out = stateToCarry([
|
||||
ev('io.lotus.voice_limit', { max_users: 5 }),
|
||||
ev('io.lotus.soundboard', { clips: [1] }, 'pack-a'),
|
||||
ev('io.lotus.room_quality', {}),
|
||||
ev('m.room.name', { name: 'x' }),
|
||||
ev('m.room.retention', { max_lifetime: 604800000 }),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
out.map((s) => `${s.type}|${s.stateKey}`),
|
||||
['io.lotus.voice_limit|', 'io.lotus.soundboard|pack-a', 'm.room.retention|'],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* [Gitea #327] Room state Synapse does NOT copy into the replacement room on
|
||||
* `POST /rooms/{id}/upgrade` (it carries only power levels, join rules,
|
||||
* name/topic/avatar, history visibility, guest access, encryption, server ACL
|
||||
* and space parent/child). Lotus keeps call policy, voice limits, soundboard,
|
||||
* message retention, role tags and room emoji packs in state, so an upgrade
|
||||
* silently dropped all of them.
|
||||
*/
|
||||
const CARRIED_TYPES = new Set([
|
||||
'm.room.retention',
|
||||
'in.cinny.room.power_level_tags',
|
||||
'im.ponies.room_emotes',
|
||||
]);
|
||||
const CARRIED_PREFIXES = ['io.lotus.'];
|
||||
|
||||
export type StateToCarry = { type: string; stateKey: string; content: Record<string, unknown> };
|
||||
|
||||
export const isCarriedStateType = (type: string): boolean =>
|
||||
CARRIED_TYPES.has(type) || CARRIED_PREFIXES.some((p) => type.startsWith(p));
|
||||
|
||||
/** The state events to re-create in the replacement room (non-empty content only). */
|
||||
export const stateToCarry = (
|
||||
events: Array<{
|
||||
getType: () => string;
|
||||
getStateKey: () => string | undefined;
|
||||
getContent: () => Record<string, unknown>;
|
||||
}>,
|
||||
): StateToCarry[] =>
|
||||
events
|
||||
.filter((ev) => isCarriedStateType(ev.getType()))
|
||||
.map((ev) => ({
|
||||
type: ev.getType(),
|
||||
stateKey: ev.getStateKey() ?? '',
|
||||
content: ev.getContent(),
|
||||
}))
|
||||
.filter((s) => s.content && Object.keys(s.content).length > 0);
|
||||
Reference in New Issue
Block a user