Refresh group state if missing group members
This commit is contained in:
parent
872f1bd843
commit
40ac288a3a
3 changed files with 73 additions and 48 deletions
|
@ -98,7 +98,7 @@ import { sleep } from './util/sleep';
|
||||||
import { groupInvitesRoute } from './util/signalRoutes';
|
import { groupInvitesRoute } from './util/signalRoutes';
|
||||||
import {
|
import {
|
||||||
decodeGroupSendEndorsementResponse,
|
decodeGroupSendEndorsementResponse,
|
||||||
isValidGroupSendEndorsementsExpiration,
|
validateGroupSendEndorsementsExpiration,
|
||||||
} from './util/groupSendEndorsements';
|
} from './util/groupSendEndorsements';
|
||||||
import { getProfile } from './util/getProfile';
|
import { getProfile } from './util/getProfile';
|
||||||
import { generateMessageId } from './util/generateMessageId';
|
import { generateMessageId } from './util/generateMessageId';
|
||||||
|
@ -3992,7 +3992,7 @@ async function updateGroupViaLogs({
|
||||||
await DataReader.getGroupSendCombinedEndorsementExpiration(groupId);
|
await DataReader.getGroupSendCombinedEndorsementExpiration(groupId);
|
||||||
|
|
||||||
if (cachedEndorsementsExpiration != null) {
|
if (cachedEndorsementsExpiration != null) {
|
||||||
const result = isValidGroupSendEndorsementsExpiration(
|
const result = validateGroupSendEndorsementsExpiration(
|
||||||
cachedEndorsementsExpiration * 1000
|
cachedEndorsementsExpiration * 1000
|
||||||
);
|
);
|
||||||
if (!result.valid) {
|
if (!result.valid) {
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { isValidGroupSendEndorsementsExpiration } from '../../util/groupSendEndorsements';
|
import { validateGroupSendEndorsementsExpiration } from '../../util/groupSendEndorsements';
|
||||||
import { DAY, HOUR, SECOND } from '../../util/durations';
|
import { DAY, HOUR, SECOND } from '../../util/durations';
|
||||||
|
|
||||||
describe('groupSendEndorsements', () => {
|
describe('groupSendEndorsements', () => {
|
||||||
describe('isValidGroupSendEndorsementsExpiration', () => {
|
describe('validateGroupSendEndorsementsExpiration', () => {
|
||||||
function validateDistance(distance: number) {
|
function validateDistance(distance: number) {
|
||||||
const expiration = Date.now() + distance;
|
const expiration = Date.now() + distance;
|
||||||
return isValidGroupSendEndorsementsExpiration(expiration);
|
return validateGroupSendEndorsementsExpiration(expiration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkValid(label: string, distance: number) {
|
function checkValid(label: string, distance: number) {
|
||||||
|
|
|
@ -129,13 +129,13 @@ function logServiceIds(list: Iterable<string>) {
|
||||||
return `${items.slice(0, 4).join(', ')}, and ${items.length - 4} others`;
|
return `${items.slice(0, 4).join(', ')}, and ${items.length - 4} others`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EndorsementsExpirationValidationResult =
|
export type ValidationResult =
|
||||||
| { valid: true; reason?: never }
|
| { valid: true; reason?: never }
|
||||||
| { valid: false; reason: string };
|
| { valid: false; reason: string };
|
||||||
|
|
||||||
export function isValidGroupSendEndorsementsExpiration(
|
export function validateGroupSendEndorsementsExpiration(
|
||||||
expiration: number
|
expiration: number
|
||||||
): EndorsementsExpirationValidationResult {
|
): ValidationResult {
|
||||||
const expSeconds = DurationInSeconds.fromMillis(expiration);
|
const expSeconds = DurationInSeconds.fromMillis(expiration);
|
||||||
const nowSeconds = DurationInSeconds.fromMillis(Date.now());
|
const nowSeconds = DurationInSeconds.fromMillis(Date.now());
|
||||||
const info = `now: ${nowSeconds}, exp: ${expSeconds}`;
|
const info = `now: ${nowSeconds}, exp: ${expSeconds}`;
|
||||||
|
@ -202,10 +202,13 @@ export class GroupSendEndorsementState {
|
||||||
);
|
);
|
||||||
|
|
||||||
const expiration = this.getExpiration();
|
const expiration = this.getExpiration();
|
||||||
|
const result = validateGroupSendEndorsementsExpiration(
|
||||||
|
expiration.getTime()
|
||||||
|
);
|
||||||
|
|
||||||
strictAssert(
|
strictAssert(
|
||||||
isValidGroupSendEndorsementsExpiration(expiration.getTime()),
|
result.valid,
|
||||||
`${this.#logId}: toToken: Cannot build token with invalid expiration`
|
`${this.#logId}: toToken: Endorsements are expired (${result.reason})`
|
||||||
);
|
);
|
||||||
|
|
||||||
const fullToken = endorsement.toFullToken(groupSecretParams, expiration);
|
const fullToken = endorsement.toFullToken(groupSecretParams, expiration);
|
||||||
|
@ -341,69 +344,91 @@ type MaybeCreateGroupSendEndorsementStateResult =
|
||||||
| { state: GroupSendEndorsementState; didRefreshGroupState: false }
|
| { state: GroupSendEndorsementState; didRefreshGroupState: false }
|
||||||
| { state: null; didRefreshGroupState: boolean };
|
| { state: null; didRefreshGroupState: boolean };
|
||||||
|
|
||||||
|
function validateGroupSendEndorsements(
|
||||||
|
members: Set<ServiceIdString>,
|
||||||
|
state: GroupSendEndorsementState | null
|
||||||
|
): ValidationResult {
|
||||||
|
// Check if we should have endorsements (pending members should not)
|
||||||
|
if (state == null) {
|
||||||
|
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||||
|
if (members.has(ourAci)) {
|
||||||
|
return { valid: false, reason: 'missing all endorsements' };
|
||||||
|
}
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for expired endorsements
|
||||||
|
const expiration = state.getExpiration().getTime();
|
||||||
|
const expirationResult = validateGroupSendEndorsementsExpiration(expiration);
|
||||||
|
if (!expirationResult.valid) {
|
||||||
|
return { valid: false, reason: expirationResult.reason };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for missing endorsements
|
||||||
|
const missing = new Set<ServiceIdString>();
|
||||||
|
for (const member of members) {
|
||||||
|
if (!state.hasMember(member)) {
|
||||||
|
missing.add(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missing.size > 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `missing ${missing.size} endorsements (${logServiceIds(missing)})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
export async function maybeCreateGroupSendEndorsementState(
|
export async function maybeCreateGroupSendEndorsementState(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
alreadyRefreshedGroupState: boolean
|
alreadyRefreshedGroupState: boolean
|
||||||
): Promise<MaybeCreateGroupSendEndorsementStateResult> {
|
): Promise<MaybeCreateGroupSendEndorsementStateResult> {
|
||||||
|
const logId = `maybeCreateGroupSendEndorsementState/groupv2(${groupId})`;
|
||||||
|
|
||||||
const conversation = window.ConversationController.get(groupId);
|
const conversation = window.ConversationController.get(groupId);
|
||||||
strictAssert(
|
strictAssert(conversation != null, `${logId}: Convertion not found`);
|
||||||
conversation != null,
|
|
||||||
'maybeCreateGroupSendEndorsementState: Convertion not found'
|
|
||||||
);
|
|
||||||
|
|
||||||
const logId = `maybeCreateGroupSendEndorsementState/${conversation.idForLogging()}`;
|
|
||||||
|
|
||||||
strictAssert(
|
strictAssert(
|
||||||
isGroupV2(conversation.attributes),
|
isGroupV2(conversation.attributes),
|
||||||
`${logId}: Conversation is not groupV2`
|
`${logId}: Conversation is not groupV2`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const secretParams = conversation.get('secretParams');
|
||||||
|
const membersV2 = conversation.get('membersV2');
|
||||||
|
strictAssert(secretParams, `${logId}: Must have secret params`);
|
||||||
|
strictAssert(membersV2, `${logId}: Must have members`);
|
||||||
|
const members = new Set(membersV2.map(member => member.aci));
|
||||||
|
|
||||||
const data = await DataReader.getGroupSendEndorsementsData(groupId);
|
const data = await DataReader.getGroupSendEndorsementsData(groupId);
|
||||||
if (data == null) {
|
const state =
|
||||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
data != null ? new GroupSendEndorsementState(data, secretParams) : null;
|
||||||
if (conversation.isMember(ourAci)) {
|
|
||||||
if (!alreadyRefreshedGroupState) {
|
|
||||||
log.info(`${logId}: Missing endorsements for group, refreshing group`);
|
|
||||||
await window.waitForEmptyEventQueue();
|
|
||||||
await maybeUpdateGroup({ conversation, force: true });
|
|
||||||
return { state: null, didRefreshGroupState: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
onFailedToSendWithEndorsements(
|
|
||||||
new Error(`${logId}: Missing all endorsements for group`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { state: null, didRefreshGroupState: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupSecretParamsBase64 = conversation.get('secretParams');
|
|
||||||
strictAssert(groupSecretParamsBase64, `${logId}: Must have secret params`);
|
|
||||||
|
|
||||||
const groupSendEndorsementState = new GroupSendEndorsementState(
|
|
||||||
data,
|
|
||||||
groupSecretParamsBase64
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = isValidGroupSendEndorsementsExpiration(
|
|
||||||
groupSendEndorsementState.getExpiration().getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Check if we need to refresh the group state.
|
||||||
|
const result = validateGroupSendEndorsements(members, state);
|
||||||
if (!result.valid) {
|
if (!result.valid) {
|
||||||
|
// If we've already refreshed the group state, we should log and move on.
|
||||||
if (alreadyRefreshedGroupState) {
|
if (alreadyRefreshedGroupState) {
|
||||||
onFailedToSendWithEndorsements(
|
onFailedToSendWithEndorsements(
|
||||||
new Error(
|
new Error(
|
||||||
`${logId}: Endorsements are expired after refreshing group (${result.reason})`
|
`${logId}: Endorsements invalid after refreshing group: ${result.reason}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return { state: null, didRefreshGroupState: false };
|
return { state: null, didRefreshGroupState: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: Endorsements are expired (${result.reason}), refreshing group`
|
`${logId}: Endorsements invalid, refreshing group: ${result.reason}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wait for all incoming messages to be processed before refreshing
|
||||||
|
// the group state to avoid incorrectly processing messages.
|
||||||
await window.waitForEmptyEventQueue();
|
await window.waitForEmptyEventQueue();
|
||||||
|
|
||||||
|
// Refresh the group state all allow the caller to try again.
|
||||||
await maybeUpdateGroup({ conversation, force: true });
|
await maybeUpdateGroup({ conversation, force: true });
|
||||||
return { state: null, didRefreshGroupState: true };
|
return { state: null, didRefreshGroupState: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { state: groupSendEndorsementState, didRefreshGroupState: false };
|
return { state, didRefreshGroupState: false };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue