Introduce versioning clock to timer system
This commit is contained in:
parent
bb1d957e49
commit
2fb50df0af
34 changed files with 703 additions and 28 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -128,7 +128,7 @@
|
|||
"@indutny/parallel-prettier": "3.0.0",
|
||||
"@indutny/rezip-electron": "1.3.1",
|
||||
"@indutny/symbolicate-mac": "2.3.0",
|
||||
"@signalapp/mock-server": "6.8.1",
|
||||
"@signalapp/mock-server": "6.9.0",
|
||||
"@storybook/addon-a11y": "8.1.11",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-controls": "8.1.11",
|
||||
|
@ -7253,9 +7253,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@signalapp/mock-server": {
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-6.8.1.tgz",
|
||||
"integrity": "sha512-RYAaNoCMuIPoMTAuvgEwMh8D12pdvpjOA/qfEOnwXRRLJU1XWXKuAHBc0uJ7deZWLM6qbC/egST/hXImLcsV7Q==",
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-6.9.0.tgz",
|
||||
"integrity": "sha512-NXiroPSMvJzfIjrj7+RJgF5v3RH4UTg7pAUCt7cghdITxuZ0SqpcJ5Od3cbuWnbSHUzlMFeaujBrKcQ5P8Fn8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@signalapp/libsignal-client": "^0.45.0",
|
||||
|
|
|
@ -211,7 +211,7 @@
|
|||
"@indutny/parallel-prettier": "3.0.0",
|
||||
"@indutny/rezip-electron": "1.3.1",
|
||||
"@indutny/symbolicate-mac": "2.3.0",
|
||||
"@signalapp/mock-server": "6.8.1",
|
||||
"@signalapp/mock-server": "6.9.0",
|
||||
"@storybook/addon-a11y": "8.1.11",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-controls": "8.1.11",
|
||||
|
|
|
@ -352,6 +352,7 @@ message DataMessage {
|
|||
optional GroupContextV2 groupV2 = 15;
|
||||
optional uint32 flags = 4;
|
||||
optional uint32 expireTimer = 5;
|
||||
optional uint32 expireTimerVersion = 23;
|
||||
optional bytes profileKey = 6;
|
||||
optional uint64 timestamp = 7;
|
||||
optional Quote quote = 8;
|
||||
|
@ -778,6 +779,7 @@ message ContactDetails {
|
|||
// reserved 6; // formerly profileKey
|
||||
// reserved 7; // formerly blocked
|
||||
optional uint32 expireTimer = 8;
|
||||
optional uint32 expireTimerVersion = 12;
|
||||
optional uint32 inboxPosition = 10;
|
||||
}
|
||||
|
||||
|
|
|
@ -1070,6 +1070,23 @@ export class ConversationController {
|
|||
}
|
||||
current.set('active_at', activeAt);
|
||||
|
||||
current.set(
|
||||
'expireTimerVersion',
|
||||
Math.max(
|
||||
obsolete.get('expireTimerVersion') ?? 1,
|
||||
current.get('expireTimerVersion') ?? 1
|
||||
)
|
||||
);
|
||||
|
||||
const obsoleteExpireTimer = obsolete.get('expireTimer');
|
||||
const currentExpireTimer = current.get('expireTimer');
|
||||
if (
|
||||
!currentExpireTimer ||
|
||||
(obsoleteExpireTimer && obsoleteExpireTimer < currentExpireTimer)
|
||||
) {
|
||||
current.set('expireTimer', obsoleteExpireTimer);
|
||||
}
|
||||
|
||||
const currentHadMessages = (current.get('messageCount') ?? 0) > 0;
|
||||
|
||||
const dataToCopy: Partial<ConversationAttributesType> = pick(
|
||||
|
|
|
@ -1856,6 +1856,7 @@ export async function startApp(): Promise<void> {
|
|||
// after connect on every startup
|
||||
await server.registerCapabilities({
|
||||
deleteSync: true,
|
||||
versionedExpirationTimer: true,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
|
|
|
@ -2049,6 +2049,7 @@ export async function createGroupV2(
|
|||
if (expireTimer) {
|
||||
await conversation.updateExpirationTimer(expireTimer, {
|
||||
reason: 'createGroupV2',
|
||||
version: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -133,6 +133,7 @@ export async function sendDeleteForEveryone(
|
|||
profileKey,
|
||||
recipients: conversation.getRecipients(),
|
||||
timestamp,
|
||||
expireTimerVersion: undefined,
|
||||
});
|
||||
strictAssert(
|
||||
proto.dataMessage,
|
||||
|
@ -202,6 +203,7 @@ export async function sendDeleteForEveryone(
|
|||
deletedForEveryoneTimestamp: targetTimestamp,
|
||||
timestamp,
|
||||
expireTimer: undefined,
|
||||
expireTimerVersion: undefined,
|
||||
contentHint,
|
||||
groupId: undefined,
|
||||
profileKey,
|
||||
|
|
|
@ -183,6 +183,7 @@ export async function sendDeleteStoryForEveryone(
|
|||
deletedForEveryoneTimestamp: targetTimestamp,
|
||||
timestamp,
|
||||
expireTimer: undefined,
|
||||
expireTimerVersion: undefined,
|
||||
contentHint,
|
||||
groupId: undefined,
|
||||
profileKey: conversation.get('profileSharing')
|
||||
|
|
|
@ -81,6 +81,7 @@ export async function sendDirectExpirationTimerUpdate(
|
|||
expireTimer === undefined
|
||||
? undefined
|
||||
: DurationInSeconds.fromSeconds(expireTimer),
|
||||
expireTimerVersion: await conversation.incrementAndGetExpireTimerVersion(),
|
||||
flags,
|
||||
profileKey,
|
||||
recipients: conversation.getRecipients(),
|
||||
|
|
|
@ -262,6 +262,7 @@ export async function sendNormalMessage(
|
|||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
groupV2: conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
}),
|
||||
|
@ -378,6 +379,7 @@ export async function sendNormalMessage(
|
|||
contentHint: ContentHint.RESENDABLE,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
groupId: undefined,
|
||||
serviceId: recipientServiceIdsWithoutMe[0],
|
||||
messageText: body,
|
||||
|
|
|
@ -119,6 +119,7 @@ export async function sendProfileKey(
|
|||
flags: Proto.DataMessage.Flags.PROFILE_KEY_UPDATE,
|
||||
profileKey,
|
||||
recipients: conversation.getRecipients(),
|
||||
expireTimerVersion: undefined,
|
||||
timestamp,
|
||||
includePniSignatureMessage: true,
|
||||
});
|
||||
|
|
|
@ -190,6 +190,7 @@ export async function sendReaction(
|
|||
const dataMessage = await messaging.getDataOrEditMessage({
|
||||
attachments: [],
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
groupV2: conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
}),
|
||||
|
@ -247,6 +248,7 @@ export async function sendReaction(
|
|||
deletedForEveryoneTimestamp: undefined,
|
||||
timestamp: pendingReaction.timestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
groupId: undefined,
|
||||
profileKey,
|
||||
|
|
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -460,6 +460,7 @@ export type ConversationAttributesType = {
|
|||
avatars?: ReadonlyArray<Readonly<AvatarDataType>>;
|
||||
description?: string;
|
||||
expireTimer?: DurationInSeconds;
|
||||
expireTimerVersion: number;
|
||||
membersV2?: Array<GroupV2MemberType>;
|
||||
pendingMembersV2?: Array<GroupV2PendingMemberType>;
|
||||
pendingAdminApprovalV2?: Array<GroupV2PendingAdminApprovalType>;
|
||||
|
|
|
@ -302,6 +302,7 @@ export class ConversationModel extends window.Backbone
|
|||
verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||
messageCount: 0,
|
||||
sentMessageCount: 0,
|
||||
expireTimerVersion: 1,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3349,6 +3350,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
await this.updateExpirationTimer(expireTimer, {
|
||||
reason: 'maybeApplyUniversalTimer',
|
||||
version: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4434,6 +4436,7 @@ export class ConversationModel extends window.Backbone
|
|||
receivedAtMS = Date.now(),
|
||||
sentAt: providedSentAt,
|
||||
source: providedSource,
|
||||
version,
|
||||
fromSync = false,
|
||||
isInitialSync = false,
|
||||
}: {
|
||||
|
@ -4442,6 +4445,7 @@ export class ConversationModel extends window.Backbone
|
|||
receivedAtMS?: number;
|
||||
sentAt?: number;
|
||||
source?: string;
|
||||
version: number | undefined;
|
||||
fromSync?: boolean;
|
||||
isInitialSync?: boolean;
|
||||
}
|
||||
|
@ -4482,6 +4486,29 @@ export class ConversationModel extends window.Backbone
|
|||
if (!expireTimer) {
|
||||
expireTimer = undefined;
|
||||
}
|
||||
|
||||
const logId =
|
||||
`updateExpirationTimer(${this.idForLogging()}, ` +
|
||||
`${expireTimer || 'disabled'}, version=${version || 0}) ` +
|
||||
`source=${source ?? '?'} reason=${reason}`;
|
||||
|
||||
if (isSetByOther) {
|
||||
const expireTimerVersion = this.getExpireTimerVersion();
|
||||
if (version) {
|
||||
if (expireTimerVersion && version < expireTimerVersion) {
|
||||
log.warn(
|
||||
`${logId}: not updating, local version is ${expireTimerVersion}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (version === expireTimerVersion) {
|
||||
log.warn(`${logId}: expire version glare`);
|
||||
} else {
|
||||
this.set({ expireTimerVersion: version });
|
||||
log.info(`${logId}: updating expire version`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
this.get('expireTimer') === expireTimer ||
|
||||
(!expireTimer && !this.get('expireTimer'))
|
||||
|
@ -4489,15 +4516,9 @@ export class ConversationModel extends window.Backbone
|
|||
return;
|
||||
}
|
||||
|
||||
const logId =
|
||||
`updateExpirationTimer(${this.idForLogging()}, ` +
|
||||
`${expireTimer || 'disabled'}) ` +
|
||||
`source=${source ?? '?'} reason=${reason}`;
|
||||
|
||||
log.info(`${logId}: updating`);
|
||||
|
||||
// if change wasn't made remotely, send it to the number/group
|
||||
if (!isSetByOther) {
|
||||
log.info(`${logId}: queuing send job`);
|
||||
// if change wasn't made remotely, send it to the number/group
|
||||
try {
|
||||
await conversationJobQueue.add({
|
||||
type: conversationQueueJobEnum.enum.DirectExpirationTimerUpdate,
|
||||
|
@ -4513,12 +4534,17 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
}
|
||||
|
||||
log.info(`${logId}: updating`);
|
||||
|
||||
const ourConversation =
|
||||
window.ConversationController.getOurConversationOrThrow();
|
||||
source = source || ourConversation.id;
|
||||
const sourceServiceId = ourConversation.get('serviceId');
|
||||
const sourceServiceId =
|
||||
window.ConversationController.get(source)?.get('serviceId');
|
||||
|
||||
this.set({ expireTimer });
|
||||
this.set({
|
||||
expireTimer,
|
||||
});
|
||||
|
||||
// This call actually removes universal timer notification and clears
|
||||
// the pending flags.
|
||||
|
@ -5129,6 +5155,48 @@ export class ConversationModel extends window.Backbone
|
|||
return areWeAdmin(this.attributes);
|
||||
}
|
||||
|
||||
getExpireTimerVersion(): number | undefined {
|
||||
return isDirectConversation(this.attributes)
|
||||
? this.get('expireTimerVersion')
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async incrementAndGetExpireTimerVersion(): Promise<number | undefined> {
|
||||
const logId = `incrementAndGetExpireTimerVersion(${this.idForLogging()})`;
|
||||
if (!isDirectConversation(this.attributes)) {
|
||||
return undefined;
|
||||
}
|
||||
const { expireTimerVersion, capabilities } = this.attributes;
|
||||
|
||||
// This should not happen in practice, but be ready to handle
|
||||
const MAX_EXPIRE_TIMER_VERSION = 0xffffffff;
|
||||
if (expireTimerVersion >= MAX_EXPIRE_TIMER_VERSION) {
|
||||
log.warn(`${logId}: expire version overflow`);
|
||||
return MAX_EXPIRE_TIMER_VERSION;
|
||||
}
|
||||
|
||||
if (expireTimerVersion <= 2) {
|
||||
if (!capabilities?.versionedExpirationTimer) {
|
||||
log.warn(`${logId}: missing recipient capability`);
|
||||
return expireTimerVersion;
|
||||
}
|
||||
const me = window.ConversationController.getOurConversationOrThrow();
|
||||
if (!me.get('capabilities')?.versionedExpirationTimer) {
|
||||
log.warn(`${logId}: missing sender capability`);
|
||||
return expireTimerVersion;
|
||||
}
|
||||
|
||||
// Increment only if sender and receiver are both capable
|
||||
} else {
|
||||
// If we or them updated the timer version past 2 - we are both capable
|
||||
}
|
||||
|
||||
const newVersion = expireTimerVersion + 1;
|
||||
this.set('expireTimerVersion', newVersion);
|
||||
await DataWriter.updateConversation(this.attributes);
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
// Set of items to captureChanges on:
|
||||
// [-] serviceId
|
||||
// [-] e164
|
||||
|
|
|
@ -1975,6 +1975,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
receivedAtMS: message.get('received_at_ms'),
|
||||
sentAt: message.get('sent_at'),
|
||||
reason: idLog,
|
||||
version: initialMessage.expireTimerVersion,
|
||||
});
|
||||
} else if (
|
||||
// We won't turn off timers for these kinds of messages:
|
||||
|
@ -1987,6 +1988,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
receivedAtMS: message.get('received_at_ms'),
|
||||
sentAt: message.get('sent_at'),
|
||||
reason: idLog,
|
||||
version: initialMessage.expireTimerVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -741,7 +741,10 @@ export class BackupExportStream extends Readable {
|
|||
|
||||
private toRecipient(
|
||||
recipientId: Long,
|
||||
convo: Omit<ConversationAttributesType, 'id' | 'version'>
|
||||
convo: Omit<
|
||||
ConversationAttributesType,
|
||||
'id' | 'version' | 'expireTimerVersion'
|
||||
>
|
||||
): Backups.IRecipient | undefined {
|
||||
const res: Backups.IRecipient = {
|
||||
id: recipientId,
|
||||
|
|
|
@ -747,6 +747,7 @@ export class BackupImportStream extends Writable {
|
|||
profileFamilyName: dropNull(contact.profileFamilyName),
|
||||
hideStory: contact.hideStory === true,
|
||||
username: dropNull(contact.username),
|
||||
expireTimerVersion: 1,
|
||||
};
|
||||
|
||||
if (contact.notRegistered) {
|
||||
|
@ -840,6 +841,7 @@ export class BackupImportStream extends Writable {
|
|||
expireTimer: expirationTimerS
|
||||
? DurationInSeconds.fromSeconds(expirationTimerS)
|
||||
: undefined,
|
||||
expireTimerVersion: 1,
|
||||
accessControl: accessControl
|
||||
? {
|
||||
attributes:
|
||||
|
|
|
@ -68,6 +68,7 @@ async function updateConversationFromContactSync(
|
|||
// setting this will make 'isSetByOther' check true.
|
||||
source: window.ConversationController.getOurConversationId(),
|
||||
receivedAt: receivedAtCounter,
|
||||
version: details.expireTimerVersion ?? 1,
|
||||
fromSync: true,
|
||||
isInitialSync,
|
||||
reason: `contact sync (sent=${sentAt})`,
|
||||
|
|
|
@ -204,6 +204,7 @@ import { redactGenericText } from '../util/privacy';
|
|||
type ConversationRow = Readonly<{
|
||||
json: string;
|
||||
profileLastFetchedAt: null | number;
|
||||
expireTimerVersion: number;
|
||||
}>;
|
||||
type ConversationRows = Array<ConversationRow>;
|
||||
type StickerRow = Readonly<{
|
||||
|
@ -547,6 +548,7 @@ export function prepare<T extends Array<unknown> | Record<string, unknown>>(
|
|||
}
|
||||
|
||||
function rowToConversation(row: ConversationRow): ConversationType {
|
||||
const { expireTimerVersion } = row;
|
||||
const parsedJson = JSON.parse(row.json);
|
||||
|
||||
let profileLastFetchedAt: undefined | number;
|
||||
|
@ -562,6 +564,7 @@ function rowToConversation(row: ConversationRow): ConversationType {
|
|||
|
||||
return {
|
||||
...parsedJson,
|
||||
expireTimerVersion,
|
||||
profileLastFetchedAt,
|
||||
};
|
||||
}
|
||||
|
@ -1635,6 +1638,7 @@ function updateConversation(db: WritableDB, data: ConversationType): void {
|
|||
profileLastFetchedAt,
|
||||
e164,
|
||||
serviceId,
|
||||
expireTimerVersion,
|
||||
} = data;
|
||||
|
||||
const membersList = getConversationMembersList(data);
|
||||
|
@ -1654,7 +1658,8 @@ function updateConversation(db: WritableDB, data: ConversationType): void {
|
|||
profileName = $profileName,
|
||||
profileFamilyName = $profileFamilyName,
|
||||
profileFullName = $profileFullName,
|
||||
profileLastFetchedAt = $profileLastFetchedAt
|
||||
profileLastFetchedAt = $profileLastFetchedAt,
|
||||
expireTimerVersion = $expireTimerVersion
|
||||
WHERE id = $id;
|
||||
`
|
||||
).run({
|
||||
|
@ -1674,6 +1679,7 @@ function updateConversation(db: WritableDB, data: ConversationType): void {
|
|||
profileFamilyName: profileFamilyName || null,
|
||||
profileFullName: combineNames(profileName, profileFamilyName) || null,
|
||||
profileLastFetchedAt: profileLastFetchedAt || null,
|
||||
expireTimerVersion,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1737,7 +1743,7 @@ function getAllConversations(db: ReadableDB): Array<ConversationType> {
|
|||
const rows: ConversationRows = db
|
||||
.prepare<EmptyQuery>(
|
||||
`
|
||||
SELECT json, profileLastFetchedAt
|
||||
SELECT json, profileLastFetchedAt, expireTimerVersion
|
||||
FROM conversations
|
||||
ORDER BY id ASC;
|
||||
`
|
||||
|
@ -1766,7 +1772,7 @@ function getAllGroupsInvolvingServiceId(
|
|||
const rows: ConversationRows = db
|
||||
.prepare<Query>(
|
||||
`
|
||||
SELECT json, profileLastFetchedAt
|
||||
SELECT json, profileLastFetchedAt, expireTimerVersion
|
||||
FROM conversations WHERE
|
||||
type = 'group' AND
|
||||
members LIKE $serviceId
|
||||
|
|
30
ts/sql/migrations/1150-expire-timer-version.ts
Normal file
30
ts/sql/migrations/1150-expire-timer-version.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { Database } from '@signalapp/better-sqlite3';
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
|
||||
export const version = 1150;
|
||||
|
||||
export function updateToSchemaVersion1150(
|
||||
currentVersion: number,
|
||||
db: Database,
|
||||
logger: LoggerType
|
||||
): void {
|
||||
if (currentVersion >= 1150) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
db.exec(`
|
||||
-- All future conversations will start from '1'
|
||||
ALTER TABLE conversations
|
||||
ADD COLUMN expireTimerVersion INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- All current conversations will start from '2'
|
||||
UPDATE conversations SET expireTimerVersion = 2;
|
||||
`);
|
||||
|
||||
db.pragma('user_version = 1150');
|
||||
})();
|
||||
logger.info('updateToSchemaVersion1150: success!');
|
||||
}
|
|
@ -90,10 +90,11 @@ import { updateToSchemaVersion1100 } from './1100-optimize-mark-call-history-rea
|
|||
import { updateToSchemaVersion1110 } from './1110-sticker-local-key';
|
||||
import { updateToSchemaVersion1120 } from './1120-messages-foreign-keys-indexes';
|
||||
import { updateToSchemaVersion1130 } from './1130-isStory-index';
|
||||
import { updateToSchemaVersion1140 } from './1140-call-links-deleted-column';
|
||||
import {
|
||||
updateToSchemaVersion1140,
|
||||
updateToSchemaVersion1150,
|
||||
version as MAX_VERSION,
|
||||
} from './1140-call-links-deleted-column';
|
||||
} from './1150-expire-timer-version';
|
||||
|
||||
function updateToSchemaVersion1(
|
||||
currentVersion: number,
|
||||
|
@ -2052,6 +2053,7 @@ export const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion1120,
|
||||
updateToSchemaVersion1130,
|
||||
updateToSchemaVersion1140,
|
||||
updateToSchemaVersion1150,
|
||||
];
|
||||
|
||||
export class DBVersionFromFutureError extends Error {
|
||||
|
|
|
@ -1629,6 +1629,7 @@ function setDisappearingMessages(
|
|||
task: async () =>
|
||||
conversation.updateExpirationTimer(valueToSet, {
|
||||
reason: 'setDisappearingMessages',
|
||||
version: undefined,
|
||||
}),
|
||||
});
|
||||
dispatch({
|
||||
|
|
|
@ -39,6 +39,7 @@ describe('Conversations', () => {
|
|||
sentMessageCount: 0,
|
||||
profileSharing: true,
|
||||
version: 0,
|
||||
expireTimerVersion: 1,
|
||||
});
|
||||
|
||||
await window.textsecure.storage.user.setCredentials({
|
||||
|
@ -132,6 +133,7 @@ describe('Conversations', () => {
|
|||
sentMessageCount: 0,
|
||||
profileSharing: true,
|
||||
version: 0,
|
||||
expireTimerVersion: 1,
|
||||
});
|
||||
|
||||
const resultNoImage = await conversation.getQuoteAttachment(
|
||||
|
|
|
@ -57,6 +57,7 @@ describe('routineProfileRefresh', () => {
|
|||
type: 'private',
|
||||
serviceId: generateAci(),
|
||||
version: 2,
|
||||
expireTimerVersion: 1,
|
||||
...overrideAttributes,
|
||||
});
|
||||
return result;
|
||||
|
|
|
@ -198,6 +198,7 @@ describe('sql/getCallHistoryGroups', () => {
|
|||
version: 0,
|
||||
id: 'id:1',
|
||||
serviceId: conversation1Uuid,
|
||||
expireTimerVersion: 1,
|
||||
};
|
||||
|
||||
const conversation2: ConversationAttributesType = {
|
||||
|
@ -205,6 +206,7 @@ describe('sql/getCallHistoryGroups', () => {
|
|||
version: 2,
|
||||
id: 'id:2',
|
||||
groupId: conversation2GroupId,
|
||||
expireTimerVersion: 1,
|
||||
};
|
||||
|
||||
await saveConversation(conversation1);
|
||||
|
@ -270,6 +272,7 @@ describe('sql/getCallHistoryGroups', () => {
|
|||
type: 'private',
|
||||
version: 0,
|
||||
id: conversationId,
|
||||
expireTimerVersion: 1,
|
||||
};
|
||||
|
||||
await saveConversation(conversation);
|
||||
|
@ -396,6 +399,7 @@ describe('sql/getCallHistoryGroups', () => {
|
|||
version: 0,
|
||||
id: 'id:1',
|
||||
serviceId: conversation1Uuid,
|
||||
expireTimerVersion: 1,
|
||||
};
|
||||
|
||||
const conversation2: ConversationAttributesType = {
|
||||
|
@ -403,6 +407,7 @@ describe('sql/getCallHistoryGroups', () => {
|
|||
version: 2,
|
||||
id: 'id:2',
|
||||
groupId: conversation2GroupId,
|
||||
expireTimerVersion: 1,
|
||||
};
|
||||
|
||||
await saveConversation(conversation1);
|
||||
|
|
|
@ -139,6 +139,7 @@ describe('updateConversationsWithUuidLookup', () => {
|
|||
sentMessageCount: 0,
|
||||
type: 'private' as const,
|
||||
version: 0,
|
||||
expireTimerVersion: 2,
|
||||
...attributes,
|
||||
});
|
||||
}
|
||||
|
|
416
ts/test-mock/messaging/expire_timer_version_test.ts
Normal file
416
ts/test-mock/messaging/expire_timer_version_test.ts
Normal file
|
@ -0,0 +1,416 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import {
|
||||
type PrimaryDevice,
|
||||
Proto,
|
||||
StorageState,
|
||||
} from '@signalapp/mock-server';
|
||||
import createDebug from 'debug';
|
||||
import Long from 'long';
|
||||
|
||||
import * as durations from '../../util/durations';
|
||||
import { uuidToBytes } from '../../util/uuidToBytes';
|
||||
import { MY_STORY_ID } from '../../types/Stories';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
import { expectSystemMessages, typeIntoInput } from '../helpers';
|
||||
|
||||
export const debug = createDebug('mock:test:messaging');
|
||||
|
||||
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||
|
||||
const DAY = 24 * 3600;
|
||||
|
||||
describe('messaging/expireTimerVersion', function (this: Mocha.Suite) {
|
||||
this.timeout(durations.MINUTE);
|
||||
|
||||
let bootstrap: Bootstrap;
|
||||
let app: App;
|
||||
let stranger: PrimaryDevice;
|
||||
const STRANGER_NAME = 'Stranger';
|
||||
|
||||
beforeEach(async () => {
|
||||
bootstrap = new Bootstrap({ contactCount: 1 });
|
||||
await bootstrap.init();
|
||||
|
||||
const {
|
||||
server,
|
||||
phone,
|
||||
contacts: [contact],
|
||||
} = bootstrap;
|
||||
|
||||
stranger = await server.createPrimaryDevice({
|
||||
profileName: STRANGER_NAME,
|
||||
});
|
||||
|
||||
let state = StorageState.getEmpty();
|
||||
|
||||
state = state.updateAccount({
|
||||
profileKey: phone.profileKey.serialize(),
|
||||
e164: phone.device.number,
|
||||
});
|
||||
|
||||
state = state.addContact(stranger, {
|
||||
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
|
||||
whitelisted: true,
|
||||
serviceE164: undefined,
|
||||
profileKey: stranger.profileKey.serialize(),
|
||||
});
|
||||
|
||||
state = state.addContact(contact, {
|
||||
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
|
||||
whitelisted: true,
|
||||
serviceE164: undefined,
|
||||
profileKey: contact.profileKey.serialize(),
|
||||
});
|
||||
contact.device.capabilities.versionedExpirationTimer = false;
|
||||
|
||||
// Put both contacts in left pane
|
||||
state = state.pin(stranger);
|
||||
state = state.pin(contact);
|
||||
|
||||
// Add my story
|
||||
state = state.addRecord({
|
||||
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
||||
record: {
|
||||
storyDistributionList: {
|
||||
allowsReplies: true,
|
||||
identifier: uuidToBytes(MY_STORY_ID),
|
||||
isBlockList: true,
|
||||
name: MY_STORY_ID,
|
||||
recipientServiceIds: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await phone.setStorageState(state);
|
||||
|
||||
app = await bootstrap.link();
|
||||
});
|
||||
|
||||
afterEach(async function (this: Mocha.Context) {
|
||||
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||
await app.close();
|
||||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
const SCENARIOS = [
|
||||
{
|
||||
name: 'they win and we start',
|
||||
theyFirst: false,
|
||||
ourTimer: 60 * DAY,
|
||||
ourVersion: 3,
|
||||
theirTimer: 90 * DAY,
|
||||
theirVersion: 4,
|
||||
finalTimer: 90 * DAY,
|
||||
finalVersion: 4,
|
||||
systemMessages: [
|
||||
'You set the disappearing message time to 60 days.',
|
||||
`${STRANGER_NAME} set the disappearing message time to 90 days.`,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'they win and start',
|
||||
theyFirst: true,
|
||||
ourTimer: 60 * DAY,
|
||||
ourVersion: 3,
|
||||
theirTimer: 90 * DAY,
|
||||
theirVersion: 4,
|
||||
finalTimer: 90 * DAY,
|
||||
finalVersion: 4,
|
||||
systemMessages: [
|
||||
`${STRANGER_NAME} set the disappearing message time to 90 days.`,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'we win and start',
|
||||
theyFirst: false,
|
||||
ourTimer: 60 * DAY,
|
||||
ourVersion: 4,
|
||||
theirTimer: 90 * DAY,
|
||||
theirVersion: 3,
|
||||
finalTimer: 60 * DAY,
|
||||
finalVersion: 4,
|
||||
systemMessages: ['You set the disappearing message time to 60 days.'],
|
||||
},
|
||||
{
|
||||
name: 'we win and they start',
|
||||
theyFirst: true,
|
||||
ourTimer: 60 * DAY,
|
||||
ourVersion: 4,
|
||||
theirTimer: 90 * DAY,
|
||||
theirVersion: 3,
|
||||
finalTimer: 60 * DAY,
|
||||
finalVersion: 4,
|
||||
systemMessages: [
|
||||
`${STRANGER_NAME} set the disappearing message time to 90 days.`,
|
||||
'You set the disappearing message time to 60 days.',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'race and we start',
|
||||
theyFirst: false,
|
||||
ourTimer: 60 * DAY,
|
||||
ourVersion: 4,
|
||||
theirTimer: 90 * DAY,
|
||||
theirVersion: 4,
|
||||
finalTimer: 90 * DAY,
|
||||
finalVersion: 4,
|
||||
systemMessages: [
|
||||
'You set the disappearing message time to 60 days.',
|
||||
`${STRANGER_NAME} set the disappearing message time to 90 days.`,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'race and they start',
|
||||
theyFirst: true,
|
||||
ourTimer: 60 * DAY,
|
||||
ourVersion: 4,
|
||||
theirTimer: 90 * DAY,
|
||||
theirVersion: 4,
|
||||
finalTimer: 60 * DAY,
|
||||
finalVersion: 4,
|
||||
systemMessages: [
|
||||
`${STRANGER_NAME} set the disappearing message time to 90 days.`,
|
||||
'You set the disappearing message time to 60 days.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const scenario of SCENARIOS) {
|
||||
const testName =
|
||||
`sets correct version after ${scenario.name}, ` +
|
||||
`theyFirst=${scenario.theyFirst}`;
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(testName, async () => {
|
||||
const { phone, desktop } = bootstrap;
|
||||
|
||||
const sendSync = async () => {
|
||||
debug('Send a sync message');
|
||||
const timestamp = bootstrap.getTimestamp();
|
||||
const destinationServiceId = stranger.device.aci;
|
||||
const content = {
|
||||
syncMessage: {
|
||||
sent: {
|
||||
destinationServiceId,
|
||||
timestamp: Long.fromNumber(timestamp),
|
||||
message: {
|
||||
body: 'request',
|
||||
timestamp: Long.fromNumber(timestamp),
|
||||
expireTimer: scenario.ourTimer,
|
||||
expireTimerVersion: scenario.ourVersion,
|
||||
},
|
||||
unidentifiedStatus: [
|
||||
{
|
||||
destinationServiceId,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const sendOptions = {
|
||||
timestamp,
|
||||
};
|
||||
await phone.sendRaw(desktop, content, sendOptions);
|
||||
};
|
||||
|
||||
const sendResponse = async () => {
|
||||
debug('Send a response message');
|
||||
const timestamp = bootstrap.getTimestamp();
|
||||
const content = {
|
||||
dataMessage: {
|
||||
body: 'response',
|
||||
timestamp: Long.fromNumber(timestamp),
|
||||
expireTimer: scenario.theirTimer,
|
||||
expireTimerVersion: scenario.theirVersion,
|
||||
},
|
||||
};
|
||||
const sendOptions = {
|
||||
timestamp,
|
||||
};
|
||||
const key = await desktop.popSingleUseKey();
|
||||
await stranger.addSingleUseKey(desktop, key);
|
||||
await stranger.sendRaw(desktop, content, sendOptions);
|
||||
};
|
||||
|
||||
if (scenario.theyFirst) {
|
||||
await sendResponse();
|
||||
await sendSync();
|
||||
} else {
|
||||
await sendSync();
|
||||
await sendResponse();
|
||||
}
|
||||
|
||||
const window = await app.getWindow();
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
|
||||
debug('opening conversation with the contact');
|
||||
await leftPane
|
||||
.locator(
|
||||
`[data-testid="${stranger.device.aci}"] >> "${stranger.profileName}"`
|
||||
)
|
||||
.click();
|
||||
|
||||
await expectSystemMessages(window, scenario.systemMessages);
|
||||
|
||||
await window.locator('.module-conversation-hero').waitFor();
|
||||
|
||||
debug('Send message to merged contact');
|
||||
{
|
||||
const compositionInput = await app.waitForEnabledComposer();
|
||||
|
||||
await typeIntoInput(compositionInput, 'Hello');
|
||||
await compositionInput.press('Enter');
|
||||
}
|
||||
|
||||
debug('Getting message to contact');
|
||||
const { body, dataMessage } = await stranger.waitForMessage();
|
||||
|
||||
assert.strictEqual(body, 'Hello');
|
||||
assert.strictEqual(dataMessage.expireTimer, scenario.finalTimer);
|
||||
assert.strictEqual(dataMessage.expireTimerVersion, scenario.finalVersion);
|
||||
});
|
||||
}
|
||||
|
||||
it('should not bump version for not capable recipient', async () => {
|
||||
const {
|
||||
contacts: [contact],
|
||||
} = bootstrap;
|
||||
|
||||
const window = await app.getWindow();
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
|
||||
debug('opening conversation with the contact');
|
||||
await leftPane
|
||||
.locator(
|
||||
`[data-testid="${contact.device.aci}"] >> "${contact.profileName}"`
|
||||
)
|
||||
.click();
|
||||
|
||||
await window.locator('.module-conversation-hero').waitFor();
|
||||
|
||||
const conversationStack = window.locator('.Inbox__conversation-stack');
|
||||
|
||||
debug('setting timer to 1 week');
|
||||
await conversationStack
|
||||
.locator('button.module-ConversationHeader__button--more')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator('.react-contextmenu-item >> "Disappearing messages"')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator(
|
||||
'.module-ConversationHeader__disappearing-timer__item >> "1 week"'
|
||||
)
|
||||
.click();
|
||||
|
||||
debug('Getting first expiration update');
|
||||
{
|
||||
const { dataMessage } = await contact.waitForMessage();
|
||||
assert.strictEqual(
|
||||
dataMessage.flags,
|
||||
Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
|
||||
);
|
||||
assert.strictEqual(dataMessage.expireTimer, 604800);
|
||||
assert.strictEqual(dataMessage.expireTimerVersion, 1);
|
||||
}
|
||||
|
||||
debug('setting timer to 4 weeks');
|
||||
await conversationStack
|
||||
.locator('button.module-ConversationHeader__button--more')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator('.react-contextmenu-item >> "Disappearing messages"')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator(
|
||||
'.module-ConversationHeader__disappearing-timer__item >> "4 weeks"'
|
||||
)
|
||||
.click();
|
||||
|
||||
debug('Getting second expiration update');
|
||||
{
|
||||
const { dataMessage } = await contact.waitForMessage();
|
||||
assert.strictEqual(
|
||||
dataMessage.flags,
|
||||
Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
|
||||
);
|
||||
assert.strictEqual(dataMessage.expireTimer, 2419200);
|
||||
assert.strictEqual(dataMessage.expireTimerVersion, 1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should bump version for capable recipient', async () => {
|
||||
const window = await app.getWindow();
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
|
||||
debug('opening conversation with the contact');
|
||||
await leftPane
|
||||
.locator(
|
||||
`[data-testid="${stranger.device.aci}"] >> "${stranger.profileName}"`
|
||||
)
|
||||
.click();
|
||||
|
||||
await window.locator('.module-conversation-hero').waitFor();
|
||||
|
||||
const conversationStack = window.locator('.Inbox__conversation-stack');
|
||||
|
||||
debug('setting timer to 1 week');
|
||||
await conversationStack
|
||||
.locator('button.module-ConversationHeader__button--more')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator('.react-contextmenu-item >> "Disappearing messages"')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator(
|
||||
'.module-ConversationHeader__disappearing-timer__item >> "1 week"'
|
||||
)
|
||||
.click();
|
||||
|
||||
debug('Getting first expiration update');
|
||||
{
|
||||
const { dataMessage } = await stranger.waitForMessage();
|
||||
assert.strictEqual(
|
||||
dataMessage.flags,
|
||||
Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
|
||||
);
|
||||
assert.strictEqual(dataMessage.expireTimer, 604800);
|
||||
assert.strictEqual(dataMessage.expireTimerVersion, 2);
|
||||
}
|
||||
|
||||
debug('setting timer to 4 weeks');
|
||||
await conversationStack
|
||||
.locator('button.module-ConversationHeader__button--more')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator('.react-contextmenu-item >> "Disappearing messages"')
|
||||
.click();
|
||||
|
||||
await window
|
||||
.locator(
|
||||
'.module-ConversationHeader__disappearing-timer__item >> "4 weeks"'
|
||||
)
|
||||
.click();
|
||||
|
||||
debug('Getting second expiration update');
|
||||
{
|
||||
const { dataMessage } = await stranger.waitForMessage();
|
||||
assert.strictEqual(
|
||||
dataMessage.flags,
|
||||
Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
|
||||
);
|
||||
assert.strictEqual(dataMessage.expireTimer, 2419200);
|
||||
assert.strictEqual(dataMessage.expireTimerVersion, 3);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -68,7 +68,7 @@ describe('pnp/merge', function (this: Mocha.Suite) {
|
|||
|
||||
serviceE164: undefined,
|
||||
identityKey: aciIdentityKey,
|
||||
givenName: pniContact.profileName,
|
||||
givenName: 'ACI Contact',
|
||||
});
|
||||
|
||||
// Put both contacts in left pane
|
||||
|
@ -454,4 +454,88 @@ describe('pnp/merge', function (this: Mocha.Suite) {
|
|||
assert.strictEqual(await messages.count(), 0, 'message count');
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves expireTimerVersion after merge', async () => {
|
||||
const { phone, desktop } = bootstrap;
|
||||
|
||||
for (const key of ['aci' as const, 'pni' as const]) {
|
||||
debug(`Send a ${key} sync message`);
|
||||
const timestamp = bootstrap.getTimestamp();
|
||||
const destinationServiceId = pniContact.device[key];
|
||||
const destination = key === 'pni' ? pniContact.device.number : undefined;
|
||||
const content = {
|
||||
syncMessage: {
|
||||
sent: {
|
||||
destinationServiceId,
|
||||
destination,
|
||||
timestamp: Long.fromNumber(timestamp),
|
||||
message: {
|
||||
timestamp: Long.fromNumber(timestamp),
|
||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
expireTimer: key === 'pni' ? 90 * 24 * 3600 : 60 * 24 * 3600,
|
||||
expireTimerVersion: key === 'pni' ? 3 : 4,
|
||||
},
|
||||
unidentifiedStatus: [
|
||||
{
|
||||
destinationServiceId,
|
||||
destination,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const sendOptions = {
|
||||
timestamp,
|
||||
};
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await phone.sendRaw(desktop, content, sendOptions);
|
||||
}
|
||||
|
||||
debug(
|
||||
'removing both contacts from storage service, adding one combined contact'
|
||||
);
|
||||
{
|
||||
const state = await phone.expectStorageState('consistency check');
|
||||
await phone.setStorageState(
|
||||
state.mergeContact(pniContact, {
|
||||
identityState: Proto.ContactRecord.IdentityState.DEFAULT,
|
||||
whitelisted: true,
|
||||
identityKey: pniContact.publicKey.serialize(),
|
||||
profileKey: pniContact.profileKey.serialize(),
|
||||
pniSignatureVerified: true,
|
||||
})
|
||||
);
|
||||
await phone.sendFetchStorage({
|
||||
timestamp: bootstrap.getTimestamp(),
|
||||
});
|
||||
await app.waitForManifestVersion(state.version);
|
||||
}
|
||||
|
||||
const window = await app.getWindow();
|
||||
const leftPane = window.locator('#LeftPane');
|
||||
|
||||
debug('opening conversation with the merged contact');
|
||||
await leftPane
|
||||
.locator(
|
||||
`[data-testid="${pniContact.device.aci}"] >> ` +
|
||||
`"${pniContact.profileName}"`
|
||||
)
|
||||
.click();
|
||||
|
||||
await window.locator('.module-conversation-hero').waitFor();
|
||||
|
||||
debug('Send message to merged contact');
|
||||
{
|
||||
const compositionInput = await app.waitForEnabledComposer();
|
||||
|
||||
await typeIntoInput(compositionInput, 'Hello merged');
|
||||
await compositionInput.press('Enter');
|
||||
}
|
||||
|
||||
debug('Getting message to merged contact');
|
||||
const { body, dataMessage } = await pniContact.waitForMessage();
|
||||
assert.strictEqual(body, 'Hello merged');
|
||||
assert.strictEqual(dataMessage.expireTimer, 60 * 24 * 3600);
|
||||
assert.strictEqual(dataMessage.expireTimerVersion, 4);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ type MessageWithAvatar<Message extends OptionalFields> = Omit<
|
|||
> & {
|
||||
avatar?: ContactAvatarType;
|
||||
expireTimer?: DurationInSeconds;
|
||||
expireTimerVersion: number | null;
|
||||
number?: string | undefined;
|
||||
};
|
||||
|
||||
|
@ -207,6 +208,7 @@ function prepareContact(
|
|||
const result = {
|
||||
...proto,
|
||||
expireTimer,
|
||||
expireTimerVersion: proto.expireTimerVersion ?? null,
|
||||
aci,
|
||||
avatar,
|
||||
number: dropNull(proto.number),
|
||||
|
|
|
@ -2281,6 +2281,7 @@ export default class MessageReceiver
|
|||
preview,
|
||||
canReplyToStory: Boolean(msg.allowsReplies),
|
||||
expireTimer: DurationInSeconds.DAY,
|
||||
expireTimerVersion: 0,
|
||||
flags: 0,
|
||||
groupV2,
|
||||
isStory: true,
|
||||
|
|
|
@ -188,6 +188,7 @@ export type MessageOptionsType = {
|
|||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
contact?: ReadonlyArray<EmbeddedContactWithUploadedAvatar>;
|
||||
expireTimer?: DurationInSeconds;
|
||||
expireTimerVersion: number | undefined;
|
||||
flags?: number;
|
||||
group?: {
|
||||
id: string;
|
||||
|
@ -238,6 +239,8 @@ class Message {
|
|||
|
||||
expireTimer?: DurationInSeconds;
|
||||
|
||||
expireTimerVersion?: number;
|
||||
|
||||
flags?: number;
|
||||
|
||||
group?: {
|
||||
|
@ -277,6 +280,7 @@ class Message {
|
|||
this.bodyRanges = options.bodyRanges;
|
||||
this.contact = options.contact;
|
||||
this.expireTimer = options.expireTimer;
|
||||
this.expireTimerVersion = options.expireTimerVersion;
|
||||
this.flags = options.flags;
|
||||
this.group = options.group;
|
||||
this.groupV2 = options.groupV2;
|
||||
|
@ -534,6 +538,9 @@ class Message {
|
|||
if (this.expireTimer) {
|
||||
proto.expireTimer = this.expireTimer;
|
||||
}
|
||||
if (this.expireTimerVersion) {
|
||||
proto.expireTimerVersion = this.expireTimerVersion;
|
||||
}
|
||||
if (this.profileKey) {
|
||||
proto.profileKey = this.profileKey;
|
||||
}
|
||||
|
@ -930,6 +937,7 @@ export default class MessageSender {
|
|||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion: undefined,
|
||||
flags,
|
||||
groupCallUpdate,
|
||||
groupV2,
|
||||
|
@ -1163,6 +1171,7 @@ export default class MessageSender {
|
|||
contentHint,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion,
|
||||
groupId,
|
||||
serviceId,
|
||||
messageText,
|
||||
|
@ -1185,6 +1194,7 @@ export default class MessageSender {
|
|||
contentHint: number;
|
||||
deletedForEveryoneTimestamp: number | undefined;
|
||||
expireTimer: DurationInSeconds | undefined;
|
||||
expireTimerVersion: number | undefined;
|
||||
groupId: string | undefined;
|
||||
serviceId: ServiceIdString;
|
||||
messageText: string | undefined;
|
||||
|
@ -1209,6 +1219,7 @@ export default class MessageSender {
|
|||
contact,
|
||||
deletedForEveryoneTimestamp,
|
||||
expireTimer,
|
||||
expireTimerVersion,
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
|
|
1
ts/textsecure/Types.d.ts
vendored
1
ts/textsecure/Types.d.ts
vendored
|
@ -204,6 +204,7 @@ export type ProcessedDataMessage = {
|
|||
groupV2?: ProcessedGroupV2Context;
|
||||
flags: number;
|
||||
expireTimer: DurationInSeconds;
|
||||
expireTimerVersion: number;
|
||||
profileKey?: string;
|
||||
timestamp: number;
|
||||
payment?: AnyPaymentEvent;
|
||||
|
|
|
@ -740,9 +740,11 @@ export type WebAPIConnectType = {
|
|||
|
||||
export type CapabilitiesType = {
|
||||
deleteSync: boolean;
|
||||
versionedExpirationTimer: boolean;
|
||||
};
|
||||
export type CapabilitiesUploadType = {
|
||||
deleteSync: true;
|
||||
versionedExpirationTimer: true;
|
||||
};
|
||||
|
||||
type StickerPackManifestType = Uint8Array;
|
||||
|
@ -2612,6 +2614,7 @@ export function initialize({
|
|||
|
||||
const capabilities: CapabilitiesUploadType = {
|
||||
deleteSync: true,
|
||||
versionedExpirationTimer: true,
|
||||
};
|
||||
|
||||
const jsonData = {
|
||||
|
@ -2666,6 +2669,7 @@ export function initialize({
|
|||
}: LinkDeviceOptionsType) {
|
||||
const capabilities: CapabilitiesUploadType = {
|
||||
deleteSync: true,
|
||||
versionedExpirationTimer: true,
|
||||
};
|
||||
|
||||
const jsonData = {
|
||||
|
|
|
@ -321,6 +321,7 @@ export function processDataMessage(
|
|||
groupV2: processGroupV2Context(message.groupV2),
|
||||
flags: message.flags ?? 0,
|
||||
expireTimer: DurationInSeconds.fromSeconds(message.expireTimer ?? 0),
|
||||
expireTimerVersion: message.expireTimerVersion ?? 0,
|
||||
profileKey:
|
||||
message.profileKey && message.profileKey.length > 0
|
||||
? Bytes.toBase64(message.profileKey)
|
||||
|
|
Loading…
Reference in a new issue