From 6dd340ca6c4dd25cf8c00a989142aa809129a7c1 Mon Sep 17 00:00:00 2001 From: William Grant Date: Mon, 3 Apr 2023 14:09:06 +0200 Subject: [PATCH] feat: handle legacy disappearing messages more gracefully due to protobuf issues added utility function for checking for undefined properties on a protobuf, renamed expireTimer to expirationTimer in some places --- ts/models/message.ts | 6 ++-- ts/node/migration/sessionMigrations.ts | 4 +-- ts/protobuf/index.ts | 3 +- ts/protobuf/utils.ts | 15 +++++++++ ts/receiver/contentMessage.ts | 43 +++++++++++++++++--------- ts/receiver/dataMessage.ts | 12 ++----- ts/receiver/queuedJob.ts | 2 -- ts/session/utils/syncUtils.ts | 7 ++++- ts/util/expiringMessages.ts | 25 +++++++++++++-- ts/util/releaseFeature.ts | 4 +-- 10 files changed, 83 insertions(+), 38 deletions(-) create mode 100644 ts/protobuf/utils.ts diff --git a/ts/models/message.ts b/ts/models/message.ts index 33cc68506..9d36b9688 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1109,7 +1109,7 @@ export class MessageModel extends Backbone.Model { : isLegacyMessage ? DisappearingMessageConversationSetting[3] : 'off'; - const expireTimer = isLegacyMessage + const expirationTimer = isLegacyMessage ? Number(dataMessage.expireTimer) : content.expirationTimer; const lastDisappearingMessageChangeTimestamp = content.lastDisappearingMessageChangeTimestamp @@ -1117,10 +1117,10 @@ export class MessageModel extends Backbone.Model { : undefined; let expireUpdate: DisappearingMessageUpdate | null = null; - if (expirationType && expireTimer !== undefined) { + if (expirationType && expirationTimer !== undefined) { expireUpdate = { expirationType, - expireTimer, + expirationTimer, lastDisappearingMessageChangeTimestamp, }; } diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts index 067643e5b..c4daadb0a 100644 --- a/ts/node/migration/sessionMigrations.ts +++ b/ts/node/migration/sessionMigrations.ts @@ -1226,8 +1226,8 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN hasOutdatedClient TEXT;`).run(); // TODO update to agreed value between platforms - const disappearingMessagesV2ReleaseTimestamp = 1680339600000; // unix 01/04/2023 09:00 - // const disappearingMessagesV2ReleaseTimestamp = 1677488400000; // unix 27/02/2023 09:00 + // const disappearingMessagesV2ReleaseTimestamp = 1680339600000; // unix 01/04/2023 09:00 + const disappearingMessagesV2ReleaseTimestamp = 1677488400000; // unix 27/02/2023 09:00 // support disppearing messages legacy mode until after the platform agreed timestamp if (Date.now() < disappearingMessagesV2ReleaseTimestamp) { diff --git a/ts/protobuf/index.ts b/ts/protobuf/index.ts index c760e2cae..c4c104c7a 100644 --- a/ts/protobuf/index.ts +++ b/ts/protobuf/index.ts @@ -1,3 +1,4 @@ import { signalservice as SignalService } from './compiled'; +import { ProtobufUtils } from './utils'; -export { SignalService }; +export { SignalService, ProtobufUtils }; diff --git a/ts/protobuf/utils.ts b/ts/protobuf/utils.ts new file mode 100644 index 000000000..25a7c5e30 --- /dev/null +++ b/ts/protobuf/utils.ts @@ -0,0 +1,15 @@ +/** + * This function is used to check that an optional property on a Protobuf object is not undefined or using a type-specific default value. + * https://protobuf.dev/programming-guides/proto/#optional + * + * @param object - A Protobuf/JavaScript object + * @param property - The property you want make sure is not undefined + * @returns true if the property is defined or false if undefined or using a type-specific default value + */ +function hasDefinedProperty(object: A, property: B) { + return object.hasOwnProperty(property) !== false; +} + +export const ProtobufUtils = { + hasDefinedProperty, +}; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index dc9226a07..29d881eb8 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -31,6 +31,7 @@ import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates'; import { DisappearingMessageConversationSetting, DisappearingMessageUpdate, + DisappearingMessageUtils, setExpirationStartTimestamp, } from '../util/expiringMessages'; import { checkIsFeatureReleased } from '../util/releaseFeature'; @@ -409,43 +410,55 @@ export async function innerHandleSwarmContentMessage( dataMessage.profileKey = null; } - // debugger; // TODO legacy messages support will be removed in a future release // We will only support legacy disappearing messages for a short period before disappearing messages v2 is unlocked const isDisappearingMessagesV2Released = await checkIsFeatureReleased( 'Disappearing Messages V2' ); - const isLegacy = Boolean( - (!content.expirationType && !content.expirationTimer) || - content.expirationType === SignalService.Content.ExpirationType.UNKNOWN - ); + const isLegacyContentMessage = DisappearingMessageUtils.isLegacyContentMessage(content); const isLegacyMessage = Boolean( - isLegacy && dataMessage.expireTimer && dataMessage.expireTimer > -1 + isLegacyContentMessage && + DisappearingMessageUtils.isLegacyDataMessage(dataMessage as SignalService.DataMessage) ); // NOTE When a legacy client sends a Conversation Setting Message dataMessage.expirationType and dataMessage.expireTimer can possibly be undefined. const isLegacyConversationSettingMessage = Boolean( - isLegacy && dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE + isLegacyContentMessage && + dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ); - const expireTimer = isDisappearingMessagesV2Released + debugger; + + let expirationTimer = isDisappearingMessagesV2Released ? content.expirationTimer : isLegacyMessage ? Number(dataMessage.expireTimer) - : 0; + : content.expirationTimer; // TODO legacy messages support will be removed in a future release - const expirationType = isDisappearingMessagesV2Released - ? DisappearingMessageConversationSetting[isLegacy ? 3 : content.expirationType] - : isLegacyMessage && expireTimer > 0 - ? DisappearingMessageConversationSetting[3] - : 'off'; + // TODO so close! just have to fix the case of turning off disappearing messages in modern clients. Probably if the timer is zero then override any settings. + let expirationType = + expirationTimer === 0 + ? 'off' + : isDisappearingMessagesV2Released + ? DisappearingMessageConversationSetting[ + isLegacyContentMessage ? 3 : content.expirationType + ] + : DisappearingMessageConversationSetting[3]; const lastDisappearingMessageChangeTimestamp = content.lastDisappearingMessageChangeTimestamp ? Number(content.lastDisappearingMessageChangeTimestamp) : undefined; + // TODO legacy messages support will be removed in a future release + // if it is a legacy message and disappearing messages v2 is released then we ignore it and use the local client's conversation settings + if (isDisappearingMessagesV2Released && isLegacyContentMessage) { + window.log.info(`WIP: received a legacy disappearing message after v2 was released.`); + expirationType = conversationModelForUIUpdate.get('expirationType'); + expirationTimer = conversationModelForUIUpdate.get('expireTimer'); + } + const expireUpdate: DisappearingMessageUpdate = { expirationType, - expireTimer, + expirationTimer, lastDisappearingMessageChangeTimestamp, isLegacyConversationSettingMessage, isLegacyMessage, diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 8fb051fd7..24017c4f4 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -249,21 +249,13 @@ export async function handleSwarmDataMessage( } else { let { expirationType, - expireTimer, + // TODO renamed expireTimer to expirationTimer + expirationTimer: expireTimer, lastDisappearingMessageChangeTimestamp, isLegacyConversationSettingMessage, - isLegacyMessage, isDisappearingMessagesV2Released, } = expireUpdate; - // TODO legacy messages support will be removed in a future release - // if it is a legacy message and disappearing messages v2 is released then we ignore it and use the local client's conversation settings - if (isDisappearingMessagesV2Released && isLegacyMessage) { - window.log.info(`WIP: received a legacy disappearing message after v2 was released.`); - expirationType = convoToAddMessageTo.get('expirationType'); - expireTimer = convoToAddMessageTo.get('expireTimer'); - } - msgModel.set({ expirationType, expireTimer, diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 676df2fde..89c63570d 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -328,8 +328,6 @@ export async function handleMessageJob( ) || messageModel.get('timestamp')} in conversation ${conversation.idForLogging()}` ); - window.log.info(`WIP: handleMessageJob()`, messageModel, conversation, regularDataMessage); - const sendingDeviceConversation = await getConversationController().getOrCreateAndWait( source, ConversationTypeEnum.PRIVATE diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 8072e4026..bc61b1ad2 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -300,7 +300,12 @@ const buildSyncExpireTimerMessage = ( timestamp: number, syncTarget: string ) => { - const { expirationType, expireTimer, lastDisappearingMessageChangeTimestamp } = expireUpdate; + const { + expirationType, + // TODO rename expireTimer to expirationTimer + expirationTimer: expireTimer, + lastDisappearingMessageChangeTimestamp, + } = expireUpdate; return new ExpirationTimerUpdateMessage({ identifier, diff --git a/ts/util/expiringMessages.ts b/ts/util/expiringMessages.ts index f3d5def11..574ba117c 100644 --- a/ts/util/expiringMessages.ts +++ b/ts/util/expiringMessages.ts @@ -8,6 +8,7 @@ import { initWallClockListener } from './wallClockListener'; import { Data } from '../data/data'; import { getConversationController } from '../session/conversations'; import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI'; +import { ProtobufUtils, SignalService } from '../protobuf'; // TODO Might need to be improved by using an enum // TODO do we need to add legacy here now that it's explicitly in the protbuf? @@ -24,8 +25,7 @@ export const DEFAULT_TIMER_OPTION = { export type DisappearingMessageUpdate = { expirationType: DisappearingMessageType; - // TODO rename to expirationTimer? - expireTimer: number; + expirationTimer: number; // This is used for the expirationTimerUpdate lastDisappearingMessageChangeTimestamp?: number; isLegacyConversationSettingMessage?: boolean; @@ -33,6 +33,27 @@ export type DisappearingMessageUpdate = { isDisappearingMessagesV2Released?: boolean; }; +// TODO legacy messages support will be removed in a future release +// NOTE We need this to check for legacy disappearing messages where the expirationType and expireTimer should be undefined on the ContentMessage +function isLegacyContentMessage(contentMessage: SignalService.Content): boolean { + return ( + (contentMessage.expirationType === SignalService.Content.ExpirationType.UNKNOWN || + !ProtobufUtils.hasDefinedProperty(contentMessage, 'expirationType')) && + !ProtobufUtils.hasDefinedProperty(contentMessage, 'expirationTimer') + ); +} + +function isLegacyDataMessage(dataMessage: SignalService.DataMessage): boolean { + return ( + ProtobufUtils.hasDefinedProperty(dataMessage, 'expireTimer') && dataMessage.expireTimer > -1 + ); +} + +export const DisappearingMessageUtils = { + isLegacyContentMessage, + isLegacyDataMessage, +}; + export async function destroyMessagesAndUpdateRedux( messages: Array<{ conversationKey: string; diff --git a/ts/util/releaseFeature.ts b/ts/util/releaseFeature.ts index abc080831..610fa9dd3 100644 --- a/ts/util/releaseFeature.ts +++ b/ts/util/releaseFeature.ts @@ -1,8 +1,8 @@ import { Data } from '../data/data'; // TODO update to agreed value between platforms -const featureReleaseTimestamp = 1680339600000; // unix 01/04/2023 09:00 -// const featureReleaseTimestamp = 1677488400000; // unix 27/02/2023 09:00 +// const featureReleaseTimestamp = 1680339600000; // unix 01/04/2023 09:00 +const featureReleaseTimestamp = 1677488400000; // unix 27/02/2023 09:00 let isFeatureReleased: boolean | undefined; /**