-
Notifications
You must be signed in to change notification settings - Fork 13k
fix(federation): handle "disinvite" and duplicated messages #37791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1685260
270e539
ed6c493
01f9f99
266f381
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1647,18 +1647,62 @@ import { SynapseClient } from '../helper/synapse-client'; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| rid = pendingInvitation?.rid!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(rejectResponse.success).toBe(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('It should remove the subscription after rejection', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const subscriptions = await getSubscriptions(rc1AdminRequestConfig); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const invitedSub = subscriptions.update.find((sub) => sub.fname?.includes(channelName)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(invitedSub).toBeFalsy(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1650
to
1657
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid flakiness: wait/poll for subscription removal instead of asserting immediately. it('should allow RC user to reject the invite 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();
+ const deadline = Date.now() + 10_000;
+ while (true) {
+ const subscriptions = await getSubscriptions(rc1AdminRequestConfig);
+ const invitedSub = subscriptions.update.find((sub) => sub.fname?.includes(channelName));
+ if (!invitedSub) break;
+ if (Date.now() > deadline) throw new Error('Timed out waiting for invited subscription removal');
+ await new Promise((r) => setTimeout(r, 250));
+ }
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
@rocket.chat/federation-sdk v0.3.4 FederationRequestError export response status💡 Result:
I don’t see the v0.3.4 source online right now. In practice, FederationRequestError objects usually expose the HTTP status in one of these places — try checking in this order:
If that doesn’t work, log the error keys (console.log(Object.keys(err)) or console.dir(err, { depth: null })) and share the output and/or the thrown error stack and I’ll look up the SDK source (or fetch v0.3.4 on npm/GitHub) and give exact accessors.
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 119
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 93
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 48
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 733
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 169
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 1805
🏁 Script executed:
# Check the imports in FederationMatrix.ts head -30 ee/packages/federation-matrix/src/FederationMatrix.tsRepository: RocketChat/Rocket.Chat
Length of output: 1321
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 193
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 48
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 48
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 48
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 3444
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 1024
Use defensive chaining for
error.response.statusto match codebase patterns: Line 929 accesseserror.response.statusdirectly without a null/undefined check, while similar error handling elsewhere in the codebase (webdav, webdavClientAdapter) uses optional chaining (error.response?.statusText). Add a guard (error.response?.status === 403) or validate thaterror.responseexists before accessingstatus.🤖 Prompt for AI Agents