-
Notifications
You must be signed in to change notification settings - Fork 21
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
feat: establish 1on1 mls conversation with new removal key [WPB-10744] #6502
Conversation
/** | ||
* Short term solution to an issues with 1:1 MLS call, see | ||
* https://wearezeta.atlassian.net/wiki/spaces/PAD/pages/1314750477/2024-07-29+1+1+calls+over+SFT#Do-not-leave-the-subconversation | ||
* Will delete a conference subconversation when hanging up on a 1:1 call instead of leaving. | ||
* | ||
* @param conversationId Id of the parent conversation which subconversation we want to leave | ||
*/ | ||
public async leave1on1ConferenceSubconversation(conversationId: QualifiedId): Promise<void> { | ||
const subconversationGroupId = await this.getSubconversationGroupId(conversationId, SUBCONVERSATION_ID.CONFERENCE); | ||
|
||
if (!subconversationGroupId) { | ||
return; | ||
} | ||
|
||
const doesGroupExistLocally = await this.mlsService.conversationExists(subconversationGroupId); | ||
if (!doesGroupExistLocally) { | ||
// If the subconversation was known by a client but is does not exist locally, we can remove it from the store. | ||
return this.clearSubconversationGroupId(conversationId, SUBCONVERSATION_ID.CONFERENCE); | ||
} | ||
|
||
try { | ||
const epochInfo = await this.getSubconversationEpochInfo(conversationId, subconversationGroupId); | ||
|
||
if (!epochInfo) { | ||
throw new Error('Failed to get epoch info for conference subconversation'); | ||
} | ||
await this.apiClient.api.conversation.deleteSubconversation(conversationId, SUBCONVERSATION_ID.CONFERENCE, { | ||
groupId: subconversationGroupId, | ||
epoch: epochInfo.epoch, | ||
}); | ||
} catch (error) { | ||
this.logger.error(`Failed to delete conference subconversation:`, error); | ||
} | ||
|
||
await this.mlsService.wipeConversation(subconversationGroupId); | ||
|
||
// once we've deleted the subconversation, we can remove it from the store | ||
await this.clearSubconversationGroupId(conversationId, SUBCONVERSATION_ID.CONFERENCE); | ||
} | ||
|
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.
This code block was a previous attempt to solve a related issue without requiring a change to the backend response, by calling the deleteSubconversation
endpoint instead of deleteSubconversationSelf
.
see #6434
This is no longer required
packages/core/src/messagingProtocols/mls/MLSService/MLSService.ts
Outdated
Show resolved
Hide resolved
} | ||
|
||
// If group is not established on backend, | ||
// we wipe the it locally (in case it exsits in the local store) and try to register it. | ||
await this.mlsService.wipeConversation(groupId); | ||
|
||
try { | ||
await this.mlsService.register1to1Conversation(groupId, otherUserId, selfUser); | ||
await this.mlsService.register1to1Conversation(groupId, otherUserId, selfUser, public_keys?.removal); |
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.
This is the main change, we can register a conversation with a removal key that comes from a different endpoint
@@ -481,7 +485,8 @@ export class MLSService extends TypedEventEmitter<Events> { | |||
} else { | |||
const mlsKeys = (await this.apiClient.api.client.getPublicKeys()).removal; | |||
const ciphersuiteSignature = getSignatureAlgorithmForCiphersuite(this.config.defaultCiphersuite); | |||
const removalKeyForSignature = mlsKeys[ciphersuiteSignature]; | |||
const removalKeyForSignature = | |||
removalKeyFor1to1Signature?.[ciphersuiteSignature] ?? mlsKeys[ciphersuiteSignature]; |
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.
if the removal key is provided by the conversation endpoint, we use that one, else, we use a removal key provided by getPublicKey (our own backend removal key)
if (isMLS1to1Conversation(response)) { | ||
return response; | ||
} | ||
return {conversation: response}; |
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.
Can't we return response.conversation and response? 🤔
Code will be more readable.
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.
And nice to have passed type for fn what we returning there ;)
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.
Can't we return response.conversation and response? 🤔 Code will be more readable.
This function returns
export interface MLS1to1Conversation {
conversation: MLSConversation;
public_keys?: {
removal: MLSPublicKeyRecord;
};
}
the optional public_keys
is needed in some cases so we can't only return MLSConversation
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.
Co-authored-by: Przemysław Jóźwik <[email protected]>
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.
Added few suggestions.
if (typeof response === 'object' && response !== null) { | ||
const conversation = response as MLS1to1Conversation; | ||
|
||
if (!!conversation.conversation && !!conversation.public_keys) { | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
return false; |
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.
It is possible to normalize the logic?
Can we return early with false value?
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.
Something like this?
export function isMLS1to1Conversation(response: unknown): response is MLS1to1Conversation {
if (typeof response === 'object' && response !== null && 'conversation' in response && 'public_keys' in response) {
return true;
}
return false;
}
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.
Yes, but I think in
is an alternative for hasOwnProperty()
.
If you are sure that these property cannot contain null/undefined, then we are good to go.
// for example
const response = {conversation: null, public_keys: undefined};
// 'conversation' in response will be true
// 'public_keys' in response will be true
packages/core/src/conversation/ConversationService/ConversationService.ts
Outdated
Show resolved
Hide resolved
…nService.ts Co-authored-by: Immad Abdul Jabbar <[email protected]>
@@ -580,19 +587,19 @@ export class ConversationService extends TypedEventEmitter<Events> { | |||
); | |||
|
|||
await this.joinByExternalCommit(mlsConversation.qualified_id); | |||
return this.getMLS1to1Conversation(otherUserId); | |||
return (await this.getMLS1to1Conversation(otherUserId)).conversation; |
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.
Can we go with smth like this? Code will look more readable :)
return (await this.getMLS1to1Conversation(otherUserId)).conversation; | |
const updatedMLSConversation = await this.getMLS1to1Conversation(otherUserId); | |
return updatedMLSConversation.conversation; |
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.
I went with this, WDYT?
const {conversation: updatedMLSConversation} = await this.getMLS1to1Conversation(otherUserId);
return updatedMLSConversation;
Quality Gate passedIssues Measures |
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.
Nice!
#6502) * feat: establish 1on1 mls conversation with new removal key [10744] * remove federated SFT 1:1 call temporary fix * type data structure for 1:1 MLS conversations * remove public_keys from NewConversation type * use nullish coalescing instead of ternary for removalKey logic * implement typeguard for the specific MLS1to1Conversation response * use union types for api-client getMLS1to1Conversation return value * use type guard and return MLS1to1Conversation for getMLS1to1Conversation in core * address tests * Update packages/api-client/src/conversation/Conversation.ts Co-authored-by: Przemysław Jóźwik <[email protected]> * rename variable in type guard * Update packages/core/src/conversation/ConversationService/ConversationService.ts Co-authored-by: Immad Abdul Jabbar <[email protected]> * address CR issue * simplify typeguard's logic * address CR --------- Co-authored-by: Przemysław Jóźwik <[email protected]> Co-authored-by: Immad Abdul Jabbar <[email protected]>
Description
See https://wearezeta.atlassian.net/browse/WPB-10737 for a more in depth description of the issue.
In a nutshell, it's possible for a federated user to create a 1:1 conversation even if it's not hosted on their own backend, in which case it can introduce a host of issues because they're lacking the correct removal key.
to address that issue, the response from the endpoint
GET /conversations/one2one/{domain}/{userId}
was changed to include that removal key, fromto
Checklist