Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-worms-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nordeck/matrix-meetings-bot': patch
---

Improve Matrix Room Version 12 support by using `/_matrix/client/v3/capabilities` endpoint to retrieve synapse room default version. The `DEFAULT_ROOM_VERSION` option is not needed and is deleted.
3 changes: 0 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,6 @@ GUEST_USER_DEFAULT_POWER_LEVEL=0

# optional - if true, bot deletes guest user power level from power level event when guest user leaves the room. It makes sure power level is cleaned up if guest user is deactivated later.
GUEST_USER_DELETE_POWER_LEVEL_ON_LEAVE=true

# optional - configure a default room version configured for synapse. It is used to make room version specific power levels changes when rooms are created the by the bot. It supports room version 12. It defaults to room 10 version behaviour. The room creator gets power level 150 when the room version is 12.
DEFAULT_ROOM_VERSION=
#
```

Expand Down
2 changes: 0 additions & 2 deletions matrix-meetings-bot/src/IAppConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,4 @@ export interface IAppConfiguration {
guest_user_prefix: string;
guest_user_default_power_level: number;
guest_user_delete_power_level_on_leave: boolean;

default_room_version?: string;
}
2 changes: 2 additions & 0 deletions matrix-meetings-bot/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { EventContentRenderer } from './EventContentRenderer';
import { IAppConfiguration } from './IAppConfiguration';
import { ModuleProviderToken } from './ModuleProviderToken';
import { JitsiClient } from './client/JitsiClient';
import { MatrixClientAdapter } from './client/MatrixClientAdapter';
import { MeetingClient } from './client/MeetingClient';
import { ReactionClient } from './client/ReactionClient';
import { WidgetClient } from './client/WidgetClient';
Expand Down Expand Up @@ -262,6 +263,7 @@ const i18nFactory: FactoryProvider<void> = {
EventContentRenderer,
JitsiClient,
MeetingClient,
MatrixClientAdapter,
ReactionClient,
WidgetClient,
CommandService,
Expand Down
90 changes: 90 additions & 0 deletions matrix-meetings-bot/src/client/MatrixClientAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2025 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { MatrixClient } from 'matrix-bot-sdk';
import { MatrixClientAdapter } from './MatrixClientAdapter';

jest.mock('matrix-bot-sdk');

describe('MatrixClientAdapter', () => {
beforeEach(() => {
jest
.spyOn(MatrixClient.prototype, 'doRequest')
.mockImplementation((method, endpoint) => {
const response =
endpoint === '/_matrix/client/v3/capabilities'
? {
capabilities: {
'm.room_versions': {
default: '12',
available: {},
},
'm.change_password': { enabled: true },
'm.set_displayname': { enabled: true },
'm.set_avatar_url': { enabled: true },
'm.3pid_changes': { enabled: true },
'm.get_login_token': { enabled: false },
'm.profile_fields': { enabled: true },
},
}
: undefined;

return Promise.resolve(response);
});
});

it('should get synapse default room version via capabilities endpoint', async () => {
const matrixClient = new MatrixClient('https://example.com', 'token123');
const matrixClientAdapter = new MatrixClientAdapter(matrixClient);

expect(
await matrixClientAdapter.getCapabilitiesDefaultRoomVersion(),
).toEqual('12');
expect(MatrixClient.prototype.doRequest).toHaveBeenCalledWith(
'GET',
'/_matrix/client/v3/capabilities',
);
expect(MatrixClient.prototype.doRequest).toHaveBeenCalledTimes(1);
});

it('should get and cache synapse default room version via capabilities endpoint', async () => {
jest.useFakeTimers();

const matrixClient = new MatrixClient('https://example.com', 'token123');
const matrixClientAdapter = new MatrixClientAdapter(matrixClient);

expect(
await matrixClientAdapter.getCapabilitiesDefaultRoomVersion(),
).toEqual('12');
expect(MatrixClient.prototype.doRequest).toHaveBeenCalledWith(
'GET',
'/_matrix/client/v3/capabilities',
);
expect(MatrixClient.prototype.doRequest).toHaveBeenCalledTimes(1);

expect(
await matrixClientAdapter.getCapabilitiesDefaultRoomVersion(),
).toEqual('12');
expect(MatrixClient.prototype.doRequest).toHaveBeenCalledTimes(1);

jest.advanceTimersByTime(7200000); // 2 hours

expect(
await matrixClientAdapter.getCapabilitiesDefaultRoomVersion(),
).toEqual('12');
expect(MatrixClient.prototype.doRequest).toHaveBeenCalledTimes(2);
});
});
57 changes: 57 additions & 0 deletions matrix-meetings-bot/src/client/MatrixClientAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2025 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Injectable } from '@nestjs/common';
import { MatrixClient } from 'matrix-bot-sdk';

// Follows what matrix bot sdk doing for versions: https://github.com/turt2live/matrix-bot-sdk/blob/ac53f0fb625911b87b3c1427661824b9a38d0514/src/MatrixClient.ts#L50
const defaultRoomVersionCacheMs = 7200000; // 2 hours

@Injectable()
export class MatrixClientAdapter {
private cachedDefaultRoomVersion?: string;
private defaultRoomVersionLastFetched: number = 0;

constructor(private matrixClient: MatrixClient) {}

/**
* Retrieves synapse default room version using '/_matrix/client/v3/capabilities' endpoint.
* @returns {Promise<string>} Resolves to the synapse default room version
*/
public async getCapabilitiesDefaultRoomVersion(): Promise<string> {
let defaultRoomVersion: string;

if (
!this.cachedDefaultRoomVersion ||
Date.now() - this.defaultRoomVersionLastFetched >=
defaultRoomVersionCacheMs
) {
const response = await this.matrixClient.doRequest(
'GET',
'/_matrix/client/v3/capabilities',
);
const responseDefaultRoomVersion =
response['capabilities']['m.room_versions']['default'];
defaultRoomVersion = responseDefaultRoomVersion;
this.cachedDefaultRoomVersion = responseDefaultRoomVersion;
this.defaultRoomVersionLastFetched = Date.now();
} else {
defaultRoomVersion = this.cachedDefaultRoomVersion;
}

return defaultRoomVersion;
}
}
4 changes: 3 additions & 1 deletion matrix-meetings-bot/src/client/MeetingClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('MeetingClient', () => {
timezone: 'Europe/Berlin',
userId: '@user-id:example.og',
},
'10',
60,
messagingPowerLevel,
),
Expand Down Expand Up @@ -166,9 +167,9 @@ describe('MeetingClient', () => {
timezone: 'Europe/Berlin',
userId: '@user-id:example.og',
},
'12',
60,
undefined,
'12',
),
).resolves.toEqual([
'!new-room-id',
Expand Down Expand Up @@ -256,6 +257,7 @@ describe('MeetingClient', () => {
timezone: 'Europe/Berlin',
userId: '@user-id:example.og',
},
'10',
60,
),
).resolves.toEqual([
Expand Down
2 changes: 1 addition & 1 deletion matrix-meetings-bot/src/client/MeetingClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export class MeetingClient {
spaceParentEventContent: ISpaceParentEventContent,
roomMatrixEvents: DeepReadonly<IRoomMatrixEvents>,
userContext: IUserContext,
defaultRoomVersion: string,
autoDeletionOffset?: number,
messagingPowerLevel?: number,
defaultRoomVersion?: string,
): Promise<
[string, DeepReadonlyArray<IStateEvent<IElementMembershipEventContent>>]
> {
Expand Down
4 changes: 0 additions & 4 deletions matrix-meetings-bot/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ function createConfiguration() {
process.env.GUEST_USER_DELETE_POWER_LEVEL_ON_LEAVE,
true,
),

default_room_version: process.env.DEFAULT_ROOM_VERSION,
};

return {
Expand Down Expand Up @@ -189,8 +187,6 @@ export const ValidationSchema = Joi.object({
GUEST_USER_PREFIX: Joi.string(),
GUEST_USER_DEFAULT_POWER_LEVEL: Joi.number(),
GUEST_USER_DELETE_POWER_LEVEL_ON_LEAVE: Joi.boolean(),

DEFAULT_ROOM_VERSION: Joi.string(),
});

export default createConfiguration;
6 changes: 5 additions & 1 deletion matrix-meetings-bot/src/service/MeetingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../IEventContentParams';
import { ModuleProviderToken } from '../ModuleProviderToken';
import { JitsiClient } from '../client/JitsiClient';
import { MatrixClientAdapter } from '../client/MatrixClientAdapter';
import { MeetingClient } from '../client/MeetingClient';
import { WidgetClient } from '../client/WidgetClient';
import { BreakoutSessionsDto } from '../dto/BreakoutSessionsDto';
Expand Down Expand Up @@ -76,6 +77,7 @@ export class MeetingService {
constructor(
private jitsiClient: JitsiClient,
private matrixClient: MatrixClient,
private matrixClientAdapter: MatrixClientAdapter,
private roomMessageService: RoomMessageService,
private meetingClient: MeetingClient,
@Inject(ModuleProviderToken.APP_CONFIGURATION)
Expand Down Expand Up @@ -164,6 +166,8 @@ export class MeetingService {
via: [new UserID(await this.matrixClient.getUserId()).domain],
};

const defaultRoomVersion =
await this.matrixClientAdapter.getCapabilitiesDefaultRoomVersion();
const [roomId, renderedMemberEventsWithReason]: [
string,
DeepReadonlyArray<IStateEvent<IElementMembershipEventContent>>,
Expand All @@ -175,9 +179,9 @@ export class MeetingService {
spaceParentEventContent,
this.roomMatrixEvents,
userContext,
defaultRoomVersion,
autoDeletionOffset,
meetingCreate.messaging_power_level,
this.appConfig.default_room_version,
);

const promises: Promise<any>[] = [];
Expand Down
24 changes: 24 additions & 0 deletions matrix-meetings-bot/test/MeetingService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { EventContentRenderer } from '../src/EventContentRenderer';
import { IAppConfiguration } from '../src/IAppConfiguration';
import { MatrixEndpoint } from '../src/MatrixEndpoint';
import { JitsiClient } from '../src/client/JitsiClient';
import { MatrixClientAdapter } from '../src/client/MatrixClientAdapter';
import { MeetingClient } from '../src/client/MeetingClient';
import { WidgetClient } from '../src/client/WidgetClient';
import { BreakoutSessionsDetailDto } from '../src/dto/BreakoutSessionsDetailDto';
Expand Down Expand Up @@ -94,7 +95,10 @@ describe('test relevant functionality of MeetingService', () => {
let appConfig: IAppConfiguration;

const clientMock: MatrixClient = mock(MatrixClient);
const matrixClientAdapterMock: MatrixClientAdapter =
mock(MatrixClientAdapter);
const client: MatrixClient = instance(clientMock);
const matrixClientAdapter = instance(matrixClientAdapterMock);
const MAIN_NON_MEETING_ROOM_ID = 'MAIN_NON_MEETING_ROOM_ID';
const PARENT_MEETING_ROOM_ID = 'PARENT_MEETING_ROOM_ID';
const MEETINGROOM_WITHOUT_WIDGETS_UNDER_MEETING_ROOM_ID =
Expand Down Expand Up @@ -295,6 +299,9 @@ describe('test relevant functionality of MeetingService', () => {
when(clientMock.getRoomState('childRoom2')).thenResolve(childRoom2); // TODO load the room with configured json
when(clientMock.getRoomState('i1')).thenResolve(i1); // TODO load the room with configured json
when(clientMock.getRoomState('i2')).thenResolve(i2); // TODO load the room with configured json
when(
matrixClientAdapterMock.getCapabilitiesDefaultRoomVersion(),
).thenResolve('10');

const input: [string, IStateEvent<any>[]][] = [
['childRoom1', childRoom1],
Expand Down Expand Up @@ -388,6 +395,7 @@ describe('test relevant functionality of MeetingService', () => {
meetingService = new MeetingService(
instance(jitsiClientMock),
client,
matrixClientAdapter,
new RoomMessageService(client, meetingClient),
new MeetingClient(client, eventContentRenderer),
appConfig,
Expand Down Expand Up @@ -632,6 +640,21 @@ describe('test relevant functionality of MeetingService', () => {
});
});

test('check the levels of powerusers for synapse default room version 12', async () => {
when(
matrixClientAdapterMock.getCapabilitiesDefaultRoomVersion(),
).thenResolve('12');

const room_props = createEvent(PARENT_MEETING_ROOM_ID);
await meetingService.createMeeting(userContext, room_props);
verify(clientMock.createRoom(anything())).once();
const roomEvent = capture(clientMock.createRoom).first()[0];
checkStandardFields(roomEvent);
expect(roomEvent?.power_level_content_override?.users).toEqual({
[CURRENT_USER]: 150,
});
});

test('check the levels of powerusers botuser=101 others 100 if sender is botuser', async () => {
const room_props = createEvent(PARENT_MEETING_ROOM_ID);
const newUserContext = { ...userContext, userId: BOT_USER };
Expand Down Expand Up @@ -680,6 +703,7 @@ describe('test relevant functionality of MeetingService', () => {
meetingService = new MeetingService(
jitsiClientMock,
client,
matrixClientAdapter,
new RoomMessageService(client, meetingClient),
new MeetingClient(client, eventContentRenderer),
appConfig,
Expand Down
Loading