Reset expire timer version after unlink
This commit is contained in:
parent
14d098f40f
commit
07a938ec98
5 changed files with 137 additions and 26 deletions
|
@ -193,6 +193,7 @@ import { cleanupMessages } from '../util/cleanup';
|
||||||
import { MessageModel } from './messages';
|
import { MessageModel } from './messages';
|
||||||
import { applyNewAvatar } from '../groups';
|
import { applyNewAvatar } from '../groups';
|
||||||
import { safeSetTimeout } from '../util/timeout';
|
import { safeSetTimeout } from '../util/timeout';
|
||||||
|
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -313,7 +314,7 @@ export class ConversationModel extends window.Backbone
|
||||||
verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||||
messageCount: 0,
|
messageCount: 0,
|
||||||
sentMessageCount: 0,
|
sentMessageCount: 0,
|
||||||
expireTimerVersion: 1,
|
expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4667,6 +4668,21 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is the initial sync, we want to use the provided expire timer & version and
|
||||||
|
// disregard our local version. We might be re-linking after the primary has
|
||||||
|
// re-registered and their expireTimerVersion may have been reset, but we don't want
|
||||||
|
// to ignore it; our local version is out of date.
|
||||||
|
if (
|
||||||
|
isInitialSync &&
|
||||||
|
this.get('expireTimerVersion') !== INITIAL_EXPIRE_TIMER_VERSION
|
||||||
|
) {
|
||||||
|
log.warn(
|
||||||
|
'updateExpirationTimer: Resetting expireTimerVersion since this is initialSync'
|
||||||
|
);
|
||||||
|
// This is reset after unlink, but we do it here as well to recover from errors
|
||||||
|
this.set('expireTimerVersion', INITIAL_EXPIRE_TIMER_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
let expireTimer: DurationInSeconds | undefined = providedExpireTimer;
|
let expireTimer: DurationInSeconds | undefined = providedExpireTimer;
|
||||||
let source = providedSource;
|
let source = providedSource;
|
||||||
if (this.get('left')) {
|
if (this.get('left')) {
|
||||||
|
@ -4687,7 +4703,7 @@ export class ConversationModel extends window.Backbone
|
||||||
`updateExpirationTimer(${this.idForLogging()}, ` +
|
`updateExpirationTimer(${this.idForLogging()}, ` +
|
||||||
`${expireTimer || 'disabled'}, version=${version || 0}) ` +
|
`${expireTimer || 'disabled'}, version=${version || 0}) ` +
|
||||||
`source=${source ?? '?'} localValue=${this.get('expireTimer')} ` +
|
`source=${source ?? '?'} localValue=${this.get('expireTimer')} ` +
|
||||||
`localVersion=${localVersion}, reason=${reason}`;
|
`localVersion=${localVersion}, reason=${reason}, isInitialSync=${isInitialSync}`;
|
||||||
|
|
||||||
if (isSetByOther) {
|
if (isSetByOther) {
|
||||||
if (version) {
|
if (version) {
|
||||||
|
|
|
@ -212,6 +212,7 @@ import {
|
||||||
replaceAllEndorsementsForGroup,
|
replaceAllEndorsementsForGroup,
|
||||||
} from './server/groupSendEndorsements';
|
} from './server/groupSendEndorsements';
|
||||||
import type { GifType } from '../components/fun/panels/FunPanelGifs';
|
import type { GifType } from '../components/fun/panels/FunPanelGifs';
|
||||||
|
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer';
|
||||||
|
|
||||||
type ConversationRow = Readonly<{
|
type ConversationRow = Readonly<{
|
||||||
json: string;
|
json: string;
|
||||||
|
@ -1663,7 +1664,9 @@ function saveConversation(db: WritableDB, data: ConversationType): void {
|
||||||
`
|
`
|
||||||
).run({
|
).run({
|
||||||
id,
|
id,
|
||||||
json: objectToJSON(omit(data, ['profileLastFetchedAt'])),
|
json: objectToJSON(
|
||||||
|
omit(data, ['profileLastFetchedAt', 'expireTimerVersion'])
|
||||||
|
),
|
||||||
|
|
||||||
e164: e164 || null,
|
e164: e164 || null,
|
||||||
serviceId: serviceId || null,
|
serviceId: serviceId || null,
|
||||||
|
@ -1799,14 +1802,20 @@ function getConversationById(
|
||||||
id: string
|
id: string
|
||||||
): ConversationType | undefined {
|
): ConversationType | undefined {
|
||||||
const row = db
|
const row = db
|
||||||
.prepare('SELECT json FROM conversations WHERE id = $id;')
|
.prepare(
|
||||||
.get<{ json: string }>({ id });
|
`
|
||||||
|
SELECT json, profileLastFetchedAt, expireTimerVersion
|
||||||
|
FROM conversations
|
||||||
|
WHERE id = $id
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get<ConversationRow>({ id });
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonToObject(row.json);
|
return rowToConversation(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllConversations(db: ReadableDB): Array<ConversationType> {
|
function getAllConversations(db: ReadableDB): Array<ConversationType> {
|
||||||
|
@ -6952,19 +6961,25 @@ function removeAllConfiguration(db: WritableDB): void {
|
||||||
|
|
||||||
db.exec(
|
db.exec(
|
||||||
`
|
`
|
||||||
|
UPDATE storyDistributions SET senderKeyInfoJson = NULL;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Update conversations */
|
||||||
|
const [updateConversationsQuery, updateConversationsParams] = sql`
|
||||||
UPDATE conversations
|
UPDATE conversations
|
||||||
SET
|
SET
|
||||||
|
expireTimerVersion = ${INITIAL_EXPIRE_TIMER_VERSION},
|
||||||
json = json_remove(
|
json = json_remove(
|
||||||
json,
|
json,
|
||||||
'$.senderKeyInfo',
|
'$.senderKeyInfo',
|
||||||
'$.storageID',
|
'$.storageID',
|
||||||
'$.needsStorageServiceSync',
|
'$.needsStorageServiceSync',
|
||||||
'$.storageUnknownFields'
|
'$.storageUnknownFields',
|
||||||
|
'$.expireTimerVersion'
|
||||||
);
|
);
|
||||||
|
`;
|
||||||
UPDATE storyDistributions SET senderKeyInfoJson = NULL;
|
db.prepare(updateConversationsQuery).run(updateConversationsParams);
|
||||||
`
|
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { SendStatus } from '../../messages/MessageSendState';
|
||||||
import { IMAGE_PNG } from '../../types/MIME';
|
import { IMAGE_PNG } from '../../types/MIME';
|
||||||
import { generateAci, generatePni } from '../../types/ServiceId';
|
import { generateAci, generatePni } from '../../types/ServiceId';
|
||||||
import { MessageModel } from '../../models/messages';
|
import { MessageModel } from '../../models/messages';
|
||||||
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
|
|
||||||
describe('Conversations', () => {
|
describe('Conversations', () => {
|
||||||
async function resetConversationController(): Promise<void> {
|
async function resetConversationController(): Promise<void> {
|
||||||
|
@ -16,15 +17,20 @@ describe('Conversations', () => {
|
||||||
await window.ConversationController.load();
|
await window.ConversationController.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(resetConversationController);
|
beforeEach(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
afterEach(resetConversationController);
|
await window.textsecure.storage.user.setCredentials({
|
||||||
|
number: '+15550000000',
|
||||||
|
aci: generateAci(),
|
||||||
|
pni: generatePni(),
|
||||||
|
deviceId: 2,
|
||||||
|
deviceName: 'my device',
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
await resetConversationController();
|
||||||
|
});
|
||||||
|
|
||||||
it('updates lastMessage even in race conditions with db', async () => {
|
it('updates lastMessage even in race conditions with db', async () => {
|
||||||
const ourNumber = '+15550000000';
|
|
||||||
const ourAci = generateAci();
|
|
||||||
const ourPni = generatePni();
|
|
||||||
|
|
||||||
// Creating a fake conversation
|
// Creating a fake conversation
|
||||||
const conversation = new window.Whisper.Conversation({
|
const conversation = new window.Whisper.Conversation({
|
||||||
avatars: [],
|
avatars: [],
|
||||||
|
@ -44,14 +50,6 @@ describe('Conversations', () => {
|
||||||
lastMessage: 'starting value',
|
lastMessage: 'starting value',
|
||||||
});
|
});
|
||||||
|
|
||||||
await window.textsecure.storage.user.setCredentials({
|
|
||||||
number: ourNumber,
|
|
||||||
aci: ourAci,
|
|
||||||
pni: ourPni,
|
|
||||||
deviceId: 2,
|
|
||||||
deviceName: 'my device',
|
|
||||||
password: 'password',
|
|
||||||
});
|
|
||||||
await window.ConversationController.load();
|
await window.ConversationController.load();
|
||||||
await window.ConversationController.getOrCreateAndWait(
|
await window.ConversationController.getOrCreateAndWait(
|
||||||
conversation.attributes.e164 ?? null,
|
conversation.attributes.e164 ?? null,
|
||||||
|
@ -163,4 +161,39 @@ describe('Conversations', () => {
|
||||||
assert.equal(resultWithImage.contentType, 'image/png');
|
assert.equal(resultWithImage.contentType, 'image/png');
|
||||||
assert.equal(resultWithImage.fileName, null);
|
assert.equal(resultWithImage.fileName, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateExpirationTimer', () => {
|
||||||
|
it('always updates if `isInitialSync` is true', async () => {
|
||||||
|
const conversation =
|
||||||
|
await window.ConversationController.getOrCreateAndWait(
|
||||||
|
generateUuid(),
|
||||||
|
'private',
|
||||||
|
{
|
||||||
|
expireTimerVersion: 42,
|
||||||
|
expireTimer: DurationInSeconds.WEEK,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Without isInitialSync, ignores
|
||||||
|
await conversation.updateExpirationTimer(DurationInSeconds.DAY, {
|
||||||
|
reason: 'test',
|
||||||
|
source: 'test',
|
||||||
|
version: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(conversation.getExpireTimerVersion(), 42);
|
||||||
|
assert.equal(conversation.get('expireTimer'), DurationInSeconds.WEEK);
|
||||||
|
|
||||||
|
// With isInitialSync, overwrites
|
||||||
|
await conversation.updateExpirationTimer(DurationInSeconds.DAY, {
|
||||||
|
reason: 'test',
|
||||||
|
source: 'test',
|
||||||
|
version: 3,
|
||||||
|
isInitialSync: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(conversation.getExpireTimerVersion(), 3);
|
||||||
|
assert.equal(conversation.get('expireTimer'), DurationInSeconds.DAY);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
45
ts/test-electron/sql/removeAllConfiguration_test.ts
Normal file
45
ts/test-electron/sql/removeAllConfiguration_test.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { v4 as generateGuid } from 'uuid';
|
||||||
|
|
||||||
|
import { DataWriter, DataReader } from '../../sql/Client';
|
||||||
|
|
||||||
|
describe('Remove all configuration test', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await DataWriter.removeAll();
|
||||||
|
});
|
||||||
|
it('Removes conversation-specific configuration', async () => {
|
||||||
|
const { attributes } =
|
||||||
|
await window.ConversationController.getOrCreateAndWait(
|
||||||
|
generateGuid(),
|
||||||
|
'private',
|
||||||
|
{
|
||||||
|
expireTimerVersion: 3,
|
||||||
|
senderKeyInfo: {
|
||||||
|
createdAtDate: Date.now(),
|
||||||
|
distributionId: generateGuid(),
|
||||||
|
memberDevices: [],
|
||||||
|
},
|
||||||
|
storageID: 'storageId',
|
||||||
|
needsStorageServiceSync: true,
|
||||||
|
storageUnknownFields: 'base64==',
|
||||||
|
name: 'Name (and all other fields) should be preserved',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await DataWriter.removeAllConfiguration();
|
||||||
|
|
||||||
|
const convoAfter = await DataReader.getConversationById(attributes.id);
|
||||||
|
assert.strictEqual(convoAfter?.expireTimerVersion, 1);
|
||||||
|
assert.isUndefined(convoAfter?.storageID);
|
||||||
|
assert.isUndefined(convoAfter?.needsStorageServiceSync);
|
||||||
|
assert.isUndefined(convoAfter?.storageUnknownFields);
|
||||||
|
assert.isUndefined(convoAfter?.senderKeyInfo);
|
||||||
|
assert.strictEqual(
|
||||||
|
convoAfter?.name,
|
||||||
|
'Name (and all other fields) should be preserved'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,6 +7,8 @@ import { isNumber } from 'lodash';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { SECOND, DurationInSeconds } from './durations';
|
import { SECOND, DurationInSeconds } from './durations';
|
||||||
|
|
||||||
|
export const INITIAL_EXPIRE_TIMER_VERSION = 1;
|
||||||
|
|
||||||
const SECONDS_PER_WEEK = 604800;
|
const SECONDS_PER_WEEK = 604800;
|
||||||
export const DEFAULT_DURATIONS_IN_SECONDS: ReadonlyArray<DurationInSeconds> = [
|
export const DEFAULT_DURATIONS_IN_SECONDS: ReadonlyArray<DurationInSeconds> = [
|
||||||
DurationInSeconds.ZERO,
|
DurationInSeconds.ZERO,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue