fix(federation): handle "disinvite" and duplicated messages (#37791)

Co-authored-by: Jéssica Souza <jessica_schelly@hotmail.com>
This commit is contained in:
Guilherme Gazzo 2025-12-15 22:17:32 +01:00 committed by GitHub
parent b81ae0b6d8
commit 1baa03cced
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 76 additions and 17 deletions

View File

@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-services": patch
"@rocket.chat/federation-matrix": patch
---
Fixes an issue where cases of invites that were canceled or disinvited were not being handled.

View File

@ -98,7 +98,7 @@
"@rocket.chat/emitter": "~0.31.25", "@rocket.chat/emitter": "~0.31.25",
"@rocket.chat/favicon": "workspace:^", "@rocket.chat/favicon": "workspace:^",
"@rocket.chat/federation-matrix": "workspace:^", "@rocket.chat/federation-matrix": "workspace:^",
"@rocket.chat/federation-sdk": "0.3.3", "@rocket.chat/federation-sdk": "0.3.5",
"@rocket.chat/freeswitch": "workspace:^", "@rocket.chat/freeswitch": "workspace:^",
"@rocket.chat/fuselage": "^0.69.0", "@rocket.chat/fuselage": "^0.69.0",
"@rocket.chat/fuselage-forms": "~0.1.1", "@rocket.chat/fuselage-forms": "~0.1.1",

View File

@ -22,7 +22,7 @@
"@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^", "@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.31.25", "@rocket.chat/emitter": "^0.31.25",
"@rocket.chat/federation-sdk": "0.3.3", "@rocket.chat/federation-sdk": "0.3.5",
"@rocket.chat/http-router": "workspace:^", "@rocket.chat/http-router": "workspace:^",
"@rocket.chat/license": "workspace:^", "@rocket.chat/license": "workspace:^",
"@rocket.chat/models": "workspace:^", "@rocket.chat/models": "workspace:^",

View File

@ -8,7 +8,7 @@ import {
UserStatus, UserStatus,
} from '@rocket.chat/core-typings'; } from '@rocket.chat/core-typings';
import type { MessageQuoteAttachment, IMessage, IRoom, IUser, IRoomNativeFederated } from '@rocket.chat/core-typings'; import type { MessageQuoteAttachment, IMessage, IRoom, IUser, IRoomNativeFederated } from '@rocket.chat/core-typings';
import { eventIdSchema, roomIdSchema, userIdSchema, federationSDK } from '@rocket.chat/federation-sdk'; import { eventIdSchema, roomIdSchema, userIdSchema, federationSDK, FederationRequestError } from '@rocket.chat/federation-sdk';
import type { EventID, UserID, FileMessageType, PresenceState } from '@rocket.chat/federation-sdk'; import type { EventID, UserID, FileMessageType, PresenceState } from '@rocket.chat/federation-sdk';
import { Logger } from '@rocket.chat/logger'; import { Logger } from '@rocket.chat/logger';
import { Users, Subscriptions, Messages, Rooms, Settings } from '@rocket.chat/models'; import { Users, Subscriptions, Messages, Rooms, Settings } from '@rocket.chat/models';
@ -923,7 +923,15 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
await Room.performAcceptRoomInvite(room, subscription, user); await Room.performAcceptRoomInvite(room, subscription, user);
} }
if (action === 'reject') { if (action === 'reject') {
await federationSDK.rejectInvite(room.federation.mrid, matrixUserId); try {
await federationSDK.rejectInvite(room.federation.mrid, matrixUserId);
} catch (error) {
if (error instanceof FederationRequestError && error.response.status === 403) {
return Room.performUserRemoval(room, user);
}
this.logger.error(error, 'Failed to reject invite in Matrix');
throw error;
}
} }
} }
} }

View File

@ -1647,18 +1647,62 @@ import { SynapseClient } from '../helper/synapse-client';
rid = pendingInvitation?.rid!; rid = pendingInvitation?.rid!;
}, 15000); }, 15000);
it('It should allow RC user to reject the invite', async () => { it('should allow RC user to reject the invite and remove the subscription', async () => {
const rejectResponse = await rejectRoomInvite(rid, rc1AdminRequestConfig); const rejectResponse = await rejectRoomInvite(rid, rc1AdminRequestConfig);
expect(rejectResponse.success).toBe(true); expect(rejectResponse.success).toBe(true);
});
it('It should remove the subscription after rejection', async () => {
const subscriptions = await getSubscriptions(rc1AdminRequestConfig); const subscriptions = await getSubscriptions(rc1AdminRequestConfig);
const invitedSub = subscriptions.update.find((sub) => sub.fname?.includes(channelName)); const invitedSub = subscriptions.update.find((sub) => sub.fname?.includes(channelName));
expect(invitedSub).toBeFalsy(); expect(invitedSub).toBeFalsy();
}); });
}); });
describe('Revoked invitation flow from Synapse', () => {
describe('Synapse revokes an invitation before the RC user responds', () => {
let matrixRoomId: string;
let channelName: string;
let rid: string;
beforeAll(async () => {
channelName = `federated-channel-revoked-${Date.now()}`;
matrixRoomId = await hs1AdminApp.createRoom(channelName);
// hs1 invites RC user
await hs1AdminApp.matrixClient.invite(matrixRoomId, federationConfig.rc1.adminMatrixUserId);
const subscriptions = await getSubscriptions(rc1AdminRequestConfig);
const pendingInvitation = subscriptions.update.find(
(subscription) => subscription.status === 'INVITED' && subscription.fname?.includes(channelName),
);
expect(pendingInvitation).not.toBeUndefined();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
rid = pendingInvitation?.rid!;
// hs1 revokes the invitation by kicking the invited user
await hs1AdminApp.matrixClient.kick(matrixRoomId, federationConfig.rc1.adminMatrixUserId, 'Invitation revoked');
}, 15000);
it('should fail when RC user tries to accept the revoked invitation', async () => {
const acceptResponse = await acceptRoomInvite(rid, rc1AdminRequestConfig);
expect(acceptResponse.success).toBe(false);
});
it('should allow RC user to reject the revoked invitation and remove the subscription', async () => {
const rejectResponse = await rejectRoomInvite(rid, rc1AdminRequestConfig);
expect(rejectResponse.success).toBe(true);
const subscriptions = await getSubscriptions(rc1AdminRequestConfig);
const invitedSub = subscriptions.update.find((sub) => sub.fname?.includes(channelName));
expect(invitedSub).toBeFalsy();
});
it('should have the RC user with leave membership on Synapse side after revoked invitation', async () => {
const member = await hs1AdminApp.findRoomMember(channelName, federationConfig.rc1.adminMatrixUserId);
expect(member?.membership).toBe('leave');
});
});
});
}); });
}); });

View File

@ -17,7 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@rocket.chat/core-typings": "workspace:^", "@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/federation-sdk": "0.3.3", "@rocket.chat/federation-sdk": "0.3.5",
"@rocket.chat/http-router": "workspace:^", "@rocket.chat/http-router": "workspace:^",
"@rocket.chat/icons": "~0.45.0", "@rocket.chat/icons": "~0.45.0",
"@rocket.chat/media-signaling": "workspace:^", "@rocket.chat/media-signaling": "workspace:^",

View File

@ -8284,7 +8284,7 @@ __metadata:
"@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/apps-engine": "workspace:^"
"@rocket.chat/core-typings": "workspace:^" "@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/federation-sdk": "npm:0.3.3" "@rocket.chat/federation-sdk": "npm:0.3.5"
"@rocket.chat/http-router": "workspace:^" "@rocket.chat/http-router": "workspace:^"
"@rocket.chat/icons": "npm:~0.45.0" "@rocket.chat/icons": "npm:~0.45.0"
"@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/jest-presets": "workspace:~"
@ -8495,7 +8495,7 @@ __metadata:
"@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/ddp-client": "workspace:^"
"@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/emitter": "npm:^0.31.25"
"@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/federation-sdk": "npm:0.3.3" "@rocket.chat/federation-sdk": "npm:0.3.5"
"@rocket.chat/http-router": "workspace:^" "@rocket.chat/http-router": "workspace:^"
"@rocket.chat/license": "workspace:^" "@rocket.chat/license": "workspace:^"
"@rocket.chat/models": "workspace:^" "@rocket.chat/models": "workspace:^"
@ -8521,9 +8521,9 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@rocket.chat/federation-sdk@npm:0.3.3": "@rocket.chat/federation-sdk@npm:0.3.5":
version: 0.3.3 version: 0.3.5
resolution: "@rocket.chat/federation-sdk@npm:0.3.3" resolution: "@rocket.chat/federation-sdk@npm:0.3.5"
dependencies: dependencies:
"@datastructures-js/priority-queue": "npm:^6.3.5" "@datastructures-js/priority-queue": "npm:^6.3.5"
"@noble/ed25519": "npm:^3.0.0" "@noble/ed25519": "npm:^3.0.0"
@ -8536,7 +8536,7 @@ __metadata:
zod: "npm:^3.24.1" zod: "npm:^3.24.1"
peerDependencies: peerDependencies:
typescript: ~5.9.2 typescript: ~5.9.2
checksum: 10/e93f0d59da8508ee0ecfb53f6d599f93130ab4b9f651d87da77615355261e4852d6f4659aae9f4c8881cf4b26a628512e6363ecc407d6e01a31a27d20b9d7684 checksum: 10/47de2265555649b375620c7a90cf84134b14f3e38d171d84276dee9196b7b303a2d8c6f693223d63ba1c464ce12c128c5f6a795c0bb42c0e5339587b2429d034
languageName: node languageName: node
linkType: hard linkType: hard
@ -9179,7 +9179,7 @@ __metadata:
"@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/favicon": "workspace:^" "@rocket.chat/favicon": "workspace:^"
"@rocket.chat/federation-matrix": "workspace:^" "@rocket.chat/federation-matrix": "workspace:^"
"@rocket.chat/federation-sdk": "npm:0.3.3" "@rocket.chat/federation-sdk": "npm:0.3.5"
"@rocket.chat/freeswitch": "workspace:^" "@rocket.chat/freeswitch": "workspace:^"
"@rocket.chat/fuselage": "npm:^0.69.0" "@rocket.chat/fuselage": "npm:^0.69.0"
"@rocket.chat/fuselage-forms": "npm:~0.1.1" "@rocket.chat/fuselage-forms": "npm:~0.1.1"